@@ -1,6 +1,14 @@
1package soy.iko.opencode.ui.components
1package soy.iko.opencode.ui.components
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 {
160 return formatted
172 return formatted
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.
181+fun absoluteTime(context: Context, epochMillis: Long?): String {
182+ if (epochMillis == null || epochMillis <= 0) return ""
183+ return DateUtils.formatDateTime(
186+ DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_SHOW_YEAR,
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
197+@OptIn(ExperimentalMaterial3Api::class)
199+fun RelativeTimeText(
201+ modifier: Modifier = Modifier,
202+ style: TextStyle = MaterialTheme.typography.labelSmall,
203+ color: Color = MaterialTheme.colorScheme.onSurfaceVariant,
205+ val relative = rememberRelativeTime(epochMillis)
206+ if (relative.isEmpty()) return
207+ val context = LocalContext.current
208+ val absolute = remember(epochMillis) { absoluteTime(context, epochMillis) }
210+ positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
211+ tooltip = { PlainTooltip { Text(absolute) } },
212+ state = rememberTooltipState(),
214+ Text(relative, style = style, color = color, modifier = modifier)