diff --git a/mobile/lib/features/channels/channel_management_actions.dart b/mobile/lib/features/channels/channel_management_actions.dart index dd89bf7954..aba86dfb8d 100644 --- a/mobile/lib/features/channels/channel_management_actions.dart +++ b/mobile/lib/features/channels/channel_management_actions.dart @@ -70,22 +70,31 @@ class ChannelActions { /// and every channel's recent messages. Missing membership fails closed. Future findJoinedChannel(String channelId) async { _ensureCommunityValid(); - final events = await _session.fetchHistory( - NostrFilter( - kinds: const [39000, 39002], - tags: { - '#d': [channelId], - }, - limit: 2, - ), - ); + // One query per kind: stale kind:39000 revisions from before the relay's + // d_tag backfill can linger and would crowd the roster out of a shared + // limit. + Future latest(int kind) async { + final events = await _session.fetchHistory( + NostrFilter( + kinds: [kind], + tags: { + '#d': [channelId], + }, + limit: 1, + ), + ); + return events + .where((e) => e.kind == kind && e.getTagValue('d') == channelId) + .firstOrNull; + } + + final metadataRequest = latest(39000); + final rosterRequest = latest(39002); + // Future.wait rethrows the first failure unchanged. + await Future.wait(eagerError: true, [metadataRequest, rosterRequest]); + final metadata = await metadataRequest; + final roster = await rosterRequest; _ensureCommunityValid(); - final metadata = events - .where((e) => e.kind == 39000 && e.getTagValue('d') == channelId) - .firstOrNull; - final roster = events - .where((e) => e.kind == 39002 && e.getTagValue('d') == channelId) - .firstOrNull; if (metadata == null || roster == null) return null; final members = membersFromEvent(roster); if (!members.any((m) => m.pubkey == _currentPubkey)) return null; diff --git a/mobile/test/features/channels/dm_open_latency_test.dart b/mobile/test/features/channels/dm_open_latency_test.dart index 03f0285cda..fd0348f8c9 100644 --- a/mobile/test/features/channels/dm_open_latency_test.dart +++ b/mobile/test/features/channels/dm_open_latency_test.dart @@ -140,42 +140,45 @@ class _DmSession extends RelaySessionNotifier { NostrFilter filter, { Duration timeout = const Duration(seconds: 8), }) async { - expect(filter.kinds, [39000, 39002]); expect(filter.tags, { '#d': [_dmId], }); onRead?.call(); - return [ - NostrEvent( - id: 'metadata', - pubkey: 'relay', - createdAt: 1, - kind: 39000, - tags: [ - ['d', _dmId], - ['name', 'DM'], - ['t', 'dm'], - ['private'], - ['hidden'], - ['p', self], - ['p', _peer], - ], - content: '', - sig: '', - ), - NostrEvent( - id: 'roster', - pubkey: 'relay', - createdAt: 1, - kind: 39002, - tags: [ - ['d', _dmId], - if (member) ['p', self, '', 'member'], - ['p', _peer, '', 'member'], - ], - content: '', - sig: '', - ), - ]; + // Serve like a relay: newest first, one limit across every kind. A stale + // metadata revision from before the d_tag backfill still lingers. + final events = [ + _event(39000, 3, [ + ['name', 'DM'], + ['t', 'dm'], + ['private'], + ['hidden'], + ['p', self], + ['p', _peer], + ]), + _event(39000, 2, [ + ['name', 'DM'], + ['t', 'dm'], + ['private'], + ]), + _event(39002, 1, [ + if (member) ['p', self, '', 'member'], + ['p', _peer, '', 'member'], + ]), + ].where((e) => filter.kinds.contains(e.kind)).toList(); + return events.take(filter.limit).toList(); } } + +NostrEvent _event(int kind, int createdAt, List> tags) => + NostrEvent( + id: '$kind-$createdAt', + pubkey: 'relay', + createdAt: createdAt, + kind: kind, + tags: [ + ['d', _dmId], + ...tags, + ], + content: '', + sig: '', + ); diff --git a/mobile/test/features/channels/me/solo_creation_tags_test.dart b/mobile/test/features/channels/me/solo_creation_tags_test.dart index 1e30cbb48e..e9dad011bc 100644 --- a/mobile/test/features/channels/me/solo_creation_tags_test.dart +++ b/mobile/test/features/channels/me/solo_creation_tags_test.dart @@ -46,7 +46,7 @@ class _Relay extends RelaySessionNotifier { Future> fetchHistory( NostrFilter filter, { Duration timeout = const Duration(seconds: 8), - }) async => filter.kinds.contains(39000) ? existing : []; + }) async => existing.where((e) => filter.kinds.contains(e.kind)).toList(); @override Future publish( diff --git a/mobile/test/features/channels/solo_creation_latency_test.dart b/mobile/test/features/channels/solo_creation_latency_test.dart index fda46d9b3f..679c93a369 100644 --- a/mobile/test/features/channels/solo_creation_latency_test.dart +++ b/mobile/test/features/channels/solo_creation_latency_test.dart @@ -144,12 +144,14 @@ class _SoloSession extends RelaySessionNotifier { NostrFilter filter, { Duration timeout = const Duration(seconds: 8), }) async { - reads++; - expect(filter.kinds, [39000, 39002]); + // Each lookup reads metadata and roster separately. + final kind = filter.kinds.single; + if (kind == 39000) reads++; + expect(kind, anyOf(39000, 39002)); expect(filter.tags, { '#d': ['solo-id'], }); - expect(filter.limit, 2); + expect(filter.limit, 1); onRead?.call(); if (failure != null) throw failure!; if (!exists) return []; @@ -182,6 +184,6 @@ class _SoloSession extends RelaySessionNotifier { content: '', sig: '', ), - ]; + ].where((e) => e.kind == kind).toList(); } }