Merge remote-tracking branch 'origin/claude/kind-keller-itzy63'

Vitor's day-divider clock fix (89232618) landed in the same file the
bubble refactor restructured, and in the three functions it moved:
`DaySeparator`, `sameDayAs` and `localDayOf` now live in
CordnMessageRow.kt beside the row that draws them.

Carried his fix over rather than dropping either side: `rememberToday`
and TODAY_POLL_MS move to CordnMessageRow.kt with the separator that
reads them, `DaySeparator` keeps its new `today` parameter, and the
screen still passes one clock per room. The conflicting hunks were the
old flat-row code the refactor deleted, so our side won those.

One tidy-up: his `val today = rememberToday()` had been inserted
directly under the PinnedRibbon comment block, which left that comment
describing the wrong statement. Moved above it with its own line.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012BfD4txdnsaPRXmNXbup9n
This commit is contained in:
Claude
2026-09-24 12:43:13 +00:00
2 changed files with 42 additions and 3 deletions
@@ -237,6 +237,9 @@ private fun CordnGroupChat(
},
) { padding ->
Column(Modifier.fillMaxSize().padding(padding)) {
// One clock for every divider in the room, so they cannot disagree.
val today = rememberToday()
// Pinned messages sit above the conversation rather than inside it.
// A pin is a claim about a message's importance, not a message, and
// leaving it only in place means the thing someone pinned scrolls
@@ -330,7 +333,7 @@ private fun CordnGroupChat(
// Drawn under the first message of each day, which in a
// reversed list means comparing against the older row.
if (!message.sameDayAs(older)) {
DaySeparator(message.envelope.createdAt)
DaySeparator(message.envelope.createdAt, today)
}
}
}
@@ -39,6 +39,7 @@ import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -84,9 +85,12 @@ import com.vitorpamplona.quartz.cordn.spec02Envelopes.CordnAnnotationIndex
import com.vitorpamplona.quartz.cordn.spec02Envelopes.CordnDeliveredMessage
import com.vitorpamplona.quartz.cordn.spec02Envelopes.CordnMessageReferences
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import kotlinx.coroutines.delay
import java.time.Duration
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import kotlin.math.abs
@@ -532,9 +536,11 @@ private fun localDayOf(epochSeconds: Long): LocalDate = Instant.ofEpochSecond(ep
* morning" sit flush against each other.
*/
@Composable
internal fun DaySeparator(createdAt: Long) {
internal fun DaySeparator(
createdAt: Long,
today: LocalDate,
) {
val day = remember(createdAt) { localDayOf(createdAt) }
val today = remember { LocalDate.now(ZoneId.systemDefault()) }
val label =
when (day) {
@@ -564,3 +570,33 @@ internal fun DaySeparator(createdAt: Long) {
HorizontalDivider(Modifier.weight(1f))
}
}
/** Upper bound on how long a stale "Today" can survive a clock correction. */
private const val TODAY_POLL_MS = 60_000L
/**
* Today, as a value that stops being today when it stops being today.
*
* [DaySeparator] used to hold `remember { LocalDate.now() }` of its own. That is a
* snapshot of the wall clock with nothing to invalidate it, and each separator keeps a
* separate one, so they can disagree: a separator composed before midnight goes on
* saying "Today" while the one for the new day says it too. Seen on the tablet — one
* room, two "Today" dividers.
*
* Polling rather than a single sleep to the next midnight, because a device clock does
* not only advance: it is corrected, and the tablet this was found on jumped nine hours
* in one step. Re-assigning an equal [LocalDate] is not a change, so a quiet minute
* costs no recomposition.
*/
@Composable
internal fun rememberToday(): LocalDate {
val zone = remember { ZoneId.systemDefault() }
return produceState(LocalDate.now(zone), zone) {
while (true) {
val now = ZonedDateTime.now(zone)
val untilMidnight = Duration.between(now, now.toLocalDate().plusDays(1).atStartOfDay(zone)).toMillis()
delay(untilMidnight.coerceIn(1_000L, TODAY_POLL_MS))
value = LocalDate.now(zone)
}
}.value
}