From 24090fe3500ed596c0d8610b91e59c8037a4a7d1 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Fri, 1 Nov 2024 00:11:26 +0700 Subject: [PATCH 1/2] android, desktop: update to Compose 1.7.0 (#5038) * docs: correction * android, desktop: update to Compose 1.7.0 - support image drag-and-drop from other applications right to a chat (with and without transparent pixels - will be png or jpg) * stable * workaround --------- Co-authored-by: Evgeny Poberezkin --- .../common/platform/Modifier.android.kt | 2 +- .../platform/PlatformTextField.android.kt | 11 ++- .../common/views/call/CallView.android.kt | 4 +- .../views/chatlist/ChatListView.android.kt | 7 +- .../helpers/WorkaroundFocusSearchLayout.kt | 41 +++++++++++ .../chat/simplex/common/platform/Modifier.kt | 2 +- .../chat/simplex/common/views/WelcomeView.kt | 1 + .../simplex/common/views/chat/ChatView.kt | 9 +-- .../simplex/common/views/chat/SendMsgView.kt | 7 +- .../views/helpers/DefaultBasicTextField.kt | 3 - .../helpers/ExposedDropDownSettingRow.kt | 4 +- .../common/views/helpers/TextEditor.kt | 1 + .../common/views/newchat/NewChatView.kt | 1 + .../simplex/common/platform/Images.desktop.kt | 32 +++++++++ .../common/platform/Modifier.desktop.kt | 72 ++++++++++++++++--- .../platform/PlatformTextField.desktop.kt | 4 +- .../chatlist/ChatListNavLinkView.desktop.kt | 18 ++--- .../common/views/helpers/Utils.desktop.kt | 4 +- apps/multiplatform/gradle.properties | 4 +- 19 files changed, 176 insertions(+), 51 deletions(-) create mode 100644 apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/WorkaroundFocusSearchLayout.kt diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Modifier.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Modifier.android.kt index 2ff2a3e021..2aa66bc69b 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Modifier.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Modifier.android.kt @@ -21,7 +21,7 @@ actual fun ProvideWindowInsets( actual fun Modifier.desktopOnExternalDrag( enabled: Boolean, onFiles: (List) -> Unit, - onImage: (Painter) -> Unit, + onImage: (File) -> Unit, onText: (String) -> Unit ): Modifier = this diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/PlatformTextField.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/PlatformTextField.android.kt index 0b17a3aadf..5365db6a4c 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/PlatformTextField.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/PlatformTextField.android.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.core.graphics.drawable.DrawableCompat +import androidx.core.view.children import androidx.core.view.inputmethod.EditorInfoCompat import androidx.core.view.inputmethod.InputConnectionCompat import androidx.core.widget.doAfterTextChanged @@ -94,8 +95,8 @@ actual fun PlatformTextField( } val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl - AndroidView(modifier = Modifier, factory = { - val editText = @SuppressLint("AppCompatCustomView") object: EditText(it) { + AndroidView(modifier = Modifier, factory = { context -> + val editText = @SuppressLint("AppCompatCustomView") object: EditText(context) { override fun setOnReceiveContentListener( mimeTypes: Array?, listener: OnReceiveContentListener? @@ -148,8 +149,12 @@ actual fun PlatformTextField( } } editText.doAfterTextChanged { text -> if (composeState.value.preview is ComposePreview.VoicePreview && text.toString() != "") editText.setText("") } - editText + val workaround = WorkaroundFocusSearchLayout(context) + workaround.addView(editText) + workaround.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + workaround }) { + val it = it.children.first() as EditText it.setTextColor(textColor.toArgb()) it.setHintTextColor(hintColor.toArgb()) it.hint = placeholder diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt index 3fc5620222..f3a9be6132 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt @@ -22,7 +22,6 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* -import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.snapshots.SnapshotStateList @@ -411,6 +410,7 @@ private fun ControlButton(icon: Painter, iconText: StringResource, enabled: Bool @Composable private fun ControlButtonWrap(enabled: Boolean = true, action: () -> Unit, background: Color = controlButtonsBackground(), size: Dp, content: @Composable () -> Unit) { + val ripple = remember { ripple(bounded = false, radius = size / 2, color = background.lighter(0.1f)) } Box( Modifier .background(background, CircleShape) @@ -419,7 +419,7 @@ private fun ControlButtonWrap(enabled: Boolean = true, action: () -> Unit, backg onClick = action, role = Role.Button, interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(bounded = false, radius = size / 2, color = background.lighter(0.1f)), + indication = ripple, enabled = enabled ), contentAlignment = Alignment.Center diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.android.kt index e0fd81f7b6..4681a5a64d 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.android.kt @@ -6,7 +6,6 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.* -import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -40,8 +39,8 @@ actual fun ActiveCallInteractiveArea(call: Call) { val onClick = { platform.androidStartCallActivity(false) } Box(Modifier.offset(y = CALL_TOP_OFFSET).height(CALL_INTERACTIVE_AREA_HEIGHT)) { val source = remember { MutableInteractionSource() } - val indication = rememberRipple(bounded = true, 3000.dp) - Box(Modifier.height(CALL_TOP_GREEN_LINE_HEIGHT).clickable(onClick = onClick, indication = indication, interactionSource = source)) { + val ripple = remember { ripple(bounded = true, 3000.dp) } + Box(Modifier.height(CALL_TOP_GREEN_LINE_HEIGHT).clickable(onClick = onClick, indication = ripple, interactionSource = source)) { GreenLine(call) } Box( @@ -50,7 +49,7 @@ actual fun ActiveCallInteractiveArea(call: Call) { .size(CALL_BOTTOM_ICON_HEIGHT) .background(SimplexGreen, CircleShape) .clip(CircleShape) - .clickable(onClick = onClick, indication = indication, interactionSource = source) + .clickable(onClick = onClick, indication = ripple, interactionSource = source) .align(Alignment.BottomCenter), contentAlignment = Alignment.Center ) { diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/WorkaroundFocusSearchLayout.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/WorkaroundFocusSearchLayout.kt new file mode 100644 index 0000000000..d111b99385 --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/WorkaroundFocusSearchLayout.kt @@ -0,0 +1,41 @@ +package chat.simplex.common.views.helpers + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.FrameLayout + +/** + * A workaround for the ANR issue on Compose 1.7.x. + * https://issuetracker.google.com/issues/369354336 + * Code from: + * https://issuetracker.google.com/issues/369354336#comment8 +*/ +class WorkaroundFocusSearchLayout : FrameLayout { + + constructor( + context: Context, + ) : super(context) + + constructor( + context: Context, + attrs: AttributeSet?, + ) : super(context, attrs) + + constructor( + context: Context, + attrs: AttributeSet?, + defStyleAttr: Int, + ) : super(context, attrs, defStyleAttr) + + constructor( + context: Context, + attrs: AttributeSet?, + defStyleAttr: Int, + defStyleRes: Int, + ) : super(context, attrs, defStyleAttr, defStyleRes) + + override fun focusSearch(focused: View?, direction: Int): View? { + return null + } +} \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Modifier.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Modifier.kt index 6683ea7d33..5281fcb1ad 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Modifier.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Modifier.kt @@ -27,7 +27,7 @@ expect fun ProvideWindowInsets( expect fun Modifier.desktopOnExternalDrag( enabled: Boolean = true, onFiles: (List) -> Unit = {}, - onImage: (Painter) -> Unit = {}, + onImage: (File) -> Unit = {}, onText: (String) -> Unit = {} ): Modifier diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt index f5dcc6b54a..0d5350dbe0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt @@ -289,6 +289,7 @@ fun ProfileNameField(name: MutableState, placeholder: String = "", isVal enabled = true, isError = false, interactionSource = remember { MutableInteractionSource() }, + colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Unspecified) ) } ) 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 d89782148a..570f763e99 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 @@ -642,14 +642,7 @@ fun ChatLayout( .desktopOnExternalDrag( enabled = remember(attachmentDisabled.value, chatInfo.value?.userCanSend) { mutableStateOf(!attachmentDisabled.value && chatInfo.value?.userCanSend == true) }.value, onFiles = { paths -> composeState.onFilesAttached(paths.map { it.toURI() }) }, - onImage = { - // TODO: file is not saved anywhere?! - val tmpFile = File.createTempFile("image", ".bmp", tmpDir) - tmpFile.deleteOnExit() - chatModel.filesToDelete.add(tmpFile) - val uri = tmpFile.toURI() - CoroutineScope(Dispatchers.IO).launch { composeState.processPickedMedia(listOf(uri), null) } - }, + onImage = { file -> CoroutineScope(Dispatchers.IO).launch { composeState.processPickedMedia(listOf(file.toURI()), null) } }, onText = { // Need to parse HTML in order to correctly display the content //composeState.value = composeState.value.copy(message = composeState.value.message + it) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt index 162e753b18..76c4fc4a62 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt @@ -7,7 +7,6 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.* import androidx.compose.material.* -import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.* @@ -423,6 +422,7 @@ private fun SendMsgButton( onLongClick: (() -> Unit)? = null ) { val interactionSource = remember { MutableInteractionSource() } + val ripple = remember { ripple(bounded = false, radius = 24.dp) } Box( modifier = Modifier.requiredSize(36.dp) .combinedClickable( @@ -431,7 +431,7 @@ private fun SendMsgButton( enabled = enabled, role = Role.Button, interactionSource = interactionSource, - indication = rememberRipple(bounded = false, radius = 24.dp) + indication = ripple ) .onRightClick { onLongClick?.invoke() }, contentAlignment = Alignment.Center @@ -454,6 +454,7 @@ private fun SendMsgButton( @Composable private fun StartLiveMessageButton(enabled: Boolean, onClick: () -> Unit) { val interactionSource = remember { MutableInteractionSource() } + val ripple = remember { ripple(bounded = false, radius = 24.dp) } Box( modifier = Modifier.requiredSize(36.dp) .clickable( @@ -461,7 +462,7 @@ private fun StartLiveMessageButton(enabled: Boolean, onClick: () -> Unit) { enabled = enabled, role = Role.Button, interactionSource = interactionSource, - indication = rememberRipple(bounded = false, radius = 24.dp) + indication = ripple ), contentAlignment = Alignment.Center ) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultBasicTextField.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultBasicTextField.kt index a6f0d2c9b6..b0366cceb3 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultBasicTextField.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultBasicTextField.kt @@ -3,7 +3,6 @@ package chat.simplex.common.views.helpers import androidx.compose.foundation.background import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.defaultMinSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.shape.ZeroCornerSize import androidx.compose.foundation.text.* import androidx.compose.material.* @@ -22,13 +21,11 @@ import androidx.compose.ui.text.input.* import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.* import chat.simplex.common.views.database.PassphraseStrength -import chat.simplex.common.views.database.validKey import chat.simplex.res.MR import kotlinx.coroutines.delay import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch -@OptIn(ExperimentalComposeUiApi::class) @Composable fun DefaultBasicTextField( modifier: Modifier, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ExposedDropDownSettingRow.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ExposedDropDownSettingRow.kt index 8349841973..7ed91adbd9 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ExposedDropDownSettingRow.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ExposedDropDownSettingRow.kt @@ -6,7 +6,6 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.* -import androidx.compose.material.ripple.rememberRipple import dev.icerock.moko.resources.compose.painterResource import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -107,6 +106,7 @@ fun ExposedDropDownSettingWithIcon( expanded.value = !expanded.value && enabled.value } ) { + val ripple = remember { ripple(bounded = false, radius = boxSize / 2, color = background.lighter(0.1f)) } Box( Modifier .background(background, CircleShape) @@ -115,7 +115,7 @@ fun ExposedDropDownSettingWithIcon( onClick = {}, role = Role.Button, interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(bounded = false, radius = boxSize / 2, color = background.lighter(0.1f)), + indication = ripple, enabled = enabled.value ), contentAlignment = Alignment.Center diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/TextEditor.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/TextEditor.kt index 45accccc59..ab7e562697 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/TextEditor.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/TextEditor.kt @@ -87,6 +87,7 @@ fun TextEditor( enabled = true, isError = false, interactionSource = remember { MutableInteractionSource() }, + colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Unspecified) ) } ) 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 8acddc2aa6..5298e11e75 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 @@ -616,6 +616,7 @@ fun LinkTextView(link: String, share: Boolean) { enabled = false, isError = false, interactionSource = remember { MutableInteractionSource() }, + colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Unspecified) ) }) } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt index e65adea70e..0f53adaf0b 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt @@ -206,3 +206,35 @@ fun BufferedImage.flip(vertically: Boolean, horizontally: Boolean): BufferedImag } return AffineTransformOp(tx, AffineTransformOp.TYPE_NEAREST_NEIGHBOR).filter(this, null) } + +fun BufferedImage.saveInTmpFile(): File? { + val formats = arrayOf("jpg", "png") + for (format in formats) { + val tmpFile = File.createTempFile("image", ".$format", tmpDir) + try { + // May fail on JPG, using PNG as an alternative + val success = ImageIO.write(this, format, tmpFile) + if (success) { + tmpFile.deleteOnExit() + chatModel.filesToDelete.add(tmpFile) + return tmpFile + } else { + tmpFile.delete() + } + } catch (e: Exception) { + Log.e(TAG, e.stackTraceToString()) + tmpFile.delete() + return null + } + } + return null +} + +fun BufferedImage.hasAlpha(): Boolean { + for (x in 0 until width) { + for (y in 0 until height) { + if (getRGB(x, y) == 0) return true + } + } + return false +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Modifier.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Modifier.desktop.kt index 97f8bc129a..150885cbc8 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Modifier.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Modifier.desktop.kt @@ -1,10 +1,17 @@ package chat.simplex.common.platform import androidx.compose.foundation.contextMenuOpenDetector +import androidx.compose.foundation.draganddrop.dragAndDropTarget import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.* -import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.draganddrop.* +import androidx.compose.ui.draganddrop.DragData import androidx.compose.ui.input.pointer.* +import java.awt.Image +import java.awt.datatransfer.DataFlavor +import java.awt.datatransfer.Transferable +import java.awt.image.BufferedImage import java.io.File import java.net.URI @@ -23,16 +30,61 @@ actual fun ProvideWindowInsets( actual fun Modifier.desktopOnExternalDrag( enabled: Boolean, onFiles: (List) -> Unit, - onImage: (Painter) -> Unit, + onImage: (File) -> Unit, onText: (String) -> Unit -): Modifier = -onExternalDrag(enabled) { - when(val data = it.dragData) { - // data.readFiles() returns filePath in URI format (where spaces replaces with %20). But it's an error-prone idea to work later - // with such format when everywhere we use absolutePath in File() format - is DragData.FilesList -> onFiles(data.readFiles().map { URI.create(it).toFile() }) - is DragData.Image -> onImage(data.readImage()) - is DragData.Text -> onText(data.readText()) +): Modifier { + val callback = remember { + object : DragAndDropTarget { + override fun onDrop(event: DragAndDropEvent): Boolean { + when (val data = event.dragData()) { + // data.readFiles() returns filePath in URI format (where spaces replaces with %20). But it's an error-prone idea to work later + // with such format when everywhere we use absolutePath in File() format + is DragData.FilesList -> { + val files = data.readFiles() + // When dragging and dropping an image from browser, it comes to FilesList section but no files inside + if (files.isNotEmpty()) { + onFiles(files.map { URI.create(it).toFile() }) + } else { + try { + val transferable = event.awtTransferable + if (transferable.isDataFlavorSupported(DataFlavor.imageFlavor)) { + onImage(DragDataImageImpl(transferable).bufferedImage().saveInTmpFile() ?: return false) + } else { + return false + } + } catch (e: Exception) { + Log.e(TAG, e.stackTraceToString()) + return false + } + } + } + is DragData.Image -> onImage(DragDataImageImpl(event.awtTransferable).bufferedImage().saveInTmpFile() ?: return false) + is DragData.Text -> onText(data.readText()) + } + return true + } + } + } + return dragAndDropTarget(shouldStartDragAndDrop = { true }, target = callback) +} + +// Copied from AwtDragData and modified +private class DragDataImageImpl(private val transferable: Transferable) { + fun bufferedImage(): BufferedImage = (transferable.getTransferData(DataFlavor.imageFlavor) as Image).bufferedImage() + private fun Image.bufferedImage(): BufferedImage { + if (this is BufferedImage && hasAlpha()) { + // Such image cannot be drawn as JPG, only PNG + return this + } + // Creating non-transparent image which can be drawn as JPG + val bufferedImage = BufferedImage(getWidth(null), getHeight(null), BufferedImage.TYPE_INT_RGB) + val g2 = bufferedImage.createGraphics() + try { + g2.drawImage(this, 0, 0, null) + } finally { + g2.dispose() + } + return bufferedImage } } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt index 5b0db7c94a..e37e99f3e9 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.input.key.* import androidx.compose.ui.platform.* @@ -109,7 +110,7 @@ actual fun PlatformTextField( maxLines = 16, keyboardOptions = KeyboardOptions.Default.copy( capitalization = KeyboardCapitalization.Sentences, - autoCorrect = true + autoCorrectEnabled = true ), modifier = Modifier .padding(vertical = 4.dp) @@ -193,6 +194,7 @@ actual fun PlatformTextField( interactionSource = remember { MutableInteractionSource() }, contentPadding = PaddingValues(), visualTransformation = VisualTransformation.None, + colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Unspecified) ) Spacer(Modifier.height(10.dp)) } 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..9789fa3d1a 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 @@ -10,20 +10,20 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.drawscope.ContentDrawScope +import androidx.compose.ui.node.DelegatableNode +import androidx.compose.ui.node.DrawModifierNode import androidx.compose.ui.unit.dp import chat.simplex.common.platform.onRightClick import chat.simplex.common.views.helpers.* -object NoIndication : Indication { - private object NoIndicationInstance : IndicationInstance { - override fun ContentDrawScope.drawIndication() { - drawContent() - } - } - @Composable - override fun rememberUpdatedInstance(interactionSource: InteractionSource): IndicationInstance { - return NoIndicationInstance +object NoIndication : IndicationNodeFactory { + // Should be as a class, not an object. Otherwise, crash + private class NoIndicationInstance : Modifier.Node(), DrawModifierNode { + override fun ContentDrawScope.draw() { drawContent() } } + override fun create(interactionSource: InteractionSource): DelegatableNode = NoIndicationInstance() + override fun hashCode(): Int = -1 + override fun equals(other: Any?) = other === this } @Composable diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt index 641ddb8744..d541a5780e 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt @@ -156,7 +156,9 @@ actual fun getFileSize(uri: URI): Long? = uri.toFile().length() actual fun getBitmapFromUri(uri: URI, withAlertOnException: Boolean): ImageBitmap? = try { - ImageIO.read(uri.inputStream()).toComposeImageBitmap() + uri.inputStream().use { + ImageIO.read(it).toComposeImageBitmap() + } } catch (e: Exception) { Log.e(TAG, "Error while decoding drawable: ${e.stackTraceToString()}") if (withAlertOnException) showImageDecodingException() diff --git a/apps/multiplatform/gradle.properties b/apps/multiplatform/gradle.properties index 32bd7579c8..d795257a76 100644 --- a/apps/multiplatform/gradle.properties +++ b/apps/multiplatform/gradle.properties @@ -21,8 +21,6 @@ kotlin.code.style=official # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library android.nonTransitiveRClass=true -# Automatically convert third-party libraries to use AndroidX -android.enableJetifier=true kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.jvm.target=11 @@ -34,4 +32,4 @@ desktop.version_code=74 kotlin.version=1.9.23 gradle.plugin.version=8.2.0 -compose.version=1.6.1 +compose.version=1.7.0 From 4162bccc468a011ec06be99aab8fee1753f75132 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Fri, 1 Nov 2024 00:26:17 +0700 Subject: [PATCH 2/2] multiplatform: edge to edge design (#5051) * multiplatform: insets * more features and better performance * calls and removed unused code * changes * removed logs * status and nav bar colors * chatList and newChatSheet search fields * overhaul * search fields, devtools, chatlist, newchatsheet, onehand on desktop, scrollbars * android, desktop: update to Compose 1.7.0 - support image drag-and-drop from other applications right to a chat (with and without transparent pixels - will be png or jpg) * stable * workaround * changes * ideal adapting height layout * dropdownmenu, userpicker, onehandui, call layout, columns * rename bars properties and strings * faster update and better layout * gallery in landscape with cutout * better cutout * 1% step on slider * app bar moves to bottom in one hand ui * default alpha * changes * userpicker colors * changes * blur * fix wrong drawing area in chatview * fix * fixed differently * changes * changes * android fix * Revert "android fix" This reverts commit 7d417afd9b011045b68921546c6218a0f97912aa. * changes * changes * blur * swap * no logs * fix build * old Android support * fix position of menu * disable blur on Android 12 * call button padding * useless code * fix padding in group info view * rename * rename * newline * one more fix * changes --------- Co-authored-by: Evgeny Poberezkin --- .../java/chat/simplex/app/MainActivity.kt | 14 +- .../main/java/chat/simplex/app/SimplexApp.kt | 92 +-- apps/multiplatform/common/build.gradle.kts | 1 - .../common/platform/Modifier.android.kt | 12 - .../platform/PlatformTextField.android.kt | 10 +- .../common/platform/Resources.android.kt | 10 +- .../platform/ScrollableColumn.android.kt | 159 ++++- .../simplex/common/platform/UI.android.kt | 30 +- .../common/views/call/CallView.android.kt | 19 +- .../views/chat/item/CIImageView.android.kt | 9 - .../views/chatlist/ChatListView.android.kt | 27 +- .../views/chatlist/UserPicker.android.kt | 133 ++-- .../views/helpers/GetImageView.android.kt | 2 + .../views/usersettings/Appearance.android.kt | 14 +- .../usersettings/SettingsView.android.kt | 3 +- .../kotlin/chat/simplex/common/App.kt | 119 ++-- .../chat/simplex/common/model/ChatModel.kt | 3 + .../chat/simplex/common/model/SimpleXAPI.kt | 9 +- .../chat/simplex/common/platform/Modifier.kt | 9 - .../chat/simplex/common/platform/Platform.kt | 5 +- .../common/platform/ScrollableColumn.kt | 34 + .../chat/simplex/common/ui/theme/Theme.kt | 36 +- .../simplex/common/ui/theme/ThemeManager.kt | 7 +- .../chat/simplex/common/views/TerminalView.kt | 147 ++--- .../chat/simplex/common/views/WelcomeView.kt | 95 ++- .../views/call/IncomingCallAlertView.kt | 2 +- .../simplex/common/views/chat/ChatInfoView.kt | 5 +- .../common/views/chat/ChatItemInfoView.kt | 8 +- .../simplex/common/views/chat/ChatView.kt | 419 ++++++------- .../simplex/common/views/chat/ComposeView.kt | 6 +- .../common/views/chat/ContactPreferences.kt | 5 +- .../simplex/common/views/chat/ScanCodeView.kt | 7 +- .../views/chat/SelectableChatItemToolbars.kt | 12 +- .../simplex/common/views/chat/SendMsgView.kt | 6 +- .../common/views/chat/VerifyCodeView.kt | 6 +- .../views/chat/group/AddGroupMembersView.kt | 5 +- .../views/chat/group/GroupChatInfoView.kt | 15 +- .../common/views/chat/group/GroupLinkView.kt | 4 +- .../views/chat/group/GroupMemberInfoView.kt | 5 +- .../views/chat/group/GroupPreferences.kt | 4 +- .../views/chat/group/GroupProfileView.kt | 8 +- .../views/chat/group/WelcomeMessageView.kt | 4 +- .../common/views/chat/item/FramedItemView.kt | 67 ++ .../views/chat/item/ImageFullScreenView.kt | 13 +- .../common/views/chatlist/ChatListView.kt | 393 +++++++----- .../views/chatlist/ServersSummaryView.kt | 20 +- .../common/views/chatlist/ShareListView.kt | 122 ++-- .../common/views/chatlist/UserPicker.kt | 22 +- .../common/views/database/ChatArchiveView.kt | 4 +- .../views/database/DatabaseEncryptionView.kt | 2 +- .../views/database/DatabaseErrorView.kt | 5 +- .../common/views/database/DatabaseView.kt | 4 +- .../common/views/helpers/AppBarTitle.kt | 71 +++ .../common/views/helpers/BlurModifier.kt | 139 +++++ .../common/views/helpers/ChatWallpaper.kt | 93 +-- .../views/helpers/ChooseAttachmentView.kt | 2 + .../common/views/helpers/CloseSheetBar.kt | 181 ------ .../common/views/helpers/CollapsingAppBar.kt | 56 +- .../views/helpers/DefaultDropdownMenu.kt | 3 +- .../common/views/helpers/DefaultTopAppBar.kt | 243 ++++++-- .../simplex/common/views/helpers/ModalView.kt | 60 +- .../common/views/helpers/SearchTextField.kt | 25 +- .../common/views/helpers/TextEditor.kt | 1 - .../common/views/helpers/ThemeModeEditor.kt | 10 +- .../views/migration/MigrateFromDevice.kt | 4 +- .../common/views/migration/MigrateToDevice.kt | 4 +- .../views/newchat/AddContactLearnMore.kt | 4 +- .../common/views/newchat/AddGroupView.kt | 16 +- .../newchat/ContactConnectionInfoView.kt | 6 +- .../common/views/newchat/NewChatSheet.kt | 583 ++++++++++-------- .../common/views/newchat/NewChatView.kt | 19 +- .../views/onboarding/CreateSimpleXAddress.kt | 8 +- .../common/views/onboarding/HowItWorks.kt | 6 +- .../views/onboarding/LinkAMobileView.kt | 55 +- .../views/onboarding/SetNotificationsMode.kt | 11 +- .../onboarding/SetupDatabasePassphrase.kt | 7 +- .../common/views/onboarding/SimpleXInfo.kt | 19 +- .../common/views/onboarding/WhatsNewView.kt | 3 +- .../common/views/remote/ConnectDesktopView.kt | 8 +- .../common/views/remote/ConnectMobileView.kt | 12 +- .../usersettings/AdvancedNetworkSettings.kt | 13 +- .../common/views/usersettings/Appearance.kt | 143 ++++- .../common/views/usersettings/CallSettings.kt | 2 +- .../views/usersettings/DeveloperView.kt | 10 +- .../common/views/usersettings/HelpView.kt | 6 +- .../views/usersettings/HiddenProfileView.kt | 5 +- .../views/usersettings/NetworkAndServers.kt | 11 +- .../usersettings/NotificationsSettingsView.kt | 12 +- .../common/views/usersettings/Preferences.kt | 4 +- .../views/usersettings/PrivacySettings.kt | 8 +- .../views/usersettings/ProtocolServerView.kt | 5 +- .../views/usersettings/ProtocolServersView.kt | 5 +- .../views/usersettings/ScanProtocolServer.kt | 6 +- .../usersettings/SetDeliveryReceiptsView.kt | 5 +- .../common/views/usersettings/SettingsView.kt | 20 +- .../usersettings/UserAddressLearnMore.kt | 6 +- .../views/usersettings/UserProfileView.kt | 6 +- .../views/usersettings/UserProfilesView.kt | 10 +- .../views/usersettings/VersionInfoView.kt | 4 +- .../commonMain/resources/MR/base/strings.xml | 3 + .../kotlin/chat/simplex/common/DesktopApp.kt | 15 +- .../common/platform/Modifier.desktop.kt | 11 - .../platform/PlatformTextField.desktop.kt | 40 +- .../platform/ScrollableColumn.desktop.kt | 210 ++++++- .../views/chatlist/ChatListView.desktop.kt | 179 ++++-- .../views/chatlist/UserPicker.desktop.kt | 10 +- .../views/usersettings/Appearance.desktop.kt | 13 +- .../usersettings/SettingsView.desktop.kt | 3 +- 108 files changed, 2651 insertions(+), 1935 deletions(-) create mode 100644 apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AppBarTitle.kt create mode 100644 apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/BlurModifier.kt delete mode 100644 apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CloseSheetBar.kt diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt index f29c0c3387..2d2829f1f2 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt @@ -4,8 +4,10 @@ import android.content.Context import android.content.Intent import android.net.Uri import android.os.* +import android.view.View import android.view.WindowManager import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge import androidx.compose.ui.platform.ClipboardManager import androidx.fragment.app.FragmentActivity import chat.simplex.app.model.NtfManager @@ -13,7 +15,6 @@ import chat.simplex.app.model.NtfManager.getUserIdFromIntent import chat.simplex.common.* import chat.simplex.common.helpers.* import chat.simplex.common.model.* -import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chatlist.* import chat.simplex.common.views.helpers.* @@ -24,13 +25,21 @@ import kotlinx.coroutines.* import java.lang.ref.WeakReference class MainActivity: FragmentActivity() { + companion object { + const val OLD_ANDROID_UI_FLAGS = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + } override fun onCreate(savedInstanceState: Bundle?) { mainActivity = WeakReference(this) platform.androidSetNightModeIfSupported() val c = CurrentColors.value.colors - platform.androidSetStatusAndNavBarColors(c.isLight, c.background, !appPrefs.oneHandUI.get(), appPrefs.oneHandUI.get()) + platform.androidSetStatusAndNavigationBarAppearance(c.isLight, c.isLight) applyAppLocale(ChatModel.controller.appPrefs.appLanguage) + // This flag makes status bar and navigation bar fully transparent. But on API level < 30 it breaks insets entirely + // https://issuetracker.google.com/issues/236862874 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + window.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS) + } super.onCreate(savedInstanceState) // testJson() // When call ended and orientation changes, it re-process old intent, it's unneeded. @@ -47,6 +56,7 @@ class MainActivity: FragmentActivity() { WindowManager.LayoutParams.FLAG_SECURE ) } + enableEdgeToEdge() setContent { AppScreen() } diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt index 40e8ffa9bc..13f9b888b9 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt @@ -7,6 +7,7 @@ import chat.simplex.common.platform.Log import android.content.Intent import android.content.pm.ActivityInfo import android.os.* +import android.view.View import androidx.compose.animation.core.* import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect @@ -16,6 +17,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.core.view.ViewCompat import androidx.lifecycle.* import androidx.work.* +import chat.simplex.app.MainActivity.Companion.OLD_ANDROID_UI_FLAGS import chat.simplex.app.model.NtfManager import chat.simplex.app.model.NtfManager.AcceptCallAction import chat.simplex.app.views.call.CallActivity @@ -26,7 +28,6 @@ import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.call.* -import chat.simplex.common.views.chatlist.statusBarColorAfterCall import chat.simplex.common.views.helpers.* import chat.simplex.common.views.onboarding.OnboardingStage import com.jakewharton.processphoenix.ProcessPhoenix @@ -274,79 +275,32 @@ class SimplexApp: Application(), LifecycleEventObserver { uiModeManager.setApplicationNightMode(mode) } - override fun androidSetDrawerStatusAndNavBarColor( - isLight: Boolean, - drawerShadingColor: Color, - toolbarOnTop: Boolean, - navBarColor: Color, - ) { - val window = mainActivity.get()?.window ?: return - - @Suppress("DEPRECATION") - val windowInsetController = ViewCompat.getWindowInsetsController(window.decorView) - // Blend status bar color to the animated color - val colors = CurrentColors.value.colors - val baseBackgroundColor = if (toolbarOnTop) colors.background.mixWith(colors.onBackground, 0.97f) else colors.background - var statusBar = baseBackgroundColor.mixWith(drawerShadingColor.copy(1f), 1 - drawerShadingColor.alpha).toArgb() - var statusBarLight = isLight - - // SimplexGreen while in call - if (window.statusBarColor == SimplexGreen.toArgb()) { - statusBarColorAfterCall.intValue = statusBar - statusBar = SimplexGreen.toArgb() - statusBarLight = false - } - window.statusBarColor = statusBar - val navBar = navBarColor.toArgb() - if (windowInsetController?.isAppearanceLightStatusBars != statusBarLight) { - windowInsetController?.isAppearanceLightStatusBars = statusBarLight - } - if (window.navigationBarColor != navBar) { - window.navigationBarColor = navBar - } - if (windowInsetController?.isAppearanceLightNavigationBars != isLight) { - windowInsetController?.isAppearanceLightNavigationBars = isLight - } - } - - override fun androidSetStatusAndNavBarColors(isLight: Boolean, backgroundColor: Color, hasTop: Boolean, hasBottom: Boolean) { + override fun androidSetStatusAndNavigationBarAppearance(isLightStatusBar: Boolean, isLightNavBar: Boolean, blackNavBar: Boolean, themeBackgroundColor: Color) { val window = mainActivity.get()?.window ?: return @Suppress("DEPRECATION") + val statusLight = isLightStatusBar && chatModel.activeCall.value == null + val navBarLight = isLightNavBar || windowOrientation() == WindowOrientation.LANDSCAPE val windowInsetController = ViewCompat.getWindowInsetsController(window.decorView) - - var statusBar = (if (hasTop && appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete) { - backgroundColor.mixWith(CurrentColors.value.colors.onBackground, 0.97f) - } else { - if (CurrentColors.value.base == DefaultTheme.SIMPLEX) { - backgroundColor.lighter(0.4f) + if (windowInsetController?.isAppearanceLightStatusBars != statusLight) { + windowInsetController?.isAppearanceLightStatusBars = statusLight + } + window.navigationBarColor = Color.Transparent.toArgb() + if (windowInsetController?.isAppearanceLightNavigationBars != navBarLight) { + windowInsetController?.isAppearanceLightNavigationBars = navBarLight + } + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + window.decorView.systemUiVisibility = if (statusLight && navBarLight) { + View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR or OLD_ANDROID_UI_FLAGS + } else if (statusLight) { + View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR or OLD_ANDROID_UI_FLAGS + } else if (navBarLight) { + View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR or OLD_ANDROID_UI_FLAGS } else { - backgroundColor + OLD_ANDROID_UI_FLAGS } - }).toArgb() - var statusBarLight = isLight - - // SimplexGreen while in call - if (window.statusBarColor == SimplexGreen.toArgb()) { - statusBarColorAfterCall.intValue = statusBar - statusBar = SimplexGreen.toArgb() - statusBarLight = false - } - val navBar = (if (hasBottom && appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete) { - backgroundColor.mixWith(CurrentColors.value.colors.onBackground, 0.97f) + window.navigationBarColor = if (blackNavBar) Color.Black.toArgb() else themeBackgroundColor.toArgb() } else { - backgroundColor - }).toArgb() - if (window.statusBarColor != statusBar) { - window.statusBarColor = statusBar - } - if (windowInsetController?.isAppearanceLightStatusBars != statusBarLight) { - windowInsetController?.isAppearanceLightStatusBars = statusBarLight - } - if (window.navigationBarColor != navBar) { - window.navigationBarColor = navBar - } - if (windowInsetController?.isAppearanceLightNavigationBars != isLight) { - windowInsetController?.isAppearanceLightNavigationBars = isLight + window.navigationBarColor = Color.Transparent.toArgb() } } @@ -401,6 +355,8 @@ class SimplexApp: Application(), LifecycleEventObserver { } return true } + + override val androidApiLevel: Int get() = Build.VERSION.SDK_INT } } } diff --git a/apps/multiplatform/common/build.gradle.kts b/apps/multiplatform/common/build.gradle.kts index 1aaa061daa..0e45c66efd 100644 --- a/apps/multiplatform/common/build.gradle.kts +++ b/apps/multiplatform/common/build.gradle.kts @@ -64,7 +64,6 @@ kotlin { implementation("androidx.activity:activity-compose:1.9.1") val workVersion = "2.9.1" implementation("androidx.work:work-runtime-ktx:$workVersion") - implementation("com.google.accompanist:accompanist-insets:0.30.1") // Video support implementation("com.google.android.exoplayer:exoplayer:2.19.1") diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Modifier.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Modifier.android.kt index 2aa66bc69b..5d07aae088 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Modifier.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Modifier.android.kt @@ -3,20 +3,8 @@ package chat.simplex.common.platform import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.painter.Painter -import com.google.accompanist.insets.navigationBarsWithImePadding import java.io.File -actual fun Modifier.navigationBarsWithImePadding(): Modifier = navigationBarsWithImePadding() - -@Composable -actual fun ProvideWindowInsets( - consumeWindowInsets: Boolean, - windowInsetsAnimationsEnabled: Boolean, - content: @Composable () -> Unit -) { - com.google.accompanist.insets.ProvideWindowInsets(content = content) -} - @Composable actual fun Modifier.desktopOnExternalDrag( enabled: Boolean, diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/PlatformTextField.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/PlatformTextField.android.kt index 5365db6a4c..7b820aa67e 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/PlatformTextField.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/PlatformTextField.android.kt @@ -6,8 +6,7 @@ import android.graphics.drawable.ColorDrawable import android.os.Build import android.text.InputType import android.util.Log -import android.view.OnReceiveContentListener -import android.view.ViewGroup +import android.view.* import android.view.inputmethod.* import android.widget.EditText import android.widget.TextView @@ -141,6 +140,13 @@ actual fun PlatformTextField( Log.e(TAG, e.stackTraceToString()) } } + editText.onFocusChangeListener = View.OnFocusChangeListener { _, hasFocus -> + // shows keyboard when user had search field on ChatView focused before clicking on this text field + // it still produce weird animation of closing/opening keyboard but the solution is to replace this Android EditText with Compose BasicTextField + if (hasFocus) { + showKeyboard = true + } + } editText.doOnTextChanged { text, _, _, _ -> if (!composeState.value.inProgress) { onMessageChange(text.toString()) 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 73c920b940..d4b77274ba 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 @@ -6,11 +6,11 @@ import android.content.Context import android.content.SharedPreferences import android.content.res.Configuration import android.text.BidiFormatter +import androidx.compose.foundation.layout.* import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.* import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight @@ -50,7 +50,11 @@ actual fun windowOrientation(): WindowOrientation = when (mainActivity.get()?.re } @Composable -actual fun windowWidth(): Dp = LocalConfiguration.current.screenWidthDp.dp +actual fun windowWidth(): Dp { + val direction = LocalLayoutDirection.current + val cutout = WindowInsets.displayCutout.only(WindowInsetsSides.Horizontal).asPaddingValues() + return LocalConfiguration.current.screenWidthDp.dp - cutout.calculateStartPadding(direction) - cutout.calculateEndPadding(direction) +} @Composable actual fun windowHeight(): Dp = LocalConfiguration.current.screenHeightDp.dp 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 6851970b81..d70177ffb9 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 @@ -7,10 +7,12 @@ import androidx.compose.foundation.lazy.* 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 androidx.compose.ui.unit.dp +import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.ui.theme.DEFAULT_PADDING +import chat.simplex.common.views.chatlist.NavigationBarBackground import chat.simplex.common.views.helpers.* import kotlinx.coroutines.flow.filter import kotlin.math.absoluteValue @@ -25,25 +27,74 @@ actual fun LazyColumnWithScrollBar( horizontalAlignment: Alignment.Horizontal, flingBehavior: FlingBehavior, userScrollEnabled: Boolean, + additionalBarOffset: State?, + fillMaxSize: Boolean, content: LazyListScope.() -> Unit ) { - val state = state ?: LocalAppBarHandler.current?.listState ?: rememberLazyListState() - val connection = LocalAppBarHandler.current?.connection + val handler = LocalAppBarHandler.current + require(handler != null) { "Using LazyColumnWithScrollBar and without AppBarHandler is an error. Use LazyColumnWithScrollBarNoAppBar instead" } + + val state = state ?: handler.listState + val connection = handler.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 (reverseLayout) { + snapshotFlow { state.layoutInfo.visibleItemsInfo.lastOrNull()?.offset ?: 0 } + .collect { scrollPosition -> + 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 + 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, content) - } else { - LazyColumn(modifier, state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content) + LazyColumn( + if (fillMaxSize) { + Modifier.fillMaxSize().copyViewToAppBar(remember { appPrefs.appearanceBarsBlurRadius.state }.value, LocalAppBarHandler.current?.graphicsLayer).then(modifier).nestedScroll(connection) + } else { + Modifier.copyViewToAppBar(remember { appPrefs.appearanceBarsBlurRadius.state }.value, LocalAppBarHandler.current?.graphicsLayer).then(modifier).nestedScroll(connection) + }, + state, + contentPadding, + reverseLayout, + verticalArrangement, + horizontalAlignment, + flingBehavior, + userScrollEnabled + ) { + content() + } +} + + +@Composable +actual fun LazyColumnWithScrollBarNoAppBar( + modifier: Modifier, + state: LazyListState?, + contentPadding: PaddingValues, + reverseLayout: Boolean, + verticalArrangement: Arrangement.Vertical, + horizontalAlignment: Alignment.Horizontal, + flingBehavior: FlingBehavior, + userScrollEnabled: Boolean, + additionalBarOffset: State?, + content: LazyListScope.() -> Unit +) { + val state = state ?: rememberLazyListState() + LazyColumn(modifier, state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled) { + content() } } @@ -54,32 +105,80 @@ actual fun ColumnWithScrollBar( horizontalAlignment: Alignment.Horizontal, state: ScrollState?, maxIntrinsicSize: Boolean, + fillMaxSize: Boolean, content: @Composable() (ColumnScope.() -> Unit) ) { - val state = state ?: LocalAppBarHandler.current?.scrollState ?: rememberScrollState() - val connection = LocalAppBarHandler.current?.connection + val handler = LocalAppBarHandler.current + require(handler != null) { "Using ColumnWithScrollBar and without AppBarHandler is an error. Use ColumnWithScrollBarNoAppBar instead" } + + val modifier = if (fillMaxSize) Modifier.fillMaxSize().then(modifier).imePadding() else modifier.imePadding() + val state = state ?: handler.scrollState + val connection = handler.connection LaunchedEffect(Unit) { snapshotFlow { state.value } .collect { scrollPosition -> - val offset = connection?.appBarOffset - if (offset != null && (offset + scrollPosition).absoluteValue > 1) { + val offset = connection.appBarOffset + if ((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) { + val oneHandUI = remember { appPrefs.oneHandUI.state } + Box(Modifier.fillMaxHeight()) { + Column( + if (maxIntrinsicSize) { + Modifier.copyViewToAppBar(remember { appPrefs.appearanceBarsBlurRadius.state }.value, LocalAppBarHandler.current?.graphicsLayer).then(modifier).nestedScroll(connection).verticalScroll(state).height(IntrinsicSize.Max) + } else { + Modifier.copyViewToAppBar(remember { appPrefs.appearanceBarsBlurRadius.state }.value, LocalAppBarHandler.current?.graphicsLayer).then(modifier).nestedScroll(connection).verticalScroll(state) + }, verticalArrangement, horizontalAlignment + ) { + if (oneHandUI.value) { + Spacer(Modifier.padding(top = DEFAULT_PADDING + 5.dp).windowInsetsTopHeight(WindowInsets.statusBars)) + content() + Spacer(Modifier.navigationBarsPadding().padding(bottom = AppBarHeight * fontSizeSqrtMultiplier)) + } else { + Spacer(Modifier.statusBarsPadding().padding(top = AppBarHeight * fontSizeSqrtMultiplier)) + content() + Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.systemBars)) + } + } + if (!oneHandUI.value) { + NavigationBarBackground(false, false) + } + } +} + +@Composable +actual fun ColumnWithScrollBarNoAppBar( + modifier: Modifier, + verticalArrangement: Arrangement.Vertical, + horizontalAlignment: Alignment.Horizontal, + state: ScrollState?, + maxIntrinsicSize: Boolean, + content: @Composable() (ColumnScope.() -> Unit) +) { + val modifier = modifier.imePadding() + val state = state ?: rememberScrollState() + val oneHandUI = remember { appPrefs.oneHandUI.state } + Box(Modifier.fillMaxHeight()) { + Column( + if (maxIntrinsicSize) { modifier.verticalScroll(state).height(IntrinsicSize.Max) } else { modifier.verticalScroll(state) - }, verticalArrangement, horizontalAlignment, content) + }, verticalArrangement, horizontalAlignment + ) { + if (oneHandUI.value) { + Spacer(Modifier.windowInsetsTopHeight(WindowInsets.systemBars)) + content() + } else { + content() + Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.systemBars)) + } + } + if (!oneHandUI.value) { + NavigationBarBackground(false, false) + } } } diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/UI.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/UI.android.kt index c90946c95b..7ab6bf525f 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/UI.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/UI.android.kt @@ -3,17 +3,18 @@ package chat.simplex.common.platform import android.app.Activity import android.content.Context import android.content.pm.ActivityInfo -import android.graphics.Rect import android.os.* import android.view.* import android.view.inputmethod.InputMethodManager import android.widget.Toast import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.ime import androidx.compose.runtime.* +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalView import chat.simplex.common.AppScreen import chat.simplex.common.model.clear -import chat.simplex.common.ui.theme.SimpleXTheme import chat.simplex.common.views.helpers.* import androidx.compose.ui.platform.LocalContext as LocalContext1 import chat.simplex.res.MR @@ -43,28 +44,13 @@ actual fun LocalMultiplatformView(): Any? = LocalView.current @Composable actual fun getKeyboardState(): State { - val keyboardState = remember { mutableStateOf(KeyboardState.Closed) } - val view = LocalView.current - DisposableEffect(view) { - val onGlobalListener = ViewTreeObserver.OnGlobalLayoutListener { - val rect = Rect() - view.getWindowVisibleDisplayFrame(rect) - val screenHeight = view.rootView.height - val keypadHeight = screenHeight - rect.bottom - keyboardState.value = if (keypadHeight > screenHeight * 0.15) { - KeyboardState.Opened - } else { - KeyboardState.Closed - } - } - view.viewTreeObserver.addOnGlobalLayoutListener(onGlobalListener) - - onDispose { - view.viewTreeObserver.removeOnGlobalLayoutListener(onGlobalListener) + val density = LocalDensity.current + val ime = WindowInsets.ime + return remember { + derivedStateOf { + if (ime.getBottom(density) == 0) KeyboardState.Closed else KeyboardState.Opened } } - - return keyboardState } actual fun hideKeyboard(view: Any?, clearFocus: Boolean) { diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt index f3a9be6132..601b907902 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt @@ -47,6 +47,7 @@ import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.onboarding.OnboardingStage import chat.simplex.res.MR import com.google.accompanist.permissions.* import dev.icerock.moko.resources.StringResource @@ -328,11 +329,14 @@ private fun ActiveCallOverlayLayout( flipCamera: () -> Unit ) { Column { - CloseSheetBar({ chatModel.activeCallViewIsCollapsed.value = true }, true, tintColor = Color(0xFFFFFFD8)) { - if (call.hasVideo) { - Text(call.contact.chatViewName, Modifier.fillMaxWidth().padding(end = DEFAULT_PADDING), color = Color(0xFFFFFFD8), style = MaterialTheme.typography.h2, overflow = TextOverflow.Ellipsis, maxLines = 1) - } - } + CallAppBar( + title = { + if (call.hasVideo) { + Text(call.contact.chatViewName, Modifier.offset(x = (-4).dp).padding(end = DEFAULT_PADDING), color = Color(0xFFFFFFD8), style = MaterialTheme.typography.h2, overflow = TextOverflow.Ellipsis, maxLines = 1) + } + }, + onBack = { chatModel.activeCallViewIsCollapsed.value = true } + ) Column(Modifier.padding(horizontal = DEFAULT_PADDING)) { @Composable fun SelectSoundDevice(size: Dp) { @@ -590,8 +594,9 @@ fun CallPermissionsView(pipActive: Boolean, hasVideo: Boolean, cancel: () -> Uni } } } else { - ModalView(background = Color.Black, showClose = false, close = {}) { - ColumnWithScrollBar(Modifier.fillMaxSize()) { + ModalView(background = Color.Black, showAppBar = false, close = {}) { + Column { + Spacer(Modifier.height(AppBarHeight * fontSizeSqrtMultiplier)) AppBarTitle(stringResource(MR.strings.permissions_required)) Spacer(Modifier.weight(1f)) val onClick = { diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.android.kt index 05a9430ff1..ae5b8043ed 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.android.kt @@ -8,7 +8,6 @@ import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.platform.LocalContext import chat.simplex.common.model.CIFile -import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.CurrentColors import chat.simplex.common.views.helpers.ModalManager @@ -39,14 +38,6 @@ actual fun SimpleAndAnimatedImageView( if (getLoadedFilePath(file) != null) { ModalManager.fullscreen.showCustomModal(animated = false) { close -> ImageFullScreenView(imageProvider, close) - if (smallView) { - DisposableEffect(Unit) { - onDispose { - val c = CurrentColors.value.colors - platform.androidSetStatusAndNavBarColors(c.isLight, c.background, !appPrefs.oneHandUI.get(), appPrefs.oneHandUI.get()) - } - } - } } } } diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.android.kt index 4681a5a64d..7db39b7d3e 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.android.kt @@ -1,6 +1,5 @@ package chat.simplex.common.views.chatlist -import android.app.Activity import androidx.compose.foundation.* import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* @@ -11,21 +10,17 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.* 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.ANDROID_CALL_TOP_PADDING import chat.simplex.common.model.durationText import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.call.* -import chat.simplex.common.views.helpers.* import chat.simplex.res.MR import dev.icerock.moko.resources.compose.painterResource import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.datetime.Clock private val CALL_INTERACTIVE_AREA_HEIGHT = 74.dp @@ -37,11 +32,12 @@ private val CALL_BOTTOM_ICON_HEIGHT = CALL_INTERACTIVE_AREA_HEIGHT + CALL_BOTTOM @Composable actual fun ActiveCallInteractiveArea(call: Call) { val onClick = { platform.androidStartCallActivity(false) } - Box(Modifier.offset(y = CALL_TOP_OFFSET).height(CALL_INTERACTIVE_AREA_HEIGHT)) { + val statusBar = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + Box(Modifier.offset(y = CALL_TOP_OFFSET).height(CALL_INTERACTIVE_AREA_HEIGHT + statusBar)) { val source = remember { MutableInteractionSource() } val ripple = remember { ripple(bounded = true, 3000.dp) } - Box(Modifier.height(CALL_TOP_GREEN_LINE_HEIGHT).clickable(onClick = onClick, indication = ripple, interactionSource = source)) { - GreenLine(call) + Box(Modifier.height(CALL_TOP_GREEN_LINE_HEIGHT + statusBar).clickable(onClick = onClick, indication = ripple, interactionSource = source)) { + GreenLine(statusBar, call) } Box( Modifier @@ -62,16 +58,13 @@ actual fun ActiveCallInteractiveArea(call: Call) { } } -// Temporary solution for storing a color that needs to be applied after call ends -var statusBarColorAfterCall = mutableIntStateOf(CurrentColors.value.colors.background.toArgb()) - @Composable -private fun GreenLine(call: Call) { +private fun GreenLine(statusBarHeight: Dp, call: Call) { Row( Modifier .fillMaxSize() .background(SimplexGreen) - .padding(top = -CALL_TOP_OFFSET) + .padding(top = -CALL_TOP_OFFSET + statusBarHeight) .padding(horizontal = DEFAULT_PADDING), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center @@ -80,12 +73,10 @@ private fun GreenLine(call: Call) { Spacer(Modifier.weight(1f)) CallDuration(call) } - val window = (LocalContext.current as Activity).window DisposableEffect(Unit) { - statusBarColorAfterCall.intValue = window.statusBarColor - window.statusBarColor = SimplexGreen.toArgb() + platform.androidSetStatusAndNavigationBarAppearance(false, CurrentColors.value.colors.isLight) onDispose { - window.statusBarColor = statusBarColorAfterCall.intValue + platform.androidSetStatusAndNavigationBarAppearance(CurrentColors.value.colors.isLight, CurrentColors.value.colors.isLight) } } } diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.android.kt index b68756c669..54e3061d25 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.android.kt @@ -19,13 +19,11 @@ import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.* -import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.User import chat.simplex.common.model.UserInfo import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* -import chat.simplex.common.views.onboarding.OnboardingStage import kotlinx.coroutines.CancellationException import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow @@ -36,6 +34,7 @@ private val USER_PICKER_ROW_PADDING = 16.dp @Composable actual fun UserPickerUsersSection( users: List, + iconColor: Color, stopped: Boolean, onUserClicked: (user: User) -> Unit, ) { @@ -140,87 +139,73 @@ actual fun PlatformUserPicker(modifier: Modifier, pickerState: MutableStateFlow< } else { Modifier } - Box( - Modifier - .fillMaxSize() - .then(clickableModifier) - .drawBehind { - val pos = when { - dismissState.progress.from == DismissValue.Default && dismissState.progress.to == DismissValue.Default -> 1f - dismissState.progress.from == DismissValue.DismissedToEnd && dismissState.progress.to == DismissValue.DismissedToEnd -> 0f - dismissState.progress.to == DismissValue.Default -> dismissState.progress.fraction - else -> 1 - dismissState.progress.fraction - } - val colors = CurrentColors.value.colors - val resultingColor = if (colors.isLight) colors.onSurface.copy(alpha = ScrimOpacity) else Color.Black.copy(0.64f) - val adjustedAlpha = resultingColor.alpha * calculateFraction(pos = pos) - val shadingColor = resultingColor.copy(alpha = adjustedAlpha) - - if (pickerState.value.isVisible()) { - platform.androidSetDrawerStatusAndNavBarColor( - isLight = colors.isLight, - drawerShadingColor = shadingColor, - toolbarOnTop = !appPrefs.oneHandUI.get(), - navBarColor = colors.background.mixWith(colors.onBackground, 1 - userPickerAlpha()) - ) - } else if (ModalManager.start.modalCount.value == 0) { - platform.androidSetDrawerStatusAndNavBarColor( - isLight = colors.isLight, - drawerShadingColor = shadingColor, - toolbarOnTop = !appPrefs.oneHandUI.get(), - navBarColor = (if (appPrefs.oneHandUI.get() && appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete) { - colors.background.mixWith(CurrentColors.value.colors.onBackground, 0.97f) - } else { - colors.background - }) - ) - } - drawRect( - if (pos != 0f) resultingColor else Color.Transparent, - alpha = calculateFraction(pos = pos) - ) - } - .graphicsLayer { - if (heightValue == 0) { - alpha = 0f - } - translationY = dismissState.offset.value - }, - contentAlignment = Alignment.BottomCenter - ) { + Box { Box( - Modifier.onSizeChanged { height.intValue = it.height } - ) { - KeyChangeEffect(pickerIsVisible) { - if (pickerState.value.isVisible()) { - try { - dismissState.animateTo(DismissValue.Default, userPickerAnimSpec()) - } catch (e: CancellationException) { - Log.e(TAG, "Cancelled animateTo: ${e.stackTraceToString()}") - pickerState.value = AnimatedViewState.GONE + Modifier + .fillMaxSize() + .then(clickableModifier) + .drawBehind { + val pos = calculatePosition(dismissState) + val colors = CurrentColors.value.colors + val resultingColor = if (colors.isLight) colors.onSurface.copy(alpha = ScrimOpacity) else Color.Black.copy(0.64f) + drawRect( + if (pos != 0f) resultingColor else Color.Transparent, + alpha = calculateFraction(pos = pos) + ) + } + .graphicsLayer { + if (heightValue == 0) { + alpha = 0f } - } else { - try { - dismissState.animateTo(DismissValue.DismissedToEnd, userPickerAnimSpec()) - } catch (e: CancellationException) { - Log.e(TAG, "Cancelled animateTo2: ${e.stackTraceToString()}") - pickerState.value = AnimatedViewState.VISIBLE + translationY = dismissState.offset.value + }, + contentAlignment = Alignment.BottomCenter + ) { + Box( + Modifier.onSizeChanged { height.intValue = it.height } + ) { + KeyChangeEffect(pickerIsVisible) { + if (pickerState.value.isVisible()) { + try { + dismissState.animateTo(DismissValue.Default, userPickerAnimSpec()) + } catch (e: CancellationException) { + Log.e(TAG, "Cancelled animateTo: ${e.stackTraceToString()}") + pickerState.value = AnimatedViewState.GONE + } + } else { + try { + dismissState.animateTo(DismissValue.DismissedToEnd, userPickerAnimSpec()) + } catch (e: CancellationException) { + Log.e(TAG, "Cancelled animateTo2: ${e.stackTraceToString()}") + pickerState.value = AnimatedViewState.VISIBLE + } } } - } - val draggableModifier = if (height.intValue != 0) - Modifier.draggableBottomDrawerModifier( - state = dismissState, - swipeDistance = height.intValue.toFloat(), - ) - else Modifier - Box(draggableModifier.then(modifier)) { - content() + val draggableModifier = if (height.intValue != 0) + Modifier.draggableBottomDrawerModifier( + state = dismissState, + swipeDistance = height.intValue.toFloat(), + ) + else Modifier + Box(draggableModifier.then(modifier).navigationBarsPadding()) { + content() + } } } + NavigationBarBackground( + modifier = Modifier.graphicsLayer { alpha = if (calculatePosition(dismissState) > 0.1f) 1f else 0f }, + color = MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, alpha = 1 - userPickerAlpha()) + ) } } +private fun calculatePosition(dismissState: DismissState): Float = when { + dismissState.progress.from == DismissValue.Default && dismissState.progress.to == DismissValue.Default -> 1f + dismissState.progress.from == DismissValue.DismissedToEnd && dismissState.progress.to == DismissValue.DismissedToEnd -> 0f + dismissState.progress.to == DismissValue.Default -> dismissState.progress.fraction + else -> 1 - dismissState.progress.fraction +} + private fun Modifier.draggableBottomDrawerModifier( state: DismissState, swipeDistance: Float, diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/GetImageView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/GetImageView.android.kt index 98d1f8fb19..1c7ba1dcf0 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/GetImageView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/GetImageView.android.kt @@ -171,6 +171,8 @@ actual fun GetImageBottomSheet( modifier = Modifier .fillMaxWidth() .wrapContentHeight() + .imePadding() + .navigationBarsPadding() .onFocusChanged { focusState -> if (!focusState.hasFocus) hideBottomSheet() } diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/Appearance.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/Appearance.android.kt index 418174a8e9..e5450e8e49 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/Appearance.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/Appearance.android.kt @@ -2,6 +2,7 @@ package chat.simplex.common.views.usersettings import SectionBottomSpacer import SectionDividerSpaced +import SectionSpacer import SectionView import android.app.Activity import android.content.ComponentName @@ -31,6 +32,7 @@ import chat.simplex.common.model.ChatModel import chat.simplex.common.platform.* import chat.simplex.common.helpers.APPLICATION_ID import chat.simplex.common.helpers.saveAppLocale +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.res.MR import dev.icerock.moko.resources.ImageResource import dev.icerock.moko.resources.compose.painterResource @@ -75,9 +77,7 @@ fun AppearanceScope.AppearanceLayout( systemDarkTheme: SharedPreference, changeIcon: (AppIcon) -> Unit, ) { - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.appearance_settings)) SectionView(stringResource(MR.strings.settings_section_title_interface), contentPadding = PaddingValues()) { val context = LocalContext.current @@ -106,15 +106,15 @@ fun AppearanceScope.AppearanceLayout( } // } - SettingsPreferenceItem(icon = null, stringResource(MR.strings.one_hand_ui), ChatModel.controller.appPrefs.oneHandUI) { - val c = CurrentColors.value.colors - platform.androidSetStatusAndNavBarColors(c.isLight, c.background, false, false) - } + SettingsPreferenceItem(icon = null, stringResource(MR.strings.one_hand_ui), ChatModel.controller.appPrefs.oneHandUI) } SectionDividerSpaced() ThemesSection(systemDarkTheme) + SectionDividerSpaced() + AppToolbarsSection() + SectionDividerSpaced() MessageShapeSection() diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.android.kt index 96b4a43e1a..04b59732dd 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.android.kt @@ -13,14 +13,13 @@ import dev.icerock.moko.resources.compose.stringResource @Composable actual fun SettingsSectionApp( showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), - showCustomModal: (@Composable ModalData.(ChatModel, () -> Unit) -> Unit) -> (() -> Unit), showVersion: () -> Unit, withAuth: (title: String, desc: String, block: () -> Unit) -> Unit ) { SectionView(stringResource(MR.strings.settings_section_title_app)) { SettingsActionItem(painterResource(MR.images.ic_restart_alt), stringResource(MR.strings.settings_restart_app), ::restartApp) SettingsActionItem(painterResource(MR.images.ic_power_settings_new), stringResource(MR.strings.settings_shutdown), { shutdownAppAlert(::shutdownApp) }) - SettingsActionItem(painterResource(MR.images.ic_code), stringResource(MR.strings.settings_developer_tools), showSettingsModal { DeveloperView(it, showCustomModal, withAuth) }) + SettingsActionItem(painterResource(MR.images.ic_code), stringResource(MR.strings.settings_developer_tools), showSettingsModal { DeveloperView(withAuth) }) AppVersionItem(showVersion) } } 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 b95aed45d2..ee38cb80fe 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 @@ -11,10 +11,13 @@ 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.clipToBounds -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.draw.* +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.* +import androidx.compose.ui.graphics.drawscope.clipRect import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.dp import chat.simplex.common.views.usersettings.SetDeliveryReceiptsView import chat.simplex.common.model.* @@ -39,14 +42,39 @@ import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.* import kotlinx.coroutines.flow.* +import kotlin.math.absoluteValue @Composable fun AppScreen() { AppBarHandler.appBarMaxHeightPx = with(LocalDensity.current) { AppBarHeight.roundToPx() } SimpleXTheme { - ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { - Surface(color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) { - MainScreen() + Surface(color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) { + // This padding applies to landscape view only taking care of navigation bar and holes in screen in status bar area + // (because nav bar and holes located on vertical sides of screen in landscape view) + val direction = LocalLayoutDirection.current + val safePadding = WindowInsets.safeDrawing.asPaddingValues() + val cutout = WindowInsets.displayCutout.asPaddingValues() + val cutoutStart = cutout.calculateStartPadding(direction) + val cutoutEnd = cutout.calculateEndPadding(direction) + val cutoutMax = maxOf(cutoutStart, cutoutEnd) + val paddingStartUntouched = safePadding.calculateStartPadding(direction) + val paddingStart = paddingStartUntouched - cutoutStart + val paddingEndUntouched = safePadding.calculateEndPadding(direction) + val paddingEnd = paddingEndUntouched - cutoutEnd + // Such a strange layout is needed because the main content should be covered by solid color in order to hide overflow + // of some elements that may have negative offset (so, can't use Row {}). + // To check: go to developer settings of Android, choose Display cutout -> Punch hole, and rotate the phone to landscape, open any chat + Box { + val fullscreenGallery = remember { chatModel.fullscreenGalleryVisible } + Box(Modifier.padding(start = paddingStart + cutoutMax, end = paddingEnd + cutoutMax).consumeWindowInsets(PaddingValues(start = paddingStartUntouched, end = paddingEndUntouched))) { + Box(Modifier.drawBehind { + if (fullscreenGallery.value) { + drawRect(Color.Black, topLeft = Offset(-(paddingStart + cutoutMax).toPx(), 0f), Size(size.width + (paddingStart + cutoutMax).toPx() + (paddingEnd + cutoutMax).toPx(), size.height)) + } + }) { + MainScreen() + } + } } } } @@ -138,7 +166,9 @@ fun MainScreen() { } SetupClipboardListener() if (appPlatform.isAndroid) { - AndroidScreen(userPickerState) + AndroidWrapInCallLayout { + AndroidScreen(userPickerState) + } } else { DesktopScreen(userPickerState) } @@ -170,7 +200,9 @@ fun MainScreen() { } } if (appPlatform.isAndroid) { - ModalManager.fullscreen.showInView() + AndroidWrapInCallLayout { + ModalManager.fullscreen.showInView() + } SwitchingUsersView() } @@ -237,19 +269,39 @@ fun MainScreen() { val ANDROID_CALL_TOP_PADDING = 40.dp +@Composable +fun AndroidWrapInCallLayout(content: @Composable () -> Unit) { + val call = remember { chatModel.activeCall}.value + val showCallArea = call != null && call.callState != CallState.WaitCapabilities && call.callState != CallState.InvitationAccepted + Box { + Box(Modifier.padding(top = if (showCallArea) ANDROID_CALL_TOP_PADDING else 0.dp)) { + content() + } + if (call != null && showCallArea) { + ActiveCallInteractiveArea(call) + } + } +} + @Composable fun AndroidScreen(userPickerState: MutableStateFlow) { BoxWithConstraints { - val call = remember { chatModel.activeCall} .value - val showCallArea = call != null && call.callState != CallState.WaitCapabilities && call.callState != CallState.InvitationAccepted val currentChatId = remember { mutableStateOf(chatModel.chatId.value) } val offset = remember { Animatable(if (chatModel.chatId.value == null) 0f else maxWidth.value) } + val cutout = WindowInsets.displayCutout.only(WindowInsetsSides.Horizontal).asPaddingValues() + val direction = LocalLayoutDirection.current + val hasCutout = cutout.calculateStartPadding(direction) + cutout.calculateEndPadding(direction) > 0.dp Box( Modifier + // clipping only for devices with cutout currently visible on sides. It prevents showing chat list with open chat view + // In order cases it's not needed to use clip + .then(if (hasCutout) Modifier.clip(RectangleShape) else Modifier) .graphicsLayer { - translationX = -offset.value.dp.toPx() + // minOf thing is needed for devices with holes in screen while the user on ChatView rotates his phone from portrait to landscape + // because in this case (at least in emulator) maxWidth changes in two steps: big first, smaller on next frame. + // But offset is remembered already, so this is a better way than dropping a value of offset + translationX = -minOf(offset.value.dp, maxWidth).toPx() } - .padding(top = if (showCallArea) ANDROID_CALL_TOP_PADDING else 0.dp) ) { StartPartOfScreen(userPickerState) } @@ -271,51 +323,40 @@ fun AndroidScreen(userPickerState: MutableStateFlow) { snapshotFlow { chatModel.chatId.value } .distinctUntilChanged() .collect { - if (it == null) { - platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, CurrentColors.value.colors.background, !appPrefs.oneHandUI.get(), appPrefs.oneHandUI.get()) - onComposed(null) - } + if (it == null) onComposed(null) currentChatId.value = it } } } - LaunchedEffect(Unit) { - snapshotFlow { ModalManager.center.modalCount.value > 0 } - .filter { chatModel.chatId.value == null } - .collect { modalBackground -> - if (chatModel.newChatSheetVisible.value) { - platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, CurrentColors.value.colors.background, false, appPrefs.oneHandUI.get()) - } else if (modalBackground) { - platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, CurrentColors.value.colors.background, false, false) - } else { - platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, CurrentColors.value.colors.background, !appPrefs.oneHandUI.get(), appPrefs.oneHandUI.get()) - } - } - } Box(Modifier - .graphicsLayer { translationX = maxWidth.toPx() - offset.value.dp.toPx() } - .padding(top = if (showCallArea) ANDROID_CALL_TOP_PADDING else 0.dp) + .then(if (hasCutout) Modifier.clip(RectangleShape) else Modifier) + .graphicsLayer { translationX = maxWidth.toPx() - minOf(offset.value.dp, maxWidth).toPx() } ) Box2@{ currentChatId.value?.let { ChatView(currentChatId, onComposed) } } - if (call != null && showCallArea) { - ActiveCallInteractiveArea(call) - } } } @Composable fun StartPartOfScreen(userPickerState: MutableStateFlow) { if (chatModel.setDeliveryReceipts.value) { - SetDeliveryReceiptsView(chatModel) + CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) { + SetDeliveryReceiptsView(chatModel) + } } else { val stopped = chatModel.chatRunning.value == false - if (chatModel.sharedContent.value == null) - ChatListView(chatModel, userPickerState, AppLock::setPerformLA, stopped) - else - ShareListView(chatModel, stopped) + if (chatModel.sharedContent.value == null) { + CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) { + ChatListView(chatModel, userPickerState, AppLock::setPerformLA, stopped) + } + } else { + // LALAL initial load of view doesn't show blur. Focusing text field shows it + CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler(keyboardCoversBar = false)) { + ShareListView(chatModel, stopped) + } + } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 6bc565097f..422eb1e77f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -90,6 +90,9 @@ object ChatModel { // Needed to check for bottom nav bar and to apply or not navigation bar color on Android val newChatSheetVisible = mutableStateOf(false) + // Needed to apply black color to left/right cutout area on Android + val fullscreenGalleryVisible = mutableStateOf(false) + // preferences val notificationPreviewMode by lazy { mutableStateOf( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 29d45a3b9b..fab85fa679 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -118,6 +118,9 @@ class AppPreferences { val privacyEncryptLocalFiles = mkBoolPreference(SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES, true) val privacyAskToApproveRelays = mkBoolPreference(SHARED_PREFS_PRIVACY_ASK_TO_APPROVE_RELAYS, true) val privacyMediaBlurRadius = mkIntPreference(SHARED_PREFS_PRIVACY_MEDIA_BLUR_RADIUS, 0) + // Blur broken on Android 12, see https://github.com/chrisbanes/haze/issues/77. And not available before 12 + val deviceSupportsBlur = appPlatform.isDesktop || (platform.androidApiLevel ?: 0) >= 32 + val appearanceBarsBlurRadius = mkIntPreference(SHARED_PREFS_APPEARANCE_BARS_BLUR_RADIUS, if (deviceSupportsBlur) 50 else 0) val experimentalCalls = mkBoolPreference(SHARED_PREFS_EXPERIMENTAL_CALLS, false) val showUnreadAndFavorites = mkBoolPreference(SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES, false) val chatArchiveName = mkStrPreference(SHARED_PREFS_CHAT_ARCHIVE_NAME, null) @@ -223,6 +226,8 @@ class AppPreferences { val chatItemTail = mkBoolPreference(SHARED_PREFS_CHAT_ITEM_TAIL, true) val fontScale = mkFloatPreference(SHARED_PREFS_FONT_SCALE, 1f) val densityScale = mkFloatPreference(SHARED_PREFS_DENSITY_SCALE, 1f) + val inAppBarsDefaultAlpha = if (deviceSupportsBlur) 0.875f else 0.975f + val inAppBarsAlpha = mkFloatPreference(SHARED_PREFS_IN_APP_BARS_ALPHA, inAppBarsDefaultAlpha) val whatsNewVersion = mkStrPreference(SHARED_PREFS_WHATS_NEW_VERSION, null) val lastMigratedVersionCode = mkIntPreference(SHARED_PREFS_LAST_MIGRATED_VERSION_CODE, 0) @@ -244,7 +249,7 @@ class AppPreferences { val iosCallKitEnabled = mkBoolPreference(SHARED_PREFS_IOS_CALL_KIT_ENABLED, true) val iosCallKitCallsInRecents = mkBoolPreference(SHARED_PREFS_IOS_CALL_KIT_CALLS_IN_RECENTS, false) - val oneHandUI = mkBoolPreference(SHARED_PREFS_ONE_HAND_UI, appPlatform.isAndroid) + val oneHandUI = mkBoolPreference(SHARED_PREFS_ONE_HAND_UI, true) val hintPreferences: List, Boolean>> = listOf( laNoticeShown to false, @@ -362,6 +367,7 @@ class AppPreferences { private const val SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES = "PrivacyEncryptLocalFiles" private const val SHARED_PREFS_PRIVACY_ASK_TO_APPROVE_RELAYS = "PrivacyAskToApproveRelays" private const val SHARED_PREFS_PRIVACY_MEDIA_BLUR_RADIUS = "PrivacyMediaBlurRadius" + private const val SHARED_PREFS_APPEARANCE_BARS_BLUR_RADIUS = "AppearanceBarsBlurRadius" const val SHARED_PREFS_PRIVACY_FULL_BACKUP = "FullBackup" private const val SHARED_PREFS_EXPERIMENTAL_CALLS = "ExperimentalCalls" private const val SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES = "ShowUnreadAndFavorites" @@ -428,6 +434,7 @@ class AppPreferences { private const val SHARED_PREFS_CHAT_ITEM_TAIL = "ChatItemTail" private const val SHARED_PREFS_FONT_SCALE = "FontScale" private const val SHARED_PREFS_DENSITY_SCALE = "DensityScale" + private const val SHARED_PREFS_IN_APP_BARS_ALPHA = "InAppBarsAlpha" private const val SHARED_PREFS_WHATS_NEW_VERSION = "WhatsNewVersion" private const val SHARED_PREFS_LAST_MIGRATED_VERSION_CODE = "LastMigratedVersionCode" private const val SHARED_PREFS_CUSTOM_DISAPPEARING_MESSAGE_TIME = "CustomDisappearingMessageTime" diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Modifier.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Modifier.kt index 5281fcb1ad..be7022ca80 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Modifier.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Modifier.kt @@ -14,15 +14,6 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.filter import java.io.File -expect fun Modifier.navigationBarsWithImePadding(): Modifier - -@Composable -expect fun ProvideWindowInsets( - consumeWindowInsets: Boolean = true, - windowInsetsAnimationsEnabled: Boolean = true, - content: @Composable () -> Unit -) - @Composable expect fun Modifier.desktopOnExternalDrag( enabled: Boolean = true, 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 5dfa5aa200..23ab450cb6 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 @@ -8,6 +8,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import chat.simplex.common.model.ChatId import chat.simplex.common.model.NotificationsMode +import chat.simplex.common.ui.theme.CurrentColors import kotlinx.coroutines.Job interface PlatformInterface { @@ -20,12 +21,12 @@ interface PlatformInterface { fun androidChatInitializedAndStarted() {} fun androidIsBackgroundCallAllowed(): Boolean = true fun androidSetNightModeIfSupported() {} - fun androidSetStatusAndNavBarColors(isLight: Boolean, backgroundColor: Color, hasTop: Boolean, hasBottom: Boolean) {} - fun androidSetDrawerStatusAndNavBarColor(isLight: Boolean, drawerShadingColor: Color, toolbarOnTop: Boolean, navBarColor: Color) {} + fun androidSetStatusAndNavigationBarAppearance(isLightStatusBar: Boolean, isLightNavBar: Boolean, blackNavBar: Boolean = false, themeBackgroundColor: Color = CurrentColors.value.colors.background) {} fun androidStartCallActivity(acceptCall: Boolean, remoteHostId: Long? = null, chatId: ChatId? = null) {} fun androidPictureInPictureAllowed(): Boolean = true fun androidCallEnded() {} fun androidRestartNetworkObserver() {} + val androidApiLevel: Int? get() = null @Composable fun androidLockPortraitOrientation() {} suspend fun androidAskToAllowBackgroundCalls(): Boolean = true @Composable fun desktopShowAppUpdateNotice() {} 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..b0be547a31 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,11 +22,44 @@ expect fun LazyColumnWithScrollBar( horizontalAlignment: Alignment.Horizontal = Alignment.Start, flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(), userScrollEnabled: Boolean = true, + additionalBarOffset: State? = null, + // by default, this function will include .fillMaxSize() without you doing anything. If you don't need it, pass `false` here + // maxSize (at least maxHeight) is needed for blur on appBars to work correctly + fillMaxSize: Boolean = true, + content: LazyListScope.() -> Unit +) + +@Composable +expect fun LazyColumnWithScrollBarNoAppBar( + modifier: Modifier = Modifier, + state: LazyListState? = null, + contentPadding: PaddingValues = PaddingValues(0.dp), + reverseLayout: Boolean = false, + verticalArrangement: Arrangement.Vertical = + if (!reverseLayout) Arrangement.Top else Arrangement.Bottom, + horizontalAlignment: Alignment.Horizontal = Alignment.Start, + flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(), + userScrollEnabled: Boolean = true, + additionalBarOffset: State? = null, content: LazyListScope.() -> Unit ) @Composable expect fun ColumnWithScrollBar( + modifier: Modifier = Modifier, + verticalArrangement: Arrangement.Vertical = Arrangement.Top, + horizontalAlignment: Alignment.Horizontal = Alignment.Start, + state: ScrollState? = null, + // set true when you want to show something in the center with respected .fillMaxSize() + maxIntrinsicSize: Boolean = false, + // by default, this function will include .fillMaxSize() without you doing anything. If you don't need it, pass `false` here + // maxSize (at least maxHeight) is needed for blur on appBars to work correctly + fillMaxSize: Boolean = true, + content: @Composable ColumnScope.() -> Unit +) + +@Composable +expect fun ColumnWithScrollBarNoAppBar( modifier: Modifier = Modifier, verticalArrangement: Arrangement.Vertical = Arrangement.Top, horizontalAlignment: Alignment.Horizontal = Alignment.Start, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt index 595c22e3e2..80542ced02 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt @@ -1,14 +1,14 @@ package chat.simplex.common.ui.theme -import androidx.compose.foundation.background import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.* +import androidx.compose.ui.graphics.layer.GraphicsLayer import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.Density -import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.* import chat.simplex.common.model.ChatController import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.platform.* @@ -587,21 +587,27 @@ data class ThemeModeOverride ( } } -fun Modifier.themedBackground(baseTheme: DefaultTheme = CurrentColors.value.base, shape: Shape = RectangleShape): Modifier { - return if (baseTheme == DefaultTheme.SIMPLEX) { - this.background(brush = Brush.linearGradient( - listOf( - CurrentColors.value.colors.background.darker(0.4f), - CurrentColors.value.colors.background.lighter(0.4f) - ), - Offset(0f, Float.POSITIVE_INFINITY), - Offset(Float.POSITIVE_INFINITY, 0f) - ), shape = shape) - } else { - this.background(color = CurrentColors.value.colors.background, shape = shape) +fun Modifier.themedBackground(baseTheme: DefaultTheme = CurrentColors.value.base, bgLayerSize: MutableState?, bgLayer: GraphicsLayer?/*, shape: Shape = RectangleShape*/): Modifier { + return drawBehind { + copyBackgroundToAppBar(bgLayerSize, bgLayer) { + if (baseTheme == DefaultTheme.SIMPLEX) { + drawRect(brush = themedBackgroundBrush()) + } else { + drawRect(CurrentColors.value.colors.background) + } + } } } +fun themedBackgroundBrush(): Brush = Brush.linearGradient( + listOf( + CurrentColors.value.colors.background.darker(0.4f), + CurrentColors.value.colors.background.lighter(0.4f) + ), + Offset(0f, Float.POSITIVE_INFINITY), + Offset(Float.POSITIVE_INFINITY, 0f) +) + val DEFAULT_PADDING = 20.dp val DEFAULT_SPACE_AFTER_ICON = 4.dp val DEFAULT_PADDING_HALF = DEFAULT_PADDING / 2 diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt index 7f19f58949..a5293b6a24 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt @@ -1,7 +1,6 @@ package chat.simplex.common.ui.theme import androidx.compose.material.Colors -import androidx.compose.material.MaterialTheme import androidx.compose.runtime.MutableState import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb @@ -107,7 +106,7 @@ object ThemeManager { CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) platform.androidSetNightModeIfSupported() val c = CurrentColors.value.colors - platform.androidSetStatusAndNavBarColors(c.isLight, c.background, !ChatController.appPrefs.oneHandUI.get(), ChatController.appPrefs.oneHandUI.get()) + platform.androidSetStatusAndNavigationBarAppearance(c.isLight, c.isLight) } fun changeDarkTheme(theme: String) { @@ -125,10 +124,6 @@ object ThemeManager { themeIds[nonSystemThemeName] = prevValue.themeId appPrefs.currentThemeIds.set(themeIds) CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) - if (name == ThemeColor.BACKGROUND) { - val c = CurrentColors.value.colors - platform.androidSetStatusAndNavBarColors(c.isLight, c.background, false, false) - } } fun applyThemeColor(name: ThemeColor, color: Color? = null, pref: MutableState) { 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 d89803f8e4..d4eb416081 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 @@ -7,40 +7,34 @@ import androidx.compose.foundation.lazy.* import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.* import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.layoutId import androidx.compose.ui.platform.LocalClipboardManager 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.* import chat.simplex.common.views.chat.* import chat.simplex.common.views.helpers.* import chat.simplex.common.model.ChatModel import chat.simplex.common.platform.* -import kotlinx.coroutines.flow.collect +import chat.simplex.common.views.chat.item.CONSOLE_COMPOSE_LAYOUT_ID +import chat.simplex.common.views.chat.item.AdaptingBottomPaddingLayout +import chat.simplex.common.views.chatlist.NavigationBarBackground import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch @Composable -fun TerminalView(floating: Boolean = false, close: () -> Unit) { +fun TerminalView(floating: Boolean = false) { val composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = false)) } - val close = { - close() - if (appPlatform.isDesktop) { - ModalManager.center.closeModals() - } - } - BackHandler(onBack = { - close() - }) TerminalLayout( composeState, floating, sendCommand = { sendCommand(chatModel, composeState) }, - close ) } @@ -69,7 +63,6 @@ fun TerminalLayout( composeState: MutableState, floating: Boolean, sendCommand: () -> Unit, - close: () -> Unit ) { val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground) val textStyle = remember { mutableStateOf(smallFont) } @@ -77,65 +70,63 @@ fun TerminalLayout( fun onMessageChange(s: String) { composeState.value = composeState.value.copy(message = s) } - - ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { - Scaffold( - topBar = { CloseSheetBar(close) }, - bottomBar = { - Column { - Divider() - Box(Modifier.padding(horizontal = 8.dp)) { - SendMsgView( - composeState = composeState, - showVoiceRecordIcon = false, - recState = remember { mutableStateOf(RecordingState.NotStarted) }, - isDirectChat = false, - liveMessageAlertShown = SharedPreference(get = { false }, set = {}), - sendMsgEnabled = true, - sendButtonEnabled = true, - nextSendGrpInv = false, - needToAllowVoiceToContact = false, - allowedVoiceByPrefs = false, - userIsObserver = false, - userCanSend = true, - allowVoiceToContact = {}, - placeholder = "", - sendMessage = { sendCommand() }, - sendLiveMessage = null, - updateLiveMessage = null, - editPrevMessage = {}, - onMessageChange = ::onMessageChange, - onFilesPasted = {}, - textStyle = textStyle - ) - } - } - }, - contentColor = LocalContentColor.current, - modifier = Modifier.navigationBarsWithImePadding() - ) { contentPadding -> - Surface( - modifier = Modifier - .padding(contentPadding) - .fillMaxWidth(), - color = MaterialTheme.colors.background, - contentColor = LocalContentColor.current + val oneHandUI = remember { appPrefs.oneHandUI.state } + Box(Modifier.fillMaxSize()) { + val composeViewHeight = remember { mutableStateOf(0.dp) } + AdaptingBottomPaddingLayout(Modifier, CONSOLE_COMPOSE_LAYOUT_ID, composeViewHeight) { + TerminalLog(floating, composeViewHeight) + Column( + Modifier + .layoutId(CONSOLE_COMPOSE_LAYOUT_ID) + .align(Alignment.BottomCenter) + .navigationBarsPadding() + .consumeWindowInsets(PaddingValues(bottom = if (oneHandUI.value) AppBarHeight * fontSizeSqrtMultiplier else 0.dp)) + .imePadding() + .padding(bottom = if (oneHandUI.value) AppBarHeight * fontSizeSqrtMultiplier else 0.dp) + .background(MaterialTheme.colors.background) ) { - TerminalLog(floating) + Divider() + Box(Modifier.padding(horizontal = 8.dp)) { + SendMsgView( + composeState = composeState, + showVoiceRecordIcon = false, + recState = remember { mutableStateOf(RecordingState.NotStarted) }, + isDirectChat = false, + liveMessageAlertShown = SharedPreference(get = { false }, set = {}), + sendMsgEnabled = true, + sendButtonEnabled = true, + nextSendGrpInv = false, + needToAllowVoiceToContact = false, + allowedVoiceByPrefs = false, + userIsObserver = false, + userCanSend = true, + allowVoiceToContact = {}, + placeholder = "", + sendMessage = { sendCommand() }, + sendLiveMessage = null, + updateLiveMessage = null, + editPrevMessage = {}, + onMessageChange = ::onMessageChange, + onFilesPasted = {}, + textStyle = textStyle + ) + } } } + if (!oneHandUI.value) { + NavigationBarBackground(true, oneHandUI.value) + } } } @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 = true + var autoScrollToBottom = listState.firstVisibleItemIndex <= 1 launch { snapshotFlow { listState.layoutInfo.totalItemsCount } .filter { autoScrollToBottom } @@ -150,12 +141,21 @@ fun TerminalLog(floating: Boolean) { launch { snapshotFlow { listState.firstVisibleItemIndex } .collect { - autoScrollToBottom = listState.firstVisibleItemIndex == 0 + autoScrollToBottom = it == 0 } } } - LazyColumnWithScrollBar(reverseLayout = true, state = listState) { + LazyColumnWithScrollBar ( + reverseLayout = true, + contentPadding = PaddingValues( + top = topPaddingToContent(), + bottom = composeViewHeight.value + ), + state = listState, + additionalBarOffset = composeViewHeight + ) { items(reversedTerminalItems, key = { item -> item.id to item.createdAtNanos }) { item -> + val clipboard = LocalClipboardManager.current val rhId = item.remoteHostId val rhIdStr = if (rhId == null) "" else "$rhId " Text( @@ -172,13 +172,15 @@ fun TerminalLog(floating: Boolean) { ModalManager.start } modalPlace.showModal(endButtons = { ShareButton { clipboard.shareText(item.details) } }) { - SelectionContainer(modifier = Modifier.verticalScroll(rememberScrollState())) { - val details = item.details - .let { - if (it.length < 100_000) it - else it.substring(0, 100_000) - } - Text(details, modifier = Modifier.heightIn(max = 50_000.dp).padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING)) + ColumnWithScrollBar { + SelectionContainer { + val details = item.details + .let { + if (it.length < 100_000) it + else it.substring(0, 100_000) + } + Text(details, modifier = Modifier.heightIn(max = 50_000.dp).padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING)) + } } } }.padding(horizontal = 8.dp, vertical = 4.dp) @@ -208,8 +210,7 @@ fun PreviewTerminalLayout() { TerminalLayout( composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }, sendCommand = {}, - floating = false, - close = {} + floating = false ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt index 0d5350dbe0..e1e3dcb56b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt @@ -40,8 +40,6 @@ fun CreateProfile(chatModel: ChatModel, close: () -> Unit) { val scrollState = rememberScrollState() val keyboardState by getKeyboardState() var savedKeyboardState by remember { mutableStateOf(keyboardState) } - - ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { Box( modifier = Modifier .fillMaxSize() @@ -50,11 +48,9 @@ fun CreateProfile(chatModel: ChatModel, close: () -> Unit) { val displayName = rememberSaveable { mutableStateOf("") } val focusRequester = remember { FocusRequester() } - ColumnWithScrollBar( - modifier = Modifier.fillMaxSize() - ) { + ColumnWithScrollBar { Column(Modifier.padding(horizontal = DEFAULT_PADDING)) { - AppBarTitle(stringResource(MR.strings.create_profile), bottomPadding = DEFAULT_PADDING) + AppBarTitle(stringResource(MR.strings.create_profile), withPadding = false, bottomPadding = DEFAULT_PADDING) Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Text( stringResource(MR.strings.display_name), @@ -102,7 +98,6 @@ fun CreateProfile(chatModel: ChatModel, close: () -> Unit) { } } } - } } @Composable @@ -111,59 +106,42 @@ fun CreateFirstProfile(chatModel: ChatModel, close: () -> Unit) { val scrollState = rememberScrollState() val keyboardState by getKeyboardState() var savedKeyboardState by remember { mutableStateOf(keyboardState) } - val handler = remember { AppBarHandler() } - CompositionLocalProvider( - LocalAppBarHandler provides handler - ) { - ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { - Column( - modifier = Modifier - .fillMaxSize() - .themedBackground(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - CloseSheetBar(close = { - if (chatModel.users.none { !it.user.hidden }) { - appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) - } else { - close() + CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) { + ModalView({ + if (chatModel.users.none { !it.user.hidden }) { + appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) + } else { + close() + } + }) { + ColumnWithScrollBar { + val displayName = rememberSaveable { mutableStateOf("") } + val focusRequester = remember { FocusRequester() } + Column(if (appPlatform.isAndroid) Modifier.fillMaxSize().padding(horizontal = DEFAULT_PADDING) else Modifier.widthIn(max = 600.dp).fillMaxHeight().padding(horizontal = DEFAULT_PADDING).align(Alignment.CenterHorizontally)) { + Box(Modifier.align(Alignment.CenterHorizontally)) { + AppBarTitle(stringResource(MR.strings.create_profile), bottomPadding = DEFAULT_PADDING, withPadding = false) } - }) - BackHandler(onBack = { - appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) - }) + ProfileNameField(displayName, stringResource(MR.strings.display_name), { it.trim() == mkValidName(it) }, focusRequester) + Spacer(Modifier.height(DEFAULT_PADDING)) + ReadableText(MR.strings.your_profile_is_stored_on_your_device, TextAlign.Start, padding = PaddingValues(), style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary)) + ReadableText(MR.strings.profile_is_only_shared_with_your_contacts, TextAlign.Start, style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary)) + } + Spacer(Modifier.fillMaxHeight().weight(1f)) + Column(Modifier.widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) { + OnboardingActionButton( + if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_PADDING * 2).fillMaxWidth() else Modifier.widthIn(min = 300.dp), + labelId = MR.strings.create_profile_button, + onboarding = null, + enabled = canCreateProfile(displayName.value), + onclick = { createProfileOnboarding(chat.simplex.common.platform.chatModel, displayName.value, close) } + ) + // Reserve space + TextButtonBelowOnboardingButton("", null) + } - ColumnWithScrollBar( - modifier = Modifier.fillMaxSize() - ) { - val displayName = rememberSaveable { mutableStateOf("") } - val focusRequester = remember { FocusRequester() } - Column(if (appPlatform.isAndroid) Modifier.fillMaxSize().padding(horizontal = DEFAULT_PADDING) else Modifier.widthIn(max = 600.dp).fillMaxHeight().padding(horizontal = DEFAULT_PADDING).align(Alignment.CenterHorizontally)) { - Box(Modifier.align(Alignment.CenterHorizontally)) { - AppBarTitle(stringResource(MR.strings.create_profile), bottomPadding = DEFAULT_PADDING, withPadding = false) - } - ProfileNameField(displayName, stringResource(MR.strings.display_name), { it.trim() == mkValidName(it) }, focusRequester) - Spacer(Modifier.height(DEFAULT_PADDING)) - ReadableText(MR.strings.your_profile_is_stored_on_your_device, TextAlign.Start, padding = PaddingValues(), style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary)) - ReadableText(MR.strings.profile_is_only_shared_with_your_contacts, TextAlign.Start, style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary)) - } - Spacer(Modifier.fillMaxHeight().weight(1f)) - Column(Modifier.widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) { - OnboardingActionButton( - if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_PADDING * 2).fillMaxWidth() else Modifier.widthIn(min = 300.dp), - labelId = MR.strings.create_profile_button, - onboarding = null, - enabled = canCreateProfile(displayName.value), - onclick = { createProfileOnboarding(chat.simplex.common.platform.chatModel, displayName.value, close) } - ) - // Reserve space - TextButtonBelowOnboardingButton("", null) - } - - LaunchedEffect(Unit) { - delay(300) - focusRequester.requestFocus() - } + LaunchedEffect(Unit) { + delay(300) + focusRequester.requestFocus() } } LaunchedEffect(Unit) { @@ -255,7 +233,6 @@ fun ProfileNameField(name: MutableState, placeholder: String = "", isVal val modifier = Modifier .fillMaxWidth() .heightIn(min = 50.dp) - .navigationBarsWithImePadding() .onFocusChanged { focused = it.isFocused } Column( Modifier diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/IncomingCallAlertView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/IncomingCallAlertView.kt index 32681234fa..4d8c1fae46 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/IncomingCallAlertView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/IncomingCallAlertView.kt @@ -53,7 +53,7 @@ fun IncomingCallAlertLayout( acceptCall: () -> Unit ) { val color = if (isInDarkTheme()) MaterialTheme.colors.surface else IncomingCallLight - Column(Modifier.fillMaxWidth().background(color).padding(top = DEFAULT_PADDING, bottom = DEFAULT_PADDING, start = DEFAULT_PADDING, end = 8.dp)) { + Column(Modifier.fillMaxWidth().background(color).statusBarsPadding().padding(top = DEFAULT_PADDING, bottom = DEFAULT_PADDING, start = DEFAULT_PADDING, end = 8.dp)) { IncomingCallInfo(invitation, chatModel) Spacer(Modifier.height(8.dp)) Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt index 9149b039ef..df13368900 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt @@ -529,10 +529,7 @@ fun ChatInfoLayout( KeyChangeEffect(chat.id) { scope.launch { scrollState.scrollTo(0) } } - ColumnWithScrollBar( - Modifier - .fillMaxWidth() - ) { + ColumnWithScrollBar { Row( Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center 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 bdbfdb89c3..30bbe72a72 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 @@ -276,7 +276,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools @Composable fun HistoryTab() { - ColumnWithScrollBar(Modifier.fillMaxWidth()) { + ColumnWithScrollBar { Details() SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true) val versions = ciInfo.itemVersions @@ -300,7 +300,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools @Composable fun QuoteTab(qi: CIQuote) { - ColumnWithScrollBar(Modifier.fillMaxWidth()) { + ColumnWithScrollBar { Details() SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true) SectionView(contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) { @@ -313,7 +313,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools @Composable fun ForwardedFromTab(forwardedFromItem: AChatItem) { - ColumnWithScrollBar(Modifier.fillMaxWidth()) { + ColumnWithScrollBar { Details() SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true) SectionView { @@ -375,7 +375,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools @Composable fun DeliveryTab(memberDeliveryStatuses: List) { - ColumnWithScrollBar(Modifier.fillMaxWidth()) { + ColumnWithScrollBar { Details() SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true) val mss = membersStatuses(chatModel, memberDeliveryStatuses) 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 570f763e99..2c43a81f7d 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 @@ -12,10 +12,11 @@ import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.* -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.draw.* import androidx.compose.ui.graphics.* +import androidx.compose.ui.graphics.layer.GraphicsLayer import androidx.compose.ui.layout.layoutId +import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.* import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource @@ -108,6 +109,7 @@ fun ChatView(staleChatId: State, onComposed: suspend (chatId: String) - } } val clipboard = LocalClipboardManager.current + CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler(chatInfo.id, keyboardCoversBar = false)) { when (chatInfo) { is ChatInfo.Direct, is ChatInfo.Group, is ChatInfo.Local -> { val perChatTheme = remember(chatInfo, CurrentColors.value.base) { if (chatInfo is ChatInfo.Direct) chatInfo.contact.uiThemes?.preferredMode(!CurrentColors.value.colors.isLight) else if (chatInfo is ChatInfo.Group) chatInfo.groupInfo.uiThemes?.preferredMode(!CurrentColors.value.colors.isLight) else null } @@ -523,28 +525,10 @@ fun ChatView(staleChatId: State, onComposed: suspend (chatId: String) - showViaProxy = chatModel.controller.appPrefs.showSentViaProxy.get(), showSearch = showSearch ) - if (appPlatform.isAndroid) { - val backgroundColor = MaterialTheme.colors.background - val backgroundColorState = rememberUpdatedState(backgroundColor) - LaunchedEffect(Unit) { - snapshotFlow { ModalManager.center.modalCount.value > 0 } - .collect { modalBackground -> - if (modalBackground) { - platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, CurrentColors.value.colors.background, false, false) - } else { - platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, backgroundColorState.value, true, false) - } - } - } - } } } is ChatInfo.ContactConnection -> { val close = { chatModel.chatId.value = null } - val handler = remember { AppBarHandler() } - CompositionLocalProvider( - LocalAppBarHandler provides handler - ) { ModalView(close, showClose = appPlatform.isAndroid, content = { ContactConnectionInfoView(chatModel, chatRh, chatInfo.contactConnection.connReqInv, chatInfo.contactConnection, false, close) }) @@ -553,14 +537,9 @@ fun ChatView(staleChatId: State, onComposed: suspend (chatId: String) - ModalManager.end.closeModals() chatModel.chatItems.clear() } - } } is ChatInfo.InvalidJSON -> { val close = { chatModel.chatId.value = null } - val handler = remember { AppBarHandler() } - CompositionLocalProvider( - LocalAppBarHandler provides handler - ) { ModalView(close, showClose = appPlatform.isAndroid, endButtons = { ShareButton { clipboard.shareText(chatInfo.json) } }, content = { InvalidJSONView(chatInfo.json) }) @@ -569,10 +548,10 @@ fun ChatView(staleChatId: State, onComposed: suspend (chatId: String) - ModalManager.end.closeModals() chatModel.chatItems.clear() } - } } else -> {} } + } } } @@ -649,67 +628,60 @@ fun ChatLayout( }, ) ) { - ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { - ModalBottomSheetLayout( - scrimColor = Color.Black.copy(alpha = 0.12F), - modifier = Modifier.navigationBarsWithImePadding(), - sheetElevation = 0.dp, - sheetContent = { - ChooseAttachmentView( - attachmentOption, - hide = { scope.launch { attachmentBottomSheetState.hide() } } - ) - }, - sheetState = attachmentBottomSheetState, - sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp) - ) { - val floatingButton: MutableState<@Composable () -> Unit> = remember { mutableStateOf({}) } - val setFloatingButton = { button: @Composable () -> Unit -> - floatingButton.value = button - } - - Scaffold( - topBar = { - if (selectedChatItems.value == null) { - val chatInfo = chatInfo.value - if (chatInfo != null) { - ChatInfoToolbar(chatInfo, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged, showSearch) - } - } else { - SelectedItemsTopToolbar(selectedChatItems) - } - }, - bottomBar = composeView, - modifier = Modifier.navigationBarsWithImePadding(), - floatingActionButton = { floatingButton.value() }, - contentColor = LocalContentColor.current, - backgroundColor = Color.Unspecified - ) { contentPadding -> - val wallpaperImage = MaterialTheme.wallpaper.type.image - val wallpaperType = MaterialTheme.wallpaper.type - val backgroundColor = MaterialTheme.wallpaper.background ?: wallpaperType.defaultBackgroundColor(CurrentColors.value.base, MaterialTheme.colors.background) - val tintColor = MaterialTheme.wallpaper.tint ?: wallpaperType.defaultTintColor(CurrentColors.value.base) - BoxWithConstraints(Modifier - .fillMaxSize() - .background(MaterialTheme.colors.background) - .then(if (wallpaperImage != null) - Modifier.drawWithCache { chatViewBackground(wallpaperImage, wallpaperType, backgroundColor, tintColor) } - else - Modifier) - .padding(contentPadding) - ) { - val remoteHostId = remember { remoteHostId }.value - val chatInfo = remember { chatInfo }.value - if (chatInfo != null) { + ModalBottomSheetLayout( + scrimColor = Color.Black.copy(alpha = 0.12F), + sheetElevation = 0.dp, + sheetContent = { + ChooseAttachmentView( + attachmentOption, + hide = { scope.launch { attachmentBottomSheetState.hide() } } + ) + }, + sheetState = attachmentBottomSheetState, + sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp) + ) { + val composeViewHeight = remember { mutableStateOf(0.dp) } + Box(Modifier.fillMaxSize().chatViewBackgroundModifier(MaterialTheme.colors, MaterialTheme.wallpaper, LocalAppBarHandler.current?.backgroundGraphicsLayerSize, LocalAppBarHandler.current?.backgroundGraphicsLayer)) { + val remoteHostId = remember { remoteHostId }.value + val chatInfo = remember { chatInfo }.value + AdaptingBottomPaddingLayout(Modifier, CHAT_COMPOSE_LAYOUT_ID, composeViewHeight) { + if (chatInfo != null) { + Box(Modifier.fillMaxSize()) { ChatItemsList( - remoteHostId, chatInfo, unreadCount, composeState, searchValue, + remoteHostId, chatInfo, unreadCount, composeState, composeViewHeight, searchValue, useLinkPreviews, linkMode, selectedChatItems, showMemberInfo, loadPrevMessages, deleteMessage, deleteMessages, receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem, updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember, - setReaction, showItemDetails, markRead, setFloatingButton, onComposed, developerTools, showViaProxy, + setReaction, showItemDetails, markRead, remember { { onComposed(it) } }, developerTools, showViaProxy, ) } } + val oneHandUI = remember { appPrefs.oneHandUI.state } + Box( + Modifier + .layoutId(CHAT_COMPOSE_LAYOUT_ID) + .align(Alignment.BottomCenter) + .imePadding() + .navigationBarsPadding() + .then(if (oneHandUI.value) Modifier.padding(bottom = AppBarHeight * fontSizeSqrtMultiplier) else Modifier) + ) { + composeView() + } + } + val oneHandUI = remember { appPrefs.oneHandUI.state } + if (oneHandUI.value) { + StatusBarBackground() + } else { + NavigationBarBackground(true, oneHandUI.value, noAlpha = true) + } + Box(if (oneHandUI.value) Modifier.align(Alignment.BottomStart).imePadding() else Modifier) { + if (selectedChatItems.value == null) { + if (chatInfo != null) { + ChatInfoToolbar(chatInfo, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged, showSearch) + } + } else { + SelectedItemsTopToolbar(selectedChatItems) + } } } } @@ -717,7 +689,7 @@ fun ChatLayout( } @Composable -fun ChatInfoToolbar( +fun BoxScope.ChatInfoToolbar( chatInfo: ChatInfo, back: () -> Unit, info: () -> Unit, @@ -869,21 +841,33 @@ fun ChatInfoToolbar( } } } - - DefaultTopAppBar( + val oneHandUI = remember { appPrefs.oneHandUI.state } + DefaultAppBar( navigationButton = { if (appPlatform.isAndroid || showSearch.value) { NavigationButtonBack(onBackClicked) } }, title = { ChatInfoToolbarTitle(chatInfo) }, onTitleClick = if (chatInfo is ChatInfo.Local) null else info, showSearch = showSearch.value, + onTop = !oneHandUI.value, onSearchValueChanged = onSearchValueChanged, - buttons = barButtons + buttons = { barButtons.forEach { it() } } ) - - Divider(Modifier.padding(top = AppBarHeight * fontSizeSqrtMultiplier)) - - Box(Modifier.fillMaxWidth().wrapContentSize(Alignment.TopEnd).offset(y = AppBarHeight * fontSizeSqrtMultiplier)) { - DefaultDropdownMenu(showMenu) { - menuItems.forEach { it() } + Box(Modifier.fillMaxWidth().wrapContentSize(Alignment.TopEnd)) { + val density = LocalDensity.current + val width = remember { mutableStateOf(250.dp) } + val height = remember { mutableStateOf(0.dp) } + DefaultDropdownMenu( + showMenu, + modifier = Modifier.onSizeChanged { with(density) { + width.value = it.width.toDp().coerceAtLeast(250.dp) + if (oneHandUI.value && (appPlatform.isDesktop || (platform.androidApiLevel ?: 0) >= 30)) height.value = it.height.toDp() + } }, + offset = DpOffset(-width.value, if (oneHandUI.value) -height.value else AppBarHeight) + ) { + if (oneHandUI.value) { + menuItems.asReversed().forEach { it() } + } else { + menuItems.forEach { it() } + } } } } @@ -927,11 +911,12 @@ private fun ContactVerifiedShield() { } @Composable -fun BoxWithConstraintsScope.ChatItemsList( +fun BoxScope.ChatItemsList( remoteHostId: Long?, chatInfo: ChatInfo, unreadCount: State, composeState: MutableState, + composeViewHeight: State, searchValue: State, useLinkPreviews: Boolean, linkMode: SimplexLinkMode, @@ -956,7 +941,6 @@ fun BoxWithConstraintsScope.ChatItemsList( setReaction: (ChatInfo, ChatItem, Boolean, MsgReaction) -> Unit, showItemDetails: (ChatInfo, ChatItem) -> Unit, markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit, - setFloatingButton: (@Composable () -> Unit) -> Unit, onComposed: suspend (chatId: String) -> Unit, developerTools: Boolean, showViaProxy: Boolean @@ -980,13 +964,18 @@ fun BoxWithConstraintsScope.ChatItemsList( PreloadItems(chatInfo.id, listState, ChatPagination.UNTIL_PRELOAD_COUNT, loadPrevMessages) Spacer(Modifier.size(8.dp)) - val reversedChatItems by remember { derivedStateOf { chatModel.chatItems.asReversed() } } - val maxHeightRounded = with(LocalDensity.current) { maxHeight.roundToPx() } - val scrollToItem: (Long) -> Unit = { itemId: Long -> - val index = reversedChatItems.indexOfFirst { it.id == itemId } - if (index != -1) { - scope.launch { listState.animateScrollToItem(kotlin.math.min(reversedChatItems.lastIndex, index + 1), -maxHeightRounded) } - } + val reversedChatItems = remember { derivedStateOf { chatModel.chatItems.asReversed() } } + val topPaddingToContentPx = rememberUpdatedState(with(LocalDensity.current) { topPaddingToContent().roundToPx() }) + val maxHeight = remember { derivedStateOf { listState.layoutInfo.viewportEndOffset - topPaddingToContentPx.value } } + val scrollToItem: State<(Long) -> Unit> = remember { + mutableStateOf( + { itemId: Long -> + val index = reversedChatItems.value.indexOfFirst { it.id == itemId } + if (index != -1) { + scope.launch { listState.animateScrollToItem(kotlin.math.min(reversedChatItems.value.lastIndex, index + 1), -maxHeight.value) } + } + } + ) } // TODO: Having this block on desktop makes ChatItemsList() to recompose twice on chatModel.chatId update instead of once LaunchedEffect(chatInfo.id) { @@ -1004,8 +993,18 @@ fun BoxWithConstraintsScope.ChatItemsList( VideoPlayerHolder.releaseAll() } ) - LazyColumnWithScrollBar(Modifier.align(Alignment.BottomCenter), state = listState, reverseLayout = true) { - itemsIndexed(reversedChatItems, key = { _, item -> item.id to item.meta.createdAt.toEpochMilliseconds() }) { i, cItem -> + LazyColumnWithScrollBar( + Modifier.align(Alignment.BottomCenter), + state = listState, + reverseLayout = true, + contentPadding = PaddingValues( + top = topPaddingToContent(), + bottom = composeViewHeight.value + ), + additionalBarOffset = composeViewHeight + ) { + itemsIndexed(reversedChatItems.value, key = { _, item -> item.id to item.meta.createdAt.toEpochMilliseconds() }) { i, cItem -> + val itemScope = rememberCoroutineScope() CompositionLocalProvider( // Makes horizontal and vertical scrolling to coexist nicely. // With default touchSlop when you scroll LazyColumn, you can unintentionally open reply view @@ -1013,10 +1012,10 @@ fun BoxWithConstraintsScope.ChatItemsList( ) { val provider = { providerForGallery(i, chatModel.chatItems.value, cItem.id) { indexInReversed -> - scope.launch { + itemScope.launch { listState.scrollToItem( - kotlin.math.min(reversedChatItems.lastIndex, indexInReversed + 1), - -maxHeightRounded + kotlin.math.min(reversedChatItems.value.lastIndex, indexInReversed + 1), + -maxHeight.value ) } } @@ -1029,7 +1028,7 @@ fun BoxWithConstraintsScope.ChatItemsList( tryOrShowError("${cItem.id}ChatItem", error = { CIBrokenComposableView(if (cItem.chatDir.sent) Alignment.CenterEnd else Alignment.CenterStart) }) { - ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp) + ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem.value, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp) } } @@ -1037,7 +1036,7 @@ fun BoxWithConstraintsScope.ChatItemsList( fun ChatItemView(cItem: ChatItem, range: IntRange?, prevItem: ChatItem?, itemSeparation: ItemSeparation, previousItemSeparation: ItemSeparation?) { val dismissState = rememberDismissState(initialValue = DismissValue.Default) { if (it == DismissValue.DismissedToStart) { - scope.launch { + itemScope.launch { if ((cItem.content is CIContent.SndMsgContent || cItem.content is CIContent.RcvMsgContent) && chatInfo !is ChatInfo.Local) { if (composeState.value.editing) { composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews) @@ -1234,16 +1233,17 @@ fun BoxWithConstraintsScope.ChatItemsList( } val range = chatViewItemsRange(currIndex, prevHidden) + val reversed = reversedChatItems.value if (revealed.value && range != null) { - reversedChatItems.subList(range.first, range.last + 1).forEachIndexed { index, ci -> - val prev = if (index + range.first == prevHidden) prevItem else reversedChatItems[index + range.first + 1] + reversed.subList(range.first, range.last + 1).forEachIndexed { index, ci -> + val prev = if (index + range.first == prevHidden) prevItem else reversed[index + range.first + 1] ChatItemView(ci, null, prev, itemSeparation, previousItemSeparation) } } else { ChatItemView(cItem, range, prevItem, itemSeparation, previousItemSeparation) } - if (i == reversedChatItems.lastIndex) { + if (i == reversed.lastIndex) { DateSeparator(cItem.meta.itemTs) } } @@ -1251,7 +1251,7 @@ fun BoxWithConstraintsScope.ChatItemsList( if (cItem.isRcvNew && chatInfo.id == ChatModel.chatId.value) { LaunchedEffect(cItem.id) { - scope.launch { + itemScope.launch { delay(600) markRead(CC.ItemRange(cItem.id, cItem.id), null) } @@ -1260,10 +1260,10 @@ fun BoxWithConstraintsScope.ChatItemsList( } } } - FloatingButtons(chatModel.chatItems, unreadCount, remoteHostId, chatInfo, searchValue, markRead, setFloatingButton, listState) + FloatingButtons(chatModel.chatItems, unreadCount, composeViewHeight, remoteHostId, chatInfo, searchValue, markRead, listState) FloatingDate( - Modifier.padding(top = 10.dp).align(Alignment.TopCenter), + Modifier.padding(top = 10.dp + topPaddingToContent()).align(Alignment.TopCenter), listState, ) @@ -1318,87 +1318,65 @@ private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems: } @Composable -fun BoxWithConstraintsScope.FloatingButtons( +fun BoxScope.FloatingButtons( chatItems: State>, unreadCount: State, + composeViewHeight: State, remoteHostId: Long?, chatInfo: ChatInfo, searchValue: State, markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit, - setFloatingButton: (@Composable () -> Unit) -> Unit, listState: LazyListState ) { val scope = rememberCoroutineScope() - var firstVisibleIndex by remember { mutableStateOf(listState.firstVisibleItemIndex) } - var lastIndexOfVisibleItems by remember { mutableStateOf(listState.layoutInfo.visibleItemsInfo.lastIndex) } - var firstItemIsVisible by remember { mutableStateOf(firstVisibleIndex == 0) } - - LaunchedEffect(listState) { - snapshotFlow { listState.firstVisibleItemIndex } - .distinctUntilChanged() - .collect { - firstVisibleIndex = it - firstItemIsVisible = firstVisibleIndex == 0 - } - } - - LaunchedEffect(listState) { - // When both snapshotFlows located in one LaunchedEffect second block will never be called because coroutine is paused on first block - // so separate them into two LaunchedEffects - snapshotFlow { listState.layoutInfo.visibleItemsInfo.lastIndex } - .distinctUntilChanged() - .collect { - lastIndexOfVisibleItems = it - } - } - val bottomUnreadCount by remember { + val maxHeight = remember { derivedStateOf { listState.layoutInfo.viewportSize.height } } + val bottomUnreadCount = remember { derivedStateOf { if (unreadCount.value == 0) return@derivedStateOf 0 val items = chatItems.value - val from = items.lastIndex - firstVisibleIndex - lastIndexOfVisibleItems + val from = items.lastIndex - listState.firstVisibleItemIndex - listState.layoutInfo.visibleItemsInfo.lastIndex if (items.size <= from || from < 0) return@derivedStateOf 0 items.subList(from, items.size).count { it.isRcvNew } } } - val firstVisibleOffset = (-with(LocalDensity.current) { maxHeight.roundToPx() } * 0.8).toInt() - LaunchedEffect(bottomUnreadCount, firstItemIsVisible) { - val showButtonWithCounter = bottomUnreadCount > 0 && !firstItemIsVisible && searchValue.value.isEmpty() - val showButtonWithArrow = !showButtonWithCounter && !firstItemIsVisible - setFloatingButton( - bottomEndFloatingButton( - bottomUnreadCount, - showButtonWithCounter, - showButtonWithArrow, - onClickArrowDown = { - scope.launch { listState.animateScrollToItem(0) } - }, - onClickCounter = { - scope.launch { listState.animateScrollToItem(kotlin.math.max(0, bottomUnreadCount - 1), firstVisibleOffset) } - } - )) - } + val showBottomButtonWithCounter = remember { derivedStateOf { bottomUnreadCount.value > 0 && listState.firstVisibleItemIndex != 0 && searchValue.value.isEmpty() } } + val showBottomButtonWithArrow = remember { derivedStateOf { !showBottomButtonWithCounter.value && listState.firstVisibleItemIndex != 0 } } + BottomEndFloatingButton( + bottomUnreadCount, + showBottomButtonWithCounter, + showBottomButtonWithArrow, + composeViewHeight, + onClickArrowDown = { + scope.launch { listState.animateScrollToItem(0) } + }, + onClickCounter = { + val firstVisibleOffset = (-maxHeight.value * 0.8).toInt() + scope.launch { listState.animateScrollToItem(kotlin.math.max(0, bottomUnreadCount.value - 1), firstVisibleOffset) } + } + ) // Don't show top FAB if is in search if (searchValue.value.isNotEmpty()) return val fabSize = 56.dp - val topUnreadCount by remember { - derivedStateOf { unreadCount.value - bottomUnreadCount } - } - val showButtonWithCounter = topUnreadCount > 0 - val height = with(LocalDensity.current) { maxHeight.toPx() } + val topUnreadCount = remember { derivedStateOf { unreadCount.value - bottomUnreadCount.value } } val showDropDown = remember { mutableStateOf(false) } TopEndFloatingButton( - Modifier.padding(end = DEFAULT_PADDING, top = 24.dp).align(Alignment.TopEnd), + Modifier.padding(end = DEFAULT_PADDING, top = 24.dp + topPaddingToContent()).align(Alignment.TopEnd), topUnreadCount, - showButtonWithCounter, - onClick = { scope.launch { listState.animateScrollBy(height) } }, + onClick = { scope.launch { listState.animateScrollBy(maxHeight.value.toFloat()) } }, onLongClick = { showDropDown.value = true } ) - Box { - DefaultDropdownMenu(showDropDown, offset = DpOffset(this@FloatingButtons.maxWidth - DEFAULT_PADDING, 24.dp + fabSize)) { + Box(Modifier.fillMaxWidth().wrapContentSize(Alignment.TopEnd)) { + val density = LocalDensity.current + val width = remember { mutableStateOf(250.dp) } + DefaultDropdownMenu( + showDropDown, + modifier = Modifier.onSizeChanged { with(density) { width.value = it.width.toDp().coerceAtLeast(250.dp) } }, + offset = DpOffset(-DEFAULT_PADDING - width.value, 24.dp + fabSize + topPaddingToContent()) + ) { ItemAction( generalGetString(MR.strings.mark_read), painterResource(MR.images.ic_check), @@ -1406,7 +1384,7 @@ fun BoxWithConstraintsScope.FloatingButtons( val minUnreadItemId = chatModel.chats.value.firstOrNull { it.remoteHostId == remoteHostId && it.id == chatInfo.id }?.chatStats?.minUnreadItemId ?: return@ItemAction markRead( CC.ItemRange(minUnreadItemId, chatItems.value[chatItems.value.size - listState.layoutInfo.visibleItemsInfo.lastIndex - 1].id - 1), - bottomUnreadCount + bottomUnreadCount.value ) showDropDown.value = false }) @@ -1468,12 +1446,11 @@ fun MemberImage(member: GroupMember) { @Composable private fun TopEndFloatingButton( modifier: Modifier = Modifier, - unreadCount: Int, - showButtonWithCounter: Boolean, + unreadCount: State, onClick: () -> Unit, onLongClick: () -> Unit ) = when { - showButtonWithCounter -> { + unreadCount.value > 0 -> { val interactionSource = interactionSourceWithDetection(onClick, onLongClick) FloatingActionButton( {}, // no action here @@ -1483,7 +1460,7 @@ private fun TopEndFloatingButton( interactionSource = interactionSource, ) { Text( - unreadCountStr(unreadCount), + unreadCountStr(unreadCount.value), color = MaterialTheme.colors.primary, fontSize = 14.sp, ) @@ -1493,6 +1470,16 @@ private fun TopEndFloatingButton( } } +@Composable +fun topPaddingToContent(): Dp { + val oneHandUI = remember { appPrefs.oneHandUI.state } + return if (oneHandUI.value) { + WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + } else { + AppBarHeight * fontSizeSqrtMultiplier + WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + } +} + @Composable private fun FloatingDate( modifier: Modifier, @@ -1502,8 +1489,9 @@ private fun FloatingDate( var isNearBottom by remember { mutableStateOf(true) } val lastVisibleItemDate = remember { derivedStateOf { - if (listState.layoutInfo.visibleItemsInfo.lastIndex >= 0 && listState.firstVisibleItemIndex >= 0) { - val lastVisibleChatItemIndex = chatModel.chatItems.value.lastIndex - listState.firstVisibleItemIndex - listState.layoutInfo.visibleItemsInfo.lastIndex + if (listState.layoutInfo.visibleItemsInfo.lastIndex >= 0) { + val lastFullyVisibleOffset = listState.layoutInfo.viewportEndOffset + val lastVisibleChatItemIndex = chatModel.chatItems.value.lastIndex - (listState.layoutInfo.visibleItemsInfo.lastOrNull { item -> item.offset + item.size <= lastFullyVisibleOffset && item.size > 0 }?.index ?: 0) val item = chatModel.chatItems.value.getOrNull(lastVisibleChatItemIndex) val timeZone = TimeZone.currentSystemDefault() item?.meta?.itemTs?.toLocalDateTime(timeZone)?.date?.atStartOfDayIn(timeZone) @@ -1690,48 +1678,44 @@ fun openGroupLink(groupInfo: GroupInfo, rhId: Long?, view: Any? = null, close: ( } } -private fun bottomEndFloatingButton( - unreadCount: Int, - showButtonWithCounter: Boolean, - showButtonWithArrow: Boolean, +@Composable +private fun BoxScope.BottomEndFloatingButton( + unreadCount: State, + showButtonWithCounter: State, + showButtonWithArrow: State, + composeViewHeight: State, onClickArrowDown: () -> Unit, onClickCounter: () -> Unit -): @Composable () -> Unit = when { - showButtonWithCounter -> { - { - FloatingActionButton( - onClick = onClickCounter, - elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp), - modifier = Modifier.size(48.dp), - backgroundColor = MaterialTheme.colors.secondaryVariant, - ) { - Text( - unreadCountStr(unreadCount), - color = MaterialTheme.colors.primary, - fontSize = 14.sp, - ) - } +) = when { + showButtonWithCounter.value -> { + FloatingActionButton( + onClick = onClickCounter, + elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp), + modifier = Modifier.padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING + composeViewHeight.value).align(Alignment.BottomEnd).size(48.dp), + backgroundColor = MaterialTheme.colors.secondaryVariant, + ) { + Text( + unreadCountStr(unreadCount.value), + color = MaterialTheme.colors.primary, + fontSize = 14.sp, + ) } } - showButtonWithArrow -> { - { - FloatingActionButton( - onClick = onClickArrowDown, - elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp), - modifier = Modifier.size(48.dp), - backgroundColor = MaterialTheme.colors.secondaryVariant, - ) { - Icon( - painter = painterResource(MR.images.ic_keyboard_arrow_down), - contentDescription = null, - tint = MaterialTheme.colors.primary - ) - } + showButtonWithArrow.value -> { + FloatingActionButton( + onClick = onClickArrowDown, + elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp), + modifier = Modifier.padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING + composeViewHeight.value).align(Alignment.BottomEnd).size(48.dp), + backgroundColor = MaterialTheme.colors.secondaryVariant, + ) { + Icon( + painter = painterResource(MR.images.ic_keyboard_arrow_down), + contentDescription = null, + tint = MaterialTheme.colors.primary + ) } } - else -> { - {} - } + else -> {} } @Composable @@ -1858,6 +1842,25 @@ private fun memberNames(member: GroupMember, prevMember: GroupMember?, memCount: } } +fun Modifier.chatViewBackgroundModifier( + colors: Colors, + wallpaper: AppWallpaper, + backgroundGraphicsLayerSize: MutableState?, + backgroundGraphicsLayer: GraphicsLayer? +): Modifier { + val wallpaperImage = wallpaper.type.image + val wallpaperType = wallpaper.type + val backgroundColor = wallpaper.background ?: wallpaperType.defaultBackgroundColor(CurrentColors.value.base, colors.background) + val tintColor = wallpaper.tint ?: wallpaperType.defaultTintColor(CurrentColors.value.base) + + return this + .then(if (wallpaperImage != null) + Modifier.drawWithCache { chatViewBackground(wallpaperImage, wallpaperType, backgroundColor, tintColor, backgroundGraphicsLayerSize, backgroundGraphicsLayer) } + else + Modifier.drawWithCache { onDrawBehind { copyBackgroundToAppBar(backgroundGraphicsLayerSize, backgroundGraphicsLayer) { drawRect(backgroundColor) } } } + ) +} + fun chatViewItemsRange(currIndex: Int?, prevHidden: Int?): IntRange? = if (currIndex != null && prevHidden != null && prevHidden > currIndex) { currIndex..prevHidden diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt index cad18af9bb..fd1d3ab92d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt @@ -13,12 +13,14 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.text.font.FontStyle import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource 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.ChatModel.controller import chat.simplex.common.model.ChatModel.filesToDelete import chat.simplex.common.model.ChatModel.withChats @@ -896,7 +898,7 @@ fun ComposeView( } } } - Column(Modifier.background(MaterialTheme.colors.background)) { + Box(Modifier.background(MaterialTheme.colors.background)) { Divider() Row(Modifier.padding(end = 8.dp), verticalAlignment = Alignment.Bottom) { val isGroupAndProhibitedFiles = chat.chatInfo is ChatInfo.Group && !chat.chatInfo.groupInfo.fullGroupPreferences.files.on(chat.chatInfo.groupInfo.membership) @@ -918,7 +920,7 @@ fun ComposeView( && !nextSendGrpInv.value IconButton( attachmentClicked, - Modifier.padding(bottom = if (appPlatform.isAndroid) 2.sp.toDp() else 5.sp.toDp() * fontSizeSqrtMultiplier), + Modifier.padding(start = 3.dp, end = 1.dp, bottom = if (appPlatform.isAndroid) 2.sp.toDp() else 5.sp.toDp() * fontSizeSqrtMultiplier), enabled = attachmentEnabled ) { Icon( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContactPreferences.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContactPreferences.kt index 725367e150..b1e9bf750e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContactPreferences.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContactPreferences.kt @@ -81,10 +81,7 @@ private fun ContactPreferencesLayout( reset: () -> Unit, savePrefs: () -> Unit, ) { - ColumnWithScrollBar( - Modifier - .fillMaxWidth(), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.contact_preferences)) val timedMessages: MutableState = remember(featuresAllowed) { mutableStateOf(featuresAllowed.timedMessagesAllowed) } val onTTLUpdated = { ttl: Int? -> diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ScanCodeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ScanCodeView.kt index 73017c3d42..a12a75b747 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ScanCodeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ScanCodeView.kt @@ -1,9 +1,11 @@ package chat.simplex.common.views.chat +import SectionBottomSpacer import androidx.compose.foundation.layout.* import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import chat.simplex.common.platform.ColumnWithScrollBar import chat.simplex.common.ui.theme.DEFAULT_PADDING import chat.simplex.common.views.helpers.* import chat.simplex.common.views.newchat.QRCodeScanner @@ -12,9 +14,7 @@ import dev.icerock.moko.resources.compose.stringResource @Composable fun ScanCodeView(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () -> Unit) { - Column( - Modifier.fillMaxSize() - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.scan_code)) QRCodeScanner { text -> verifyCode(text) { @@ -28,5 +28,6 @@ fun ScanCodeView(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () } } Text(stringResource(MR.strings.scan_code_from_contacts_app), Modifier.padding(horizontal = DEFAULT_PADDING)) + SectionBottomSpacer() } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SelectableChatItemToolbars.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SelectableChatItemToolbars.kt index 5cf9ebb6c7..838398c503 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SelectableChatItemToolbars.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SelectableChatItemToolbars.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.platform.BackHandler import chat.simplex.common.platform.chatModel import chat.simplex.common.views.helpers.* @@ -20,11 +21,12 @@ import chat.simplex.res.MR import dev.icerock.moko.resources.compose.painterResource @Composable -fun SelectedItemsTopToolbar(selectedChatItems: MutableState?>) { +fun BoxScope.SelectedItemsTopToolbar(selectedChatItems: MutableState?>) { val onBackClicked = { selectedChatItems.value = null } BackHandler(onBack = onBackClicked) val count = selectedChatItems.value?.size ?: 0 - DefaultTopAppBar( + val oneHandUI = remember { appPrefs.oneHandUI.state } + DefaultAppBar( navigationButton = { NavigationButtonClose(onButtonClicked = onBackClicked) }, title = { Text( @@ -39,10 +41,9 @@ fun SelectedItemsTopToolbar(selectedChatItems: MutableState?>) { ) }, onTitleClick = null, - showSearch = false, + onTop = !oneHandUI.value, onSearchValueChanged = {}, ) - Divider(Modifier.padding(top = AppBarHeight * fontSizeSqrtMultiplier)) } @Composable @@ -68,6 +69,8 @@ fun SelectedItemsBottomToolbar( Modifier .matchParentSize() .background(MaterialTheme.colors.background) + .padding(horizontal = 2.dp) + .height(AppBarHeight * fontSizeSqrtMultiplier) .pointerInput(Unit) { detectGesture { true @@ -103,6 +106,7 @@ fun SelectedItemsBottomToolbar( ) } } + Divider(Modifier.align(Alignment.TopStart)) } LaunchedEffect(chatInfo, chatItems, selectedChatItems.value) { recheckItems(chatInfo, chatItems, selectedChatItems, deleteEnabled, deleteForEveryoneEnabled, canModerate, moderateEnabled, forwardEnabled, deleteCountProhibited, forwardCountProhibited) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt index 76c4fc4a62..d912f8e030 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.* @@ -60,7 +61,8 @@ fun SendMsgView( ) { val showCustomDisappearingMessageDialog = remember { mutableStateOf(false) } - Box(Modifier.padding(vertical = if (appPlatform.isAndroid) 8.dp else 6.dp)) { + val padding = if (appPlatform.isAndroid) PaddingValues(vertical = 8.dp) else PaddingValues(top = 3.dp, bottom = 4.dp) + Box(Modifier.padding(padding)) { val cs = composeState.value var progressByTimeout by rememberSaveable { mutableStateOf(false) } LaunchedEffect(composeState.value.inProgress) { @@ -146,7 +148,7 @@ fun SendMsgView( && (cs.preview !is ComposePreview.VoicePreview || !stopRecOnNextClick.value) && cs.contextItem is ComposeContextItem.NoContextItem ) { - Spacer(Modifier.width(10.dp)) + Spacer(Modifier.width(12.dp)) StartLiveMessageButton(userCanSend) { if (composeState.value.preview is ComposePreview.NoPreview) { startLiveMessage(scope, sendLiveMessage, updateLiveMessage, sendButtonSize, sendButtonAlpha, composeState, liveMessageAlertShown) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/VerifyCodeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/VerifyCodeView.kt index 5bd707ab66..69087ecd60 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/VerifyCodeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/VerifyCodeView.kt @@ -56,11 +56,7 @@ private fun VerifyCodeLayout( connectionVerified: Boolean, verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, ) { - ColumnWithScrollBar( - Modifier - .fillMaxSize() - .padding(horizontal = DEFAULT_PADDING) - ) { + ColumnWithScrollBar(Modifier.padding(horizontal = DEFAULT_PADDING)) { AppBarTitle(stringResource(MR.strings.security_code), withPadding = false) val splitCode = splitToParts(connectionCode, 24) Row(Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING_HALF), horizontalArrangement = Arrangement.Center) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt index 18a3a0d14d..b351f56c29 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt @@ -130,10 +130,7 @@ fun AddGroupMembersLayout( } } - ColumnWithScrollBar( - Modifier - .fillMaxWidth(), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.button_add_members)) profileText() Spacer(Modifier.size(DEFAULT_PADDING)) 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 a14d227074..76f2866950 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 @@ -25,6 +25,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.* import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* @@ -283,9 +284,14 @@ fun ModalData.GroupChatInfoLayout( if (s.isEmpty()) members else members.filter { m -> m.anyNameContains(s) } } } + Box { + val oneHandUI = remember { appPrefs.oneHandUI.state } LazyColumnWithScrollBar( - Modifier - .fillMaxWidth(), + contentPadding = if (oneHandUI.value) { + PaddingValues(top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + DEFAULT_PADDING + 5.dp, bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()) + } else { + PaddingValues(top = topPaddingToContent()) + }, state = listState ) { item { @@ -397,6 +403,11 @@ fun ModalData.GroupChatInfoLayout( } } SectionBottomSpacer() + Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars)) + } + } + if (!oneHandUI.value) { + NavigationBarBackground(oneHandUI.value, oneHandUI.value) } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt index 5291520566..956ee575de 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt @@ -119,9 +119,7 @@ fun GroupLinkLayout( ) } - ColumnWithScrollBar( - Modifier, - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.group_link)) Text( stringResource(MR.strings.you_can_share_group_link_anybody_will_be_able_to_connect), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt index 9981d70a52..a03cff2bb0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt @@ -313,10 +313,7 @@ fun GroupMemberInfoLayout( } } - ColumnWithScrollBar( - Modifier - .fillMaxWidth(), - ) { + ColumnWithScrollBar { Row( Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt index b7d66dd4f6..128dfe2d97 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt @@ -82,9 +82,7 @@ private fun GroupPreferencesLayout( reset: () -> Unit, savePrefs: () -> Unit, ) { - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.group_preferences)) val timedMessages = remember(preferences) { mutableStateOf(preferences.timedMessages.enable) } val onTTLUpdated = { ttl: Int? -> diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt index 6375ef1a20..e81722f3f0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt @@ -82,10 +82,9 @@ fun GroupProfileLayout( }, close) } } - ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { ModalBottomSheetLayout( scrimColor = Color.Black.copy(alpha = 0.12F), - modifier = Modifier.navigationBarsWithImePadding(), + modifier = Modifier.imePadding(), sheetContent = { GetImageBottomSheet( chosenImage, @@ -98,9 +97,7 @@ fun GroupProfileLayout( sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp) ) { ModalView(close = closeWithAlert) { - ColumnWithScrollBar( - Modifier - ) { + ColumnWithScrollBar { Column( Modifier.fillMaxWidth() .padding(horizontal = DEFAULT_PADDING) @@ -177,7 +174,6 @@ fun GroupProfileLayout( } } } - } } private fun canUpdateProfile(displayName: String, groupProfile: GroupProfile): Boolean = diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/WelcomeMessageView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/WelcomeMessageView.kt index b6312e4d82..7f0af360e7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/WelcomeMessageView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/WelcomeMessageView.kt @@ -95,9 +95,7 @@ private fun GroupWelcomeLayout( linkMode: SimplexLinkMode, save: () -> Unit, ) { - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - ) { + ColumnWithScrollBar { val editMode = remember { mutableStateOf(true) } AppBarTitle(stringResource(MR.strings.group_welcome_title)) val wt = rememberSaveable { welcomeText } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt index f346402957..f0480a5c50 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.layout.* +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.UriHandler import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource @@ -320,6 +321,8 @@ fun CIMarkdownText( const val CHAT_IMAGE_LAYOUT_ID = "chatImage" const val CHAT_BUBBLE_LAYOUT_ID = "chatBubble" +const val CHAT_COMPOSE_LAYOUT_ID = "chatCompose" +const val CONSOLE_COMPOSE_LAYOUT_ID = "consoleCompose" /** * Equal to [androidx.compose.ui.unit.Constraints.MaxFocusMask], which is 0x3FFFF - 1 * Other values make a crash `java.lang.IllegalArgumentException: Can't represent a width of 123456 and height of 9909 in Constraints` @@ -398,6 +401,70 @@ fun DependentLayout( } } } + +// The purpose of this layout is to make measuring of bottom compose view and adapt top lazy column to its size in the same frame (not on the next frame as you would expect). +// So, steps are: +// - measuring the layout: measured height of compose view before this step is 0, it's added to content padding of lazy column (so it's == 0) +// - measured the layout: measured height of compose view now is correct, but it's not yet applied to lazy column content padding (so it's == 0) and lazy column is placed higher than compose view in view with respect to compose view's height +// - on next frame measured height is correct and content padding is the same, lazy column placed to occupy all parent view's size +// - every added/removed line in compose view goes through the same process. +@Composable +fun AdaptingBottomPaddingLayout( + modifier: Modifier = Modifier, + mainLayoutId: String, + expectedHeight: MutableState, + content: @Composable () -> Unit +) { + val expected = with(LocalDensity.current) { expectedHeight.value.roundToPx() } + Layout( + content = content, + modifier = modifier + ) { measureable, constraints -> + require(measureable.size <= 2) { "Should be exactly one or two elements in this layout, you have ${measureable.size}" } + val mainPlaceable = measureable.firstOrNull { it.layoutId == mainLayoutId }!!.measure(constraints) + val placeables: List = measureable.map { + if (it.layoutId == mainLayoutId) + mainPlaceable + else + it.measure(constraints.copy(maxHeight = if (expected != mainPlaceable.measuredHeight) constraints.maxHeight - mainPlaceable.measuredHeight + expected else constraints.maxHeight)) } + expectedHeight.value = mainPlaceable.measuredHeight.toDp() + layout(constraints.maxWidth, constraints.maxHeight) { + var y = 0 + placeables.forEach { + if (it !== mainPlaceable) { + it.place(0, y) + y += it.measuredHeight + } else { + it.place(0, constraints.maxHeight - mainPlaceable.measuredHeight) + y += it.measuredHeight + } + } + } + } +} + +@Composable +fun CenteredRowLayout( + modifier: Modifier = Modifier, + content: @Composable () -> Unit +) { + Layout( + content = content, + modifier = modifier + ) { measureable, constraints -> + require(measureable.size == 3) { "Should be exactly three elements in this layout, you have ${measureable.size}" } + val first = measureable[0].measure(constraints.copy(minWidth = 0, minHeight = 0)) + val third = measureable[2].measure(constraints.copy(minWidth = first.measuredWidth, minHeight = 0)) + val second = measureable[1].measure(constraints.copy(minWidth = 0, minHeight = 0, maxWidth = (constraints.maxWidth - first.measuredWidth - third.measuredWidth).coerceAtLeast(0))) + // Limit width for every other element to width of important element and height for a sum of all elements. + layout(constraints.maxWidth, constraints.maxHeight) { + first.place(0, ((constraints.maxHeight - first.measuredHeight) / 2).coerceAtLeast(0)) + second.place((constraints.maxWidth - second.measuredWidth) / 2, ((constraints.maxHeight - second.measuredHeight) / 2).coerceAtLeast(0)) + third.place(constraints.maxWidth - third.measuredWidth, ((constraints.maxHeight - third.measuredHeight) / 2).coerceAtLeast(0)) + } + } +} + /* class EditedProvider: PreviewParameterProvider { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.kt index ab3918549d..70d6fa4aa8 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.kt @@ -12,7 +12,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.* import androidx.compose.ui.input.pointer.* import androidx.compose.ui.layout.onGloballyPositioned -import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.CryptoFile import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.CurrentColors @@ -58,9 +57,17 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () -> val playersToRelease = rememberSaveable { mutableSetOf() } DisposableEffectOnGone( always = { - platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, Color.Black, false, false) + platform.androidSetStatusAndNavigationBarAppearance(false, false, blackNavBar = true) + chatModel.fullscreenGalleryVisible.value = true }, - whenGone = { playersToRelease.forEach { VideoPlayerHolder.release(it, true, true) } } + whenDispose = { + val c = CurrentColors.value.colors + platform.androidSetStatusAndNavigationBarAppearance(c.isLight, c.isLight) + chatModel.fullscreenGalleryVisible.value = false + }, + whenGone = { + playersToRelease.forEach { VideoPlayerHolder.release(it, true, true) } + } ) @Composable 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 50949b0b16..586bca87d0 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.* @@ -34,6 +33,7 @@ import chat.simplex.common.views.onboarding.shouldShowWhatsNew import chat.simplex.common.platform.* import chat.simplex.common.views.call.Call import chat.simplex.common.views.chat.item.CIFileViewScope +import chat.simplex.common.views.chat.topPaddingToContent import chat.simplex.common.views.newchat.* import chat.simplex.common.views.usersettings.* import chat.simplex.res.MR @@ -41,7 +41,6 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.serialization.json.Json -import java.net.URI import kotlin.time.Duration.Companion.seconds private fun showNewChatSheet(oneHandUI: State) { @@ -55,7 +54,7 @@ private fun showNewChatSheet(oneHandUI: State) { chatModel.newChatSheetVisible.value = false close() } - ModalView(close, closeOnTop = !oneHandUI.value) { + ModalView(close, showAppBar = !oneHandUI.value) { if (appPlatform.isAndroid) { BackHandler { close() @@ -122,11 +121,7 @@ fun ToggleChatListCard() { SharedPreferenceToggle( appPrefs.oneHandUI, - enabled = true, - onChange = { - val c = CurrentColors.value.colors - platform.androidSetStatusAndNavBarColors(c.isLight, c.background, !appPrefs.oneHandUI.get(), appPrefs.oneHandUI.get()) - } + enabled = true ) } } @@ -154,74 +149,36 @@ fun ChatListView(chatModel: ChatModel, userPickerState: MutableStateFlow, listState: LazyListState) { + if (!chatModel.desktopNoUserNoRemote) { + ChatList(searchText = searchText, listState) + } + if (chatModel.chats.value.isEmpty() && !chatModel.switchingUsersAndHosts.value && !chatModel.desktopNoUserNoRemote) { + Text( + stringResource( + if (chatModel.chatRunning.value == null) MR.strings.loading_chats else MR.strings.you_have_no_chats + ), Modifier.align(Alignment.Center), color = MaterialTheme.colors.secondary + ) + } +} + +@Composable +private fun BoxScope.NewChatSheetFloatingButton(oneHandUI: State, stopped: Boolean) { + FloatingActionButton( + onClick = { + if (!stopped) { + showNewChatSheet(oneHandUI) + } + }, + Modifier + .navigationBarsPadding() + .padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING) + .align(Alignment.BottomEnd) + .size(AppBarHeight * fontSizeSqrtMultiplier), + elevation = FloatingActionButtonDefaults.elevation( + defaultElevation = 0.dp, + pressedElevation = 0.dp, + hoveredElevation = 0.dp, + focusedElevation = 0.dp, + ), + backgroundColor = if (!stopped) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, + contentColor = Color.White + ) { + Icon(painterResource(MR.images.ic_edit_filled), stringResource(MR.strings.add_contact_or_create_group), Modifier.size(22.dp * fontSizeSqrtMultiplier)) + } +} + @Composable private fun ConnectButton(text: String, onClick: () -> Unit) { Button( @@ -256,7 +253,7 @@ private fun ConnectButton(text: String, onClick: () -> Unit) { } @Composable -private fun ChatListToolbar(userPickerState: MutableStateFlow, stopped: Boolean, setPerformLA: (Boolean) -> Unit) { +private fun ChatListToolbar(userPickerState: MutableStateFlow, listState: LazyListState, stopped: Boolean, setPerformLA: (Boolean) -> Unit) { val serversSummary: MutableState = remember { mutableStateOf(null) } val barButtons = arrayListOf<@Composable RowScope.() -> Unit>() val updatingProgress = remember { chatModel.updatingProgress }.value @@ -265,6 +262,18 @@ private fun ChatListToolbar(userPickerState: MutableStateFlow if (oneHandUI.value) { val sp16 = with(LocalDensity.current) { 16.sp.toDp() } + if (appPlatform.isDesktop && oneHandUI.value) { + val call = remember { chatModel.activeCall } + if (call.value != null) { + barButtons.add { + val c = call.value + if (c != null) { + ActiveCallInteractiveArea(c) + Spacer(Modifier.width(5.dp)) + } + } + } + } if (!stopped) { barButtons.add { IconButton( @@ -323,7 +332,9 @@ private fun ChatListToolbar(userPickerState: MutableStateFlow } } val clipboard = LocalClipboardManager.current - DefaultTopAppBar( + val scope = rememberCoroutineScope() + val canScrollToZero = remember { derivedStateOf { listState.firstVisibleItemIndex != 0 || listState.firstVisibleItemScrollOffset != 0 } } + DefaultAppBar( navigationButton = { if (chatModel.users.isEmpty() && !chatModel.desktopNoUserNoRemote) { NavigationButtonMenu { @@ -351,15 +362,14 @@ private fun ChatListToolbar(userPickerState: MutableStateFlow SubscriptionStatusIndicator( click = { ModalManager.start.closeModals() + val summary = serversSummary.value ModalManager.start.showModalCloseable( endButtons = { - val summary = serversSummary.value if (summary != null) { ShareButton { val json = Json { prettyPrint = true } - val text = json.encodeToString(PresentedServersSummary.serializer(), summary) clipboard.shareText(text) } @@ -370,10 +380,10 @@ private fun ChatListToolbar(userPickerState: MutableStateFlow ) } }, - onTitleClick = null, - showSearch = false, + onTitleClick = if (canScrollToZero.value) { { scrollToBottom(scope, listState) } } else null, + onTop = !oneHandUI.value, onSearchValueChanged = {}, - buttons = barButtons + buttons = { barButtons.forEach { it() } } ) } @@ -491,74 +501,78 @@ fun connectIfOpenedViaUri(rhId: Long?, uri: String, chatModel: ChatModel) { @Composable private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState, searchShowingSimplexLink: MutableState, searchChatFilteredBySimplexLink: MutableState) { - Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { - val focusRequester = remember { FocusRequester() } - var focused by remember { mutableStateOf(false) } - Icon( - painterResource(MR.images.ic_search), - contentDescription = null, - Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING_HALF).size(22.dp * fontSizeSqrtMultiplier), - tint = MaterialTheme.colors.secondary - ) - SearchTextField( - Modifier.weight(1f).onFocusChanged { focused = it.hasFocus }.focusRequester(focusRequester), - placeholder = stringResource(MR.strings.search_or_paste_simplex_link), - alwaysVisible = true, - searchText = searchText, - enabled = !remember { searchShowingSimplexLink }.value, - trailingContent = null, - ) { - searchText.value = searchText.value.copy(it) - } - val hasText = remember { derivedStateOf { searchText.value.text.isNotEmpty() } } - if (hasText.value) { - val hideSearchOnBack: () -> Unit = { searchText.value = TextFieldValue() } - BackHandler(onBack = hideSearchOnBack) - KeyChangeEffect(chatModel.currentRemoteHost.value) { - hideSearchOnBack() + Box { + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { + val focusRequester = remember { FocusRequester() } + var focused by remember { mutableStateOf(false) } + Icon( + painterResource(MR.images.ic_search), + contentDescription = null, + Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING_HALF).size(22.dp * fontSizeSqrtMultiplier), + tint = MaterialTheme.colors.secondary + ) + SearchTextField( + Modifier.weight(1f).onFocusChanged { focused = it.hasFocus }.focusRequester(focusRequester), + placeholder = stringResource(MR.strings.search_or_paste_simplex_link), + alwaysVisible = true, + searchText = searchText, + enabled = !remember { searchShowingSimplexLink }.value, + trailingContent = null, + ) { + searchText.value = searchText.value.copy(it) } - } else { - val padding = if (appPlatform.isDesktop) 0.dp else 7.dp - if (chatModel.chats.value.isNotEmpty()) { - ToggleFilterEnabledButton() - } - Spacer(Modifier.width(padding)) - } - val focusManager = LocalFocusManager.current - val keyboardState = getKeyboardState() - LaunchedEffect(keyboardState.value) { - if (keyboardState.value == KeyboardState.Closed && focused) { - focusManager.clearFocus() - } - } - val view = LocalMultiplatformView() - LaunchedEffect(Unit) { - snapshotFlow { searchText.value.text } - .distinctUntilChanged() - .collect { - val link = strHasSingleSimplexLink(it.trim()) - if (link != null) { - // if SimpleX link is pasted, show connection dialogue - hideKeyboard(view) - if (link.format is Format.SimplexLink) { - val linkText = link.simplexLinkText(link.format.linkType, link.format.smpHosts) - searchText.value = searchText.value.copy(linkText, selection = TextRange.Zero) - } - searchShowingSimplexLink.value = true - searchChatFilteredBySimplexLink.value = null - connect(link.text, searchChatFilteredBySimplexLink) { searchText.value = TextFieldValue() } - } else if (!searchShowingSimplexLink.value || it.isEmpty()) { - if (it.isNotEmpty()) { - // if some other text is pasted, enter search mode - focusRequester.requestFocus() - } else if (listState.layoutInfo.totalItemsCount > 0) { - listState.scrollToItem(0) - } - searchShowingSimplexLink.value = false - searchChatFilteredBySimplexLink.value = null - } + val hasText = remember { derivedStateOf { searchText.value.text.isNotEmpty() } } + if (hasText.value) { + val hideSearchOnBack: () -> Unit = { searchText.value = TextFieldValue() } + BackHandler(onBack = hideSearchOnBack) + KeyChangeEffect(chatModel.currentRemoteHost.value) { + hideSearchOnBack() } + } else { + val padding = if (appPlatform.isDesktop) 0.dp else 7.dp + if (chatModel.chats.value.isNotEmpty()) { + ToggleFilterEnabledButton() + } + Spacer(Modifier.width(padding)) + } + val focusManager = LocalFocusManager.current + val keyboardState = getKeyboardState() + LaunchedEffect(keyboardState.value) { + if (keyboardState.value == KeyboardState.Closed && focused) { + focusManager.clearFocus() + } + } + val view = LocalMultiplatformView() + LaunchedEffect(Unit) { + snapshotFlow { searchText.value.text } + .distinctUntilChanged() + .collect { + val link = strHasSingleSimplexLink(it.trim()) + if (link != null) { + // if SimpleX link is pasted, show connection dialogue + hideKeyboard(view) + if (link.format is Format.SimplexLink) { + val linkText = link.simplexLinkText(link.format.linkType, link.format.smpHosts) + searchText.value = searchText.value.copy(linkText, selection = TextRange.Zero) + } + searchShowingSimplexLink.value = true + searchChatFilteredBySimplexLink.value = null + connect(link.text, searchChatFilteredBySimplexLink) { searchText.value = TextFieldValue() } + } else if (!searchShowingSimplexLink.value || it.isEmpty()) { + if (it.isNotEmpty()) { + // if some other text is pasted, enter search mode + focusRequester.requestFocus() + } else if (listState.layoutInfo.totalItemsCount > 0) { + listState.scrollToItem(0) + } + searchShowingSimplexLink.value = false + searchChatFilteredBySimplexLink.value = null + } + } + } } + val oneHandUI = remember { appPrefs.oneHandUI.state } + Divider(Modifier.align(if (oneHandUI.value) Alignment.TopStart else Alignment.BottomStart)) } } @@ -590,8 +604,37 @@ enum class ScrollDirection { } @Composable -private fun ChatList(chatModel: ChatModel, searchText: MutableState) { - val listState = rememberLazyListState(lazyListState.first, lazyListState.second) +fun BoxScope.StatusBarBackground() { + if (appPlatform.isAndroid) { + val finalColor = MaterialTheme.colors.background.copy(0.88f) + Box(Modifier.fillMaxWidth().windowInsetsTopHeight(WindowInsets.statusBars).background(finalColor)) + } +} + +@Composable +fun BoxScope.NavigationBarBackground(appBarOnBottom: Boolean = false, mixedColor: Boolean, noAlpha: Boolean = false) { + if (appPlatform.isAndroid) { + val barPadding = WindowInsets.navigationBars.asPaddingValues() + val paddingBottom = barPadding.calculateBottomPadding() + val color = if (mixedColor) MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f) else MaterialTheme.colors.background + val finalColor = color.copy(if (noAlpha) 1f else if (appBarOnBottom) remember { appPrefs.inAppBarsAlpha.state }.value else 0.6f) + Box(Modifier.align(Alignment.BottomStart).height(paddingBottom).fillMaxWidth().background(finalColor)) + } +} + +@Composable +fun BoxScope.NavigationBarBackground(modifier: Modifier, color: Color = MaterialTheme.colors.background) { + val keyboardState = getKeyboardState() + if (appPlatform.isAndroid && keyboardState.value == KeyboardState.Closed) { + val barPadding = WindowInsets.navigationBars.asPaddingValues() + val paddingBottom = barPadding.calculateBottomPadding() + val finalColor = color.copy(0.6f) + Box(modifier.align(Alignment.BottomStart).height(paddingBottom).fillMaxWidth().background(finalColor)) + } +} + +@Composable +private fun BoxScope.ChatList(searchText: MutableState, listState: LazyListState) { var scrollDirection by remember { mutableStateOf(ScrollDirection.Idle) } var previousIndex by remember { mutableStateOf(0) } var previousScrollOffset by remember { mutableStateOf(0) } @@ -628,40 +671,45 @@ private fun ChatList(chatModel: ChatModel, searchText: MutableState(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(), + if (!oneHandUI.value) Modifier.imePadding() else Modifier, listState, reverseLayout = oneHandUI.value ) { + 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 (listState.firstVisibleItemIndex == 0) offsetMultiplier * listState.firstVisibleItemScrollOffset else offsetMultiplier * 1000 + 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 { - 0 + when (listState.firstVisibleItemIndex) { + 0 -> 0 + 1 -> offsetMultiplier * listState.firstVisibleItemScrollOffset + else -> offsetMultiplier * 1000 + } } IntOffset(0, y) } - .background(MaterialTheme.colors.background), + .background(MaterialTheme.colors.background) ) { if (oneHandUI.value) { - Divider() - } - ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink) - if (!oneHandUI.value) { - Divider() + Column(Modifier.consumeWindowInsets(WindowInsets.navigationBars).consumeWindowInsets(PaddingValues(bottom = AppBarHeight))) { + ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink) + Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime)) + } + } else { + ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink) } } } - if (appPlatform.isAndroid && !oneHandUICardShown.value && chats.count() > 1) { + if (!oneHandUICardShown.value && chats.size > 1) { item { ToggleChatListCard() } @@ -672,17 +720,30 @@ private fun ChatList(chatModel: ChatModel, searchText: MutableState= 3) appPrefs.oneHandUICardShown.set(true) + } + } } fun filteredChats( @@ -727,3 +788,7 @@ private fun filtered(chat: Chat): Boolean = (chat.chatInfo.chatSettings?.favorite ?: false) || chat.chatStats.unreadChat || (chat.chatInfo.ntfsEnabled && chat.chatStats.unreadCount > 0) + +fun scrollToBottom(scope: CoroutineScope, listState: LazyListState) { + scope.launch { try { listState.animateScrollToItem(0) } catch (e: Exception) { Log.e(TAG, e.stackTraceToString()) } } +} 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 6219252b54..4e3ee2340c 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 @@ -620,9 +620,7 @@ fun ModalData.SMPServerSummaryView( ModalView( close = close ) { - ColumnWithScrollBar( - Modifier.fillMaxSize(), - ) { + ColumnWithScrollBar { val bottomPadding = DEFAULT_PADDING AppBarTitle( stringResource(MR.strings.smp_server), @@ -645,9 +643,7 @@ fun ModalData.DetailedXFTPStatsView( ModalView( close = close ) { - ColumnWithScrollBar( - Modifier.fillMaxSize(), - ) { + ColumnWithScrollBar { Box(contentAlignment = Alignment.Center) { val bottomPadding = DEFAULT_PADDING AppBarTitle( @@ -671,9 +667,7 @@ fun ModalData.DetailedSMPStatsView( ModalView( close = close ) { - ColumnWithScrollBar( - Modifier.fillMaxSize(), - ) { + ColumnWithScrollBar { Box(contentAlignment = Alignment.Center) { val bottomPadding = DEFAULT_PADDING AppBarTitle( @@ -697,9 +691,7 @@ fun ModalData.XFTPServerSummaryView( ModalView( close = close ) { - ColumnWithScrollBar( - Modifier.fillMaxSize(), - ) { + ColumnWithScrollBar { Box(contentAlignment = Alignment.Center) { val bottomPadding = DEFAULT_PADDING AppBarTitle( @@ -715,9 +707,7 @@ fun ModalData.XFTPServerSummaryView( @Composable fun ModalData.ServersSummaryView(rh: RemoteHostInfo?, serversSummary: MutableState) { - ColumnWithScrollBar( - Modifier.fillMaxSize(), - ) { + ColumnWithScrollBar { var showUserSelection by remember { mutableStateOf(false) } val selectedUserCategory = remember { stateGetOrPut("selectedUserCategory") { PresentedUserCategory.ALL_USERS } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt index 769a0b83f6..9ca2c1e2cd 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt @@ -11,10 +11,13 @@ import androidx.compose.ui.graphics.Color import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp import chat.simplex.common.model.* import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.views.helpers.* import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.themedBackground +import chat.simplex.common.views.chat.topPaddingToContent import chat.simplex.common.views.newchat.ActiveProfilePicker import chat.simplex.res.MR @@ -22,26 +25,7 @@ import chat.simplex.res.MR fun ShareListView(chatModel: ChatModel, stopped: Boolean) { var searchInList by rememberSaveable { mutableStateOf("") } val oneHandUI = remember { appPrefs.oneHandUI.state } - - Scaffold( - contentColor = LocalContentColor.current, - topBar = { - if (!oneHandUI.value) { - Column { - ShareListToolbar(chatModel, stopped) { searchInList = it.trim() } - Divider() - } - } - }, - bottomBar = { - if (oneHandUI.value) { - Column { - Divider() - ShareListToolbar(chatModel, stopped) { searchInList = it.trim() } - } - } - } - ) { + Box(Modifier.fillMaxSize().themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer)) { val sharedContent = chatModel.sharedContent.value var isMediaOrFileAttachment = false var isVoice = false @@ -69,22 +53,24 @@ fun ShareListView(chatModel: ChatModel, stopped: Boolean) { } null -> {} } - Box(Modifier.padding(it)) { - Column( - modifier = Modifier.fillMaxSize() - ) { - if (chatModel.chats.value.isNotEmpty()) { - ShareList( - chatModel, - search = searchInList, - isMediaOrFileAttachment = isMediaOrFileAttachment, - isVoice = isVoice, - hasSimplexLink = hasSimplexLink, - ) - } else { - EmptyList() - } - } + if (chatModel.chats.value.isNotEmpty()) { + ShareList( + chatModel, + search = searchInList, + isMediaOrFileAttachment = isMediaOrFileAttachment, + isVoice = isVoice, + hasSimplexLink = hasSimplexLink, + ) + } else { + EmptyList() + } + if (oneHandUI.value) { + StatusBarBackground() + } else { + NavigationBarBackground(oneHandUI.value, true) + } + Box(Modifier.align(if (oneHandUI.value) Alignment.BottomStart else Alignment.TopStart)) { + ShareListToolbar(chatModel, stopped) { searchInList = it.trim() } } } } @@ -108,7 +94,6 @@ private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchVal if (showSearch) { BackHandler(onBack = hideSearchOnBack) } - val barButtons = arrayListOf<@Composable RowScope.() -> Unit>() val users by remember { derivedStateOf { chatModel.users.filter { u -> u.user.activeUser || !u.user.hidden } } } val navButton: @Composable RowScope.() -> Unit = { when { @@ -118,13 +103,13 @@ private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchVal .filter { u -> !u.user.activeUser && !u.user.hidden } .all { u -> u.unreadCount == 0 } UserProfileButton(chatModel.currentUser.value?.profile?.image, allRead) { - ModalManager.start.showCustomModal { close -> + ModalManager.start.showCustomModal(keyboardCoversBar = false) { close -> val search = rememberSaveable { mutableStateOf("") } ModalView( { close() }, - endButtons = { - SearchTextField(Modifier.fillMaxWidth(), placeholder = stringResource(MR.strings.search_verb), alwaysVisible = true) { search.value = it } - }, + showSearch = true, + searchAlwaysVisible = true, + onSearchValueChanged = { search.value = it }, content = { ActiveProfilePicker( search = search, @@ -148,31 +133,8 @@ private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchVal }) } } - if (chatModel.chats.value.size >= 8) { - barButtons.add { - IconButton({ showSearch = true }) { - Icon(painterResource(MR.images.ic_search_500), stringResource(MR.strings.search_verb), tint = MaterialTheme.colors.primary) - } - } - } - if (stopped) { - barButtons.add { - IconButton(onClick = { - AlertManager.shared.showAlertMsg( - generalGetString(MR.strings.chat_is_stopped_indication), - generalGetString(MR.strings.you_can_start_chat_via_setting_or_by_restarting_the_app) - ) - }) { - Icon( - painterResource(MR.images.ic_report_filled), - generalGetString(MR.strings.chat_is_stopped_indication), - tint = Color.Red, - ) - } - } - } - DefaultTopAppBar( + DefaultAppBar( navigationButton = navButton, title = { Row(verticalAlignment = Alignment.CenterVertically) { @@ -191,8 +153,29 @@ private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchVal }, onTitleClick = null, showSearch = showSearch, + onTop = !remember { appPrefs.oneHandUI.state }.value, onSearchValueChanged = onSearchValueChanged, - buttons = barButtons + buttons = { + if (chatModel.chats.value.size >= 8) { + IconButton({ showSearch = true }) { + Icon(painterResource(MR.images.ic_search_500), stringResource(MR.strings.search_verb), tint = MaterialTheme.colors.primary) + } + } + if (stopped) { + IconButton(onClick = { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.chat_is_stopped_indication), + generalGetString(MR.strings.you_can_start_chat_via_setting_or_by_restarting_the_app) + ) + }) { + Icon( + painterResource(MR.images.ic_report_filled), + generalGetString(MR.strings.chat_is_stopped_indication), + tint = Color.Red, + ) + } + } + } ) } @@ -211,8 +194,13 @@ private fun ShareList( filteredChats(false, mutableStateOf(false), mutableStateOf(null), search, sorted) } } + val topPaddingToContent = topPaddingToContent() LazyColumnWithScrollBar( - modifier = Modifier.fillMaxSize(), + modifier = Modifier.then(if (oneHandUI.value) Modifier.consumeWindowInsets(WindowInsets.navigationBars.only(WindowInsetsSides.Vertical)) else Modifier).imePadding(), + contentPadding = PaddingValues( + top = if (oneHandUI.value) WindowInsets.statusBars.asPaddingValues().calculateTopPadding() else topPaddingToContent, + bottom = if (oneHandUI.value) WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier else 0.dp + ), reverseLayout = oneHandUI.value ) { items(chats) { chat -> diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt index 4a3bce7752..2709c7760b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.* import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatController.stopRemoteHostAndReloadHosts import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.ui.theme.* @@ -137,12 +138,16 @@ fun UserPicker( } } + val oneHandUI = remember { appPrefs.oneHandUI.state } + val iconColor = MaterialTheme.colors.secondaryVariant + val background = if (appPlatform.isAndroid) MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, alpha = 1 - userPickerAlpha()) else MaterialTheme.colors.surface PlatformUserPicker( modifier = Modifier .height(IntrinsicSize.Min) .fillMaxWidth() - .then(if (newChat.isVisible()) Modifier.shadow(8.dp, clip = true) else Modifier) - .background(if (appPlatform.isAndroid) MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, alpha = 1 - userPickerAlpha()) else MaterialTheme.colors.surface) + .then(if (newChat.isVisible()) Modifier.shadow(8.dp, clip = true, ambientColor = background) else Modifier) + .padding(top = if (appPlatform.isDesktop && oneHandUI.value) 7.dp else 0.dp) + .background(background) .padding(bottom = USER_PICKER_SECTION_SPACING - DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL), pickerState = userPickerState ) { @@ -198,12 +203,13 @@ fun UserPicker( UserPickerUsersSection( users = users, onUserClicked = onUserClicked, + iconColor = iconColor, stopped = stopped ) } } else if (currentUser != null) { SectionItemView({ onUserClicked(currentUser) }, 80.dp, padding = PaddingValues(start = 16.dp, end = DEFAULT_PADDING), disabled = stopped) { - ProfilePreview(currentUser.profile, stopped = stopped) + ProfilePreview(currentUser.profile, iconColor = iconColor, stopped = stopped) } } } @@ -234,6 +240,7 @@ fun UserPicker( Column(modifier = Modifier.padding(vertical = DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL)) { UserPickerUsersSection( users = inactiveUsers, + iconColor = iconColor, onUserClicked = onUserClicked, stopped = stopped ) @@ -265,13 +272,15 @@ fun UserPicker( generalGetString(MR.strings.auth_open_chat_profiles), generalGetString(MR.strings.auth_log_in_using_credential) ) { - ModalManager.start.showCustomModal { close -> + ModalManager.start.showCustomModal(keyboardCoversBar = false) { close -> val search = rememberSaveable { mutableStateOf("") } val profileHidden = rememberSaveable { mutableStateOf(false) } ModalView( { close() }, - endButtons = { - SearchTextField(Modifier.fillMaxWidth(), placeholder = stringResource(MR.strings.search_verb), alwaysVisible = true) { search.value = it } + showSearch = true, + searchAlwaysVisible = true, + onSearchValueChanged = { + search.value = it }, content = { UserProfilesView(chatModel, search, profileHidden) }) } @@ -519,6 +528,7 @@ private fun DevicePickerRow( @Composable expect fun UserPickerUsersSection( users: List, + iconColor: Color, stopped: Boolean, onUserClicked: (user: User) -> Unit, ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/ChatArchiveView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/ChatArchiveView.kt index 6846d1c735..96acea5446 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/ChatArchiveView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/ChatArchiveView.kt @@ -46,9 +46,7 @@ fun ChatArchiveLayout( saveArchive: () -> Unit, deleteArchiveAlert: () -> Unit ) { - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - ) { + ColumnWithScrollBar { AppBarTitle(title) SectionView(stringResource(MR.strings.chat_archive_section)) { SettingsActionItem( 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 b73a0ca0bc..654d250274 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 @@ -203,7 +203,7 @@ fun DatabaseEncryptionLayout( Layout() } } else { - ColumnWithScrollBar(Modifier.fillMaxWidth(), maxIntrinsicSize = true) { + ColumnWithScrollBar(maxIntrinsicSize = true) { Layout() } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseErrorView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseErrorView.kt index 333c73e195..9264ca69af 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseErrorView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseErrorView.kt @@ -77,10 +77,7 @@ fun DatabaseErrorView( Text(String.format(generalGetString(MR.strings.database_migrations), ms.joinToString(", "))) } - ColumnWithScrollBar( - Modifier.fillMaxSize(), - verticalArrangement = Arrangement.Center, - ) { + ColumnWithScrollBarNoAppBar(Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center) { val buttonEnabled = validKey(dbKey.value) && !progressIndicator.value when (val status = chatDbStatus.value) { is DBMigrationResult.ErrorNotADatabase -> diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt index b287847ace..d36bd255e3 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt @@ -156,9 +156,7 @@ fun DatabaseLayout( val stopped = !runChat val operationsDisabled = (!stopped || progressIndicator) && !chatModel.desktopNoUserNoRemote - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.your_chat_database)) if (!chatModel.desktopNoUserNoRemote) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AppBarTitle.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AppBarTitle.kt new file mode 100644 index 0000000000..195ec020e5 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AppBarTitle.kt @@ -0,0 +1,71 @@ +package chat.simplex.common.views.helpers + +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.graphics.* +import androidx.compose.ui.unit.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.chatlist.* +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource +import kotlin.math.absoluteValue + +@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) + Brush.linearGradient(listOf(titleColor.darker(0.2f), titleColor.lighter(0.35f)), Offset(0f, Float.POSITIVE_INFINITY), Offset(Float.POSITIVE_INFINITY, 0f)) + else // color is not updated when changing themes if I pass null here + Brush.linearGradient(listOf(titleColor, titleColor), Offset(0f, Float.POSITIVE_INFINITY), Offset(Float.POSITIVE_INFINITY, 0f)) + Column { + Text( + title, + Modifier + .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.Start + ) + if (hostDevice != null) { + 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 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.Start) { + DevicePill( + active = true, + onClick = {}, + actionButtonVisible = false, + icon = painterResource(if (hostDevice.first == null) MR.images.ic_desktop else MR.images.ic_smartphone_300), + text = hostDevice.second + ) + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/BlurModifier.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/BlurModifier.kt new file mode 100644 index 0000000000..096b6c55ac --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/BlurModifier.kt @@ -0,0 +1,139 @@ +package chat.simplex.common.views.helpers + +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.runtime.State +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.* +import androidx.compose.ui.graphics.drawscope.clipRect +import androidx.compose.ui.graphics.drawscope.translate +import androidx.compose.ui.graphics.layer.GraphicsLayer +import androidx.compose.ui.graphics.layer.drawLayer +import androidx.compose.ui.unit.* +import chat.simplex.common.platform.appPlatform +import chat.simplex.common.ui.theme.CurrentColors + +fun Modifier.blurredBackgroundModifier( + keyboardInset: WindowInsets, + handler: AppBarHandler?, + blurRadius: State, + prefAlpha: State, + keyboardCoversBar: Boolean, + onTop: Boolean, + density: Density +): Modifier { + val graphicsLayer = handler?.graphicsLayer + val backgroundGraphicsLayer = handler?.backgroundGraphicsLayer + val backgroundGraphicsLayerSize = handler?.backgroundGraphicsLayerSize + if (handler == null || graphicsLayer == null || backgroundGraphicsLayer == null || blurRadius.value == 0 || prefAlpha.value == 1f || backgroundGraphicsLayerSize === null) + return this + + return if (appPlatform.isAndroid) { + this.androidBlurredModifier(keyboardInset, blurRadius.value, keyboardCoversBar, onTop, graphicsLayer, backgroundGraphicsLayer, backgroundGraphicsLayerSize, density) + } else { + this.desktopBlurredModifier(keyboardInset, blurRadius, keyboardCoversBar, onTop, graphicsLayer, backgroundGraphicsLayer, backgroundGraphicsLayerSize, density) + } +} + +// this is more performant version than for Android but can't be used on desktop because on first frame it shows transparent view +// which is very noticeable on desktop and unnoticeable on Android +private fun Modifier.androidBlurredModifier( + keyboardInset: WindowInsets, + blurRadius: Int, + keyboardCoversBar: Boolean, + onTop: Boolean, + graphicsLayer: GraphicsLayer, + backgroundGraphicsLayer: GraphicsLayer, + backgroundGraphicsLayerSize: State, + density: Density +): Modifier = this + .graphicsLayer { + renderEffect = if (blurRadius > 0) BlurEffect(blurRadius.dp.toPx(), blurRadius.dp.toPx()) else null + clip = blurRadius > 0 + } + .graphicsLayer { + if (!onTop) { + val bgSize = when { + backgroundGraphicsLayerSize.value.height == 0 && backgroundGraphicsLayer.size.height != 0 -> backgroundGraphicsLayer.size.height + backgroundGraphicsLayerSize.value.height == 0 -> graphicsLayer.size.height + else -> backgroundGraphicsLayerSize.value.height + } + val keyboardHeightCovered = if (!keyboardCoversBar) keyboardInset.getBottom(density) else 0 + translationY = -bgSize + size.height + keyboardHeightCovered + } + } + .drawBehind { + drawRect(Color.Black) + if (onTop) { + clipRect { + if (backgroundGraphicsLayer.size != IntSize.Zero) { + drawLayer(backgroundGraphicsLayer) + } else { + drawRect(CurrentColors.value.colors.background, size = Size(graphicsLayer.size.width.toFloat(), graphicsLayer.size.height.toFloat())) + } + drawLayer(graphicsLayer) + } + } else { + if (backgroundGraphicsLayer.size != IntSize.Zero) { + drawLayer(backgroundGraphicsLayer) + } else { + drawRect(CurrentColors.value.colors.background, size = Size(graphicsLayer.size.width.toFloat(), graphicsLayer.size.height.toFloat())) + } + drawLayer(graphicsLayer) + } + } + .graphicsLayer { + if (!onTop) { + val bgSize = when { + backgroundGraphicsLayerSize.value.height == 0 && backgroundGraphicsLayer.size.height != 0 -> backgroundGraphicsLayer.size.height + backgroundGraphicsLayerSize.value.height == 0 -> graphicsLayer.size.height + else -> backgroundGraphicsLayerSize.value.height + } + val keyboardHeightCovered = if (!keyboardCoversBar) keyboardInset.getBottom(density) else 0 + translationY -= -bgSize + size.height + keyboardHeightCovered + } + } + +private fun Modifier.desktopBlurredModifier( + keyboardInset: WindowInsets, + blurRadius: State, + keyboardCoversBar: Boolean, + onTop: Boolean, + graphicsLayer: GraphicsLayer, + backgroundGraphicsLayer: GraphicsLayer, + backgroundGraphicsLayerSize: State, + density: Density +): Modifier = this + .graphicsLayer { + renderEffect = if (blurRadius.value > 0) BlurEffect(blurRadius.value.dp.toPx(), blurRadius.value.dp.toPx()) else null + clip = blurRadius.value > 0 + } + .drawBehind { + drawRect(Color.Black) + if (onTop) { + clipRect { + if (backgroundGraphicsLayer.size != IntSize.Zero) { + drawLayer(backgroundGraphicsLayer) + } else { + drawRect(CurrentColors.value.colors.background, size = Size(graphicsLayer.size.width.toFloat(), graphicsLayer.size.height.toFloat())) + } + drawLayer(graphicsLayer) + } + } else { + val bgSize = when { + backgroundGraphicsLayerSize.value.height == 0 && backgroundGraphicsLayer.size.height != 0 -> backgroundGraphicsLayer.size.height + backgroundGraphicsLayerSize.value.height == 0 -> graphicsLayer.size.height + else -> backgroundGraphicsLayerSize.value.height + } + val keyboardHeightCovered = if (!keyboardCoversBar) keyboardInset.getBottom(density) else 0 + translate(top = -bgSize + size.height + keyboardHeightCovered) { + if (backgroundGraphicsLayer.size != IntSize.Zero) { + drawLayer(backgroundGraphicsLayer) + } else { + drawRect(CurrentColors.value.colors.background, size = Size(graphicsLayer.size.width.toFloat(), graphicsLayer.size.height.toFloat())) + } + drawLayer(graphicsLayer) + } + } + } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatWallpaper.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatWallpaper.kt index 2941b748c7..c1a76d7bf8 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatWallpaper.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatWallpaper.kt @@ -1,11 +1,13 @@ package chat.simplex.common.views.helpers +import androidx.compose.runtime.* import androidx.compose.ui.draw.CacheDrawScope import androidx.compose.ui.draw.DrawResult import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.drawscope.* +import androidx.compose.ui.graphics.layer.GraphicsLayer import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.* import chat.simplex.common.model.ChatController.appPrefs @@ -381,7 +383,14 @@ private fun drawToBitmap(image: ImageBitmap, imageScale: Float, tint: Color, siz return bitmap } -fun CacheDrawScope.chatViewBackground(image: ImageBitmap, imageType: WallpaperType, background: Color, tint: Color): DrawResult { +fun CacheDrawScope.chatViewBackground( + image: ImageBitmap, + imageType: WallpaperType, + background: Color, + tint: Color, + graphicsLayerSize: MutableState? = null, + backgroundGraphicsLayer: GraphicsLayer? = null +): DrawResult { val imageScale = if (imageType is WallpaperType.Preset) { (imageType.scale ?: 1f) * imageType.predefinedImageScale } else if (imageType is WallpaperType.Image && imageType.scaleType == WallpaperScaleType.REPEAT) { @@ -396,53 +405,55 @@ fun CacheDrawScope.chatViewBackground(image: ImageBitmap, imageType: WallpaperTy } return onDrawBehind { - val quality = if (appPlatform.isAndroid) FilterQuality.High else FilterQuality.Low - drawRect(background) - when (imageType) { - is WallpaperType.Preset -> drawImage(image) - is WallpaperType.Image -> when (val scaleType = imageType.scaleType ?: WallpaperScaleType.FILL) { - WallpaperScaleType.REPEAT -> drawImage(image) - WallpaperScaleType.FILL, WallpaperScaleType.FIT -> { - clipRect { - val scale = scaleType.contentScale.computeScaleFactor(Size(image.width.toFloat(), image.height.toFloat()), Size(size.width, size.height)) - val scaledWidth = (image.width * scale.scaleX).roundToInt() - val scaledHeight = (image.height * scale.scaleY).roundToInt() - // Large image will cause freeze - if (image.width > 4320 || image.height > 4320) return@clipRect + copyBackgroundToAppBar(graphicsLayerSize, backgroundGraphicsLayer) { + val quality = if (appPlatform.isAndroid) FilterQuality.High else FilterQuality.Low + drawRect(background) + when (imageType) { + is WallpaperType.Preset -> drawImage(image) + is WallpaperType.Image -> when (val scaleType = imageType.scaleType ?: WallpaperScaleType.FILL) { + WallpaperScaleType.REPEAT -> drawImage(image) + WallpaperScaleType.FILL, WallpaperScaleType.FIT -> { + clipRect { + val scale = scaleType.contentScale.computeScaleFactor(Size(image.width.toFloat(), image.height.toFloat()), Size(size.width, size.height)) + val scaledWidth = (image.width * scale.scaleX).roundToInt() + val scaledHeight = (image.height * scale.scaleY).roundToInt() + // Large image will cause freeze + if (image.width > 4320 || image.height > 4320) return@clipRect - drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality) - if (scaleType == WallpaperScaleType.FIT) { - if (scaledWidth < size.width) { - // has black lines at left and right sides - var x = (size.width - scaledWidth) / 2 - while (x > 0) { - drawImage(image, dstOffset = IntOffset(x = (x - scaledWidth).roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality) - x -= scaledWidth - } - x = size.width - (size.width - scaledWidth) / 2 - while (x < size.width) { - drawImage(image, dstOffset = IntOffset(x = x.roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality) - x += scaledWidth - } - } else { - // has black lines at top and bottom sides - var y = (size.height - scaledHeight) / 2 - while (y > 0) { - drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = (y - scaledHeight).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality) - y -= scaledHeight - } - y = size.height - (size.height - scaledHeight) / 2 - while (y < size.height) { - drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = y.roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality) - y += scaledHeight + drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality) + if (scaleType == WallpaperScaleType.FIT) { + if (scaledWidth < size.width) { + // has black lines at left and right sides + var x = (size.width - scaledWidth) / 2 + while (x > 0) { + drawImage(image, dstOffset = IntOffset(x = (x - scaledWidth).roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality) + x -= scaledWidth + } + x = size.width - (size.width - scaledWidth) / 2 + while (x < size.width) { + drawImage(image, dstOffset = IntOffset(x = x.roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality) + x += scaledWidth + } + } else { + // has black lines at top and bottom sides + var y = (size.height - scaledHeight) / 2 + while (y > 0) { + drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = (y - scaledHeight).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality) + y -= scaledHeight + } + y = size.height - (size.height - scaledHeight) / 2 + while (y < size.height) { + drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = y.roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality) + y += scaledHeight + } } } } + drawRect(tint) } - drawRect(tint) } + is WallpaperType.Empty -> {} } - is WallpaperType.Empty -> {} } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChooseAttachmentView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChooseAttachmentView.kt index aa3c4560ea..33cf7c2263 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChooseAttachmentView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChooseAttachmentView.kt @@ -19,6 +19,8 @@ fun ChooseAttachmentView(attachmentOption: MutableState, hide Box( modifier = Modifier .fillMaxWidth() + .navigationBarsPadding() + .imePadding() .wrapContentHeight() .onFocusChanged { focusState -> if (!focusState.hasFocus) hide() 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 deleted file mode 100644 index 104c05309c..0000000000 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CloseSheetBar.kt +++ /dev/null @@ -1,181 +0,0 @@ -package chat.simplex.common.views.helpers - -import androidx.compose.foundation.layout.* -import androidx.compose.material.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -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.ui.draw.* -import androidx.compose.ui.graphics.* -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.* -import chat.simplex.common.platform.appPlatform -import chat.simplex.common.ui.theme.* -import chat.simplex.common.views.chatlist.DevicePill -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(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, - verticalAlignment = Alignment.CenterVertically - ) { - 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, - 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, - overflow = TextOverflow.Ellipsis - ) - } - } else { - Spacer(Modifier.weight(1f)) - } - Row { - endButtons() - } - } - } - ) - 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) - Brush.linearGradient(listOf(titleColor.darker(0.2f), titleColor.lighter(0.35f)), Offset(0f, Float.POSITIVE_INFINITY), Offset(Float.POSITIVE_INFINITY, 0f)) - else // color is not updated when changing themes if I pass null here - Brush.linearGradient(listOf(titleColor, titleColor), Offset(0f, Float.POSITIVE_INFINITY), Offset(Float.POSITIVE_INFINITY, 0f)) - Column { - Text( - title, - Modifier - .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.Start - ) - if (hostDevice != null) { - 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.Start) { - DevicePill( - active = true, - onClick = {}, - actionButtonVisible = false, - icon = painterResource(if (hostDevice.first == null) MR.images.ic_desktop else MR.images.ic_smartphone_300), - text = hostDevice.second - ) - } -} - -@Preview/*( - uiMode = Configuration.UI_MODE_NIGHT_YES, - showBackground = true, - name = "Dark Mode" -)*/ -@Composable -fun PreviewCloseSheetBar() { - SimpleXTheme { - CloseSheetBar(close = {}) - } -} 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 index 4410f7ada5..50942169b3 100644 --- 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 @@ -3,15 +3,67 @@ 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.Modifier +import androidx.compose.ui.draw.* import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.layer.GraphicsLayer +import androidx.compose.ui.graphics.layer.drawLayer +import androidx.compose.ui.graphics.rememberGraphicsLayer import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollSource -import androidx.compose.ui.unit.Velocity +import androidx.compose.ui.unit.IntSize +import chat.simplex.common.model.ChatController.appPrefs val LocalAppBarHandler: ProvidableCompositionLocal = staticCompositionLocalOf { null } +@Composable +fun rememberAppBarHandler(key1: Any? = null, key2: Any? = null, keyboardCoversBar: Boolean = true): AppBarHandler { + val graphicsLayer = rememberGraphicsLayer() + val backgroundGraphicsLayer = rememberGraphicsLayer() + return remember(key1, key2) { AppBarHandler(graphicsLayer, backgroundGraphicsLayer, keyboardCoversBar) } +} + +@Composable +fun adjustAppBarHandler(handler: AppBarHandler): AppBarHandler { + val graphicsLayer = rememberGraphicsLayer() + val backgroundGraphicsLayer = rememberGraphicsLayer() + if (handler.graphicsLayer == null || handler.graphicsLayer?.isReleased == true || handler.backgroundGraphicsLayer?.isReleased == true) { + handler.graphicsLayer = graphicsLayer + handler.backgroundGraphicsLayer = backgroundGraphicsLayer + } + return handler +} + +fun Modifier.copyViewToAppBar(blurRadius: Int, graphicsLayer: GraphicsLayer?): Modifier { + return if (blurRadius > 0 && graphicsLayer != null) { + this.drawWithContent { + graphicsLayer.record { + this@drawWithContent.drawContent() + } + drawLayer(graphicsLayer) + } + } else this +} + +fun DrawScope.copyBackgroundToAppBar(graphicsLayerSize: MutableState?, backgroundGraphicsLayer: GraphicsLayer?, scope: DrawScope.() -> Unit) { + val blurRadius = appPrefs.appearanceBarsBlurRadius.get() + if (blurRadius > 0 && graphicsLayerSize != null && backgroundGraphicsLayer != null) { + graphicsLayerSize.value = backgroundGraphicsLayer.size + backgroundGraphicsLayer.record { + scope() + } + drawLayer(backgroundGraphicsLayer) + } else { + scope() + } +} + @Stable class AppBarHandler( + var graphicsLayer: GraphicsLayer?, + var backgroundGraphicsLayer: GraphicsLayer?, + val keyboardCoversBar: Boolean = true, listState: LazyListState = LazyListState(0, 0), scrollState: ScrollState = ScrollState(initial = 0) ) { @@ -24,6 +76,8 @@ class AppBarHandler( val connection = CollapsingAppBarNestedScrollConnection() + val backgroundGraphicsLayerSize: MutableState = mutableStateOf(IntSize.Zero) + companion object { var appBarMaxHeightPx: Int = 0 } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultDropdownMenu.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultDropdownMenu.kt index 267fc86462..1f00af2809 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultDropdownMenu.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultDropdownMenu.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.unit.dp @Composable fun DefaultDropdownMenu( showMenu: MutableState, + modifier: Modifier = Modifier, offset: DpOffset = DpOffset(0.dp, 0.dp), dropdownMenuItems: (@Composable () -> Unit)? ) { @@ -23,7 +24,7 @@ fun DefaultDropdownMenu( DropdownMenu( expanded = showMenu.value, onDismissRequest = { showMenu.value = false }, - modifier = Modifier + modifier = modifier .widthIn(min = 250.dp) .background(MaterialTheme.colors.surface) .padding(vertical = 4.dp), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultTopAppBar.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultTopAppBar.kt index 28e9a997ae..cf0c5f7e96 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultTopAppBar.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultTopAppBar.kt @@ -3,44 +3,120 @@ package chat.simplex.common.views.helpers import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.material.* -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.* import androidx.compose.ui.graphics.* -import androidx.compose.ui.unit.Dp +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.* import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource -import androidx.compose.ui.unit.dp +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.chat.item.CenteredRowLayout import chat.simplex.res.MR +import kotlin.math.absoluteValue @Composable -fun DefaultTopAppBar( +fun DefaultAppBar( navigationButton: (@Composable RowScope.() -> Unit)? = null, - title: (@Composable () -> Unit)?, + title: (@Composable () -> Unit)? = null, + fixedTitleText: String? = null, onTitleClick: (() -> Unit)? = null, - showSearch: Boolean, - onSearchValueChanged: (String) -> Unit, - buttons: List<@Composable RowScope.() -> Unit> = emptyList(), + onTop: Boolean, + showSearch: Boolean = false, + searchAlwaysVisible: Boolean = false, + onSearchValueChanged: (String) -> Unit = {}, + buttons: @Composable RowScope.() -> Unit = {}, ) { // If I just disable clickable modifier when don't need it, it will stop passing clicks to search. Replacing the whole modifier val modifier = if (!showSearch) { Modifier.clickable(enabled = onTitleClick != null, onClick = onTitleClick ?: { }) - } else Modifier + } else Modifier.imePadding() - TopAppBar( - modifier = modifier, - title = { - if (!showSearch) { - title?.invoke() - } else { - SearchTextField(Modifier.fillMaxWidth(), alwaysVisible = false, onValueChange = onSearchValueChanged) + val themeBackgroundMix = MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f) + val prefAlpha = remember { appPrefs.inAppBarsAlpha.state } + val handler = LocalAppBarHandler.current + val connection = LocalAppBarHandler.current?.connection + val titleText = remember(handler?.title?.value, fixedTitleText) { + if (fixedTitleText != null) { + mutableStateOf(fixedTitleText) + } else { + handler?.title ?: mutableStateOf("") + } + } + val keyboardInset = WindowInsets.ime + Box(modifier) { + val density = LocalDensity.current + val blurRadius = remember { appPrefs.appearanceBarsBlurRadius.state } + Box(Modifier + .matchParentSize() + .blurredBackgroundModifier(keyboardInset, handler, blurRadius, prefAlpha, handler?.keyboardCoversBar == true, onTop, density) + .drawWithCache { + // store it as a variable, don't put it inside if without holding it here. Compiler don't see it changes otherwise + val alpha = prefAlpha.value + val backgroundColor = if (title != null || fixedTitleText != null || connection == null || !onTop) { + themeBackgroundMix.copy(alpha) + } else { + themeBackgroundMix.copy(topTitleAlpha(false, connection)) + } + onDrawBehind { + drawRect(backgroundColor) + } } - }, - backgroundColor = MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f), - navigationIcon = navigationButton, - buttons = if (!showSearch) buttons else emptyList(), - centered = !showSearch, + ) + Box( + Modifier + .fillMaxWidth() + .then(if (!onTop) Modifier.navigationBarsPadding() else Modifier) + .heightIn(min = AppBarHeight * fontSizeSqrtMultiplier) + ) { + AppBar( + title = { + if (showSearch) { + SearchTextField(Modifier.fillMaxWidth(), alwaysVisible = searchAlwaysVisible, reducedCloseButtonPadding = 12.dp, onValueChange = onSearchValueChanged) + } else if (title != null) { + title() + } else if (titleText.value.isNotEmpty() && connection != null) { + Row( + Modifier + .graphicsLayer { + alpha = if (fixedTitleText != null) 1f else topTitleAlpha(true, connection) + } + ) { + Text( + titleText.value, + fontWeight = FontWeight.SemiBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + }, + navigationIcon = navigationButton, + buttons = if (!showSearch) buttons else {{}}, + centered = !showSearch && (title != null || !onTop), + onTop = onTop, + ) + AppBarDivider(onTop, title != null || fixedTitleText != null, connection) + } + } +} + + +@Composable +fun CallAppBar( + title: @Composable () -> Unit, + onBack: () -> Unit +) { + AppBar( + title, + navigationIcon = { NavigationButtonBack(tintColor = Color(0xFFFFFFD8), onButtonClicked = onBack) }, + centered = false, + onTop = true ) } @@ -83,58 +159,107 @@ fun NavigationButtonMenu(onButtonClicked: () -> Unit) { } @Composable -private fun TopAppBar( +private fun BoxScope.AppBarDivider(onTop: Boolean, fixedAlpha: Boolean, connection: CollapsingAppBarNestedScrollConnection?) { + if (connection != null) { + Divider( + Modifier + .align(if (onTop) Alignment.BottomStart else Alignment.TopStart) + .graphicsLayer { + alpha = if (!onTop || fixedAlpha) 1f else topTitleAlpha(false, connection, 1f) + } + ) + } else { + Divider(Modifier.align(if (onTop) Alignment.BottomStart else Alignment.TopStart)) + } +} + +@Composable +private fun AppBar( title: @Composable () -> Unit, modifier: Modifier = Modifier, navigationIcon: @Composable (RowScope.() -> Unit)? = null, - buttons: List<@Composable RowScope.() -> Unit> = emptyList(), - backgroundColor: Color = MaterialTheme.colors.primarySurface, + buttons: @Composable RowScope.() -> Unit = {}, centered: Boolean, + onTop: Boolean, ) { - Box( - modifier - .fillMaxWidth() - .height(AppBarHeight * fontSizeSqrtMultiplier) - .background(backgroundColor) - .padding(horizontal = 4.dp), - contentAlignment = Alignment.CenterStart, + val adjustedModifier = modifier + .then(if (onTop) Modifier.statusBarsPadding() else Modifier) + .height(AppBarHeight * fontSizeSqrtMultiplier) + .fillMaxWidth() + .padding(horizontal = AppBarHorizontalPadding) + if (centered) { + AppBarCenterAligned(adjustedModifier, title, navigationIcon, buttons) + } else { + AppBarStartAligned(adjustedModifier, title, navigationIcon, buttons) + } +} + +@Composable +private fun AppBarStartAligned( + modifier: Modifier, + title: @Composable () -> Unit, + navigationIcon: @Composable (RowScope.() -> Unit)? = null, + buttons: @Composable RowScope.() -> Unit +) { + Row( + modifier, + verticalAlignment = Alignment.CenterVertically ) { if (navigationIcon != null) { - Row( - Modifier - .fillMaxHeight() - .width(TitleInsetWithIcon - AppBarHorizontalPadding), - verticalAlignment = Alignment.CenterVertically, - content = navigationIcon - ) + navigationIcon() + Spacer(Modifier.width(AppBarHorizontalPadding)) + } else { + Spacer(Modifier.width(DEFAULT_PADDING)) + } + Row(Modifier + .weight(1f) + .padding(end = DEFAULT_PADDING_HALF) + ) { + title() } Row( - Modifier - .fillMaxHeight() - .fillMaxWidth(), horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically, ) { - buttons.forEach { it() } - } - val startPadding = if (navigationIcon != null) TitleInsetWithIcon else TitleInsetWithoutIcon - val endPadding = (buttons.size * 50f).dp - Box( - Modifier - .fillMaxWidth() - .padding( - start = if (centered) kotlin.math.max(startPadding.value, endPadding.value).dp else startPadding, - end = if (centered) kotlin.math.max(startPadding.value, endPadding.value).dp else endPadding - ), - contentAlignment = Alignment.Center - ) { - title() + buttons() } } } +@Composable +private fun AppBarCenterAligned( + modifier: Modifier, + title: @Composable () -> Unit, + navigationIcon: @Composable (RowScope.() -> Unit)? = null, + buttons: @Composable RowScope.() -> Unit, +) { + CenteredRowLayout(modifier) { + if (navigationIcon != null) { + Row( + Modifier.padding(end = AppBarHorizontalPadding), + verticalAlignment = Alignment.CenterVertically, + content = navigationIcon + ) + } else { + Spacer(Modifier) + } + Row( + Modifier.padding(end = DEFAULT_PADDING_HALF) + ) { + title() + } + Row( + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically, + ) { + buttons() + } + } +} + +private fun topTitleAlpha(text: Boolean, connection: CollapsingAppBarNestedScrollConnection, alpha: Float = appPrefs.inAppBarsAlpha.get()) = + if (connection.appBarOffset.absoluteValue < AppBarHandler.appBarMaxHeightPx / 3) 0f + else ((-connection.appBarOffset * 1.5f) / (AppBarHandler.appBarMaxHeightPx)).coerceIn(0f, if (text) 1f else alpha) + val AppBarHeight = 56.dp -val AppBarHorizontalPadding = 4.dp -val BottomAppBarHeight = 60.dp -private val TitleInsetWithoutIcon = DEFAULT_PADDING - AppBarHorizontalPadding -val TitleInsetWithIcon = 72.dp +val AppBarHorizontalPadding = 2.dp 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 4c35e72701..c181f74e99 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 @@ -6,12 +6,14 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.* 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 import chat.simplex.common.model.ChatModel import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.chatlist.StatusBarBackground import kotlinx.coroutines.flow.MutableStateFlow import java.util.concurrent.atomic.AtomicBoolean import kotlin.math.min @@ -21,24 +23,40 @@ import kotlin.math.sqrt fun ModalView( close: () -> Unit, showClose: Boolean = true, + showAppBar: Boolean = true, enableClose: Boolean = true, - background: Color = MaterialTheme.colors.background, + background: Color = Color.Unspecified, modifier: Modifier = Modifier, - closeOnTop: Boolean = true, + showSearch: Boolean = false, + searchAlwaysVisible: Boolean = false, + onSearchValueChanged: (String) -> Unit = {}, endButtons: @Composable RowScope.() -> Unit = {}, - content: @Composable () -> Unit, + content: @Composable BoxScope.() -> Unit, ) { - if (showClose) { + if (showClose && showAppBar) { BackHandler(enabled = enableClose, onBack = close) } + val oneHandUI = remember { appPrefs.oneHandUI.state } Surface(Modifier.fillMaxSize(), contentColor = LocalContentColor.current) { - Column(if (background != MaterialTheme.colors.background) Modifier.background(background) else Modifier.themedBackground()) { - if (closeOnTop) { - CloseSheetBar(if (enableClose) close else null, showClose, endButtons = endButtons) - } + Box(if (background != Color.Unspecified) Modifier.background(background) else Modifier.themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer)) { Box(modifier = modifier) { content() } + if (showAppBar) { + if (oneHandUI.value) { + StatusBarBackground() + } + Box(Modifier.align(if (oneHandUI.value) Alignment.BottomStart else Alignment.TopStart)) { + DefaultAppBar( + navigationButton = if (showClose) {{ NavigationButtonBack(onButtonClicked = if (enableClose) close else null) }} else null, + onTop = !oneHandUI.value, + showSearch = showSearch, + searchAlwaysVisible = searchAlwaysVisible, + onSearchValueChanged = onSearchValueChanged, + buttons = endButtons + ) + } + } } } } @@ -47,7 +65,7 @@ enum class ModalPlacement { START, CENTER, END, FULLSCREEN } -class ModalData() { +class ModalData(val keyboardCoversBar: Boolean = true) { private val state = mutableMapOf>() fun stateGetOrPut (key: String, default: () -> T): MutableState = state.getOrPut(key) { mutableStateOf(default() as Any) } as MutableState @@ -55,7 +73,7 @@ class ModalData() { fun stateGetOrPutNullable (key: String, default: () -> T?): MutableState = state.getOrPut(key) { mutableStateOf(default() as Any?) } as MutableState - val appBarHandler = AppBarHandler() + val appBarHandler = AppBarHandler(null, null, keyboardCoversBar = keyboardCoversBar) } class ModalManager(private val placement: ModalPlacement? = null) { @@ -69,23 +87,21 @@ class ModalManager(private val placement: ModalPlacement? = null) { private var passcodeView: MutableStateFlow<(@Composable (close: () -> Unit) -> Unit)?> = MutableStateFlow(null) private var onTimePasscodeView: MutableStateFlow<(@Composable (close: () -> Unit) -> Unit)?> = MutableStateFlow(null) - fun showModal(settings: Boolean = false, showClose: Boolean = true, closeOnTop: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.() -> Unit) { - val data = ModalData() + fun showModal(settings: Boolean = false, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.() -> Unit) { showCustomModal { close -> - ModalView(close, showClose = showClose, closeOnTop = closeOnTop, endButtons = endButtons, content = { data.content() }) + ModalView(close, showClose = showClose, endButtons = endButtons, content = { content() }) } } - fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, closeOnTop: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.(close: () -> Unit) -> Unit) { - val data = ModalData() + fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.(close: () -> Unit) -> Unit) { showCustomModal { close -> - ModalView(close, showClose = showClose, endButtons = endButtons, closeOnTop = closeOnTop, content = { data.content(close) }) + ModalView(close, showClose = showClose, endButtons = endButtons, content = { content(close) }) } } - fun showCustomModal(animated: Boolean = true, modal: @Composable ModalData.(close: () -> Unit) -> Unit) { + fun showCustomModal(animated: Boolean = true, keyboardCoversBar: Boolean = true, modal: @Composable ModalData.(close: () -> Unit) -> Unit) { Log.d(TAG, "ModalManager.showCustomModal") - val data = ModalData() + val data = ModalData(keyboardCoversBar = keyboardCoversBar) // Means, animation is in progress or not started yet. Do not wait until animation finishes, just remove all from screen. // This is useful when invoking close() and ShowCustomModal one after another without delay. Otherwise, screen will hold prev view if (toRemove.isNotEmpty()) { @@ -146,9 +162,7 @@ class ModalManager(private val placement: ModalPlacement? = null) { // Without animation if (modalCount.value > 0 && modalViews.lastOrNull()?.first == false) { modalViews.lastOrNull()?.let { - CompositionLocalProvider( - LocalAppBarHandler provides it.second.appBarHandler - ) { + CompositionLocalProvider(LocalAppBarHandler provides adjustAppBarHandler(it.second.appBarHandler)) { it.third(it.second, ::closeModal) } } @@ -164,9 +178,7 @@ class ModalManager(private val placement: ModalPlacement? = null) { } ) { modalViews.getOrNull(it - 1)?.let { - CompositionLocalProvider( - LocalAppBarHandler provides it.second.appBarHandler - ) { + CompositionLocalProvider(LocalAppBarHandler provides adjustAppBarHandler(it.second.appBarHandler)) { it.third(it.second, ::closeModal) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SearchTextField.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SearchTextField.kt index 60dceab4ad..7124f34ac0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SearchTextField.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SearchTextField.kt @@ -2,7 +2,7 @@ package chat.simplex.common.views.helpers import androidx.compose.foundation.background import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.ZeroCornerSize import androidx.compose.foundation.text.* import androidx.compose.material.* @@ -18,12 +18,9 @@ import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.* 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.platform.* import chat.simplex.res.MR import kotlinx.coroutines.delay @@ -38,6 +35,7 @@ fun SearchTextField( placeholder: String = stringResource(MR.strings.search_verb), enabled: Boolean = true, trailingContent: @Composable (() -> Unit)? = null, + reducedCloseButtonPadding: Dp = 0.dp, onValueChange: (String) -> Unit ) { val focusRequester = remember { FocusRequester() } @@ -81,15 +79,20 @@ fun SearchTextField( ) val shape = MaterialTheme.shapes.small.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize) val interactionSource = remember { MutableInteractionSource() } + val textStyle = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground) + // sizing is done differently on Android and desktop in order to have the same height of search and compose view on desktop + // see PlatformTextField.desktop + SendMsgView + val padding = if (appPlatform.isAndroid) PaddingValues() else PaddingValues(top = 3.dp, bottom = 4.dp) BasicTextField( value = searchText.value, modifier = modifier .background(colors.backgroundColor(enabled).value, shape) .indicatorLine(enabled, false, interactionSource, colors) .focusRequester(focusRequester) + .padding(padding) .defaultMinSize( minWidth = TextFieldDefaults.MinWidth, - minHeight = TextFieldDefaults.MinHeight + minHeight = if (appPlatform.isAndroid) TextFieldDefaults.MinHeight else 0.dp ), onValueChange = { searchText.value = it @@ -100,18 +103,14 @@ fun SearchTextField( visualTransformation = VisualTransformation.None, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), singleLine = true, - textStyle = TextStyle( - color = MaterialTheme.colors.onBackground, - fontWeight = FontWeight.Normal, - fontSize = 15.sp - ), + textStyle = textStyle, interactionSource = interactionSource, decorationBox = @Composable { innerTextField -> TextFieldDefaults.TextFieldDecorationBox( value = searchText.value.text, innerTextField = innerTextField, placeholder = { - Text(placeholder, maxLines = 1, overflow = TextOverflow.Ellipsis) + Text(placeholder, style = textStyle.copy(color = MaterialTheme.colors.secondary), maxLines = 1, overflow = TextOverflow.Ellipsis) }, trailingIcon = if (searchText.value.text.isNotEmpty()) {{ IconButton({ @@ -121,7 +120,7 @@ fun SearchTextField( } searchText.value = TextFieldValue(""); onValueChange("") - }) { + }, Modifier.offset(x = reducedCloseButtonPadding)) { Icon(painterResource(MR.images.ic_close), stringResource(MR.strings.icon_descr_close_button), tint = MaterialTheme.colors.primary,) } }} else trailingContent, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/TextEditor.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/TextEditor.kt index ab7e562697..da16e2b7e7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/TextEditor.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/TextEditor.kt @@ -57,7 +57,6 @@ fun TextEditor( ) { val textFieldModifier = modifier .fillMaxWidth() - .navigationBarsWithImePadding() .onFocusChanged { focused = it.isFocused } .padding(10.dp) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ThemeModeEditor.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ThemeModeEditor.kt index a77290d90f..d7cdf0e2e3 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ThemeModeEditor.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ThemeModeEditor.kt @@ -32,10 +32,7 @@ fun ModalData.UserWallpaperEditor( globalThemeUsed: MutableState, save: suspend (applyToMode: DefaultThemeMode?, ThemeModeOverride?) -> Unit ) { - ColumnWithScrollBar( - Modifier - .fillMaxSize() - ) { + ColumnWithScrollBar { val applyToMode = remember { stateGetOrPutNullable("applyToMode") { applyToMode } } var showMore by remember { stateGetOrPut("showMore") { false } } val themeModeOverride = remember { stateGetOrPut("themeModeOverride") { theme } } @@ -231,10 +228,7 @@ fun ModalData.ChatWallpaperEditor( globalThemeUsed: MutableState, save: suspend (applyToMode: DefaultThemeMode?, ThemeModeOverride?) -> Unit ) { - ColumnWithScrollBar( - Modifier - .fillMaxSize() - ) { + ColumnWithScrollBar { val applyToMode = remember { stateGetOrPutNullable("applyToMode") { applyToMode } } var showMore by remember { stateGetOrPut("showMore") { false } } val themeModeOverride = remember { stateGetOrPut("themeModeOverride") { theme } } 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 4cc7899cc8..e2ee8878c8 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 @@ -149,9 +149,7 @@ private fun MigrateFromDeviceLayout( ) { val tempDatabaseFile = rememberSaveable { mutableStateOf(fileForTemporaryDatabase()) } - ColumnWithScrollBar( - Modifier.fillMaxSize(), maxIntrinsicSize = true - ) { + ColumnWithScrollBar(maxIntrinsicSize = true) { AppBarTitle(stringResource(MR.strings.migrate_from_device_title)) SectionByState(migrationState, tempDatabaseFile.value, chatReceiver) SectionBottomSpacer() 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 415f5cdd57..90f8593c4a 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 @@ -162,9 +162,7 @@ private fun ModalData.MigrateToDeviceLayout( close: () -> Unit, ) { val tempDatabaseFile = rememberSaveable { mutableStateOf(fileForTemporaryDatabase()) } - ColumnWithScrollBar( - Modifier.fillMaxSize(), maxIntrinsicSize = true - ) { + ColumnWithScrollBar(maxIntrinsicSize = true) { AppBarTitle(stringResource(MR.strings.migrate_to_device_title)) SectionByState(migrationState, tempDatabaseFile.value, chatReceiver, close) SectionBottomSpacer() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddContactLearnMore.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddContactLearnMore.kt index da59050a3a..077abd1b98 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddContactLearnMore.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddContactLearnMore.kt @@ -15,9 +15,7 @@ import chat.simplex.res.MR @Composable fun AddContactLearnMore(close: () -> Unit) { - ColumnWithScrollBar( - Modifier.padding(horizontal = DEFAULT_PADDING), - ) { + ColumnWithScrollBar(Modifier.padding(horizontal = DEFAULT_PADDING)) { AppBarTitle(stringResource(MR.strings.one_time_link), withPadding = false) ReadableText(MR.strings.scan_qr_to_connect_to_contact) ReadableText(MR.strings.if_you_cant_meet_in_person) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt index c430a62340..e1d3d6541a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt @@ -84,10 +84,9 @@ fun AddGroupLayout( val focusRequester = remember { FocusRequester() } val incognito = remember { mutableStateOf(incognitoPref.get()) } - ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { ModalBottomSheetLayout( scrimColor = Color.Black.copy(alpha = 0.12F), - modifier = Modifier.navigationBarsWithImePadding(), + modifier = Modifier.imePadding(), sheetContent = { GetImageBottomSheet( chosenImage, @@ -100,11 +99,7 @@ fun AddGroupLayout( sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp) ) { ModalView(close = close) { - ColumnWithScrollBar( - Modifier - .fillMaxSize() - .padding(horizontal = DEFAULT_PADDING) - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.create_secret_group_title), hostDevice(rhId)) Box( Modifier @@ -122,7 +117,7 @@ fun AddGroupLayout( } } } - Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + Row(Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Text( stringResource(MR.strings.group_display_name_field), fontSize = 16.sp @@ -134,7 +129,9 @@ fun AddGroupLayout( } } } - ProfileNameField(displayName, "", { isValidDisplayName(it.trim()) }, focusRequester) + Box(Modifier.padding(horizontal = DEFAULT_PADDING)) { + ProfileNameField(displayName, "", { isValidDisplayName(it.trim()) }, focusRequester) + } Spacer(Modifier.height(8.dp)) SettingsActionItem( @@ -170,7 +167,6 @@ fun AddGroupLayout( } } } - } } fun canCreateProfile(displayName: String): Boolean = displayName.trim().isNotEmpty() && isValidDisplayName(displayName.trim()) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt index 64ff7e4f40..1623f8510d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt @@ -89,7 +89,7 @@ private fun ContactConnectionInfoLayout( SettingsActionItemWithContent( icon = painterResource(MR.images.ic_theater_comedy_filled), text = null, - click = { ModalManager.start.showModal { IncognitoView() } }, + click = { ModalManager.end.showModal { IncognitoView() } }, iconColor = Indigo, extraPadding = false ) { @@ -105,9 +105,7 @@ private fun ContactConnectionInfoLayout( } } - ColumnWithScrollBar( - Modifier, - ) { + ColumnWithScrollBar { AppBarTitle( stringResource( if (contactConnection.initiated) MR.strings.you_invited_a_contact 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 1a3ea10806..02996381f8 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 @@ -1,9 +1,7 @@ package chat.simplex.common.views.newchat -import SectionDivider import SectionDividerSpaced import SectionItemView -import SectionSpacer import SectionView import TextIconSpaced import androidx.compose.desktop.ui.tooling.preview.Preview @@ -14,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 @@ -32,56 +29,43 @@ import chat.simplex.common.model.* import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* -import chat.simplex.common.views.chatlist.ScrollDirection +import chat.simplex.common.views.chat.topPaddingToContent +import chat.simplex.common.views.chatlist.* import chat.simplex.common.views.contacts.* import chat.simplex.common.views.helpers.* import chat.simplex.res.MR import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter -import java.net.URI @Composable fun ModalData.NewChatSheet(rh: RemoteHostInfo?, close: () -> Unit) { val oneHandUI = remember { appPrefs.oneHandUI.state } - val keyboardState by getKeyboardState() - val showToolbarInOneHandUI = remember { derivedStateOf { keyboardState == KeyboardState.Closed && oneHandUI.value } } - Scaffold( - bottomBar = { - if (showToolbarInOneHandUI.value) { - Column { - Divider() - CloseSheetBar( - close = close, - showClose = true, - endButtons = { Spacer(Modifier.minimumInteractiveComponentSize()) }, - arrangement = Arrangement.Bottom, - closeBarTitle = generalGetString(MR.strings.new_message), - barPaddingValues = PaddingValues(horizontal = 0.dp) - ) - } - } + Box { + val closeAll = { ModalManager.start.closeModals() } + + Column(modifier = Modifier.fillMaxSize()) { + NewChatSheetLayout( + addContact = { + ModalManager.start.showModalCloseable(endButtons = { AddContactLearnMoreButton() }) { _ -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.INVITE, close = closeAll) } + }, + scanPaste = { + 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) } + }, + rh = rh, + close = close + ) } - ) { - Column( - modifier = Modifier.fillMaxSize().padding(it) - ) { - val closeAll = { ModalManager.start.closeModals() } - - Column(modifier = Modifier.fillMaxSize()) { - NewChatSheetLayout( - addContact = { - ModalManager.start.showModalCloseable(endButtons = { AddContactLearnMoreButton() }) { _ -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.INVITE, close = closeAll ) } - }, - scanPaste = { - 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) } - }, - rh = rh, - close = close + if (oneHandUI.value) { + Column(Modifier.align(Alignment.BottomCenter)) { + DefaultAppBar( + navigationButton = { NavigationButtonBack(onButtonClicked = close) }, + fixedTitleText = generalGetString(MR.strings.new_message), + onTop = false, ) } } @@ -187,168 +171,258 @@ private fun ModalData.NewChatSheetLayout( derivedStateOf { filterContactTypes(chatModel.chats.value, deletedContactTypes) } } - LazyColumnWithScrollBar( - Modifier.fillMaxSize(), - listState, - 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 + 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, + ) + ) + + @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, + showAppBar = !oneHandUI.value, + ) { + if (oneHandUI.value) { + BackHandler(onBack = closeDeletedChats) + } + 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 { - 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( + state = listState, + reverseLayout = oneHandUI.value + ) { + item { Spacer(Modifier.height(blankSpaceSize)) } + 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 - } - } else { - 0 - } - IntOffset(0, y) - } - .background(MaterialTheme.colors.background) - ) { - Divider() - ContactsSearchBar( - listState = listState, - searchText = searchText, - searchShowingSimplexLink = searchShowingSimplexLink, - searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink, - close = close, - ) - if (!oneHandUI.value) { - Divider() - } - } - } - item { - if (searchText.value.text.isEmpty()) { - Spacer(Modifier.padding(bottom = 27.dp)) - } - - 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() - }) - } + when (listState.firstVisibleItemIndex) { + 0 -> 0 + 1 -> listState.firstVisibleItemScrollOffset + else -> 1000 } } - ) { - 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) + 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() + Column(Modifier.consumeWindowInsets(WindowInsets.navigationBars).consumeWindowInsets(PaddingValues(bottom = AppBarHeight * fontSizeSqrtMultiplier))) { + ContactsSearchBar( + listState = listState, + searchText = searchText, + searchShowingSimplexLink = searchShowingSimplexLink, + searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink, + close = close, + ) + Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime)) } } } - } - - 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 { + DeletedChatsItem(actionButtonsOriginal.asReversed()) + } + 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) + } + } + + @Composable + fun NonOneHandLazyColumn() { + val blankSpaceSize = topPaddingToContent() + LazyColumnWithScrollBar( + Modifier.imePadding(), + state = 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(oneHandUI.value, true) } } } @@ -554,26 +628,7 @@ private fun contactTypesSearchTargets(baseContactTypes: List, searc @Composable private fun ModalData.DeletedContactsView(rh: RemoteHostInfo?, closeDeletedChats: () -> Unit, close: () -> Unit) { val oneHandUI = remember { appPrefs.oneHandUI.state } - val keyboardState by getKeyboardState() - val showToolbarInOneHandUI = remember { derivedStateOf { keyboardState == KeyboardState.Closed && oneHandUI.value } } - - Scaffold( - bottomBar = { - if (showToolbarInOneHandUI.value) { - Column { - Divider() - CloseSheetBar( - close = closeDeletedChats, - showClose = true, - endButtons = { Spacer(Modifier.minimumInteractiveComponentSize()) }, - arrangement = Arrangement.Bottom, - closeBarTitle = generalGetString(MR.strings.deleted_chats), - barPaddingValues = PaddingValues(horizontal = 0.dp) - ) - } - } - } - ) { contentPadding -> + Box { val listState = remember { appBarHandler.listState } val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) } val searchShowingSimplexLink = remember { mutableStateOf(false) } @@ -590,57 +645,93 @@ private fun ModalData.DeletedContactsView(rh: RemoteHostInfo?, closeDeletedChats contactChats = allChats ) - LazyColumnWithScrollBar( - Modifier.fillMaxSize(), - contentPadding = contentPadding, - reverseLayout = oneHandUI.value, - ) { - item { - if (!oneHandUI.value) { - Box(contentAlignment = Alignment.Center) { - val bottomPadding = DEFAULT_PADDING - AppBarTitle( - stringResource(MR.strings.deleted_chats), - hostDevice(rh?.remoteHostId), - bottomPadding = bottomPadding - ) - } - } - } - item { - if (!oneHandUI.value) { - Divider() - } - ContactsSearchBar( - listState = listState, - searchText = searchText, - searchShowingSimplexLink = searchShowingSimplexLink, - searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink, - close = close, - ) - Divider() - } - - item { - if (filteredContactChats.isEmpty() && allChats.isNotEmpty()) { - Column(Modifier.fillMaxSize().padding(DEFAULT_PADDING)) { - Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { - Text( - generalGetString(MR.strings.no_filtered_contacts), - color = MaterialTheme.colors.secondary, + Box { + val topPaddingToContent = topPaddingToContent() + LazyColumnWithScrollBar( + if (!oneHandUI.value) Modifier.imePadding() else Modifier, + contentPadding = PaddingValues( + top = if (!oneHandUI.value) topPaddingToContent else 0.dp, + bottom = if (oneHandUI.value) WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier else 0.dp + ), + reverseLayout = oneHandUI.value, + ) { + item { + if (!oneHandUI.value) { + Box(contentAlignment = Alignment.Center) { + val bottomPadding = DEFAULT_PADDING + AppBarTitle( + stringResource(MR.strings.deleted_chats), + hostDevice(rh?.remoteHostId), + bottomPadding = bottomPadding ) } } } - } + item { + if (!oneHandUI.value) { + Divider() + ContactsSearchBar( + listState = listState, + searchText = searchText, + searchShowingSimplexLink = searchShowingSimplexLink, + searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink, + close = close, + ) + } else { + Column(Modifier.consumeWindowInsets(WindowInsets.navigationBars).consumeWindowInsets(PaddingValues(bottom = AppBarHeight))) { + ContactsSearchBar( + listState = listState, + searchText = searchText, + searchShowingSimplexLink = searchShowingSimplexLink, + searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink, + close = close, + ) + Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime)) + } + } + Divider() + } - itemsIndexed(filteredContactChats) { index, chat -> - val nextChatSelected = remember(chat.id, filteredContactChats) { - derivedStateOf { - chatModel.chatId.value != null && filteredContactChats.getOrNull(index + 1)?.id == chatModel.chatId.value + item { + if (filteredContactChats.isEmpty() && allChats.isNotEmpty()) { + Column(Modifier.fillMaxSize().padding(DEFAULT_PADDING)) { + Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { + Text( + generalGetString(MR.strings.no_filtered_contacts), + color = MaterialTheme.colors.secondary, + ) + } + } } } - ContactListNavLinkView(chat, nextChatSelected, showDeletedChatIcon = false) + + 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 = false) + } + if (appPlatform.isAndroid) { + item { + Spacer(if (oneHandUI.value) Modifier.windowInsetsTopHeight(WindowInsets.statusBars) else Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars)) + } + } + } + if (oneHandUI.value) { + StatusBarBackground() + } else { + NavigationBarBackground(oneHandUI.value, true) + } + } + if (oneHandUI.value) { + Column(Modifier.align(Alignment.BottomCenter)) { + DefaultAppBar( + navigationButton = { NavigationButtonBack(onButtonClicked = closeDeletedChats) }, + fixedTitleText = generalGetString(MR.strings.deleted_chats), + onTop = false, + ) } } } 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 5298e11e75..61403e07a4 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 @@ -29,10 +29,12 @@ import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.sp import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.chat.topPaddingToContent import chat.simplex.common.views.helpers.* import chat.simplex.common.views.usersettings.* import chat.simplex.res.MR @@ -398,8 +400,12 @@ fun ActiveProfilePicker( .fillMaxSize() .alpha(if (progressByTimeout) 0.6f else 1f) ) { - LazyColumnWithScrollBar(userScrollEnabled = !switchingProfile.value) { + LazyColumnWithScrollBar(Modifier.padding(top = topPaddingToContent()), userScrollEnabled = !switchingProfile.value) { item { + val oneHandUI = remember { appPrefs.oneHandUI.state } + if (oneHandUI.value) { + Spacer(Modifier.padding(top = DEFAULT_PADDING + 5.dp)) + } AppBarTitle(stringResource(MR.strings.select_chat_profile), hostDevice(rhId), bottomPadding = DEFAULT_PADDING) } val activeProfile = filteredProfiles.firstOrNull { it.activeUser } @@ -434,6 +440,9 @@ fun ActiveProfilePicker( ProfilePickerUserOption(p) } } + item { + Spacer(Modifier.imePadding().padding(bottom = DEFAULT_BOTTOM_PADDING)) + } } } if (progressByTimeout) { @@ -472,13 +481,13 @@ private fun InviteView(rhId: Long?, connReqInvitation: String, contactConnection end = 16.dp ), click = { - ModalManager.start.showCustomModal { close -> + ModalManager.start.showCustomModal(keyboardCoversBar = false) { close -> val search = rememberSaveable { mutableStateOf("") } ModalView( { close() }, - endButtons = { - SearchTextField(Modifier.fillMaxWidth(), placeholder = stringResource(MR.strings.search_verb), alwaysVisible = true) { search.value = it } - }, + showSearch = true, + searchAlwaysVisible = true, + onSearchValueChanged = { search.value = it }, content = { ActiveProfilePicker( search = search, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/CreateSimpleXAddress.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/CreateSimpleXAddress.kt index 20a7ada3aa..28ad0fdb7b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/CreateSimpleXAddress.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/CreateSimpleXAddress.kt @@ -76,15 +76,9 @@ private fun CreateSimpleXAddressLayout( createAddress: () -> Unit, nextStep: () -> Unit, ) { - val handler = remember { AppBarHandler() } - CompositionLocalProvider( - LocalAppBarHandler provides handler - ) { + CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) { ModalView({}, showClose = false) { ColumnWithScrollBar( - Modifier - .fillMaxSize() - .themedBackground(), horizontalAlignment = Alignment.CenterHorizontally, ) { AppBarTitle(stringResource(MR.strings.simplex_address)) 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 9c7e2bdce7..98e8ec971d 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,11 +23,7 @@ import dev.icerock.moko.resources.StringResource @Composable fun HowItWorks(user: User?, onboardingStage: SharedPreference? = null) { - ColumnWithScrollBar( - Modifier - .fillMaxWidth() - .padding(DEFAULT_PADDING), - ) { + ColumnWithScrollBar(Modifier.padding(DEFAULT_PADDING)) { AppBarTitle(stringResource(MR.strings.how_simplex_works), withPadding = false) ReadableText(MR.strings.many_people_asked_how_can_it_deliver) ReadableText(MR.strings.to_protect_privacy_simplex_has_ids_for_queues) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/LinkAMobileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/LinkAMobileView.kt index f0e34218d1..9e48f4b2bd 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/LinkAMobileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/LinkAMobileView.kt @@ -7,21 +7,16 @@ import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatModel -import chat.simplex.common.platform.BackHandler import chat.simplex.common.platform.chatModel import chat.simplex.common.ui.theme.DEFAULT_PADDING -import chat.simplex.common.ui.theme.themedBackground import chat.simplex.common.views.helpers.* import chat.simplex.common.views.remote.AddingMobileDevice import chat.simplex.common.views.remote.DeviceNameField import chat.simplex.common.views.usersettings.PreferenceToggle import chat.simplex.res.MR -import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource @Composable @@ -59,34 +54,32 @@ private fun LinkAMobileLayout( staleQrCode: MutableState, updateDeviceName: (String) -> Unit, ) { - Column(Modifier.themedBackground()) { - CloseSheetBar(close = { - appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) - }) - BackHandler(onBack = { - appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) - }) - AppBarTitle(stringResource(if (remember { chatModel.remoteHosts }.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles)) - Row(Modifier.weight(1f).padding(horizontal = DEFAULT_PADDING * 2), verticalAlignment = Alignment.CenterVertically) { - Column( - Modifier.weight(0.3f), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - SectionView(generalGetString(MR.strings.this_device_name).uppercase()) { - DeviceNameField(deviceName.value ?: "") { updateDeviceName(it) } - SectionTextFooter(generalGetString(MR.strings.this_device_name_shared_with_mobile)) - PreferenceToggle(stringResource(MR.strings.multicast_discoverable_via_local_network), checked = remember { ChatModel.controller.appPrefs.offerRemoteMulticast.state }.value) { - ChatModel.controller.appPrefs.offerRemoteMulticast.set(it) + ModalView({ appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) }) { + Column(Modifier.fillMaxSize().padding(top = AppBarHeight * fontSizeSqrtMultiplier)) { + Box(Modifier.align(Alignment.CenterHorizontally)) { + AppBarTitle(stringResource(if (remember { chatModel.remoteHosts }.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles)) + } + Row(Modifier.weight(1f).padding(horizontal = DEFAULT_PADDING * 2), verticalAlignment = Alignment.CenterVertically) { + Column( + Modifier.weight(0.3f), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + SectionView(generalGetString(MR.strings.this_device_name).uppercase()) { + DeviceNameField(deviceName.value ?: "") { updateDeviceName(it) } + SectionTextFooter(generalGetString(MR.strings.this_device_name_shared_with_mobile)) + PreferenceToggle(stringResource(MR.strings.multicast_discoverable_via_local_network), checked = remember { ChatModel.controller.appPrefs.offerRemoteMulticast.state }.value) { + ChatModel.controller.appPrefs.offerRemoteMulticast.set(it) + } } } - } - Box(Modifier.weight(0.7f)) { - AddingMobileDevice(false, staleQrCode, connecting) { - // currentRemoteHost will be set instantly but remoteHosts may be delayed - if (chatModel.remoteHosts.isEmpty() && chatModel.currentRemoteHost.value == null) { - chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) - } else { - chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.OnboardingComplete) + Box(Modifier.weight(0.7f)) { + AddingMobileDevice(false, staleQrCode, connecting) { + // currentRemoteHost will be set instantly but remoteHosts may be delayed + if (chatModel.remoteHosts.isEmpty() && chatModel.currentRemoteHost.value == null) { + chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) + } else { + chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.OnboardingComplete) + } } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.kt index 1903b3cf81..e480d4330b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.kt @@ -25,16 +25,9 @@ import chat.simplex.res.MR @Composable fun SetNotificationsMode(m: ChatModel) { - val handler = remember { AppBarHandler() } - CompositionLocalProvider( - LocalAppBarHandler provides handler - ) { + CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) { ModalView({}, showClose = false) { - ColumnWithScrollBar( - modifier = Modifier - .fillMaxSize() - .themedBackground() - ) { + ColumnWithScrollBar(Modifier.themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer)) { Box(Modifier.align(Alignment.CenterHorizontally)) { AppBarTitle(stringResource(MR.strings.onboarding_notifications_mode_title)) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt index 858ca68af3..d0a3e601d2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt @@ -104,13 +104,10 @@ private fun SetupDatabasePassphraseLayout( onConfirmEncrypt: () -> Unit, nextStep: () -> Unit, ) { - val handler = remember { AppBarHandler() } - CompositionLocalProvider( - LocalAppBarHandler provides handler - ) { + CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) { ModalView({}, showClose = false) { ColumnWithScrollBar( - Modifier.fillMaxSize().themedBackground().padding(bottom = DEFAULT_PADDING * 2), + Modifier.themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer).padding(bottom = DEFAULT_PADDING * 2), horizontalAlignment = Alignment.CenterHorizontally, ) { AppBarTitle(stringResource(MR.strings.setup_database_passphrase)) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt index c176950902..e43404cb07 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt @@ -31,15 +31,17 @@ import dev.icerock.moko.resources.StringResource @Composable fun SimpleXInfo(chatModel: ChatModel, onboarding: Boolean = true) { if (onboarding) { - ModalView({}, showClose = false, endButtons = { - IconButton({ ModalManager.fullscreen.showModal { HowItWorks(chatModel.currentUser.value, null) }}) { - Icon(painterResource(MR.images.ic_info), null, Modifier.size(28.dp), tint = MaterialTheme.colors.primary) + CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) { + ModalView({}, showClose = false, endButtons = { + IconButton({ ModalManager.fullscreen.showModal { HowItWorks(chatModel.currentUser.value, null) } }) { + Icon(painterResource(MR.images.ic_info), null, Modifier.size(28.dp), tint = MaterialTheme.colors.primary) + } + }) { + SimpleXInfoLayout( + user = chatModel.currentUser.value, + onboardingStage = chatModel.controller.appPrefs.onboardingStage + ) } - }) { - SimpleXInfoLayout( - user = chatModel.currentUser.value, - onboardingStage = chatModel.controller.appPrefs.onboardingStage - ) } } else { SimpleXInfoLayout( @@ -56,7 +58,6 @@ fun SimpleXInfoLayout( ) { ColumnWithScrollBar( Modifier - .fillMaxSize() .padding(horizontal = DEFAULT_PADDING), horizontalAlignment = Alignment.CenterHorizontally ) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt index 703f3b8915..bdbef3b654 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt @@ -119,11 +119,10 @@ fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) { ModalView(close = close) { ColumnWithScrollBar( Modifier - .fillMaxSize() .padding(horizontal = DEFAULT_PADDING), verticalArrangement = Arrangement.spacedBy(DEFAULT_PADDING.times(0.75f)) ) { - AppBarTitle(String.format(generalGetString(MR.strings.new_in_version), v.version), bottomPadding = DEFAULT_PADDING) + AppBarTitle(String.format(generalGetString(MR.strings.new_in_version), v.version), withPadding = false, bottomPadding = DEFAULT_PADDING) v.features.forEach { feature -> if (feature.show) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectDesktopView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectDesktopView.kt index eb7fd7b6b5..c3eed3118e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectDesktopView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectDesktopView.kt @@ -74,9 +74,7 @@ private fun ConnectDesktopLayout(deviceName: String, close: () -> Unit) { val sessionAddress = remember { mutableStateOf("") } val remoteCtrls = remember { mutableStateListOf() } val session = remember { chatModel.remoteCtrlSession }.value - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - ) { + ColumnWithScrollBar { val discovery = if (session == null) null else session.sessionState is UIRemoteCtrlSessionState.Searching if (discovery == true || (discovery == null && !showConnectScreen.value)) { SearchingDesktop(deviceName, remoteCtrls) @@ -408,9 +406,7 @@ private fun DesktopAddressView(sessionAddress: MutableState) { @Composable private fun LinkedDesktopsView(remoteCtrls: SnapshotStateList) { - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.linked_desktops)) SectionView(stringResource(MR.strings.desktop_devices).uppercase()) { remoteCtrls.forEach { rc -> diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt index 92503f273e..e727b94781 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt @@ -89,7 +89,7 @@ fun ConnectMobileLayout( connectDesktop: () -> Unit, deleteHost: (RemoteHostInfo) -> Unit, ) { - ColumnWithScrollBar(Modifier.fillMaxWidth()) { + ColumnWithScrollBar { AppBarTitle(stringResource(if (remember { chatModel.remoteHosts }.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles)) SectionView(generalGetString(MR.strings.this_device_name).uppercase()) { DeviceNameField(deviceName.value ?: "") { updateDeviceName(it) } @@ -176,7 +176,15 @@ private fun ConnectMobileViewLayout( refreshQrCode: () -> Unit = {}, UnderQrLayout: @Composable () -> Unit = {}, ) { - ColumnWithScrollBar(Modifier.fillMaxWidth()) { + @Composable + fun ScrollableLayout(content: @Composable ColumnScope.() -> Unit) { + if (LocalAppBarHandler.current != null) { + ColumnWithScrollBar(content = content) + } else { + ColumnWithScrollBarNoAppBar(content = content) + } + } + ScrollableLayout { if (title != null) { AppBarTitle(title) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/AdvancedNetworkSettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/AdvancedNetworkSettings.kt index 35e0a3c6d8..5757b5d1f4 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/AdvancedNetworkSettings.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/AdvancedNetworkSettings.kt @@ -202,10 +202,7 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U ) { val secondsLabel = stringResource(MR.strings.network_option_seconds_label) - ColumnWithScrollBar( - Modifier - .fillMaxWidth(), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.network_settings_title)) if (currentRemoteHost == null) { @@ -328,9 +325,7 @@ private fun SMPProxyModePicker( icon = painterResource(MR.images.ic_settings_ethernet), onSelected = { showModal { - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.network_smp_proxy_mode_private_routing)) SectionViewSelectableCards(null, smpProxyMode, values, updateSMPProxyMode) } @@ -365,9 +360,7 @@ private fun SMPProxyFallbackPicker( enabled = enabled, onSelected = { showModal { - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.network_smp_proxy_fallback_allow_downgrade)) SectionViewSelectableCards(null, smpProxyFallback, values, updateSMPProxyFallback) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt index f2cd26803b..b4fead6692 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt @@ -4,9 +4,11 @@ import SectionBottomSpacer import SectionDividerSpaced import SectionItemView import SectionItemViewSpaceBetween +import SectionItemViewWithoutMinPadding import SectionSpacer import SectionView import androidx.compose.foundation.* +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.grid.* import androidx.compose.foundation.shape.CircleShape @@ -39,6 +41,7 @@ import chat.simplex.common.views.chat.item.msgTailWidthDp import chat.simplex.res.MR import com.godaddy.android.colorpicker.ClassicColorPicker import com.godaddy.android.colorpicker.HsvColor +import dev.icerock.moko.resources.StringResource import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.datetime.Clock @@ -86,27 +89,114 @@ object AppearanceScope { } @Composable - fun MessageShapeSection() { - SectionView(stringResource(MR.strings.settings_section_title_message_shape).uppercase(), contentPadding = PaddingValues()) { - Row(modifier = Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING + 4.dp ) ,verticalAlignment = Alignment.CenterVertically) { - Text(stringResource(MR.strings.settings_message_shape_corner), color = colors.onBackground) - Spacer(Modifier.width(10.dp)) - Slider( - remember { appPreferences.chatItemRoundness.state }.value, - valueRange = 0f..1f, - steps = 20, - onValueChange = { - val diff = it % 0.05f - appPreferences.chatItemRoundness.set(it + (if (diff >= 0.025f) -diff + 0.05f else -diff)) - saveThemeToDatabase(null) - }, - colors = SliderDefaults.colors( - activeTickColor = Color.Transparent, - inactiveTickColor = Color.Transparent, + fun AppToolbarsSection() { + BoxWithConstraints { + SectionView(stringResource(MR.strings.appearance_app_toolbars).uppercase()) { + SectionItemViewWithoutMinPadding { + Box(Modifier.weight(1f)) { + Text( + stringResource(MR.strings.appearance_in_app_bars_alpha), + Modifier.clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { + appPrefs.inAppBarsAlpha.set(appPrefs.inAppBarsDefaultAlpha) + }, + maxLines = 1 + ) + } + Spacer(Modifier.padding(end = 10.dp)) + Slider( + (1 - remember { appPrefs.inAppBarsAlpha.state }.value).coerceIn(0f, 0.5f), + onValueChange = { + val diff = it % 0.025f + appPrefs.inAppBarsAlpha.set(1f - (String.format(Locale.US, "%.3f", it + (if (diff >= 0.0125f) -diff + 0.025f else -diff)).toFloatOrNull() ?: 1f)) + }, + Modifier.widthIn(max = (this@BoxWithConstraints.maxWidth - DEFAULT_PADDING * 2) * 0.618f), + valueRange = 0f..0.5f, + steps = 21, + colors = SliderDefaults.colors( + activeTickColor = Color.Transparent, + inactiveTickColor = Color.Transparent, + ) ) - ) + } + // In Android in OneHandUI there is a problem with setting initial value of blur if it was 0 before entering the screen. + // So doing in two steps works ok + fun saveBlur(value: Int) { + val oneHandUI = appPrefs.oneHandUI.get() + val pref = appPrefs.appearanceBarsBlurRadius + if (appPlatform.isAndroid && oneHandUI && pref.get() == 0) { + pref.set(if (value > 2) value - 1 else value + 1) + withApi { + delay(50) + pref.set(value) + } + } else { + pref.set(value) + } + } + val blur = remember { appPrefs.appearanceBarsBlurRadius.state } + if (appPrefs.deviceSupportsBlur || blur.value > 0) { + SectionItemViewWithoutMinPadding { + Box(Modifier.weight(1f)) { + Text( + stringResource(MR.strings.appearance_bars_blur_radius), + Modifier.clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { + saveBlur(50) + }, + maxLines = 1 + ) + } + Spacer(Modifier.padding(end = 10.dp)) + Slider( + blur.value.toFloat() / 100f, + onValueChange = { + val diff = it % 0.05f + saveBlur(((String.format(Locale.US, "%.2f", it + (if (diff >= 0.025f) -diff + 0.05f else -diff)).toFloatOrNull() ?: 1f) * 100).toInt()) + }, + Modifier.widthIn(max = (this@BoxWithConstraints.maxWidth - DEFAULT_PADDING * 2) * 0.618f), + valueRange = 0f..1f, + steps = 21, + colors = SliderDefaults.colors( + activeTickColor = Color.Transparent, + inactiveTickColor = Color.Transparent, + ) + ) + } + } + } + } + } + + @Composable + fun MessageShapeSection() { + BoxWithConstraints { + SectionView(stringResource(MR.strings.settings_section_title_message_shape).uppercase()) { + SectionItemViewWithoutMinPadding { + Text(stringResource(MR.strings.settings_message_shape_corner), Modifier.weight(1f)) + Spacer(Modifier.width(10.dp)) + Slider( + remember { appPreferences.chatItemRoundness.state }.value, + onValueChange = { + val diff = it % 0.05f + appPreferences.chatItemRoundness.set(it + (if (diff >= 0.025f) -diff + 0.05f else -diff)) + saveThemeToDatabase(null) + }, + Modifier.widthIn(max = (this@BoxWithConstraints.maxWidth - DEFAULT_PADDING * 2) * 0.618f), + valueRange = 0f..1f, + steps = 20, + colors = SliderDefaults.colors( + activeTickColor = Color.Transparent, + inactiveTickColor = Color.Transparent, + ) + ) + } + SettingsPreferenceItem(icon = null, stringResource(MR.strings.settings_message_shape_tail), appPreferences.chatItemTail) } - SettingsPreferenceItem(icon = null, stringResource(MR.strings.settings_message_shape_tail), appPreferences.chatItemTail) } } @@ -115,7 +205,7 @@ object AppearanceScope { val localFontScale = remember { mutableStateOf(appPrefs.fontScale.get()) } SectionView(stringResource(MR.strings.appearance_font_size).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) { Row(Modifier.padding(top = 10.dp), verticalAlignment = Alignment.CenterVertically) { - Box(Modifier.size(60.dp) + Box(Modifier.size(50.dp) .background(MaterialTheme.colors.surface, RoundedCornerShape(percent = 22)) .clip(RoundedCornerShape(percent = 22)) .clickable { @@ -129,7 +219,7 @@ object AppearanceScope { Text("Aa", color = if (localFontScale.value == 1f) MaterialTheme.colors.primary else MaterialTheme.colors.onBackground) } } - Spacer(Modifier.width(10.dp)) + Spacer(Modifier.width(15.dp)) // Text("${(localFontScale.value * 100).roundToInt()}%", Modifier.width(70.dp), textAlign = TextAlign.Center, fontSize = 12.sp) if (appPlatform.isAndroid) { Slider( @@ -185,7 +275,7 @@ object AppearanceScope { Column(Modifier .drawWithCache { if (wallpaperImage != null && wallpaperType != null && backgroundColor != null && tintColor != null) { - chatViewBackground(wallpaperImage, wallpaperType, backgroundColor, tintColor) + chatViewBackground(wallpaperImage, wallpaperType, backgroundColor, tintColor, null, null) } else { onDrawBehind { drawRect(themeBackgroundColor) @@ -514,9 +604,7 @@ object AppearanceScope { @Composable fun CustomizeThemeView(onChooseType: (WallpaperType?) -> Unit) { - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - ) { + ColumnWithScrollBar { val currentTheme by CurrentColors.collectAsState() AppBarTitle(stringResource(MR.strings.customize_theme_title)) @@ -909,10 +997,7 @@ object AppearanceScope { currentColors: () -> ThemeManager.ActiveTheme, onColorChange: (Color?) -> Unit, ) { - ColumnWithScrollBar( - Modifier - .fillMaxWidth() - ) { + ColumnWithScrollBar(Modifier.imePadding()) { AppBarTitle(name.text) val supportedLiveChange = name in listOf(ThemeColor.SECONDARY, ThemeColor.BACKGROUND, ThemeColor.SURFACE, ThemeColor.RECEIVED_MESSAGE, ThemeColor.SENT_MESSAGE, ThemeColor.SENT_QUOTE, ThemeColor.WALLPAPER_BACKGROUND, ThemeColor.WALLPAPER_TINT) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/CallSettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/CallSettings.kt index 468a192f09..cb36e4ae1a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/CallSettings.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/CallSettings.kt @@ -36,7 +36,7 @@ fun CallSettingsLayout( callOnLockScreen: SharedPreference, editIceServers: () -> Unit, ) { - ColumnWithScrollBar(Modifier.fillMaxWidth()) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.your_calls)) val lockCallState = remember { mutableStateOf(callOnLockScreen.get()) } SectionView(stringResource(MR.strings.settings_section_title_settings)) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt index 2123d98f41..87770e9ffd 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt @@ -22,12 +22,10 @@ import chat.simplex.common.views.helpers.* import chat.simplex.res.MR @Composable -fun DeveloperView( - m: ChatModel, - showCustomModal: (@Composable ModalData.(ChatModel, () -> Unit) -> Unit) -> (() -> Unit), - withAuth: (title: String, desc: String, block: () -> Unit) -> Unit +fun DeveloperView(withAuth: (title: String, desc: String, block: () -> Unit) -> Unit ) { - ColumnWithScrollBar(Modifier.fillMaxWidth()) { + val m = chatModel + ColumnWithScrollBar { val uriHandler = LocalUriHandler.current AppBarTitle(stringResource(MR.strings.settings_developer_tools)) val developerTools = m.controller.appPrefs.developerTools @@ -35,7 +33,7 @@ fun DeveloperView( val unchangedHints = mutableStateOf(unchangedHintPreferences()) SectionView { InstallTerminalAppItem(uriHandler) - ChatConsoleItem { withAuth(generalGetString(MR.strings.auth_open_chat_console), generalGetString(MR.strings.auth_log_in_using_credential), showCustomModal { it, close -> TerminalView(false, close) }) } + ChatConsoleItem { withAuth(generalGetString(MR.strings.auth_open_chat_console), generalGetString(MR.strings.auth_log_in_using_credential)) { ModalManager.start.showModalCloseable { TerminalView(false) } } } ResetHintsItem(unchangedHints) SettingsPreferenceItem(painterResource(MR.images.ic_code), stringResource(MR.strings.show_developer_options), developerTools) SectionTextFooter( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/HelpView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/HelpView.kt index c2bf69bc0e..aaaef31583 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/HelpView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/HelpView.kt @@ -21,11 +21,7 @@ fun HelpView(userDisplayName: String) { @Composable fun HelpLayout(userDisplayName: String) { - ColumnWithScrollBar( - Modifier - .fillMaxWidth() - .padding(horizontal = DEFAULT_PADDING), - ){ + ColumnWithScrollBar(Modifier.padding(horizontal = DEFAULT_PADDING)){ AppBarTitle(String.format(stringResource(MR.strings.personal_welcome), userDisplayName), withPadding = false) ChatHelpView() } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/HiddenProfileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/HiddenProfileView.kt index e5116f9149..55bd796a3b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/HiddenProfileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/HiddenProfileView.kt @@ -56,10 +56,7 @@ private fun HiddenProfileLayout( user: User, saveProfilePassword: (String) -> Unit ) { - ColumnWithScrollBar( - Modifier - .fillMaxWidth(), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.hide_profile)) SectionView(contentPadding = PaddingValues(start = 8.dp, end = DEFAULT_PADDING)) { UserProfileRow(user) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt index dc3def3884..2c4870b121 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt @@ -109,7 +109,7 @@ fun NetworkAndServersView() { toggleSocksProxy: (Boolean) -> Unit, ) { val m = chatModel - ColumnWithScrollBar(Modifier.fillMaxWidth()) { + ColumnWithScrollBar { val showModal = { it: @Composable ModalData.() -> Unit -> ModalManager.start.showModal(content = it) } val showCustomModal = { it: @Composable (close: () -> Unit) -> Unit -> ModalManager.start.showCustomModal { close -> it(close) }} @@ -304,10 +304,7 @@ fun SocksProxySettings( } }, ) { - ColumnWithScrollBar( - Modifier - .fillMaxWidth() - ) { + ColumnWithScrollBar { AppBarTitle(generalGetString(MR.strings.network_socks_proxy_settings)) SectionView(stringResource(MR.strings.network_socks_proxy).uppercase()) { Column(Modifier.padding(horizontal = DEFAULT_PADDING)) { @@ -479,9 +476,7 @@ fun SessionModePicker( icon = painterResource(MR.images.ic_safety_divider), onSelected = { showModal { - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.network_session_mode_transport_isolation)) SectionViewSelectable(null, sessionMode, values, updateSessionMode) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NotificationsSettingsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NotificationsSettingsView.kt index 515d73a426..60bde83c17 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NotificationsSettingsView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NotificationsSettingsView.kt @@ -56,9 +56,7 @@ fun NotificationsSettingsLayout( val modes = remember { notificationModes() } val previewModes = remember { notificationPreviewModes() } - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.notifications)) SectionView(null) { if (appPlatform == AppPlatform.ANDROID) { @@ -90,9 +88,7 @@ fun NotificationsModeView( onNotificationsModeSelected: (NotificationsMode) -> Unit, ) { val modes = remember { notificationModes() } - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.settings_notifications_mode_title).lowercase().capitalize(Locale.current)) SectionViewSelectable(null, notificationsMode, modes, onNotificationsModeSelected) } @@ -104,9 +100,7 @@ fun NotificationPreviewView( onNotificationPreviewModeSelected: (NotificationPreviewMode) -> Unit, ) { val previewModes = remember { notificationPreviewModes() } - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.settings_notification_preview_title)) SectionViewSelectable(null, notificationPreviewMode, previewModes, onNotificationPreviewModeSelected) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Preferences.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Preferences.kt index 96a0bdcda3..bc27773ca6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Preferences.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Preferences.kt @@ -66,9 +66,7 @@ private fun PreferencesLayout( reset: () -> Unit, savePrefs: () -> Unit, ) { - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.your_preferences)) val timedMessages = remember(preferences) { mutableStateOf(preferences.timedMessages.allow) } TimedMessagesFeatureSection(timedMessages) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt index abf318390f..9ec2d29843 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt @@ -55,9 +55,7 @@ fun PrivacySettingsView( showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), setPerformLA: (Boolean) -> Unit ) { - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - ) { + ColumnWithScrollBar { val simplexLinkMode = chatModel.controller.appPrefs.simplexLinkMode AppBarTitle(stringResource(MR.strings.your_privacy)) PrivacyDeviceSection(showSettingsModal, setPerformLA) @@ -514,9 +512,7 @@ fun SimplexLockView( } } - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.chat_lock)) SectionView { EnableLock(remember { appPrefs.performLA.state }) { performLAToggle -> diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServerView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServerView.kt index 3a1a1cb8f3..be566e6c5a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServerView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServerView.kt @@ -75,10 +75,7 @@ private fun ProtocolServerLayout( onUpdate: (ServerCfg) -> Unit, onDelete: () -> Unit, ) { - ColumnWithScrollBar( - Modifier - .fillMaxWidth() - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(if (server.preset) MR.strings.smp_servers_preset_server else MR.strings.smp_servers_your_server)) if (server.preset) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServersView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServersView.kt index 5d5f1d039a..f5e3cda2c7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServersView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServersView.kt @@ -192,10 +192,7 @@ private fun ProtocolServersLayout( saveSMPServers: () -> Unit, showServer: (ServerCfg) -> Unit, ) { - ColumnWithScrollBar( - Modifier - .fillMaxWidth() - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(if (serverProtocol == ServerProtocol.SMP) MR.strings.your_SMP_servers else MR.strings.your_XFTP_servers)) val configuredServers = servers.filter { it.preset || it.enabled } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ScanProtocolServer.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ScanProtocolServer.kt index 7c2c578d6a..966f44cac7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ScanProtocolServer.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ScanProtocolServer.kt @@ -7,6 +7,7 @@ import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.dp import chat.simplex.common.model.ServerAddress.Companion.parseServerAddress import chat.simplex.common.model.ServerCfg +import chat.simplex.common.platform.ColumnWithScrollBar import chat.simplex.common.ui.theme.DEFAULT_PADDING import chat.simplex.common.views.helpers.* import chat.simplex.common.views.newchat.QRCodeScanner @@ -17,10 +18,7 @@ expect fun ScanProtocolServer(rhId: Long?, onNext: (ServerCfg) -> Unit) @Composable fun ScanProtocolServerLayout(rhId: Long?, onNext: (ServerCfg) -> Unit) { - Column( - Modifier - .fillMaxSize() - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.smp_servers_scan_qr)) QRCodeScanner { text -> val res = parseServerAddress(text) 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 0229e7da2a..ef4acdeac6 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 @@ -74,10 +74,7 @@ private fun SetDeliveryReceiptsLayout( userCount: Int, ) { Box(Modifier.padding(top = DEFAULT_PADDING)) { - ColumnWithScrollBar( - Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { + ColumnWithScrollBar(horizontalAlignment = Alignment.CenterHorizontally) { AppBarTitle(stringResource(MR.strings.delivery_receipts_title)) Spacer(Modifier.weight(1f)) 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 bb4a0b61b0..78c5e3b212 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 @@ -39,7 +39,6 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, close: ( val user = chatModel.currentUser.value val stopped = chatModel.chatRunning.value == false SettingsLayout( - profile = user?.profile, stopped, chatModel.chatDbEncrypted.value == true, remember { chatModel.controller.appPrefs.storeDBPassphrase.state }.value, @@ -53,9 +52,9 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, close: ( val search = rememberSaveable { mutableStateOf("") } ModalView( { close() }, - endButtons = { - SearchTextField(Modifier.fillMaxWidth(), placeholder = stringResource(MR.strings.search_verb), alwaysVisible = true) { search.value = it } - }, + showSearch = true, + searchAlwaysVisible = true, + onSearchValueChanged = { search.value = it }, content = { modalView(chatModel, search) }) } }, @@ -80,7 +79,6 @@ val simplexTeamUri = @Composable fun SettingsLayout( - profile: LocalProfile?, stopped: Boolean, encrypted: Boolean, passphraseSaved: Boolean, @@ -94,18 +92,12 @@ fun SettingsLayout( showVersion: () -> Unit, withAuth: (title: String, desc: String, block: () -> Unit) -> Unit, ) { - val scope = rememberCoroutineScope() val view = LocalMultiplatformView() LaunchedEffect(Unit) { hideKeyboard(view) } - val theme = CurrentColors.collectAsState() val uriHandler = LocalUriHandler.current - ColumnWithScrollBar( - Modifier - .fillMaxSize() - .themedBackground(theme.value.base) - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.your_settings)) SectionView(stringResource(MR.strings.settings_section_title_settings)) { @@ -142,7 +134,7 @@ fun SettingsLayout( } SectionDividerSpaced() - SettingsSectionApp(showSettingsModal, showCustomModal, showVersion, withAuth) + SettingsSectionApp(showSettingsModal, showVersion, withAuth) SectionBottomSpacer() } } @@ -150,7 +142,6 @@ fun SettingsLayout( @Composable expect fun SettingsSectionApp( showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), - showCustomModal: (@Composable ModalData.(ChatModel, () -> Unit) -> Unit) -> (() -> Unit), showVersion: () -> Unit, withAuth: (title: String, desc: String, block: () -> Unit) -> Unit ) @@ -488,7 +479,6 @@ private fun runAuth(title: String, desc: String, onFinish: (success: Boolean) -> fun PreviewSettingsLayout() { SimpleXTheme { SettingsLayout( - profile = LocalProfile.sampleData, stopped = false, encrypted = false, passphraseSaved = false, 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 1ac0cd7ecd..6d6b72d2d1 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,7 @@ import chat.simplex.res.MR @Composable fun UserAddressLearnMore() { - ColumnWithScrollBar( - Modifier - .fillMaxHeight() - .padding(horizontal = DEFAULT_PADDING) - ) { + ColumnWithScrollBar(Modifier .padding(horizontal = DEFAULT_PADDING)) { 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) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfileView.kt index 10acaffe1a..90122bd29d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfileView.kt @@ -71,10 +71,8 @@ fun UserProfileLayout( val keyboardState by getKeyboardState() var savedKeyboardState by remember { mutableStateOf(keyboardState) } val focusRequester = remember { FocusRequester() } - ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { ModalBottomSheetLayout( scrimColor = Color.Black.copy(alpha = 0.12F), - modifier = Modifier.navigationBarsWithImePadding(), sheetContent = { GetImageBottomSheet( chosenImage, @@ -90,7 +88,6 @@ fun UserProfileLayout( displayName.value == profile.displayName && fullName.value == profile.fullName && profile.image == profileImage.value - val closeWithAlert = { if (dataUnchanged || !canSaveProfile(displayName.value, profile)) { close() @@ -103,7 +100,7 @@ fun UserProfileLayout( Modifier .padding(horizontal = DEFAULT_PADDING), ) { - AppBarTitle(stringResource(MR.strings.your_current_profile)) + AppBarTitle(stringResource(MR.strings.your_current_profile), withPadding = false) ReadableText(generalGetString(MR.strings.your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it), TextAlign.Center) Column( Modifier @@ -170,7 +167,6 @@ fun UserProfileLayout( } } } - } } @Composable diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt index dcf8351166..fa9e709d4b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt @@ -151,10 +151,7 @@ private fun UserProfilesLayout( unmuteUser: (User) -> Unit, showHiddenProfile: (User) -> Unit, ) { - ColumnWithScrollBar( - Modifier - .fillMaxWidth() - ) { + ColumnWithScrollBar { if (profileHidden.value) { SectionView { SettingsActionItem(painterResource(MR.images.ic_lock_open_right), stringResource(MR.strings.enter_password_to_show), click = { @@ -252,10 +249,7 @@ enum class UserProfileAction { @Composable private fun ProfileActionView(action: UserProfileAction, user: User, doAction: (String) -> Unit) { - ColumnWithScrollBar( - Modifier - .fillMaxWidth() - ) { + ColumnWithScrollBar { val actionPassword = rememberSaveable { mutableStateOf("") } val passwordValid by remember { derivedStateOf { actionPassword.value == actionPassword.value.trim() } } val actionEnabled by remember { derivedStateOf { actionPassword.value != "" && passwordValid && correctPassword(user, actionPassword.value) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/VersionInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/VersionInfoView.kt index 06a4762210..52addd146b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/VersionInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/VersionInfoView.kt @@ -1,6 +1,5 @@ package chat.simplex.common.views.usersettings -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material.Text import androidx.compose.runtime.Composable @@ -8,6 +7,7 @@ import androidx.compose.ui.Modifier import dev.icerock.moko.resources.compose.stringResource import chat.simplex.common.BuildConfigCommon import chat.simplex.common.model.CoreVersionInfo +import chat.simplex.common.platform.ColumnWithScrollBar import chat.simplex.common.platform.appPlatform import chat.simplex.common.ui.theme.DEFAULT_PADDING import chat.simplex.common.views.helpers.AppBarTitle @@ -15,7 +15,7 @@ import chat.simplex.res.MR @Composable fun VersionInfoView(info: CoreVersionInfo) { - Column( + ColumnWithScrollBar( Modifier.padding(horizontal = DEFAULT_PADDING), ) { AppBarTitle(stringResource(MR.strings.app_version_title), withPadding = false) diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index fd6988d5e8..1ab7e3aed2 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -1755,6 +1755,9 @@ Remove image Font size Zoom + App toolbars + Transparency + Blur System mode 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 1fb739946c..25d85a6b7d 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,12 +198,17 @@ 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 { - TerminalView(true) { hiddenUntilRestart = true } - ModalManager.floatingTerminal.showInView() - DisposableEffect(Unit) { - onDispose { - ModalManager.floatingTerminal.closeModals() + CompositionLocalProvider(LocalAppBarHandler provides data.appBarHandler) { + ModalView({ hiddenUntilRestart = true }) { + TerminalView(true) + } + ModalManager.floatingTerminal.showInView() + DisposableEffect(Unit) { + onDispose { + ModalManager.floatingTerminal.closeModals() + } } } } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Modifier.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Modifier.desktop.kt index 150885cbc8..b090e301d5 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Modifier.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Modifier.desktop.kt @@ -15,17 +15,6 @@ import java.awt.image.BufferedImage import java.io.File import java.net.URI -actual fun Modifier.navigationBarsWithImePadding(): Modifier = this - -@Composable -actual fun ProvideWindowInsets( - consumeWindowInsets: Boolean, - windowInsetsAnimationsEnabled: Boolean, - content: @Composable () -> Unit -) { - content() -} - @Composable actual fun Modifier.desktopOnExternalDrag( enabled: Boolean, diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt index e37e99f3e9..e7bcf4802a 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt @@ -1,10 +1,12 @@ package chat.simplex.common.platform +import androidx.compose.foundation.background import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.* +import androidx.compose.material.TextFieldDefaults.textFieldWithLabelPadding import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -113,7 +115,9 @@ actual fun PlatformTextField( autoCorrectEnabled = true ), modifier = Modifier - .padding(vertical = 4.dp) + .padding(start = startPadding, end = endPadding) + .offset(y = (-5).dp) + .fillMaxWidth() .focusRequester(focusRequester) .onPreviewKeyEvent { if ((it.key == Key.Enter || it.key == Key.NumPadEnter) && it.type == KeyEventType.KeyDown) { @@ -177,30 +181,24 @@ actual fun PlatformTextField( }, cursorBrush = SolidColor(MaterialTheme.colors.secondary), decorationBox = { innerTextField -> - Row(verticalAlignment = Alignment.Bottom) { CompositionLocalProvider( LocalLayoutDirection provides if (isRtlByCharacters) LayoutDirection.Rtl else LocalLayoutDirection.current ) { - Column(Modifier.weight(1f).padding(start = startPadding, end = endPadding)) { - Spacer(Modifier.height(8.dp)) - TextFieldDefaults.TextFieldDecorationBox( - value = textFieldValue.text, - innerTextField = innerTextField, - placeholder = { Text(placeholder, style = textStyle.value.copy(color = MaterialTheme.colors.secondary)) }, - singleLine = false, - enabled = true, - isError = false, - trailingIcon = null, - interactionSource = remember { MutableInteractionSource() }, - contentPadding = PaddingValues(), - visualTransformation = VisualTransformation.None, - colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Unspecified) - ) - Spacer(Modifier.height(10.dp)) - } + TextFieldDefaults.TextFieldDecorationBox( + value = textFieldValue.text, + innerTextField = innerTextField, + placeholder = { Text(placeholder, style = textStyle.value.copy(color = MaterialTheme.colors.secondary)) }, + singleLine = false, + enabled = true, + isError = false, + trailingIcon = null, + interactionSource = remember { MutableInteractionSource() }, + contentPadding = textFieldWithLabelPadding(start = 0.dp, end = 0.dp), + visualTransformation = VisualTransformation.None, + colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Unspecified) + ) } - } - }, + } ) showDeleteTextButton.value = cs.message.split("\n").size >= 4 && !cs.inProgress if (composeState.value.preview is ComposePreview.VoicePreview) { 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 e6b26f9290..a294f1cc60 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,11 +15,14 @@ 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.model.ChatController.appPrefs +import chat.simplex.common.ui.theme.DEFAULT_PADDING import chat.simplex.common.views.helpers.* import kotlinx.coroutines.* import kotlinx.coroutines.flow.filter -import kotlin.math.absoluteValue +import kotlin.math.* @Composable actual fun LazyColumnWithScrollBar( @@ -31,6 +34,78 @@ actual fun LazyColumnWithScrollBar( horizontalAlignment: Alignment.Horizontal, flingBehavior: FlingBehavior, userScrollEnabled: Boolean, + additionalBarOffset: State?, + fillMaxSize: Boolean, + content: LazyListScope.() -> Unit +) { + val handler = LocalAppBarHandler.current + require(handler != null) { "Using LazyColumnWithScrollBar and without AppBarHandler is an error. Use LazyColumnWithScrollBarNoAppBar instead" } + + 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 ?: handler.listState + val connection = handler.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) { + if (reverseLayout) { + snapshotFlow { state.layoutInfo.visibleItemsInfo.lastOrNull()?.offset ?: 0 } + .collect { scrollPosition -> + 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 + scrollPosition + state.layoutInfo.afterContentPadding).absoluteValue > 1 || scrollBarDraggingState.value) { + connection.appBarOffset = -scrollPosition.toFloat() + //Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}") + } + } + } + } + val modifier = if (fillMaxSize) Modifier.fillMaxSize().then(modifier) else modifier + Box(Modifier.copyViewToAppBar(remember { appPrefs.appearanceBarsBlurRadius.state }.value, LocalAppBarHandler.current?.graphicsLayer).nestedScroll(connection)) { + LazyColumn(modifier.then(scrollModifier), state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content) + ScrollBar(reverseLayout, state, scrollBarAlpha, scrollJob, scrollBarDraggingState, additionalBarOffset) + } +} + +@Composable +actual fun LazyColumnWithScrollBarNoAppBar( + modifier: Modifier, + state: LazyListState?, + contentPadding: PaddingValues, + reverseLayout: Boolean, + verticalArrangement: Arrangement.Vertical, + horizontalAlignment: Alignment.Horizontal, + flingBehavior: FlingBehavior, + userScrollEnabled: Boolean, + additionalBarOffset: State?, content: LazyListScope.() -> Unit ) { val scope = rememberCoroutineScope() @@ -51,32 +126,110 @@ actual fun LazyColumnWithScrollBar( } } } - val state = state ?: LocalAppBarHandler.current?.listState ?: rememberLazyListState() - val connection = LocalAppBarHandler.current?.connection + val state = state ?: rememberLazyListState() // 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) { + Box { 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, additionalBarOffset) + } +} + +@Composable +private fun ScrollBar( + reverseLayout: Boolean, + state: LazyListState, + scrollBarAlpha: Animatable, + scrollJob: MutableState, + scrollBarDraggingState: MutableState, + additionalBarHeight: State? +) { + val oneHandUI = remember { appPrefs.oneHandUI.state } + val padding = if (additionalBarHeight != null) { + PaddingValues(top = if (oneHandUI.value) 0.dp else AppBarHeight * fontSizeSqrtMultiplier, bottom = additionalBarHeight.value) + } else if (reverseLayout) { + PaddingValues(bottom = AppBarHeight * fontSizeSqrtMultiplier) + } else { + PaddingValues(top = if (oneHandUI.value) 0.dp else AppBarHeight * fontSizeSqrtMultiplier) + } + Box(Modifier.fillMaxSize().padding(padding), contentAlignment = Alignment.CenterEnd) { + DesktopScrollBar(rememberScrollbarAdapter(state), Modifier.fillMaxHeight(), scrollBarAlpha, scrollJob, reverseLayout, scrollBarDraggingState) } } @Composable actual fun ColumnWithScrollBar( + modifier: Modifier, + verticalArrangement: Arrangement.Vertical, + horizontalAlignment: Alignment.Horizontal, + state: ScrollState?, + maxIntrinsicSize: Boolean, + fillMaxSize: Boolean, + content: @Composable() (ColumnScope.() -> Unit) +) { + val handler = LocalAppBarHandler.current + require(handler != null) { "Using ColumnWithScrollBar and without AppBarHandler is an error. Use ColumnWithScrollBarNoAppBar instead" } + + 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 ?: handler.scrollState + val connection = handler.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 + scrollPosition).absoluteValue > 1 || scrollBarDraggingState.value) { + connection.appBarOffset = -scrollPosition.toFloat() +// Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}") + } + } + } + val modifier = if (fillMaxSize) Modifier.fillMaxSize().then(modifier) else modifier + Box(Modifier.nestedScroll(connection)) { + val oneHandUI = remember { appPrefs.oneHandUI.state } + val padding = if (oneHandUI.value) PaddingValues(bottom = AppBarHeight * fontSizeSqrtMultiplier) else PaddingValues(top = AppBarHeight * fontSizeSqrtMultiplier) + Column( + if (maxIntrinsicSize) { + modifier.copyViewToAppBar(remember { appPrefs.appearanceBarsBlurRadius.state }.value, LocalAppBarHandler.current?.graphicsLayer).verticalScroll(state).height(IntrinsicSize.Max).then(scrollModifier) + } else { + modifier.then(scrollModifier).copyViewToAppBar(remember { appPrefs.appearanceBarsBlurRadius.state }.value, LocalAppBarHandler.current?.graphicsLayer).verticalScroll(state) + }, + verticalArrangement, horizontalAlignment + ) { + Spacer(if (oneHandUI.value) Modifier.padding(top = DEFAULT_PADDING + 5.dp) else Modifier.padding(padding)) + content() + if (oneHandUI.value) { + Spacer(Modifier.padding(padding)) + } + } + Box(Modifier.fillMaxSize().padding(padding), contentAlignment = Alignment.CenterEnd) { + DesktopScrollBar(rememberScrollbarAdapter(state), Modifier.fillMaxHeight(), scrollBarAlpha, scrollJob, false, scrollBarDraggingState) + } + } +} + +@Composable +actual fun ColumnWithScrollBarNoAppBar( modifier: Modifier, verticalArrangement: Arrangement.Vertical, horizontalAlignment: Alignment.Horizontal, @@ -102,29 +255,20 @@ actual fun ColumnWithScrollBar( } } } - val state = state ?: LocalAppBarHandler.current?.scrollState ?: rememberScrollState() - val connection = LocalAppBarHandler.current?.connection + val state = state ?: rememberScrollState() // 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) { + Box { Column( if (maxIntrinsicSize) { modifier.verticalScroll(state).height(IntrinsicSize.Max).then(scrollModifier) } else { - modifier.verticalScroll(state).then(scrollModifier) + modifier.then(scrollModifier).verticalScroll(state) }, - verticalArrangement, horizontalAlignment, content) + verticalArrangement, horizontalAlignment) { + content() + } Box(Modifier.fillMaxSize(), 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/ChatListView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.desktop.kt index bf2e118cd1..a1df7091d6 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.desktop.kt @@ -1,86 +1,155 @@ package chat.simplex.common.views.chatlist import androidx.compose.foundation.* +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Icon -import androidx.compose.material.MaterialTheme +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 androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.call.Call -import chat.simplex.common.views.call.CallMediaType import chat.simplex.common.views.chat.item.ItemAction import chat.simplex.common.views.helpers.* import chat.simplex.res.MR import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource -import kotlinx.coroutines.flow.MutableStateFlow @Composable actual fun ActiveCallInteractiveArea(call: Call) { val showMenu = remember { mutableStateOf(false) } - CompositionLocalProvider( - LocalIndication provides NoIndication + val oneHandUI = remember { appPrefs.oneHandUI.state } + if (oneHandUI.value) { + ActiveCallInteractiveAreaOneHand(call, showMenu) + } else { + CompositionLocalProvider( + LocalIndication provides NoIndication + ) { + ActiveCallInteractiveAreaNonOneHand(call, showMenu) + } + } +} + +@Composable +private fun ActiveCallInteractiveAreaOneHand(call: Call, showMenu: MutableState) { + Box( + Modifier + .minimumInteractiveComponentSize() + .combinedClickable(onClick = { + val chat = chatModel.getChat(call.contact.id) + if (chat != null) { + withBGApi { + openChat(chat.remoteHostId, chat.chatInfo, chatModel) + } + } + }, + onLongClick = { showMenu.value = true }, + role = Role.Button, + interactionSource = remember { MutableInteractionSource() }, + indication = remember { ripple(bounded = false, radius = 24.dp) } + ) + .onRightClick { showMenu.value = true }, + contentAlignment = Alignment.Center + ) { + ProfileImage( + image = call.contact.profile.image, + size = 37.dp * fontSizeSqrtMultiplier, + color = MaterialTheme.colors.secondaryVariant.mixWith(MaterialTheme.colors.onBackground, 0.97f) + ) + Box( + Modifier.offset(x = 1.dp, y = (-1).dp).background(SimplexGreen, CircleShape).padding(3.dp) + .align(Alignment.TopEnd) + ) { + if (call.hasVideo) { + Icon( + painterResource(MR.images.ic_videocam_filled), + stringResource(MR.strings.icon_descr_video_call), + Modifier.size(12.dp), + tint = Color.White + ) + } else { + Icon( + painterResource(MR.images.ic_call_filled), + stringResource(MR.strings.icon_descr_audio_call), + Modifier.size(12.dp), + tint = Color.White + ) + } + } + DefaultDropdownMenu(showMenu) { + ItemAction( + stringResource(MR.strings.icon_descr_hang_up), + painterResource(MR.images.ic_call_end_filled), + color = MaterialTheme.colors.error, + onClick = { + withBGApi { chatModel.callManager.endCall(call) } + showMenu.value = false + }) + } + } +} + +@Composable +private fun ActiveCallInteractiveAreaNonOneHand(call: Call, showMenu: MutableState) { + Box( + Modifier + .fillMaxSize(), + contentAlignment = Alignment.BottomEnd ) { Box( Modifier - .fillMaxSize(), - contentAlignment = Alignment.BottomEnd - ) { - Box( - Modifier - .padding(end = 15.dp, bottom = 92.dp) - .size(67.dp) - .combinedClickable(onClick = { - val chat = chatModel.getChat(call.contact.id) - if (chat != null) { - withBGApi { - openChat(chat.remoteHostId, chat.chatInfo, chatModel) - } + .padding(end = 15.dp, bottom = 92.dp) + .size(67.dp) + .combinedClickable(onClick = { + val chat = chatModel.getChat(call.contact.id) + if (chat != null) { + withBGApi { + openChat(chat.remoteHostId, chat.chatInfo, chatModel) } - }, - onLongClick = { showMenu.value = true }) - .onRightClick { showMenu.value = true }, - contentAlignment = Alignment.Center - ) { - Box(Modifier.background(MaterialTheme.colors.background, CircleShape)) { - ProfileImageForActiveCall(size = 56.dp, image = call.contact.profile.image) - } - Box( - Modifier.padding().background(SimplexGreen, CircleShape).padding(4.dp) - .align(Alignment.TopEnd) - ) { - if (call.hasVideo) { - Icon( - painterResource(MR.images.ic_videocam_filled), - stringResource(MR.strings.icon_descr_video_call), - Modifier.size(18.dp), - tint = Color.White - ) - } else { - Icon( - painterResource(MR.images.ic_call_filled), - stringResource(MR.strings.icon_descr_audio_call), - Modifier.size(18.dp), - tint = Color.White - ) } + }, + onLongClick = { showMenu.value = true }) + .onRightClick { showMenu.value = true }, + contentAlignment = Alignment.Center + ) { + Box(Modifier.background(MaterialTheme.colors.background, CircleShape)) { + ProfileImageForActiveCall(size = 56.dp, image = call.contact.profile.image) + } + Box( + Modifier.padding().background(SimplexGreen, CircleShape).padding(4.dp) + .align(Alignment.TopEnd) + ) { + if (call.hasVideo) { + Icon( + painterResource(MR.images.ic_videocam_filled), + stringResource(MR.strings.icon_descr_video_call), + Modifier.size(18.dp), + tint = Color.White + ) + } else { + Icon( + painterResource(MR.images.ic_call_filled), + stringResource(MR.strings.icon_descr_audio_call), + Modifier.size(18.dp), + tint = Color.White + ) } - DefaultDropdownMenu(showMenu) { - ItemAction( - stringResource(MR.strings.icon_descr_hang_up), - painterResource(MR.images.ic_call_end_filled), - color = MaterialTheme.colors.error, - onClick = { - withBGApi { chatModel.callManager.endCall(call) } - showMenu.value = false - }) - } + } + DefaultDropdownMenu(showMenu) { + ItemAction( + stringResource(MR.strings.icon_descr_hang_up), + painterResource(MR.images.ic_call_end_filled), + color = MaterialTheme.colors.error, + onClick = { + withBGApi { chatModel.callManager.endCall(call) } + showMenu.value = false + }) } } } 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..3855835ab6 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 @@ -10,11 +10,13 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight 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.* @@ -25,6 +27,7 @@ import kotlinx.coroutines.flow.MutableStateFlow @Composable actual fun UserPickerUsersSection( users: List, + iconColor: Color, stopped: Boolean, onUserClicked: (user: User) -> Unit, ) { @@ -37,7 +40,7 @@ actual fun UserPickerUsersSection( .padding(horizontal = horizontalPadding) .height((55.dp + 16.sp.toDp()) * rowsToDisplay + (if (rowsToDisplay > 1) DEFAULT_PADDING else 0.dp)) ) { - ColumnWithScrollBar( + ColumnWithScrollBarNoAppBar( verticalArrangement = Arrangement.spacedBy(DEFAULT_PADDING) ) { val spaceBetween = (((DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier) - (horizontalPadding)) - (65.dp * 5)) / 5 @@ -57,7 +60,7 @@ actual fun UserPickerUsersSection( ) { val user = u.user Box { - ProfileImage(size = 55.dp, image = user.profile.image, color = MaterialTheme.colors.secondaryVariant) + ProfileImage(size = 55.dp, image = user.profile.image, color = iconColor) if (u.unreadCount > 0 && !user.activeUser) { unreadBadge(u.unreadCount, user.showNtfs, true) @@ -95,7 +98,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 } + ColumnWithScrollBarNoAppBar(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 244504f4c7..91ff8831ce 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 @@ -2,6 +2,7 @@ package chat.simplex.common.views.usersettings import SectionBottomSpacer import SectionDividerSpaced +import SectionSpacer import SectionView import androidx.compose.foundation.* import androidx.compose.foundation.layout.* @@ -39,9 +40,7 @@ fun AppearanceScope.AppearanceLayout( languagePref: SharedPreference, systemDarkTheme: SharedPreference, ) { - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.appearance_settings)) SectionView(stringResource(MR.strings.settings_section_title_language), contentPadding = PaddingValues()) { val state = rememberSaveable { mutableStateOf(languagePref.get() ?: "system") } @@ -58,10 +57,14 @@ fun AppearanceScope.AppearanceLayout( } } } + SettingsPreferenceItem(icon = null, stringResource(MR.strings.one_hand_ui), ChatModel.controller.appPrefs.oneHandUI) } SectionDividerSpaced() ThemesSection(systemDarkTheme) + SectionDividerSpaced() + AppToolbarsSection() + SectionDividerSpaced() MessageShapeSection() @@ -83,7 +86,7 @@ fun DensityScaleSection() { val localDensityScale = remember { mutableStateOf(appPrefs.densityScale.get()) } SectionView(stringResource(MR.strings.appearance_zoom).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) { Row(Modifier.padding(top = 10.dp), verticalAlignment = Alignment.CenterVertically) { - Box(Modifier.size(60.dp) + Box(Modifier.size(50.dp) .background(MaterialTheme.colors.surface, RoundedCornerShape(percent = 22)) .clip(RoundedCornerShape(percent = 22)) .clickable { @@ -101,7 +104,7 @@ fun DensityScaleSection() { ) } } - Spacer(Modifier.width(10.dp)) + Spacer(Modifier.width(15.dp)) Slider( localDensityScale.value, valueRange = 1f..2f, diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.desktop.kt index ee8ae93de5..5b4a044df3 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.desktop.kt @@ -18,12 +18,11 @@ import dev.icerock.moko.resources.compose.stringResource @Composable actual fun SettingsSectionApp( showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), - showCustomModal: (@Composable ModalData.(ChatModel, () -> Unit) -> Unit) -> (() -> Unit), showVersion: () -> Unit, withAuth: (title: String, desc: String, block: () -> Unit) -> Unit ) { SectionView(stringResource(MR.strings.settings_section_title_app)) { - SettingsActionItem(painterResource(MR.images.ic_code), stringResource(MR.strings.settings_developer_tools), showSettingsModal { DeveloperView(it, showCustomModal, withAuth) }) + SettingsActionItem(painterResource(MR.images.ic_code), stringResource(MR.strings.settings_developer_tools), showSettingsModal { DeveloperView(withAuth) }) val selectedChannel = remember { appPrefs.appUpdateChannel.state } val values = AppUpdatesChannel.entries.map { it to it.text } ExposedDropDownSettingRow(stringResource(MR.strings.app_check_for_updates), values, selectedChannel) {