diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index ade0551e..573e21f8 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,3 +1,12 @@ +## 6.4.0 + +🐞 Fixed + +- [[#1293]](https://github.com/GetStream/stream-chat-flutter/issues/1293) Fixed wrong message order when sending + messages quickly. +- [[#1612]](https://github.com/GetStream/stream-chat-flutter/issues/1612) Fixed `Channel.isMutedStream` does not emit + when channel mute expires. + ## 6.3.0 🐞 Fixed diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 54feb2ab..9d80fcb6 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -7,6 +7,7 @@ import 'package:rxdart/rxdart.dart'; import 'package:stream_chat/src/client/retry_queue.dart'; import 'package:stream_chat/src/core/util/utils.dart'; import 'package:stream_chat/stream_chat.dart'; +import 'package:synchronized/synchronized.dart'; /// The maximum time the incoming [Event.typingStart] event is valid before a /// [Event.typingStop] event is emitted automatically. @@ -563,6 +564,8 @@ class Channel { }); } + final _sendMessageLock = Lock(); + /// Send a [message] to this channel. /// /// If [skipPush] is true the message will not send a push notification. @@ -586,7 +589,7 @@ class Channel { ); // ignore: parameter_assignments message = message.copyWith( - createdAt: message.createdAt, + localCreatedAt: DateTime.now(), user: _client.state.currentUser, quotedMessage: quotedMessage, status: MessageSendingStatus.sending, @@ -615,14 +618,21 @@ class Channel { message = await attachmentsUploadCompleter.future; } - final response = await _client.sendMessage( - message, - id!, - type, - skipPush: skipPush, - skipEnrichUrl: skipEnrichUrl, + // Wait for the previous sendMessage call to finish. Otherwise, the order + // of messages will not be maintained. + final response = await _sendMessageLock.synchronized( + () => _client.sendMessage( + message, + id!, + type, + skipPush: skipPush, + skipEnrichUrl: skipEnrichUrl, + ), ); - state!.updateMessage(response.message); + + final sentMessage = response.message.syncWith(message); + + state!.updateMessage(sentMessage); if (cooldown > 0) cooldownStartedAt = DateTime.now(); return response; } catch (e) { @@ -633,6 +643,8 @@ class Channel { } } + final _updateMessageLock = Lock(); + /// Updates the [message] in this channel. /// /// Waits for a [_messageAttachmentsUploadCompleter] to complete @@ -652,7 +664,7 @@ class Channel { // ignore: parameter_assignments message = message.copyWith( status: MessageSendingStatus.updating, - updatedAt: message.updatedAt, + localUpdatedAt: DateTime.now(), attachments: message.attachments.map( (it) { if (it.uploadState.isSuccess) return it; @@ -678,16 +690,20 @@ class Channel { message = await attachmentsUploadCompleter.future; } - final response = await _client.updateMessage( - message, - skipEnrichUrl: skipEnrichUrl, + // Wait for the previous update call to finish. Otherwise, the order of + // messages will not be maintained. + final response = await _updateMessageLock.synchronized( + () => _client.updateMessage( + message, + skipEnrichUrl: skipEnrichUrl, + ), ); - final m = response.message.copyWith( - ownReactions: message.ownReactions, - ); + final updatedMessage = response.message + .syncWith(message) + .copyWith(ownReactions: message.ownReactions); - state?.updateMessage(m); + state?.updateMessage(updatedMessage); return response; } catch (e) { @@ -714,16 +730,20 @@ class Channel { bool skipEnrichUrl = false, }) async { try { - final response = await _client.partialUpdateMessage( - message.id, - set: set, - unset: unset, - skipEnrichUrl: skipEnrichUrl, + // Wait for the previous update call to finish. Otherwise, the order of + // messages will not be maintained. + final response = await _updateMessageLock.synchronized( + () => _client.partialUpdateMessage( + message.id, + set: set, + unset: unset, + skipEnrichUrl: skipEnrichUrl, + ), ); - final updatedMessage = response.message.copyWith( - ownReactions: message.ownReactions, - ); + final updatedMessage = response.message + .syncWith(message) + .copyWith(ownReactions: message.ownReactions); state?.updateMessage(updatedMessage); @@ -736,6 +756,8 @@ class Channel { } } + final _deleteMessageLock = Lock(); + /// Deletes the [message] from the channel. Future deleteMessage(Message message, {bool? hard}) async { final hardDelete = hard ?? false; @@ -746,7 +768,7 @@ class Channel { state!.deleteMessage( message.copyWith( type: 'deleted', - deletedAt: message.deletedAt ?? DateTime.now(), + localDeletedAt: DateTime.now(), status: MessageSendingStatus.sent, ), hardDelete: hardDelete, @@ -770,13 +792,18 @@ class Channel { state?.deleteMessage(message, hardDelete: hardDelete); - final response = await _client.deleteMessage(message.id, hard: hard); - - state?.deleteMessage( - message.copyWith(status: MessageSendingStatus.sent), - hardDelete: hardDelete, + // Wait for the previous delete call to finish. Otherwise, the order of + // messages will not be maintained. + final response = await _deleteMessageLock.synchronized( + () => _client.deleteMessage(message.id, hard: hard), ); + final deletedMessage = message.copyWith( + status: MessageSendingStatus.sent, + ); + + state?.deleteMessage(deletedMessage, hardDelete: hardDelete); + return response; } catch (e) { if (e is StreamChatNetworkError && e.isRetriable) { @@ -1420,15 +1447,32 @@ class Channel { ); } + // Timer to keep track of mute expiration. This is used to update the channel + // state when the mute expires. + Timer? _muteExpirationTimer; + /// Mutes the channel. Future mute({Duration? expiration}) { _checkInitialized(); + + // If there is a expiration set, we will set a timer to automatically unmute + // the channel when the mute expires. + if (expiration != null) { + _muteExpirationTimer?.cancel(); + _muteExpirationTimer = Timer(expiration, unmute); + } + return _client.muteChannel(cid!, expiration: expiration); } /// Unmute the channel. Future unmute() { _checkInitialized(); + + // Cancel the mute expiration timer if it is set. + _muteExpirationTimer?.cancel(); + _muteExpirationTimer = null; + return _client.unmuteChannel(cid!); } @@ -1558,6 +1602,7 @@ class Channel { void dispose() { client.state.removeChannel('$cid'); state?.dispose(); + _muteExpirationTimer?.cancel(); _keyStrokeHandler.cancel(); } @@ -1942,8 +1987,9 @@ class ChannelClientState { ) .listen((event) { final message = event.message!; - if (isUpToDate || - (message.parentId != null && message.showInChannel != true)) { + final showInChannel = + message.parentId != null && message.showInChannel != true; + if (isUpToDate || showInChannel) { updateMessage(message); } @@ -1960,10 +2006,10 @@ class ChannelClientState { var newMessages = [...messages]; final oldIndex = newMessages.indexWhere((m) => m.id == message.id); if (oldIndex != -1) { - var updatedMessage = message; + final oldMessage = newMessages[oldIndex]; + var updatedMessage = message.syncWith(oldMessage); // Add quoted message to the message if it is not present. if (message.quotedMessageId != null && message.quotedMessage == null) { - final oldMessage = newMessages[oldIndex]; updatedMessage = updatedMessage.copyWith( quotedMessage: oldMessage.quotedMessage, ); @@ -1980,7 +2026,7 @@ class ChannelClientState { return it.copyWith( quotedMessage: updatedMessage.copyWith( type: 'deleted', - deletedAt: updatedMessage.deletedAt ?? DateTime.now(), + deletedAt: DateTime.now(), ), ); }).toList(); @@ -2004,7 +2050,7 @@ class ChannelClientState { } _channelState = _channelState.copyWith( - messages: newMessages..sort(_sortByCreatedAt), + messages: newMessages.sorted(_sortByCreatedAt), pinnedMessages: newPinnedMessages, channel: _channelState.channel?.copyWith( lastMessageAt: message.createdAt, @@ -2226,7 +2272,7 @@ class ChannelClientState { ...newThreads[parentId]!.where( (newMessage) => !messages.any((m) => m.id == newMessage.id), ), - ]..sort(_sortByCreatedAt); + ].sorted(_sortByCreatedAt); } else { newThreads[parentId] = messages; } @@ -2245,15 +2291,10 @@ class ChannelClientState { /// Update channelState with updated information. void updateChannelState(ChannelState updatedState) { - final _existingStateMessages = _channelState.messages ?? []; - final _updatedStateMessages = updatedState.messages ?? []; + final _existingStateMessages = [...messages]; final newMessages = [ - ..._updatedStateMessages, - ..._existingStateMessages - .where((m) => - !_updatedStateMessages.any((newMessage) => newMessage.id == m.id)) - .toList(), - ]..sort(_sortByCreatedAt); + ..._existingStateMessages.merge(updatedState.messages), + ].sorted(_sortByCreatedAt); final _existingStateWatchers = _channelState.watchers ?? []; final _updatedStateWatchers = updatedState.watchers ?? []; @@ -2471,3 +2512,21 @@ bool _pinIsValid(Message message) { final now = DateTime.now(); return message.pinExpires!.isAfter(now); } + +extension on Iterable { + Iterable merge(Iterable? other) { + if (other == null) return this; + + final messageMap = {for (final message in this) message.id: message}; + + for (final message in other) { + messageMap.update( + message.id, + message.syncWith, + ifAbsent: () => message, + ); + } + + return messageMap.values; + } +} diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index f131b12f..ec6443e9 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -32,7 +32,7 @@ import 'package:stream_chat/src/event_type.dart'; import 'package:stream_chat/src/ws/connection_status.dart'; import 'package:stream_chat/src/ws/websocket.dart'; import 'package:stream_chat/version.dart'; -import 'package:synchronized/extension.dart'; +import 'package:synchronized/synchronized.dart'; /// Handler function used for logging records. Function requires a single /// [LogRecord] as the only parameter. @@ -526,10 +526,13 @@ class StreamChatClient { event.type == eventType4); } + // Lock to make sure only one sync process is running at a time. + final _syncLock = Lock(); + /// Get the events missed while offline to sync the offline storage /// Will automatically fetch [cids] and [lastSyncedAt] if [persistenceEnabled] Future sync({List? cids, DateTime? lastSyncAt}) { - return synchronized(() async { + return _syncLock.synchronized(() async { final channels = cids ?? await chatPersistenceClient?.getChannelCids(); if (channels == null || channels.isEmpty) { return; diff --git a/packages/stream_chat/lib/src/core/models/message.dart b/packages/stream_chat/lib/src/core/models/message.dart index f8631e1c..59491c5c 100644 --- a/packages/stream_chat/lib/src/core/models/message.dart +++ b/packages/stream_chat/lib/src/core/models/message.dart @@ -64,8 +64,11 @@ class Message extends Equatable { this.showInChannel, this.command, DateTime? createdAt, + this.localCreatedAt, DateTime? updatedAt, - this.deletedAt, + this.localUpdatedAt, + DateTime? deletedAt, + this.localDeletedAt, this.user, this.pinned = false, this.pinnedAt, @@ -76,8 +79,9 @@ class Message extends Equatable { this.i18n, }) : id = id ?? const Uuid().v4(), pinExpires = pinExpires?.toUtc(), - _createdAt = createdAt, - _updatedAt = updatedAt, + remoteCreatedAt = createdAt, + remoteUpdatedAt = updatedAt, + remoteDeletedAt = deletedAt, _quotedMessageId = quotedMessageId; /// Create a new instance from JSON. @@ -161,21 +165,49 @@ class Message extends Equatable { @JsonKey(includeToJson: false) final String? command; - final DateTime? _createdAt; - - /// Reserved field indicating when the message was deleted. + /// Indicates when the message was created. + /// + /// Returns the latest between [localCreatedAt] and [remoteCreatedAt]. + /// If both are null, returns [DateTime.now]. @JsonKey(includeToJson: false) - final DateTime? deletedAt; + DateTime get createdAt => localCreatedAt ?? remoteCreatedAt ?? DateTime.now(); - /// Reserved field indicating when the message was created. + /// Indicates when the message was created locally. + @JsonKey(includeToJson: false, includeFromJson: false) + final DateTime? localCreatedAt; + + /// Indicates when the message was created on the server. + @JsonKey(includeToJson: false, includeFromJson: false) + final DateTime? remoteCreatedAt; + + /// Indicates when the message was updated last time. + /// + /// Returns the latest between [localUpdatedAt] and [remoteUpdatedAt]. + /// If both are null, returns [createdAt]. @JsonKey(includeToJson: false) - DateTime get createdAt => _createdAt ?? DateTime.now(); + DateTime get updatedAt => localUpdatedAt ?? remoteUpdatedAt ?? createdAt; - final DateTime? _updatedAt; + /// Indicates when the message was updated locally. + @JsonKey(includeToJson: false, includeFromJson: false) + final DateTime? localUpdatedAt; - /// Reserved field indicating when the message was updated last time. + /// Indicates when the message was updated on the server. + @JsonKey(includeToJson: false, includeFromJson: false) + final DateTime? remoteUpdatedAt; + + /// Indicates when the message was deleted. + /// + /// Returns the latest between [localDeletedAt] and [remoteDeletedAt]. @JsonKey(includeToJson: false) - DateTime get updatedAt => _updatedAt ?? DateTime.now(); + DateTime? get deletedAt => localDeletedAt ?? remoteDeletedAt; + + /// Indicates when the message was deleted locally. + @JsonKey(includeToJson: false, includeFromJson: false) + final DateTime? localDeletedAt; + + /// Indicates when the message was deleted on the server. + @JsonKey(includeToJson: false, includeFromJson: false) + final DateTime? remoteDeletedAt; /// User who sent the message. @JsonKey(includeToJson: false) @@ -273,8 +305,11 @@ class Message extends Equatable { bool? showInChannel, String? command, DateTime? createdAt, + DateTime? localCreatedAt, DateTime? updatedAt, + DateTime? localUpdatedAt, DateTime? deletedAt, + DateTime? localDeletedAt, User? user, bool? pinned, DateTime? pinnedAt, @@ -338,9 +373,12 @@ class Message extends Equatable { threadParticipants: threadParticipants ?? this.threadParticipants, showInChannel: showInChannel ?? this.showInChannel, command: command ?? this.command, - createdAt: createdAt ?? _createdAt, - updatedAt: updatedAt ?? _updatedAt, - deletedAt: deletedAt ?? this.deletedAt, + createdAt: createdAt ?? remoteCreatedAt, + localCreatedAt: localCreatedAt ?? this.localCreatedAt, + updatedAt: updatedAt ?? remoteUpdatedAt, + localUpdatedAt: localUpdatedAt ?? this.localUpdatedAt, + deletedAt: deletedAt ?? remoteDeletedAt, + localDeletedAt: localDeletedAt ?? this.localDeletedAt, user: user ?? this.user, pinned: pinned ?? this.pinned, pinnedAt: pinnedAt ?? this.pinnedAt, @@ -374,9 +412,12 @@ class Message extends Equatable { threadParticipants: other.threadParticipants, showInChannel: other.showInChannel, command: other.command, - createdAt: other.createdAt, - updatedAt: other.updatedAt, - deletedAt: other.deletedAt, + createdAt: other.remoteCreatedAt, + localCreatedAt: other.localCreatedAt, + updatedAt: other.remoteUpdatedAt, + localUpdatedAt: other.localUpdatedAt, + deletedAt: other.remoteDeletedAt, + localDeletedAt: other.localDeletedAt, user: other.user, pinned: other.pinned, pinnedAt: other.pinnedAt, @@ -387,6 +428,28 @@ class Message extends Equatable { i18n: other.i18n, ); + /// Returns a new [Message] that is [other] with local changes applied to it. + /// + /// This ensures that the local sync changes are not lost when the message is + /// updated on the server. + /// + /// For example, when a message is sent, it is immediately shown + /// optimistically in the UI. When the message is received from the server, + /// it will not contain the local changes. This method can be used to merge + /// the local changes back into the message. + /// + /// This also helps in maintaining the order of the messages in the channel + /// when the messages are sorted by the [createdAt] field. + Message syncWith(Message? other) { + if (other == null) return this; + + return copyWith( + localCreatedAt: other.localCreatedAt, + localUpdatedAt: other.localUpdatedAt, + localDeletedAt: other.localDeletedAt, + ); + } + @override List get props => [ id, @@ -407,9 +470,12 @@ class Message extends Equatable { shadowed, silent, command, - _createdAt, - _updatedAt, - deletedAt, + localCreatedAt, + remoteCreatedAt, + localUpdatedAt, + remoteUpdatedAt, + localDeletedAt, + remoteDeletedAt, user, pinned, pinnedAt, diff --git a/packages/stream_chat/lib/version.dart b/packages/stream_chat/lib/version.dart index bf392fb3..a2fb6fe6 100644 --- a/packages/stream_chat/lib/version.dart +++ b/packages/stream_chat/lib/version.dart @@ -3,4 +3,4 @@ import 'package:stream_chat/src/client/client.dart'; /// Current package version /// Used in [StreamChatClient] to build the `x-stream-client` header // ignore: constant_identifier_names -const PACKAGE_VERSION = '6.3.0'; +const PACKAGE_VERSION = '6.4.0'; diff --git a/packages/stream_chat/pubspec.yaml b/packages/stream_chat/pubspec.yaml index db3bad2c..3bd6b49a 100644 --- a/packages/stream_chat/pubspec.yaml +++ b/packages/stream_chat/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat homepage: https://getstream.io/ description: The official Dart client for Stream Chat, a service for building chat applications. -version: 6.3.0 +version: 6.4.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues diff --git a/packages/stream_chat/test/src/client/channel_test.dart b/packages/stream_chat/test/src/client/channel_test.dart index 12a35629..3b4a1b6a 100644 --- a/packages/stream_chat/test/src/client/channel_test.dart +++ b/packages/stream_chat/test/src/client/channel_test.dart @@ -2481,6 +2481,31 @@ void main() { )).called(1); }); + test('`.mute with expiration`', () async { + const expiration = Duration(seconds: 3); + + when(() => client.muteChannel( + channelCid, + expiration: expiration, + )).thenAnswer((_) async => EmptyResponse()); + + when(() => client.unmuteChannel(channelCid)) + .thenAnswer((_) async => EmptyResponse()); + + final res = await channel.mute(expiration: expiration); + + expect(res, isNotNull); + + verify(() => client.muteChannel( + channelCid, + expiration: expiration, + )).called(1); + + // wait for expiration + await Future.delayed(expiration); + verify(() => client.unmuteChannel(channelCid)).called(1); + }); + test('`.unmute`', () async { when( () => client.unmuteChannel(channelCid), diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 77a7f55e..2e6c73f0 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,3 +1,100 @@ +## 6.4.0 + +🐞 Fixed + +- [[#1600]](https://github.com/GetStream/stream-chat-flutter/issues/1600) Fixed type `ImageDecoderCallback` not found + error on pre-Flutter 3.10.0 versions. +- [[#1605]](https://github.com/GetStream/stream-chat-flutter/issues/1605) Fixed Null exception is thrown on message list + for unread messages when `ScrollToBottomButton` is pressed. +- [[#1615]](https://github.com/GetStream/stream-chat-flutter/issues/1615) Fixed `StreamAttachmentPickerBottomSheet` not + able to find the `StreamChatTheme` when used in nested MaterialApp. + +✅ Added + +- Added support for `StreamMessageInput.allowedAttachmentPickerTypes` to specify the allowed attachment picker types. + [#1601](https://github.com/GetStream/stream-chat-flutter/issues/1376) + + ```dart + StreamMessageInput( + ..., + allowedAttachmentPickerTypes: const [ + AttachmentPickerType.files, + AttachmentPickerType.images, + ], + ) + ``` + +- Added support for `StreamMessageWidget.onConfirmDeleteTap` to override the default action on delete confirmation. + [#1604](https://github.com/GetStream/stream-chat-flutter/issues/1604) + + ```dart + StreamMessageWidget( + ..., + onConfirmDeleteTap: (message) async { + final channel = StreamChannel.of(context).channel; + await channel.deleteMessage(message, hard: false); + }, + ) + ``` + +- Added support for `StreamMessageWidget.quotedMessageBuilder` and `StreamMessageInput.quotedMessageBuilder` to override + the default quoted message widget. [#1547](https://github.com/GetStream/stream-chat-flutter/issues/1547) + + ```dart + StreamMessageWidget( + ..., + quotedMessageBuilder: (context, message) { + return Container( + color: Colors.red, + child: Text('Quoted Message'), + ); + }, + ) + ``` + +- Added support for `StreamChannelAvatar.ownSpaceAvatarBuilder`, `StreamChannelAvatar.oneToOneAvatarBuilder` and + `StreamChannelAvatar.groupAvatarBuilder` to override the default avatar + widget.[#1614](https://github.com/GetStream/stream-chat-flutter/issues/1614) + + ```dart + StreamChannelAvatar( + ..., + ownSpaceAvatarBuilder: (context, channel) { + return Container( + color: Colors.red, + child: Text('Own Space Avatar'), + ); + }, + oneToOneAvatarBuilder: (context, channel) { + return Container( + color: Colors.red, + child: Text('One to One Avatar'), + ); + }, + groupAvatarBuilder: (context, channel) { + return Container( + color: Colors.red, + child: Text('Group Avatar'), + ); + }, + ) + ``` + +- Added support for `StreamMessageInput.contentInsertionConfiguration` to specify the content insertion configuration. + [#1613](https://github.com/GetStream/stream-chat-flutter/issues/1613) + + ```dart + StreamMessageInput( + ..., + contentInsertionConfiguration: ContentInsertionConfiguration( + onContentInserted: (content) { + // Do something with the content. + controller.addAttachment(...); + }, + ), + ) + ``` + ## 6.3.0 🐞 Fixed diff --git a/packages/stream_chat_flutter/example/lib/debug/actions/ban_user.dart b/packages/stream_chat_flutter/example/lib/debug/actions/ban_user.dart new file mode 100644 index 00000000..62cec6dc --- /dev/null +++ b/packages/stream_chat_flutter/example/lib/debug/actions/ban_user.dart @@ -0,0 +1,41 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'package:stream_chat_flutter_example/debug/error_dialog.dart'; + +class DebugBanUser extends StatelessWidget { + const DebugBanUser({ + super.key, + required this.client, + }); + + final StreamChatClient client; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8), + child: TextField( + decoration: const InputDecoration( + labelText: 'Ban User', + hintText: 'User Id', + isDense: true, + border: OutlineInputBorder(), + ), + onSubmitted: (value) async { + final userId = value.trim(); + try { + debugPrint('[banUser] userId: $userId'); + final result = await client.banUser(userId); + debugPrint('[banUser] completed: $result'); + } catch (e) { + debugPrint('[banUser] failed: $e'); + showErrorDialog(context, e, 'Ban User'); + } + }, + ), + ); + } +} diff --git a/packages/stream_chat_flutter/example/lib/debug/actions/mute_user.dart b/packages/stream_chat_flutter/example/lib/debug/actions/mute_user.dart new file mode 100644 index 00000000..5253669e --- /dev/null +++ b/packages/stream_chat_flutter/example/lib/debug/actions/mute_user.dart @@ -0,0 +1,41 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'package:stream_chat_flutter_example/debug/error_dialog.dart'; + +class DebugMuteUser extends StatelessWidget { + const DebugMuteUser({ + super.key, + required this.client, + }); + + final StreamChatClient client; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8), + child: TextField( + decoration: const InputDecoration( + labelText: 'Mute User', + hintText: 'User Id', + isDense: true, + border: OutlineInputBorder(), + ), + onSubmitted: (value) async { + final userId = value.trim(); + try { + debugPrint('[muteUser] userId: $userId'); + final result = await client.muteUser(userId); + debugPrint('[muteUser] completed: $result'); + } catch (e) { + debugPrint('[muteUser] failed: $e'); + showErrorDialog(context, e, 'Mute User'); + } + }, + ), + ); + } +} diff --git a/packages/stream_chat_flutter/example/lib/debug/actions/remove_shadow_ban.dart b/packages/stream_chat_flutter/example/lib/debug/actions/remove_shadow_ban.dart new file mode 100644 index 00000000..5ab29797 --- /dev/null +++ b/packages/stream_chat_flutter/example/lib/debug/actions/remove_shadow_ban.dart @@ -0,0 +1,41 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'package:stream_chat_flutter_example/debug/error_dialog.dart'; + +class DebugRemoveShadowBan extends StatelessWidget { + const DebugRemoveShadowBan({ + super.key, + required this.client, + }); + + final StreamChatClient client; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8), + child: TextField( + decoration: const InputDecoration( + labelText: 'Remove Shadow Ban', + hintText: 'User Id', + isDense: true, + border: OutlineInputBorder(), + ), + onSubmitted: (value) async { + final userId = value.trim(); + try { + debugPrint('[removeShadowBan] userId: $userId'); + final result = await client.removeShadowBan(userId); + debugPrint('[removeShadowBan] result: $result'); + } catch (e) { + debugPrint('[removeShadowBan] failed: $e'); + showErrorDialog(context, e, 'Remove Shadow Ban'); + } + }, + ), + ); + } +} diff --git a/packages/stream_chat_flutter/example/lib/debug/actions/shadow_ban.dart b/packages/stream_chat_flutter/example/lib/debug/actions/shadow_ban.dart new file mode 100644 index 00000000..9e5c5553 --- /dev/null +++ b/packages/stream_chat_flutter/example/lib/debug/actions/shadow_ban.dart @@ -0,0 +1,41 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'package:stream_chat_flutter_example/debug/error_dialog.dart'; + +class DebugShadowBan extends StatelessWidget { + const DebugShadowBan({ + super.key, + required this.client, + }); + + final StreamChatClient client; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8), + child: TextField( + decoration: const InputDecoration( + labelText: 'Shadow Ban', + hintText: 'User Id', + isDense: true, + border: OutlineInputBorder(), + ), + onSubmitted: (value) async { + final userId = value.trim(); + try { + debugPrint('[shadowBan] userId: $userId'); + final result = await client.shadowBan(userId); + debugPrint('[shadowBan] completed: $result'); + } catch (e) { + debugPrint('[shadowBan] failed: $e'); + showErrorDialog(context, e, 'Shadow Ban'); + } + }, + ), + ); + } +} diff --git a/packages/stream_chat_flutter/example/lib/debug/actions/unban_user.dart b/packages/stream_chat_flutter/example/lib/debug/actions/unban_user.dart new file mode 100644 index 00000000..5a9ac755 --- /dev/null +++ b/packages/stream_chat_flutter/example/lib/debug/actions/unban_user.dart @@ -0,0 +1,41 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'package:stream_chat_flutter_example/debug/error_dialog.dart'; + +class DebugUnbanUser extends StatelessWidget { + const DebugUnbanUser({ + super.key, + required this.client, + }); + + final StreamChatClient client; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8), + child: TextField( + decoration: const InputDecoration( + labelText: 'Unban User', + hintText: 'User Id', + isDense: true, + border: OutlineInputBorder(), + ), + onSubmitted: (value) async { + final userId = value.trim(); + try { + debugPrint('[unbanUser] userId: $userId'); + final result = await client.unbanUser(userId); + debugPrint('[unbanUser] completed: $result'); + } catch (e) { + debugPrint('[unbanUser] failed: $e'); + showErrorDialog(context, e, 'Unban User'); + } + }, + ), + ); + } +} diff --git a/packages/stream_chat_flutter/example/lib/debug/actions/unmute_user.dart b/packages/stream_chat_flutter/example/lib/debug/actions/unmute_user.dart new file mode 100644 index 00000000..359ae341 --- /dev/null +++ b/packages/stream_chat_flutter/example/lib/debug/actions/unmute_user.dart @@ -0,0 +1,41 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'package:stream_chat_flutter_example/debug/error_dialog.dart'; + +class DebugUnmuteUser extends StatelessWidget { + const DebugUnmuteUser({ + super.key, + required this.client, + }); + + final StreamChatClient client; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8), + child: TextField( + decoration: const InputDecoration( + labelText: 'Unmute User', + hintText: 'User Id', + isDense: true, + border: OutlineInputBorder(), + ), + onSubmitted: (value) async { + final userId = value.trim(); + try { + debugPrint('[unmuteUser] userId: $userId'); + final result = await client.unmuteUser(userId); + debugPrint('[unmuteUser] completed: $result'); + } catch (e) { + debugPrint('[unmuteUser] failed: $e'); + showErrorDialog(context, e, 'Unmute User'); + } + }, + ), + ); + } +} diff --git a/packages/stream_chat_flutter/example/lib/debug/channel_page.dart b/packages/stream_chat_flutter/example/lib/debug/channel_page.dart new file mode 100644 index 00000000..b5e93cf5 --- /dev/null +++ b/packages/stream_chat_flutter/example/lib/debug/channel_page.dart @@ -0,0 +1,126 @@ +// ignore_for_file: public_member_api_docs + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_chat_flutter_example/debug/actions/ban_user.dart'; +import 'package:stream_chat_flutter_example/debug/actions/mute_user.dart'; +import 'package:stream_chat_flutter_example/debug/actions/remove_shadow_ban.dart'; +import 'package:stream_chat_flutter_example/debug/actions/shadow_ban.dart'; +import 'package:stream_chat_flutter_example/debug/actions/unban_user.dart'; +import 'package:stream_chat_flutter_example/debug/actions/unmute_user.dart'; +import 'package:stream_chat_flutter_example/debug/members.dart'; +import 'package:stream_chat_flutter_example/debug/mutes.dart'; + +class DebugChannelPage extends StatefulWidget { + const DebugChannelPage({super.key}); + + @override + State createState() { + return _DebugChannelPageState(); + } +} + +class _DebugChannelPageState extends State { + late final Channel _channel = StreamChannel.of(context).channel; + + StreamSubscription? _channelSubscription; + StreamSubscription? _ownUserSubscription; + + ChannelState? _channelState; + OwnUser? _ownUser; + + @override + void initState() { + super.initState(); + _channelSubscription = _channel.state!.channelStateStream.listen((state) { + setState(() => _channelState = state); + }); + _ownUserSubscription = + _channel.client.state.currentUserStream.listen((ownUser) { + setState(() => _ownUser = ownUser); + }); + } + + @override + void dispose() { + super.dispose(); + _channelSubscription?.cancel(); + _ownUserSubscription?.cancel(); + } + + @override + Widget build(BuildContext context) { + final members = + _channelState?.members ?? _channel.state?.members ?? const []; + final mutes = + _ownUser?.mutes ?? _channel.client.state.currentUser?.mutes ?? const []; + //SingleChildScrollView + return Scaffold( + appBar: AppBar( + title: Text(_channel.name ?? _channel.cid ?? '?'), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.only(bottom: 16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + DebugMe(client: _channel.client), + DebugMembers(members: members), + const SizedBox(height: 8), + DebugMutes(mutes: mutes), + const SizedBox(height: 16), + DebugMuteUser(client: _channel.client), + const SizedBox(height: 8), + DebugUnmuteUser(client: _channel.client), + const SizedBox(height: 8), + DebugBanUser(client: _channel.client), + const SizedBox(height: 8), + DebugUnbanUser(client: _channel.client), + const SizedBox(height: 8), + DebugShadowBan(client: _channel.client), + const SizedBox(height: 8), + DebugRemoveShadowBan(client: _channel.client), + ], + ), + ), + ); + } +} + +class DebugMe extends StatelessWidget { + const DebugMe({ + super.key, + required this.client, + }); + + final StreamChatClient client; + + @override + Widget build(BuildContext context) { + return Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12), + child: Row( + children: [ + const Text( + 'Me: ', + style: TextStyle( + color: Colors.red, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + Text( + client.state.currentUser?.id ?? '?', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ); + } +} diff --git a/packages/stream_chat_flutter/example/lib/debug/error_dialog.dart b/packages/stream_chat_flutter/example/lib/debug/error_dialog.dart new file mode 100644 index 00000000..6e7fb750 --- /dev/null +++ b/packages/stream_chat_flutter/example/lib/debug/error_dialog.dart @@ -0,0 +1,28 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; + +Future showErrorDialog( + BuildContext context, + Object e, + String operation, +) async { + return showDialog( + context: context, + barrierDismissible: false, // user must tap button! + builder: (BuildContext context) { + return AlertDialog( + title: Text('$operation Failed'), + content: SingleChildScrollView(child: Text('$e')), + actions: [ + TextButton( + child: const Text('Close'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ); + }, + ); +} diff --git a/packages/stream_chat_flutter/example/lib/debug/members.dart b/packages/stream_chat_flutter/example/lib/debug/members.dart new file mode 100644 index 00000000..d0214e00 --- /dev/null +++ b/packages/stream_chat_flutter/example/lib/debug/members.dart @@ -0,0 +1,57 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +class DebugMembers extends StatelessWidget { + const DebugMembers({ + super.key, + required this.members, + }); + + final List members; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + color: Colors.orange, + padding: const EdgeInsets.all(8), + child: const Text('Members'), + ), + Container( + color: Colors.orange, + height: 100, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: members.length, + itemBuilder: (BuildContext context, int index) { + final member = members[index]; + return Padding( + padding: const EdgeInsets.all(8), + child: ColoredBox( + color: Colors.yellow, + child: Padding( + padding: const EdgeInsets.all(8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text(member.user?.name ?? '?'), + Text('ID: ${member.user?.id ?? '?'}'), + Text('Ban: ${member.banned ? 'T' : 'F'}'), + Text('ShBan: ${member.shadowBanned ? 'T' : 'F'}'), + ], + ), + ), + ), + ); + }, + ), + ), + ], + ); + } +} diff --git a/packages/stream_chat_flutter/example/lib/debug/mutes.dart b/packages/stream_chat_flutter/example/lib/debug/mutes.dart new file mode 100644 index 00000000..fe54b06e --- /dev/null +++ b/packages/stream_chat_flutter/example/lib/debug/mutes.dart @@ -0,0 +1,55 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +class DebugMutes extends StatelessWidget { + const DebugMutes({super.key, required this.mutes}); + + final List mutes; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + color: Colors.lightBlueAccent, + padding: const EdgeInsets.all(8), + child: const Text('Mutes'), + ), + Container( + color: Colors.lightBlueAccent, + height: 80, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: mutes.length, + itemBuilder: (BuildContext context, int index) { + final mute = mutes[index]; + return Padding( + padding: const EdgeInsets.all(8), + child: ColoredBox( + color: Colors.yellow, + child: Padding( + padding: const EdgeInsets.all(8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text('By: ${mute.user.name} (${mute.user.id})'), + Text( + 'Who: ${mute.target.name} (${mute.target.id})', + ), + Text('Exp: ${mute.expires}'), + ], + ), + ), + ), + ); + }, + ), + ), + ], + ); + } +} diff --git a/packages/stream_chat_flutter/example/lib/main.dart b/packages/stream_chat_flutter/example/lib/main.dart index e4f291fe..f0acc0fa 100644 --- a/packages/stream_chat_flutter/example/lib/main.dart +++ b/packages/stream_chat_flutter/example/lib/main.dart @@ -1,8 +1,11 @@ // ignore_for_file: public_member_api_docs +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:responsive_builder/responsive_builder.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_chat_flutter_example/debug/channel_page.dart'; import 'package:stream_chat_localizations/stream_chat_localizations.dart'; Future main() async { @@ -255,6 +258,19 @@ class _ChannelPageState extends State { widget.onBackPressed!(context); } : null, + onImageTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return StreamChannel( + channel: StreamChannel.of(context).channel, + child: const DebugChannelPage(), + ); + }, + ), + ); + }, showBackButton: widget.showBackButton, ), body: Column( diff --git a/packages/stream_chat_flutter/lib/src/avatars/group_avatar.dart b/packages/stream_chat_flutter/lib/src/avatars/group_avatar.dart index 0571c7fd..be70bdbf 100644 --- a/packages/stream_chat_flutter/lib/src/avatars/group_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/avatars/group_avatar.dart @@ -1,6 +1,14 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// WidgetBuilder for [StreamGroupAvatar]. +typedef StreamGroupAvatarBuilder = Widget Function( + BuildContext context, + List members, + // ignore: avoid_positional_boolean_parameters + bool isSelected, +); + /// {@template streamGroupAvatar} /// Widget for constructing a group of images /// {@endtemplate} diff --git a/packages/stream_chat_flutter/lib/src/avatars/user_avatar.dart b/packages/stream_chat_flutter/lib/src/avatars/user_avatar.dart index 2290fca9..72ec020b 100644 --- a/packages/stream_chat_flutter/lib/src/avatars/user_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/avatars/user_avatar.dart @@ -2,6 +2,14 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// WidgetBuilder for [StreamUserAvatar]. +typedef StreamUserAvatarBuilder = Widget Function( + BuildContext context, + User user, + // ignore: avoid_positional_boolean_parameters + bool isSelected, +); + /// {@template streamUserAvatar} /// Displays a user's avatar. /// {@endtemplate} diff --git a/packages/stream_chat_flutter/lib/src/channel/stream_channel_avatar.dart b/packages/stream_chat_flutter/lib/src/channel/stream_channel_avatar.dart index 5ad7a000..9090c33a 100644 --- a/packages/stream_chat_flutter/lib/src/channel/stream_channel_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/channel/stream_channel_avatar.dart @@ -54,6 +54,9 @@ class StreamChannelAvatar extends StatelessWidget { this.selected = false, this.selectionColor, this.selectionThickness = 4, + this.ownSpaceAvatarBuilder, + this.oneToOneAvatarBuilder, + this.groupAvatarBuilder, }) : assert( channel.state != null, 'Channel ${channel.id} is not initialized', @@ -80,6 +83,21 @@ class StreamChannelAvatar extends StatelessWidget { /// Thickness of selection image final double selectionThickness; + /// Builder to create avatar for own space channel. + /// + /// Defaults to [StreamUserAvatar]. + final StreamUserAvatarBuilder? ownSpaceAvatarBuilder; + + /// Builder to create avatar for one to one channel. + /// + /// Defaults to [StreamUserAvatar]. + final StreamUserAvatarBuilder? oneToOneAvatarBuilder; + + /// Builder to create avatar for group channel. + /// + /// Defaults to [StreamGroupAvatar]. + final StreamGroupAvatarBuilder? groupAvatarBuilder; + @override Widget build(BuildContext context) { final client = channel.client.state; @@ -146,15 +164,22 @@ class StreamChannelAvatar extends StatelessWidget { return BetterStreamBuilder( stream: client.currentUserStream.map((it) => it!), initialData: currentUser, - builder: (context, user) => StreamUserAvatar( - borderRadius: borderRadius ?? previewTheme?.borderRadius, - user: user, - constraints: constraints ?? previewTheme?.constraints, - onTap: onTap != null ? (_) => onTap!() : null, - selected: selected, - selectionColor: selectionColor ?? colorTheme.accentPrimary, - selectionThickness: selectionThickness, - ), + builder: (context, user) { + final ownSpaceBuilder = ownSpaceAvatarBuilder; + if (ownSpaceBuilder != null) { + return ownSpaceBuilder(context, user, selected); + } + + return StreamUserAvatar( + borderRadius: borderRadius ?? previewTheme?.borderRadius, + user: user, + constraints: constraints ?? previewTheme?.constraints, + onTap: onTap != null ? (_) => onTap!() : null, + selected: selected, + selectionColor: selectionColor ?? colorTheme.accentPrimary, + selectionThickness: selectionThickness, + ); + }, ); } @@ -169,18 +194,30 @@ class StreamChannelAvatar extends StatelessWidget { ), ), initialData: member, - builder: (context, member) => StreamUserAvatar( - borderRadius: borderRadius ?? previewTheme?.borderRadius, - user: member.user!, - constraints: constraints ?? previewTheme?.constraints, - onTap: onTap != null ? (_) => onTap!() : null, - selected: selected, - selectionColor: selectionColor ?? colorTheme.accentPrimary, - selectionThickness: selectionThickness, - ), + builder: (context, member) { + final oneToOneBuilder = oneToOneAvatarBuilder; + if (oneToOneBuilder != null) { + return oneToOneBuilder(context, member.user!, selected); + } + + return StreamUserAvatar( + borderRadius: borderRadius ?? previewTheme?.borderRadius, + user: member.user!, + constraints: constraints ?? previewTheme?.constraints, + onTap: onTap != null ? (_) => onTap!() : null, + selected: selected, + selectionColor: selectionColor ?? colorTheme.accentPrimary, + selectionThickness: selectionThickness, + ); + }, ); } + final groupBuilder = groupAvatarBuilder; + if (groupBuilder != null) { + return groupBuilder(context, otherMembers, selected); + } + // Group conversation return StreamGroupAvatar( channel: channel, diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal/message_actions_modal.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal/message_actions_modal.dart index b6c43c76..7fcda8a9 100644 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal/message_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal/message_actions_modal.dart @@ -19,6 +19,7 @@ class MessageActionsModal extends StatefulWidget { this.showDeleteMessage = true, this.showEditMessage = true, this.onReplyTap, + this.onConfirmDeleteTap, this.onThreadReplyTap, this.showCopyMessage = true, this.showReplyMessage = true, @@ -44,6 +45,9 @@ class MessageActionsModal extends StatefulWidget { /// The action to perform when "reply" is tapped final OnMessageTap? onReplyTap; + /// The action to perform when delete confirmation button is tapped. + final Future Function(Message)? onConfirmDeleteTap; + /// Message in focus for actions final Message message; @@ -363,7 +367,12 @@ class _MessageActionsModalState extends State { if (answer == true) { try { Navigator.of(context).pop(); - await StreamChannel.of(context).channel.deleteMessage(widget.message); + final onConfirmDeleteTap = widget.onConfirmDeleteTap; + if (onConfirmDeleteTap != null) { + await onConfirmDeleteTap(widget.message); + } else { + await StreamChannel.of(context).channel.deleteMessage(widget.message); + } } catch (err) { _showErrorAlertBottomSheet(); } diff --git a/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/stream_attachment_picker.dart b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/stream_attachment_picker.dart index c3de7e13..6c8f1f59 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/stream_attachment_picker.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/stream_attachment_picker.dart @@ -693,6 +693,7 @@ Widget mobileAttachmentPickerBuilder({ required BuildContext context, required StreamAttachmentPickerController controller, Iterable? customOptions, + List allowedTypes = AttachmentPickerType.values, ThumbnailSize attachmentThumbnailSize = const ThumbnailSize(400, 400), ThumbnailFormat attachmentThumbnailFormat = ThumbnailFormat.jpeg, int attachmentThumbnailQuality = 100, @@ -702,74 +703,76 @@ Widget mobileAttachmentPickerBuilder({ controller: controller, onSendAttachments: Navigator.of(context).pop, options: { - if (customOptions != null) ...customOptions, - AttachmentPickerOption( - key: 'gallery-picker', - icon: StreamSvgIcon.pictures(size: 36).toIconThemeSvgIcon(), - supportedTypes: [ - AttachmentPickerType.images, - AttachmentPickerType.videos, - ], - optionViewBuilder: (context, controller) { - final selectedIds = controller.value.map((it) => it.id); - return StreamGalleryPicker( - selectedMediaItems: selectedIds, - mediaThumbnailSize: attachmentThumbnailSize, - mediaThumbnailFormat: attachmentThumbnailFormat, - mediaThumbnailQuality: attachmentThumbnailQuality, - mediaThumbnailScale: attachmentThumbnailScale, - onMediaItemSelected: (media) async { - if (selectedIds.contains(media.id)) { - return controller.removeAssetAttachment(media); - } - return controller.addAssetAttachment(media); - }, - ); - }, - ), - AttachmentPickerOption( - key: 'file-picker', - icon: StreamSvgIcon.files(size: 36).toIconThemeSvgIcon(), - supportedTypes: [AttachmentPickerType.files], - optionViewBuilder: (context, controller) { - return StreamFilePicker( - onFilePicked: (file) async { - if (file != null) await controller.addAttachment(file); - return Navigator.pop(context, controller.value); - }, - ); - }, - ), - AttachmentPickerOption( - key: 'image-picker', - icon: StreamSvgIcon.camera(size: 36).toIconThemeSvgIcon(), - supportedTypes: [AttachmentPickerType.images], - optionViewBuilder: (context, controller) { - return StreamImagePicker( - onImagePicked: (image) async { - if (image != null) { - await controller.addAttachment(image); - } - return Navigator.pop(context, controller.value); - }, - ); - }, - ), - AttachmentPickerOption( - key: 'video-picker', - icon: StreamSvgIcon.record(size: 36).toIconThemeSvgIcon(), - supportedTypes: [AttachmentPickerType.videos], - optionViewBuilder: (context, controller) { - return StreamVideoPicker( - onVideoPicked: (video) async { - if (video != null) { - await controller.addAttachment(video); - } - return Navigator.pop(context, controller.value); - }, - ); - }, - ), + ...{ + if (customOptions != null) ...customOptions, + AttachmentPickerOption( + key: 'gallery-picker', + icon: StreamSvgIcon.pictures(size: 36).toIconThemeSvgIcon(), + supportedTypes: [ + AttachmentPickerType.images, + AttachmentPickerType.videos, + ], + optionViewBuilder: (context, controller) { + final selectedIds = controller.value.map((it) => it.id); + return StreamGalleryPicker( + selectedMediaItems: selectedIds, + mediaThumbnailSize: attachmentThumbnailSize, + mediaThumbnailFormat: attachmentThumbnailFormat, + mediaThumbnailQuality: attachmentThumbnailQuality, + mediaThumbnailScale: attachmentThumbnailScale, + onMediaItemSelected: (media) async { + if (selectedIds.contains(media.id)) { + return controller.removeAssetAttachment(media); + } + return controller.addAssetAttachment(media); + }, + ); + }, + ), + AttachmentPickerOption( + key: 'file-picker', + icon: StreamSvgIcon.files(size: 36).toIconThemeSvgIcon(), + supportedTypes: [AttachmentPickerType.files], + optionViewBuilder: (context, controller) { + return StreamFilePicker( + onFilePicked: (file) async { + if (file != null) await controller.addAttachment(file); + return Navigator.pop(context, controller.value); + }, + ); + }, + ), + AttachmentPickerOption( + key: 'image-picker', + icon: StreamSvgIcon.camera(size: 36).toIconThemeSvgIcon(), + supportedTypes: [AttachmentPickerType.images], + optionViewBuilder: (context, controller) { + return StreamImagePicker( + onImagePicked: (image) async { + if (image != null) { + await controller.addAttachment(image); + } + return Navigator.pop(context, controller.value); + }, + ); + }, + ), + AttachmentPickerOption( + key: 'video-picker', + icon: StreamSvgIcon.record(size: 36).toIconThemeSvgIcon(), + supportedTypes: [AttachmentPickerType.videos], + optionViewBuilder: (context, controller) { + return StreamVideoPicker( + onVideoPicked: (video) async { + if (video != null) { + await controller.addAttachment(video); + } + return Navigator.pop(context, controller.value); + }, + ); + }, + ), + }..where((option) => option.supportedTypes.every(allowedTypes.contains)), }, ); } @@ -779,6 +782,7 @@ Widget webOrDesktopAttachmentPickerBuilder({ required BuildContext context, required StreamAttachmentPickerController controller, Iterable? customOptions, + List allowedTypes = AttachmentPickerType.values, ThumbnailSize attachmentThumbnailSize = const ThumbnailSize(400, 400), ThumbnailFormat attachmentThumbnailFormat = ThumbnailFormat.jpeg, int attachmentThumbnailQuality = 100, @@ -787,25 +791,27 @@ Widget webOrDesktopAttachmentPickerBuilder({ return StreamWebOrDesktopAttachmentPickerBottomSheet( controller: controller, options: { - if (customOptions != null) ...customOptions, - WebOrDesktopAttachmentPickerOption( - key: 'image-picker', - type: AttachmentPickerType.images, - icon: StreamSvgIcon.pictures(size: 36).toIconThemeSvgIcon(), - title: context.translations.uploadAPhotoLabel, - ), - WebOrDesktopAttachmentPickerOption( - key: 'video-picker', - type: AttachmentPickerType.videos, - icon: StreamSvgIcon.record(size: 36).toIconThemeSvgIcon(), - title: context.translations.uploadAVideoLabel, - ), - WebOrDesktopAttachmentPickerOption( - key: 'file-picker', - type: AttachmentPickerType.files, - icon: StreamSvgIcon.files(size: 36).toIconThemeSvgIcon(), - title: context.translations.uploadAFileLabel, - ), + ...{ + if (customOptions != null) ...customOptions, + WebOrDesktopAttachmentPickerOption( + key: 'image-picker', + type: AttachmentPickerType.images, + icon: StreamSvgIcon.pictures(size: 36).toIconThemeSvgIcon(), + title: context.translations.uploadAPhotoLabel, + ), + WebOrDesktopAttachmentPickerOption( + key: 'video-picker', + type: AttachmentPickerType.videos, + icon: StreamSvgIcon.record(size: 36).toIconThemeSvgIcon(), + title: context.translations.uploadAVideoLabel, + ), + WebOrDesktopAttachmentPickerOption( + key: 'file-picker', + type: AttachmentPickerType.files, + icon: StreamSvgIcon.files(size: 36).toIconThemeSvgIcon(), + title: context.translations.uploadAFileLabel, + ), + }.where((option) => option.supportedTypes.every(allowedTypes.contains)), }, onOptionTap: (context, controller, option) async { final attachment = await StreamAttachmentHandler.instance.pickFile( diff --git a/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/stream_attachment_picker_bottom_sheet.dart b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/stream_attachment_picker_bottom_sheet.dart index 11fb966b..bc53e4db 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/stream_attachment_picker_bottom_sheet.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/stream_attachment_picker_bottom_sheet.dart @@ -66,6 +66,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; Future showStreamAttachmentPickerModalBottomSheet({ required BuildContext context, Iterable? customOptions, + List allowedTypes = AttachmentPickerType.values, List? initialAttachments, StreamAttachmentPickerController? controller, Color? backgroundColor, @@ -117,6 +118,7 @@ Future showStreamAttachmentPickerModalBottomSheet({ return webOrDesktopAttachmentPickerBuilder.call( context: context, controller: controller, + allowedTypes: allowedTypes, customOptions: customOptions?.map( WebOrDesktopAttachmentPickerOption.fromAttachmentPickerOption, ), @@ -130,6 +132,7 @@ Future showStreamAttachmentPickerModalBottomSheet({ return mobileAttachmentPickerBuilder.call( context: context, controller: controller, + allowedTypes: allowedTypes, customOptions: customOptions, attachmentThumbnailSize: attachmentThumbnailSize, attachmentThumbnailFormat: attachmentThumbnailFormat, diff --git a/packages/stream_chat_flutter/lib/src/message_input/quoted_message_widget.dart b/packages/stream_chat_flutter/lib/src/message_input/quoted_message_widget.dart index 554e8d12..a61ced63 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/quoted_message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/quoted_message_widget.dart @@ -20,7 +20,6 @@ class StreamQuotedMessageWidget extends StatelessWidget { this.textLimit = 170, this.attachmentThumbnailBuilders, this.padding = const EdgeInsets.all(8), - this.onTap, this.onQuotedMessageClear, }); @@ -46,9 +45,6 @@ class StreamQuotedMessageWidget extends StatelessWidget { /// Padding around the widget final EdgeInsetsGeometry padding; - /// Callback for tap on widget - final GestureTapCallback? onTap; - /// Callback for clearing quoted messages. final VoidCallback? onQuotedMessageClear; @@ -77,19 +73,12 @@ class StreamQuotedMessageWidget extends StatelessWidget { showOnlineStatus: false, ), ]; - return MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: onTap, - child: Padding( - padding: padding, - child: Row( - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: reverse ? children.reversed.toList() : children, - ), - ), + return Padding( + padding: padding, + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: reverse ? children.reversed.toList() : children, ), ); } @@ -258,23 +247,26 @@ class _ParseAttachments extends StatelessWidget { child = attachmentBuilder(context, attachment); } } - child = AbsorbPointer(child: child); + + final isImageFile = attachment.title?.mimeType?.type == 'image'; + final isVideoFile = attachment.title?.mimeType?.type == 'video'; + return Material( clipBehavior: Clip.hardEdge, type: MaterialType.transparency, - shape: attachment.type == 'file' + shape: attachment.type == 'file' && (!isImageFile && !isVideoFile) ? null : RoundedRectangleBorder( side: const BorderSide(width: 0, color: Colors.transparent), borderRadius: BorderRadius.circular(8), ), - child: child, + child: AbsorbPointer(child: child), ); } Map get _defaultAttachmentBuilder { - return { + final builders = { 'image': (_, attachment) { return StreamImageAttachment( attachment: attachment, @@ -315,16 +307,33 @@ class _ParseAttachments extends StatelessWidget { fit: BoxFit.cover, ); }, - 'file': (_, attachment) { - return SizedBox( - height: 32, - width: 32, - child: getFileTypeImage( - attachment.extraData['mime_type'] as String?, - ), - ); - }, }; + + builders['file'] = (_, attachment) { + return SizedBox( + height: 32, + width: 32, + child: Builder( + builder: (context) { + final isImageFile = attachment.title?.mimeType?.type == 'image'; + if (isImageFile) { + return builders['image']!(context, attachment); + } + + final isVideoFile = attachment.title?.mimeType?.type == 'video'; + if (isVideoFile) { + return builders['video']!(context, attachment); + } + + return getFileTypeImage( + attachment.extraData['mime_type'] as String?, + ); + }, + ), + ); + }; + + return builders; } } diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart index c3a7f7ef..d4581ea0 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart @@ -122,12 +122,14 @@ class StreamMessageInput extends StatefulWidget { this.maxAttachmentSize = kDefaultMaxAttachmentSize, this.onError, this.attachmentLimit = 10, + this.allowedAttachmentPickerTypes = AttachmentPickerType.values, this.onAttachmentLimitExceed, this.attachmentButtonBuilder, this.commandButtonBuilder, this.customAutocompleteTriggers = const [], this.mentionAllAppUsers = false, this.sendButtonBuilder, + this.quotedMessageBuilder, this.shouldKeepFocusAfterMessage, this.validator = _defaultValidator, this.restorationId, @@ -143,6 +145,7 @@ class StreamMessageInput extends StatefulWidget { _defaultClearQuotedMessageKeyPredicate, this.ogPreviewFilter = _defaultOgPreviewFilter, this.hintGetter = _defaultHintGetter, + this.contentInsertionConfiguration, }); /// The predicate used to send a message on desktop/web @@ -233,6 +236,12 @@ class StreamMessageInput extends StatefulWidget { /// A limit for the no. of attachments that can be sent with a single message. final int attachmentLimit; + /// The list of allowed attachment types which can be picked using the + /// attachment button. + /// + /// By default, all the attachment types are allowed. + final List allowedAttachmentPickerTypes; + /// A callback for when the [attachmentLimit] is exceeded. /// /// This will override the default error alert behaviour. @@ -258,6 +267,9 @@ class StreamMessageInput extends StatefulWidget { /// Builder for creating send button final MessageRelatedBuilder? sendButtonBuilder; + /// Builder for building quoted message + final Widget Function(BuildContext, Message)? quotedMessageBuilder; + /// Defines if the [StreamMessageInput] loses focuses after a message is sent. /// The default behaviour keeps focus until a command is enabled. final bool? shouldKeepFocusAfterMessage; @@ -295,6 +307,9 @@ class StreamMessageInput extends StatefulWidget { /// Returns the hint text for the message input. final HintGetter hintGetter; + /// {@macro flutter.widgets.editableText.contentInsertionConfiguration} + final ContentInsertionConfiguration? contentInsertionConfiguration; + static String? _defaultHintGetter( BuildContext context, HintType type, @@ -771,8 +786,8 @@ class StreamMessageInputState extends State Future _onAttachmentButtonPressed() async { final attachments = await showStreamAttachmentPickerModalBottomSheet( context: context, + allowedTypes: widget.allowedAttachmentPickerTypes, initialAttachments: _effectiveController.attachments, - useRootNavigator: true, ); if (attachments != null) { @@ -860,6 +875,8 @@ class StreamMessageInputState extends State decoration: _getInputDecoration(context), textCapitalization: widget.textCapitalization, autocorrect: widget.autoCorrect, + contentInsertionConfiguration: + widget.contentInsertionConfiguration, ), ), ), @@ -1118,14 +1135,20 @@ class StreamMessageInputState extends State if (!_hasQuotedMessage) return const Offstage(); final containsUrl = _effectiveController.message.quotedMessage!.attachments .any((element) => element.titleLink != null); - return StreamQuotedMessageWidget( - reverse: true, - showBorder: !containsUrl, - message: _effectiveController.message.quotedMessage!, - messageTheme: _streamChatTheme.otherMessageTheme, - padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), - onQuotedMessageClear: widget.onQuotedMessageCleared, - ); + + return widget.quotedMessageBuilder?.call( + context, + _effectiveController.message.quotedMessage!, + ) ?? + StreamQuotedMessageWidget( + reverse: true, + showBorder: !containsUrl, + message: _effectiveController.message.quotedMessage!, + messageTheme: _streamChatTheme.otherMessageTheme, + padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), + onQuotedMessageClear: widget.onQuotedMessageCleared, + attachmentThumbnailBuilders: widget.attachmentThumbnailBuilders, + ); } Widget _buildAttachments() { diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart index a04ab829..292c9141 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart @@ -120,6 +120,7 @@ class StreamMessageTextField extends StatefulWidget { this.restorationId, this.scribbleEnabled = true, this.enableIMEPersonalizedLearning = true, + this.contentInsertionConfiguration, }) : assert(obscuringCharacter.length == 1, ''), smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled), @@ -526,6 +527,9 @@ class StreamMessageTextField extends StatefulWidget { /// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning} final bool enableIMEPersonalizedLearning; + /// {@macro flutter.widgets.editableText.contentInsertionConfiguration} + final ContentInsertionConfiguration? contentInsertionConfiguration; + @override _StreamMessageTextFieldState createState() => _StreamMessageTextFieldState(); @@ -622,6 +626,9 @@ class StreamMessageTextField extends StatefulWidget { properties.add(DiagnosticsProperty( 'enableIMEPersonalizedLearning', enableIMEPersonalizedLearning, defaultValue: true)); + properties.add(DiagnosticsProperty( + 'contentInsertionConfiguration', contentInsertionConfiguration, + defaultValue: null)); } } @@ -727,6 +734,7 @@ class _StreamMessageTextFieldState extends State restorationId: widget.restorationId, scribbleEnabled: widget.scribbleEnabled, enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, + contentInsertionConfiguration: widget.contentInsertionConfiguration, ); @override diff --git a/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart index 4c29eb69..8863c991 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart @@ -852,20 +852,25 @@ class _StreamMessageListViewState extends State { streamChannel!.channel.markRead(); } - final index = unreadCount > 0 ? unreadCount + 1 : 0; - + // If the channel is not up to date, we need to reload it before scrolling + // to the end of the list. if (!_upToDate) { - _bottomPaginationActive = false; - initialAlignment = 0; + // Reset the pagination variables. initialIndex = 0; + initialAlignment = 0; + _bottomPaginationActive = false; + + // Reload the channel to get the latest messages. await streamChannel!.reloadChannel(); - WidgetsBinding.instance.addPostFrameCallback((_) { - _scrollController!.jumpTo(index: index); - }); - } else { + // Wait for the frame to be rendered with the updated channel state. + await WidgetsBinding.instance.endOfFrame; + } + + // Scroll to the end of the list. + if (_scrollController?.isAttached == true) { _scrollController!.scrollTo( - index: index, + index: 0, duration: const Duration(seconds: 1), curve: Curves.easeInOut, ); diff --git a/packages/stream_chat_flutter/lib/src/message_widget/message_card.dart b/packages/stream_chat_flutter/lib/src/message_widget/message_card.dart index e7f1d506..5cd93efc 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/message_card.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/message_card.dart @@ -29,6 +29,7 @@ class MessageCard extends StatefulWidget { this.borderSide, this.borderRadiusGeometry, this.textBuilder, + this.quotedMessageBuilder, this.onLinkTap, this.onMentionTap, this.onQuotedMessageTap, @@ -82,6 +83,9 @@ class MessageCard extends StatefulWidget { /// {@macro textBuilder} final Widget Function(BuildContext, Message)? textBuilder; + /// {@macro quotedMessageBuilder} + final Widget Function(BuildContext, Message)? quotedMessageBuilder; + /// {@macro onLinkTap} final void Function(String)? onLinkTap; @@ -129,8 +133,12 @@ class _MessageCardState extends State { @override Widget build(BuildContext context) { + final onQuotedMessageTap = widget.onQuotedMessageTap; + final quotedMessageBuilder = widget.quotedMessageBuilder; + return Card( elevation: 0, + clipBehavior: Clip.hardEdge, margin: EdgeInsets.symmetric( horizontal: (widget.isFailedState ? 15.0 : 0.0) + (widget.showUserAvatar == DisplayWidget.gone ? 0 : 4.0), @@ -150,15 +158,27 @@ class _MessageCardState extends State { maxWidth: widthLimit ?? double.infinity, ), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ if (widget.hasQuotedMessage) - QuotedMessage( - reverse: widget.reverse, - message: widget.message, - hasNonUrlAttachments: widget.hasNonUrlAttachments, - onQuotedMessageTap: widget.onQuotedMessageTap, + MouseRegion( + cursor: SystemMouseCursors.click, + child: InkWell( + onTap: !widget.message.quotedMessage!.isDeleted && + onQuotedMessageTap != null + ? () => onQuotedMessageTap(widget.message.quotedMessageId) + : null, + child: quotedMessageBuilder?.call( + context, + widget.message.quotedMessage!, + ) ?? + QuotedMessage( + reverse: widget.reverse, + message: widget.message, + hasNonUrlAttachments: widget.hasNonUrlAttachments, + ), + ), ), if (widget.hasNonUrlAttachments) ParseAttachments( diff --git a/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart index b002d2b5..61332a3c 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart @@ -61,6 +61,7 @@ class StreamMessageWidget extends StatefulWidget { this.showInChannelIndicator = false, this.onReplyTap, this.onThreadTap, + this.onConfirmDeleteTap, this.showUsername = true, this.showTimestamp = true, this.showReactions = true, @@ -78,6 +79,7 @@ class StreamMessageWidget extends StatefulWidget { this.onMessageActions, this.onShowMessage, this.userAvatarBuilder, + this.quotedMessageBuilder, this.editMessageInputBuilder, this.textBuilder, @Deprecated(''' @@ -307,6 +309,11 @@ class StreamMessageWidget extends StatefulWidget { /// {@endtemplate} final void Function(Message)? onReplyTap; + /// {@template onDeleteTap} + /// The function called when delete confirmation button is tapped. + /// {@endtemplate} + final Future Function(Message)? onConfirmDeleteTap; + /// {@template editMessageInputBuilder} /// Widget builder for edit message layout /// {@endtemplate} @@ -348,6 +355,11 @@ class StreamMessageWidget extends StatefulWidget { /// {@endtemplate} final Widget Function(BuildContext, User)? userAvatarBuilder; + /// {@template quotedMessageBuilder} + /// Widget builder for building quoted message + /// {@endtemplate} + final Widget Function(BuildContext, Message)? quotedMessageBuilder; + /// {@template message} /// The message to display. /// {@endtemplate} @@ -568,8 +580,10 @@ class StreamMessageWidget extends StatefulWidget { void Function(User)? onMentionTap, void Function(Message)? onThreadTap, void Function(Message)? onReplyTap, + Future Function(Message)? onConfirmDeleteTap, Widget Function(BuildContext, Message)? editMessageInputBuilder, Widget Function(BuildContext, Message)? textBuilder, + Widget Function(BuildContext, Message)? quotedMessageBuilder, @Deprecated(''' Use [bottomRowBuilderWithDefaultWidget] instead. Will be removed in the next major version. @@ -659,9 +673,11 @@ class StreamMessageWidget extends StatefulWidget { onMentionTap: onMentionTap ?? this.onMentionTap, onThreadTap: onThreadTap ?? this.onThreadTap, onReplyTap: onReplyTap ?? this.onReplyTap, + onConfirmDeleteTap: onConfirmDeleteTap ?? this.onConfirmDeleteTap, editMessageInputBuilder: editMessageInputBuilder ?? this.editMessageInputBuilder, textBuilder: textBuilder ?? this.textBuilder, + quotedMessageBuilder: quotedMessageBuilder ?? this.quotedMessageBuilder, bottomRowBuilderWithDefaultWidget: _bottomRowBuilderWithDefaultWidget, onMessageActions: onMessageActions ?? this.onMessageActions, message: message ?? this.message, @@ -957,6 +973,7 @@ class _StreamMessageWidgetState extends State borderSide: widget.borderSide, borderRadiusGeometry: widget.borderRadiusGeometry, textBuilder: widget.textBuilder, + quotedMessageBuilder: widget.quotedMessageBuilder, onLinkTap: widget.onLinkTap, onMentionTap: widget.onMentionTap, onQuotedMessageTap: widget.onQuotedMessageTap, @@ -1098,16 +1115,21 @@ class _StreamMessageWidgetState extends State ), onClick: () async { Navigator.of(context, rootNavigator: true).pop(); - final deleted = await showDialog( + final deleted = await showDialog( context: context, barrierDismissible: false, builder: (_) => const DeleteMessageDialog(), ); - if (deleted) { + if (deleted == true) { try { - await StreamChannel.of(context) - .channel - .deleteMessage(widget.message); + final onConfirmDeleteTap = widget.onConfirmDeleteTap; + if (onConfirmDeleteTap != null) { + await onConfirmDeleteTap(widget.message); + } else { + await StreamChannel.of(context) + .channel + .deleteMessage(widget.message); + } } catch (e) { showDialog( context: context, @@ -1197,21 +1219,4 @@ class _StreamMessageWidgetState extends State ), ); } - - void retryMessage(BuildContext context) { - final channel = StreamChannel.of(context).channel; - if (widget.message.status == MessageSendingStatus.failed) { - channel.sendMessage(widget.message); - return; - } - if (widget.message.status == MessageSendingStatus.failed_update) { - channel.updateMessage(widget.message); - return; - } - - if (widget.message.status == MessageSendingStatus.failed_delete) { - channel.deleteMessage(widget.message); - return; - } - } } diff --git a/packages/stream_chat_flutter/lib/src/message_widget/message_widget_content.dart b/packages/stream_chat_flutter/lib/src/message_widget/message_widget_content.dart index c9e403ad..f8543013 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/message_widget_content.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/message_widget_content.dart @@ -63,6 +63,7 @@ class MessageWidgetContent extends StatelessWidget { this.onMentionTap, this.onLinkTap, this.textBuilder, + this.quotedMessageBuilder, @Deprecated(''' Use [bottomRowBuilderWithDefaultWidget] instead. Will be removed in the next major version. @@ -170,6 +171,9 @@ class MessageWidgetContent extends StatelessWidget { /// {@macro textBuilder} final Widget Function(BuildContext, Message)? textBuilder; + /// {@macro quotedMessageBuilder} + final Widget Function(BuildContext, Message)? quotedMessageBuilder; + /// {@macro showReactionPickerIndicator} final bool showReactionPickerIndicator; @@ -351,6 +355,8 @@ class MessageWidgetContent extends StatelessWidget { onMentionTap: onMentionTap, onLinkTap: onLinkTap, textBuilder: textBuilder, + quotedMessageBuilder: + quotedMessageBuilder, borderRadiusGeometry: borderRadiusGeometry, borderSide: borderSide, diff --git a/packages/stream_chat_flutter/lib/src/message_widget/quoted_message.dart b/packages/stream_chat_flutter/lib/src/message_widget/quoted_message.dart index f75bdbe7..673853af 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/quoted_message.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/quoted_message.dart @@ -7,63 +7,41 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// /// Used in [QuotedMessageCard]. Should not be used elsewhere. /// {@endtemplate} -class QuotedMessage extends StatefulWidget { +class QuotedMessage extends StatelessWidget { /// {@macro quotedMessage} const QuotedMessage({ super.key, required this.message, required this.reverse, required this.hasNonUrlAttachments, - this.onQuotedMessageTap, }); /// {@macro message} final Message message; - /// {@macro onQuotedMessageTap} - final OnQuotedMessageTap? onQuotedMessageTap; - /// {@macro reverse} final bool reverse; /// {@macro hasNonUrlAttachments} final bool hasNonUrlAttachments; - @override - State createState() => _QuotedMessageState(); -} - -class _QuotedMessageState extends State { - late StreamChatState _streamChat; - late StreamChatThemeData _streamChatTheme; - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - _streamChatTheme = StreamChatTheme.of(context); - _streamChat = StreamChat.of(context); - } - @override Widget build(BuildContext context) { - final isMyMessage = widget.message.user?.id == _streamChat.currentUser?.id; - final onTap = widget.message.quotedMessage?.isDeleted != true && - widget.onQuotedMessageTap != null - ? () => widget.onQuotedMessageTap!(widget.message.quotedMessageId) - : null; - final chatThemeData = _streamChatTheme; + final streamChat = StreamChat.of(context); + final chatThemeData = StreamChatTheme.of(context); + + final isMyMessage = message.user?.id == streamChat.currentUser?.id; return StreamQuotedMessageWidget( - onTap: onTap, - message: widget.message.quotedMessage!, + message: message.quotedMessage!, messageTheme: isMyMessage ? chatThemeData.otherMessageTheme : chatThemeData.ownMessageTheme, - reverse: widget.reverse, + reverse: reverse, padding: EdgeInsets.only( right: 8, left: 8, top: 8, - bottom: widget.hasNonUrlAttachments ? 8 : 0, + bottom: hasNonUrlAttachments ? 8 : 0, ), ); } diff --git a/packages/stream_chat_flutter/lib/src/scroll_view/photo_gallery/stream_photo_gallery_tile.dart b/packages/stream_chat_flutter/lib/src/scroll_view/photo_gallery/stream_photo_gallery_tile.dart index b93686d0..2f03613c 100644 --- a/packages/stream_chat_flutter/lib/src/scroll_view/photo_gallery/stream_photo_gallery_tile.dart +++ b/packages/stream_chat_flutter/lib/src/scroll_view/photo_gallery/stream_photo_gallery_tile.dart @@ -202,9 +202,10 @@ class MediaThumbnailProvider extends ImageProvider { } @override - ImageStreamCompleter loadImage( + @Deprecated('Will get replaced by loadImage in the next major version.') + ImageStreamCompleter loadBuffer( MediaThumbnailProvider key, - ImageDecoderCallback decode, + DecoderBufferCallback decode, ) { return MultiFrameImageStreamCompleter( codec: _loadAsync(key, decode), @@ -219,9 +220,10 @@ class MediaThumbnailProvider extends ImageProvider { ); } + @Deprecated('Will get replaced by loadImage in the next major version.') Future _loadAsync( MediaThumbnailProvider key, - ImageDecoderCallback decode, + DecoderBufferCallback decode, ) async { assert(key == this, '$key is not $this'); final bytes = await media.thumbnailDataWithSize( diff --git a/packages/stream_chat_flutter/lib/src/scroll_view/user_scroll_view/stream_user_list_tile.dart b/packages/stream_chat_flutter/lib/src/scroll_view/user_scroll_view/stream_user_list_tile.dart index dbb0f6a5..5ee49dcc 100644 --- a/packages/stream_chat_flutter/lib/src/scroll_view/user_scroll_view/stream_user_list_tile.dart +++ b/packages/stream_chat_flutter/lib/src/scroll_view/user_scroll_view/stream_user_list_tile.dart @@ -157,6 +157,7 @@ class StreamUserListTile extends StatelessWidget { trailing: selected ? selectedWidget : null, title: title, subtitle: subtitle, + tileColor: tileColor, ); } } diff --git a/packages/stream_chat_flutter/pubspec.yaml b/packages/stream_chat_flutter/pubspec.yaml index 5342f4ab..45f43a5f 100644 --- a/packages/stream_chat_flutter/pubspec.yaml +++ b/packages/stream_chat_flutter/pubspec.yaml @@ -1,7 +1,7 @@ name: 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. -version: 6.3.0 +version: 6.4.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -38,7 +38,7 @@ dependencies: rxdart: ^0.27.0 share_plus: ^6.3.0 shimmer: ^3.0.0 - stream_chat_flutter_core: ^6.3.0 + stream_chat_flutter_core: ^6.4.0 synchronized: ^3.0.0 thumblr: ^0.0.4 url_launcher: ^6.1.0 diff --git a/packages/stream_chat_flutter_core/CHANGELOG.md b/packages/stream_chat_flutter_core/CHANGELOG.md index d694a731..dbab9611 100644 --- a/packages/stream_chat_flutter_core/CHANGELOG.md +++ b/packages/stream_chat_flutter_core/CHANGELOG.md @@ -1,3 +1,7 @@ +## 6.4.0 + +- Updated `stream_chat` dependency to [`6.4.0`](https://pub.dev/packages/stream_chat/changelog). + ## 6.3.0 - Updated `stream_chat` dependency to [`6.3.0`](https://pub.dev/packages/stream_chat/changelog). diff --git a/packages/stream_chat_flutter_core/lib/src/stream_channel_list_event_handler.dart b/packages/stream_chat_flutter_core/lib/src/stream_channel_list_event_handler.dart index 0fc34dfd..fe53339a 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_channel_list_event_handler.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_channel_list_event_handler.dart @@ -108,7 +108,7 @@ class StreamChannelListEventHandler { final channels = [...controller.currentItems]; final channelIndex = channels.indexWhere((it) => it.cid == channelCid); - if (channelIndex <= 0) { + if (channelIndex < 0) { // If the channel is not in the list, It might be hidden. // So, we just refresh the list. await controller.refresh(resetValue: false); diff --git a/packages/stream_chat_flutter_core/pubspec.yaml b/packages/stream_chat_flutter_core/pubspec.yaml index bfd0d881..852b0afc 100644 --- a/packages/stream_chat_flutter_core/pubspec.yaml +++ b/packages/stream_chat_flutter_core/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat_flutter_core homepage: https://github.com/GetStream/stream-chat-flutter description: Stream Chat official Flutter SDK Core. Build your own chat experience using Dart and Flutter. -version: 6.3.0 +version: 6.4.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -17,7 +17,7 @@ dependencies: freezed_annotation: ^2.0.3 meta: ^1.8.0 rxdart: ^0.27.0 - stream_chat: ^6.3.0 + stream_chat: ^6.4.0 dev_dependencies: build_runner: ^2.3.3 diff --git a/packages/stream_chat_localizations/CHANGELOG.md b/packages/stream_chat_localizations/CHANGELOG.md index 50af8f39..628f82dc 100644 --- a/packages/stream_chat_localizations/CHANGELOG.md +++ b/packages/stream_chat_localizations/CHANGELOG.md @@ -1,3 +1,7 @@ +## 5.4.0 + +* Updated `stream_chat_flutter` dependency to [`6.4.0`](https://pub.dev/packages/stream_chat_flutter/changelog). + ## 5.3.0 * Updated `stream_chat_flutter` dependency to [`6.3.0`](https://pub.dev/packages/stream_chat_flutter/changelog). diff --git a/packages/stream_chat_localizations/pubspec.yaml b/packages/stream_chat_localizations/pubspec.yaml index f5bcdc19..ecd1ae78 100644 --- a/packages/stream_chat_localizations/pubspec.yaml +++ b/packages/stream_chat_localizations/pubspec.yaml @@ -1,6 +1,6 @@ name: stream_chat_localizations description: The Official localizations for Stream Chat Flutter, a service for building chat applications -version: 5.3.0 +version: 5.4.0 homepage: 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 @@ -14,7 +14,7 @@ dependencies: sdk: flutter flutter_localizations: sdk: flutter - stream_chat_flutter: ^6.3.0 + stream_chat_flutter: ^6.4.0 dev_dependencies: dart_code_metrics: ^5.7.2 diff --git a/packages/stream_chat_persistence/CHANGELOG.md b/packages/stream_chat_persistence/CHANGELOG.md index d2b3d075..152ed256 100644 --- a/packages/stream_chat_persistence/CHANGELOG.md +++ b/packages/stream_chat_persistence/CHANGELOG.md @@ -1,3 +1,7 @@ +## 6.4.0 + +- Updated `stream_chat` dependency to [`6.4.0`](https://pub.dev/packages/stream_chat/changelog). + ## 6.3.0 - Updated `stream_chat` dependency to [`6.3.0`](https://pub.dev/packages/stream_chat/changelog). diff --git a/packages/stream_chat_persistence/lib/src/dao/message_dao.dart b/packages/stream_chat_persistence/lib/src/dao/message_dao.dart index fa69fa0c..b21bd3cd 100644 --- a/packages/stream_chat_persistence/lib/src/dao/message_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/message_dao.dart @@ -5,7 +5,6 @@ import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; import 'package:stream_chat_persistence/src/entity/messages.dart'; import 'package:stream_chat_persistence/src/entity/users.dart'; - import 'package:stream_chat_persistence/src/mapper/mapper.dart'; part 'message_dao.g.dart'; diff --git a/packages/stream_chat_persistence/lib/src/db/drift_chat_database.dart b/packages/stream_chat_persistence/lib/src/db/drift_chat_database.dart index 131a5768..f0b07095 100644 --- a/packages/stream_chat_persistence/lib/src/db/drift_chat_database.dart +++ b/packages/stream_chat_persistence/lib/src/db/drift_chat_database.dart @@ -49,7 +49,7 @@ class DriftChatDatabase extends _$DriftChatDatabase { // you should bump this number whenever you change or add a table definition. @override - int get schemaVersion => 11; + int get schemaVersion => 12; @override MigrationStrategy get migration => MigrationStrategy( diff --git a/packages/stream_chat_persistence/lib/src/db/drift_chat_database.g.dart b/packages/stream_chat_persistence/lib/src/db/drift_chat_database.g.dart index 0dcd0107..04572675 100644 --- a/packages/stream_chat_persistence/lib/src/db/drift_chat_database.g.dart +++ b/packages/stream_chat_persistence/lib/src/db/drift_chat_database.g.dart @@ -227,13 +227,13 @@ class $ChannelsTable extends Channels } static TypeConverter, String> $converterownCapabilities = - ListConverter(); + ListConverter(); static TypeConverter?, String?> $converterownCapabilitiesn = NullAwareTypeConverter.wrap($converterownCapabilities); static TypeConverter, String> $converterconfig = MapConverter(); static TypeConverter, String> $converterextraData = - MapConverter(); + MapConverter(); static TypeConverter?, String?> $converterextraDatan = NullAwareTypeConverter.wrap($converterextraData); } @@ -757,28 +757,42 @@ class $MessagesTable extends Messages late final GeneratedColumn command = GeneratedColumn( 'command', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); - static const VerificationMeta _createdAtMeta = - const VerificationMeta('createdAt'); + static const VerificationMeta _localCreatedAtMeta = + const VerificationMeta('localCreatedAt'); @override - late final GeneratedColumn createdAt = GeneratedColumn( - 'created_at', aliasedName, false, - type: DriftSqlType.dateTime, - requiredDuringInsert: false, - defaultValue: currentDateAndTime); - static const VerificationMeta _updatedAtMeta = - const VerificationMeta('updatedAt'); + late final GeneratedColumn localCreatedAt = + GeneratedColumn('local_created_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _remoteCreatedAtMeta = + const VerificationMeta('remoteCreatedAt'); @override - late final GeneratedColumn updatedAt = GeneratedColumn( - 'updated_at', aliasedName, false, - type: DriftSqlType.dateTime, - requiredDuringInsert: false, - defaultValue: currentDateAndTime); - static const VerificationMeta _deletedAtMeta = - const VerificationMeta('deletedAt'); + late final GeneratedColumn remoteCreatedAt = + GeneratedColumn('remote_created_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _localUpdatedAtMeta = + const VerificationMeta('localUpdatedAt'); @override - late final GeneratedColumn deletedAt = GeneratedColumn( - 'deleted_at', aliasedName, true, - type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn localUpdatedAt = + GeneratedColumn('local_updated_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _remoteUpdatedAtMeta = + const VerificationMeta('remoteUpdatedAt'); + @override + late final GeneratedColumn remoteUpdatedAt = + GeneratedColumn('remote_updated_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _localDeletedAtMeta = + const VerificationMeta('localDeletedAt'); + @override + late final GeneratedColumn localDeletedAt = + GeneratedColumn('local_deleted_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _remoteDeletedAtMeta = + const VerificationMeta('remoteDeletedAt'); + @override + late final GeneratedColumn remoteDeletedAt = + GeneratedColumn('remote_deleted_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); static const VerificationMeta _userIdMeta = const VerificationMeta('userId'); @override late final GeneratedColumn userId = GeneratedColumn( @@ -853,9 +867,12 @@ class $MessagesTable extends Messages showInChannel, shadowed, command, - createdAt, - updatedAt, - deletedAt, + localCreatedAt, + remoteCreatedAt, + localUpdatedAt, + remoteUpdatedAt, + localDeletedAt, + remoteDeletedAt, userId, pinned, pinnedAt, @@ -924,17 +941,41 @@ class $MessagesTable extends Messages context.handle(_commandMeta, command.isAcceptableOrUnknown(data['command']!, _commandMeta)); } - if (data.containsKey('created_at')) { - context.handle(_createdAtMeta, - createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + if (data.containsKey('local_created_at')) { + context.handle( + _localCreatedAtMeta, + localCreatedAt.isAcceptableOrUnknown( + data['local_created_at']!, _localCreatedAtMeta)); } - if (data.containsKey('updated_at')) { - context.handle(_updatedAtMeta, - updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); + if (data.containsKey('remote_created_at')) { + context.handle( + _remoteCreatedAtMeta, + remoteCreatedAt.isAcceptableOrUnknown( + data['remote_created_at']!, _remoteCreatedAtMeta)); } - if (data.containsKey('deleted_at')) { - context.handle(_deletedAtMeta, - deletedAt.isAcceptableOrUnknown(data['deleted_at']!, _deletedAtMeta)); + if (data.containsKey('local_updated_at')) { + context.handle( + _localUpdatedAtMeta, + localUpdatedAt.isAcceptableOrUnknown( + data['local_updated_at']!, _localUpdatedAtMeta)); + } + if (data.containsKey('remote_updated_at')) { + context.handle( + _remoteUpdatedAtMeta, + remoteUpdatedAt.isAcceptableOrUnknown( + data['remote_updated_at']!, _remoteUpdatedAtMeta)); + } + if (data.containsKey('local_deleted_at')) { + context.handle( + _localDeletedAtMeta, + localDeletedAt.isAcceptableOrUnknown( + data['local_deleted_at']!, _localDeletedAtMeta)); + } + if (data.containsKey('remote_deleted_at')) { + context.handle( + _remoteDeletedAtMeta, + remoteDeletedAt.isAcceptableOrUnknown( + data['remote_deleted_at']!, _remoteDeletedAtMeta)); } if (data.containsKey('user_id')) { context.handle(_userIdMeta, @@ -1012,12 +1053,18 @@ class $MessagesTable extends Messages .read(DriftSqlType.bool, data['${effectivePrefix}shadowed'])!, command: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}command']), - createdAt: attachedDatabase.typeMapping - .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, - updatedAt: attachedDatabase.typeMapping - .read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, - deletedAt: attachedDatabase.typeMapping - .read(DriftSqlType.dateTime, data['${effectivePrefix}deleted_at']), + localCreatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, data['${effectivePrefix}local_created_at']), + remoteCreatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, data['${effectivePrefix}remote_created_at']), + localUpdatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, data['${effectivePrefix}local_updated_at']), + remoteUpdatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, data['${effectivePrefix}remote_updated_at']), + localDeletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, data['${effectivePrefix}local_deleted_at']), + remoteDeletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, data['${effectivePrefix}remote_deleted_at']), userId: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}user_id']), pinned: attachedDatabase.typeMapping @@ -1044,23 +1091,23 @@ class $MessagesTable extends Messages } static TypeConverter, String> $converterattachments = - ListConverter(); + ListConverter(); static TypeConverter $converterstatus = MessageSendingStatusConverter(); static TypeConverter, String> $convertermentionedUsers = - ListConverter(); + ListConverter(); static TypeConverter, String> $converterreactionCounts = - MapConverter(); + MapConverter(); static TypeConverter?, String?> $converterreactionCountsn = NullAwareTypeConverter.wrap($converterreactionCounts); static TypeConverter, String> $converterreactionScores = - MapConverter(); + MapConverter(); static TypeConverter?, String?> $converterreactionScoresn = NullAwareTypeConverter.wrap($converterreactionScores); static TypeConverter?, String?> $converteri18n = - NullableMapConverter(); + NullableMapConverter(); static TypeConverter, String> $converterextraData = - MapConverter(); + MapConverter(); static TypeConverter?, String?> $converterextraDatan = NullAwareTypeConverter.wrap($converterextraData); } @@ -1109,14 +1156,23 @@ class MessageEntity extends DataClass implements Insertable { /// A used command name. final String? command; - /// The DateTime when the message was created. - final DateTime createdAt; + /// The DateTime on which the message was created on the client. + final DateTime? localCreatedAt; - /// The DateTime when the message was updated last time. - final DateTime updatedAt; + /// The DateTime on which the message was created on the server. + final DateTime? remoteCreatedAt; - /// The DateTime when the message was deleted. - final DateTime? deletedAt; + /// The DateTime on which the message was updated on the client. + final DateTime? localUpdatedAt; + + /// The DateTime on which the message was updated on the server. + final DateTime? remoteUpdatedAt; + + /// The DateTime on which the message was deleted on the client. + final DateTime? localDeletedAt; + + /// The DateTime on which the message was deleted on the server. + final DateTime? remoteDeletedAt; /// Id of the User who sent the message final String? userId; @@ -1156,9 +1212,12 @@ class MessageEntity extends DataClass implements Insertable { this.showInChannel, required this.shadowed, this.command, - required this.createdAt, - required this.updatedAt, - this.deletedAt, + this.localCreatedAt, + this.remoteCreatedAt, + this.localUpdatedAt, + this.remoteUpdatedAt, + this.localDeletedAt, + this.remoteDeletedAt, this.userId, required this.pinned, this.pinnedAt, @@ -1214,10 +1273,23 @@ class MessageEntity extends DataClass implements Insertable { if (!nullToAbsent || command != null) { map['command'] = Variable(command); } - map['created_at'] = Variable(createdAt); - map['updated_at'] = Variable(updatedAt); - if (!nullToAbsent || deletedAt != null) { - map['deleted_at'] = Variable(deletedAt); + if (!nullToAbsent || localCreatedAt != null) { + map['local_created_at'] = Variable(localCreatedAt); + } + if (!nullToAbsent || remoteCreatedAt != null) { + map['remote_created_at'] = Variable(remoteCreatedAt); + } + if (!nullToAbsent || localUpdatedAt != null) { + map['local_updated_at'] = Variable(localUpdatedAt); + } + if (!nullToAbsent || remoteUpdatedAt != null) { + map['remote_updated_at'] = Variable(remoteUpdatedAt); + } + if (!nullToAbsent || localDeletedAt != null) { + map['local_deleted_at'] = Variable(localDeletedAt); + } + if (!nullToAbsent || remoteDeletedAt != null) { + map['remote_deleted_at'] = Variable(remoteDeletedAt); } if (!nullToAbsent || userId != null) { map['user_id'] = Variable(userId); @@ -1264,9 +1336,12 @@ class MessageEntity extends DataClass implements Insertable { showInChannel: serializer.fromJson(json['showInChannel']), shadowed: serializer.fromJson(json['shadowed']), command: serializer.fromJson(json['command']), - createdAt: serializer.fromJson(json['createdAt']), - updatedAt: serializer.fromJson(json['updatedAt']), - deletedAt: serializer.fromJson(json['deletedAt']), + localCreatedAt: serializer.fromJson(json['localCreatedAt']), + remoteCreatedAt: serializer.fromJson(json['remoteCreatedAt']), + localUpdatedAt: serializer.fromJson(json['localUpdatedAt']), + remoteUpdatedAt: serializer.fromJson(json['remoteUpdatedAt']), + localDeletedAt: serializer.fromJson(json['localDeletedAt']), + remoteDeletedAt: serializer.fromJson(json['remoteDeletedAt']), userId: serializer.fromJson(json['userId']), pinned: serializer.fromJson(json['pinned']), pinnedAt: serializer.fromJson(json['pinnedAt']), @@ -1295,9 +1370,12 @@ class MessageEntity extends DataClass implements Insertable { 'showInChannel': serializer.toJson(showInChannel), 'shadowed': serializer.toJson(shadowed), 'command': serializer.toJson(command), - 'createdAt': serializer.toJson(createdAt), - 'updatedAt': serializer.toJson(updatedAt), - 'deletedAt': serializer.toJson(deletedAt), + 'localCreatedAt': serializer.toJson(localCreatedAt), + 'remoteCreatedAt': serializer.toJson(remoteCreatedAt), + 'localUpdatedAt': serializer.toJson(localUpdatedAt), + 'remoteUpdatedAt': serializer.toJson(remoteUpdatedAt), + 'localDeletedAt': serializer.toJson(localDeletedAt), + 'remoteDeletedAt': serializer.toJson(remoteDeletedAt), 'userId': serializer.toJson(userId), 'pinned': serializer.toJson(pinned), 'pinnedAt': serializer.toJson(pinnedAt), @@ -1324,9 +1402,12 @@ class MessageEntity extends DataClass implements Insertable { Value showInChannel = const Value.absent(), bool? shadowed, Value command = const Value.absent(), - DateTime? createdAt, - DateTime? updatedAt, - Value deletedAt = const Value.absent(), + Value localCreatedAt = const Value.absent(), + Value remoteCreatedAt = const Value.absent(), + Value localUpdatedAt = const Value.absent(), + Value remoteUpdatedAt = const Value.absent(), + Value localDeletedAt = const Value.absent(), + Value remoteDeletedAt = const Value.absent(), Value userId = const Value.absent(), bool? pinned, Value pinnedAt = const Value.absent(), @@ -1355,9 +1436,21 @@ class MessageEntity extends DataClass implements Insertable { showInChannel.present ? showInChannel.value : this.showInChannel, shadowed: shadowed ?? this.shadowed, command: command.present ? command.value : this.command, - createdAt: createdAt ?? this.createdAt, - updatedAt: updatedAt ?? this.updatedAt, - deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + localCreatedAt: + localCreatedAt.present ? localCreatedAt.value : this.localCreatedAt, + remoteCreatedAt: remoteCreatedAt.present + ? remoteCreatedAt.value + : this.remoteCreatedAt, + localUpdatedAt: + localUpdatedAt.present ? localUpdatedAt.value : this.localUpdatedAt, + remoteUpdatedAt: remoteUpdatedAt.present + ? remoteUpdatedAt.value + : this.remoteUpdatedAt, + localDeletedAt: + localDeletedAt.present ? localDeletedAt.value : this.localDeletedAt, + remoteDeletedAt: remoteDeletedAt.present + ? remoteDeletedAt.value + : this.remoteDeletedAt, userId: userId.present ? userId.value : this.userId, pinned: pinned ?? this.pinned, pinnedAt: pinnedAt.present ? pinnedAt.value : this.pinnedAt, @@ -1385,9 +1478,12 @@ class MessageEntity extends DataClass implements Insertable { ..write('showInChannel: $showInChannel, ') ..write('shadowed: $shadowed, ') ..write('command: $command, ') - ..write('createdAt: $createdAt, ') - ..write('updatedAt: $updatedAt, ') - ..write('deletedAt: $deletedAt, ') + ..write('localCreatedAt: $localCreatedAt, ') + ..write('remoteCreatedAt: $remoteCreatedAt, ') + ..write('localUpdatedAt: $localUpdatedAt, ') + ..write('remoteUpdatedAt: $remoteUpdatedAt, ') + ..write('localDeletedAt: $localDeletedAt, ') + ..write('remoteDeletedAt: $remoteDeletedAt, ') ..write('userId: $userId, ') ..write('pinned: $pinned, ') ..write('pinnedAt: $pinnedAt, ') @@ -1416,9 +1512,12 @@ class MessageEntity extends DataClass implements Insertable { showInChannel, shadowed, command, - createdAt, - updatedAt, - deletedAt, + localCreatedAt, + remoteCreatedAt, + localUpdatedAt, + remoteUpdatedAt, + localDeletedAt, + remoteDeletedAt, userId, pinned, pinnedAt, @@ -1446,9 +1545,12 @@ class MessageEntity extends DataClass implements Insertable { other.showInChannel == this.showInChannel && other.shadowed == this.shadowed && other.command == this.command && - other.createdAt == this.createdAt && - other.updatedAt == this.updatedAt && - other.deletedAt == this.deletedAt && + other.localCreatedAt == this.localCreatedAt && + other.remoteCreatedAt == this.remoteCreatedAt && + other.localUpdatedAt == this.localUpdatedAt && + other.remoteUpdatedAt == this.remoteUpdatedAt && + other.localDeletedAt == this.localDeletedAt && + other.remoteDeletedAt == this.remoteDeletedAt && other.userId == this.userId && other.pinned == this.pinned && other.pinnedAt == this.pinnedAt && @@ -1474,9 +1576,12 @@ class MessagesCompanion extends UpdateCompanion { final Value showInChannel; final Value shadowed; final Value command; - final Value createdAt; - final Value updatedAt; - final Value deletedAt; + final Value localCreatedAt; + final Value remoteCreatedAt; + final Value localUpdatedAt; + final Value remoteUpdatedAt; + final Value localDeletedAt; + final Value remoteDeletedAt; final Value userId; final Value pinned; final Value pinnedAt; @@ -1501,9 +1606,12 @@ class MessagesCompanion extends UpdateCompanion { this.showInChannel = const Value.absent(), this.shadowed = const Value.absent(), this.command = const Value.absent(), - this.createdAt = const Value.absent(), - this.updatedAt = const Value.absent(), - this.deletedAt = const Value.absent(), + this.localCreatedAt = const Value.absent(), + this.remoteCreatedAt = const Value.absent(), + this.localUpdatedAt = const Value.absent(), + this.remoteUpdatedAt = const Value.absent(), + this.localDeletedAt = const Value.absent(), + this.remoteDeletedAt = const Value.absent(), this.userId = const Value.absent(), this.pinned = const Value.absent(), this.pinnedAt = const Value.absent(), @@ -1529,9 +1637,12 @@ class MessagesCompanion extends UpdateCompanion { this.showInChannel = const Value.absent(), this.shadowed = const Value.absent(), this.command = const Value.absent(), - this.createdAt = const Value.absent(), - this.updatedAt = const Value.absent(), - this.deletedAt = const Value.absent(), + this.localCreatedAt = const Value.absent(), + this.remoteCreatedAt = const Value.absent(), + this.localUpdatedAt = const Value.absent(), + this.remoteUpdatedAt = const Value.absent(), + this.localDeletedAt = const Value.absent(), + this.remoteDeletedAt = const Value.absent(), this.userId = const Value.absent(), this.pinned = const Value.absent(), this.pinnedAt = const Value.absent(), @@ -1560,9 +1671,12 @@ class MessagesCompanion extends UpdateCompanion { Expression? showInChannel, Expression? shadowed, Expression? command, - Expression? createdAt, - Expression? updatedAt, - Expression? deletedAt, + Expression? localCreatedAt, + Expression? remoteCreatedAt, + Expression? localUpdatedAt, + Expression? remoteUpdatedAt, + Expression? localDeletedAt, + Expression? remoteDeletedAt, Expression? userId, Expression? pinned, Expression? pinnedAt, @@ -1588,9 +1702,12 @@ class MessagesCompanion extends UpdateCompanion { if (showInChannel != null) 'show_in_channel': showInChannel, if (shadowed != null) 'shadowed': shadowed, if (command != null) 'command': command, - if (createdAt != null) 'created_at': createdAt, - if (updatedAt != null) 'updated_at': updatedAt, - if (deletedAt != null) 'deleted_at': deletedAt, + if (localCreatedAt != null) 'local_created_at': localCreatedAt, + if (remoteCreatedAt != null) 'remote_created_at': remoteCreatedAt, + if (localUpdatedAt != null) 'local_updated_at': localUpdatedAt, + if (remoteUpdatedAt != null) 'remote_updated_at': remoteUpdatedAt, + if (localDeletedAt != null) 'local_deleted_at': localDeletedAt, + if (remoteDeletedAt != null) 'remote_deleted_at': remoteDeletedAt, if (userId != null) 'user_id': userId, if (pinned != null) 'pinned': pinned, if (pinnedAt != null) 'pinned_at': pinnedAt, @@ -1618,9 +1735,12 @@ class MessagesCompanion extends UpdateCompanion { Value? showInChannel, Value? shadowed, Value? command, - Value? createdAt, - Value? updatedAt, - Value? deletedAt, + Value? localCreatedAt, + Value? remoteCreatedAt, + Value? localUpdatedAt, + Value? remoteUpdatedAt, + Value? localDeletedAt, + Value? remoteDeletedAt, Value? userId, Value? pinned, Value? pinnedAt, @@ -1645,9 +1765,12 @@ class MessagesCompanion extends UpdateCompanion { showInChannel: showInChannel ?? this.showInChannel, shadowed: shadowed ?? this.shadowed, command: command ?? this.command, - createdAt: createdAt ?? this.createdAt, - updatedAt: updatedAt ?? this.updatedAt, - deletedAt: deletedAt ?? this.deletedAt, + localCreatedAt: localCreatedAt ?? this.localCreatedAt, + remoteCreatedAt: remoteCreatedAt ?? this.remoteCreatedAt, + localUpdatedAt: localUpdatedAt ?? this.localUpdatedAt, + remoteUpdatedAt: remoteUpdatedAt ?? this.remoteUpdatedAt, + localDeletedAt: localDeletedAt ?? this.localDeletedAt, + remoteDeletedAt: remoteDeletedAt ?? this.remoteDeletedAt, userId: userId ?? this.userId, pinned: pinned ?? this.pinned, pinnedAt: pinnedAt ?? this.pinnedAt, @@ -1713,14 +1836,23 @@ class MessagesCompanion extends UpdateCompanion { if (command.present) { map['command'] = Variable(command.value); } - if (createdAt.present) { - map['created_at'] = Variable(createdAt.value); + if (localCreatedAt.present) { + map['local_created_at'] = Variable(localCreatedAt.value); } - if (updatedAt.present) { - map['updated_at'] = Variable(updatedAt.value); + if (remoteCreatedAt.present) { + map['remote_created_at'] = Variable(remoteCreatedAt.value); } - if (deletedAt.present) { - map['deleted_at'] = Variable(deletedAt.value); + if (localUpdatedAt.present) { + map['local_updated_at'] = Variable(localUpdatedAt.value); + } + if (remoteUpdatedAt.present) { + map['remote_updated_at'] = Variable(remoteUpdatedAt.value); + } + if (localDeletedAt.present) { + map['local_deleted_at'] = Variable(localDeletedAt.value); + } + if (remoteDeletedAt.present) { + map['remote_deleted_at'] = Variable(remoteDeletedAt.value); } if (userId.present) { map['user_id'] = Variable(userId.value); @@ -1771,9 +1903,12 @@ class MessagesCompanion extends UpdateCompanion { ..write('showInChannel: $showInChannel, ') ..write('shadowed: $shadowed, ') ..write('command: $command, ') - ..write('createdAt: $createdAt, ') - ..write('updatedAt: $updatedAt, ') - ..write('deletedAt: $deletedAt, ') + ..write('localCreatedAt: $localCreatedAt, ') + ..write('remoteCreatedAt: $remoteCreatedAt, ') + ..write('localUpdatedAt: $localUpdatedAt, ') + ..write('remoteUpdatedAt: $remoteUpdatedAt, ') + ..write('localDeletedAt: $localDeletedAt, ') + ..write('remoteDeletedAt: $remoteDeletedAt, ') ..write('userId: $userId, ') ..write('pinned: $pinned, ') ..write('pinnedAt: $pinnedAt, ') @@ -1905,28 +2040,42 @@ class $PinnedMessagesTable extends PinnedMessages late final GeneratedColumn command = GeneratedColumn( 'command', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); - static const VerificationMeta _createdAtMeta = - const VerificationMeta('createdAt'); + static const VerificationMeta _localCreatedAtMeta = + const VerificationMeta('localCreatedAt'); @override - late final GeneratedColumn createdAt = GeneratedColumn( - 'created_at', aliasedName, false, - type: DriftSqlType.dateTime, - requiredDuringInsert: false, - defaultValue: currentDateAndTime); - static const VerificationMeta _updatedAtMeta = - const VerificationMeta('updatedAt'); + late final GeneratedColumn localCreatedAt = + GeneratedColumn('local_created_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _remoteCreatedAtMeta = + const VerificationMeta('remoteCreatedAt'); @override - late final GeneratedColumn updatedAt = GeneratedColumn( - 'updated_at', aliasedName, false, - type: DriftSqlType.dateTime, - requiredDuringInsert: false, - defaultValue: currentDateAndTime); - static const VerificationMeta _deletedAtMeta = - const VerificationMeta('deletedAt'); + late final GeneratedColumn remoteCreatedAt = + GeneratedColumn('remote_created_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _localUpdatedAtMeta = + const VerificationMeta('localUpdatedAt'); @override - late final GeneratedColumn deletedAt = GeneratedColumn( - 'deleted_at', aliasedName, true, - type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn localUpdatedAt = + GeneratedColumn('local_updated_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _remoteUpdatedAtMeta = + const VerificationMeta('remoteUpdatedAt'); + @override + late final GeneratedColumn remoteUpdatedAt = + GeneratedColumn('remote_updated_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _localDeletedAtMeta = + const VerificationMeta('localDeletedAt'); + @override + late final GeneratedColumn localDeletedAt = + GeneratedColumn('local_deleted_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _remoteDeletedAtMeta = + const VerificationMeta('remoteDeletedAt'); + @override + late final GeneratedColumn remoteDeletedAt = + GeneratedColumn('remote_deleted_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); static const VerificationMeta _userIdMeta = const VerificationMeta('userId'); @override late final GeneratedColumn userId = GeneratedColumn( @@ -2002,9 +2151,12 @@ class $PinnedMessagesTable extends PinnedMessages showInChannel, shadowed, command, - createdAt, - updatedAt, - deletedAt, + localCreatedAt, + remoteCreatedAt, + localUpdatedAt, + remoteUpdatedAt, + localDeletedAt, + remoteDeletedAt, userId, pinned, pinnedAt, @@ -2074,17 +2226,41 @@ class $PinnedMessagesTable extends PinnedMessages context.handle(_commandMeta, command.isAcceptableOrUnknown(data['command']!, _commandMeta)); } - if (data.containsKey('created_at')) { - context.handle(_createdAtMeta, - createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + if (data.containsKey('local_created_at')) { + context.handle( + _localCreatedAtMeta, + localCreatedAt.isAcceptableOrUnknown( + data['local_created_at']!, _localCreatedAtMeta)); } - if (data.containsKey('updated_at')) { - context.handle(_updatedAtMeta, - updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); + if (data.containsKey('remote_created_at')) { + context.handle( + _remoteCreatedAtMeta, + remoteCreatedAt.isAcceptableOrUnknown( + data['remote_created_at']!, _remoteCreatedAtMeta)); } - if (data.containsKey('deleted_at')) { - context.handle(_deletedAtMeta, - deletedAt.isAcceptableOrUnknown(data['deleted_at']!, _deletedAtMeta)); + if (data.containsKey('local_updated_at')) { + context.handle( + _localUpdatedAtMeta, + localUpdatedAt.isAcceptableOrUnknown( + data['local_updated_at']!, _localUpdatedAtMeta)); + } + if (data.containsKey('remote_updated_at')) { + context.handle( + _remoteUpdatedAtMeta, + remoteUpdatedAt.isAcceptableOrUnknown( + data['remote_updated_at']!, _remoteUpdatedAtMeta)); + } + if (data.containsKey('local_deleted_at')) { + context.handle( + _localDeletedAtMeta, + localDeletedAt.isAcceptableOrUnknown( + data['local_deleted_at']!, _localDeletedAtMeta)); + } + if (data.containsKey('remote_deleted_at')) { + context.handle( + _remoteDeletedAtMeta, + remoteDeletedAt.isAcceptableOrUnknown( + data['remote_deleted_at']!, _remoteDeletedAtMeta)); } if (data.containsKey('user_id')) { context.handle(_userIdMeta, @@ -2162,12 +2338,18 @@ class $PinnedMessagesTable extends PinnedMessages .read(DriftSqlType.bool, data['${effectivePrefix}shadowed'])!, command: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}command']), - createdAt: attachedDatabase.typeMapping - .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, - updatedAt: attachedDatabase.typeMapping - .read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, - deletedAt: attachedDatabase.typeMapping - .read(DriftSqlType.dateTime, data['${effectivePrefix}deleted_at']), + localCreatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, data['${effectivePrefix}local_created_at']), + remoteCreatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, data['${effectivePrefix}remote_created_at']), + localUpdatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, data['${effectivePrefix}local_updated_at']), + remoteUpdatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, data['${effectivePrefix}remote_updated_at']), + localDeletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, data['${effectivePrefix}local_deleted_at']), + remoteDeletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, data['${effectivePrefix}remote_deleted_at']), userId: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}user_id']), pinned: attachedDatabase.typeMapping @@ -2195,23 +2377,23 @@ class $PinnedMessagesTable extends PinnedMessages } static TypeConverter, String> $converterattachments = - ListConverter(); + ListConverter(); static TypeConverter $converterstatus = MessageSendingStatusConverter(); static TypeConverter, String> $convertermentionedUsers = - ListConverter(); + ListConverter(); static TypeConverter, String> $converterreactionCounts = - MapConverter(); + MapConverter(); static TypeConverter?, String?> $converterreactionCountsn = NullAwareTypeConverter.wrap($converterreactionCounts); static TypeConverter, String> $converterreactionScores = - MapConverter(); + MapConverter(); static TypeConverter?, String?> $converterreactionScoresn = NullAwareTypeConverter.wrap($converterreactionScores); static TypeConverter?, String?> $converteri18n = - NullableMapConverter(); + NullableMapConverter(); static TypeConverter, String> $converterextraData = - MapConverter(); + MapConverter(); static TypeConverter?, String?> $converterextraDatan = NullAwareTypeConverter.wrap($converterextraData); } @@ -2261,14 +2443,23 @@ class PinnedMessageEntity extends DataClass /// A used command name. final String? command; - /// The DateTime when the message was created. - final DateTime createdAt; + /// The DateTime on which the message was created on the client. + final DateTime? localCreatedAt; - /// The DateTime when the message was updated last time. - final DateTime updatedAt; + /// The DateTime on which the message was created on the server. + final DateTime? remoteCreatedAt; - /// The DateTime when the message was deleted. - final DateTime? deletedAt; + /// The DateTime on which the message was updated on the client. + final DateTime? localUpdatedAt; + + /// The DateTime on which the message was updated on the server. + final DateTime? remoteUpdatedAt; + + /// The DateTime on which the message was deleted on the client. + final DateTime? localDeletedAt; + + /// The DateTime on which the message was deleted on the server. + final DateTime? remoteDeletedAt; /// Id of the User who sent the message final String? userId; @@ -2308,9 +2499,12 @@ class PinnedMessageEntity extends DataClass this.showInChannel, required this.shadowed, this.command, - required this.createdAt, - required this.updatedAt, - this.deletedAt, + this.localCreatedAt, + this.remoteCreatedAt, + this.localUpdatedAt, + this.remoteUpdatedAt, + this.localDeletedAt, + this.remoteDeletedAt, this.userId, required this.pinned, this.pinnedAt, @@ -2366,10 +2560,23 @@ class PinnedMessageEntity extends DataClass if (!nullToAbsent || command != null) { map['command'] = Variable(command); } - map['created_at'] = Variable(createdAt); - map['updated_at'] = Variable(updatedAt); - if (!nullToAbsent || deletedAt != null) { - map['deleted_at'] = Variable(deletedAt); + if (!nullToAbsent || localCreatedAt != null) { + map['local_created_at'] = Variable(localCreatedAt); + } + if (!nullToAbsent || remoteCreatedAt != null) { + map['remote_created_at'] = Variable(remoteCreatedAt); + } + if (!nullToAbsent || localUpdatedAt != null) { + map['local_updated_at'] = Variable(localUpdatedAt); + } + if (!nullToAbsent || remoteUpdatedAt != null) { + map['remote_updated_at'] = Variable(remoteUpdatedAt); + } + if (!nullToAbsent || localDeletedAt != null) { + map['local_deleted_at'] = Variable(localDeletedAt); + } + if (!nullToAbsent || remoteDeletedAt != null) { + map['remote_deleted_at'] = Variable(remoteDeletedAt); } if (!nullToAbsent || userId != null) { map['user_id'] = Variable(userId); @@ -2416,9 +2623,12 @@ class PinnedMessageEntity extends DataClass showInChannel: serializer.fromJson(json['showInChannel']), shadowed: serializer.fromJson(json['shadowed']), command: serializer.fromJson(json['command']), - createdAt: serializer.fromJson(json['createdAt']), - updatedAt: serializer.fromJson(json['updatedAt']), - deletedAt: serializer.fromJson(json['deletedAt']), + localCreatedAt: serializer.fromJson(json['localCreatedAt']), + remoteCreatedAt: serializer.fromJson(json['remoteCreatedAt']), + localUpdatedAt: serializer.fromJson(json['localUpdatedAt']), + remoteUpdatedAt: serializer.fromJson(json['remoteUpdatedAt']), + localDeletedAt: serializer.fromJson(json['localDeletedAt']), + remoteDeletedAt: serializer.fromJson(json['remoteDeletedAt']), userId: serializer.fromJson(json['userId']), pinned: serializer.fromJson(json['pinned']), pinnedAt: serializer.fromJson(json['pinnedAt']), @@ -2447,9 +2657,12 @@ class PinnedMessageEntity extends DataClass 'showInChannel': serializer.toJson(showInChannel), 'shadowed': serializer.toJson(shadowed), 'command': serializer.toJson(command), - 'createdAt': serializer.toJson(createdAt), - 'updatedAt': serializer.toJson(updatedAt), - 'deletedAt': serializer.toJson(deletedAt), + 'localCreatedAt': serializer.toJson(localCreatedAt), + 'remoteCreatedAt': serializer.toJson(remoteCreatedAt), + 'localUpdatedAt': serializer.toJson(localUpdatedAt), + 'remoteUpdatedAt': serializer.toJson(remoteUpdatedAt), + 'localDeletedAt': serializer.toJson(localDeletedAt), + 'remoteDeletedAt': serializer.toJson(remoteDeletedAt), 'userId': serializer.toJson(userId), 'pinned': serializer.toJson(pinned), 'pinnedAt': serializer.toJson(pinnedAt), @@ -2476,9 +2689,12 @@ class PinnedMessageEntity extends DataClass Value showInChannel = const Value.absent(), bool? shadowed, Value command = const Value.absent(), - DateTime? createdAt, - DateTime? updatedAt, - Value deletedAt = const Value.absent(), + Value localCreatedAt = const Value.absent(), + Value remoteCreatedAt = const Value.absent(), + Value localUpdatedAt = const Value.absent(), + Value remoteUpdatedAt = const Value.absent(), + Value localDeletedAt = const Value.absent(), + Value remoteDeletedAt = const Value.absent(), Value userId = const Value.absent(), bool? pinned, Value pinnedAt = const Value.absent(), @@ -2507,9 +2723,21 @@ class PinnedMessageEntity extends DataClass showInChannel.present ? showInChannel.value : this.showInChannel, shadowed: shadowed ?? this.shadowed, command: command.present ? command.value : this.command, - createdAt: createdAt ?? this.createdAt, - updatedAt: updatedAt ?? this.updatedAt, - deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + localCreatedAt: + localCreatedAt.present ? localCreatedAt.value : this.localCreatedAt, + remoteCreatedAt: remoteCreatedAt.present + ? remoteCreatedAt.value + : this.remoteCreatedAt, + localUpdatedAt: + localUpdatedAt.present ? localUpdatedAt.value : this.localUpdatedAt, + remoteUpdatedAt: remoteUpdatedAt.present + ? remoteUpdatedAt.value + : this.remoteUpdatedAt, + localDeletedAt: + localDeletedAt.present ? localDeletedAt.value : this.localDeletedAt, + remoteDeletedAt: remoteDeletedAt.present + ? remoteDeletedAt.value + : this.remoteDeletedAt, userId: userId.present ? userId.value : this.userId, pinned: pinned ?? this.pinned, pinnedAt: pinnedAt.present ? pinnedAt.value : this.pinnedAt, @@ -2537,9 +2765,12 @@ class PinnedMessageEntity extends DataClass ..write('showInChannel: $showInChannel, ') ..write('shadowed: $shadowed, ') ..write('command: $command, ') - ..write('createdAt: $createdAt, ') - ..write('updatedAt: $updatedAt, ') - ..write('deletedAt: $deletedAt, ') + ..write('localCreatedAt: $localCreatedAt, ') + ..write('remoteCreatedAt: $remoteCreatedAt, ') + ..write('localUpdatedAt: $localUpdatedAt, ') + ..write('remoteUpdatedAt: $remoteUpdatedAt, ') + ..write('localDeletedAt: $localDeletedAt, ') + ..write('remoteDeletedAt: $remoteDeletedAt, ') ..write('userId: $userId, ') ..write('pinned: $pinned, ') ..write('pinnedAt: $pinnedAt, ') @@ -2568,9 +2799,12 @@ class PinnedMessageEntity extends DataClass showInChannel, shadowed, command, - createdAt, - updatedAt, - deletedAt, + localCreatedAt, + remoteCreatedAt, + localUpdatedAt, + remoteUpdatedAt, + localDeletedAt, + remoteDeletedAt, userId, pinned, pinnedAt, @@ -2598,9 +2832,12 @@ class PinnedMessageEntity extends DataClass other.showInChannel == this.showInChannel && other.shadowed == this.shadowed && other.command == this.command && - other.createdAt == this.createdAt && - other.updatedAt == this.updatedAt && - other.deletedAt == this.deletedAt && + other.localCreatedAt == this.localCreatedAt && + other.remoteCreatedAt == this.remoteCreatedAt && + other.localUpdatedAt == this.localUpdatedAt && + other.remoteUpdatedAt == this.remoteUpdatedAt && + other.localDeletedAt == this.localDeletedAt && + other.remoteDeletedAt == this.remoteDeletedAt && other.userId == this.userId && other.pinned == this.pinned && other.pinnedAt == this.pinnedAt && @@ -2626,9 +2863,12 @@ class PinnedMessagesCompanion extends UpdateCompanion { final Value showInChannel; final Value shadowed; final Value command; - final Value createdAt; - final Value updatedAt; - final Value deletedAt; + final Value localCreatedAt; + final Value remoteCreatedAt; + final Value localUpdatedAt; + final Value remoteUpdatedAt; + final Value localDeletedAt; + final Value remoteDeletedAt; final Value userId; final Value pinned; final Value pinnedAt; @@ -2653,9 +2893,12 @@ class PinnedMessagesCompanion extends UpdateCompanion { this.showInChannel = const Value.absent(), this.shadowed = const Value.absent(), this.command = const Value.absent(), - this.createdAt = const Value.absent(), - this.updatedAt = const Value.absent(), - this.deletedAt = const Value.absent(), + this.localCreatedAt = const Value.absent(), + this.remoteCreatedAt = const Value.absent(), + this.localUpdatedAt = const Value.absent(), + this.remoteUpdatedAt = const Value.absent(), + this.localDeletedAt = const Value.absent(), + this.remoteDeletedAt = const Value.absent(), this.userId = const Value.absent(), this.pinned = const Value.absent(), this.pinnedAt = const Value.absent(), @@ -2681,9 +2924,12 @@ class PinnedMessagesCompanion extends UpdateCompanion { this.showInChannel = const Value.absent(), this.shadowed = const Value.absent(), this.command = const Value.absent(), - this.createdAt = const Value.absent(), - this.updatedAt = const Value.absent(), - this.deletedAt = const Value.absent(), + this.localCreatedAt = const Value.absent(), + this.remoteCreatedAt = const Value.absent(), + this.localUpdatedAt = const Value.absent(), + this.remoteUpdatedAt = const Value.absent(), + this.localDeletedAt = const Value.absent(), + this.remoteDeletedAt = const Value.absent(), this.userId = const Value.absent(), this.pinned = const Value.absent(), this.pinnedAt = const Value.absent(), @@ -2712,9 +2958,12 @@ class PinnedMessagesCompanion extends UpdateCompanion { Expression? showInChannel, Expression? shadowed, Expression? command, - Expression? createdAt, - Expression? updatedAt, - Expression? deletedAt, + Expression? localCreatedAt, + Expression? remoteCreatedAt, + Expression? localUpdatedAt, + Expression? remoteUpdatedAt, + Expression? localDeletedAt, + Expression? remoteDeletedAt, Expression? userId, Expression? pinned, Expression? pinnedAt, @@ -2740,9 +2989,12 @@ class PinnedMessagesCompanion extends UpdateCompanion { if (showInChannel != null) 'show_in_channel': showInChannel, if (shadowed != null) 'shadowed': shadowed, if (command != null) 'command': command, - if (createdAt != null) 'created_at': createdAt, - if (updatedAt != null) 'updated_at': updatedAt, - if (deletedAt != null) 'deleted_at': deletedAt, + if (localCreatedAt != null) 'local_created_at': localCreatedAt, + if (remoteCreatedAt != null) 'remote_created_at': remoteCreatedAt, + if (localUpdatedAt != null) 'local_updated_at': localUpdatedAt, + if (remoteUpdatedAt != null) 'remote_updated_at': remoteUpdatedAt, + if (localDeletedAt != null) 'local_deleted_at': localDeletedAt, + if (remoteDeletedAt != null) 'remote_deleted_at': remoteDeletedAt, if (userId != null) 'user_id': userId, if (pinned != null) 'pinned': pinned, if (pinnedAt != null) 'pinned_at': pinnedAt, @@ -2770,9 +3022,12 @@ class PinnedMessagesCompanion extends UpdateCompanion { Value? showInChannel, Value? shadowed, Value? command, - Value? createdAt, - Value? updatedAt, - Value? deletedAt, + Value? localCreatedAt, + Value? remoteCreatedAt, + Value? localUpdatedAt, + Value? remoteUpdatedAt, + Value? localDeletedAt, + Value? remoteDeletedAt, Value? userId, Value? pinned, Value? pinnedAt, @@ -2797,9 +3052,12 @@ class PinnedMessagesCompanion extends UpdateCompanion { showInChannel: showInChannel ?? this.showInChannel, shadowed: shadowed ?? this.shadowed, command: command ?? this.command, - createdAt: createdAt ?? this.createdAt, - updatedAt: updatedAt ?? this.updatedAt, - deletedAt: deletedAt ?? this.deletedAt, + localCreatedAt: localCreatedAt ?? this.localCreatedAt, + remoteCreatedAt: remoteCreatedAt ?? this.remoteCreatedAt, + localUpdatedAt: localUpdatedAt ?? this.localUpdatedAt, + remoteUpdatedAt: remoteUpdatedAt ?? this.remoteUpdatedAt, + localDeletedAt: localDeletedAt ?? this.localDeletedAt, + remoteDeletedAt: remoteDeletedAt ?? this.remoteDeletedAt, userId: userId ?? this.userId, pinned: pinned ?? this.pinned, pinnedAt: pinnedAt ?? this.pinnedAt, @@ -2865,14 +3123,23 @@ class PinnedMessagesCompanion extends UpdateCompanion { if (command.present) { map['command'] = Variable(command.value); } - if (createdAt.present) { - map['created_at'] = Variable(createdAt.value); + if (localCreatedAt.present) { + map['local_created_at'] = Variable(localCreatedAt.value); } - if (updatedAt.present) { - map['updated_at'] = Variable(updatedAt.value); + if (remoteCreatedAt.present) { + map['remote_created_at'] = Variable(remoteCreatedAt.value); } - if (deletedAt.present) { - map['deleted_at'] = Variable(deletedAt.value); + if (localUpdatedAt.present) { + map['local_updated_at'] = Variable(localUpdatedAt.value); + } + if (remoteUpdatedAt.present) { + map['remote_updated_at'] = Variable(remoteUpdatedAt.value); + } + if (localDeletedAt.present) { + map['local_deleted_at'] = Variable(localDeletedAt.value); + } + if (remoteDeletedAt.present) { + map['remote_deleted_at'] = Variable(remoteDeletedAt.value); } if (userId.present) { map['user_id'] = Variable(userId.value); @@ -2923,9 +3190,12 @@ class PinnedMessagesCompanion extends UpdateCompanion { ..write('showInChannel: $showInChannel, ') ..write('shadowed: $shadowed, ') ..write('command: $command, ') - ..write('createdAt: $createdAt, ') - ..write('updatedAt: $updatedAt, ') - ..write('deletedAt: $deletedAt, ') + ..write('localCreatedAt: $localCreatedAt, ') + ..write('remoteCreatedAt: $remoteCreatedAt, ') + ..write('localUpdatedAt: $localUpdatedAt, ') + ..write('remoteUpdatedAt: $remoteUpdatedAt, ') + ..write('localDeletedAt: $localDeletedAt, ') + ..write('remoteDeletedAt: $remoteDeletedAt, ') ..write('userId: $userId, ') ..write('pinned: $pinned, ') ..write('pinnedAt: $pinnedAt, ') @@ -3060,7 +3330,7 @@ class $PinnedMessageReactionsTable extends PinnedMessageReactions } static TypeConverter, String> $converterextraData = - MapConverter(); + MapConverter(); static TypeConverter?, String?> $converterextraDatan = NullAwareTypeConverter.wrap($converterextraData); } @@ -3403,7 +3673,7 @@ class $ReactionsTable extends Reactions } static TypeConverter, String> $converterextraData = - MapConverter(); + MapConverter(); static TypeConverter?, String?> $converterextraDatan = NullAwareTypeConverter.wrap($converterextraData); } @@ -3790,7 +4060,7 @@ class $UsersTable extends Users with TableInfo<$UsersTable, UserEntity> { } static TypeConverter, String> $converterextraData = - MapConverter(); + MapConverter(); } class UserEntity extends DataClass implements Insertable { diff --git a/packages/stream_chat_persistence/lib/src/entity/messages.dart b/packages/stream_chat_persistence/lib/src/entity/messages.dart index fe20013a..5d585619 100644 --- a/packages/stream_chat_persistence/lib/src/entity/messages.dart +++ b/packages/stream_chat_persistence/lib/src/entity/messages.dart @@ -53,14 +53,52 @@ class Messages extends Table { /// A used command name. TextColumn get command => text().nullable()(); - /// The DateTime when the message was created. - DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + /// The DateTime on which the message was created. + /// + /// Returns the latest between [localCreatedAt] and [remoteCreatedAt]. + /// If both are null, returns [currentDateAndTime]. + Expression get createdAt { + return coalesce( + [localCreatedAt, remoteCreatedAt, currentDateAndTime], + ); + } - /// The DateTime when the message was updated last time. - DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + /// The DateTime on which the message was created on the client. + DateTimeColumn get localCreatedAt => dateTime().nullable()(); - /// The DateTime when the message was deleted. - DateTimeColumn get deletedAt => dateTime().nullable()(); + /// The DateTime on which the message was created on the server. + DateTimeColumn get remoteCreatedAt => dateTime().nullable()(); + + /// The DateTime on which the message was updated last time. + /// + /// Returns the latest between [localUpdatedAt] and [remoteUpdatedAt]. + /// If both are null, returns [createdAt]. + Expression get updatedAt { + return coalesce( + [localUpdatedAt, remoteUpdatedAt, createdAt], + ); + } + + /// The DateTime on which the message was updated on the client. + DateTimeColumn get localUpdatedAt => dateTime().nullable()(); + + /// The DateTime on which the message was updated on the server. + DateTimeColumn get remoteUpdatedAt => dateTime().nullable()(); + + /// The DateTime on which the message was deleted. + /// + /// Returns the latest between [localDeletedAt] and [remoteDeletedAt]. + Expression get deletedAt { + return coalesce( + [localDeletedAt, remoteDeletedAt], + ); + } + + /// The DateTime on which the message was deleted on the client. + DateTimeColumn get localDeletedAt => dateTime().nullable()(); + + /// The DateTime on which the message was deleted on the server. + DateTimeColumn get remoteDeletedAt => dateTime().nullable()(); /// Id of the User who sent the message TextColumn get userId => text().nullable()(); diff --git a/packages/stream_chat_persistence/lib/src/mapper/message_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/message_mapper.dart index 72d3d30f..0fd69db8 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/message_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/message_mapper.dart @@ -21,9 +21,13 @@ extension MessageEntityX on MessageEntity { final json = jsonDecode(it); return Attachment.fromData(json); }).toList(), - createdAt: createdAt, extraData: extraData ?? {}, - updatedAt: updatedAt, + createdAt: remoteCreatedAt, + localCreatedAt: localCreatedAt, + updatedAt: remoteUpdatedAt, + localUpdatedAt: localUpdatedAt, + deletedAt: remoteDeletedAt, + localDeletedAt: localDeletedAt, id: id, type: type, status: status, @@ -37,7 +41,6 @@ extension MessageEntityX on MessageEntity { showInChannel: showInChannel, text: messageText, user: user, - deletedAt: deletedAt, pinned: pinned, pinnedAt: pinnedAt, pinExpires: pinExpires, @@ -59,7 +62,8 @@ extension MessageX on Message { parentId: parentId, quotedMessageId: quotedMessageId, command: command, - createdAt: createdAt, + remoteCreatedAt: remoteCreatedAt, + localCreatedAt: localCreatedAt, shadowed: shadowed, showInChannel: showInChannel, replyCount: replyCount, @@ -67,10 +71,12 @@ extension MessageX on Message { reactionCounts: reactionCounts, mentionedUsers: mentionedUsers.map(jsonEncode).toList(), status: status, - updatedAt: updatedAt, + remoteUpdatedAt: remoteUpdatedAt, + localUpdatedAt: localUpdatedAt, extraData: extraData, userId: user?.id, - deletedAt: deletedAt, + remoteDeletedAt: remoteDeletedAt, + localDeletedAt: localDeletedAt, messageText: text, pinned: pinned, pinnedAt: pinnedAt, diff --git a/packages/stream_chat_persistence/lib/src/mapper/pinned_message_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/pinned_message_mapper.dart index 91a25a33..d4089a7c 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/pinned_message_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/pinned_message_mapper.dart @@ -21,9 +21,13 @@ extension PinnedMessageEntityX on PinnedMessageEntity { final json = jsonDecode(it); return Attachment.fromData(json); }).toList(), - createdAt: createdAt, extraData: extraData ?? {}, - updatedAt: updatedAt, + createdAt: remoteCreatedAt, + localCreatedAt: localCreatedAt, + updatedAt: remoteUpdatedAt, + localUpdatedAt: localUpdatedAt, + deletedAt: remoteDeletedAt, + localDeletedAt: localDeletedAt, id: id, type: type, status: status, @@ -37,7 +41,6 @@ extension PinnedMessageEntityX on PinnedMessageEntity { showInChannel: showInChannel, text: messageText, user: user, - deletedAt: deletedAt, pinned: pinned, pinnedAt: pinnedAt, pinExpires: pinExpires, @@ -60,7 +63,8 @@ extension PMessageX on Message { parentId: parentId, quotedMessageId: quotedMessageId, command: command, - createdAt: createdAt, + remoteCreatedAt: remoteCreatedAt, + localCreatedAt: localCreatedAt, shadowed: shadowed, showInChannel: showInChannel, replyCount: replyCount, @@ -68,10 +72,12 @@ extension PMessageX on Message { reactionCounts: reactionCounts, mentionedUsers: mentionedUsers.map(jsonEncode).toList(), status: status, - updatedAt: updatedAt, + remoteUpdatedAt: remoteUpdatedAt, + localUpdatedAt: localUpdatedAt, extraData: extraData, userId: user?.id, - deletedAt: deletedAt, + remoteDeletedAt: remoteDeletedAt, + localDeletedAt: localDeletedAt, messageText: text, pinned: pinned, pinnedAt: pinnedAt, diff --git a/packages/stream_chat_persistence/pubspec.yaml b/packages/stream_chat_persistence/pubspec.yaml index 9a1a6148..b4dfa792 100644 --- a/packages/stream_chat_persistence/pubspec.yaml +++ b/packages/stream_chat_persistence/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat_persistence homepage: https://github.com/GetStream/stream-chat-flutter description: Official Stream Chat Persistence library. Build your own chat experience using Dart and Flutter. -version: 6.3.0 +version: 6.4.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -18,7 +18,7 @@ dependencies: path: ^1.8.2 path_provider: ^2.0.1 sqlite3_flutter_libs: ^0.5.0 - stream_chat: ^6.3.0 + stream_chat: ^6.4.0 dev_dependencies: build_runner: ^2.3.3 diff --git a/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart index ccb9108e..b6524861 100644 --- a/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart @@ -38,7 +38,8 @@ void main() { parentId: 'testParentId', quotedMessageId: quotedMessage.id, command: 'testCommand', - createdAt: DateTime.now(), + localCreatedAt: DateTime.now(), + remoteCreatedAt: DateTime.now().add(const Duration(seconds: 1)), shadowed: math.Random().nextBool(), showInChannel: math.Random().nextBool(), replyCount: 33, @@ -52,10 +53,12 @@ void main() { jsonEncode(User(id: 'testuser')), ], status: MessageSendingStatus.sent, - updatedAt: DateTime.now(), + localUpdatedAt: DateTime.now(), + remoteUpdatedAt: DateTime.now().add(const Duration(seconds: 1)), extraData: {'extra_test_data': 'extraData'}, userId: user.id, - deletedAt: DateTime.now(), + localDeletedAt: DateTime.now(), + remoteDeletedAt: DateTime.now().add(const Duration(seconds: 1)), messageText: 'Hello', pinned: true, pinExpires: DateTime.now().toUtc(), @@ -81,7 +84,8 @@ void main() { expect(message.parentId, entity.parentId); expect(message.quotedMessageId, entity.quotedMessageId); expect(message.command, entity.command); - expect(message.createdAt, isSameDateAs(entity.createdAt)); + expect(message.localCreatedAt, isSameDateAs(entity.localCreatedAt)); + expect(message.remoteCreatedAt, isSameDateAs(entity.remoteCreatedAt)); expect(message.shadowed, entity.shadowed); expect(message.showInChannel, entity.showInChannel); for (var i = 0; i < message.mentionedUsers.length; i++) { @@ -93,10 +97,12 @@ void main() { expect(message.reactionScores, entity.reactionScores); expect(message.reactionCounts, entity.reactionCounts); expect(message.status, entity.status); - expect(message.updatedAt, isSameDateAs(entity.updatedAt)); + expect(message.localUpdatedAt, isSameDateAs(entity.localUpdatedAt)); + expect(message.remoteUpdatedAt, isSameDateAs(entity.remoteUpdatedAt)); expect(message.extraData, entity.extraData); expect(message.user!.id, entity.userId); - expect(message.deletedAt, isSameDateAs(entity.deletedAt)); + expect(message.localDeletedAt, isSameDateAs(entity.localDeletedAt)); + expect(message.remoteDeletedAt, isSameDateAs(entity.remoteDeletedAt)); expect(message.text, entity.messageText); expect(message.pinned, entity.pinned); expect(message.pinExpires, isSameDateAs(entity.pinExpires)); @@ -144,7 +150,8 @@ void main() { parentId: 'testParentId', quotedMessageId: quotedMessage.id, command: 'testCommand', - createdAt: DateTime.now(), + localCreatedAt: DateTime.now(), + createdAt: DateTime.now().add(const Duration(seconds: 1)), shadowed: math.Random().nextBool(), showInChannel: math.Random().nextBool(), replyCount: 33, @@ -157,10 +164,12 @@ void main() { (prev, curr) => prev?..update(curr.type, (value) => value + 1, ifAbsent: () => 1), ), - updatedAt: DateTime.now(), + localUpdatedAt: DateTime.now(), + updatedAt: DateTime.now().add(const Duration(seconds: 1)), extraData: const {'extra_test_data': 'extraData'}, user: user, - deletedAt: DateTime.now(), + localDeletedAt: DateTime.now(), + deletedAt: DateTime.now().add(const Duration(seconds: 1)), text: 'Hello', pinned: true, pinExpires: DateTime.now(), @@ -179,7 +188,8 @@ void main() { expect(entity.parentId, message.parentId); expect(entity.quotedMessageId, message.quotedMessageId); expect(entity.command, message.command); - expect(entity.createdAt, isSameDateAs(message.createdAt)); + expect(entity.localCreatedAt, isSameDateAs(message.localCreatedAt)); + expect(entity.remoteCreatedAt, isSameDateAs(message.remoteCreatedAt)); expect(entity.shadowed, message.shadowed); expect(entity.showInChannel, message.showInChannel); expect(entity.replyCount, message.replyCount); @@ -188,10 +198,12 @@ void main() { expect(entity.reactionScores, message.reactionScores); expect(entity.reactionCounts, message.reactionCounts); expect(entity.status, message.status); - expect(entity.updatedAt, isSameDateAs(message.updatedAt)); + expect(entity.localUpdatedAt, isSameDateAs(message.localUpdatedAt)); + expect(entity.remoteUpdatedAt, isSameDateAs(message.remoteUpdatedAt)); expect(entity.extraData, message.extraData); expect(entity.userId, message.user!.id); - expect(entity.deletedAt, isSameDateAs(message.deletedAt)); + expect(entity.localDeletedAt, isSameDateAs(message.localDeletedAt)); + expect(entity.remoteDeletedAt, isSameDateAs(message.remoteDeletedAt)); expect(entity.messageText, message.text); expect(entity.pinned, message.pinned); expect(entity.pinExpires, isSameDateAs(message.pinExpires)); diff --git a/packages/stream_chat_persistence/test/src/mapper/pinned_message_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/pinned_message_mapper_test.dart index fd3e075c..490af8ca 100644 --- a/packages/stream_chat_persistence/test/src/mapper/pinned_message_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/pinned_message_mapper_test.dart @@ -38,7 +38,8 @@ void main() { parentId: 'testParentId', quotedMessageId: quotedMessage.id, command: 'testCommand', - createdAt: DateTime.now(), + localCreatedAt: DateTime.now(), + remoteCreatedAt: DateTime.now().add(const Duration(seconds: 1)), shadowed: math.Random().nextBool(), showInChannel: math.Random().nextBool(), replyCount: 33, @@ -48,12 +49,16 @@ void main() { (prev, curr) => prev?..update(curr.type, (value) => value + 1, ifAbsent: () => 1), ), - mentionedUsers: [], + mentionedUsers: [ + jsonEncode(User(id: 'testuser')), + ], status: MessageSendingStatus.sent, - updatedAt: DateTime.now(), + localUpdatedAt: DateTime.now(), + remoteUpdatedAt: DateTime.now().add(const Duration(seconds: 1)), extraData: {'extra_test_data': 'extraData'}, userId: user.id, - deletedAt: DateTime.now(), + localDeletedAt: DateTime.now(), + remoteDeletedAt: DateTime.now().add(const Duration(seconds: 1)), messageText: 'Hello', pinned: true, pinExpires: DateTime.now().toUtc(), @@ -79,17 +84,25 @@ void main() { expect(message.parentId, entity.parentId); expect(message.quotedMessageId, entity.quotedMessageId); expect(message.command, entity.command); - expect(message.createdAt, isSameDateAs(entity.createdAt)); + expect(message.localCreatedAt, isSameDateAs(entity.localCreatedAt)); + expect(message.remoteCreatedAt, isSameDateAs(entity.remoteCreatedAt)); expect(message.shadowed, entity.shadowed); expect(message.showInChannel, entity.showInChannel); + for (var i = 0; i < message.mentionedUsers.length; i++) { + final entityMentionedUser = + User.fromJson(jsonDecode(entity.mentionedUsers[i])); + expect(message.mentionedUsers[i].id, entityMentionedUser.id); + } expect(message.replyCount, entity.replyCount); expect(message.reactionScores, entity.reactionScores); expect(message.reactionCounts, entity.reactionCounts); expect(message.status, entity.status); - expect(message.updatedAt, isSameDateAs(entity.updatedAt)); + expect(message.localUpdatedAt, isSameDateAs(entity.localUpdatedAt)); + expect(message.remoteUpdatedAt, isSameDateAs(entity.remoteUpdatedAt)); expect(message.extraData, entity.extraData); expect(message.user!.id, entity.userId); - expect(message.deletedAt, isSameDateAs(entity.deletedAt)); + expect(message.localDeletedAt, isSameDateAs(entity.localDeletedAt)); + expect(message.remoteDeletedAt, isSameDateAs(entity.remoteDeletedAt)); expect(message.text, entity.messageText); expect(message.pinned, entity.pinned); expect(message.pinExpires, isSameDateAs(entity.pinExpires)); @@ -108,7 +121,7 @@ void main() { } }); - test('toPinnedEntity should map message into PinnedMessageEntity', () { + test('toEntity should map message into MessageEntity', () { const cid = 'testCid'; final user = User(id: 'testUserId'); final quotedMessage = Message(id: 'testQuotedMessageId'); @@ -137,20 +150,26 @@ void main() { parentId: 'testParentId', quotedMessageId: quotedMessage.id, command: 'testCommand', - createdAt: DateTime.now(), + localCreatedAt: DateTime.now(), + createdAt: DateTime.now().add(const Duration(seconds: 1)), shadowed: math.Random().nextBool(), showInChannel: math.Random().nextBool(), replyCount: 33, + mentionedUsers: [ + User(id: 'testuser'), + ], reactionScores: {for (final r in reactions) r.type: r.score}, reactionCounts: reactions.fold( {}, (prev, curr) => prev?..update(curr.type, (value) => value + 1, ifAbsent: () => 1), ), - updatedAt: DateTime.now(), + localUpdatedAt: DateTime.now(), + updatedAt: DateTime.now().add(const Duration(seconds: 1)), extraData: const {'extra_test_data': 'extraData'}, user: user, - deletedAt: DateTime.now(), + localDeletedAt: DateTime.now(), + deletedAt: DateTime.now().add(const Duration(seconds: 1)), text: 'Hello', pinned: true, pinExpires: DateTime.now(), @@ -169,17 +188,22 @@ void main() { expect(entity.parentId, message.parentId); expect(entity.quotedMessageId, message.quotedMessageId); expect(entity.command, message.command); - expect(entity.createdAt, isSameDateAs(message.createdAt)); + expect(entity.localCreatedAt, isSameDateAs(message.localCreatedAt)); + expect(entity.remoteCreatedAt, isSameDateAs(message.remoteCreatedAt)); expect(entity.shadowed, message.shadowed); expect(entity.showInChannel, message.showInChannel); expect(entity.replyCount, message.replyCount); + expect( + entity.mentionedUsers, message.mentionedUsers.map(jsonEncode).toList()); expect(entity.reactionScores, message.reactionScores); expect(entity.reactionCounts, message.reactionCounts); expect(entity.status, message.status); - expect(entity.updatedAt, isSameDateAs(message.updatedAt)); + expect(entity.localUpdatedAt, isSameDateAs(message.localUpdatedAt)); + expect(entity.remoteUpdatedAt, isSameDateAs(message.remoteUpdatedAt)); expect(entity.extraData, message.extraData); expect(entity.userId, message.user!.id); - expect(entity.deletedAt, isSameDateAs(message.deletedAt)); + expect(entity.localDeletedAt, isSameDateAs(message.localDeletedAt)); + expect(entity.remoteDeletedAt, isSameDateAs(message.remoteDeletedAt)); expect(entity.messageText, message.text); expect(entity.pinned, message.pinned); expect(entity.pinExpires, isSameDateAs(message.pinExpires));