From 445a20b2f3fe0497cdd4307b5e6c89e80a9c7678 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Jun 2026 19:19:16 +0000 Subject: [PATCH] fix: preserve user-provided media descriptions for accessibility The blanket NIP-31 alt removal also dropped genuine accessibility descriptions (image descriptions for the blind) on a few media paths where the user's caption was only stored in the event-level alt tag. Restore them via the proper, non-deprecated fields: - NIP-94 FileHeaderEvent (kind 1063) and NIP-17 encrypted file headers (kind 15): write the `alt` tag only when the user actually provided a caption (the NIP-94 accessibility description), never the old boilerplate fallback. - MIP-04 encrypted group media (kind 9): route the caption into the imeta `alt` field via buildMip04IMetaTag instead of an event-level alt tag. Re-adds the narrowly-scoped TagArrayBuilder.alt() / AltTag.assemble() write helpers (documented as accessibility-only, not for deprecated NIP-31 boilerplate). Boilerplate alt tags on all other event kinds remain removed. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_014xAESAz1H1VNjmQpMVqBXj --- .../ui/screen/loggedIn/AccountViewModel.kt | 1 - .../marmotGroup/send/MarmotFileSender.kt | 2 +- .../privateDM/send/upload/ChatFileSender.kt | 6 ++++ .../mip04EncryptedMedia/Mip04IMetaTag.kt | 4 +++ .../vitorpamplona/quartz/nip31Alts/AltTag.kt | 2 ++ .../quartz/nip31Alts/TagArrayBuilderExt.kt | 33 +++++++++++++++++++ .../nip94FileMetadata/FileHeaderEvent.kt | 7 ++++ 7 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip31Alts/TagArrayBuilderExt.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index ca7156d8ec..6a813c66c9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -1747,7 +1747,6 @@ class AccountViewModel( nostrGroupId: String, url: String, imeta: com.vitorpamplona.quartz.nip92IMeta.IMetaTag, - caption: String? = null, ) { val template = eventTemplate( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotFileSender.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotFileSender.kt index b16d142265..bf512b716d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotFileSender.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotFileSender.kt @@ -44,13 +44,13 @@ class MarmotFileSender( dimensions = upload.dimensions, blurhash = upload.blurhash, thumbhash = upload.thumbhash, + alt = upload.caption, ) accountViewModel.sendMarmotGroupMediaMessage( nostrGroupId = nostrGroupId, url = upload.url, imeta = imeta, - caption = upload.caption, ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileSender.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileSender.kt index a3e90d44ed..531dec2695 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileSender.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileSender.kt @@ -28,6 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.references.references import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent +import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning import com.vitorpamplona.quartz.nip92IMeta.imetas import com.vitorpamplona.quartz.utils.ciphers.AESGCM @@ -65,6 +66,11 @@ class ChatFileSender( blurhash = result.fileHeader.blurHash?.blurhash, thumbhash = result.fileHeader.thumbHash?.thumbhash, ) { + // NIP-94 accessibility description of the file (kept; the deprecated + // generic NIP-31 boilerplate alt is not written). + if (!caption.isNullOrEmpty()) { + alt(caption) + } contentWarningReason?.let { contentWarning(it) } }, ) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/mip04EncryptedMedia/Mip04IMetaTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/mip04EncryptedMedia/Mip04IMetaTag.kt index 7f78d36dd1..c07fffca78 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/mip04EncryptedMedia/Mip04IMetaTag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/mip04EncryptedMedia/Mip04IMetaTag.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.quartz.marmot.mip04EncryptedMedia import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.core.toHexKey +import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.nip92IMeta.IMetaTag import com.vitorpamplona.quartz.nip92IMeta.IMetaTagBuilder import com.vitorpamplona.quartz.utils.Log @@ -167,6 +168,7 @@ fun buildMip04IMetaTag( dimensions: String? = null, blurhash: String? = null, thumbhash: String? = null, + alt: String? = null, ): IMetaTag = IMetaTagBuilder(url) .apply { @@ -178,4 +180,6 @@ fun buildMip04IMetaTag( dimensions?.let { add(Mip04Fields.DIMENSIONS, it) } blurhash?.let { add(Mip04Fields.BLURHASH, it) } thumbhash?.let { add(Mip04Fields.THUMBHASH, it) } + // imeta accessibility description (NIP-92/94 "alt"); only when provided. + alt?.ifBlank { null }?.let { add(AltTag.TAG_NAME, it) } }.build() diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip31Alts/AltTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip31Alts/AltTag.kt index 5b19459769..d2d75fda14 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip31Alts/AltTag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip31Alts/AltTag.kt @@ -35,5 +35,7 @@ class AltTag { ensure(tag[1].isNotEmpty()) { return null } return tag[1] } + + fun assemble(altDescriptor: String) = arrayOf(TAG_NAME, altDescriptor) } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip31Alts/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip31Alts/TagArrayBuilderExt.kt new file mode 100644 index 0000000000..10287e42bc --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip31Alts/TagArrayBuilderExt.kt @@ -0,0 +1,33 @@ +/* + * 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.quartz.nip31Alts + +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder + +/** + * Writes an `alt` tag. The generic NIP-31 "alt" client-hint is deprecated, so do + * NOT use this to add a boilerplate description of the event kind. It remains only + * for file-metadata events (e.g. NIP-94 kind 1063, NIP-17 encrypted file headers) + * where the `alt` tag is the accessibility description of the file itself, and only + * when the user actually provided one. + */ +fun TagArrayBuilder.alt(altDescriptor: String) = addUnique(AltTag.assemble(altDescriptor)) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip94FileMetadata/FileHeaderEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip94FileMetadata/FileHeaderEvent.kt index 1c1e3c73a0..5ff71a5190 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip94FileMetadata/FileHeaderEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip94FileMetadata/FileHeaderEvent.kt @@ -26,6 +26,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.core.any import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.nip50Search.SearchableEvent import com.vitorpamplona.quartz.nip94FileMetadata.tags.BlurhashTag import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag @@ -99,6 +100,9 @@ class FileHeaderEvent( initializer: TagArrayBuilder.() -> Unit = {}, ) = eventTemplate(KIND, caption ?: "", createdAt) { url(url) + // NIP-94 accessibility description of the file (kept; the deprecated + // generic NIP-31 boilerplate alt is not written). + caption?.ifBlank { null }?.let { alt(it) } initializer() } @@ -118,6 +122,9 @@ class FileHeaderEvent( initializer: TagArrayBuilder.() -> Unit = {}, ) = eventTemplate(KIND, caption ?: "", createdAt) { url(url) + // NIP-94 accessibility description of the file (kept; the deprecated + // generic NIP-31 boilerplate alt is not written). + caption?.ifBlank { null }?.let { alt(it) } hash?.let { hash(it) } size?.let { fileSize(it) }