From 885aa9cfa55a7ba66ff9318f40022154b9206c0c Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Mon, 19 Aug 2024 18:43:54 +0000 Subject: [PATCH] android, desktop: scrolling moves title to app bar (#4703) * android, desktop: scrolling moves title to app bar * one place should be without padding * scroll related changes for both platforms * adapt code to universal ColumnWithScrollBar * show in center * small adjustments * new chat sheet fix * divider + mix background color for desktop * coerce * different transition * desktop title starts from left * host starts from left too * different coefficient * settings title --- .../common/platform/Resources.android.kt | 3 + .../platform/ScrollableColumn.android.kt | 62 ++++++- .../kotlin/chat/simplex/common/App.kt | 2 + .../chat/simplex/common/platform/Platform.kt | 3 - .../chat/simplex/common/platform/Resources.kt | 3 + .../common/platform/ScrollableColumn.kt | 6 +- .../chat/simplex/common/views/TerminalView.kt | 8 +- .../common/views/chat/ChatItemInfoView.kt | 4 - .../simplex/common/views/chat/ChatView.kt | 38 ++-- .../views/chat/group/GroupChatInfoView.kt | 1 - .../common/views/chatlist/ChatListView.kt | 9 +- .../views/chatlist/ServersSummaryView.kt | 32 ++-- .../views/database/DatabaseEncryptionView.kt | 165 +++++++++--------- .../common/views/helpers/CloseSheetBar.kt | 86 +++++++-- .../common/views/helpers/CollapsingAppBar.kt | 44 +++++ .../simplex/common/views/helpers/ModalView.kt | 23 ++- .../views/migration/MigrateFromDevice.kt | 14 +- .../common/views/migration/MigrateToDevice.kt | 11 +- .../common/views/newchat/NewChatSheet.kt | 14 +- .../common/views/newchat/NewChatView.kt | 118 ++++++------- .../common/views/onboarding/HowItWorks.kt | 7 +- .../usersettings/SetDeliveryReceiptsView.kt | 39 ++--- .../common/views/usersettings/SettingsView.kt | 134 +++++++------- .../usersettings/UserAddressLearnMore.kt | 9 +- .../views/usersettings/UserAddressView.kt | 2 +- .../common/platform/Resources.desktop.kt | 3 + .../platform/ScrollableColumn.desktop.kt | 129 +++++++++----- .../kotlin/chat/simplex/desktop/Main.kt | 32 ---- 28 files changed, 583 insertions(+), 418 deletions(-) create mode 100644 apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CollapsingAppBar.kt diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Resources.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Resources.android.kt index 91d19759ea..73c920b940 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Resources.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Resources.android.kt @@ -52,6 +52,9 @@ actual fun windowOrientation(): WindowOrientation = when (mainActivity.get()?.re @Composable actual fun windowWidth(): Dp = LocalConfiguration.current.screenWidthDp.dp +@Composable +actual fun windowHeight(): Dp = LocalConfiguration.current.screenHeightDp.dp + actual fun desktopExpandWindowToWidth(width: Dp) {} actual fun isRtl(text: CharSequence): Boolean = BidiFormatter.getInstance().isRtl(text) 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 199e719703..6851970b81 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 @@ -4,14 +4,21 @@ import androidx.compose.foundation.* import androidx.compose.foundation.gestures.FlingBehavior import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.* -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.dp +import chat.simplex.common.views.helpers.* +import kotlinx.coroutines.flow.filter +import kotlin.math.absoluteValue @Composable actual fun LazyColumnWithScrollBar( modifier: Modifier, - state: LazyListState, + state: LazyListState?, contentPadding: PaddingValues, reverseLayout: Boolean, verticalArrangement: Arrangement.Vertical, @@ -20,7 +27,24 @@ actual fun LazyColumnWithScrollBar( userScrollEnabled: Boolean, content: LazyListScope.() -> Unit ) { - LazyColumn(modifier, state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content) + 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 (offset != null && (offset + scrollPosition).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, content) + } else { + LazyColumn(modifier, state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content) + } } @Composable @@ -28,8 +52,34 @@ actual fun ColumnWithScrollBar( modifier: Modifier, verticalArrangement: Arrangement.Vertical, horizontalAlignment: Alignment.Horizontal, - state: ScrollState, - content: @Composable ColumnScope.() -> Unit + state: ScrollState?, + maxIntrinsicSize: Boolean, + content: @Composable() (ColumnScope.() -> Unit) ) { - Column(modifier.verticalScroll(rememberScrollState()), verticalArrangement, horizontalAlignment, content) + val state = state ?: LocalAppBarHandler.current?.scrollState ?: rememberScrollState() + val connection = LocalAppBarHandler.current?.connection + LaunchedEffect(Unit) { + snapshotFlow { state.value } + .collect { scrollPosition -> + val offset = connection?.appBarOffset + if (offset != null && (offset + scrollPosition).absoluteValue > 1) { + connection.appBarOffset = -scrollPosition.toFloat() +// Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}") + } + } + } + if (connection != null) { + Column( + if (maxIntrinsicSize) { + modifier.nestedScroll(connection).verticalScroll(state).height(IntrinsicSize.Max) + } else { + modifier.nestedScroll(connection).verticalScroll(state) + }, verticalArrangement, horizontalAlignment, content) + } else { + Column(if (maxIntrinsicSize) { + modifier.verticalScroll(state).height(IntrinsicSize.Max) + } else { + modifier.verticalScroll(state) + }, verticalArrangement, horizontalAlignment, content) + } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt index 1486fa8314..94ca307529 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp import chat.simplex.common.views.usersettings.SetDeliveryReceiptsView import chat.simplex.common.model.* @@ -50,6 +51,7 @@ data class SettingsViewState( @Composable fun AppScreen() { + AppBarHandler.appBarMaxHeightPx = with(LocalDensity.current) { AppBarHeight.roundToPx() } SimpleXTheme { ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { Surface(color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt index b48f7cebec..44fcddb54c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt @@ -28,9 +28,6 @@ interface PlatformInterface { fun androidRestartNetworkObserver() {} @Composable fun androidLockPortraitOrientation() {} suspend fun androidAskToAllowBackgroundCalls(): Boolean = true - @Composable fun desktopScrollBarComponents(): Triple, Modifier, MutableState> = remember { Triple(Animatable(0f), Modifier, mutableStateOf(Job())) } - @Composable fun desktopScrollBar(state: LazyListState, modifier: Modifier, scrollBarAlpha: Animatable, scrollJob: MutableState, reversed: Boolean) {} - @Composable fun desktopScrollBar(state: ScrollState, modifier: Modifier, scrollBarAlpha: Animatable, scrollJob: MutableState, reversed: Boolean) {} @Composable fun desktopShowAppUpdateNotice() {} } /** diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Resources.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Resources.kt index 8e45ded4f0..fd712624bd 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Resources.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Resources.kt @@ -30,6 +30,9 @@ expect fun windowOrientation(): WindowOrientation @Composable expect fun windowWidth(): Dp +@Composable +expect fun windowHeight(): Dp + expect fun desktopExpandWindowToWidth(width: Dp) expect fun isRtl(text: CharSequence): Boolean 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 d4fa2fe125..532bfddfcf 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 @@ -13,7 +13,7 @@ import androidx.compose.ui.unit.dp @Composable expect fun LazyColumnWithScrollBar( modifier: Modifier = Modifier, - state: LazyListState = rememberLazyListState(), + state: LazyListState? = null, contentPadding: PaddingValues = PaddingValues(0.dp), reverseLayout: Boolean = false, verticalArrangement: Arrangement.Vertical = @@ -29,6 +29,8 @@ expect fun ColumnWithScrollBar( modifier: Modifier = Modifier, verticalArrangement: Arrangement.Vertical = Arrangement.Top, horizontalAlignment: Alignment.Horizontal = Alignment.Start, - state: ScrollState = rememberScrollState(), + state: ScrollState? = null, + // set true when you want to show something in the center with respected .fillMaxSize() + maxIntrinsicSize: Boolean = false, content: @Composable ColumnScope.() -> 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 259b7e2320..1afbb0bdcb 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 @@ -125,19 +125,13 @@ fun TerminalLayout( } } -private var lazyListState = 0 to 0 - @Composable fun TerminalLog() { - val listState = rememberLazyListState(lazyListState.first, lazyListState.second) - DisposableEffect(Unit) { - onDispose { lazyListState = listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset } - } val reversedTerminalItems by remember { derivedStateOf { chatModel.terminalItems.value.asReversed() } } val clipboard = LocalClipboardManager.current - LazyColumnWithScrollBar(state = listState, reverseLayout = true) { + LazyColumnWithScrollBar(reverseLayout = true) { items(reversedTerminalItems) { 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/chat/ChatItemInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt index c7df983324..a5593693c0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt @@ -277,7 +277,6 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools @Composable fun HistoryTab() { - // LALAL SCROLLBAR DOESN'T WORK ColumnWithScrollBar(Modifier.fillMaxWidth()) { Details() SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true) @@ -302,7 +301,6 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools @Composable fun QuoteTab(qi: CIQuote) { - // LALAL SCROLLBAR DOESN'T WORK ColumnWithScrollBar(Modifier.fillMaxWidth()) { Details() SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true) @@ -316,7 +314,6 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools @Composable fun ForwardedFromTab(forwardedFromItem: AChatItem) { - // LALAL SCROLLBAR DOESN'T WORK ColumnWithScrollBar(Modifier.fillMaxWidth()) { Details() SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true) @@ -379,7 +376,6 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools @Composable fun DeliveryTab(memberDeliveryStatuses: List) { - // LALAL SCROLLBAR DOESN'T WORK ColumnWithScrollBar(Modifier.fillMaxWidth()) { Details() SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true) 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 a5541a315b..e90eed547d 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 @@ -504,24 +504,34 @@ fun ChatView(staleChatId: State, onComposed: suspend (chatId: String) - } is ChatInfo.ContactConnection -> { val close = { chatModel.chatId.value = null } - ModalView(close, showClose = appPlatform.isAndroid, content = { - ContactConnectionInfoView(chatModel, chatRh, chatInfo.contactConnection.connReqInv, chatInfo.contactConnection, false, close) - }) - LaunchedEffect(chatInfo.id) { - onComposed(chatInfo.id) - ModalManager.end.closeModals() - chatModel.chatItems.clear() + val handler = remember { AppBarHandler() } + CompositionLocalProvider( + LocalAppBarHandler provides handler + ) { + ModalView(close, showClose = appPlatform.isAndroid, content = { + ContactConnectionInfoView(chatModel, chatRh, chatInfo.contactConnection.connReqInv, chatInfo.contactConnection, false, close) + }) + LaunchedEffect(chatInfo.id) { + onComposed(chatInfo.id) + ModalManager.end.closeModals() + chatModel.chatItems.clear() + } } } is ChatInfo.InvalidJSON -> { val close = { chatModel.chatId.value = null } - ModalView(close, showClose = appPlatform.isAndroid, endButtons = { ShareButton { clipboard.shareText(chatInfo.json) } }, content = { - InvalidJSONView(chatInfo.json) - }) - LaunchedEffect(chatInfo.id) { - onComposed(chatInfo.id) - ModalManager.end.closeModals() - chatModel.chatItems.clear() + val handler = remember { AppBarHandler() } + CompositionLocalProvider( + LocalAppBarHandler provides handler + ) { + ModalView(close, showClose = appPlatform.isAndroid, endButtons = { ShareButton { clipboard.shareText(chatInfo.json) } }, content = { + InvalidJSONView(chatInfo.json) + }) + LaunchedEffect(chatInfo.id) { + onComposed(chatInfo.id) + ModalManager.end.closeModals() + chatModel.chatItems.clear() + } } } else -> {} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt index 6e39e8756e..ea07231eb0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt @@ -284,7 +284,6 @@ fun GroupChatInfoLayout( if (s.isEmpty()) members else members.filter { m -> m.anyNameContains(s) } } } - // LALAL strange scrolling LazyColumnWithScrollBar( Modifier .fillMaxWidth(), 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 b1c353d420..6f595e2ccf 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 @@ -185,7 +185,14 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf scaffoldState = scaffoldState, drawerContent = { tryOrShowError("Settings", error = { ErrorSettingsView() }) { - SettingsView(chatModel, setPerformLA, scaffoldState.drawerState) + val handler = remember { AppBarHandler() } + CompositionLocalProvider( + LocalAppBarHandler provides handler + ) { + ModalView(showClose = appPlatform.isDesktop, close = { scope.launch { scaffoldState.drawerState.close() } }) { + SettingsView(chatModel, setPerformLA, scaffoldState.drawerState) + } + } } }, contentColor = LocalContentColor.current, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ServersSummaryView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ServersSummaryView.kt index 334335a80f..f5343efc18 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ServersSummaryView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ServersSummaryView.kt @@ -615,14 +615,12 @@ fun ModalData.SMPServerSummaryView( ColumnWithScrollBar( Modifier.fillMaxSize(), ) { - Box(contentAlignment = Alignment.Center) { - val bottomPadding = DEFAULT_PADDING - AppBarTitle( - stringResource(MR.strings.smp_server), - hostDevice(rh?.remoteHostId), - bottomPadding = bottomPadding - ) - } + val bottomPadding = DEFAULT_PADDING + AppBarTitle( + stringResource(MR.strings.smp_server), + hostDevice(rh?.remoteHostId), + bottomPadding = bottomPadding + ) SMPServerSummaryLayout(summary, statsStartedAt, rh) } } @@ -709,7 +707,7 @@ fun ModalData.XFTPServerSummaryView( @Composable fun ModalData.ServersSummaryView(rh: RemoteHostInfo?, serversSummary: MutableState) { - Column( + ColumnWithScrollBar( Modifier.fillMaxSize(), ) { var showUserSelection by remember { mutableStateOf(false) } @@ -760,14 +758,12 @@ fun ModalData.ServersSummaryView(rh: RemoteHostInfo?, serversSummary: MutableSta Column( Modifier.fillMaxSize(), ) { - Box(contentAlignment = Alignment.Center) { - val bottomPadding = DEFAULT_PADDING - AppBarTitle( - stringResource(MR.strings.servers_info), - hostDevice(rh?.remoteHostId), - bottomPadding = bottomPadding - ) - } + val bottomPadding = DEFAULT_PADDING + AppBarTitle( + stringResource(MR.strings.servers_info), + hostDevice(rh?.remoteHostId), + bottomPadding = bottomPadding + ) if (serversSummary.value == null) { Box( modifier = Modifier @@ -827,7 +823,7 @@ fun ModalData.ServersSummaryView(rh: RemoteHostInfo?, serversSummary: MutableSta verticalAlignment = Alignment.Top, userScrollEnabled = appPlatform.isAndroid ) { index -> - ColumnWithScrollBar( + Column( Modifier .fillMaxSize(), verticalArrangement = Arrangement.Top diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.kt index a2aeac21a2..b73a0ca0bc 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.kt @@ -107,101 +107,104 @@ fun DatabaseEncryptionLayout( migration: Boolean, onConfirmEncrypt: () -> Unit, ) { - val (scrollBarAlpha, scrollModifier, scrollJob) = platform.desktopScrollBarComponents() - val scrollState = rememberScrollState() - Column( - if (!migration) Modifier.fillMaxWidth().verticalScroll(scrollState).then(if (appPlatform.isDesktop) scrollModifier else Modifier) else Modifier.fillMaxWidth(), - ) { - if (!migration) { - AppBarTitle(stringResource(MR.strings.database_passphrase)) - } else { - ChatStoppedView() - SectionSpacer() - } - SectionView(if (migration) generalGetString(MR.strings.database_passphrase).uppercase() else null) { - SavePassphraseSetting( - useKeychain.value, - initialRandomDBPassphrase.value, - storedKey.value, - enabled = (!initialRandomDBPassphrase.value && !progressIndicator.value) || migration - ) { checked -> - if (checked) { - setUseKeychain(true, useKeychain, migration) - } else if (storedKey.value && !migration) { - // Don't show in migration process since it will remove the key after successful encryption - removePassphraseAlert { - removePassphraseFromKeyChain(useKeychain, storedKey, false) - } - } else { - setUseKeychain(false, useKeychain, migration) - } + @Composable + fun Layout() { + Column { + if (!migration) { + AppBarTitle(stringResource(MR.strings.database_passphrase)) + } else { + ChatStoppedView() + SectionSpacer() } + SectionView(if (migration) generalGetString(MR.strings.database_passphrase).uppercase() else null) { + SavePassphraseSetting( + useKeychain.value, + initialRandomDBPassphrase.value, + storedKey.value, + enabled = (!initialRandomDBPassphrase.value && !progressIndicator.value) || migration + ) { checked -> + if (checked) { + setUseKeychain(true, useKeychain, migration) + } else if (storedKey.value && !migration) { + // Don't show in migration process since it will remove the key after successful encryption + removePassphraseAlert { + removePassphraseFromKeyChain(useKeychain, storedKey, false) + } + } else { + setUseKeychain(false, useKeychain, migration) + } + } + + if (!initialRandomDBPassphrase.value && chatDbEncrypted == true) { + PassphraseField( + currentKey, + generalGetString(MR.strings.current_passphrase), + modifier = Modifier.padding(horizontal = DEFAULT_PADDING), + isValid = ::validKey, + keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }), + ) + } - if (!initialRandomDBPassphrase.value && chatDbEncrypted == true) { PassphraseField( - currentKey, - generalGetString(MR.strings.current_passphrase), + newKey, + generalGetString(MR.strings.new_passphrase), modifier = Modifier.padding(horizontal = DEFAULT_PADDING), + showStrength = true, isValid = ::validKey, keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }), ) - } - - PassphraseField( - newKey, - generalGetString(MR.strings.new_passphrase), - modifier = Modifier.padding(horizontal = DEFAULT_PADDING), - showStrength = true, - isValid = ::validKey, - keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }), - ) - val onClickUpdate = { - // Don't do things concurrently. Shouldn't be here concurrently, just in case - if (!progressIndicator.value) { - if (currentKey.value == "") { - if (useKeychain.value) - encryptDatabaseSavedAlert(onConfirmEncrypt) - else - encryptDatabaseAlert(onConfirmEncrypt) - } else { - if (useKeychain.value) - changeDatabaseKeySavedAlert(onConfirmEncrypt) - else - changeDatabaseKeyAlert(onConfirmEncrypt) + val onClickUpdate = { + // Don't do things concurrently. Shouldn't be here concurrently, just in case + if (!progressIndicator.value) { + if (currentKey.value == "") { + if (useKeychain.value) + encryptDatabaseSavedAlert(onConfirmEncrypt) + else + encryptDatabaseAlert(onConfirmEncrypt) + } else { + if (useKeychain.value) + changeDatabaseKeySavedAlert(onConfirmEncrypt) + else + changeDatabaseKeyAlert(onConfirmEncrypt) + } } } + val disabled = currentKey.value == newKey.value || + newKey.value != confirmNewKey.value || + newKey.value.isEmpty() || + !validKey(currentKey.value) || + !validKey(newKey.value) || + progressIndicator.value + + PassphraseField( + confirmNewKey, + generalGetString(MR.strings.confirm_new_passphrase), + modifier = Modifier.padding(horizontal = DEFAULT_PADDING), + isValid = { confirmNewKey.value == "" || newKey.value == confirmNewKey.value }, + keyboardActions = KeyboardActions(onDone = { + if (!disabled) onClickUpdate() + defaultKeyboardAction(ImeAction.Done) + }), + ) + + SectionItemViewSpaceBetween(onClickUpdate, disabled = disabled, minHeight = TextFieldDefaults.MinHeight) { + Text(generalGetString(if (migration) MR.strings.set_passphrase else MR.strings.update_database_passphrase), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary) + } } - val disabled = currentKey.value == newKey.value || - newKey.value != confirmNewKey.value || - newKey.value.isEmpty() || - !validKey(currentKey.value) || - !validKey(newKey.value) || - progressIndicator.value - PassphraseField( - confirmNewKey, - generalGetString(MR.strings.confirm_new_passphrase), - modifier = Modifier.padding(horizontal = DEFAULT_PADDING), - isValid = { confirmNewKey.value == "" || newKey.value == confirmNewKey.value }, - keyboardActions = KeyboardActions(onDone = { - if (!disabled) onClickUpdate() - defaultKeyboardAction(ImeAction.Done) - }), - ) - - SectionItemViewSpaceBetween(onClickUpdate, disabled = disabled, minHeight = TextFieldDefaults.MinHeight) { - Text(generalGetString(if (migration) MR.strings.set_passphrase else MR.strings.update_database_passphrase), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary) + Column { + DatabaseEncryptionFooter(useKeychain, chatDbEncrypted, storedKey, initialRandomDBPassphrase, migration) } + SectionBottomSpacer() } - - Column { - DatabaseEncryptionFooter(useKeychain, chatDbEncrypted, storedKey, initialRandomDBPassphrase, migration) - } - SectionBottomSpacer() } - if (appPlatform.isDesktop && !migration) { - Box(Modifier.fillMaxSize()) { - platform.desktopScrollBar(scrollState, Modifier.align(Alignment.CenterEnd).fillMaxHeight(), scrollBarAlpha, scrollJob, false) + if (migration) { + Column(Modifier.fillMaxWidth()) { + Layout() + } + } else { + ColumnWithScrollBar(Modifier.fillMaxWidth(), maxIntrinsicSize = true) { + Layout() } } } 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 0ff68de3a0..080edd22b2 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 @@ -6,59 +6,90 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color 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.ui.draw.* +import androidx.compose.ui.graphics.* import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.* +import chat.simplex.common.platform.appPlatform import chat.simplex.common.ui.theme.* import chat.simplex.res.MR import dev.icerock.moko.resources.compose.painterResource +import kotlin.math.absoluteValue @Composable fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, tintColor: Color = if (close != null) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, arrangement: Arrangement.Vertical = Arrangement.Top, closeBarTitle: String? = null, barPaddingValues: PaddingValues = PaddingValues(horizontal = AppBarHorizontalPadding), endButtons: @Composable RowScope.() -> Unit = {}) { var rowModifier = Modifier .fillMaxWidth() .height(AppBarHeight * fontSizeSqrtMultiplier) - + val themeBackgroundMix = MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f) if (!closeBarTitle.isNullOrEmpty()) { - rowModifier = rowModifier.background(MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f)) + rowModifier = rowModifier.background(themeBackgroundMix) } + val handler = LocalAppBarHandler.current + val connection = LocalAppBarHandler.current?.connection + val title = remember(handler?.title?.value) { handler?.title ?: mutableStateOf("") } Column( verticalArrangement = arrangement, modifier = Modifier .fillMaxWidth() .heightIn(min = AppBarHeight * fontSizeSqrtMultiplier) + .drawWithCache { + val backgroundColor = if (appPlatform.isDesktop && connection != null) themeBackgroundMix.copy(alpha = topTitleAlpha(connection)) else Color.Transparent + onDrawBehind { + if (appPlatform.isDesktop) { + drawRect(backgroundColor) + } + } + } ) { Row( modifier = Modifier.padding(barPaddingValues), content = { Row( rowModifier, - horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - if (showClose) { + if (showClose) { NavigationButtonBack(tintColor = tintColor, onButtonClicked = close) } else { Spacer(Modifier) } if (!closeBarTitle.isNullOrEmpty()) { Row( + Modifier.weight(1f), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically ) { Text( closeBarTitle, - color = MaterialTheme.colors.onBackground, fontWeight = FontWeight.SemiBold, + maxLines = 1 ) } + } else if (title.value.isNotEmpty() && connection != null) { + Row( + Modifier + .padding(start = if (showClose) 0.dp else DEFAULT_PADDING_HALF) + .weight(1f) // hides the title if something wants full width (eg, search field in chat profiles screen) + .graphicsLayer { + alpha = topTitleAlpha((connection)) + } + .padding(start = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + title.value, + fontWeight = FontWeight.SemiBold, + maxLines = 1 + ) + } + } else { + Spacer(Modifier.weight(1f)) } Row { endButtons() @@ -66,11 +97,24 @@ fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, tintColor: Co } } ) + if (closeBarTitle.isNullOrEmpty() && title.value.isNotEmpty() && connection != null) { + Divider( + Modifier + .graphicsLayer { + alpha = topTitleAlpha(connection) + } + ) + } } } @Composable fun AppBarTitle(title: String, hostDevice: Pair? = null, withPadding: Boolean = true, bottomPadding: Dp = DEFAULT_PADDING * 1.5f + 8.dp) { + val handler = LocalAppBarHandler.current + val connection = handler?.connection + LaunchedEffect(title) { + handler?.title?.value = title + } val theme = CurrentColors.collectAsState() val titleColor = MaterialTheme.appColors.title val brush = if (theme.value.base == DefaultTheme.SIMPLEX) @@ -81,23 +125,37 @@ fun AppBarTitle(title: String, hostDevice: Pair? = null, withPad Text( title, Modifier - .fillMaxWidth() - .padding(start = if (withPadding) DEFAULT_PADDING else 0.dp, end = if (withPadding) DEFAULT_PADDING else 0.dp,), + .padding(start = if (withPadding) DEFAULT_PADDING else 0.dp, top = DEFAULT_PADDING_HALF, end = if (withPadding) DEFAULT_PADDING else 0.dp,) + .graphicsLayer { + alpha = bottomTitleAlpha(connection) + }, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.h1.copy(brush = brush), color = MaterialTheme.colors.primaryVariant, - textAlign = TextAlign.Center + textAlign = TextAlign.Start ) if (hostDevice != null) { - HostDeviceTitle(hostDevice) + Box(Modifier.padding(start = if (withPadding) DEFAULT_PADDING else 0.dp, end = if (withPadding) DEFAULT_PADDING else 0.dp).graphicsLayer { + alpha = bottomTitleAlpha(connection) + }) { + HostDeviceTitle(hostDevice) + } } Spacer(Modifier.height(bottomPadding)) } } +private fun topTitleAlpha(connection: CollapsingAppBarNestedScrollConnection) = + if (connection.appBarOffset.absoluteValue < AppBarHandler.appBarMaxHeightPx / 3) 0f + else ((-connection.appBarOffset * 1.5f) / (AppBarHandler.appBarMaxHeightPx)).coerceIn(0f, 1f) + +private fun bottomTitleAlpha(connection: CollapsingAppBarNestedScrollConnection?) = + if ((connection?.appBarOffset ?: 0f).absoluteValue < AppBarHandler.appBarMaxHeightPx / 3) 1f + else ((AppBarHandler.appBarMaxHeightPx) + (connection?.appBarOffset ?: 0f) / 1.5f).coerceAtLeast(0f) / AppBarHandler.appBarMaxHeightPx + @Composable private fun HostDeviceTitle(hostDevice: Pair, extraPadding: Boolean = false) { - Row(Modifier.fillMaxWidth().padding(top = 5.dp, bottom = if (extraPadding) DEFAULT_PADDING * 2 else DEFAULT_PADDING_HALF), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center) { + Row(Modifier.fillMaxWidth().padding(top = 5.dp, bottom = if (extraPadding) DEFAULT_PADDING * 2 else DEFAULT_PADDING_HALF), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start) { Icon(painterResource(if (hostDevice.first == null) MR.images.ic_desktop else MR.images.ic_smartphone_300), null, Modifier.size(15.dp), tint = MaterialTheme.colors.secondary) Spacer(Modifier.width(10.dp)) Text(hostDevice.second, color = MaterialTheme.colors.secondary) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CollapsingAppBar.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CollapsingAppBar.kt new file mode 100644 index 0000000000..4410f7ada5 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CollapsingAppBar.kt @@ -0,0 +1,44 @@ +package chat.simplex.common.views.helpers + +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.* +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.unit.Velocity + +val LocalAppBarHandler: ProvidableCompositionLocal = staticCompositionLocalOf { null } + +@Stable +class AppBarHandler( + listState: LazyListState = LazyListState(0, 0), + scrollState: ScrollState = ScrollState(initial = 0) +) { + val title = mutableStateOf("") + var listState by mutableStateOf(listState, structuralEqualityPolicy()) + internal set + + var scrollState by mutableStateOf(scrollState, structuralEqualityPolicy()) + internal set + + val connection = CollapsingAppBarNestedScrollConnection() + + companion object { + var appBarMaxHeightPx: Int = 0 + } +} + +class CollapsingAppBarNestedScrollConnection(): NestedScrollConnection { + var appBarOffset: Float by mutableFloatStateOf(0f) + + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + appBarOffset += available.y + return Offset(0f, 0f) + } + + override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset { + appBarOffset -= available.y + return Offset(x = 0f, 0f) + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt index d44476b91b..bfd61a2add 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt @@ -2,11 +2,12 @@ package chat.simplex.common.views.helpers import androidx.compose.animation.* import androidx.compose.animation.core.* +import androidx.compose.foundation.ScrollState import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.material.* import androidx.compose.runtime.* -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import chat.simplex.common.model.ChatController.appPrefs @@ -48,13 +49,15 @@ enum class ModalPlacement { START, CENTER, END, FULLSCREEN } -class ModalData { +class ModalData() { private val state = mutableMapOf>() fun stateGetOrPut (key: String, default: () -> T): MutableState = state.getOrPut(key) { mutableStateOf(default() as Any) } as MutableState fun stateGetOrPutNullable (key: String, default: () -> T?): MutableState = state.getOrPut(key) { mutableStateOf(default() as Any?) } as MutableState + + val appBarHandler = AppBarHandler() } class ModalManager(private val placement: ModalPlacement? = null) { @@ -139,7 +142,13 @@ class ModalManager(private val placement: ModalPlacement? = null) { fun showInView() { // Without animation if (modalCount.value > 0 && modalViews.lastOrNull()?.first == false) { - modalViews.lastOrNull()?.let { it.third(it.second, ::closeModal) } + modalViews.lastOrNull()?.let { + CompositionLocalProvider( + LocalAppBarHandler provides it.second.appBarHandler + ) { + it.third(it.second, ::closeModal) + } + } return } AnimatedContent(targetState = modalCount.value, @@ -151,7 +160,13 @@ class ModalManager(private val placement: ModalPlacement? = null) { }.using(SizeTransform(clip = false)) } ) { - modalViews.getOrNull(it - 1)?.let { it.third(it.second, ::closeModal) } + modalViews.getOrNull(it - 1)?.let { + CompositionLocalProvider( + LocalAppBarHandler provides it.second.appBarHandler + ) { + it.third(it.second, ::closeModal) + } + } // This is needed because if we delete from modalViews immediately on request, animation will be bad if (toRemove.isNotEmpty() && it == modalCount.value && transition.currentState == EnterExitState.Visible && !transition.isRunning) { runAtomically { toRemove.removeIf { elem -> modalViews.removeAt(elem); true } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateFromDevice.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateFromDevice.kt index 152befb82d..dbb805971e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateFromDevice.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateFromDevice.kt @@ -4,7 +4,7 @@ import SectionBottomSpacer import SectionSpacer import SectionTextFooter import SectionView -import androidx.compose.foundation.* +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.* @@ -17,7 +17,6 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.* -import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatController.getNetCfg import chat.simplex.common.model.ChatController.startChat import chat.simplex.common.model.ChatController.startChatWithTemporaryDatabase @@ -147,20 +146,13 @@ private fun MigrateFromDeviceLayout( ) { val tempDatabaseFile = rememberSaveable { mutableStateOf(fileForTemporaryDatabase()) } - val (scrollBarAlpha, scrollModifier, scrollJob) = platform.desktopScrollBarComponents() - val scrollState = rememberScrollState() - Column( - Modifier.fillMaxSize().verticalScroll(scrollState).then(if (appPlatform.isDesktop) scrollModifier else Modifier).height(IntrinsicSize.Max), + ColumnWithScrollBar( + Modifier.fillMaxSize(), maxIntrinsicSize = true ) { AppBarTitle(stringResource(MR.strings.migrate_from_device_title)) SectionByState(migrationState, tempDatabaseFile.value, chatReceiver) SectionBottomSpacer() } - if (appPlatform.isDesktop) { - Box(Modifier.fillMaxSize()) { - platform.desktopScrollBar(scrollState, Modifier.align(Alignment.CenterEnd).fillMaxHeight(), scrollBarAlpha, scrollJob, false) - } - } platform.androidLockPortraitOrientation() } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt index f8a82d010d..4fa36b06f0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt @@ -155,20 +155,13 @@ private fun ModalData.MigrateToDeviceLayout( close: () -> Unit, ) { val tempDatabaseFile = rememberSaveable { mutableStateOf(fileForTemporaryDatabase()) } - val (scrollBarAlpha, scrollModifier, scrollJob) = platform.desktopScrollBarComponents() - val scrollState = rememberScrollState() - Column( - Modifier.fillMaxSize().verticalScroll(scrollState).then(if (appPlatform.isDesktop) scrollModifier else Modifier).height(IntrinsicSize.Max), + ColumnWithScrollBar( + Modifier.fillMaxSize(), maxIntrinsicSize = true ) { AppBarTitle(stringResource(MR.strings.migrate_to_device_title)) SectionByState(migrationState, tempDatabaseFile.value, chatReceiver, close) SectionBottomSpacer() } - if (appPlatform.isDesktop) { - Box(Modifier.fillMaxSize()) { - platform.desktopScrollBar(scrollState, Modifier.align(Alignment.CenterEnd).fillMaxHeight(), scrollBarAlpha, scrollJob, false) - } - } platform.androidLockPortraitOrientation() } 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 759c090bc7..3146c14589 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 @@ -68,10 +68,10 @@ fun NewChatSheet(rh: RemoteHostInfo?, close: () -> Unit) { Column(modifier = Modifier.fillMaxSize()) { NewChatSheetLayout( addContact = { - ModalManager.start.showModalCloseable { _ -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.INVITE, close = closeAll ) } + ModalManager.start.showModalCloseable(endButtons = { AddContactLearnMoreButton() }) { _ -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.INVITE, close = closeAll ) } }, scanPaste = { - ModalManager.start.showModalCloseable { _ -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.CONNECT, showQRCodeScanner = appPlatform.isAndroid, close = closeAll) } + ModalManager.start.showModalCloseable(endButtons = { AddContactLearnMoreButton() }) { _ -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.CONNECT, showQRCodeScanner = appPlatform.isAndroid, close = closeAll) } }, createGroup = { ModalManager.start.showCustomModal { close -> AddGroupView(chatModel, chatModel.currentRemoteHost.value, close, closeAll) } @@ -194,7 +194,15 @@ private fun NewChatSheetLayout( (appPlatform.isAndroid && keyboardState == KeyboardState.Opened) ) { 0 - } else if (listState.firstVisibleItemIndex == 0) offsetMultiplier * listState.firstVisibleItemScrollOffset else offsetMultiplier * 1000 + } 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 + } else { + offsetMultiplier * 1000 + } } else { 0 } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt index 007f474265..a872f2548d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt @@ -5,6 +5,7 @@ import SectionItemView import SectionTextFooter import SectionView import androidx.compose.foundation.* +import androidx.compose.foundation.gestures.scrollBy import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.pager.HorizontalPager @@ -96,66 +97,61 @@ fun ModalData.NewChatView(rh: RemoteHostInfo?, selection: NewChatOption, showQRC } } - Column( - Modifier.fillMaxSize(), - ) { - Box(contentAlignment = Alignment.Center) { - val bottomPadding = DEFAULT_PADDING - AppBarTitle(stringResource(MR.strings.new_chat), hostDevice(rh?.remoteHostId), bottomPadding = bottomPadding) - Column(Modifier.align(Alignment.CenterEnd).padding(bottom = bottomPadding, end = DEFAULT_PADDING)) { - AddContactLearnMoreButton() + BoxWithConstraints { + ColumnWithScrollBar { + AppBarTitle(stringResource(MR.strings.new_chat), hostDevice(rh?.remoteHostId), bottomPadding = DEFAULT_PADDING) + val scope = rememberCoroutineScope() + val pagerState = rememberPagerState( + initialPage = selection.value.ordinal, + initialPageOffsetFraction = 0f + ) { NewChatOption.values().size } + KeyChangeEffect(pagerState.currentPage) { + selection.value = NewChatOption.values()[pagerState.currentPage] } - } - val scope = rememberCoroutineScope() - val pagerState = rememberPagerState( - initialPage = selection.value.ordinal, - initialPageOffsetFraction = 0f - ) { NewChatOption.values().size } - KeyChangeEffect(pagerState.currentPage) { - selection.value = NewChatOption.values()[pagerState.currentPage] - } - TabRow( - selectedTabIndex = pagerState.currentPage, - backgroundColor = Color.Transparent, - contentColor = MaterialTheme.colors.primary, - ) { - tabTitles.forEachIndexed { index, it -> - LeadingIconTab( - selected = pagerState.currentPage == index, - onClick = { - scope.launch { - pagerState.animateScrollToPage(index) - } - }, - text = { Text(it, fontSize = 13.sp) }, - icon = { - Icon( - if (NewChatOption.INVITE.ordinal == index) painterResource(MR.images.ic_repeat_one) else painterResource(MR.images.ic_qr_code), - it - ) - }, - selectedContentColor = MaterialTheme.colors.primary, - unselectedContentColor = MaterialTheme.colors.secondary, - ) - } - } - - HorizontalPager(state = pagerState, Modifier.fillMaxSize(), verticalAlignment = Alignment.Top, userScrollEnabled = appPlatform.isAndroid) { index -> - // LALAL SCROLLBAR DOESN'T WORK - ColumnWithScrollBar( - Modifier - .fillMaxSize(), - verticalArrangement = if (index == NewChatOption.INVITE.ordinal && connReqInvitation.isEmpty()) Arrangement.Center else Arrangement.Top) { - Spacer(Modifier.height(DEFAULT_PADDING)) - when (index) { - NewChatOption.INVITE.ordinal -> { - PrepareAndInviteView(rh?.remoteHostId, contactConnection, connReqInvitation, creatingConnReq) - } - NewChatOption.CONNECT.ordinal -> { - ConnectView(rh?.remoteHostId, showQRCodeScanner, pastedLink, close) - } + TabRow( + selectedTabIndex = pagerState.currentPage, + backgroundColor = Color.Transparent, + contentColor = MaterialTheme.colors.primary, + ) { + tabTitles.forEachIndexed { index, it -> + LeadingIconTab( + selected = pagerState.currentPage == index, + onClick = { + scope.launch { + pagerState.animateScrollToPage(index) + } + }, + text = { Text(it, fontSize = 13.sp) }, + icon = { + Icon( + if (NewChatOption.INVITE.ordinal == index) painterResource(MR.images.ic_repeat_one) else painterResource(MR.images.ic_qr_code), + it + ) + }, + selectedContentColor = MaterialTheme.colors.primary, + unselectedContentColor = MaterialTheme.colors.secondary, + ) + } + } + + HorizontalPager(state = pagerState, Modifier, pageNestedScrollConnection = LocalAppBarHandler.current!!.connection, verticalAlignment = Alignment.Top, userScrollEnabled = appPlatform.isAndroid) { index -> + Column( + Modifier + .fillMaxWidth() + .heightIn(min = this@BoxWithConstraints.maxHeight - 150.dp), + verticalArrangement = if (index == NewChatOption.INVITE.ordinal && connReqInvitation.isEmpty()) Arrangement.Center else Arrangement.Top + ) { + Spacer(Modifier.height(DEFAULT_PADDING)) + when (index) { + NewChatOption.INVITE.ordinal -> { + PrepareAndInviteView(rh?.remoteHostId, contactConnection, connReqInvitation, creatingConnReq) + } + NewChatOption.CONNECT.ordinal -> { + ConnectView(rh?.remoteHostId, showQRCodeScanner, pastedLink, close) + } + } + SectionBottomSpacer() } - SectionBottomSpacer() } } } @@ -228,18 +224,18 @@ private fun InviteView(rhId: Long?, connReqInvitation: String, contactConnection } @Composable -private fun AddContactLearnMoreButton() { +fun AddContactLearnMoreButton() { IconButton( { ModalManager.start.showModalCloseable { close -> AddContactLearnMore(close) } - }, - Modifier.size(18.dp * fontSizeSqrtMultiplier) + } ) { Icon( painterResource(MR.images.ic_info), stringResource(MR.strings.learn_more), + tint = MaterialTheme.colors.primary ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/HowItWorks.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/HowItWorks.kt index 3f2c035335..9c7e2bdce7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/HowItWorks.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/HowItWorks.kt @@ -23,9 +23,10 @@ import dev.icerock.moko.resources.StringResource @Composable fun HowItWorks(user: User?, onboardingStage: SharedPreference? = null) { - ColumnWithScrollBar(Modifier - .fillMaxWidth() - .padding(DEFAULT_PADDING), + ColumnWithScrollBar( + Modifier + .fillMaxWidth() + .padding(DEFAULT_PADDING), ) { AppBarTitle(stringResource(MR.strings.how_simplex_works), withPadding = false) ReadableText(MR.strings.many_people_asked_how_can_it_deliver) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SetDeliveryReceiptsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SetDeliveryReceiptsView.kt index f0960c1511..2f6c0395ec 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SetDeliveryReceiptsView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SetDeliveryReceiptsView.kt @@ -73,33 +73,30 @@ private fun SetDeliveryReceiptsLayout( skip: () -> Unit, userCount: Int, ) { + // This view located in the left panel which means it has to have a padding from right side in order + // to see scroll bar. And this padding should be applied to upper element, not scrollable column modifier val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp - val (scrollBarAlpha, scrollModifier, scrollJob) = platform.desktopScrollBarComponents() - val scrollState = rememberScrollState() - Column( - Modifier.fillMaxSize().verticalScroll(scrollState).then(if (appPlatform.isDesktop) scrollModifier else Modifier).padding(top = DEFAULT_PADDING, end = endPadding), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - AppBarTitle(stringResource(MR.strings.delivery_receipts_title)) + Box(Modifier.padding(top = DEFAULT_PADDING, end = endPadding)) { + ColumnWithScrollBar( + Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + AppBarTitle(stringResource(MR.strings.delivery_receipts_title)) - Spacer(Modifier.weight(1f)) + Spacer(Modifier.weight(1f)) - EnableReceiptsButton(enableReceipts) - if (userCount > 1) { - TextBelowButton(stringResource(MR.strings.sending_delivery_receipts_will_be_enabled_all_profiles)) - } else { - TextBelowButton(stringResource(MR.strings.sending_delivery_receipts_will_be_enabled)) - } + EnableReceiptsButton(enableReceipts) + if (userCount > 1) { + TextBelowButton(stringResource(MR.strings.sending_delivery_receipts_will_be_enabled_all_profiles)) + } else { + TextBelowButton(stringResource(MR.strings.sending_delivery_receipts_will_be_enabled)) + } - Spacer(Modifier.weight(1f)) + Spacer(Modifier.weight(1f)) - SkipButton(skip) + SkipButton(skip) - SectionBottomSpacer() - } - if (appPlatform.isDesktop) { - Box(Modifier.fillMaxSize().padding(end = endPadding)) { - platform.desktopScrollBar(scrollState, Modifier.align(Alignment.CenterEnd).fillMaxHeight(), scrollBarAlpha, scrollJob, false) + SectionBottomSpacer() } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt index 02716fa17e..6c07a9ceeb 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt @@ -111,85 +111,73 @@ fun SettingsLayout( } val theme = CurrentColors.collectAsState() val uriHandler = LocalUriHandler.current - Box(Modifier.fillMaxSize()) { - ColumnWithScrollBar( - Modifier - .fillMaxSize() - .themedBackground(theme.value.base) - .padding(top = if (appPlatform.isAndroid) DEFAULT_PADDING else DEFAULT_PADDING * 2.8f) - ) { - AppBarTitle(stringResource(MR.strings.your_settings)) + ColumnWithScrollBar( + Modifier + .fillMaxSize() + .themedBackground(theme.value.base) + ) { + AppBarTitle(stringResource(MR.strings.your_settings)) - SectionView(stringResource(MR.strings.settings_section_title_you)) { - val profileHidden = rememberSaveable { mutableStateOf(false) } - if (profile != null) { - SectionItemView(showCustomModal { chatModel, close -> UserProfileView(chatModel, close) }, 80.dp, padding = PaddingValues(start = 16.dp, end = DEFAULT_PADDING), disabled = stopped) { - ProfilePreview(profile, stopped = stopped) - } - SettingsActionItem(painterResource(MR.images.ic_manage_accounts), stringResource(MR.strings.your_chat_profiles), { withAuth(generalGetString(MR.strings.auth_open_chat_profiles), generalGetString(MR.strings.auth_log_in_using_credential)) { showSettingsModalWithSearch { it, search -> UserProfilesView(it, search, profileHidden, drawerState) } } }, disabled = stopped, extraPadding = true) - SettingsActionItem(painterResource(MR.images.ic_qr_code), stringResource(MR.strings.your_simplex_contact_address), showCustomModal { it, close -> UserAddressView(it, shareViaProfile = it.currentUser.value!!.addressShared, close = close) }, disabled = stopped, extraPadding = true) - ChatPreferencesItem(showCustomModal, stopped = stopped) - } else if (chatModel.localUserCreated.value == false) { - SettingsActionItem(painterResource(MR.images.ic_manage_accounts), stringResource(MR.strings.create_chat_profile), { withAuth(generalGetString(MR.strings.auth_open_chat_profiles), generalGetString(MR.strings.auth_log_in_using_credential)) { ModalManager.center.showModalCloseable { close -> - LaunchedEffect(Unit) { - closeSettings() + SectionView(stringResource(MR.strings.settings_section_title_you)) { + val profileHidden = rememberSaveable { mutableStateOf(false) } + if (profile != null) { + SectionItemView(showCustomModal { chatModel, close -> UserProfileView(chatModel, close) }, 80.dp, padding = PaddingValues(start = 16.dp, end = DEFAULT_PADDING), disabled = stopped) { + ProfilePreview(profile, stopped = stopped) + } + SettingsActionItem(painterResource(MR.images.ic_manage_accounts), stringResource(MR.strings.your_chat_profiles), { withAuth(generalGetString(MR.strings.auth_open_chat_profiles), generalGetString(MR.strings.auth_log_in_using_credential)) { showSettingsModalWithSearch { it, search -> UserProfilesView(it, search, profileHidden, drawerState) } } }, disabled = stopped, extraPadding = true) + SettingsActionItem(painterResource(MR.images.ic_qr_code), stringResource(MR.strings.your_simplex_contact_address), showCustomModal { it, close -> UserAddressView(it, shareViaProfile = it.currentUser.value!!.addressShared, close = close) }, disabled = stopped, extraPadding = true) + ChatPreferencesItem(showCustomModal, stopped = stopped) + } else if (chatModel.localUserCreated.value == false) { + SettingsActionItem(painterResource(MR.images.ic_manage_accounts), stringResource(MR.strings.create_chat_profile), { + withAuth(generalGetString(MR.strings.auth_open_chat_profiles), generalGetString(MR.strings.auth_log_in_using_credential)) { + ModalManager.center.showModalCloseable { close -> + LaunchedEffect(Unit) { + closeSettings() + } + CreateProfile(chatModel, close) } - CreateProfile(chatModel, close) - } } }, disabled = stopped, extraPadding = true) - } - if (appPlatform.isDesktop) { - SettingsActionItem(painterResource(MR.images.ic_smartphone), stringResource(if (remember { chatModel.remoteHosts }.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles), showModal { ConnectMobileView() }, disabled = stopped, extraPadding = true) - } else { - SettingsActionItem(painterResource(MR.images.ic_desktop), stringResource(MR.strings.settings_section_title_use_from_desktop), showCustomModal{ it, close -> ConnectDesktopView(close) }, disabled = stopped, extraPadding = true) - } - SettingsActionItem(painterResource(MR.images.ic_ios_share), stringResource(MR.strings.migrate_from_device_to_another_device), { withAuth(generalGetString(MR.strings.auth_open_migration_to_another_device), generalGetString(MR.strings.auth_log_in_using_credential)) { ModalManager.fullscreen.showCustomModal { close -> MigrateFromDeviceView(close) } }}, disabled = stopped, extraPadding = true) + } + }, disabled = stopped, extraPadding = true) } - SectionDividerSpaced() - - SectionView(stringResource(MR.strings.settings_section_title_settings)) { - SettingsActionItem(painterResource(if (notificationsMode.value == NotificationsMode.OFF) MR.images.ic_bolt_off else MR.images.ic_bolt), stringResource(MR.strings.notifications), showSettingsModal { NotificationsSettingsView(it) }, disabled = stopped, extraPadding = true) - SettingsActionItem(painterResource(MR.images.ic_wifi_tethering), stringResource(MR.strings.network_and_servers), showSettingsModal { NetworkAndServersView() }, disabled = stopped, extraPadding = true) - SettingsActionItem(painterResource(MR.images.ic_videocam), stringResource(MR.strings.settings_audio_video_calls), showSettingsModal { CallSettingsView(it, showModal) }, disabled = stopped, extraPadding = true) - SettingsActionItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.privacy_and_security), showSettingsModal { PrivacySettingsView(it, showSettingsModal, setPerformLA) }, disabled = stopped, extraPadding = true) - SettingsActionItem(painterResource(MR.images.ic_light_mode), stringResource(MR.strings.appearance_settings), showSettingsModal { AppearanceView(it) }, extraPadding = true) - DatabaseItem(encrypted, passphraseSaved, showSettingsModal { DatabaseView(it, showSettingsModal) }, stopped) + if (appPlatform.isDesktop) { + SettingsActionItem(painterResource(MR.images.ic_smartphone), stringResource(if (remember { chatModel.remoteHosts }.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles), showModal { ConnectMobileView() }, disabled = stopped, extraPadding = true) + } else { + SettingsActionItem(painterResource(MR.images.ic_desktop), stringResource(MR.strings.settings_section_title_use_from_desktop), showCustomModal { it, close -> ConnectDesktopView(close) }, disabled = stopped, extraPadding = true) } - SectionDividerSpaced() - - SectionView(stringResource(MR.strings.settings_section_title_help)) { - SettingsActionItem(painterResource(MR.images.ic_help), stringResource(MR.strings.how_to_use_simplex_chat), showModal { HelpView(userDisplayName ?: "") }, disabled = stopped, extraPadding = true) - SettingsActionItem(painterResource(MR.images.ic_add), stringResource(MR.strings.whats_new), showCustomModal { _, close -> WhatsNewView(viaSettings = true, close) }, disabled = stopped, extraPadding = true) - SettingsActionItem(painterResource(MR.images.ic_info), stringResource(MR.strings.about_simplex_chat), showModal { SimpleXInfo(it, onboarding = false) }, extraPadding = true) - if (!chatModel.desktopNoUserNoRemote) { - SettingsActionItem(painterResource(MR.images.ic_tag), stringResource(MR.strings.chat_with_the_founder), { uriHandler.openVerifiedSimplexUri(simplexTeamUri) }, textColor = MaterialTheme.colors.primary, disabled = stopped, extraPadding = true) - } - SettingsActionItem(painterResource(MR.images.ic_mail), stringResource(MR.strings.send_us_an_email), { uriHandler.openUriCatching("mailto:chat@simplex.chat") }, textColor = MaterialTheme.colors.primary, extraPadding = true) - } - SectionDividerSpaced() - - SectionView(stringResource(MR.strings.settings_section_title_support)) { - ContributeItem(uriHandler) - RateAppItem(uriHandler) - StarOnGithubItem(uriHandler) - } - SectionDividerSpaced() - - SettingsSectionApp(showSettingsModal, showCustomModal, showVersion, withAuth) - SectionBottomSpacer() + SettingsActionItem(painterResource(MR.images.ic_ios_share), stringResource(MR.strings.migrate_from_device_to_another_device), { withAuth(generalGetString(MR.strings.auth_open_migration_to_another_device), generalGetString(MR.strings.auth_log_in_using_credential)) { ModalManager.fullscreen.showCustomModal { close -> MigrateFromDeviceView(close) } } }, disabled = stopped, extraPadding = true) } - if (appPlatform.isDesktop) { - Box( - Modifier - .fillMaxWidth() - .height(AppBarHeight * fontSizeSqrtMultiplier) - .background(MaterialTheme.colors.background) - .background(if (isInDarkTheme()) ToolbarDark else ToolbarLight) - .padding(start = 4.dp), - contentAlignment = Alignment.CenterStart - ) { - NavigationButtonBack(closeSettings, height = 24.sp.toDp()) - } + SectionDividerSpaced() + + SectionView(stringResource(MR.strings.settings_section_title_settings)) { + SettingsActionItem(painterResource(if (notificationsMode.value == NotificationsMode.OFF) MR.images.ic_bolt_off else MR.images.ic_bolt), stringResource(MR.strings.notifications), showSettingsModal { NotificationsSettingsView(it) }, disabled = stopped, extraPadding = true) + SettingsActionItem(painterResource(MR.images.ic_wifi_tethering), stringResource(MR.strings.network_and_servers), showSettingsModal { NetworkAndServersView() }, disabled = stopped, extraPadding = true) + SettingsActionItem(painterResource(MR.images.ic_videocam), stringResource(MR.strings.settings_audio_video_calls), showSettingsModal { CallSettingsView(it, showModal) }, disabled = stopped, extraPadding = true) + SettingsActionItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.privacy_and_security), showSettingsModal { PrivacySettingsView(it, showSettingsModal, setPerformLA) }, disabled = stopped, extraPadding = true) + SettingsActionItem(painterResource(MR.images.ic_light_mode), stringResource(MR.strings.appearance_settings), showSettingsModal { AppearanceView(it) }, extraPadding = true) + DatabaseItem(encrypted, passphraseSaved, showSettingsModal { DatabaseView(it, showSettingsModal) }, stopped) } + SectionDividerSpaced() + + SectionView(stringResource(MR.strings.settings_section_title_help)) { + SettingsActionItem(painterResource(MR.images.ic_help), stringResource(MR.strings.how_to_use_simplex_chat), showModal { HelpView(userDisplayName ?: "") }, disabled = stopped, extraPadding = true) + SettingsActionItem(painterResource(MR.images.ic_add), stringResource(MR.strings.whats_new), showCustomModal { _, close -> WhatsNewView(viaSettings = true, close) }, disabled = stopped, extraPadding = true) + SettingsActionItem(painterResource(MR.images.ic_info), stringResource(MR.strings.about_simplex_chat), showModal { SimpleXInfo(it, onboarding = false) }, extraPadding = true) + if (!chatModel.desktopNoUserNoRemote) { + SettingsActionItem(painterResource(MR.images.ic_tag), stringResource(MR.strings.chat_with_the_founder), { uriHandler.openVerifiedSimplexUri(simplexTeamUri) }, textColor = MaterialTheme.colors.primary, disabled = stopped, extraPadding = true) + } + SettingsActionItem(painterResource(MR.images.ic_mail), stringResource(MR.strings.send_us_an_email), { uriHandler.openUriCatching("mailto:chat@simplex.chat") }, textColor = MaterialTheme.colors.primary, extraPadding = true) + } + SectionDividerSpaced() + + SectionView(stringResource(MR.strings.settings_section_title_support)) { + ContributeItem(uriHandler) + RateAppItem(uriHandler) + StarOnGithubItem(uriHandler) + } + SectionDividerSpaced() + + SettingsSectionApp(showSettingsModal, showCustomModal, showVersion, withAuth) + SectionBottomSpacer() } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressLearnMore.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressLearnMore.kt index 63baece5cf..1ac0cd7ecd 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressLearnMore.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressLearnMore.kt @@ -13,11 +13,12 @@ import chat.simplex.res.MR @Composable fun UserAddressLearnMore() { - ColumnWithScrollBar(Modifier - .fillMaxHeight() - .padding(horizontal = DEFAULT_PADDING) + ColumnWithScrollBar( + Modifier + .fillMaxHeight() + .padding(horizontal = DEFAULT_PADDING) ) { - AppBarTitle(stringResource(MR.strings.simplex_address)) + AppBarTitle(stringResource(MR.strings.simplex_address), withPadding = false) ReadableText(MR.strings.you_can_share_your_address) ReadableText(MR.strings.you_wont_lose_your_contacts_if_delete_address) ReadableText(MR.strings.you_can_accept_or_reject_connection) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt index b595bd4e0e..e6d3202cd0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt @@ -174,7 +174,7 @@ private fun UserAddressLayout( saveAas: (AutoAcceptState, MutableState) -> Unit, ) { ColumnWithScrollBar { - AppBarTitle(stringResource(MR.strings.simplex_address), hostDevice(user?.remoteHostId), withPadding = false) + AppBarTitle(stringResource(MR.strings.simplex_address), hostDevice(user?.remoteHostId)) Column( Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING_HALF), horizontalAlignment = Alignment.CenterHorizontally, diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Resources.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Resources.desktop.kt index 9d39753f23..951185dc98 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Resources.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Resources.desktop.kt @@ -77,6 +77,9 @@ actual fun windowOrientation(): WindowOrientation = @Composable actual fun windowWidth(): Dp = simplexWindowState.windowState.size.width +@Composable +actual fun windowHeight(): Dp = simplexWindowState.windowState.size.height + actual fun desktopExpandWindowToWidth(width: Dp) { if (simplexWindowState.windowState.size.width >= width) return simplexWindowState.windowState.size = simplexWindowState.windowState.size.copy(width = width) 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 91e9c70a20..e6b26f9290 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 @@ -13,16 +13,18 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment 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 chat.simplex.common.views.helpers.detectCursorMove -import chat.simplex.common.views.helpers.mixWith +import chat.simplex.common.views.helpers.* import kotlinx.coroutines.* +import kotlinx.coroutines.flow.filter +import kotlin.math.absoluteValue @Composable actual fun LazyColumnWithScrollBar( modifier: Modifier, - state: LazyListState, + state: LazyListState?, contentPadding: PaddingValues, reverseLayout: Boolean, verticalArrangement: Arrangement.Vertical, @@ -30,44 +32,6 @@ actual fun LazyColumnWithScrollBar( flingBehavior: FlingBehavior, userScrollEnabled: Boolean, content: LazyListScope.() -> Unit -) { - if (appPlatform.isAndroid) { - LazyColumn(modifier, state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content) - } else { - val scope = rememberCoroutineScope() - val scrollBarAlpha = remember { Animatable(0f) } - val scrollJob: MutableState = remember { mutableStateOf(Job()) } - val scrollModifier = remember { - Modifier - .pointerInput(Unit) { - detectCursorMove { - scope.launch { - scrollBarAlpha.animateTo(1f) - } - scrollJob.value.cancel() - scrollJob.value = scope.launch { - delay(1000L) - scrollBarAlpha.animateTo(0f) - } - } - } - } - Box { - LazyColumn(modifier.then(if (appPlatform.isDesktop) scrollModifier else Modifier), state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content) - Box(Modifier.fillMaxSize(), contentAlignment = Alignment.CenterEnd) { - DesktopScrollBar(rememberScrollbarAdapter(state), Modifier.align(Alignment.CenterEnd).fillMaxHeight(), scrollBarAlpha, scrollJob, reverseLayout) - } - } - } -} - -@Composable -actual fun ColumnWithScrollBar( - modifier: Modifier, - verticalArrangement: Arrangement.Vertical, - horizontalAlignment: Alignment.Horizontal, - state: ScrollState, - content: @Composable ColumnScope.() -> Unit ) { val scope = rememberCoroutineScope() val scrollBarAlpha = remember { Animatable(0f) } @@ -87,20 +51,95 @@ actual fun ColumnWithScrollBar( } } } - Column(modifier.verticalScroll(state).then(scrollModifier), verticalArrangement, horizontalAlignment, content) - Box(Modifier.fillMaxSize(), contentAlignment = Alignment.CenterEnd) { - DesktopScrollBar(rememberScrollbarAdapter(state), Modifier.align(Alignment.CenterEnd).fillMaxHeight(), scrollBarAlpha, scrollJob, false) + val state = state ?: LocalAppBarHandler.current?.listState ?: rememberLazyListState() + val connection = LocalAppBarHandler.current?.connection + // When scroll bar is dragging, there is no scroll event in nested scroll modifier. So, listen for changes on lazy column state + // (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 (offset != null && ((offset + scrollPosition).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) + } } } @Composable -fun DesktopScrollBar(adapter: androidx.compose.foundation.v2.ScrollbarAdapter, modifier: Modifier, scrollBarAlpha: Animatable, scrollJob: MutableState, reversed: Boolean) { +actual fun ColumnWithScrollBar( + modifier: Modifier, + verticalArrangement: Arrangement.Vertical, + horizontalAlignment: Alignment.Horizontal, + state: ScrollState?, + maxIntrinsicSize: Boolean, + content: @Composable() (ColumnScope.() -> Unit) +) { + val scope = rememberCoroutineScope() + val scrollBarAlpha = remember { Animatable(0f) } + val scrollJob: MutableState = remember { mutableStateOf(Job()) } + val scrollModifier = remember { + Modifier + .pointerInput(Unit) { + detectCursorMove { + scope.launch { + scrollBarAlpha.animateTo(1f) + } + scrollJob.value.cancel() + scrollJob.value = scope.launch { + delay(1000L) + scrollBarAlpha.animateTo(0f) + } + } + } + } + val state = state ?: LocalAppBarHandler.current?.scrollState ?: rememberScrollState() + val connection = LocalAppBarHandler.current?.connection + // When scroll bar is dragging, there is no scroll event in nested scroll modifier. So, listen for changes on column state + // (exact scroll position is available but in Int, not Float) + val scrollBarDraggingState = remember { mutableStateOf(false) } + LaunchedEffect(Unit) { + snapshotFlow { state.value } + .collect { scrollPosition -> + val offset = connection?.appBarOffset + if (offset != null && ((offset + scrollPosition).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) { + Column( + if (maxIntrinsicSize) { + modifier.verticalScroll(state).height(IntrinsicSize.Max).then(scrollModifier) + } else { + modifier.verticalScroll(state).then(scrollModifier) + }, + verticalArrangement, horizontalAlignment, content) + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.CenterEnd) { + DesktopScrollBar(rememberScrollbarAdapter(state), Modifier.fillMaxHeight(), scrollBarAlpha, scrollJob, false, scrollBarDraggingState) + } + } +} + +@Composable +fun DesktopScrollBar(adapter: androidx.compose.foundation.v2.ScrollbarAdapter, modifier: Modifier, scrollBarAlpha: Animatable, scrollJob: MutableState, reversed: Boolean, updateDraggingState: MutableState = remember { mutableStateOf(false) }) { val scope = rememberCoroutineScope() val interactionSource = remember { MutableInteractionSource() } val isHovered by interactionSource.collectIsHoveredAsState() val isDragged by interactionSource.collectIsDraggedAsState() LaunchedEffect(isHovered, isDragged) { scrollJob.value.cancel() + updateDraggingState.value = isDragged if (isHovered || isDragged) { scrollBarAlpha.animateTo(1f) } else { diff --git a/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt b/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt index 8e1643571b..f0679c0fa1 100644 --- a/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt +++ b/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt @@ -48,38 +48,6 @@ private fun initHaskell() { initHS() platform = object: PlatformInterface { - @Composable - override fun desktopScrollBarComponents(): Triple, Modifier, MutableState> { - val scope = rememberCoroutineScope() - val scrollBarAlpha = remember { Animatable(0f) } - val scrollJob: MutableState = remember { mutableStateOf(Job()) } - val modifier = remember { - Modifier.pointerInput(Unit) { - detectCursorMove { - scope.launch { - scrollBarAlpha.animateTo(1f) - } - scrollJob.value.cancel() - scrollJob.value = scope.launch { - delay(1000L) - scrollBarAlpha.animateTo(0f) - } - } - } - } - return Triple(scrollBarAlpha, modifier, scrollJob) - } - - @Composable - override fun desktopScrollBar(state: LazyListState, modifier: Modifier, scrollBarAlpha: Animatable, scrollJob: MutableState, reversed: Boolean) { - DesktopScrollBar(rememberScrollbarAdapter(scrollState = state), modifier, scrollBarAlpha, scrollJob, reversed) - } - - @Composable - override fun desktopScrollBar(state: ScrollState, modifier: Modifier, scrollBarAlpha: Animatable, scrollJob: MutableState, reversed: Boolean) { - DesktopScrollBar(rememberScrollbarAdapter(scrollState = state), modifier, scrollBarAlpha, scrollJob, reversed) - } - @Composable override fun desktopShowAppUpdateNotice() { fun showNoticeIfNeeded() {