From a347889c57a5c99ef14e8b4cc5567733bee68635 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 15 Jul 2026 23:52:20 +0000 Subject: [PATCH] fix(nip29): audit fixes for pinned-message jump and back-fill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Jump effect: key on the note id alone and always clear the request after one attempt. Previously keyed on (id, items.list), so a message arriving mid-jump cancelled the scroll animation and could leave the request stuck (never cleared when the target wasn't loaded), blocking repeat taps. Now the animation can't be interrupted and a re-tap always re-fires. - Back-fill pinned bodies: add an id-scoped filter for the group's pinnedEventIds on the host relay. A pin can point at a message older than the 200-item timeline window, which the h-scoped timeline never returns — without this the bar shows a blank preview and can't jump. - Re-invalidate the relay-group filter when the pin list changes (keyed on the pin ids only, so roster/metadata churn doesn't re-subscribe), so the new pin ids are actually requested when 39005 arrives. --- .../loggedIn/chats/feed/ChatFeedView.kt | 10 ++--- .../ChannelFilterAssemblerSubscription.kt | 15 +++++++ .../FilterMetadataToRelayGroup.kt | 41 +++++++++++++------ 3 files changed, 49 insertions(+), 17 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatFeedView.kt index 6ce084fb51..d00d3120de 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatFeedView.kt @@ -194,18 +194,18 @@ fun ChatFeedLoaded( } } - // External jump request (pinned-message bar). Re-runs when the requested id changes or the loaded - // list shifts (so a pin that arrives after the tap still scrolls once loaded). Clears the request - // as soon as it lands, so a repeat tap on the same pin fires again. + // External jump request (pinned-message bar). Keyed on the id alone, so a message arriving mid-jump + // can't cancel the scroll animation or restart the effect. Always clears the request after one + // attempt — even when the target isn't loaded — so it never sticks and a repeat tap fires again. val jumpId = jumpToNoteId?.value - LaunchedEffect(jumpId, items.list) { + LaunchedEffect(jumpId) { if (jumpId != null) { val index = items.list.indexOfFirst { it.idHex == jumpId } if (index >= 0) { listState.animateScrollToItem(index) highlightedNoteId.value = jumpId - onJumpHandled() } + onJumpHandled() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/ChannelFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/ChannelFilterAssemblerSubscription.kt index 7c748382b6..e0154c8373 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/ChannelFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/ChannelFilterAssemblerSubscription.kt @@ -26,6 +26,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.model.Channel +import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -58,4 +59,18 @@ fun ChannelFilterAssemblerSubscription( dataSource.invalidateFilters() } } + + // Relay groups: when the kind-39005 pin list changes, re-invalidate so the id-based back-fill + // for pinned message bodies (see filterMetadataToRelayGroup) picks up the new ids. Keyed on the + // pin list alone, so unrelated roster/metadata churn doesn't force a re-subscribe. + if (channel is RelayGroupChannel) { + val metadataState by channel + .flow() + .metadata.stateFlow + .collectAsStateWithLifecycle() + val pinnedIds = (metadataState.channel as? RelayGroupChannel)?.pinnedEventIds ?: channel.pinnedEventIds + LaunchedEffect(pinnedIds) { + dataSource.invalidateFilters() + } + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMetadataToRelayGroup.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMetadataToRelayGroup.kt index 1e62bbbe40..005b066e1c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMetadataToRelayGroup.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMetadataToRelayGroup.kt @@ -49,15 +49,32 @@ private val RELAY_GROUP_METADATA_KINDS = fun filterMetadataToRelayGroup( channel: RelayGroupChannel, since: SincePerRelayMap?, -): List = - channel.relays().toSet().map { - RelayBasedFilter( - relay = it, - filter = - Filter( - kinds = RELAY_GROUP_METADATA_KINDS, - tags = mapOf("d" to listOf(channel.groupId.id)), - since = since?.get(it)?.time, - ), - ) - } +): List { + val relays = channel.relays().toSet() + val directory = + relays.map { + RelayBasedFilter( + relay = it, + filter = + Filter( + kinds = RELAY_GROUP_METADATA_KINDS, + tags = mapOf("d" to listOf(channel.groupId.id)), + since = since?.get(it)?.time, + ), + ) + } + + // Back-fill the bodies of pinned messages by id from the host relay. A pin can point at a + // message older than the 200-item timeline window, which the `h`-scoped timeline filter would + // never return — without this the pin bar shows a blank preview and can't jump to it. No `since`: + // pinned events are immutable, so we want them regardless of age. + val pinnedIds = channel.pinnedEventIds + val pins = + if (pinnedIds.isEmpty()) { + emptyList() + } else { + relays.map { RelayBasedFilter(relay = it, filter = Filter(ids = pinnedIds)) } + } + + return directory + pins +}