diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 191e1f51e..dbc29e603 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -2,6 +2,8 @@ ✅ Added +- Added support for `Channel.markUnreadByTimestamp` and `Client.markChannelUnreadByTimestamp` + methods to mark all messages after a given timestamp as unread. - Added support for `hideHistoryBefore` in `Channel.addMembers` and `Client.addChannelMembers` to specify a timestamp before which channel history should be hidden for newly added members. When provided, it takes precedence over the `hideHistory` boolean flag. diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index dcbb5b84d..95bdfce11 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -1643,10 +1643,9 @@ class Channel { return _client.markChannelRead(id!, type, messageId: messageId); } - /// Mark message as unread. + /// Marks the channel as unread by a given [messageId]. /// - /// You have to provide a [messageId] from which you want the channel - /// to be marked as unread. + /// All messages from the provided message onwards will be marked as unread. Future markUnread(String messageId) async { _checkInitialized(); @@ -1660,6 +1659,22 @@ class Channel { return _client.markChannelUnread(id!, type, messageId); } + /// Marks the channel as unread by a given [timestamp]. + /// + /// All messages after the provided timestamp will be marked as unread. + Future markUnreadByTimestamp(DateTime timestamp) async { + _checkInitialized(); + + if (!canUseReadReceipts) { + throw const StreamChatError( + 'Cannot mark as unread: Channel does not support read events. ' + 'Enable read_events in your channel type configuration.', + ); + } + + return _client.markChannelUnreadByTimestamp(id!, type, timestamp); + } + /// Mark the thread with [threadId] in the channel as read. Future markThreadRead(String threadId) async { _checkInitialized(); diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index 293b7de10..6682ec28c 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -1343,9 +1343,10 @@ class StreamChatClient { messageId: messageId, ); - /// Mark [channelId] of type [channelType] all messages as read - /// Optionally provide a [messageId] if you want to mark a - /// particular message as read + /// Marks the [channelId] of type [channelType] as unread + /// by a given [messageId]. + /// + /// All messages from the provided message onwards will be marked as unread. Future markChannelUnread( String channelId, String channelType, @@ -1357,6 +1358,21 @@ class StreamChatClient { messageId, ); + /// Marks the [channelId] of type [channelType] as unread + /// by a given [timestamp]. + /// + /// All messages after the provided timestamp will be marked as unread. + Future markChannelUnreadByTimestamp( + String channelId, + String channelType, + DateTime timestamp, + ) => + _chatApi.channel.markUnreadByTimestamp( + channelId, + channelType, + timestamp, + ); + /// Mark the thread with [threadId] in the channel with [channelId] of type /// [channelType] as read. Future markThreadRead( diff --git a/packages/stream_chat/lib/src/core/api/channel_api.dart b/packages/stream_chat/lib/src/core/api/channel_api.dart index 80bb1606f..77c2425a5 100644 --- a/packages/stream_chat/lib/src/core/api/channel_api.dart +++ b/packages/stream_chat/lib/src/core/api/channel_api.dart @@ -328,11 +328,13 @@ class ChannelApi { return EmptyResponse.fromJson(response.data); } - /// Marks all messages from the provided [messageId] onwards as unread + /// Marks the channel as unread by a given [messageId]. + /// + /// All messages from the provided message onwards will be marked as unread. Future markUnread( String channelId, String channelType, - String? messageId, + String messageId, ) async { final response = await _client.post( '${_getChannelUrl(channelId, channelType)}/unread', @@ -341,6 +343,21 @@ class ChannelApi { return EmptyResponse.fromJson(response.data); } + /// Marks the channel as unread by a given [timestamp]. + /// + /// All messages after the provided timestamp will be marked as unread. + Future markUnreadByTimestamp( + String channelId, + String channelType, + DateTime timestamp, + ) async { + final response = await _client.post( + '${_getChannelUrl(channelId, channelType)}/unread', + data: {'message_timestamp': timestamp.toUtc().toIso8601String()}, + ); + return EmptyResponse.fromJson(response.data); + } + /// Mark the provided [threadId] of the channel as read. Future markThreadRead( String channelId, diff --git a/packages/stream_chat/test/src/client/channel_test.dart b/packages/stream_chat/test/src/client/channel_test.dart index 55ae1c75a..952447d29 100644 --- a/packages/stream_chat/test/src/client/channel_test.dart +++ b/packages/stream_chat/test/src/client/channel_test.dart @@ -6819,6 +6819,64 @@ void main() { }, ); + test( + ".markUnreadByTimestamp should throw if we don't have the capability", + () async { + final channelState = _generateChannelState( + channelId, + channelType, + ownCapabilities: [], // no readEvents capability + ); + + final channel = Channel.fromState(client, channelState); + addTearDown(channel.dispose); + + final timestamp = DateTime.parse('2024-01-01T00:00:00Z'); + + await expectLater( + channel.markUnreadByTimestamp(timestamp), + throwsA(isA()), + ); + }, + ); + + test( + '.markUnreadByTimestamp should succeed if we have the capability', + () async { + final channelState = _generateChannelState( + channelId, + channelType, + ownCapabilities: [ChannelCapability.readEvents], + ); + + final channel = Channel.fromState(client, channelState); + addTearDown(channel.dispose); + + final timestamp = DateTime.parse('2024-01-01T00:00:00Z'); + + when( + () => client.markChannelUnreadByTimestamp( + channelId, + channelType, + timestamp, + ), + ).thenAnswer((_) async => EmptyResponse()); + + await expectLater( + channel.markUnreadByTimestamp(timestamp), + completes, + ); + + verify( + () => client.markChannelUnreadByTimestamp( + channelId, + channelType, + timestamp, + ), + ).called(1); + }, + ); + test( ".markThreadRead should throw if we don't have the capability", () async { diff --git a/packages/stream_chat/test/src/client/client_test.dart b/packages/stream_chat/test/src/client/client_test.dart index 4d750dde4..932398336 100644 --- a/packages/stream_chat/test/src/client/client_test.dart +++ b/packages/stream_chat/test/src/client/client_test.dart @@ -2099,6 +2099,33 @@ void main() { verifyNoMoreInteractions(api.channel); }); + test('`.markChannelUnreadByTimestamp`', () async { + const channelType = 'test-channel-type'; + const channelId = 'test-channel-id'; + final timestamp = DateTime.parse('2024-01-01T00:00:00Z'); + + when(() => api.channel.markUnreadByTimestamp( + channelId, + channelType, + timestamp, + )).thenAnswer((_) async => EmptyResponse()); + + final res = await client.markChannelUnreadByTimestamp( + channelId, + channelType, + timestamp, + ); + + expect(res, isNotNull); + + verify(() => api.channel.markUnreadByTimestamp( + channelId, + channelType, + timestamp, + )).called(1); + verifyNoMoreInteractions(api.channel); + }); + test('`.createPoll`', () async { final poll = Poll( name: 'What is your favorite color?', diff --git a/packages/stream_chat/test/src/core/api/channel_api_test.dart b/packages/stream_chat/test/src/core/api/channel_api_test.dart index 7c12a3d3b..ae5a0dba8 100644 --- a/packages/stream_chat/test/src/core/api/channel_api_test.dart +++ b/packages/stream_chat/test/src/core/api/channel_api_test.dart @@ -684,6 +684,60 @@ void main() { verifyNoMoreInteractions(client); }); + test('markUnread', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + const messageId = 'test-message-id'; + + final path = '${_getChannelUrl(channelId, channelType)}/unread'; + + when(() => client.post( + path, + data: {'message_id': messageId}, + )) + .thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await channelApi.markUnread( + channelId, + channelType, + messageId, + ); + + expect(res, isNotNull); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('markUnreadByTimestamp', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + final timestamp = DateTime.parse('2024-01-01T00:00:00Z'); + + final path = '${_getChannelUrl(channelId, channelType)}/unread'; + + when(() => client.post( + path, + data: { + 'message_timestamp': timestamp.toUtc().toIso8601String(), + }, + )) + .thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await channelApi.markUnreadByTimestamp( + channelId, + channelType, + timestamp, + ); + + expect(res, isNotNull); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + test('archiveChannel', () async { const channelId = 'test-channel-id'; const channelType = 'test-channel-type';