git.iko.soy/opencode-android
on d643c13 help sign in
65 lines 2.7 kB history raw
1package soy.iko.opencode.ui.components
3import androidx.compose.runtime.Composable
4import androidx.compose.runtime.LaunchedEffect
5import androidx.compose.runtime.getValue
6import androidx.compose.runtime.mutableLongStateOf
7import androidx.compose.runtime.remember
8import androidx.compose.runtime.setValue
9import androidx.lifecycle.compose.LocalLifecycleOwner
10import kotlinx.coroutines.delay
11import java.util.concurrent.TimeUnit
13/** Formats an epoch-millis timestamp as a short relative string (e.g. "3m", "2h", "5d"). */
14fun relativeTime(epochMillis: Long?): String {
15 if (epochMillis == null || epochMillis <= 0) return ""
16 val diff = System.currentTimeMillis() - epochMillis
17 if (diff < 0) return "now"
18 val minutes = TimeUnit.MILLISECONDS.toMinutes(diff)
19 if (minutes < 1) return "now"
20 if (minutes < 60) return "${minutes}m"
21 val hours = TimeUnit.MILLISECONDS.toHours(diff)
22 if (hours < 24) return "${hours}h"
23 val days = TimeUnit.MILLISECONDS.toDays(diff)
24 if (days < 7) return "${days}d"
25 val weeks = days / 7
26 if (weeks < 5) return "${weeks}w"
27 val months = days / 30
28 if (months < 12) return "${months}mo"
29 val years = (days / 365).coerceAtLeast(1)
30 return "${years}y"
33/**
34 * Returns a relative-time string for [epochMillis] that auto-refreshes so the label
35 * doesn't go stale (e.g. "3m" → "4m" → "5m" while the screen is visible). Re-evaluates
36 * every [intervalMs] (default 30 s — coarse enough for battery, fine enough for the
37 * smallest unit we show, which is minutes). Pauses the timer when the lifecycle is
38 * not at least resumed (screen off / app backgrounded) to avoid battery drain.
39 */
40@Composable
41fun rememberRelativeTime(epochMillis: Long?, intervalMs: Long = 30_000L): String {
42 var tick by remember { mutableLongStateOf(0L) }
43 val lifecycleOwner = LocalLifecycleOwner.current
44 LaunchedEffect(epochMillis, lifecycleOwner) {
45 if (epochMillis == null) return@LaunchedEffect
46 // Observe lifecycle to pause the timer when the screen isn't visible.
47 val observer = androidx.lifecycle.LifecycleEventObserver { _, event ->
48 if (event == androidx.lifecycle.Lifecycle.Event.ON_RESUME) {
49 tick++
50 }
51 }
52 lifecycleOwner.lifecycle.addObserver(observer)
53 try {
54 while (true) {
55 delay(intervalMs)
56 tick++
57 }
58 } finally {
59 lifecycleOwner.lifecycle.removeObserver(observer)
60 }
61 }
62 // Read tick so Compose tracks it as a dependency and recomposes when it changes.
63 @Suppress("UNUSED_EXPRESSION") tick
64 return relativeTime(epochMillis)