From 28b927e93a3ae14cf795448107024acd94fc9641 Mon Sep 17 00:00:00 2001 From: Tom Brow Date: Fri, 6 Nov 2026 13:06:19 -0800 Subject: [PATCH] fix(mobile): let the synced marker catch up after filtered reads Advance a filtered row's thread or channel marker through every event that is now read, not just up to the row's own latest event, so replies read under one filter sync once the hidden ones are read under another. Co-authored-by: studio Co-Authored-By: Claude Opus 5.5 Signed-off-by: Tom Brow --- .../lib/features/activity/activity_page.dart | 43 ++++++++++++------- .../features/activity/activity_page_test.dart | 3 +- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/mobile/lib/features/activity/activity_page.dart b/mobile/lib/features/activity/activity_page.dart index b0f8225caa..60285d402d 100644 --- a/mobile/lib/features/activity/activity_page.dart +++ b/mobile/lib/features/activity/activity_page.dart @@ -1,7 +1,7 @@ import 'package:buzz/shared/widgets/feedback_snackbar.dart'; import 'dart:async'; -import 'dart:math' show max, min; +import 'dart:math' show max; import '../../shared/widgets/ios_message_create_button.dart'; import '../../shared/widgets/ios_glass_navigation_button.dart'; @@ -213,9 +213,11 @@ class ActivityPage extends HookConsumerWidget { return null; }, [preloadPubkeysKey]); - // A filtered row holds only part of its conversation. Advance its synced - // thread or channel marker only up to the first unread event the filter - // hides, and give shown events past that point local per-message marks. + // A filtered row holds only part of its conversation. Its synced thread + // or channel marker is one watermark, so advance it through every event + // that is now read (shown here, or already read) and stop just before the + // first unread event the filter hides. Shown events past that point get + // local per-message marks until the hidden one is read too. void markPartialRowRead( InboxItem item, String channelId, { @@ -231,21 +233,30 @@ class ActivityPage extends HookConsumerWidget { (rootId == null || (isThreadReply(e.tags) && threadReferenceOf(e.tags).rootId == rootId)); - var cap = item.latestActivityAt; - for (final e in allItems.expand((i) => i.groupItems)) { - if (shown.containsKey(e.id) || - !inContext(e) || - isInboxEventRead(e, markerOf: markerOf)) { - continue; + final events = { + for (final e in [ + ...allItems.expand((i) => i.groupItems), + ...shown.values, + ]) + if (inContext(e)) e.id: e, + }.values.toList()..sort((a, b) => a.createdAt.compareTo(b.createdAt)); + int? cap; + for (final e in events) { + if (!shown.containsKey(e.id) && + !isInboxEventRead(e, markerOf: markerOf)) { + cap = e.createdAt - 1; + break; } - cap = min(cap, e.createdAt - 1); + cap = e.createdAt; + } + if (cap != null) { + notifier.markContextRead( + rootId == null ? channelId : threadContextKey(rootId), + cap, + ); } - notifier.markContextRead( - rootId == null ? channelId : threadContextKey(rootId), - cap, - ); for (final e in shown.values) { - if (inContext(e) && e.createdAt <= cap) continue; + if (inContext(e) && cap != null && e.createdAt <= cap) continue; notifier.markContextRead(msgContextKey(e.id), e.createdAt); } } diff --git a/mobile/test/features/activity/activity_page_test.dart b/mobile/test/features/activity/activity_page_test.dart index e1976ed0cb..23064b7639 100644 --- a/mobile/test/features/activity/activity_page_test.dart +++ b/mobile/test/features/activity/activity_page_test.dart @@ -943,7 +943,8 @@ void main() { const Offset(-500, 0), ); await tester.pumpAndSettle(); - expect(threadMarker(), now - 300); + // With the agent reply read too, the synced marker covers both. + expect(threadMarker(), now - 200); await chooseSender('Anyone'); expect(find.byKey(const ValueKey('inbox-unread-dot-p1')), findsNothing); },