Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
12 changes: 12 additions & 0 deletions packages/stream_chat/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
## Upcoming

✅ Added

- Added `markUnreadByTimestamp` method to `Channel` and `Client` to mark all messages after a given
timestamp as unread.

🔄 Changed

- Made the `messageId` parameter optional in `Channel.markUnread` and `Client.markChannelUnread`
methods. If not provided, the entire channel is marked as unread.

## 9.21.0

🐞 Fixed
Expand Down
24 changes: 20 additions & 4 deletions packages/stream_chat/lib/src/client/channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1641,11 +1641,11 @@ class Channel {
return _client.markChannelRead(id!, type, messageId: messageId);
}

/// Mark message as unread.
/// Marks the channel as unread.
///
/// You have to provide a [messageId] from which you want the channel
/// to be marked as unread.
Future<EmptyResponse> markUnread(String messageId) async {
/// Optionally provide a [messageId] to only mark messages from that ID
/// onwards as unread.
Future<EmptyResponse> markUnread([String? messageId]) async {
_checkInitialized();

if (!canUseReadReceipts) {
Expand All @@ -1658,6 +1658,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
28 changes: 22 additions & 6 deletions packages/stream_chat/lib/src/client/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1341,20 +1341,36 @@ 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.
///
/// Optionally provide a [messageId] to only mark messages from that ID
/// onwards as unread.
Future<EmptyResponse> markChannelUnread(
String channelId,
String channelType,
String messageId,
) =>
String channelType, [
String? messageId,
]) =>
_chatApi.channel.markUnread(
channelId,
channelType,
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
24 changes: 21 additions & 3 deletions packages/stream_chat/lib/src/core/api/channel_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -323,15 +323,33 @@ class ChannelApi {
return EmptyResponse.fromJson(response.data);
}

/// Marks all messages from the provided [messageId] onwards as unread
/// Marks the channel as unread.
///
/// Optionally provide a [messageId] to only mark messages from that ID
/// onwards as unread.
Future<EmptyResponse> markUnread(
String channelId,
String channelType,
String channelType, [
String? messageId,
]) async {
final response = await _client.post(
'${_getChannelUrl(channelId, channelType)}/unread',
data: {if (messageId != null) 'message_id': messageId},
);
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_id': messageId},
data: {'message_timestamp': timestamp.toUtc().toIso8601String()},
);
return EmptyResponse.fromJson(response.data);
}
Expand Down
93 changes: 92 additions & 1 deletion packages/stream_chat/test/src/client/channel_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6731,7 +6731,7 @@ void main() {
addTearDown(channel.dispose);

await expectLater(
channel.markUnread('message-id-123'),
channel.markUnread(),
throwsA(isA<StreamChatError>()),
);
},
Expand All @@ -6749,6 +6749,39 @@ void main() {
final channel = Channel.fromState(client, channelState);
addTearDown(channel.dispose);

when(
() => client.markChannelUnread(
channelId,
channelType,
),
).thenAnswer((_) async => EmptyResponse());

await expectLater(
channel.markUnread(),
completes,
);

verify(
() => client.markChannelUnread(
channelId,
channelType,
),
).called(1);
},
);

test(
'.markUnread with messageId 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);

when(
() => client.markChannelUnread(
channelId,
Expand All @@ -6772,6 +6805,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
46 changes: 46 additions & 0 deletions packages/stream_chat/test/src/client/client_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2037,6 +2037,25 @@ void main() {
test('`.markChannelUnread`', () async {
const channelType = 'test-channel-type';
const channelId = 'test-channel-id';

when(() => api.channel.markUnread(channelId, channelType, null))
.thenAnswer((_) async => EmptyResponse());

final res = await client.markChannelUnread(
channelId,
channelType,
);

expect(res, isNotNull);

verify(() => api.channel.markUnread(channelId, channelType, null))
.called(1);
verifyNoMoreInteractions(api.channel);
});

test('`.markChannelUnread` with messageId', () async {
const channelType = 'test-channel-type';
const channelId = 'test-channel-id';
const messageId = 'test-message-id';

when(() => api.channel.markUnread(channelId, channelType, messageId))
Expand All @@ -2055,6 +2074,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
78 changes: 78 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,84 @@ void main() {
verifyNoMoreInteractions(client);
});

test('markUnread', () async {
const channelId = 'test-channel-id';
const channelType = 'test-channel-type';

final path = '${_getChannelUrl(channelId, channelType)}/unread';

when(() => client.post(
path,
data: {},
))
.thenAnswer(
(_) async => successResponse(path, data: <String, dynamic>{}));

final res = await channelApi.markUnread(
channelId,
channelType,
);

expect(res, isNotNull);

verify(() => client.post(path, data: any(named: 'data'))).called(1);
verifyNoMoreInteractions(client);
});

test('markUnread with messageId', () 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