mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-10 16:33:27 +00:00
feat(highlights): highlight-a-note menu action; drop composer preview
- Add a "Highlight" action to the note action menu (3-dot menu + chat long-press sheet). It opens the NIP-84 composer pre-tagged with the note as source — an `a` reference for an addressable article, else an `e` reference, plus the author `p` — with the passage left for the user to type/paste. Prose kinds only (text notes, long-form), and never a private rumor (a public highlight would leak an e-tag of the unsigned rumor). This is the in-app entry point for "post with the source as an event". - Remove the live preview from the New Highlight screen per review. - Add two real-world regression tests for the shared-highlight parser: Chrome "copy link to highlight" with a prefix/suffix fragment (incl. an encoded hyphen inside the prefix) and a long start-only fragment with encoded commas. Verified with :quartz:jvmTest and :amethyst:compileFdroidDebugKotlin.
This commit is contained in:
+19
@@ -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))
|
||||
}
|
||||
|
||||
-29
@@ -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(
|
||||
|
||||
@@ -2771,7 +2771,7 @@
|
||||
<string name="new_highlight_source_label">Source URL</string>
|
||||
<string name="new_highlight_note_label">Your note (optional)</string>
|
||||
<string name="new_highlight_note_placeholder">Add your thoughts…</string>
|
||||
<string name="new_highlight_preview_label">Preview</string>
|
||||
<string name="highlight_action">Highlight</string>
|
||||
<string name="copy_url_to_clipboard">Copy URL to clipboard</string>
|
||||
<string name="copy_the_note_id_to_the_clipboard">Copy Note ID to clipboard</string>
|
||||
<string name="add_media_to_gallery">Add Media to Gallery</string>
|
||||
|
||||
+36
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user