From c7f5ab83b010e759d82fcc1959ee0b3400302298 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 16 Jun 2026 00:26:13 +0000 Subject: [PATCH] feat: pre-load reply threads from the feed When a reply is visible in NoteCompose, eagerly subscribe to its thread root (filter on the root's e/a tag, like the thread screen) so opening the conversation finds it already loaded. The observer resolves the root and keys on its id, so sibling replies share one subscription and root notes are skipped. --- .../amethyst/ui/note/types/Text.kt | 5 ++++ .../ThreadFilterAssemblerSubscription.kt | 29 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt index 874936fe20..c082b39990 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt @@ -48,6 +48,7 @@ import com.vitorpamplona.amethyst.ui.note.ReplyNoteComposition import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags import com.vitorpamplona.amethyst.ui.note.nip22Comments.DisplayExternalId import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.PreloadThreadForReply import com.vitorpamplona.amethyst.ui.theme.HalfVertSpacer import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.placeholderText @@ -82,6 +83,10 @@ fun RenderTextEvent( val noteEvent = note.event ?: return if (unPackReply != ReplyRenderType.NONE) { + // Eagerly pull the rest of this reply's thread while it's on screen, so opening + // the conversation finds it already loaded. No-op when the note is a root itself. + PreloadThreadForReply(note, accountViewModel) + val canShowReply by remember(note) { derivedStateOf { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/ThreadFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/ThreadFilterAssemblerSubscription.kt index f30c0221c8..75e206a528 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/ThreadFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/ThreadFilterAssemblerSubscription.kt @@ -22,8 +22,11 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import com.vitorpamplona.amethyst.commons.model.ThreadAssembler import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.core.HexKey @@ -52,3 +55,29 @@ fun ThreadFilterAssemblerSubscription( LifecycleAwareKeyDataSourceSubscription(state, filterAssembler) } + +/** + * Eagerly pre-loads the whole thread of a reply that is visible in a feed. + * + * When a reply shows up in `NoteCompose`, this resolves the thread root and opens + * the same root subscription the thread screen uses (a filter on the root's `e`/`a` + * tag, covering NIP-10 and NIP-22 event/addressable roots), so tapping into the + * conversation finds it already loaded. Keying on the resolved root id means every + * visible reply that shares a root collapses onto a single subscription, and a note + * that is itself a root (no parent) is skipped — nothing to pre-load. + */ +@Composable +fun PreloadThreadForReply( + note: Note, + accountViewModel: AccountViewModel, +) { + val rootId = + remember(note) { + val root = ThreadAssembler(LocalCache).findRoot(note.idHex) + if (root != null && root != note) root.idHex else null + } + + if (rootId != null) { + ThreadFilterAssemblerSubscription(rootId, accountViewModel) + } +}