Skip to content

Commit 90de52d

Browse files
committed
feat(notifications): add notification feed and mark as read/seen
This commit introduces a notification feed to the sample app and implements the ability to mark notifications as read or seen. Key changes: - **Notification Feed UI:** - Added `NotificationFeed` widget to display a list of aggregated notifications. - Added `NotificationItem` widget to render individual notification items with icons, text, and an unread indicator. - Integrated the notification feed into the `UserFeedScreen` accessible via a new notification icon in the app bar, which displays a badge for unseen notifications. - Notifications can be marked as read by tapping on them or by using a "Mark all read" button. - Notifications are automatically marked as seen when the notification feed is opened. - **Mark Activity Logic:** - Implemented `onActivityMarked` in `FeedNotifier` to update the `FeedState` when activities are marked as read or seen. - Added helper methods (`_markAllRead`, `_markAllSeen`, `_markRead`, `_markSeen`) in `FeedNotifier` to handle different marking scenarios and update `NotificationStatusResponse`. - Introduced `MarkActivityDataHandler` extension on `MarkActivityData` to simplify handling different mark operations. - **Styling and Refinements:** - Refactored `ActionButton` to always display the count and use `TextButton.icon`. Removed the `showCountWhenZero` property. - Updated `AppTextTheme` for `bodyBold` to use `FontWeight.w500`. - Used `switch` expressions for icon selection in `ActivityContent` and `NotificationItem`. - **Exports:** Added `AggregatedActivityData`, `FeedMemberData`, and `FollowData` to `models.dart`. - **Feed Repository:** Ensured `aggregatedActivities` in `GetOrCreateFeedData` are sorted.
1 parent c4b5431 commit 90de52d

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';
@@ -177,14 +179,19 @@ class FeedStateNotifier extends StateNotifier<FeedState> {
177179

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

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

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

0 commit comments

Comments
 (0)