diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Chat.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Chat.kt index c516dc6b9a..a95b8763fe 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Chat.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Chat.kt @@ -41,10 +41,8 @@ import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.placeholderText -import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags import com.vitorpamplona.quartz.nip10Notes.BaseNoteEvent -import com.vitorpamplona.quartz.nip92IMeta.imetas @Composable fun RenderChat( @@ -66,7 +64,7 @@ fun RenderChat( // from the content so those attachments render — symmetric to ChannelChat.imageMessage, which // appends the URL on send. Encrypted-blob key/nonce are already registered by URL, so the shared // pipeline fetches and decrypts them transparently. - val displayContent = remember(noteEvent) { appendMissingImetaUrls(noteEvent) } + val displayContent = remember(noteEvent) { appendMissingImetaUrls(noteEvent.content, noteEvent) } // A boosted note inside a zap/nutzap/onchain activity card is always shown as a // compact 2-line preview, even when the logged-in user is only a zap-split @@ -129,16 +127,3 @@ fun RenderChat( } } } - -/** - * Returns [event]'s content with every NIP-92 `imeta` URL that is not already present appended on its - * own line, so an attachment carried only as an `imeta` tag (empty/incomplete content) still renders. - * Returns the original content unchanged when every imeta URL is already inline (the common case), so - * normal messages are untouched. - */ -private fun appendMissingImetaUrls(event: Event): String { - val content = event.content - val missing = event.imetas().map { it.url }.filter { it.isNotBlank() && !content.contains(it) } - if (missing.isEmpty()) return content - return (listOf(content) + missing).filter { it.isNotBlank() }.joinToString("\n") -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ImetaContent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ImetaContent.kt new file mode 100644 index 0000000000..8ddd7facc2 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ImetaContent.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.note.types + +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip92IMeta.imetas + +/** + * When [content] is blank, returns [event]'s NIP-92 `imeta` URLs joined one per line so an attachment + * carried only as an `imeta` tag (empty content) still renders. The shared media renderer only shows + * media whose URL appears in the text, but some NIP-C7/Concord clients (e.g. Ditto/Soapbox Armada) + * attach an image purely as an `imeta` tag and leave the content empty — symmetric to + * [com.vitorpamplona.quartz.concord.cord03Channels.ChannelChat.imageMessage], which appends the URL + * on send. Encrypted-blob key/nonce are already registered by URL, so the shared pipeline fetches and + * decrypts them transparently. + * + * Returns [content] unchanged whenever it is non-blank (the common case) or [event] is null / carries + * no imeta URLs, so any message that already has text is untouched. + */ +fun appendMissingImetaUrls( + content: String, + event: Event?, +): String { + if (event == null || content.isNotBlank()) return content + val urls = event.imetas().map { it.url }.filter { it.isNotBlank() } + if (urls.isEmpty()) return content + return urls.joinToString("\n") +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderRegularTextNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderRegularTextNote.kt index 25fb82f0a0..d9d3c0ba5e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderRegularTextNote.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderRegularTextNote.kt @@ -35,6 +35,7 @@ import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.LoadDecryptedContentOrNull +import com.vitorpamplona.amethyst.ui.note.types.appendMissingImetaUrls import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.jumboEmojiCount import com.vitorpamplona.amethyst.ui.stringRes @@ -76,8 +77,15 @@ fun RenderRegularTextNote( } else { val tags = remember(note.event) { note.event?.tags?.toImmutableListOfLists() ?: EmptyTagList } + // Some NIP-C7/Concord clients (e.g. Ditto/Soapbox Armada) attach an image purely as a + // NIP-92 `imeta` tag and leave the content empty. The shared renderer only shows media + // whose URL appears in the text, so append any imeta URL missing from the (decrypted) + // content — encrypted-blob key/nonce are already registered by URL, so the shared + // pipeline fetches and decrypts them transparently. + val displayContent = remember(note.event, eventContent) { appendMissingImetaUrls(eventContent, note.event) } + TranslatableRichTextViewer( - content = eventContent, + content = displayContent, canPreview = canPreview, quotesLeft = if (innerQuote) 0 else 1, modifier = Modifier,