1package soy.iko.opencode.ui.components
3import androidx.compose.runtime.Composable
4import androidx.compose.runtime.compositionLocalOf
5import androidx.compose.runtime.derivedStateOf
6import androidx.compose.runtime.getValue
7import androidx.compose.runtime.mutableLongStateOf
8import androidx.compose.runtime.remember
9import androidx.compose.runtime.setValue
10import androidx.lifecycle.Lifecycle
11import androidx.lifecycle.compose.LocalLifecycleOwner
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)
36/** Re-evaluation interval for relative-time labels — coarse enough for battery, fine
37 * enough for the smallest unit we show (minutes). */
38private const val RELATIVE_TIME_INTERVAL_MS = 30_000L
41 * A shared, screen-level "tick" that advances periodically so every relative-time label
42 * in the subtree refreshes together. Provide it once near the top of a scrollable screen:
44 * val tick = rememberRelativeTimeTick()
45 * CompositionLocalProvider(LocalRelativeTimeTick provides tick) { ... }
47 * [rememberRelativeTime] then reads this single source instead of each timestamp spinning
48 * up its own coroutine + lifecycle observer — a list with many timestamped rows avoids a
49 * timer churn as items enter and leave the viewport.
51 * Defaults to 0 (no provider); [rememberRelativeTime] falls back to a self-managed timer
52 * then, so it keeps auto-refreshing even without a provider.
54val LocalRelativeTimeTick = compositionLocalOf<Long> { 0L }
57 * Advances a tick counter while the lifecycle is at least resumed (immediately on resume,
58 * then every [intervalMs]), and suspends while the screen isn't visible to avoid battery
59 * drain. Provides one shared time source for an entire subtree via [LocalRelativeTimeTick].
62fun rememberRelativeTimeTick(intervalMs: Long = RELATIVE_TIME_INTERVAL_MS): Long {
63 var tick by remember { mutableLongStateOf(0L) }
64 val lifecycleOwner = LocalLifecycleOwner.current
66 androidx.compose.runtime.LaunchedEffect(lifecycleOwner) {
67 lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
68 // Refresh immediately on resume (so a stale label doesn't wait an interval),
69 // then periodically so labels stay current.
81 * Formats [epochMillis] relative to now, recomputing as time passes. Prefers the shared
82 * screen-level tick from [LocalRelativeTimeTick] (one timer for the whole subtree); when
83 * no provider is present it falls back to its own lifecycle-bound timer, so callers don't
84 * need a provider to get auto-refreshing labels.
87fun rememberRelativeTime(epochMillis: Long?): String {
88 val sharedTick = LocalRelativeTimeTick.current
89 return if (sharedTick != 0L) {
90 // Reading sharedTick above registers a recomposition dependency on the shared
91 // ticker, so the label recomputes (cheaply) whenever the screen-level tick advances.
92 relativeTime(epochMillis)
94 rememberRelativeTimeStandalone(epochMillis)
98/** Self-contained auto-refreshing label used when no [LocalRelativeTimeTick] provider exists. */
100private fun rememberRelativeTimeStandalone(epochMillis: Long?): String {
101 var tick by remember { mutableLongStateOf(0L) }
102 val lifecycleOwner = LocalLifecycleOwner.current
104 androidx.compose.runtime.LaunchedEffect(epochMillis, lifecycleOwner) {
105 if (epochMillis == null) return@LaunchedEffect
106 lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
107 // Tick immediately on resume so a stale label refreshes without waiting for
108 // the first interval, then periodically.
111 delay(RELATIVE_TIME_INTERVAL_MS)
117 // derivedStateOf reads tick so Compose recomposes when it changes, and recomputes
118 // the relative-time string from the current wall clock on each tick. Keying remember
119 // on epochMillis ensures a new derivedStateOf is created when the timestamp changes.
120 val formatted by remember(epochMillis) {
122 // Read tick to create a recomposition dependency on the timer.
124 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