From 8b8a16845e5a7e455712623326128a1ba709a3a4 Mon Sep 17 00:00:00 2001 From: Tom Brow Date: Thu, 29 Oct 2026 10:37:14 -0700 Subject: [PATCH] mobile: share and test the page typing selector Channel and thread pages now watch one visibleTypingProvider, and a test checks that the user's own echoed indicator does not notify it while another user's typing does. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Tom Brow --- .../channels/channel_detail_page.dart | 10 ++--- .../channels/channel_typing_provider.dart | 12 ++++++ .../features/channels/thread_detail_page.dart | 10 ++--- .../channel_typing_provider_test.dart | 43 +++++++++++++++++++ 4 files changed, 63 insertions(+), 12 deletions(-) diff --git a/mobile/lib/features/channels/channel_detail_page.dart b/mobile/lib/features/channels/channel_detail_page.dart index 2d88a15ddc..20e5bfa96c 100644 --- a/mobile/lib/features/channels/channel_detail_page.dart +++ b/mobile/lib/features/channels/channel_detail_page.dart @@ -366,12 +366,10 @@ class _ResolvedChannelDetailPage extends HookConsumerWidget { // Ordinary channels keep thread activity inside the thread view. final typingEntries = ref .watch( - channelTypingProvider(channel.id).select( - (all) => VisibleTyping( - all, - selfPubkey: currentPubkey, - include: (e) => personalChat || e.threadHeadId == null, - ), + visibleTypingProvider( + channel.id, + selfPubkey: currentPubkey, + include: (e) => personalChat || e.threadHeadId == null, ), ) .entries; diff --git a/mobile/lib/features/channels/channel_typing_provider.dart b/mobile/lib/features/channels/channel_typing_provider.dart index 8664cbcf22..bd11d3e448 100644 --- a/mobile/lib/features/channels/channel_typing_provider.dart +++ b/mobile/lib/features/channels/channel_typing_provider.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:hooks_riverpod/misc.dart' show ProviderListenable; import '../../shared/relay/relay.dart'; @@ -56,6 +57,17 @@ class VisibleTyping { int get hashCode => Object.hashAll(entries); } +/// What a page watches to show typing in [channelId]: everyone but +/// [selfPubkey], filtered by [include]. The user's own echoed indicator +/// refreshes its expiry every few seconds, and this skips those updates. +ProviderListenable visibleTypingProvider( + String channelId, { + required String? selfPubkey, + required bool Function(TypingEntry entry) include, +}) => channelTypingProvider( + channelId, +).select((all) => VisibleTyping(all, selfPubkey: selfPubkey, include: include)); + /// Tracks who is currently typing in a specific channel. /// /// Subscribes to typing and message events so a reply ends its typing signal. diff --git a/mobile/lib/features/channels/thread_detail_page.dart b/mobile/lib/features/channels/thread_detail_page.dart index 9261724625..b13fa8447e 100644 --- a/mobile/lib/features/channels/thread_detail_page.dart +++ b/mobile/lib/features/channels/thread_detail_page.dart @@ -786,12 +786,10 @@ class ThreadDetailPage extends HookConsumerWidget { // Thread-scoped typing indicators (exclude self). final threadTyping = ref .watch( - channelTypingProvider(channelId).select( - (all) => VisibleTyping( - all, - selfPubkey: currentPubkey, - include: (e) => e.threadHeadId == threadHead.id, - ), + visibleTypingProvider( + channelId, + selfPubkey: currentPubkey, + include: (e) => e.threadHeadId == threadHead.id, ), ) .entries; diff --git a/mobile/test/features/channels/channel_typing_provider_test.dart b/mobile/test/features/channels/channel_typing_provider_test.dart index 052a9856ba..df6e0d6103 100644 --- a/mobile/test/features/channels/channel_typing_provider_test.dart +++ b/mobile/test/features/channels/channel_typing_provider_test.dart @@ -247,4 +247,47 @@ void main() { container.dispose(); }), ); + test( + 'pages ignore the user\'s own typing echo but see others', + () => fakeAsync((async) { + final relay = _Relay(); + final container = ProviderContainer( + overrides: [relaySessionProvider.overrideWith(() => relay)], + ); + final seen = >[]; + container.listen( + visibleTypingProvider( + 'chat', + selfPubkey: 'ME', + include: (e) => e.threadHeadId == null, + ), + (_, next) => seen.add([for (final e in next.entries) e.pubkey]), + ); + _settle(container, async); + void typing(String id, String pubkey) => relay.receive( + NostrEvent( + id: id, + pubkey: pubkey, + kind: 20002, + createdAt: DateTime.now().millisecondsSinceEpoch ~/ 1000, + content: '', + sig: '', + tags: [ + ['h', 'chat'], + ], + ), + ); + for (var i = 0; i < 3; i++) { + typing('self-$i', 'me'); + async.elapse(const Duration(seconds: 3)); + } + expect(container.read(channelTypingProvider('chat')), hasLength(1)); + expect(seen, isEmpty); + typing('other', 'agent'); + expect(seen, [ + ['agent'], + ]); + container.dispose(); + }), + ); }