git.iko.soy/opencode-android
on 325e359 help sign in
80 lines 3.4 kB history raw
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"
28 val weeks = days / 7
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)
33 return "${years}y"
36/**
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.
42 */
43@Composable
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) {
51 tick++
52 onPauseOrDispose { /* nothing to clean up */ }
53 }
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) {
61 while (true) {
62 delay(intervalMs)
63 tick++
64 }
65 }
66 }
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) {
73 derivedStateOf {
74 // Read tick to create a recomposition dependency on the timer.
75 tick
76 relativeTime(epochMillis)
77 }
78 }
79 return formatted