diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Highlight.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Highlight.kt index 4c94485d01..0bb73dc393 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Highlight.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Highlight.kt @@ -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 @@ -56,7 +57,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 @@ -66,6 +66,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 @@ -410,7 +411,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) @@ -420,7 +425,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) } }, + ) } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip84Highlights/HighlightEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip84Highlights/HighlightEvent.kt index 3b8c81ff3c..e968685fbe 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip84Highlights/HighlightEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip84Highlights/HighlightEvent.kt @@ -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", , , "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+") diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip84Highlights/HighlightEventTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip84Highlights/HighlightEventTest.kt index 3d53f36d3a..e22eef856a 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip84Highlights/HighlightEventTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip84Highlights/HighlightEventTest.kt @@ -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()) + } }