From 387faa0c2779db39ea67bb1a87bd5c35f3356891 Mon Sep 17 00:00:00 2001 From: Diogo Cunha Date: Fri, 12 Jul 2024 23:02:14 +0100 Subject: [PATCH] android: implemented one hand ui for chat list screen --- .../chatlist/ChatListNavLinkView.android.kt | 7 ++ .../views/chatlist/ChatListNavLinkView.kt | 20 ++++- .../common/views/chatlist/ChatListView.kt | 76 ++++++++++++++++--- .../views/usersettings/DeveloperView.kt | 4 +- .../chatlist/ChatListNavLinkView.desktop.kt | 1 + 5 files changed, 93 insertions(+), 15 deletions(-) diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.android.kt index f0f733111a..01614e09ea 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.android.kt @@ -6,6 +6,7 @@ import androidx.compose.material.Divider import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale import androidx.compose.ui.unit.dp import chat.simplex.common.platform.onRightClick import chat.simplex.common.views.helpers.* @@ -19,8 +20,14 @@ actual fun ChatListNavLinkLayout( disabled: Boolean, selectedChat: State, nextChatSelected: State, + oneHandUI: State ) { var modifier = Modifier.fillMaxWidth() + + if (oneHandUI.value) { + modifier = modifier.scale(scaleX = 1f, scaleY = -1f) + } + if (!disabled) modifier = modifier .combinedClickable(onClick = click, onLongClick = { showMenu.value = true }) .onRightClick { showMenu.value = true } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt index f3d25c896b..f7e9855b86 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt @@ -1,6 +1,7 @@ package chat.simplex.common.views.chatlist import SectionItemView +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.* @@ -32,7 +33,7 @@ import kotlinx.coroutines.delay import kotlinx.datetime.Clock @Composable -fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { +fun ChatListNavLinkView(chat: Chat, nextChatSelected: State, oneHandUI: State) { val showMenu = remember { mutableStateOf(false) } val showMarkRead = remember(chat.chatStats.unreadCount, chat.chatStats.unreadChat) { chat.chatStats.unreadCount > 0 || chat.chatStats.unreadChat @@ -47,6 +48,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { val showChatPreviews = chatModel.showChatPreviews.value val inProgress = remember { mutableStateOf(false) } var progressByTimeout by rememberSaveable { mutableStateOf(false) } + LaunchedEffect(inProgress.value) { progressByTimeout = if (inProgress.value) { delay(1000) @@ -75,6 +77,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { disabled, selectedChat, nextChatSelected, + oneHandUI ) } is ChatInfo.Group -> @@ -94,6 +97,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { disabled, selectedChat, nextChatSelected, + oneHandUI ) is ChatInfo.Local -> { ChatListNavLinkLayout( @@ -112,6 +116,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { disabled, selectedChat, nextChatSelected, + oneHandUI ) } is ChatInfo.ContactRequest -> @@ -131,6 +136,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { disabled, selectedChat, nextChatSelected, + oneHandUI ) is ChatInfo.ContactConnection -> ChatListNavLinkLayout( @@ -151,6 +157,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { disabled, selectedChat, nextChatSelected, + oneHandUI ) is ChatInfo.InvalidJSON -> ChatListNavLinkLayout( @@ -167,6 +174,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { disabled, selectedChat, nextChatSelected, + oneHandUI ) } } @@ -903,6 +911,7 @@ expect fun ChatListNavLinkLayout( disabled: Boolean, selectedChat: State, nextChatSelected: State, + oneHandUI: State ) @Preview/*( @@ -945,7 +954,8 @@ fun PreviewChatListNavLinkDirect() { showMenu = remember { mutableStateOf(false) }, disabled = false, selectedChat = remember { mutableStateOf(false) }, - nextChatSelected = remember { mutableStateOf(false) } + nextChatSelected = remember { mutableStateOf(false) }, + oneHandUI = remember { mutableStateOf(false) } ) } } @@ -990,7 +1000,8 @@ fun PreviewChatListNavLinkGroup() { showMenu = remember { mutableStateOf(false) }, disabled = false, selectedChat = remember { mutableStateOf(false) }, - nextChatSelected = remember { mutableStateOf(false) } + nextChatSelected = remember { mutableStateOf(false) }, + oneHandUI = remember { mutableStateOf(false) } ) } } @@ -1012,7 +1023,8 @@ fun PreviewChatListNavLinkContactRequest() { showMenu = remember { mutableStateOf(false) }, disabled = false, selectedChat = remember { mutableStateOf(false) }, - nextChatSelected = remember { mutableStateOf(false) } + nextChatSelected = remember { mutableStateOf(false) }, + oneHandUI = remember { mutableStateOf(false) } ) } } 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 eb76b08cac..332c1d070e 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 @@ -1,5 +1,6 @@ package chat.simplex.common.views.chatlist +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.* @@ -10,6 +11,7 @@ import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale import androidx.compose.ui.focus.* import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.painter.Painter @@ -51,6 +53,8 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf if (animated) newChatSheetState.value = AnimatedViewState.HIDING else newChatSheetState.value = AnimatedViewState.GONE } + val oneHandUI = remember { chatModel.controller.appPrefs.oneHandUI } + LaunchedEffect(Unit) { if (shouldShowWhatsNew(chatModel)) { delay(1000L) @@ -87,13 +91,20 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf drawerGesturesEnabled = appPlatform.isAndroid, floatingActionButton = { if (searchText.value.text.isEmpty() && !chatModel.desktopNoUserNoRemote && chatModel.chatRunning.value == true) { + var bottom = DEFAULT_PADDING + if (oneHandUI.state.value) { + bottom = DEFAULT_BOTTOM_PADDING + } else { + bottom -= 16.dp + } + FloatingActionButton( onClick = { if (!stopped) { if (newChatSheetState.value.isVisible()) hideNewChatSheet(true) else showNewChatSheet() } }, - Modifier.padding(end = DEFAULT_PADDING - 16.dp + endPadding, bottom = DEFAULT_PADDING - 16.dp).size(AppBarHeight * fontSizeSqrtMultiplier), + Modifier.padding(end = DEFAULT_PADDING - 16.dp + endPadding, bottom = bottom).size(AppBarHeight * fontSizeSqrtMultiplier), elevation = FloatingActionButtonDefaults.elevation( defaultElevation = 0.dp, pressedElevation = 0.dp, @@ -108,13 +119,18 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf } } ) { - Box(Modifier.padding(it).padding(end = endPadding)) { + var modifier = Modifier.padding(it).padding(end = endPadding) + if (oneHandUI.state.value) { + modifier = modifier.scale(scaleX = 1f, scaleY = -1f) + } + + Box(modifier) { Box( modifier = Modifier .fillMaxSize() ) { if (!chatModel.desktopNoUserNoRemote) { - ChatList(chatModel, searchText = searchText) + ChatList(chatModel, searchText = searchText, oneHandUI = oneHandUI) } if (chatModel.chats.isEmpty() && !chatModel.switchingUsersAndHosts.value && !chatModel.desktopNoUserNoRemote) { Text(stringResource( @@ -460,8 +476,14 @@ fun connectIfOpenedViaUri(rhId: Long?, uri: URI, chatModel: ChatModel) { } @Composable -private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState, searchShowingSimplexLink: MutableState, searchChatFilteredBySimplexLink: MutableState) { - Row(verticalAlignment = Alignment.CenterVertically) { +private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState, searchShowingSimplexLink: MutableState, searchChatFilteredBySimplexLink: MutableState, oneHandUI: SharedPreference) { + var modifier = Modifier.fillMaxWidth(); + + if (oneHandUI.state.value) { + modifier = modifier.scale(scaleX = 1f, scaleY = -1f) + } + + Row(verticalAlignment = Alignment.CenterVertically, modifier = modifier) { val focusRequester = remember { FocusRequester() } var focused by remember { mutableStateOf(false) } Icon(painterResource(MR.images.ic_search), null, Modifier.padding(horizontal = DEFAULT_PADDING_HALF).size(24.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.secondary) @@ -550,9 +572,35 @@ private fun ErrorSettingsView() { private var lazyListState = 0 to 0 +enum class ScrollDirection { + Up, Down, Idle +} + @Composable -private fun ChatList(chatModel: ChatModel, searchText: MutableState) { +private fun ChatList(chatModel: ChatModel, searchText: MutableState, oneHandUI: SharedPreference) { val listState = rememberLazyListState(lazyListState.first, lazyListState.second) + var scrollDirection by remember { mutableStateOf(ScrollDirection.Idle) } + var previousIndex by remember { mutableStateOf(0) } + var previousScrollOffset by remember { mutableStateOf(0) } + + LaunchedEffect(listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset) { + val currentIndex = listState.firstVisibleItemIndex + val currentScrollOffset = listState.firstVisibleItemScrollOffset + val threshold = 25 + + scrollDirection = when { + currentIndex > previousIndex -> ScrollDirection.Down + currentIndex < previousIndex -> ScrollDirection.Up + currentScrollOffset > previousScrollOffset + threshold -> ScrollDirection.Down + currentScrollOffset < previousScrollOffset - threshold -> ScrollDirection.Up + currentScrollOffset == previousScrollOffset -> ScrollDirection.Idle + else -> scrollDirection + } + + previousIndex = currentIndex + previousScrollOffset = currentScrollOffset + } + DisposableEffect(Unit) { onDispose { lazyListState = listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset } } @@ -573,7 +621,9 @@ private fun ChatList(chatModel: ChatModel, searchText: MutableState diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.desktop.kt index 189f1842dd..c7968d8160 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.desktop.kt @@ -35,6 +35,7 @@ actual fun ChatListNavLinkLayout( disabled: Boolean, selectedChat: State, nextChatSelected: State, + oneHandUI: State ) { var modifier = Modifier.fillMaxWidth() if (!disabled) modifier = modifier