fix(highlights): attribute to the author-marked p tag, drop alt captions
A kind:9802 highlight that mentions users before its author rendered the wrong name and a stray "published by …" caption. - HighlightEvent.author() took the first p tag regardless of role, so a highlight with leading "mention" p tags was attributed to a mention instead of the "author"-marked one. Prefer the NIP-84 author marker, falling back to the first p tag for highlights (including Amethyst's own) that omit the marker. - DisplayEntryForNote used the source note's title/subject/alt as a caption. For a kind-1 note there is no title/subject, so it fell to the NIP-31 alt tag — which clients like Jumble fill with a generic "This event was published by https://jumble.imwald.eu." line. Drop alt from the lookup and name the source by its event kind instead, kept clickable to the note. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LJh3QYonEa9hw6mJcnrmve
This commit is contained in:
@@ -40,6 +40,7 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
||||
@@ -54,7 +55,6 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNo
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo
|
||||
import com.vitorpamplona.amethyst.ui.components.ClickableUrl
|
||||
import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji
|
||||
import com.vitorpamplona.amethyst.ui.components.DisplayEvent
|
||||
import com.vitorpamplona.amethyst.ui.components.RenderUserAsClickableText
|
||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
import com.vitorpamplona.amethyst.ui.components.measureSpaceWidth
|
||||
@@ -64,6 +64,7 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.kindNameFor
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.core.firstTagValueFor
|
||||
@@ -406,7 +407,11 @@ fun DisplayEntryForNote(
|
||||
|
||||
val noteEvent = noteState.note.event as? BaseThreadedEvent ?: return
|
||||
|
||||
val description = remember(noteEvent) { noteEvent.tags.firstTagValueFor("title", "subject", "alt") }
|
||||
// A real title/subject from an article or wiki page describes the source well. `alt`
|
||||
// (NIP-31) is deliberately excluded: it's accessibility fallback text, not a caption, and
|
||||
// clients such as Jumble fill it with a generic "This event was published by …" line that
|
||||
// has nothing to do with the highlighted passage.
|
||||
val description = remember(noteEvent) { noteEvent.tags.firstTagValueFor("title", "subject") }
|
||||
|
||||
Text("-", maxLines = 1)
|
||||
|
||||
@@ -416,7 +421,13 @@ fun DisplayEntryForNote(
|
||||
onClick = { routeFor(note, accountViewModel.account)?.let { nav.nav(it) } },
|
||||
)
|
||||
} else {
|
||||
DisplayEvent(noteEvent.id, note.toNostrUri(), null, accountViewModel, nav)
|
||||
// No title to show — name the source by its event kind (e.g. "Note", "Blogs") rather
|
||||
// than a raw @note1… id, and keep it clickable through to the source event.
|
||||
val kindName = kindNameFor(LocalContext.current, noteEvent.kind)
|
||||
ClickableTextPrimary(
|
||||
text = kindName,
|
||||
onClick = { routeFor(note, accountViewModel.account)?.let { nav.nav(it) } },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+23
-1
@@ -116,7 +116,26 @@ class HighlightEvent(
|
||||
|
||||
fun inUrl() = tags.firstNotNullOfOrNull(ReferenceTag::parse)
|
||||
|
||||
fun author() = firstTaggedUserId()
|
||||
/**
|
||||
* The pubkey of the author of the highlighted content.
|
||||
*
|
||||
* NIP-84 marks that person with an `"author"` role on their `p` tag
|
||||
* (`["p", <pubkey>, <relay>, "author"]`) precisely so it can be told apart from the
|
||||
* `"mention"` p tags a highlight may also carry. Prefer the marked tag; fall back to the
|
||||
* first `p` tag for older/simpler highlights (including Amethyst's own) that tag only the
|
||||
* author and omit the role marker.
|
||||
*
|
||||
* Without this the first `p` tag wins regardless of role, so a highlight that mentions
|
||||
* other users before the author is attributed to a mention instead of the real author.
|
||||
*/
|
||||
fun author() =
|
||||
tags.firstNotNullOfOrNull { tag ->
|
||||
if (tag.size > 3 && tag[0] == PTag.TAG_NAME && tag[3] == AUTHOR_MARKER && tag[1].isNotEmpty()) {
|
||||
tag[1]
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} ?: firstTaggedUserId()
|
||||
|
||||
fun quote() = content
|
||||
|
||||
@@ -160,6 +179,9 @@ class HighlightEvent(
|
||||
companion object {
|
||||
const val KIND = 9802
|
||||
|
||||
/** NIP-84 role marker on the `p` tag that identifies the highlighted content's author. */
|
||||
private const val AUTHOR_MARKER = "author"
|
||||
|
||||
/** Any run of whitespace (spaces, tabs, newlines) — collapsed to a single space. */
|
||||
private val WHITESPACE_RUN = Regex("\\s+")
|
||||
|
||||
|
||||
+44
@@ -131,4 +131,48 @@ class HighlightEventTest {
|
||||
assertNull(bare.textQuoteSelector())
|
||||
assertNull(bare.contextOrReconstructed())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun prefersTheAuthorMarkedPTagOverEarlierMentions() {
|
||||
// A real kind:9802 highlighting a nostr note: three `mention` p tags precede the
|
||||
// `author`-marked one. author() must return the author, not the first mention.
|
||||
val highlight =
|
||||
HighlightEvent(
|
||||
id = "710dd9bcaa29618ad660db1a10fa0df12e684b161755369e14a459a98f80cc78",
|
||||
pubKey = "7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194",
|
||||
createdAt = 1772184734,
|
||||
tags =
|
||||
arrayOf(
|
||||
arrayOf("p", "0c45d7d45edb0fadda4215d36ca0d9aba0c771b85d3717764b8a128d5e443e4d", "", "mention"),
|
||||
arrayOf("p", "99bb5591c9116600f845107d31f9b59e2f7c7e09a1ff802e84f1d43da557ca64", "", "mention"),
|
||||
arrayOf("p", "4d7842051782e0d3feb034d150adc2b6bae4ee3b49786793bffa468b6f5b96b3", "", "mention"),
|
||||
arrayOf("e", "54f1c0fbc3305dd98b3ce8e63ef04e9f4b149dc8de38b4275fae49beddb794eb", "wss://nos.lol/", "source"),
|
||||
arrayOf("p", "dd664d5e4016433a8cd69f005ae1480804351789b59de5af06276de65633d319", "", "author"),
|
||||
),
|
||||
content = "Family and friendship and faith give men a sense of purpose.",
|
||||
sig = "00",
|
||||
)
|
||||
|
||||
assertEquals("dd664d5e4016433a8cd69f005ae1480804351789b59de5af06276de65633d319", highlight.author())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fallsBackToTheFirstPTagWhenNoAuthorMarkerIsPresent() {
|
||||
// Amethyst's own highlight publisher tags only the author, with no role marker.
|
||||
val highlight =
|
||||
HighlightEvent(
|
||||
id = "00",
|
||||
pubKey = "00",
|
||||
createdAt = 0,
|
||||
tags =
|
||||
arrayOf(
|
||||
arrayOf("a", "30023:eaa06714ac905aa5583860391e161edc7a815359b7c3e9b9b202c0558aefbeac:bitcoin-here-now"),
|
||||
arrayOf("p", "eaa06714ac905aa5583860391e161edc7a815359b7c3e9b9b202c0558aefbeac"),
|
||||
),
|
||||
content = "a highlight of an article",
|
||||
sig = "00",
|
||||
)
|
||||
|
||||
assertEquals("eaa06714ac905aa5583860391e161edc7a815359b7c3e9b9b202c0558aefbeac", highlight.author())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user