From e684cb02a3d2e0ff3bba5860f7cf765c8232a687 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 14 Jul 2026 14:31:23 +0000 Subject: [PATCH] feat(ui): tap explainers for the PoW, private-rumor and expiration markers Following the OTS pill's pattern, tapping now explains what the marker means: PoW describes the mining difficulty as an anti-spam stamp (pow_info_description), the private-rumor lock explains the unsigned private delivery and its deniability (private_rumor_info_*), and the expiration pill shows the request to delete plus the exact expiry date/time (expiration_info_description). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01AgEpNtwnetPhETXQSdq4b5 --- .../amethyst/ui/note/NoteCompose.kt | 35 +++++++++++++++---- .../amethyst/ui/note/elements/DisplayPoW.kt | 20 ++++++++--- .../loggedIn/chats/feed/ChatMessageCompose.kt | 2 +- .../loggedIn/threadview/ThreadFeedView.kt | 4 +-- amethyst/src/main/res/values/strings.xml | 4 +++ 5 files changed, 51 insertions(+), 14 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 56c40f13d6..b1810aac12 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -355,6 +355,8 @@ import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.withContext +import java.text.SimpleDateFormat +import java.util.Date @Composable fun NoteCompose( @@ -1761,7 +1763,10 @@ fun SecondUserInfoRow( } @Composable -fun DisplayExpiration(expirationDate: Long) { +fun DisplayExpiration( + expirationDate: Long, + accountViewModel: AccountViewModel, +) { val context = LocalContext.current // Beyond a year timeAheadNoDot switches to a full date, which is too wide // for the pill: clamp it. @@ -1775,6 +1780,13 @@ fun DisplayExpiration(expirationDate: Long) { symbol = MaterialSymbols.Timer, text = text, contentDescription = stringRes(R.string.expiration_date_label), + onClick = { + accountViewModel.toastManager.toast( + R.string.expiration_date_label, + R.string.expiration_info_description, + SimpleDateFormat.getDateTimeInstance().format(Date(expirationDate * 1000)), + ) + }, ) } @@ -1886,13 +1898,13 @@ fun FirstUserInfoRow( val pow = remember(noteEvent) { noteEvent?.strongPoWOrNull() } if (pow != null) { - DisplayPoW(pow) + DisplayPoW(pow, accountViewModel) } - Expiration(baseNote) + Expiration(baseNote, accountViewModel) if (baseNote.isPrivateRumor()) { - PrivateRumorMark() + PrivateRumorMark(accountViewModel) } if (isPinned) { @@ -1924,10 +1936,16 @@ fun PinnedMark() { } @Composable -fun PrivateRumorMark() { +fun PrivateRumorMark(accountViewModel: AccountViewModel) { QuietMark( symbol = MaterialSymbols.Lock, contentDescription = stringRes(R.string.private_rumor_mark), + onClick = { + accountViewModel.toastManager.toast( + R.string.private_rumor_info_title, + R.string.private_rumor_info_description, + ) + }, ) } @@ -1968,12 +1986,15 @@ fun JumpToParentReplyButton( } @Composable -fun Expiration(note: Note) { +fun Expiration( + note: Note, + accountViewModel: AccountViewModel, +) { val event = note.event if (event != null) { val expires = remember(event) { event.expiration() } if (expires != null) { - DisplayExpiration(expires) + DisplayExpiration(expires, accountViewModel) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayPoW.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayPoW.kt index e6fc7b772e..681975f006 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayPoW.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayPoW.kt @@ -25,6 +25,8 @@ import androidx.compose.ui.tooling.preview.Preview import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.ui.note.HeaderPill +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn @@ -32,20 +34,30 @@ import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn @Preview fun DisplayPoWPreview() { ThemeComparisonColumn( - toPreview = { DisplayPoW(pow = 24) }, + toPreview = { DisplayPoW(pow = 24, accountViewModel = mockAccountViewModel()) }, ) } /** * Compact pill showing the proof of work a received note carries: a gear plus - * the difficulty in leading zero bits. Sits inline in note headers, so it - * stays at text height. + * the difficulty in leading zero bits. Tapping it explains what the number + * means. Sits inline in note headers, so it stays at text height. */ @Composable -fun DisplayPoW(pow: Int) { +fun DisplayPoW( + pow: Int, + accountViewModel: AccountViewModel, +) { HeaderPill( symbol = MaterialSymbols.Manufacturing, text = pow.toString(), contentDescription = stringRes(R.string.pow_settings_title), + onClick = { + accountViewModel.toastManager.toast( + R.string.pow_settings_title, + R.string.pow_info_description, + pow.toString(), + ) + }, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt index 30ca93806a..f6b09bb796 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt @@ -289,7 +289,7 @@ fun NormalChatNote( val pow = remember(note) { note.event?.strongPoWOrNull() } if (pow != null) { Spacer(StdHorzSpacer) - DisplayPoW(pow) + DisplayPoW(pow, accountViewModel) } } else { DisplayDraftChat() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt index d3a2b8a1dd..51993a25a9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt @@ -740,7 +740,7 @@ private fun FullBleedNoteCompose( CheckAndDisplayEditStatus(editState) - Expiration(baseNote) + Expiration(baseNote, accountViewModel) // The dotted TimeAgo carries its own " • " separator and the // options button has slack around its icon: keep the pair unspaced. @@ -781,7 +781,7 @@ private fun FullBleedNoteCompose( val pow = remember(noteEvent) { noteEvent.strongPoWOrNull() } if (pow != null) { - DisplayPoW(pow) + DisplayPoW(pow, accountViewModel) } if (isDraft) { diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index a9ed842bdd..7a864a29f1 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1234,6 +1234,8 @@ is a public bookmark here is a private bookmark here Private — only visible to tagged participants + Private Note + This note was delivered privately and is not signed. Only the tagged participants can see it, and nobody can prove to outsiders who wrote it. Make private: gift-wrap the note to the notified users only Make public Replies to a private note always stay private @@ -2822,6 +2824,7 @@ Add expiration date Remove expiration date Expiration Date + The author asked relays to delete this note after %1$s. It will disappear from feeds once it expires. Post will be hidden by clients after this date (NIP-40) Select expiration date and time Expires in %1$s @@ -3776,6 +3779,7 @@ Added at the end of the message when opening a new post, reply, quote, or article. Leave empty to disable. Your signature Proof of Work + The author spent computing power to mine this note to difficulty %1$s — a proof-of-work stamp that makes spamming expensive. Difficulty Mines a NIP-13 proof of work into your posts before publishing so relays and readers can weigh them against spam. Higher values take exponentially longer to mine. Posts publish in the background once mined. Off