Skip to content

Commit

Permalink
feat(llc): mark unread feature - core (#1880)
Browse files Browse the repository at this point in the history
* mark message as unread feature

* jump to last read message

* changelog

* error handling

* tweak

* changelog

* fixed changelog
  • Loading branch information
Brazol authored Mar 27, 2024
1 parent 6e58645 commit f9bab6d
Show file tree
Hide file tree
Showing 22 changed files with 306 additions and 67 deletions.
5 changes: 5 additions & 0 deletions packages/stream_chat/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## Unreleased

✅ Added
- Added `markUnread` method to `Channel` that marks messages from the provided message id onwards as unread

## 7.1.0

🐞 Fixed
Expand Down
63 changes: 61 additions & 2 deletions packages/stream_chat/lib/src/client/channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1341,8 +1341,8 @@ class Channel {

/// Mark all messages as read.
///
/// Optionally provide a [messageId] if you want to mark a
/// particular message as read.
/// Optionally provide a [messageId] if you want to mark channel as
/// read from a particular message onwards.
Future<EmptyResponse> markRead({String? messageId}) async {
_checkInitialized();
client.state.totalUnreadCount =
Expand All @@ -1351,6 +1351,37 @@ class Channel {
return _client.markChannelRead(id!, type, messageId: messageId);
}

/// Mark message 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 {
_checkInitialized();

final response = await _client.markChannelUnread(id!, type, messageId);

final lastReadDate = state!.currentUserRead?.lastRead;
final currentUnread = state!.currentUserRead?.unreadMessages ?? 0;

final messagesFromMarked = state!.messages
.where((message) => message.user?.id != client.state.currentUser?.id)
.skipWhile((message) => message.id != messageId)
.toList();
final channelUnreadCount = max(currentUnread, messagesFromMarked.length);
final additionalTotalUnreadCount = currentUnread > 0
? messagesFromMarked
.takeWhile((message) =>
lastReadDate == null ||
message.createdAt.isBefore(lastReadDate))
.length
: messagesFromMarked.length;

client.state.totalUnreadCount += additionalTotalUnreadCount;
state!.unreadCount = channelUnreadCount;

return response;
}

void _initState(ChannelState channelState) {
state = ChannelClientState(this, channelState);

Expand Down Expand Up @@ -1761,6 +1792,8 @@ class ChannelClientState {

_listenReadEvents();

_listenUnreadEvents();

_listenChannelTruncated();

_listenChannelUpdated();
Expand Down Expand Up @@ -2242,6 +2275,31 @@ class ChannelClientState {
return updateMessage(message);
}

void _listenUnreadEvents() {
if (_channelState.channel?.config.readEvents == false) {
return;
}

_subscriptions.add(
_channel.on(EventType.notificationMarkUnread).listen((Event event) {
if (event.user?.id != _channel._client.state.currentUser!.id) return;

final readList = <Read>[
..._channelState.read?.where((r) => r.user.id != event.user!.id) ??
<Read>[],
if (event.lastReadAt != null)
Read(
user: event.user!,
lastRead: event.lastReadAt!,
unreadMessages: event.unreadMessages ?? 0,
lastReadMessageId: event.lastReadMessageId,
)
];

_channelState = _channelState.copyWith(read: readList);
}));
}

void _listenReadEvents() {
if (_channelState.channel?.config.readEvents == false) {
return;
Expand All @@ -2262,6 +2320,7 @@ class ChannelClientState {
readList.add(Read(
user: event.user!,
lastRead: event.createdAt,
lastReadMessageId: messages.lastOrNull?.id,
));
_channelState = _channelState.copyWith(read: readList);
}
Expand Down
14 changes: 14 additions & 0 deletions packages/stream_chat/lib/src/client/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1184,6 +1184,20 @@ 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
Future<EmptyResponse> markChannelUnread(
String channelId,
String channelType,
String messageId,
) =>
_chatApi.channel.markUnread(
channelId,
channelType,
messageId,
);

/// Update or Create the given user object.
Future<UpdateUsersResponse> updateUser(User user) => updateUsers([user]);

Expand Down
17 changes: 15 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 @@ -310,8 +310,8 @@ class ChannelApi {
}

/// Mark [channelId] of type [channelType] all messages as read
/// Optionally provide a [messageId] if you want to mark a
/// particular message as read
/// Optionally provide a [messageId] if you want to mark channel as
/// read from particular message onwards
Future<EmptyResponse> markRead(
String channelId,
String channelType, {
Expand All @@ -324,6 +324,19 @@ class ChannelApi {
return EmptyResponse.fromJson(response.data);
}

/// Marks all messages from the provided [messageId] onwards as unread
Future<EmptyResponse> markUnread(
String channelId,
String channelType,
String? messageId,
) async {
final response = await _client.post(
'${_getChannelUrl(channelId, channelType)}/unread',
data: {'message_id': messageId},
);
return EmptyResponse.fromJson(response.data);
}

/// Stop watching the channel
Future<EmptyResponse> stopWatching(
String channelId,
Expand Down
18 changes: 18 additions & 0 deletions packages/stream_chat/lib/src/core/models/event.dart
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,24 @@ class Event {
/// Map of custom channel extraData
final Map<String, Object?> extraData;

/// Create date of the last read message (notification.mark_unread)
@JsonKey(includeToJson: false, includeFromJson: false)
DateTime? get lastReadAt {
if (extraData.containsKey('last_read_at')) {
return DateTime.parse(extraData['last_read_at']! as String);
}

return null;
}

/// The number of unread messages (notification.mark_unread)
@JsonKey(includeToJson: false, includeFromJson: false)
int? get unreadMessages => extraData['unread_messages'] as int?;

/// The id of the last read message (notification.mark_read)
@JsonKey(includeToJson: false, includeFromJson: false)
String? get lastReadMessageId => extraData['last_read_message_id'] as String?;

/// Known top level fields.
/// Useful for [Serializer] methods.
static final topLevelFields = [
Expand Down
7 changes: 7 additions & 0 deletions packages/stream_chat/lib/src/core/models/read.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class Read extends Equatable {
const Read({
required this.lastRead,
required this.user,
this.lastReadMessageId,
this.unreadMessages = 0,
});

Expand All @@ -26,24 +27,30 @@ class Read extends Equatable {
/// Number of unread messages
final int unreadMessages;

/// The id of the last read message
final String? lastReadMessageId;

/// Serialize to json
Map<String, dynamic> toJson() => _$ReadToJson(this);

/// Creates a copy of [Read] with specified attributes overridden.
Read copyWith({
DateTime? lastRead,
String? lastReadMessageId,
User? user,
int? unreadMessages,
}) =>
Read(
lastRead: lastRead ?? this.lastRead,
lastReadMessageId: lastReadMessageId ?? this.lastReadMessageId,
user: user ?? this.user,
unreadMessages: unreadMessages ?? this.unreadMessages,
);

@override
List<Object?> get props => [
lastRead,
lastReadMessageId,
user,
unreadMessages,
];
Expand Down
2 changes: 2 additions & 0 deletions packages/stream_chat/lib/src/core/models/read.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions packages/stream_chat/lib/src/event_type.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ class EventType {
/// Event sent when the unread count changes
static const String notificationMarkRead = 'notification.mark_read';

/// Event sent when the unread count changes
static const String notificationMarkUnread = 'notification.mark_unread';

/// Event sent when deleting a new message
static const String messageDeleted = 'message.deleted';

Expand Down
3 changes: 2 additions & 1 deletion packages/stream_chat/test/fixtures/read.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
"id": "bbb19d9a-ee50-45bc-84e5-0584e79d0c9e"
},
"last_read": "2020-01-28T22:17:30.966485504Z",
"unread_messages": 10
"unread_messages": 10,
"last_read_message_id": "8cc1301d-2d47-4305-945a-cd8e19b736d6"
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ void main() {
(index) => Read(
lastRead: DateTime.now(),
user: User(id: 'test-user-id-$index'),
lastReadMessageId: 'last-message-id-$index',
),
);
final watchers = List.generate(
Expand Down
Loading

0 comments on commit f9bab6d

Please sign in to comment.