Merge pull request #3559 from vitorpamplona/claude/notecompose-pow-pill-style-vr6hbt

Unify note-header markers into a pill / quiet-mark design system
This commit is contained in:
Vitor Pamplona
2026-07-14 11:19:34 -04:00
committed by GitHub
62 changed files with 913 additions and 443 deletions
@@ -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,
)
}
}
@@ -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<GenericLoadable<EditState>>,
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)
}
}
}
@@ -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,
)
}
@@ -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,
) {
@@ -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() },
)
}
@@ -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,
) {
@@ -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)) },
)
}
}
@@ -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,
)
},
)
},
)
@@ -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(),
)
},
)
}
@@ -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,
)
}
@@ -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) },
)
}
}
@@ -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<EditState>>(
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<GenericLoadable<EditState>> = EmptyState,
) {
FirstUserInfoRow(
baseNote = note,
showAuthorPicture = false,
isPinned = isPinned,
editState = editState,
accountViewModel = accountViewModel,
nav = EmptyNav(),
)
}
@@ -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,
)
}
@@ -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()
@@ -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
@@ -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) {
@@ -2797,7 +2797,6 @@
<string name="nsite_source">Zdroj:</string>
<string name="nsite_servers">Servery:</string>
<string name="nsite_open">Otevřít</string>
<string name="existed_since">OTS: %1$s</string>
<string name="ots_info_title">Důkaz časového razítka</string>
<string name="ots_info_description">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.</string>
<string name="edit_article">Upravit článek</string>
@@ -2683,7 +2683,6 @@
<string name="nsite_source">Quelle:</string>
<string name="nsite_servers">Server:</string>
<string name="nsite_open">Öffnen</string>
<string name="existed_since">OTS: %1$s</string>
<string name="ots_info_title">Zeitstempel Beweis</string>
<string name="ots_info_description">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.</string>
<string name="edit_article">Artikel bearbeiten</string>
@@ -680,7 +680,6 @@ anz der Bedingungen ist erforderlich</string>
<string name="forked_from">Geforkt von</string>
<string name="git_web_address">Internet:</string>
<string name="git_clone_address">Klonen:</string>
<string name="existed_since">OTS: %1$s</string>
<string name="ots_info_title">Zeitstempel Beweis</string>
<string name="ots_info_description">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.</string>
<string name="edit_post">Beitrag bearbeiten</string>
@@ -2125,7 +2125,6 @@
<string name="nsite_source">Fonto:</string>
<string name="nsite_servers">Serviloj:</string>
<string name="nsite_open">Malfermi</string>
<string name="existed_since">OTS: %1$s</string>
<string name="ots_info_title">Tempomarko-Pruvo</string>
<string name="ots_info_description">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.</string>
<string name="edit_article">Redakti Artikolon</string>
@@ -2297,7 +2297,6 @@
<string name="nsite_source">Fuente:</string>
<string name="nsite_servers">Servidores:</string>
<string name="nsite_open">Abrir</string>
<string name="existed_since">OTS: %1$s</string>
<string name="ots_info_title">Prueba de marca de tiempo</string>
<string name="ots_info_description">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.</string>
<string name="edit_article">Editar artículo</string>
@@ -2288,7 +2288,6 @@
<string name="nsite_source">Fuente:</string>
<string name="nsite_servers">Servidores:</string>
<string name="nsite_open">Abrir</string>
<string name="existed_since">OTS: %1$s</string>
<string name="ots_info_title">Prueba de marca de tiempo</string>
<string name="ots_info_description">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.</string>
<string name="edit_article">Editar artículo</string>
@@ -2288,7 +2288,6 @@
<string name="nsite_source">Fuente:</string>
<string name="nsite_servers">Servidores:</string>
<string name="nsite_open">Abrir</string>
<string name="existed_since">OTS: %1$s</string>
<string name="ots_info_title">Prueba de marca de tiempo</string>
<string name="ots_info_description">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.</string>
<string name="edit_article">Editar artículo</string>
@@ -2618,8 +2618,6 @@
<string name="git_clone_address">Clon:</string>
<string name="existed_since">OTS: %1$s</string>
<string name="ots_info_title">Prueba de marca de tiempo</string>
<string name="ots_info_description">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.</string>
@@ -2253,7 +2253,6 @@
<string name="nsite_source">منبع:</string>
<string name="nsite_servers">سرورها:</string>
<string name="nsite_open">باز کردن</string>
<string name="existed_since">OTS: %1$s</string>
<string name="ots_info_title">اثبات مهر زمان</string>
<string name="ots_info_description">مدرکی وجود دارد که ثابت کند این پست پیش از %1$s امضا شده است. این مدرک در بلاکچین بیتکوین در آن تاریخ و زمان ثبت شده است.</string>
<string name="edit_article">ویرایش مقاله</string>
@@ -665,7 +665,6 @@
<string name="forked_from">انشعاب شده از</string>
<string name="git_web_address">وب:</string>
<string name="git_clone_address">نسخه برداری:</string>
<string name="existed_since">OTS: %1$s</string>
<string name="ots_info_title">اثبات مهر زمان</string>
<string name="ots_info_description">مدرکی وجود دارد که ثابت کند این پست پیش از %1$s امضا شده است. این مدرک در بلاکچین بیتکوین در آن تاریخ و زمان ثبت شده است.</string>
<string name="edit_post">ویرایش نوشته</string>
@@ -2511,7 +2511,6 @@
<string name="nsite_source">Source :</string>
<string name="nsite_servers">Serveurs :</string>
<string name="nsite_open">Ouvrir</string>
<string name="existed_since">OTS: %1$s</string>
<string name="ots_info_title">Preuve d\'horodatage</string>
<string name="ots_info_description">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.</string>
<string name="edit_article">Modifier l\'article</string>
@@ -670,7 +670,6 @@
<string name="forked_from">Forké depuis</string>
<string name="git_web_address">Web :</string>
<string name="git_clone_address">Clone :</string>
<string name="existed_since">OTS: %1$s</string>
<string name="ots_info_title">Preuve d\'horodatage</string>
<string name="ots_info_description">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.</string>
<string name="edit_post">Modifier le Message</string>
@@ -2730,7 +2730,6 @@
<string name="nsite_source">स्रोत :</string>
<string name="nsite_servers">सेवासंगणक :</string>
<string name="nsite_open">खोलें</string>
<string name="existed_since">ओ॰टी॰एस : %1$s</string>
<string name="ots_info_title">समयांकन प्रमाण</string>
<string name="ots_info_description">प्रमाण उपलब्ध है कि इस पत्र पर हस्ताक्षर किया गया %1$s के कुछ पहले। प्रमाण अंकित किया गया द्व्यंकरूप्य खण्डश्रृंखला में उस समय उस दिन पर।</string>
<string name="edit_article">लेख सम्पादित करें</string>
@@ -2731,7 +2731,6 @@
<string name="nsite_source">Forrás:</string>
<string name="nsite_servers">Kiszolgálók:</string>
<string name="nsite_open">Megnyitás</string>
<string name="existed_since">OTS: %1$s</string>
<string name="ots_info_title">Időbélyeg-igazolás</string>
<string name="ots_info_description">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.</string>
<string name="edit_article">Cikk szerkesztése</string>
@@ -2726,8 +2726,6 @@
<string name="nsite_open">Buka</string>
<string name="existed_since">OTS: %1$s</string>
<string name="ots_info_title">Bukti Stempel Waktu</string>
<string name="ots_info_description">Ada bukti bahwa postingan ini ditandatangani sebelum %1$s. Bukti ini dicap dalam blockchain Bitcoin pada tanggal dan waktu tersebut.</string>
@@ -2818,8 +2818,6 @@
<string name="nsite_open">開く</string>
<string name="existed_since">OTS: %1$s</string>
<string name="ots_info_title">タイムスタンプ証明</string>
<string name="ots_info_description">この投稿が %1$s より前に署名されたことの証明があります。この証明はその日時に Bitcoin ブロックチェーンに刻印されています。</string>
@@ -3798,8 +3798,6 @@
<string name="nsite_servers">Servers:</string>
<string name="existed_since">OTS: %1$s</string>
<string name="ots_info_title">Bewijs van tijdstempel</string>
<string name="ots_info_description">Er is cryptografisch bewijs dat dit bericht vóór %1$s is ondertekend. Dit bewijs is vastgelegd in de Bitcoin-blockchain.</string>
@@ -2566,7 +2566,6 @@
<string name="nsite_source">Bron:</string>
<string name="nsite_servers">Servers:</string>
<string name="nsite_open">Openen</string>
<string name="existed_since">OTS: %1$s</string>
<string name="ots_info_title">Bewijs van tijdstempel</string>
<string name="ots_info_description">Er is cryptografisch bewijs dat dit bericht vóór %1$s is ondertekend. Dit bewijs is vastgelegd in de Bitcoin-blockchain.</string>
<string name="edit_article">Artikel bewerken</string>
@@ -669,7 +669,6 @@
<string name="forked_from">Geforked van</string>
<string name="git_web_address">Web:</string>
<string name="git_clone_address">Kopiëren:</string>
<string name="existed_since">OTS: %1$s</string>
<string name="ots_info_title">Bewijs van tijdstempel</string>
<string name="ots_info_description">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.</string>
<string name="edit_post">Bericht wijzigen</string>
@@ -2798,7 +2798,6 @@ Zaplanowane posty z innych kont nie zostaną opublikowane, dopóki to konto jest
<string name="nsite_source">Źródło:</string>
<string name="nsite_servers">Serwery:</string>
<string name="nsite_open">Otwórz</string>
<string name="existed_since">OTS: %1$s</string>
<string name="ots_info_title">Potwierdzenie znacznika czasu</string>
<string name="ots_info_description">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.</string>
<string name="edit_article">Redaguj artykuł</string>
@@ -2680,7 +2680,6 @@
<string name="nsite_source">Fonte:</string>
<string name="nsite_servers">Servidores:</string>
<string name="nsite_open">Abrir</string>
<string name="existed_since">OTS: %1$s</string>
<string name="ots_info_title">Prova de Carimbo de data/hora</string>
<string name="ots_info_description">Há prova de que esta postagem foi assinada antes de %1$s. A prova foi carimbada no blockchain do Bitcoin naquela data e hora.</string>
<string name="edit_article">Editar artigo</string>
@@ -2596,8 +2596,6 @@
<string name="nsite_open">Открыть</string>
<string name="existed_since">OTS: %1$s</string>
<string name="ots_info_title">Доказательство временно́й метки</string>
<string name="ots_info_description">Есть доказательство того, что эта публикация была подписана до %1$s. Доказательство зафиксировано в блокчейне Bitcoin в указанную дату и время.</string>
@@ -2813,7 +2813,6 @@ Za ohranitev zasebnosti to denarnico polni in prazni prek ne-zasebnih računov,
<string name="nsite_source">Vir:</string>
<string name="nsite_servers">Strežniki:</string>
<string name="nsite_open">Odpri</string>
<string name="existed_since">OTS: %1$s</string>
<string name="ots_info_title">Dokaz časovnega žiga</string>
<string name="ots_info_description">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.</string>
<string name="edit_article">Uredi članek</string>
@@ -2688,7 +2688,6 @@
<string name="nsite_source">Källa:</string>
<string name="nsite_servers">Servrar:</string>
<string name="nsite_open">Öppna</string>
<string name="existed_since">OTS: %1$s</string>
<string name="ots_info_title">Tidsstämpel Bevis</string>
<string name="ots_info_description">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.</string>
<string name="edit_article">Redigera artikel</string>
@@ -2125,7 +2125,6 @@
<string name="nsite_source">மூலம்:</string>
<string name="nsite_servers">சேவையகங்கள்:</string>
<string name="nsite_open">திறக்கவும்</string>
<string name="existed_since">OTS: %1$s</string>
<string name="ots_info_title">நேர முத்திரை சான்று</string>
<string name="ots_info_description">இந்த இடுகை %1$s-க்கு முன்பு ஒரு நேரத்தில் கையொப்பமிடப்பட்டது என்பதற்கான சான்று உள்ளது. சான்று அந்த தேதியிலும் நேரத்திலும் Bitcoin blockchain-ல் முத்திரையிடப்பட்டது.</string>
<string name="edit_article">கட்டுரையைத் திருத்து</string>
@@ -2171,7 +2171,6 @@
<string name="nsite_source">แหล่งที่มา:</string>
<string name="nsite_servers">เซิร์ฟเวอร์:</string>
<string name="nsite_open">เปิด</string>
<string name="existed_since">OTS: %1$s</string>
<string name="ots_info_title">Timestamp ถูกยืนยัน</string>
<string name="ots_info_description">มีหลักฐานว่าโพสต์นี้มีการลงนามก่อน %1$s หลักฐานถูกประทับตราใน Bitcoin blockchain ณ วันและเวลาดังกล่าว</string>
<string name="edit_article">แก้ไขบทความ</string>
@@ -574,7 +574,6 @@
<string name="forked_from">คัดลอกมาจาก</string>
<string name="git_web_address">เว็บไซน์</string>
<string name="git_clone_address">สำเนา:</string>
<string name="existed_since">OTS: %1$s</string>
<string name="ots_info_title">Timestamp ถูกยืนยัน</string>
<string name="ots_info_description">มีหลักฐานว่าโพสต์นี้มีการลงนามก่อน %1$s หลักฐานถูกประทับตราใน Bitcoin blockchain ณ วันและเวลาดังกล่าว</string>
<string name="edit_post">แก้ไขโพสต์</string>
@@ -2996,8 +2996,6 @@
<string name="nsite_open">Aç</string>
<string name="existed_since">OTS: %1$s</string>
<string name="ots_info_title">Zaman Damgası Kanıtı</string>
<string name="ots_info_description">Bu gönderinin %1$s tarihinden önce imzalandığına dair kanıt var. Kanıt, o tarih ve saatte Bitcoin blok zincirine işlendi.</string>
@@ -2786,8 +2786,6 @@
<string name="nsite_open">Відкрити</string>
<string name="existed_since">OTS: %1$s</string>
<string name="ots_info_title">Доказ часової позначки</string>
<string name="ots_info_description">Є доказ того, що ця публікація була підписана до %1$s. Доказ засвідчено в блокчейні Bitcoin на ту дату та час.</string>
@@ -2697,7 +2697,6 @@
<string name="nsite_source">源:</string>
<string name="nsite_servers">服务器:</string>
<string name="nsite_open">打开</string>
<string name="existed_since">OTS:%1$s</string>
<string name="ots_info_title">OpenTimestamps 证明</string>
<string name="ots_info_description">%1$s之前的某个时候签署了此帖子的证明。此证明是在那个日期和时间在比特币区块链中盖章的。</string>
<string name="edit_article">编辑文章</string>
@@ -2232,7 +2232,6 @@
<string name="nsite_source">源:</string>
<string name="nsite_servers">服务器:</string>
<string name="nsite_open">打开</string>
<string name="existed_since">OTS:%1$s</string>
<string name="ots_info_title">OpenTimestamps 证明</string>
<string name="ots_info_description">%1$s之前的某个时候签署了此帖子的证明。此证明是在那个日期和时间在比特币区块链中盖章的。</string>
<string name="edit_article">编辑文章</string>
@@ -2232,7 +2232,6 @@
<string name="nsite_source">源:</string>
<string name="nsite_servers">服务器:</string>
<string name="nsite_open">打开</string>
<string name="existed_since">OTS:%1$s</string>
<string name="ots_info_title">OpenTimestamps 证明</string>
<string name="ots_info_description">%1$s之前的某个时候签署了此帖子的证明。此证明是在那个日期和时间在比特币区块链中盖章的。</string>
<string name="edit_article">编辑文章</string>
@@ -2209,7 +2209,6 @@
<string name="nsite_source">來源:</string>
<string name="nsite_servers">伺服器:</string>
<string name="nsite_open">開啟</string>
<string name="existed_since">OTS:%1$s</string>
<string name="ots_info_title">時間戳證明</string>
<string name="ots_info_description">有在 %1$s 之前的某個時間簽署了此帖子的證明。此證明是在那個日期和時間在比特幣區塊鏈中記錄的時間戳。</string>
<string name="edit_article">編輯文章</string>
@@ -4214,8 +4214,6 @@
<string name="nsite_open">打开</string>
<string name="existed_since">OTS:%1$s</string>
<string name="ots_info_title">OpenTimestamps 证明</string>
<string name="ots_info_description">%1$s之前的某个时候签署了此帖子的证明。此证明是在那个日期和时间在比特币区块链中盖章的。</string>
+8 -1
View File
@@ -45,6 +45,7 @@
<string name="timestamp_it">Timestamp it</string>
<string name="timestamp_pending">Timestamp: Pending Confirmations</string>
<string name="timestamp_pending_short">OTS: Pending</string>
<string name="one_year_plus">1y+</string>
<string name="request_deletion">Request Deletion</string>
<string name="block_report">Block / Report</string>
<string name="block_hide_user"><![CDATA[Block & Hide User]]></string>
@@ -1233,6 +1234,8 @@
<string name="public_bookmark_presence_indicator">is a public bookmark here</string>
<string name="private_bookmark_presence_indicator">is a private bookmark here</string>
<string name="private_rumor_mark">Private — only visible to tagged participants</string>
<string name="private_rumor_info_title">Private Note</string>
<string name="private_rumor_info_description">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.</string>
<string name="private_note">Make private: gift-wrap the note to the notified users only</string>
<string name="disable_private_note">Make public</string>
<string name="private_note_locked">Replies to a private note always stay private</string>
@@ -2823,6 +2826,7 @@
<string name="add_expiration_date">Add expiration date</string>
<string name="remove_expiration_date">Remove expiration date</string>
<string name="expiration_date_label">Expiration Date</string>
<string name="expiration_info_description">The author asked relays to delete this note after %1$s. It will disappear from feeds once it expires.</string>
<string name="expiration_date_explainer">Post will be hidden by clients after this date (NIP-40)</string>
<string name="expiration_date_select">Select expiration date and time</string>
<string name="expiration_expires_in">Expires in %1$s</string>
@@ -3019,10 +3023,10 @@
<string name="nsite_source">Source:</string>
<string name="nsite_servers">Servers:</string>
<string name="nsite_open">Open</string>
<string name="existed_since">OTS: %1$s</string>
<string name="ots_info_title">Timestamp Proof</string>
<string name="ots_info_description">There\'s proof this post was signed sometime before %1$s. The proof was stamped in the Bitcoin blockchain at that date and time.</string>
<string name="ots_info_pending_description">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.</string>
<string name="edit_article">Edit Article</string>
<string name="edit_post">Edit Post</string>
@@ -3043,6 +3047,8 @@
<string name="it_s_not_possible_to_react_to_a_draft_note">It\'s not possible to react a draft note</string>
<string name="it_s_not_possible_to_zap_to_a_draft_note">It\'s not possible to zap a draft note</string>
<string name="draft_note">Draft Note</string>
<string name="draft">Draft</string>
<string name="bounty_label">Bounty</string>
<string name="load_from_text">From Msg</string>
@@ -3875,6 +3881,7 @@
<string name="compose_signature_setting_description">Added at the end of the message when opening a new post, reply, quote, or article. Leave empty to disable.</string>
<string name="compose_signature_setting_hint">Your signature</string>
<string name="pow_settings_title">Proof of Work</string>
<string name="pow_info_description">The author spent computing power to mine this note to difficulty %1$s — a proof-of-work stamp that makes spamming expensive.</string>
<string name="pow_difficulty_title">Difficulty</string>
<string name="pow_difficulty_explainer">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.</string>
<string name="pow_difficulty_off">Off</string>
@@ -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")
@@ -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),
)
}
@@ -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,
)
}
}
}
@@ -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,
)
}
}
}
@@ -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)
@@ -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
+12
View File
@@ -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 <ttf>` to patch the existing font).
@@ -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 <path-to-ttf>
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'<path[^>]*\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])
@@ -0,0 +1,130 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="2000.000000pt" height="2056.000000pt" viewBox="0 0 2000.000000 2056.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.16, written by Peter Selinger 2001-2019
</metadata>
<g transform="translate(0.000000,2056.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M5403 20550 c-99 -20 -200 -78 -273 -156 -106 -114 -129 -183 -141
-419 -10 -213 -34 -326 -102 -469 -56 -117 -111 -198 -194 -287 -92 -97 -180
-161 -308 -224 -166 -81 -320 -115 -535 -115 -159 0 -268 -41 -376 -142 -78
-73 -136 -181 -154 -285 -8 -48 -10 -196 -8 -474 4 -388 5 -406 26 -463 60
-161 165 -266 322 -324 55 -21 93 -26 245 -33 195 -9 235 -15 350 -54 362
-121 632 -425 711 -800 112 -530 -198 -1065 -719 -1240 -88 -29 -243 -55 -333
-55 -166 0 -316 -96 -386 -246 -121 -259 33 -553 317 -605 85 -16 309 1 472
36 670 143 1221 636 1433 1285 86 261 116 529 90 791 -69 681 -482 1270 -1096
1564 -185 88 -406 151 -629 177 -60 7 -94 14 -75 15 348 25 661 132 947 323
219 146 385 312 534 534 173 256 292 583 315 864 4 45 10 79 14 77 4 -2 10
-42 14 -87 4 -46 18 -134 32 -197 93 -417 318 -789 637 -1058 557 -467 1315
-588 1982 -317 448 183 817 534 1021 969 88 188 155 429 171 610 3 42 9 78 13
80 4 2 10 -31 13 -74 14 -185 88 -443 181 -635 228 -470 634 -826 1127 -990
338 -113 669 -132 1014 -61 582 121 1079 510 1340 1049 96 199 165 431 181
616 9 93 22 135 23 70 2 -133 61 -383 132 -560 210 -525 645 -937 1182 -1115
335 -111 667 -131 1011 -60 641 133 1174 588 1411 1205 59 152 100 329 121
525 l8 70 8 -90 c32 -356 177 -720 404 -1012 130 -166 328 -342 509 -453 248
-152 555 -254 830 -278 l90 -8 -104 -12 c-240 -28 -437 -85 -639 -182 -976
-471 -1377 -1637 -895 -2604 250 -503 706 -873 1243 -1011 112 -28 266 -55
330 -56 67 -1 19 -14 -105 -28 -192 -22 -420 -92 -607 -188 -1064 -541 -1392
-1883 -700 -2858 224 -315 571 -572 945 -700 128 -43 311 -84 419 -92 43 -3
75 -9 73 -13 -2 -4 -38 -10 -80 -13 -113 -10 -265 -43 -395 -87 -705 -234
-1207 -840 -1311 -1580 -7 -52 -13 -167 -13 -255 0 -88 6 -203 13 -255 104
-740 606 -1346 1311 -1580 130 -44 282 -77 395 -87 42 -3 78 -9 80 -13 2 -4
-31 -10 -74 -13 -43 -3 -125 -16 -182 -28 -621 -128 -1145 -556 -1392 -1139
-242 -570 -195 -1232 126 -1758 301 -494 825 -841 1377 -912 188 -24 185 -23
90 -31 -432 -39 -860 -241 -1174 -553 -336 -335 -534 -768 -569 -1246 -1 -19
-8 15 -15 75 -80 676 -478 1244 -1077 1536 -622 303 -1345 254 -1920 -129
-327 -219 -572 -518 -718 -877 -75 -184 -121 -368 -134 -534 -7 -101 -15 -86
-32 65 -71 623 -499 1208 -1089 1486 -819 387 -1795 154 -2354 -563 -217 -278
-370 -658 -395 -980 -3 -43 -9 -76 -13 -74 -4 2 -10 38 -13 80 -16 183 -87
434 -176 620 -225 472 -637 837 -1128 1001 -920 308 -1931 -112 -2353 -979
-100 -206 -171 -447 -186 -635 -4 -45 -10 -85 -14 -87 -4 -2 -10 31 -13 74
-14 175 -84 430 -169 611 -196 419 -545 761 -965 950 -186 83 -412 143 -589
157 -47 3 -83 8 -81 10 3 2 44 8 93 14 660 74 1233 478 1525 1074 215 439 256
942 114 1409 -183 603 -644 1077 -1243 1276 -224 75 -530 116 -672 92 -144
-25 -263 -112 -321 -236 -136 -290 71 -616 392 -616 213 0 455 -81 628 -210
389 -290 538 -785 372 -1239 -119 -326 -400 -580 -739 -667 -75 -20 -137 -27
-277 -34 -193 -8 -247 -20 -343 -76 -115 -67 -217 -214 -241 -348 -17 -93 -14
-804 3 -887 22 -100 69 -184 146 -260 109 -107 221 -149 400 -149 318 0 579
-101 792 -308 218 -211 328 -483 328 -811 0 -180 42 -293 150 -402 73 -74 155
-121 252 -144 91 -21 825 -21 916 0 191 45 342 196 387 387 8 35 15 112 15
173 0 325 112 591 337 806 430 409 1106 393 1513 -37 202 -213 300 -463 300
-768 0 -62 7 -138 15 -174 45 -191 196 -342 387 -387 91 -21 825 -21 916 0
150 36 283 141 347 277 43 90 55 152 55 279 0 308 100 564 303 776 305 318
774 417 1184 250 237 -96 457 -311 569 -552 61 -132 84 -245 94 -450 8 -199
20 -250 81 -350 66 -111 204 -207 337 -234 33 -7 199 -11 445 -11 439 0 469 4
588 66 125 66 229 204 258 342 6 29 11 99 11 155 0 454 230 827 624 1012 537
253 1174 20 1427 -521 66 -139 90 -250 99 -459 9 -193 21 -249 77 -344 64
-111 209 -213 341 -240 86 -18 821 -14 900 4 136 32 248 111 324 229 57 89 69
139 79 331 4 94 15 202 23 241 97 457 465 797 926 854 46 5 126 10 177 10 50
0 116 5 145 11 167 35 317 167 380 335 21 57 22 74 26 480 4 467 1 498 -59
616 -57 114 -187 220 -314 258 -22 6 -121 15 -220 20 -203 10 -295 29 -436 89
-160 69 -322 197 -431 341 -62 81 -151 261 -177 359 -69 252 -43 518 74 756
113 230 273 387 508 501 142 68 258 94 472 104 142 6 192 12 239 29 146 52
255 161 317 316 24 59 24 64 27 481 3 352 1 432 -12 487 -45 195 -196 346
-388 392 -36 8 -112 15 -174 15 -305 0 -558 100 -771 303 -438 420 -438 1124
0 1544 213 203 466 303 771 303 62 0 138 7 174 15 192 46 343 197 388 392 13
55 15 135 12 487 -3 417 -3 422 -27 481 -62 155 -171 264 -317 316 -47 17 -97
23 -239 29 -213 10 -324 35 -469 103 -165 78 -302 188 -407 326 -65 86 -153
265 -179 366 -48 186 -48 388 1 566 26 98 115 278 177 359 111 146 269 271
432 341 140 60 233 79 435 89 99 5 198 14 220 20 127 38 257 144 314 258 60
118 63 149 59 616 -4 406 -5 423 -26 480 -63 168 -213 300 -380 335 -29 6 -95
11 -145 11 -313 0 -574 97 -784 293 -394 367 -452 987 -133 1416 209 281 525
441 872 441 146 0 215 13 308 57 136 65 240 197 275 348 14 58 16 132 13 489
-4 406 -5 423 -26 480 -30 82 -72 143 -140 208 -102 98 -211 138 -372 138
-452 0 -812 204 -1009 571 -86 160 -118 293 -130 539 -6 118 -14 190 -25 220
-54 146 -175 267 -322 322 -57 21 -74 22 -480 26 -353 3 -432 1 -487 -12 -180
-42 -315 -164 -378 -342 -17 -47 -23 -96 -29 -239 -13 -270 -57 -419 -185
-610 -70 -106 -226 -255 -331 -316 -99 -58 -235 -112 -339 -135 -97 -21 -322
-24 -410 -5 -168 35 -325 104 -452 200 -284 214 -433 526 -433 908 0 50 -5
116 -11 145 -35 167 -167 317 -335 380 -57 21 -74 22 -480 26 -353 3 -432 1
-487 -12 -198 -46 -351 -201 -393 -395 -7 -36 -14 -116 -14 -177 0 -297 -99
-547 -300 -758 -272 -286 -665 -396 -1055 -296 -101 26 -280 114 -366 179
-190 144 -335 364 -395 596 -20 78 -27 140 -34 280 -6 142 -12 192 -29 239
-52 146 -161 255 -316 317 -59 23 -64 24 -477 27 -268 2 -438 -1 -475 -8 -95
-17 -196 -71 -268 -143 -111 -111 -155 -231 -155 -423 0 -361 -153 -675 -429
-885 -86 -65 -265 -153 -366 -179 -384 -99 -783 12 -1052 293 -203 213 -303
466 -303 771 0 192 -44 312 -155 423 -71 70 -173 126 -264 143 -64 12 -826 12
-888 0z M7805 16719 c-287 -54 -531 -250 -643 -521 -62 -148 -62 -154 -62
-956 0 -640 2 -735 16 -783 23 -76 56 -128 119 -188 99 -94 229 -135 356 -113
184 32 324 169 359 351 6 32 10 247 10 526 0 443 1 477 20 536 41 132 147 239
277 279 65 20 87 20 3378 20 3254 0 3314 0 3376 -19 136 -43 242 -152 283
-290 15 -50 16 -525 16 -5275 0 -3482 -4 -5236 -11 -5265 -35 -152 -168 -285
-320 -320 -62 -15 -6627 -14 -6689 0 -145 33 -266 146 -311 288 -18 58 -19 95
-19 536 0 279 -4 494 -10 526 -44 232 -263 390 -486 352 -166 -29 -297 -141
-346 -296 -17 -55 -18 -110 -18 -788 0 -804 0 -808 62 -958 58 -140 176 -286
303 -377 66 -47 172 -97 265 -126 l75 -23 3785 -3 c2643 -2 3806 1 3855 8 170
26 328 105 457 228 133 127 215 275 252 454 15 70 16 607 16 5757 0 6220 5
5741 -59 5907 -72 187 -237 368 -416 456 -63 32 -163 64 -239 77 -77 14 -7577
13 -7651 0z M11479 14127 c-155 -54 -259 -174 -288 -331 -8 -41 -11 -478 -11
-1416 l0 -1357 -57 -38 c-228 -151 -366 -418 -366 -705 0 -422 288 -766 708
-845 166 -32 343 -8 503 66 146 67 282 182 357 302 l30 47 722 0 c497 0 739 4
775 11 200 43 338 215 338 423 0 177 -104 327 -280 402 -43 18 -82 19 -800 22
l-755 3 -23 40 c-35 61 -165 192 -233 236 l-59 38 0 1381 c0 1349 0 1383 -20
1446 -37 120 -135 225 -253 270 -32 12 -85 22 -137 25 -71 3 -96 0 -151 -20z
M4944 12608 c-253 -16 -522 -104 -774 -251 -141 -83 -226 -144 -595 -433 -335
-261 -521 -350 -760 -361 -136 -7 -263 9 -377 47 -172 58 -299 136 -588 361
-377 294 -547 401 -790 494 -186 72 -522 141 -644 132 -131 -9 -244 -72 -329
-183 -130 -171 -109 -423 47 -580 98 -97 221 -141 371 -130 75 5 103 3 165
-14 175 -49 309 -131 650 -396 466 -363 725 -495 1105 -565 141 -26 397 -36
530 -20 403 48 706 190 1158 544 369 289 452 347 597 416 126 61 221 82 365
82 343 0 542 -88 990 -442 218 -171 310 -238 446 -320 227 -137 455 -223 707
-265 117 -19 322 -30 322 -17 0 6 18 13 41 17 110 18 235 119 291 235 27 57
33 80 36 161 3 78 0 106 -17 155 -43 122 -138 223 -253 267 -39 15 -90 22
-193 28 -123 6 -153 12 -250 44 -179 60 -289 129 -625 391 -343 268 -555 398
-802 489 -248 92 -543 133 -824 114z M4887 9810 c-266 -33 -522 -128 -777
-285 -94 -59 -281 -197 -520 -384 -400 -315 -636 -408 -940 -372 -236 29 -391
104 -665 325 -438 354 -600 458 -871 561 -114 44 -310 97 -404 110 -25 3 -90
15 -145 27 -88 18 -109 19 -175 9 -268 -41 -442 -299 -375 -556 58 -220 272
-366 492 -335 130 18 366 -64 563 -195 47 -32 184 -137 305 -234 318 -255 484
-362 700 -448 256 -102 573 -149 840 -124 415 39 746 192 1215 561 407 320
518 393 690 448 289 94 634 27 950 -183 38 -25 167 -122 287 -216 120 -94 279
-211 353 -261 241 -161 496 -270 746 -318 133 -25 373 -47 383 -33 3 5 26 15
51 22 127 36 228 122 283 239 30 63 32 74 32 178 0 107 -1 112 -37 185 -46 93
-113 158 -206 202 -61 29 -79 32 -202 38 -285 15 -446 92 -845 404 -369 288
-514 382 -740 480 -253 108 -460 153 -750 160 -93 2 -201 0 -238 -5z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.0 KiB

+4
View File
@@ -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))"