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 <noreply@anthropic.com>
Signed-off-by: Tom Brow <tomb@block.xyz>
This commit is contained in:
Tom Brow
2026-10-29 10:37:14 -07:00
parent 5ff7541314
commit 8b8a16845e
4 changed files with 63 additions and 12 deletions

View File

@@ -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;

View File

@@ -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<VisibleTyping> 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.

View File

@@ -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;

View File

@@ -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 = <List<String>>[];
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();
}),
);
}