Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/stream_chat/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## Upcoming

✅ Added

- Added support for `Channel.markUnreadByTimestamp` and `Client.markChannelUnreadByTimestamp`
methods to mark all messages after a given timestamp as unread.

## 9.21.0

🐞 Fixed
Expand Down
21 changes: 18 additions & 3 deletions packages/stream_chat/lib/src/client/channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1641,10 +1641,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<EmptyResponse> markUnread(String messageId) async {
_checkInitialized();

Expand All @@ -1658,6 +1657,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<EmptyResponse> 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<EmptyResponse> markThreadRead(String threadId) async {
_checkInitialized();
Expand Down
22 changes: 19 additions & 3 deletions packages/stream_chat/lib/src/client/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1341,9 +1341,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<EmptyResponse> markChannelUnread(
String channelId,
String channelType,
Expand All @@ -1355,6 +1356,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<EmptyResponse> 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<EmptyResponse> markThreadRead(
Expand Down
21 changes: 19 additions & 2 deletions packages/stream_chat/lib/src/core/api/channel_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -323,11 +323,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<EmptyResponse> markUnread(
String channelId,
String channelType,
String? messageId,
String messageId,
) async {
final response = await _client.post(
'${_getChannelUrl(channelId, channelType)}/unread',
Expand All @@ -336,6 +338,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<EmptyResponse> 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<EmptyResponse> markThreadRead(
String channelId,
Expand Down
58 changes: 58 additions & 0 deletions packages/stream_chat/test/src/client/channel_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6772,6 +6772,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<StreamChatError>()),
);
},
);

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 {
Expand Down
27 changes: 27 additions & 0 deletions packages/stream_chat/test/src/client/client_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2055,6 +2055,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?',
Expand Down
54 changes: 54 additions & 0 deletions packages/stream_chat/test/src/core/api/channel_api_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,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: <String, dynamic>{}));

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: <String, dynamic>{}));

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';
Expand Down