@@ -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+}