diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/ScrollableColumn.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/ScrollableColumn.android.kt index eb33457eaf..2308bae119 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/ScrollableColumn.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/ScrollableColumn.android.kt @@ -24,7 +24,7 @@ actual fun LazyColumnWithScrollBar( horizontalAlignment: Alignment.Horizontal, flingBehavior: FlingBehavior, userScrollEnabled: Boolean, - additionalBarHeight: State?, + additionalBarOffset: State?, content: LazyListScope.() -> Unit ) { val state = state ?: LocalAppBarHandler.current?.listState ?: rememberLazyListState() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/ScrollableColumn.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/ScrollableColumn.kt index 4fc5d66f45..7bf79e8431 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/ScrollableColumn.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/ScrollableColumn.kt @@ -22,7 +22,7 @@ expect fun LazyColumnWithScrollBar( horizontalAlignment: Alignment.Horizontal = Alignment.Start, flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(), userScrollEnabled: Boolean = true, - additionalBarHeight: State? = null, + additionalBarOffset: State? = null, content: LazyListScope.() -> Unit ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt index d83ec47068..c2876e555d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt @@ -9,6 +9,7 @@ import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.layoutId import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalDensity @@ -23,10 +24,10 @@ import chat.simplex.common.views.chat.* import chat.simplex.common.views.helpers.* import chat.simplex.common.model.ChatModel import chat.simplex.common.platform.* +import chat.simplex.common.views.chat.item.CONSOLE_COMPOSE_LAYOUT_ID +import chat.simplex.common.views.chat.item.AdaptingBottomPaddingLayout import chat.simplex.common.views.chatlist.NavigationBarBackground -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @Composable @@ -79,46 +80,46 @@ fun TerminalLayout( composeState.value = composeState.value.copy(message = s) } Box(Modifier.fillMaxSize()) { - val fontSizeSqrtMultiplier = fontSizeSqrtMultiplier - val composeViewHeight = remember { mutableStateOf(AppBarHeight * fontSizeSqrtMultiplier) } - TerminalLog(floating, composeViewHeight) - NavigationBarBackground(true) - val density = LocalDensity.current - Column( - Modifier - .align(Alignment.BottomCenter) - .navigationBarsPadding() - .imePadding() - .background(MaterialTheme.colors.background.copy(remember { appPrefs.barsAlpha.state }.value)) - .onSizeChanged { composeViewHeight.value = with(density) { it.height.toDp() } } - ) { - Divider() - Box(Modifier.padding(horizontal = 8.dp)) { - SendMsgView( - composeState = composeState, - showVoiceRecordIcon = false, - recState = remember { mutableStateOf(RecordingState.NotStarted) }, - isDirectChat = false, - liveMessageAlertShown = SharedPreference(get = { false }, set = {}), - sendMsgEnabled = true, - sendButtonEnabled = true, - nextSendGrpInv = false, - needToAllowVoiceToContact = false, - allowedVoiceByPrefs = false, - userIsObserver = false, - userCanSend = true, - allowVoiceToContact = {}, - placeholder = "", - sendMessage = { sendCommand() }, - sendLiveMessage = null, - updateLiveMessage = null, - editPrevMessage = {}, - onMessageChange = ::onMessageChange, - onFilesPasted = {}, - textStyle = textStyle - ) + val composeViewHeight = remember { mutableStateOf(0.dp) } + AdaptingBottomPaddingLayout(Modifier, CONSOLE_COMPOSE_LAYOUT_ID, composeViewHeight) { + TerminalLog(floating, composeViewHeight) + Column( + Modifier + .layoutId(CONSOLE_COMPOSE_LAYOUT_ID) + .align(Alignment.BottomCenter) + .navigationBarsPadding() + .imePadding() + .background(MaterialTheme.colors.background.copy(remember { appPrefs.barsAlpha.state }.value)) + ) { + Divider() + Box(Modifier.padding(horizontal = 8.dp)) { + SendMsgView( + composeState = composeState, + showVoiceRecordIcon = false, + recState = remember { mutableStateOf(RecordingState.NotStarted) }, + isDirectChat = false, + liveMessageAlertShown = SharedPreference(get = { false }, set = {}), + sendMsgEnabled = true, + sendButtonEnabled = true, + nextSendGrpInv = false, + needToAllowVoiceToContact = false, + allowedVoiceByPrefs = false, + userIsObserver = false, + userCanSend = true, + allowVoiceToContact = {}, + placeholder = "", + sendMessage = { sendCommand() }, + sendLiveMessage = null, + updateLiveMessage = null, + editPrevMessage = {}, + onMessageChange = ::onMessageChange, + onFilesPasted = {}, + textStyle = textStyle + ) + } } } + NavigationBarBackground(true) } } @@ -133,7 +134,6 @@ fun TerminalLog(floating: Boolean, composeViewHeight: State) { launch { snapshotFlow { listState.layoutInfo.totalItemsCount } .filter { autoScrollToBottom } - .onEach { delay(100) } .collect { try { listState.scrollToItem(0) @@ -144,21 +144,19 @@ fun TerminalLog(floating: Boolean, composeViewHeight: State) { } launch { snapshotFlow { listState.firstVisibleItemIndex } - .onEach { delay(100) } .collect { autoScrollToBottom = it == 0 } } } LazyColumnWithScrollBar ( - modifier = Modifier.consumeWindowInsets(WindowInsets.navigationBars.only(WindowInsetsSides.Vertical)).imePadding(), reverseLayout = true, contentPadding = PaddingValues( top = topPaddingToContent(), - bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + composeViewHeight.value + bottom = composeViewHeight.value ), state = listState, - additionalBarHeight = composeViewHeight + additionalBarOffset = composeViewHeight ) { items(reversedTerminalItems, key = { item -> item.id to item.createdAtNanos }) { item -> val clipboard = LocalClipboardManager.current diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index dfede187e3..375989c9ad 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -654,7 +654,6 @@ fun ChatLayout( val backgroundColor = MaterialTheme.wallpaper.background ?: wallpaperType.defaultBackgroundColor(CurrentColors.value.base, MaterialTheme.colors.background) val tintColor = MaterialTheme.wallpaper.tint ?: wallpaperType.defaultTintColor(CurrentColors.value.base) val composeViewHeight = remember { mutableStateOf(0.dp) } - val composeHeight = composeViewHeight.value Box( Modifier .fillMaxSize() @@ -663,24 +662,29 @@ fun ChatLayout( Modifier.drawWithCache { chatViewBackground(wallpaperImage, wallpaperType, backgroundColor, tintColor) } else Modifier) - // don't show initial frame of chat if height of compose area is not known yet. Otherwise, the view jumps - .graphicsLayer { alpha = if (composeHeight > 0.dp) 1f else 0f }, ) { val remoteHostId = remember { remoteHostId }.value val chatInfo = remember { chatInfo }.value - if (chatInfo != null) { - ChatItemsList( - remoteHostId, chatInfo, unreadCount, composeState, composeViewHeight, searchValue, - useLinkPreviews, linkMode, selectedChatItems, showMemberInfo, loadPrevMessages, deleteMessage, deleteMessages, - receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem, - updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember, - setReaction, showItemDetails, markRead, remember { {onComposed(it)} }, developerTools, showViaProxy, - ) - } - val density = LocalDensity.current - println("LALAL height ${composeViewHeight.value} ${AppBarHeight * fontSizeSqrtMultiplier}") - Box(Modifier.align(Alignment.BottomCenter).imePadding().navigationBarsPadding().onSizeChanged { composeViewHeight.value = with(density) { it.height.toDp() } }) { - composeView() + AdaptingBottomPaddingLayout(Modifier, CHAT_COMPOSE_LAYOUT_ID, composeViewHeight) { + if (chatInfo != null) { + Box(Modifier.fillMaxSize()) { + ChatItemsList( + remoteHostId, chatInfo, unreadCount, composeState, composeViewHeight, searchValue, + useLinkPreviews, linkMode, selectedChatItems, showMemberInfo, loadPrevMessages, deleteMessage, deleteMessages, + receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem, + updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember, + setReaction, showItemDetails, markRead, remember { { onComposed(it) } }, developerTools, showViaProxy, + ) + } + } + Box(Modifier + .layoutId(CHAT_COMPOSE_LAYOUT_ID) + .align(Alignment.BottomCenter) + .imePadding() + .navigationBarsPadding() + ) { + composeView() + } } NavigationBarBackground(true) Column { @@ -993,14 +997,14 @@ fun BoxScope.ChatItemsList( } ) LazyColumnWithScrollBar( - Modifier.consumeWindowInsets(WindowInsets.navigationBars.only(WindowInsetsSides.Vertical)).imePadding(), + Modifier.align(Alignment.BottomCenter), state = listState, reverseLayout = true, contentPadding = PaddingValues( top = topPaddingToContent(), - bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + composeViewHeight.value + bottom = composeViewHeight.value ), - additionalBarHeight = composeViewHeight + additionalBarOffset = composeViewHeight ) { itemsIndexed(reversedChatItems.value, key = { _, item -> item.id to item.meta.createdAt.toEpochMilliseconds() }) { i, cItem -> println("LALAL RELOAD ITEM $i") @@ -1678,7 +1682,7 @@ private fun BoxScope.BottomEndFloatingButton( FloatingActionButton( onClick = onClickCounter, elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp), - modifier = Modifier.padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING + composeViewHeight.value).align(Alignment.BottomEnd).imePadding().navigationBarsPadding().size(48.dp), + modifier = Modifier.padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING + composeViewHeight.value).align(Alignment.BottomEnd).size(48.dp), backgroundColor = MaterialTheme.colors.secondaryVariant, ) { Text( @@ -1692,7 +1696,7 @@ private fun BoxScope.BottomEndFloatingButton( FloatingActionButton( onClick = onClickArrowDown, elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp), - modifier = Modifier.padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING + composeViewHeight.value).align(Alignment.BottomEnd).imePadding().navigationBarsPadding().size(48.dp), + modifier = Modifier.padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING + composeViewHeight.value).align(Alignment.BottomEnd).size(48.dp), backgroundColor = MaterialTheme.colors.secondaryVariant, ) { Icon( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt index f346402957..77bcf70349 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.layout.* +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.UriHandler import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource @@ -320,6 +321,8 @@ fun CIMarkdownText( const val CHAT_IMAGE_LAYOUT_ID = "chatImage" const val CHAT_BUBBLE_LAYOUT_ID = "chatBubble" +const val CHAT_COMPOSE_LAYOUT_ID = "chatCompose" +const val CONSOLE_COMPOSE_LAYOUT_ID = "consoleCompose" /** * Equal to [androidx.compose.ui.unit.Constraints.MaxFocusMask], which is 0x3FFFF - 1 * Other values make a crash `java.lang.IllegalArgumentException: Can't represent a width of 123456 and height of 9909 in Constraints` @@ -398,6 +401,47 @@ fun DependentLayout( } } } + +// The purpose of this layout is to make measuring of bottom compose view and adapt top lazy column to its size in the same frame (not on the next frame as you would expect). +// So, steps are: +// - measuring the layout: measured height of compose view before this step is 0, it's added to content padding of lazy column (so it's == 0) +// - measured the layout: measured height of compose view now is correct, but it's not yet applied to lazy column content padding (so it's == 0) and lazy column is placed higher than compose view in view with respect to compose view's height +// - on next frame measured height is correct and content padding is the same, lazy column placed to occupy all parent view's size +// - every added/removed line in compose view goes through the same process. +@Composable +fun AdaptingBottomPaddingLayout( + modifier: Modifier = Modifier, + mainLayoutId: String, + expectedHeight: MutableState, + content: @Composable () -> Unit +) { + val expected = with(LocalDensity.current) { expectedHeight.value.roundToPx() } + Layout( + content = content, + modifier = modifier + ) { measureable, constraints -> + require(measureable.size <= 2) { "Should be exactly one or two elements in this layout, you have ${measureable.size}" } + val mainPlaceable = measureable.firstOrNull { it.layoutId == mainLayoutId }!!.measure(constraints) + val placeables: List = measureable.map { + if (it.layoutId == mainLayoutId) + mainPlaceable + else + it.measure(constraints.copy(maxHeight = if (expected != mainPlaceable.measuredHeight) constraints.maxHeight - mainPlaceable.measuredHeight + expected else constraints.maxHeight)) } + expectedHeight.value = mainPlaceable.measuredHeight.toDp() + layout(constraints.maxWidth, constraints.maxHeight) { + var y = 0 + placeables.forEach { + if (it !== mainPlaceable) { + it.place(0, y) + y += it.measuredHeight + } else { + it.place(0, constraints.maxHeight - mainPlaceable.measuredHeight) + y += it.measuredHeight + } + } + } + } +} /* class EditedProvider: PreviewParameterProvider { diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/ScrollableColumn.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/ScrollableColumn.desktop.kt index 296bd28355..3a565fd47d 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/ScrollableColumn.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/ScrollableColumn.desktop.kt @@ -32,7 +32,7 @@ actual fun LazyColumnWithScrollBar( horizontalAlignment: Alignment.Horizontal, flingBehavior: FlingBehavior, userScrollEnabled: Boolean, - additionalBarHeight: State?, + additionalBarOffset: State?, content: LazyListScope.() -> Unit ) { val scope = rememberCoroutineScope() @@ -87,7 +87,7 @@ actual fun LazyColumnWithScrollBar( } Box(if (connection != null) Modifier.nestedScroll(connection) else Modifier) { LazyColumn(modifier.then(scrollModifier), state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content) - ScrollBar(reverseLayout, state, scrollBarAlpha, scrollJob, scrollBarDraggingState, additionalBarHeight) + ScrollBar(reverseLayout, state, scrollBarAlpha, scrollJob, scrollBarDraggingState, additionalBarOffset) } }