From 448cd1c1d8dc3077c10d662a9afeaa022a725165 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 1 May 2026 07:34:12 +0000 Subject: [PATCH 1/2] feat: jump-to-parent icon on replies in Full UI mode Adds a small chevron-up button next to the timestamp on each reply. Tapping it navigates to the note this reply is replying to so users can follow conversation chains in dense threads. Hidden in Simplified and Performance UI modes. --- .../amethyst/ui/note/NoteCompose.kt | 47 +++++++++++++++++++ amethyst/src/main/res/values/strings.xml | 1 + 2 files changed, 48 insertions(+) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index b2cf50fba..3ac761a5a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -62,11 +62,13 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannelPicture import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeCommunityApprovalNeedStatus import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEdits import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent +import com.vitorpamplona.amethyst.ui.components.ClickableBox import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.layouts.GenericRepostLayout @@ -210,6 +212,7 @@ import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent import com.vitorpamplona.quartz.nip01Core.tags.geohash.geoHashOrScope import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip13Pow.strongPoWOrNull import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent @@ -1672,6 +1675,8 @@ fun FirstUserInfoRow( Expiration(baseNote) + JumpToParentReplyButton(baseNote, accountViewModel, nav) + TimeAgo(baseNote) if (moreOptions == null) { @@ -1692,6 +1697,48 @@ fun PinnedMark() { ) } +@Composable +fun JumpToParentReplyButton( + baseNote: Note, + accountViewModel: AccountViewModel, + nav: INav, +) { + if (!accountViewModel.settings.isCompleteUIMode()) return + if (baseNote.event !is BaseThreadedEvent) return + + val parentNote = + remember(baseNote) { + val noteEvent = baseNote.event as? BaseThreadedEvent ?: return@remember null + val replyingTo = noteEvent.replyingToAddressOrEvent() + val resolved = + if (replyingTo != null) { + val newNote = accountViewModel.getNoteIfExists(replyingTo) + if (newNote != null && LocalCache.getAnyChannel(newNote) == null && newNote.event?.kind != CommunityDefinitionEvent.KIND) { + newNote + } else { + baseNote.replyTo?.lastOrNull { it.event?.kind != CommunityDefinitionEvent.KIND } + } + } else { + baseNote.replyTo?.lastOrNull { it.event?.kind != CommunityDefinitionEvent.KIND } + } + resolved?.takeIf { LocalCache.getAnyChannel(it) == null } + } ?: return + + ClickableBox( + modifier = Modifier.padding(start = 5.dp).size(20.dp), + onClick = { + nav.nav { routeFor(parentNote, accountViewModel.account) } + }, + ) { + Icon( + symbol = MaterialSymbols.KeyboardArrowUp, + contentDescription = stringRes(R.string.jump_to_parent_reply), + modifier = Modifier.size(18.dp), + tint = MaterialTheme.colorScheme.placeholderText, + ) + } +} + @Composable fun Expiration(note: Note) { val event = note.event diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index a30b9e49d..7f51350ed 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1724,6 +1724,7 @@ Close all reactions to this post Reply + Jump to the note this reply is replying to Boost Or Quote Like Zap From 3d2dc7b300781689ae2d7b079803d23722e5ad33 Mon Sep 17 00:00:00 2001 From: davotoula Date: Fri, 1 May 2026 11:08:55 +0200 Subject: [PATCH 2/2] Code review: flatten parent-reply resolution in JumpToParentReplyButton --- .../amethyst/ui/note/NoteCompose.kt | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 3ac761a5a..2058b0c57 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -1709,18 +1709,12 @@ fun JumpToParentReplyButton( val parentNote = remember(baseNote) { val noteEvent = baseNote.event as? BaseThreadedEvent ?: return@remember null - val replyingTo = noteEvent.replyingToAddressOrEvent() - val resolved = - if (replyingTo != null) { - val newNote = accountViewModel.getNoteIfExists(replyingTo) - if (newNote != null && LocalCache.getAnyChannel(newNote) == null && newNote.event?.kind != CommunityDefinitionEvent.KIND) { - newNote - } else { - baseNote.replyTo?.lastOrNull { it.event?.kind != CommunityDefinitionEvent.KIND } - } - } else { - baseNote.replyTo?.lastOrNull { it.event?.kind != CommunityDefinitionEvent.KIND } - } + val direct = + noteEvent + .replyingToAddressOrEvent() + ?.let { accountViewModel.getNoteIfExists(it) } + ?.takeIf { it.event !is CommunityDefinitionEvent && LocalCache.getAnyChannel(it) == null } + val resolved = direct ?: baseNote.replyTo?.lastOrNull { it.event !is CommunityDefinitionEvent } resolved?.takeIf { LocalCache.getAnyChannel(it) == null } } ?: return