git.iko.soy/opencode-android
on master help sign in
Reveal absolute timestamp on long-press of relative time labels
2811bd0iko
app/src/main/java/soy/iko/opencode/ui/chat/MessageBubble.kt+4−9
@@ -33,7 +33,7 @@ import soy.iko.opencode.data.model.UserMessage
33import soy.iko.opencode.data.network.NetworkConfig
33import soy.iko.opencode.data.network.NetworkConfig
34import soy.iko.opencode.ui.components.ImageLoadContext
34import soy.iko.opencode.ui.components.ImageLoadContext
35import soy.iko.opencode.ui.components.copyToClipboard
35import soy.iko.opencode.ui.components.copyToClipboard
36−import soy.iko.opencode.ui.components.rememberRelativeTime
36+import soy.iko.opencode.ui.components.RelativeTimeText
37
37
38/**
38/**
39 * Resolve an assistant message's model id to a friendly label from the loaded catalog,
39 * Resolve an assistant message's model id to a friendly label from the loaded catalog,
@@ -250,14 +250,9 @@ private fun AssistantBlock(
250
250
251@Composable
251@Composable
252private fun MessageTimestampText(info: soy.iko.opencode.data.model.MessageInfo) {
252private fun MessageTimestampText(info: soy.iko.opencode.data.model.MessageInfo) {
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).
253− val t = rememberRelativeTime(info.time?.created)
255+ RelativeTimeText(info.time?.created)
254− if (t.isNotEmpty()) {
255− Text(
256− t,
257− style = MaterialTheme.typography.labelSmall,
258− color = MaterialTheme.colorScheme.onSurfaceVariant,
259− )
260− }
261}
256}
262
257
263// NumberFormat.getNumberInstance performs an expensive ICU locale lookup + object
258// 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 @@
1package soy.iko.opencode.ui.components
1package soy.iko.opencode.ui.components
2
2
3+import android.content.Context
3import android.text.format.DateUtils
4import 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
4import androidx.compose.runtime.Composable
12import androidx.compose.runtime.Composable
5import androidx.compose.runtime.compositionLocalOf
13import androidx.compose.runtime.compositionLocalOf
6import androidx.compose.runtime.derivedStateOf
14import androidx.compose.runtime.derivedStateOf
@@ -8,6 +16,10 @@ import androidx.compose.runtime.getValue
8import androidx.compose.runtime.mutableLongStateOf
16import androidx.compose.runtime.mutableLongStateOf
9import androidx.compose.runtime.remember
17import androidx.compose.runtime.remember
10import androidx.compose.runtime.setValue
18import 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
11import androidx.lifecycle.Lifecycle
23import androidx.lifecycle.Lifecycle
12import androidx.lifecycle.compose.LocalLifecycleOwner
24import androidx.lifecycle.compose.LocalLifecycleOwner
13import androidx.lifecycle.repeatOnLifecycle
25import androidx.lifecycle.repeatOnLifecycle
@@ -159,3 +171,46 @@ private fun rememberRelativeTimeStandalone(epochMillis: Long?): String {
159 }
171 }
160 return formatted
172 return formatted
161}
173}
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
87import soy.iko.opencode.R
87import soy.iko.opencode.R
88import soy.iko.opencode.ui.components.ConnectionBanner
88import soy.iko.opencode.ui.components.ConnectionBanner
89import soy.iko.opencode.ui.components.LocalRelativeTimeTick
89import soy.iko.opencode.ui.components.LocalRelativeTimeTick
90−import soy.iko.opencode.ui.components.rememberRelativeTime
90+import soy.iko.opencode.ui.components.RelativeTimeText
91import soy.iko.opencode.ui.components.rememberRelativeTimeTick
91import soy.iko.opencode.ui.components.rememberRelativeTimeTick
92import soy.iko.opencode.ui.vmFactory
92import soy.iko.opencode.ui.vmFactory
93
93
@@ -586,15 +586,11 @@ private fun SessionCard(
586 overflow = TextOverflow.Ellipsis,
586 overflow = TextOverflow.Ellipsis,
587 )
587 )
588 }
588 }
589+ // Long-press the relative label to reveal the full absolute timestamp.
590+ RelativeTimeText(
589− val time = rememberRelativeTime(session.time?.updated ?: session.time?.created)
591+ 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),
592+ modifier = Modifier.padding(top = 2.dp),
596− )
593+ )
597− }
598 }
594 }
599 // Overflow menu replaces the inline edit + delete IconButtons so the
595 // Overflow menu replaces the inline edit + delete IconButtons so the
600 // card row is decluttered — two always-visible icons per row made a
596 // card row is decluttered — two always-visible icons per row made a