From 9d52daab582f0c29f008c5e0b277b738308f6fc3 Mon Sep 17 00:00:00 2001 From: Taylor Ho Date: Mon, 19 Oct 2026 15:36:04 -0700 Subject: [PATCH] fix(mobile): load active filter activity outside Riverpod selectors Read the channels notifier before evaluating the selector so activity history requests can run. Exercise the real provider with signed old activity and verify the seven-day filter excludes it. Co-authored-by: Carl Signed-off-by: Taylor Ho --- .../channel_filter_activity.dart | 3 +- .../channel_filter/channel_filter_test.dart | 75 +++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/mobile/lib/features/channels/channel_filter/channel_filter_activity.dart b/mobile/lib/features/channels/channel_filter/channel_filter_activity.dart index 3e244a9421..ad01587f24 100644 --- a/mobile/lib/features/channels/channel_filter/channel_filter_activity.dart +++ b/mobile/lib/features/channels/channel_filter/channel_filter_activity.dart @@ -48,6 +48,7 @@ final channelFilterActivityProvider = FutureProvider.autoDispose .family((ref, channelId) async { // Every live message and foreground refresh updates this owner. Re-read // only the affected channel; never trust its unverified presentation stamp. + final channelsNotifier = ref.watch(channelsProvider.notifier); ref.watch( channelsProvider.select( (value) => ( @@ -55,7 +56,7 @@ final channelFilterActivityProvider = FutureProvider.autoDispose ?.where((c) => c.id == channelId) .firstOrNull ?.lastMessageAt, - ref.read(channelsProvider.notifier).activityRevisionFor(channelId), + channelsNotifier.activityRevisionFor(channelId), ), ), ); diff --git a/mobile/test/features/channels/channel_filter/channel_filter_test.dart b/mobile/test/features/channels/channel_filter/channel_filter_test.dart index adc33f94c8..7222d0087c 100644 --- a/mobile/test/features/channels/channel_filter/channel_filter_test.dart +++ b/mobile/test/features/channels/channel_filter/channel_filter_test.dart @@ -1,3 +1,6 @@ +import 'package:buzz/features/channels/channels_provider.dart'; +import 'package:buzz/features/channels/channel.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'dart:async'; import 'dart:convert'; import 'package:buzz/features/channels/channel_filter/channel_filter_manager.dart'; @@ -9,6 +12,57 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:nostr/nostr.dart' as nostr; void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + test( + 'activity provider reads history and excludes a quiet channel from Active', + () async { + final keys = nostr.Keys.generate(); + final old = DateTime.now().millisecondsSinceEpoch ~/ 1000 - 8 * 86400; + final event = NostrEvent.fromJson( + nostr.Event.from( + kind: EventKind.streamMessage, + tags: [ + ['h', 'quiet-channel'], + ], + content: 'Earlier activity', + secretKey: nostr.Nip19.decode(payload: keys.nsec).data, + createdAt: old, + verify: false, + ).toMap(), + ); + final session = _ActivitySession(event); + final container = ProviderContainer( + overrides: [ + channelsProvider.overrideWith(_ActivityChannels.new), + relaySessionProvider.overrideWith(() => session), + ], + ); + addTearDown(container.dispose); + await container.read(channelsProvider.future); + final subscription = container.listen( + channelFilterActivityProvider('quiet-channel'), + (_, _) {}, + ); + addTearDown(subscription.close); + final activity = await container.read( + channelFilterActivityProvider('quiet-channel').future, + ); + expect(session.requests, 1); + expect(activity, old); + expect( + channelMatchesFilter( + mode: ChannelFilterMode.active, + unread: false, + activitySeconds: activity, + activityKnown: true, + nowSeconds: old + 8 * 86400, + windowSeconds: 7 * 86400, + ), + isFalse, + ); + }, + ); + test( 'metadata wins and edits preserve reset tombstones, unknown fields and windows', () { @@ -296,3 +350,24 @@ void main() { expect(verifiedFilterActivity([]), isNull); }); } + +class _ActivityChannels extends ChannelsNotifier { + @override + Future> build() async => []; +} + +class _ActivitySession extends RelaySessionNotifier { + _ActivitySession(this.event); + final NostrEvent event; + int requests = 0; + @override + SessionState build() => const SessionState(status: SessionStatus.connected); + @override + Future> fetchHistory( + NostrFilter filter, { + Duration timeout = const Duration(seconds: 8), + }) async { + requests++; + return [event]; + } +}