From eb1158f0434cbd4b0b0d38986b7fc32ee1ba5b52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= Date: Wed, 8 May 2024 17:27:43 +0200 Subject: [PATCH 1/8] fix: crashing message composer input [WPB-8727] --- .../ui/calling/controlbuttons/CameraButton.kt | 4 +- .../controlbuttons/CameraFlipButton.kt | 4 +- .../calling/controlbuttons/DeclineButton.kt | 4 +- .../controlbuttons/MicrophoneButton.kt | 4 +- .../calling/controlbuttons/SpeakerButton.kt | 5 +- .../wire/android/ui/common/AppExtensions.kt | 4 +- .../common/textfield/StateSyncingModifier.kt | 94 +++++++++ .../ui/common/textfield/WireTextField.kt | 14 +- .../ui/common/textfield/WireTextField2.kt | 191 ++++++++++++++++++ .../messagecomposer/MessageComposerInput.kt | 4 +- .../ui/common/snackbar/SwipeableSnackbar.kt | 4 +- gradle/libs.versions.toml | 3 +- 12 files changed, 312 insertions(+), 23 deletions(-) create mode 100644 app/src/main/kotlin/com/wire/android/ui/common/textfield/StateSyncingModifier.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextField2.kt diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/CameraButton.kt b/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/CameraButton.kt index aec1317a0fa..472756b7e71 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/CameraButton.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/CameraButton.kt @@ -22,7 +22,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentSize -import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material.ripple import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.runtime.remember @@ -62,7 +62,7 @@ fun CameraButton( .wrapContentSize() .clickable( interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple( + indication = ripple( bounded = false, radius = dimensions().defaultCallingControlsSize / 2 ), diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/CameraFlipButton.kt b/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/CameraFlipButton.kt index 29c06df9314..5b8920776e1 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/CameraFlipButton.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/CameraFlipButton.kt @@ -21,7 +21,7 @@ package com.wire.android.ui.calling.controlbuttons import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.wrapContentSize -import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material.ripple import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.runtime.remember @@ -46,7 +46,7 @@ fun CameraFlipButton( .wrapContentSize() .clickable( interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(bounded = false, radius = dimensions().defaultCallingControlsSize / 2), + indication = ripple(bounded = false, radius = dimensions().defaultCallingControlsSize / 2), role = Role.Button, onClick = onCameraFlipButtonClicked diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/DeclineButton.kt b/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/DeclineButton.kt index d3a34db51bd..696d3602633 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/DeclineButton.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/DeclineButton.kt @@ -21,7 +21,7 @@ package com.wire.android.ui.calling.controlbuttons import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.size -import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material.ripple import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.runtime.Composable @@ -45,7 +45,7 @@ fun DeclineButton(buttonClicked: () -> Unit) { modifier = Modifier .clickable( interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple( + indication = ripple( bounded = false, radius = dimensions().outgoingCallHangUpButtonSize / 2 ), diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/MicrophoneButton.kt b/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/MicrophoneButton.kt index 5eac7fbd901..a19c42d80b1 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/MicrophoneButton.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/MicrophoneButton.kt @@ -22,7 +22,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentSize -import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material.ripple import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.runtime.remember @@ -49,7 +49,7 @@ fun MicrophoneButton( .wrapContentSize() .clickable( interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(bounded = false, radius = dimensions().defaultCallingControlsSize / 2), + indication = ripple(bounded = false, radius = dimensions().defaultCallingControlsSize / 2), role = Role.Button, onClick = { onMicrophoneButtonClicked() } ), diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/SpeakerButton.kt b/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/SpeakerButton.kt index 91a0949c9ce..0771baf44cc 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/SpeakerButton.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/SpeakerButton.kt @@ -22,12 +22,11 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentSize -import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material.ripple import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role @@ -50,7 +49,7 @@ fun SpeakerButton( .wrapContentSize() .clickable( interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(bounded = false, radius = dimensions().defaultCallingControlsSize / 2), + indication = ripple(bounded = false, radius = dimensions().defaultCallingControlsSize / 2), role = Role.Button, onClick = { onSpeakerButtonClicked() } ), diff --git a/app/src/main/kotlin/com/wire/android/ui/common/AppExtensions.kt b/app/src/main/kotlin/com/wire/android/ui/common/AppExtensions.kt index dab64da1e8e..6272bc25548 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/AppExtensions.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/AppExtensions.kt @@ -21,7 +21,7 @@ package com.wire.android.ui.common import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material.ripple import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.State @@ -62,7 +62,7 @@ fun Modifier.selectableBackground(isSelected: Boolean, onClick: () -> Unit): Mod selected = isSelected, onClick = { onClick() }, interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(bounded = true, color = MaterialTheme.colorScheme.onBackground.copy(0.5f)), + indication = ripple(bounded = true, color = MaterialTheme.colorScheme.onBackground.copy(0.5f)), role = Role.Tab ) diff --git a/app/src/main/kotlin/com/wire/android/ui/common/textfield/StateSyncingModifier.kt b/app/src/main/kotlin/com/wire/android/ui/common/textfield/StateSyncingModifier.kt new file mode 100644 index 00000000000..1421d6cf106 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/common/textfield/StateSyncingModifier.kt @@ -0,0 +1,94 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.ui.common.textfield + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.text.input.TextFieldCharSequence +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.ui.Modifier +import androidx.compose.ui.node.ModifierNodeElement +import androidx.compose.ui.node.ObserverModifierNode +import androidx.compose.ui.node.observeReads +import androidx.compose.ui.platform.InspectorInfo +import androidx.compose.ui.text.input.TextFieldValue +import io.github.esentsov.PackagePrivate + +/** + * Enables us to temporarily still use TextFieldValue and onValueChanged callback instead of TextFieldState directly, + * also allows us to get selection updates as by default BasicTextField2 callback only gives a String without selection. + * @sample androidx.compose.foundation.samples.BasicTextFieldWithValueOnValueChangeSample + * TODO: Remove this class once all WireTextField usages are migrated to use TextFieldState. + */ +@PackagePrivate +internal class StateSyncingModifier( + private val state: TextFieldState, + private val value: TextFieldValue, + private val onValueChanged: (TextFieldValue) -> Unit, +) : ModifierNodeElement() { + override fun create(): StateSyncingModifierNode = StateSyncingModifierNode(state, onValueChanged) + override fun update(node: StateSyncingModifierNode) { + node.update(value, onValueChanged) + } + override fun equals(other: Any?): Boolean = false + override fun hashCode(): Int = state.hashCode() + override fun InspectorInfo.inspectableProperties() {} +} + +@OptIn(ExperimentalFoundationApi::class) +@PackagePrivate +internal class StateSyncingModifierNode( + private val state: TextFieldState, + private var onValueChanged: (TextFieldValue) -> Unit, +) : Modifier.Node(), ObserverModifierNode { + override val shouldAutoInvalidate: Boolean + get() = false + fun update(value: TextFieldValue, onValueChanged: (TextFieldValue) -> Unit) { + this.onValueChanged = onValueChanged + if (value.text != state.text.toString() || value.selection != state.text.selection) { + state.edit { + if (value.text != state.text.toString()) { + replace(0, length, value.text) + } + if (value.selection != state.text.selection) { + selection = value.selection + } + } + onValueChanged(value) + } + } + override fun onAttach() { + observeTextState(fireOnValueChanged = false) + } + override fun onObservedReadsChanged() { + observeTextState() + } + private fun observeTextState(fireOnValueChanged: Boolean = true) { + lateinit var text: TextFieldCharSequence + observeReads { + text = state.text + } + if (fireOnValueChanged) { + val newValue = TextFieldValue( + text = text.toString(), + selection = text.selection, + composition = text.composition + ) + onValueChanged(newValue) + } + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextField.kt b/app/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextField.kt index 270ef97bffd..4a0d1bc28c6 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextField.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextField.kt @@ -77,6 +77,7 @@ import com.wire.android.ui.common.Tint import com.wire.android.ui.theme.wireDimensions import com.wire.android.ui.theme.wireTypography import com.wire.android.util.EMPTY +import io.github.esentsov.PackagePrivate @Composable internal fun WireTextField( @@ -156,7 +157,7 @@ internal fun WireTextField( decorationBox = { innerTextField -> InnerText( innerTextField, - value, + value.text.isEmpty(), leadingIcon, trailingIcon, placeholderText, @@ -219,10 +220,11 @@ fun Label( } } +@PackagePrivate @Composable -private fun InnerText( +internal fun InnerText( innerTextField: @Composable () -> Unit, - value: TextFieldValue, + shouldShowPlaceholder: Boolean, leadingIcon: @Composable (() -> Unit)? = null, trailingIcon: @Composable (() -> Unit)? = null, placeholderText: String? = null, @@ -232,12 +234,12 @@ private fun InnerText( inputMinHeight: Dp = 48.dp, colors: WireTextFieldColors = wireTextFieldColors(), shouldDetectTaps: Boolean = false, - onClick: (Offset) -> Unit = { } + onTap: (Offset) -> Unit = { } ) { var modifier: Modifier = Modifier if (shouldDetectTaps) { modifier = modifier.pointerInput(Unit) { - detectTapGestures(onTap = onClick) + detectTapGestures(onTap = onTap) } } @@ -266,7 +268,7 @@ private fun InnerText( top = 2.dp, bottom = 2.dp ) ) { - if (value.text.isEmpty() && placeholderText != null) { + if (shouldShowPlaceholder && placeholderText != null) { Text( text = placeholderText, style = placeholderTextStyle, diff --git a/app/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextField2.kt b/app/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextField2.kt new file mode 100644 index 00000000000..6ce85dacc12 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextField2.kt @@ -0,0 +1,191 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.android.ui.common.textfield + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.InputTransformation +import androidx.compose.foundation.text.input.TextFieldLineLimits +import androidx.compose.foundation.text.input.maxLength +import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDirection +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.wire.android.ui.theme.WireTheme +import com.wire.android.ui.theme.wireDimensions +import com.wire.android.ui.theme.wireTypography +import com.wire.android.util.EMPTY +import com.wire.android.util.ui.PreviewMultipleThemes + +/** + * Hybrid text field that uses new BasicTextField2 which resolves multiple issues that old ones had. It's been renamed to BasicTextField + * as well in the newest compose version. The difference is that this new text field takes TextFieldState, all other BasicTextFields + * which take TextFieldValue or String with onValueChange callback are the previous generation ones. + * This hybrid is created to allow us to still pass TextFieldValue and onValueChange callback but already use the new text input version. + */ +@OptIn(ExperimentalFoundationApi::class) +@Composable +internal fun WireTextField2( + value: TextFieldValue, + onValueChange: (TextFieldValue) -> Unit, + placeholderText: String? = null, + labelText: String? = null, + labelMandatoryIcon: Boolean = false, + descriptionText: String? = null, + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, + readOnly: Boolean = false, + state: WireTextFieldState = WireTextFieldState.Default, + maxLines: Int = 1, + singleLine: Boolean = true, + maxTextLength: Int = 8000, + keyboardOptions: KeyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences, autoCorrect = true), + keyboardActions: KeyboardActions = KeyboardActions.Default, + scrollState: ScrollState = rememberScrollState(), + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + textStyle: TextStyle = MaterialTheme.wireTypography.body01, + placeholderTextStyle: TextStyle = MaterialTheme.wireTypography.body01, + placeholderAlignment: Alignment.Horizontal = Alignment.Start, + inputMinHeight: Dp = MaterialTheme.wireDimensions.textFieldMinHeight, + shape: Shape = RoundedCornerShape(MaterialTheme.wireDimensions.textFieldCornerSize), + colors: WireTextFieldColors = wireTextFieldColors(), + modifier: Modifier = Modifier, + onSelectedLineIndexChanged: (Int) -> Unit = { }, + onLineBottomYCoordinateChanged: (Float) -> Unit = { }, + shouldDetectTaps: Boolean = false, + testTag: String = String.EMPTY, + onTap: (Offset) -> Unit = { }, +) { + val textState = rememberTextFieldState(value.text, value.selection) + val lineLimits = if (singleLine) TextFieldLineLimits.SingleLine else TextFieldLineLimits.MultiLine(1, maxLines) + + Column(modifier = modifier) { + if (labelText != null) { + Label(labelText, labelMandatoryIcon, state, interactionSource, colors) + } + + BasicTextField( + state = textState, + textStyle = textStyle.copy(color = colors.textColor(state = state).value, textDirection = TextDirection.ContentOrLtr), + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + lineLimits = lineLimits, + inputTransformation = InputTransformation.maxLength(maxTextLength), + scrollState = scrollState, + readOnly = readOnly, + enabled = state !is WireTextFieldState.Disabled, + cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), + interactionSource = interactionSource, + modifier = Modifier + .fillMaxWidth() + .background(color = colors.backgroundColor(state).value, shape = shape) + .border(width = 1.dp, color = colors.borderColor(state, interactionSource).value, shape = shape) + .semantics { + (labelText ?: placeholderText ?: descriptionText)?.let { + contentDescription = it + } + } + .testTag(testTag) + .then( + StateSyncingModifier( + state = textState, + value = value, + onValueChanged = onValueChange + ) + ), + decorator = { innerTextField -> + InnerText( + innerTextField = innerTextField, + shouldShowPlaceholder = textState.text.isEmpty(), + leadingIcon = leadingIcon, + trailingIcon = trailingIcon, + placeholderText = placeholderText, + state = state, + placeholderTextStyle = placeholderTextStyle, + placeholderAlignment = placeholderAlignment, + inputMinHeight = inputMinHeight, + colors = colors, + shouldDetectTaps = shouldDetectTaps, + onTap = onTap, + ) + }, + onTextLayout = { + it()?.let { + val lineOfText = it.getLineForOffset(textState.text.selection.end) + val bottomYCoordinate = it.getLineBottom(lineOfText) + onSelectedLineIndexChanged(lineOfText) + onLineBottomYCoordinateChanged(bottomYCoordinate) + } + } + ) + + val bottomText = when { + state is WireTextFieldState.Error && state.errorText != null -> state.errorText + !descriptionText.isNullOrEmpty() -> descriptionText + else -> String.EMPTY + } + AnimatedVisibility(visible = bottomText.isNotEmpty()) { + Text( + text = bottomText, + style = MaterialTheme.wireTypography.label04, + textAlign = TextAlign.Start, + color = colors.descriptionColor(state).value, + modifier = Modifier.padding(top = 4.dp) + ) + } + } +} + +@PreviewMultipleThemes +@Composable +fun PreviewWireTextField2() = WireTheme { + WireTextField2( + value = TextFieldValue("text"), + onValueChange = {}, + modifier = Modifier.padding(16.dp) + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposerInput.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposerInput.kt index 3a1bafc0baa..3b09f96122b 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposerInput.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposerInput.kt @@ -59,7 +59,7 @@ import com.wire.android.R import com.wire.android.ui.common.colorsScheme import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.spacers.VerticalSpace -import com.wire.android.ui.common.textfield.WireTextField +import com.wire.android.ui.common.textfield.WireTextField2 import com.wire.android.ui.common.textfield.WireTextFieldColors import com.wire.android.ui.home.conversations.UsersTypingIndicatorForConversation import com.wire.android.ui.home.conversations.messages.QuotedMessagePreview @@ -301,7 +301,7 @@ private fun MessageComposerTextInput( } } - WireTextField( + WireTextField2( value = messageText, onValueChange = onMessageTextChanged, colors = colors, diff --git a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/snackbar/SwipeableSnackbar.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/snackbar/SwipeableSnackbar.kt index 91e126d15b2..f9e5bbf8659 100644 --- a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/snackbar/SwipeableSnackbar.kt +++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/snackbar/SwipeableSnackbar.kt @@ -18,6 +18,7 @@ package com.wire.android.ui.common.snackbar import androidx.compose.animation.core.SpringSpec +import androidx.compose.animation.splineBasedDecay import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.gestures.AnchoredDraggableState import androidx.compose.foundation.gestures.DraggableAnchors @@ -89,7 +90,8 @@ fun SwipeableSnackbar( anchors = anchors, positionalThreshold = positionalThreshold, velocityThreshold = velocityThreshold, - animationSpec = SpringSpec(), + snapAnimationSpec = SpringSpec(), + decayAnimationSpec = splineBasedDecay(density), confirmValueChange = { true } ) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7a4eb4a4699..5836616dcab 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -45,6 +45,7 @@ androidx-startup = "1.1.1" # Compose composeBom = "2024.04.01" +compose-foundation = "1.7.0-alpha05" # remove when composeBom contains new stable version of BasicTextField2 compose-activity = "1.8.2" compose-compiler = "1.5.11" compose-constraint = "1.0.1" @@ -181,7 +182,7 @@ hilt-work = { module = "androidx.hilt:hilt-work", version.ref = "hilt-work" } # Compose BOM compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } -compose-foundation = { module = "androidx.compose.foundation:foundation" } +compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose-foundation" } compose-material-core = { module = "androidx.compose.material:material" } compose-material-icons = { module = "androidx.compose.material:material-icons-extended" } compose-material-ripple = { module = "androidx.compose.material:material-ripple" } From 9e57d8fe955aa502eae94421f7a6b8907833c435 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= Date: Wed, 8 May 2024 17:52:04 +0200 Subject: [PATCH 2/8] detekt --- .../wire/android/ui/common/textfield/StateSyncingModifier.kt | 2 ++ .../com/wire/android/ui/common/textfield/WireTextField2.kt | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/common/textfield/StateSyncingModifier.kt b/app/src/main/kotlin/com/wire/android/ui/common/textfield/StateSyncingModifier.kt index 1421d6cf106..9877bef3f56 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/textfield/StateSyncingModifier.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/textfield/StateSyncingModifier.kt @@ -44,8 +44,10 @@ internal class StateSyncingModifier( override fun update(node: StateSyncingModifierNode) { node.update(value, onValueChanged) } + @Suppress("EqualsAlwaysReturnsTrueOrFalse") override fun equals(other: Any?): Boolean = false override fun hashCode(): Int = state.hashCode() + @Suppress("EmptyFunctionBlock") override fun InspectorInfo.inspectableProperties() {} } diff --git a/app/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextField2.kt b/app/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextField2.kt index 6ce85dacc12..42c366630d1 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextField2.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextField2.kt @@ -83,7 +83,10 @@ internal fun WireTextField2( maxLines: Int = 1, singleLine: Boolean = true, maxTextLength: Int = 8000, - keyboardOptions: KeyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences, autoCorrect = true), + keyboardOptions: KeyboardOptions = KeyboardOptions( + capitalization = KeyboardCapitalization.Sentences, + autoCorrect = true + ), keyboardActions: KeyboardActions = KeyboardActions.Default, scrollState: ScrollState = rememberScrollState(), interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, From 36c2335f6839fdb825b808a46359742d2c3462c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= Date: Wed, 8 May 2024 18:04:51 +0200 Subject: [PATCH 3/8] add decayAnimationSpec --- .../ui/home/conversations/messages/item/RegularMessageItem.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItem.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItem.kt index 88050562468..f2c731f0e43 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItem.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItem.kt @@ -20,6 +20,7 @@ package com.wire.android.ui.home.conversations.messages.item import androidx.compose.animation.core.FastOutLinearInEasing import androidx.compose.animation.core.tween +import androidx.compose.animation.splineBasedDecay import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.gestures.AnchoredDraggableState import androidx.compose.foundation.gestures.DraggableAnchors @@ -307,7 +308,8 @@ private fun SwipableToReplyBox( initialValue = SwipeAnchor.CENTERED, positionalThreshold = { dragWidth }, velocityThreshold = { screenWidth }, - animationSpec = tween(), + snapAnimationSpec = tween(), + decayAnimationSpec = splineBasedDecay(density), confirmValueChange = { changedValue -> if (changedValue == SwipeAnchor.START_TO_END) { // Attempt to finish dismiss, notify reply intention From 39fd0250d936a2fc857143eba6ded0090f99506e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= Date: Wed, 8 May 2024 18:12:19 +0200 Subject: [PATCH 4/8] detekt --- .../android/ui/common/textfield/StateSyncingModifier.kt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/src/main/kotlin/com/wire/android/ui/common/textfield/StateSyncingModifier.kt b/app/src/main/kotlin/com/wire/android/ui/common/textfield/StateSyncingModifier.kt index 9877bef3f56..afba166ade1 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/textfield/StateSyncingModifier.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/textfield/StateSyncingModifier.kt @@ -40,13 +40,18 @@ internal class StateSyncingModifier( private val value: TextFieldValue, private val onValueChanged: (TextFieldValue) -> Unit, ) : ModifierNodeElement() { + override fun create(): StateSyncingModifierNode = StateSyncingModifierNode(state, onValueChanged) + override fun update(node: StateSyncingModifierNode) { node.update(value, onValueChanged) } + @Suppress("EqualsAlwaysReturnsTrueOrFalse") override fun equals(other: Any?): Boolean = false + override fun hashCode(): Int = state.hashCode() + @Suppress("EmptyFunctionBlock") override fun InspectorInfo.inspectableProperties() {} } @@ -59,6 +64,7 @@ internal class StateSyncingModifierNode( ) : Modifier.Node(), ObserverModifierNode { override val shouldAutoInvalidate: Boolean get() = false + fun update(value: TextFieldValue, onValueChanged: (TextFieldValue) -> Unit) { this.onValueChanged = onValueChanged if (value.text != state.text.toString() || value.selection != state.text.selection) { @@ -73,12 +79,15 @@ internal class StateSyncingModifierNode( onValueChanged(value) } } + override fun onAttach() { observeTextState(fireOnValueChanged = false) } + override fun onObservedReadsChanged() { observeTextState() } + private fun observeTextState(fireOnValueChanged: Boolean = true) { lateinit var text: TextFieldCharSequence observeReads { From beae5ba2e2043f2051c660c69820f8e64665fac5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= Date: Wed, 8 May 2024 18:58:03 +0200 Subject: [PATCH 5/8] trigger build From 2c3a5689610d77a796dd0c574e5bebb7c76139bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= Date: Thu, 9 May 2024 09:05:35 +0200 Subject: [PATCH 6/8] use dimensions --- .../com/wire/android/ui/common/textfield/WireTextField2.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextField2.kt b/app/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextField2.kt index 42c366630d1..25c770f9ca0 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextField2.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextField2.kt @@ -55,6 +55,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import com.wire.android.ui.common.dimensions import com.wire.android.ui.theme.WireTheme import com.wire.android.ui.theme.wireDimensions import com.wire.android.ui.theme.wireTypography @@ -126,7 +127,7 @@ internal fun WireTextField2( modifier = Modifier .fillMaxWidth() .background(color = colors.backgroundColor(state).value, shape = shape) - .border(width = 1.dp, color = colors.borderColor(state, interactionSource).value, shape = shape) + .border(width = dimensions().spacing1x, color = colors.borderColor(state, interactionSource).value, shape = shape) .semantics { (labelText ?: placeholderText ?: descriptionText)?.let { contentDescription = it @@ -177,7 +178,7 @@ internal fun WireTextField2( style = MaterialTheme.wireTypography.label04, textAlign = TextAlign.Start, color = colors.descriptionColor(state).value, - modifier = Modifier.padding(top = 4.dp) + modifier = Modifier.padding(top = dimensions().spacing4x) ) } } From d5807057fe2fa9485e5eda3e15e0b0743a7ae041 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= Date: Thu, 9 May 2024 09:36:44 +0200 Subject: [PATCH 7/8] trigger build From 1be7f9909f9dbd89c8fa833cd2c9a390eaa5ee1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= Date: Fri, 10 May 2024 14:51:27 +0200 Subject: [PATCH 8/8] set compose-material-android library version explicitly --- app/build.gradle.kts | 1 + core/ui-common/build.gradle.kts | 1 + features/sketch/build.gradle.kts | 1 + features/template/build.gradle.kts | 1 + gradle/libs.versions.toml | 2 ++ 5 files changed, 6 insertions(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ae26201ea83..e3f131194d9 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -135,6 +135,7 @@ dependencies { implementation(libs.compose.ui) implementation(libs.compose.foundation) + implementation(libs.compose.material.android) // we still cannot get rid of material2 because swipeable is still missing - https://issuetracker.google.com/issues/229839039 // https://developer.android.com/jetpack/compose/designsystems/material2-material3#components-and implementation(libs.compose.material.core) diff --git a/core/ui-common/build.gradle.kts b/core/ui-common/build.gradle.kts index 5262f93c7f0..b01cc651ee2 100644 --- a/core/ui-common/build.gradle.kts +++ b/core/ui-common/build.gradle.kts @@ -17,6 +17,7 @@ dependencies { implementation(composeBom) implementation(libs.compose.ui) implementation(libs.compose.foundation) + implementation(libs.compose.material.android) implementation(libs.compose.ui.graphics) implementation(libs.compose.material.core) implementation(libs.compose.material3) diff --git a/features/sketch/build.gradle.kts b/features/sketch/build.gradle.kts index 5f7e54c713a..c06d5df7849 100644 --- a/features/sketch/build.gradle.kts +++ b/features/sketch/build.gradle.kts @@ -14,6 +14,7 @@ dependencies { implementation(composeBom) implementation(libs.compose.ui) implementation(libs.compose.foundation) + implementation(libs.compose.material.android) implementation(libs.compose.ui.graphics) implementation(libs.compose.material.core) implementation(libs.compose.material3) diff --git a/features/template/build.gradle.kts b/features/template/build.gradle.kts index 8f553b3fa3b..89bb9604ba3 100644 --- a/features/template/build.gradle.kts +++ b/features/template/build.gradle.kts @@ -12,6 +12,7 @@ dependencies { implementation(composeBom) implementation(libs.compose.ui) implementation(libs.compose.foundation) + implementation(libs.compose.material.android) testImplementation(libs.junit4) androidTestImplementation(libs.androidx.test.extJunit) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5836616dcab..9f1e52e832c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -46,6 +46,7 @@ androidx-startup = "1.1.1" # Compose composeBom = "2024.04.01" compose-foundation = "1.7.0-alpha05" # remove when composeBom contains new stable version of BasicTextField2 +compose-material-android = "1.7.0-alpha05" # remove when composeBom contains new stable version of BasicTextField2 compose-activity = "1.8.2" compose-compiler = "1.5.11" compose-constraint = "1.0.1" @@ -183,6 +184,7 @@ hilt-work = { module = "androidx.hilt:hilt-work", version.ref = "hilt-work" } # Compose BOM compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose-foundation" } +compose-material-android = { module = "androidx.compose.material:material-android", version.ref = "compose-material-android" } compose-material-core = { module = "androidx.compose.material:material" } compose-material-icons = { module = "androidx.compose.material:material-icons-extended" } compose-material-ripple = { module = "androidx.compose.material:material-ripple" }