-
Notifications
You must be signed in to change notification settings - Fork 372
feat(llc, ui, persistence): Add message delivery receipts #2429
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 4 commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
255a4e0
feat(llc): Add message delivery receipts
xsahil03x caafe98
fix: Clear delivery candidates on cancel
xsahil03x 46ab6d6
test: fix existing tests
xsahil03x 2e5b853
feat: Add delivery receipts privacy setting
xsahil03x adbbcfa
fix: Ensure `deliveriesOf` considers already read messages
xsahil03x b9549bb
feat: Add delivered status to sending indicator
xsahil03x 0b8ca0e
feat(sample): Add message info screen
xsahil03x b55799c
feat(sample): Add message info screen
xsahil03x 0f943f5
test: Enhance StreamSendingIndicator tests for delivered and read mes…
xsahil03x 6200b73
test: Add tests for delivery receipts privacy settings
xsahil03x 70554a4
test: Add tests for markChannelsDelivered functionality
xsahil03x dab12df
test: Add tests for ChannelReadHelper functionality
xsahil03x 6ea2c4b
test: Add tests for message delivery and read state preservation
xsahil03x a75e65c
test: Add delivery submission tests for channel.query
xsahil03x 93b550a
test: Add test for channel delivery submission on new message
xsahil03x bd84adf
test: Add tests for ChannelDeliveryReporter functionality
xsahil03x 76a5477
chore: fix lints
xsahil03x 18f872b
fix: correct muted user check in message visibility logic
xsahil03x cb1e1d8
test: Add unit tests for MessageRules functionality
xsahil03x 21a3d3f
Merge branch 'master' into feat/mark-message-delivered
xsahil03x 5a5f7e9
test: add more tests
xsahil03x 050d945
test: update merging logic to preserve last delivered message values
xsahil03x 9332107
feat(persistence): Add support for message delivery receipts
xsahil03x 28f74ee
Merge branch 'master' into feat/mark-message-delivered
xsahil03x 1738b53
Discard changes to packages/stream_chat_persistence/CHANGELOG.md
xsahil03x 95cd62e
chore: update CHANGELOG.md
xsahil03x File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
200 changes: 200 additions & 0 deletions
200
packages/stream_chat/lib/src/client/channel_delivery_reporter.dart
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,200 @@ | ||
| import 'package:logging/logging.dart'; | ||
| import 'package:rate_limiter/rate_limiter.dart'; | ||
| import 'package:stream_chat/src/client/channel.dart'; | ||
| import 'package:stream_chat/src/core/models/message.dart'; | ||
| import 'package:stream_chat/src/core/models/message_delivery.dart'; | ||
| import 'package:stream_chat/src/core/util/message_rules.dart'; | ||
| import 'package:synchronized/synchronized.dart'; | ||
|
|
||
| /// A callback that sends delivery receipts for multiple channels. | ||
| /// | ||
| /// Each [MessageDeliveryInfo] represents an acknowledgment that the current | ||
| /// user has received a message. | ||
| typedef MarkChannelsDelivered = Future<void> Function( | ||
| Iterable<MessageDelivery> deliveries, | ||
| ); | ||
|
|
||
| /// Manages the delivery reporting for channel messages. | ||
| /// | ||
| /// Collects channels that need delivery acknowledgments and efficiently | ||
| /// reports them to the server. | ||
| class ChannelDeliveryReporter { | ||
| /// Creates a new channel delivery reporter. | ||
| /// | ||
| /// The [onMarkChannelsDelivered] callback is invoked when delivery receipts | ||
| /// are ready to be sent to the server. | ||
| /// | ||
| /// The [throttleDuration] controls how frequently delivery receipts are sent. | ||
| /// | ||
| /// The optional [logger] logs warnings and errors during operation. | ||
| ChannelDeliveryReporter({ | ||
| Logger? logger, | ||
| required this.onMarkChannelsDelivered, | ||
| Duration throttleDuration = const Duration(seconds: 1), | ||
| }) : _logger = logger, | ||
| _markAsDeliveredThrottleDuration = throttleDuration; | ||
|
|
||
| final Logger? _logger; | ||
| final Duration _markAsDeliveredThrottleDuration; | ||
|
|
||
| /// The callback invoked to send delivery receipts. | ||
| /// | ||
| /// Receives delivery receipts acknowledging that messages were received. | ||
| final MarkChannelsDelivered onMarkChannelsDelivered; | ||
|
|
||
| final _deliveryCandidatesLock = Lock(); | ||
| final _deliveryCandidates = <String /* cid */, Message /* message */ >{}; | ||
|
|
||
| /// Submits [channels] for delivery reporting. | ||
| /// | ||
| /// Marks each channel's last message as delivered if it meets the delivery | ||
| /// requirements according to [MessageRules.canMarkAsDelivered]. Channels | ||
| /// without a valid cid or last message are skipped. | ||
| /// | ||
| /// Typically used after message.new events or initial channel queries. For | ||
| /// read/delivered events see [reconcileDelivery], for hidden/left channels | ||
| /// see [cancelDelivery]. | ||
| Future<void> submitForDelivery(Iterable<Channel> channels) async { | ||
| await _deliveryCandidatesLock.synchronized(() { | ||
| for (final channel in channels) { | ||
| final channelCid = channel.cid; | ||
| if (channelCid == null) continue; | ||
|
|
||
| final lastMessage = channel.state?.lastMessage; | ||
| if (lastMessage == null) continue; | ||
|
|
||
| // Only submit for delivery if the message can be marked as delivered. | ||
| if (!MessageRules.canMarkAsDelivered(lastMessage, channel)) continue; | ||
|
|
||
| _logger?.fine( | ||
| 'Submitted channel $channelCid for delivery ' | ||
| '(message: ${lastMessage.id})', | ||
| ); | ||
|
|
||
| // Update the latest message for the channel | ||
| _deliveryCandidates[channelCid] = lastMessage; | ||
| } | ||
| }); | ||
|
|
||
| // Trigger mark channels delivered request | ||
| _throttledMarkCandidatesAsDelivered.call(); | ||
| } | ||
|
|
||
| /// Reconciles delivery reporting for [channels] with their current state. | ||
| /// | ||
| /// Re-evaluates whether messages still need to be marked as delivered based | ||
| /// on the channel's current state. Stops tracking messages that are already | ||
| /// read, delivered, or otherwise don't need delivery reporting. | ||
| /// | ||
| /// This prevents duplicate delivery reports when a message is marked | ||
| /// delivered on another device, and avoids unnecessary reports when a user | ||
| /// reads a channel (since read supersedes delivered). | ||
| /// | ||
| /// Typically used after message.read or message.delivered events. See | ||
| /// [cancelDelivery] to remove channels entirely, or [submitForDelivery] | ||
| /// to add new messages. | ||
| /// | ||
| /// ```dart | ||
| /// // After a message.read or message.delivered event | ||
| /// reporter.reconcileDelivery([channel]); | ||
| /// ``` | ||
| Future<void> reconcileDelivery(Iterable<Channel> channels) async { | ||
| return _deliveryCandidatesLock.synchronized(() { | ||
| for (final channel in channels) { | ||
| final channelCid = channel.cid; | ||
| if (channelCid == null) continue; | ||
|
|
||
| // Get the existing candidate message | ||
| final message = _deliveryCandidates[channelCid]; | ||
| if (message == null) continue; | ||
|
|
||
| // If the message can still be marked as delivered, keep it | ||
| if (MessageRules.canMarkAsDelivered(message, channel)) continue; | ||
|
|
||
| _logger?.fine( | ||
| 'Reconciled delivery for channel $channelCid ' | ||
| '(message: ${message.id}), removing from candidates', | ||
| ); | ||
|
|
||
| // Otherwise, remove it from the candidates | ||
| _deliveryCandidates.remove(channelCid); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| /// Cancels pending delivery reports for [channels]. | ||
| /// | ||
| /// Prevents the specified channels from being marked as delivered. Typically | ||
| /// used when channels are hidden, left, or removed from view. | ||
| /// | ||
| /// See [reconcileDelivery] to re-evaluate based on current read/delivered | ||
| /// state instead of removing channels entirely. | ||
| Future<void> cancelDelivery(Iterable<String> channels) { | ||
| return _deliveryCandidatesLock.synchronized(() { | ||
| for (final channelCid in channels) { | ||
| if (!_deliveryCandidates.containsKey(channelCid)) continue; | ||
|
|
||
| final message = _deliveryCandidates.remove(channelCid); | ||
|
|
||
| _logger?.fine( | ||
| 'Canceled delivery for channel $channelCid ' | ||
| '(message: ${message?.id})', | ||
| ); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| late final _throttledMarkCandidatesAsDelivered = Throttle( | ||
| leading: false, | ||
| _markCandidatesAsDelivered, | ||
| _markAsDeliveredThrottleDuration, | ||
| ); | ||
|
|
||
| static const _maxCandidatesPerBatch = 100; | ||
| Future<void> _markCandidatesAsDelivered() async { | ||
| // We only process at-most 100 channels at a time to avoid large payloads. | ||
| final batch = {..._deliveryCandidates}.entries.take(_maxCandidatesPerBatch); | ||
| final messageDeliveries = batch.map( | ||
| (it) => MessageDelivery(channelCid: it.key, messageId: it.value.id), | ||
| ); | ||
|
|
||
| if (messageDeliveries.isEmpty) return; | ||
|
|
||
| _logger?.info('Marking ${messageDeliveries.length} channels as delivered'); | ||
|
|
||
| try { | ||
| await onMarkChannelsDelivered(messageDeliveries); | ||
|
|
||
| // Clear the successfully delivered candidates. If a channel's message ID | ||
| // has changed since we started delivery, keep it for the next batch. | ||
| await _deliveryCandidatesLock.synchronized(() { | ||
| for (final delivery in messageDeliveries) { | ||
| final deliveredChannelCid = delivery.channelCid; | ||
| final deliveredMessageId = delivery.messageId; | ||
|
|
||
| final currentMessage = _deliveryCandidates[deliveredChannelCid]; | ||
| // Skip removal if a newer message has been added while we were | ||
| // processing the current batch. | ||
| if (currentMessage?.id != deliveredMessageId) continue; | ||
| _deliveryCandidates.remove(deliveredChannelCid); | ||
| } | ||
|
|
||
| // Schedule the next batch if there are remaining candidates. | ||
| if (_deliveryCandidates.isNotEmpty) { | ||
| _throttledMarkCandidatesAsDelivered.call(); | ||
| } | ||
| }); | ||
| } catch (e, stk) { | ||
| _logger?.warning('Failed to mark channels as delivered', e, stk); | ||
| } | ||
| } | ||
|
|
||
| /// Cancels all pending delivery reports. | ||
| /// | ||
| /// Typically used when shutting down the reporter or permanently stopping | ||
| /// delivery reporting. | ||
| void cancel() { | ||
| _throttledMarkCandidatesAsDelivered.cancel(); | ||
| _deliveryCandidatesLock.synchronized(_deliveryCandidates.clear); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 2 additions & 0 deletions
2
packages/stream_chat/lib/src/core/models/channel_config.g.dart
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.