1package soy.iko.opencode.ui.components
3import androidx.compose.runtime.Composable
4import androidx.compose.runtime.derivedStateOf
5import androidx.compose.runtime.getValue
6import androidx.compose.runtime.mutableLongStateOf
7import androidx.compose.runtime.remember
8import androidx.compose.runtime.setValue
9import androidx.lifecycle.Lifecycle
10import androidx.lifecycle.compose.LocalLifecycleOwner
11import androidx.lifecycle.compose.LifecycleResumeEffect
12import androidx.lifecycle.repeatOnLifecycle
13import kotlinx.coroutines.delay
14import java.util.concurrent.TimeUnit
16/** Formats an epoch-millis timestamp as a short relative string (e.g. "3m", "2h", "5d"). */
17fun relativeTime(epochMillis: Long?): String {
18 if (epochMillis == null || epochMillis <= 0) return ""
19 val diff = System.currentTimeMillis() - epochMillis
20 if (diff < 0) return "now"
21 val minutes = TimeUnit.MILLISECONDS.toMinutes(diff)
22 if (minutes < 1) return "now"
23 if (minutes < 60) return "${minutes}m"
24 val hours = TimeUnit.MILLISECONDS.toHours(diff)
25 if (hours < 24) return "${hours}h"
26 val days = TimeUnit.MILLISECONDS.toDays(diff)
27 if (days < 7) return "${days}d"
29 if (weeks < 5) return "${weeks}w"
30 val months = days / 30
31 if (months < 12) return "${months}mo"
32 val years = (days / 365).coerceAtLeast(1)
37 * Returns a relative-time string for [epochMillis] that auto-refreshes so the label
38 * doesn't go stale (e.g. "3m" → "4m" → "5m" while the screen is visible). Re-evaluates
39 * every [intervalMs] (default 30 s — coarse enough for battery, fine enough for the
40 * smallest unit we show, which is minutes). Pauses the timer when the lifecycle is
41 * not at least resumed (screen off / app backgrounded) to avoid battery drain.
44fun rememberRelativeTime(epochMillis: Long?, intervalMs: Long = 30_000L): String {
45 var tick by remember { mutableLongStateOf(0L) }
46 val lifecycleOwner = LocalLifecycleOwner.current
48 // Tick immediately on resume so a stale label is refreshed without waiting for
49 // the first delay interval to elapse.
50 LifecycleResumeEffect(epochMillis, lifecycleOwner) {
52 onPauseOrDispose { /* nothing to clean up */ }
55 // Drive the periodic refresh with repeatOnLifecycle so the delay loop is
56 // suspended (not running) when the screen isn't at least resumed. This avoids
57 // unnecessary CPU usage from timer coroutines while the app is backgrounded.
58 androidx.compose.runtime.LaunchedEffect(epochMillis, lifecycleOwner) {
59 if (epochMillis == null) return@LaunchedEffect
60 lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
68 // derivedStateOf reads tick so Compose recomposes when it changes, and recomputes
69 // the relative-time string from the current wall clock on each tick.
70 // Keying remember on epochMillis ensures a new derivedStateOf is created when the
71 // timestamp changes, so the new value is reflected immediately.
72 val formatted by remember(epochMillis) {
74 // Read tick to create a recomposition dependency on the timer.
76 relativeTime(epochMillis)
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list