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:
@@ -21,11 +21,14 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.note
|
package com.vitorpamplona.amethyst.ui.note
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.text.format.DateFormat
|
||||||
import android.text.format.DateUtils
|
import android.text.format.DateUtils
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import kotlin.math.round
|
import kotlin.math.round
|
||||||
|
|
||||||
@@ -42,6 +45,51 @@ var monthFormatter = SimpleDateFormat(MONTH_DATE_FORMAT, locale)
|
|||||||
var yearNoDayFormatter = SimpleDateFormat(YEAR_NO_DAY_DATE_FORMAT, locale)
|
var yearNoDayFormatter = SimpleDateFormat(YEAR_NO_DAY_DATE_FORMAT, locale)
|
||||||
var monthNoDayFormatter = SimpleDateFormat(MONTH_NO_DAY_DATE_FORMAT, locale)
|
var monthNoDayFormatter = SimpleDateFormat(MONTH_NO_DAY_DATE_FORMAT, locale)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a Unix timestamp (seconds) as an absolute date/time string, picking the
|
||||||
|
* granularity from how far away the timestamp is:
|
||||||
|
* - same day → time only (e.g. "14:32" / "2:32 PM", locale-aware)
|
||||||
|
* - same year → "MMM dd, HH:mm"
|
||||||
|
* - older → "MMM dd, yyyy"
|
||||||
|
*
|
||||||
|
* Used by [com.vitorpamplona.amethyst.ui.note.elements.TimeAgo] when the user
|
||||||
|
* taps the relative timestamp to reveal the absolute one.
|
||||||
|
*/
|
||||||
|
fun timeAbsolute(
|
||||||
|
time: Long?,
|
||||||
|
context: Context,
|
||||||
|
prefix: String = " • ",
|
||||||
|
): String {
|
||||||
|
if (time == null) return " "
|
||||||
|
if (time == 0L) return prefix + stringRes(context, R.string.never)
|
||||||
|
|
||||||
|
if (locale != Locale.getDefault()) {
|
||||||
|
locale = Locale.getDefault()
|
||||||
|
yearFormatter = SimpleDateFormat(YEAR_DATE_FORMAT, locale)
|
||||||
|
monthFormatter = SimpleDateFormat(MONTH_DATE_FORMAT, locale)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = DateFormat.getTimeFormat(context).format(Date(timeMs))
|
||||||
|
|
||||||
|
return when {
|
||||||
|
sameDay -> prefix + timeOfDay
|
||||||
|
sameYear -> prefix + monthFormatter.format(timeMs) + ", " + timeOfDay
|
||||||
|
else -> prefix + yearFormatter.format(timeMs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun timeAbsoluteNoDot(
|
||||||
|
time: Long?,
|
||||||
|
context: Context,
|
||||||
|
): String = timeAbsolute(time, context, prefix = "")
|
||||||
|
|
||||||
fun timeAgo(
|
fun timeAgo(
|
||||||
time: Long?,
|
time: Long?,
|
||||||
context: Context,
|
context: Context,
|
||||||
|
|||||||
@@ -20,17 +20,24 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.ui.note.elements
|
package com.vitorpamplona.amethyst.ui.note.elements
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
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.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.timeAbsolute
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.timeAbsoluteNoDot
|
||||||
import com.vitorpamplona.amethyst.ui.note.timeAgo
|
import com.vitorpamplona.amethyst.ui.note.timeAgo
|
||||||
import com.vitorpamplona.amethyst.ui.note.timeAgoShort
|
import com.vitorpamplona.amethyst.ui.note.timeAgoShort
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
@@ -48,11 +55,14 @@ fun TimeAgo(time: Long) {
|
|||||||
// Subscribe to the shared coarse ticker; `derivedStateOf` ensures the Text only
|
// Subscribe to the shared coarse ticker; `derivedStateOf` ensures the Text only
|
||||||
// recomposes when the formatted string actually flips (e.g. 1m → 2m), not on every tick.
|
// recomposes when the formatted string actually flips (e.g. 1m → 2m), not on every tick.
|
||||||
val nowState = LocalNowSeconds.current
|
val nowState = LocalNowSeconds.current
|
||||||
|
var showAbsolute by rememberSaveable(time) { mutableStateOf(false) }
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
|
||||||
val timeStr by
|
val timeStr by
|
||||||
remember(time, context, nowState) {
|
remember(time, context, nowState) {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
nowState.value
|
nowState.value
|
||||||
timeAgo(time, context = context)
|
if (showAbsolute) timeAbsolute(time, context) else timeAgo(time, context = context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,6 +70,11 @@ fun TimeAgo(time: Long) {
|
|||||||
text = timeStr,
|
text = timeStr,
|
||||||
color = MaterialTheme.colorScheme.placeholderText,
|
color = MaterialTheme.colorScheme.placeholderText,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
|
modifier =
|
||||||
|
Modifier.clickable(
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
indication = null,
|
||||||
|
) { showAbsolute = !showAbsolute },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,12 +85,16 @@ fun NormalTimeAgo(
|
|||||||
) {
|
) {
|
||||||
val nowStr = stringRes(id = R.string.now)
|
val nowStr = stringRes(id = R.string.now)
|
||||||
val nowState = LocalNowSeconds.current
|
val nowState = LocalNowSeconds.current
|
||||||
|
val context = LocalContext.current
|
||||||
|
var showAbsolute by rememberSaveable(baseNote.idHex) { mutableStateOf(false) }
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
|
||||||
val time by
|
val time by
|
||||||
remember(baseNote, nowStr, nowState) {
|
remember(baseNote, nowStr, nowState) {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
nowState.value
|
nowState.value
|
||||||
timeAgoShort(baseNote.createdAt() ?: 0L, nowStr)
|
val createdAt = baseNote.createdAt() ?: 0L
|
||||||
|
if (showAbsolute) timeAbsoluteNoDot(createdAt, context) else timeAgoShort(createdAt, nowStr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,6 +102,10 @@ fun NormalTimeAgo(
|
|||||||
text = time,
|
text = time,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
modifier = modifier,
|
modifier =
|
||||||
|
modifier.clickable(
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
indication = null,
|
||||||
|
) { showAbsolute = !showAbsolute },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-1
@@ -20,6 +20,8 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
@@ -28,7 +30,10 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
@@ -38,6 +43,7 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
|||||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.ui.note.elements.LocalNowSeconds
|
import com.vitorpamplona.amethyst.ui.note.elements.LocalNowSeconds
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.timeAbsoluteNoDot
|
||||||
import com.vitorpamplona.amethyst.ui.note.timeAgoShort
|
import com.vitorpamplona.amethyst.ui.note.timeAgoShort
|
||||||
import com.vitorpamplona.amethyst.ui.note.timeAheadNoDot
|
import com.vitorpamplona.amethyst.ui.note.timeAheadNoDot
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
@@ -50,11 +56,16 @@ import com.vitorpamplona.quartz.nip40Expiration.expiration
|
|||||||
fun ChatTimeAgo(baseNote: Note) {
|
fun ChatTimeAgo(baseNote: Note) {
|
||||||
val nowStr = stringRes(id = R.string.now)
|
val nowStr = stringRes(id = R.string.now)
|
||||||
val nowState = LocalNowSeconds.current
|
val nowState = LocalNowSeconds.current
|
||||||
|
val context = LocalContext.current
|
||||||
|
var showAbsolute by rememberSaveable(baseNote.idHex) { mutableStateOf(false) }
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
|
||||||
val time by
|
val time by
|
||||||
remember(baseNote, nowStr, nowState) {
|
remember(baseNote, nowStr, nowState) {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
nowState.value
|
nowState.value
|
||||||
timeAgoShort(baseNote.createdAt() ?: 0L, nowStr)
|
val createdAt = baseNote.createdAt() ?: 0L
|
||||||
|
if (showAbsolute) timeAbsoluteNoDot(createdAt, context) else timeAgoShort(createdAt, nowStr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,6 +74,11 @@ fun ChatTimeAgo(baseNote: Note) {
|
|||||||
color = MaterialTheme.colorScheme.placeholderText,
|
color = MaterialTheme.colorScheme.placeholderText,
|
||||||
fontSize = Font12SP,
|
fontSize = Font12SP,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
|
modifier =
|
||||||
|
Modifier.clickable(
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
indication = null,
|
||||||
|
) { showAbsolute = !showAbsolute },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+12
-1
@@ -20,6 +20,8 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.material3.LocalTextStyle
|
import androidx.compose.material3.LocalTextStyle
|
||||||
@@ -31,6 +33,7 @@ import androidx.compose.runtime.derivedStateOf
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
@@ -62,6 +65,7 @@ import com.vitorpamplona.amethyst.ui.note.LoadPublicChatChannel
|
|||||||
import com.vitorpamplona.amethyst.ui.note.NonClickableUserPictures
|
import com.vitorpamplona.amethyst.ui.note.NonClickableUserPictures
|
||||||
import com.vitorpamplona.amethyst.ui.note.ObserveDraftEvent
|
import com.vitorpamplona.amethyst.ui.note.ObserveDraftEvent
|
||||||
import com.vitorpamplona.amethyst.ui.note.elements.LocalNowSeconds
|
import com.vitorpamplona.amethyst.ui.note.elements.LocalNowSeconds
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.timeAbsolute
|
||||||
import com.vitorpamplona.amethyst.ui.note.timeAgo
|
import com.vitorpamplona.amethyst.ui.note.timeAgo
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.header.RoomNameDisplay
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.header.RoomNameDisplay
|
||||||
@@ -486,17 +490,24 @@ private fun TimeAgo(channelLastTime: Long?) {
|
|||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val nowState = LocalNowSeconds.current
|
val nowState = LocalNowSeconds.current
|
||||||
|
var showAbsolute by rememberSaveable(channelLastTime) { mutableStateOf(false) }
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
val timeAgo by
|
val timeAgo by
|
||||||
remember(channelLastTime, context, nowState) {
|
remember(channelLastTime, context, nowState) {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
nowState.value
|
nowState.value
|
||||||
timeAgo(channelLastTime, context)
|
if (showAbsolute) timeAbsolute(channelLastTime, context) else timeAgo(channelLastTime, context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
text = timeAgo,
|
text = timeAgo,
|
||||||
color = MaterialTheme.colorScheme.grayText,
|
color = MaterialTheme.colorScheme.grayText,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
|
modifier =
|
||||||
|
Modifier.clickable(
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
indication = null,
|
||||||
|
) { showAbsolute = !showAbsolute },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+41
@@ -21,7 +21,10 @@
|
|||||||
package com.vitorpamplona.amethyst.commons.util
|
package com.vitorpamplona.amethyst.commons.util
|
||||||
|
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
|
import java.text.DateFormat
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
private const val YEAR_DATE_FORMAT = "MMM dd, yyyy"
|
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 locale = Locale.getDefault()
|
||||||
private var yearFormatter = SimpleDateFormat(YEAR_DATE_FORMAT, locale)
|
private var yearFormatter = SimpleDateFormat(YEAR_DATE_FORMAT, locale)
|
||||||
private var monthFormatter = SimpleDateFormat(MONTH_DATE_FORMAT, locale)
|
private var monthFormatter = SimpleDateFormat(MONTH_DATE_FORMAT, locale)
|
||||||
|
private var timeOnlyFormatter = DateFormat.getTimeInstance(DateFormat.SHORT, locale)
|
||||||
|
|
||||||
private fun updateFormattersIfNeeded() {
|
private fun updateFormattersIfNeeded() {
|
||||||
if (locale != Locale.getDefault()) {
|
if (locale != Locale.getDefault()) {
|
||||||
locale = Locale.getDefault()
|
locale = Locale.getDefault()
|
||||||
yearFormatter = SimpleDateFormat(YEAR_DATE_FORMAT, locale)
|
yearFormatter = SimpleDateFormat(YEAR_DATE_FORMAT, locale)
|
||||||
monthFormatter = SimpleDateFormat(MONTH_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.
|
* Extension function for Long timestamps.
|
||||||
*/
|
*/
|
||||||
fun Long.toTimeAgo(withDot: Boolean = true): String = timeAgo(this, withDot)
|
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)
|
||||||
|
|||||||
+3
-3
@@ -56,13 +56,13 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.rememberMaterialSymbolPa
|
|||||||
import com.vitorpamplona.amethyst.commons.state.EventCollectionState
|
import com.vitorpamplona.amethyst.commons.state.EventCollectionState
|
||||||
import com.vitorpamplona.amethyst.commons.ui.components.EmptyState
|
import com.vitorpamplona.amethyst.commons.ui.components.EmptyState
|
||||||
import com.vitorpamplona.amethyst.commons.ui.components.LoadingState
|
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.account.AccountState
|
||||||
import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache
|
import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache
|
||||||
import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager
|
import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager
|
||||||
import com.vitorpamplona.amethyst.desktop.subscriptions.DesktopRelaySubscriptionsCoordinator
|
import com.vitorpamplona.amethyst.desktop.subscriptions.DesktopRelaySubscriptionsCoordinator
|
||||||
import com.vitorpamplona.amethyst.desktop.subscriptions.createNotificationsSubscription
|
import com.vitorpamplona.amethyst.desktop.subscriptions.createNotificationsSubscription
|
||||||
import com.vitorpamplona.amethyst.desktop.subscriptions.rememberSubscription
|
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.Event
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArrayOrNull
|
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArrayOrNull
|
||||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||||
@@ -342,8 +342,8 @@ fun NotificationCard(notification: NotificationItem) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
ToggleableTimeAgoText(
|
||||||
text = notification.timestamp.toTimeAgo(withDot = false),
|
timestamp = notification.timestamp,
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
|
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
|
||||||
)
|
)
|
||||||
|
|||||||
+3
-3
@@ -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.service.upload.UploadOrchestrator
|
||||||
import com.vitorpamplona.amethyst.commons.ui.components.LoadingState
|
import com.vitorpamplona.amethyst.commons.ui.components.LoadingState
|
||||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
|
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.ChatNewMessageState
|
||||||
import com.vitorpamplona.amethyst.commons.viewmodels.ChatroomFeedViewModel
|
import com.vitorpamplona.amethyst.commons.viewmodels.ChatroomFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.desktop.DesktopPreferences
|
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.DesktopFilePicker
|
||||||
import com.vitorpamplona.amethyst.desktop.ui.media.MediaAttachmentRow
|
import com.vitorpamplona.amethyst.desktop.ui.media.MediaAttachmentRow
|
||||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
||||||
@@ -528,8 +528,8 @@ private fun MessageWithReactions(
|
|||||||
|
|
||||||
// Timestamp
|
// Timestamp
|
||||||
note.createdAt()?.let { timestamp ->
|
note.createdAt()?.let { timestamp ->
|
||||||
Text(
|
ToggleableTimeAgoText(
|
||||||
text = timestamp.toTimeAgo(withDot = false),
|
timestamp = timestamp,
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f),
|
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f),
|
||||||
)
|
)
|
||||||
|
|||||||
+3
-3
@@ -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.Icon
|
||||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||||
import com.vitorpamplona.amethyst.commons.ui.components.UserAvatar
|
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 com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -363,8 +363,8 @@ private fun ConversationCard(
|
|||||||
|
|
||||||
if (item.lastMessageTimestamp > 0) {
|
if (item.lastMessageTimestamp > 0) {
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
Text(
|
ToggleableTimeAgoText(
|
||||||
text = item.lastMessageTimestamp.toTimeAgo(withDot = false),
|
timestamp = item.lastMessageTimestamp,
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f),
|
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f),
|
||||||
)
|
)
|
||||||
|
|||||||
+67
@@ -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 },
|
||||||
|
)
|
||||||
|
}
|
||||||
+3
-3
@@ -59,8 +59,8 @@ import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
|
|||||||
import com.vitorpamplona.amethyst.commons.richtext.UrlParser
|
import com.vitorpamplona.amethyst.commons.richtext.UrlParser
|
||||||
import com.vitorpamplona.amethyst.commons.richtext.Urls
|
import com.vitorpamplona.amethyst.commons.richtext.Urls
|
||||||
import com.vitorpamplona.amethyst.commons.ui.components.UserAvatar
|
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.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.AnimatedGifImage
|
||||||
import com.vitorpamplona.amethyst.desktop.ui.media.AudioPlayer
|
import com.vitorpamplona.amethyst.desktop.ui.media.AudioPlayer
|
||||||
import com.vitorpamplona.amethyst.desktop.ui.media.DesktopVideoPlayer
|
import com.vitorpamplona.amethyst.desktop.ui.media.DesktopVideoPlayer
|
||||||
@@ -202,8 +202,8 @@ fun NoteCard(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Timestamp
|
// Timestamp
|
||||||
Text(
|
ToggleableTimeAgoText(
|
||||||
text = note.createdAt.toTimeAgo(withDot = false),
|
timestamp = note.createdAt,
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user