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 9939907e47..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 @@ -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 @@ -61,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 @@ -77,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 @@ -90,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 @@ -205,10 +206,7 @@ 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.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 +214,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 @@ -353,9 +351,12 @@ 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 +import java.text.SimpleDateFormat +import java.util.Date @Composable fun NoteCompose( @@ -761,7 +762,6 @@ fun InnerNoteWithReactions( if (showSecondRow) { SecondUserInfoRow( baseNote, - editState, accountViewModel, nav, ) @@ -883,7 +883,6 @@ fun NoteBody( if (showSecondRow) { SecondUserInfoRow( baseNote, - editState, accountViewModel, nav, ) @@ -1737,7 +1736,6 @@ fun ReplyNoteComposition( @Composable fun SecondUserInfoRow( note: Note, - editState: State>, accountViewModel: AccountViewModel, nav: INav, ) { @@ -1746,6 +1744,7 @@ fun SecondUserInfoRow( Row( verticalAlignment = CenterVertically, + horizontalArrangement = Arrangement.spacedBy(Size5dp), modifier = UserNameMaxRowHeight, ) { Column(modifier = Modifier.weight(1f)) { @@ -1756,47 +1755,39 @@ 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) + DisplayReward(baseReward, note, accountViewModel) } - - val pow = remember(noteEvent) { noteEvent.strongPoWOrNull() } - if (pow != null) { - Spacer(StdHorzSpacer) - DisplayPoW(pow) - } - - DisplayOtsIfInOriginal(note, editState, 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(15.dp), - tint = MaterialTheme.colorScheme.placeholderText, - ) - val context = LocalContext.current - Text( - text = timeAheadNoDot(expirationDate, context), - color = MaterialTheme.colorScheme.placeholderText, - maxLines = 1, - modifier = Modifier.padding(start = 3.dp), - ) - } +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. + val text = + if (expirationDate - TimeUtils.now() > TimeUtils.ONE_YEAR) { + stringRes(R.string.one_year_plus) + } else { + timeAheadNoDot(expirationDate, context) + } + HeaderPill( + 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)), + ) + }, + ) } @Composable @@ -1816,25 +1807,12 @@ fun DisplayOtsIfInOriginal( @Composable fun DisplayDraft() { - Text( - "Draft", - fontWeight = FontWeight.Bold, - 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 @@ -1847,7 +1825,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 @@ -1864,17 +1846,21 @@ 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) } + CheckAndDisplayEditStatus(editState) + + if (isDraft) { + DisplayDraft() + } + if (isDraft) { ObserveDraftEvent(baseNote, accountViewModel) { draftNote -> val isCommunityPost = @@ -1901,55 +1887,65 @@ 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, accountViewModel) } + Expiration(baseNote, accountViewModel) + if (baseNote.isPrivateRumor()) { - PrivateRumorMark() + PrivateRumorMark(accountViewModel) } if (isPinned) { PinnedMark() } - Expiration(baseNote) - JumpToParentReplyButton(baseNote, accountViewModel, nav) - TimeAgo(baseNote) + // 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, style = TimeAgoStyle.DottedTight) - if (moreOptions == null) { - MoreOptionsButton(baseNote, editState, accountViewModel, nav) - } else { - moreOptions() + if (moreOptions == null) { + MoreOptionsButton(baseNote, editState, accountViewModel, nav) + } else { + moreOptions() + } } } } @Composable fun PinnedMark() { - Icon( + QuietMark( symbol = MaterialSymbols.PushPin, contentDescription = stringRes(R.string.pinned_notes), - modifier = Modifier.padding(start = 5.dp).size(16.dp), - tint = MaterialTheme.colorScheme.placeholderText, ) } @Composable -fun PrivateRumorMark() { - Icon( +fun PrivateRumorMark(accountViewModel: AccountViewModel) { + QuietMark( symbol = MaterialSymbols.Lock, contentDescription = stringRes(R.string.private_rumor_mark), - modifier = Modifier.padding(start = 5.dp).size(16.dp), - tint = MaterialTheme.colorScheme.placeholderText, + onClick = { + accountViewModel.toastManager.toast( + R.string.private_rumor_info_title, + R.string.private_rumor_info_description, + ) + }, ) } @@ -1975,7 +1971,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) } }, @@ -1990,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/BoostedMark.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/BoostedMark.kt deleted file mode 100644 index b91bdbeb81..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/BoostedMark.kt +++ /dev/null @@ -1,41 +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.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.HalfStartPadding -import com.vitorpamplona.amethyst.ui.theme.placeholderText - -@Composable -fun BoostedMark() { - Text( - stringRes(id = R.string.boosted), - fontWeight = FontWeight.Bold, - 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..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 @@ -34,7 +34,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.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 @@ -45,7 +45,7 @@ fun DisplayFollowingCommunityInPost( accountViewModel: AccountViewModel, nav: INav, ) { - Column(HalfStartPadding) { + Column { Row(verticalAlignment = Alignment.CenterVertically) { DisplayCommunity(baseNote, accountViewModel, nav) } } } @@ -64,7 +64,7 @@ private fun DisplayCommunity( ClickableTextColor( label, - 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 8ebe574745..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 @@ -20,38 +20,33 @@ */ 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.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 -import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding -import com.vitorpamplona.amethyst.ui.theme.placeholderText +/** + * 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) { - 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, - ), - maxLines = 1, - modifier = HalfStartPadding.clickable { editState.nextModification() }, + val label = + if (editState.showingVersion.value == editState.lastVersionId()) { + null + } else if (editState.showingVersion.value == editState.originalVersionId()) { + stringRes(id = R.string.original) + } else { + "#${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/DisplayHashtags.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayHashtags.kt index cc4f929afb..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 @@ -38,6 +38,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.lessImportantLink import com.vitorpamplona.quartz.nip01Core.tags.hashtags.firstIsTaggedHashes import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip73ExternalIds.topics.HashtagId @@ -82,7 +83,7 @@ private fun DisplayTagList( ) { ClickableTextColor( "#$firstTag", - 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/DisplayLocation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayLocation.kt index e6d942c620..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,20 +20,23 @@ */ 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.foundation.layout.widthIn 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 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 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. + * 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( geohashStr: String, @@ -41,25 +44,11 @@ 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, + 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/DisplayOts.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt index ec5b9bb06b..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 @@ -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.commons.ui.note.HeaderPill 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 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,33 @@ 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.OpenTimestamps, + text = timeStr, + 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, + // The stamp icon plus an ellipsis: attestation requested, not yet + // 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/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayPoW.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayPoW.kt index e257452632..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 @@ -20,25 +20,13 @@ */ 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.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 @@ -46,39 +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 bolt plus - * the difficulty in leading zero bits. Sits inline in note headers, so it - * stays at text height. + * Compact pill showing the proof of work a received note carries: a gear plus + * 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) { - 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, - ) - } +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/note/elements/DisplayReward.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt index b392b6a50c..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 @@ -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,17 @@ 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.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 -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 +79,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 +94,7 @@ private fun RenderPledgeAmount( baseNote: Note, baseReward: Reward, accountViewModel: AccountViewModel, + onClick: () -> Unit, ) { val repliesState by observeNoteReplies(baseNote, accountViewModel) var reward by remember { @@ -142,16 +124,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..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,25 +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.quartz.experimental.forks.IForkableEvent @Composable @@ -53,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, @@ -74,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.primary, - 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/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..efac0ed3ed --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteHeaderMarkersPreview.kt @@ -0,0 +1,231 @@ +/* + * 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.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +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.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.quartz.nip01Core.metadata.MetadataEvent +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +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 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, 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`, 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 + * (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 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 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( + MetadataEvent(AUTHOR_METADATA_ID, AUTHOR, now, emptyArray(), """{"name":"Vitor"}""", "x"), + null, + true, + ) + LocalCache.justConsume( + MetadataEvent(LONG_NAME_METADATA_ID, LONG_NAME_AUTHOR, now, emptyArray(), """{"name":"A user with a very long display name"}""", "x"), + null, + true, + ) + + LocalCache.justConsume(plainEvent, null, true) + LocalCache.justConsume(TextNoteEvent(EDIT_VERSION_ID, AUTHOR, now - 60, emptyArray(), "GM! (fixed typo)", "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) + LocalCache.justConsume(draftEvent, null, true) + LocalCache.justConsume( + TextNoteEvent(COMMUNITY_POST_ID, AUTHOR, now - 300, arrayOf(arrayOf("a", COMMUNITY_ADDRESS)), "hello community", "x"), + null, + true, + ) + LocalCache.justConsume( + TextNoteEvent( + POW_ID, + AUTHOR, + now - 300, + arrayOf( + arrayOf("g", GEOHASH), + arrayOf("nonce", "776", "24"), + arrayOf("expiration", expiresAt), + ), + "mined and placed", + "x", + ), + null, + true, + ) + 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, + true, + ) + } + } + + // 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(horizontal = 10.dp, vertical = 6.dp)) { + // Bare minimum: username + time + options + HeaderRowSample(plain, accountViewModel) + + // Quiet icon marks: private rumor + pinned + HeaderRowSample(rumorPinned, accountViewModel, isPinned = true) + + // Quiet text mark, tappable: edited + HeaderRowSample(edited, accountViewModel, editState = editedState) + + // Quiet text mark: draft + HeaderRowSample(draft, accountViewModel) + + // 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 + HeaderRowSample(kitchenSink, accountViewModel, isPinned = true, editState = editedState) + } + } +} + +@Composable +private fun HeaderRowSample( + note: Note, + accountViewModel: AccountViewModel, + isPinned: Boolean = false, + editState: State> = EmptyState, +) { + FirstUserInfoRow( + baseNote = note, + showAuthorPicture = false, + isPinned = isPinned, + editState = editState, + accountViewModel = accountViewModel, + nav = EmptyNav(), + ) +} 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/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/chats/rooms/ChatroomHeaderCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt index 442e49cf2d..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 @@ -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 @@ -58,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 @@ -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 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..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 @@ -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 @@ -703,7 +704,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 { @@ -736,14 +740,21 @@ private fun FullBleedNoteCompose( CheckAndDisplayEditStatus(editState) - Expiration(baseNote) + Expiration(baseNote, accountViewModel) - TimeAgo(note = baseNote) + // 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, style = TimeAgoStyle.DottedTight) - MoreOptionsButton(baseNote, editState, accountViewModel, nav) + MoreOptionsButton(baseNote, editState, accountViewModel, nav) + } } - Row(verticalAlignment = Alignment.CenterVertically) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(Size5dp), + ) { Column( Modifier.weight(1f), ) { @@ -765,12 +776,12 @@ 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() } if (pow != null) { - DisplayPoW(pow) + DisplayPoW(pow, accountViewModel) } if (isDraft) { diff --git a/amethyst/src/main/res/values-cs/strings.xml b/amethyst/src/main/res/values-cs/strings.xml index 706a9fdc4b..d60ee8dc55 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 c8903d7063..be4bfcb024 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 b8ce45f477..3b8df32c0f 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 d57b90ddfe..2c9f2c92f3 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 d83d1cbde8..7835174c36 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 8ffe8921b4..201d7e725d 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 89536d9bac..7e01e4ac0c 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 e096ff2974..4b2fd9b00d 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 c451b2e909..93a41749de 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 88635bb0a0..bb876140cb 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 9bc2d19653..5137fd21d6 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 8f23756f3a..fa993c1730 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 7dd53b26e3..42ecf5cf1d 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 6bbc9cff50..664c06a385 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 dcb597e18d..49b6c3d5ac 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 3e56dab2f5..23ab209d4c 100644 --- a/amethyst/src/main/res/values-zh-rCN/strings.xml +++ b/amethyst/src/main/res/values-zh-rCN/strings.xml @@ -2697,7 +2697,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 21cb66c936..b93c51de96 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 65bb95dfd3..8436609a81 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 98a024b0ee..9c9f7e0b50 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 46168a2f19..fc352bc4ad 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 @@ -1233,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 @@ -2823,6 +2826,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 @@ -3019,10 +3023,10 @@ 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. + 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 @@ -3043,6 +3047,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 @@ -3875,6 +3881,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 diff --git a/commons/src/commonMain/composeResources/font/material_symbols_outlined.ttf b/commons/src/commonMain/composeResources/font/material_symbols_outlined.ttf index 07e1c3e472..590cbff2c3 100644 Binary files a/commons/src/commonMain/composeResources/font/material_symbols_outlined.ttf and b/commons/src/commonMain/composeResources/font/material_symbols_outlined.ttf differ 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 adee791893..80bac6a653 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") @@ -168,6 +169,12 @@ object MaterialSymbols { val Numbers = MaterialSymbol("\uEAC7") val OpenInBrowser = MaterialSymbol("\uE89D") val OpenInFull = MaterialSymbol("\uF1CE") + + // Custom glyph (not part of Google's Material Symbols): the OpenTimestamps + // stamp logo, baked into the subset font by + // tools/material-symbols-subset/add_custom_glyphs.py from custom/opentimestamps.svg. + val OpenTimestamps = MaterialSymbol("\uF8F0") + val Paid = MaterialSymbol("\uF041") val PanTool = MaterialSymbol("\uE925") val Pause = MaterialSymbol("\uE034") 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/HeaderPill.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/HeaderPill.kt new file mode 100644 index 0000000000..6fcb46946e --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/HeaderPill.kt @@ -0,0 +1,84 @@ +/* + * 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.note + +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.graphics.Color +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, ...) 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( + symbol: MaterialSymbol, + text: String, + modifier: Modifier = Modifier, + contentDescription: String? = null, + iconTint: Color? = null, + onClick: (() -> Unit)? = null, +) { + Surface( + shape = RoundedCornerShape(6.dp), + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.07f), + contentColor = MaterialTheme.colorScheme.placeholderText, + 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 = iconTint ?: MaterialTheme.colorScheme.placeholderText, + modifier = Modifier.size(13.dp), + ) + Text( + text = text, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.placeholderText, + maxLines = 1, + overflow = TextOverflow.MiddleEllipsis, + ) + } + } +} 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 new file mode 100644 index 0000000000..2e0374c0d1 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/QuietMark.kt @@ -0,0 +1,77 @@ +/* + * 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.note + +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.commons.ui.theme.placeholderText + +/** + * 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: 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. + */ +@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(16.dp), + ) + } + if (text != null) { + Text( + text = text, + color = MaterialTheme.colorScheme.placeholderText, + fontWeight = FontWeight.Bold, + 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) 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 diff --git a/tools/material-symbols-subset/README.md b/tools/material-symbols-subset/README.md index c6f689020b..9db8fea879 100644 --- a/tools/material-symbols-subset/README.md +++ b/tools/material-symbols-subset/README.md @@ -54,3 +54,15 @@ Commit the regenerated `.ttf` alongside the `MaterialSymbols.kt` change. glyph and OpenType feature not reachable from those codepoints, and rewrites `name`, `cmap`, and `GSUB` tables accordingly. The Compose resource pipeline treats the result as a normal TTF — no code changes needed. + +## Custom glyphs + +`custom/` holds traced SVG outlines for icons the upstream font will never +carry (third-party logos — currently the OpenTimestamps stamp). +`add_custom_glyphs.py` bakes them into the subset TTF at end-of-PUA +codepoints (U+F8F0 and up); `subset.sh` runs it automatically after +`pyftsubset`, so regenerating the subset keeps them. To add one: trace the +logo to a single-color SVG (potrace output works as-is), drop it in +`custom/`, register it in the `CUSTOM` map in `add_custom_glyphs.py`, add the +matching `MaterialSymbol("\uF8Fx")` to `MaterialSymbols.kt`, and rerun +`subset.sh` (or `add_custom_glyphs.py ` 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..67352f537f --- /dev/null +++ b/tools/material-symbols-subset/add_custom_glyphs.py @@ -0,0 +1,103 @@ +#!/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, 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"), 20 / 960, 940 / 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, 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 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, 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) + 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))"