Skip to content

Commit 04dad55

Browse files
authored
feat(notifications): add notification feed and mark as read/seen (#20)
1 parent 9f55916 commit 04dad55

File tree

10 files changed

+545
-55
lines changed

10 files changed

+545
-55
lines changed

packages/stream_feeds/lib/src/models.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
export 'models/activity_data.dart';
2+
export 'models/aggregated_activity_data.dart';
23
export 'models/feed_data.dart';
34
export 'models/feed_id.dart';
45
export 'models/feed_input_data.dart';
6+
export 'models/feed_member_data.dart';
57
export 'models/feed_member_request_data.dart';
68
export 'models/feeds_config.dart';
9+
export 'models/follow_data.dart';
710
export 'models/poll_data.dart';
811
export 'models/request/activity_add_comment_request.dart'
912
show ActivityAddCommentRequest;

packages/stream_feeds/lib/src/models/mark_activity_data.dart

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,39 @@ class MarkActivityData with _$MarkActivityData {
4545
final List<String>? markWatched;
4646
}
4747

48+
extension MarkActivityDataHandler<R> on MarkActivityData {
49+
R handle({
50+
R Function()? markAllRead,
51+
R Function()? markAllSeen,
52+
R Function(Set<String> read)? markRead,
53+
R Function(Set<String> seen)? markSeen,
54+
R Function(Set<String> watched)? markWatched,
55+
required R Function(MarkActivityData data) orElse,
56+
}) {
57+
if (this.markAllRead case true) {
58+
return markAllRead?.call() ?? orElse(this);
59+
}
60+
61+
if (this.markAllSeen case true) {
62+
return markAllSeen?.call() ?? orElse(this);
63+
}
64+
65+
if (this.markRead case final read?) {
66+
return markRead?.call(read.toSet()) ?? orElse(this);
67+
}
68+
69+
if (this.markSeen case final seen?) {
70+
return markSeen?.call(seen.toSet()) ?? orElse(this);
71+
}
72+
73+
if (this.markWatched case final watched?) {
74+
return markWatched?.call(watched.toSet()) ?? orElse(this);
75+
}
76+
77+
return orElse(this);
78+
}
79+
}
80+
4881
/// Extension function to convert an [ActivityMarkEvent] to a [MarkActivityData] model.
4982
extension ActivityMarkEventMapper on ActivityMarkEvent {
5083
/// Converts this API activity mark event to a domain [MarkActivityData] instance.

packages/stream_feeds/lib/src/repository/feeds_repository.dart

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import 'package:collection/collection.dart';
12
import 'package:stream_core/stream_core.dart';
23

34
import '../generated/api/api.dart' as api;
45
import '../models/activity_data.dart';
56
import '../models/activity_pin_data.dart';
7+
import '../models/aggregated_activity_data.dart';
68
import '../models/feed_data.dart';
79
import '../models/feed_id.dart';
810
import '../models/feed_member_data.dart';
@@ -54,7 +56,9 @@ class FeedsRepository {
5456

5557
return GetOrCreateFeedData(
5658
activities: PaginationResult(
57-
items: response.activities.map((a) => a.toModel()).toList(),
59+
items: response.activities
60+
.map((a) => a.toModel())
61+
.sorted(ActivitiesSort.defaultSort.compare),
5862
pagination: PaginationData(
5963
next: response.next,
6064
previous: response.prev,
@@ -78,6 +82,9 @@ class FeedsRepository {
7882
ownCapabilities: response.ownCapabilities,
7983
pinnedActivities:
8084
response.pinnedActivities.map((a) => a.toModel()).toList(),
85+
aggregatedActivities:
86+
response.aggregatedActivities.map((a) => a.toModel()).toList(),
87+
notificationStatus: response.notificationStatus,
8188
);
8289
});
8390
}

packages/stream_feeds/lib/src/state/feed_state.dart

Lines changed: 77 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import 'dart:math';
2+
13
import 'package:collection/collection.dart';
24
import 'package:freezed_annotation/freezed_annotation.dart';
35
import 'package:state_notifier/state_notifier.dart';
@@ -178,14 +180,19 @@ class FeedStateNotifier extends StateNotifier<FeedState> {
178180

179181
/// Handles updates to the feed state when an activity is marked read or seen.
180182
void onActivityMarked(MarkActivityData markData) {
181-
// TODO: Handle activity marking (read/seen/watched) operations
182-
183-
// Note: The mark data contains information about:
184-
// - markAllRead: Whether all activities should be marked as read
185-
// - markAllSeen: Whether all activities should be marked as seen
186-
// - markRead: List of specific activity IDs marked as read
187-
// - markSeen: List of specific activity IDs marked as seen
188-
// - markWatched: List of specific activity IDs marked as watched
183+
// Update the state based on the type of mark operation
184+
state = markData.handle(
185+
// If markAllRead is true, mark all activities as read
186+
markAllRead: () => _markAllRead(state),
187+
// If markAllSeen is true, mark all activities as seen
188+
markAllSeen: () => _markAllSeen(state),
189+
// If markRead contains specific IDs, mark those as read
190+
markRead: (read) => _markRead(read, state),
191+
// If markSeen contains specific IDs, mark those as seen
192+
markSeen: (seen) => _markSeen(seen, state),
193+
// For other cases, return the current state without changes
194+
orElse: (MarkActivityData data) => state,
195+
);
189196
}
190197

191198
/// Handles updates to the feed state when a bookmark is added or removed.
@@ -377,6 +384,68 @@ class FeedStateNotifier extends StateNotifier<FeedState> {
377384
return _addFollow(follow, removedFollowState);
378385
}
379386

387+
FeedState _markAllRead(FeedState state) {
388+
final aggregatedActivities = state.aggregatedActivities;
389+
final readActivities = aggregatedActivities.map((it) => it.group).toList();
390+
391+
// Set unread count to 0 and update read activities
392+
final updatedNotificationStatus = state.notificationStatus?.copyWith(
393+
unread: 0,
394+
readActivities: readActivities,
395+
lastReadAt: DateTime.timestamp(),
396+
);
397+
398+
return state.copyWith(notificationStatus: updatedNotificationStatus);
399+
}
400+
401+
FeedState _markAllSeen(FeedState state) {
402+
final aggregatedActivities = state.aggregatedActivities;
403+
final seenActivities = aggregatedActivities.map((it) => it.group).toList();
404+
405+
// Set unseen count to 0 and update seen activities
406+
final updatedNotificationStatus = state.notificationStatus?.copyWith(
407+
unseen: 0,
408+
seenActivities: seenActivities,
409+
lastSeenAt: DateTime.timestamp(),
410+
);
411+
412+
return state.copyWith(notificationStatus: updatedNotificationStatus);
413+
}
414+
415+
FeedState _markRead(Set<String> readIds, FeedState state) {
416+
final readActivities = state.notificationStatus?.readActivities?.toSet();
417+
final updatedReadActivities = readActivities?.union(readIds).toList();
418+
419+
// Decrease unread count by the number of newly read activities
420+
final unreadCount = state.notificationStatus?.unread ?? 0;
421+
final updatedUnreadCount = max(unreadCount - readIds.length, 0);
422+
423+
final updatedNotificationStatus = state.notificationStatus?.copyWith(
424+
unread: updatedUnreadCount,
425+
readActivities: updatedReadActivities,
426+
lastReadAt: DateTime.timestamp(),
427+
);
428+
429+
return state.copyWith(notificationStatus: updatedNotificationStatus);
430+
}
431+
432+
FeedState _markSeen(Set<String> seenIds, FeedState state) {
433+
final seenActivities = state.notificationStatus?.seenActivities?.toSet();
434+
final updatedSeenActivities = seenActivities?.union(seenIds).toList();
435+
436+
// Decrease unseen count by the number of newly seen activities
437+
final unseenCount = state.notificationStatus?.unseen ?? 0;
438+
final updatedUnseenCount = max(unseenCount - seenIds.length, 0);
439+
440+
final updatedNotificationStatus = state.notificationStatus?.copyWith(
441+
unseen: updatedUnseenCount,
442+
seenActivities: updatedSeenActivities,
443+
lastSeenAt: DateTime.timestamp(),
444+
);
445+
446+
return state.copyWith(notificationStatus: updatedNotificationStatus);
447+
}
448+
380449
@override
381450
void dispose() {
382451
_removeMemberListListener?.call();

0 commit comments

Comments
 (0)