feat: tap TimeAgo to toggle relative ↔ absolute timestamp

Every TimeAgo composable (Android NoteCompose timestamp, NormalTimeAgo,
ChatTimeAgo, ChatroomHeaderCompose last-message time) and every Desktop
timestamp Text (NoteCard, NotificationsScreen, ChatPane, ConversationListPane)
is now clickable and toggles to a scale-adjusted absolute date/time:

  - same day → time only (e.g. "14:32"), locale-aware
  - same year → "MMM dd, HH:mm"
  - older    → "MMM dd, yyyy"

State is hoisted per-call site via rememberSaveable so the toggle survives
scroll-induced disposal in lazy lists. Desktop call sites share a small
ToggleableTimeAgoText wrapper; Android keeps its existing composable shapes
and just gains a clickable modifier + state.

https://claude.ai/code/session_01AuPon9VQeRfKV1BTVQuKGC
This commit is contained in:
Claude
2026-05-13 19:17:55 +00:00
parent 781e8484ac
commit 38463e5f2c
10 changed files with 223 additions and 17 deletions
@@ -56,13 +56,13 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.rememberMaterialSymbolPa
import com.vitorpamplona.amethyst.commons.state.EventCollectionState
import com.vitorpamplona.amethyst.commons.ui.components.EmptyState
import com.vitorpamplona.amethyst.commons.ui.components.LoadingState
import com.vitorpamplona.amethyst.commons.util.toTimeAgo
import com.vitorpamplona.amethyst.desktop.account.AccountState
import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache
import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager
import com.vitorpamplona.amethyst.desktop.subscriptions.DesktopRelaySubscriptionsCoordinator
import com.vitorpamplona.amethyst.desktop.subscriptions.createNotificationsSubscription
import com.vitorpamplona.amethyst.desktop.subscriptions.rememberSubscription
import com.vitorpamplona.amethyst.desktop.ui.components.ToggleableTimeAgoText
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArrayOrNull
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
@@ -342,8 +342,8 @@ fun NotificationCard(notification: NotificationItem) {
)
}
Text(
text = notification.timestamp.toTimeAgo(withDot = false),
ToggleableTimeAgoText(
timestamp = notification.timestamp,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
)
@@ -84,10 +84,10 @@ import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider
import com.vitorpamplona.amethyst.commons.service.upload.UploadOrchestrator
import com.vitorpamplona.amethyst.commons.ui.components.LoadingState
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
import com.vitorpamplona.amethyst.commons.util.toTimeAgo
import com.vitorpamplona.amethyst.commons.viewmodels.ChatNewMessageState
import com.vitorpamplona.amethyst.commons.viewmodels.ChatroomFeedViewModel
import com.vitorpamplona.amethyst.desktop.DesktopPreferences
import com.vitorpamplona.amethyst.desktop.ui.components.ToggleableTimeAgoText
import com.vitorpamplona.amethyst.desktop.ui.media.DesktopFilePicker
import com.vitorpamplona.amethyst.desktop.ui.media.MediaAttachmentRow
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
@@ -528,8 +528,8 @@ private fun MessageWithReactions(
// Timestamp
note.createdAt()?.let { timestamp ->
Text(
text = timestamp.toTimeAgo(withDot = false),
ToggleableTimeAgoText(
timestamp = timestamp,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f),
)
@@ -66,7 +66,7 @@ import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.commons.ui.components.UserAvatar
import com.vitorpamplona.amethyst.commons.util.toTimeAgo
import com.vitorpamplona.amethyst.desktop.ui.components.ToggleableTimeAgoText
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
import kotlinx.coroutines.launch
@@ -363,8 +363,8 @@ private fun ConversationCard(
if (item.lastMessageTimestamp > 0) {
Spacer(Modifier.width(8.dp))
Text(
text = item.lastMessageTimestamp.toTimeAgo(withDot = false),
ToggleableTimeAgoText(
timestamp = item.lastMessageTimestamp,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f),
)
@@ -0,0 +1,67 @@
/*
* 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.desktop.ui.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
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.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import com.vitorpamplona.amethyst.commons.util.timeAbsolute
import com.vitorpamplona.amethyst.commons.util.toTimeAgo
/**
* Timestamp text that toggles between a relative ("5m") and an absolute, scale-adjusted
* ("14:32" / "Dec 12, 14:32" / "Dec 12, 2023") form when the user clicks it.
*
* The toggle is local to the call site and persists across recomposition via
* [rememberSaveable], so it survives scroll-induced disposal in a LazyColumn.
*/
@Composable
fun ToggleableTimeAgoText(
timestamp: Long,
style: TextStyle,
color: Color,
modifier: Modifier = Modifier,
) {
var showAbsolute by rememberSaveable(timestamp) { mutableStateOf(false) }
val interactionSource = remember { MutableInteractionSource() }
val text =
if (showAbsolute) timeAbsolute(timestamp, withDot = false) else timestamp.toTimeAgo(withDot = false)
Text(
text = text,
style = style,
color = color,
modifier =
modifier.clickable(
interactionSource = interactionSource,
indication = null,
) { showAbsolute = !showAbsolute },
)
}
@@ -59,8 +59,8 @@ import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
import com.vitorpamplona.amethyst.commons.richtext.UrlParser
import com.vitorpamplona.amethyst.commons.richtext.Urls
import com.vitorpamplona.amethyst.commons.ui.components.UserAvatar
import com.vitorpamplona.amethyst.commons.util.toTimeAgo
import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache
import com.vitorpamplona.amethyst.desktop.ui.components.ToggleableTimeAgoText
import com.vitorpamplona.amethyst.desktop.ui.media.AnimatedGifImage
import com.vitorpamplona.amethyst.desktop.ui.media.AudioPlayer
import com.vitorpamplona.amethyst.desktop.ui.media.DesktopVideoPlayer
@@ -202,8 +202,8 @@ fun NoteCard(
}
// Timestamp
Text(
text = note.createdAt.toTimeAgo(withDot = false),
ToggleableTimeAgoText(
timestamp = note.createdAt,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)