Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions packages/stream_chat/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
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 @@ -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<EmptyResponse> markUnread(String messageId) async {
_checkInitialized();

Expand All @@ -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<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 @@ -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<EmptyResponse> markChannelUnread(
String channelId,
String channelType,
Expand All @@ -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<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 @@ -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<EmptyResponse> markUnread(
String channelId,
String channelType,
String? messageId,
String messageId,
) async {
final response = await _client.post(
'${_getChannelUrl(channelId, channelType)}/unread',
Expand All @@ -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<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 @@ -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<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 @@ -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?',
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 @@ -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: <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