diff --git a/mobile/lib/features/channels/channel_messages_provider.dart b/mobile/lib/features/channels/channel_messages_provider.dart index 2adac63238..3bac9d92bc 100644 --- a/mobile/lib/features/channels/channel_messages_provider.dart +++ b/mobile/lib/features/channels/channel_messages_provider.dart @@ -624,7 +624,11 @@ class ChannelMessagesNotifier extends Notifier>> { _rotateDeletionProofTargets(missingIds); /// Snapshots cached overlay replies covered by a query for this outer root. - Set cachedThreadReplyIds(String rootId) { + Set cachedThreadReplyIds(String rootId) => + _cachedThreadReplyIdsByRoot(only: rootId)[rootId] ?? {}; + + /// Snapshots cached overlay replies for every outer root, or only [only]. + Map> _cachedThreadReplyIdsByRoot({String? only}) { final deletedIds = { for (final event in [ ..._windowStore.liveAux, @@ -636,15 +640,21 @@ class ChannelMessagesNotifier extends Notifier>> { for (final tag in event.tags) if (tag.length > 1 && tag[0] == 'e') tag[1], }; - return { - for (final entry in _localReplyRoots.entries) - if (entry.value == rootId && !deletedIds.contains(entry.key)) entry.key, - for (final event in _cachedThreadEvents) - if (event.threadReference.parentId != null && - event.threadReference.rootId == rootId && - !deletedIds.contains(event.id)) - event.id, - }; + final byRoot = >{}; + void add(String root, String id) { + if ((only == null || root == only) && !deletedIds.contains(id)) { + (byRoot[root] ??= {}).add(id); + } + } + + for (final entry in _localReplyRoots.entries) { + add(entry.value, entry.key); + } + for (final event in _cachedThreadEvents) { + final thread = event.threadReference; + if (thread.parentId != null) add(thread.rootId!, event.id); + } + return byRoot; } /// Applies explicit deletion evidence fetched for cached replies. diff --git a/mobile/lib/features/channels/channel_messages_provider/thread_summaries.dart b/mobile/lib/features/channels/channel_messages_provider/thread_summaries.dart index 7d22de342b..8e6cd2a1e8 100644 --- a/mobile/lib/features/channels/channel_messages_provider/thread_summaries.dart +++ b/mobile/lib/features/channels/channel_messages_provider/thread_summaries.dart @@ -2,6 +2,8 @@ part of '../channel_messages_provider.dart'; extension _ThreadSummaryState on ChannelMessagesNotifier { void _reconcilePageSummaries(ChannelWindowPage page, int pageVersion) { + // Nothing below changes the cached replies, so index them once per page. + final cachedReplies = _cachedThreadReplyIdsByRoot(); for (final row in page.rows) { final root = row.event.id; if ((_threadEvidenceVersions[root] ?? 0) > pageVersion) continue; @@ -10,7 +12,7 @@ extension _ThreadSummaryState on ChannelMessagesNotifier { _summaryRefreshes.cancel(root); _setThreadQueryVersion(root, pageVersion); _clearDeletionUncertainty(root, pageVersion); - if (cachedThreadReplyIds(root).length > + if ((cachedReplies[root]?.length ?? 0) > (row.thread?.descendantCount ?? 0)) { _queueOverflowSummary(root); } diff --git a/mobile/lib/shared/relay/thread_reference.dart b/mobile/lib/shared/relay/thread_reference.dart index b3ef371dce..7931887f51 100644 --- a/mobile/lib/shared/relay/thread_reference.dart +++ b/mobile/lib/shared/relay/thread_reference.dart @@ -3,8 +3,18 @@ /// The last valid 64-character ASCII-hex marker wins. IDs are lowercased for /// client lookup. Root-only events remain top-level; a reply without a valid /// root uses its parent as root. +/// +/// Timeline and thread-summary code reads this many times per event, so the +/// result is kept per tag list. Event tags are never changed after an event is +/// built. ({String? parentId, String? rootId}) parseThreadReference( List> tags, +) => _threadReferences[tags] ??= _parseThreadReference(tags); + +final _threadReferences = Expando<({String? parentId, String? rootId})>(); + +({String? parentId, String? rootId}) _parseThreadReference( + List> tags, ) { String? parentId; String? rootId;