git.iko.soy/opencode-android
on master help sign in
Reveal absolute timestamp on long-press of relative time labels

Add a reusable RelativeTimeText component that keeps the compact relative label ("3m", "2h") but shows the full, locale-aware absolute timestamp in a tooltip on long-press/hover. Wire it into chat message timestamps and session list rows, where the timestamp stands alone. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

commit2811bd0e88c8a458b0fe68338a4608f3882a4162 authoriko <mail@iko.soy> dateThu 2 Jul 2026, 04:17 (3 months ago) parentfe5705b
3 files changed+65−19
app/src/main/java/soy/iko/opencode/ui/chat/MessageBubble.kt+4−9 app/src/main/java/soy/iko/opencode/ui/components/TimeFormat.kt+55 app/src/main/java/soy/iko/opencode/ui/session/SessionListScreen.kt+6−10
app/src/main/java/soy/iko/opencode/ui/chat/MessageBubble.kt+4−9
@@ -33,7 +33,7 @@ import soy.iko.opencode.data.model.UserMessage
3333import soy.iko.opencode.data.network.NetworkConfig
3434import soy.iko.opencode.ui.components.ImageLoadContext
3535import soy.iko.opencode.ui.components.copyToClipboard
36−import soy.iko.opencode.ui.components.rememberRelativeTime
36+import soy.iko.opencode.ui.components.RelativeTimeText
3737
3838/**
3939 * Resolve an assistant message's model id to a friendly label from the loaded catalog,
@@ -250,14 +250,9 @@ private fun AssistantBlock(
250250
251251@Composable
252252private fun MessageTimestampText(info: soy.iko.opencode.data.model.MessageInfo) {
253− val t = rememberRelativeTime(info.time?.created)
254− if (t.isNotEmpty()) {
255− Text(
256− t,
257− style = MaterialTheme.typography.labelSmall,
258− color = MaterialTheme.colorScheme.onSurfaceVariant,
259− )
260− }
253+ // Compact relative label by default; long-press reveals the full absolute timestamp
254+ // (RelativeTimeText renders nothing when the time is absent, so no isNotEmpty guard).
255+ RelativeTimeText(info.time?.created)
261256}
262257
263258// NumberFormat.getNumberInstance performs an expensive ICU locale lookup + object
app/src/main/java/soy/iko/opencode/ui/components/TimeFormat.kt+55
@@ -1,6 +1,14 @@
11package soy.iko.opencode.ui.components
22
3+import android.content.Context
34import android.text.format.DateUtils
5+import androidx.compose.material3.ExperimentalMaterial3Api
6+import androidx.compose.material3.MaterialTheme
7+import androidx.compose.material3.PlainTooltip
8+import androidx.compose.material3.Text
9+import androidx.compose.material3.TooltipBox
10+import androidx.compose.material3.TooltipDefaults
11+import androidx.compose.material3.rememberTooltipState
412import androidx.compose.runtime.Composable
513import androidx.compose.runtime.compositionLocalOf
614import androidx.compose.runtime.derivedStateOf
@@ -8,6 +16,10 @@ import androidx.compose.runtime.getValue
816import androidx.compose.runtime.mutableLongStateOf
917import androidx.compose.runtime.remember
1018import androidx.compose.runtime.setValue
19+import androidx.compose.ui.Modifier
20+import androidx.compose.ui.graphics.Color
21+import androidx.compose.ui.platform.LocalContext
22+import androidx.compose.ui.text.TextStyle
1123import androidx.lifecycle.Lifecycle
1224import androidx.lifecycle.compose.LocalLifecycleOwner
1325import androidx.lifecycle.repeatOnLifecycle
@@ -159,3 +171,46 @@ private fun rememberRelativeTimeStandalone(epochMillis: Long?): String {
159171 }
160172 return formatted
161173}
174+
175+/**
176+ * Formats [epochMillis] as a full, locale-aware absolute timestamp (date + time + year),
177+ * e.g. "Jul 2, 2026, 14:33". Complements the compact [relativeTime] label shown in lists
178+ * and bubbles — the relative form is scannable, the absolute form is precise. Returns an
179+ * empty string for a null/invalid timestamp. Uses the device locale and 12/24-hour setting.
180+ */
181+fun absoluteTime(context: Context, epochMillis: Long?): String {
182+ if (epochMillis == null || epochMillis <= 0) return ""
183+ return DateUtils.formatDateTime(
184+ context,
185+ epochMillis,
186+ DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_SHOW_YEAR,
187+ )
188+}
189+
190+/**
191+ * A relative-time label (auto-refreshing, per [rememberRelativeTime]) that reveals the full
192+ * absolute timestamp in a tooltip on long-press (or hover). Lets a compact "3m"/"2h" stay
193+ * the default while the exact date/time is one long-press away — without spending screen
194+ * space on it. Renders nothing when the timestamp is absent so callers can drop it in
195+ * unconditionally.
196+ */
197+@OptIn(ExperimentalMaterial3Api::class)
198+@Composable
199+fun RelativeTimeText(
200+ epochMillis: Long?,
201+ modifier: Modifier = Modifier,
202+ style: TextStyle = MaterialTheme.typography.labelSmall,
203+ color: Color = MaterialTheme.colorScheme.onSurfaceVariant,
204+) {
205+ val relative = rememberRelativeTime(epochMillis)
206+ if (relative.isEmpty()) return
207+ val context = LocalContext.current
208+ val absolute = remember(epochMillis) { absoluteTime(context, epochMillis) }
209+ TooltipBox(
210+ positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
211+ tooltip = { PlainTooltip { Text(absolute) } },
212+ state = rememberTooltipState(),
213+ ) {
214+ Text(relative, style = style, color = color, modifier = modifier)
215+ }
216+}
app/src/main/java/soy/iko/opencode/ui/session/SessionListScreen.kt+6−10
@@ -87,7 +87,7 @@ import soy.iko.opencode.di.AppContainer
8787import soy.iko.opencode.R
8888import soy.iko.opencode.ui.components.ConnectionBanner
8989import soy.iko.opencode.ui.components.LocalRelativeTimeTick
90−import soy.iko.opencode.ui.components.rememberRelativeTime
90+import soy.iko.opencode.ui.components.RelativeTimeText
9191import soy.iko.opencode.ui.components.rememberRelativeTimeTick
9292import soy.iko.opencode.ui.vmFactory
9393
@@ -586,15 +586,11 @@ private fun SessionCard(
586586 overflow = TextOverflow.Ellipsis,
587587 )
588588 }
589− val time = rememberRelativeTime(session.time?.updated ?: session.time?.created)
590− if (time.isNotEmpty()) {
591− Text(
592− time,
593− style = MaterialTheme.typography.labelSmall,
594− color = MaterialTheme.colorScheme.onSurfaceVariant,
595− modifier = Modifier.padding(top = 2.dp),
596− )
597− }
589+ // Long-press the relative label to reveal the full absolute timestamp.
590+ RelativeTimeText(
591+ session.time?.updated ?: session.time?.created,
592+ modifier = Modifier.padding(top = 2.dp),
593+ )
598594 }
599595 // Overflow menu replaces the inline edit + delete IconButtons so the
600596 // card row is decluttered — two always-visible icons per row made a