From 718e2cba0bdf1d8a8f437ea08d2c5b73217ba58d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 13 Jul 2026 01:32:32 +0000 Subject: [PATCH 01/13] feat(ui): move PoW, OTS and location pills to the note header's first line Restyle DisplayPoW, DisplayOts and DisplayLocation as compact squared chips via a new shared HeaderPill composable, matching the relay information pills in the NIP-29 chat headers (which now reuse the same composable). Move the three pills from SecondUserInfoRow to FirstUserInfoRow in NoteCompose, and shrink the secondary markers on that line (boosted, hashtag, community, draft, edited, expiration, pinned, private-rumor) to 12sp/smaller icons since the row now carries more information. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01AgEpNtwnetPhETXQSdq4b5 --- .../amethyst/ui/note/NoteCompose.kt | 41 +++++----- .../amethyst/ui/note/elements/BoostedMark.kt | 2 + .../ui/note/elements/DisplayCommunity.kt | 3 + .../ui/note/elements/DisplayEditStatus.kt | 2 + .../ui/note/elements/DisplayHashtags.kt | 3 + .../ui/note/elements/DisplayLocation.kt | 36 +++------ .../amethyst/ui/note/elements/DisplayOts.kt | 53 ++++++------- .../amethyst/ui/note/elements/DisplayPoW.kt | 46 ++--------- .../amethyst/ui/note/elements/HeaderPill.kt | 79 +++++++++++++++++++ .../chats/rooms/ChatroomHeaderCompose.kt | 37 ++------- 10 files changed, 155 insertions(+), 147 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/HeaderPill.kt 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 9939907e47..6286a1a484 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 @@ -761,7 +761,6 @@ fun InnerNoteWithReactions( if (showSecondRow) { SecondUserInfoRow( baseNote, - editState, accountViewModel, nav, ) @@ -883,7 +882,6 @@ fun NoteBody( if (showSecondRow) { SecondUserInfoRow( baseNote, - editState, accountViewModel, nav, ) @@ -1737,7 +1735,6 @@ fun ReplyNoteComposition( @Composable fun SecondUserInfoRow( note: Note, - editState: State>, accountViewModel: AccountViewModel, nav: INav, ) { @@ -1756,25 +1753,11 @@ fun SecondUserInfoRow( } } - val geo = remember(noteEvent) { noteEvent.geoHashOrScope() } - if (geo != null) { - Spacer(StdHorzSpacer) - DisplayLocation(geo, accountViewModel, nav) - } - val baseReward = remember(noteEvent) { noteEvent.bountyBaseReward()?.let { Reward(it) } } if (baseReward != null) { Spacer(StdHorzSpacer) DisplayReward(baseReward, note, accountViewModel, nav) } - - val pow = remember(noteEvent) { noteEvent.strongPoWOrNull() } - if (pow != null) { - Spacer(StdHorzSpacer) - DisplayPoW(pow) - } - - DisplayOtsIfInOriginal(note, editState, accountViewModel) } } @@ -1786,13 +1769,14 @@ fun DisplayExpiration(expirationDate: Long) { Icon( symbol = MaterialSymbols.Timer, contentDescription = stringRes(R.string.expiration_date_label), - modifier = Modifier.padding(start = 5.dp).size(15.dp), + modifier = Modifier.padding(start = 5.dp).size(12.dp), tint = MaterialTheme.colorScheme.placeholderText, ) val context = LocalContext.current Text( text = timeAheadNoDot(expirationDate, context), color = MaterialTheme.colorScheme.placeholderText, + fontSize = Font12SP, maxLines = 1, modifier = Modifier.padding(start = 3.dp), ) @@ -1819,6 +1803,7 @@ fun DisplayDraft() { Text( "Draft", fontWeight = FontWeight.Bold, + fontSize = Font12SP, color = MaterialTheme.colorScheme.placeholderText, maxLines = 1, modifier = HalfStartPadding, @@ -1919,6 +1904,22 @@ fun FirstUserInfoRow( PinnedMark() } + val noteEvent = baseNote.event + + val geo = remember(noteEvent) { noteEvent?.geoHashOrScope() } + if (geo != null) { + Spacer(StdHorzSpacer) + DisplayLocation(geo, accountViewModel, nav) + } + + val pow = remember(noteEvent) { noteEvent?.strongPoWOrNull() } + if (pow != null) { + Spacer(StdHorzSpacer) + DisplayPoW(pow) + } + + DisplayOtsIfInOriginal(baseNote, editState, accountViewModel) + Expiration(baseNote) JumpToParentReplyButton(baseNote, accountViewModel, nav) @@ -1938,7 +1939,7 @@ fun PinnedMark() { Icon( symbol = MaterialSymbols.PushPin, contentDescription = stringRes(R.string.pinned_notes), - modifier = Modifier.padding(start = 5.dp).size(16.dp), + modifier = Modifier.padding(start = 5.dp).size(14.dp), tint = MaterialTheme.colorScheme.placeholderText, ) } @@ -1948,7 +1949,7 @@ fun PrivateRumorMark() { Icon( symbol = MaterialSymbols.Lock, contentDescription = stringRes(R.string.private_rumor_mark), - modifier = Modifier.padding(start = 5.dp).size(16.dp), + modifier = Modifier.padding(start = 5.dp).size(14.dp), tint = MaterialTheme.colorScheme.placeholderText, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/BoostedMark.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/BoostedMark.kt index b91bdbeb81..d214b3391c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/BoostedMark.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/BoostedMark.kt @@ -26,6 +26,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.text.font.FontWeight import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.Font12SP import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding import com.vitorpamplona.amethyst.ui.theme.placeholderText @@ -34,6 +35,7 @@ fun BoostedMark() { Text( stringRes(id = R.string.boosted), fontWeight = FontWeight.Bold, + fontSize = Font12SP, color = MaterialTheme.colorScheme.placeholderText, maxLines = 1, modifier = HalfStartPadding, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayCommunity.kt index 3e32487fc8..404688cc03 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayCommunity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayCommunity.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.note.elements import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -34,6 +35,7 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNo import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.theme.Font12SP import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip72ModCommunities.communityAddress @@ -64,6 +66,7 @@ private fun DisplayCommunity( ClickableTextColor( label, + style = LocalTextStyle.current.copy(fontSize = Font12SP), linkColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.52f), overflow = TextOverflow.Ellipsis, maxLines = 1, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayEditStatus.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayEditStatus.kt index 8ebe574745..ee31e07dc3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayEditStatus.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayEditStatus.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.text.font.FontWeight import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.note.types.EditState import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.Font12SP import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding import com.vitorpamplona.amethyst.ui.theme.placeholderText @@ -50,6 +51,7 @@ fun DisplayEditStatus(editState: EditState) { LocalTextStyle.current.copy( color = MaterialTheme.colorScheme.placeholderText, fontWeight = FontWeight.Bold, + fontSize = Font12SP, ), maxLines = 1, modifier = HalfStartPadding.clickable { editState.nextModification() }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayHashtags.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayHashtags.kt index cc4f929afb..b9f73c3367 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayHashtags.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayHashtags.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.note.elements import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -38,6 +39,7 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.theme.Font12SP import com.vitorpamplona.quartz.nip01Core.tags.hashtags.firstIsTaggedHashes import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip73ExternalIds.topics.HashtagId @@ -82,6 +84,7 @@ private fun DisplayTagList( ) { ClickableTextColor( "#$firstTag", + style = LocalTextStyle.current.copy(fontSize = Font12SP), linkColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.52f), overflow = TextOverflow.Ellipsis, maxLines = 1, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayLocation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayLocation.kt index e6d942c620..cf240cb7f2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayLocation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayLocation.kt @@ -20,20 +20,17 @@ */ package com.vitorpamplona.amethyst.ui.note.elements -import androidx.compose.material3.LocalTextStyle -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.text.LinkAnnotation -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.withLink +import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.note.creators.location.LoadCityName import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.theme.Font14SP +/** + * Compact pill showing the geohash a note is scoped to, resolved to a city + * name. Tapping it opens the geohash feed. Sits inline in note headers. + */ @Composable fun DisplayLocation( geohashStr: String, @@ -41,25 +38,10 @@ fun DisplayLocation( nav: INav, ) { LoadCityName(geohashStr) { cityName -> - Text( - text = - buildAnnotatedString { - withLink( - LinkAnnotation.Clickable("cityname") { nav.nav(Route.Geohash(geohashStr)) }, - ) { - append(cityName) - } - }, - style = - LocalTextStyle.current.copy( - color = - MaterialTheme.colorScheme.primary.copy( - alpha = 0.52f, - ), - fontSize = Font14SP, - fontWeight = FontWeight.Bold, - ), - maxLines = 1, + HeaderPill( + symbol = MaterialSymbols.LocationOn, + text = cityName, + onClick = { nav.nav(Route.Geohash(geohashStr)) }, ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt index ec5b9bb06b..fdf5f6890c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt @@ -20,27 +20,27 @@ */ package com.vitorpamplona.amethyst.ui.note.elements -import androidx.compose.material3.LocalTextStyle -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.font.FontWeight import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.ui.components.buildLinkString +import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.note.LoadOts import com.vitorpamplona.amethyst.ui.note.timeAgoNoDot import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.amethyst.ui.theme.Font14SP -import com.vitorpamplona.amethyst.ui.theme.lessImportantLink +import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding import java.text.SimpleDateFormat import java.util.Date +/** + * Compact pill showing the note's NIP-03 OpenTimestamps proof: how long ago + * the note is proven to have existed. Tapping it explains the proof and shows + * the attested date. Sits inline in note headers. + */ @Composable fun DisplayOts( note: Note, @@ -60,31 +60,26 @@ fun DisplayOts( ) } - Text( - text = - buildLinkString(stringRes(R.string.existed_since, timeStr)) { - accountViewModel.toastManager.toast( - R.string.ots_info_title, - R.string.ots_info_description, - SimpleDateFormat.getDateTimeInstance().format(Date(unixtimestamp * 1000)), - ) - }, - style = - LocalTextStyle.current.copy( - color = MaterialTheme.colorScheme.lessImportantLink, - fontSize = Font14SP, - fontWeight = FontWeight.Bold, - ), - maxLines = 1, + HeaderPill( + symbol = MaterialSymbols.CheckCircle, + text = stringRes(R.string.existed_since, timeStr), + modifier = HalfStartPadding, + contentDescription = stringRes(R.string.ots_info_title), + onClick = { + accountViewModel.toastManager.toast( + R.string.ots_info_title, + R.string.ots_info_description, + SimpleDateFormat.getDateTimeInstance().format(Date(unixtimestamp * 1000)), + ) + }, ) }, whenPending = { - Text( - stringRes(id = R.string.timestamp_pending_short), - color = MaterialTheme.colorScheme.lessImportantLink, - fontSize = Font14SP, - fontWeight = FontWeight.Bold, - maxLines = 1, + HeaderPill( + symbol = MaterialSymbols.Schedule, + text = stringRes(id = R.string.timestamp_pending_short), + modifier = HalfStartPadding, + contentDescription = stringRes(R.string.ots_info_title), ) }, ) 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 e257452632..64aad9648a 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 @@ -20,24 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.note.elements -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn @@ -51,34 +36,15 @@ fun DisplayPoWPreview() { } /** - * Compact pill showing the proof of work a received note carries: a bolt plus + * 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. */ @Composable fun DisplayPoW(pow: Int) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(2.dp), - modifier = - Modifier - .clip(RoundedCornerShape(50)) - .background(MaterialTheme.colorScheme.secondaryContainer) - .padding(horizontal = 5.dp, vertical = 1.dp), - ) { - Icon( - symbol = MaterialSymbols.Manufacturing, - contentDescription = stringRes(R.string.pow_settings_title), - tint = MaterialTheme.colorScheme.onSecondaryContainer, - modifier = Modifier.size(10.dp), - ) - Text( - text = pow.toString(), - color = MaterialTheme.colorScheme.onSecondaryContainer, - fontSize = 10.sp, - lineHeight = 10.sp, - fontWeight = FontWeight.Bold, - maxLines = 1, - ) - } + HeaderPill( + symbol = MaterialSymbols.Manufacturing, + text = pow.toString(), + contentDescription = stringRes(R.string.pow_settings_title), + ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/HeaderPill.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/HeaderPill.kt new file mode 100644 index 0000000000..2e70a1f398 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/HeaderPill.kt @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.note.elements + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.icons.symbols.Icon +import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol + +/** + * Compact squared chip for note-header metadata (PoW, OTS, location, relay + * hosts, ...). Matches the relay information pills used in the NIP-29 chat + * headers so all header badges share one look: a small icon plus a short + * label on a secondary-container surface with slightly rounded corners. + */ +@Composable +fun HeaderPill( + symbol: MaterialSymbol, + text: String, + modifier: Modifier = Modifier, + contentDescription: String? = null, + onClick: (() -> Unit)? = null, +) { + Surface( + shape = RoundedCornerShape(6.dp), + color = MaterialTheme.colorScheme.secondaryContainer, + modifier = if (onClick != null) modifier.clickable(onClick = onClick) else modifier, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(3.dp), + modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp), + ) { + Icon( + symbol = symbol, + contentDescription = contentDescription, + tint = MaterialTheme.colorScheme.onSecondaryContainer, + modifier = Modifier.size(11.dp), + ) + Text( + text = text, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSecondaryContainer, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt index 442e49cf2d..976cbe3032 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt @@ -20,20 +20,14 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -78,6 +72,7 @@ import com.vitorpamplona.amethyst.ui.note.LoadDecryptedContentOrNull import com.vitorpamplona.amethyst.ui.note.LoadPublicChatChannel import com.vitorpamplona.amethyst.ui.note.NonClickableUserPictures import com.vitorpamplona.amethyst.ui.note.ObserveDraftEvent +import com.vitorpamplona.amethyst.ui.note.elements.HeaderPill import com.vitorpamplona.amethyst.ui.note.elements.TimeAgoStyle import com.vitorpamplona.amethyst.ui.note.elements.ToggleableTimeAgoText import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -453,31 +448,11 @@ private fun RelayNameChip( label: String, onClick: () -> Unit, ) { - Surface( - shape = RoundedCornerShape(6.dp), - color = MaterialTheme.colorScheme.secondaryContainer, - modifier = Modifier.clickable(onClick = onClick), - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(3.dp), - modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp), - ) { - Icon( - symbol = MaterialSymbols.Dns, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSecondaryContainer, - modifier = Modifier.size(11.dp), - ) - Text( - text = label, - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSecondaryContainer, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - } + HeaderPill( + symbol = MaterialSymbols.Dns, + text = label, + onClick = onClick, + ) } @Composable From 2fefc3a409dcd3a7d14faca73953e4baed87546a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 13 Jul 2026 12:28:59 +0000 Subject: [PATCH 02/13] feat(ui): unify note-header markers into a two-tier pill/quiet-mark system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tier 1 (HeaderPill): tappable or verifiable metadata now renders as the squared secondary-container chip — location, PoW, OTS (already pills), plus expiration and the bounty reward (bolt icon + amount, tap opens the pledge dialog; icon turns orange once the user has pledged). Tier 2 (QuietMark, new): passive state markers — Boosted, Draft, Edited/ Original, pinned, private rumor — share one spec: 12sp medium gray text with an optional 12dp icon. Draft is no longer a hardcoded English literal (new R.string.draft). Cross-cutting cleanups: - Header rows (NoteCompose First/SecondUserInfoRow, ThreadFeedView) own spacing via Arrangement.spacedBy(5dp) instead of per-marker paddings. - Hashtag/community/fork links use the existing lessImportantLink theme color instead of ad-hoc primary.copy(alpha = 0.52f) (fork was full primary, louder than everything else on the line). - NIP-05 badge drops hardcoded Color.Yellow/Color.Red for theme-aware placeholderText/error. - Note-header timestamps switch to the Short style at 12sp; other TimeAgo callers keep the dotted default. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01AgEpNtwnetPhETXQSdq4b5 --- .../ui/note/NIP05VerificationDisplay.kt | 4 +- .../amethyst/ui/note/NoteCompose.kt | 78 ++++++------------ .../amethyst/ui/note/elements/BoostedMark.kt | 15 +--- .../ui/note/elements/DisplayCommunity.kt | 6 +- .../ui/note/elements/DisplayEditStatus.kt | 40 +++------- .../ui/note/elements/DisplayHashtags.kt | 3 +- .../amethyst/ui/note/elements/DisplayOts.kt | 3 - .../ui/note/elements/DisplayReward.kt | 45 +++-------- .../amethyst/ui/note/elements/ForkInfo.kt | 3 +- .../amethyst/ui/note/elements/HeaderPill.kt | 4 +- .../amethyst/ui/note/elements/QuietMark.kt | 80 +++++++++++++++++++ .../amethyst/ui/note/elements/TimeAgo.kt | 17 +++- .../loggedIn/threadview/ThreadFeedView.kt | 16 +++- amethyst/src/main/res/values/strings.xml | 2 + 14 files changed, 167 insertions(+), 149 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/QuietMark.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt index 6bcb102918..b87ed48c44 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt @@ -472,7 +472,7 @@ fun RenderNIP05VerifiedSymbol( symbol = MaterialSymbols.Downloading, contentDescription = stringRes(id = R.string.nip05_checking), modifier = modifier, - tint = Color.Yellow, + tint = MaterialTheme.colorScheme.placeholderText, ) } @@ -492,7 +492,7 @@ fun RenderNIP05VerifiedSymbol( symbol = MaterialSymbols.Report, contentDescription = stringRes(id = R.string.nip05_failed), modifier = modifier, - tint = Color.Red, + tint = MaterialTheme.colorScheme.error, ) } } 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 6286a1a484..39ef85a00c 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 @@ -25,6 +25,7 @@ package com.vitorpamplona.amethyst.ui.note import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -32,7 +33,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton @@ -53,7 +53,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R @@ -85,11 +84,14 @@ import com.vitorpamplona.amethyst.ui.note.elements.DisplayLocation import com.vitorpamplona.amethyst.ui.note.elements.DisplayOts import com.vitorpamplona.amethyst.ui.note.elements.DisplayPoW import com.vitorpamplona.amethyst.ui.note.elements.DisplayReward +import com.vitorpamplona.amethyst.ui.note.elements.HeaderPill import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton +import com.vitorpamplona.amethyst.ui.note.elements.QuietMark import com.vitorpamplona.amethyst.ui.note.elements.Reward import com.vitorpamplona.amethyst.ui.note.elements.ShowForkInformation import com.vitorpamplona.amethyst.ui.note.elements.StaleRelayHint import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo +import com.vitorpamplona.amethyst.ui.note.elements.TimeAgoStyle import com.vitorpamplona.amethyst.ui.note.types.BadgeDisplay import com.vitorpamplona.amethyst.ui.note.types.DisplayBlockedRelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayBroadcastRelayList @@ -207,8 +209,6 @@ import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.ui.theme.Font12SP import com.vitorpamplona.amethyst.ui.theme.HalfDoubleVertSpacer -import com.vitorpamplona.amethyst.ui.theme.HalfPadding -import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding import com.vitorpamplona.amethyst.ui.theme.Height4dpModifier import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.Size25dp @@ -216,7 +216,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size30dp import com.vitorpamplona.amethyst.ui.theme.Size34dp import com.vitorpamplona.amethyst.ui.theme.Size55Modifier import com.vitorpamplona.amethyst.ui.theme.Size55dp -import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer +import com.vitorpamplona.amethyst.ui.theme.Size5dp import com.vitorpamplona.amethyst.ui.theme.UserNameMaxRowHeight import com.vitorpamplona.amethyst.ui.theme.UserNameRowHeight import com.vitorpamplona.amethyst.ui.theme.channelNotePictureModifier @@ -1743,6 +1743,7 @@ fun SecondUserInfoRow( Row( verticalAlignment = CenterVertically, + horizontalArrangement = Arrangement.spacedBy(Size5dp), modifier = UserNameMaxRowHeight, ) { Column(modifier = Modifier.weight(1f)) { @@ -1755,32 +1756,19 @@ fun SecondUserInfoRow( val baseReward = remember(noteEvent) { noteEvent.bountyBaseReward()?.let { Reward(it) } } if (baseReward != null) { - Spacer(StdHorzSpacer) - DisplayReward(baseReward, note, accountViewModel, nav) + DisplayReward(baseReward, note, accountViewModel) } } } @Composable fun DisplayExpiration(expirationDate: Long) { - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - symbol = MaterialSymbols.Timer, - contentDescription = stringRes(R.string.expiration_date_label), - modifier = Modifier.padding(start = 5.dp).size(12.dp), - tint = MaterialTheme.colorScheme.placeholderText, - ) - val context = LocalContext.current - Text( - text = timeAheadNoDot(expirationDate, context), - color = MaterialTheme.colorScheme.placeholderText, - fontSize = Font12SP, - maxLines = 1, - modifier = Modifier.padding(start = 3.dp), - ) - } + val context = LocalContext.current + HeaderPill( + symbol = MaterialSymbols.Timer, + text = timeAheadNoDot(expirationDate, context), + contentDescription = stringRes(R.string.expiration_date_label), + ) } @Composable @@ -1800,26 +1788,12 @@ fun DisplayOtsIfInOriginal( @Composable fun DisplayDraft() { - Text( - "Draft", - fontWeight = FontWeight.Bold, - fontSize = Font12SP, - color = MaterialTheme.colorScheme.placeholderText, - maxLines = 1, - modifier = HalfStartPadding, - ) + QuietMark(text = stringRes(R.string.draft)) } @Composable fun DisplayDraftChat() { - Text( - "Draft", - color = MaterialTheme.colorScheme.placeholderText, - modifier = Modifier, - fontWeight = FontWeight.Bold, - fontSize = Font12SP, - maxLines = 1, - ) + QuietMark(text = stringRes(R.string.draft)) } @Composable @@ -1832,7 +1806,11 @@ fun FirstUserInfoRow( nav: INav, moreOptions: (@Composable () -> Unit)? = null, ) { - Row(verticalAlignment = CenterVertically, modifier = UserNameRowHeight) { + Row( + verticalAlignment = CenterVertically, + horizontalArrangement = Arrangement.spacedBy(Size5dp), + modifier = UserNameRowHeight, + ) { val isRepost = baseNote.event is RepostEvent || baseNote.event is GenericRepostEvent val isDraft = baseNote.isDraft() val textColor = if (isRepost) MaterialTheme.colorScheme.grayText else Color.Unspecified @@ -1849,12 +1827,10 @@ fun FirstUserInfoRow( if (zapSender != null) { if (showAuthorPicture) { UserPicture(zapSender, Size25dp, accountViewModel = accountViewModel, nav = nav) - Spacer(HalfPadding) } UsernameDisplay(zapSender, Modifier.weight(1f), textColor = textColor, accountViewModel = accountViewModel) } else if (showAuthorPicture) { NoteAuthorPicture(baseNote, Size25dp, accountViewModel = accountViewModel, nav = nav) - Spacer(HalfPadding) NoteUsernameDisplay(baseNote, Modifier.weight(1f), textColor = textColor, accountViewModel = accountViewModel) } else { NoteUsernameDisplay(baseNote, Modifier.weight(1f), textColor = textColor, accountViewModel = accountViewModel) @@ -1908,13 +1884,11 @@ fun FirstUserInfoRow( val geo = remember(noteEvent) { noteEvent?.geoHashOrScope() } if (geo != null) { - Spacer(StdHorzSpacer) DisplayLocation(geo, accountViewModel, nav) } val pow = remember(noteEvent) { noteEvent?.strongPoWOrNull() } if (pow != null) { - Spacer(StdHorzSpacer) DisplayPoW(pow) } @@ -1924,7 +1898,7 @@ fun FirstUserInfoRow( JumpToParentReplyButton(baseNote, accountViewModel, nav) - TimeAgo(baseNote) + TimeAgo(baseNote, style = TimeAgoStyle.Short, fontSize = Font12SP) if (moreOptions == null) { MoreOptionsButton(baseNote, editState, accountViewModel, nav) @@ -1936,21 +1910,17 @@ fun FirstUserInfoRow( @Composable fun PinnedMark() { - Icon( + QuietMark( symbol = MaterialSymbols.PushPin, contentDescription = stringRes(R.string.pinned_notes), - modifier = Modifier.padding(start = 5.dp).size(14.dp), - tint = MaterialTheme.colorScheme.placeholderText, ) } @Composable fun PrivateRumorMark() { - Icon( + QuietMark( symbol = MaterialSymbols.Lock, contentDescription = stringRes(R.string.private_rumor_mark), - modifier = Modifier.padding(start = 5.dp).size(14.dp), - tint = MaterialTheme.colorScheme.placeholderText, ) } @@ -1976,7 +1946,7 @@ fun JumpToParentReplyButton( } ?: return ClickableBox( - modifier = Modifier.padding(start = 5.dp).size(20.dp), + modifier = Modifier.size(20.dp), onClick = { nav.nav { routeFor(parentNote, accountViewModel.account) } }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/BoostedMark.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/BoostedMark.kt index d214b3391c..7b81088adc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/BoostedMark.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/BoostedMark.kt @@ -20,24 +20,11 @@ */ package com.vitorpamplona.amethyst.ui.note.elements -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.text.font.FontWeight import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.amethyst.ui.theme.Font12SP -import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding -import com.vitorpamplona.amethyst.ui.theme.placeholderText @Composable fun BoostedMark() { - Text( - stringRes(id = R.string.boosted), - fontWeight = FontWeight.Bold, - fontSize = Font12SP, - color = MaterialTheme.colorScheme.placeholderText, - maxLines = 1, - modifier = HalfStartPadding, - ) + QuietMark(text = stringRes(id = R.string.boosted)) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayCommunity.kt index 404688cc03..64b27c68a5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayCommunity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayCommunity.kt @@ -36,7 +36,7 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.Font12SP -import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding +import com.vitorpamplona.amethyst.ui.theme.lessImportantLink import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip72ModCommunities.communityAddress import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent @@ -47,7 +47,7 @@ fun DisplayFollowingCommunityInPost( accountViewModel: AccountViewModel, nav: INav, ) { - Column(HalfStartPadding) { + Column { Row(verticalAlignment = Alignment.CenterVertically) { DisplayCommunity(baseNote, accountViewModel, nav) } } } @@ -67,7 +67,7 @@ private fun DisplayCommunity( ClickableTextColor( label, style = LocalTextStyle.current.copy(fontSize = Font12SP), - linkColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.52f), + linkColor = MaterialTheme.colorScheme.lessImportantLink, overflow = TextOverflow.Ellipsis, maxLines = 1, ) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayEditStatus.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayEditStatus.kt index ee31e07dc3..48a2e6b60f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayEditStatus.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayEditStatus.kt @@ -20,40 +20,24 @@ */ package com.vitorpamplona.amethyst.ui.note.elements -import androidx.compose.foundation.clickable -import androidx.compose.material3.LocalTextStyle -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.font.FontWeight import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.note.types.EditState import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.amethyst.ui.theme.Font12SP -import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding -import com.vitorpamplona.amethyst.ui.theme.placeholderText @Composable fun DisplayEditStatus(editState: EditState) { - Text( - text = - buildAnnotatedString { - if (editState.showingVersion.value == editState.originalVersionId()) { - append(stringRes(id = R.string.original)) - } else if (editState.showingVersion.value == editState.lastVersionId()) { - append(stringRes(id = R.string.edited)) - } else { - append(stringRes(id = R.string.edited_number, editState.versionId())) - } - }, - style = - LocalTextStyle.current.copy( - color = MaterialTheme.colorScheme.placeholderText, - fontWeight = FontWeight.Bold, - fontSize = Font12SP, - ), - maxLines = 1, - modifier = HalfStartPadding.clickable { editState.nextModification() }, + val label = + if (editState.showingVersion.value == editState.originalVersionId()) { + stringRes(id = R.string.original) + } else if (editState.showingVersion.value == editState.lastVersionId()) { + stringRes(id = R.string.edited) + } else { + stringRes(id = R.string.edited_number, editState.versionId()) + } + + QuietMark( + text = label, + onClick = { editState.nextModification() }, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayHashtags.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayHashtags.kt index b9f73c3367..cc48d6d08e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayHashtags.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayHashtags.kt @@ -40,6 +40,7 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.Font12SP +import com.vitorpamplona.amethyst.ui.theme.lessImportantLink import com.vitorpamplona.quartz.nip01Core.tags.hashtags.firstIsTaggedHashes import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip73ExternalIds.topics.HashtagId @@ -85,7 +86,7 @@ private fun DisplayTagList( ClickableTextColor( "#$firstTag", style = LocalTextStyle.current.copy(fontSize = Font12SP), - linkColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.52f), + linkColor = MaterialTheme.colorScheme.lessImportantLink, overflow = TextOverflow.Ellipsis, maxLines = 1, ) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt index fdf5f6890c..c5cf453138 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt @@ -32,7 +32,6 @@ import com.vitorpamplona.amethyst.ui.note.LoadOts import com.vitorpamplona.amethyst.ui.note.timeAgoNoDot import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding import java.text.SimpleDateFormat import java.util.Date @@ -63,7 +62,6 @@ fun DisplayOts( HeaderPill( symbol = MaterialSymbols.CheckCircle, text = stringRes(R.string.existed_since, timeStr), - modifier = HalfStartPadding, contentDescription = stringRes(R.string.ots_info_title), onClick = { accountViewModel.toastManager.toast( @@ -78,7 +76,6 @@ fun DisplayOts( HeaderPill( symbol = MaterialSymbols.Schedule, text = stringRes(id = R.string.timestamp_pending_short), - modifier = HalfStartPadding, contentDescription = stringRes(R.string.ots_info_title), ) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt index b392b6a50c..0b36780bd1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.note.elements -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.IntrinsicSize @@ -55,20 +54,16 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.ui.components.ClickableTextColor +import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteReplies -import com.vitorpamplona.amethyst.ui.navigation.navs.INav -import com.vitorpamplona.amethyst.ui.navigation.routes.Route -import com.vitorpamplona.amethyst.ui.note.ZapIcon -import com.vitorpamplona.amethyst.ui.note.ZappedIcon import com.vitorpamplona.amethyst.ui.note.buttons.CloseButton import com.vitorpamplona.amethyst.ui.note.buttons.PostButton import com.vitorpamplona.amethyst.ui.note.showAmount import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.amethyst.ui.theme.Size20Modifier +import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange import com.vitorpamplona.amethyst.ui.theme.placeholderText import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -83,28 +78,13 @@ fun DisplayReward( baseReward: Reward, baseNote: Note, accountViewModel: AccountViewModel, - nav: INav, ) { var popupExpanded by remember { mutableStateOf(false) } - Column { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.clickable { popupExpanded = true }, - ) { - ClickableTextColor( - "#bounty", - linkColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.52f), - ) { - nav.nav(Route.Hashtag("bounty")) - } + RenderPledgeAmount(baseNote, baseReward, accountViewModel) { popupExpanded = true } - RenderPledgeAmount(baseNote, baseReward, accountViewModel) - } - - if (popupExpanded) { - AddBountyAmountDialog(baseNote, accountViewModel) { popupExpanded = false } - } + if (popupExpanded) { + AddBountyAmountDialog(baseNote, accountViewModel) { popupExpanded = false } } } @@ -113,6 +93,7 @@ private fun RenderPledgeAmount( baseNote: Note, baseReward: Reward, accountViewModel: AccountViewModel, + onClick: () -> Unit, ) { val repliesState by observeNoteReplies(baseNote, accountViewModel) var reward by remember { @@ -142,16 +123,12 @@ private fun RenderPledgeAmount( } } - if (hasPledge) { - ZappedIcon(modifier = Size20Modifier) - } else { - ZapIcon(modifier = Size20Modifier, MaterialTheme.colorScheme.placeholderText) - } - - Text( + HeaderPill( + symbol = MaterialSymbols.Bolt, text = reward, - color = MaterialTheme.colorScheme.placeholderText, - maxLines = 1, + contentDescription = stringRes(R.string.bounty_label), + iconTint = if (hasPledge) BitcoinOrange else null, + onClick = onClick, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt index e39303df66..7b6486bfd9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt @@ -39,6 +39,7 @@ import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Font14SP +import com.vitorpamplona.amethyst.ui.theme.lessImportantLink import com.vitorpamplona.quartz.experimental.forks.IForkableEvent @Composable @@ -85,7 +86,7 @@ fun ForkInformationRowLightColor( clickablePart = stringRes(id = R.string.forked_from) + " " + (userState?.info?.bestName() ?: author.pubkeyDisplayHex()), maxLines = 1, route = route, - overrideColor = MaterialTheme.colorScheme.primary, + overrideColor = MaterialTheme.colorScheme.lessImportantLink, fontSize = Font14SP, nav = nav, tags = userState?.tags, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/HeaderPill.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/HeaderPill.kt index 2e70a1f398..6661391c8b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/HeaderPill.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/HeaderPill.kt @@ -32,6 +32,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.icons.symbols.Icon @@ -49,6 +50,7 @@ fun HeaderPill( text: String, modifier: Modifier = Modifier, contentDescription: String? = null, + iconTint: Color? = null, onClick: (() -> Unit)? = null, ) { Surface( @@ -64,7 +66,7 @@ fun HeaderPill( Icon( symbol = symbol, contentDescription = contentDescription, - tint = MaterialTheme.colorScheme.onSecondaryContainer, + tint = iconTint ?: MaterialTheme.colorScheme.onSecondaryContainer, modifier = Modifier.size(11.dp), ) Text( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/QuietMark.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/QuietMark.kt new file mode 100644 index 0000000000..acd339471e --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/QuietMark.kt @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.note.elements + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.icons.symbols.Icon +import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol +import com.vitorpamplona.amethyst.ui.theme.Font12SP +import com.vitorpamplona.amethyst.ui.theme.placeholderText + +/** + * Quiet passive-state marker for note headers: Boosted, Draft, Edited, pinned, + * private rumor, ... One spec for all of them so the header reads as a single + * system: 12sp medium gray text with an optional 12dp icon. Contrast with + * [HeaderPill], which is the loud tier for tappable/verifiable metadata. + * + * Spacing between markers is owned by the parent row (`Arrangement.spacedBy`), + * not baked in here. + */ +@Composable +fun QuietMark( + text: String? = null, + symbol: MaterialSymbol? = null, + contentDescription: String? = null, + modifier: Modifier = Modifier, + onClick: (() -> Unit)? = null, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(3.dp), + modifier = if (onClick != null) modifier.clickable(onClick = onClick) else modifier, + ) { + if (symbol != null) { + Icon( + symbol = symbol, + contentDescription = contentDescription, + tint = MaterialTheme.colorScheme.placeholderText, + modifier = Modifier.size(12.dp), + ) + } + if (text != null) { + Text( + text = text, + color = MaterialTheme.colorScheme.placeholderText, + fontSize = Font12SP, + lineHeight = Font12SP, + fontWeight = FontWeight.Medium, + maxLines = 1, + ) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/TimeAgo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/TimeAgo.kt index c23fd2d41c..af77ed2b82 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/TimeAgo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/TimeAgo.kt @@ -125,16 +125,25 @@ fun ToggleableTimeAgoText( } @Composable -fun TimeAgo(note: Note) { +fun TimeAgo( + note: Note, + style: TimeAgoStyle = TimeAgoStyle.Dotted, + fontSize: TextUnit = TextUnit.Unspecified, +) { val time = note.createdAt() ?: return - TimeAgo(time) + TimeAgo(time, style, fontSize) } @Composable -fun TimeAgo(time: Long) { +fun TimeAgo( + time: Long, + style: TimeAgoStyle = TimeAgoStyle.Dotted, + fontSize: TextUnit = TextUnit.Unspecified, +) { ToggleableTimeAgoText( timestamp = time, - style = TimeAgoStyle.Dotted, + style = style, + fontSize = fontSize, color = MaterialTheme.colorScheme.placeholderText, ) } 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 a2ccc498b7..11c461fee7 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 @@ -127,6 +127,7 @@ import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton import com.vitorpamplona.amethyst.ui.note.elements.Reward import com.vitorpamplona.amethyst.ui.note.elements.ShowForkInformation import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo +import com.vitorpamplona.amethyst.ui.note.elements.TimeAgoStyle import com.vitorpamplona.amethyst.ui.note.observeEdits import com.vitorpamplona.amethyst.ui.note.showAmount import com.vitorpamplona.amethyst.ui.note.types.AudioHeader @@ -231,6 +232,7 @@ import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.ui.theme.EditFieldBorder import com.vitorpamplona.amethyst.ui.theme.EditFieldTrailingIconModifier import com.vitorpamplona.amethyst.ui.theme.FeedPadding +import com.vitorpamplona.amethyst.ui.theme.Font12SP import com.vitorpamplona.amethyst.ui.theme.PaddingHorizontal12Modifier import com.vitorpamplona.amethyst.ui.theme.Size20dp import com.vitorpamplona.amethyst.ui.theme.Size40dp @@ -703,7 +705,10 @@ private fun FullBleedNoteCompose( } Column(modifier = Modifier.padding(start = 10.dp)) { - Row(verticalAlignment = Alignment.CenterVertically) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(Size5dp), + ) { if (zapSender != null) { UsernameDisplay(zapSender, Modifier.weight(1f), accountViewModel = accountViewModel) } else { @@ -738,12 +743,15 @@ private fun FullBleedNoteCompose( Expiration(baseNote) - TimeAgo(note = baseNote) + TimeAgo(note = baseNote, style = TimeAgoStyle.Short, fontSize = Font12SP) MoreOptionsButton(baseNote, editState, accountViewModel, nav) } - Row(verticalAlignment = Alignment.CenterVertically) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(Size5dp), + ) { Column( Modifier.weight(1f), ) { @@ -765,7 +773,7 @@ private fun FullBleedNoteCompose( val baseReward = remember { noteEvent.bountyBaseReward()?.let { Reward(it) } } if (baseReward != null) { - DisplayReward(baseReward, baseNote, accountViewModel, nav) + DisplayReward(baseReward, baseNote, accountViewModel) } val pow = remember(noteEvent) { noteEvent.strongPoWOrNull() } diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 994f57d2a0..a3ef6e6b7d 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -3041,6 +3041,8 @@ It\'s not possible to react a draft note It\'s not possible to zap a draft note Draft Note + Draft + Bounty From Msg From b81116865c5bd55c3bb22f618cc5a0a55ed0559b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 13 Jul 2026 15:32:01 +0000 Subject: [PATCH 03/13] feat(ui): add density previews for the note-header marker system NoteHeaderFirstRowDensityPreview stacks first-row samples from bare (username + time + options) to fully loaded (hashtag, edited, draft, lock, pin, location/PoW/OTS/expiration pills, jump-to-parent) under a long username, in dark and light themes. Uses the real QuietMark/ HeaderPill components with static stand-ins only for the data-loading markers. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01AgEpNtwnetPhETXQSdq4b5 --- .../note/elements/NoteHeaderMarkersPreview.kt | 188 ++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteHeaderMarkersPreview.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteHeaderMarkersPreview.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteHeaderMarkersPreview.kt new file mode 100644 index 0000000000..584c725b2a --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteHeaderMarkersPreview.kt @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.note.elements + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.icons.symbols.Icon +import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.ui.note.DisplayDraft +import com.vitorpamplona.amethyst.ui.note.DisplayExpiration +import com.vitorpamplona.amethyst.ui.note.PinnedMark +import com.vitorpamplona.amethyst.ui.note.PrivateRumorMark +import com.vitorpamplona.amethyst.ui.note.VerticalDotsIcon +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.Font12SP +import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn +import com.vitorpamplona.amethyst.ui.theme.lessImportantLink +import com.vitorpamplona.amethyst.ui.theme.placeholderText +import com.vitorpamplona.quartz.utils.TimeUtils + +/** + * Design-system preview for the note-header first row: how the two marker + * tiers ([HeaderPill] and [QuietMark]) compose next to the username and + * timestamp as the line fills up. Uses the same layout contract as + * `FirstUserInfoRow` (weight-1 username, `Arrangement.spacedBy(5.dp)`), + * with static stand-ins only for the data-loading markers (location, OTS, + * hashtag). + */ +@Preview(widthDp = 400) +@Composable +fun NoteHeaderFirstRowDensityPreview() { + ThemeComparisonColumn { + Column(Modifier.padding(vertical = 6.dp)) { + // Bare minimum: username + time + options + PreviewHeaderRow {} + + // One quiet mark: repost + PreviewHeaderRow { BoostedMark() } + + // Quiet text marks: edited + draft + PreviewHeaderRow { + QuietMark(text = stringRes(R.string.edited), onClick = {}) + DisplayDraft() + } + + // Quiet icon marks: private rumor + pinned + PreviewHeaderRow { + PrivateRumorMark() + PinnedMark() + } + + // Soft link + one pill + PreviewHeaderRow { + PreviewHashtag() + DisplayPoW(24) + } + + // Pill cluster: location + PoW + OTS + PreviewHeaderRow { + PreviewLocationPill() + DisplayPoW(28) + PreviewOtsPill() + } + + // Expiring note with a parent to jump to + PreviewHeaderRow { + DisplayExpiration(TimeUtils.now() + 7200) + PreviewJumpToParent() + } + + // Kitchen sink under a long username: everything competes for space + PreviewHeaderRow(username = "A user with a very long display name") { + PreviewHashtag() + QuietMark(text = stringRes(R.string.edited), onClick = {}) + DisplayDraft() + PrivateRumorMark() + PinnedMark() + PreviewLocationPill() + DisplayPoW(32) + PreviewOtsPill() + DisplayExpiration(TimeUtils.now() + 7200) + PreviewJumpToParent() + } + } + } +} + +/** Same layout contract as `FirstUserInfoRow`: weight-1 bold username, 5dp gaps, time + options at the end. */ +@Composable +private fun PreviewHeaderRow( + username: String = "Vitor", + markers: @Composable RowScope.() -> Unit, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(5.dp), + modifier = Modifier.fillMaxWidth().padding(horizontal = 10.dp, vertical = 5.dp), + ) { + Text( + text = username, + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + + markers() + + TimeAgo(TimeUtils.now() - 300, style = TimeAgoStyle.Short, fontSize = Font12SP) + + VerticalDotsIcon() + } +} + +/** Static stand-in for `DisplayFollowingHashtagsInPost` (which needs account state). */ +@Composable +private fun PreviewHashtag() { + Text( + text = "#nostr", + color = MaterialTheme.colorScheme.lessImportantLink, + fontSize = Font12SP, + maxLines = 1, + ) +} + +/** Static stand-in for `DisplayLocation` (which resolves the geohash to a city name). */ +@Composable +private fun PreviewLocationPill() { + HeaderPill( + symbol = MaterialSymbols.LocationOn, + text = "Belo Horizonte", + onClick = {}, + ) +} + +/** Static stand-in for `DisplayOts` (which loads and verifies the attestation). */ +@Composable +private fun PreviewOtsPill() { + HeaderPill( + symbol = MaterialSymbols.CheckCircle, + text = stringRes(R.string.existed_since, "2y"), + onClick = {}, + ) +} + +/** Static stand-in for `JumpToParentReplyButton` (which needs the parent note). */ +@Composable +private fun PreviewJumpToParent() { + Icon( + symbol = MaterialSymbols.KeyboardArrowUp, + contentDescription = null, + modifier = Modifier.size(18.dp), + tint = MaterialTheme.colorScheme.placeholderText, + ) +} From 3cba5d3b65348744a7a05994b3e04f9fb6577aea Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 13 Jul 2026 15:53:03 +0000 Subject: [PATCH 04/13] refactor(ui): drive header-marker previews through the real FirstUserInfoRow Replace the hand-built preview rows and static stand-ins with the actual FirstUserInfoRow over notes seeded into LocalCache (the ZapPollNote/Poll preview pattern): metadata for usernames, a repost, an empty-sig rumor, a draft wrap, a community post, and PoW ids with committed nonces plus geohash/expiration tags. The geohash resolves through a seeded CachedReversedGeoLocations entry and the OTS pill renders its real pending branch via account.settings.pendingAttestations. Only the followed-hashtag label and the confirmed OTS state stay out - both are computed in effects that static previews never run, as documented in the file. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01AgEpNtwnetPhETXQSdq4b5 --- .../note/elements/NoteHeaderMarkersPreview.kt | 319 ++++++++++-------- 1 file changed, 186 insertions(+), 133 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteHeaderMarkersPreview.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteHeaderMarkersPreview.kt index 584c725b2a..ea1c4287df 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteHeaderMarkersPreview.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteHeaderMarkersPreview.kt @@ -20,169 +20,222 @@ */ package com.vitorpamplona.amethyst.ui.note.elements -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.icons.symbols.Icon -import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols -import com.vitorpamplona.amethyst.ui.note.DisplayDraft -import com.vitorpamplona.amethyst.ui.note.DisplayExpiration -import com.vitorpamplona.amethyst.ui.note.PinnedMark -import com.vitorpamplona.amethyst.ui.note.PrivateRumorMark -import com.vitorpamplona.amethyst.ui.note.VerticalDotsIcon -import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.amethyst.ui.theme.Font12SP +import com.vitorpamplona.amethyst.commons.ui.components.GenericLoadable +import com.vitorpamplona.amethyst.model.FeatureSetType +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.service.location.CachedReversedGeoLocations +import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav +import com.vitorpamplona.amethyst.ui.note.FirstUserInfoRow +import com.vitorpamplona.amethyst.ui.note.types.EditState +import com.vitorpamplona.amethyst.ui.note.types.EmptyState +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn -import com.vitorpamplona.amethyst.ui.theme.lessImportantLink -import com.vitorpamplona.amethyst.ui.theme.placeholderText +import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +import com.vitorpamplona.quartz.nip18Reposts.RepostEvent +import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent import com.vitorpamplona.quartz.utils.TimeUtils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext + +private val AUTHOR = "a".repeat(64) +private val LONG_NAME_AUTHOR = "b".repeat(64) + +private val AUTHOR_METADATA_ID = "e1".repeat(32) +private val LONG_NAME_METADATA_ID = "e2".repeat(32) + +private val PLAIN_ID = "1".repeat(64) +private val REPOST_ID = "2".repeat(64) +private val RUMOR_ID = "3".repeat(64) +private val EDITED_ID = "4".repeat(64) +private val EDIT_VERSION_ID = "5".repeat(64) +private val DRAFT_ID = "6".repeat(64) +private val COMMUNITY_POST_ID = "7".repeat(64) + +// 24 leading zero bits so strongPoWOrNull(min = 20) accepts the committed nonce. +private val POW_ID = "0".repeat(6) + "8".repeat(58) +private val KITCHEN_SINK_ID = "0".repeat(6) + "9".repeat(58) + +private const val GEOHASH = "u1x1" +private val COMMUNITY_ADDRESS = "34550:" + "c".repeat(64) + ":amethyst-users" /** - * Design-system preview for the note-header first row: how the two marker - * tiers ([HeaderPill] and [QuietMark]) compose next to the username and - * timestamp as the line fills up. Uses the same layout contract as - * `FirstUserInfoRow` (weight-1 username, `Arrangement.spacedBy(5.dp)`), - * with static stand-ins only for the data-loading markers (location, OTS, - * hashtag). + * Design-system preview for the note-header first row, rendered by the REAL + * `FirstUserInfoRow` over notes seeded into [LocalCache] — the same pattern + * the ZapPollNote/Poll previews use. Rows go from bare to fully loaded so the + * two marker tiers ([HeaderPill] and [QuietMark]) can be reviewed together. + * + * Two markers cannot appear in a static preview because their visibility is + * computed inside effects that previews never run: the followed-hashtag label + * (needs the account follow-list flows) and the *confirmed* OTS pill (needs + * attestation verification). The community label and the *pending* OTS pill + * shown here exercise the same two visual styles through real code. */ @Preview(widthDp = 400) @Composable fun NoteHeaderFirstRowDensityPreview() { + val accountViewModel = mockAccountViewModel() + val nav = EmptyNav() + + // The jump-to-parent arrow only renders in complete UI mode. + accountViewModel.settings.uiSettingsFlow.featureSet.value = FeatureSetType.COMPLETE + + // Let DisplayLocation resolve the geohash synchronously from the cache. + CachedReversedGeoLocations.locationNames.put(GEOHASH, "Belo Horizonte") + + val now = TimeUtils.now() + val expiresAt = (now + 7200).toString() + + val plain: Note + val repost: Note + val rumorPinned: Note + val edited: Note + val editVersion: Note + val draft: Note + val communityPost: Note + val geoPowExpiring: Note + val kitchenSink: Note + + runBlocking { + withContext(Dispatchers.IO) { + LocalCache.justConsume( + MetadataEvent(AUTHOR_METADATA_ID, AUTHOR, now, emptyArray(), """{"name":"Vitor"}""", "x"), + null, + false, + ) + LocalCache.justConsume( + MetadataEvent(LONG_NAME_METADATA_ID, LONG_NAME_AUTHOR, now, emptyArray(), """{"name":"A user with a very long display name"}""", "x"), + null, + false, + ) + + LocalCache.justConsume(TextNoteEvent(PLAIN_ID, AUTHOR, now - 300, emptyArray(), "GM", "x"), null, false) + LocalCache.justConsume(TextNoteEvent(EDIT_VERSION_ID, AUTHOR, now - 60, emptyArray(), "GM! (fixed typo)", "x"), null, false) + LocalCache.justConsume(RepostEvent(REPOST_ID, AUTHOR, now - 300, arrayOf(arrayOf("e", PLAIN_ID)), "", "x"), null, false) + // An empty sig is what marks a note as a private rumor. + LocalCache.justConsume(TextNoteEvent(RUMOR_ID, AUTHOR, now - 300, emptyArray(), "just between us", ""), null, false) + LocalCache.justConsume(TextNoteEvent(EDITED_ID, AUTHOR, now - 300, emptyArray(), "GM!", "x"), null, false) + LocalCache.justConsume(DraftWrapEvent(DRAFT_ID, AUTHOR, now - 300, emptyArray(), "", "x"), null, false) + LocalCache.justConsume( + TextNoteEvent(COMMUNITY_POST_ID, AUTHOR, now - 300, arrayOf(arrayOf("a", COMMUNITY_ADDRESS)), "hello community", "x"), + null, + false, + ) + LocalCache.justConsume( + TextNoteEvent( + POW_ID, + AUTHOR, + now - 300, + arrayOf( + arrayOf("g", GEOHASH), + arrayOf("nonce", "776", "24"), + arrayOf("expiration", expiresAt), + ), + "mined and placed", + "x", + ), + null, + false, + ) + LocalCache.justConsume( + TextNoteEvent( + KITCHEN_SINK_ID, + LONG_NAME_AUTHOR, + now - 300, + arrayOf( + arrayOf("e", PLAIN_ID), + arrayOf("a", COMMUNITY_ADDRESS), + arrayOf("g", GEOHASH), + arrayOf("nonce", "776", "24"), + arrayOf("expiration", expiresAt), + ), + "everything everywhere all at once", + // Empty sig: also a private rumor. + "", + ), + null, + false, + ) + + plain = LocalCache.getOrCreateNote(PLAIN_ID) + repost = LocalCache.getOrCreateNote(REPOST_ID) + rumorPinned = LocalCache.getOrCreateNote(RUMOR_ID) + edited = LocalCache.getOrCreateNote(EDITED_ID) + editVersion = LocalCache.getOrCreateNote(EDIT_VERSION_ID) + draft = LocalCache.getOrCreateNote(DRAFT_ID) + communityPost = LocalCache.getOrCreateNote(COMMUNITY_POST_ID) + geoPowExpiring = LocalCache.getOrCreateNote(POW_ID) + kitchenSink = LocalCache.getOrCreateNote(KITCHEN_SINK_ID) + } + } + + // LoadOts renders the pending pill synchronously from this map; the + // confirmed pill needs async verification a static preview can't run. + accountViewModel.account.settings.pendingAttestations + .update { it + (POW_ID to "stamp") + (KITCHEN_SINK_ID to "stamp") } + + val editedState = + remember { + mutableStateOf>( + GenericLoadable.Loaded(EditState().apply { updateModifications(listOf(editVersion)) }), + ) + } + ThemeComparisonColumn { - Column(Modifier.padding(vertical = 6.dp)) { + Column(Modifier.padding(horizontal = 10.dp, vertical = 6.dp)) { // Bare minimum: username + time + options - PreviewHeaderRow {} + HeaderRowSample(plain, accountViewModel) // One quiet mark: repost - PreviewHeaderRow { BoostedMark() } - - // Quiet text marks: edited + draft - PreviewHeaderRow { - QuietMark(text = stringRes(R.string.edited), onClick = {}) - DisplayDraft() - } + HeaderRowSample(repost, accountViewModel) // Quiet icon marks: private rumor + pinned - PreviewHeaderRow { - PrivateRumorMark() - PinnedMark() - } + HeaderRowSample(rumorPinned, accountViewModel, isPinned = true) - // Soft link + one pill - PreviewHeaderRow { - PreviewHashtag() - DisplayPoW(24) - } + // Quiet text mark, tappable: edited + HeaderRowSample(edited, accountViewModel, editState = editedState) - // Pill cluster: location + PoW + OTS - PreviewHeaderRow { - PreviewLocationPill() - DisplayPoW(28) - PreviewOtsPill() - } + // Quiet text mark: draft + HeaderRowSample(draft, accountViewModel) - // Expiring note with a parent to jump to - PreviewHeaderRow { - DisplayExpiration(TimeUtils.now() + 7200) - PreviewJumpToParent() - } + // Soft link: community label + HeaderRowSample(communityPost, accountViewModel) + + // Pill cluster: location + PoW + pending OTS + expiration + HeaderRowSample(geoPowExpiring, accountViewModel) // Kitchen sink under a long username: everything competes for space - PreviewHeaderRow(username = "A user with a very long display name") { - PreviewHashtag() - QuietMark(text = stringRes(R.string.edited), onClick = {}) - DisplayDraft() - PrivateRumorMark() - PinnedMark() - PreviewLocationPill() - DisplayPoW(32) - PreviewOtsPill() - DisplayExpiration(TimeUtils.now() + 7200) - PreviewJumpToParent() - } + HeaderRowSample(kitchenSink, accountViewModel, isPinned = true, editState = editedState) } } } -/** Same layout contract as `FirstUserInfoRow`: weight-1 bold username, 5dp gaps, time + options at the end. */ @Composable -private fun PreviewHeaderRow( - username: String = "Vitor", - markers: @Composable RowScope.() -> Unit, +private fun HeaderRowSample( + note: Note, + accountViewModel: AccountViewModel, + isPinned: Boolean = false, + editState: State> = EmptyState, ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(5.dp), - modifier = Modifier.fillMaxWidth().padding(horizontal = 10.dp, vertical = 5.dp), - ) { - Text( - text = username, - fontWeight = FontWeight.Bold, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.weight(1f), - ) - - markers() - - TimeAgo(TimeUtils.now() - 300, style = TimeAgoStyle.Short, fontSize = Font12SP) - - VerticalDotsIcon() - } -} - -/** Static stand-in for `DisplayFollowingHashtagsInPost` (which needs account state). */ -@Composable -private fun PreviewHashtag() { - Text( - text = "#nostr", - color = MaterialTheme.colorScheme.lessImportantLink, - fontSize = Font12SP, - maxLines = 1, - ) -} - -/** Static stand-in for `DisplayLocation` (which resolves the geohash to a city name). */ -@Composable -private fun PreviewLocationPill() { - HeaderPill( - symbol = MaterialSymbols.LocationOn, - text = "Belo Horizonte", - onClick = {}, - ) -} - -/** Static stand-in for `DisplayOts` (which loads and verifies the attestation). */ -@Composable -private fun PreviewOtsPill() { - HeaderPill( - symbol = MaterialSymbols.CheckCircle, - text = stringRes(R.string.existed_since, "2y"), - onClick = {}, - ) -} - -/** Static stand-in for `JumpToParentReplyButton` (which needs the parent note). */ -@Composable -private fun PreviewJumpToParent() { - Icon( - symbol = MaterialSymbols.KeyboardArrowUp, - contentDescription = null, - modifier = Modifier.size(18.dp), - tint = MaterialTheme.colorScheme.placeholderText, + FirstUserInfoRow( + baseNote = note, + showAuthorPicture = false, + isPinned = isPinned, + editState = editState, + accountViewModel = accountViewModel, + nav = EmptyNav(), ) } From 1d385cae0f01c131cbb5befc7fc3d64c82214446 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 13 Jul 2026 16:04:36 +0000 Subject: [PATCH 05/13] refactor(ui): move HeaderPill and QuietMark to commons/ui/note The two note-header marker primitives are pure Compose with no Android dependencies, so they belong in the shared layer where the desktop app (and future iOS) can render the same design system. QuietMark's gray comes from a new commons ColorScheme.placeholderText extension (onSurface at 42%, matching the Android theme's value). The thin app-side wrappers (BoostedMark, DisplayPoW, DisplayDraft, DisplayOts, DisplayLocation, DisplayReward, ...) stay in amethyst: they bind the primitives to Android string resources (with existing translations that commons' compose-resources catalog does not have) and to app state (LocalCache loaders, AccountViewModel, navigation). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01AgEpNtwnetPhETXQSdq4b5 --- .../vitorpamplona/amethyst/ui/note/NoteCompose.kt | 4 ++-- .../amethyst/ui/note/elements/BoostedMark.kt | 1 + .../amethyst/ui/note/elements/DisplayEditStatus.kt | 1 + .../amethyst/ui/note/elements/DisplayLocation.kt | 1 + .../amethyst/ui/note/elements/DisplayOts.kt | 1 + .../amethyst/ui/note/elements/DisplayPoW.kt | 1 + .../amethyst/ui/note/elements/DisplayReward.kt | 1 + .../ui/note/elements/NoteHeaderMarkersPreview.kt | 3 ++- .../loggedIn/chats/rooms/ChatroomHeaderCompose.kt | 2 +- .../amethyst/commons/ui/note}/HeaderPill.kt | 2 +- .../amethyst/commons/ui/note}/QuietMark.kt | 12 +++++++----- .../amethyst/commons/ui/theme/ThemeExtensions.kt | 8 ++++++++ 12 files changed, 27 insertions(+), 10 deletions(-) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note}/HeaderPill.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note}/QuietMark.kt (91%) 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 39ef85a00c..579844d990 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 @@ -60,6 +60,8 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.commons.ui.components.GenericLoadable +import com.vitorpamplona.amethyst.commons.ui.note.HeaderPill +import com.vitorpamplona.amethyst.commons.ui.note.QuietMark import com.vitorpamplona.amethyst.commons.ui.state.produceCachedStateAsync import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache @@ -84,9 +86,7 @@ import com.vitorpamplona.amethyst.ui.note.elements.DisplayLocation import com.vitorpamplona.amethyst.ui.note.elements.DisplayOts import com.vitorpamplona.amethyst.ui.note.elements.DisplayPoW import com.vitorpamplona.amethyst.ui.note.elements.DisplayReward -import com.vitorpamplona.amethyst.ui.note.elements.HeaderPill import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton -import com.vitorpamplona.amethyst.ui.note.elements.QuietMark import com.vitorpamplona.amethyst.ui.note.elements.Reward import com.vitorpamplona.amethyst.ui.note.elements.ShowForkInformation import com.vitorpamplona.amethyst.ui.note.elements.StaleRelayHint diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/BoostedMark.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/BoostedMark.kt index 7b81088adc..c48531aaca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/BoostedMark.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/BoostedMark.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.note.elements import androidx.compose.runtime.Composable import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.ui.note.QuietMark import com.vitorpamplona.amethyst.ui.stringRes @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayEditStatus.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayEditStatus.kt index 48a2e6b60f..0ffae65a1d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayEditStatus.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayEditStatus.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.note.elements import androidx.compose.runtime.Composable import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.ui.note.QuietMark import com.vitorpamplona.amethyst.ui.note.types.EditState import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayLocation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayLocation.kt index cf240cb7f2..a32e65d1b5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayLocation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayLocation.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.note.elements import androidx.compose.runtime.Composable import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.ui.note.HeaderPill import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.note.creators.location.LoadCityName diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt index c5cf453138..319293d69c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt @@ -27,6 +27,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext 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.model.Note import com.vitorpamplona.amethyst.ui.note.LoadOts import com.vitorpamplona.amethyst.ui.note.timeAgoNoDot 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 64aad9648a..e6fc7b772e 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 @@ -24,6 +24,7 @@ import androidx.compose.runtime.Composable 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.stringRes import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt index 0b36780bd1..fbd4270e07 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt @@ -55,6 +55,7 @@ import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.viewModel 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.model.Account import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteReplies diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteHeaderMarkersPreview.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteHeaderMarkersPreview.kt index ea1c4287df..02438cf59a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteHeaderMarkersPreview.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteHeaderMarkersPreview.kt @@ -76,7 +76,8 @@ private val COMMUNITY_ADDRESS = "34550:" + "c".repeat(64) + ":amethyst-users" * Design-system preview for the note-header first row, rendered by the REAL * `FirstUserInfoRow` over notes seeded into [LocalCache] — the same pattern * the ZapPollNote/Poll previews use. Rows go from bare to fully loaded so the - * two marker tiers ([HeaderPill] and [QuietMark]) can be reviewed together. + * two marker tiers (`HeaderPill` and `QuietMark`, now in `commons/ui/note`) + * can be reviewed together. * * Two markers cannot appear in a static preview because their visibility is * computed inside effects that previews never run: the followed-hashtag label diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt index 976cbe3032..52aec3f3c0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt @@ -52,6 +52,7 @@ import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.commons.model.marmotGroups.MarmotGroupChatroom import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel +import com.vitorpamplona.amethyst.commons.ui.note.HeaderPill import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User @@ -72,7 +73,6 @@ import com.vitorpamplona.amethyst.ui.note.LoadDecryptedContentOrNull import com.vitorpamplona.amethyst.ui.note.LoadPublicChatChannel import com.vitorpamplona.amethyst.ui.note.NonClickableUserPictures import com.vitorpamplona.amethyst.ui.note.ObserveDraftEvent -import com.vitorpamplona.amethyst.ui.note.elements.HeaderPill import com.vitorpamplona.amethyst.ui.note.elements.TimeAgoStyle import com.vitorpamplona.amethyst.ui.note.elements.ToggleableTimeAgoText import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/HeaderPill.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/HeaderPill.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/HeaderPill.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/HeaderPill.kt index 6661391c8b..6c4dc4726b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/HeaderPill.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/HeaderPill.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.note.elements +package com.vitorpamplona.amethyst.commons.ui.note import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/QuietMark.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/QuietMark.kt similarity index 91% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/QuietMark.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/QuietMark.kt index acd339471e..8f3cea79a7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/QuietMark.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/QuietMark.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.note.elements +package com.vitorpamplona.amethyst.commons.ui.note import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -31,10 +31,12 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol -import com.vitorpamplona.amethyst.ui.theme.Font12SP -import com.vitorpamplona.amethyst.ui.theme.placeholderText +import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText + +private val QuietMarkFontSize = 12.sp /** * Quiet passive-state marker for note headers: Boosted, Draft, Edited, pinned, @@ -70,8 +72,8 @@ fun QuietMark( Text( text = text, color = MaterialTheme.colorScheme.placeholderText, - fontSize = Font12SP, - lineHeight = Font12SP, + fontSize = QuietMarkFontSize, + lineHeight = QuietMarkFontSize, fontWeight = FontWeight.Medium, maxLines = 1, ) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/theme/ThemeExtensions.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/theme/ThemeExtensions.kt index 0e8e0866fd..ad4488e405 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/theme/ThemeExtensions.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/theme/ThemeExtensions.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.commons.ui.theme import androidx.compose.material3.ColorScheme +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.luminance @@ -36,3 +37,10 @@ val ColorScheme.isLight: Boolean */ val ColorScheme.onBackgroundColorFilter: ColorFilter get() = ColorFilter.tint(onBackground) + +/** + * De-emphasized text/icon color for secondary markers and hints. Matches the + * Android app's placeholderText, which is the palette's onSurface at 42%. + */ +val ColorScheme.placeholderText: Color + get() = onSurface.copy(alpha = 0.42f) From 86da29fa788522451b753c9108a5c0d486ba6419 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 14 Jul 2026 01:26:33 +0000 Subject: [PATCH 06/13] fix(ui): mute the header pill wash, restore dotted timestamps, tighten menu gap - HeaderPill's secondary-container background pulled too much attention; it now uses a faint onSurface wash (7%) with placeholderText content, so pills read as tappable metadata without competing with the note. - Note-header timestamps go back to the original dotted format at the default size; the TimeAgo style/fontSize params are reverted. - The timestamp + more-options pair renders unspaced again (the dot and the button's icon inset provide the separation), fixing the oversized gap the row-level spacedBy introduced before the 3-dot menu. - The header preview now consumes its fabricated events as already-verified (they cannot pass id/sig checks, which left every row bare), gives the repost a parseable inner event, and fetches the draft through its AddressableNote (draft wraps are addressable events). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01AgEpNtwnetPhETXQSdq4b5 --- .../amethyst/ui/note/NoteCompose.kt | 16 +++++----- .../note/elements/NoteHeaderMarkersPreview.kt | 30 +++++++++++-------- .../amethyst/ui/note/elements/TimeAgo.kt | 17 +++-------- .../loggedIn/threadview/ThreadFeedView.kt | 10 ++++--- .../amethyst/commons/ui/note/HeaderPill.kt | 15 ++++++---- 5 files changed, 46 insertions(+), 42 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 579844d990..88b1c511eb 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 @@ -91,7 +91,6 @@ import com.vitorpamplona.amethyst.ui.note.elements.Reward import com.vitorpamplona.amethyst.ui.note.elements.ShowForkInformation import com.vitorpamplona.amethyst.ui.note.elements.StaleRelayHint import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo -import com.vitorpamplona.amethyst.ui.note.elements.TimeAgoStyle import com.vitorpamplona.amethyst.ui.note.types.BadgeDisplay import com.vitorpamplona.amethyst.ui.note.types.DisplayBlockedRelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayBroadcastRelayList @@ -207,7 +206,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.workouts.ExerciseTemplateDi import com.vitorpamplona.amethyst.ui.screen.loggedIn.workouts.WorkoutDisplay import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer -import com.vitorpamplona.amethyst.ui.theme.Font12SP import com.vitorpamplona.amethyst.ui.theme.HalfDoubleVertSpacer import com.vitorpamplona.amethyst.ui.theme.Height4dpModifier import com.vitorpamplona.amethyst.ui.theme.Size10dp @@ -1898,12 +1896,16 @@ fun FirstUserInfoRow( JumpToParentReplyButton(baseNote, accountViewModel, nav) - TimeAgo(baseNote, style = TimeAgoStyle.Short, fontSize = Font12SP) + // The dotted TimeAgo carries its own " • " separator and the options + // button has slack around its icon, so this pair stays unspaced. + Row(verticalAlignment = CenterVertically) { + TimeAgo(baseNote) - if (moreOptions == null) { - MoreOptionsButton(baseNote, editState, accountViewModel, nav) - } else { - moreOptions() + if (moreOptions == null) { + MoreOptionsButton(baseNote, editState, accountViewModel, nav) + } else { + moreOptions() + } } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteHeaderMarkersPreview.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteHeaderMarkersPreview.kt index 02438cf59a..432e585803 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteHeaderMarkersPreview.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteHeaderMarkersPreview.kt @@ -110,30 +110,36 @@ fun NoteHeaderFirstRowDensityPreview() { val geoPowExpiring: Note val kitchenSink: Note + val plainEvent = TextNoteEvent(PLAIN_ID, AUTHOR, now - 300, emptyArray(), "GM", "x") + + // Drafts are addressable: consumption lands on the AddressableNote, so the + // preview must fetch it by address rather than by event id. + val draftEvent = DraftWrapEvent(DRAFT_ID, AUTHOR, now - 300, arrayOf(arrayOf("d", "preview-draft")), "", "x") + runBlocking { withContext(Dispatchers.IO) { LocalCache.justConsume( MetadataEvent(AUTHOR_METADATA_ID, AUTHOR, now, emptyArray(), """{"name":"Vitor"}""", "x"), null, - false, + true, ) LocalCache.justConsume( MetadataEvent(LONG_NAME_METADATA_ID, LONG_NAME_AUTHOR, now, emptyArray(), """{"name":"A user with a very long display name"}""", "x"), null, - false, + true, ) - LocalCache.justConsume(TextNoteEvent(PLAIN_ID, AUTHOR, now - 300, emptyArray(), "GM", "x"), null, false) - LocalCache.justConsume(TextNoteEvent(EDIT_VERSION_ID, AUTHOR, now - 60, emptyArray(), "GM! (fixed typo)", "x"), null, false) - LocalCache.justConsume(RepostEvent(REPOST_ID, AUTHOR, now - 300, arrayOf(arrayOf("e", PLAIN_ID)), "", "x"), null, false) + LocalCache.justConsume(plainEvent, null, true) + LocalCache.justConsume(TextNoteEvent(EDIT_VERSION_ID, AUTHOR, now - 60, emptyArray(), "GM! (fixed typo)", "x"), null, true) + LocalCache.justConsume(RepostEvent(REPOST_ID, AUTHOR, now - 300, arrayOf(arrayOf("e", PLAIN_ID)), plainEvent.toJson(), "x"), null, true) // An empty sig is what marks a note as a private rumor. - LocalCache.justConsume(TextNoteEvent(RUMOR_ID, AUTHOR, now - 300, emptyArray(), "just between us", ""), null, false) - LocalCache.justConsume(TextNoteEvent(EDITED_ID, AUTHOR, now - 300, emptyArray(), "GM!", "x"), null, false) - LocalCache.justConsume(DraftWrapEvent(DRAFT_ID, AUTHOR, now - 300, emptyArray(), "", "x"), null, false) + LocalCache.justConsume(TextNoteEvent(RUMOR_ID, AUTHOR, now - 300, emptyArray(), "just between us", ""), null, true) + LocalCache.justConsume(TextNoteEvent(EDITED_ID, AUTHOR, now - 300, emptyArray(), "GM!", "x"), null, true) + LocalCache.justConsume(draftEvent, null, true) LocalCache.justConsume( TextNoteEvent(COMMUNITY_POST_ID, AUTHOR, now - 300, arrayOf(arrayOf("a", COMMUNITY_ADDRESS)), "hello community", "x"), null, - false, + true, ) LocalCache.justConsume( TextNoteEvent( @@ -149,7 +155,7 @@ fun NoteHeaderFirstRowDensityPreview() { "x", ), null, - false, + true, ) LocalCache.justConsume( TextNoteEvent( @@ -168,7 +174,7 @@ fun NoteHeaderFirstRowDensityPreview() { "", ), null, - false, + true, ) plain = LocalCache.getOrCreateNote(PLAIN_ID) @@ -176,7 +182,7 @@ fun NoteHeaderFirstRowDensityPreview() { rumorPinned = LocalCache.getOrCreateNote(RUMOR_ID) edited = LocalCache.getOrCreateNote(EDITED_ID) editVersion = LocalCache.getOrCreateNote(EDIT_VERSION_ID) - draft = LocalCache.getOrCreateNote(DRAFT_ID) + draft = LocalCache.getOrCreateAddressableNote(draftEvent.address()) communityPost = LocalCache.getOrCreateNote(COMMUNITY_POST_ID) geoPowExpiring = LocalCache.getOrCreateNote(POW_ID) kitchenSink = LocalCache.getOrCreateNote(KITCHEN_SINK_ID) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/TimeAgo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/TimeAgo.kt index af77ed2b82..c23fd2d41c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/TimeAgo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/TimeAgo.kt @@ -125,25 +125,16 @@ fun ToggleableTimeAgoText( } @Composable -fun TimeAgo( - note: Note, - style: TimeAgoStyle = TimeAgoStyle.Dotted, - fontSize: TextUnit = TextUnit.Unspecified, -) { +fun TimeAgo(note: Note) { val time = note.createdAt() ?: return - TimeAgo(time, style, fontSize) + TimeAgo(time) } @Composable -fun TimeAgo( - time: Long, - style: TimeAgoStyle = TimeAgoStyle.Dotted, - fontSize: TextUnit = TextUnit.Unspecified, -) { +fun TimeAgo(time: Long) { ToggleableTimeAgoText( timestamp = time, - style = style, - fontSize = fontSize, + style = TimeAgoStyle.Dotted, color = MaterialTheme.colorScheme.placeholderText, ) } 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 11c461fee7..f2762f30bf 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 @@ -127,7 +127,6 @@ import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton import com.vitorpamplona.amethyst.ui.note.elements.Reward import com.vitorpamplona.amethyst.ui.note.elements.ShowForkInformation import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo -import com.vitorpamplona.amethyst.ui.note.elements.TimeAgoStyle import com.vitorpamplona.amethyst.ui.note.observeEdits import com.vitorpamplona.amethyst.ui.note.showAmount import com.vitorpamplona.amethyst.ui.note.types.AudioHeader @@ -232,7 +231,6 @@ import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.ui.theme.EditFieldBorder import com.vitorpamplona.amethyst.ui.theme.EditFieldTrailingIconModifier import com.vitorpamplona.amethyst.ui.theme.FeedPadding -import com.vitorpamplona.amethyst.ui.theme.Font12SP import com.vitorpamplona.amethyst.ui.theme.PaddingHorizontal12Modifier import com.vitorpamplona.amethyst.ui.theme.Size20dp import com.vitorpamplona.amethyst.ui.theme.Size40dp @@ -743,9 +741,13 @@ private fun FullBleedNoteCompose( Expiration(baseNote) - TimeAgo(note = baseNote, style = TimeAgoStyle.Short, fontSize = Font12SP) + // The dotted TimeAgo carries its own " • " separator and the + // options button has slack around its icon: keep the pair unspaced. + Row(verticalAlignment = Alignment.CenterVertically) { + TimeAgo(note = baseNote) - MoreOptionsButton(baseNote, editState, accountViewModel, nav) + MoreOptionsButton(baseNote, editState, accountViewModel, nav) + } } Row( diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/HeaderPill.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/HeaderPill.kt index 6c4dc4726b..312ca0d584 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/HeaderPill.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/HeaderPill.kt @@ -37,12 +37,14 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol +import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText /** * Compact squared chip for note-header metadata (PoW, OTS, location, relay - * hosts, ...). Matches the relay information pills used in the NIP-29 chat - * headers so all header badges share one look: a small icon plus a short - * label on a secondary-container surface with slightly rounded corners. + * hosts, ...) so all header badges share one look: a small icon plus a short + * label on a faint surface tint with slightly rounded corners. The wash is + * deliberately subtle — just enough of an outline to read as tappable + * metadata without competing with the note content. */ @Composable fun HeaderPill( @@ -55,7 +57,8 @@ fun HeaderPill( ) { Surface( shape = RoundedCornerShape(6.dp), - color = MaterialTheme.colorScheme.secondaryContainer, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.07f), + contentColor = MaterialTheme.colorScheme.placeholderText, modifier = if (onClick != null) modifier.clickable(onClick = onClick) else modifier, ) { Row( @@ -66,13 +69,13 @@ fun HeaderPill( Icon( symbol = symbol, contentDescription = contentDescription, - tint = iconTint ?: MaterialTheme.colorScheme.onSecondaryContainer, + tint = iconTint ?: MaterialTheme.colorScheme.placeholderText, modifier = Modifier.size(11.dp), ) Text( text = text, style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSecondaryContainer, + color = MaterialTheme.colorScheme.placeholderText, maxLines = 1, overflow = TextOverflow.Ellipsis, ) From efae1dec22023378e486c67bed7b402f848cbc4b Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 14 Jul 2026 08:21:44 -0400 Subject: [PATCH 07/13] Adjustments --- .../amethyst/ui/note/NoteCompose.kt | 42 +++++++++---------- .../note/elements/NoteHeaderMarkersPreview.kt | 31 +++++--------- 2 files changed, 32 insertions(+), 41 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 88b1c511eb..041d3b37ff 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 @@ -1834,6 +1834,16 @@ fun FirstUserInfoRow( NoteUsernameDisplay(baseNote, Modifier.weight(1f), textColor = textColor, accountViewModel = accountViewModel) } + if (isRepost) { + BoostedMark() + } + + CheckAndDisplayEditStatus(editState) + + if (isDraft) { + DisplayDraft() + } + if (isDraft) { ObserveDraftEvent(baseNote, accountViewModel) { draftNote -> val isCommunityPost = @@ -1860,16 +1870,22 @@ fun FirstUserInfoRow( } } - if (isRepost) { - BoostedMark() + val noteEvent = baseNote.event + + val geo = remember(noteEvent) { noteEvent?.geoHashOrScope() } + if (geo != null) { + DisplayLocation(geo, accountViewModel, nav) } - CheckAndDisplayEditStatus(editState) + DisplayOtsIfInOriginal(baseNote, editState, accountViewModel) - if (isDraft) { - DisplayDraft() + val pow = remember(noteEvent) { noteEvent?.strongPoWOrNull() } + if (pow != null) { + DisplayPoW(pow) } + Expiration(baseNote) + if (baseNote.isPrivateRumor()) { PrivateRumorMark() } @@ -1878,22 +1894,6 @@ fun FirstUserInfoRow( PinnedMark() } - val noteEvent = baseNote.event - - val geo = remember(noteEvent) { noteEvent?.geoHashOrScope() } - if (geo != null) { - DisplayLocation(geo, accountViewModel, nav) - } - - val pow = remember(noteEvent) { noteEvent?.strongPoWOrNull() } - if (pow != null) { - DisplayPoW(pow) - } - - DisplayOtsIfInOriginal(baseNote, editState, accountViewModel) - - Expiration(baseNote) - JumpToParentReplyButton(baseNote, accountViewModel, nav) // The dotted TimeAgo carries its own " • " separator and the options diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteHeaderMarkersPreview.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteHeaderMarkersPreview.kt index 432e585803..5ea22ae855 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteHeaderMarkersPreview.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteHeaderMarkersPreview.kt @@ -29,6 +29,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.model.nip18Reposts.RepostAction.repost import com.vitorpamplona.amethyst.commons.ui.components.GenericLoadable import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.LocalCache @@ -100,22 +101,22 @@ fun NoteHeaderFirstRowDensityPreview() { val now = TimeUtils.now() val expiresAt = (now + 7200).toString() - val plain: Note - val repost: Note - val rumorPinned: Note - val edited: Note - val editVersion: Note - val draft: Note - val communityPost: Note - val geoPowExpiring: Note - val kitchenSink: Note - val plainEvent = TextNoteEvent(PLAIN_ID, AUTHOR, now - 300, emptyArray(), "GM", "x") // Drafts are addressable: consumption lands on the AddressableNote, so the // preview must fetch it by address rather than by event id. val draftEvent = DraftWrapEvent(DRAFT_ID, AUTHOR, now - 300, arrayOf(arrayOf("d", "preview-draft")), "", "x") + val plain: Note = LocalCache.getOrCreateNote(PLAIN_ID) + val repost: Note = LocalCache.getOrCreateNote(REPOST_ID) + val rumorPinned: Note = LocalCache.getOrCreateNote(RUMOR_ID) + val edited: Note = LocalCache.getOrCreateNote(EDITED_ID) + val editVersion: Note = LocalCache.getOrCreateNote(EDIT_VERSION_ID) + val draft: Note = LocalCache.getOrCreateAddressableNote(draftEvent.address()) + val communityPost: Note = LocalCache.getOrCreateNote(COMMUNITY_POST_ID) + val geoPowExpiring: Note = LocalCache.getOrCreateNote(POW_ID) + val kitchenSink: Note = LocalCache.getOrCreateNote(KITCHEN_SINK_ID) + runBlocking { withContext(Dispatchers.IO) { LocalCache.justConsume( @@ -176,16 +177,6 @@ fun NoteHeaderFirstRowDensityPreview() { null, true, ) - - plain = LocalCache.getOrCreateNote(PLAIN_ID) - repost = LocalCache.getOrCreateNote(REPOST_ID) - rumorPinned = LocalCache.getOrCreateNote(RUMOR_ID) - edited = LocalCache.getOrCreateNote(EDITED_ID) - editVersion = LocalCache.getOrCreateNote(EDIT_VERSION_ID) - draft = LocalCache.getOrCreateAddressableNote(draftEvent.address()) - communityPost = LocalCache.getOrCreateNote(COMMUNITY_POST_ID) - geoPowExpiring = LocalCache.getOrCreateNote(POW_ID) - kitchenSink = LocalCache.getOrCreateNote(KITCHEN_SINK_ID) } } From 41ef1d89b3856736a50999b0ac24ae87c4e5a687 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 14 Jul 2026 12:59:30 +0000 Subject: [PATCH 08/13] feat(ui): add the OpenTimestamps logo as a custom glyph and use it in the OTS pill Bake the official OpenTimestamps stamp logo (traced from opentimestamps/logo vector.svg; monochrome outline, tinted at render time like every other glyph) into the Material Symbols subset font at U+F8F0. New tools/material-symbols-subset/add_custom_glyphs.py converts the traced SVGs in custom/ into TrueType glyphs and is invoked by subset.sh after pyftsubset, so font regenerations keep them. With the logo identifying the pill, drop the verbose "OTS:" prefix: the pill now reads icon + "2y" (or icon + "Pending", new R.string.pending). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01AgEpNtwnetPhETXQSdq4b5 --- .../amethyst/ui/note/elements/DisplayOts.kt | 8 +- amethyst/src/main/res/values/strings.xml | 1 + .../font/material_symbols_outlined.ttf | Bin 464004 -> 466160 bytes .../commons/icons/symbols/MaterialSymbols.kt | 6 + tools/material-symbols-subset/README.md | 12 ++ .../add_custom_glyphs.py | 104 ++++++++++++++ .../custom/opentimestamps.svg | 130 ++++++++++++++++++ tools/material-symbols-subset/subset.sh | 4 + 8 files changed, 261 insertions(+), 4 deletions(-) create mode 100644 tools/material-symbols-subset/add_custom_glyphs.py create mode 100644 tools/material-symbols-subset/custom/opentimestamps.svg diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt index 319293d69c..ea62ebdb9d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt @@ -61,8 +61,8 @@ fun DisplayOts( } HeaderPill( - symbol = MaterialSymbols.CheckCircle, - text = stringRes(R.string.existed_since, timeStr), + symbol = MaterialSymbols.OpenTimestamps, + text = timeStr, contentDescription = stringRes(R.string.ots_info_title), onClick = { accountViewModel.toastManager.toast( @@ -75,8 +75,8 @@ fun DisplayOts( }, whenPending = { HeaderPill( - symbol = MaterialSymbols.Schedule, - text = stringRes(id = R.string.timestamp_pending_short), + symbol = MaterialSymbols.OpenTimestamps, + text = stringRes(id = R.string.pending), contentDescription = stringRes(R.string.ots_info_title), ) }, diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index a3ef6e6b7d..71c5cba0c2 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -45,6 +45,7 @@ Timestamp it Timestamp: Pending Confirmations OTS: Pending + Pending Request Deletion Block / Report diff --git a/commons/src/commonMain/composeResources/font/material_symbols_outlined.ttf b/commons/src/commonMain/composeResources/font/material_symbols_outlined.ttf index 01bd58472eab53e9922f2ac64092cd463a60370c..fde397e9efc2d552e8205b3b0ec61759f5f3a6df 100644 GIT binary patch delta 2930 zcmY*bYlu|W9Y62KJ@?#uXXeh#x-Z>_JIQ9N>+C!rQ4*V)q@)j%Xt9N2m$+(ZR+qS~ zrmyM4#)U{LEG7LILMW6#5VkZa{h&zFQXAUlL+O`N8d0&0QlSmxL#X|oJFcep%(?gc zpTGa>EPL|j!#965e5TgB`Vk@Y^kZMx`Q(c)t?VM?+6p0j@bgb?ZSH(x|3i@dlo0Zj zr}pgJbL!^uON6}tD(JW7zdrvQiT`nykn4v*?|yo5ere&`7f%u*t`VXx9)$eGe=oic z`J0emKQw=A3Ezy6tN+2*9D4TogBSjM_K&1T5;#0}{tyg)D8IFlkQ=WOwe>TH7v>Li ze|zQv0vLut=P(%hk{$!S2lV9O#pM(GUfX*U^oNA7o6jD7dj7quwM&G!Uj%w_{=^bJ z&Fhe#fc~Z<^NR~#dh&bk5b}ouNHAVHdTjZ`Urv3uN65Q~ta_4AsId;mgk#d_`EG@W4)Ny7h991oE~#9M>Njbm(zjBYD9JNbAq) z*564(1V2yD5V=}ks_~Y`#Okzgl{GhS7b%$?vC+(8t#0cfWKyG_gPa(AhxI zEya6=c3?g^O8dD4K|;$x6dsxsbOx0={RreVDo}IH zfY(3Pwv7jV0ImiP&H8K+NFgosv}yx5Sm->25Duz68<>3}-&SY@a@%YOFPwrBfa-Kp!a|JojI&-2N4C?_yi2#r~tTY0A?GjF$H<(m#Sii z72LH8s5=t$HE@GPW2#q}gi=?Feg|#Hrdb^<@b2^y@NFd4>>#&V6LXN~H8ckp(-QH9 zP#RNH0TSHMv0A-8*9>W4ZD;N6Y^unuF*_~bJFiv)WW85|yc@EC{rJ?x1i%{st0yk@Ok^toFA6@>~`A*O!H!3kXHFk{_LVN>7-Cj|vR z@Dbb=RX`&GDjG6QKcR%Af(xZ0XO*OsTds|)OP-}lN0Mu+;^{;((QsbSycAnUDOHt; z=^?MeDH*NglNpZEkLCG2iJ+AEo^@%N3m*?xlWfdLVPm(kRP}XhI0e9r)-y)q$VDSd z^U8#ze6W%;DmOPKD;7TF=7Cn0^Z|xieJD2=JI<%e-nz|MWQB-q;x;&471~P8m{7J7 zdnFX>ZDgKN@5keq$9$Aq!CgL>0-Q<1Q|K5rkd1{1gyo~mNQP8Kx!}wJoI&Qe(GR~PS)ETs3>{yRi5UTZ64LHiUvO_WIY^ici(*S`1 z(8|I=iAPMvkq!a7(ty!L0NgSWml$#>X+2Pim6R;YzFgLblX7KZM!BJCpgsFw$5UWX z7SXBuKl#8EwIF&hIk}+p|Rzi!o+>niwTyF^Fr#$u}5o|F9j4>OJIrP0@9T(Md zWGw7u^1#TbWjJIHk5q~v(k-|QVrv=-Z6+tjHX=1v$w#P|E>{M86>*z;$}X;!bKu4C zNMvgpe2jT;@u0&ElADKydz(Eb%GlIYMylafVai6hlq!xZhzPFHZc*_u?N1ZHFoUWb zSDi|=vzR~11ZH5^nK)Z#Xn{L3A}Dj17_U*3;8>Y7@i^k@_6G^E^x}9jj6SB=3l=6q zz*ff*Iw~=c7~Le6EaJ{>@}+>Jol{hDRWn`}bq0t|A|n7tnez4i&A{M`Qe0L{+5{62 zt_77Usv%uOU&d{qKM^I-vjdd~n3A%XmA&fs*HC}&6Oa6!VfHLsmYFtaA+vcsLK+-6 z+C(&+`QqcB-nXU9S*#dcKy2+cvt{&L3Jfv#p~$FVr;!G?+cEIUc+9%PpDC{6AU?p7 z$TB#r7zJKrd2vNFE;vW27*jft>Ik#Kl`uSzhsmPUYxEt(@IYd@RfrO*XkHL9%(uXh z0^J(J)tdgz7S503-;d8-y0*6IB4&&oUmJgCp`0d1mllpJKeM=SYDb!J-=3GN hH!km9rI)Y&V8@kHJFZY{hRv}BwtQp974_)W{{d$pa4G-* delta 750 zcmYjOUr1A76hG(Q{nce<(?myE>>;EYMU+UO1N-w3WNH#c*k#U*a&6>V69!t@Tlezj}a|2g1?ah(iJj& znQ^Qz zE+sbOE${`{K|7I!t^AO;D2ehPLX*k>VWl~p^M<-N9$qmBViG5`TldGxJB(ky+s7fSk(^9 zq|xDFISitwuu`TSW+keNuu^LXb4^hLS;;sEZHR~DRM{?hk~IQ%VJLGGxAM`FX3aC^ zRI%Q8ugZ9Lac$rOXwJHgD@zhKG5+hSu?a#GSD?aH{tIE_#` to patch the existing font). diff --git a/tools/material-symbols-subset/add_custom_glyphs.py b/tools/material-symbols-subset/add_custom_glyphs.py new file mode 100644 index 0000000000..158c98e438 --- /dev/null +++ b/tools/material-symbols-subset/add_custom_glyphs.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +"""Append custom (non-Material) glyphs to the subset Material Symbols font. + +Some icons Amethyst needs are third-party logos that Google's Material +Symbols font will never carry (e.g. the OpenTimestamps stamp). Their traced +SVG outlines live in `custom/` and this script bakes them into the subset +TTF at private-use codepoints, so `MaterialSymbols.kt` can reference them +exactly like any Material glyph. + +subset.sh runs this automatically after pyftsubset; it can also be run +directly against an existing font: + + python3 add_custom_glyphs.py + +The SVGs are potrace output: the raw path coordinates are y-up already (the +file's group transform re-flips them for SVG display), so they map into font +space with a plain uniform scale + translate. +""" + +import os +import re +import sys + +from fontTools.pens.boundsPen import BoundsPen +from fontTools.pens.cu2quPen import Cu2QuPen +from fontTools.pens.transformPen import TransformPen +from fontTools.pens.ttGlyphPen import TTGlyphPen +from fontTools.svgLib.path import parse_path +from fontTools.ttLib import TTFont + +HERE = os.path.dirname(os.path.abspath(__file__)) + +# codepoint -> (glyph name, traced SVG). Keep codepoints at the very end of +# the BMP private-use area (U+F8xx tail) to stay clear of the range Material +# Symbols actually allocates. +CUSTOM = { + 0xF8F0: ("opentimestamps", os.path.join(HERE, "custom", "opentimestamps.svg")), +} + +# Fit content into the same square Material Symbols draw in (its check_circle +# glyph spans 80..880 in a 960 upm font). Scaled to the font's real upm. +BOX_MIN_FRAC = 80 / 960 +BOX_MAX_FRAC = 880 / 960 + + +def svg_path_data(svg_file): + with open(svg_file, "r", encoding="utf-8") as f: + svg = f.read() + return re.findall(r']*\bd="([^"]+)"', svg) + + +def draw_svg(paths, pen): + for d in paths: + parse_path(d, pen) + + +def build_glyph(paths, upm): + # First pass: raw outline bounds. + bounds = BoundsPen(None) + draw_svg(paths, bounds) + x_min, y_min, x_max, y_max = bounds.bounds + + # Uniform scale into the Material content box, centered on both axes. + box_min = BOX_MIN_FRAC * upm + box_max = BOX_MAX_FRAC * upm + box = box_max - box_min + scale = box / max(x_max - x_min, y_max - y_min) + tx = box_min + (box - (x_max - x_min) * scale) / 2 - x_min * scale + ty = box_min + (box - (y_max - y_min) * scale) / 2 - y_min * scale + + tt_pen = TTGlyphPen(None) + quad_pen = Cu2QuPen(tt_pen, max_err=1.0) + draw_svg(paths, TransformPen(quad_pen, (scale, 0, 0, scale, tx, ty))) + return tt_pen.glyph() + + +def main(font_path): + font = TTFont(font_path) + upm = font["head"].unitsPerEm + order = font.getGlyphOrder() + + for codepoint, (name, svg_file) in sorted(CUSTOM.items()): + glyph = build_glyph(svg_path_data(svg_file), upm) + + if name not in order: + order.append(name) + font.setGlyphOrder(order) + font["glyf"][name] = glyph + glyph.recalcBounds(font["glyf"]) + font["hmtx"][name] = (upm, glyph.xMin) + for table in font["cmap"].tables: + if table.isUnicode(): + table.cmap[codepoint] = name + print(f"Added {name} at U+{codepoint:04X}") + + font["maxp"].numGlyphs = len(order) + font.save(font_path) + print(f"Wrote {font_path}") + + +if __name__ == "__main__": + if len(sys.argv) != 2: + sys.exit(__doc__) + main(sys.argv[1]) diff --git a/tools/material-symbols-subset/custom/opentimestamps.svg b/tools/material-symbols-subset/custom/opentimestamps.svg new file mode 100644 index 0000000000..054387593f --- /dev/null +++ b/tools/material-symbols-subset/custom/opentimestamps.svg @@ -0,0 +1,130 @@ + + + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + + + diff --git a/tools/material-symbols-subset/subset.sh b/tools/material-symbols-subset/subset.sh index 0689a78aec..640964d342 100755 --- a/tools/material-symbols-subset/subset.sh +++ b/tools/material-symbols-subset/subset.sh @@ -59,4 +59,8 @@ pyftsubset "$SOURCE_TTF" \ --glyph-names \ --symbol-cmap +# Bake in the custom (non-Material) glyphs from custom/ — third-party logos +# like OpenTimestamps that the upstream font will never carry. +python3 "$REPO_ROOT/tools/material-symbols-subset/add_custom_glyphs.py" "$TARGET" + echo "Wrote $TARGET ($(du -h "$TARGET" | cut -f1))" From 170bc7c121e0cdd1a4b36952096780c02a46910f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 14 Jul 2026 13:30:38 +0000 Subject: [PATCH 09/13] fix(ui): restore full-size quiet marks, tighten the dotted timestamp, drop Boosted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Quiet marks go back to the row's regular text size in bold with 16dp icons; the hashtag/community soft links lose their 12sp override too (the smaller tier read as too small). A new TimeAgoStyle.DottedTight renders "• 5m" without the leading space for rows whose spacedBy already provides the gap, removing the double space before the timestamp. The OTS pending pill shrinks to the stamp icon plus an ellipsis (the words move to the content description). The Boosted mark is removed entirely — from the Android header, the commons component, and the desktop feed — since the repost context is already visible. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01AgEpNtwnetPhETXQSdq4b5 --- .../amethyst/ui/note/NoteCompose.kt | 8 +--- .../amethyst/ui/note/elements/BoostedMark.kt | 31 -------------- .../ui/note/elements/DisplayCommunity.kt | 3 -- .../ui/note/elements/DisplayHashtags.kt | 3 -- .../amethyst/ui/note/elements/DisplayOts.kt | 6 ++- .../note/elements/NoteHeaderMarkersPreview.kt | 8 ---- .../amethyst/ui/note/elements/TimeAgo.kt | 22 ++++++++-- .../loggedIn/threadview/ThreadFeedView.kt | 3 +- amethyst/src/main/res/values/strings.xml | 1 - .../commons/ui/elements/BoostedMark.kt | 40 ------------------- .../amethyst/commons/ui/note/QuietMark.kt | 15 +++---- .../amethyst/desktop/ui/FeedScreen.kt | 2 - 12 files changed, 31 insertions(+), 111 deletions(-) delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/BoostedMark.kt delete mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/elements/BoostedMark.kt 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 041d3b37ff..f28d0fc270 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 @@ -78,7 +78,6 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.routeEditDraftTo import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.DisplayZapSplits -import com.vitorpamplona.amethyst.ui.note.elements.BoostedMark import com.vitorpamplona.amethyst.ui.note.elements.DisplayEditStatus import com.vitorpamplona.amethyst.ui.note.elements.DisplayFollowingCommunityInPost import com.vitorpamplona.amethyst.ui.note.elements.DisplayFollowingHashtagsInPost @@ -91,6 +90,7 @@ import com.vitorpamplona.amethyst.ui.note.elements.Reward import com.vitorpamplona.amethyst.ui.note.elements.ShowForkInformation import com.vitorpamplona.amethyst.ui.note.elements.StaleRelayHint import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo +import com.vitorpamplona.amethyst.ui.note.elements.TimeAgoStyle import com.vitorpamplona.amethyst.ui.note.types.BadgeDisplay import com.vitorpamplona.amethyst.ui.note.types.DisplayBlockedRelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayBroadcastRelayList @@ -1834,10 +1834,6 @@ fun FirstUserInfoRow( NoteUsernameDisplay(baseNote, Modifier.weight(1f), textColor = textColor, accountViewModel = accountViewModel) } - if (isRepost) { - BoostedMark() - } - CheckAndDisplayEditStatus(editState) if (isDraft) { @@ -1899,7 +1895,7 @@ fun FirstUserInfoRow( // The dotted TimeAgo carries its own " • " separator and the options // button has slack around its icon, so this pair stays unspaced. Row(verticalAlignment = CenterVertically) { - TimeAgo(baseNote) + TimeAgo(baseNote, style = TimeAgoStyle.DottedTight) if (moreOptions == null) { MoreOptionsButton(baseNote, editState, accountViewModel, nav) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/BoostedMark.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/BoostedMark.kt deleted file mode 100644 index c48531aaca..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/BoostedMark.kt +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.ui.note.elements - -import androidx.compose.runtime.Composable -import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.ui.note.QuietMark -import com.vitorpamplona.amethyst.ui.stringRes - -@Composable -fun BoostedMark() { - QuietMark(text = stringRes(id = R.string.boosted)) -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayCommunity.kt index 64b27c68a5..a2a68c1fca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayCommunity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayCommunity.kt @@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.ui.note.elements import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -35,7 +34,6 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNo import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.theme.Font12SP import com.vitorpamplona.amethyst.ui.theme.lessImportantLink import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip72ModCommunities.communityAddress @@ -66,7 +64,6 @@ private fun DisplayCommunity( ClickableTextColor( label, - style = LocalTextStyle.current.copy(fontSize = Font12SP), linkColor = MaterialTheme.colorScheme.lessImportantLink, overflow = TextOverflow.Ellipsis, maxLines = 1, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayHashtags.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayHashtags.kt index cc48d6d08e..cd56c146fc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayHashtags.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayHashtags.kt @@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.note.elements import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -39,7 +38,6 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.theme.Font12SP import com.vitorpamplona.amethyst.ui.theme.lessImportantLink import com.vitorpamplona.quartz.nip01Core.tags.hashtags.firstIsTaggedHashes import com.vitorpamplona.quartz.nip22Comments.CommentEvent @@ -85,7 +83,6 @@ private fun DisplayTagList( ) { ClickableTextColor( "#$firstTag", - style = LocalTextStyle.current.copy(fontSize = Font12SP), linkColor = MaterialTheme.colorScheme.lessImportantLink, overflow = TextOverflow.Ellipsis, maxLines = 1, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt index ea62ebdb9d..43ffee19e3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt @@ -74,10 +74,12 @@ fun DisplayOts( ) }, whenPending = { + // The stamp icon plus an ellipsis: attestation requested, not yet + // anchored. The content description carries the words. HeaderPill( symbol = MaterialSymbols.OpenTimestamps, - text = stringRes(id = R.string.pending), - contentDescription = stringRes(R.string.ots_info_title), + text = "…", + contentDescription = stringRes(R.string.timestamp_pending_short), ) }, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteHeaderMarkersPreview.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteHeaderMarkersPreview.kt index 5ea22ae855..efac0ed3ed 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteHeaderMarkersPreview.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteHeaderMarkersPreview.kt @@ -29,7 +29,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.commons.model.nip18Reposts.RepostAction.repost import com.vitorpamplona.amethyst.commons.ui.components.GenericLoadable import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.LocalCache @@ -44,7 +43,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent -import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.coroutines.Dispatchers @@ -59,7 +57,6 @@ private val AUTHOR_METADATA_ID = "e1".repeat(32) private val LONG_NAME_METADATA_ID = "e2".repeat(32) private val PLAIN_ID = "1".repeat(64) -private val REPOST_ID = "2".repeat(64) private val RUMOR_ID = "3".repeat(64) private val EDITED_ID = "4".repeat(64) private val EDIT_VERSION_ID = "5".repeat(64) @@ -108,7 +105,6 @@ fun NoteHeaderFirstRowDensityPreview() { val draftEvent = DraftWrapEvent(DRAFT_ID, AUTHOR, now - 300, arrayOf(arrayOf("d", "preview-draft")), "", "x") val plain: Note = LocalCache.getOrCreateNote(PLAIN_ID) - val repost: Note = LocalCache.getOrCreateNote(REPOST_ID) val rumorPinned: Note = LocalCache.getOrCreateNote(RUMOR_ID) val edited: Note = LocalCache.getOrCreateNote(EDITED_ID) val editVersion: Note = LocalCache.getOrCreateNote(EDIT_VERSION_ID) @@ -132,7 +128,6 @@ fun NoteHeaderFirstRowDensityPreview() { LocalCache.justConsume(plainEvent, null, true) LocalCache.justConsume(TextNoteEvent(EDIT_VERSION_ID, AUTHOR, now - 60, emptyArray(), "GM! (fixed typo)", "x"), null, true) - LocalCache.justConsume(RepostEvent(REPOST_ID, AUTHOR, now - 300, arrayOf(arrayOf("e", PLAIN_ID)), plainEvent.toJson(), "x"), null, true) // An empty sig is what marks a note as a private rumor. LocalCache.justConsume(TextNoteEvent(RUMOR_ID, AUTHOR, now - 300, emptyArray(), "just between us", ""), null, true) LocalCache.justConsume(TextNoteEvent(EDITED_ID, AUTHOR, now - 300, emptyArray(), "GM!", "x"), null, true) @@ -197,9 +192,6 @@ fun NoteHeaderFirstRowDensityPreview() { // Bare minimum: username + time + options HeaderRowSample(plain, accountViewModel) - // One quiet mark: repost - HeaderRowSample(repost, accountViewModel) - // Quiet icon marks: private rumor + pinned HeaderRowSample(rumorPinned, accountViewModel, isPinned = true) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/TimeAgo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/TimeAgo.kt index c23fd2d41c..a552e99cfa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/TimeAgo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/TimeAgo.kt @@ -53,6 +53,12 @@ enum class TimeAgoStyle { /** "• 5m", uses [timeAgo]. Used by note timestamps and chatroom row last-message time. */ Dotted, + /** + * "• 5m" without the leading space — for rows whose `Arrangement.spacedBy` + * already provides the gap before the timestamp (the note-header first row). + */ + DottedTight, + /** "5m" (no dot, no leading space), uses [timeAgoShort]. Used by chat bubbles and channel headers. */ Short, } @@ -96,6 +102,7 @@ fun ToggleableTimeAgoText( if (showAbsolute) { when (style) { TimeAgoStyle.Dotted -> timeAbsolute(timestamp, context) + TimeAgoStyle.DottedTight -> timeAbsolute(timestamp, context).trimStart() TimeAgoStyle.Short -> timeAbsoluteNoDot(timestamp, context) } } else { @@ -104,6 +111,7 @@ fun ToggleableTimeAgoText( nowState.value when (style) { TimeAgoStyle.Dotted -> timeAgo(timestamp, context) + TimeAgoStyle.DottedTight -> timeAgo(timestamp, context).trimStart() TimeAgoStyle.Short -> timeAgoShort(timestamp, nowStr) } } @@ -125,16 +133,22 @@ fun ToggleableTimeAgoText( } @Composable -fun TimeAgo(note: Note) { +fun TimeAgo( + note: Note, + style: TimeAgoStyle = TimeAgoStyle.Dotted, +) { val time = note.createdAt() ?: return - TimeAgo(time) + TimeAgo(time, style) } @Composable -fun TimeAgo(time: Long) { +fun TimeAgo( + time: Long, + style: TimeAgoStyle = TimeAgoStyle.Dotted, +) { ToggleableTimeAgoText( timestamp = time, - style = TimeAgoStyle.Dotted, + style = style, color = MaterialTheme.colorScheme.placeholderText, ) } 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 f2762f30bf..d3a2b8a1dd 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 @@ -127,6 +127,7 @@ import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton import com.vitorpamplona.amethyst.ui.note.elements.Reward import com.vitorpamplona.amethyst.ui.note.elements.ShowForkInformation import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo +import com.vitorpamplona.amethyst.ui.note.elements.TimeAgoStyle import com.vitorpamplona.amethyst.ui.note.observeEdits import com.vitorpamplona.amethyst.ui.note.showAmount import com.vitorpamplona.amethyst.ui.note.types.AudioHeader @@ -744,7 +745,7 @@ private fun FullBleedNoteCompose( // The dotted TimeAgo carries its own " • " separator and the // options button has slack around its icon: keep the pair unspaced. Row(verticalAlignment = Alignment.CenterVertically) { - TimeAgo(note = baseNote) + TimeAgo(note = baseNote, style = TimeAgoStyle.DottedTight) MoreOptionsButton(baseNote, editState, accountViewModel, nav) } diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 71c5cba0c2..a3ef6e6b7d 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -45,7 +45,6 @@ Timestamp it Timestamp: Pending Confirmations OTS: Pending - Pending Request Deletion Block / Report diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/elements/BoostedMark.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/elements/BoostedMark.kt deleted file mode 100644 index 8308fdfcfc..0000000000 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/elements/BoostedMark.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.commons.ui.elements - -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp - -@Composable -fun BoostedMark() { - Text( - "Boosted", - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 1, - modifier = Modifier.padding(start = 5.dp), - ) -} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/QuietMark.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/QuietMark.kt index 8f3cea79a7..2e0374c0d1 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/QuietMark.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/QuietMark.kt @@ -31,18 +31,15 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText -private val QuietMarkFontSize = 12.sp - /** - * Quiet passive-state marker for note headers: Boosted, Draft, Edited, pinned, + * Quiet passive-state marker for note headers: Draft, Edited, pinned, * private rumor, ... One spec for all of them so the header reads as a single - * system: 12sp medium gray text with an optional 12dp icon. Contrast with - * [HeaderPill], which is the loud tier for tappable/verifiable metadata. + * system: bold gray text at the row's text size with an optional 16dp icon. + * Contrast with [HeaderPill], the chip tier for tappable/verifiable metadata. * * Spacing between markers is owned by the parent row (`Arrangement.spacedBy`), * not baked in here. @@ -65,16 +62,14 @@ fun QuietMark( symbol = symbol, contentDescription = contentDescription, tint = MaterialTheme.colorScheme.placeholderText, - modifier = Modifier.size(12.dp), + modifier = Modifier.size(16.dp), ) } if (text != null) { Text( text = text, color = MaterialTheme.colorScheme.placeholderText, - fontSize = QuietMarkFontSize, - lineHeight = QuietMarkFontSize, - fontWeight = FontWeight.Medium, + fontWeight = FontWeight.Bold, maxLines = 1, ) } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt index ec07c0e06b..1eace52ec5 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt @@ -99,7 +99,6 @@ import com.vitorpamplona.amethyst.commons.search.QuerySerializer import com.vitorpamplona.amethyst.commons.search.SearchResultFilter import com.vitorpamplona.amethyst.commons.ui.components.EmptyState import com.vitorpamplona.amethyst.commons.ui.components.LoadingState -import com.vitorpamplona.amethyst.commons.ui.elements.BoostedMark import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState import com.vitorpamplona.amethyst.commons.ui.feeds.NewPostsChip import com.vitorpamplona.amethyst.commons.ui.feeds.StickToTopOnPrepend @@ -304,7 +303,6 @@ private fun FeedNoteCardBody( ) }, ) - BoostedMark() } // Original note content with actions inside card From 811d9cf80243b596ef8bdbe2f60295b697d39204 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 14 Jul 2026 13:46:12 +0000 Subject: [PATCH 10/13] feat(ui): icon-only fork mark, pencil edit mark, tighter expiration and location pills - The fork marker drops "Forked from " for a bare fork-right icon (new MaterialSymbols.ForkRight, U+EBAC); tapping still opens the original version. Font regenerated via subset.sh, which also re-baked the custom OpenTimestamps glyph, proving the custom-glyph pipeline survives regeneration. - The edited mark becomes a pencil: bare pencil for the latest edit, pencil + "#2"/"original" only while cycling versions on tap. - The expiration pill clamps beyond one year to "1y+" instead of switching to a full date. - The location pill caps at 110dp and ellipsizes, so unbounded city names cannot squeeze the author's name out of the row. - Remove the now-unused existed_since string from all 35 locale files. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01AgEpNtwnetPhETXQSdq4b5 --- .../amethyst/ui/note/NoteCompose.kt | 11 +++++- .../ui/note/elements/DisplayEditStatus.kt | 16 ++++++-- .../ui/note/elements/DisplayLocation.kt | 6 +++ .../amethyst/ui/note/elements/ForkInfo.kt | 36 ++++++------------ amethyst/src/main/res/values-cs/strings.xml | 1 - .../src/main/res/values-de-rDE/strings.xml | 1 - amethyst/src/main/res/values-de/strings.xml | 1 - amethyst/src/main/res/values-eo/strings.xml | 1 - .../src/main/res/values-es-rES/strings.xml | 1 - .../src/main/res/values-es-rMX/strings.xml | 1 - .../src/main/res/values-es-rUS/strings.xml | 1 - amethyst/src/main/res/values-es/strings.xml | 2 - .../src/main/res/values-fa-rIR/strings.xml | 1 - amethyst/src/main/res/values-fa/strings.xml | 1 - .../src/main/res/values-fr-rFR/strings.xml | 1 - amethyst/src/main/res/values-fr/strings.xml | 1 - .../src/main/res/values-hi-rIN/strings.xml | 1 - .../src/main/res/values-hu-rHU/strings.xml | 1 - amethyst/src/main/res/values-in/strings.xml | 2 - amethyst/src/main/res/values-ja/strings.xml | 2 - .../src/main/res/values-nl-rBE/strings.xml | 2 - .../src/main/res/values-nl-rNL/strings.xml | 1 - amethyst/src/main/res/values-nl/strings.xml | 1 - .../src/main/res/values-pl-rPL/strings.xml | 1 - .../src/main/res/values-pt-rBR/strings.xml | 1 - amethyst/src/main/res/values-ru/strings.xml | 2 - .../src/main/res/values-sl-rSI/strings.xml | 1 - .../src/main/res/values-sv-rSE/strings.xml | 1 - amethyst/src/main/res/values-ta/strings.xml | 1 - .../src/main/res/values-th-rTH/strings.xml | 1 - amethyst/src/main/res/values-th/strings.xml | 1 - amethyst/src/main/res/values-tr/strings.xml | 2 - amethyst/src/main/res/values-uk/strings.xml | 2 - .../src/main/res/values-zh-rCN/strings.xml | 1 - .../src/main/res/values-zh-rHK/strings.xml | 1 - .../src/main/res/values-zh-rSG/strings.xml | 1 - .../src/main/res/values-zh-rTW/strings.xml | 1 - amethyst/src/main/res/values-zh/strings.xml | 2 - amethyst/src/main/res/values/strings.xml | 2 +- .../font/material_symbols_outlined.ttf | Bin 466160 -> 469812 bytes .../commons/icons/symbols/MaterialSymbols.kt | 1 + 41 files changed, 42 insertions(+), 72 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 f28d0fc270..56c40f13d6 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 @@ -351,6 +351,7 @@ import com.vitorpamplona.quartz.nipF4Podcasts.metadata.PodcastMetadataEvent import com.vitorpamplona.quartz.nipXXPodcasting20.episode.Podcasting20EpisodeEvent import com.vitorpamplona.quartz.nipXXPodcasting20.metadata.Podcasting20PodcastMetadata import com.vitorpamplona.quartz.nipXXPodcasting20.trailer.Podcasting20TrailerEvent +import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.withContext @@ -1762,9 +1763,17 @@ fun SecondUserInfoRow( @Composable fun DisplayExpiration(expirationDate: Long) { val context = LocalContext.current + // Beyond a year timeAheadNoDot switches to a full date, which is too wide + // for the pill: clamp it. + val text = + if (expirationDate - TimeUtils.now() > TimeUtils.ONE_YEAR) { + stringRes(R.string.one_year_plus) + } else { + timeAheadNoDot(expirationDate, context) + } HeaderPill( symbol = MaterialSymbols.Timer, - text = timeAheadNoDot(expirationDate, context), + text = text, contentDescription = stringRes(R.string.expiration_date_label), ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayEditStatus.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayEditStatus.kt index 0ffae65a1d..355d205e2a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayEditStatus.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayEditStatus.kt @@ -22,23 +22,31 @@ package com.vitorpamplona.amethyst.ui.note.elements import androidx.compose.runtime.Composable import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.ui.note.QuietMark import com.vitorpamplona.amethyst.ui.note.types.EditState import com.vitorpamplona.amethyst.ui.stringRes +/** + * Pencil marking an edited note; tapping cycles through the versions. The + * bare pencil is the latest edit — a suffix appears only while browsing + * older versions. + */ @Composable fun DisplayEditStatus(editState: EditState) { val label = - if (editState.showingVersion.value == editState.originalVersionId()) { + if (editState.showingVersion.value == editState.lastVersionId()) { + null + } else if (editState.showingVersion.value == editState.originalVersionId()) { stringRes(id = R.string.original) - } else if (editState.showingVersion.value == editState.lastVersionId()) { - stringRes(id = R.string.edited) } else { - stringRes(id = R.string.edited_number, editState.versionId()) + "#${editState.versionId()}" } QuietMark( + symbol = MaterialSymbols.Edit, text = label, + contentDescription = stringRes(id = R.string.edited), onClick = { editState.nextModification() }, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayLocation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayLocation.kt index a32e65d1b5..e533cc1cb3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayLocation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayLocation.kt @@ -20,7 +20,10 @@ */ package com.vitorpamplona.amethyst.ui.note.elements +import androidx.compose.foundation.layout.widthIn import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.ui.note.HeaderPill import com.vitorpamplona.amethyst.ui.navigation.navs.INav @@ -31,6 +34,8 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel /** * Compact pill showing the geohash a note is scoped to, resolved to a city * name. Tapping it opens the geohash feed. Sits inline in note headers. + * City names are unbounded user data, so the pill is capped and ellipsizes + * to protect the author's name from being squeezed out of the row. */ @Composable fun DisplayLocation( @@ -42,6 +47,7 @@ fun DisplayLocation( HeaderPill( symbol = MaterialSymbols.LocationOn, text = cityName, + modifier = Modifier.widthIn(max = 110.dp), onClick = { nav.nav(Route.Geohash(geohashStr)) }, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt index 7b6486bfd9..d9a32e94a8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt @@ -20,26 +20,21 @@ */ package com.vitorpamplona.amethyst.ui.note.elements -import androidx.compose.foundation.layout.Row -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.ui.note.QuietMark import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo -import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji import com.vitorpamplona.amethyst.ui.components.LoadNote import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.amethyst.ui.theme.Font14SP -import com.vitorpamplona.amethyst.ui.theme.lessImportantLink import com.vitorpamplona.quartz.experimental.forks.IForkableEvent @Composable @@ -54,20 +49,21 @@ fun ShowForkInformation( if (forkedAddress != null) { LoadAddressableNote(forkedAddress, accountViewModel) { addressableNote -> if (addressableNote != null) { - ForkInformationRowLightColor(addressableNote, modifier, accountViewModel, nav) + ForkMark(addressableNote, modifier, accountViewModel, nav) } } } else if (forkedEvent != null) { LoadNote(forkedEvent, accountViewModel) { event -> if (event != null) { - ForkInformationRowLightColor(event, modifier, accountViewModel, nav) + ForkMark(event, modifier, accountViewModel, nav) } } } } +/** Fork-right icon marking a forked note; tapping opens the original version. */ @Composable -fun ForkInformationRowLightColor( +fun ForkMark( originalVersion: Note, modifier: Modifier = Modifier, accountViewModel: AccountViewModel, @@ -75,22 +71,14 @@ fun ForkInformationRowLightColor( ) { val noteState by observeNote(originalVersion, accountViewModel) val note = noteState.note - val author = note.author ?: return val route = remember(note) { routeFor(note, accountViewModel.account) } if (route != null) { - Row(modifier, verticalAlignment = Alignment.CenterVertically) { - val userState by observeUserInfo(author, accountViewModel) - - CreateClickableTextWithEmoji( - clickablePart = stringRes(id = R.string.forked_from) + " " + (userState?.info?.bestName() ?: author.pubkeyDisplayHex()), - maxLines = 1, - route = route, - overrideColor = MaterialTheme.colorScheme.lessImportantLink, - fontSize = Font14SP, - nav = nav, - tags = userState?.tags, - ) - } + QuietMark( + symbol = MaterialSymbols.ForkRight, + contentDescription = stringRes(id = R.string.forked_from), + modifier = modifier, + onClick = { nav.nav(route) }, + ) } } diff --git a/amethyst/src/main/res/values-cs/strings.xml b/amethyst/src/main/res/values-cs/strings.xml index f9a50c85e7..b27e809d56 100644 --- a/amethyst/src/main/res/values-cs/strings.xml +++ b/amethyst/src/main/res/values-cs/strings.xml @@ -2797,7 +2797,6 @@ Zdroj: Servery: Otevřít - OTS: %1$s Důkaz časového razítka Existuje důkaz, že tento příspěvek byl podepsán někdy před %1$s. Důkaz byl označen v Bitcoin blockchainu v tomto datu a čase. Upravit článek diff --git a/amethyst/src/main/res/values-de-rDE/strings.xml b/amethyst/src/main/res/values-de-rDE/strings.xml index 090be920d0..1f106783f7 100644 --- a/amethyst/src/main/res/values-de-rDE/strings.xml +++ b/amethyst/src/main/res/values-de-rDE/strings.xml @@ -2683,7 +2683,6 @@ Quelle: Server: Öffnen - OTS: %1$s Zeitstempel Beweis Es gibt einen Beweis, dass dieser Beitrag irgendwann vor %1$s signiert wurde. Der Beweis wurde zu diesem Datum und Uhrzeit in der Bitcoin-Blockchain gestempelt. Artikel bearbeiten diff --git a/amethyst/src/main/res/values-de/strings.xml b/amethyst/src/main/res/values-de/strings.xml index 12b2abea2c..33afa5019d 100644 --- a/amethyst/src/main/res/values-de/strings.xml +++ b/amethyst/src/main/res/values-de/strings.xml @@ -680,7 +680,6 @@ anz der Bedingungen ist erforderlich Geforkt von Internet: Klonen: - OTS: %1$s Zeitstempel Beweis Es gibt einen Beweis, dass dieser Beitrag irgendwann vor %1$s signiert wurde. Der Beweis wurde zu diesem Datum und Uhrzeit in der Bitcoin-Blockchain gestempelt. Beitrag bearbeiten diff --git a/amethyst/src/main/res/values-eo/strings.xml b/amethyst/src/main/res/values-eo/strings.xml index 0c1c4e18cf..2d610d685d 100644 --- a/amethyst/src/main/res/values-eo/strings.xml +++ b/amethyst/src/main/res/values-eo/strings.xml @@ -2125,7 +2125,6 @@ Fonto: Serviloj: Malfermi -OTS: %1$s Tempomarko-Pruvo Estas pruvo ke ĉi tiu afiŝo estis subskribita iam antaŭ %1$s. La pruvo estis stampita en la Bitcoin-blokĉeno en tiu dato kaj horo. Redakti Artikolon diff --git a/amethyst/src/main/res/values-es-rES/strings.xml b/amethyst/src/main/res/values-es-rES/strings.xml index 4243cf8549..8a758dad23 100644 --- a/amethyst/src/main/res/values-es-rES/strings.xml +++ b/amethyst/src/main/res/values-es-rES/strings.xml @@ -2297,7 +2297,6 @@ Fuente: Servidores: Abrir - OTS: %1$s Prueba de marca de tiempo Hay prueba de que esta publicación se firmó en algún momento antes de %1$s. La prueba se marcó en la cadena de bloques de Bitcoin en esa fecha y hora. Editar artículo diff --git a/amethyst/src/main/res/values-es-rMX/strings.xml b/amethyst/src/main/res/values-es-rMX/strings.xml index 6df3bf7d0f..6c76d29dcc 100644 --- a/amethyst/src/main/res/values-es-rMX/strings.xml +++ b/amethyst/src/main/res/values-es-rMX/strings.xml @@ -2288,7 +2288,6 @@ Fuente: Servidores: Abrir - OTS: %1$s Prueba de marca de tiempo Hay prueba de que esta publicación se firmó en algún momento antes de %1$s. La prueba se marcó en la cadena de bloques de Bitcoin en esa fecha y hora. Editar artículo diff --git a/amethyst/src/main/res/values-es-rUS/strings.xml b/amethyst/src/main/res/values-es-rUS/strings.xml index 05a196bd7d..feb5c202e2 100644 --- a/amethyst/src/main/res/values-es-rUS/strings.xml +++ b/amethyst/src/main/res/values-es-rUS/strings.xml @@ -2288,7 +2288,6 @@ Fuente: Servidores: Abrir - OTS: %1$s Prueba de marca de tiempo Hay prueba de que esta publicación se firmó en algún momento antes de %1$s. La prueba se marcó en la cadena de bloques de Bitcoin en esa fecha y hora. Editar artículo diff --git a/amethyst/src/main/res/values-es/strings.xml b/amethyst/src/main/res/values-es/strings.xml index 8ca8201d19..8af516d72d 100644 --- a/amethyst/src/main/res/values-es/strings.xml +++ b/amethyst/src/main/res/values-es/strings.xml @@ -2618,8 +2618,6 @@ Clon: -OTS: %1$s - Prueba de marca de tiempo Hay prueba de que esta publicación se firmó en algún momento antes de %1$s. La prueba se marcó en la cadena de bloques de Bitcoin en esa fecha y hora. diff --git a/amethyst/src/main/res/values-fa-rIR/strings.xml b/amethyst/src/main/res/values-fa-rIR/strings.xml index 10dad02bfb..429bc76504 100644 --- a/amethyst/src/main/res/values-fa-rIR/strings.xml +++ b/amethyst/src/main/res/values-fa-rIR/strings.xml @@ -2253,7 +2253,6 @@ منبع: سرورها: باز کردن - OTS: %1$s اثبات مهر زمان مدرکی وجود دارد که ثابت کند این پست پیش از %1$s امضا شده است. این مدرک در بلاکچین بیتکوین در آن تاریخ و زمان ثبت شده است. ویرایش مقاله diff --git a/amethyst/src/main/res/values-fa/strings.xml b/amethyst/src/main/res/values-fa/strings.xml index d2c9bce73b..1cf69b0c1c 100644 --- a/amethyst/src/main/res/values-fa/strings.xml +++ b/amethyst/src/main/res/values-fa/strings.xml @@ -665,7 +665,6 @@ انشعاب شده از وب: نسخه برداری: - OTS: %1$s اثبات مهر زمان مدرکی وجود دارد که ثابت کند این پست پیش از %1$s امضا شده است. این مدرک در بلاکچین بیتکوین در آن تاریخ و زمان ثبت شده است. ویرایش نوشته diff --git a/amethyst/src/main/res/values-fr-rFR/strings.xml b/amethyst/src/main/res/values-fr-rFR/strings.xml index 5caec58f87..3b445362af 100644 --- a/amethyst/src/main/res/values-fr-rFR/strings.xml +++ b/amethyst/src/main/res/values-fr-rFR/strings.xml @@ -2511,7 +2511,6 @@ Source : Serveurs : Ouvrir - OTS: %1$s Preuve d\'horodatage Il y a une preuve que ce message a été signé avant %1$s. La preuve a été estampillée dans la blockchain Bitcoin à cette date et à cette heure. Modifier l\'article diff --git a/amethyst/src/main/res/values-fr/strings.xml b/amethyst/src/main/res/values-fr/strings.xml index abbf896ec1..6bb2c6ea7c 100644 --- a/amethyst/src/main/res/values-fr/strings.xml +++ b/amethyst/src/main/res/values-fr/strings.xml @@ -670,7 +670,6 @@ Forké depuis Web : Clone : - OTS: %1$s Preuve d\'horodatage Il y a une preuve que ce message a été signé avant %1$s. La preuve a été estampillée dans la blockchain Bitcoin à cette date et à cette heure. Modifier le Message diff --git a/amethyst/src/main/res/values-hi-rIN/strings.xml b/amethyst/src/main/res/values-hi-rIN/strings.xml index 90d73c13a3..1124b60683 100644 --- a/amethyst/src/main/res/values-hi-rIN/strings.xml +++ b/amethyst/src/main/res/values-hi-rIN/strings.xml @@ -2730,7 +2730,6 @@ स्रोत : सेवासंगणक : खोलें - ओ॰टी॰एस : %1$s समयांकन प्रमाण प्रमाण उपलब्ध है कि इस पत्र पर हस्ताक्षर किया गया %1$s के कुछ पहले। प्रमाण अंकित किया गया द्व्यंकरूप्य खण्डश्रृंखला में उस समय उस दिन पर। लेख सम्पादित करें diff --git a/amethyst/src/main/res/values-hu-rHU/strings.xml b/amethyst/src/main/res/values-hu-rHU/strings.xml index c667c9f116..517a94c213 100644 --- a/amethyst/src/main/res/values-hu-rHU/strings.xml +++ b/amethyst/src/main/res/values-hu-rHU/strings.xml @@ -2731,7 +2731,6 @@ Forrás: Kiszolgálók: Megnyitás - OTS: %1$s Időbélyeg-igazolás Bizonyíték van arra, hogy ezt a bejegyzést valamikor %1$s előtt írták alá. A bizonyítékot ezen a napon és időpontban bélyegezték a Bitcoin blokkláncába. Cikk szerkesztése diff --git a/amethyst/src/main/res/values-in/strings.xml b/amethyst/src/main/res/values-in/strings.xml index 51cc6610c8..bcb14b65b9 100644 --- a/amethyst/src/main/res/values-in/strings.xml +++ b/amethyst/src/main/res/values-in/strings.xml @@ -2726,8 +2726,6 @@ Buka - OTS: %1$s - Bukti Stempel Waktu Ada bukti bahwa postingan ini ditandatangani sebelum %1$s. Bukti ini dicap dalam blockchain Bitcoin pada tanggal dan waktu tersebut. diff --git a/amethyst/src/main/res/values-ja/strings.xml b/amethyst/src/main/res/values-ja/strings.xml index 8ddae5afed..59ec50d5b1 100644 --- a/amethyst/src/main/res/values-ja/strings.xml +++ b/amethyst/src/main/res/values-ja/strings.xml @@ -2818,8 +2818,6 @@ 開く - OTS: %1$s - タイムスタンプ証明 この投稿が %1$s より前に署名されたことの証明があります。この証明はその日時に Bitcoin ブロックチェーンに刻印されています。 diff --git a/amethyst/src/main/res/values-nl-rBE/strings.xml b/amethyst/src/main/res/values-nl-rBE/strings.xml index 73a4c7371e..6bb2bf83b3 100644 --- a/amethyst/src/main/res/values-nl-rBE/strings.xml +++ b/amethyst/src/main/res/values-nl-rBE/strings.xml @@ -3798,8 +3798,6 @@ Servers: -OTS: %1$s - Bewijs van tijdstempel Er is cryptografisch bewijs dat dit bericht vóór %1$s is ondertekend. Dit bewijs is vastgelegd in de Bitcoin-blockchain. diff --git a/amethyst/src/main/res/values-nl-rNL/strings.xml b/amethyst/src/main/res/values-nl-rNL/strings.xml index 48933a0fb3..4bed90f621 100644 --- a/amethyst/src/main/res/values-nl-rNL/strings.xml +++ b/amethyst/src/main/res/values-nl-rNL/strings.xml @@ -2566,7 +2566,6 @@ Bron: Servers: Openen - OTS: %1$s Bewijs van tijdstempel Er is cryptografisch bewijs dat dit bericht vóór %1$s is ondertekend. Dit bewijs is vastgelegd in de Bitcoin-blockchain. Artikel bewerken diff --git a/amethyst/src/main/res/values-nl/strings.xml b/amethyst/src/main/res/values-nl/strings.xml index 6ab57fbb53..e43d1b58d8 100644 --- a/amethyst/src/main/res/values-nl/strings.xml +++ b/amethyst/src/main/res/values-nl/strings.xml @@ -669,7 +669,6 @@ Geforked van Web: Kopiëren: - OTS: %1$s Bewijs van tijdstempel Er is bewijs dat dit bericht enige tijd voor %1$sis getekend. Het bewijs is op dat tijdstip en op dat moment getypeerd in de Bitcoin-blockchain. Bericht wijzigen diff --git a/amethyst/src/main/res/values-pl-rPL/strings.xml b/amethyst/src/main/res/values-pl-rPL/strings.xml index cf336b07d8..23c0a216cf 100644 --- a/amethyst/src/main/res/values-pl-rPL/strings.xml +++ b/amethyst/src/main/res/values-pl-rPL/strings.xml @@ -2798,7 +2798,6 @@ Zaplanowane posty z innych kont nie zostaną opublikowane, dopóki to konto jest Źródło: Serwery: Otwórz - OTS: %1$s Potwierdzenie znacznika czasu Istnieje dowód na to, że ten post został podpisany przed %1$s. Dowód został opatrzony pieczęcią w łańcuchu bloków Bitcoin w tym dniu i czasie. Redaguj artykuł diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index 2a7f29143b..4b0e0381de 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -2680,7 +2680,6 @@ Fonte: Servidores: Abrir - OTS: %1$s Prova de Carimbo de data/hora Há prova de que esta postagem foi assinada antes de %1$s. A prova foi carimbada no blockchain do Bitcoin naquela data e hora. Editar artigo diff --git a/amethyst/src/main/res/values-ru/strings.xml b/amethyst/src/main/res/values-ru/strings.xml index 6d6d33c1ad..cc57214b33 100644 --- a/amethyst/src/main/res/values-ru/strings.xml +++ b/amethyst/src/main/res/values-ru/strings.xml @@ -2596,8 +2596,6 @@ Открыть - OTS: %1$s - Доказательство временно́й метки Есть доказательство того, что эта публикация была подписана до %1$s. Доказательство зафиксировано в блокчейне Bitcoin в указанную дату и время. diff --git a/amethyst/src/main/res/values-sl-rSI/strings.xml b/amethyst/src/main/res/values-sl-rSI/strings.xml index 7470dcfef9..53783ca50f 100644 --- a/amethyst/src/main/res/values-sl-rSI/strings.xml +++ b/amethyst/src/main/res/values-sl-rSI/strings.xml @@ -2813,7 +2813,6 @@ Za ohranitev zasebnosti to denarnico polni in prazni prek ne-zasebnih računov, Vir: Strežniki: Odpri - OTS: %1$s Dokaz časovnega žiga Obstaja dokaz, da je bil ta zapisek podpisan pred %1$s. Dokaz je bil ožigosan v Bitcoin verigi blokov na ta datum in čas. Uredi članek diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index 83a1991f8d..734be267e9 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -2688,7 +2688,6 @@ Källa: Servrar: Öppna - OTS: %1$s Tidsstämpel Bevis Det finns bevis på att detta inlägg signerades någon gång före %1$s. Beviset stämplades i Bitcoin-blockchainen vid det datumet och den tiden. Redigera artikel diff --git a/amethyst/src/main/res/values-ta/strings.xml b/amethyst/src/main/res/values-ta/strings.xml index e7f9170abc..bcc0f3b33c 100644 --- a/amethyst/src/main/res/values-ta/strings.xml +++ b/amethyst/src/main/res/values-ta/strings.xml @@ -2125,7 +2125,6 @@ மூலம்: சேவையகங்கள்: திறக்கவும் -OTS: %1$s நேர முத்திரை சான்று இந்த இடுகை %1$s-க்கு முன்பு ஒரு நேரத்தில் கையொப்பமிடப்பட்டது என்பதற்கான சான்று உள்ளது. சான்று அந்த தேதியிலும் நேரத்திலும் Bitcoin blockchain-ல் முத்திரையிடப்பட்டது. கட்டுரையைத் திருத்து diff --git a/amethyst/src/main/res/values-th-rTH/strings.xml b/amethyst/src/main/res/values-th-rTH/strings.xml index 7be1e93544..2779a9625d 100644 --- a/amethyst/src/main/res/values-th-rTH/strings.xml +++ b/amethyst/src/main/res/values-th-rTH/strings.xml @@ -2171,7 +2171,6 @@ แหล่งที่มา: เซิร์ฟเวอร์: เปิด - OTS: %1$s Timestamp ถูกยืนยัน มีหลักฐานว่าโพสต์นี้มีการลงนามก่อน %1$s หลักฐานถูกประทับตราใน Bitcoin blockchain ณ วันและเวลาดังกล่าว แก้ไขบทความ diff --git a/amethyst/src/main/res/values-th/strings.xml b/amethyst/src/main/res/values-th/strings.xml index d6a900f1ac..b2ffd57fbb 100644 --- a/amethyst/src/main/res/values-th/strings.xml +++ b/amethyst/src/main/res/values-th/strings.xml @@ -574,7 +574,6 @@ คัดลอกมาจาก เว็บไซน์ สำเนา: - OTS: %1$s Timestamp ถูกยืนยัน มีหลักฐานว่าโพสต์นี้มีการลงนามก่อน %1$s หลักฐานถูกประทับตราใน Bitcoin blockchain ณ วันและเวลาดังกล่าว แก้ไขโพสต์ diff --git a/amethyst/src/main/res/values-tr/strings.xml b/amethyst/src/main/res/values-tr/strings.xml index 561c98aa9c..d29471ab49 100644 --- a/amethyst/src/main/res/values-tr/strings.xml +++ b/amethyst/src/main/res/values-tr/strings.xml @@ -2996,8 +2996,6 @@ Aç - OTS: %1$s - Zaman Damgası Kanıtı Bu gönderinin %1$s tarihinden önce imzalandığına dair kanıt var. Kanıt, o tarih ve saatte Bitcoin blok zincirine işlendi. diff --git a/amethyst/src/main/res/values-uk/strings.xml b/amethyst/src/main/res/values-uk/strings.xml index d587710e89..4d980bb3ba 100644 --- a/amethyst/src/main/res/values-uk/strings.xml +++ b/amethyst/src/main/res/values-uk/strings.xml @@ -2786,8 +2786,6 @@ Відкрити - OTS: %1$s - Доказ часової позначки Є доказ того, що ця публікація була підписана до %1$s. Доказ засвідчено в блокчейні Bitcoin на ту дату та час. diff --git a/amethyst/src/main/res/values-zh-rCN/strings.xml b/amethyst/src/main/res/values-zh-rCN/strings.xml index 3ff2b4c951..ff2d4ec596 100644 --- a/amethyst/src/main/res/values-zh-rCN/strings.xml +++ b/amethyst/src/main/res/values-zh-rCN/strings.xml @@ -2695,7 +2695,6 @@ 源: 服务器: 打开 - OTS:%1$s OpenTimestamps 证明 %1$s之前的某个时候签署了此帖子的证明。此证明是在那个日期和时间在比特币区块链中盖章的。 编辑文章 diff --git a/amethyst/src/main/res/values-zh-rHK/strings.xml b/amethyst/src/main/res/values-zh-rHK/strings.xml index fea511090e..2afb33976f 100644 --- a/amethyst/src/main/res/values-zh-rHK/strings.xml +++ b/amethyst/src/main/res/values-zh-rHK/strings.xml @@ -2232,7 +2232,6 @@ 源: 服务器: 打开 - OTS:%1$s OpenTimestamps 证明 %1$s之前的某个时候签署了此帖子的证明。此证明是在那个日期和时间在比特币区块链中盖章的。 编辑文章 diff --git a/amethyst/src/main/res/values-zh-rSG/strings.xml b/amethyst/src/main/res/values-zh-rSG/strings.xml index 4d2541c844..6e804d6206 100644 --- a/amethyst/src/main/res/values-zh-rSG/strings.xml +++ b/amethyst/src/main/res/values-zh-rSG/strings.xml @@ -2232,7 +2232,6 @@ 源: 服务器: 打开 - OTS:%1$s OpenTimestamps 证明 %1$s之前的某个时候签署了此帖子的证明。此证明是在那个日期和时间在比特币区块链中盖章的。 编辑文章 diff --git a/amethyst/src/main/res/values-zh-rTW/strings.xml b/amethyst/src/main/res/values-zh-rTW/strings.xml index 8feb85b6b3..accf6196d6 100644 --- a/amethyst/src/main/res/values-zh-rTW/strings.xml +++ b/amethyst/src/main/res/values-zh-rTW/strings.xml @@ -2209,7 +2209,6 @@ 來源: 伺服器: 開啟 - OTS:%1$s 時間戳證明 有在 %1$s 之前的某個時間簽署了此帖子的證明。此證明是在那個日期和時間在比特幣區塊鏈中記錄的時間戳。 編輯文章 diff --git a/amethyst/src/main/res/values-zh/strings.xml b/amethyst/src/main/res/values-zh/strings.xml index 4dd6d7bff0..b9f3309a64 100644 --- a/amethyst/src/main/res/values-zh/strings.xml +++ b/amethyst/src/main/res/values-zh/strings.xml @@ -4214,8 +4214,6 @@ 打开 -OTS:%1$s - OpenTimestamps 证明 %1$s之前的某个时候签署了此帖子的证明。此证明是在那个日期和时间在比特币区块链中盖章的。 diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index a3ef6e6b7d..9005e9f7e7 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -45,6 +45,7 @@ Timestamp it Timestamp: Pending Confirmations OTS: Pending + 1y+ Request Deletion Block / Report @@ -3017,7 +3018,6 @@ Source: Servers: Open -OTS: %1$s Timestamp Proof There\'s proof this post was signed sometime before %1$s. The proof was stamped in the Bitcoin blockchain at that date and time. diff --git a/commons/src/commonMain/composeResources/font/material_symbols_outlined.ttf b/commons/src/commonMain/composeResources/font/material_symbols_outlined.ttf index fde397e9efc2d552e8205b3b0ec61759f5f3a6df..d75ba235aab931c0844fa28d9529614b88afa8b8 100644 GIT binary patch delta 8827 zcmZ`;30&0G_CNRD-|siWFbo3@Gsvt63Zj4l0s<1?hGs6frsl3$Zlv}^>p381re!I` zDD_PJoB6EB#OIXHT-sbxb15_OsTmp)vNH_(|J)yn_V@j-pF6+#eb0LDx#!%&SoMYJ z;1!b{2mtuthXC54W5=iE|4{HLK0cr<^V9;sIiezsbi+ELfTf8 znlW}<>bO^{mOB9Mt;P3mX8vvFQc&OB4^WYg@1tignwh!PylyEfuL6)R%|rUe+C_y( z=kn#x&Rk~C#4lPjcov_^XBV!Vw=C>L8Gz;pz=;vhqQI-BlDPntYe7mZoIiKwoVb(k z)}sR^CWGjh=Q1tqRfow;P$^1PAV>Hse50Q?TgyF=@r*gt*Ohts{2>Xnoo zM?tu={6(YueCeJoFNF*stMXQpXGm4vHS$N^F?xqo742q22|7lRjp5YcC+;#n1?ec- z-+(U)*ifVrqY3%AC`sCgkG$^`6Ya`C&p^qR7O1eb*>EduE^f$LYAvzpZN01`tY6#S zvhKHbwQ8(!)^O`KtG9KS<(}nT%SP)G%K~eG1;b@IVfnY^L(Auue_0YOQ!EqAPV+*p z#o}w;VyZPQHoalmXAU;4F~tLz+Hm@r7MKQ_x|>o=Np_PbfYIORkDq?V?p(~sj3l7k zI6q)$KtO;o;9%G0fB^y10=fq%yMEYpf&W?mgsyr3|D*o%4BHJ2T*807VY`2VVGDla zfdRu$hVzCShV_QA{+s>R_>2DehLZ-|K|`xyp#LcUG<~uDvSFrSoner{ep3INVWgoK zfc~n%!$9;^`hEC)u|cW-!l2SG(EqG&(f9Ft$?u-uI6c$%(v|AU{CfGV_v^3Q=;N(D z=ku)3zkMpTv$+AiU_a;1Qt8p^#8Q!@_x$ga^x6(U9b42s9w@MT1 zo#q{;`KTKnmI}{j6$_QG$pPbf?ho=Z1}>Yd%{!qA&D&1|N+##+B)SBWPKH0=dH5vB z$c^*SL8AB@=_7wekode49>ErZo7XXjmf*2_@EsYo#1}0XNE1d`q)3^qVJY=r{&5q^LRa0&WB9881v;CG^gvrr7@!3CG$BAkX( za0XJKEA$36R6#Y|fiN;14njKYg+1^Gl#rQZHbg@Wd73PMzK}$ogQa8?>;n_rAklD> zjDar&*abWC4tSonUk%U)%no=bpx7uF6OEIN+l^;Tded-IuBp&eYqpu^n+wdhEIP|* z%QDMB%gVLz1HieUUj|2-VwbA_5OSBeZBt^&cc(zr-ZK!-yMEFyd=UFksh%)qByckSea`ls)c2jfC;K)r$2OmR$2KaYN){WAK!+|T|&zl*Vo*#5DzW8aIt7^jO{5cjXR|HOBXpBBF^erx=` z__OhC{XP5p_Ydzsw0~CrfA;^ie|3T;VOqkQ2|p$36B83hC$=O_P1=%lcz|j^p8?Yb zd^+IDfTm<~a#ZrW$+w;u_{8fez9|_gIVqo~+)VA8IzRPvsw-`5T29*0w1$Cw2ad51 z+&S>aL9+&ZJg9DP;NVGv?Sl&j|1^XR89(IRA@xJ!ht3&#XlP@4NP0^8g!I|z_Vg|3 zpQIm8|1-UPSirE5VFQN!V_5m{>BCPvsdzHx$@x$2d-Cdt6(deQHTJ0!BYTcqIP%J< zo};oy?H%1^^pm6aj=nM`am)*2sxwkEE{^Rv_RDcS#=SSL--YqI@oD3aOwdkvdcwL1 zzfLqw+&b~kNy(G4C)Gba>ggks$>ik8D<`*3nK)(Vl-j8YQ(u~~2cT%TradZ1g3&OV zWwK1Hi=JSFUU+7QVh%*dMH`}7kOCihnX$IKUsKJR^(nS3w&e9G`WPXmtvD~GUsu)3 zQ>T*oSM&4RO7cu;fi~g)XbbtPCX#pL31ctvS3QA|k%8nU|7^_;j2un-kVll-QglT- zQzyktse&wKW9jH2boWtFMJKC!XUQF?>?J+4QOpl*`FkIYYr-?1Y;C1_G_Po2jT z{QDE0de7J?zF9PmzxGdQYqlCG?O>y9K+29H*Ng8?VR*mh9WE;^dijz-C7MN7(p5#- zKh_Dx+_EO5otdRU&1js0bfHov>+=b0+EOZQ48Yj;#ao)h!_3 zeFfxnD3EhWKz_&va``-vD`7y2X92l(6v)lVKyD=fxsw8<)E`KBZy*(a15$kjNNoX- zhAbeBF+kc*0i`OiQ-uK3;{Z^NKTz#Fpt^%V_2obV>VTRPfCgp(4W0!wbTrVg#XuvI zfcC8dnh+24iOE0*odr5{8qkr;fo9AAIu+k%9tS!f`4`3jUA7Qt7V^Cu0CaUI(411B zxh|laP*2`EJJ5nnK(}84`adZ^_xJ++C>dxWDkwtC83FY8HK5<_0eT7zp0)t}z6~g@ zo?c1=`Xf5>%LSm;3b5IzYn~d|{48Mh z=i7iS8V)RT1F)qPz+OxNmbDbvOGsS(F|ajffUU#1J`>pMXzL_?`weVS`O?3)cK(f*xn>ypRNaX0Og$M@DUrZW9Pxnj-!GL z0Vf2ZYp85C&^O7=^qUeL$Fa0E8(52-9)SRNFy#c0UNu7l82M zSrAsu0Kwi3gq%7M*2aUdZXXD^a^VdX2ycD^LjGD13eJGA!wtfog&_Ph7KHtX9YjN) zuL0qUd=UQqJP1c55RQg{a7+)v34DHq#=o5m!gu&QxdEpOgwyEMdHY@vE-we6_z(y; zTRAf8VF@j?lRSLcCv4YAv(`0gGME76e}RMwmY63hchx(<@(2#~x}Kr3+r~`r=~tm z^a7PlT{OJ@GZBH7rUyWLbDl&rj|ZE9zkFHxuv zsxgsh%4A%Wa*)h7?R6Fa!a6_TDF zNLuZbxov3nkQ0N9(7I!?ClLsJb&PE0iyC)M<}%vU&ezY8yEcUWRv~loSRbe$#{`6y z*2-Lw$hEJQe8>ee2<3rQBG)EDKQbefV5R(!s1RCer6F9fAV`J`D0nG|S|uclNOtw4 zw>^;FKcX$f4jg85)4hPa_q6KJXiA=TVYXvkdp3O~|km(Vyob7C`k=819)X;_4?4Kmxb`1A#@qvtvrqf^|uKXP-3R-4pybQ2`wwh=?|e!9foLhN`$adAl8F-l3Yl+1=btO)d3x;BIPZ!EkxBxNH8z zQNx4gq1Suhqa}z^L7=p)tqs~7fp5~T9yaTJ*BoOaXji4jeUBVBU!?|g?Y<+9)>o;& zfZIbHV_w3{IQAiv9@(Vq?yknh=9+47H8wjozlodFgHlm^VCXZBSj?T?69;*N>V|7I zCIc2ky%eC)&@huJtR_cQQxg`Z0ZmC*SeR0yQR38SbU94NiRzj#e1suYQxjJ6`0+J0 z*1CYTxLaM#?G21IwAR$Lwl*{{4a3g{Q)6RGi#(yRgVunL1|%Z6m0rCHYv_dwum+s8 zN~_9cDs7c37hD)kk^}9z(4GTiEuL`Bp>iEQv>(Tz!>*=WXzJjxcQReOhD_ON{6l64 z)`ULzyX8Qv38SzkWMEAgk2N6$YruG{0b{TRjK>-<0&74YY*#Uf?hg#@gEV4Ph zp)>Ije>CguIZ8RddDi8>m?&=c`rD*W;V1B`(Ag9oCtPLEiN^yn{Tg{c%`5 z>wi0W@+61qJw0=kn4%AJ)E14^q+$bAj?{H|EDv!xk7S`zPB@-~T^=0$2RX=75VCu8 zCRtO9BXA=%dnr3pD#z63_nm%!Fk1#Tj^szA2Ek@Px!S|Q(*RtqoHT^?2f=f7X42Y8dSI1jHyna5;wr2a_tauIi) z%A@uC;y${q^OkPfMguuydcPBz^gfk4rPUwEO1!bpEOab>pX!~LKA;16OR6uFnenoA zW)xBnZgJ{C8ItgR-g=Ps<@WH7KaML7(A*^GjsNhS2U3I+jE3 zyEKxhouOsakKt?bJ()uR*cINR3wV2Wr>+y~R!>)OYX!}ns;@NDVH~ni_Mk1AN?Cv2 zi{*REkO%6zH5clc29~;co2b)&$kfC@KyfAyC*Waz1uKbhIrJz zo6R5FPD{84pp|5}7ptrRObSW4VpPt7WJ=Lax=!oA+eW2F5!~-q|H@U<&T< zkj*j|ht_OnFYv=sx2+TEQ^5LiXx1(nO0%h*+jp@<-Vzq?mm$7G`p;M*k4eTs8RGtK zI>>(GQ0C_{)El9XKW8hrMTE?b)Z>gd`y8Kfoc+!NGw7tu>W5JFNp_pBcHTMJU)~XK zKgYK5!xDc<=IV;j@=I(Phf1$>Lcu??e{p|{Z^+OX)TO`4W^#YCDrAWF^9L)~GX7^o zXtNCQ?s`r$yCFM41%5h`kSm)C-oe=6&fy&{aiqQ`8JvYZgm9xh34hMSKkp$C|B6h+ zejR^-!AWp>;FNGGaf(Ru#3uuz+wE$ty<1gLQB{Zi8*6oyyIR|tYHMpMD=Ko0I^aMX zl-{kVsJQoM)!o`wm&=XARj11}*4DO_gN}N6d23Xpw!BGgv#Xoxnk6r-#>*?$Pg&XM zY4vQX46)*WjEUQeN$LH{82>_g-av)y=B<%F3#$ss^_^ z*XZBS>MnJ+HPqMQEl^i?uhgw%*z>fsv?~2`jcRobP{G&juB`yk9HPb!NFCBm4XurK z6wdA{BVHlp6|JqUjV>*PwknmER;%?;tu+P&V47TmJ9mC7siC$=UDeqtU1T@GRb6`L zPOdQ~qD$rXmAZ%vfAIF8&A-k+$v-M4Ha0fY6q9Q#DJl7;Ioj9PS4*)6uCDPN?pssW z(A+F&eSLF{*Uy|Ovj>))In!2KhU3pN)#dk&e0c=NcNebb8U;b53T(zbl#)`(+VR2{ zDPfXALMYdWKeZu1K&|Heo~Kf+z^02~gDy#~TqC9fw*T!-P1u@&LdBY@nwZ2|ni?9L znsWVGJcQ=zX6A9Py4GG>TV35))g&~xwdF9<*4*;{8&>?)u;(L&(XJqJ!%8JlLtQ&Y zsi6s!YN4^bQBYuC=HgNEYgP%3_ZkIN={<~FMMXoo+b%SA7;bFN#iONy_SRawJ~1@q zx83+lulsgs1BOnLFiJAea9g9strisZ?La&mT^R1B#&{W(U7nX~zLmM+kn8BT!YE#RHJ4>57NOynh3Q=Vz8jr_w{8e4cmbOHyUfKCI{$ZJ zg)CStLvL`wYT-1us8fi%w)-KJt`NKPGPPJE@=9PqXrD%WO(0&*m@eWXilkkBGKrUx z>waP*|C{-$Rfe8INM{qnxtX-^PH1(w_&fJ+R)WYki|hEd-qR&yx(6Y(Oa6v|cv)?i#>VNBTmbL(-3<8wCLbvoL-foFocq?itXI{trYkp>;aRn{R1;ki?s?f!IHN;xG-7LY*x^ zk{n)FxXc`m%ux~2&pei@M41^67JSyhoS7&s;y3yA!7?*Hasfl6U-@pj#>h|%m(P%@ zxv}6`GV}yOGiFKQJ_v1ECPRa5*iI~y0#VRezFHd0cOvQ)ne+~l7QZ45<=ltzC4QI! zP(ek$w2?1!)pnVyKSGDLOMQ8?OABRa7(&6HO4a-@>A#R6egHDQkY45f6@Me^;=Ahm qt@JHl-_?sU#7~a)l5~%6bX&13$bXDX_*I(H6aC8<6qEfa!2bg1t_2qW delta 5944 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 F{|4V`38(-7 diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/symbols/MaterialSymbols.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/symbols/MaterialSymbols.kt index 7ef5919971..714013ec98 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/symbols/MaterialSymbols.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/symbols/MaterialSymbols.kt @@ -117,6 +117,7 @@ object MaterialSymbols { val FitnessCenter = MaterialSymbol("\uEB43") val Folder = MaterialSymbol("\uE2C7") val FolderZip = MaterialSymbol("\uEB2C") + val ForkRight = MaterialSymbol("\uEBAC") val FormatBold = MaterialSymbol("\uE238") val FormatItalic = MaterialSymbol("\uE23F") val FormatListNumbered = MaterialSymbol("\uE242") From 568aa9b005d3c7a5b82eb6a99f7a0276097fde92 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 14 Jul 2026 13:49:08 +0000 Subject: [PATCH 11/13] fix(ui): use middle ellipsis in HeaderPill labels Truncated pill labels (capped city names, relay hosts) keep their distinguishing endings, matching the codebase convention for URLs and relay links. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01AgEpNtwnetPhETXQSdq4b5 --- .../com/vitorpamplona/amethyst/commons/ui/note/HeaderPill.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/HeaderPill.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/HeaderPill.kt index 312ca0d584..068a06e7d9 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/HeaderPill.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/HeaderPill.kt @@ -77,7 +77,7 @@ fun HeaderPill( style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.placeholderText, maxLines = 1, - overflow = TextOverflow.Ellipsis, + overflow = TextOverflow.MiddleEllipsis, ) } } From 500de62841a3cc51516ddadbc4a024c10c1fdc7e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 14 Jul 2026 14:15:56 +0000 Subject: [PATCH 12/13] fix(ui): explain pending OTS on tap, render the stamp glyph and pill icons larger MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tapping the pending OTS pill now shows a toast explaining that the attestation is waiting to be stamped into the Bitcoin blockchain (new ots_info_pending_description). The OpenTimestamps glyph gets a near-full-em content box — its fine outline read much lighter than Material's solid shapes at the standard 80..880 bounds — and HeaderPill icons go from 11dp to 13dp. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01AgEpNtwnetPhETXQSdq4b5 --- .../amethyst/ui/note/elements/DisplayOts.kt | 9 +++++- amethyst/src/main/res/values/strings.xml | 1 + .../font/material_symbols_outlined.ttf | Bin 469812 -> 469856 bytes .../amethyst/commons/ui/note/HeaderPill.kt | 2 +- .../add_custom_glyphs.py | 29 +++++++++--------- 5 files changed, 24 insertions(+), 17 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt index 43ffee19e3..22c3720962 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt @@ -75,11 +75,18 @@ fun DisplayOts( }, whenPending = { // The stamp icon plus an ellipsis: attestation requested, not yet - // anchored. The content description carries the words. + // anchored. The content description carries the words; tapping + // explains what is being waited on. HeaderPill( symbol = MaterialSymbols.OpenTimestamps, text = "…", contentDescription = stringRes(R.string.timestamp_pending_short), + onClick = { + accountViewModel.toastManager.toast( + R.string.ots_info_title, + R.string.ots_info_pending_description, + ) + }, ) }, ) diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 9005e9f7e7..a9ed842bdd 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -3021,6 +3021,7 @@ Timestamp Proof There\'s proof this post was signed sometime before %1$s. The proof was stamped in the Bitcoin blockchain at that date and time. + A timestamp proof for this post has been requested and is waiting to be stamped in the Bitcoin blockchain. It usually completes within a few hours. Edit Article Edit Post diff --git a/commons/src/commonMain/composeResources/font/material_symbols_outlined.ttf b/commons/src/commonMain/composeResources/font/material_symbols_outlined.ttf index d75ba235aab931c0844fa28d9529614b88afa8b8..aed61e63f444fc798feb320f8b9ba482543dd9d4 100644 GIT binary patch delta 2369 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 (glyph name, traced SVG). Keep codepoints at the very end of -# the BMP private-use area (U+F8xx tail) to stay clear of the range Material -# Symbols actually allocates. +# codepoint -> (glyph name, traced SVG, content-box fractions). Keep +# codepoints at the very end of the BMP private-use area (U+F8xx tail) to +# stay clear of the range Material Symbols actually allocates. +# +# Material's own solid shapes draw inside 80..880 of a 960 upm em; a glyph +# with fine outlines (like the OpenTimestamps stamp) reads much lighter at +# the same bounds, so it gets a near-full-em box to appear the same size. CUSTOM = { - 0xF8F0: ("opentimestamps", os.path.join(HERE, "custom", "opentimestamps.svg")), + 0xF8F0: ("opentimestamps", os.path.join(HERE, "custom", "opentimestamps.svg"), 20 / 960, 940 / 960), } -# Fit content into the same square Material Symbols draw in (its check_circle -# glyph spans 80..880 in a 960 upm font). Scaled to the font's real upm. -BOX_MIN_FRAC = 80 / 960 -BOX_MAX_FRAC = 880 / 960 - def svg_path_data(svg_file): with open(svg_file, "r", encoding="utf-8") as f: @@ -54,15 +53,15 @@ def draw_svg(paths, pen): parse_path(d, pen) -def build_glyph(paths, upm): +def build_glyph(paths, upm, box_min_frac, box_max_frac): # First pass: raw outline bounds. bounds = BoundsPen(None) draw_svg(paths, bounds) x_min, y_min, x_max, y_max = bounds.bounds - # Uniform scale into the Material content box, centered on both axes. - box_min = BOX_MIN_FRAC * upm - box_max = BOX_MAX_FRAC * upm + # Uniform scale into the content box, centered on both axes. + box_min = box_min_frac * upm + box_max = box_max_frac * upm box = box_max - box_min scale = box / max(x_max - x_min, y_max - y_min) tx = box_min + (box - (x_max - x_min) * scale) / 2 - x_min * scale @@ -79,8 +78,8 @@ def main(font_path): upm = font["head"].unitsPerEm order = font.getGlyphOrder() - for codepoint, (name, svg_file) in sorted(CUSTOM.items()): - glyph = build_glyph(svg_path_data(svg_file), upm) + for codepoint, (name, svg_file, box_min_frac, box_max_frac) in sorted(CUSTOM.items()): + glyph = build_glyph(svg_path_data(svg_file), upm, box_min_frac, box_max_frac) if name not in order: order.append(name) From e684cb02a3d2e0ff3bba5860f7cf765c8232a687 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 14 Jul 2026 14:31:23 +0000 Subject: [PATCH 13/13] 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