refactor(timeago): consolidate toggle into one stable composable

Audit follow-up — the toggle behaviour now lives in a single
`ToggleableTimeAgoText` core in `ui/note/elements/TimeAgo.kt`. `TimeAgo`,
`NormalTimeAgo`, `ChatTimeAgo`, and `ChatroomHeaderCompose.TimeAgo` are
thin wrappers that pick a `TimeAgoStyle` (Dotted / Short) and pass
colour/font params; no per-site duplication of state + clickable +
derivedStateOf.

Performance fixes that matter for a feed with hundreds of timestamps:

- `rememberSaveable` → `remember`. Persisting a transient peek-toggle to
  the SavedStateRegistry for every visible+scrolled-past note was pure
  memory bloat. Recycling now resets to relative, which is the expected
  behaviour for a transient inspect action.
- The relative-mode `derivedStateOf` lambda reads `nowState.value` only
  when displaying a relative time. An item the user has frozen to its
  absolute date no longer re-evaluates every 30-second tick.
- Core composable takes only stable primitive params (Long, Color,
  TextUnit, TextOverflow, enum) so Compose can skip it entirely when
  inputs don't change.
- Desktop wrapper dropped the redundant `derivedStateOf`: its formatter
  reads no State, so derivedStateOf had nothing to observe.

https://claude.ai/code/session_01AuPon9VQeRfKV1BTVQuKGC
This commit is contained in:
Claude
2026-05-13 19:31:15 +00:00
parent 38463e5f2c
commit 4d2886d162
4 changed files with 112 additions and 115 deletions
@@ -27,7 +27,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -39,8 +38,16 @@ import com.vitorpamplona.amethyst.commons.util.toTimeAgo
* Timestamp text that toggles between a relative ("5m") and an absolute, scale-adjusted
* ("14:32" / "Dec 12, 14:32" / "Dec 12, 2023") form when the user clicks it.
*
* The toggle is local to the call site and persists across recomposition via
* [rememberSaveable], so it survives scroll-induced disposal in a LazyColumn.
* Notes for the audit:
* - Plain [remember] (not `rememberSaveable`): the toggle is a transient peek. Persisting
* it for every visible+scrolled-past notification/chat/note across config changes is
* pure memory bloat — recycling resets to relative, which is the right default.
* - Both formatters here are pure functions of the timestamp, so for desktop we don't need
* any tick subscription at all (desktop has no shared "now" ticker). The string is
* stable for a given (timestamp, showAbsolute) pair, so a [derivedStateOf] also isn't
* needed — we just compute on flip.
* - All params are primitives/value types, so the Compose compiler can skip this composable
* when nothing changes.
*/
@Composable
fun ToggleableTimeAgoText(
@@ -49,13 +56,11 @@ fun ToggleableTimeAgoText(
color: Color,
modifier: Modifier = Modifier,
) {
var showAbsolute by rememberSaveable(timestamp) { mutableStateOf(false) }
val interactionSource = remember { MutableInteractionSource() }
val text =
if (showAbsolute) timeAbsolute(timestamp, withDot = false) else timestamp.toTimeAgo(withDot = false)
var showAbsolute by remember(timestamp) { mutableStateOf(false) }
Text(
text = text,
text = if (showAbsolute) timeAbsolute(timestamp, withDot = false) else timestamp.toTimeAgo(withDot = false),
style = style,
color = color,
modifier =