···11/// Notifier for managing notification list state.
12///
13/// Watches the notifications stream and provides methods for
14-/// refresh and load more pagination.
01516@ProviderFor(NotificationsNotifier)
17final notificationsProvider = NotificationsNotifierProvider._();
···19/// Notifier for managing notification list state.
20///
21/// Watches the notifications stream and provides methods for
22-/// refresh and load more pagination.
023final class NotificationsNotifierProvider
24- extends $StreamNotifierProvider<NotificationsNotifier, List<AppNotification>> {
25 /// Notifier for managing notification list state.
26 ///
27 /// Watches the notifications stream and provides methods for
28- /// refresh and load more pagination.
029 NotificationsNotifierProvider._()
30 : super(
31 from: null,
···45 NotificationsNotifier create() => NotificationsNotifier();
46}
4748-String _$notificationsNotifierHash() => r'4c8135e870f771092ea4d88fbdde28fd426b7d3f';
4950/// Notifier for managing notification list state.
51///
52/// Watches the notifications stream and provides methods for
53-/// refresh and load more pagination.
05455-abstract class _$NotificationsNotifier extends $StreamNotifier<List<AppNotification>> {
56- Stream<List<AppNotification>> build();
57 @$mustCallSuper
58 @override
59 void runBuild() {
60- final ref = this.ref as $Ref<AsyncValue<List<AppNotification>>, List<AppNotification>>;
61 final element =
62 ref.element
63 as $ClassProviderElement<
64- AnyNotifier<AsyncValue<List<AppNotification>>, List<AppNotification>>,
65- AsyncValue<List<AppNotification>>,
66 Object?,
67 Object?
68 >;
···11/// Notifier for managing notification list state.
12///
13/// Watches the notifications stream and provides methods for
14+/// refresh and load more pagination. Returns grouped notifications
15+/// for compact display.
1617@ProviderFor(NotificationsNotifier)
18final notificationsProvider = NotificationsNotifierProvider._();
···20/// Notifier for managing notification list state.
21///
22/// Watches the notifications stream and provides methods for
23+/// refresh and load more pagination. Returns grouped notifications
24+/// for compact display.
25final class NotificationsNotifierProvider
26+ extends $StreamNotifierProvider<NotificationsNotifier, List<GroupedNotification>> {
27 /// Notifier for managing notification list state.
28 ///
29 /// Watches the notifications stream and provides methods for
30+ /// refresh and load more pagination. Returns grouped notifications
31+ /// for compact display.
32 NotificationsNotifierProvider._()
33 : super(
34 from: null,
···48 NotificationsNotifier create() => NotificationsNotifier();
49}
5051+String _$notificationsNotifierHash() => r'287578fad2ea840c95e2dac8998c4763146fb8cd';
5253/// Notifier for managing notification list state.
54///
55/// Watches the notifications stream and provides methods for
56+/// refresh and load more pagination. Returns grouped notifications
57+/// for compact display.
5859+abstract class _$NotificationsNotifier extends $StreamNotifier<List<GroupedNotification>> {
60+ Stream<List<GroupedNotification>> build();
61 @$mustCallSuper
62 @override
63 void runBuild() {
64+ final ref = this.ref as $Ref<AsyncValue<List<GroupedNotification>>, List<GroupedNotification>>;
65 final element =
66 ref.element
67 as $ClassProviderElement<
68+ AnyNotifier<AsyncValue<List<GroupedNotification>>, List<GroupedNotification>>,
69+ AsyncValue<List<GroupedNotification>>,
70 Object?,
71 Object?
72 >;
···1+import '../../../infrastructure/db/app_database.dart';
2+import 'notification.dart';
3+import 'notification_type.dart';
4+5+/// Represents a group of similar notifications for compact display.
6+///
7+/// Groups notifications of the same type, on the same subject,
8+/// within a 24-hour window.
9+class GroupedNotification {
10+ GroupedNotification({
11+ required this.type,
12+ required this.actors,
13+ required this.subjectUri,
14+ required this.mostRecentTimestamp,
15+ required this.hasUnread,
16+ required this.totalCount,
17+ required this.notifications,
18+ });
19+20+ /// The notification type shared by all in this group.
21+ final NotificationType type;
22+23+ /// Actors who triggered notifications in this group.
24+ final List<Profile> actors;
25+26+ /// URI of the subject (post/profile) this group is about.
27+ final String? subjectUri;
28+29+ /// Timestamp of the most recent notification in the group.
30+ final DateTime mostRecentTimestamp;
31+32+ /// Whether any notification in this group is unread.
33+ final bool hasUnread;
34+35+ /// Total number of notifications in this group.
36+ final int totalCount;
37+38+ /// All underlying notifications in this group.
39+ final List<AppNotification> notifications;
40+41+ /// Maximum number of actors to display inline.
42+ static const int maxDisplayActors = 5;
43+44+ /// Maximum time difference for grouping (24 hours).
45+ static const Duration groupingWindow = Duration(hours: 24);
46+47+ /// Returns a formatted display text for this group.
48+ ///
49+ /// Examples:
50+ /// - Single actor: "Alice liked your post"
51+ /// - Two actors: "Alice and Bob liked your post"
52+ /// - Many actors: "Alice, Bob and 3 others liked your post"
53+ String get displayText {
54+ if (actors.isEmpty) return type.displayText;
55+56+ final names = actors.take(2).map((a) => a.displayName ?? a.handle).toList();
57+ final remaining = totalCount - names.length;
58+59+ if (totalCount == 1) {
60+ return '${names[0]} ${type.displayText}';
61+ } else if (totalCount == 2) {
62+ return '${names[0]} and ${names[1]} ${type.displayText}';
63+ } else {
64+ return '${names[0]}, ${names[1]} and $remaining others ${type.displayText}';
65+ }
66+ }
67+68+ /// Groups a list of notifications by type and subject within 24 hours.
69+ ///
70+ /// Returns a list of [GroupedNotification] sorted by most recent timestamp.
71+ static List<GroupedNotification> groupNotifications(List<AppNotification> notifications) {
72+ if (notifications.isEmpty) return [];
73+74+ final sorted = [...notifications]..sort((a, b) => b.indexedAt.compareTo(a.indexedAt));
75+76+ final groups = <GroupedNotification>[];
77+ var currentGroup = <AppNotification>[sorted.first];
78+ var currentType = sorted.first.type;
79+ var currentSubject = sorted.first.reasonSubjectUri;
80+ var groupStartTime = sorted.first.indexedAt;
81+82+ for (var i = 1; i < sorted.length; i++) {
83+ final notification = sorted[i];
84+ final timeDiff = groupStartTime.difference(notification.indexedAt);
85+ final sameType = notification.type == currentType;
86+ final sameSubject = notification.reasonSubjectUri == currentSubject;
87+ final withinWindow = timeDiff <= groupingWindow;
88+89+ if (sameType && sameSubject && withinWindow) {
90+ currentGroup.add(notification);
91+ } else {
92+ groups.add(_createGroup(currentGroup));
93+ currentGroup = [notification];
94+ currentType = notification.type;
95+ currentSubject = notification.reasonSubjectUri;
96+ groupStartTime = notification.indexedAt;
97+ }
98+ }
99+100+ if (currentGroup.isNotEmpty) {
101+ groups.add(_createGroup(currentGroup));
102+ }
103+104+ return groups;
105+ }
106+107+ static GroupedNotification _createGroup(List<AppNotification> notifications) {
108+ final seenDids = <String>{};
109+ final uniqueActors = <Profile>[];
110+ for (final n in notifications) {
111+ if (!seenDids.contains(n.actor.did)) {
112+ seenDids.add(n.actor.did);
113+ uniqueActors.add(n.actor);
114+ }
115+ }
116+117+ return GroupedNotification(
118+ type: notifications.first.type,
119+ actors: uniqueActors,
120+ subjectUri: notifications.first.reasonSubjectUri,
121+ mostRecentTimestamp: notifications.first.indexedAt,
122+ hasUnread: notifications.any((n) => !n.isRead),
123+ totalCount: uniqueActors.length,
124+ notifications: notifications,
125+ );
126+ }
127+}