From bee88f70a363b60db73bf001aafbd07851209cde Mon Sep 17 00:00:00 2001 From: Tom Brow Date: Fri, 6 Nov 2026 12:42:15 -0800 Subject: [PATCH] fix(mobile): read only shown events from filtered Activity rows With Threads, Mentions or a People/Agents filter active, a row holds only part of its conversation. Marking it read advanced the thread or channel marker through its latest event, which also read the replies the filter hid. Filtered rows now write msg: marks for just the events they show, as filtered DM rows already did. Follow-up to #9213 (review comment r4497322772). Co-authored-by: studio Co-Authored-By: Claude Opus 5.5 Signed-off-by: Tom Brow --- .../lib/features/activity/activity_page.dart | 6 +-- .../features/activity/activity_page_test.dart | 54 +++++++++++++++++++ 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/mobile/lib/features/activity/activity_page.dart b/mobile/lib/features/activity/activity_page.dart index 9c85d9a45b..afbea12ab7 100644 --- a/mobile/lib/features/activity/activity_page.dart +++ b/mobile/lib/features/activity/activity_page.dart @@ -186,8 +186,6 @@ class ActivityPage extends HookConsumerWidget { (channelById[id]?.isDm ?? false) || ref.read(channelsProvider.notifier).hiddenDmIds.contains(id), ); - // A filtered DM row holds only part of its conversation, so the DM - // channel marker would also read events this view hides. final rowsArePartial = filter.value == InboxFilter.thread || filter.value == InboxFilter.mention || @@ -220,8 +218,10 @@ class ActivityPage extends HookConsumerWidget { ref .read(inboxLocalStateProvider.notifier) .clearUnread(groupedInboxItemIds(item)); + // A filtered row holds only part of its conversation, so channel and + // thread markers would also read events this view hides. final isDmRow = item.conversationId.startsWith('dm:'); - if (isDmRow && rowsArePartial) { + if (rowsArePartial && item.item.channelId != null) { for (final e in {item.item, ...item.groupItems}) { 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 d3b42abce2..eb4711f912 100644 --- a/mobile/test/features/activity/activity_page_test.dart +++ b/mobile/test/features/activity/activity_page_test.dart @@ -879,6 +879,60 @@ void main() { }, ); + testWidgets( + 'reading a thread row under People leaves hidden agent replies unread', + (tester) async { + FeedItem threadEvent(String id, String pubkey, int age) => FeedItem( + id: id, + kind: 9, + pubkey: pubkey, + content: 'body $id', + createdAt: now - age, + channelId: 'ch1', + channelName: 'general', + tags: const [ + ['e', threadIdRoot1, '', 'root'], + ['e', threadIdRoot1, '', 'reply'], + ], + category: 'mention', + ); + + await tester.pumpWidget( + await buildTestable( + knownAgentPubkeys: const {'agent_pk'}, + feed: HomeFeedResponse( + mentions: [ + threadEvent('g1', 'agent_pk', 300), + threadEvent('p1', 'bob_pk', 200), + ], + needsAction: const [], + activity: const [], + agentActivity: const [], + ), + ), + ); + await tester.pumpAndSettle(); + + Future chooseSender(String label) async { + await tester.tap(find.byKey(const ValueKey('activity-sender-menu'))); + await tester.pumpAndSettle(); + await tester.tap(find.text(label).last); + await tester.pumpAndSettle(); + } + + await chooseSender('People'); + await tester.drag( + find.byKey(const ValueKey('inbox-row-p1')), + const Offset(-500, 0), + ); + await tester.pumpAndSettle(); + expect(find.byKey(const ValueKey('inbox-unread-dot-p1')), findsNothing); + + await chooseSender('Anyone'); + expect(find.byKey(const ValueKey('inbox-unread-dot-p1')), findsOneWidget); + }, + ); + testWidgets('sender filter preloads authors hidden under newer rows', ( tester, ) async {