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
@@ -21,7 +21,10 @@
package com.vitorpamplona.amethyst.commons.util
import com.vitorpamplona.quartz.utils.TimeUtils
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date
import java.util.Locale
private const val YEAR_DATE_FORMAT = "MMM dd, yyyy"
@@ -30,12 +33,14 @@ private const val MONTH_DATE_FORMAT = "MMM dd"
private var locale = Locale.getDefault()
private var yearFormatter = SimpleDateFormat(YEAR_DATE_FORMAT, locale)
private var monthFormatter = SimpleDateFormat(MONTH_DATE_FORMAT, locale)
private var timeOnlyFormatter = DateFormat.getTimeInstance(DateFormat.SHORT, locale)
private fun updateFormattersIfNeeded() {
if (locale != Locale.getDefault()) {
locale = Locale.getDefault()
yearFormatter = SimpleDateFormat(YEAR_DATE_FORMAT, locale)
monthFormatter = SimpleDateFormat(MONTH_DATE_FORMAT, locale)
timeOnlyFormatter = DateFormat.getTimeInstance(DateFormat.SHORT, locale)
}
}
@@ -145,3 +150,39 @@ fun dateFormatter(
* Extension function for Long timestamps.
*/
fun Long.toTimeAgo(withDot: Boolean = true): String = timeAgo(this, withDot)
/**
* Formats a Unix timestamp (seconds) as an absolute date/time string. Granularity
* depends on how far in the past the timestamp is:
* - same day → time only (e.g. "14:32"), locale-aware
* - same year → "MMM dd, HH:mm"
* - older → "MMM dd, yyyy"
*/
fun timeAbsolute(
time: Long?,
withDot: Boolean = true,
never: String = "never",
): String {
if (time == null) return " "
val prefix = if (withDot) "" else ""
if (time == 0L) return prefix + never
updateFormattersIfNeeded()
val timeMs = time * 1000
val now = Calendar.getInstance()
val then = Calendar.getInstance().apply { timeInMillis = timeMs }
val sameYear = now.get(Calendar.YEAR) == then.get(Calendar.YEAR)
val sameDay = sameYear && now.get(Calendar.DAY_OF_YEAR) == then.get(Calendar.DAY_OF_YEAR)
val timeOfDay = timeOnlyFormatter.format(Date(timeMs))
return when {
sameDay -> prefix + timeOfDay
sameYear -> prefix + monthFormatter.format(timeMs) + ", " + timeOfDay
else -> prefix + yearFormatter.format(timeMs)
}
}
fun Long.toTimeAbsolute(withDot: Boolean = true): String = timeAbsolute(this, withDot)