From 70673f900a948d81474b19b7ac35ad52b567a4ea Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 27 Feb 2025 13:21:13 +0530 Subject: [PATCH 1/4] fix(llc, core, ui): fix not able to edit ogAttachments. (#2122) Co-authored-by: xsahil03x <25670178+xsahil03x@users.noreply.github.com> --- packages/stream_chat/CHANGELOG.md | 4 +++ .../lib/src/core/models/attachment.dart | 20 ++++-------- .../lib/src/core/models/attachment.g.dart | 2 +- .../test/fixtures/message_to_json.json | 1 - .../test/src/core/models/message_test.dart | 2 -- packages/stream_chat_flutter/CHANGELOG.md | 6 ++++ .../message_input/stream_message_input.dart | 10 +++--- .../src/attachment/giphy_attachment_test.dart | 1 + .../goldens/ci/edit_message_sheet_0.png | Bin 4456 -> 5362 bytes .../stream_chat_flutter/test/src/mocks.dart | 5 ++- .../stream_chat_flutter_core/CHANGELOG.md | 19 ++++++++---- .../src/stream_message_input_controller.dart | 29 ++++++++++-------- 12 files changed, 57 insertions(+), 42 deletions(-) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 71dbd85269..f47221afd7 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -4,6 +4,10 @@ - [[#1774]](https://github.com/GetStream/stream-chat-flutter/issues/1774) Fixed failed to execute 'close' on 'WebSocket'. - [[#2016]](https://github.com/GetStream/stream-chat-flutter/issues/2016) Fix muted channel's unreadCount incorrectly updated. + +🔄 Changed + +- Refactored identifying the `Attachment.uploadState` logic for local and remote attachments. Also updated the logic for determining the attachment type to check for ogScrapeUrl instead of `AttachmentType.giphy`. ## 9.4.0 diff --git a/packages/stream_chat/lib/src/core/models/attachment.dart b/packages/stream_chat/lib/src/core/models/attachment.dart index afc825b6a3..00bd8d96fa 100644 --- a/packages/stream_chat/lib/src/core/models/attachment.dart +++ b/packages/stream_chat/lib/src/core/models/attachment.dart @@ -51,11 +51,10 @@ class Attachment extends Equatable { this.originalHeight, Map extraData = const {}, this.file, - UploadState? uploadState, + this.uploadState = const UploadState.preparing(), }) : id = id ?? const Uuid().v4(), _type = type, title = title ?? file?.name, - _uploadState = uploadState, localUri = file?.path != null ? Uri.parse(file!.path!) : null, // For backwards compatibility, // set 'file_size', 'mime_type' in [extraData]. @@ -97,9 +96,9 @@ class Attachment extends Equatable { ///The attachment type based on the URL resource. This can be: audio, ///image or video String? get type { - // If the attachment contains titleLink but is not of type giphy, we - // consider it as a urlPreview. - if (_type != AttachmentType.giphy && titleLink != null) { + // If the attachment contains ogScrapeUrl as well as titleLink, we consider + // it as a urlPreview. + if (ogScrapeUrl != null && titleLink != null) { return AttachmentType.urlPreview; } @@ -163,15 +162,8 @@ class Attachment extends Equatable { final AttachmentFile? file; /// The current upload state of the attachment - UploadState get uploadState { - if (_uploadState case final state?) return state; - - return ((assetUrl != null || imageUrl != null || thumbUrl != null) - ? const UploadState.success() - : const UploadState.preparing()); - } - - final UploadState? _uploadState; + @JsonKey(defaultValue: UploadState.success) + final UploadState uploadState; /// Map of custom channel extraData final Map extraData; diff --git a/packages/stream_chat/lib/src/core/models/attachment.g.dart b/packages/stream_chat/lib/src/core/models/attachment.g.dart index 689f8871e2..42d656b758 100644 --- a/packages/stream_chat/lib/src/core/models/attachment.g.dart +++ b/packages/stream_chat/lib/src/core/models/attachment.g.dart @@ -36,7 +36,7 @@ Attachment _$AttachmentFromJson(Map json) => Attachment( ? null : AttachmentFile.fromJson(json['file'] as Map), uploadState: json['upload_state'] == null - ? null + ? const UploadState.success() : UploadState.fromJson(json['upload_state'] as Map), ); diff --git a/packages/stream_chat/test/fixtures/message_to_json.json b/packages/stream_chat/test/fixtures/message_to_json.json index 3cf86bd7d0..8388eb8220 100644 --- a/packages/stream_chat/test/fixtures/message_to_json.json +++ b/packages/stream_chat/test/fixtures/message_to_json.json @@ -10,7 +10,6 @@ "title": "The Lion King Disney GIF - Find & Share on GIPHY", "thumb_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", "text": "Discover & share this Lion King Live Action GIF with everyone you know. GIPHY is how you search, share, discover, and create GIFs.", - "og_scrape_url": "https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA", "image_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", "author_name": "GIPHY", "asset_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.mp4", diff --git a/packages/stream_chat/test/src/core/models/message_test.dart b/packages/stream_chat/test/src/core/models/message_test.dart index c627a739d5..3ee7e96a7e 100644 --- a/packages/stream_chat/test/src/core/models/message_test.dart +++ b/packages/stream_chat/test/src/core/models/message_test.dart @@ -51,8 +51,6 @@ void main() { 'https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif', 'asset_url': 'https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.mp4', - 'og_scrape_url': - 'https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA' }) ], showInChannel: true, diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 1dfc7c9258..13fca25cb2 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,3 +1,9 @@ +## Upcoming + +🐞 Fixed + +- Fixed `StreamMessageInput` not able to edit the ogAttachments. + ## 9.4.0 🔄 Changed 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 01d7ecfe2d..10921a3b72 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 @@ -480,16 +480,18 @@ class StreamMessageInputState extends State assert(_controller != null, ''); registerForRestoration(_controller!, 'messageInputController'); - _effectiveController - ..removeListener(_onChangedDebounced) - ..addListener(_onChangedDebounced); - if (!_isEditing && _timeOut <= 0) _startSlowMode(); + _initialiseEffectiveController(); } void _initialiseEffectiveController() { _effectiveController ..removeListener(_onChangedDebounced) ..addListener(_onChangedDebounced); + + // Call the listener once to make sure the initial state is reflected + // correctly in the UI. + _onChangedDebounced.call(); + if (!_isEditing && _timeOut <= 0) _startSlowMode(); } diff --git a/packages/stream_chat_flutter/test/src/attachment/giphy_attachment_test.dart b/packages/stream_chat_flutter/test/src/attachment/giphy_attachment_test.dart index 755686c711..d947bd2ed6 100644 --- a/packages/stream_chat_flutter/test/src/attachment/giphy_attachment_test.dart +++ b/packages/stream_chat_flutter/test/src/attachment/giphy_attachment_test.dart @@ -38,6 +38,7 @@ void main() { extraData: const { 'mime_type': 'gif', }, + uploadState: const UploadState.success(), ), ), ), diff --git a/packages/stream_chat_flutter/test/src/bottom_sheets/goldens/ci/edit_message_sheet_0.png b/packages/stream_chat_flutter/test/src/bottom_sheets/goldens/ci/edit_message_sheet_0.png index 035e74551f08ea1d4b18c506d16fdd48907ab7cf..3983228cc2d2d8871434e2bd9a15e4caa209e3d2 100644 GIT binary patch literal 5362 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

Gn%>1==wR-mvCx&>C`sIdU_?l!{RSjk?H0m2 z$B3Fh-K7Ic>+{h=(QDw1mF9}uR$C0eqyFUQAO7^|8RK&Xboz4zC<^K_zc!!R;4!zN zaztDr-KY0A4^4I_!~dK%uBC4f>9u;LlpP;l-lPMXePNSY&@%IJ;?{D)RKV(Iy!pNW zRUQdU&u9#1(LLp3v%tHm1QV8<{-XCaO)aTiWT`{#`ZRsb-pVc((_gR&*W6tbCkuol z_9(=tTF92_b@sTP{f5?*#wAbs61kG)WC3n!-T*73sC2|4Cm3V#`V-FlmRBk*?*X+Z zk*30En>M)AgPkTybK~cL*lte+$3j2*7c=1;`r+HNoi`R(qYHwdm%YZm=T@m^r$b}v zDNhbNq^b13Hw=j3p>9;C^ZElMQX#MdKo6oc*5zZ*TROJCHvZ|SP+i^Y>-*xI^5nIq zb$x$<8tG(Ko&p`!D|W;48(%xbyPTV;A*Q>;AWFEdfk7c{AiLn3kI1rfGjJ1}EKx(F zqn5coj8EMvCDyC?(ntQ*YD}14t)nd)>BCV$xc*xkjZDXhvfPc|8V`PZ%!S6^9yW+( zE%_V^%81$ZU}vdJt@?y{KU-*ZMr)Q(dJrMxfF)~a$Z~sV1s)=P{rU>{uB_~A z?yCIXrLDNI3YiAVN@wy7v-|h+fF+v0oE8N_aF)7QIOtwx_2H#xO1ft)lPA!8&?!|{ z5ICg^z!CQLtrP!xh6kR|u`&bLxR=qB)gab^-m4UfYOWeb$gX#JIkz}!VVn)yk~Jy(aY(86;t43uOu2@ zNlYNigVFS^CwsWL34JSPO*@OVY9sr`y?L{oh3b67$q|k3(*$1xGQxhm1uhYc0{{d= zfeC;TLjideloOzID}yfmxVQ{;J~hn8hrbVM(v_8pq|TqCB)w2o)ILIOME))$7A@cB z^1-^&v3-<{|HD;FicoT=5Q-oQw)Ada#M8Iu|8dkeO@c*a(HAImG%nFPLDGC;Hp@0M#gMEnb*wH1i~ literal 4456 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 { attachments = []; } - // Only used to store the value locally in order to remove it if we call - // [clearOGAttachment] or [setOGAttachment] again. - Attachment? _ogAttachment; - /// Returns the og attachment of the message if set - Attachment? get ogAttachment => - attachments.firstWhereOrNull((it) => it.id == _ogAttachment?.id); + Attachment? get ogAttachment { + return attachments.firstWhereOrNull((it) => it.ogScrapeUrl != null); + } /// Sets the og attachment in the message. void setOGAttachment(Attachment attachment) { - attachments = [...attachments] - ..remove(_ogAttachment) - ..insert(0, attachment); - _ogAttachment = attachment; + final updatedAttachments = [...attachments]; + // Remove the existing og attachment if it exists. + if (ogAttachment case final existingOGAttachment?) { + updatedAttachments.remove(existingOGAttachment); + } + + // Add the new og attachment at the beginning of the list. + updatedAttachments.insert(0, attachment); + + // Update the attachments list. + attachments = updatedAttachments; } /// Removes the og attachment. void clearOGAttachment() { - if (_ogAttachment != null) { - removeAttachment(_ogAttachment!); + if (ogAttachment case final existingOGAttachment?) { + removeAttachment(existingOGAttachment); } - _ogAttachment = null; } /// Returns the poll in the message. From 6edd8d3620000e36568f58358cfdfcc1405fdc11 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 27 Feb 2025 16:26:41 +0530 Subject: [PATCH 2/4] fix(ui): fix deleted messages showing pinned background. (#2125) --- packages/stream_chat_flutter/CHANGELOG.md | 1 + .../lib/src/message_widget/message_widget.dart | 8 ++++---- .../lib/src/message_widget/message_widget_content.dart | 4 +--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 13fca25cb2..3428be49be 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -3,6 +3,7 @@ 🐞 Fixed - Fixed `StreamMessageInput` not able to edit the ogAttachments. +- Fixed `MessageWidget` showing pinned background for deleted messages. ## 9.4.0 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 44c003a065..5260c51f28 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 @@ -605,7 +605,7 @@ class _StreamMessageWidgetState extends State /// {@template isPinned} /// Whether [StreamMessageWidget.message] is pinned or not. /// {@endtemplate} - bool get isPinned => widget.message.pinned; + bool get isPinned => widget.message.pinned && !widget.message.isDeleted; /// {@template shouldShowReactions} /// Should show message reactions if [StreamMessageWidget.showReactions] is @@ -680,7 +680,7 @@ class _StreamMessageWidgetState extends State type: MaterialType.transparency, child: AnimatedContainer( duration: const Duration(seconds: 1), - color: widget.message.pinned && widget.showPinHighlight + color: isPinned && widget.showPinHighlight ? _streamChatTheme.colorTheme.highlight // ignore: deprecated_member_use : _streamChatTheme.colorTheme.barsBg.withOpacity(0), @@ -891,13 +891,13 @@ class _StreamMessageWidgetState extends State ), title: Text( context.translations.togglePinUnpinText( - pinned: widget.message.pinned, + pinned: isPinned, ), ), onClick: () async { Navigator.of(context, rootNavigator: true).pop(); try { - if (!widget.message.pinned) { + if (!isPinned) { await channel.pinMessage(widget.message); } else { await channel.unpinMessage(widget.message); 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 963b33f87e..a31a6f602b 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 @@ -259,9 +259,7 @@ class MessageWidgetContent extends StatelessWidget { reverse ? CrossAxisAlignment.end : CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - if (message.pinned && - message.pinnedBy != null && - showPinHighlight) + if (isPinned && message.pinnedBy != null && showPinHighlight) PinnedMessage( pinnedBy: message.pinnedBy!, currentUser: streamChat.currentUser!, From 12dc578d60183a47c35287e3459e2cdeff416724 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 28 Feb 2025 17:59:09 +0530 Subject: [PATCH 3/4] feat(llc): add support for system messages not updating `channel.lastMessageAt` (#2126) --- packages/stream_chat/CHANGELOG.md | 4 + .../stream_chat/lib/src/client/channel.dart | 43 ++- .../lib/src/core/models/channel_config.dart | 8 + .../lib/src/core/models/channel_config.g.dart | 4 + .../lib/src/core/models/event.dart | 7 + .../lib/src/core/models/event.g.dart | 5 + .../lib/src/core/models/message.dart | 10 + .../lib/src/core/models/message.g.dart | 3 + .../test/fixtures/channel_state.json | 6 +- packages/stream_chat/test/fixtures/event.json | 3 +- .../stream_chat/test/fixtures/message.json | 5 +- .../test/src/client/channel_test.dart | 246 ++++++++++++++++++ .../src/core/models/channel_state_test.dart | 8 +- .../test/src/core/models/event_test.dart | 9 + .../test/src/core/models/message_test.dart | 2 + 15 files changed, 349 insertions(+), 14 deletions(-) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index f47221afd7..a70ef5f36f 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -9,6 +9,10 @@ - Refactored identifying the `Attachment.uploadState` logic for local and remote attachments. Also updated the logic for determining the attachment type to check for ogScrapeUrl instead of `AttachmentType.giphy`. +✅ Added + +- [[#2101]](https://github.com/GetStream/stream-chat-flutter/issues/2101) Added support for system messages not updating `channel.lastMessageAt` + ## 9.4.0 🔄 Changed diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index d16ea79ec1..8f98b70992 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -1911,11 +1911,13 @@ class ChannelClientState { ChannelClientState( this._channel, ChannelState channelState, - //ignore: unnecessary_parenthesis - ) : _debouncedUpdatePersistenceChannelState = ((ChannelState state) => - _channel._client.chatPersistenceClient - ?.updateChannelState(state)) - .debounced(const Duration(seconds: 1)) { + ) : _debouncedUpdatePersistenceChannelState = debounce( + (ChannelState state) { + final persistenceClient = _channel._client.chatPersistenceClient; + return persistenceClient?.updateChannelState(state); + }, + const Duration(seconds: 1), + ) { _retryQueue = RetryQueue( channel: _channel, logger: _channel.client.detachedLogger( @@ -2495,6 +2497,28 @@ class ChannelClientState { })); } + // Logic taken from the backend SDK + // https://github.com/GetStream/chat/blob/9245c2b3f7e679267d57ee510c60e93de051cb8e/types/channel.go#L1136-L1150 + bool _shouldUpdateChannelLastMessageAt(Message message) { + if (message.shadowed) return false; + if (message.isEphemeral) return false; + + final config = channelState.channel?.config; + if (message.isSystem && config?.skipLastMsgUpdateForSystemMsgs == true) { + return false; + } + + final currentUser = _channel._client.state.currentUser; + final restrictedVisibility = message.restrictedVisibility; + if (restrictedVisibility case final visibility?) { + if (visibility.isNotEmpty && !visibility.contains(currentUser?.id)) { + return false; + } + } + + return true; + } + /// Updates the [message] in the state if it exists. Adds it otherwise. void updateMessage(Message message) { // Determine if the message should be displayed in the channel view. @@ -2547,12 +2571,19 @@ class ChannelClientState { // Handle updates to pinned messages. final newPinnedMessages = _updatePinnedMessages(message); + // Calculate the new last message at time. + var lastMessageAt = _channelState.channel?.lastMessageAt; + lastMessageAt ??= message.createdAt; + if (_shouldUpdateChannelLastMessageAt(message)) { + lastMessageAt = [lastMessageAt, message.createdAt].max; + } + // Apply the updated lists to the channel state. _channelState = _channelState.copyWith( messages: newMessages.sorted(_sortByCreatedAt), pinnedMessages: newPinnedMessages, channel: _channelState.channel?.copyWith( - lastMessageAt: message.createdAt, + lastMessageAt: lastMessageAt, ), ); } diff --git a/packages/stream_chat/lib/src/core/models/channel_config.dart b/packages/stream_chat/lib/src/core/models/channel_config.dart index 7aa625a231..b15c109926 100644 --- a/packages/stream_chat/lib/src/core/models/channel_config.dart +++ b/packages/stream_chat/lib/src/core/models/channel_config.dart @@ -24,6 +24,7 @@ class ChannelConfig { this.typingEvents = false, this.uploads = false, this.urlEnrichment = false, + this.skipLastMsgUpdateForSystemMsgs = false, }) : createdAt = createdAt ?? DateTime.now(), updatedAt = updatedAt ?? DateTime.now(); @@ -79,6 +80,13 @@ class ChannelConfig { /// True if urls appears as attachments final bool urlEnrichment; + /// If true the last message at date will not be updated when a system message + /// is added. + /// + /// This is useful for scenarios where you want to track the last time a user + /// message was added to the channel. + final bool skipLastMsgUpdateForSystemMsgs; + /// Serialize to json Map toJson() => _$ChannelConfigToJson(this); } diff --git a/packages/stream_chat/lib/src/core/models/channel_config.g.dart b/packages/stream_chat/lib/src/core/models/channel_config.g.dart index efacc44db7..11f188a61f 100644 --- a/packages/stream_chat/lib/src/core/models/channel_config.g.dart +++ b/packages/stream_chat/lib/src/core/models/channel_config.g.dart @@ -31,6 +31,8 @@ ChannelConfig _$ChannelConfigFromJson(Map json) => typingEvents: json['typing_events'] as bool? ?? false, uploads: json['uploads'] as bool? ?? false, urlEnrichment: json['url_enrichment'] as bool? ?? false, + skipLastMsgUpdateForSystemMsgs: + json['skip_last_msg_update_for_system_msgs'] as bool? ?? false, ); Map _$ChannelConfigToJson(ChannelConfig instance) => @@ -51,4 +53,6 @@ Map _$ChannelConfigToJson(ChannelConfig instance) => 'typing_events': instance.typingEvents, 'uploads': instance.uploads, 'url_enrichment': instance.urlEnrichment, + 'skip_last_msg_update_for_system_msgs': + instance.skipLastMsgUpdateForSystemMsgs, }; diff --git a/packages/stream_chat/lib/src/core/models/event.dart b/packages/stream_chat/lib/src/core/models/event.dart index 99563e6eb4..0be0dca501 100644 --- a/packages/stream_chat/lib/src/core/models/event.dart +++ b/packages/stream_chat/lib/src/core/models/event.dart @@ -26,6 +26,7 @@ class Event { this.member, this.channelId, this.channelType, + this.channelLastMessageAt, this.parentId, this.hardDelete, this.aiState, @@ -58,6 +59,9 @@ class Event { /// The channel type to which the event belongs final String? channelType; + /// The dateTime at which the last message was sent in the channel. + final DateTime? channelLastMessageAt; + /// The connection id in which the event has been sent final String? connectionId; @@ -168,6 +172,7 @@ class Event { 'member', 'channel_id', 'channel_type', + 'channel_last_message_at', 'parent_id', 'hard_delete', 'is_local', @@ -190,6 +195,7 @@ class Event { String? cid, String? channelId, String? channelType, + DateTime? channelLastMessageAt, String? connectionId, DateTime? createdAt, OwnUser? me, @@ -231,6 +237,7 @@ class Event { member: member ?? this.member, channelId: channelId ?? this.channelId, channelType: channelType ?? this.channelType, + channelLastMessageAt: channelLastMessageAt ?? this.channelLastMessageAt, parentId: parentId ?? this.parentId, hardDelete: hardDelete ?? this.hardDelete, aiState: aiState ?? this.aiState, diff --git a/packages/stream_chat/lib/src/core/models/event.g.dart b/packages/stream_chat/lib/src/core/models/event.g.dart index ee1bf2051c..e0923750fc 100644 --- a/packages/stream_chat/lib/src/core/models/event.g.dart +++ b/packages/stream_chat/lib/src/core/models/event.g.dart @@ -42,6 +42,9 @@ Event _$EventFromJson(Map json) => Event( : Member.fromJson(json['member'] as Map), channelId: json['channel_id'] as String?, channelType: json['channel_type'] as String?, + channelLastMessageAt: json['channel_last_message_at'] == null + ? null + : DateTime.parse(json['channel_last_message_at'] as String), parentId: json['parent_id'] as String?, hardDelete: json['hard_delete'] as bool?, aiState: $enumDecodeNullable(_$AITypingStateEnumMap, json['ai_state'], @@ -62,6 +65,8 @@ Map _$EventToJson(Event instance) => { 'cid': instance.cid, 'channel_id': instance.channelId, 'channel_type': instance.channelType, + 'channel_last_message_at': + instance.channelLastMessageAt?.toIso8601String(), 'connection_id': instance.connectionId, 'created_at': instance.createdAt.toIso8601String(), 'me': instance.me?.toJson(), diff --git a/packages/stream_chat/lib/src/core/models/message.dart b/packages/stream_chat/lib/src/core/models/message.dart index ac1be2e491..b3a8e890bd 100644 --- a/packages/stream_chat/lib/src/core/models/message.dart +++ b/packages/stream_chat/lib/src/core/models/message.dart @@ -56,6 +56,7 @@ class Message extends Equatable { this.extraData = const {}, this.state = const MessageState.initial(), this.i18n, + this.restrictedVisibility, }) : id = id ?? const Uuid().v4(), pinExpires = pinExpires?.toUtc(), remoteCreatedAt = createdAt, @@ -237,6 +238,10 @@ class Message extends Equatable { String? get pollId => _pollId ?? poll?.id; final String? _pollId; + /// The list of those users who have restricted visibility for this message. + @JsonKey(includeToJson: false) + final List? restrictedVisibility; + /// Message custom extraData. final Map extraData; @@ -291,6 +296,7 @@ class Message extends Equatable { 'i18n', 'poll', 'poll_id', + 'restricted_visibility', ]; /// Serialize to json. @@ -335,6 +341,7 @@ class Message extends Equatable { Map? extraData, MessageState? state, Map? i18n, + List? restrictedVisibility, }) { assert(() { if (pinExpires is! DateTime && @@ -408,6 +415,7 @@ class Message extends Equatable { extraData: extraData ?? this.extraData, state: state ?? this.state, i18n: i18n ?? this.i18n, + restrictedVisibility: restrictedVisibility ?? this.restrictedVisibility, ); } @@ -450,6 +458,7 @@ class Message extends Equatable { extraData: other.extraData, state: other.state, i18n: other.i18n, + restrictedVisibility: other.restrictedVisibility, ); } @@ -512,5 +521,6 @@ class Message extends Equatable { extraData, state, i18n, + restrictedVisibility, ]; } diff --git a/packages/stream_chat/lib/src/core/models/message.g.dart b/packages/stream_chat/lib/src/core/models/message.g.dart index efb7d0dd37..2a0ff4e719 100644 --- a/packages/stream_chat/lib/src/core/models/message.g.dart +++ b/packages/stream_chat/lib/src/core/models/message.g.dart @@ -76,6 +76,9 @@ Message _$MessageFromJson(Map json) => Message( i18n: (json['i18n'] as Map?)?.map( (k, e) => MapEntry(k, e as String), ), + restrictedVisibility: (json['restricted_visibility'] as List?) + ?.map((e) => e as String) + .toList(), ); Map _$MessageToJson(Message instance) => { diff --git a/packages/stream_chat/test/fixtures/channel_state.json b/packages/stream_chat/test/fixtures/channel_state.json index 4814654332..528259be0f 100644 --- a/packages/stream_chat/test/fixtures/channel_state.json +++ b/packages/stream_chat/test/fixtures/channel_state.json @@ -1,4 +1,3 @@ - { "channel": { "id": "dev", @@ -79,7 +78,10 @@ "pinned_at": null, "pin_expires": null, "pinned_by": null, - "poll_id": null + "poll_id": null, + "restricted_visibility": [ + "user-id-3" + ] }, { "id": "dry-meadow-0-e8e74482-b4cd-48db-9d1e-30e6c191786f", diff --git a/packages/stream_chat/test/fixtures/event.json b/packages/stream_chat/test/fixtures/event.json index cc567a9b8e..4bcb908693 100644 --- a/packages/stream_chat/test/fixtures/event.json +++ b/packages/stream_chat/test/fixtures/event.json @@ -29,5 +29,6 @@ "ai_state": "AI_STATE_THINKING", "ai_message": "Some message", "unread_thread_messages": 2, - "unread_threads": 3 + "unread_threads": 3, + "channel_last_message_at": "2019-03-27T17:40:17.155892Z" } \ No newline at end of file diff --git a/packages/stream_chat/test/fixtures/message.json b/packages/stream_chat/test/fixtures/message.json index 97202415cd..6b99f2fbc1 100644 --- a/packages/stream_chat/test/fixtures/message.json +++ b/packages/stream_chat/test/fixtures/message.json @@ -62,5 +62,8 @@ "reply_count": 0, "created_at": "2020-01-28T22:17:31.107978Z", "updated_at": "2020-01-28T22:17:31.130506Z", - "mentioned_users": [] + "mentioned_users": [], + "restricted_visibility": [ + "user-id-3" + ] } \ No newline at end of file diff --git a/packages/stream_chat/test/src/client/channel_test.dart b/packages/stream_chat/test/src/client/channel_test.dart index c61b458e21..aefc42c336 100644 --- a/packages/stream_chat/test/src/client/channel_test.dart +++ b/packages/stream_chat/test/src/client/channel_test.dart @@ -1,4 +1,7 @@ +// ignore_for_file: lines_longer_than_80_chars + import 'package:mocktail/mocktail.dart'; +import 'package:rxdart/rxdart.dart'; import 'package:stream_chat/src/client/retry_policy.dart'; import 'package:stream_chat/src/core/models/banned_user.dart'; import 'package:stream_chat/stream_chat.dart'; @@ -12,6 +15,7 @@ void main() { ChannelState _generateChannelState( String channelId, String channelType, { + DateTime? lastMessageAt, bool mockChannelConfig = false, }) { ChannelConfig? config; @@ -24,6 +28,7 @@ void main() { id: channelId, type: channelType, config: config, + lastMessageAt: lastMessageAt, ); final state = ChannelState(channel: channel); return state; @@ -2851,4 +2856,245 @@ void main() { }); }); }); + + group('WS events', () { + late final client = MockStreamChatClient(); + + setUpAll(() { + // Fallback values + registerFallbackValue(FakeMessage()); + registerFallbackValue(FakeAttachmentFile()); + registerFallbackValue(FakeEvent()); + + // detached loggers + when(() => client.detachedLogger(any())).thenAnswer((invocation) { + final name = invocation.positionalArguments.first; + return _createLogger(name); + }); + + final retryPolicy = RetryPolicy( + shouldRetry: (_, __, ___) => false, + delayFactor: Duration.zero, + ); + when(() => client.retryPolicy).thenReturn(retryPolicy); + + // fake clientState + final clientState = FakeClientState(); + when(() => client.state).thenReturn(clientState); + + // client logger + when(() => client.logger).thenReturn(_createLogger('mock-client-logger')); + }); + + group( + '${EventType.messageNew} or ${EventType.notificationMessageNew}', + () { + final initialLastMessageAt = DateTime.now(); + late PublishSubject eventController; + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + late Channel channel; + + setUp(() { + final localEvent = Event(type: 'event.local'); + when(() => client.on(any(), any(), any(), any())) + .thenAnswer((_) => Stream.value(localEvent)); + + eventController = PublishSubject(); + when(() => client.on( + EventType.messageNew, + EventType.notificationMessageNew, + )).thenAnswer((_) => eventController.stream); + + final channelState = _generateChannelState( + channelId, + channelType, + mockChannelConfig: true, + lastMessageAt: initialLastMessageAt, + ); + + channel = Channel.fromState(client, channelState); + }); + + tearDown(() { + eventController.close(); + channel.dispose(); + }); + + Event createNewMessageEvent(Message message) { + return Event( + cid: channel.cid, + type: EventType.messageNew, + message: message, + ); + } + + test( + "should update 'channel.lastMessageAt'", + () async { + expect(channel.lastMessageAt, equals(initialLastMessageAt)); + + final message = Message( + id: 'test-message-id', + user: client.state.currentUser, + createdAt: initialLastMessageAt.add(const Duration(seconds: 3)), + ); + + final newMessageEvent = createNewMessageEvent(message); + eventController.add(newMessageEvent); + + // Wait for the event to get processed + await Future.delayed(Duration.zero); + + expect(channel.lastMessageAt, equals(message.createdAt)); + expect(channel.lastMessageAt, isNot(initialLastMessageAt)); + }, + ); + + test( + "should update 'channel.lastMessageAt' when Message has restricted visibility only for the current user", + () async { + expect(channel.lastMessageAt, equals(initialLastMessageAt)); + + final message = Message( + id: 'test-message-id', + user: client.state.currentUser, + // Message is visible to the current user. + restrictedVisibility: [client.state.currentUser!.id], + createdAt: initialLastMessageAt.add(const Duration(seconds: 3)), + ); + + final newMessageEvent = createNewMessageEvent(message); + eventController.add(newMessageEvent); + + // Wait for the event to get processed + await Future.delayed(Duration.zero); + + expect(channel.lastMessageAt, equals(message.createdAt)); + expect(channel.lastMessageAt, isNot(initialLastMessageAt)); + }, + ); + + test( + "should not update 'channel.lastMessageAt' when 'message.createdAt' is older", + () async { + expect(channel.lastMessageAt, equals(initialLastMessageAt)); + + final message = Message( + id: 'test-message-id', + user: client.state.currentUser, + // Older than the current 'channel.lastMessageAt'. + createdAt: initialLastMessageAt.subtract(const Duration(days: 1)), + ); + + final newMessageEvent = createNewMessageEvent(message); + eventController.add(newMessageEvent); + + // Wait for the event to get processed + await Future.delayed(Duration.zero); + + expect(channel.lastMessageAt, isNot(message.createdAt)); + expect(channel.lastMessageAt, equals(initialLastMessageAt)); + }, + ); + + test( + "should not update 'channel.lastMessageAt' when Message is shadowed", + () async { + expect(channel.lastMessageAt, equals(initialLastMessageAt)); + + final message = Message( + id: 'test-message-id', + user: client.state.currentUser, + shadowed: true, + createdAt: initialLastMessageAt.add(const Duration(seconds: 3)), + ); + + final newMessageEvent = createNewMessageEvent(message); + eventController.add(newMessageEvent); + + // Wait for the event to get processed + await Future.delayed(Duration.zero); + + expect(channel.lastMessageAt, isNot(message.createdAt)); + expect(channel.lastMessageAt, equals(initialLastMessageAt)); + }, + ); + + test( + "should not update 'channel.lastMessageAt' when Message is ephemeral", + () async { + expect(channel.lastMessageAt, equals(initialLastMessageAt)); + + final message = Message( + type: 'ephemeral', + id: 'test-message-id', + user: client.state.currentUser, + createdAt: initialLastMessageAt.add(const Duration(seconds: 3)), + ); + + final newMessageEvent = createNewMessageEvent(message); + eventController.add(newMessageEvent); + + // Wait for the event to get processed + await Future.delayed(Duration.zero); + + expect(channel.lastMessageAt, isNot(message.createdAt)); + expect(channel.lastMessageAt, equals(initialLastMessageAt)); + }, + ); + + test( + "should not update 'channel.lastMessageAt' when Message has restricted visibility but not for the current user", + () async { + expect(channel.lastMessageAt, equals(initialLastMessageAt)); + + final message = Message( + id: 'test-message-id', + user: client.state.currentUser, + // Message is only visible to user-1 not the current user. + restrictedVisibility: const ['user-1'], + createdAt: initialLastMessageAt.add(const Duration(seconds: 3)), + ); + + final newMessageEvent = createNewMessageEvent(message); + eventController.add(newMessageEvent); + + // Wait for the event to get processed + await Future.delayed(Duration.zero); + + expect(channel.lastMessageAt, isNot(message.createdAt)); + expect(channel.lastMessageAt, equals(initialLastMessageAt)); + }, + ); + + test( + "should not update 'channel.lastMessageAt' when Message is system and skip is enabled", + () async { + expect(channel.lastMessageAt, equals(initialLastMessageAt)); + + when( + () => channel.config?.skipLastMsgUpdateForSystemMsgs, + ).thenReturn(true); + + final message = Message( + type: 'system', + id: 'test-message-id', + user: client.state.currentUser, + createdAt: initialLastMessageAt.add(const Duration(seconds: 3)), + ); + + final newMessageEvent = createNewMessageEvent(message); + eventController.add(newMessageEvent); + + // Wait for the event to get processed + await Future.delayed(Duration.zero); + + expect(channel.lastMessageAt, isNot(message.createdAt)); + expect(channel.lastMessageAt, equals(initialLastMessageAt)); + }, + ); + }, + ); + }); } diff --git a/packages/stream_chat/test/src/core/models/channel_state_test.dart b/packages/stream_chat/test/src/core/models/channel_state_test.dart index 269225c9de..6441ef4651 100644 --- a/packages/stream_chat/test/src/core/models/channel_state_test.dart +++ b/packages/stream_chat/test/src/core/models/channel_state_test.dart @@ -1,5 +1,3 @@ -import 'dart:convert'; - import 'package:stream_chat/stream_chat.dart'; import 'package:test/test.dart'; @@ -42,6 +40,10 @@ void main() { DateTime.parse('2020-01-29T03:23:02.843948Z'), ); expect(channelState.messages![0].user, isA()); + expect( + channelState.messages![0].restrictedVisibility, + isA>(), + ); expect(channelState.watcherCount, 5); }); @@ -59,8 +61,6 @@ void main() { watchers: [], ); - print(jsonEncode(channelState.messages?.first)); - expect( channelState.toJson(), jsonFixture('channel_state_to_json.json'), diff --git a/packages/stream_chat/test/src/core/models/event_test.dart b/packages/stream_chat/test/src/core/models/event_test.dart index 2293bbad96..8374df3afd 100644 --- a/packages/stream_chat/test/src/core/models/event_test.dart +++ b/packages/stream_chat/test/src/core/models/event_test.dart @@ -18,6 +18,7 @@ void main() { expect(event.aiMessage, 'Some message'); expect(event.unreadThreadMessages, 2); expect(event.unreadThreads, 3); + expect(event.channelLastMessageAt, isA()); }); test('should serialize to json correctly', () { @@ -36,6 +37,7 @@ void main() { messageId: 'messageId', unreadThreadMessages: 2, unreadThreads: 3, + channelLastMessageAt: DateTime.parse('2019-03-27T17:40:17.155892Z'), ); expect( @@ -66,6 +68,7 @@ void main() { 'thread': null, 'unread_thread_messages': 2, 'unread_threads': 3, + 'channel_last_message_at': '2019-03-27T17:40:17.155892Z', }, ); }); @@ -82,6 +85,7 @@ void main() { expect(newEvent.isLocal, false); expect(newEvent.unreadThreadMessages, 2); expect(newEvent.unreadThreads, 3); + expect(newEvent.channelLastMessageAt, isA()); newEvent = event.copyWith( type: 'test', @@ -94,6 +98,7 @@ void main() { channelType: 'testtype', unreadThreadMessages: 6, unreadThreads: 7, + channelLastMessageAt: DateTime.parse('2020-01-29T03:22:47.636130Z'), ); expect(newEvent.channelType, 'testtype'); @@ -106,6 +111,10 @@ void main() { expect(newEvent.user!.id, 'test'); expect(newEvent.unreadThreadMessages, 6); expect(newEvent.unreadThreads, 7); + expect( + newEvent.channelLastMessageAt, + DateTime.parse('2020-01-29T03:22:47.636130Z'), + ); }); }); } diff --git a/packages/stream_chat/test/src/core/models/message_test.dart b/packages/stream_chat/test/src/core/models/message_test.dart index 3ee7e96a7e..8e71fd9190 100644 --- a/packages/stream_chat/test/src/core/models/message_test.dart +++ b/packages/stream_chat/test/src/core/models/message_test.dart @@ -29,6 +29,7 @@ void main() { expect(message.pinExpires, null); expect(message.pinnedBy, null); expect(message.i18n, null); + expect(message.restrictedVisibility, isA>()); }); test('should serialize to json correctly', () { @@ -55,6 +56,7 @@ void main() { ], showInChannel: true, parentId: 'parentId', + restrictedVisibility: const ['user-id-3'], extraData: const {'hey': 'test'}, ); From b4a288ad1385c4fe77eb0ba1c6d57010dc657ddb Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 3 Mar 2025 15:17:07 +0530 Subject: [PATCH 4/4] feat(llc, ui, persistence): add support for private messages (#2127) --- packages/stream_chat/CHANGELOG.md | 1 + .../stream_chat/lib/src/client/channel.dart | 9 +- .../lib/src/core/models/message.dart | 60 ++++++- .../lib/src/core/models/message.g.dart | 2 + .../test/fixtures/channel_state_to_json.json | 5 +- .../test/fixtures/message_to_json.json | 3 + .../test/src/core/models/channel_test.dart | 3 - .../test/src/core/models/message_test.dart | 84 ++++++++++ packages/stream_chat_flutter/CHANGELOG.md | 4 + .../message_list_view/message_list_view.dart | 14 +- packages/stream_chat_persistence/CHANGELOG.md | 4 + .../lib/src/db/drift_chat_database.dart | 2 +- .../lib/src/db/drift_chat_database.g.dart | 150 ++++++++++++++++++ .../lib/src/entity/messages.dart | 4 + .../lib/src/mapper/message_mapper.dart | 2 + .../lib/src/mapper/pinned_message_mapper.dart | 2 + .../test/src/mapper/message_mapper_test.dart | 4 + .../mapper/pinned_message_mapper_test.dart | 4 + 18 files changed, 342 insertions(+), 15 deletions(-) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index a70ef5f36f..bfe34e7356 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -12,6 +12,7 @@ ✅ Added - [[#2101]](https://github.com/GetStream/stream-chat-flutter/issues/2101) Added support for system messages not updating `channel.lastMessageAt` +- Added support for sending private or restricted visibility messages. ## 9.4.0 diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 8f98b70992..4280fa4606 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -2508,12 +2508,9 @@ class ChannelClientState { return false; } - final currentUser = _channel._client.state.currentUser; - final restrictedVisibility = message.restrictedVisibility; - if (restrictedVisibility case final visibility?) { - if (visibility.isNotEmpty && !visibility.contains(currentUser?.id)) { - return false; - } + final currentUserId = _channel._client.state.currentUser?.id; + if (currentUserId case final userId? when message.isNotVisibleTo(userId)) { + return false; } return true; diff --git a/packages/stream_chat/lib/src/core/models/message.dart b/packages/stream_chat/lib/src/core/models/message.dart index b3a8e890bd..75e45f0924 100644 --- a/packages/stream_chat/lib/src/core/models/message.dart +++ b/packages/stream_chat/lib/src/core/models/message.dart @@ -238,8 +238,12 @@ class Message extends Equatable { String? get pollId => _pollId ?? poll?.id; final String? _pollId; - /// The list of those users who have restricted visibility for this message. - @JsonKey(includeToJson: false) + /// The list of user ids that should be able to see the message. + /// + /// If null or empty, the message is visible to all users. + /// If populated, only users whose ids are included in this list can see + /// the message. + @JsonKey(includeIfNull: false) final List? restrictedVisibility; /// Message custom extraData. @@ -524,3 +528,55 @@ class Message extends Equatable { restrictedVisibility, ]; } + +/// Extension that adds visibility control functionality to Message objects. +/// +/// This extension provides methods to determine if a message is visible to a +/// specific user based on the [Message.restrictedVisibility] list. +extension MessageVisibility on Message { + /// Checks if this message has any visibility restrictions applied. + /// + /// Returns true if the restrictedVisibility list exists and contains at + /// least one entry, indicating that visibility of this message is restricted + /// to specific users. + /// + /// Returns false if the restrictedVisibility list is null or empty, + /// indicating that this message is visible to all users. + bool get hasRestrictedVisibility { + final visibility = restrictedVisibility; + if (visibility == null || visibility.isEmpty) return false; + + return true; + } + + /// Determines if a message is visible to a specific user based on + /// restricted visibility settings. + /// + /// Returns true in the following cases: + /// - The restrictedVisibility list is null or empty (visible to everyone) + /// - The provided userId is found in the restrictedVisibility list + /// + /// Returns false if the restrictedVisibility list exists and doesn't + /// contain the provided userId. + /// + /// [userId] The unique identifier of the user to check visibility for. + bool isVisibleTo(String userId) { + final visibility = restrictedVisibility; + if (visibility == null || visibility.isEmpty) return true; + + return visibility.contains(userId); + } + + /// Determines if a message is not visible to a specific user based on + /// restricted visibility settings. + /// + /// Returns true if the restrictedVisibility list exists and doesn't + /// contain the provided userId. + /// + /// Returns false in the following cases: + /// - The restrictedVisibility list is null or empty (visible to everyone) + /// - The provided userId is found in the restrictedVisibility list + /// + /// [userId] The unique identifier of the user to check visibility for. + bool isNotVisibleTo(String userId) => !isVisibleTo(userId); +} diff --git a/packages/stream_chat/lib/src/core/models/message.g.dart b/packages/stream_chat/lib/src/core/models/message.g.dart index 2a0ff4e719..168b5f678e 100644 --- a/packages/stream_chat/lib/src/core/models/message.g.dart +++ b/packages/stream_chat/lib/src/core/models/message.g.dart @@ -94,5 +94,7 @@ Map _$MessageToJson(Message instance) => { 'pinned': instance.pinned, 'pin_expires': instance.pinExpires?.toIso8601String(), 'poll_id': instance.pollId, + if (instance.restrictedVisibility case final value?) + 'restricted_visibility': value, 'extra_data': instance.extraData, }; diff --git a/packages/stream_chat/test/fixtures/channel_state_to_json.json b/packages/stream_chat/test/fixtures/channel_state_to_json.json index b305869e42..f15f7320d1 100644 --- a/packages/stream_chat/test/fixtures/channel_state_to_json.json +++ b/packages/stream_chat/test/fixtures/channel_state_to_json.json @@ -25,7 +25,10 @@ "silent": false, "pinned": false, "pin_expires": null, - "poll_id": null + "poll_id": null, + "restricted_visibility": [ + "user-id-3" + ] }, { "id": "dry-meadow-0-e8e74482-b4cd-48db-9d1e-30e6c191786f", diff --git a/packages/stream_chat/test/fixtures/message_to_json.json b/packages/stream_chat/test/fixtures/message_to_json.json index 8388eb8220..c4568628d3 100644 --- a/packages/stream_chat/test/fixtures/message_to_json.json +++ b/packages/stream_chat/test/fixtures/message_to_json.json @@ -23,5 +23,8 @@ "pin_expires": null, "poll_id": null, "show_in_channel": true, + "restricted_visibility": [ + "user-id-3" + ], "hey": "test" } \ No newline at end of file diff --git a/packages/stream_chat/test/src/core/models/channel_test.dart b/packages/stream_chat/test/src/core/models/channel_test.dart index e67c185e97..04925fdd81 100644 --- a/packages/stream_chat/test/src/core/models/channel_test.dart +++ b/packages/stream_chat/test/src/core/models/channel_test.dart @@ -61,7 +61,6 @@ void main() { expect(channel.hidden, false); expect(channel.extraData['hidden'], false); - print(channel.toJson()); expect(channel.toJson(), { 'id': 'cid', 'type': 'test', @@ -105,7 +104,6 @@ void main() { expect(channel.disabled, false); expect(channel.extraData['disabled'], false); - print(channel.toJson()); expect(channel.toJson(), { 'id': 'cid', 'type': 'test', @@ -150,7 +148,6 @@ void main() { expect(channel.truncatedAt, currentDate); expect(channel.extraData['truncated_at'], currentDate.toIso8601String()); - print(channel.toJson()); expect(channel.toJson(), { 'id': 'cid', 'type': 'test', diff --git a/packages/stream_chat/test/src/core/models/message_test.dart b/packages/stream_chat/test/src/core/models/message_test.dart index 8e71fd9190..c55dd4fd70 100644 --- a/packages/stream_chat/test/src/core/models/message_test.dart +++ b/packages/stream_chat/test/src/core/models/message_test.dart @@ -1,3 +1,5 @@ +// ignore_for_file: avoid_redundant_argument_values + import 'package:stream_chat/src/core/models/attachment.dart'; import 'package:stream_chat/src/core/models/message.dart'; import 'package:stream_chat/src/core/models/reaction.dart'; @@ -66,4 +68,86 @@ void main() { ); }); }); + + group('MessageVisibility Extension Tests', () { + group('hasRestrictedVisibility', () { + test('should return false when restrictedVisibility is null', () { + final message = Message(restrictedVisibility: null); + expect(message.hasRestrictedVisibility, false); + }); + + test('should return false when restrictedVisibility is empty', () { + final message = Message(restrictedVisibility: const []); + expect(message.hasRestrictedVisibility, false); + }); + + test('should return true when restrictedVisibility has entries', () { + final message = Message(restrictedVisibility: const ['user1', 'user2']); + expect(message.hasRestrictedVisibility, true); + }); + }); + + group('isVisibleTo', () { + test('should return true when restrictedVisibility is null', () { + final message = Message(restrictedVisibility: null); + expect(message.isVisibleTo('anyUser'), true); + }); + + test('should return true when restrictedVisibility is empty', () { + final message = Message(restrictedVisibility: const []); + expect(message.isVisibleTo('anyUser'), true); + }); + + test('should return true when user is in restrictedVisibility list', () { + final message = + Message(restrictedVisibility: const ['user1', 'user2', 'user3']); + expect(message.isVisibleTo('user2'), true); + }); + + test('should return false when user is not in restrictedVisibility list', + () { + final message = + Message(restrictedVisibility: const ['user1', 'user2', 'user3']); + expect(message.isVisibleTo('user4'), false); + }); + + test('should handle case sensitivity correctly', () { + final message = Message(restrictedVisibility: const ['User1', 'USER2']); + expect(message.isVisibleTo('user1'), false, + reason: 'Should be case sensitive'); + expect(message.isVisibleTo('User1'), true); + }); + }); + + group('isNotVisibleTo', () { + test('should return false when restrictedVisibility is null', () { + final message = Message(restrictedVisibility: null); + expect(message.isNotVisibleTo('anyUser'), false); + }); + + test('should return false when restrictedVisibility is empty', () { + final message = Message(restrictedVisibility: const []); + expect(message.isNotVisibleTo('anyUser'), false); + }); + + test('should return false when user is in restrictedVisibility list', () { + final message = + Message(restrictedVisibility: const ['user1', 'user2', 'user3']); + expect(message.isNotVisibleTo('user2'), false); + }); + + test('should return true when user is not in restrictedVisibility list', + () { + final message = + Message(restrictedVisibility: const ['user1', 'user2', 'user3']); + expect(message.isNotVisibleTo('user4'), true); + }); + + test('should be the exact opposite of isVisibleTo', () { + final message = Message(restrictedVisibility: const ['user1', 'user2']); + const userId = 'testUser'; + expect(message.isNotVisibleTo(userId), !message.isVisibleTo(userId)); + }); + }); + }); } diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 3428be49be..12a88c94c6 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -5,6 +5,10 @@ - Fixed `StreamMessageInput` not able to edit the ogAttachments. - Fixed `MessageWidget` showing pinned background for deleted messages. +🔄 Changed + +- Updated the message list view to prevent pinning messages that have restricted visibility. + ## 9.4.0 🔄 Changed 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 9f86679faf..8c788880ed 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 @@ -1095,7 +1095,12 @@ class _StreamMessageListViewState extends State { FocusScope.of(context).unfocus(); }, showPinButton: currentUserMember != null && - _userPermissions.contains(PermissionType.pinMessage), + _userPermissions.contains(PermissionType.pinMessage) && + // Pinning a restricted visibility message is not allowed, simply + // because pinning a message is meant to bring attention to that + // message, that is not possible with a message that is only visible + // to a subset of users. + !message.hasRestrictedVisibility, ); if (widget.parentMessageBuilder != null) { @@ -1453,7 +1458,12 @@ class _StreamMessageListViewState extends State { FocusScope.of(context).unfocus(); }, showPinButton: currentUserMember != null && - _userPermissions.contains(PermissionType.pinMessage), + _userPermissions.contains(PermissionType.pinMessage) && + // Pinning a restricted visibility message is not allowed, simply + // because pinning a message is meant to bring attention to that + // message, that is not possible with a message that is only visible + // to a subset of users. + !message.hasRestrictedVisibility, ); if (widget.messageBuilder != null) { diff --git a/packages/stream_chat_persistence/CHANGELOG.md b/packages/stream_chat_persistence/CHANGELOG.md index 2a32634fc4..2114f698de 100644 --- a/packages/stream_chat_persistence/CHANGELOG.md +++ b/packages/stream_chat_persistence/CHANGELOG.md @@ -1,3 +1,7 @@ +## Upcoming + +- Added support for `Message.restrictedVisibility` field. + ## 9.4.0 - Updated minimum Flutter version to 3.27.4 for the SDK. 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 5acd58f154..4ad14290a7 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 @@ -53,7 +53,7 @@ class DriftChatDatabase extends _$DriftChatDatabase { // you should bump this number whenever you change or add a table definition. @override - int get schemaVersion => 16; + int get schemaVersion => 17; @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 92a8e9f592..5944d086b4 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 @@ -863,6 +863,15 @@ class $MessagesTable extends Messages i18n = GeneratedColumn('i18n', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false) .withConverter?>($MessagesTable.$converteri18n); + static const VerificationMeta _restrictedVisibilityMeta = + const VerificationMeta('restrictedVisibility'); + @override + late final GeneratedColumnWithTypeConverter?, String> + restrictedVisibility = GeneratedColumn( + 'restricted_visibility', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false) + .withConverter?>( + $MessagesTable.$converterrestrictedVisibilityn); static const VerificationMeta _extraDataMeta = const VerificationMeta('extraData'); @override @@ -902,6 +911,7 @@ class $MessagesTable extends Messages pinnedByUserId, channelCid, i18n, + restrictedVisibility, extraData ]; @override @@ -1048,6 +1058,8 @@ class $MessagesTable extends Messages context.missing(_channelCidMeta); } context.handle(_i18nMeta, const VerificationResult.success()); + context.handle( + _restrictedVisibilityMeta, const VerificationResult.success()); context.handle(_extraDataMeta, const VerificationResult.success()); return context; } @@ -1121,6 +1133,9 @@ class $MessagesTable extends Messages .read(DriftSqlType.string, data['${effectivePrefix}channel_cid'])!, i18n: $MessagesTable.$converteri18n.fromSql(attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}i18n'])), + restrictedVisibility: $MessagesTable.$converterrestrictedVisibilityn + .fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string, + data['${effectivePrefix}restricted_visibility'])), extraData: $MessagesTable.$converterextraDatan.fromSql(attachedDatabase .typeMapping .read(DriftSqlType.string, data['${effectivePrefix}extra_data'])), @@ -1146,6 +1161,10 @@ class $MessagesTable extends Messages NullAwareTypeConverter.wrap($converterreactionScores); static TypeConverter?, String?> $converteri18n = NullableMapConverter(); + static TypeConverter, String> $converterrestrictedVisibility = + ListConverter(); + static TypeConverter?, String?> $converterrestrictedVisibilityn = + NullAwareTypeConverter.wrap($converterrestrictedVisibility); static TypeConverter, String> $converterextraData = MapConverter(); static TypeConverter?, String?> $converterextraDatan = @@ -1241,6 +1260,9 @@ class MessageEntity extends DataClass implements Insertable { /// A Map of [messageText] translations. final Map? i18n; + /// The list of user ids that should be able to see the message. + final List? restrictedVisibility; + /// Message custom extraData final Map? extraData; const MessageEntity( @@ -1273,6 +1295,7 @@ class MessageEntity extends DataClass implements Insertable { this.pinnedByUserId, required this.channelCid, this.i18n, + this.restrictedVisibility, this.extraData}); @override Map toColumns(bool nullToAbsent) { @@ -1356,6 +1379,11 @@ class MessageEntity extends DataClass implements Insertable { if (!nullToAbsent || i18n != null) { map['i18n'] = Variable($MessagesTable.$converteri18n.toSql(i18n)); } + if (!nullToAbsent || restrictedVisibility != null) { + map['restricted_visibility'] = Variable($MessagesTable + .$converterrestrictedVisibilityn + .toSql(restrictedVisibility)); + } if (!nullToAbsent || extraData != null) { map['extra_data'] = Variable( $MessagesTable.$converterextraDatan.toSql(extraData)); @@ -1399,6 +1427,8 @@ class MessageEntity extends DataClass implements Insertable { pinnedByUserId: serializer.fromJson(json['pinnedByUserId']), channelCid: serializer.fromJson(json['channelCid']), i18n: serializer.fromJson?>(json['i18n']), + restrictedVisibility: + serializer.fromJson?>(json['restrictedVisibility']), extraData: serializer.fromJson?>(json['extraData']), ); } @@ -1436,6 +1466,8 @@ class MessageEntity extends DataClass implements Insertable { 'pinnedByUserId': serializer.toJson(pinnedByUserId), 'channelCid': serializer.toJson(channelCid), 'i18n': serializer.toJson?>(i18n), + 'restrictedVisibility': + serializer.toJson?>(restrictedVisibility), 'extraData': serializer.toJson?>(extraData), }; } @@ -1470,6 +1502,7 @@ class MessageEntity extends DataClass implements Insertable { Value pinnedByUserId = const Value.absent(), String? channelCid, Value?> i18n = const Value.absent(), + Value?> restrictedVisibility = const Value.absent(), Value?> extraData = const Value.absent()}) => MessageEntity( id: id ?? this.id, @@ -1518,6 +1551,9 @@ class MessageEntity extends DataClass implements Insertable { pinnedByUserId.present ? pinnedByUserId.value : this.pinnedByUserId, channelCid: channelCid ?? this.channelCid, i18n: i18n.present ? i18n.value : this.i18n, + restrictedVisibility: restrictedVisibility.present + ? restrictedVisibility.value + : this.restrictedVisibility, extraData: extraData.present ? extraData.value : this.extraData, ); MessageEntity copyWithCompanion(MessagesCompanion data) { @@ -1582,6 +1618,9 @@ class MessageEntity extends DataClass implements Insertable { channelCid: data.channelCid.present ? data.channelCid.value : this.channelCid, i18n: data.i18n.present ? data.i18n.value : this.i18n, + restrictedVisibility: data.restrictedVisibility.present + ? data.restrictedVisibility.value + : this.restrictedVisibility, extraData: data.extraData.present ? data.extraData.value : this.extraData, ); } @@ -1618,6 +1657,7 @@ class MessageEntity extends DataClass implements Insertable { ..write('pinnedByUserId: $pinnedByUserId, ') ..write('channelCid: $channelCid, ') ..write('i18n: $i18n, ') + ..write('restrictedVisibility: $restrictedVisibility, ') ..write('extraData: $extraData') ..write(')')) .toString(); @@ -1654,6 +1694,7 @@ class MessageEntity extends DataClass implements Insertable { pinnedByUserId, channelCid, i18n, + restrictedVisibility, extraData ]); @override @@ -1689,6 +1730,7 @@ class MessageEntity extends DataClass implements Insertable { other.pinnedByUserId == this.pinnedByUserId && other.channelCid == this.channelCid && other.i18n == this.i18n && + other.restrictedVisibility == this.restrictedVisibility && other.extraData == this.extraData); } @@ -1722,6 +1764,7 @@ class MessagesCompanion extends UpdateCompanion { final Value pinnedByUserId; final Value channelCid; final Value?> i18n; + final Value?> restrictedVisibility; final Value?> extraData; final Value rowid; const MessagesCompanion({ @@ -1754,6 +1797,7 @@ class MessagesCompanion extends UpdateCompanion { this.pinnedByUserId = const Value.absent(), this.channelCid = const Value.absent(), this.i18n = const Value.absent(), + this.restrictedVisibility = const Value.absent(), this.extraData = const Value.absent(), this.rowid = const Value.absent(), }); @@ -1787,6 +1831,7 @@ class MessagesCompanion extends UpdateCompanion { this.pinnedByUserId = const Value.absent(), required String channelCid, this.i18n = const Value.absent(), + this.restrictedVisibility = const Value.absent(), this.extraData = const Value.absent(), this.rowid = const Value.absent(), }) : id = Value(id), @@ -1824,6 +1869,7 @@ class MessagesCompanion extends UpdateCompanion { Expression? pinnedByUserId, Expression? channelCid, Expression? i18n, + Expression? restrictedVisibility, Expression? extraData, Expression? rowid, }) { @@ -1858,6 +1904,8 @@ class MessagesCompanion extends UpdateCompanion { if (pinnedByUserId != null) 'pinned_by_user_id': pinnedByUserId, if (channelCid != null) 'channel_cid': channelCid, if (i18n != null) 'i18n': i18n, + if (restrictedVisibility != null) + 'restricted_visibility': restrictedVisibility, if (extraData != null) 'extra_data': extraData, if (rowid != null) 'rowid': rowid, }); @@ -1893,6 +1941,7 @@ class MessagesCompanion extends UpdateCompanion { Value? pinnedByUserId, Value? channelCid, Value?>? i18n, + Value?>? restrictedVisibility, Value?>? extraData, Value? rowid}) { return MessagesCompanion( @@ -1925,6 +1974,7 @@ class MessagesCompanion extends UpdateCompanion { pinnedByUserId: pinnedByUserId ?? this.pinnedByUserId, channelCid: channelCid ?? this.channelCid, i18n: i18n ?? this.i18n, + restrictedVisibility: restrictedVisibility ?? this.restrictedVisibility, extraData: extraData ?? this.extraData, rowid: rowid ?? this.rowid, ); @@ -2026,6 +2076,11 @@ class MessagesCompanion extends UpdateCompanion { map['i18n'] = Variable($MessagesTable.$converteri18n.toSql(i18n.value)); } + if (restrictedVisibility.present) { + map['restricted_visibility'] = Variable($MessagesTable + .$converterrestrictedVisibilityn + .toSql(restrictedVisibility.value)); + } if (extraData.present) { map['extra_data'] = Variable( $MessagesTable.$converterextraDatan.toSql(extraData.value)); @@ -2068,6 +2123,7 @@ class MessagesCompanion extends UpdateCompanion { ..write('pinnedByUserId: $pinnedByUserId, ') ..write('channelCid: $channelCid, ') ..write('i18n: $i18n, ') + ..write('restrictedVisibility: $restrictedVisibility, ') ..write('extraData: $extraData, ') ..write('rowid: $rowid') ..write(')')) @@ -2274,6 +2330,15 @@ class $PinnedMessagesTable extends PinnedMessages type: DriftSqlType.string, requiredDuringInsert: false) .withConverter?>( $PinnedMessagesTable.$converteri18n); + static const VerificationMeta _restrictedVisibilityMeta = + const VerificationMeta('restrictedVisibility'); + @override + late final GeneratedColumnWithTypeConverter?, String> + restrictedVisibility = GeneratedColumn( + 'restricted_visibility', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false) + .withConverter?>( + $PinnedMessagesTable.$converterrestrictedVisibilityn); static const VerificationMeta _extraDataMeta = const VerificationMeta('extraData'); @override @@ -2313,6 +2378,7 @@ class $PinnedMessagesTable extends PinnedMessages pinnedByUserId, channelCid, i18n, + restrictedVisibility, extraData ]; @override @@ -2460,6 +2526,8 @@ class $PinnedMessagesTable extends PinnedMessages context.missing(_channelCidMeta); } context.handle(_i18nMeta, const VerificationResult.success()); + context.handle( + _restrictedVisibilityMeta, const VerificationResult.success()); context.handle(_extraDataMeta, const VerificationResult.success()); return context; } @@ -2534,6 +2602,9 @@ class $PinnedMessagesTable extends PinnedMessages i18n: $PinnedMessagesTable.$converteri18n.fromSql(attachedDatabase .typeMapping .read(DriftSqlType.string, data['${effectivePrefix}i18n'])), + restrictedVisibility: $PinnedMessagesTable.$converterrestrictedVisibilityn + .fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string, + data['${effectivePrefix}restricted_visibility'])), extraData: $PinnedMessagesTable.$converterextraDatan.fromSql( attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}extra_data'])), @@ -2559,6 +2630,10 @@ class $PinnedMessagesTable extends PinnedMessages NullAwareTypeConverter.wrap($converterreactionScores); static TypeConverter?, String?> $converteri18n = NullableMapConverter(); + static TypeConverter, String> $converterrestrictedVisibility = + ListConverter(); + static TypeConverter?, String?> $converterrestrictedVisibilityn = + NullAwareTypeConverter.wrap($converterrestrictedVisibility); static TypeConverter, String> $converterextraData = MapConverter(); static TypeConverter?, String?> $converterextraDatan = @@ -2655,6 +2730,9 @@ class PinnedMessageEntity extends DataClass /// A Map of [messageText] translations. final Map? i18n; + /// The list of user ids that should be able to see the message. + final List? restrictedVisibility; + /// Message custom extraData final Map? extraData; const PinnedMessageEntity( @@ -2687,6 +2765,7 @@ class PinnedMessageEntity extends DataClass this.pinnedByUserId, required this.channelCid, this.i18n, + this.restrictedVisibility, this.extraData}); @override Map toColumns(bool nullToAbsent) { @@ -2771,6 +2850,11 @@ class PinnedMessageEntity extends DataClass map['i18n'] = Variable($PinnedMessagesTable.$converteri18n.toSql(i18n)); } + if (!nullToAbsent || restrictedVisibility != null) { + map['restricted_visibility'] = Variable($PinnedMessagesTable + .$converterrestrictedVisibilityn + .toSql(restrictedVisibility)); + } if (!nullToAbsent || extraData != null) { map['extra_data'] = Variable( $PinnedMessagesTable.$converterextraDatan.toSql(extraData)); @@ -2814,6 +2898,8 @@ class PinnedMessageEntity extends DataClass pinnedByUserId: serializer.fromJson(json['pinnedByUserId']), channelCid: serializer.fromJson(json['channelCid']), i18n: serializer.fromJson?>(json['i18n']), + restrictedVisibility: + serializer.fromJson?>(json['restrictedVisibility']), extraData: serializer.fromJson?>(json['extraData']), ); } @@ -2851,6 +2937,8 @@ class PinnedMessageEntity extends DataClass 'pinnedByUserId': serializer.toJson(pinnedByUserId), 'channelCid': serializer.toJson(channelCid), 'i18n': serializer.toJson?>(i18n), + 'restrictedVisibility': + serializer.toJson?>(restrictedVisibility), 'extraData': serializer.toJson?>(extraData), }; } @@ -2885,6 +2973,7 @@ class PinnedMessageEntity extends DataClass Value pinnedByUserId = const Value.absent(), String? channelCid, Value?> i18n = const Value.absent(), + Value?> restrictedVisibility = const Value.absent(), Value?> extraData = const Value.absent()}) => PinnedMessageEntity( id: id ?? this.id, @@ -2933,6 +3022,9 @@ class PinnedMessageEntity extends DataClass pinnedByUserId.present ? pinnedByUserId.value : this.pinnedByUserId, channelCid: channelCid ?? this.channelCid, i18n: i18n.present ? i18n.value : this.i18n, + restrictedVisibility: restrictedVisibility.present + ? restrictedVisibility.value + : this.restrictedVisibility, extraData: extraData.present ? extraData.value : this.extraData, ); PinnedMessageEntity copyWithCompanion(PinnedMessagesCompanion data) { @@ -2997,6 +3089,9 @@ class PinnedMessageEntity extends DataClass channelCid: data.channelCid.present ? data.channelCid.value : this.channelCid, i18n: data.i18n.present ? data.i18n.value : this.i18n, + restrictedVisibility: data.restrictedVisibility.present + ? data.restrictedVisibility.value + : this.restrictedVisibility, extraData: data.extraData.present ? data.extraData.value : this.extraData, ); } @@ -3033,6 +3128,7 @@ class PinnedMessageEntity extends DataClass ..write('pinnedByUserId: $pinnedByUserId, ') ..write('channelCid: $channelCid, ') ..write('i18n: $i18n, ') + ..write('restrictedVisibility: $restrictedVisibility, ') ..write('extraData: $extraData') ..write(')')) .toString(); @@ -3069,6 +3165,7 @@ class PinnedMessageEntity extends DataClass pinnedByUserId, channelCid, i18n, + restrictedVisibility, extraData ]); @override @@ -3104,6 +3201,7 @@ class PinnedMessageEntity extends DataClass other.pinnedByUserId == this.pinnedByUserId && other.channelCid == this.channelCid && other.i18n == this.i18n && + other.restrictedVisibility == this.restrictedVisibility && other.extraData == this.extraData); } @@ -3137,6 +3235,7 @@ class PinnedMessagesCompanion extends UpdateCompanion { final Value pinnedByUserId; final Value channelCid; final Value?> i18n; + final Value?> restrictedVisibility; final Value?> extraData; final Value rowid; const PinnedMessagesCompanion({ @@ -3169,6 +3268,7 @@ class PinnedMessagesCompanion extends UpdateCompanion { this.pinnedByUserId = const Value.absent(), this.channelCid = const Value.absent(), this.i18n = const Value.absent(), + this.restrictedVisibility = const Value.absent(), this.extraData = const Value.absent(), this.rowid = const Value.absent(), }); @@ -3202,6 +3302,7 @@ class PinnedMessagesCompanion extends UpdateCompanion { this.pinnedByUserId = const Value.absent(), required String channelCid, this.i18n = const Value.absent(), + this.restrictedVisibility = const Value.absent(), this.extraData = const Value.absent(), this.rowid = const Value.absent(), }) : id = Value(id), @@ -3239,6 +3340,7 @@ class PinnedMessagesCompanion extends UpdateCompanion { Expression? pinnedByUserId, Expression? channelCid, Expression? i18n, + Expression? restrictedVisibility, Expression? extraData, Expression? rowid, }) { @@ -3273,6 +3375,8 @@ class PinnedMessagesCompanion extends UpdateCompanion { if (pinnedByUserId != null) 'pinned_by_user_id': pinnedByUserId, if (channelCid != null) 'channel_cid': channelCid, if (i18n != null) 'i18n': i18n, + if (restrictedVisibility != null) + 'restricted_visibility': restrictedVisibility, if (extraData != null) 'extra_data': extraData, if (rowid != null) 'rowid': rowid, }); @@ -3308,6 +3412,7 @@ class PinnedMessagesCompanion extends UpdateCompanion { Value? pinnedByUserId, Value? channelCid, Value?>? i18n, + Value?>? restrictedVisibility, Value?>? extraData, Value? rowid}) { return PinnedMessagesCompanion( @@ -3340,6 +3445,7 @@ class PinnedMessagesCompanion extends UpdateCompanion { pinnedByUserId: pinnedByUserId ?? this.pinnedByUserId, channelCid: channelCid ?? this.channelCid, i18n: i18n ?? this.i18n, + restrictedVisibility: restrictedVisibility ?? this.restrictedVisibility, extraData: extraData ?? this.extraData, rowid: rowid ?? this.rowid, ); @@ -3444,6 +3550,11 @@ class PinnedMessagesCompanion extends UpdateCompanion { map['i18n'] = Variable( $PinnedMessagesTable.$converteri18n.toSql(i18n.value)); } + if (restrictedVisibility.present) { + map['restricted_visibility'] = Variable($PinnedMessagesTable + .$converterrestrictedVisibilityn + .toSql(restrictedVisibility.value)); + } if (extraData.present) { map['extra_data'] = Variable( $PinnedMessagesTable.$converterextraDatan.toSql(extraData.value)); @@ -3486,6 +3597,7 @@ class PinnedMessagesCompanion extends UpdateCompanion { ..write('pinnedByUserId: $pinnedByUserId, ') ..write('channelCid: $channelCid, ') ..write('i18n: $i18n, ') + ..write('restrictedVisibility: $restrictedVisibility, ') ..write('extraData: $extraData, ') ..write('rowid: $rowid') ..write(')')) @@ -8091,6 +8203,7 @@ typedef $$MessagesTableCreateCompanionBuilder = MessagesCompanion Function({ Value pinnedByUserId, required String channelCid, Value?> i18n, + Value?> restrictedVisibility, Value?> extraData, Value rowid, }); @@ -8124,6 +8237,7 @@ typedef $$MessagesTableUpdateCompanionBuilder = MessagesCompanion Function({ Value pinnedByUserId, Value channelCid, Value?> i18n, + Value?> restrictedVisibility, Value?> extraData, Value rowid, }); @@ -8274,6 +8388,11 @@ class $$MessagesTableFilterComposer column: $table.i18n, builder: (column) => ColumnWithTypeConverterFilters(column)); + ColumnWithTypeConverterFilters?, List, String> + get restrictedVisibility => $composableBuilder( + column: $table.restrictedVisibility, + builder: (column) => ColumnWithTypeConverterFilters(column)); + ColumnWithTypeConverterFilters?, Map, String> get extraData => $composableBuilder( @@ -8428,6 +8547,10 @@ class $$MessagesTableOrderingComposer ColumnOrderings get i18n => $composableBuilder( column: $table.i18n, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get restrictedVisibility => $composableBuilder( + column: $table.restrictedVisibility, + builder: (column) => ColumnOrderings(column)); + ColumnOrderings get extraData => $composableBuilder( column: $table.extraData, builder: (column) => ColumnOrderings(column)); @@ -8549,6 +8672,10 @@ class $$MessagesTableAnnotationComposer GeneratedColumnWithTypeConverter?, String> get i18n => $composableBuilder(column: $table.i18n, builder: (column) => column); + GeneratedColumnWithTypeConverter?, String> + get restrictedVisibility => $composableBuilder( + column: $table.restrictedVisibility, builder: (column) => column); + GeneratedColumnWithTypeConverter?, String> get extraData => $composableBuilder( column: $table.extraData, builder: (column) => column); @@ -8647,6 +8774,7 @@ class $$MessagesTableTableManager extends RootTableManager< Value pinnedByUserId = const Value.absent(), Value channelCid = const Value.absent(), Value?> i18n = const Value.absent(), + Value?> restrictedVisibility = const Value.absent(), Value?> extraData = const Value.absent(), Value rowid = const Value.absent(), }) => @@ -8680,6 +8808,7 @@ class $$MessagesTableTableManager extends RootTableManager< pinnedByUserId: pinnedByUserId, channelCid: channelCid, i18n: i18n, + restrictedVisibility: restrictedVisibility, extraData: extraData, rowid: rowid, ), @@ -8713,6 +8842,7 @@ class $$MessagesTableTableManager extends RootTableManager< Value pinnedByUserId = const Value.absent(), required String channelCid, Value?> i18n = const Value.absent(), + Value?> restrictedVisibility = const Value.absent(), Value?> extraData = const Value.absent(), Value rowid = const Value.absent(), }) => @@ -8746,6 +8876,7 @@ class $$MessagesTableTableManager extends RootTableManager< pinnedByUserId: pinnedByUserId, channelCid: channelCid, i18n: i18n, + restrictedVisibility: restrictedVisibility, extraData: extraData, rowid: rowid, ), @@ -8847,6 +8978,7 @@ typedef $$PinnedMessagesTableCreateCompanionBuilder = PinnedMessagesCompanion Value pinnedByUserId, required String channelCid, Value?> i18n, + Value?> restrictedVisibility, Value?> extraData, Value rowid, }); @@ -8881,6 +9013,7 @@ typedef $$PinnedMessagesTableUpdateCompanionBuilder = PinnedMessagesCompanion Value pinnedByUserId, Value channelCid, Value?> i18n, + Value?> restrictedVisibility, Value?> extraData, Value rowid, }); @@ -9026,6 +9159,11 @@ class $$PinnedMessagesTableFilterComposer column: $table.i18n, builder: (column) => ColumnWithTypeConverterFilters(column)); + ColumnWithTypeConverterFilters?, List, String> + get restrictedVisibility => $composableBuilder( + column: $table.restrictedVisibility, + builder: (column) => ColumnWithTypeConverterFilters(column)); + ColumnWithTypeConverterFilters?, Map, String> get extraData => $composableBuilder( @@ -9165,6 +9303,10 @@ class $$PinnedMessagesTableOrderingComposer ColumnOrderings get i18n => $composableBuilder( column: $table.i18n, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get restrictedVisibility => $composableBuilder( + column: $table.restrictedVisibility, + builder: (column) => ColumnOrderings(column)); + ColumnOrderings get extraData => $composableBuilder( column: $table.extraData, builder: (column) => ColumnOrderings(column)); } @@ -9269,6 +9411,10 @@ class $$PinnedMessagesTableAnnotationComposer GeneratedColumnWithTypeConverter?, String> get i18n => $composableBuilder(column: $table.i18n, builder: (column) => column); + GeneratedColumnWithTypeConverter?, String> + get restrictedVisibility => $composableBuilder( + column: $table.restrictedVisibility, builder: (column) => column); + GeneratedColumnWithTypeConverter?, String> get extraData => $composableBuilder( column: $table.extraData, builder: (column) => column); @@ -9350,6 +9496,7 @@ class $$PinnedMessagesTableTableManager extends RootTableManager< Value pinnedByUserId = const Value.absent(), Value channelCid = const Value.absent(), Value?> i18n = const Value.absent(), + Value?> restrictedVisibility = const Value.absent(), Value?> extraData = const Value.absent(), Value rowid = const Value.absent(), }) => @@ -9383,6 +9530,7 @@ class $$PinnedMessagesTableTableManager extends RootTableManager< pinnedByUserId: pinnedByUserId, channelCid: channelCid, i18n: i18n, + restrictedVisibility: restrictedVisibility, extraData: extraData, rowid: rowid, ), @@ -9416,6 +9564,7 @@ class $$PinnedMessagesTableTableManager extends RootTableManager< Value pinnedByUserId = const Value.absent(), required String channelCid, Value?> i18n = const Value.absent(), + Value?> restrictedVisibility = const Value.absent(), Value?> extraData = const Value.absent(), Value rowid = const Value.absent(), }) => @@ -9449,6 +9598,7 @@ class $$PinnedMessagesTableTableManager extends RootTableManager< pinnedByUserId: pinnedByUserId, channelCid: channelCid, i18n: i18n, + restrictedVisibility: restrictedVisibility, extraData: extraData, rowid: rowid, ), diff --git a/packages/stream_chat_persistence/lib/src/entity/messages.dart b/packages/stream_chat_persistence/lib/src/entity/messages.dart index 69c38f0c83..30f7e45a23 100644 --- a/packages/stream_chat_persistence/lib/src/entity/messages.dart +++ b/packages/stream_chat_persistence/lib/src/entity/messages.dart @@ -126,6 +126,10 @@ class Messages extends Table { TextColumn get i18n => text().nullable().map(NullableMapConverter())(); + /// The list of user ids that should be able to see the message. + TextColumn get restrictedVisibility => + text().nullable().map(ListConverter())(); + /// Message custom extraData TextColumn get extraData => text().nullable().map(MapConverter())(); 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 33e3071412..faedfda3e2 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/message_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/message_mapper.dart @@ -52,6 +52,7 @@ extension MessageEntityX on MessageEntity { mentionedUsers: mentionedUsers.map((e) => User.fromJson(jsonDecode(e))).toList(), i18n: i18n, + restrictedVisibility: restrictedVisibility, ); } @@ -89,5 +90,6 @@ extension MessageX on Message { pinExpires: pinExpires, pinnedByUserId: pinnedBy?.id, i18n: i18n, + restrictedVisibility: restrictedVisibility, ); } 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 1f0b8d4fb3..a41bdd4ebe 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 @@ -52,6 +52,7 @@ extension PinnedMessageEntityX on PinnedMessageEntity { mentionedUsers: mentionedUsers.map((e) => User.fromJson(jsonDecode(e))).toList(), i18n: i18n, + restrictedVisibility: restrictedVisibility, ); } @@ -90,5 +91,6 @@ extension PMessageX on Message { pinExpires: pinExpires, pinnedByUserId: pinnedBy?.id, i18n: i18n, + restrictedVisibility: restrictedVisibility, ); } 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 76dc763fc6..4bdf284cb9 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 @@ -81,6 +81,7 @@ void main() { 'hi_text': 'नमस्ते', 'language': 'en', }, + restrictedVisibility: const ['user-id-2'], ); final message = entity.toMessage( user: user, @@ -137,6 +138,7 @@ void main() { expect(messageAttachment.type, entityAttachment.type); expect(messageAttachment.assetUrl, entityAttachment.assetUrl); } + expect(message.restrictedVisibility, entity.restrictedVisibility); }); test('toEntity should map message into MessageEntity', () { @@ -210,6 +212,7 @@ void main() { 'hi_text': 'नमस्ते', 'language': 'en', }, + restrictedVisibility: const ['user-id-2'], ); final entity = message.toEntity(cid: cid); expect(entity, isA()); @@ -251,5 +254,6 @@ void main() { message.attachments.map((it) => jsonEncode(it.toData())).toList(), ); expect(entity.i18n, message.i18n); + expect(entity.restrictedVisibility, message.restrictedVisibility); }); } 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 9d54faea59..2095a3fdb6 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 @@ -81,6 +81,7 @@ void main() { 'hi_text': 'नमस्ते', 'language': 'en', }, + restrictedVisibility: const ['user-id-2'], ); final message = entity.toMessage( user: user, @@ -137,6 +138,7 @@ void main() { expect(messageAttachment.type, entityAttachment.type); expect(messageAttachment.assetUrl, entityAttachment.assetUrl); } + expect(message.restrictedVisibility, entity.restrictedVisibility); }); test('toEntity should map message into MessageEntity', () { @@ -210,6 +212,7 @@ void main() { 'hi_text': 'नमस्ते', 'language': 'en', }, + restrictedVisibility: const ['user-id-2'], ); final entity = message.toPinnedEntity(cid: cid); expect(entity, isA()); @@ -251,5 +254,6 @@ void main() { message.attachments.map((it) => jsonEncode(it.toData())).toList(), ); expect(entity.i18n, message.i18n); + expect(entity.restrictedVisibility, message.restrictedVisibility); }); }