From f734865a31d664956aaa13bcbe70717efc6e097a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 28 Jul 2026 16:29:32 +0000 Subject: [PATCH 1/3] fix(ime): stop the soft keyboard state from getting stuck open MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Leaving a chat with the keyboard up via a back gesture left a large IME padding stranded at the bottom — and, because WindowInsets.ime is a single app-wide holder, on every other screen too — until some later inset pass happened to rebalance it. It only reproduced on release builds. Root cause: the chat composers install a BackHandler that flushes the draft and pops the screen. When that pop runs while the keyboard is still visible, the predictive-back window animation races the IME's close animation. On an optimized release build the window animation wins, and the IME WindowInsetsAnimation is cancelled before its terminal (zero) frame reaches Compose, so the shared insets holder stays "animating" and every imePadding() in the app freezes at the keyboard height. Debug and benchmark builds are slow enough that the IME animation completes first, which is why they were unaffected. Fixes: - New KeyboardAwareBackHandler: while the keyboard is on screen it does not consume back, so the system dismisses the keyboard first (its animation completes cleanly); the next back runs the original handler. The top-bar back arrow remains an always-available exit. Adopted in the DM, public-chat and new-group-DM composers. - Rederive keyboardAsState() from WindowInsets.ime instead of the pre-edge-to-edge getWindowVisibleDisplayFrame/OnGlobalLayout heuristic, which under enableEdgeToEdge() could itself latch Opened after the keyboard closed. Now it tracks the same inset that drives imePadding(). - Concord channel chat and the minichat thread view used a bare Material3 Scaffold whose content insets ignore the IME; add imePadding() so the composer rides above the keyboard while typing. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012dT2RLbPZzan2hULL2cmc6 --- .../ui/navigation/bottombars/KeyboardState.kt | 87 +++++++++++-------- .../loggedIn/chats/minichat/MinichatScreen.kt | 5 +- .../chats/privateDM/send/NewGroupDMScreen.kt | 4 +- .../send/PrivateMessageEditFieldRow.kt | 4 +- .../concord/ConcordChannelScreen.kt | 6 +- .../chats/publicChannels/send/EditFieldRow.kt | 4 +- 6 files changed, 66 insertions(+), 44 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/KeyboardState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/KeyboardState.kt index 68e9ba6c66..1457927832 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/KeyboardState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/KeyboardState.kt @@ -20,52 +20,67 @@ */ package com.vitorpamplona.amethyst.ui.navigation.bottombars -import android.graphics.Rect -import android.view.View -import android.view.ViewTreeObserver +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.ime import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.State -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember -import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.platform.LocalDensity enum class KeyboardState { Opened, Closed, } +/** + * Whether the soft keyboard is currently on screen, derived from [WindowInsets.ime]. + * + * This intentionally reads the same animated IME inset that drives `Modifier.imePadding()` + * everywhere else in the app, so the two can never disagree. The previous implementation + * measured `View.getWindowVisibleDisplayFrame` from a `ViewTreeObserver` global-layout + * listener — a pre-edge-to-edge heuristic. Under `enableEdgeToEdge()` + * (`decorFitsSystemWindows = false`) the window content no longer resizes for the IME, so + * that listener fired when the keyboard appeared but frequently never fired again when it + * closed, latching the state at [Opened] even though the keyboard was gone (leaving the + * bottom navigation bar hidden and a stranded gap at the bottom). The IME inset always + * animates back to zero on the persistent root view, so this reading can't get stuck. + */ @Composable fun keyboardAsState(): State { - val view = LocalView.current - - val keyboardState = remember(view) { mutableStateOf(isKeyboardOpen(view)) } - - DisposableEffect(view) { - val onGlobalListener = - ViewTreeObserver.OnGlobalLayoutListener { - val newKeyboardValue = isKeyboardOpen(view) - - if (newKeyboardValue != keyboardState.value) { - keyboardState.value = newKeyboardValue - } - } - view.viewTreeObserver.addOnGlobalLayoutListener(onGlobalListener) - onDispose { view.viewTreeObserver.removeOnGlobalLayoutListener(onGlobalListener) } - } - - return keyboardState -} - -fun isKeyboardOpen(view: View): KeyboardState { - val rect = Rect() - view.getWindowVisibleDisplayFrame(rect) - val screenHeight = view.rootView.height - val keypadHeight = screenHeight - rect.bottom - - return if (keypadHeight > screenHeight * 0.15) { - KeyboardState.Opened - } else { - KeyboardState.Closed + val density = LocalDensity.current + val imeInsets = WindowInsets.ime + return remember(density, imeInsets) { + derivedStateOf { + if (imeInsets.getBottom(density) > 0) KeyboardState.Opened else KeyboardState.Closed + } } } + +/** + * A [BackHandler] that steps aside while the soft keyboard is on screen. + * + * Chat composers (and draft-saving editors) intercept back to flush a draft and pop the screen. + * When that pop happens while the keyboard is still up, it races the predictive-back window + * animation against the IME's close animation. On release builds — fast enough that the window + * animation wins — the IME [WindowInsetsAnimationCompat][androidx.core.view.WindowInsetsAnimationCompat] + * is cancelled before its terminal (zero) frame reaches Compose, so the shared `WindowInsets.ime` + * holder stays "animating" and every `Modifier.imePadding()` in the app freezes at the keyboard + * height until a later inset pass rebalances it (the "stuck IME padding" that survives leaving the + * screen). + * + * Gating on [keyboardAsState] fixes it: while the keyboard is visible we do NOT consume back, so the + * system dismisses the keyboard first with its own animation (which completes cleanly). The next + * back — keyboard already down — runs [onBack] as before. The top bar's back arrow stays an + * always-available exit, so this can never trap the user even if the inset reading were itself stale. + */ +@Composable +fun KeyboardAwareBackHandler( + enabled: Boolean = true, + onBack: () -> Unit, +) { + val keyboardState by keyboardAsState() + BackHandler(enabled = enabled && keyboardState == KeyboardState.Closed, onBack = onBack) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/minichat/MinichatScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/minichat/MinichatScreen.kt index 6f15e93574..8263fa2a2c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/minichat/MinichatScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/minichat/MinichatScreen.kt @@ -25,6 +25,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -174,7 +175,9 @@ fun MinichatScreen( ) }, ) { padding -> - Column(Modifier.fillMaxHeight().padding(padding)) { + // imePadding so the reply composer rides above the soft keyboard — the bare Material3 + // Scaffold's content insets cover the system bars but not the IME. + Column(Modifier.fillMaxHeight().imePadding().padding(padding)) { // Every reply here is rooted at [rootNote], which is already pinned at the top — so // suppress the redundant reply-to-root preview each reply would otherwise render. CompositionLocalProvider(LocalSuppressReplyToNoteId provides rootId) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt index d87f54987d..ce87e19328 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt @@ -21,7 +21,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send import android.net.Uri -import androidx.activity.compose.BackHandler import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy import androidx.compose.foundation.layout.Box @@ -87,6 +86,7 @@ import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideoButton import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField import com.vitorpamplona.amethyst.ui.components.ZoomableContentView +import com.vitorpamplona.amethyst.ui.navigation.bottombars.KeyboardAwareBackHandler import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.navs.Nav import com.vitorpamplona.amethyst.ui.navigation.routes.routeToMessage @@ -169,7 +169,7 @@ fun NewGroupDMScreen( WatchAndLoadMyEmojiList(accountViewModel) - BackHandler { + KeyboardAwareBackHandler { accountViewModel.launchSigner { postViewModel.sendDraftSync() postViewModel.cancel() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt index fc64efe0ab..390d718d9b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send -import androidx.activity.compose.BackHandler import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -60,6 +59,7 @@ import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField +import com.vitorpamplona.amethyst.ui.navigation.bottombars.KeyboardAwareBackHandler import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor @@ -110,7 +110,7 @@ fun PrivateMessageEditFieldRow( onSendNewMessage: () -> Unit, nav: INav, ) { - BackHandler { + KeyboardAwareBackHandler { if (channelScreenModel.message.text.isNotBlank()) { accountViewModel.launchSigner { channelScreenModel.sendDraftSync() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelScreen.kt index 160acdf42a..761bd36f17 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelScreen.kt @@ -26,6 +26,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material3.ExperimentalMaterial3Api @@ -185,7 +186,10 @@ fun ConcordChannelScreen( ) }, ) { padding -> - Column(Modifier.fillMaxHeight().padding(padding)) { + // imePadding so the composer rides above the soft keyboard. The bare Material3 Scaffold's + // content insets cover the system bars but not the IME, so without this the message field + // sat behind the keyboard while typing. + Column(Modifier.fillMaxHeight().imePadding().padding(padding)) { Column(Modifier.fillMaxHeight().weight(1f, true)) { RefreshingChatroomFeedView( feedContentState = feedViewModel.feedState, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt index 8176dc4d5b..f14cd1d59e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send -import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth @@ -54,6 +53,7 @@ import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField +import com.vitorpamplona.amethyst.ui.navigation.bottombars.KeyboardAwareBackHandler import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.ShowUserSuggestionList import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -78,7 +78,7 @@ fun EditFieldRow( onSendNewMessage: suspend () -> Unit, nav: INav, ) { - BackHandler { + KeyboardAwareBackHandler { accountViewModel.launchSigner { channelScreenModel.sendDraftSync() channelScreenModel.cancel() From 39e272f478c7885881508be48ee5e7b70590c2c3 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 28 Jul 2026 17:05:57 +0000 Subject: [PATCH 2/3] fix(ime): stop nav-bar padding from stacking on top of the IME inset in chats MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit While the keyboard was up, the public / relay-group / live-activity / ephemeral chats — and the Concord channel + minichat views — left an extra navigation-bar-height gap between the composer and the keyboard. WindowInsets.ime is measured from the bottom of the screen, so it already spans the nav-bar band; reserving the nav bar again on top of it double-counts. NIP-17 DMs were unaffected because their scaffold reserves the nav bar with the consuming navigationBarsPadding(), which excludes the already-consumed IME inset (a union), while the bottom-bar chats reserved it as a plain, non-excluding pad. - DisappearingScaffold: when the bottom-bar slot renders empty it now reserves max(0, navigationBars - ime) instead of the full nav-bar inset, so the reservation drops to zero while the keyboard is up (the root imePadding has already lifted the scaffold above it). Covers every bottom-bar chat at once. - ConcordChannelScreen / MinichatScreen: use the standard padding(padding).consumeWindowInsets(padding).imePadding() union instead of summing a plain padding(padding) with imePadding(). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012dT2RLbPZzan2hULL2cmc6 --- .../ui/layouts/DisappearingScaffold.kt | 19 ++++++++++++++----- .../loggedIn/chats/minichat/MinichatScreen.kt | 15 ++++++++++++--- .../concord/ConcordChannelScreen.kt | 16 ++++++++++++---- 3 files changed, 38 insertions(+), 12 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingScaffold.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingScaffold.kt index a9c963f370..cf6a752c31 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingScaffold.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingScaffold.kt @@ -25,6 +25,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.ime import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.navigationBarsPadding @@ -160,6 +161,7 @@ private fun ScaffoldLayout( mainContent: @Composable (padding: PaddingValues) -> Unit, ) { val navBarInsets = WindowInsets.navigationBars + val imeInsets = WindowInsets.ime SubcomposeLayout { constraints -> val layoutWidth = constraints.maxWidth val layoutHeight = constraints.maxHeight @@ -195,13 +197,20 @@ private fun ScaffoldLayout( }.firstOrNull()?.measure(looseConstraints) } // When the bar lambda is provided but its content emits nothing (e.g. AppBottomBar - // hides itself on canPop entries), reserve the system-nav-bar inset so the FAB and - // content stay clear of the navigation bar instead of sliding under it. The - // `bottomBar == null` branch is already handled by navigationBarsPadding on - // rootModifier. + // hides itself on canPop entries, or while the keyboard is up), reserve the + // system-nav-bar inset so the FAB and content stay clear of the navigation bar instead + // of sliding under it. Subtract the IME inset: the root imePadding has already lifted the + // whole scaffold above the keyboard, and WindowInsets.ime spans the nav-bar band, so + // reserving the full nav bar on top of that would double-count and strand a gap above the + // keyboard (the `bottomBar == null` branch gets this for free via navigationBarsPadding, + // which excludes the consumed IME inset). The `bottomBar == null` case is on rootModifier. val bottomHeight = (bottomPlaceable?.height ?: 0).let { measured -> - if (bottomBar != null && measured == 0) navBarInsets.getBottom(this) else measured + if (bottomBar != null && measured == 0) { + (navBarInsets.getBottom(this) - imeInsets.getBottom(this)).coerceAtLeast(0) + } else { + measured + } } // Publish the measured limits so the nested-scroll connection can clamp correctly. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/minichat/MinichatScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/minichat/MinichatScreen.kt index 8263fa2a2c..01af32e136 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/minichat/MinichatScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/minichat/MinichatScreen.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.minichat import android.widget.Toast import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -175,9 +176,17 @@ fun MinichatScreen( ) }, ) { padding -> - // imePadding so the reply composer rides above the soft keyboard — the bare Material3 - // Scaffold's content insets cover the system bars but not the IME. - Column(Modifier.fillMaxHeight().imePadding().padding(padding)) { + // The reply composer must ride above the soft keyboard — the bare Material3 Scaffold's + // content insets cover the system bars but not the IME. consumeWindowInsets(padding) before + // imePadding() unions the two so the nav-bar inset already in `padding` is dropped while the + // keyboard is up instead of stacking on top of it. + Column( + Modifier + .fillMaxHeight() + .padding(padding) + .consumeWindowInsets(padding) + .imePadding(), + ) { // Every reply here is rooted at [rootNote], which is already pinned at the top — so // suppress the redundant reply-to-root preview each reply would otherwise render. CompositionLocalProvider(LocalSuppressReplyToNoteId provides rootId) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelScreen.kt index 761bd36f17..13d186171f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelScreen.kt @@ -24,6 +24,7 @@ import android.widget.Toast import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding @@ -186,10 +187,17 @@ fun ConcordChannelScreen( ) }, ) { padding -> - // imePadding so the composer rides above the soft keyboard. The bare Material3 Scaffold's - // content insets cover the system bars but not the IME, so without this the message field - // sat behind the keyboard while typing. - Column(Modifier.fillMaxHeight().imePadding().padding(padding)) { + // The composer must ride above the soft keyboard: the bare Material3 Scaffold's content + // insets cover the system bars but not the IME. consumeWindowInsets(padding) before + // imePadding() unions the two so the nav-bar inset already in `padding` is dropped while + // the keyboard is up (WindowInsets.ime already spans that band) instead of stacking on top. + Column( + Modifier + .fillMaxHeight() + .padding(padding) + .consumeWindowInsets(padding) + .imePadding(), + ) { Column(Modifier.fillMaxHeight().weight(1f, true)) { RefreshingChatroomFeedView( feedContentState = feedViewModel.feedState, From eeefacca9ec4f88e3b0c2181abca3cf0864b509f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 28 Jul 2026 17:25:34 +0000 Subject: [PATCH 3/3] fix(ime): union nav-bar and IME insets on the two remaining bare-Scaffold forms Sweep of every imePadding() in the app for the same nav-bar-over-IME double-count fixed for the chats. Two more bare Material3 Scaffold screens applied the scaffold content padding (which carries the nav-bar inset) and then imePadding() without consuming in between, so the nav bar stacked on top of the IME while the keyboard was up: - NewGeohashChatScreen (geohash create form) - MarmotGroupInfoScreen (has the add-member search field) Both now consumeWindowInsets(pad) before imePadding(), matching the idiom the other forms already use. Every other imePadding() call was verified fine: DisappearingScaffold-based screens are handled by the scaffold's own reservation, dialogs/bottom sheets carry no nav-bar content padding, and the rest either apply imePadding() alone at a root or already use the navigationBarsPadding()/consumeWindowInsets union. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012dT2RLbPZzan2hULL2cmc6 --- .../screen/loggedIn/chats/geohashChat/NewGeohashChatScreen.kt | 4 ++++ .../loggedIn/chats/marmotGroup/MarmotGroupInfoScreen.kt | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/NewGeohashChatScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/NewGeohashChatScreen.kt index ebf206147a..13f71901cd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/NewGeohashChatScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/NewGeohashChatScreen.kt @@ -25,6 +25,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding @@ -106,6 +107,9 @@ fun NewGeohashChatScreen( .fillMaxSize() .padding(top = pad.calculateTopPadding(), bottom = pad.calculateBottomPadding()) .padding(horizontal = 16.dp) + // consume the scaffold insets so the trailing imePadding() only adds the part of + // the IME the nav-bar padding above doesn't already cover (union, not a sum). + .consumeWindowInsets(pad) .verticalScroll(rememberScrollState()) .imePadding(), verticalArrangement = Arrangement.spacedBy(20.dp), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupInfoScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupInfoScreen.kt index 65ccc7dcd9..0d8f4c3428 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupInfoScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupInfoScreen.kt @@ -32,6 +32,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding @@ -171,6 +172,9 @@ fun MarmotGroupInfoScreen( Modifier .fillMaxSize() .padding(padding) + // consume the scaffold insets so imePadding() unions with the nav-bar inset in + // `padding` instead of stacking on top of it while the keyboard is up. + .consumeWindowInsets(padding) .imePadding(), ) { // Group header section (fixed at top)