From c06fa67ccc1bfbab9ee3e3c015af996ab1797646 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 28 Jul 2026 22:24:35 +0200 Subject: [PATCH] wear: fix header size flip-flopping - drive header from the debounced dockedAtNewest state instead of raw per-frame layout geometry; header is full-size at the newest, compacts when browsing history --- .../java/com/bitchat/watch/ui/ChatScaffold.kt | 35 ++++++++++--------- .../java/com/bitchat/watch/ui/ChatScreen.kt | 5 +-- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt b/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt index 442665ae..6ff8c200 100644 --- a/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt +++ b/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt @@ -82,10 +82,13 @@ fun ChatScaffold( previousCount = messages.size } - // Bottom clearance hysteresis: expand near the newest message so it sits comfortably above - // the floating buttons; collapse when reading history so text flows behind them. - // (Thresholds 40/120 straddle the 48dp padding delta, breaking the maxValue feedback loop.) - var padExpanded by remember { mutableStateOf(true) } + // "Docked at newest" is the single source of truth for the chat's resting state: + // bottom clearance expanded (newest message sits above the floating buttons), action + // bar visible, header at full size. Scrolling into history collapses all three together; + // returning to the bottom restores them. Thresholds 40/120 straddle the 48dp padding + // delta, breaking the maxValue feedback loop, and give the header/buttons flicker-free + // hysteresis. + var dockedAtNewest by remember { mutableStateOf(true) } // Action bar hides while scrolling into history, returns toward the newest. val buttonsVisible = remember { mutableStateOf(true) } LaunchedEffect(columnState, messages.size) { @@ -96,12 +99,12 @@ fun ChatScaffold( }.collect { position -> val dist = bottomDist() if (dist < 40) { - padExpanded = true + dockedAtNewest = true buttonsVisible.value = true } else if (dist in 121..10_000) { // Clearly reading history (MAX_VALUE = last item not laid out yet; transient - // right after a new message arrives, so it must not collapse the padding). - padExpanded = false + // right after a new message arrives, so it must not undock the state). + dockedAtNewest = false } when { dist >= 40 && position < lastPosition - 24 -> buttonsVisible.value = false @@ -111,27 +114,25 @@ fun ChatScaffold( } } - // Stick to bottom: on new messages, follow to the last item while the user is near the - // bottom, and re-align whenever the bottom clearance expands (padding growth changes the - // scroll range). Gating on padExpanded (maintained by the layout collector above) avoids - // the stale-layoutInfo race of computing the distance here directly. - LaunchedEffect(columnState, messages.size, padExpanded) { - if (messages.isNotEmpty() && padExpanded) { + // Stick to bottom: on new messages, follow to the last item while the user is docked at + // the newest, and re-align whenever the bottom clearance expands (padding growth changes + // the scroll range). Gating on dockedAtNewest (maintained by the layout collector above) + // avoids the stale-layoutInfo race of computing the distance here directly. + LaunchedEffect(columnState, messages.size, dockedAtNewest) { + if (messages.isNotEmpty() && dockedAtNewest) { // scrollBy to the end of the range: animateScrollToItem stops as soon as the item // is partially visible, which left the last message cropped behind the buttons. columnState.scroll { scrollBy(Float.MAX_VALUE) } } } val listBottomPadding by animateDpAsState( - targetValue = if (padExpanded) 56.dp else 8.dp, + targetValue = if (dockedAtNewest) 56.dp else 8.dp, animationSpec = tween(BitchatMotion.STANDARD_MS), label = "listBottomPad" ) - // Header is dense near the newest messages, expands when reading history. - val headerExpanded = bottomDist() > 60 Column(modifier = Modifier.fillMaxSize()) { - header(headerExpanded) + header(dockedAtNewest) ChatBody( messages = messages, myPeerID = myPeerID, diff --git a/wear/src/main/java/com/bitchat/watch/ui/ChatScreen.kt b/wear/src/main/java/com/bitchat/watch/ui/ChatScreen.kt index 7ead1fc4..027c327c 100644 --- a/wear/src/main/java/com/bitchat/watch/ui/ChatScreen.kt +++ b/wear/src/main/java/com/bitchat/watch/ui/ChatScreen.kt @@ -100,8 +100,9 @@ private fun ChatHeader( expanded: Boolean, onOpenPeople: () -> Unit ) { - // Collapsing header: dense (small title, tiny icons) at the newest messages so the chat - // gets maximum space; scales up smoothly when the user scrolls into history. + // Collapsing header: full-size title and icons while docked at the newest messages (the + // chat's resting state); scales down smoothly as the user scrolls into history so the + // conversation gets maximum room. val spec = androidx.compose.animation.core.tween( BitchatMotion.STANDARD_MS )