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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014xAESAz1H1VNjmQpMVqBXj
This commit is contained in:
Claude
2026-06-20 19:19:16 +00:00
parent 18f736abf7
commit 445a20b2f3
7 changed files with 53 additions and 2 deletions
@@ -1747,7 +1747,6 @@ class AccountViewModel(
nostrGroupId: String,
url: String,
imeta: com.vitorpamplona.quartz.nip92IMeta.IMetaTag,
caption: String? = null,
) {
val template =
eventTemplate(
@@ -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,
)
}
}
@@ -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) }
},
)
@@ -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()
@@ -35,5 +35,7 @@ class AltTag {
ensure(tag[1].isNotEmpty()) { return null }
return tag[1]
}
fun assemble(altDescriptor: String) = arrayOf(TAG_NAME, altDescriptor)
}
}
@@ -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 <T : Event> TagArrayBuilder<T>.alt(altDescriptor: String) = addUnique(AltTag.assemble(altDescriptor))
@@ -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<FileHeaderEvent>.() -> 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<FileHeaderEvent>.() -> 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) }