From fb593de335596f85318aa9eb57838afdcccaca86 Mon Sep 17 00:00:00 2001 From: Avently <7953703+avently@users.noreply.github.com> Date: Fri, 18 Oct 2024 23:34:12 +0700 Subject: [PATCH] search fields, devtools, chatlist, newchatsheet, onehand on desktop, scrollbars --- .../platform/ScrollableColumn.android.kt | 37 +- .../common/platform/ScrollableColumn.kt | 2 + .../chat/simplex/common/views/TerminalView.kt | 33 +- .../common/views/chatlist/ChatListView.kt | 31 +- .../common/views/helpers/CloseSheetBar.kt | 5 +- .../common/views/newchat/NewChatSheet.kt | 374 ++++++++++-------- .../kotlin/chat/simplex/common/DesktopApp.kt | 10 +- .../platform/ScrollableColumn.desktop.kt | 64 ++- .../views/chatlist/UserPicker.desktop.kt | 4 +- .../views/usersettings/Appearance.desktop.kt | 1 + 10 files changed, 336 insertions(+), 225 deletions(-) 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 5f013095b6..eb33457eaf 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 @@ -8,6 +8,7 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.unit.Dp import chat.simplex.common.views.chatlist.NavigationBarBackground import chat.simplex.common.views.helpers.* import kotlinx.coroutines.flow.filter @@ -23,23 +24,37 @@ actual fun LazyColumnWithScrollBar( horizontalAlignment: Alignment.Horizontal, flingBehavior: FlingBehavior, userScrollEnabled: Boolean, + additionalBarHeight: State?, content: LazyListScope.() -> Unit ) { val state = state ?: LocalAppBarHandler.current?.listState ?: rememberLazyListState() val connection = LocalAppBarHandler.current?.connection LaunchedEffect(Unit) { - snapshotFlow { state.firstVisibleItemScrollOffset } - .filter { state.firstVisibleItemIndex == 0 } - .collect { scrollPosition -> - val offset = connection?.appBarOffset - if (reverseLayout) { - // always show app bar in reverse layout - connection?.appBarOffset = -1000f - } else if (offset != null && (offset + scrollPosition).absoluteValue > 1) { - connection.appBarOffset = -scrollPosition.toFloat() -// Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}") + if (reverseLayout) { + snapshotFlow { state.layoutInfo.visibleItemsInfo.lastOrNull()?.offset ?: 0 } + .collect { scrollPosition -> + val offset = connection?.appBarOffset + if (offset != null) { + connection.appBarOffset = if (state.layoutInfo.visibleItemsInfo.lastOrNull()?.index == state.layoutInfo.totalItemsCount - 1) { + state.layoutInfo.viewportEndOffset - scrollPosition.toFloat() - state.layoutInfo.afterContentPadding + } else { + // show always when last item is not visible + -1000f + } + //Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}") + } } - } + } else { + snapshotFlow { state.firstVisibleItemScrollOffset } + .filter { state.firstVisibleItemIndex == 0 } + .collect { scrollPosition -> + val offset = connection?.appBarOffset + if (offset != null && (offset + scrollPosition + state.layoutInfo.afterContentPadding).absoluteValue > 1) { + connection.appBarOffset = -scrollPosition.toFloat() + //Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}") + } + } + } } if (connection != null) { LazyColumn(modifier.nestedScroll(connection), state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled) { 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 532bfddfcf..4fc5d66f45 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 @@ -8,6 +8,7 @@ import androidx.compose.foundation.lazy.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @Composable @@ -21,6 +22,7 @@ expect fun LazyColumnWithScrollBar( horizontalAlignment: Alignment.Horizontal = Alignment.Start, flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(), userScrollEnabled: Boolean = true, + additionalBarHeight: 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 bda3f8ddcc..e3d0d37e3c 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,12 +9,13 @@ import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp +import androidx.compose.ui.unit.* import chat.simplex.common.model.* import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.ui.theme.* @@ -23,7 +24,9 @@ import chat.simplex.common.views.helpers.* import chat.simplex.common.model.ChatModel import chat.simplex.common.platform.* 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 @@ -76,14 +79,17 @@ fun TerminalLayout( composeState.value = composeState.value.copy(message = s) } Box(Modifier.fillMaxSize()) { - TerminalLog(floating) + val composeViewHeight = remember { mutableStateOf(0.dp) } + TerminalLog(floating, composeViewHeight) NavigationBarBackground() + 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)) { @@ -116,17 +122,18 @@ fun TerminalLayout( } @Composable -fun TerminalLog(floating: Boolean) { +fun TerminalLog(floating: Boolean, composeViewHeight: State) { val reversedTerminalItems by remember { derivedStateOf { chatModel.terminalItems.value.asReversed() } } val clipboard = LocalClipboardManager.current val listState = LocalAppBarHandler.current?.listState ?: rememberLazyListState() LaunchedEffect(Unit) { - var autoScrollToBottom = listState.firstVisibleItemIndex == 0 + var autoScrollToBottom = listState.firstVisibleItemIndex <= 1 launch { - snapshotFlow { listState.layoutInfo.totalItemsCount } + snapshotFlow { listState.layoutInfo.totalItemsCount to composeViewHeight.value } .filter { autoScrollToBottom } + .onEach { delay(100) } .collect { try { listState.scrollToItem(0) @@ -137,19 +144,21 @@ fun TerminalLog(floating: Boolean) { } launch { snapshotFlow { listState.firstVisibleItemIndex } + .onEach { delay(100) } .collect { - autoScrollToBottom = listState.firstVisibleItemIndex == 0 + autoScrollToBottom = it == 0 } } } LazyColumnWithScrollBar ( reverseLayout = true, - contentPadding = PaddingValues( - top = topPaddingToContent(), - bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier - ), - state = listState + contentPadding = PaddingValues(top = topPaddingToContent()), + state = listState, + additionalBarHeight = composeViewHeight ) { + item { + Spacer(Modifier.imePadding().navigationBarsPadding().padding(bottom = composeViewHeight.value)) + } items(reversedTerminalItems, key = { item -> item.id to item.createdAtNanos }) { item -> val rhId = item.remoteHostId val rhIdStr = if (rhId == null) "" else "$rhId " diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt index 6f7dd6c45b..a6f97279dc 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt @@ -10,8 +10,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier +import androidx.compose.ui.* import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.* import androidx.compose.ui.graphics.* @@ -646,26 +645,24 @@ private fun BoxScope.ChatList(searchText: MutableState) { val searchChatFilteredBySimplexLink = remember { mutableStateOf(null) } val chats = filteredChats(showUnreadAndFavorites, searchShowingSimplexLink, searchChatFilteredBySimplexLink, searchText.value.text, allChats.value.toList()) val topPaddingToContent = topPaddingToContent() + val blankSpaceSize = if (oneHandUI.value) WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier else topPaddingToContent LazyColumnWithScrollBar( Modifier.fillMaxSize().then(if (!oneHandUI.value) Modifier.imePadding() else Modifier), listState, reverseLayout = oneHandUI.value ) { - item { - Spacer(Modifier.height(if (oneHandUI.value) WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier else topPaddingToContent)) - } + item { Spacer(Modifier.height(blankSpaceSize)) } stickyHeader { Column( Modifier + .zIndex(1f) .offset { - val y = if (searchText.value.text.isEmpty()) { - val offsetMultiplier = if (oneHandUI.value) 1 else -1 - if ( - (oneHandUI.value && scrollDirection == ScrollDirection.Up) || - (appPlatform.isAndroid && keyboardState == KeyboardState.Opened) - ) { - 0 - } else if (oneHandUI.value && listState.firstVisibleItemIndex == 0) { + val offsetMultiplier = if (oneHandUI.value) 1 else -1 + val y = if (searchText.value.text.isNotEmpty() || (appPlatform.isAndroid && keyboardState == KeyboardState.Opened) || scrollDirection == ScrollDirection.Up) { + if (listState.firstVisibleItemIndex == 0) -offsetMultiplier * listState.firstVisibleItemScrollOffset + else -offsetMultiplier * blankSpaceSize.roundToPx() + } else { + if (oneHandUI.value && listState.firstVisibleItemIndex == 0) { listState.firstVisibleItemScrollOffset } else if (!oneHandUI.value && listState.firstVisibleItemIndex == 0) { 0 @@ -674,13 +671,11 @@ private fun BoxScope.ChatList(searchText: MutableState) { } else { offsetMultiplier * 1000 } - } else { - 0 } - println("LALAL ${listState.firstVisibleItemIndex} ${listState.firstVisibleItemScrollOffset} ${listState.layoutInfo.beforeContentPadding}") + println("LALAL ${listState.firstVisibleItemIndex} ${listState.firstVisibleItemScrollOffset} ${listState.layoutInfo.beforeContentPadding} ${listState.layoutInfo.viewportStartOffset}") IntOffset(0, y) } - .background(MaterialTheme.colors.background), + .background(MaterialTheme.colors.background) ) { if (oneHandUI.value) { Divider() @@ -715,7 +710,7 @@ private fun BoxScope.ChatList(searchText: MutableState) { } } if (chats.isEmpty() && chatModel.chats.value.isNotEmpty()) { - Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Box(Modifier.fillMaxSize().imePadding(), contentAlignment = Alignment.Center) { Text(generalGetString(MR.strings.no_filtered_chats), color = MaterialTheme.colors.secondary) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CloseSheetBar.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CloseSheetBar.kt index 51ffc22690..4a56ac09a5 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CloseSheetBar.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CloseSheetBar.kt @@ -9,7 +9,6 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.desktop.ui.tooling.preview.Preview -import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.ui.draw.* @@ -17,9 +16,7 @@ import androidx.compose.ui.graphics.* import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.* import chat.simplex.common.model.ChatController.appPrefs -import chat.simplex.common.platform.appPlatform import chat.simplex.common.ui.theme.* -import chat.simplex.common.ui.theme.ThemeManager.toReadableHex import chat.simplex.common.views.chatlist.* import chat.simplex.res.MR import dev.icerock.moko.resources.compose.painterResource @@ -50,7 +47,7 @@ fun CloseSheetBar( .clickable(interactionSource = interactionSource, indication = null) { /* receive clicks to not allow to click through */ } .heightIn(min = AppBarHeight * fontSizeSqrtMultiplier) .drawWithCache { - val backgroundColor = if (connection != null) themeBackgroundMix.copy(alpha = topTitleAlpha(false, connection)) else Color.Transparent + val backgroundColor = if (arrangement == Arrangement.Bottom) themeBackgroundMix else if (connection != null) themeBackgroundMix.copy(alpha = topTitleAlpha(false, connection)) else Color.Transparent onDrawBehind { drawRect(backgroundColor) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt index 3e2fbab23e..9f2089b6e7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt @@ -12,8 +12,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier +import androidx.compose.ui.* import androidx.compose.ui.focus.* import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.painter.Painter @@ -176,76 +175,124 @@ private fun ModalData.NewChatSheetLayout( derivedStateOf { filterContactTypes(chatModel.chats.value, deletedContactTypes) } } - Box { - val topPaddingToContent = topPaddingToContent() - LazyColumnWithScrollBar( - Modifier.fillMaxSize().then(if (!oneHandUI.value) Modifier.imePadding() else Modifier), - listState, - contentPadding = PaddingValues( - top = if (!oneHandUI.value) topPaddingToContent else 0.dp, - bottom = if (oneHandUI.value) WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier else 0.dp + val actionButtonsOriginal = listOf( + Triple( + painterResource(MR.images.ic_add_link), + stringResource(MR.strings.add_contact_tab), + addContact, ), - reverseLayout = oneHandUI.value - ) { - if (!oneHandUI.value) { - item { - Box(contentAlignment = Alignment.Center) { - val bottomPadding = DEFAULT_PADDING - AppBarTitle( - stringResource(MR.strings.new_message), - hostDevice(rh?.remoteHostId), - bottomPadding = bottomPadding + Triple( + painterResource(MR.images.ic_qr_code), + if (appPlatform.isAndroid) stringResource(MR.strings.scan_paste_link) else stringResource(MR.strings.paste_link), + scanPaste, + ), + Triple( + painterResource(MR.images.ic_group), + stringResource(MR.strings.create_group_button), + createGroup, + ) + ) + + @Composable + fun DeletedChatsItem(actionButtons: List Unit>>) { + if (searchText.value.text.isEmpty()) { + Spacer(Modifier.padding(bottom = 27.dp)) + } + + if (searchText.value.text.isEmpty()) { + Row { + SectionView { + actionButtons.map { + NewChatButton( + icon = it.first, + text = it.second, + click = it.third, + ) + } + } + } + if (deletedChats.isNotEmpty()) { + SectionDividerSpaced(maxBottomPadding = false) + SectionView { + SectionItemView( + click = { + ModalManager.start.showCustomModal { closeDeletedChats -> + ModalView( + close = closeDeletedChats, + closeOnTop = !oneHandUI.value, + ) { + DeletedContactsView(rh = rh, closeDeletedChats = closeDeletedChats, close = { + ModalManager.start.closeModals() + }) + } + } + } + ) { + Icon( + painterResource(MR.images.ic_inventory_2), + contentDescription = stringResource(MR.strings.deleted_chats), + tint = MaterialTheme.colors.secondary, + ) + TextIconSpaced(false) + Text(text = stringResource(MR.strings.deleted_chats), color = MaterialTheme.colors.onBackground) + } + } + } + } + } + + @Composable + fun NoFilteredContactsItem() { + if (filteredContactChats.isEmpty() && allChats.isNotEmpty()) { + Column(sectionModifier.fillMaxSize().padding(DEFAULT_PADDING)) { + Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { + Text( + generalGetString(MR.strings.no_filtered_contacts), + color = MaterialTheme.colors.secondary ) } } } - stickyHeader { - val scrolledSomething by remember { derivedStateOf { listState.firstVisibleItemScrollOffset > 0 } } - Column( - Modifier - .offset { - val y = if (searchText.value.text.isEmpty()) { - val offsetMultiplier = if (oneHandUI.value) 1 else -1 + } - if ( - (oneHandUI.value && scrollDirection == ScrollDirection.Up) || - (appPlatform.isAndroid && keyboardState == KeyboardState.Opened) - ) { - 0 - } else if (oneHandUI.value && listState.firstVisibleItemIndex == 0) { - listState.firstVisibleItemScrollOffset - } else if (!oneHandUI.value && listState.firstVisibleItemIndex == 0) { - 0 - } else if (!oneHandUI.value && listState.firstVisibleItemIndex == 1) { - -listState.firstVisibleItemScrollOffset + @Composable + fun OneHandLazyColumn() { + val blankSpaceSize = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier + LazyColumnWithScrollBar( + Modifier.fillMaxSize(), + listState, + contentPadding = PaddingValues(bottom = blankSpaceSize), + reverseLayout = oneHandUI.value + ) { + stickyHeader { + val scrolledSomething by remember { derivedStateOf { listState.firstVisibleItemScrollOffset > 0 || listState.firstVisibleItemIndex > 0 } } + Column( + Modifier + .zIndex(1f) + .offset { + val y = if (searchText.value.text.isNotEmpty() || (appPlatform.isAndroid && keyboardState == KeyboardState.Opened)) { + if (listState.firstVisibleItemIndex == 0) -minOf(listState.firstVisibleItemScrollOffset, blankSpaceSize.roundToPx()) + else -blankSpaceSize.roundToPx() } else { - offsetMultiplier * 1000 + if (listState.firstVisibleItemIndex == 0) { + listState.firstVisibleItemScrollOffset + } else { + 1000 + } } - } else { - 0 + IntOffset(0, y) } - IntOffset(0, y) - } - // show background when something is scrolled because otherwise the bar is transparent. - // not using background always because of gradient in SimpleX theme - .background(if (scrolledSomething && keyboardState == KeyboardState.Opened) { - MaterialTheme.colors.background - } else { - Color.Unspecified - } - ) - ) { - Divider() - if (!oneHandUI.value) { - ContactsSearchBar( - listState = listState, - searchText = searchText, - searchShowingSimplexLink = searchShowingSimplexLink, - searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink, - close = close, - ) + // show background when something is scrolled because otherwise the bar is transparent. + // not using background always because of gradient in SimpleX theme + .background( + if (scrolledSomething && (keyboardState == KeyboardState.Opened || searchText.value.text.isNotEmpty())) { + MaterialTheme.colors.background + } else { + Color.Unspecified + } + ) + ) { Divider() - } else { Column(Modifier.consumeWindowInsets(WindowInsets.navigationBars).consumeWindowInsets(PaddingValues(bottom = AppBarHeight))) { ContactsSearchBar( listState = listState, @@ -258,120 +305,125 @@ private fun ModalData.NewChatSheetLayout( } } } - } - item { - if (searchText.value.text.isEmpty()) { - Spacer(Modifier.padding(bottom = 27.dp)) + item { + DeletedChatsItem(actionButtonsOriginal.asReversed()) } - - val actionButtonsOriginal = listOf( - Triple( - painterResource(MR.images.ic_add_link), - stringResource(MR.strings.add_contact_tab), - addContact, - ), - Triple( - painterResource(MR.images.ic_qr_code), - if (appPlatform.isAndroid) stringResource(MR.strings.scan_paste_link) else stringResource(MR.strings.paste_link), - scanPaste, - ), - Triple( - painterResource(MR.images.ic_group), - stringResource(MR.strings.create_group_button), - createGroup, - ) - ) - - val actionButtons by remember(oneHandUI.value) { - derivedStateOf { - if (oneHandUI.value) actionButtonsOriginal.asReversed() else actionButtonsOriginal - } - } - - if (searchText.value.text.isEmpty()) { - Row { - SectionView { - actionButtons.map { - NewChatButton( - icon = it.first, - text = it.second, - click = it.third, - ) - } - } - } - if (deletedChats.isNotEmpty()) { - SectionDividerSpaced(maxBottomPadding = false) - SectionView { - SectionItemView( - click = { - ModalManager.start.showCustomModal { closeDeletedChats -> - ModalView( - close = closeDeletedChats, - closeOnTop = !oneHandUI.value, - ) { - DeletedContactsView(rh = rh, closeDeletedChats = closeDeletedChats, close = { - ModalManager.start.closeModals() - }) - } - } - } - ) { - Icon( - painterResource(MR.images.ic_inventory_2), - contentDescription = stringResource(MR.strings.deleted_chats), - tint = MaterialTheme.colors.secondary, - ) - TextIconSpaced(false) - Text(text = stringResource(MR.strings.deleted_chats), color = MaterialTheme.colors.onBackground) - } - } - } - } - } - - item { - if (filteredContactChats.isNotEmpty() && searchText.value.text.isEmpty()) { - if (!oneHandUI.value) { - SectionDividerSpaced() - SectionView(stringResource(MR.strings.contact_list_header_title).uppercase(), headerBottomPadding = DEFAULT_PADDING_HALF) {} - } else { + item { + if (filteredContactChats.isNotEmpty() && searchText.value.text.isEmpty()) { SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = false) SectionView(stringResource(MR.strings.contact_list_header_title).uppercase(), headerBottomPadding = DEFAULT_PADDING_HALF) {} Spacer(Modifier.height(DEFAULT_PADDING_HALF)) } } - } - - item { - if (filteredContactChats.isEmpty() && allChats.isNotEmpty()) { - Column(sectionModifier.fillMaxSize().padding(DEFAULT_PADDING)) { - Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { - Text( - generalGetString(MR.strings.no_filtered_contacts), - color = MaterialTheme.colors.secondary - ) + item { + NoFilteredContactsItem() + } + itemsIndexed(filteredContactChats) { index, chat -> + val nextChatSelected = remember(chat.id, filteredContactChats) { + derivedStateOf { + chatModel.chatId.value != null && filteredContactChats.getOrNull(index + 1)?.id == chatModel.chatId.value } } + ContactListNavLinkView(chat, nextChatSelected, showDeletedChatIcon = true) } - } - - itemsIndexed(filteredContactChats) { index, chat -> - val nextChatSelected = remember(chat.id, filteredContactChats) { - derivedStateOf { - chatModel.chatId.value != null && filteredContactChats.getOrNull(index + 1)?.id == chatModel.chatId.value + if (appPlatform.isAndroid) { + item { + Spacer(Modifier.windowInsetsTopHeight(WindowInsets.statusBars)) } } - ContactListNavLinkView(chat, nextChatSelected, showDeletedChatIcon = true) - } - if (appPlatform.isAndroid) { - item { - Spacer(if (oneHandUI.value) Modifier.windowInsetsTopHeight(WindowInsets.statusBars) else Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars)) - } } } + + @Composable + fun NonOneHandLazyColumn() { + val blankSpaceSize = topPaddingToContent() + LazyColumnWithScrollBar( + Modifier.fillMaxSize().imePadding(), + listState, + reverseLayout = false + ) { + item { + Box(Modifier.padding(top = blankSpaceSize)) { + AppBarTitle( + stringResource(MR.strings.new_message), + hostDevice(rh?.remoteHostId), + bottomPadding = DEFAULT_PADDING + ) + } + } + stickyHeader { + val scrolledSomething by remember { derivedStateOf { listState.firstVisibleItemScrollOffset > 0 || listState.firstVisibleItemIndex > 0 } } + Column( + Modifier + .zIndex(1f) + .offset { + val y = if (searchText.value.text.isNotEmpty() || (appPlatform.isAndroid && keyboardState == KeyboardState.Opened)) { + if (listState.firstVisibleItemIndex == 0) (listState.firstVisibleItemScrollOffset - (listState.layoutInfo.visibleItemsInfo[0].size - blankSpaceSize.roundToPx())).coerceAtLeast(0) + else blankSpaceSize.roundToPx() + } else { + when (listState.firstVisibleItemIndex) { + 0 -> 0 + 1 -> -listState.firstVisibleItemScrollOffset + else -> -1000 + } + } + IntOffset(0, y) + } + // show background when something is scrolled because otherwise the bar is transparent. + // not using background always because of gradient in SimpleX theme + .background( + if (scrolledSomething && (keyboardState == KeyboardState.Opened || searchText.value.text.isNotEmpty())) { + MaterialTheme.colors.background + } else { + Color.Unspecified + } + ) + ) { + Divider() + ContactsSearchBar( + listState = listState, + searchText = searchText, + searchShowingSimplexLink = searchShowingSimplexLink, + searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink, + close = close, + ) + Divider() + } + } + item { + DeletedChatsItem(actionButtonsOriginal) + } + item { + if (filteredContactChats.isNotEmpty() && searchText.value.text.isEmpty()) { + SectionDividerSpaced() + SectionView(stringResource(MR.strings.contact_list_header_title).uppercase(), headerBottomPadding = DEFAULT_PADDING_HALF) {} + } + } + item { + NoFilteredContactsItem() + } + itemsIndexed(filteredContactChats) { index, chat -> + val nextChatSelected = remember(chat.id, filteredContactChats) { + derivedStateOf { + chatModel.chatId.value != null && filteredContactChats.getOrNull(index + 1)?.id == chatModel.chatId.value + } + } + ContactListNavLinkView(chat, nextChatSelected, showDeletedChatIcon = true) + } + if (appPlatform.isAndroid) { + item { + Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars)) + } + } + } + } + + Box { if (oneHandUI.value) { + OneHandLazyColumn() StatusBarBackground() + } else { + NonOneHandLazyColumn() } NavigationBarBackground() } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt index 91bc82c492..2f5e20e445 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt @@ -198,10 +198,14 @@ private fun ApplicationScope.AppWindow(closedByError: MutableState) { val cWindowState = rememberWindowState(placement = WindowPlacement.Floating, width = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier, height = 768.dp) Window(state = cWindowState, onCloseRequest = { hiddenUntilRestart = true }, title = stringResource(MR.strings.chat_console)) { + val data = remember { ModalData() } SimpleXTheme { - Column { - CloseSheetBar(close = { hiddenUntilRestart = true }) - TerminalView(true) + CompositionLocalProvider( + LocalAppBarHandler provides data.appBarHandler + ) { + ModalView({ hiddenUntilRestart = true }) { + TerminalView(true) + } ModalManager.floatingTerminal.showInView() DisposableEffect(Unit) { onDispose { 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 72bc53ee58..296bd28355 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 @@ -15,6 +15,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.pointer.* +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import chat.simplex.common.views.helpers.* import kotlinx.coroutines.* @@ -31,6 +32,7 @@ actual fun LazyColumnWithScrollBar( horizontalAlignment: Alignment.Horizontal, flingBehavior: FlingBehavior, userScrollEnabled: Boolean, + additionalBarHeight: State?, content: LazyListScope.() -> Unit ) { val scope = rememberCoroutineScope() @@ -57,24 +59,56 @@ actual fun LazyColumnWithScrollBar( // (only first visible row is useful because LazyColumn doesn't have absolute scroll position, only relative to row) val scrollBarDraggingState = remember { mutableStateOf(false) } LaunchedEffect(Unit) { - snapshotFlow { state.firstVisibleItemScrollOffset } - .filter { state.firstVisibleItemIndex == 0 } - .collect { scrollPosition -> - val offset = connection?.appBarOffset - if (reverseLayout) { - // always show app bar in reverse layout - connection?.appBarOffset = -1000f - } else if (offset != null && ((offset + scrollPosition).absoluteValue > 1 || scrollBarDraggingState.value)) { - connection.appBarOffset = -scrollPosition.toFloat() -// Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}") + if (reverseLayout) { + snapshotFlow { state.layoutInfo.visibleItemsInfo.lastOrNull()?.offset ?: 0 } + .collect { scrollPosition -> + val offset = connection?.appBarOffset + if (offset != null) { + connection.appBarOffset = if (state.layoutInfo.visibleItemsInfo.lastOrNull()?.index == state.layoutInfo.totalItemsCount - 1) { + state.layoutInfo.viewportEndOffset - scrollPosition.toFloat() - state.layoutInfo.afterContentPadding + } else { + // show always when last item is not visible + -1000f + } + //Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}") + } } - } + } else { + snapshotFlow { state.firstVisibleItemScrollOffset } + .filter { state.firstVisibleItemIndex == 0 } + .collect { scrollPosition -> + val offset = connection?.appBarOffset + if (offset != null && ((offset + scrollPosition + state.layoutInfo.afterContentPadding).absoluteValue > 1 || scrollBarDraggingState.value)) { + connection.appBarOffset = -scrollPosition.toFloat() + //Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}") + } + } + } } Box(if (connection != null) Modifier.nestedScroll(connection) else Modifier) { LazyColumn(modifier.then(scrollModifier), state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content) - Box(Modifier.fillMaxSize(), contentAlignment = Alignment.CenterEnd) { - DesktopScrollBar(rememberScrollbarAdapter(state), Modifier.fillMaxHeight(), scrollBarAlpha, scrollJob, reverseLayout, scrollBarDraggingState) - } + ScrollBar(reverseLayout, state, scrollBarAlpha, scrollJob, scrollBarDraggingState, additionalBarHeight) + } +} + +@Composable +private fun ScrollBar( + reverseLayout: Boolean, + state: LazyListState, + scrollBarAlpha: Animatable, + scrollJob: MutableState, + scrollBarDraggingState: MutableState, + additionalBarHeight: State? +) { + val padding = if (additionalBarHeight != null) { + PaddingValues(top = AppBarHeight * fontSizeSqrtMultiplier, bottom = additionalBarHeight.value) + } else if (reverseLayout) { + PaddingValues(bottom = AppBarHeight * fontSizeSqrtMultiplier) + } else { + PaddingValues(top = AppBarHeight * fontSizeSqrtMultiplier) + } + Box(Modifier.fillMaxSize().padding(padding), contentAlignment = Alignment.CenterEnd) { + DesktopScrollBar(rememberScrollbarAdapter(state), Modifier.fillMaxHeight(), scrollBarAlpha, scrollJob, reverseLayout, scrollBarDraggingState) } } @@ -133,7 +167,7 @@ actual fun ColumnWithScrollBar( } content() } - Box(Modifier.fillMaxSize(), contentAlignment = Alignment.CenterEnd) { + Box(Modifier.fillMaxSize().padding(top = AppBarHeight * fontSizeSqrtMultiplier), contentAlignment = Alignment.CenterEnd) { DesktopScrollBar(rememberScrollbarAdapter(state), Modifier.fillMaxHeight(), scrollBarAlpha, scrollJob, false, scrollBarDraggingState) } } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.desktop.kt index d9b53b9485..29f38d1b03 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.desktop.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.User import chat.simplex.common.model.UserInfo import chat.simplex.common.platform.* @@ -95,7 +96,8 @@ actual fun PlatformUserPicker(modifier: Modifier, pickerState: MutableStateFlow< .clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = { pickerState.value = AnimatedViewState.HIDING }), contentAlignment = Alignment.TopStart ) { - ColumnWithScrollBar(modifier) { + val oneHandUI = remember { appPrefs.oneHandUI.state } + ColumnWithScrollBar(modifier.align(if (oneHandUI.value) Alignment.BottomCenter else Alignment.TopCenter)) { content() } } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/Appearance.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/Appearance.desktop.kt index 82a6861d76..2d5de02827 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/Appearance.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/Appearance.desktop.kt @@ -59,6 +59,7 @@ fun AppearanceScope.AppearanceLayout( } } } + SettingsPreferenceItem(icon = null, stringResource(MR.strings.one_hand_ui), ChatModel.controller.appPrefs.oneHandUI) } SectionDividerSpaced() ThemesSection(systemDarkTheme)