diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteActionSections.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteActionSections.kt
index c1bd667a34..e05844f5c5 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteActionSections.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteActionSections.kt
@@ -171,6 +171,25 @@ fun noteActionSections(
}
},
)
+ // Highlight this note/article as its source: opens the NIP-84 composer with the
+ // nostr source pre-tagged (`a` for an addressable article, else `e`) plus the author,
+ // and the passage left for the user to type or paste. Prose kinds only, and never a
+ // private rumor (a public highlight would e-tag the unsigned rumor onto relays).
+ if (!isPrivateRumor && (note.event is TextNoteEvent || note.event is LongTextNoteEvent)) {
+ add(
+ NoteAction(MaterialSymbols.FormatQuote, stringRes(R.string.highlight_action)) {
+ val author = note.author?.pubkeyHex
+ val route =
+ if (note is AddressableNote) {
+ Route.NewHighlight(sourceAddress = note.address.toValue(), author = author)
+ } else {
+ Route.NewHighlight(sourceEventId = note.idHex, author = author)
+ }
+ nav.nav(route)
+ handlers.onDismiss()
+ },
+ )
+ }
if (!isPrivateRumor) {
add(NoteAction(MaterialSymbols.Share, stringRes(R.string.quick_action_share), onClick = handlers.onShare))
}
diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/highlights/NewHighlightScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/highlights/NewHighlightScreen.kt
index 981258687a..f87a0102d6 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/highlights/NewHighlightScreen.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/highlights/NewHighlightScreen.kt
@@ -66,7 +66,6 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
-import com.vitorpamplona.amethyst.commons.ui.note.HighlightedQuote
import com.vitorpamplona.amethyst.ui.navigation.navs.Nav
import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -140,10 +139,6 @@ fun NewHighlightScreen(
onPassageChange = { postViewModel.quote = it },
)
- if (postViewModel.quote.isNotBlank()) {
- HighlightPreview(passage = postViewModel.quote)
- }
-
IconField(
symbol = MaterialSymbols.Link,
value = postViewModel.url,
@@ -248,30 +243,6 @@ private fun HighlightEditorCard(
}
}
-/** A live render of the passage exactly as it will appear in the feed — the highlighter pen. */
-@Composable
-private fun HighlightPreview(passage: String) {
- Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
- Text(
- text = stringRes(R.string.new_highlight_preview_label),
- style = MaterialTheme.typography.labelSmall,
- color = MaterialTheme.colorScheme.onSurfaceVariant,
- )
- Surface(
- shape = RoundedCornerShape(16.dp),
- color = MaterialTheme.colorScheme.surface,
- tonalElevation = 1.dp,
- modifier = Modifier.fillMaxWidth(),
- ) {
- HighlightedQuote(
- text = passage,
- highlight = passage.indices,
- modifier = Modifier.padding(16.dp),
- )
- }
- }
-}
-
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun IconField(
diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml
index 1f2231c66c..8cef251ec7 100644
--- a/amethyst/src/main/res/values/strings.xml
+++ b/amethyst/src/main/res/values/strings.xml
@@ -2771,7 +2771,7 @@
Source URL
Your note (optional)
Add your thoughts…
- Preview
+ Highlight
Copy URL to clipboard
Copy Note ID to clipboard
Add Media to Gallery
diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip84Highlights/parse/SharedHighlightParserTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip84Highlights/parse/SharedHighlightParserTest.kt
index 3f4dd8a73b..a38a47e055 100644
--- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip84Highlights/parse/SharedHighlightParserTest.kt
+++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip84Highlights/parse/SharedHighlightParserTest.kt
@@ -171,4 +171,40 @@ class SharedHighlightParserTest {
assertNull(result.prefix)
assertNull(result.suffix)
}
+
+ @Test
+ fun realWorldChromeCopyLinkWithPrefixSuffixAndEncodedHyphen() {
+ // Chrome "copy link to highlight": a short selection with prefix/suffix anchors, and an
+ // encoded hyphen (%2D "pró-Irã") inside the prefix that must not be read as a delimiter.
+ val result =
+ SharedHighlightParser.parse(
+ "\"baseados\"\n https://g1.globo.com/mundo/noticia/2026/07/28/ira-rompe-tregua-e-lanca-misseis-balisticos-contra-bases-dos-eua-no-oriente-medio.ghtml" +
+ "#:~:text=Saudita%20lan%C3%A7aram%20ataques%20em%20conjunto%20contra%20militantes%20pr%C3%B3%2DIr%C3%A3-,baseados,-no%20Iraque.",
+ )
+ assertEquals("baseados", result.quote)
+ assertEquals(
+ "https://g1.globo.com/mundo/noticia/2026/07/28/ira-rompe-tregua-e-lanca-misseis-balisticos-contra-bases-dos-eua-no-oriente-medio.ghtml",
+ result.url,
+ )
+ assertEquals("Saudita lançaram ataques em conjunto contra militantes pró-Irã", result.prefix)
+ assertEquals("no Iraque", result.suffix)
+ }
+
+ @Test
+ fun realWorldChromeCopyLinkStartOnlyWithEncodedCommas() {
+ // A long start-only fragment whose commas are encoded (%2C) so they don't split the
+ // directive, and whose apostrophe (’) inside the passage must survive quote-trimming.
+ val passage =
+ "cherish those corners. Geohot’s blog is one such corner that I rediscovered recently. " +
+ "Oh, what a joy to read something human from an actual human. His stuff makes me cry, laugh, and everything in"
+ val result =
+ SharedHighlightParser.parse(
+ "\"$passage\"\n https://dergigi.com/2026/07/28/typing/" +
+ "#:~:text=cherish%20those%20corners.%20Geohot%E2%80%99s%20blog%20is%20one%20such%20corner%20that%20I%20rediscovered%20recently.%20Oh%2C%20what%20a%20joy%20to%20read%20something%20human%20from%20an%20actual%20human.%20His%20stuff%20makes%20me%20cry%2C%20laugh%2C%20and%20everything%20in",
+ )
+ assertEquals(passage, result.quote)
+ assertEquals("https://dergigi.com/2026/07/28/typing/", result.url)
+ assertNull(result.prefix)
+ assertNull(result.suffix)
+ }
}