diff --git a/app/src/main/kotlin/io/flamenet/messenger/app/ChatScreens.kt b/app/src/main/kotlin/io/flamenet/messenger/app/ChatScreens.kt index d19fe95..220f4a3 100644 --- a/app/src/main/kotlin/io/flamenet/messenger/app/ChatScreens.kt +++ b/app/src/main/kotlin/io/flamenet/messenger/app/ChatScreens.kt @@ -1715,8 +1715,17 @@ fun ColumnScope.ChatScreen( val listState = rememberLazyListState() // Open AT the newest message, a jump, not a tour of the history. + // + // `requestScrollToItem`, not `scrollToItem`, here and for the keyboard + // below. `scrollToItem` forces a layout on the spot, and this screen is + // composed inside the Scaffold's MEASURE pass, so an effect that runs + // straight away asks for a layout in the middle of one: + // "performMeasureAndLayout called during measure layout", thrown from the + // keyboard effect in the on-device suite (ChatFlowTest, 2026-09-18 to + // 09-28). The request is applied by the next layout instead, which comes + // at once because the keyboard is what is changing the layout. LaunchedEffect(Unit) { - if (chat.lines.isNotEmpty()) listState.scrollToItem(chat.lines.lastIndex) + if (chat.lines.isNotEmpty()) listState.requestScrollToItem(chat.lines.lastIndex) } // Follow the conversation as it grows. LaunchedEffect(chat.lines.size) { @@ -1737,7 +1746,7 @@ fun ColumnScope.ChatScreen( // of that lands short by however much the viewport shrank afterwards. val imeBottom = WindowInsets.ime.getBottom(LocalDensity.current) LaunchedEffect(imeBottom) { - if (chat.lines.isNotEmpty()) listState.scrollToItem(chat.lines.lastIndex) + if (chat.lines.isNotEmpty()) listState.requestScrollToItem(chat.lines.lastIndex) } // One bar, not three stacked rows, and it wears the channel's own colour. diff --git a/app/src/test/kotlin/io/flamenet/messenger/app/TheKeyboardDoesNotHideTheMessagesTest.kt b/app/src/test/kotlin/io/flamenet/messenger/app/TheKeyboardDoesNotHideTheMessagesTest.kt index 0034f7d..54ac025 100644 --- a/app/src/test/kotlin/io/flamenet/messenger/app/TheKeyboardDoesNotHideTheMessagesTest.kt +++ b/app/src/test/kotlin/io/flamenet/messenger/app/TheKeyboardDoesNotHideTheMessagesTest.kt @@ -58,10 +58,19 @@ class TheKeyboardDoesNotHideTheMessagesTest { // The inset arrives as a series of values while the keyboard slides. // One animated jump fired at the start of that lands short by however // much the viewport shrank afterwards. + // + // Instant means `requestScrollToItem`, applied by the next layout, and + // NOT `scrollToItem`, which forces a layout on the spot: this screen is + // composed inside the Scaffold's measure pass, and on-device that threw + // "performMeasureAndLayout called during measure layout". val block = screens.substringAfter("LaunchedEffect(imeBottom) {").substringBefore("}") assertTrue( - block.contains("listState.scrollToItem("), - "the keyboard follow should scroll instantly: $block", + block.contains("listState.requestScrollToItem("), + "the keyboard follow should scroll instantly, by request: $block", + ) + assertTrue( + !block.contains("listState.scrollToItem("), + "scrollToItem forces a layout and throws when it runs inside one: $block", ) assertTrue( !block.contains("animateScrollToItem"),