Merge branch 'message-input-controller' of https://github.com/GetStream/stream-chat-flutter into message-input-controller
This commit is contained in:
@@ -33,7 +33,7 @@ jobs:
|
|||||||
flutter-version: ${{ env.flutter_version }}
|
flutter-version: ${{ env.flutter_version }}
|
||||||
|
|
||||||
- name: "Install Tools"
|
- name: "Install Tools"
|
||||||
run: flutter pub global activate melos 1.0.0-dev.3
|
run: flutter pub global activate melos 1.0.0-dev.10
|
||||||
- name: "Bootstrap Workspace"
|
- name: "Bootstrap Workspace"
|
||||||
run: melos bootstrap
|
run: melos bootstrap
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ name: stream_flutter_workflow
|
|||||||
env:
|
env:
|
||||||
ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true'
|
ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true'
|
||||||
flutter_version: "2.5.1"
|
flutter_version: "2.5.1"
|
||||||
|
melos_version: "1.0.0-dev.10"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
@@ -31,7 +32,7 @@ jobs:
|
|||||||
flutter-version: ${{ env.flutter_version }}
|
flutter-version: ${{ env.flutter_version }}
|
||||||
- name: "Install Tools"
|
- name: "Install Tools"
|
||||||
run: |
|
run: |
|
||||||
flutter pub global activate melos 1.0.0-dev.3
|
flutter pub global activate melos ${{ env.melos_version }}
|
||||||
- name: "Bootstrap Workspace"
|
- name: "Bootstrap Workspace"
|
||||||
run: melos bootstrap
|
run: melos bootstrap
|
||||||
- name: "Dart Analyze"
|
- name: "Dart Analyze"
|
||||||
@@ -60,7 +61,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
flutter-version: ${{ env.flutter_version }}
|
flutter-version: ${{ env.flutter_version }}
|
||||||
- name: "Install Tools"
|
- name: "Install Tools"
|
||||||
run: flutter pub global activate melos
|
run: |
|
||||||
|
flutter pub global activate melos ${{ env.melos_version }}
|
||||||
- name: "Bootstrap Workspace"
|
- name: "Bootstrap Workspace"
|
||||||
run: melos bootstrap
|
run: melos bootstrap
|
||||||
- name: "Melos Format"
|
- name: "Melos Format"
|
||||||
@@ -71,7 +73,7 @@ jobs:
|
|||||||
|
|
||||||
test:
|
test:
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
timeout-minutes: 15
|
timeout-minutes: 20
|
||||||
steps:
|
steps:
|
||||||
- name: "Git Checkout"
|
- name: "Git Checkout"
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
@@ -88,7 +90,7 @@ jobs:
|
|||||||
flutter-version: ${{ env.flutter_version }}
|
flutter-version: ${{ env.flutter_version }}
|
||||||
- name: "Install Tools"
|
- name: "Install Tools"
|
||||||
run: |
|
run: |
|
||||||
flutter pub global activate melos
|
flutter pub global activate melos ${{ env.melos_version }}
|
||||||
pub global activate remove_from_coverage
|
pub global activate remove_from_coverage
|
||||||
- name: "Bootstrap Workspace"
|
- name: "Bootstrap Workspace"
|
||||||
run: melos bootstrap
|
run: melos bootstrap
|
||||||
|
|||||||
@@ -154,18 +154,6 @@ dart_code_metrics:
|
|||||||
# Dart Specific
|
# Dart Specific
|
||||||
- binary-expression-operand-order
|
- binary-expression-operand-order
|
||||||
- double-literal-format
|
- double-literal-format
|
||||||
- prefer-match-file-name:
|
|
||||||
exclude:
|
|
||||||
- packages/*/test/**
|
|
||||||
- packages/*/example/**
|
|
||||||
- packages/**/util/**
|
|
||||||
- packages/**/utils.dart
|
|
||||||
- packages/stream_chat/lib/src/client/client.dart
|
|
||||||
- packages/stream_chat/lib/src/core/api/responses.dart
|
|
||||||
- packages/stream_chat/lib/src/core/api/requests.dart
|
|
||||||
- packages/stream_chat/lib/src/core/platform_detector/**
|
|
||||||
- packages/stream_chat_persistence/lib/src/db/shared/**
|
|
||||||
- packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart
|
|
||||||
- no-boolean-literal-compare
|
- no-boolean-literal-compare
|
||||||
- no-equal-then-else
|
- no-equal-then-else
|
||||||
- no-empty-block:
|
- no-empty-block:
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 121 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 380 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 259 KiB |
@@ -10,7 +10,7 @@ Customizing Message Actions
|
|||||||
|
|
||||||
Message actions pop up in message overlay, when you long-press a message.
|
Message actions pop up in message overlay, when you long-press a message.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
We have provided granular control over these actions.
|
We have provided granular control over these actions.
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,31 @@
|
|||||||
|
## Upcoming
|
||||||
|
|
||||||
|
✅ Added
|
||||||
|
|
||||||
|
- Added `client.enrichUrl` endpoint for enriching URLs with metadata.
|
||||||
|
|
||||||
|
## 3.3.1
|
||||||
|
|
||||||
|
🐞 Fixed
|
||||||
|
|
||||||
|
- [[#799]](https://github.com/GetStream/stream-chat-flutter/issues/799) Fixed `totalUnreadCount` is not updating when
|
||||||
|
app is resumed from background mode.
|
||||||
|
- Fix retry mechanism failing in some cases.
|
||||||
|
|
||||||
|
## 3.3.0
|
||||||
|
|
||||||
|
✅ Added
|
||||||
|
|
||||||
|
- Extra properties added to `PaginationParams` to aid in fetching messages.
|
||||||
|
- Added hard delete functionality.
|
||||||
|
|
||||||
|
🐞 Fixed
|
||||||
|
|
||||||
|
- `closeConnection()` now uses `normalClosure` status when closing websocket.
|
||||||
|
- Fixed local unread count indicator increasing for thread replies.
|
||||||
|
- Fixed user presence indicator not updating correctly.
|
||||||
|
- `ChannelEvent.membersCount` defaults to 0 avoiding parsing errors due to missing `members_count` field.
|
||||||
|
|
||||||
## 3.2.0
|
## 3.2.0
|
||||||
|
|
||||||
🐞 Fixed
|
🐞 Fixed
|
||||||
|
|||||||
@@ -233,10 +233,10 @@ class _MessageViewState extends State<MessageView> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -648,7 +648,7 @@ class Channel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Deletes the [message] from the channel.
|
/// Deletes the [message] from the channel.
|
||||||
Future<EmptyResponse> deleteMessage(Message message) async {
|
Future<EmptyResponse> deleteMessage(Message message, {bool? hard}) async {
|
||||||
// Directly deleting the local messages which are not yet sent to server
|
// Directly deleting the local messages which are not yet sent to server
|
||||||
if (message.status == MessageSendingStatus.sending ||
|
if (message.status == MessageSendingStatus.sending ||
|
||||||
message.status == MessageSendingStatus.failed) {
|
message.status == MessageSendingStatus.failed) {
|
||||||
@@ -675,7 +675,7 @@ class Channel {
|
|||||||
|
|
||||||
state?.addMessage(message);
|
state?.addMessage(message);
|
||||||
|
|
||||||
final response = await _client.deleteMessage(message.id);
|
final response = await _client.deleteMessage(message.id, hard: hard);
|
||||||
|
|
||||||
state?.addMessage(message.copyWith(status: MessageSendingStatus.sent));
|
state?.addMessage(message.copyWith(status: MessageSendingStatus.sent));
|
||||||
|
|
||||||
@@ -1466,8 +1466,6 @@ class ChannelClientState {
|
|||||||
|
|
||||||
_listenMemberRemoved();
|
_listenMemberRemoved();
|
||||||
|
|
||||||
_computeUnread();
|
|
||||||
|
|
||||||
_startCleaning();
|
_startCleaning();
|
||||||
|
|
||||||
_startCleaningPinnedMessages();
|
_startCleaningPinnedMessages();
|
||||||
@@ -1490,15 +1488,6 @@ class ChannelClientState {
|
|||||||
|
|
||||||
final _subscriptions = <StreamSubscription>[];
|
final _subscriptions = <StreamSubscription>[];
|
||||||
|
|
||||||
void _computeUnread() {
|
|
||||||
final userRead = channelState.read.firstWhereOrNull(
|
|
||||||
(r) => r.user.id == _channel._client.state.currentUser?.id,
|
|
||||||
);
|
|
||||||
if (userRead != null && userRead.unreadMessages > 0) {
|
|
||||||
unreadCount = userRead.unreadMessages;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _checkExpiredAttachmentMessages(ChannelState channelState) async {
|
void _checkExpiredAttachmentMessages(ChannelState channelState) async {
|
||||||
final expiredAttachmentMessagesId = channelState.messages
|
final expiredAttachmentMessagesId = channelState.messages
|
||||||
.where((m) =>
|
.where((m) =>
|
||||||
@@ -1603,7 +1592,7 @@ class ChannelClientState {
|
|||||||
message.createdAt.isBefore(
|
message.createdAt.isBefore(
|
||||||
DateTime.now().subtract(
|
DateTime.now().subtract(
|
||||||
const Duration(
|
const Duration(
|
||||||
seconds: 1,
|
seconds: 5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1663,7 +1652,11 @@ class ChannelClientState {
|
|||||||
void _listenMessageDeleted() {
|
void _listenMessageDeleted() {
|
||||||
_subscriptions.add(_channel.on(EventType.messageDeleted).listen((event) {
|
_subscriptions.add(_channel.on(EventType.messageDeleted).listen((event) {
|
||||||
final message = event.message!;
|
final message = event.message!;
|
||||||
|
if (event.hardDelete == true) {
|
||||||
|
removeMessage(message, hardDelete: true);
|
||||||
|
} else {
|
||||||
addMessage(message);
|
addMessage(message);
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1705,7 +1698,7 @@ class ChannelClientState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_channelState = _channelState.copyWith(
|
_channelState = _channelState.copyWith(
|
||||||
messages: newMessages,
|
messages: newMessages..sort(_sortByCreatedAt),
|
||||||
channel: _channelState.channel?.copyWith(
|
channel: _channelState.channel?.copyWith(
|
||||||
lastMessageAt: message.createdAt,
|
lastMessageAt: message.createdAt,
|
||||||
),
|
),
|
||||||
@@ -1718,7 +1711,7 @@ class ChannelClientState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Remove a [message] from this [channelState].
|
/// Remove a [message] from this [channelState].
|
||||||
void removeMessage(Message message) {
|
void removeMessage(Message message, {bool hardDelete = false}) {
|
||||||
final parentId = message.parentId;
|
final parentId = message.parentId;
|
||||||
// i.e. it's a thread message
|
// i.e. it's a thread message
|
||||||
// 1. Remove the thread message
|
// 1. Remove the thread message
|
||||||
@@ -1740,7 +1733,10 @@ class ChannelClientState {
|
|||||||
} else {
|
} else {
|
||||||
// Remove regular message
|
// Remove regular message
|
||||||
final allMessages = [...messages];
|
final allMessages = [...messages];
|
||||||
if (allMessages.remove(message)) {
|
if (hardDelete) {
|
||||||
|
allMessages.removeWhere((e) => e.id == message.id);
|
||||||
|
_channelState = _channelState.copyWith(messages: allMessages);
|
||||||
|
} else if (allMessages.remove(message)) {
|
||||||
_channelState = _channelState.copyWith(messages: allMessages);
|
_channelState = _channelState.copyWith(messages: allMessages);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1843,15 +1839,34 @@ class ChannelClientState {
|
|||||||
/// Channel read list as a stream.
|
/// Channel read list as a stream.
|
||||||
Stream<List<Read>> get readStream => channelStateStream.map((cs) => cs.read);
|
Stream<List<Read>> get readStream => channelStateStream.map((cs) => cs.read);
|
||||||
|
|
||||||
final BehaviorSubject<int> _unreadCountController = BehaviorSubject.seeded(0);
|
bool _isCurrentUserRead(Read read) =>
|
||||||
|
read.user.id == _channel._client.state.currentUser!.id;
|
||||||
|
|
||||||
set unreadCount(int value) => _unreadCountController.add(value);
|
/// Channel read for the logged in user.
|
||||||
|
Read? get currentUserRead => read.firstWhereOrNull(_isCurrentUserRead);
|
||||||
|
|
||||||
|
/// Channel read for the logged in user as a stream.
|
||||||
|
Stream<Read?> get currentUserReadStream =>
|
||||||
|
readStream.map((read) => read.firstWhereOrNull(_isCurrentUserRead));
|
||||||
|
|
||||||
/// Unread count getter as a stream.
|
/// Unread count getter as a stream.
|
||||||
Stream<int> get unreadCountStream => _unreadCountController.stream.distinct();
|
Stream<int> get unreadCountStream =>
|
||||||
|
currentUserReadStream.map((read) => read?.unreadMessages ?? 0);
|
||||||
|
|
||||||
/// Unread count getter.
|
/// Unread count getter.
|
||||||
int get unreadCount => _unreadCountController.value;
|
int get unreadCount => currentUserRead?.unreadMessages ?? 0;
|
||||||
|
|
||||||
|
/// Setter for unread count.
|
||||||
|
set unreadCount(int count) {
|
||||||
|
final reads = [..._channelState.read];
|
||||||
|
final currentUserReadIndex = reads.indexWhere(_isCurrentUserRead);
|
||||||
|
|
||||||
|
if (currentUserReadIndex < 0) return;
|
||||||
|
|
||||||
|
reads[currentUserReadIndex] =
|
||||||
|
reads[currentUserReadIndex].copyWith(unreadMessages: count);
|
||||||
|
_channelState = _channelState.copyWith(read: reads);
|
||||||
|
}
|
||||||
|
|
||||||
bool _countMessageAsUnread(Message message) {
|
bool _countMessageAsUnread(Message message) {
|
||||||
final userId = _channel.client.state.currentUser?.id;
|
final userId = _channel.client.state.currentUser?.id;
|
||||||
@@ -1860,10 +1875,13 @@ class ChannelClientState {
|
|||||||
(m) => m.user.id == message.user?.id,
|
(m) => m.user.id == message.user?.id,
|
||||||
) !=
|
) !=
|
||||||
null;
|
null;
|
||||||
|
final isThreadMessage = message.parentId != null;
|
||||||
|
|
||||||
return !message.silent &&
|
return !message.silent &&
|
||||||
!message.shadowed &&
|
!message.shadowed &&
|
||||||
message.user?.id != userId &&
|
message.user?.id != userId &&
|
||||||
!userIsMuted;
|
!userIsMuted &&
|
||||||
|
!isThreadMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update threads with updated information about messages.
|
/// Update threads with updated information about messages.
|
||||||
@@ -2108,7 +2126,6 @@ class ChannelClientState {
|
|||||||
/// Call this method to dispose this object.
|
/// Call this method to dispose this object.
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_debouncedUpdatePersistenceChannelState.cancel();
|
_debouncedUpdatePersistenceChannelState.cancel();
|
||||||
_unreadCountController.close();
|
|
||||||
_retryQueue.dispose();
|
_retryQueue.dispose();
|
||||||
_subscriptions.forEach((s) => s.cancel());
|
_subscriptions.forEach((s) => s.cancel());
|
||||||
_channelStateController.close();
|
_channelStateController.close();
|
||||||
|
|||||||
@@ -44,10 +44,6 @@ final _levelEmojiMapper = {
|
|||||||
Level.SEVERE: '🚨',
|
Level.SEVERE: '🚨',
|
||||||
};
|
};
|
||||||
|
|
||||||
final _userAgent = 'stream-chat-dart-client-'
|
|
||||||
'${CurrentPlatform.name}-'
|
|
||||||
'${PACKAGE_VERSION.split('+')[0]}';
|
|
||||||
|
|
||||||
/// The official Dart client for Stream Chat,
|
/// The official Dart client for Stream Chat,
|
||||||
/// a service for building chat applications.
|
/// a service for building chat applications.
|
||||||
/// This library can be used on any Dart project and on both mobile and web apps
|
/// This library can be used on any Dart project and on both mobile and web apps
|
||||||
@@ -86,7 +82,7 @@ class StreamChatClient {
|
|||||||
location: location,
|
location: location,
|
||||||
connectTimeout: connectTimeout,
|
connectTimeout: connectTimeout,
|
||||||
receiveTimeout: receiveTimeout,
|
receiveTimeout: receiveTimeout,
|
||||||
headers: {'X-Stream-Client': _userAgent},
|
headers: {'X-Stream-Client': defaultUserAgent},
|
||||||
);
|
);
|
||||||
|
|
||||||
_chatApi = chatApi ??
|
_chatApi = chatApi ??
|
||||||
@@ -106,7 +102,7 @@ class StreamChatClient {
|
|||||||
tokenManager: _tokenManager,
|
tokenManager: _tokenManager,
|
||||||
handler: handleEvent,
|
handler: handleEvent,
|
||||||
logger: detachedLogger('🔌'),
|
logger: detachedLogger('🔌'),
|
||||||
queryParameters: {'X-Stream-Client': _userAgent},
|
queryParameters: {'X-Stream-Client': defaultUserAgent},
|
||||||
);
|
);
|
||||||
|
|
||||||
_retryPolicy = retryPolicy ??
|
_retryPolicy = retryPolicy ??
|
||||||
@@ -131,6 +127,14 @@ class StreamChatClient {
|
|||||||
_originalChatPersistenceClient = value;
|
_originalChatPersistenceClient = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Default user agent for all requests
|
||||||
|
static String defaultUserAgent = 'stream-chat-dart-client-'
|
||||||
|
'${CurrentPlatform.name}-'
|
||||||
|
'${PACKAGE_VERSION.split('+')[0]}';
|
||||||
|
|
||||||
|
/// Additionals headers for all requests
|
||||||
|
static Map<String, Object?> additionalHeaders = {};
|
||||||
|
|
||||||
ChatPersistenceClient? _originalChatPersistenceClient;
|
ChatPersistenceClient? _originalChatPersistenceClient;
|
||||||
|
|
||||||
/// Chat persistence client
|
/// Chat persistence client
|
||||||
@@ -391,6 +395,9 @@ class StreamChatClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _handleHealthCheckEvent(Event event) {
|
void _handleHealthCheckEvent(Event event) {
|
||||||
|
final user = event.me;
|
||||||
|
if (user != null) state.currentUser = user;
|
||||||
|
|
||||||
final connectionId = event.connectionId;
|
final connectionId = event.connectionId;
|
||||||
if (connectionId != null) {
|
if (connectionId != null) {
|
||||||
_connectionIdManager.setConnectionId(connectionId);
|
_connectionIdManager.setConnectionId(connectionId);
|
||||||
@@ -1209,8 +1216,14 @@ class StreamChatClient {
|
|||||||
);
|
);
|
||||||
|
|
||||||
/// Deletes the given message
|
/// Deletes the given message
|
||||||
Future<EmptyResponse> deleteMessage(String messageId) =>
|
Future<EmptyResponse> deleteMessage(String messageId, {bool? hard}) async {
|
||||||
_chatApi.message.deleteMessage(messageId);
|
final response =
|
||||||
|
await _chatApi.message.deleteMessage(messageId, hard: hard);
|
||||||
|
if (hard == true) {
|
||||||
|
await _chatPersistenceClient?.deleteMessageById(messageId);
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
/// Get a message by [messageId]
|
/// Get a message by [messageId]
|
||||||
Future<GetMessageResponse> getMessage(String messageId) =>
|
Future<GetMessageResponse> getMessage(String messageId) =>
|
||||||
@@ -1303,6 +1316,10 @@ class StreamChatClient {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// Get OpenGraph data of the given [url].
|
||||||
|
Future<OGAttachmentResponse> enrichUrl(String url) =>
|
||||||
|
_chatApi.general.enrichUrl(url);
|
||||||
|
|
||||||
/// Closes the [_ws] connection and resets the [state]
|
/// Closes the [_ws] connection and resets the [state]
|
||||||
/// If [flushChatPersistence] is true the client deletes all offline
|
/// If [flushChatPersistence] is true the client deletes all offline
|
||||||
/// user's data.
|
/// user's data.
|
||||||
@@ -1428,6 +1445,7 @@ class ClientState {
|
|||||||
.listen((Event event) async {
|
.listen((Event event) async {
|
||||||
final eventChannel = event.channel!;
|
final eventChannel = event.channel!;
|
||||||
await _client.chatPersistenceClient?.deleteChannels([eventChannel.cid]);
|
await _client.chatPersistenceClient?.deleteChannels([eventChannel.cid]);
|
||||||
|
channels[eventChannel.cid]?.dispose();
|
||||||
channels = channels..remove(eventChannel.cid);
|
channels = channels..remove(eventChannel.cid);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,13 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:logging/logging.dart';
|
|
||||||
import 'package:rxdart/rxdart.dart';
|
import 'package:rxdart/rxdart.dart';
|
||||||
import 'package:stream_chat/src/client/channel.dart';
|
|
||||||
import 'package:stream_chat/src/client/retry_policy.dart';
|
import 'package:stream_chat/src/client/retry_policy.dart';
|
||||||
import 'package:stream_chat/src/core/error/error.dart';
|
|
||||||
import 'package:stream_chat/src/core/models/message.dart';
|
|
||||||
import 'package:stream_chat/src/event_type.dart';
|
|
||||||
import 'package:stream_chat/stream_chat.dart';
|
import 'package:stream_chat/stream_chat.dart';
|
||||||
|
|
||||||
/// The retry queue associated to a channel
|
/// The retry queue associated to a channel.
|
||||||
class RetryQueue {
|
class RetryQueue {
|
||||||
/// Instantiate a new RetryQueue object
|
/// Instantiate a new RetryQueue object.
|
||||||
RetryQueue({
|
RetryQueue({
|
||||||
required this.channel,
|
required this.channel,
|
||||||
this.logger,
|
this.logger,
|
||||||
@@ -22,13 +17,13 @@ class RetryQueue {
|
|||||||
_listenFailedEvents();
|
_listenFailedEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The channel of this queue
|
/// The channel of this queue.
|
||||||
final Channel channel;
|
final Channel channel;
|
||||||
|
|
||||||
/// The client associated with this [channel]
|
/// The client associated with this [channel].
|
||||||
final StreamChatClient client;
|
final StreamChatClient client;
|
||||||
|
|
||||||
/// The logger associated to this queue
|
/// The logger associated to this queue.
|
||||||
final Logger? logger;
|
final Logger? logger;
|
||||||
|
|
||||||
late final RetryPolicy _retryPolicy;
|
late final RetryPolicy _retryPolicy;
|
||||||
@@ -68,17 +63,18 @@ class RetryQueue {
|
|||||||
}).addTo(_compositeSubscription);
|
}).addTo(_compositeSubscription);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a list of messages
|
/// Add a list of messages.
|
||||||
void add(List<Message> messages) {
|
void add(List<Message> messages) {
|
||||||
if (messages.isEmpty) return;
|
if (messages.isEmpty) return;
|
||||||
if (_messageQueue.containsAllMessage(messages)) return;
|
if (!_messageQueue.containsAllMessage(messages)) {
|
||||||
|
|
||||||
logger?.info('Adding ${messages.length} messages');
|
logger?.info('Adding ${messages.length} messages');
|
||||||
final messageList = _messageQueue.toList();
|
final messageList = _messageQueue.toList();
|
||||||
// we should not add message if already available in the queue
|
// we should not add message if already available in the queue
|
||||||
_messageQueue.addAll(messages.where(
|
_messageQueue.addAll(messages.where(
|
||||||
(it) => !messageList.any((m) => m.id == it.id),
|
(it) => !messageList.any((m) => m.id == it.id),
|
||||||
));
|
));
|
||||||
|
}
|
||||||
|
|
||||||
_startRetrying();
|
_startRetrying();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,17 +86,21 @@ class RetryQueue {
|
|||||||
while (_messageQueue.isNotEmpty) {
|
while (_messageQueue.isNotEmpty) {
|
||||||
logger?.info('${_messageQueue.length} messages remaining in the queue');
|
logger?.info('${_messageQueue.length} messages remaining in the queue');
|
||||||
final message = _messageQueue.first;
|
final message = _messageQueue.first;
|
||||||
await _runAndRetry(message);
|
final succeeded = await _runAndRetry(message);
|
||||||
|
if (!succeeded) {
|
||||||
|
_messageQueue.toList().forEach(_sendFailedEvent);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_isRetrying = false;
|
_isRetrying = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _runAndRetry(Message message) async {
|
Future<bool> _runAndRetry(Message message) async {
|
||||||
var attempt = 1;
|
var attempt = 1;
|
||||||
|
|
||||||
final maxAttempt = _retryPolicy.maxRetryAttempts;
|
final maxAttempt = _retryPolicy.maxRetryAttempts;
|
||||||
// early return in case maxAttempt is less than 0
|
// early return in case maxAttempt is less than 0
|
||||||
if (attempt > maxAttempt) return;
|
if (attempt > maxAttempt) return false;
|
||||||
|
|
||||||
// ignore: literal_only_boolean_expressions
|
// ignore: literal_only_boolean_expressions
|
||||||
while (true) {
|
while (true) {
|
||||||
@@ -109,8 +109,13 @@ class RetryQueue {
|
|||||||
await _retryMessage(message);
|
await _retryMessage(message);
|
||||||
logger?.info('Message (${message.id}) sent successfully');
|
logger?.info('Message (${message.id}) sent successfully');
|
||||||
_messageQueue.removeMessage(message);
|
_messageQueue.removeMessage(message);
|
||||||
break;
|
return true;
|
||||||
} on StreamChatError catch (e) {
|
} catch (e) {
|
||||||
|
if (e is! StreamChatNetworkError || !e.isRetriable) {
|
||||||
|
_messageQueue.removeMessage(message);
|
||||||
|
_sendFailedEvent(message);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
// retry logic
|
// retry logic
|
||||||
final maxAttempt = _retryPolicy.maxRetryAttempts;
|
final maxAttempt = _retryPolicy.maxRetryAttempts;
|
||||||
if (attempt < maxAttempt) {
|
if (attempt < maxAttempt) {
|
||||||
@@ -143,16 +148,9 @@ class RetryQueue {
|
|||||||
_sendFailedEvent(message);
|
_sendFailedEvent(message);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
logger?.info(
|
|
||||||
'API call failed due to unknown error (attempt $attempt). '
|
|
||||||
'Giving up for now, will retry when connection recovers. '
|
|
||||||
'Error was $e',
|
|
||||||
);
|
|
||||||
_sendFailedEvent(message);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _sendFailedEvent(Message message) {
|
void _sendFailedEvent(Message message) {
|
||||||
@@ -177,10 +175,10 @@ class RetryQueue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether our [_messageQueue] has messages or not
|
/// Whether our [_messageQueue] has messages or not.
|
||||||
bool get hasMessages => _messageQueue.isNotEmpty;
|
bool get hasMessages => _messageQueue.isNotEmpty;
|
||||||
|
|
||||||
/// Call this method to dispose this object
|
/// Call this method to dispose this object.
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_messageQueue.clear();
|
_messageQueue.clear();
|
||||||
_compositeSubscription.dispose();
|
_compositeSubscription.dispose();
|
||||||
|
|||||||
@@ -96,4 +96,16 @@ class GeneralApi {
|
|||||||
|
|
||||||
return QueryMembersResponse.fromJson(response.data);
|
return QueryMembersResponse.fromJson(response.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get OpenGraph data of the given [url].
|
||||||
|
Future<OGAttachmentResponse> enrichUrl(String url) async {
|
||||||
|
final response = await _client.get(
|
||||||
|
'/og',
|
||||||
|
queryParameters: {
|
||||||
|
'url': url,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return OGAttachmentResponse.fromJson(response.data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,10 +80,16 @@ class MessageApi {
|
|||||||
|
|
||||||
/// Deletes the given [messageId]
|
/// Deletes the given [messageId]
|
||||||
Future<EmptyResponse> deleteMessage(
|
Future<EmptyResponse> deleteMessage(
|
||||||
String messageId,
|
String messageId, {
|
||||||
) async {
|
bool? hard,
|
||||||
|
}) async {
|
||||||
final response = await _client.delete(
|
final response = await _client.delete(
|
||||||
'/messages/$messageId',
|
'/messages/$messageId',
|
||||||
|
queryParameters: hard != null
|
||||||
|
? {
|
||||||
|
'hard': hard,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
);
|
);
|
||||||
return EmptyResponse.fromJson(response.data);
|
return EmptyResponse.fromJson(response.data);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,8 +60,11 @@ class PaginationParams extends Equatable {
|
|||||||
/// ```
|
/// ```
|
||||||
const PaginationParams({
|
const PaginationParams({
|
||||||
this.limit = 10,
|
this.limit = 10,
|
||||||
|
this.before = 10,
|
||||||
|
this.after = 10,
|
||||||
this.offset,
|
this.offset,
|
||||||
this.next,
|
this.next,
|
||||||
|
this.idAround,
|
||||||
this.greaterThan,
|
this.greaterThan,
|
||||||
this.greaterThanOrEqual,
|
this.greaterThanOrEqual,
|
||||||
this.lessThan,
|
this.lessThan,
|
||||||
@@ -78,12 +81,22 @@ class PaginationParams extends Equatable {
|
|||||||
/// The amount of items requested from the APIs.
|
/// The amount of items requested from the APIs.
|
||||||
final int limit;
|
final int limit;
|
||||||
|
|
||||||
|
/// The amount of items requested before message ID from the APIs.
|
||||||
|
final int before;
|
||||||
|
|
||||||
|
/// The amount of items requested after message ID from the APIs.
|
||||||
|
final int after;
|
||||||
|
|
||||||
/// The offset of requesting items.
|
/// The offset of requesting items.
|
||||||
final int? offset;
|
final int? offset;
|
||||||
|
|
||||||
/// A key used to paginate.
|
/// A key used to paginate.
|
||||||
final String? next;
|
final String? next;
|
||||||
|
|
||||||
|
/// Message ID to fetch messages around
|
||||||
|
@JsonKey(name: 'id_around')
|
||||||
|
final String? idAround;
|
||||||
|
|
||||||
/// Filter on ids greater than the given value.
|
/// Filter on ids greater than the given value.
|
||||||
@JsonKey(name: 'id_gt')
|
@JsonKey(name: 'id_gt')
|
||||||
final String? greaterThan;
|
final String? greaterThan;
|
||||||
@@ -106,7 +119,10 @@ class PaginationParams extends Equatable {
|
|||||||
/// Creates a copy of [PaginationParams] with specified attributes overridden.
|
/// Creates a copy of [PaginationParams] with specified attributes overridden.
|
||||||
PaginationParams copyWith({
|
PaginationParams copyWith({
|
||||||
int? limit,
|
int? limit,
|
||||||
|
int? before,
|
||||||
|
int? after,
|
||||||
int? offset,
|
int? offset,
|
||||||
|
String? idAround,
|
||||||
String? next,
|
String? next,
|
||||||
String? greaterThan,
|
String? greaterThan,
|
||||||
String? greaterThanOrEqual,
|
String? greaterThanOrEqual,
|
||||||
@@ -115,7 +131,10 @@ class PaginationParams extends Equatable {
|
|||||||
}) =>
|
}) =>
|
||||||
PaginationParams(
|
PaginationParams(
|
||||||
limit: limit ?? this.limit,
|
limit: limit ?? this.limit,
|
||||||
|
before: before ?? this.before,
|
||||||
|
after: limit ?? this.after,
|
||||||
offset: offset ?? this.offset,
|
offset: offset ?? this.offset,
|
||||||
|
idAround: idAround ?? this.idAround,
|
||||||
next: next ?? this.next,
|
next: next ?? this.next,
|
||||||
greaterThan: greaterThan ?? this.greaterThan,
|
greaterThan: greaterThan ?? this.greaterThan,
|
||||||
greaterThanOrEqual: greaterThanOrEqual ?? this.greaterThanOrEqual,
|
greaterThanOrEqual: greaterThanOrEqual ?? this.greaterThanOrEqual,
|
||||||
@@ -126,8 +145,11 @@ class PaginationParams extends Equatable {
|
|||||||
@override
|
@override
|
||||||
List<Object?> get props => [
|
List<Object?> get props => [
|
||||||
limit,
|
limit,
|
||||||
|
before,
|
||||||
|
after,
|
||||||
offset,
|
offset,
|
||||||
next,
|
next,
|
||||||
|
idAround,
|
||||||
greaterThan,
|
greaterThan,
|
||||||
greaterThanOrEqual,
|
greaterThanOrEqual,
|
||||||
lessThan,
|
lessThan,
|
||||||
|
|||||||
@@ -21,8 +21,11 @@ Map<String, dynamic> _$SortOptionToJson<T>(SortOption<T> instance) =>
|
|||||||
PaginationParams _$PaginationParamsFromJson(Map<String, dynamic> json) =>
|
PaginationParams _$PaginationParamsFromJson(Map<String, dynamic> json) =>
|
||||||
PaginationParams(
|
PaginationParams(
|
||||||
limit: json['limit'] as int? ?? 10,
|
limit: json['limit'] as int? ?? 10,
|
||||||
|
before: json['before'] as int? ?? 10,
|
||||||
|
after: json['after'] as int? ?? 10,
|
||||||
offset: json['offset'] as int?,
|
offset: json['offset'] as int?,
|
||||||
next: json['next'] as String?,
|
next: json['next'] as String?,
|
||||||
|
idAround: json['id_around'] as String?,
|
||||||
greaterThan: json['id_gt'] as String?,
|
greaterThan: json['id_gt'] as String?,
|
||||||
greaterThanOrEqual: json['id_gte'] as String?,
|
greaterThanOrEqual: json['id_gte'] as String?,
|
||||||
lessThan: json['id_lt'] as String?,
|
lessThan: json['id_lt'] as String?,
|
||||||
@@ -32,6 +35,8 @@ PaginationParams _$PaginationParamsFromJson(Map<String, dynamic> json) =>
|
|||||||
Map<String, dynamic> _$PaginationParamsToJson(PaginationParams instance) {
|
Map<String, dynamic> _$PaginationParamsToJson(PaginationParams instance) {
|
||||||
final val = <String, dynamic>{
|
final val = <String, dynamic>{
|
||||||
'limit': instance.limit,
|
'limit': instance.limit,
|
||||||
|
'before': instance.before,
|
||||||
|
'after': instance.after,
|
||||||
};
|
};
|
||||||
|
|
||||||
void writeNotNull(String key, dynamic value) {
|
void writeNotNull(String key, dynamic value) {
|
||||||
@@ -42,6 +47,7 @@ Map<String, dynamic> _$PaginationParamsToJson(PaginationParams instance) {
|
|||||||
|
|
||||||
writeNotNull('offset', instance.offset);
|
writeNotNull('offset', instance.offset);
|
||||||
writeNotNull('next', instance.next);
|
writeNotNull('next', instance.next);
|
||||||
|
writeNotNull('id_around', instance.idAround);
|
||||||
writeNotNull('id_gt', instance.greaterThan);
|
writeNotNull('id_gt', instance.greaterThan);
|
||||||
writeNotNull('id_gte', instance.greaterThanOrEqual);
|
writeNotNull('id_gte', instance.greaterThanOrEqual);
|
||||||
writeNotNull('id_lt', instance.lessThan);
|
writeNotNull('id_lt', instance.lessThan);
|
||||||
|
|||||||
@@ -442,3 +442,43 @@ class ChannelStateResponse extends _BaseResponse {
|
|||||||
static ChannelStateResponse fromJson(Map<String, dynamic> json) =>
|
static ChannelStateResponse fromJson(Map<String, dynamic> json) =>
|
||||||
_$ChannelStateResponseFromJson(json);
|
_$ChannelStateResponseFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Model response for [Client.enrichUrl] api call.
|
||||||
|
@JsonSerializable(createToJson: false)
|
||||||
|
class OGAttachmentResponse extends _BaseResponse {
|
||||||
|
/// The URL of the page that was scraped.
|
||||||
|
late String ogScrapeUrl;
|
||||||
|
|
||||||
|
/// The URL of the asset.
|
||||||
|
String? assetUrl;
|
||||||
|
|
||||||
|
/// The URL of the author.
|
||||||
|
String? authorLink;
|
||||||
|
|
||||||
|
/// The name of the author.
|
||||||
|
String? authorName;
|
||||||
|
|
||||||
|
/// The URL of the image.
|
||||||
|
String? imageUrl;
|
||||||
|
|
||||||
|
/// The text of the attachment.
|
||||||
|
String? text;
|
||||||
|
|
||||||
|
/// The URL of the thumbnail.
|
||||||
|
String? thumbUrl;
|
||||||
|
|
||||||
|
/// The title of the attachment.
|
||||||
|
String? title;
|
||||||
|
|
||||||
|
/// The URL of the title.
|
||||||
|
String? titleLink;
|
||||||
|
|
||||||
|
/// The type of the attachment.
|
||||||
|
///
|
||||||
|
/// 'video' | 'audio' | 'image'
|
||||||
|
String? type;
|
||||||
|
|
||||||
|
/// Create a new instance from a [json].
|
||||||
|
static OGAttachmentResponse fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$OGAttachmentResponseFromJson(json);
|
||||||
|
}
|
||||||
|
|||||||
@@ -273,3 +273,18 @@ ChannelStateResponse _$ChannelStateResponseFromJson(
|
|||||||
?.map((e) => Read.fromJson(e as Map<String, dynamic>))
|
?.map((e) => Read.fromJson(e as Map<String, dynamic>))
|
||||||
.toList() ??
|
.toList() ??
|
||||||
[];
|
[];
|
||||||
|
|
||||||
|
OGAttachmentResponse _$OGAttachmentResponseFromJson(
|
||||||
|
Map<String, dynamic> json) =>
|
||||||
|
OGAttachmentResponse()
|
||||||
|
..duration = json['duration'] as String?
|
||||||
|
..ogScrapeUrl = json['og_scrape_url'] as String
|
||||||
|
..assetUrl = json['asset_url'] as String?
|
||||||
|
..authorLink = json['author_link'] as String?
|
||||||
|
..authorName = json['author_name'] as String?
|
||||||
|
..imageUrl = json['image_url'] as String?
|
||||||
|
..text = json['text'] as String?
|
||||||
|
..thumbUrl = json['thumb_url'] as String?
|
||||||
|
..title = json['title'] as String?
|
||||||
|
..titleLink = json['title_link'] as String?
|
||||||
|
..type = json['type'] as String?;
|
||||||
|
|||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:stream_chat/stream_chat.dart';
|
||||||
|
|
||||||
|
/// Interceptor that sets additional headers for all requests.
|
||||||
|
class AdditionalHeadersInterceptor extends Interceptor {
|
||||||
|
@override
|
||||||
|
Future<void> onRequest(
|
||||||
|
RequestOptions options,
|
||||||
|
RequestInterceptorHandler handler,
|
||||||
|
) async {
|
||||||
|
options.headers = {
|
||||||
|
...options.headers,
|
||||||
|
...StreamChatClient.additionalHeaders,
|
||||||
|
};
|
||||||
|
return handler.next(options);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import 'package:logging/logging.dart';
|
|||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
import 'package:stream_chat/src/core/error/error.dart';
|
import 'package:stream_chat/src/core/error/error.dart';
|
||||||
import 'package:stream_chat/src/core/http/connection_id_manager.dart';
|
import 'package:stream_chat/src/core/http/connection_id_manager.dart';
|
||||||
|
import 'package:stream_chat/src/core/http/interceptor/additional_headers_interceptor.dart';
|
||||||
import 'package:stream_chat/src/core/http/interceptor/auth_interceptor.dart';
|
import 'package:stream_chat/src/core/http/interceptor/auth_interceptor.dart';
|
||||||
import 'package:stream_chat/src/core/http/interceptor/connection_id_interceptor.dart';
|
import 'package:stream_chat/src/core/http/interceptor/connection_id_interceptor.dart';
|
||||||
import 'package:stream_chat/src/core/http/interceptor/logging_interceptor.dart';
|
import 'package:stream_chat/src/core/http/interceptor/logging_interceptor.dart';
|
||||||
@@ -41,6 +42,7 @@ class StreamHttpClient {
|
|||||||
..._options.headers,
|
..._options.headers,
|
||||||
}
|
}
|
||||||
..interceptors.addAll([
|
..interceptors.addAll([
|
||||||
|
AdditionalHeadersInterceptor(),
|
||||||
if (tokenManager != null) AuthInterceptor(this, tokenManager),
|
if (tokenManager != null) AuthInterceptor(this, tokenManager),
|
||||||
if (connectionIdManager != null)
|
if (connectionIdManager != null)
|
||||||
ConnectionIdInterceptor(connectionIdManager),
|
ConnectionIdInterceptor(connectionIdManager),
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ final _privateConstructorUsedError = UnsupportedError(
|
|||||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more informations: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
|
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more informations: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
|
||||||
|
|
||||||
UploadState _$UploadStateFromJson(Map<String, dynamic> json) {
|
UploadState _$UploadStateFromJson(Map<String, dynamic> json) {
|
||||||
switch (json['runtimeType'] as String?) {
|
switch (json['runtimeType']) {
|
||||||
case 'preparing':
|
case 'preparing':
|
||||||
return Preparing.fromJson(json);
|
return Preparing.fromJson(json);
|
||||||
case 'inProgress':
|
case 'inProgress':
|
||||||
@@ -55,7 +55,7 @@ class _$UploadStateTearOff {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
UploadState fromJson(Map<String, Object> json) {
|
UploadState fromJson(Map<String, Object?> json) {
|
||||||
return UploadState.fromJson(json);
|
return UploadState.fromJson(json);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -153,11 +153,14 @@ class _$PreparingCopyWithImpl<$Res> extends _$UploadStateCopyWithImpl<$Res>
|
|||||||
/// @nodoc
|
/// @nodoc
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class _$Preparing implements Preparing {
|
class _$Preparing implements Preparing {
|
||||||
const _$Preparing();
|
const _$Preparing({String? $type}) : $type = $type ?? 'preparing';
|
||||||
|
|
||||||
factory _$Preparing.fromJson(Map<String, dynamic> json) =>
|
factory _$Preparing.fromJson(Map<String, dynamic> json) =>
|
||||||
_$$PreparingFromJson(json);
|
_$$PreparingFromJson(json);
|
||||||
|
|
||||||
|
@JsonKey(name: 'runtimeType')
|
||||||
|
final String $type;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'UploadState.preparing()';
|
return 'UploadState.preparing()';
|
||||||
@@ -165,7 +168,8 @@ class _$Preparing implements Preparing {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(dynamic other) {
|
bool operator ==(dynamic other) {
|
||||||
return identical(this, other) || (other is Preparing);
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType && other is Preparing);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -247,7 +251,7 @@ class _$Preparing implements Preparing {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
return _$$PreparingToJson(this)..['runtimeType'] = 'preparing';
|
return _$$PreparingToJson(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,7 +299,9 @@ class _$InProgressCopyWithImpl<$Res> extends _$UploadStateCopyWithImpl<$Res>
|
|||||||
/// @nodoc
|
/// @nodoc
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class _$InProgress implements InProgress {
|
class _$InProgress implements InProgress {
|
||||||
const _$InProgress({required this.uploaded, required this.total});
|
const _$InProgress(
|
||||||
|
{required this.uploaded, required this.total, String? $type})
|
||||||
|
: $type = $type ?? 'inProgress';
|
||||||
|
|
||||||
factory _$InProgress.fromJson(Map<String, dynamic> json) =>
|
factory _$InProgress.fromJson(Map<String, dynamic> json) =>
|
||||||
_$$InProgressFromJson(json);
|
_$$InProgressFromJson(json);
|
||||||
@@ -305,6 +311,9 @@ class _$InProgress implements InProgress {
|
|||||||
@override
|
@override
|
||||||
final int total;
|
final int total;
|
||||||
|
|
||||||
|
@JsonKey(name: 'runtimeType')
|
||||||
|
final String $type;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'UploadState.inProgress(uploaded: $uploaded, total: $total)';
|
return 'UploadState.inProgress(uploaded: $uploaded, total: $total)';
|
||||||
@@ -313,19 +322,15 @@ class _$InProgress implements InProgress {
|
|||||||
@override
|
@override
|
||||||
bool operator ==(dynamic other) {
|
bool operator ==(dynamic other) {
|
||||||
return identical(this, other) ||
|
return identical(this, other) ||
|
||||||
(other is InProgress &&
|
(other.runtimeType == runtimeType &&
|
||||||
|
other is InProgress &&
|
||||||
(identical(other.uploaded, uploaded) ||
|
(identical(other.uploaded, uploaded) ||
|
||||||
const DeepCollectionEquality()
|
other.uploaded == uploaded) &&
|
||||||
.equals(other.uploaded, uploaded)) &&
|
(identical(other.total, total) || other.total == total));
|
||||||
(identical(other.total, total) ||
|
|
||||||
const DeepCollectionEquality().equals(other.total, total)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode => Object.hash(runtimeType, uploaded, total);
|
||||||
runtimeType.hashCode ^
|
|
||||||
const DeepCollectionEquality().hash(uploaded) ^
|
|
||||||
const DeepCollectionEquality().hash(total);
|
|
||||||
|
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
@override
|
@override
|
||||||
@@ -408,7 +413,7 @@ class _$InProgress implements InProgress {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
return _$$InProgressToJson(this)..['runtimeType'] = 'inProgress';
|
return _$$InProgressToJson(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -419,8 +424,8 @@ abstract class InProgress implements UploadState {
|
|||||||
factory InProgress.fromJson(Map<String, dynamic> json) =
|
factory InProgress.fromJson(Map<String, dynamic> json) =
|
||||||
_$InProgress.fromJson;
|
_$InProgress.fromJson;
|
||||||
|
|
||||||
int get uploaded => throw _privateConstructorUsedError;
|
int get uploaded;
|
||||||
int get total => throw _privateConstructorUsedError;
|
int get total;
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
$InProgressCopyWith<InProgress> get copyWith =>
|
$InProgressCopyWith<InProgress> get copyWith =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
@@ -445,11 +450,14 @@ class _$SuccessCopyWithImpl<$Res> extends _$UploadStateCopyWithImpl<$Res>
|
|||||||
/// @nodoc
|
/// @nodoc
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class _$Success implements Success {
|
class _$Success implements Success {
|
||||||
const _$Success();
|
const _$Success({String? $type}) : $type = $type ?? 'success';
|
||||||
|
|
||||||
factory _$Success.fromJson(Map<String, dynamic> json) =>
|
factory _$Success.fromJson(Map<String, dynamic> json) =>
|
||||||
_$$SuccessFromJson(json);
|
_$$SuccessFromJson(json);
|
||||||
|
|
||||||
|
@JsonKey(name: 'runtimeType')
|
||||||
|
final String $type;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'UploadState.success()';
|
return 'UploadState.success()';
|
||||||
@@ -457,7 +465,8 @@ class _$Success implements Success {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(dynamic other) {
|
bool operator ==(dynamic other) {
|
||||||
return identical(this, other) || (other is Success);
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType && other is Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -539,7 +548,7 @@ class _$Success implements Success {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
return _$$SuccessToJson(this)..['runtimeType'] = 'success';
|
return _$$SuccessToJson(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -581,7 +590,8 @@ class _$FailedCopyWithImpl<$Res> extends _$UploadStateCopyWithImpl<$Res>
|
|||||||
/// @nodoc
|
/// @nodoc
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class _$Failed implements Failed {
|
class _$Failed implements Failed {
|
||||||
const _$Failed({required this.error});
|
const _$Failed({required this.error, String? $type})
|
||||||
|
: $type = $type ?? 'failed';
|
||||||
|
|
||||||
factory _$Failed.fromJson(Map<String, dynamic> json) =>
|
factory _$Failed.fromJson(Map<String, dynamic> json) =>
|
||||||
_$$FailedFromJson(json);
|
_$$FailedFromJson(json);
|
||||||
@@ -589,6 +599,9 @@ class _$Failed implements Failed {
|
|||||||
@override
|
@override
|
||||||
final String error;
|
final String error;
|
||||||
|
|
||||||
|
@JsonKey(name: 'runtimeType')
|
||||||
|
final String $type;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'UploadState.failed(error: $error)';
|
return 'UploadState.failed(error: $error)';
|
||||||
@@ -597,14 +610,13 @@ class _$Failed implements Failed {
|
|||||||
@override
|
@override
|
||||||
bool operator ==(dynamic other) {
|
bool operator ==(dynamic other) {
|
||||||
return identical(this, other) ||
|
return identical(this, other) ||
|
||||||
(other is Failed &&
|
(other.runtimeType == runtimeType &&
|
||||||
(identical(other.error, error) ||
|
other is Failed &&
|
||||||
const DeepCollectionEquality().equals(other.error, error)));
|
(identical(other.error, error) || other.error == error));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode => Object.hash(runtimeType, error);
|
||||||
runtimeType.hashCode ^ const DeepCollectionEquality().hash(error);
|
|
||||||
|
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
@override
|
@override
|
||||||
@@ -687,7 +699,7 @@ class _$Failed implements Failed {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
return _$$FailedToJson(this)..['runtimeType'] = 'failed';
|
return _$$FailedToJson(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -696,7 +708,7 @@ abstract class Failed implements UploadState {
|
|||||||
|
|
||||||
factory Failed.fromJson(Map<String, dynamic> json) = _$Failed.fromJson;
|
factory Failed.fromJson(Map<String, dynamic> json) = _$Failed.fromJson;
|
||||||
|
|
||||||
String get error => throw _privateConstructorUsedError;
|
String get error;
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
$FailedCopyWith<Failed> get copyWith => throw _privateConstructorUsedError;
|
$FailedCopyWith<Failed> get copyWith => throw _privateConstructorUsedError;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,31 +22,42 @@ Map<String, dynamic> _$AttachmentFileToJson(AttachmentFile instance) =>
|
|||||||
'size': instance.size,
|
'size': instance.size,
|
||||||
};
|
};
|
||||||
|
|
||||||
_$Preparing _$$PreparingFromJson(Map<String, dynamic> json) => _$Preparing();
|
_$Preparing _$$PreparingFromJson(Map<String, dynamic> json) => _$Preparing(
|
||||||
|
$type: json['runtimeType'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$$PreparingToJson(_$Preparing instance) =>
|
Map<String, dynamic> _$$PreparingToJson(_$Preparing instance) =>
|
||||||
<String, dynamic>{};
|
<String, dynamic>{
|
||||||
|
'runtimeType': instance.$type,
|
||||||
|
};
|
||||||
|
|
||||||
_$InProgress _$$InProgressFromJson(Map<String, dynamic> json) => _$InProgress(
|
_$InProgress _$$InProgressFromJson(Map<String, dynamic> json) => _$InProgress(
|
||||||
uploaded: json['uploaded'] as int,
|
uploaded: json['uploaded'] as int,
|
||||||
total: json['total'] as int,
|
total: json['total'] as int,
|
||||||
|
$type: json['runtimeType'] as String?,
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$$InProgressToJson(_$InProgress instance) =>
|
Map<String, dynamic> _$$InProgressToJson(_$InProgress instance) =>
|
||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
'uploaded': instance.uploaded,
|
'uploaded': instance.uploaded,
|
||||||
'total': instance.total,
|
'total': instance.total,
|
||||||
|
'runtimeType': instance.$type,
|
||||||
};
|
};
|
||||||
|
|
||||||
_$Success _$$SuccessFromJson(Map<String, dynamic> json) => _$Success();
|
_$Success _$$SuccessFromJson(Map<String, dynamic> json) => _$Success(
|
||||||
|
$type: json['runtimeType'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$$SuccessToJson(_$Success instance) =>
|
Map<String, dynamic> _$$SuccessToJson(_$Success instance) => <String, dynamic>{
|
||||||
<String, dynamic>{};
|
'runtimeType': instance.$type,
|
||||||
|
};
|
||||||
|
|
||||||
_$Failed _$$FailedFromJson(Map<String, dynamic> json) => _$Failed(
|
_$Failed _$$FailedFromJson(Map<String, dynamic> json) => _$Failed(
|
||||||
error: json['error'] as String,
|
error: json['error'] as String,
|
||||||
|
$type: json['runtimeType'] as String?,
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$$FailedToJson(_$Failed instance) => <String, dynamic>{
|
Map<String, dynamic> _$$FailedToJson(_$Failed instance) => <String, dynamic>{
|
||||||
'error': instance.error,
|
'error': instance.error,
|
||||||
|
'runtimeType': instance.$type,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class Event {
|
|||||||
this.channelId,
|
this.channelId,
|
||||||
this.channelType,
|
this.channelType,
|
||||||
this.parentId,
|
this.parentId,
|
||||||
|
this.hardDelete,
|
||||||
this.extraData = const {},
|
this.extraData = const {},
|
||||||
this.isLocal = true,
|
this.isLocal = true,
|
||||||
}) : createdAt = createdAt?.toUtc() ?? DateTime.now().toUtc();
|
}) : createdAt = createdAt?.toUtc() ?? DateTime.now().toUtc();
|
||||||
@@ -91,6 +92,10 @@ class Event {
|
|||||||
@JsonKey(defaultValue: false)
|
@JsonKey(defaultValue: false)
|
||||||
final bool isLocal;
|
final bool isLocal;
|
||||||
|
|
||||||
|
/// This is true if the message has been hard deleted
|
||||||
|
@JsonKey(includeIfNull: false)
|
||||||
|
final bool? hardDelete;
|
||||||
|
|
||||||
/// Map of custom channel extraData
|
/// Map of custom channel extraData
|
||||||
final Map<String, Object?> extraData;
|
final Map<String, Object?> extraData;
|
||||||
|
|
||||||
@@ -113,6 +118,7 @@ class Event {
|
|||||||
'channel_id',
|
'channel_id',
|
||||||
'channel_type',
|
'channel_type',
|
||||||
'parent_id',
|
'parent_id',
|
||||||
|
'hard_delete',
|
||||||
'is_local',
|
'is_local',
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -139,6 +145,7 @@ class Event {
|
|||||||
int? unreadChannels,
|
int? unreadChannels,
|
||||||
bool? online,
|
bool? online,
|
||||||
String? parentId,
|
String? parentId,
|
||||||
|
bool? hardDelete,
|
||||||
Map<String, Object?>? extraData,
|
Map<String, Object?>? extraData,
|
||||||
}) =>
|
}) =>
|
||||||
Event(
|
Event(
|
||||||
@@ -158,6 +165,7 @@ class Event {
|
|||||||
channelId: channelId ?? this.channelId,
|
channelId: channelId ?? this.channelId,
|
||||||
channelType: channelType ?? this.channelType,
|
channelType: channelType ?? this.channelType,
|
||||||
parentId: parentId ?? this.parentId,
|
parentId: parentId ?? this.parentId,
|
||||||
|
hardDelete: hardDelete ?? this.hardDelete,
|
||||||
extraData: extraData ?? this.extraData,
|
extraData: extraData ?? this.extraData,
|
||||||
isLocal: isLocal,
|
isLocal: isLocal,
|
||||||
);
|
);
|
||||||
@@ -181,7 +189,7 @@ class EventChannel extends ChannelModel {
|
|||||||
required DateTime createdAt,
|
required DateTime createdAt,
|
||||||
required DateTime updatedAt,
|
required DateTime updatedAt,
|
||||||
DateTime? deletedAt,
|
DateTime? deletedAt,
|
||||||
required int memberCount,
|
int memberCount = 0,
|
||||||
Map<String, Object?>? extraData,
|
Map<String, Object?>? extraData,
|
||||||
int cooldown = 0,
|
int cooldown = 0,
|
||||||
String? team,
|
String? team,
|
||||||
|
|||||||
@@ -37,11 +37,13 @@ Event _$EventFromJson(Map<String, dynamic> json) => Event(
|
|||||||
channelId: json['channel_id'] as String?,
|
channelId: json['channel_id'] as String?,
|
||||||
channelType: json['channel_type'] as String?,
|
channelType: json['channel_type'] as String?,
|
||||||
parentId: json['parent_id'] as String?,
|
parentId: json['parent_id'] as String?,
|
||||||
|
hardDelete: json['hard_delete'] as bool?,
|
||||||
extraData: json['extra_data'] as Map<String, dynamic>? ?? const {},
|
extraData: json['extra_data'] as Map<String, dynamic>? ?? const {},
|
||||||
isLocal: json['is_local'] as bool? ?? false,
|
isLocal: json['is_local'] as bool? ?? false,
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$EventToJson(Event instance) => <String, dynamic>{
|
Map<String, dynamic> _$EventToJson(Event instance) {
|
||||||
|
final val = <String, dynamic>{
|
||||||
'type': instance.type,
|
'type': instance.type,
|
||||||
'cid': instance.cid,
|
'cid': instance.cid,
|
||||||
'channel_id': instance.channelId,
|
'channel_id': instance.channelId,
|
||||||
@@ -59,9 +61,19 @@ Map<String, dynamic> _$EventToJson(Event instance) => <String, dynamic>{
|
|||||||
'online': instance.online,
|
'online': instance.online,
|
||||||
'parent_id': instance.parentId,
|
'parent_id': instance.parentId,
|
||||||
'is_local': instance.isLocal,
|
'is_local': instance.isLocal,
|
||||||
'extra_data': instance.extraData,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
void writeNotNull(String key, dynamic value) {
|
||||||
|
if (value != null) {
|
||||||
|
val[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeNotNull('hard_delete', instance.hardDelete);
|
||||||
|
val['extra_data'] = instance.extraData;
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
EventChannel _$EventChannelFromJson(Map<String, dynamic> json) => EventChannel(
|
EventChannel _$EventChannelFromJson(Map<String, dynamic> json) => EventChannel(
|
||||||
members: (json['members'] as List<dynamic>?)
|
members: (json['members'] as List<dynamic>?)
|
||||||
?.map((e) => Member.fromJson(e as Map<String, dynamic>))
|
?.map((e) => Member.fromJson(e as Map<String, dynamic>))
|
||||||
@@ -82,7 +94,7 @@ EventChannel _$EventChannelFromJson(Map<String, dynamic> json) => EventChannel(
|
|||||||
deletedAt: json['deleted_at'] == null
|
deletedAt: json['deleted_at'] == null
|
||||||
? null
|
? null
|
||||||
: DateTime.parse(json['deleted_at'] as String),
|
: DateTime.parse(json['deleted_at'] as String),
|
||||||
memberCount: json['member_count'] as int,
|
memberCount: json['member_count'] as int? ?? 0,
|
||||||
extraData: json['extra_data'] as Map<String, dynamic>?,
|
extraData: json['extra_data'] as Map<String, dynamic>?,
|
||||||
cooldown: json['cooldown'] as int? ?? 0,
|
cooldown: json['cooldown'] as int? ?? 0,
|
||||||
team: json['team'] as String?,
|
team: json['team'] as String?,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class _PinExpires {
|
|||||||
|
|
||||||
const _pinExpires = _PinExpires();
|
const _pinExpires = _PinExpires();
|
||||||
|
|
||||||
/// Enum defining the status of a sending message
|
/// Enum defining the status of a sending message.
|
||||||
enum MessageSendingStatus {
|
enum MessageSendingStatus {
|
||||||
/// Message is being sent
|
/// Message is being sent
|
||||||
sending,
|
sending,
|
||||||
@@ -40,10 +40,10 @@ enum MessageSendingStatus {
|
|||||||
sent,
|
sent,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The class that contains the information about a message
|
/// The class that contains the information about a message.
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class Message extends Equatable {
|
class Message extends Equatable {
|
||||||
/// Constructor used for json serialization
|
/// Constructor used for json serialization.
|
||||||
Message({
|
Message({
|
||||||
String? id,
|
String? id,
|
||||||
this.text,
|
this.text,
|
||||||
@@ -58,44 +58,47 @@ class Message extends Equatable {
|
|||||||
this.ownReactions,
|
this.ownReactions,
|
||||||
this.parentId,
|
this.parentId,
|
||||||
this.quotedMessage,
|
this.quotedMessage,
|
||||||
this.quotedMessageId,
|
String? quotedMessageId,
|
||||||
this.replyCount = 0,
|
this.replyCount = 0,
|
||||||
this.threadParticipants,
|
this.threadParticipants,
|
||||||
this.showInChannel,
|
this.showInChannel,
|
||||||
this.command,
|
this.command,
|
||||||
DateTime? createdAt,
|
DateTime? createdAt,
|
||||||
DateTime? updatedAt,
|
DateTime? updatedAt,
|
||||||
|
this.deletedAt,
|
||||||
this.user,
|
this.user,
|
||||||
this.pinned = false,
|
this.pinned = false,
|
||||||
this.pinnedAt,
|
this.pinnedAt,
|
||||||
DateTime? pinExpires,
|
DateTime? pinExpires,
|
||||||
this.pinnedBy,
|
this.pinnedBy,
|
||||||
this.extraData = const {},
|
this.extraData = const {},
|
||||||
this.deletedAt,
|
this.status = MessageSendingStatus.sending,
|
||||||
this.status = MessageSendingStatus.sent,
|
|
||||||
this.i18n,
|
this.i18n,
|
||||||
}) : id = id ?? const Uuid().v4(),
|
}) : id = id ?? const Uuid().v4(),
|
||||||
pinExpires = pinExpires?.toUtc(),
|
pinExpires = pinExpires?.toUtc(),
|
||||||
createdAt = createdAt ?? DateTime.now(),
|
_createdAt = createdAt,
|
||||||
updatedAt = updatedAt ?? DateTime.now();
|
_updatedAt = updatedAt,
|
||||||
|
_quotedMessageId = quotedMessageId;
|
||||||
|
|
||||||
/// Create a new instance from a json
|
/// Create a new instance from JSON.
|
||||||
factory Message.fromJson(Map<String, dynamic> json) => _$MessageFromJson(
|
factory Message.fromJson(Map<String, dynamic> json) => _$MessageFromJson(
|
||||||
Serializer.moveToExtraDataFromRoot(json, topLevelFields),
|
Serializer.moveToExtraDataFromRoot(json, topLevelFields),
|
||||||
|
).copyWith(
|
||||||
|
status: MessageSendingStatus.sent,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// The message ID. This is either created by Stream or set client side when
|
/// The message ID. This is either created by Stream or set client side when
|
||||||
/// the message is added.
|
/// the message is added.
|
||||||
final String id;
|
final String id;
|
||||||
|
|
||||||
/// The text of this message
|
/// The text of this message.
|
||||||
final String? text;
|
final String? text;
|
||||||
|
|
||||||
/// The status of a sending message
|
/// The status of a sending message.
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
final MessageSendingStatus status;
|
final MessageSendingStatus status;
|
||||||
|
|
||||||
/// The message type
|
/// The message type.
|
||||||
@JsonKey(
|
@JsonKey(
|
||||||
includeIfNull: false,
|
includeIfNull: false,
|
||||||
toJson: Serializer.readOnly,
|
toJson: Serializer.readOnly,
|
||||||
@@ -107,15 +110,15 @@ class Message extends Equatable {
|
|||||||
@JsonKey(includeIfNull: false)
|
@JsonKey(includeIfNull: false)
|
||||||
final List<Attachment> attachments;
|
final List<Attachment> attachments;
|
||||||
|
|
||||||
/// The list of user mentioned in the message
|
/// The list of user mentioned in the message.
|
||||||
@JsonKey(toJson: User.toIds)
|
@JsonKey(toJson: User.toIds)
|
||||||
final List<User> mentionedUsers;
|
final List<User> mentionedUsers;
|
||||||
|
|
||||||
/// A map describing the count of number of every reaction
|
/// A map describing the count of number of every reaction.
|
||||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||||
final Map<String, int>? reactionCounts;
|
final Map<String, int>? reactionCounts;
|
||||||
|
|
||||||
/// A map describing the count of score of every reaction
|
/// A map describing the count of score of every reaction.
|
||||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||||
final Map<String, int>? reactionScores;
|
final Map<String, int>? reactionScores;
|
||||||
|
|
||||||
@@ -130,12 +133,14 @@ class Message extends Equatable {
|
|||||||
/// The ID of the parent message, if the message is a thread reply.
|
/// The ID of the parent message, if the message is a thread reply.
|
||||||
final String? parentId;
|
final String? parentId;
|
||||||
|
|
||||||
/// A quoted reply message
|
/// A quoted reply message.
|
||||||
@JsonKey(toJson: Serializer.readOnly)
|
@JsonKey(toJson: Serializer.readOnly)
|
||||||
final Message? quotedMessage;
|
final Message? quotedMessage;
|
||||||
|
|
||||||
|
final String? _quotedMessageId;
|
||||||
|
|
||||||
/// The ID of the quoted message, if the message is a quoted reply.
|
/// The ID of the quoted message, if the message is a quoted reply.
|
||||||
final String? quotedMessageId;
|
String? get quotedMessageId => _quotedMessageId ?? quotedMessage?.id;
|
||||||
|
|
||||||
/// Reserved field indicating the number of replies for this message.
|
/// Reserved field indicating the number of replies for this message.
|
||||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||||
@@ -148,10 +153,10 @@ class Message extends Equatable {
|
|||||||
/// Check if this message needs to show in the channel.
|
/// Check if this message needs to show in the channel.
|
||||||
final bool? showInChannel;
|
final bool? showInChannel;
|
||||||
|
|
||||||
/// If true the message is silent
|
/// If true the message is silent.
|
||||||
final bool silent;
|
final bool silent;
|
||||||
|
|
||||||
/// If true the message is shadowed
|
/// If true the message is shadowed.
|
||||||
@JsonKey(
|
@JsonKey(
|
||||||
includeIfNull: false,
|
includeIfNull: false,
|
||||||
toJson: Serializer.readOnly,
|
toJson: Serializer.readOnly,
|
||||||
@@ -162,56 +167,61 @@ class Message extends Equatable {
|
|||||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||||
final String? command;
|
final String? command;
|
||||||
|
|
||||||
/// Reserved field indicating when the message was created.
|
final DateTime? _createdAt;
|
||||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
|
||||||
final DateTime createdAt;
|
|
||||||
|
|
||||||
/// Reserved field indicating when the message was updated last time.
|
|
||||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
|
||||||
final DateTime updatedAt;
|
|
||||||
|
|
||||||
/// User who sent the message
|
|
||||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
|
||||||
final User? user;
|
|
||||||
|
|
||||||
/// If true the message is pinned
|
|
||||||
final bool pinned;
|
|
||||||
|
|
||||||
/// Reserved field indicating when the message was pinned
|
|
||||||
@JsonKey(toJson: Serializer.readOnly)
|
|
||||||
final DateTime? pinnedAt;
|
|
||||||
|
|
||||||
/// Reserved field indicating when the message will expire
|
|
||||||
///
|
|
||||||
/// if `null` message has no expiry
|
|
||||||
final DateTime? pinExpires;
|
|
||||||
|
|
||||||
/// Reserved field indicating who pinned the message
|
|
||||||
@JsonKey(toJson: Serializer.readOnly)
|
|
||||||
final User? pinnedBy;
|
|
||||||
|
|
||||||
/// Message custom extraData
|
|
||||||
@JsonKey(includeIfNull: false)
|
|
||||||
final Map<String, Object?> extraData;
|
|
||||||
|
|
||||||
/// True if the message is a system info
|
|
||||||
bool get isSystem => type == 'system';
|
|
||||||
|
|
||||||
/// True if the message has been deleted
|
|
||||||
bool get isDeleted => type == 'deleted';
|
|
||||||
|
|
||||||
/// True if the message is ephemeral
|
|
||||||
bool get isEphemeral => type == 'ephemeral';
|
|
||||||
|
|
||||||
/// Reserved field indicating when the message was deleted.
|
/// Reserved field indicating when the message was deleted.
|
||||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||||
final DateTime? deletedAt;
|
final DateTime? deletedAt;
|
||||||
|
|
||||||
|
/// Reserved field indicating when the message was created.
|
||||||
|
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||||
|
DateTime get createdAt => _createdAt ?? DateTime.now();
|
||||||
|
|
||||||
|
final DateTime? _updatedAt;
|
||||||
|
|
||||||
|
/// Reserved field indicating when the message was updated last time.
|
||||||
|
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||||
|
DateTime get updatedAt => _updatedAt ?? DateTime.now();
|
||||||
|
|
||||||
|
/// User who sent the message.
|
||||||
|
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||||
|
final User? user;
|
||||||
|
|
||||||
|
/// If true the message is pinned.
|
||||||
|
final bool pinned;
|
||||||
|
|
||||||
|
/// Reserved field indicating when the message was pinned.
|
||||||
|
@JsonKey(toJson: Serializer.readOnly)
|
||||||
|
final DateTime? pinnedAt;
|
||||||
|
|
||||||
|
/// Reserved field indicating when the message will expire.
|
||||||
|
///
|
||||||
|
/// If `null` message has no expiry.
|
||||||
|
final DateTime? pinExpires;
|
||||||
|
|
||||||
|
/// Reserved field indicating who pinned the message.
|
||||||
|
@JsonKey(toJson: Serializer.readOnly)
|
||||||
|
final User? pinnedBy;
|
||||||
|
|
||||||
|
/// Message custom extraData.
|
||||||
|
@JsonKey(includeIfNull: false)
|
||||||
|
final Map<String, Object?> extraData;
|
||||||
|
|
||||||
|
/// True if the message is a system info.
|
||||||
|
bool get isSystem => type == 'system';
|
||||||
|
|
||||||
|
/// True if the message has been deleted.
|
||||||
|
bool get isDeleted => type == 'deleted';
|
||||||
|
|
||||||
|
/// True if the message is ephemeral.
|
||||||
|
bool get isEphemeral => type == 'ephemeral';
|
||||||
|
|
||||||
/// A Map of translations.
|
/// A Map of translations.
|
||||||
@JsonKey(includeIfNull: false)
|
@JsonKey(includeIfNull: false)
|
||||||
final Map<String, String>? i18n;
|
final Map<String, String>? i18n;
|
||||||
|
|
||||||
/// Known top level fields.
|
/// Known top level fields.
|
||||||
|
///
|
||||||
/// Useful for [Serializer] methods.
|
/// Useful for [Serializer] methods.
|
||||||
static const topLevelFields = [
|
static const topLevelFields = [
|
||||||
'id',
|
'id',
|
||||||
@@ -244,7 +254,7 @@ class Message extends Equatable {
|
|||||||
'i18n',
|
'i18n',
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Serialize to json
|
/// Serialize to json.
|
||||||
Map<String, dynamic> toJson() => Serializer.moveFromExtraDataToRoot(
|
Map<String, dynamic> toJson() => Serializer.moveFromExtraDataToRoot(
|
||||||
_$MessageToJson(this),
|
_$MessageToJson(this),
|
||||||
);
|
);
|
||||||
@@ -256,6 +266,8 @@ class Message extends Equatable {
|
|||||||
String? type,
|
String? type,
|
||||||
List<Attachment>? attachments,
|
List<Attachment>? attachments,
|
||||||
List<User>? mentionedUsers,
|
List<User>? mentionedUsers,
|
||||||
|
bool? silent,
|
||||||
|
bool? shadowed,
|
||||||
Map<String, int>? reactionCounts,
|
Map<String, int>? reactionCounts,
|
||||||
Map<String, int>? reactionScores,
|
Map<String, int>? reactionScores,
|
||||||
List<Reaction>? latestReactions,
|
List<Reaction>? latestReactions,
|
||||||
@@ -266,8 +278,6 @@ class Message extends Equatable {
|
|||||||
int? replyCount,
|
int? replyCount,
|
||||||
List<User>? threadParticipants,
|
List<User>? threadParticipants,
|
||||||
bool? showInChannel,
|
bool? showInChannel,
|
||||||
bool? shadowed,
|
|
||||||
bool? silent,
|
|
||||||
String? command,
|
String? command,
|
||||||
DateTime? createdAt,
|
DateTime? createdAt,
|
||||||
DateTime? updatedAt,
|
DateTime? updatedAt,
|
||||||
@@ -295,30 +305,30 @@ class Message extends Equatable {
|
|||||||
type: type ?? this.type,
|
type: type ?? this.type,
|
||||||
attachments: attachments ?? this.attachments,
|
attachments: attachments ?? this.attachments,
|
||||||
mentionedUsers: mentionedUsers ?? this.mentionedUsers,
|
mentionedUsers: mentionedUsers ?? this.mentionedUsers,
|
||||||
|
silent: silent ?? this.silent,
|
||||||
|
shadowed: shadowed ?? this.shadowed,
|
||||||
reactionCounts: reactionCounts ?? this.reactionCounts,
|
reactionCounts: reactionCounts ?? this.reactionCounts,
|
||||||
reactionScores: reactionScores ?? this.reactionScores,
|
reactionScores: reactionScores ?? this.reactionScores,
|
||||||
latestReactions: latestReactions ?? this.latestReactions,
|
latestReactions: latestReactions ?? this.latestReactions,
|
||||||
ownReactions: ownReactions ?? this.ownReactions,
|
ownReactions: ownReactions ?? this.ownReactions,
|
||||||
parentId: parentId ?? this.parentId,
|
parentId: parentId ?? this.parentId,
|
||||||
quotedMessage: quotedMessage ?? this.quotedMessage,
|
quotedMessage: quotedMessage ?? this.quotedMessage,
|
||||||
quotedMessageId: quotedMessageId ?? this.quotedMessageId,
|
quotedMessageId: quotedMessageId ?? _quotedMessageId,
|
||||||
replyCount: replyCount ?? this.replyCount,
|
replyCount: replyCount ?? this.replyCount,
|
||||||
threadParticipants: threadParticipants ?? this.threadParticipants,
|
threadParticipants: threadParticipants ?? this.threadParticipants,
|
||||||
showInChannel: showInChannel ?? this.showInChannel,
|
showInChannel: showInChannel ?? this.showInChannel,
|
||||||
command: command ?? this.command,
|
command: command ?? this.command,
|
||||||
createdAt: createdAt ?? this.createdAt,
|
createdAt: createdAt ?? _createdAt,
|
||||||
silent: silent ?? this.silent,
|
updatedAt: updatedAt ?? _updatedAt,
|
||||||
extraData: extraData ?? this.extraData,
|
|
||||||
user: user ?? this.user,
|
|
||||||
shadowed: shadowed ?? this.shadowed,
|
|
||||||
updatedAt: updatedAt ?? this.updatedAt,
|
|
||||||
deletedAt: deletedAt ?? this.deletedAt,
|
deletedAt: deletedAt ?? this.deletedAt,
|
||||||
status: status ?? this.status,
|
user: user ?? this.user,
|
||||||
pinned: pinned ?? this.pinned,
|
pinned: pinned ?? this.pinned,
|
||||||
pinnedAt: pinnedAt ?? this.pinnedAt,
|
pinnedAt: pinnedAt ?? this.pinnedAt,
|
||||||
pinnedBy: pinnedBy ?? this.pinnedBy,
|
|
||||||
pinExpires:
|
pinExpires:
|
||||||
pinExpires == _pinExpires ? this.pinExpires : pinExpires as DateTime?,
|
pinExpires == _pinExpires ? this.pinExpires : pinExpires as DateTime?,
|
||||||
|
pinnedBy: pinnedBy ?? this.pinnedBy,
|
||||||
|
extraData: extraData ?? this.extraData,
|
||||||
|
status: status ?? this.status,
|
||||||
i18n: i18n ?? this.i18n,
|
i18n: i18n ?? this.i18n,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -331,6 +341,8 @@ class Message extends Equatable {
|
|||||||
type: other.type,
|
type: other.type,
|
||||||
attachments: other.attachments,
|
attachments: other.attachments,
|
||||||
mentionedUsers: other.mentionedUsers,
|
mentionedUsers: other.mentionedUsers,
|
||||||
|
silent: other.silent,
|
||||||
|
shadowed: other.shadowed,
|
||||||
reactionCounts: other.reactionCounts,
|
reactionCounts: other.reactionCounts,
|
||||||
reactionScores: other.reactionScores,
|
reactionScores: other.reactionScores,
|
||||||
latestReactions: other.latestReactions,
|
latestReactions: other.latestReactions,
|
||||||
@@ -343,17 +355,15 @@ class Message extends Equatable {
|
|||||||
showInChannel: other.showInChannel,
|
showInChannel: other.showInChannel,
|
||||||
command: other.command,
|
command: other.command,
|
||||||
createdAt: other.createdAt,
|
createdAt: other.createdAt,
|
||||||
silent: other.silent,
|
|
||||||
extraData: other.extraData,
|
|
||||||
user: other.user,
|
|
||||||
shadowed: other.shadowed,
|
|
||||||
updatedAt: other.updatedAt,
|
updatedAt: other.updatedAt,
|
||||||
deletedAt: other.deletedAt,
|
deletedAt: other.deletedAt,
|
||||||
status: other.status,
|
user: other.user,
|
||||||
pinned: other.pinned,
|
pinned: other.pinned,
|
||||||
pinnedAt: other.pinnedAt,
|
pinnedAt: other.pinnedAt,
|
||||||
pinExpires: other.pinExpires,
|
pinExpires: other.pinExpires,
|
||||||
pinnedBy: other.pinnedBy,
|
pinnedBy: other.pinnedBy,
|
||||||
|
extraData: other.extraData,
|
||||||
|
status: other.status,
|
||||||
i18n: other.i18n,
|
i18n: other.i18n,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
import 'package:stream_chat/src/core/models/user.dart';
|
import 'package:stream_chat/src/core/models/user.dart';
|
||||||
|
|
||||||
@@ -5,9 +6,9 @@ part 'read.g.dart';
|
|||||||
|
|
||||||
/// The class that defines a read event
|
/// The class that defines a read event
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class Read {
|
class Read extends Equatable {
|
||||||
/// Constructor used for json serialization
|
/// Constructor used for json serialization
|
||||||
Read({
|
const Read({
|
||||||
required this.lastRead,
|
required this.lastRead,
|
||||||
required this.user,
|
required this.user,
|
||||||
this.unreadMessages = 0,
|
this.unreadMessages = 0,
|
||||||
@@ -39,4 +40,11 @@ class Read {
|
|||||||
user: user ?? this.user,
|
user: user ?? this.user,
|
||||||
unreadMessages: unreadMessages ?? this.unreadMessages,
|
unreadMessages: unreadMessages ?? this.unreadMessages,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [
|
||||||
|
lastRead,
|
||||||
|
user,
|
||||||
|
unreadMessages,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -179,5 +179,14 @@ class User extends Equatable {
|
|||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [id, role];
|
List<Object?> get props => [
|
||||||
|
id,
|
||||||
|
role,
|
||||||
|
lastActive,
|
||||||
|
online,
|
||||||
|
extraData,
|
||||||
|
banned,
|
||||||
|
teams,
|
||||||
|
language,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,6 @@ abstract class ChatPersistenceClient {
|
|||||||
members: data[0] as List<Member>,
|
members: data[0] as List<Member>,
|
||||||
// ignore: cast_nullable_to_non_nullable
|
// ignore: cast_nullable_to_non_nullable
|
||||||
read: data[1] as List<Read>,
|
read: data[1] as List<Read>,
|
||||||
// ignore: cast_nullable_to_non_nullable
|
|
||||||
channel: data[2] as ChannelModel?,
|
channel: data[2] as ChannelModel?,
|
||||||
// ignore: cast_nullable_to_non_nullable
|
// ignore: cast_nullable_to_non_nullable
|
||||||
messages: data[3] as List<Message>,
|
messages: data[3] as List<Message>,
|
||||||
|
|||||||
@@ -121,7 +121,8 @@ class WebSocket with TimerHelper {
|
|||||||
_logger?.info('Closing connection with $baseUrl');
|
_logger?.info('Closing connection with $baseUrl');
|
||||||
if (_webSocketChannel != null) {
|
if (_webSocketChannel != null) {
|
||||||
_unsubscribeFromWebSocketChannel();
|
_unsubscribeFromWebSocketChannel();
|
||||||
_webSocketChannel?.sink.close(status.goingAway);
|
_webSocketChannel?.sink
|
||||||
|
.close(_manuallyClosed ? status.normalClosure : status.goingAway);
|
||||||
_webSocketChannel = null;
|
_webSocketChannel = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -309,7 +310,10 @@ class WebSocket with TimerHelper {
|
|||||||
Event? event;
|
Event? event;
|
||||||
try {
|
try {
|
||||||
event = Event.fromJson(jsonData);
|
event = Event.fromJson(jsonData);
|
||||||
} catch (_) {}
|
} catch (e, stk) {
|
||||||
|
_logger?.warning('Error parsing an event: $e');
|
||||||
|
_logger?.warning('Stack trace: $stk');
|
||||||
|
}
|
||||||
|
|
||||||
if (event == null) return;
|
if (event == null) return;
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export 'package:dio/src/options.dart';
|
|||||||
export 'package:dio/src/options.dart' show ProgressCallback;
|
export 'package:dio/src/options.dart' show ProgressCallback;
|
||||||
export 'package:logging/logging.dart' show Logger, Level;
|
export 'package:logging/logging.dart' show Logger, Level;
|
||||||
export 'package:rate_limiter/rate_limiter.dart';
|
export 'package:rate_limiter/rate_limiter.dart';
|
||||||
|
export 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
export './src/core/api/attachment_file_uploader.dart'
|
export './src/core/api/attachment_file_uploader.dart'
|
||||||
show AttachmentFileUploader;
|
show AttachmentFileUploader;
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ import 'package:stream_chat/src/client/client.dart';
|
|||||||
/// Current package version
|
/// Current package version
|
||||||
/// Used in [StreamChatClient] to build the `x-stream-client` header
|
/// Used in [StreamChatClient] to build the `x-stream-client` header
|
||||||
// ignore: constant_identifier_names
|
// ignore: constant_identifier_names
|
||||||
const PACKAGE_VERSION = '3.2.0';
|
const PACKAGE_VERSION = '3.3.1';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
name: stream_chat
|
name: stream_chat
|
||||||
homepage: https://getstream.io/
|
homepage: https://getstream.io/
|
||||||
description: The official Dart client for Stream Chat, a service for building chat applications.
|
description: The official Dart client for Stream Chat, a service for building chat applications.
|
||||||
version: 3.2.0
|
version: 3.3.1
|
||||||
repository: https://github.com/GetStream/stream-chat-flutter
|
repository: https://github.com/GetStream/stream-chat-flutter
|
||||||
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
||||||
|
|
||||||
@@ -13,10 +13,10 @@ dependencies:
|
|||||||
collection: ^1.15.0
|
collection: ^1.15.0
|
||||||
dio: ^4.0.0
|
dio: ^4.0.0
|
||||||
equatable: ^2.0.0
|
equatable: ^2.0.0
|
||||||
freezed_annotation: ^0.15.0
|
freezed_annotation: ^1.0.0
|
||||||
http_parser: ^4.0.0
|
http_parser: ^4.0.0
|
||||||
jose: ^0.3.2
|
jose: ^0.3.2
|
||||||
json_annotation: ^4.0.1
|
json_annotation: ^4.3.0
|
||||||
logging: ^1.0.1
|
logging: ^1.0.1
|
||||||
meta: ^1.3.0
|
meta: ^1.3.0
|
||||||
mime: ^1.0.0
|
mime: ^1.0.0
|
||||||
@@ -28,7 +28,7 @@ dependencies:
|
|||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
build_runner: ^2.0.1
|
build_runner: ^2.0.1
|
||||||
dart_code_metrics: ^4.4.0
|
dart_code_metrics: ^4.4.0
|
||||||
freezed: ^0.15.0+1
|
freezed: ^1.0.0
|
||||||
json_serializable: ^6.0.1
|
json_serializable: ^6.0.1
|
||||||
mocktail: ^0.2.0
|
mocktail: ^0.2.0
|
||||||
test: ^1.17.12
|
test: ^1.17.12
|
||||||
@@ -244,9 +244,13 @@ void main() {
|
|||||||
|
|
||||||
group('`.sendMessage`', () {
|
group('`.sendMessage`', () {
|
||||||
test('should work fine', () async {
|
test('should work fine', () async {
|
||||||
final message = Message(id: 'test-message-id');
|
final message = Message(
|
||||||
|
id: 'test-message-id',
|
||||||
|
user: client.state.currentUser,
|
||||||
|
);
|
||||||
|
|
||||||
final sendMessageResponse = SendMessageResponse()..message = message;
|
final sendMessageResponse = SendMessageResponse()
|
||||||
|
..message = message.copyWith(status: MessageSendingStatus.sent);
|
||||||
|
|
||||||
when(() => client.sendMessage(
|
when(() => client.sendMessage(
|
||||||
any(that: isSameMessageAs(message)),
|
any(that: isSameMessageAs(message)),
|
||||||
@@ -329,6 +333,7 @@ void main() {
|
|||||||
.map((it) =>
|
.map((it) =>
|
||||||
it.copyWith(uploadState: const UploadState.success()))
|
it.copyWith(uploadState: const UploadState.success()))
|
||||||
.toList(growable: false),
|
.toList(growable: false),
|
||||||
|
status: MessageSendingStatus.sent,
|
||||||
));
|
));
|
||||||
|
|
||||||
expectLater(
|
expectLater(
|
||||||
@@ -455,7 +460,10 @@ void main() {
|
|||||||
|
|
||||||
group('`.updateMessage`', () {
|
group('`.updateMessage`', () {
|
||||||
test('should work fine', () async {
|
test('should work fine', () async {
|
||||||
final message = Message(id: 'test-message-id');
|
final message = Message(
|
||||||
|
id: 'test-message-id',
|
||||||
|
status: MessageSendingStatus.sent,
|
||||||
|
);
|
||||||
|
|
||||||
final updateMessageResponse = UpdateMessageResponse()
|
final updateMessageResponse = UpdateMessageResponse()
|
||||||
..message = message;
|
..message = message;
|
||||||
@@ -530,6 +538,7 @@ void main() {
|
|||||||
any(that: isSameMessageAs(message)),
|
any(that: isSameMessageAs(message)),
|
||||||
)).thenAnswer((_) async => UpdateMessageResponse()
|
)).thenAnswer((_) async => UpdateMessageResponse()
|
||||||
..message = message.copyWith(
|
..message = message.copyWith(
|
||||||
|
status: MessageSendingStatus.sent,
|
||||||
attachments: attachments
|
attachments: attachments
|
||||||
.map((it) =>
|
.map((it) =>
|
||||||
it.copyWith(uploadState: const UploadState.success()))
|
it.copyWith(uploadState: const UploadState.success()))
|
||||||
@@ -678,7 +687,7 @@ void main() {
|
|||||||
[
|
[
|
||||||
isSameMessageAs(
|
isSameMessageAs(
|
||||||
updateMessageResponse.message.copyWith(
|
updateMessageResponse.message.copyWith(
|
||||||
status: MessageSendingStatus.sent,
|
status: MessageSendingStatus.sending,
|
||||||
),
|
),
|
||||||
matchText: true,
|
matchText: true,
|
||||||
matchSendingStatus: true,
|
matchSendingStatus: true,
|
||||||
@@ -707,7 +716,10 @@ void main() {
|
|||||||
group('`.deleteMessage`', () {
|
group('`.deleteMessage`', () {
|
||||||
test('should work fine', () async {
|
test('should work fine', () async {
|
||||||
const messageId = 'test-message-id';
|
const messageId = 'test-message-id';
|
||||||
final message = Message(id: messageId);
|
final message = Message(
|
||||||
|
id: messageId,
|
||||||
|
status: MessageSendingStatus.sent,
|
||||||
|
);
|
||||||
|
|
||||||
when(() => client.deleteMessage(messageId))
|
when(() => client.deleteMessage(messageId))
|
||||||
.thenAnswer((_) async => EmptyResponse());
|
.thenAnswer((_) async => EmptyResponse());
|
||||||
@@ -1077,7 +1089,10 @@ void main() {
|
|||||||
group('`.sendReaction`', () {
|
group('`.sendReaction`', () {
|
||||||
test('should work fine', () async {
|
test('should work fine', () async {
|
||||||
const type = 'test-reaction-type';
|
const type = 'test-reaction-type';
|
||||||
final message = Message(id: 'test-message-id');
|
final message = Message(
|
||||||
|
id: 'test-message-id',
|
||||||
|
status: MessageSendingStatus.sent,
|
||||||
|
);
|
||||||
|
|
||||||
final reaction = Reaction(type: type, messageId: message.id);
|
final reaction = Reaction(type: type, messageId: message.id);
|
||||||
|
|
||||||
@@ -1120,7 +1135,10 @@ void main() {
|
|||||||
'should restore previous message if `client.sendReaction` throws',
|
'should restore previous message if `client.sendReaction` throws',
|
||||||
() async {
|
() async {
|
||||||
const type = 'test-reaction-type';
|
const type = 'test-reaction-type';
|
||||||
final message = Message(id: 'test-message-id');
|
final message = Message(
|
||||||
|
id: 'test-message-id',
|
||||||
|
status: MessageSendingStatus.sent,
|
||||||
|
);
|
||||||
|
|
||||||
final reaction = Reaction(type: type, messageId: message.id);
|
final reaction = Reaction(type: type, messageId: message.id);
|
||||||
|
|
||||||
@@ -1181,6 +1199,7 @@ void main() {
|
|||||||
latestReactions: [prevReaction],
|
latestReactions: [prevReaction],
|
||||||
reactionScores: const {prevType: 1},
|
reactionScores: const {prevType: 1},
|
||||||
reactionCounts: const {prevType: 1},
|
reactionCounts: const {prevType: 1},
|
||||||
|
status: MessageSendingStatus.sent,
|
||||||
);
|
);
|
||||||
|
|
||||||
const type = 'test-reaction-type-2';
|
const type = 'test-reaction-type-2';
|
||||||
@@ -1212,7 +1231,7 @@ void main() {
|
|||||||
emitsInOrder([
|
emitsInOrder([
|
||||||
[
|
[
|
||||||
isSameMessageAs(
|
isSameMessageAs(
|
||||||
newMessage.copyWith(status: MessageSendingStatus.sent),
|
newMessage,
|
||||||
matchReactions: true,
|
matchReactions: true,
|
||||||
matchSendingStatus: true,
|
matchSendingStatus: true,
|
||||||
),
|
),
|
||||||
@@ -1255,6 +1274,7 @@ void main() {
|
|||||||
latestReactions: [reaction],
|
latestReactions: [reaction],
|
||||||
reactionScores: const {type: 1},
|
reactionScores: const {type: 1},
|
||||||
reactionCounts: const {type: 1},
|
reactionCounts: const {type: 1},
|
||||||
|
status: MessageSendingStatus.sent,
|
||||||
);
|
);
|
||||||
|
|
||||||
when(() => client.deleteReaction(messageId, type))
|
when(() => client.deleteReaction(messageId, type))
|
||||||
@@ -1302,6 +1322,7 @@ void main() {
|
|||||||
latestReactions: [reaction],
|
latestReactions: [reaction],
|
||||||
reactionScores: const {type: 1},
|
reactionScores: const {type: 1},
|
||||||
reactionCounts: const {type: 1},
|
reactionCounts: const {type: 1},
|
||||||
|
status: MessageSendingStatus.sent,
|
||||||
);
|
);
|
||||||
|
|
||||||
when(() => client.deleteReaction(messageId, type))
|
when(() => client.deleteReaction(messageId, type))
|
||||||
|
|||||||
@@ -1,20 +1,7 @@
|
|||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
import 'package:stream_chat/src/client/client.dart';
|
|
||||||
import 'package:stream_chat/src/core/api/device_api.dart';
|
import 'package:stream_chat/src/core/api/device_api.dart';
|
||||||
import 'package:stream_chat/src/core/api/requests.dart';
|
|
||||||
import 'package:stream_chat/src/core/api/responses.dart';
|
|
||||||
import 'package:stream_chat/src/core/error/error.dart';
|
|
||||||
import 'package:stream_chat/src/core/http/token.dart';
|
import 'package:stream_chat/src/core/http/token.dart';
|
||||||
import 'package:stream_chat/src/core/models/channel_model.dart';
|
|
||||||
import 'package:stream_chat/src/core/models/event.dart';
|
|
||||||
import 'package:stream_chat/src/core/models/filter.dart';
|
|
||||||
import 'package:stream_chat/src/core/models/message.dart';
|
|
||||||
import 'package:stream_chat/src/core/models/own_user.dart';
|
|
||||||
import 'package:stream_chat/src/core/models/user.dart';
|
|
||||||
import 'package:stream_chat/src/event_type.dart';
|
|
||||||
import 'package:stream_chat/src/ws/connection_status.dart';
|
|
||||||
import 'package:stream_chat/stream_chat.dart';
|
import 'package:stream_chat/stream_chat.dart';
|
||||||
import 'package:test/scaffolding.dart';
|
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
import '../fakes.dart';
|
import '../fakes.dart';
|
||||||
@@ -2314,6 +2301,33 @@ void main() {
|
|||||||
verifyNoMoreInteractions(api.message);
|
verifyNoMoreInteractions(api.message);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('`.enrichUrl`', () async {
|
||||||
|
const url =
|
||||||
|
'https://www.techyourchance.com/finite-state-machine-with-unit-tests-real-world-example';
|
||||||
|
|
||||||
|
when(() => api.general.enrichUrl(url)).thenAnswer(
|
||||||
|
(_) async => OGAttachmentResponse()
|
||||||
|
..type = 'image'
|
||||||
|
..ogScrapeUrl = url
|
||||||
|
..authorName = 'TechYourChance'
|
||||||
|
..title = 'Finite State Machine with Unit Tests: Real World Example',
|
||||||
|
);
|
||||||
|
|
||||||
|
final res = await client.enrichUrl(url);
|
||||||
|
|
||||||
|
expect(res, isNotNull);
|
||||||
|
expect(res.type, 'image');
|
||||||
|
expect(res.ogScrapeUrl, url);
|
||||||
|
expect(res.authorName, 'TechYourChance');
|
||||||
|
expect(
|
||||||
|
res.title,
|
||||||
|
'Finite State Machine with Unit Tests: Real World Example',
|
||||||
|
);
|
||||||
|
|
||||||
|
verify(() => api.general.enrichUrl(url)).called(1);
|
||||||
|
verifyNoMoreInteractions(api.general);
|
||||||
|
});
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'''setting the `currentUser` should also compute and update the unreadCounts''',
|
'''setting the `currentUser` should also compute and update the unreadCounts''',
|
||||||
() {
|
() {
|
||||||
|
|||||||
@@ -3,10 +3,6 @@ import 'dart:convert';
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
import 'package:stream_chat/src/core/api/general_api.dart';
|
import 'package:stream_chat/src/core/api/general_api.dart';
|
||||||
import 'package:stream_chat/src/core/api/requests.dart';
|
|
||||||
import 'package:stream_chat/src/core/models/channel_model.dart';
|
|
||||||
import 'package:stream_chat/src/core/models/event.dart';
|
|
||||||
import 'package:stream_chat/src/core/models/filter.dart';
|
|
||||||
import 'package:stream_chat/stream_chat.dart';
|
import 'package:stream_chat/stream_chat.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
@@ -281,4 +277,39 @@ void main() {
|
|||||||
verifyNoMoreInteractions(client);
|
verifyNoMoreInteractions(client);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('enrichUrl', () async {
|
||||||
|
const path = '/og';
|
||||||
|
const url =
|
||||||
|
'https://www.techyourchance.com/finite-state-machine-with-unit-tests-real-world-example';
|
||||||
|
|
||||||
|
when(() => client.get(
|
||||||
|
path,
|
||||||
|
queryParameters: {'url': url},
|
||||||
|
)).thenAnswer((_) async => successResponse(path, data: {
|
||||||
|
'type': 'image',
|
||||||
|
'og_scrape_url': url,
|
||||||
|
'author_name': 'TechYourChance',
|
||||||
|
'title': 'Finite State Machine with Unit Tests: Real World Example',
|
||||||
|
}));
|
||||||
|
|
||||||
|
final res = await generalApi.enrichUrl(url);
|
||||||
|
|
||||||
|
expect(res, isNotNull);
|
||||||
|
expect(res.type, 'image');
|
||||||
|
expect(res.ogScrapeUrl, url);
|
||||||
|
expect(res.authorName, 'TechYourChance');
|
||||||
|
expect(
|
||||||
|
res.title,
|
||||||
|
'Finite State Machine with Unit Tests: Real World Example',
|
||||||
|
);
|
||||||
|
|
||||||
|
verify(
|
||||||
|
() => client.get(
|
||||||
|
path,
|
||||||
|
queryParameters: {'url': url},
|
||||||
|
),
|
||||||
|
).called(1);
|
||||||
|
verifyNoMoreInteractions(client);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:stream_chat/src/core/http/interceptor/additional_headers_interceptor.dart';
|
||||||
|
import 'package:stream_chat/stream_chat.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late AdditionalHeadersInterceptor additionalHeadersInterceptor;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
additionalHeadersInterceptor = AdditionalHeadersInterceptor();
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'`onRequest` should add additional headers in the request',
|
||||||
|
() async {
|
||||||
|
final options = RequestOptions(path: 'test-path');
|
||||||
|
final handler = RequestInterceptorHandler();
|
||||||
|
|
||||||
|
StreamChatClient.additionalHeaders = {'test-header': 'test-value'};
|
||||||
|
additionalHeadersInterceptor.onRequest(options, handler);
|
||||||
|
|
||||||
|
final updatedOptions = (await handler.future).data as RequestOptions;
|
||||||
|
final updateHeaders = updatedOptions.headers;
|
||||||
|
|
||||||
|
expect(updateHeaders.containsKey('test-header'), isTrue);
|
||||||
|
expect(updateHeaders['test-header'], 'test-value');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import 'package:mocktail/mocktail.dart';
|
|||||||
import 'package:stream_chat/src/core/api/responses.dart';
|
import 'package:stream_chat/src/core/api/responses.dart';
|
||||||
import 'package:stream_chat/src/core/error/error.dart';
|
import 'package:stream_chat/src/core/error/error.dart';
|
||||||
import 'package:stream_chat/src/core/http/connection_id_manager.dart';
|
import 'package:stream_chat/src/core/http/connection_id_manager.dart';
|
||||||
|
import 'package:stream_chat/src/core/http/interceptor/additional_headers_interceptor.dart';
|
||||||
import 'package:stream_chat/src/core/http/interceptor/auth_interceptor.dart';
|
import 'package:stream_chat/src/core/http/interceptor/auth_interceptor.dart';
|
||||||
import 'package:stream_chat/src/core/http/interceptor/connection_id_interceptor.dart';
|
import 'package:stream_chat/src/core/http/interceptor/connection_id_interceptor.dart';
|
||||||
import 'package:stream_chat/src/core/http/interceptor/logging_interceptor.dart';
|
import 'package:stream_chat/src/core/http/interceptor/logging_interceptor.dart';
|
||||||
@@ -48,12 +49,23 @@ void main() {
|
|||||||
return dioError;
|
return dioError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test('UserAgentInterceptor should be added', () {
|
||||||
|
const apiKey = 'api-key';
|
||||||
|
final client = StreamHttpClient(apiKey);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
client.httpClient.interceptors
|
||||||
|
.whereType<AdditionalHeadersInterceptor>()
|
||||||
|
.length,
|
||||||
|
1);
|
||||||
|
});
|
||||||
|
|
||||||
test('AuthInterceptor should be added if tokenManager is provided', () {
|
test('AuthInterceptor should be added if tokenManager is provided', () {
|
||||||
const apiKey = 'api-key';
|
const apiKey = 'api-key';
|
||||||
final client = StreamHttpClient(apiKey, tokenManager: TokenManager());
|
final client = StreamHttpClient(apiKey, tokenManager: TokenManager());
|
||||||
|
|
||||||
expect(client.httpClient.interceptors.length, 1);
|
expect(
|
||||||
expect(client.httpClient.interceptors.first, isA<AuthInterceptor>());
|
client.httpClient.interceptors.whereType<AuthInterceptor>().length, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test(
|
test(
|
||||||
@@ -65,10 +77,11 @@ void main() {
|
|||||||
connectionIdManager: ConnectionIdManager(),
|
connectionIdManager: ConnectionIdManager(),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(client.httpClient.interceptors.length, 1);
|
|
||||||
expect(
|
expect(
|
||||||
client.httpClient.interceptors.first,
|
client.httpClient.interceptors
|
||||||
isA<ConnectionIdInterceptor>(),
|
.whereType<ConnectionIdInterceptor>()
|
||||||
|
.length,
|
||||||
|
1,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -80,10 +93,9 @@ void main() {
|
|||||||
logger: Logger('test-logger'),
|
logger: Logger('test-logger'),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(client.httpClient.interceptors.length, 1);
|
|
||||||
expect(
|
expect(
|
||||||
client.httpClient.interceptors.first,
|
client.httpClient.interceptors.whereType<LoggingInterceptor>().length,
|
||||||
isA<LoggingInterceptor>(),
|
1,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -111,27 +123,6 @@ void main() {
|
|||||||
verify(() => logger.severe(any())).called(greaterThan(0));
|
verify(() => logger.severe(any())).called(greaterThan(0));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('`.lock` should lock the dio client', () async {
|
|
||||||
final client = StreamHttpClient('api-key');
|
|
||||||
expect(client.httpClient.interceptors.requestLock.locked, isFalse);
|
|
||||||
client.lock();
|
|
||||||
expect(client.httpClient.interceptors.requestLock.locked, isTrue);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('`.unlock` should unlock the dio client', () async {
|
|
||||||
final client = StreamHttpClient('api-key');
|
|
||||||
expect(client.httpClient.interceptors.requestLock.locked, isFalse);
|
|
||||||
client.lock();
|
|
||||||
expect(client.httpClient.interceptors.requestLock.locked, isTrue);
|
|
||||||
client.unlock();
|
|
||||||
expect(client.httpClient.interceptors.requestLock.locked, isFalse);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('`.clear` should clear and unlock the dio client', () async {
|
|
||||||
final client = StreamHttpClient('api-key')..clear();
|
|
||||||
expect(client.httpClient.interceptors.requestLock.locked, isFalse);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('`.close` should close the dio client', () async {
|
test('`.close` should close the dio client', () async {
|
||||||
final client = StreamHttpClient('api-key')..close(force: true);
|
final client = StreamHttpClient('api-key')..close(force: true);
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -70,13 +70,19 @@ void main() {
|
|||||||
expect(
|
expect(
|
||||||
newReaction.extraData, {'updated_at': '2020-01-28T22:17:31.108742Z'});
|
newReaction.extraData, {'updated_at': '2020-01-28T22:17:31.108742Z'});
|
||||||
|
|
||||||
|
final newUserCreateTime = DateTime.now();
|
||||||
|
|
||||||
newReaction = reaction.copyWith(
|
newReaction = reaction.copyWith(
|
||||||
type: 'lol',
|
type: 'lol',
|
||||||
createdAt: DateTime.parse('2021-01-28T22:17:31.108742Z'),
|
createdAt: DateTime.parse('2021-01-28T22:17:31.108742Z'),
|
||||||
extraData: {},
|
extraData: {},
|
||||||
messageId: 'test',
|
messageId: 'test',
|
||||||
score: 2,
|
score: 2,
|
||||||
user: User(id: 'test'),
|
user: User(
|
||||||
|
id: 'test',
|
||||||
|
createdAt: newUserCreateTime,
|
||||||
|
updatedAt: newUserCreateTime,
|
||||||
|
),
|
||||||
userId: 'test',
|
userId: 'test',
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -88,12 +94,21 @@ void main() {
|
|||||||
expect(newReaction.extraData, {});
|
expect(newReaction.extraData, {});
|
||||||
expect(newReaction.messageId, 'test');
|
expect(newReaction.messageId, 'test');
|
||||||
expect(newReaction.score, 2);
|
expect(newReaction.score, 2);
|
||||||
expect(newReaction.user, User(id: 'test'));
|
expect(
|
||||||
|
newReaction.user,
|
||||||
|
User(
|
||||||
|
id: 'test',
|
||||||
|
createdAt: newUserCreateTime,
|
||||||
|
updatedAt: newUserCreateTime,
|
||||||
|
),
|
||||||
|
);
|
||||||
expect(newReaction.userId, 'test');
|
expect(newReaction.userId, 'test');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('merge', () {
|
test('merge', () {
|
||||||
final reaction = Reaction.fromJson(jsonFixture('reaction.json'));
|
final reaction = Reaction.fromJson(jsonFixture('reaction.json'));
|
||||||
|
final newUserCreateTime = DateTime.now();
|
||||||
|
|
||||||
final newReaction = reaction.merge(
|
final newReaction = reaction.merge(
|
||||||
Reaction(
|
Reaction(
|
||||||
type: 'lol',
|
type: 'lol',
|
||||||
@@ -101,7 +116,11 @@ void main() {
|
|||||||
extraData: {},
|
extraData: {},
|
||||||
messageId: 'test',
|
messageId: 'test',
|
||||||
score: 2,
|
score: 2,
|
||||||
user: User(id: 'test'),
|
user: User(
|
||||||
|
id: 'test',
|
||||||
|
createdAt: newUserCreateTime,
|
||||||
|
updatedAt: newUserCreateTime,
|
||||||
|
),
|
||||||
userId: 'test',
|
userId: 'test',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -114,7 +133,14 @@ void main() {
|
|||||||
expect(newReaction.extraData, {});
|
expect(newReaction.extraData, {});
|
||||||
expect(newReaction.messageId, 'test');
|
expect(newReaction.messageId, 'test');
|
||||||
expect(newReaction.score, 2);
|
expect(newReaction.score, 2);
|
||||||
expect(newReaction.user, User(id: 'test'));
|
expect(
|
||||||
|
newReaction.user,
|
||||||
|
User(
|
||||||
|
id: 'test',
|
||||||
|
createdAt: newUserCreateTime,
|
||||||
|
updatedAt: newUserCreateTime,
|
||||||
|
),
|
||||||
|
);
|
||||||
expect(newReaction.userId, 'test');
|
expect(newReaction.userId, 'test');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,18 +6,40 @@
|
|||||||
|
|
||||||
- `MessageInput` now works with a `MessageInputController` instead of a `TextEditingController`
|
- `MessageInput` now works with a `MessageInputController` instead of a `TextEditingController`
|
||||||
|
|
||||||
|
## 3.3.2
|
||||||
|
|
||||||
|
- Updated `stream_chat_flutter_core` dependency to [`3.3.1`](https://pub.dev/packages/stream_chat_flutter_core/changelog).
|
||||||
|
|
||||||
|
## 3.3.1
|
||||||
|
|
||||||
|
✅ Added
|
||||||
|
|
||||||
|
- `MessageListView` now allows more better control over spacing after messages using `spacingWidgetBuilder`.
|
||||||
|
- `StreamChannel` can now fetch messages around a message ID with the `queryAroundMessage` call.
|
||||||
|
- Added `MessageListView.keyboardDismissBehavior` property.
|
||||||
|
|
||||||
|
🐞 Fixed
|
||||||
|
|
||||||
|
- [[#766]]`AttachmentActionsModal` now has customisation options for actions.
|
||||||
|
- Fixed `MessageWidget` null errors associated with `channel.memberCount`.
|
||||||
|
- Fixed adding attachments on web.
|
||||||
|
- [[#767]](https://github.com/GetStream/stream-chat-flutter/issues/767): Fix `MessageInput` focus behaviour when sending messages.
|
||||||
|
- Fixed user presence indicator not updating correctly.
|
||||||
|
- Do not use `withData: true` in `FilePicker` calls.
|
||||||
|
- Fixed read indicator not updating correctly in specific situations.
|
||||||
|
|
||||||
## 3.2.0
|
## 3.2.0
|
||||||
|
|
||||||
- Updated Dart SDK constraints to `>=2.14.0 <3.0.0`
|
- Updated Dart SDK constraints to `>=2.14.0 <3.0.0`.
|
||||||
- Updated `stream_chat_flutter_core` dependency to [`3.2.0`](https://pub.dev/packages/stream_chat_flutter_core/changelog).
|
- Updated `stream_chat_flutter_core` dependency to [`3.2.0`](https://pub.dev/packages/stream_chat_flutter_core/changelog).
|
||||||
|
|
||||||
🐞 Fixed
|
🐞 Fixed
|
||||||
|
|
||||||
- Fixed message highlight animation alignment in `MessageListView`
|
- Fixed message highlight animation alignment in `MessageListView`.
|
||||||
- [[#491]](https://github.com/GetStream/stream-chat-flutter/issues/491): Fix `MediaListView` showing media in wrong order.
|
- [[#491]](https://github.com/GetStream/stream-chat-flutter/issues/491): Fix `MediaListView` showing media in wrong order.
|
||||||
- Fixed `MessageListView` initialIndex not working in some cases.
|
- Fixed `MessageListView` initialIndex not working in some cases.
|
||||||
- Improved `MessageListView` rendering in case of reordering.
|
- Improved `MessageListView` rendering in case of reordering.
|
||||||
- Fix image thumbnail generation when using Stream CDN
|
- Fix image thumbnail generation when using Stream CDN.
|
||||||
|
|
||||||
✅ Added
|
✅ Added
|
||||||
|
|
||||||
|
|||||||
@@ -126,7 +126,9 @@ class ThreadPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
MessageInput(
|
MessageInput(
|
||||||
parentMessage: parent,
|
messageInputController: MessageInputController(
|
||||||
|
message: Message(parentId: parent!.id),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -68,7 +68,8 @@ class MyApp extends StatelessWidget {
|
|||||||
backgroundImage: DecorationImage(
|
backgroundImage: DecorationImage(
|
||||||
image: AssetImage('assets/background_doodle.png'),
|
image: AssetImage('assets/background_doodle.png'),
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
)),
|
),
|
||||||
|
),
|
||||||
otherMessageTheme: MessageThemeData(
|
otherMessageTheme: MessageThemeData(
|
||||||
messageBackgroundColor: colorTheme.textHighEmphasis,
|
messageBackgroundColor: colorTheme.textHighEmphasis,
|
||||||
messageTextStyle: TextStyle(
|
messageTextStyle: TextStyle(
|
||||||
@@ -165,7 +166,9 @@ class ThreadPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
MessageInput(
|
MessageInput(
|
||||||
parentMessage: parent,
|
messageInputController: MessageInputController(
|
||||||
|
message: Message(parentId: parent!.id),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -27,9 +27,12 @@ dependencies:
|
|||||||
cupertino_icons: ^1.0.3
|
cupertino_icons: ^1.0.3
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
stream_chat_flutter: ^2.2.1
|
stream_chat_flutter:
|
||||||
stream_chat_localizations: ^1.1.0
|
path: ../
|
||||||
stream_chat_persistence: ^2.2.0
|
stream_chat_localizations:
|
||||||
|
path: ../../stream_chat_localizations
|
||||||
|
stream_chat_persistence:
|
||||||
|
path: ../../stream_chat_persistence
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ class PositionedList extends StatefulWidget {
|
|||||||
this.addSemanticIndexes = true,
|
this.addSemanticIndexes = true,
|
||||||
this.addRepaintBoundaries = true,
|
this.addRepaintBoundaries = true,
|
||||||
this.addAutomaticKeepAlives = true,
|
this.addAutomaticKeepAlives = true,
|
||||||
|
this.keyboardDismissBehavior,
|
||||||
}) : assert((positionedIndex == 0) || (positionedIndex < itemCount),
|
}) : assert((positionedIndex == 0) || (positionedIndex < itemCount),
|
||||||
'positionedIndex cannot be 0 and must be smaller than itemCount'),
|
'positionedIndex cannot be 0 and must be smaller than itemCount'),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
@@ -134,6 +135,10 @@ class PositionedList extends StatefulWidget {
|
|||||||
/// See [SliverChildBuilderDelegate.addAutomaticKeepAlives].
|
/// See [SliverChildBuilderDelegate.addAutomaticKeepAlives].
|
||||||
final bool addAutomaticKeepAlives;
|
final bool addAutomaticKeepAlives;
|
||||||
|
|
||||||
|
/// [ScrollViewKeyboardDismissBehavior] the defines how this [PositionedList] will
|
||||||
|
/// dismiss the keyboard automatically.
|
||||||
|
final ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<StatefulWidget> createState() => _PositionedListState();
|
State<StatefulWidget> createState() => _PositionedListState();
|
||||||
}
|
}
|
||||||
@@ -173,6 +178,7 @@ class _PositionedListState extends State<PositionedList> {
|
|||||||
anchor: widget.alignment,
|
anchor: widget.alignment,
|
||||||
center: _centerKey,
|
center: _centerKey,
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
|
keyboardDismissBehavior: widget.keyboardDismissBehavior,
|
||||||
scrollDirection: widget.scrollDirection,
|
scrollDirection: widget.scrollDirection,
|
||||||
reverse: widget.reverse,
|
reverse: widget.reverse,
|
||||||
cacheExtent: widget.cacheExtent,
|
cacheExtent: widget.cacheExtent,
|
||||||
|
|||||||
@@ -28,9 +28,12 @@ class UnboundedCustomScrollView extends CustomScrollView {
|
|||||||
List<Widget> slivers = const <Widget>[],
|
List<Widget> slivers = const <Widget>[],
|
||||||
int? semanticChildCount,
|
int? semanticChildCount,
|
||||||
DragStartBehavior dragStartBehavior = DragStartBehavior.start,
|
DragStartBehavior dragStartBehavior = DragStartBehavior.start,
|
||||||
|
ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior,
|
||||||
}) : _anchor = anchor,
|
}) : _anchor = anchor,
|
||||||
super(
|
super(
|
||||||
key: key,
|
key: key,
|
||||||
|
keyboardDismissBehavior: keyboardDismissBehavior ??
|
||||||
|
ScrollViewKeyboardDismissBehavior.manual,
|
||||||
scrollDirection: scrollDirection,
|
scrollDirection: scrollDirection,
|
||||||
reverse: reverse,
|
reverse: reverse,
|
||||||
controller: controller,
|
controller: controller,
|
||||||
|
|||||||
+9
@@ -52,6 +52,7 @@ class ScrollablePositionedList extends StatefulWidget {
|
|||||||
this.addRepaintBoundaries = true,
|
this.addRepaintBoundaries = true,
|
||||||
this.minCacheExtent,
|
this.minCacheExtent,
|
||||||
this.findChildIndexCallback,
|
this.findChildIndexCallback,
|
||||||
|
this.keyboardDismissBehavior,
|
||||||
}) : itemPositionsNotifier = itemPositionsListener as ItemPositionsNotifier?,
|
}) : itemPositionsNotifier = itemPositionsListener as ItemPositionsNotifier?,
|
||||||
separatorBuilder = null,
|
separatorBuilder = null,
|
||||||
super(key: key);
|
super(key: key);
|
||||||
@@ -77,6 +78,7 @@ class ScrollablePositionedList extends StatefulWidget {
|
|||||||
this.addRepaintBoundaries = true,
|
this.addRepaintBoundaries = true,
|
||||||
this.minCacheExtent,
|
this.minCacheExtent,
|
||||||
this.findChildIndexCallback,
|
this.findChildIndexCallback,
|
||||||
|
this.keyboardDismissBehavior,
|
||||||
}) : assert(separatorBuilder != null, 'seperatorBuilder cannot be null'),
|
}) : assert(separatorBuilder != null, 'seperatorBuilder cannot be null'),
|
||||||
itemPositionsNotifier = itemPositionsListener as ItemPositionsNotifier?,
|
itemPositionsNotifier = itemPositionsListener as ItemPositionsNotifier?,
|
||||||
super(key: key);
|
super(key: key);
|
||||||
@@ -92,6 +94,10 @@ class ScrollablePositionedList extends StatefulWidget {
|
|||||||
/// index of the child element with that associated key, or null if not found.
|
/// index of the child element with that associated key, or null if not found.
|
||||||
final ChildIndexGetter? findChildIndexCallback;
|
final ChildIndexGetter? findChildIndexCallback;
|
||||||
|
|
||||||
|
/// [ScrollViewKeyboardDismissBehavior] the defines how this [PositionedList] will
|
||||||
|
/// dismiss the keyboard automatically.
|
||||||
|
final ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior;
|
||||||
|
|
||||||
/// Number of items the [itemBuilder] can produce.
|
/// Number of items the [itemBuilder] can produce.
|
||||||
final int itemCount;
|
final int itemCount;
|
||||||
|
|
||||||
@@ -344,6 +350,7 @@ class _ScrollablePositionedListState extends State<ScrollablePositionedList>
|
|||||||
child: NotificationListener<ScrollNotification>(
|
child: NotificationListener<ScrollNotification>(
|
||||||
onNotification: (_) => _isTransitioning,
|
onNotification: (_) => _isTransitioning,
|
||||||
child: PositionedList(
|
child: PositionedList(
|
||||||
|
keyboardDismissBehavior: widget.keyboardDismissBehavior,
|
||||||
itemBuilder: widget.itemBuilder,
|
itemBuilder: widget.itemBuilder,
|
||||||
separatorBuilder: widget.separatorBuilder,
|
separatorBuilder: widget.separatorBuilder,
|
||||||
itemCount: widget.itemCount,
|
itemCount: widget.itemCount,
|
||||||
@@ -374,6 +381,8 @@ class _ScrollablePositionedListState extends State<ScrollablePositionedList>
|
|||||||
child: NotificationListener<ScrollNotification>(
|
child: NotificationListener<ScrollNotification>(
|
||||||
onNotification: (_) => false,
|
onNotification: (_) => false,
|
||||||
child: PositionedList(
|
child: PositionedList(
|
||||||
|
keyboardDismissBehavior:
|
||||||
|
widget.keyboardDismissBehavior,
|
||||||
itemBuilder: widget.itemBuilder,
|
itemBuilder: widget.itemBuilder,
|
||||||
separatorBuilder: widget.separatorBuilder,
|
separatorBuilder: widget.separatorBuilder,
|
||||||
itemCount: widget.itemCount,
|
itemCount: widget.itemCount,
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ class AttachmentActionsModal extends StatelessWidget {
|
|||||||
this.onShowMessage,
|
this.onShowMessage,
|
||||||
this.imageDownloader,
|
this.imageDownloader,
|
||||||
this.fileDownloader,
|
this.fileDownloader,
|
||||||
|
this.showReply = true,
|
||||||
|
this.showShowInChat = true,
|
||||||
|
this.showSave = true,
|
||||||
|
this.showDelete = true,
|
||||||
|
this.customActions = const [],
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
/// The message containing the attachments
|
/// The message containing the attachments
|
||||||
@@ -41,6 +46,49 @@ class AttachmentActionsModal extends StatelessWidget {
|
|||||||
/// Callback to provide download files
|
/// Callback to provide download files
|
||||||
final AttachmentDownloader? fileDownloader;
|
final AttachmentDownloader? fileDownloader;
|
||||||
|
|
||||||
|
/// Show reply option
|
||||||
|
final bool showReply;
|
||||||
|
|
||||||
|
/// Show show in chat option
|
||||||
|
final bool showShowInChat;
|
||||||
|
|
||||||
|
/// Show save option
|
||||||
|
final bool showSave;
|
||||||
|
|
||||||
|
/// Show delete option
|
||||||
|
final bool showDelete;
|
||||||
|
|
||||||
|
/// List of custom actions
|
||||||
|
final List<AttachmentAction> customActions;
|
||||||
|
|
||||||
|
/// Creates a copy of [MessageWidget] with specified attributes overridden.
|
||||||
|
AttachmentActionsModal copyWith({
|
||||||
|
Key? key,
|
||||||
|
int? currentIndex,
|
||||||
|
Message? message,
|
||||||
|
VoidCallback? onShowMessage,
|
||||||
|
AttachmentDownloader? imageDownloader,
|
||||||
|
AttachmentDownloader? fileDownloader,
|
||||||
|
bool? showReply,
|
||||||
|
bool? showShowInChat,
|
||||||
|
bool? showSave,
|
||||||
|
bool? showDelete,
|
||||||
|
List<AttachmentAction>? customActions,
|
||||||
|
}) =>
|
||||||
|
AttachmentActionsModal(
|
||||||
|
key: key ?? this.key,
|
||||||
|
currentIndex: currentIndex ?? this.currentIndex,
|
||||||
|
message: message ?? this.message,
|
||||||
|
onShowMessage: onShowMessage ?? this.onShowMessage,
|
||||||
|
imageDownloader: imageDownloader ?? this.imageDownloader,
|
||||||
|
fileDownloader: fileDownloader ?? this.fileDownloader,
|
||||||
|
showReply: showReply ?? this.showReply,
|
||||||
|
showShowInChat: showShowInChat ?? this.showShowInChat,
|
||||||
|
showSave: showSave ?? this.showSave,
|
||||||
|
showDelete: showDelete ?? this.showDelete,
|
||||||
|
customActions: customActions ?? this.customActions,
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => GestureDetector(
|
Widget build(BuildContext context) => GestureDetector(
|
||||||
behavior: HitTestBehavior.translucent,
|
behavior: HitTestBehavior.translucent,
|
||||||
@@ -67,6 +115,7 @@ class AttachmentActionsModal extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
|
if (showReply)
|
||||||
_buildButton(
|
_buildButton(
|
||||||
context,
|
context,
|
||||||
context.translations.replyLabel,
|
context.translations.replyLabel,
|
||||||
@@ -78,6 +127,7 @@ class AttachmentActionsModal extends StatelessWidget {
|
|||||||
Navigator.pop(context, ReturnActionType.reply);
|
Navigator.pop(context, ReturnActionType.reply);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
if (showShowInChat)
|
||||||
_buildButton(
|
_buildButton(
|
||||||
context,
|
context,
|
||||||
context.translations.showInChatLabel,
|
context.translations.showInChatLabel,
|
||||||
@@ -87,6 +137,7 @@ class AttachmentActionsModal extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
onShowMessage,
|
onShowMessage,
|
||||||
),
|
),
|
||||||
|
if (showSave)
|
||||||
_buildButton(
|
_buildButton(
|
||||||
context,
|
context,
|
||||||
message.attachments[currentIndex].type == 'video'
|
message.attachments[currentIndex].type == 'video'
|
||||||
@@ -142,7 +193,8 @@ class AttachmentActionsModal extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (StreamChat.of(context).currentUser?.id ==
|
if (StreamChat.of(context).currentUser?.id ==
|
||||||
message.user?.id)
|
message.user?.id &&
|
||||||
|
showDelete)
|
||||||
_buildButton(
|
_buildButton(
|
||||||
context,
|
context,
|
||||||
context.translations.deleteLabel.capitalize(),
|
context.translations.deleteLabel.capitalize(),
|
||||||
@@ -171,6 +223,16 @@ class AttachmentActionsModal extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
color: theme.colorTheme.accentError,
|
color: theme.colorTheme.accentError,
|
||||||
),
|
),
|
||||||
|
...customActions
|
||||||
|
.map(
|
||||||
|
(e) => _buildButton(
|
||||||
|
context,
|
||||||
|
e.actionTitle,
|
||||||
|
e.icon,
|
||||||
|
e.onTap,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
]
|
]
|
||||||
.map<Widget>((e) => Align(
|
.map<Widget>((e) => Align(
|
||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
@@ -193,7 +255,7 @@ class AttachmentActionsModal extends StatelessWidget {
|
|||||||
Widget _buildButton(
|
Widget _buildButton(
|
||||||
context,
|
context,
|
||||||
String title,
|
String title,
|
||||||
StreamSvgIcon icon,
|
Widget icon,
|
||||||
VoidCallback? onTap, {
|
VoidCallback? onTap, {
|
||||||
Color? color,
|
Color? color,
|
||||||
Key? key,
|
Key? key,
|
||||||
@@ -331,3 +393,22 @@ class _DownloadProgress {
|
|||||||
|
|
||||||
int get toPercentage => (received * 100) ~/ total;
|
int get toPercentage => (received * 100) ~/ total;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Class for custom attachment action
|
||||||
|
class AttachmentAction {
|
||||||
|
/// Constructor for custom attachment action
|
||||||
|
AttachmentAction({
|
||||||
|
required this.actionTitle,
|
||||||
|
required this.icon,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Title for the attachment action
|
||||||
|
String actionTitle;
|
||||||
|
|
||||||
|
/// Icon for the attachment action
|
||||||
|
Widget icon;
|
||||||
|
|
||||||
|
/// Callback for when the action is tapped
|
||||||
|
VoidCallback onTap;
|
||||||
|
}
|
||||||
|
|||||||
@@ -59,9 +59,10 @@ class ChannelInfo extends StatelessWidget {
|
|||||||
final memberCount = channel.memberCount;
|
final memberCount = channel.memberCount;
|
||||||
if (memberCount != null && memberCount > 2) {
|
if (memberCount != null && memberCount > 2) {
|
||||||
var text = context.translations.membersCountText(memberCount);
|
var text = context.translations.membersCountText(memberCount);
|
||||||
final watcherCount = channel.state?.watcherCount ?? 0;
|
final onlineCount =
|
||||||
if (watcherCount > 0) {
|
members?.where((m) => m.user?.online == true).length ?? 0;
|
||||||
text += ' ${context.translations.watchersCountText(watcherCount)}';
|
if (onlineCount > 0) {
|
||||||
|
text += ', ${context.translations.watchersCountText(onlineCount)}';
|
||||||
}
|
}
|
||||||
alternativeWidget = Text(
|
alternativeWidget = Text(
|
||||||
text,
|
text,
|
||||||
|
|||||||
@@ -126,16 +126,26 @@ class ChannelPreview extends StatelessWidget {
|
|||||||
streamChatState.currentUser?.id) {
|
streamChatState.currentUser?.id) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(right: 4),
|
padding: const EdgeInsets.only(right: 4),
|
||||||
child: SendingIndicator(
|
child: BetterStreamBuilder<List<Read>>(
|
||||||
|
stream: channel.state?.readStream,
|
||||||
|
initialData: channel.state?.read,
|
||||||
|
builder: (context, data) {
|
||||||
|
final readList = data.where((it) =>
|
||||||
|
it.user.id !=
|
||||||
|
channel.client.state.currentUser?.id &&
|
||||||
|
(it.lastRead
|
||||||
|
.isAfter(lastMessage!.createdAt) ||
|
||||||
|
it.lastRead.isAtSameMomentAs(
|
||||||
|
lastMessage.createdAt,
|
||||||
|
)));
|
||||||
|
final isMessageRead = readList.length >=
|
||||||
|
(channel.memberCount ?? 0) - 1;
|
||||||
|
return SendingIndicator(
|
||||||
message: lastMessage!,
|
message: lastMessage!,
|
||||||
size: channelPreviewTheme.indicatorIconSize,
|
size: channelPreviewTheme.indicatorIconSize,
|
||||||
isMessageRead: channel.state!.read
|
isMessageRead: isMessageRead,
|
||||||
.where((element) =>
|
);
|
||||||
element.user.id !=
|
},
|
||||||
channel.client.state.currentUser!.id)
|
|
||||||
.where((element) => element.lastRead
|
|
||||||
.isAfter(lastMessage.createdAt))
|
|
||||||
.isNotEmpty,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:characters/characters.dart';
|
import 'package:characters/characters.dart';
|
||||||
import 'package:diacritic/diacritic.dart';
|
import 'package:diacritic/diacritic.dart';
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stream_chat_flutter/src/emoji/emoji.dart';
|
import 'package:stream_chat_flutter/src/emoji/emoji.dart';
|
||||||
import 'package:stream_chat_flutter/src/localization/translations.dart';
|
import 'package:stream_chat_flutter/src/localization/translations.dart';
|
||||||
@@ -46,7 +47,7 @@ extension IterableX<T> on Iterable<T> {
|
|||||||
extension PlatformFileX on PlatformFile {
|
extension PlatformFileX on PlatformFile {
|
||||||
/// Converts the [PlatformFile] into [AttachmentFile]
|
/// Converts the [PlatformFile] into [AttachmentFile]
|
||||||
AttachmentFile get toAttachmentFile => AttachmentFile(
|
AttachmentFile get toAttachmentFile => AttachmentFile(
|
||||||
path: path,
|
path: kIsWeb ? null : path,
|
||||||
name: name,
|
name: name,
|
||||||
bytes: bytes,
|
bytes: bytes,
|
||||||
size: size,
|
size: size,
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ class FullScreenMedia extends StatefulWidget {
|
|||||||
this.startIndex = 0,
|
this.startIndex = 0,
|
||||||
String? userName,
|
String? userName,
|
||||||
this.onShowMessage,
|
this.onShowMessage,
|
||||||
|
this.attachmentActionsModalBuilder,
|
||||||
}) : userName = userName ?? '',
|
}) : userName = userName ?? '',
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
@@ -51,6 +52,11 @@ class FullScreenMedia extends StatefulWidget {
|
|||||||
/// Callback for when show message is tapped
|
/// Callback for when show message is tapped
|
||||||
final ShowMessageCallback? onShowMessage;
|
final ShowMessageCallback? onShowMessage;
|
||||||
|
|
||||||
|
/// Widget builder for attachment actions modal
|
||||||
|
/// [defaultActionsModal] is the default [AttachmentActionsModal] config
|
||||||
|
/// Use [defaultActionsModal.copyWith] to easily customize it
|
||||||
|
final AttachmentActionsBuilder? attachmentActionsModalBuilder;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_FullScreenMediaState createState() => _FullScreenMediaState();
|
_FullScreenMediaState createState() => _FullScreenMediaState();
|
||||||
}
|
}
|
||||||
@@ -196,6 +202,8 @@ class _FullScreenMediaState extends State<FullScreenMedia>
|
|||||||
StreamChannel.of(context).channel,
|
StreamChannel.of(context).channel,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
attachmentActionsModalBuilder:
|
||||||
|
widget.attachmentActionsModalBuilder,
|
||||||
),
|
),
|
||||||
if (!widget.message.isEphemeral)
|
if (!widget.message.isEphemeral)
|
||||||
GalleryFooter(
|
GalleryFooter(
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:dio/dio.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|||||||
@@ -6,6 +6,15 @@ import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
|||||||
import 'package:stream_chat_flutter/src/theme/themes.dart';
|
import 'package:stream_chat_flutter/src/theme/themes.dart';
|
||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
|
|
||||||
|
/// Widget builder for attachment actions modal
|
||||||
|
/// [defaultActionsModal] is the default [AttachmentActionsModal] config
|
||||||
|
/// Use [defaultActionsModal.copyWith] to easily customize it
|
||||||
|
typedef AttachmentActionsBuilder = Widget Function(
|
||||||
|
BuildContext context,
|
||||||
|
Attachment attachment,
|
||||||
|
AttachmentActionsModal defaultActionsModal,
|
||||||
|
);
|
||||||
|
|
||||||
/// Header/AppBar widget for media display screen
|
/// Header/AppBar widget for media display screen
|
||||||
class GalleryHeader extends StatelessWidget implements PreferredSizeWidget {
|
class GalleryHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||||
/// Creates a channel header
|
/// Creates a channel header
|
||||||
@@ -21,6 +30,7 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
this.userName = '',
|
this.userName = '',
|
||||||
this.sentAt = '',
|
this.sentAt = '',
|
||||||
this.backgroundColor,
|
this.backgroundColor,
|
||||||
|
this.attachmentActionsModalBuilder,
|
||||||
}) : preferredSize = const Size.fromHeight(kToolbarHeight),
|
}) : preferredSize = const Size.fromHeight(kToolbarHeight),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
@@ -55,6 +65,11 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
/// The background color of this [GalleryHeader].
|
/// The background color of this [GalleryHeader].
|
||||||
final Color? backgroundColor;
|
final Color? backgroundColor;
|
||||||
|
|
||||||
|
/// Widget builder for attachment actions modal
|
||||||
|
/// [defaultActionsModal] is the default [AttachmentActionsModal] config
|
||||||
|
/// Use [defaultActionsModal.copyWith] to easily customize it
|
||||||
|
final AttachmentActionsBuilder? attachmentActionsModalBuilder;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final galleryHeaderThemeData = GalleryHeaderTheme.of(context);
|
final galleryHeaderThemeData = GalleryHeaderTheme.of(context);
|
||||||
@@ -123,17 +138,26 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
final galleryHeaderThemeData =
|
final galleryHeaderThemeData =
|
||||||
StreamChatTheme.of(context).galleryHeaderTheme;
|
StreamChatTheme.of(context).galleryHeaderTheme;
|
||||||
|
|
||||||
|
final defaultModal = AttachmentActionsModal(
|
||||||
|
message: message,
|
||||||
|
currentIndex: currentIndex,
|
||||||
|
onShowMessage: onShowMessage,
|
||||||
|
);
|
||||||
|
|
||||||
|
final effectiveModal = attachmentActionsModalBuilder?.call(
|
||||||
|
context,
|
||||||
|
message.attachments[currentIndex],
|
||||||
|
defaultModal,
|
||||||
|
) ??
|
||||||
|
defaultModal;
|
||||||
|
|
||||||
final result = await showDialog(
|
final result = await showDialog(
|
||||||
useRootNavigator: false,
|
useRootNavigator: false,
|
||||||
context: context,
|
context: context,
|
||||||
barrierColor: galleryHeaderThemeData.bottomSheetBarrierColor,
|
barrierColor: galleryHeaderThemeData.bottomSheetBarrierColor,
|
||||||
builder: (context) => StreamChannel(
|
builder: (context) => StreamChannel(
|
||||||
channel: channel,
|
channel: channel,
|
||||||
child: AttachmentActionsModal(
|
child: effectiveModal,
|
||||||
message: message,
|
|
||||||
currentIndex: currentIndex,
|
|
||||||
onShowMessage: onShowMessage,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ class GroupAvatar extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
initialData: member,
|
initialData: member,
|
||||||
builder: (context, member) => UserAvatar(
|
builder: (context, member) => UserAvatar(
|
||||||
|
showOnlineStatus: false,
|
||||||
user: member.user!,
|
user: member.user!,
|
||||||
borderRadius: BorderRadius.zero,
|
borderRadius: BorderRadius.zero,
|
||||||
),
|
),
|
||||||
@@ -118,6 +119,7 @@ class GroupAvatar extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
initialData: member,
|
initialData: member,
|
||||||
builder: (context, member) => UserAvatar(
|
builder: (context, member) => UserAvatar(
|
||||||
|
showOnlineStatus: false,
|
||||||
user: member.user!,
|
user: member.user!,
|
||||||
borderRadius: BorderRadius.zero,
|
borderRadius: BorderRadius.zero,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:jiffy/jiffy.dart';
|
import 'package:jiffy/jiffy.dart';
|
||||||
import 'package:stream_chat_flutter/src/connection_status_builder.dart';
|
import 'package:stream_chat_flutter/src/connection_status_builder.dart';
|
||||||
import 'package:stream_chat_flutter/src/message_input.dart';
|
import 'package:stream_chat_flutter/src/message_input/message_input.dart';
|
||||||
import 'package:stream_chat_flutter/src/message_list_view.dart';
|
import 'package:stream_chat_flutter/src/message_list_view.dart';
|
||||||
import 'package:stream_chat_flutter/src/message_search_list_view.dart';
|
import 'package:stream_chat_flutter/src/message_search_list_view.dart';
|
||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'
|
||||||
|
|||||||
@@ -610,7 +610,9 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
|||||||
widget.editMessageInputBuilder!(context, widget.message)
|
widget.editMessageInputBuilder!(context, widget.message)
|
||||||
else
|
else
|
||||||
MessageInput(
|
MessageInput(
|
||||||
editMessage: widget.message,
|
messageInputController: MessageInputController(
|
||||||
|
message: widget.message,
|
||||||
|
),
|
||||||
preMessageSending: (m) {
|
preMessageSending: (m) {
|
||||||
FocusScope.of(context).unfocus();
|
FocusScope.of(context).unfocus();
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
|
|||||||
+186
-226
@@ -4,7 +4,6 @@ import 'dart:math';
|
|||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
@@ -14,20 +13,19 @@ import 'package:stream_chat_flutter/src/commands_overlay.dart';
|
|||||||
import 'package:stream_chat_flutter/src/emoji/emoji.dart';
|
import 'package:stream_chat_flutter/src/emoji/emoji.dart';
|
||||||
import 'package:stream_chat_flutter/src/emoji_overlay.dart';
|
import 'package:stream_chat_flutter/src/emoji_overlay.dart';
|
||||||
import 'package:stream_chat_flutter/src/extension.dart';
|
import 'package:stream_chat_flutter/src/extension.dart';
|
||||||
import 'package:stream_chat_flutter/src/message_list_view.dart';
|
|
||||||
import 'package:stream_chat_flutter/src/multi_overlay.dart';
|
import 'package:stream_chat_flutter/src/multi_overlay.dart';
|
||||||
import 'package:stream_chat_flutter/src/quoted_message_widget.dart';
|
import 'package:stream_chat_flutter/src/quoted_message_widget.dart';
|
||||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
|
||||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
|
||||||
import 'package:stream_chat_flutter/src/user_mentions_overlay.dart';
|
import 'package:stream_chat_flutter/src/user_mentions_overlay.dart';
|
||||||
import 'package:stream_chat_flutter/src/video_service.dart';
|
import 'package:stream_chat_flutter/src/video_service.dart';
|
||||||
import 'package:stream_chat_flutter/src/video_thumbnail_image.dart';
|
import 'package:stream_chat_flutter/src/video_thumbnail_image.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
|
||||||
import 'package:video_compress/video_compress.dart';
|
import 'package:video_compress/video_compress.dart';
|
||||||
|
|
||||||
export 'package:video_compress/video_compress.dart' show VideoQuality;
|
export 'package:video_compress/video_compress.dart' show VideoQuality;
|
||||||
|
|
||||||
|
/// A function that returns true if the message is valid and can be sent.
|
||||||
|
typedef MessageValidator = bool Function(Message message);
|
||||||
|
|
||||||
/// A callback that can be passed to [MessageInput.onError].
|
/// A callback that can be passed to [MessageInput.onError].
|
||||||
///
|
///
|
||||||
/// This callback should not throw.
|
/// This callback should not throw.
|
||||||
@@ -42,13 +40,14 @@ typedef ErrorListener = void Function(
|
|||||||
///
|
///
|
||||||
/// This callback should not throw.
|
/// This callback should not throw.
|
||||||
///
|
///
|
||||||
/// It exists merely for showing custom error, and should not be used otherwise.
|
/// It exists merely for showing a custom error, and should not be used
|
||||||
|
/// otherwise.
|
||||||
typedef AttachmentLimitExceedListener = void Function(
|
typedef AttachmentLimitExceedListener = void Function(
|
||||||
int limit,
|
int limit,
|
||||||
String error,
|
String error,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Builder for attachment thumbnails
|
/// Builder for attachment thumbnails.
|
||||||
typedef AttachmentThumbnailBuilder = Widget Function(
|
typedef AttachmentThumbnailBuilder = Widget Function(
|
||||||
BuildContext,
|
BuildContext,
|
||||||
Attachment,
|
Attachment,
|
||||||
@@ -77,8 +76,8 @@ typedef ActionButtonBuilder = Widget Function(
|
|||||||
IconButton defaultActionButton,
|
IconButton defaultActionButton,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Widget builder for widgets that require may required data from the
|
/// Widget builder for widgets that may require data from the
|
||||||
/// [MessageInputController]
|
/// [MessageInputController].
|
||||||
typedef MessageRelatedBuilder = Widget Function(
|
typedef MessageRelatedBuilder = Widget Function(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
MessageInputController messageInputController,
|
MessageInputController messageInputController,
|
||||||
@@ -91,7 +90,7 @@ typedef AttachmentsPickerBuilder = Widget Function(
|
|||||||
StreamAttachmentPicker defaultPicker,
|
StreamAttachmentPicker defaultPicker,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Location for actions on the [MessageInput]
|
/// Location for actions on the [MessageInput].
|
||||||
enum ActionsLocation {
|
enum ActionsLocation {
|
||||||
/// Align to left
|
/// Align to left
|
||||||
left,
|
left,
|
||||||
@@ -106,7 +105,7 @@ enum ActionsLocation {
|
|||||||
rightInside,
|
rightInside,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Default attachments for widget
|
/// Default attachments for widget.
|
||||||
enum DefaultAttachmentTypes {
|
enum DefaultAttachmentTypes {
|
||||||
/// Image Attachment
|
/// Image Attachment
|
||||||
image,
|
image,
|
||||||
@@ -118,7 +117,7 @@ enum DefaultAttachmentTypes {
|
|||||||
file,
|
file,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Available locations for the sendMessage button relative to the textField
|
/// Available locations for the `sendMessage` button relative to the textField.
|
||||||
enum SendButtonLocation {
|
enum SendButtonLocation {
|
||||||
/// inside the textField
|
/// inside the textField
|
||||||
inside,
|
inside,
|
||||||
@@ -131,17 +130,17 @@ const _kMinMediaPickerSize = 360.0;
|
|||||||
|
|
||||||
const _kDefaultMaxAttachmentSize = 20971520; // 20MB in Bytes
|
const _kDefaultMaxAttachmentSize = 20971520; // 20MB in Bytes
|
||||||
|
|
||||||
/// Inactive state
|
/// Inactive state:
|
||||||
///
|
///
|
||||||
/// 
|
/// 
|
||||||
/// 
|
/// 
|
||||||
///
|
///
|
||||||
/// Focused state
|
/// Focused state:
|
||||||
///
|
///
|
||||||
/// 
|
/// 
|
||||||
/// 
|
/// 
|
||||||
///
|
///
|
||||||
/// Widget used to enter the message and add attachments
|
/// Widget used to enter a message and add attachments:
|
||||||
///
|
///
|
||||||
/// ```dart
|
/// ```dart
|
||||||
/// class ChannelPage extends StatelessWidget {
|
/// class ChannelPage extends StatelessWidget {
|
||||||
@@ -176,20 +175,16 @@ const _kDefaultMaxAttachmentSize = 20971520; // 20MB in Bytes
|
|||||||
/// as the bottom widget.
|
/// as the bottom widget.
|
||||||
///
|
///
|
||||||
/// The widget renders the ui based on the first ancestor of
|
/// The widget renders the ui based on the first ancestor of
|
||||||
/// type [StreamChatTheme].
|
/// type [StreamChatTheme]. Modify it to change the widget appearance.
|
||||||
/// Modify it to change the widget appearance.
|
|
||||||
class MessageInput extends StatefulWidget {
|
class MessageInput extends StatefulWidget {
|
||||||
/// Instantiate a new MessageInput
|
/// Instantiate a new MessageInput
|
||||||
const MessageInput({
|
const MessageInput({
|
||||||
Key? key,
|
Key? key,
|
||||||
this.onMessageSent,
|
this.onMessageSent,
|
||||||
this.preMessageSending,
|
this.preMessageSending,
|
||||||
this.parentMessage,
|
|
||||||
this.editMessage,
|
|
||||||
this.maxHeight = 150,
|
this.maxHeight = 150,
|
||||||
this.keyboardType = TextInputType.multiline,
|
this.keyboardType = TextInputType.multiline,
|
||||||
this.disableAttachments = false,
|
this.disableAttachments = false,
|
||||||
this.initialMessage,
|
|
||||||
this.messageInputController,
|
this.messageInputController,
|
||||||
this.actions = const [],
|
this.actions = const [],
|
||||||
this.actionsLocation = ActionsLocation.left,
|
this.actionsLocation = ActionsLocation.left,
|
||||||
@@ -218,76 +213,67 @@ class MessageInput extends StatefulWidget {
|
|||||||
this.mentionAllAppUsers = false,
|
this.mentionAllAppUsers = false,
|
||||||
this.attachmentsPickerBuilder,
|
this.attachmentsPickerBuilder,
|
||||||
this.sendButtonBuilder,
|
this.sendButtonBuilder,
|
||||||
}) : assert(
|
this.shouldKeepFocusAfterMessage,
|
||||||
initialMessage == null || editMessage == null,
|
this.validator = _defaultValidator,
|
||||||
"Can't provide both `initialMessage` and `editMessage`",
|
this.restorationId,
|
||||||
),
|
}) : super(key: key);
|
||||||
super(key: key);
|
|
||||||
|
|
||||||
/// List of options for showing overlays
|
/// List of options for showing overlays.
|
||||||
final List<OverlayOptions> customOverlays;
|
final List<OverlayOptions> customOverlays;
|
||||||
|
|
||||||
/// Message to edit
|
/// Video quality to use when compressing the videos.
|
||||||
final Message? editMessage;
|
|
||||||
|
|
||||||
/// Video quality to use when compressing the videos
|
|
||||||
final VideoQuality compressedVideoQuality;
|
final VideoQuality compressedVideoQuality;
|
||||||
|
|
||||||
/// Frame rate to use when compressing the videos
|
/// Frame rate to use when compressing the videos.
|
||||||
final int compressedVideoFrameRate;
|
final int compressedVideoFrameRate;
|
||||||
|
|
||||||
/// Max attachment size in bytes
|
/// Max attachment size in bytes:
|
||||||
/// Defaults to 20 MB
|
/// - Defaults to 20 MB
|
||||||
/// do not set it if you're using our default CDN
|
/// - Do not set it if you're using our default CDN
|
||||||
final int maxAttachmentSize;
|
final int maxAttachmentSize;
|
||||||
|
|
||||||
/// Message to start with
|
/// Function called after sending the message.
|
||||||
final Message? initialMessage;
|
|
||||||
|
|
||||||
/// Function called after sending the message
|
|
||||||
final void Function(Message)? onMessageSent;
|
final void Function(Message)? onMessageSent;
|
||||||
|
|
||||||
/// Function called right before sending the message
|
/// Function called right before sending the message.
|
||||||
/// Use this to transform the message
|
///
|
||||||
|
/// Use this to transform the message.
|
||||||
final FutureOr<Message> Function(Message)? preMessageSending;
|
final FutureOr<Message> Function(Message)? preMessageSending;
|
||||||
|
|
||||||
/// Parent message in case of a thread
|
/// Maximum Height for the TextField to grow before it starts scrolling.
|
||||||
final Message? parentMessage;
|
|
||||||
|
|
||||||
/// Maximum Height for the TextField to grow before it starts scrolling
|
|
||||||
final double maxHeight;
|
final double maxHeight;
|
||||||
|
|
||||||
/// The keyboard type assigned to the TextField
|
/// The keyboard type assigned to the TextField.
|
||||||
final TextInputType keyboardType;
|
final TextInputType keyboardType;
|
||||||
|
|
||||||
/// If true the attachments button will not be displayed
|
/// If true the attachments button will not be displayed.
|
||||||
final bool disableAttachments;
|
final bool disableAttachments;
|
||||||
|
|
||||||
/// Use this property to hide/show the commands button
|
/// Use this property to hide/show the commands button.
|
||||||
final bool showCommandsButton;
|
final bool showCommandsButton;
|
||||||
|
|
||||||
/// Hide send as dm checkbox
|
/// Hide send as dm checkbox.
|
||||||
final bool hideSendAsDm;
|
final bool hideSendAsDm;
|
||||||
|
|
||||||
/// The text controller of the TextField
|
/// The text controller of the TextField.
|
||||||
final MessageInputController? messageInputController;
|
final MessageInputController? messageInputController;
|
||||||
|
|
||||||
/// List of action widgets
|
/// List of action widgets.
|
||||||
final List<Widget> actions;
|
final List<Widget> actions;
|
||||||
|
|
||||||
/// The location of the custom actions
|
/// The location of the custom actions.
|
||||||
final ActionsLocation actionsLocation;
|
final ActionsLocation actionsLocation;
|
||||||
|
|
||||||
/// Map that defines a thumbnail builder for an attachment type
|
/// Map that defines a thumbnail builder for an attachment type.
|
||||||
final Map<String, AttachmentThumbnailBuilder>? attachmentThumbnailBuilders;
|
final Map<String, AttachmentThumbnailBuilder>? attachmentThumbnailBuilders;
|
||||||
|
|
||||||
/// The focus node associated to the TextField
|
/// The focus node associated to the TextField.
|
||||||
final FocusNode? focusNode;
|
final FocusNode? focusNode;
|
||||||
|
|
||||||
///
|
/// The message that is being quoted.
|
||||||
final Message? quotedMessage;
|
final Message? quotedMessage;
|
||||||
|
|
||||||
///
|
/// Callback invoked when the quoted message is cleared.
|
||||||
final VoidCallback? onQuotedMessageCleared;
|
final VoidCallback? onQuotedMessageCleared;
|
||||||
|
|
||||||
/// The location of the send button
|
/// The location of the send button
|
||||||
@@ -342,6 +328,19 @@ class MessageInput extends StatefulWidget {
|
|||||||
/// Builder for creating send button
|
/// Builder for creating send button
|
||||||
final MessageRelatedBuilder? sendButtonBuilder;
|
final MessageRelatedBuilder? sendButtonBuilder;
|
||||||
|
|
||||||
|
/// Defines if the [MessageInput] loses focuses after a message is sent.
|
||||||
|
/// The default behaviour keeps focus until a command is enabled.
|
||||||
|
final bool? shouldKeepFocusAfterMessage;
|
||||||
|
|
||||||
|
/// A callback function that validates the message.
|
||||||
|
final MessageValidator validator;
|
||||||
|
|
||||||
|
/// Restoration ID to save and restore the state of the MessageInput.
|
||||||
|
final String? restorationId;
|
||||||
|
|
||||||
|
static bool _defaultValidator(Message message) =>
|
||||||
|
message.text?.isNotEmpty == true || message.attachments.isNotEmpty;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MessageInputState createState() => MessageInputState();
|
MessageInputState createState() => MessageInputState();
|
||||||
|
|
||||||
@@ -358,40 +357,77 @@ class MessageInput extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// State of [MessageInput]
|
/// State of [MessageInput]
|
||||||
class MessageInputState extends State<MessageInput> {
|
class MessageInputState extends State<MessageInput>
|
||||||
|
with RestorationMixin<MessageInput> {
|
||||||
final _imagePicker = ImagePicker();
|
final _imagePicker = ImagePicker();
|
||||||
late final _focusNode = widget.focusNode ?? FocusNode();
|
late final _focusNode = widget.focusNode ?? FocusNode();
|
||||||
bool _inputEnabled = true;
|
bool _inputEnabled = true;
|
||||||
bool _commandEnabled = false;
|
bool get _commandEnabled => _effectiveController.value.command != null;
|
||||||
bool _showCommandsOverlay = false;
|
bool _showCommandsOverlay = false;
|
||||||
bool _showMentionsOverlay = false;
|
bool _showMentionsOverlay = false;
|
||||||
|
|
||||||
Command? _chosenCommand;
|
|
||||||
bool _actionsShrunk = false;
|
bool _actionsShrunk = false;
|
||||||
bool _openFilePickerSection = false;
|
bool _openFilePickerSection = false;
|
||||||
|
|
||||||
/// The editing controller passed to the input TextField
|
|
||||||
late final MessageInputController messageInputController =
|
|
||||||
widget.messageInputController ?? MessageInputController();
|
|
||||||
|
|
||||||
late StreamChatThemeData _streamChatTheme;
|
late StreamChatThemeData _streamChatTheme;
|
||||||
late MessageInputThemeData _messageInputTheme;
|
late MessageInputThemeData _messageInputTheme;
|
||||||
|
|
||||||
bool get _hasQuotedMessage => widget.quotedMessage != null;
|
bool get _hasQuotedMessage =>
|
||||||
|
_effectiveController.value.quotedMessage != null;
|
||||||
|
|
||||||
bool get _messageIsPresent => messageInputController.text.trim().isNotEmpty;
|
bool get _isEditing =>
|
||||||
|
_effectiveController.value.status != MessageSendingStatus.sending;
|
||||||
|
|
||||||
|
RestorableMessageInputController? _controller;
|
||||||
|
MessageInputController get _effectiveController =>
|
||||||
|
widget.messageInputController ?? _controller!.value;
|
||||||
|
|
||||||
|
void _createLocalController([Message? message]) {
|
||||||
|
assert(_controller == null, '');
|
||||||
|
_controller = RestorableMessageInputController(message: message);
|
||||||
|
print('_controller?.value: ${_controller?.value}');
|
||||||
|
}
|
||||||
|
|
||||||
|
void _registerController() {
|
||||||
|
assert(_controller != null, '');
|
||||||
|
registerForRestoration(_controller!, 'messageInputController');
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
if (widget.editMessage != null || widget.initialMessage != null) {
|
if (widget.messageInputController == null) {
|
||||||
_parseExistingMessage(widget.editMessage ?? widget.initialMessage!);
|
_createLocalController();
|
||||||
|
print('_controller?.value: ${_controller?.value}');
|
||||||
}
|
}
|
||||||
messageInputController.textEditingController
|
_effectiveController.textEditingController.addListener(_onChangedDebounced);
|
||||||
.addListener(_onChangedDebounced);
|
|
||||||
_focusNode.addListener(_focusNodeListener);
|
_focusNode.addListener(_focusNodeListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant MessageInput oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (widget.messageInputController == null &&
|
||||||
|
oldWidget.messageInputController != null) {
|
||||||
|
_createLocalController(oldWidget.messageInputController!.value);
|
||||||
|
} else if (widget.messageInputController != null &&
|
||||||
|
oldWidget.messageInputController == null) {
|
||||||
|
unregisterFromRestoration(_controller!);
|
||||||
|
_controller!.dispose();
|
||||||
|
_controller = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
|
||||||
|
if (_controller != null) {
|
||||||
|
_registerController();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get restorationId => widget.restorationId;
|
||||||
|
|
||||||
void _focusNodeListener() {
|
void _focusNodeListener() {
|
||||||
if (_focusNode.hasFocus) {
|
if (_focusNode.hasFocus) {
|
||||||
_openFilePickerSection = false;
|
_openFilePickerSection = false;
|
||||||
@@ -430,9 +466,9 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Widget child = ValueListenableBuilder<Message>(
|
Widget child = MessageValueListenableBuilder(
|
||||||
valueListenable: messageInputController,
|
valueListenable: _effectiveController,
|
||||||
builder: (context, value, wid) => DecoratedBox(
|
builder: (context, value, _) => DecoratedBox(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: _messageInputTheme.inputBackgroundColor,
|
color: _messageInputTheme.inputBackgroundColor,
|
||||||
),
|
),
|
||||||
@@ -479,7 +515,8 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
child: _buildTextField(context),
|
child: _buildTextField(context),
|
||||||
),
|
),
|
||||||
if (widget.parentMessage != null && !widget.hideSendAsDm)
|
if (_effectiveController.value.parentId != null &&
|
||||||
|
!widget.hideSendAsDm)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
right: 12,
|
right: 12,
|
||||||
@@ -495,7 +532,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (widget.editMessage == null) {
|
if (_isEditing) {
|
||||||
child = Material(
|
child = Material(
|
||||||
elevation: 8,
|
elevation: 8,
|
||||||
child: child,
|
child: child,
|
||||||
@@ -512,12 +549,12 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
),
|
),
|
||||||
OverlayOptions(
|
OverlayOptions(
|
||||||
visible: _focusNode.hasFocus &&
|
visible: _focusNode.hasFocus &&
|
||||||
messageInputController.text.isNotEmpty &&
|
_effectiveController.text.isNotEmpty &&
|
||||||
messageInputController.baseOffset > 0 &&
|
_effectiveController.baseOffset > 0 &&
|
||||||
messageInputController.text
|
_effectiveController.text
|
||||||
.substring(
|
.substring(
|
||||||
0,
|
0,
|
||||||
messageInputController.baseOffset,
|
_effectiveController.baseOffset,
|
||||||
)
|
)
|
||||||
.contains(':'),
|
.contains(':'),
|
||||||
widget: _buildEmojiOverlay(),
|
widget: _buildEmojiOverlay(),
|
||||||
@@ -553,7 +590,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
height: 16,
|
height: 16,
|
||||||
width: 16,
|
width: 16,
|
||||||
foregroundDecoration: BoxDecoration(
|
foregroundDecoration: BoxDecoration(
|
||||||
border: messageInputController.showInChannel
|
border: _effectiveController.showInChannel
|
||||||
? null
|
? null
|
||||||
: Border.all(
|
: Border.all(
|
||||||
color: _streamChatTheme.colorTheme.textHighEmphasis
|
color: _streamChatTheme.colorTheme.textHighEmphasis
|
||||||
@@ -565,20 +602,18 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
child: Center(
|
child: Center(
|
||||||
child: Material(
|
child: Material(
|
||||||
borderRadius: BorderRadius.circular(3),
|
borderRadius: BorderRadius.circular(3),
|
||||||
color: messageInputController.showInChannel
|
color: _effectiveController.showInChannel
|
||||||
? _streamChatTheme.colorTheme.accentPrimary
|
? _streamChatTheme.colorTheme.accentPrimary
|
||||||
: _streamChatTheme.colorTheme.barsBg,
|
: _streamChatTheme.colorTheme.barsBg,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
setState(() {
|
_effectiveController.showInChannel =
|
||||||
messageInputController.showInChannel =
|
!_effectiveController.showInChannel;
|
||||||
!messageInputController.showInChannel;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
child: AnimatedCrossFade(
|
child: AnimatedCrossFade(
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
reverseDuration: const Duration(milliseconds: 300),
|
reverseDuration: const Duration(milliseconds: 300),
|
||||||
crossFadeState: messageInputController.showInChannel
|
crossFadeState: _effectiveController.showInChannel
|
||||||
? CrossFadeState.showFirst
|
? CrossFadeState.showFirst
|
||||||
: CrossFadeState.showSecond,
|
: CrossFadeState.showSecond,
|
||||||
firstChild: StreamSvgIcon.check(
|
firstChild: StreamSvgIcon.check(
|
||||||
@@ -609,14 +644,14 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
|
|
||||||
Widget _buildSendButton(BuildContext context) {
|
Widget _buildSendButton(BuildContext context) {
|
||||||
if (widget.sendButtonBuilder != null) {
|
if (widget.sendButtonBuilder != null) {
|
||||||
return widget.sendButtonBuilder!(context, messageInputController);
|
return widget.sendButtonBuilder!(context, _effectiveController);
|
||||||
}
|
}
|
||||||
|
|
||||||
return StreamMessageSendButton(
|
return StreamMessageSendButton(
|
||||||
onSendMessage: sendMessage,
|
onSendMessage: sendMessage,
|
||||||
timeOut: _timeOut,
|
timeOut: _timeOut,
|
||||||
isIdle: !_messageIsPresent && messageInputController.attachments.isEmpty,
|
isIdle: !widget.validator(_effectiveController.message),
|
||||||
isEditEnabled: widget.editMessage != null,
|
isEditEnabled: _isEditing,
|
||||||
idleSendButton: widget.idleSendButton,
|
idleSendButton: widget.idleSendButton,
|
||||||
activeSendButton: widget.activeSendButton,
|
activeSendButton: widget.activeSendButton,
|
||||||
);
|
);
|
||||||
@@ -663,7 +698,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
if (!widget.disableAttachments)
|
if (!widget.disableAttachments)
|
||||||
_buildAttachmentButton(context),
|
_buildAttachmentButton(context),
|
||||||
if (widget.showCommandsButton &&
|
if (widget.showCommandsButton &&
|
||||||
widget.editMessage == null &&
|
!_isEditing &&
|
||||||
channel.state != null &&
|
channel.state != null &&
|
||||||
channel.config?.commands.isNotEmpty == true)
|
channel.config?.commands.isNotEmpty == true)
|
||||||
_buildCommandButton(context),
|
_buildCommandButton(context),
|
||||||
@@ -714,7 +749,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
maxLines: null,
|
maxLines: null,
|
||||||
onSubmitted: (_) => sendMessage(),
|
onSubmitted: (_) => sendMessage(),
|
||||||
keyboardType: widget.keyboardType,
|
keyboardType: widget.keyboardType,
|
||||||
controller: messageInputController,
|
controller: _effectiveController,
|
||||||
focusNode: _focusNode,
|
focusNode: _focusNode,
|
||||||
style: _messageInputTheme.inputTextStyle,
|
style: _messageInputTheme.inputTextStyle,
|
||||||
autofocus: widget.autofocus,
|
autofocus: widget.autofocus,
|
||||||
@@ -786,7 +821,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
size: 16,
|
size: 16,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
_chosenCommand?.name.toUpperCase() ?? '',
|
_effectiveController.value.command!.toUpperCase(),
|
||||||
style:
|
style:
|
||||||
_streamChatTheme.textTheme.footnoteBold.copyWith(
|
_streamChatTheme.textTheme.footnoteBold.copyWith(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
@@ -820,9 +855,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
height: 24,
|
height: 24,
|
||||||
width: 24,
|
width: 24,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: _effectiveController.clear,
|
||||||
setState(() => _commandEnabled = false);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (!_commandEnabled &&
|
if (!_commandEnabled &&
|
||||||
@@ -837,14 +870,16 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
|
|
||||||
late final _onChangedDebounced = debounce(
|
late final _onChangedDebounced = debounce(
|
||||||
() {
|
() {
|
||||||
var value = messageInputController.text;
|
var value = _effectiveController.text;
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
value = value.trim();
|
value = value.trim();
|
||||||
|
|
||||||
final channel = StreamChannel.of(context).channel;
|
final channel = StreamChannel.of(context).channel;
|
||||||
if (value.isNotEmpty) {
|
if (value.isNotEmpty) {
|
||||||
|
channel
|
||||||
|
.keyStroke(_effectiveController.value.parentId)
|
||||||
// ignore: no-empty-block
|
// ignore: no-empty-block
|
||||||
channel.keyStroke(widget.parentMessage?.id).catchError((e) {});
|
.catchError((e) {});
|
||||||
}
|
}
|
||||||
|
|
||||||
var actionsLength = widget.actions.length;
|
var actionsLength = widget.actions.length;
|
||||||
@@ -864,10 +899,10 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
String _getHint(BuildContext context) {
|
String _getHint(BuildContext context) {
|
||||||
if (_commandEnabled && _chosenCommand!.name == 'giphy') {
|
if (_commandEnabled && _effectiveController.value.command == 'giphy') {
|
||||||
return context.translations.searchGifLabel;
|
return context.translations.searchGifLabel;
|
||||||
}
|
}
|
||||||
if (messageInputController.attachments.isNotEmpty) {
|
if (_effectiveController.attachments.isNotEmpty) {
|
||||||
return context.translations.addACommentOrSendLabel;
|
return context.translations.addACommentOrSendLabel;
|
||||||
}
|
}
|
||||||
if (_timeOut != 0) {
|
if (_timeOut != 0) {
|
||||||
@@ -879,16 +914,16 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
|
|
||||||
void _checkEmoji(String s, BuildContext context) {
|
void _checkEmoji(String s, BuildContext context) {
|
||||||
if (s.isNotEmpty &&
|
if (s.isNotEmpty &&
|
||||||
messageInputController.baseOffset > 0 &&
|
_effectiveController.baseOffset > 0 &&
|
||||||
messageInputController.text
|
_effectiveController.text
|
||||||
.substring(
|
.substring(
|
||||||
0,
|
0,
|
||||||
messageInputController.baseOffset,
|
_effectiveController.baseOffset,
|
||||||
)
|
)
|
||||||
.contains(':')) {
|
.contains(':')) {
|
||||||
final textToSelection = messageInputController.text.substring(
|
final textToSelection = _effectiveController.text.substring(
|
||||||
0,
|
0,
|
||||||
messageInputController.selectionStart,
|
_effectiveController.selectionStart,
|
||||||
);
|
);
|
||||||
final splits = textToSelection.split(':');
|
final splits = textToSelection.split(':');
|
||||||
final query = splits[splits.length - 2].toLowerCase();
|
final query = splits[splits.length - 2].toLowerCase();
|
||||||
@@ -902,11 +937,11 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
|
|
||||||
void _checkMentions(String s, BuildContext context) {
|
void _checkMentions(String s, BuildContext context) {
|
||||||
if (s.isNotEmpty &&
|
if (s.isNotEmpty &&
|
||||||
messageInputController.baseOffset > 0 &&
|
_effectiveController.baseOffset > 0 &&
|
||||||
messageInputController.text
|
_effectiveController.text
|
||||||
.substring(
|
.substring(
|
||||||
0,
|
0,
|
||||||
messageInputController.baseOffset,
|
_effectiveController.baseOffset,
|
||||||
)
|
)
|
||||||
.split(' ')
|
.split(' ')
|
||||||
.last
|
.last
|
||||||
@@ -943,7 +978,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildCommandsOverlayEntry() {
|
Widget _buildCommandsOverlayEntry() {
|
||||||
final text = messageInputController.text.trimLeft();
|
final text = _effectiveController.text.trimLeft();
|
||||||
|
|
||||||
final renderObject = context.findRenderObject() as RenderBox?;
|
final renderObject = context.findRenderObject() as RenderBox?;
|
||||||
if (renderObject == null) {
|
if (renderObject == null) {
|
||||||
@@ -959,7 +994,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
|
|
||||||
Widget _buildFilePickerSection() {
|
Widget _buildFilePickerSection() {
|
||||||
final picker = StreamAttachmentPicker(
|
final picker = StreamAttachmentPicker(
|
||||||
messageInputController: messageInputController,
|
messageInputController: _effectiveController,
|
||||||
onFilePicked: pickFile,
|
onFilePicked: pickFile,
|
||||||
isOpen: _openFilePickerSection,
|
isOpen: _openFilePickerSection,
|
||||||
pickerSize: _openFilePickerSection ? _kMinMediaPickerSize : 0,
|
pickerSize: _openFilePickerSection ? _kMinMediaPickerSize : 0,
|
||||||
@@ -968,18 +1003,13 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
maxAttachmentSize: widget.maxAttachmentSize,
|
maxAttachmentSize: widget.maxAttachmentSize,
|
||||||
compressedVideoQuality: widget.compressedVideoQuality,
|
compressedVideoQuality: widget.compressedVideoQuality,
|
||||||
compressedVideoFrameRate: widget.compressedVideoFrameRate,
|
compressedVideoFrameRate: widget.compressedVideoFrameRate,
|
||||||
onChangeInputState: (val) {
|
|
||||||
setState(() {
|
|
||||||
_inputEnabled = val;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onError: _showErrorAlert,
|
onError: _showErrorAlert,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (_openFilePickerSection && widget.attachmentsPickerBuilder != null) {
|
if (_openFilePickerSection && widget.attachmentsPickerBuilder != null) {
|
||||||
return widget.attachmentsPickerBuilder!(
|
return widget.attachmentsPickerBuilder!(
|
||||||
context,
|
context,
|
||||||
messageInputController,
|
_effectiveController,
|
||||||
picker,
|
picker,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -988,14 +1018,15 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMentionsOverlayEntry() {
|
Widget _buildMentionsOverlayEntry() {
|
||||||
if (messageInputController.selectionStart < 0) {
|
final channel = StreamChannel.of(context).channel;
|
||||||
|
if (_effectiveController.selectionStart < 0 || channel.state == null) {
|
||||||
return const Offstage();
|
return const Offstage();
|
||||||
}
|
}
|
||||||
|
|
||||||
final splits = messageInputController.text
|
final splits = _effectiveController.text
|
||||||
.substring(
|
.substring(
|
||||||
0,
|
0,
|
||||||
messageInputController.selectionStart,
|
_effectiveController.selectionStart,
|
||||||
)
|
)
|
||||||
.split('@');
|
.split('@');
|
||||||
final query = splits.last.toLowerCase();
|
final query = splits.last.toLowerCase();
|
||||||
@@ -1020,23 +1051,19 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
query: query,
|
query: query,
|
||||||
mentionAllAppUsers: widget.mentionAllAppUsers,
|
mentionAllAppUsers: widget.mentionAllAppUsers,
|
||||||
client: StreamChat.of(context).client,
|
client: StreamChat.of(context).client,
|
||||||
channel: StreamChannel.of(context).channel,
|
channel: channel,
|
||||||
size: Size(renderObject.size.width - 16, 400),
|
size: Size(renderObject.size.width - 16, 400),
|
||||||
mentionsTileBuilder: tileBuilder,
|
mentionsTileBuilder: tileBuilder,
|
||||||
onMentionUserTap: (user) {
|
onMentionUserTap: (user) {
|
||||||
messageInputController.addMentionedUser(user);
|
_effectiveController.addMentionedUser(user);
|
||||||
splits[splits.length - 1] = user.name;
|
splits[splits.length - 1] = user.name;
|
||||||
final rejoin = splits.join('@');
|
final rejoin = splits.join('@');
|
||||||
|
|
||||||
messageInputController.textEditingController.value = TextEditingValue(
|
_effectiveController.text = rejoin +
|
||||||
text: rejoin +
|
_effectiveController.text.substring(
|
||||||
messageInputController.text.substring(
|
_effectiveController.selectionStart,
|
||||||
messageInputController.selectionStart,
|
|
||||||
),
|
|
||||||
selection: TextSelection.collapsed(
|
|
||||||
offset: rejoin.length,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
_onChangedDebounced.cancel();
|
_onChangedDebounced.cancel();
|
||||||
setState(() => _showMentionsOverlay = false);
|
setState(() => _showMentionsOverlay = false);
|
||||||
},
|
},
|
||||||
@@ -1044,14 +1071,14 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEmojiOverlay() {
|
Widget _buildEmojiOverlay() {
|
||||||
if (messageInputController.baseOffset < 0) {
|
if (_effectiveController.baseOffset < 0) {
|
||||||
return const Offstage();
|
return const Offstage();
|
||||||
}
|
}
|
||||||
|
|
||||||
final splits = messageInputController.text
|
final splits = _effectiveController.text
|
||||||
.substring(
|
.substring(
|
||||||
0,
|
0,
|
||||||
messageInputController.baseOffset,
|
_effectiveController.baseOffset,
|
||||||
)
|
)
|
||||||
.split(':');
|
.split(':');
|
||||||
|
|
||||||
@@ -1071,22 +1098,17 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
void _chooseEmoji(List<String> splits, Emoji emoji) {
|
void _chooseEmoji(List<String> splits, Emoji emoji) {
|
||||||
final rejoin = splits.sublist(0, splits.length - 1).join(':') + emoji.char!;
|
final rejoin = splits.sublist(0, splits.length - 1).join(':') + emoji.char!;
|
||||||
|
|
||||||
messageInputController.textEditingController.value = TextEditingValue(
|
_effectiveController.text = rejoin +
|
||||||
text: rejoin +
|
_effectiveController.text.substring(
|
||||||
messageInputController.text.substring(
|
_effectiveController.selectionStart,
|
||||||
messageInputController.selectionStart,
|
|
||||||
),
|
|
||||||
selection: TextSelection.collapsed(
|
|
||||||
offset: rejoin.length,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _setCommand(Command c) {
|
void _setCommand(Command c) {
|
||||||
messageInputController.clear();
|
_effectiveController
|
||||||
|
..clear()
|
||||||
|
..command = c;
|
||||||
setState(() {
|
setState(() {
|
||||||
_chosenCommand = c;
|
|
||||||
_commandEnabled = true;
|
|
||||||
_showCommandsOverlay = false;
|
_showCommandsOverlay = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1105,11 +1127,11 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildAttachments() {
|
Widget _buildAttachments() {
|
||||||
if (messageInputController.attachments.isEmpty) return const Offstage();
|
if (_effectiveController.attachments.isEmpty) return const Offstage();
|
||||||
final fileAttachments = messageInputController.attachments
|
final fileAttachments = _effectiveController.attachments
|
||||||
.where((it) => it.type == 'file')
|
.where((it) => it.type == 'file')
|
||||||
.toList(growable: false);
|
.toList(growable: false);
|
||||||
final remainingAttachments = messageInputController.attachments
|
final remainingAttachments = _effectiveController.attachments
|
||||||
.where((it) => it.type != 'file')
|
.where((it) => it.type != 'file')
|
||||||
.toList(growable: false);
|
.toList(growable: false);
|
||||||
return Column(
|
return Column(
|
||||||
@@ -1127,9 +1149,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
(e) => ClipRRect(
|
(e) => ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
child: FileAttachment(
|
child: FileAttachment(
|
||||||
message: Message(
|
message: Message(), // dummy message
|
||||||
status: MessageSendingStatus.sending,
|
|
||||||
), // dummy message
|
|
||||||
attachment: e,
|
attachment: e,
|
||||||
size: Size(
|
size: Size(
|
||||||
MediaQuery.of(context).size.width * 0.65,
|
MediaQuery.of(context).size.width * 0.65,
|
||||||
@@ -1196,9 +1216,10 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
focusElevation: 0,
|
focusElevation: 0,
|
||||||
hoverElevation: 0,
|
hoverElevation: 0,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
setState(
|
_effectiveController.value = _effectiveController.value.copyWith(
|
||||||
() => messageInputController.attachments
|
attachments: _effectiveController.attachments
|
||||||
.removeWhere((e) => e.id == attachment.id),
|
.where((it) => it.id != attachment.id)
|
||||||
|
.toList(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
fillColor:
|
fillColor:
|
||||||
@@ -1278,7 +1299,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildCommandButton(BuildContext context) {
|
Widget _buildCommandButton(BuildContext context) {
|
||||||
final s = messageInputController.text.trim();
|
final s = _effectiveController.text.trim();
|
||||||
final defaultButton = IconButton(
|
final defaultButton = IconButton(
|
||||||
icon: StreamSvgIcon.lightning(
|
icon: StreamSvgIcon.lightning(
|
||||||
color: s.isNotEmpty
|
color: s.isNotEmpty
|
||||||
@@ -1406,14 +1427,13 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
///
|
///
|
||||||
/// Note: Only meant to be used from outside the state.
|
/// Note: Only meant to be used from outside the state.
|
||||||
void addAttachment(Attachment attachment) {
|
void addAttachment(Attachment attachment) {
|
||||||
setState(() => _addAttachments([attachment]));
|
_addAttachments([attachment]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds an attachment to the [messageInputController.attachments] map
|
/// Adds an attachment to the [messageInputController.attachments] map
|
||||||
void _addAttachments(Iterable<Attachment> attachments) {
|
void _addAttachments(Iterable<Attachment> attachments) {
|
||||||
final limit = widget.attachmentLimit;
|
final limit = widget.attachmentLimit;
|
||||||
final length =
|
final length = _effectiveController.attachments.length + attachments.length;
|
||||||
messageInputController.attachments.length + attachments.length;
|
|
||||||
if (length > limit) {
|
if (length > limit) {
|
||||||
final onAttachmentLimitExceed = widget.onAttachmentLimitExceed;
|
final onAttachmentLimitExceed = widget.onAttachmentLimitExceed;
|
||||||
if (onAttachmentLimitExceed != null) {
|
if (onAttachmentLimitExceed != null) {
|
||||||
@@ -1427,7 +1447,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
for (final attachment in attachments) {
|
for (final attachment in attachments) {
|
||||||
messageInputController.addAttachment(attachment);
|
_effectiveController.addAttachment(attachment);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1476,7 +1496,6 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
}
|
}
|
||||||
final res = await FilePicker.platform.pickFiles(
|
final res = await FilePicker.platform.pickFiles(
|
||||||
type: type,
|
type: type,
|
||||||
withData: true,
|
|
||||||
);
|
);
|
||||||
if (res?.files.isNotEmpty == true) {
|
if (res?.files.isNotEmpty == true) {
|
||||||
file = res!.files.single.toAttachmentFile;
|
file = res!.files.single.toAttachmentFile;
|
||||||
@@ -1534,7 +1553,6 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_addAttachments([
|
_addAttachments([
|
||||||
attachment.copyWith(
|
attachment.copyWith(
|
||||||
file: file,
|
file: file,
|
||||||
@@ -1542,62 +1560,19 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
..update('file_size', ((_) => file!.size!)),
|
..update('file_size', ((_) => file!.size!)),
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sends the current message
|
/// Sends the current message
|
||||||
Future<void> sendMessage() async {
|
Future<void> sendMessage() async {
|
||||||
var text = messageInputController.text.trim();
|
var message = _effectiveController.value;
|
||||||
final attachments = messageInputController.attachments;
|
|
||||||
|
|
||||||
if (text.isEmpty && attachments.isEmpty) {
|
var shouldKeepFocus = widget.shouldKeepFocusAfterMessage;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final shouldUnfocus = _commandEnabled;
|
shouldKeepFocus ??= !_commandEnabled;
|
||||||
|
|
||||||
if (_commandEnabled) {
|
_effectiveController.reset();
|
||||||
text = '${'/${_chosenCommand!.name} '}$text';
|
|
||||||
}
|
|
||||||
|
|
||||||
messageInputController
|
|
||||||
..text = ''
|
|
||||||
..clearAttachments();
|
|
||||||
widget.onQuotedMessageCleared?.call();
|
widget.onQuotedMessageCleared?.call();
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_commandEnabled = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
Message message;
|
|
||||||
if (widget.editMessage != null) {
|
|
||||||
message = widget.editMessage!.copyWith(
|
|
||||||
text: text,
|
|
||||||
attachments: attachments,
|
|
||||||
mentionedUsers: messageInputController.mentionedUsers
|
|
||||||
.where((u) => text.contains('@${u.name}'))
|
|
||||||
.toList(),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
message = (widget.initialMessage ?? Message()).copyWith(
|
|
||||||
parentId: widget.parentMessage?.id,
|
|
||||||
text: text,
|
|
||||||
attachments: attachments,
|
|
||||||
mentionedUsers: messageInputController.mentionedUsers
|
|
||||||
.where((u) => text.contains('@${u.name}'))
|
|
||||||
.toList(),
|
|
||||||
showInChannel: widget.parentMessage != null
|
|
||||||
? messageInputController.showInChannel
|
|
||||||
: null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (widget.quotedMessage != null) {
|
|
||||||
message = message.copyWith(
|
|
||||||
quotedMessageId: widget.quotedMessage!.id,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (widget.preMessageSending != null) {
|
if (widget.preMessageSending != null) {
|
||||||
message = await widget.preMessageSending!(message);
|
message = await widget.preMessageSending!(message);
|
||||||
}
|
}
|
||||||
@@ -1608,25 +1583,23 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
await streamChannel.reloadChannel();
|
await streamChannel.reloadChannel();
|
||||||
}
|
}
|
||||||
|
|
||||||
messageInputController.clearMentionedUsers();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Future sendingFuture;
|
Future sendingFuture;
|
||||||
if (widget.editMessage == null ||
|
if (!_isEditing) {
|
||||||
widget.editMessage!.status == MessageSendingStatus.failed ||
|
|
||||||
widget.editMessage!.status == MessageSendingStatus.sending) {
|
|
||||||
sendingFuture = channel.sendMessage(message);
|
sendingFuture = channel.sendMessage(message);
|
||||||
} else {
|
} else {
|
||||||
sendingFuture = channel.updateMessage(message);
|
sendingFuture = channel.updateMessage(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!shouldUnfocus) {
|
if (shouldKeepFocus) {
|
||||||
FocusScope.of(context).requestFocus(_focusNode);
|
FocusScope.of(context).requestFocus(_focusNode);
|
||||||
|
} else {
|
||||||
|
FocusScope.of(context).unfocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
final resp = await sendingFuture;
|
final resp = await sendingFuture;
|
||||||
if (resp.message?.type == 'error') {
|
if (resp.message?.type == 'error') {
|
||||||
_parseExistingMessage(message);
|
_effectiveController.value = message;
|
||||||
}
|
}
|
||||||
_startSlowMode();
|
_startSlowMode();
|
||||||
widget.onMessageSent?.call(resp.message);
|
widget.onMessageSent?.call(resp.message);
|
||||||
@@ -1705,36 +1678,23 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _parseExistingMessage(Message message) {
|
|
||||||
final messageText = message.text;
|
|
||||||
if (messageText != null) messageInputController.text = messageText;
|
|
||||||
_addAttachments(message.attachments);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
messageInputController.textEditingController
|
_effectiveController.textEditingController
|
||||||
.removeListener(_onChangedDebounced);
|
.removeListener(_onChangedDebounced);
|
||||||
messageInputController.dispose();
|
_controller?.dispose();
|
||||||
_focusNode.removeListener(_focusNodeListener);
|
_focusNode.removeListener(_focusNodeListener);
|
||||||
_stopSlowMode();
|
_stopSlowMode();
|
||||||
_onChangedDebounced.cancel();
|
_onChangedDebounced.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _initialized = false;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didChangeDependencies() {
|
void didChangeDependencies() {
|
||||||
_streamChatTheme = StreamChatTheme.of(context);
|
_streamChatTheme = StreamChatTheme.of(context);
|
||||||
_messageInputTheme = MessageInputTheme.of(context);
|
_messageInputTheme = MessageInputTheme.of(context);
|
||||||
if (widget.editMessage == null && _timeOut <= 0) _startSlowMode();
|
if (!_isEditing && _timeOut <= 0) _startSlowMode();
|
||||||
|
|
||||||
if ((widget.editMessage != null || widget.initialMessage != null) &&
|
|
||||||
!_initialized) {
|
|
||||||
FocusScope.of(context).requestFocus(_focusNode);
|
|
||||||
_initialized = true;
|
|
||||||
}
|
|
||||||
super.didChangeDependencies();
|
super.didChangeDependencies();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+20
-11
@@ -21,30 +21,41 @@ typedef CustomAttachmentIconBuilder = Widget Function(
|
|||||||
bool active,
|
bool active,
|
||||||
);
|
);
|
||||||
|
|
||||||
///
|
/// A widget that allows to pick an attachment.
|
||||||
class StreamAttachmentPicker extends StatefulWidget {
|
class StreamAttachmentPicker extends StatefulWidget {
|
||||||
|
/// True if the picker is open.
|
||||||
final bool isOpen;
|
final bool isOpen;
|
||||||
|
|
||||||
|
/// The picker size in height.
|
||||||
final double pickerSize;
|
final double pickerSize;
|
||||||
|
|
||||||
|
/// The [MessageInputController] linked to this picker.
|
||||||
final MessageInputController messageInputController;
|
final MessageInputController messageInputController;
|
||||||
|
|
||||||
|
/// The limit of attachments that can be picked.
|
||||||
final int attachmentLimit;
|
final int attachmentLimit;
|
||||||
|
|
||||||
|
/// The callback for when the attachment limit is exceeded.
|
||||||
final AttachmentLimitExceedListener? onAttachmentLimitExceeded;
|
final AttachmentLimitExceedListener? onAttachmentLimitExceeded;
|
||||||
final ValueChanged<bool>? onChangeInputState;
|
|
||||||
final ValueChanged<String>? onError;
|
final ValueChanged<String>? onError;
|
||||||
final FilePickerCallback onFilePicked;
|
final FilePickerCallback onFilePicked;
|
||||||
|
|
||||||
/// Video quality to use when compressing the videos
|
/// Video quality to use when compressing the videos.
|
||||||
final VideoQuality compressedVideoQuality;
|
final VideoQuality compressedVideoQuality;
|
||||||
|
|
||||||
/// Frame rate to use when compressing the videos
|
/// Frame rate to use when compressing the videos.
|
||||||
final int compressedVideoFrameRate;
|
final int compressedVideoFrameRate;
|
||||||
|
|
||||||
/// Max attachment size in bytes
|
/// Max attachment size in bytes:
|
||||||
/// Defaults to 20 MB
|
/// - Defaults to 20 MB
|
||||||
/// do not set it if you're using our default CDN
|
/// - Do not set it if you're using our default CDN
|
||||||
final int maxAttachmentSize;
|
final int maxAttachmentSize;
|
||||||
|
|
||||||
|
/// The list of attachment types that can be picked.
|
||||||
final List<DefaultAttachmentTypes> allowedAttachmentTypes;
|
final List<DefaultAttachmentTypes> allowedAttachmentTypes;
|
||||||
|
|
||||||
|
/// The list of custom attachment types that can be picked.
|
||||||
final List<CustomAttachmentType> customAttachmentTypes;
|
final List<CustomAttachmentType> customAttachmentTypes;
|
||||||
|
|
||||||
const StreamAttachmentPicker({
|
const StreamAttachmentPicker({
|
||||||
@@ -58,7 +69,6 @@ class StreamAttachmentPicker extends StatefulWidget {
|
|||||||
this.maxAttachmentSize = 20971520,
|
this.maxAttachmentSize = 20971520,
|
||||||
this.compressedVideoQuality = VideoQuality.DefaultQuality,
|
this.compressedVideoQuality = VideoQuality.DefaultQuality,
|
||||||
this.compressedVideoFrameRate = 30,
|
this.compressedVideoFrameRate = 30,
|
||||||
this.onChangeInputState,
|
|
||||||
this.onError,
|
this.onError,
|
||||||
this.allowedAttachmentTypes = const [
|
this.allowedAttachmentTypes = const [
|
||||||
DefaultAttachmentTypes.image,
|
DefaultAttachmentTypes.image,
|
||||||
@@ -99,7 +109,6 @@ class StreamAttachmentPicker extends StatefulWidget {
|
|||||||
compressedVideoQuality ?? this.compressedVideoQuality,
|
compressedVideoQuality ?? this.compressedVideoQuality,
|
||||||
compressedVideoFrameRate:
|
compressedVideoFrameRate:
|
||||||
compressedVideoFrameRate ?? this.compressedVideoFrameRate,
|
compressedVideoFrameRate ?? this.compressedVideoFrameRate,
|
||||||
onChangeInputState: onChangeInputState ?? this.onChangeInputState,
|
|
||||||
onError: onError ?? this.onError,
|
onError: onError ?? this.onError,
|
||||||
allowedAttachmentTypes:
|
allowedAttachmentTypes:
|
||||||
allowedAttachmentTypes ?? this.allowedAttachmentTypes,
|
allowedAttachmentTypes ?? this.allowedAttachmentTypes,
|
||||||
@@ -116,8 +125,8 @@ class _StreamAttachmentPickerState extends State<StreamAttachmentPicker> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var _streamChatTheme = StreamChatTheme.of(context);
|
final _streamChatTheme = StreamChatTheme.of(context);
|
||||||
var messageInputController = widget.messageInputController;
|
final messageInputController = widget.messageInputController;
|
||||||
|
|
||||||
final _attachmentContainsImage =
|
final _attachmentContainsImage =
|
||||||
messageInputController.attachments.any((it) => it.type == 'image');
|
messageInputController.attachments.any((it) => it.type == 'image');
|
||||||
+28
-11
@@ -1,15 +1,11 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
|
/// A widget that displays a sending button.
|
||||||
class StreamMessageSendButton extends StatelessWidget {
|
class StreamMessageSendButton extends StatelessWidget {
|
||||||
final int timeOut;
|
/// Returns a [StreamMessageSendButton] with the given [timeOut], [isIdle],
|
||||||
final bool isIdle;
|
/// [isCommandEnabled], [isEditEnabled], [idleSendButton], [activeSendButton],
|
||||||
final bool isCommandEnabled;
|
/// [onSendMessage].
|
||||||
final bool isEditEnabled;
|
|
||||||
final Widget? idleSendButton;
|
|
||||||
final Widget? activeSendButton;
|
|
||||||
final VoidCallback onSendMessage;
|
|
||||||
|
|
||||||
const StreamMessageSendButton({
|
const StreamMessageSendButton({
|
||||||
Key? key,
|
Key? key,
|
||||||
this.timeOut = 0,
|
this.timeOut = 0,
|
||||||
@@ -21,9 +17,30 @@ class StreamMessageSendButton extends StatelessWidget {
|
|||||||
required this.onSendMessage,
|
required this.onSendMessage,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// Time out related to slow mode.
|
||||||
|
final int timeOut;
|
||||||
|
|
||||||
|
/// If true the button will be disabled.
|
||||||
|
final bool isIdle;
|
||||||
|
|
||||||
|
/// True if a command is being sent.
|
||||||
|
final bool isCommandEnabled;
|
||||||
|
|
||||||
|
/// True if in editing mode.
|
||||||
|
final bool isEditEnabled;
|
||||||
|
|
||||||
|
/// The widget to display when the button is disabled.
|
||||||
|
final Widget? idleSendButton;
|
||||||
|
|
||||||
|
/// The widget to display when the button is enabled.
|
||||||
|
final Widget? activeSendButton;
|
||||||
|
|
||||||
|
/// The callback to call when the button is pressed.
|
||||||
|
final VoidCallback onSendMessage;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var _streamChatTheme = StreamChatTheme.of(context);
|
final _streamChatTheme = StreamChatTheme.of(context);
|
||||||
|
|
||||||
late Widget sendButton;
|
late Widget sendButton;
|
||||||
if (timeOut > 0) {
|
if (timeOut > 0) {
|
||||||
@@ -46,7 +63,7 @@ class StreamMessageSendButton extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildIdleSendButton(BuildContext context) {
|
Widget _buildIdleSendButton(BuildContext context) {
|
||||||
var _messageInputTheme = MessageInputTheme.of(context);
|
final _messageInputTheme = MessageInputTheme.of(context);
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
@@ -58,7 +75,7 @@ class StreamMessageSendButton extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSendButton(BuildContext context) {
|
Widget _buildSendButton(BuildContext context) {
|
||||||
var _messageInputTheme = MessageInputTheme.of(context);
|
final _messageInputTheme = MessageInputTheme.of(context);
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
+37
-48
@@ -1,3 +1,5 @@
|
|||||||
|
// ignore_for_file: prefer-trailing-comma, cascade_invocations
|
||||||
|
|
||||||
import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle;
|
import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle;
|
||||||
|
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
@@ -16,11 +18,12 @@ export 'package:flutter/services.dart'
|
|||||||
SmartQuotesType,
|
SmartQuotesType,
|
||||||
SmartDashesType;
|
SmartDashesType;
|
||||||
|
|
||||||
|
/// A widget the wraps the [TextField] and adds some StreamChat specifics.
|
||||||
class StreamMessageTextField extends StatefulWidget {
|
class StreamMessageTextField extends StatefulWidget {
|
||||||
/// Creates a Material Design text field.
|
/// Creates a Material Design text field.
|
||||||
///
|
///
|
||||||
/// If [decoration] is non-null (which is the default), the text field requires
|
/// If [decoration] is non-null (which is the default), the text field
|
||||||
/// one of its ancestors to be a [Material] widget.
|
/// requires one of its ancestors to be a [Material] widget.
|
||||||
///
|
///
|
||||||
/// To remove the decoration entirely (including the extra padding introduced
|
/// To remove the decoration entirely (including the extra padding introduced
|
||||||
/// by the decoration to save space for the labels), set the [decoration] to
|
/// by the decoration to save space for the labels), set the [decoration] to
|
||||||
@@ -45,10 +48,6 @@ class StreamMessageTextField extends StatefulWidget {
|
|||||||
/// which is evaluated after the supplied [inputFormatters], if any.
|
/// which is evaluated after the supplied [inputFormatters], if any.
|
||||||
/// The [maxLength] value must be either null or greater than zero.
|
/// The [maxLength] value must be either null or greater than zero.
|
||||||
///
|
///
|
||||||
/// If [maxLengthEnforced] is set to false, then more than [maxLength]
|
|
||||||
/// characters may be entered, and the error counter and divider will
|
|
||||||
/// switch to the [decoration].errorStyle when the limit is exceeded.
|
|
||||||
///
|
|
||||||
/// The text cursor is not shown if [showCursor] is false or if [showCursor]
|
/// The text cursor is not shown if [showCursor] is false or if [showCursor]
|
||||||
/// is null (the default) and [readOnly] is true.
|
/// is null (the default) and [readOnly] is true.
|
||||||
///
|
///
|
||||||
@@ -58,7 +57,7 @@ class StreamMessageTextField extends StatefulWidget {
|
|||||||
/// must not be null.
|
/// must not be null.
|
||||||
///
|
///
|
||||||
/// The [textAlign], [autofocus], [obscureText], [readOnly], [autocorrect],
|
/// The [textAlign], [autofocus], [obscureText], [readOnly], [autocorrect],
|
||||||
/// [maxLengthEnforced], [scrollPadding], [maxLines], [maxLength],
|
/// [scrollPadding], [maxLines], [maxLength],
|
||||||
/// [selectionHeightStyle], [selectionWidthStyle], [enableSuggestions], and
|
/// [selectionHeightStyle], [selectionWidthStyle], [enableSuggestions], and
|
||||||
/// [enableIMEPersonalizedLearning] arguments must not be null.
|
/// [enableIMEPersonalizedLearning] arguments must not be null.
|
||||||
///
|
///
|
||||||
@@ -113,7 +112,7 @@ class StreamMessageTextField extends StatefulWidget {
|
|||||||
this.selectionHeightStyle = ui.BoxHeightStyle.tight,
|
this.selectionHeightStyle = ui.BoxHeightStyle.tight,
|
||||||
this.selectionWidthStyle = ui.BoxWidthStyle.tight,
|
this.selectionWidthStyle = ui.BoxWidthStyle.tight,
|
||||||
this.keyboardAppearance,
|
this.keyboardAppearance,
|
||||||
this.scrollPadding = const EdgeInsets.all(20.0),
|
this.scrollPadding = const EdgeInsets.all(20),
|
||||||
this.dragStartBehavior = DragStartBehavior.start,
|
this.dragStartBehavior = DragStartBehavior.start,
|
||||||
this.enableInteractiveSelection = true,
|
this.enableInteractiveSelection = true,
|
||||||
this.selectionControls,
|
this.selectionControls,
|
||||||
@@ -125,51 +124,44 @@ class StreamMessageTextField extends StatefulWidget {
|
|||||||
this.autofillHints,
|
this.autofillHints,
|
||||||
this.restorationId,
|
this.restorationId,
|
||||||
this.enableIMEPersonalizedLearning = true,
|
this.enableIMEPersonalizedLearning = true,
|
||||||
}) : assert(textAlign != null),
|
}) : assert(obscuringCharacter.length == 1,
|
||||||
assert(readOnly != null),
|
'`obscuringCharacter.length` must be 1'),
|
||||||
assert(autofocus != null),
|
|
||||||
assert(obscuringCharacter != null && obscuringCharacter.length == 1),
|
|
||||||
assert(obscureText != null),
|
|
||||||
assert(autocorrect != null),
|
|
||||||
smartDashesType = smartDashesType ??
|
smartDashesType = smartDashesType ??
|
||||||
(obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
|
(obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
|
||||||
smartQuotesType = smartQuotesType ??
|
smartQuotesType = smartQuotesType ??
|
||||||
(obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
|
(obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
|
||||||
assert(enableSuggestions != null),
|
|
||||||
assert(enableInteractiveSelection != null),
|
|
||||||
assert(maxLengthEnforced != null),
|
|
||||||
assert(
|
assert(
|
||||||
maxLengthEnforced || maxLengthEnforcement == null,
|
maxLengthEnforced || maxLengthEnforcement == null,
|
||||||
'maxLengthEnforced is deprecated, use only maxLengthEnforcement',
|
'maxLengthEnforced is deprecated, use only maxLengthEnforcement',
|
||||||
),
|
),
|
||||||
assert(scrollPadding != null),
|
assert(maxLines == null || maxLines > 0,
|
||||||
assert(dragStartBehavior != null),
|
'`maxLines` needs to be left as null or bigger than 0'),
|
||||||
assert(selectionHeightStyle != null),
|
assert(minLines == null || minLines > 0,
|
||||||
assert(selectionWidthStyle != null),
|
'`minLines` needs to be left as null or bigger than 0'),
|
||||||
assert(maxLines == null || maxLines > 0),
|
|
||||||
assert(minLines == null || minLines > 0),
|
|
||||||
assert(
|
assert(
|
||||||
(maxLines == null) || (minLines == null) || (maxLines >= minLines),
|
(maxLines == null) || (minLines == null) || (maxLines >= minLines),
|
||||||
"minLines can't be greater than maxLines",
|
"minLines can't be greater than maxLines",
|
||||||
),
|
),
|
||||||
assert(expands != null),
|
|
||||||
assert(
|
assert(
|
||||||
!expands || (maxLines == null && minLines == null),
|
!expands || (maxLines == null && minLines == null),
|
||||||
'minLines and maxLines must be null when expands is true.',
|
'minLines and maxLines must be null when expands is true.',
|
||||||
),
|
),
|
||||||
assert(!obscureText || maxLines == 1,
|
assert(!obscureText || maxLines == 1,
|
||||||
'Obscured fields cannot be multiline.'),
|
'Obscured fields cannot be multiline.'),
|
||||||
assert(maxLength == null ||
|
assert(
|
||||||
|
maxLength == null ||
|
||||||
maxLength == TextField.noMaxLength ||
|
maxLength == TextField.noMaxLength ||
|
||||||
maxLength > 0),
|
maxLength > 0,
|
||||||
// Assert the following instead of setting it directly to avoid surprising the user by silently changing the value they set.
|
'`maxLength` needs to be null or a positive integer'),
|
||||||
|
|
||||||
|
// Assert the following instead of setting it directly to avoid
|
||||||
|
// surprising the user by silently changing the value they set.
|
||||||
assert(
|
assert(
|
||||||
!identical(textInputAction, TextInputAction.newline) ||
|
!identical(textInputAction, TextInputAction.newline) ||
|
||||||
maxLines == 1 ||
|
maxLines == 1 ||
|
||||||
!identical(keyboardType, TextInputType.text),
|
!identical(keyboardType, TextInputType.text),
|
||||||
'Use keyboardType TextInputType.multiline when using TextInputAction.newline on a multiline TextField.',
|
'''Use keyboardType TextInputType.multiline when using TextInputAction.newline on a multiline TextField.''',
|
||||||
),
|
),
|
||||||
assert(enableIMEPersonalizedLearning != null),
|
|
||||||
keyboardType = keyboardType ??
|
keyboardType = keyboardType ??
|
||||||
(maxLines == 1 ? TextInputType.text : TextInputType.multiline),
|
(maxLines == 1 ? TextInputType.text : TextInputType.multiline),
|
||||||
toolbarOptions = toolbarOptions ??
|
toolbarOptions = toolbarOptions ??
|
||||||
@@ -228,7 +220,8 @@ class StreamMessageTextField extends StatefulWidget {
|
|||||||
/// cause the focus to change, and will not make the keyboard visible.
|
/// cause the focus to change, and will not make the keyboard visible.
|
||||||
///
|
///
|
||||||
/// This widget builds an [EditableText] and will ensure that the keyboard is
|
/// This widget builds an [EditableText] and will ensure that the keyboard is
|
||||||
/// showing when it is tapped by calling [EditableTextState.requestKeyboard()].
|
/// showing when it is tapped by calling
|
||||||
|
/// [EditableTextState.requestKeyboard()].
|
||||||
final FocusNode? focusNode;
|
final FocusNode? focusNode;
|
||||||
|
|
||||||
/// The decoration to show around the text field.
|
/// The decoration to show around the text field.
|
||||||
@@ -328,16 +321,20 @@ class StreamMessageTextField extends StatefulWidget {
|
|||||||
/// If set, a character counter will be displayed below the
|
/// If set, a character counter will be displayed below the
|
||||||
/// field showing how many characters have been entered. If set to a number
|
/// field showing how many characters have been entered. If set to a number
|
||||||
/// greater than 0, it will also display the maximum number allowed. If set
|
/// greater than 0, it will also display the maximum number allowed. If set
|
||||||
/// to [TextField.noMaxLength] then only the current character count is displayed.
|
/// to [TextField.noMaxLength] then only the current character count is
|
||||||
|
/// displayed.
|
||||||
///
|
///
|
||||||
/// After [maxLength] characters have been input, additional input
|
/// After [maxLength] characters have been input, additional input
|
||||||
/// is ignored, unless [maxLengthEnforcement] is set to
|
/// is ignored, unless [maxLengthEnforcement] is set to
|
||||||
/// [MaxLengthEnforcement.none].
|
/// [MaxLengthEnforcement.none].
|
||||||
///
|
///
|
||||||
/// The text field enforces the length with a [LengthLimitingTextInputFormatter],
|
/// The text field enforces the length with a
|
||||||
/// which is evaluated after the supplied [inputFormatters], if any.
|
/// [LengthLimitingTextInputFormatter], which is evaluated after the supplied
|
||||||
|
/// [inputFormatters], if any.
|
||||||
|
///
|
||||||
|
/// This value must be either null, [TextField.noMaxLength], or greater than
|
||||||
|
/// 0.
|
||||||
///
|
///
|
||||||
/// This value must be either null, [TextField.noMaxLength], or greater than 0.
|
|
||||||
/// If null (the default) then there is no limit to the number of characters
|
/// If null (the default) then there is no limit to the number of characters
|
||||||
/// that can be entered. If set to [TextField.noMaxLength], then no limit will
|
/// that can be entered. If set to [TextField.noMaxLength], then no limit will
|
||||||
/// be enforced, but the number of characters entered will still be displayed.
|
/// be enforced, but the number of characters entered will still be displayed.
|
||||||
@@ -345,12 +342,6 @@ class StreamMessageTextField extends StatefulWidget {
|
|||||||
/// Whitespace characters (e.g. newline, space, tab) are included in the
|
/// Whitespace characters (e.g. newline, space, tab) are included in the
|
||||||
/// character count.
|
/// character count.
|
||||||
///
|
///
|
||||||
/// If [maxLengthEnforced] is set to false or [maxLengthEnforcement] is
|
|
||||||
/// [MaxLengthEnforcement.none], then more than [maxLength]
|
|
||||||
/// characters may be entered, but the error counter and divider will switch
|
|
||||||
/// to the [decoration]'s [InputDecoration.errorStyle] when the limit is
|
|
||||||
/// exceeded.
|
|
||||||
///
|
|
||||||
/// {@macro flutter.services.lengthLimitingTextInputFormatter.maxLength}
|
/// {@macro flutter.services.lengthLimitingTextInputFormatter.maxLength}
|
||||||
final int? maxLength;
|
final int? maxLength;
|
||||||
|
|
||||||
@@ -444,7 +435,8 @@ class StreamMessageTextField extends StatefulWidget {
|
|||||||
///
|
///
|
||||||
/// This setting is only honored on iOS devices.
|
/// This setting is only honored on iOS devices.
|
||||||
///
|
///
|
||||||
/// If unset, defaults to the brightness of [ThemeData.primaryColorBrightness].
|
/// If unset, defaults to the brightness of
|
||||||
|
/// [ThemeData.primaryColorBrightness].
|
||||||
final Brightness? keyboardAppearance;
|
final Brightness? keyboardAppearance;
|
||||||
|
|
||||||
/// {@macro flutter.widgets.editableText.scrollPadding}
|
/// {@macro flutter.widgets.editableText.scrollPadding}
|
||||||
@@ -488,14 +480,16 @@ class StreamMessageTextField extends StatefulWidget {
|
|||||||
/// widget.
|
/// widget.
|
||||||
///
|
///
|
||||||
/// If [mouseCursor] is a [MaterialStateProperty<MouseCursor>],
|
/// If [mouseCursor] is a [MaterialStateProperty<MouseCursor>],
|
||||||
/// [MaterialStateProperty.resolve] is used for the following [MaterialState]s:
|
/// [MaterialStateProperty.resolve] is used for the following
|
||||||
|
/// [MaterialState]s:
|
||||||
///
|
///
|
||||||
/// * [MaterialState.error].
|
/// * [MaterialState.error].
|
||||||
/// * [MaterialState.hovered].
|
/// * [MaterialState.hovered].
|
||||||
/// * [MaterialState.focused].
|
/// * [MaterialState.focused].
|
||||||
/// * [MaterialState.disabled].
|
/// * [MaterialState.disabled].
|
||||||
///
|
///
|
||||||
/// If this property is null, [MaterialStateMouseCursor.textable] will be used.
|
/// If this property is null, [MaterialStateMouseCursor.textable] will be
|
||||||
|
/// used.
|
||||||
///
|
///
|
||||||
/// The [mouseCursor] is the only property of [TextField] that controls the
|
/// The [mouseCursor] is the only property of [TextField] that controls the
|
||||||
/// appearance of the mouse pointer. All other properties related to "cursor"
|
/// appearance of the mouse pointer. All other properties related to "cursor"
|
||||||
@@ -610,10 +604,6 @@ class StreamMessageTextField extends StatefulWidget {
|
|||||||
properties.add(
|
properties.add(
|
||||||
DiagnosticsProperty<bool>('expands', expands, defaultValue: false));
|
DiagnosticsProperty<bool>('expands', expands, defaultValue: false));
|
||||||
properties.add(IntProperty('maxLength', maxLength, defaultValue: null));
|
properties.add(IntProperty('maxLength', maxLength, defaultValue: null));
|
||||||
properties.add(FlagProperty('maxLengthEnforced',
|
|
||||||
value: maxLengthEnforced,
|
|
||||||
defaultValue: true,
|
|
||||||
ifFalse: 'maxLength not enforced'));
|
|
||||||
properties.add(EnumProperty<MaxLengthEnforcement>(
|
properties.add(EnumProperty<MaxLengthEnforcement>(
|
||||||
'maxLengthEnforcement', maxLengthEnforcement,
|
'maxLengthEnforcement', maxLengthEnforcement,
|
||||||
defaultValue: null));
|
defaultValue: null));
|
||||||
@@ -741,7 +731,6 @@ class _StreamMessageTextFieldState extends State<StreamMessageTextField>
|
|||||||
minLines: widget.minLines,
|
minLines: widget.minLines,
|
||||||
expands: widget.expands,
|
expands: widget.expands,
|
||||||
maxLength: widget.maxLength,
|
maxLength: widget.maxLength,
|
||||||
maxLengthEnforced: widget.maxLengthEnforced,
|
|
||||||
maxLengthEnforcement: widget.maxLengthEnforcement,
|
maxLengthEnforcement: widget.maxLengthEnforcement,
|
||||||
onEditingComplete: widget.onEditingComplete,
|
onEditingComplete: widget.onEditingComplete,
|
||||||
onSubmitted: widget.onSubmitted,
|
onSubmitted: widget.onSubmitted,
|
||||||
@@ -58,6 +58,46 @@ typedef OnMessageTap = void Function(Message);
|
|||||||
/// Callback on reply tapped
|
/// Callback on reply tapped
|
||||||
typedef ReplyTapCallback = void Function(Message);
|
typedef ReplyTapCallback = void Function(Message);
|
||||||
|
|
||||||
|
/// Spacing Types (These are properties of a message to help inform the decision
|
||||||
|
/// of how much space / which widget to build after it)
|
||||||
|
enum SpacingType {
|
||||||
|
/// Message is a thread
|
||||||
|
thread,
|
||||||
|
|
||||||
|
/// There is a >1s time diff between current and last message
|
||||||
|
timeDiff,
|
||||||
|
|
||||||
|
/// Next message is by a different user
|
||||||
|
otherUser,
|
||||||
|
|
||||||
|
/// Message is deleted
|
||||||
|
deleted,
|
||||||
|
|
||||||
|
/// No other conditions are valid, default spacing (This will likely be the
|
||||||
|
/// only rule in the list provided)
|
||||||
|
defaultSpacing,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builder for building certain spacing after widgets.
|
||||||
|
/// This spacing can be in form of any widgets you like.
|
||||||
|
/// A List of [SpacingType] is provided to help inform the decision of
|
||||||
|
/// what to build after the message.
|
||||||
|
///
|
||||||
|
/// As an example:
|
||||||
|
/// MessageListView(
|
||||||
|
/// spacingWidgetBuilder: (context, list) {
|
||||||
|
/// if(list.contains(SpacingType.defaultSpacing)) {
|
||||||
|
/// return SizedBox(height: 2.0,);
|
||||||
|
/// } else {
|
||||||
|
/// return SizedBox(height: 8.0,);
|
||||||
|
/// }
|
||||||
|
/// },
|
||||||
|
/// ),
|
||||||
|
typedef SpacingWidgetBuilder = Widget Function(
|
||||||
|
BuildContext context,
|
||||||
|
List<SpacingType> spacingTypes,
|
||||||
|
);
|
||||||
|
|
||||||
/// Class for message details
|
/// Class for message details
|
||||||
// ignore: prefer-match-file-name
|
// ignore: prefer-match-file-name
|
||||||
class MessageDetails {
|
class MessageDetails {
|
||||||
@@ -171,8 +211,14 @@ class MessageListView extends StatefulWidget {
|
|||||||
this.reverse = true,
|
this.reverse = true,
|
||||||
this.paginationLimit = 20,
|
this.paginationLimit = 20,
|
||||||
this.paginationLoadingIndicatorBuilder,
|
this.paginationLoadingIndicatorBuilder,
|
||||||
|
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.onDrag,
|
||||||
|
this.spacingWidgetBuilder,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// [ScrollViewKeyboardDismissBehavior] the defines how this [PositionedList] will
|
||||||
|
/// dismiss the keyboard automatically.
|
||||||
|
final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior;
|
||||||
|
|
||||||
/// Function used to build a custom message widget
|
/// Function used to build a custom message widget
|
||||||
final MessageBuilder? messageBuilder;
|
final MessageBuilder? messageBuilder;
|
||||||
|
|
||||||
@@ -289,6 +335,12 @@ class MessageListView extends StatefulWidget {
|
|||||||
/// Builder used to build the loading indicator shown while paginating.
|
/// Builder used to build the loading indicator shown while paginating.
|
||||||
final WidgetBuilder? paginationLoadingIndicatorBuilder;
|
final WidgetBuilder? paginationLoadingIndicatorBuilder;
|
||||||
|
|
||||||
|
/// This allows a user to customise the space after a message
|
||||||
|
/// A List of [SpacingType] is provided to provide more data about the
|
||||||
|
/// type of message (thread, difference in time between current and last
|
||||||
|
/// message, default spacing, etc)
|
||||||
|
final SpacingWidgetBuilder? spacingWidgetBuilder;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_MessageListViewState createState() => _MessageListViewState();
|
_MessageListViewState createState() => _MessageListViewState();
|
||||||
}
|
}
|
||||||
@@ -443,9 +495,6 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
childAnchor: Alignment.topCenter,
|
childAnchor: Alignment.topCenter,
|
||||||
message: statusString,
|
message: statusString,
|
||||||
child: LazyLoadScrollView(
|
child: LazyLoadScrollView(
|
||||||
onPageScrollStart: () {
|
|
||||||
FocusScope.of(context).unfocus();
|
|
||||||
},
|
|
||||||
onStartOfPage: () async {
|
onStartOfPage: () async {
|
||||||
_inBetweenList = false;
|
_inBetweenList = false;
|
||||||
if (!_upToDate) {
|
if (!_upToDate) {
|
||||||
@@ -471,6 +520,7 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
key: (initialIndex != 0 && initialAlignment != 0)
|
key: (initialIndex != 0 && initialAlignment != 0)
|
||||||
? ValueKey('$initialIndex-$initialAlignment')
|
? ValueKey('$initialIndex-$initialAlignment')
|
||||||
: null,
|
: null,
|
||||||
|
keyboardDismissBehavior: widget.keyboardDismissBehavior,
|
||||||
itemPositionsListener: _itemPositionListener,
|
itemPositionsListener: _itemPositionListener,
|
||||||
initialScrollIndex: initialIndex,
|
initialScrollIndex: initialIndex,
|
||||||
initialAlignment: initialAlignment,
|
initialAlignment: initialAlignment,
|
||||||
@@ -564,17 +614,38 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
Units.MINUTE,
|
Units.MINUTE,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final spacingRules = <SpacingType>[];
|
||||||
|
|
||||||
final isNextUserSame =
|
final isNextUserSame =
|
||||||
message.user!.id == nextMessage.user?.id;
|
message.user!.id == nextMessage.user?.id;
|
||||||
final isThread = message.replyCount! > 0;
|
final isThread = message.replyCount! > 0;
|
||||||
final isDeleted = message.isDeleted;
|
final isDeleted = message.isDeleted;
|
||||||
if (timeDiff >= 1 ||
|
final hasTimeDiff = timeDiff >= 1;
|
||||||
!isNextUserSame ||
|
|
||||||
isThread ||
|
if (hasTimeDiff) {
|
||||||
isDeleted) {
|
spacingRules.add(SpacingType.timeDiff);
|
||||||
return const SizedBox(height: 8);
|
|
||||||
}
|
}
|
||||||
return const SizedBox(height: 2);
|
|
||||||
|
if (!isNextUserSame) {
|
||||||
|
spacingRules.add(SpacingType.otherUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isThread) {
|
||||||
|
spacingRules.add(SpacingType.thread);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDeleted) {
|
||||||
|
spacingRules.add(SpacingType.deleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spacingRules.isNotEmpty) {
|
||||||
|
return widget.spacingWidgetBuilder
|
||||||
|
?.call(context, spacingRules) ??
|
||||||
|
const SizedBox(height: 8);
|
||||||
|
}
|
||||||
|
return widget.spacingWidgetBuilder
|
||||||
|
?.call(context, [SpacingType.defaultSpacing]) ??
|
||||||
|
const SizedBox(height: 2);
|
||||||
},
|
},
|
||||||
itemBuilder: (context, i) {
|
itemBuilder: (context, i) {
|
||||||
if (i == itemCount - 1) {
|
if (i == itemCount - 1) {
|
||||||
@@ -1003,15 +1074,6 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final channel = streamChannel!.channel;
|
|
||||||
final readList = channel.state?.read.where((read) {
|
|
||||||
if (read.user.id == userId) return false;
|
|
||||||
return read.lastRead.isAfter(message.createdAt) ||
|
|
||||||
read.lastRead.isAtSameMomentAs(message.createdAt);
|
|
||||||
}).toList() ??
|
|
||||||
[];
|
|
||||||
|
|
||||||
final allRead = readList.length >= (channel.memberCount ?? 0) - 1;
|
|
||||||
final hasFileAttachment =
|
final hasFileAttachment =
|
||||||
message.attachments.any((it) => it.type == 'file');
|
message.attachments.any((it) => it.type == 'file');
|
||||||
|
|
||||||
@@ -1140,8 +1202,6 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
messageTheme: isMyMessage
|
messageTheme: isMyMessage
|
||||||
? _streamTheme.ownMessageTheme
|
? _streamTheme.ownMessageTheme
|
||||||
: _streamTheme.otherMessageTheme,
|
: _streamTheme.otherMessageTheme,
|
||||||
readList: readList,
|
|
||||||
allRead: allRead,
|
|
||||||
onReturnAction: (action) {
|
onReturnAction: (action) {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case ReturnActionType.none:
|
case ReturnActionType.none:
|
||||||
|
|||||||
@@ -97,14 +97,20 @@ class MessageWidget extends StatefulWidget {
|
|||||||
this.deletedBottomRowBuilder,
|
this.deletedBottomRowBuilder,
|
||||||
this.onReturnAction,
|
this.onReturnAction,
|
||||||
this.customAttachmentBuilders,
|
this.customAttachmentBuilders,
|
||||||
this.readList,
|
|
||||||
this.padding,
|
this.padding,
|
||||||
this.textPadding = const EdgeInsets.symmetric(
|
this.textPadding = const EdgeInsets.symmetric(
|
||||||
horizontal: 16,
|
horizontal: 16,
|
||||||
vertical: 8,
|
vertical: 8,
|
||||||
),
|
),
|
||||||
this.attachmentPadding = EdgeInsets.zero,
|
this.attachmentPadding = EdgeInsets.zero,
|
||||||
this.allRead = false,
|
@Deprecated('''
|
||||||
|
allRead is now deprecated and it will be removed in future releases.
|
||||||
|
The MessageWidget now listens for read events on its own.
|
||||||
|
''') this.allRead = false,
|
||||||
|
@Deprecated('''
|
||||||
|
readList is now deprecated and it will be removed in future releases.
|
||||||
|
The MessageWidget now listens for read events on its own.
|
||||||
|
''') this.readList,
|
||||||
this.onQuotedMessageTap,
|
this.onQuotedMessageTap,
|
||||||
this.customActions = const [],
|
this.customActions = const [],
|
||||||
this.onAttachmentTap,
|
this.onAttachmentTap,
|
||||||
@@ -508,7 +514,6 @@ class MessageWidget extends StatefulWidget {
|
|||||||
showUserAvatar: showUserAvatar ?? this.showUserAvatar,
|
showUserAvatar: showUserAvatar ?? this.showUserAvatar,
|
||||||
showSendingIndicator: showSendingIndicator ?? this.showSendingIndicator,
|
showSendingIndicator: showSendingIndicator ?? this.showSendingIndicator,
|
||||||
showReactions: showReactions ?? this.showReactions,
|
showReactions: showReactions ?? this.showReactions,
|
||||||
allRead: allRead ?? this.allRead,
|
|
||||||
showThreadReplyIndicator:
|
showThreadReplyIndicator:
|
||||||
showThreadReplyIndicator ?? this.showThreadReplyIndicator,
|
showThreadReplyIndicator ?? this.showThreadReplyIndicator,
|
||||||
showInChannelIndicator:
|
showInChannelIndicator:
|
||||||
@@ -517,7 +522,6 @@ class MessageWidget extends StatefulWidget {
|
|||||||
onLinkTap: onLinkTap ?? this.onLinkTap,
|
onLinkTap: onLinkTap ?? this.onLinkTap,
|
||||||
showReactionPickerIndicator:
|
showReactionPickerIndicator:
|
||||||
showReactionPickerIndicator ?? this.showReactionPickerIndicator,
|
showReactionPickerIndicator ?? this.showReactionPickerIndicator,
|
||||||
readList: readList ?? this.readList,
|
|
||||||
onShowMessage: onShowMessage ?? this.onShowMessage,
|
onShowMessage: onShowMessage ?? this.onShowMessage,
|
||||||
onReturnAction: onReturnAction ?? this.onReturnAction,
|
onReturnAction: onReturnAction ?? this.onReturnAction,
|
||||||
showUsername: showUsername ?? this.showUsername,
|
showUsername: showUsername ?? this.showUsername,
|
||||||
@@ -558,8 +562,6 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
|
|
||||||
bool get showTimeStamp => widget.showTimestamp;
|
bool get showTimeStamp => widget.showTimestamp;
|
||||||
|
|
||||||
bool get isMessageRead => widget.readList?.isNotEmpty == true;
|
|
||||||
|
|
||||||
bool get showInChannel => widget.showInChannelIndicator;
|
bool get showInChannel => widget.showInChannelIndicator;
|
||||||
|
|
||||||
bool get hasQuotedMessage => widget.message.quotedMessage != null;
|
bool get hasQuotedMessage => widget.message.quotedMessage != null;
|
||||||
@@ -830,8 +832,8 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
),
|
),
|
||||||
if (isFailedState)
|
if (isFailedState)
|
||||||
Positioned(
|
Positioned(
|
||||||
left: widget.reverse ? 0 : null,
|
right: widget.reverse ? 0 : null,
|
||||||
right: widget.reverse ? null : 0,
|
left: widget.reverse ? null : 0,
|
||||||
bottom: showBottomRow ? 18 : -2,
|
bottom: showBottomRow ? 18 : -2,
|
||||||
child: StreamSvgIcon.error(size: 20),
|
child: StreamSvgIcon.error(size: 20),
|
||||||
),
|
),
|
||||||
@@ -1230,6 +1232,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
Widget _buildSendingIndicator() {
|
Widget _buildSendingIndicator() {
|
||||||
final style = widget.messageTheme.createdAtStyle;
|
final style = widget.messageTheme.createdAtStyle;
|
||||||
final message = widget.message;
|
final message = widget.message;
|
||||||
|
final memberCount = StreamChannel.of(context).channel.memberCount ?? 0;
|
||||||
|
|
||||||
if (hasNonUrlAttachments &&
|
if (hasNonUrlAttachments &&
|
||||||
(message.status == MessageSendingStatus.sending ||
|
(message.status == MessageSendingStatus.sending ||
|
||||||
@@ -1252,6 +1255,17 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final channel = StreamChannel.of(context).channel;
|
||||||
|
|
||||||
|
return BetterStreamBuilder<List<Read>>(
|
||||||
|
stream: channel.state?.readStream,
|
||||||
|
initialData: channel.state?.read,
|
||||||
|
builder: (context, data) {
|
||||||
|
final readList = data.where((it) =>
|
||||||
|
it.user.id != _streamChat.currentUser?.id &&
|
||||||
|
(it.lastRead.isAfter(message.createdAt) ||
|
||||||
|
it.lastRead.isAtSameMomentAs(message.createdAt)));
|
||||||
|
final isMessageRead = readList.length >= (channel.memberCount ?? 0) - 1;
|
||||||
Widget child = SendingIndicator(
|
Widget child = SendingIndicator(
|
||||||
message: message,
|
message: message,
|
||||||
isMessageRead: isMessageRead,
|
isMessageRead: isMessageRead,
|
||||||
@@ -1260,9 +1274,9 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
if (isMessageRead) {
|
if (isMessageRead) {
|
||||||
child = Row(
|
child = Row(
|
||||||
children: [
|
children: [
|
||||||
if (StreamChannel.of(context).channel.memberCount! > 2)
|
if (memberCount > 2)
|
||||||
Text(
|
Text(
|
||||||
widget.readList!.length.toString(),
|
readList.length.toString(),
|
||||||
style: style.copyWith(
|
style: style.copyWith(
|
||||||
color: _streamChatTheme.colorTheme.accentPrimary,
|
color: _streamChatTheme.colorTheme.accentPrimary,
|
||||||
),
|
),
|
||||||
@@ -1273,6 +1287,8 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return child;
|
return child;
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildUserAvatar() => Transform.translate(
|
Widget _buildUserAvatar() => Transform.translate(
|
||||||
|
|||||||
@@ -110,7 +110,15 @@ class StreamChatState extends State<StreamChat> {
|
|||||||
onBackgroundEventReceived: widget.onBackgroundEventReceived,
|
onBackgroundEventReceived: widget.onBackgroundEventReceived,
|
||||||
backgroundKeepAlive: widget.backgroundKeepAlive,
|
backgroundKeepAlive: widget.backgroundKeepAlive,
|
||||||
connectivityStream: widget.connectivityStream,
|
connectivityStream: widget.connectivityStream,
|
||||||
child: widget.child ?? const Offstage(),
|
child: Builder(
|
||||||
|
builder: (context) {
|
||||||
|
StreamChatClient.additionalHeaders = {
|
||||||
|
'X-Stream-Client':
|
||||||
|
'${StreamChatClient.defaultUserAgent}-ui',
|
||||||
|
};
|
||||||
|
return widget.child ?? const Offstage();
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:flutter/material.dart' hide TextTheme;
|
import 'package:flutter/material.dart' hide TextTheme;
|
||||||
import 'package:stream_chat_flutter/src/channel_preview.dart';
|
import 'package:stream_chat_flutter/src/channel_preview.dart';
|
||||||
import 'package:stream_chat_flutter/src/gradient_avatar.dart';
|
import 'package:stream_chat_flutter/src/gradient_avatar.dart';
|
||||||
import 'package:stream_chat_flutter/src/message_input.dart';
|
import 'package:stream_chat_flutter/src/message_input/message_input.dart';
|
||||||
import 'package:stream_chat_flutter/src/reaction_icon.dart';
|
import 'package:stream_chat_flutter/src/reaction_icon.dart';
|
||||||
import 'package:stream_chat_flutter/src/theme/themes.dart';
|
import 'package:stream_chat_flutter/src/theme/themes.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export 'package:jiffy/jiffy.dart';
|
|||||||
export 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
export 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
|
|
||||||
export 'src/attachment/attachment.dart';
|
export 'src/attachment/attachment.dart';
|
||||||
|
export 'src/attachment_actions_modal.dart';
|
||||||
export 'src/back_button.dart';
|
export 'src/back_button.dart';
|
||||||
export 'src/channel_avatar.dart';
|
export 'src/channel_avatar.dart';
|
||||||
export 'src/channel_header.dart';
|
export 'src/channel_header.dart';
|
||||||
@@ -22,16 +23,16 @@ export 'src/localization/stream_chat_localizations.dart';
|
|||||||
export 'src/localization/translations.dart' show DefaultTranslations;
|
export 'src/localization/translations.dart' show DefaultTranslations;
|
||||||
export 'src/mention_tile.dart';
|
export 'src/mention_tile.dart';
|
||||||
export 'src/message_action.dart';
|
export 'src/message_action.dart';
|
||||||
export 'src/message_input.dart';
|
export 'src/message_input/message_input.dart';
|
||||||
export 'src/message_list_view.dart';
|
export 'src/message_list_view.dart';
|
||||||
export 'src/message_search_item.dart';
|
export 'src/message_search_item.dart';
|
||||||
export 'src/message_search_list_view.dart';
|
export 'src/message_search_list_view.dart';
|
||||||
export 'src/message_text.dart';
|
export 'src/message_text.dart';
|
||||||
export 'src/message_widget.dart';
|
export 'src/message_widget.dart';
|
||||||
export 'src/mip/countdown_button.dart';
|
export 'src/message_input/countdown_button.dart';
|
||||||
export 'src/mip/stream_attachment_picker.dart';
|
export 'src/message_input/stream_attachment_picker.dart';
|
||||||
export 'src/mip/stream_message_send_button.dart';
|
export 'src/message_input/stream_message_send_button.dart';
|
||||||
export 'src/mip/stream_message_text_field.dart';
|
export 'src/message_input/stream_message_text_field.dart';
|
||||||
export 'src/option_list_tile.dart';
|
export 'src/option_list_tile.dart';
|
||||||
export 'src/reaction_icon.dart';
|
export 'src/reaction_icon.dart';
|
||||||
export 'src/reaction_picker.dart';
|
export 'src/reaction_picker.dart';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
name: stream_chat_flutter
|
name: stream_chat_flutter
|
||||||
homepage: https://github.com/GetStream/stream-chat-flutter
|
homepage: https://github.com/GetStream/stream-chat-flutter
|
||||||
description: Stream Chat official Flutter SDK. Build your own chat experience using Dart and Flutter.
|
description: Stream Chat official Flutter SDK. Build your own chat experience using Dart and Flutter.
|
||||||
version: 3.2.0
|
version: 3.3.2
|
||||||
repository: https://github.com/GetStream/stream-chat-flutter
|
repository: https://github.com/GetStream/stream-chat-flutter
|
||||||
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ dependencies:
|
|||||||
rxdart: ^0.27.0
|
rxdart: ^0.27.0
|
||||||
share_plus: ^3.0.4
|
share_plus: ^3.0.4
|
||||||
shimmer: ^2.0.0
|
shimmer: ^2.0.0
|
||||||
stream_chat_flutter_core: ^3.2.0
|
stream_chat_flutter_core: ^3.3.1
|
||||||
substring_highlight: ^1.0.26
|
substring_highlight: ^1.0.26
|
||||||
synchronized: ^3.0.0
|
synchronized: ^3.0.0
|
||||||
url_launcher: ^6.0.3
|
url_launcher: ^6.0.3
|
||||||
|
|||||||
@@ -1,7 +1,17 @@
|
|||||||
|
# Upcoming
|
||||||
|
|
||||||
✅ Added
|
✅ Added
|
||||||
|
|
||||||
- Added `MessageInputController` to hold `Message` related data.
|
- Added `MessageInputController` to hold `Message` related data.
|
||||||
|
|
||||||
|
## 3.3.1
|
||||||
|
|
||||||
|
- Updated `stream_chat` dependency to [`3.3.1`](https://pub.dev/packages/stream_chat/changelog).
|
||||||
|
|
||||||
|
## 3.3.0
|
||||||
|
|
||||||
|
- Updated `stream_chat` dependency to [`3.3.0`](https://pub.dev/packages/stream_chat/changelog).
|
||||||
|
|
||||||
## 3.2.0
|
## 3.2.0
|
||||||
|
|
||||||
- Updated `stream_chat` dependency to [`3.2.0`](https://pub.dev/packages/stream_chat/changelog).
|
- Updated `stream_chat` dependency to [`3.2.0`](https://pub.dev/packages/stream_chat/changelog).
|
||||||
|
|||||||
@@ -83,9 +83,12 @@ class HomeScreen extends StatelessWidget {
|
|||||||
channelListController: channelListController,
|
channelListController: channelListController,
|
||||||
filter: Filter.and([
|
filter: Filter.and([
|
||||||
Filter.equal('type', 'messaging'),
|
Filter.equal('type', 'messaging'),
|
||||||
Filter.in_('members', [
|
Filter.in_(
|
||||||
|
'members',
|
||||||
|
[
|
||||||
StreamChatCore.of(context).currentUser!.id,
|
StreamChatCore.of(context).currentUser!.id,
|
||||||
])
|
],
|
||||||
|
)
|
||||||
]),
|
]),
|
||||||
emptyBuilder: (BuildContext context) => const Center(
|
emptyBuilder: (BuildContext context) => const Center(
|
||||||
child: Text('Looks like you are not in any channels'),
|
child: Text('Looks like you are not in any channels'),
|
||||||
@@ -120,7 +123,7 @@ class HomeScreen extends StatelessWidget {
|
|||||||
itemBuilder: (BuildContext context, int index) {
|
itemBuilder: (BuildContext context, int index) {
|
||||||
final _item = channels[index];
|
final _item = channels[index];
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(_item.name!),
|
title: Text(_item.name ?? ''),
|
||||||
subtitle: StreamBuilder<Message?>(
|
subtitle: StreamBuilder<Message?>(
|
||||||
stream: _item.state!.lastMessageStream,
|
stream: _item.state!.lastMessageStream,
|
||||||
initialData: _item.state!.lastMessage,
|
initialData: _item.state!.lastMessage,
|
||||||
@@ -318,10 +321,10 @@ class _MessageScreenState extends State<MessageScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ class ChannelListCoreState extends State<ChannelListCore> {
|
|||||||
presence: widget.presence,
|
presence: widget.presence,
|
||||||
memberLimit: widget.memberLimit,
|
memberLimit: widget.memberLimit,
|
||||||
messageLimit: widget.messageLimit,
|
messageLimit: widget.messageLimit,
|
||||||
paginationParams: PaginationParams(limit: widget.limit),
|
paginationParams: PaginationParams(limit: widget.limit, offset: 0),
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Fetches more channels with updated pagination and updates the widget
|
/// Fetches more channels with updated pagination and updates the widget
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:rxdart/rxdart.dart';
|
import 'package:rxdart/rxdart.dart';
|
||||||
import 'package:stream_chat/stream_chat.dart';
|
import 'package:stream_chat/stream_chat.dart';
|
||||||
|
|||||||
@@ -4,142 +4,193 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:stream_chat/stream_chat.dart';
|
import 'package:stream_chat/stream_chat.dart';
|
||||||
|
|
||||||
|
/// A value listenable builder related to a [Message].
|
||||||
|
///
|
||||||
|
/// Pass in a [MessageInputController] as the `valueListenable`.
|
||||||
|
typedef MessageValueListenableBuilder = ValueListenableBuilder<Message>;
|
||||||
|
|
||||||
/// Controller for storing and mutating a [Message] value.
|
/// Controller for storing and mutating a [Message] value.
|
||||||
class MessageInputController extends ValueNotifier<Message> {
|
class MessageInputController extends ValueNotifier<Message> {
|
||||||
/// Creates a controller for an editable text field.
|
/// Creates a controller for an editable text field.
|
||||||
///
|
///
|
||||||
/// This constructor treats a null [message] argument as if it were the empty
|
/// This constructor treats a null [message] argument as if it were the empty
|
||||||
/// message.
|
/// message.
|
||||||
factory MessageInputController({Message? message}) =>
|
factory MessageInputController({
|
||||||
MessageInputController._(message ?? Message());
|
Message? message,
|
||||||
|
}) =>
|
||||||
|
MessageInputController._(
|
||||||
|
initialMessage: message ?? Message(),
|
||||||
|
);
|
||||||
|
|
||||||
/// Creates a controller for an editable text field from an initial [text].
|
/// Creates a controller for an editable text field from an initial [text].
|
||||||
factory MessageInputController.fromText(String? text) =>
|
factory MessageInputController.fromText(String? text) =>
|
||||||
MessageInputController._(Message(text: text));
|
MessageInputController._(
|
||||||
|
initialMessage: Message(text: text),
|
||||||
|
);
|
||||||
|
|
||||||
/// Creates a controller for an editable text field from an initial
|
/// Creates a controller for an editable text field from initial
|
||||||
/// [attachments].
|
/// [attachments].
|
||||||
factory MessageInputController.fromAttachments(
|
factory MessageInputController.fromAttachments(
|
||||||
List<Attachment> attachments,
|
List<Attachment> attachments,
|
||||||
) =>
|
) =>
|
||||||
MessageInputController._(Message(attachments: attachments));
|
MessageInputController._(
|
||||||
|
initialMessage: Message(attachments: attachments),
|
||||||
|
);
|
||||||
|
|
||||||
MessageInputController._(Message message)
|
MessageInputController._({
|
||||||
: _textEditingController = TextEditingController(text: message.text),
|
required Message initialMessage,
|
||||||
super(message);
|
}) : _textEditingController =
|
||||||
|
TextEditingController(text: initialMessage.text),
|
||||||
|
_initialMessage = initialMessage,
|
||||||
|
super(initialMessage) {
|
||||||
|
addListener(_textEditingSyncer);
|
||||||
|
}
|
||||||
|
|
||||||
///
|
void _textEditingSyncer() {
|
||||||
|
final cleanText = value.command == null
|
||||||
|
? value.text
|
||||||
|
: value.text?.replaceFirst(
|
||||||
|
'/${value.command} ',
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (cleanText != _textEditingController.text) {
|
||||||
|
final previousOffset = _textEditingController.value.selection.start;
|
||||||
|
final previousText = _textEditingController.text;
|
||||||
|
final diff = (cleanText?.length ?? 0) - previousText.length;
|
||||||
|
_textEditingController
|
||||||
|
..text = cleanText ?? ''
|
||||||
|
..selection = TextSelection.collapsed(
|
||||||
|
offset: previousOffset + diff,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the current message associated with this controller.
|
||||||
|
Message get message => value;
|
||||||
|
|
||||||
|
/// Returns the controller of the text field linked to this controller.
|
||||||
TextEditingController get textEditingController => _textEditingController;
|
TextEditingController get textEditingController => _textEditingController;
|
||||||
final TextEditingController _textEditingController;
|
final TextEditingController _textEditingController;
|
||||||
|
|
||||||
///
|
/// Returns the text of the message.
|
||||||
String get text => _textEditingController.text;
|
String get text => _textEditingController.text;
|
||||||
|
|
||||||
///
|
Message _initialMessage;
|
||||||
|
|
||||||
|
/// Sets the message.
|
||||||
set message(Message message) {
|
set message(Message message) {
|
||||||
value = message;
|
value = message;
|
||||||
}
|
}
|
||||||
|
|
||||||
set text(String newText) {
|
/// Sets a command for the message.
|
||||||
value = value.copyWith(text: newText);
|
set command(Command command) {
|
||||||
_textEditingController
|
value = value.copyWith(
|
||||||
..text = newText
|
command: command.name,
|
||||||
..selection = TextSelection.fromPosition(
|
text: '/${command.name} ',
|
||||||
TextPosition(offset: _textEditingController.text.length),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
/// Sets the text of the message.
|
||||||
set textEditingValue(TextEditingValue newValue) {
|
set text(String newText) {
|
||||||
_textEditingController.value = newValue;
|
var newTextWithCommand = newText;
|
||||||
value = value.copyWith(text: _textEditingController.text);
|
if (value.command != null) {
|
||||||
|
if (!newText.startsWith('/${value.command}')) {
|
||||||
|
newTextWithCommand = '/${value.command} $newText';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value = value.copyWith(text: newTextWithCommand);
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
/// Returns the baseOffset of the text field.
|
||||||
int get baseOffset => textEditingController.selection.baseOffset;
|
int get baseOffset => textEditingController.selection.baseOffset;
|
||||||
|
|
||||||
///
|
/// Returns the start of the selection of the text field.
|
||||||
int get selectionStart => textEditingController.selection.start;
|
int get selectionStart => textEditingController.selection.start;
|
||||||
|
|
||||||
|
/// Sets the [showInChannel] flag of the message.
|
||||||
set showInChannel(bool newValue) {
|
set showInChannel(bool newValue) {
|
||||||
value = value.copyWith(showInChannel: newValue);
|
value = value.copyWith(showInChannel: newValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
/// Returns true if the message is in a thread and
|
||||||
|
/// should be shown in the main channel as well.
|
||||||
bool get showInChannel => value.showInChannel ?? false;
|
bool get showInChannel => value.showInChannel ?? false;
|
||||||
|
|
||||||
///
|
/// Returns the attachments of the message.
|
||||||
List<Attachment> get attachments => value.attachments;
|
List<Attachment> get attachments => value.attachments;
|
||||||
|
|
||||||
|
/// Sets the list of [attachments] for the message.
|
||||||
set attachments(List<Attachment> attachments) {
|
set attachments(List<Attachment> attachments) {
|
||||||
value = value.copyWith(attachments: attachments);
|
value = value.copyWith(attachments: attachments);
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
/// Adds a new attachment to the message.
|
||||||
void addAttachment(Attachment attachment) {
|
void addAttachment(Attachment attachment) {
|
||||||
attachments = [...attachments, attachment];
|
attachments = [...attachments, attachment];
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
/// Adds a new attachment at the specified [index].
|
||||||
void addAttachmentAt(int index, Attachment attachment) {
|
void addAttachmentAt(int index, Attachment attachment) {
|
||||||
attachments = [...attachments]..insert(index, attachment);
|
attachments = [...attachments]..insert(index, attachment);
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
/// Removes the specified [attachment] from the message.
|
||||||
void removeAttachment(Attachment attachment) {
|
void removeAttachment(Attachment attachment) {
|
||||||
attachments = [...attachments]..remove(attachment);
|
attachments = [...attachments]..remove(attachment);
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
/// Remove the attachment with the given [attachmentId].
|
||||||
void removeAttachmentById(String attachmentId) {
|
void removeAttachmentById(String attachmentId) {
|
||||||
attachments = [...attachments]..removeWhere((it) => it.id == attachmentId);
|
attachments = [...attachments]..removeWhere((it) => it.id == attachmentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
/// Removes the attachment at the given [index].
|
||||||
void removeAttachmentAt(int index) {
|
void removeAttachmentAt(int index) {
|
||||||
attachments = [...attachments]..removeAt(index);
|
attachments = [...attachments]..removeAt(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
/// Clears the message attachments.
|
||||||
void clearAttachments() {
|
void clearAttachments() {
|
||||||
attachments = [];
|
attachments = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
/// Returns the list of mentioned users in the message.
|
||||||
List<User> get mentionedUsers => value.mentionedUsers;
|
List<User> get mentionedUsers => value.mentionedUsers;
|
||||||
|
|
||||||
|
/// Sets the mentioned users.
|
||||||
set mentionedUsers(List<User> users) {
|
set mentionedUsers(List<User> users) {
|
||||||
value = value.copyWith(mentionedUsers: users);
|
value = value.copyWith(mentionedUsers: users);
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
/// Adds a user to the list of mentioned users.
|
||||||
void addMentionedUser(User user) {
|
void addMentionedUser(User user) {
|
||||||
mentionedUsers = [...mentionedUsers, user];
|
mentionedUsers = [...mentionedUsers, user];
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
/// Removes the specified [user] from the mentioned users list.
|
||||||
void removeMentionedUser(User user) {
|
void removeMentionedUser(User user) {
|
||||||
mentionedUsers = [...mentionedUsers]..remove(user);
|
mentionedUsers = [...mentionedUsers]..remove(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
/// Removes the mentioned user with the given [userId].
|
||||||
void removeMentionedUserById(String userId) {
|
void removeMentionedUserById(String userId) {
|
||||||
mentionedUsers = [...mentionedUsers]..removeWhere((it) => it.id == userId);
|
mentionedUsers = [...mentionedUsers]..removeWhere((it) => it.id == userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
/// Removes all mentioned users from the message.
|
||||||
void clearMentionedUsers() {
|
void clearMentionedUsers() {
|
||||||
mentionedUsers = [];
|
mentionedUsers = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the [value] to empty.
|
/// Sets the [message], or [value], to empty.
|
||||||
///
|
///
|
||||||
/// After calling this function, [text], [attachments] and [mentionedUsers]
|
/// After calling this function, [text], [attachments] and [mentionedUsers]
|
||||||
/// all will be empty.
|
/// will all be empty.
|
||||||
///
|
///
|
||||||
/// Calling this will notify all the listeners of this
|
/// Calling this will notify all the listeners of this
|
||||||
/// [MessageInputController] that they need to update
|
/// [MessageInputController] that they need to update
|
||||||
/// (it calls [notifyListeners]). For this reason,
|
/// (calls [notifyListeners]). For this reason,
|
||||||
/// this method should only be called between frames, e.g. in response to user
|
/// this method should only be called between frames, e.g. in response to user
|
||||||
/// actions, not during the build, layout, or paint phases.
|
/// actions, not during the build, layout, or paint phases.
|
||||||
void clear() {
|
void clear() {
|
||||||
@@ -147,10 +198,21 @@ class MessageInputController extends ValueNotifier<Message> {
|
|||||||
_textEditingController.clear();
|
_textEditingController.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the [value] to the initial [Message] value.
|
||||||
|
void reset({bool resetId = true}) {
|
||||||
|
if (resetId) {
|
||||||
|
_initialMessage = _initialMessage.copyWith(
|
||||||
|
id: const Uuid().v4(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
value = _initialMessage;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
super.dispose();
|
removeListener(_textEditingSyncer);
|
||||||
_textEditingController.dispose();
|
_textEditingController.dispose();
|
||||||
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,16 +227,13 @@ class RestorableMessageInputController
|
|||||||
extends RestorableChangeNotifier<MessageInputController> {
|
extends RestorableChangeNotifier<MessageInputController> {
|
||||||
/// Creates a [RestorableMessageInputController].
|
/// Creates a [RestorableMessageInputController].
|
||||||
///
|
///
|
||||||
/// This constructor treats a null `text` argument as if it were the empty
|
/// This constructor creates a default [Message] when no `message` argument
|
||||||
/// string.
|
/// is supplied.
|
||||||
RestorableMessageInputController({Message? message})
|
RestorableMessageInputController({Message? message})
|
||||||
: _initialValue = message ?? Message();
|
: _initialValue = message ?? Message();
|
||||||
|
|
||||||
/// Creates a [RestorableMessageInputController] from an initial
|
/// Creates a [RestorableMessageInputController] from an initial
|
||||||
/// [TextEditingValue].
|
/// [text] value.
|
||||||
///
|
|
||||||
/// This constructor treats a null `value` argument as if it were
|
|
||||||
/// [TextEditingValue.empty].
|
|
||||||
factory RestorableMessageInputController.fromText(String? text) =>
|
factory RestorableMessageInputController.fromText(String? text) =>
|
||||||
RestorableMessageInputController(message: Message(text: text));
|
RestorableMessageInputController(message: Message(text: text));
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stream_chat/stream_chat.dart';
|
import 'package:stream_chat/stream_chat.dart';
|
||||||
import 'package:stream_chat_flutter_core/src/better_stream_builder.dart';
|
import 'package:stream_chat_flutter_core/src/better_stream_builder.dart';
|
||||||
|
|||||||
@@ -227,13 +227,13 @@ class StreamChannelState extends State<StreamChannel> {
|
|||||||
preferOffline: preferOffline,
|
preferOffline: preferOffline,
|
||||||
);
|
);
|
||||||
|
|
||||||
Future<List<ChannelState>> _queryAtMessage({
|
Future<ChannelState?> _queryAtMessage({
|
||||||
String? messageId,
|
String? messageId,
|
||||||
int before = 20,
|
int before = 20,
|
||||||
int after = 20,
|
int after = 20,
|
||||||
bool preferOffline = false,
|
bool preferOffline = false,
|
||||||
}) async {
|
}) async {
|
||||||
if (channel.state == null) return [];
|
if (channel.state == null) return null;
|
||||||
channel.state!.isUpToDate = false;
|
channel.state!.isUpToDate = false;
|
||||||
channel.state!.truncate();
|
channel.state!.truncate();
|
||||||
|
|
||||||
@@ -245,23 +245,33 @@ class StreamChannelState extends State<StreamChannel> {
|
|||||||
preferOffline: preferOffline,
|
preferOffline: preferOffline,
|
||||||
);
|
);
|
||||||
channel.state!.isUpToDate = true;
|
channel.state!.isUpToDate = true;
|
||||||
return [];
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Future.wait([
|
return queryAroundMessage(
|
||||||
queryBeforeMessage(
|
|
||||||
messageId,
|
messageId,
|
||||||
limit: before,
|
before: before,
|
||||||
|
after: after,
|
||||||
preferOffline: preferOffline,
|
preferOffline: preferOffline,
|
||||||
),
|
);
|
||||||
queryAfterMessage(
|
|
||||||
messageId,
|
|
||||||
limit: after,
|
|
||||||
preferOffline: preferOffline,
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
Future<ChannelState> queryAroundMessage(
|
||||||
|
String messageId, {
|
||||||
|
int before = 20,
|
||||||
|
int after = 20,
|
||||||
|
bool preferOffline = false,
|
||||||
|
}) =>
|
||||||
|
channel.query(
|
||||||
|
messagesPagination: PaginationParams(
|
||||||
|
idAround: messageId,
|
||||||
|
before: before,
|
||||||
|
after: after,
|
||||||
|
),
|
||||||
|
preferOffline: preferOffline,
|
||||||
|
);
|
||||||
|
|
||||||
///
|
///
|
||||||
Future<ChannelState> queryBeforeMessage(
|
Future<ChannelState> queryBeforeMessage(
|
||||||
String messageId, {
|
String messageId, {
|
||||||
|
|||||||
@@ -95,7 +95,12 @@ class StreamChatCoreState extends State<StreamChatCore>
|
|||||||
Timer? _disconnectTimer;
|
Timer? _disconnectTimer;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => widget.child;
|
Widget build(BuildContext context) {
|
||||||
|
StreamChatClient.additionalHeaders = {
|
||||||
|
'X-Stream-Client': '${StreamChatClient.defaultUserAgent}-core',
|
||||||
|
};
|
||||||
|
return widget.child;
|
||||||
|
}
|
||||||
|
|
||||||
// coverage:ignore-start
|
// coverage:ignore-start
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
name: stream_chat_flutter_core
|
name: stream_chat_flutter_core
|
||||||
homepage: https://github.com/GetStream/stream-chat-flutter
|
homepage: https://github.com/GetStream/stream-chat-flutter
|
||||||
description: Stream Chat official Flutter SDK Core. Build your own chat experience using Dart and Flutter.
|
description: Stream Chat official Flutter SDK Core. Build your own chat experience using Dart and Flutter.
|
||||||
version: 3.2.0
|
version: 3.3.1
|
||||||
repository: https://github.com/GetStream/stream-chat-flutter
|
repository: https://github.com/GetStream/stream-chat-flutter
|
||||||
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=2.12.0 <3.0.0'
|
sdk: '>=2.14.0 <3.0.0'
|
||||||
flutter: ">=1.17.0"
|
flutter: ">=1.17.0"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
collection: ^1.15.0
|
collection: ^1.15.0
|
||||||
connectivity_plus: ^2.0.2
|
connectivity_plus: ^2.1.0
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
meta: ^1.3.0
|
meta: ^1.3.0
|
||||||
rxdart: ^0.27.0
|
rxdart: ^0.27.0
|
||||||
stream_chat: ^3.2.0
|
stream_chat: ^3.3.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
dart_code_metrics: ^4.4.0
|
dart_code_metrics: ^4.4.0
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
|||||||
import 'mocks.dart';
|
import 'mocks.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
const pagination = PaginationParams(limit: 3);
|
const pagination = PaginationParams(limit: 3, offset: 0);
|
||||||
|
|
||||||
List<Channel> _generateChannels(
|
List<Channel> _generateChannels(
|
||||||
StreamChatClient client, {
|
StreamChatClient client, {
|
||||||
@@ -476,7 +476,7 @@ void main() {
|
|||||||
_stateSetter?.call(() => limit = 6);
|
_stateSetter?.call(() => limit = 6);
|
||||||
|
|
||||||
final updatedChannels = _generateChannels(mockClient, count: limit);
|
final updatedChannels = _generateChannels(mockClient, count: limit);
|
||||||
final updatedPagination = pagination.copyWith(limit: limit);
|
final updatedPagination = PaginationParams(limit: limit, offset: 0);
|
||||||
when(() => mockClient.queryChannels(
|
when(() => mockClient.queryChannels(
|
||||||
filter: any(named: 'filter'),
|
filter: any(named: 'filter'),
|
||||||
sort: any(named: 'sort'),
|
sort: any(named: 'sort'),
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
testWidgets(
|
||||||
|
'should instantiate a new MessageInputController with default validator'
|
||||||
|
' and empty message',
|
||||||
|
(tester) async {
|
||||||
|
final controller = MessageInputController();
|
||||||
|
|
||||||
|
expect(controller.isValid, false);
|
||||||
|
controller.text = 'test';
|
||||||
|
expect(controller.isValid, true);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'should instantiate a new MessageInputController with default validator'
|
||||||
|
' and specified message',
|
||||||
|
(tester) async {
|
||||||
|
final message = Message(text: 'test');
|
||||||
|
final controller = MessageInputController(
|
||||||
|
message: message,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(controller.message, message);
|
||||||
|
expect(controller.isValid, true);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -518,7 +518,7 @@ void main() {
|
|||||||
_stateSetter?.call(() => limit = 6);
|
_stateSetter?.call(() => limit = 6);
|
||||||
|
|
||||||
final updatedMessageResponseList = _generateMessages(count: limit);
|
final updatedMessageResponseList = _generateMessages(count: limit);
|
||||||
final updatedPagination = pagination.copyWith(limit: limit);
|
final updatedPagination = PaginationParams(limit: limit);
|
||||||
when(() => mockClient.search(
|
when(() => mockClient.search(
|
||||||
testFilter,
|
testFilter,
|
||||||
query: any(named: 'query'),
|
query: any(named: 'query'),
|
||||||
|
|||||||
@@ -189,9 +189,7 @@ void main() {
|
|||||||
membersPagination: any(named: 'membersPagination'),
|
membersPagination: any(named: 'membersPagination'),
|
||||||
watchersPagination: any(named: 'watchersPagination'),
|
watchersPagination: any(named: 'watchersPagination'),
|
||||||
preferOffline: any(named: 'preferOffline'),
|
preferOffline: any(named: 'preferOffline'),
|
||||||
)).called(
|
)).called(1);
|
||||||
2, // Fetching After messages + Fetching Before messages,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -214,14 +212,10 @@ void main() {
|
|||||||
child: const Offstage(key: childKey),
|
child: const Offstage(key: childKey),
|
||||||
);
|
);
|
||||||
|
|
||||||
final beforePagination = PaginationParams(
|
final paginationParams = PaginationParams(
|
||||||
lessThan: initialMessageId,
|
idAround: initialMessageId,
|
||||||
limit: 20,
|
after: 20,
|
||||||
);
|
before: 20,
|
||||||
|
|
||||||
final afterPagination = PaginationParams(
|
|
||||||
greaterThanOrEqual: initialMessageId,
|
|
||||||
limit: 20,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
when(() => mockChannel.initialized).thenAnswer((_) async => true);
|
when(() => mockChannel.initialized).thenAnswer((_) async => true);
|
||||||
@@ -232,17 +226,7 @@ void main() {
|
|||||||
state: any(named: 'state'),
|
state: any(named: 'state'),
|
||||||
watch: any(named: 'watch'),
|
watch: any(named: 'watch'),
|
||||||
presence: any(named: 'presence'),
|
presence: any(named: 'presence'),
|
||||||
messagesPagination: beforePagination,
|
messagesPagination: paginationParams,
|
||||||
membersPagination: any(named: 'membersPagination'),
|
|
||||||
watchersPagination: any(named: 'watchersPagination'),
|
|
||||||
preferOffline: any(named: 'preferOffline'),
|
|
||||||
)).thenAnswer((_) async => ChannelState(messages: messages));
|
|
||||||
|
|
||||||
when(() => mockChannel.query(
|
|
||||||
state: any(named: 'state'),
|
|
||||||
watch: any(named: 'watch'),
|
|
||||||
presence: any(named: 'presence'),
|
|
||||||
messagesPagination: afterPagination,
|
|
||||||
membersPagination: any(named: 'membersPagination'),
|
membersPagination: any(named: 'membersPagination'),
|
||||||
watchersPagination: any(named: 'watchersPagination'),
|
watchersPagination: any(named: 'watchersPagination'),
|
||||||
preferOffline: any(named: 'preferOffline'),
|
preferOffline: any(named: 'preferOffline'),
|
||||||
@@ -267,17 +251,7 @@ void main() {
|
|||||||
state: any(named: 'state'),
|
state: any(named: 'state'),
|
||||||
watch: any(named: 'watch'),
|
watch: any(named: 'watch'),
|
||||||
presence: any(named: 'presence'),
|
presence: any(named: 'presence'),
|
||||||
messagesPagination: beforePagination,
|
messagesPagination: paginationParams,
|
||||||
membersPagination: any(named: 'membersPagination'),
|
|
||||||
watchersPagination: any(named: 'watchersPagination'),
|
|
||||||
preferOffline: any(named: 'preferOffline'),
|
|
||||||
)).called(1);
|
|
||||||
|
|
||||||
verify(() => mockChannel.query(
|
|
||||||
state: any(named: 'state'),
|
|
||||||
watch: any(named: 'watch'),
|
|
||||||
presence: any(named: 'presence'),
|
|
||||||
messagesPagination: afterPagination,
|
|
||||||
membersPagination: any(named: 'membersPagination'),
|
membersPagination: any(named: 'membersPagination'),
|
||||||
watchersPagination: any(named: 'watchersPagination'),
|
watchersPagination: any(named: 'watchersPagination'),
|
||||||
preferOffline: any(named: 'preferOffline'),
|
preferOffline: any(named: 'preferOffline'),
|
||||||
@@ -285,29 +259,15 @@ void main() {
|
|||||||
|
|
||||||
_stateSetter?.call(() => initialMessageId = 'testInitialMessageId2');
|
_stateSetter?.call(() => initialMessageId = 'testInitialMessageId2');
|
||||||
|
|
||||||
final updatedBeforePagination = beforePagination.copyWith(
|
final updatedPaginationParams = paginationParams.copyWith(
|
||||||
lessThan: initialMessageId,
|
idAround: initialMessageId,
|
||||||
);
|
|
||||||
|
|
||||||
final updatedAfterPagination = afterPagination.copyWith(
|
|
||||||
greaterThanOrEqual: initialMessageId,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
when(() => mockChannel.query(
|
when(() => mockChannel.query(
|
||||||
state: any(named: 'state'),
|
state: any(named: 'state'),
|
||||||
watch: any(named: 'watch'),
|
watch: any(named: 'watch'),
|
||||||
presence: any(named: 'presence'),
|
presence: any(named: 'presence'),
|
||||||
messagesPagination: updatedBeforePagination,
|
messagesPagination: updatedPaginationParams,
|
||||||
membersPagination: any(named: 'membersPagination'),
|
|
||||||
watchersPagination: any(named: 'watchersPagination'),
|
|
||||||
preferOffline: any(named: 'preferOffline'),
|
|
||||||
)).thenAnswer((_) async => ChannelState(messages: messages));
|
|
||||||
|
|
||||||
when(() => mockChannel.query(
|
|
||||||
state: any(named: 'state'),
|
|
||||||
watch: any(named: 'watch'),
|
|
||||||
presence: any(named: 'presence'),
|
|
||||||
messagesPagination: updatedAfterPagination,
|
|
||||||
membersPagination: any(named: 'membersPagination'),
|
membersPagination: any(named: 'membersPagination'),
|
||||||
watchersPagination: any(named: 'watchersPagination'),
|
watchersPagination: any(named: 'watchersPagination'),
|
||||||
preferOffline: any(named: 'preferOffline'),
|
preferOffline: any(named: 'preferOffline'),
|
||||||
@@ -319,17 +279,7 @@ void main() {
|
|||||||
state: any(named: 'state'),
|
state: any(named: 'state'),
|
||||||
watch: any(named: 'watch'),
|
watch: any(named: 'watch'),
|
||||||
presence: any(named: 'presence'),
|
presence: any(named: 'presence'),
|
||||||
messagesPagination: updatedBeforePagination,
|
messagesPagination: updatedPaginationParams,
|
||||||
membersPagination: any(named: 'membersPagination'),
|
|
||||||
watchersPagination: any(named: 'watchersPagination'),
|
|
||||||
preferOffline: any(named: 'preferOffline'),
|
|
||||||
)).called(1);
|
|
||||||
|
|
||||||
verify(() => mockChannel.query(
|
|
||||||
state: any(named: 'state'),
|
|
||||||
watch: any(named: 'watch'),
|
|
||||||
presence: any(named: 'presence'),
|
|
||||||
messagesPagination: updatedAfterPagination,
|
|
||||||
membersPagination: any(named: 'membersPagination'),
|
membersPagination: any(named: 'membersPagination'),
|
||||||
watchersPagination: any(named: 'watchersPagination'),
|
watchersPagination: any(named: 'watchersPagination'),
|
||||||
preferOffline: any(named: 'preferOffline'),
|
preferOffline: any(named: 'preferOffline'),
|
||||||
|
|||||||
@@ -496,7 +496,7 @@ void main() {
|
|||||||
_stateSetter?.call(() => limit = 6);
|
_stateSetter?.call(() => limit = 6);
|
||||||
|
|
||||||
final updatedUsers = _generateUsers(count: limit);
|
final updatedUsers = _generateUsers(count: limit);
|
||||||
final updatedPagination = pagination.copyWith(limit: limit);
|
final updatedPagination = PaginationParams(limit: limit);
|
||||||
when(() => mockClient.queryUsers(
|
when(() => mockClient.queryUsers(
|
||||||
filter: any(named: 'filter'),
|
filter: any(named: 'filter'),
|
||||||
sort: any(named: 'sort'),
|
sort: any(named: 'sort'),
|
||||||
|
|||||||
@@ -374,8 +374,10 @@ class NnStreamChatLocalizations extends GlobalStreamChatLocalizations {
|
|||||||
String get youText => 'You';
|
String get youText => 'You';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String galleryPaginationText(
|
String galleryPaginationText({
|
||||||
{required int currentPage, required int totalPages}) =>
|
required int currentPage,
|
||||||
|
required int totalPages,
|
||||||
|
}) =>
|
||||||
'$currentPage of $totalPages';
|
'$currentPage of $totalPages';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ class _MessageViewState extends State<MessageView> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ void main() {
|
|||||||
role: 'testRole',
|
role: 'testRole',
|
||||||
createdAt: DateTime.now(),
|
createdAt: DateTime.now(),
|
||||||
updatedAt: DateTime.now(),
|
updatedAt: DateTime.now(),
|
||||||
lastActive: DateTime.now(),
|
|
||||||
online: math.Random().nextBool(),
|
online: math.Random().nextBool(),
|
||||||
banned: math.Random().nextBool(),
|
banned: math.Random().nextBool(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -157,7 +157,6 @@ void main() {
|
|||||||
(prev, curr) =>
|
(prev, curr) =>
|
||||||
prev?..update(curr.type, (value) => value + 1, ifAbsent: () => 1),
|
prev?..update(curr.type, (value) => value + 1, ifAbsent: () => 1),
|
||||||
),
|
),
|
||||||
status: MessageSendingStatus.sending,
|
|
||||||
updatedAt: DateTime.now(),
|
updatedAt: DateTime.now(),
|
||||||
extraData: const {'extra_test_data': 'extraData'},
|
extraData: const {'extra_test_data': 'extraData'},
|
||||||
user: user,
|
user: user,
|
||||||
|
|||||||
@@ -147,7 +147,6 @@ void main() {
|
|||||||
(prev, curr) =>
|
(prev, curr) =>
|
||||||
prev?..update(curr.type, (value) => value + 1, ifAbsent: () => 1),
|
prev?..update(curr.type, (value) => value + 1, ifAbsent: () => 1),
|
||||||
),
|
),
|
||||||
status: MessageSendingStatus.sending,
|
|
||||||
updatedAt: DateTime.now(),
|
updatedAt: DateTime.now(),
|
||||||
extraData: const {'extra_test_data': 'extraData'},
|
extraData: const {'extra_test_data': 'extraData'},
|
||||||
user: user,
|
user: user,
|
||||||
|
|||||||
Reference in New Issue
Block a user