diff --git a/app/src/main/kotlin/com/wire/android/ui/common/textfield/CodeTextField.kt b/app/src/main/kotlin/com/wire/android/ui/common/textfield/CodeTextField.kt index 1f19379769b..af7d5553dde 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/textfield/CodeTextField.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/textfield/CodeTextField.kt @@ -69,7 +69,7 @@ fun CodeTextField( maxHorizontalSpacing = maxHorizontalSpacing, horizontalAlignment = horizontalAlignment, modifier = modifier, - innerBasicTextField = { decorator, textFieldModifier, _ -> + innerBasicTextField = { decorator, textFieldModifier -> BasicTextField( state = textState, textStyle = textStyle, diff --git a/app/src/main/kotlin/com/wire/android/ui/common/textfield/CodeTextFieldLayout.kt b/app/src/main/kotlin/com/wire/android/ui/common/textfield/CodeTextFieldLayout.kt index 60bcadf6e54..d84925299ec 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/textfield/CodeTextFieldLayout.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/textfield/CodeTextFieldLayout.kt @@ -104,8 +104,7 @@ internal fun CodeTextFieldLayout( } } }, - textFieldModifier = Modifier, - decorationBox = {} + textFieldModifier = Modifier ) val bottomText = when { state is WireTextFieldState.Error && state.errorText != null -> state.errorText diff --git a/app/src/main/kotlin/com/wire/android/ui/common/textfield/WirePasswordTextField.kt b/app/src/main/kotlin/com/wire/android/ui/common/textfield/WirePasswordTextField.kt index c87cf1fdb2f..87626e8f3aa 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/textfield/WirePasswordTextField.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/textfield/WirePasswordTextField.kt @@ -110,7 +110,7 @@ fun WirePasswordTextField( modifier = modifier.then(autoFillModifier(autoFillType, textState::setTextAndPlaceCursorAtEnd)), testTag = testTag, onTap = onTap, - innerBasicTextField = { decorator, textFieldModifier, _ -> + innerBasicTextField = { decorator, textFieldModifier -> BasicSecureTextField( state = textState, textStyle = textStyle.copy(color = colors.textColor(state = state).value, textDirection = TextDirection.ContentOrLtr), 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 2350ba611a3..0bf843388d9 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 @@ -25,7 +25,6 @@ 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.KeyboardActionHandler @@ -43,8 +42,6 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable -import androidx.compose.runtime.State -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -56,13 +53,10 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import com.wire.android.ui.common.colorsScheme -import com.wire.android.ui.home.conversations.model.UIMention import com.wire.android.ui.theme.WireTheme import com.wire.android.ui.theme.wireDimensions import com.wire.android.ui.theme.wireTypography @@ -140,7 +134,7 @@ internal fun WireTextField( ), onTap = onTap, testTag = testTag, - innerBasicTextField = { decorator, textFieldModifier, _ -> + innerBasicTextField = { decorator, textFieldModifier -> BasicTextField( state = textState, textStyle = textStyle.copy( @@ -169,97 +163,6 @@ internal fun WireTextField( ) } -@Composable -internal fun WireTextField( - textFieldValue: State, - onValueChange: (TextFieldValue) -> Unit, - modifier: Modifier = Modifier, - mentions: List = emptyList(), - placeholderText: String? = null, - labelText: String? = null, - labelMandatoryIcon: Boolean = false, - descriptionText: String? = null, - semanticDescription: String? = null, - leadingIcon: @Composable (() -> Unit)? = null, - trailingIcon: @Composable (() -> Unit)? = null, - state: WireTextFieldState = WireTextFieldState.Default, - keyboardOptions: KeyboardOptions = KeyboardOptions.DefaultText, - keyboardActions: KeyboardActions = KeyboardActions.Default, - singleLine: Boolean = false, - maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, - minLines: Int = 1, - 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(), - onSelectedLineIndexChanged: (Int) -> Unit = { }, - onLineBottomYCoordinateChanged: (Float) -> Unit = { }, - onTap: ((Offset) -> Unit)? = null, - testTag: String = String.EMPTY -) { - WireTextFieldLayout( - modifier = modifier, - shouldShowPlaceholder = textFieldValue.value.text.isEmpty(), - placeholderText = placeholderText, - labelText = labelText, - labelMandatoryIcon = labelMandatoryIcon, - descriptionText = descriptionText, - semanticDescription = semanticDescription, - leadingIcon = leadingIcon, - trailingIcon = trailingIcon, - state = state, - interactionSource = interactionSource, - placeholderTextStyle = placeholderTextStyle, - placeholderAlignment = placeholderAlignment, - inputMinHeight = inputMinHeight, - shape = shape, - colors = colors, - onTap = onTap, - testTag = testTag, - innerBasicTextField = { _, textFieldModifier, decoratorBox -> - BasicTextField( - value = textFieldValue.value, - onValueChange = onValueChange, - textStyle = textStyle.copy( - color = colors.textColor(state = state).value, - textDirection = TextDirection.ContentOrLtr - ), - keyboardOptions = keyboardOptions, - keyboardActions = keyboardActions, - readOnly = state is WireTextFieldState.ReadOnly, - enabled = state !is WireTextFieldState.Disabled, - singleLine = singleLine, - maxLines = maxLines, - minLines = minLines, - cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), - interactionSource = interactionSource, - modifier = textFieldModifier, - decorationBox = decoratorBox, - onTextLayout = onTextLayout( - textFieldValue, - onSelectedLineIndexChanged, - onLineBottomYCoordinateChanged - ), - visualTransformation = MentionVisualTransformation(colorsScheme().primary, mentions), - ) - } - ) -} - -private fun onTextLayout( - textFieldValue: State, - onSelectedLineIndexChanged: (Int) -> Unit = { }, - onLineBottomYCoordinateChanged: (Float) -> Unit = { }, -): (TextLayoutResult) -> Unit = { - val lineOfText = it.getLineForOffset(textFieldValue.value.selection.end) - val bottomYCoordinate = it.getLineBottom(lineOfText) - onSelectedLineIndexChanged(lineOfText) - onLineBottomYCoordinateChanged(bottomYCoordinate) -} - private fun onTextLayout( state: TextFieldState, onSelectedLineIndexChanged: (Int) -> Unit = { }, @@ -300,16 +203,6 @@ private fun KeyboardOptions.Companion.defaultEmail(imeAction: ImeAction): Keyboa ) } -@PreviewMultipleThemes -@Composable -fun PreviewWireTextFieldWithTextFieldValue() = WireTheme { - WireTextField( - modifier = Modifier.padding(16.dp), - textFieldValue = remember { mutableStateOf(TextFieldValue("text")) }, - onValueChange = {} - ) -} - @PreviewMultipleThemes @Composable fun PreviewWireTextField() = WireTheme { diff --git a/app/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextFieldLayout.kt b/app/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextFieldLayout.kt index d93d1e73309..b99c98af954 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextFieldLayout.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextFieldLayout.kt @@ -111,21 +111,6 @@ internal fun WireTextFieldLayout( onTap = onTap, ) }, - decorationBox = { innerTextField -> - InnerTextLayout( - innerTextField = innerTextField, - shouldShowPlaceholder = shouldShowPlaceholder, - leadingIcon = leadingIcon, - trailingIcon = trailingIcon, - placeholderText = placeholderText, - style = state, - placeholderTextStyle = placeholderTextStyle, - placeholderAlignment = placeholderAlignment, - inputMinHeight = inputMinHeight, - colors = colors, - onTap = onTap, - ) - }, textFieldModifier = Modifier .fillMaxWidth() .background(color = colors.backgroundColor(state).value, shape = shape) @@ -233,9 +218,5 @@ private fun Alignment.Horizontal.toAlignment(): Alignment = Alignment { size, sp fun interface InnerBasicTextFieldBuilder { @Composable - fun Build( - decorator: TextFieldDecorator, - textFieldModifier: Modifier, - decorationBox: @Composable (innerTextField: @Composable () -> Unit) -> Unit - ) + fun Build(decorator: TextFieldDecorator, textFieldModifier: Modifier) } diff --git a/app/src/main/kotlin/com/wire/android/ui/common/textfield/mention/MentionAdjuster.kt b/app/src/main/kotlin/com/wire/android/ui/common/textfield/mention/MentionAdjuster.kt deleted file mode 100644 index bf220943e78..00000000000 --- a/app/src/main/kotlin/com/wire/android/ui/common/textfield/mention/MentionAdjuster.kt +++ /dev/null @@ -1,96 +0,0 @@ -/* - * 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.mention - -import androidx.compose.ui.text.TextRange -import com.wire.android.ui.home.conversations.model.UIMention - -/** - * Adjusts mentions based on changes in the text. - */ -class MentionAdjuster { - - /** - * Adjusts mentions based on the deleted text. - * @param mentions The list of mentions in the text. - * @param deletedLength The length of the deleted text. - * @param text The new text after deletion. - * @param selection The current selection. - */ - @Suppress("NestedBlockDepth") - fun adjustMentionsForDeletion( - mentions: List, - deletedLength: Int, - text: String, - selection: TextRange - ): Pair, TextRange> { - val updatedMentions = mutableListOf() - var newSelection = selection - - mentions.forEach { mention -> - if (selection.start >= mention.start + mention.length) { - // No change for mentions that are before the deleted text. - updatedMentions.add(mention) - // if the cursor is at the end of the mention, select te mention - if (mention.start + mention.length == selection.end) { - newSelection = TextRange(mention.start, mention.start + mention.length) - } - } else { - // Handle mentions that were affected by the deletion and adjusting their start position. - val newStart = mention.start - deletedLength - if (newStart >= 0) { - val mentionSubstring = text.substring(newStart, newStart + mention.length) - if (mentionSubstring == mention.handler) { - updatedMentions.add(mention.copy(start = newStart)) - } - } - } - } - - return Pair(updatedMentions, newSelection) - } - - /** - * Adjusts mentions based on the inserted text. - * @param mentions The list of mentions in the text. - * @param text The new text after insertion. - * @param addedLength The length of the inserted text. - * @param selection The current selection. - */ - fun adjustMentionsForInsertion( - mentions: List, - text: String, - selection: TextRange, - addedLength: Int - ): Pair, TextRange> { - val updatedMentions = mutableListOf() - // Adjust mentions based on the inserted text. - mentions.forEach { mention -> - val mentionSubstring = text.substring(mention.start, mention.start + mention.length) - if (mentionSubstring == mention.handler) { - // No change if the mention text remains the same. - updatedMentions.add(mention) - } else { - // Handle mentions that were affected by the insertion and shift their start position. - updatedMentions.add(mention.copy(start = mention.start + addedLength)) - } - } - - return Pair(updatedMentions, selection) - } -} diff --git a/app/src/main/kotlin/com/wire/android/ui/common/textfield/mention/MentionSelectionManager.kt b/app/src/main/kotlin/com/wire/android/ui/common/textfield/mention/MentionSelectionManager.kt deleted file mode 100644 index eb03e606906..00000000000 --- a/app/src/main/kotlin/com/wire/android/ui/common/textfield/mention/MentionSelectionManager.kt +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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.mention - -import androidx.compose.ui.text.TextRange -import com.wire.android.ui.home.conversations.model.UIMention - -/** - * Manages the selection for mentions. - */ -class MentionSelectionManager { - - /** - * if the selection is inside the mention's range, it will return the mention's range. - * Otherwise, it will return the new selection. - * @param oldSelection the old selection. - * @param newSelection the new selection. - * @param mentions the list of mentions. - * @return the new selection. - */ - fun updateSelectionForMention( - oldSelection: TextRange, - newSelection: TextRange, - mentions: List - ): TextRange { - if (oldSelection != newSelection) { - mentions.forEach { mention -> - if (newSelection.isInside(mention)) { - return TextRange(mention.start, mention.start + mention.length) - } - } - } - return newSelection - } - - /** - * Extension function to check if the selection is inside the mention's range. - */ - private fun TextRange.isInside(mention: UIMention): Boolean { - return this.start in mention.start until mention.start + mention.length && - this.end in mention.start until mention.start + mention.length - } -} diff --git a/app/src/main/kotlin/com/wire/android/ui/common/textfield/mention/MentionUpdateCoordinator.kt b/app/src/main/kotlin/com/wire/android/ui/common/textfield/mention/MentionUpdateCoordinator.kt deleted file mode 100644 index b4a1c883f46..00000000000 --- a/app/src/main/kotlin/com/wire/android/ui/common/textfield/mention/MentionUpdateCoordinator.kt +++ /dev/null @@ -1,88 +0,0 @@ -/* - * 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.mention - -import androidx.compose.ui.text.input.TextFieldValue -import com.wire.android.ui.home.conversations.model.UIMention - -/** - * Manages the updates to a `TextFieldValue` based on changes in the text and mentions. - */ -class MentionUpdateCoordinator( - private val mentionAdjuster: MentionAdjuster = MentionAdjuster(), - private val selectionManager: MentionSelectionManager = MentionSelectionManager() -) { - @Suppress("ReturnCount") - fun handle( - oldTextFieldValue: TextFieldValue, - newTextFieldValue: TextFieldValue, - mentions: List, - updateMentions: (List) -> Unit - ): TextFieldValue { - if (newTextFieldValue.text.isEmpty()) { - updateMentions(emptyList()) - return newTextFieldValue - } - - // If there are no mentions, simply return the new TextFieldValue. - if (mentions.isEmpty()) { - return newTextFieldValue - } - - val deletedLength = oldTextFieldValue.text.length - newTextFieldValue.text.length - val addedLength = newTextFieldValue.text.length - oldTextFieldValue.text.length - - when { - deletedLength > 0 -> { - val result = mentionAdjuster.adjustMentionsForDeletion( - mentions = mentions, - deletedLength = deletedLength, - text = newTextFieldValue.text, - selection = newTextFieldValue.selection - ) - updateMentions(result.first) - return newTextFieldValue.copy(selection = result.second) - } - - addedLength > 0 -> { - val result = - mentionAdjuster.adjustMentionsForInsertion( - mentions = mentions, - text = newTextFieldValue.text, - selection = newTextFieldValue.selection, - addedLength = addedLength - ) - updateMentions(result.first) - return newTextFieldValue.copy(selection = result.second) - } - } - - // To select the mention if the user clicks on it - val newSelection = if (oldTextFieldValue.text == newTextFieldValue.text) { - selectionManager.updateSelectionForMention( - oldTextFieldValue.selection, - newTextFieldValue.selection, - mentions - ) - } else { - newTextFieldValue.selection - } - - return newTextFieldValue.copy(selection = newSelection) - } -} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/EnabledMessageComposer.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/EnabledMessageComposer.kt index fc14ab01833..cf94cf8bfe1 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/EnabledMessageComposer.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/EnabledMessageComposer.kt @@ -69,11 +69,9 @@ import com.wire.android.ui.common.banner.SecurityClassificationBannerForConversa import com.wire.android.ui.common.bottombar.bottomNavigationBarHeight import com.wire.android.ui.common.colorsScheme import com.wire.android.ui.common.dimensions -import com.wire.android.ui.common.textfield.mention.MentionUpdateCoordinator import com.wire.android.ui.home.conversations.ConversationActionPermissionType import com.wire.android.ui.home.conversations.UsersTypingIndicatorForConversation import com.wire.android.ui.home.conversations.model.UriAsset -import com.wire.android.ui.home.messagecomposer.model.update import com.wire.android.ui.home.messagecomposer.state.AdditionalOptionSubMenuState import com.wire.android.ui.home.messagecomposer.state.InputType import com.wire.android.ui.home.messagecomposer.state.MessageComposerStateHolder @@ -207,19 +205,7 @@ fun EnabledMessageComposer( ActiveMessageComposerInput( conversationId = conversationId, messageComposition = messageComposition.value, - messageTextFieldValue = inputStateHolder.messageTextFieldValue, - onValueChange = { newTextField -> - val updatedTextField = MentionUpdateCoordinator().handle( - inputStateHolder.messageTextFieldValue.value, - newTextField, - messageComposition.value.selectedMentions, - updateMentions = { mentions -> - messageComposition.update { it.copy(selectedMentions = mentions) } - } - ) - inputStateHolder.messageTextFieldValue.value = updatedTextField - }, - mentions = messageComposition.value.selectedMentions, + messageTextState = inputStateHolder.messageTextState, isTextExpanded = inputStateHolder.isTextExpanded, inputType = messageCompositionInputStateHolder.inputType, focusRequester = messageCompositionInputStateHolder.focusRequester, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposer.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposer.kt index 253e8584d35..2d005dddbe3 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposer.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposer.kt @@ -27,6 +27,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -47,7 +48,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.TextLayoutResult -import androidx.compose.ui.text.input.TextFieldValue import com.wire.android.R import com.wire.android.ui.common.TextWithLearnMore import com.wire.android.ui.common.banner.SecurityClassificationBannerForConversation @@ -252,7 +252,7 @@ private fun BaseComposerPreview( ) } - val messageTextFieldValue = remember { mutableStateOf(TextFieldValue()) } + val messageTextState = rememberTextFieldState() val messageComposition = remember { mutableStateOf(MessageComposition(ConversationId("value", "domain"))) } val keyboardController = LocalSoftwareKeyboardController.current @@ -261,7 +261,7 @@ private fun BaseComposerPreview( mutableStateOf( MessageCompositionHolder( messageComposition = messageComposition, - messageTextFieldValue = messageTextFieldValue, + messageTextState = messageTextState, onClearDraft = {}, onSaveDraft = {}, onSearchMentionQueryChanged = {}, @@ -276,7 +276,7 @@ private fun BaseComposerPreview( messageComposerStateHolder = MessageComposerStateHolder( messageComposerViewState = messageComposerViewState, messageCompositionInputStateHolder = MessageCompositionInputStateHolder( - messageTextFieldValue = messageTextFieldValue, + messageTextState = messageTextState, keyboardController = keyboardController, focusRequester = focusRequester ), 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 1a4888f7f8c..42b69b8585a 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 @@ -29,15 +29,14 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.State import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi @@ -52,7 +51,6 @@ import androidx.compose.ui.input.key.onPreInterceptKeyBeforeSoftKeyboard import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.Dimension @@ -68,7 +66,6 @@ import com.wire.android.ui.common.textfield.WireTextFieldColors import com.wire.android.ui.common.textfield.WireTextFieldState import com.wire.android.ui.home.conversations.UsersTypingIndicatorForConversation import com.wire.android.ui.home.conversations.messages.QuotedMessagePreview -import com.wire.android.ui.home.conversations.model.UIMention import com.wire.android.ui.home.messagecomposer.actions.SelfDeletingMessageActionArgs import com.wire.android.ui.home.messagecomposer.actions.SelfDeletingMessageActionViewModel import com.wire.android.ui.home.messagecomposer.actions.SelfDeletingMessageActionViewModelImpl @@ -86,9 +83,7 @@ import com.wire.kalium.logic.data.message.SelfDeletionTimer fun ActiveMessageComposerInput( conversationId: ConversationId, messageComposition: MessageComposition, - messageTextFieldValue: State, - onValueChange: (TextFieldValue) -> Unit, - mentions: List, + messageTextState: TextFieldState, isTextExpanded: Boolean, inputType: InputType, focusRequester: FocusRequester, @@ -131,9 +126,7 @@ fun ActiveMessageComposerInput( InputContent( conversationId = conversationId, - messageTextFieldValue = messageTextFieldValue, - onValueChange = onValueChange, - mentions = mentions, + messageTextState = messageTextState, isTextExpanded = isTextExpanded, inputType = inputType, focusRequester = focusRequester, @@ -170,9 +163,7 @@ fun ActiveMessageComposerInput( @Composable private fun InputContent( conversationId: ConversationId, - messageTextFieldValue: State, - onValueChange: (TextFieldValue) -> Unit, - mentions: List, + messageTextState: TextFieldState, isTextExpanded: Boolean, inputType: InputType, focusRequester: FocusRequester, @@ -215,9 +206,7 @@ private fun InputContent( isTextExpanded = isTextExpanded, focusRequester = focusRequester, colors = inputType.inputTextColor(isSelfDeleting = viewModel.state().duration != null), - messageTextFieldValue = messageTextFieldValue, - onValueChange = onValueChange, - mentions = mentions, + messageTextState = messageTextState, placeHolderText = viewModel.state().duration?.let { stringResource(id = R.string.self_deleting_message_label) } ?: inputType.labelText(), onFocused = onFocused, @@ -274,12 +263,10 @@ private fun InputContent( @OptIn(ExperimentalComposeUiApi::class) @Composable private fun MessageComposerTextInput( - mentions: List, + messageTextState: TextFieldState, isTextExpanded: Boolean, focusRequester: FocusRequester, colors: WireTextFieldColors, - messageTextFieldValue: State, - onValueChange: (TextFieldValue) -> Unit, placeHolderText: String, onTextCollapse: () -> Unit, onFocused: () -> Unit, @@ -297,9 +284,7 @@ private fun MessageComposerTextInput( } WireTextField( - textFieldValue = messageTextFieldValue, - onValueChange = onValueChange, - mentions = mentions, + textState = messageTextState, colors = colors, textStyle = MaterialTheme.wireTypography.body01, // Add an extra space so that the cursor is placed one space before "Type a message" @@ -366,9 +351,7 @@ private fun PreviewActiveMessageComposerInput(inputType: InputType, isTextExpand ActiveMessageComposerInput( conversationId = ConversationId("conversationId", "domain"), messageComposition = MessageComposition(ConversationId("conversationId", "domain")), - mentions = emptyList(), - messageTextFieldValue = remember { mutableStateOf(TextFieldValue()) }, - onValueChange = {}, + messageTextState = TextFieldState(""), isTextExpanded = isTextExpanded, inputType = inputType, focusRequester = FocusRequester(), diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageComposerStateHolder.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageComposerStateHolder.kt index 0ea6534dc7d..a62b7eb83a6 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageComposerStateHolder.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageComposerStateHolder.kt @@ -18,6 +18,8 @@ package com.wire.android.ui.home.messagecomposer.state +import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.State @@ -27,8 +29,6 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.text.TextRange -import androidx.compose.ui.text.input.TextFieldValue import com.wire.android.ui.home.conversations.MessageComposerViewState import com.wire.android.ui.home.conversations.model.UIMessage import com.wire.android.ui.home.messagecomposer.model.MessageComposition @@ -53,14 +53,11 @@ fun rememberMessageComposerStateHolder( mutableStateOf(draftMessageComposition) } - val messageTextFieldValue = remember { mutableStateOf(TextFieldValue()) } + val messageTextState = rememberTextFieldState() LaunchedEffect(draftMessageComposition.draftText) { if (draftMessageComposition.draftText.isNotBlank()) { - messageTextFieldValue.value = messageTextFieldValue.value.copy( - text = draftMessageComposition.draftText, - selection = TextRange(draftMessageComposition.draftText.length) // Place cursor at the end of the new text - ) + messageTextState.setTextAndPlaceCursorAtEnd(draftMessageComposition.draftText) } } @@ -68,7 +65,7 @@ fun rememberMessageComposerStateHolder( mutableStateOf( MessageCompositionHolder( messageComposition = messageComposition, - messageTextFieldValue = messageTextFieldValue, + messageTextState = messageTextState, onClearDraft = onClearDraft, onSaveDraft = onSaveDraft, onSearchMentionQueryChanged = onSearchMentionQueryChanged, @@ -87,14 +84,14 @@ fun rememberMessageComposerStateHolder( val messageCompositionInputStateHolder = rememberSaveable( saver = MessageCompositionInputStateHolder.saver( - messageTextFieldValue = messageTextFieldValue, + messageTextState = messageTextState, keyboardController = keyboardController, focusRequester = focusRequester, density = density ) ) { MessageCompositionInputStateHolder( - messageTextFieldValue = messageTextFieldValue, + messageTextState = messageTextState, keyboardController = keyboardController, focusRequester = focusRequester ) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageCompositionHolder.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageCompositionHolder.kt index 9c0b29b89fb..323eab83c65 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageCompositionHolder.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageCompositionHolder.kt @@ -17,10 +17,12 @@ */ package com.wire.android.ui.home.messagecomposer.state +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.text.input.clearText +import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd import androidx.compose.runtime.MutableState import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.text.TextRange -import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.substring import com.wire.android.ui.home.conversations.model.UIMention import com.wire.android.ui.home.conversations.model.UIMessage @@ -51,7 +53,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged @Suppress("TooManyFunctions") class MessageCompositionHolder( val messageComposition: MutableState, - var messageTextFieldValue: MutableState, + var messageTextState: TextFieldState, val onClearDraft: () -> Unit, private val onSaveDraft: (MessageDraft) -> Unit, private val onSearchMentionQueryChanged: (String) -> Unit, @@ -70,7 +72,7 @@ class MessageCompositionHolder( editMessageId = null ) } - onSaveDraft(messageComposition.value.toDraft(messageTextFieldValue.value.text)) + onSaveDraft(messageComposition.value.toDraft(messageTextState.text.toString())) } fun setReply(message: UIMessage.Regular) { @@ -94,7 +96,7 @@ class MessageCompositionHolder( ) } } - onSaveDraft(messageComposition.value.toDraft(messageTextFieldValue.value.text)) + onSaveDraft(messageComposition.value.toDraft(messageTextState.text.toString())) } fun clearReply() { @@ -108,15 +110,20 @@ class MessageCompositionHolder( } suspend fun handleMessageTextUpdates() { - snapshotFlow { messageTextFieldValue.value.text to messageTextFieldValue.value.selection } + snapshotFlow { messageTextState.text to messageTextState.selection } .distinctUntilChanged() .collectLatest { (messageText, selection) -> - updateTypingEvent(messageText) - requestMentionSuggestionIfNeeded(messageText, selection) - onSaveDraft(messageComposition.value.toDraft(messageText)) + updateTypingEvent(messageText.toString()) + updateMentionsIfNeeded(messageText.toString()) + requestMentionSuggestionIfNeeded(messageText.toString(), selection) + onSaveDraft(messageComposition.value.toDraft(messageText.toString())) } } + private fun updateMentionsIfNeeded(messageText: String) { + messageComposition.update { it.copy(selectedMentions = it.getSelectedMentions(messageText)) } + } + private fun updateTypingEvent(messageText: String) { when { messageText.isEmpty() -> onTypingEvent(TypingIndicatorMode.STOPPED) @@ -155,8 +162,8 @@ class MessageCompositionHolder( } fun startMention() { - val beforeSelection = messageTextFieldValue.value.text - .subSequence(0, messageTextFieldValue.value.selection.min) + val beforeSelection = messageTextState.text + .subSequence(0, messageTextState.selection.min) .run { if (endsWith(String.WHITE_SPACE) || endsWith(String.NEW_LINE_SYMBOL) || this == String.EMPTY) { this.toString() @@ -167,10 +174,10 @@ class MessageCompositionHolder( } } - val afterSelection = messageTextFieldValue.value.text + val afterSelection = messageTextState.text .subSequence( - messageTextFieldValue.value.selection.max, - messageTextFieldValue.value.text.length + messageTextState.selection.max, + messageTextState.text.length ) val resultText = StringBuilder(beforeSelection) @@ -179,42 +186,35 @@ class MessageCompositionHolder( .toString() val newSelection = TextRange(beforeSelection.length + 1) - messageTextFieldValue.value = messageTextFieldValue.value.copy( - text = messageTextFieldValue.value.text.replaceRange(0, messageTextFieldValue.value.text.length, resultText), + messageTextState.edit { + replace(0, messageTextState.text.length, resultText) selection = newSelection - ) + } requestMentionSuggestionIfNeeded(resultText, newSelection) } fun addMention(contact: Contact) { - val mentionToAdd = UIMention( - start = currentMentionStartIndex(messageTextFieldValue.value.text, messageTextFieldValue.value.selection), + val mention = UIMention( + start = currentMentionStartIndex(messageTextState.text.toString(), messageTextState.selection), length = contact.name.length + 1, // +1 cause there is an "@" before it userId = UserId(contact.id, contact.domain), handler = String.MENTION_SYMBOL + contact.name ) - val updatedList = mutableListOf() - messageComposition.value.selectedMentions.forEach { mention -> - if (messageTextFieldValue.value.selection.start < mention.start) { - updatedList.add(mention.copy(start = mention.start + mentionToAdd.length)) - } else { - updatedList.add(mention) - } - } - updatedList.add(mentionToAdd) - insertMentionIntoText(mentionToAdd) + insertMentionIntoText(mention) messageComposition.update { - it.copy(selectedMentions = updatedList.sortedBy { it.start }) + it.copy( + selectedMentions = it.selectedMentions.plus(mention).sortedBy { it.start } + ) } } private fun insertMentionIntoText(mention: UIMention) { - val beforeMentionText = messageTextFieldValue.value.text + val beforeMentionText = messageTextState.text .subSequence(0, mention.start) - val afterMentionText = messageTextFieldValue.value.text + val afterMentionText = messageTextState.text .subSequence( - messageTextFieldValue.value.selection.max, - messageTextFieldValue.value.text.length + messageTextState.selection.max, + messageTextState.text.length ) val resultText = StringBuilder() .append(beforeMentionText) @@ -227,18 +227,15 @@ class MessageCompositionHolder( // + 1 cause we add space after mention and move selector there val newSelection = TextRange(beforeMentionText.length + mention.handler.length + 1) - messageTextFieldValue.value = messageTextFieldValue.value.copy( - text = messageTextFieldValue.value.text.replaceRange(0, messageTextFieldValue.value.text.length, resultText), + messageTextState.edit { + replace(0, messageTextState.text.length, resultText) selection = newSelection - ) + } onSaveDraft(messageComposition.value.toDraft(resultText)) } fun setEditText(messageId: String, editMessageText: String, mentions: List) { - messageTextFieldValue.value = messageTextFieldValue.value.copy( - text = editMessageText, - selection = TextRange(editMessageText.length) // Place cursor at the end of the new text - ) + messageTextState.setTextAndPlaceCursorAtEnd(editMessageText) messageComposition.update { it.copy( selectedMentions = mentions.mapNotNull { it.toUiMention(editMessageText) }, @@ -252,9 +249,9 @@ class MessageCompositionHolder( markdown: RichTextMarkdown, ) { val isHeader = markdown == RichTextMarkdown.Header - val range = messageTextFieldValue.value.selection - val selectedText = messageTextFieldValue.value.text.substring(messageTextFieldValue.value.selection) - val stringBuilder = StringBuilder(messageTextFieldValue.value.text) + val range = messageTextState.selection + val selectedText = messageTextState.text.substring(messageTextState.selection) + val stringBuilder = StringBuilder(messageTextState.text) val markdownLength = markdown.value.length val markdownLengthComplete = if (isHeader) markdownLength else (markdownLength * RICH_TEXT_MARKDOWN_MULTIPLIER) @@ -287,15 +284,15 @@ class MessageCompositionHolder( } val newMessageText = stringBuilder.toString() - messageTextFieldValue.value = messageTextFieldValue.value.copy( - text = messageTextFieldValue.value.text.replaceRange(0, messageTextFieldValue.value.text.length, newMessageText), + messageTextState.edit { + replace(0, messageTextState.text.length, newMessageText) selection = TextRange(selectionStart, selectionEnd) - ) + } onSaveDraft(messageComposition.value.toDraft(newMessageText)) } fun clearMessage() { - messageTextFieldValue.value = TextFieldValue(String.EMPTY) + messageTextState.clearText() messageComposition.update { it.copy( quotedMessageId = null, @@ -308,7 +305,7 @@ class MessageCompositionHolder( } fun toMessageBundle(conversationId: ConversationId) = - messageComposition.value.toMessageBundle(conversationId, messageTextFieldValue.value.text) + messageComposition.value.toMessageBundle(conversationId, messageTextState.text.toString()) private fun currentMentionStartIndex(messageText: String, selection: TextRange): Int { val lastIndexOfAt = messageText.lastIndexOf(String.MENTION_SYMBOL, selection.min - 1) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageCompositionInputStateHolder.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageCompositionInputStateHolder.kt index 6d76e876a00..b86d0ac9d6c 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageCompositionInputStateHolder.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageCompositionInputStateHolder.kt @@ -18,8 +18,8 @@ package com.wire.android.ui.home.messagecomposer.state import androidx.annotation.VisibleForTesting +import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState import androidx.compose.runtime.Stable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -30,7 +30,6 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.SoftwareKeyboardController import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -43,7 +42,7 @@ import com.wire.android.util.isNotMarkdownBlank @Stable class MessageCompositionInputStateHolder( - val messageTextFieldValue: MutableState, + val messageTextState: TextFieldState, private val keyboardController: SoftwareKeyboardController?, val focusRequester: FocusRequester ) { @@ -65,12 +64,12 @@ class MessageCompositionInputStateHolder( val inputType: InputType by derivedStateOf { when (val state = compositionState) { is CompositionState.Composing -> InputType.Composing( - isSendButtonEnabled = messageTextFieldValue.value.text.isNotMarkdownBlank() + isSendButtonEnabled = messageTextState.text.isNotMarkdownBlank() ) is CompositionState.Editing -> InputType.Editing( - isEditButtonEnabled = messageTextFieldValue.value.text != state.originalMessageText && - messageTextFieldValue.value.text.isNotMarkdownBlank() + isEditButtonEnabled = messageTextState.text != state.originalMessageText && + messageTextState.text.isNotMarkdownBlank() ) } } @@ -172,7 +171,7 @@ class MessageCompositionInputStateHolder( val composeTextHeight = 128.dp fun saver( - messageTextFieldValue: MutableState, + messageTextState: TextFieldState, keyboardController: SoftwareKeyboardController?, focusRequester: FocusRequester, density: Density @@ -190,7 +189,7 @@ class MessageCompositionInputStateHolder( restore = { savedState -> with(density) { MessageCompositionInputStateHolder( - messageTextFieldValue = messageTextFieldValue, + messageTextState = messageTextState, keyboardController = keyboardController, focusRequester = focusRequester ).apply { diff --git a/app/src/test/kotlin/com/wire/android/ui/common/textfield/mention/MentionAdjusterTest.kt b/app/src/test/kotlin/com/wire/android/ui/common/textfield/mention/MentionAdjusterTest.kt deleted file mode 100644 index abc2c8b43db..00000000000 --- a/app/src/test/kotlin/com/wire/android/ui/common/textfield/mention/MentionAdjusterTest.kt +++ /dev/null @@ -1,163 +0,0 @@ -/* - * 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.mention - -import androidx.compose.ui.text.TextRange -import com.wire.android.framework.TestUser -import com.wire.android.ui.home.conversations.model.UIMention -import org.junit.Assert.assertEquals -import org.junit.Test - -class MentionAdjusterTest { - - private val mentionAdjuster: MentionAdjuster = MentionAdjuster() - - // --- adjustMentionsForDeletion Tests --- - - @Test - fun `Given deleted text does not affect any mentions, When adjustMentionsForDeletion is called, Then mentions remain unchanged`() { - // Given - val mentions = listOf(UIMention(start = 6, length = 5, handler = "@user", userId = TestUser.USER_ID)) - val deletedLength = 2 - val text = "Hello @user again" - val selection = TextRange(13, 10) - - // When - val (updatedMentions, updatedSelection) = mentionAdjuster.adjustMentionsForDeletion( - mentions = mentions, - deletedLength = deletedLength, - text = text, - selection = selection - ) - - // Then - assertEquals(mentions, updatedMentions) - assertEquals(selection, updatedSelection) - } - - @Test - fun `Given deleted text affects mentions, When adjustMentionsForDeletion is called, Then mentions are adjusted and selection updated`() { - // Given - val mentions = listOf(UIMention(start = 12, length = 5, handler = "@user", userId = TestUser.USER_ID)) - val deletedLength = 2 // Simulate deleting 2 characters before the mention - val text = "Hello ain @user" - val selection = TextRange(7, 7) - - // When - val (updatedMentions, updatedSelection) = mentionAdjuster.adjustMentionsForDeletion( - mentions = mentions, - deletedLength = deletedLength, - text = text, - selection = selection - ) - - // Then - val expectedMention = UIMention(start = 10, length = 5, handler = "@user", userId = TestUser.USER_ID) - assertEquals(listOf(expectedMention), updatedMentions) - assertEquals(selection, updatedSelection) - } - - @Test - fun `Given cursor is at end of a mention, When adjustMentionsForDeletion is called, Then selection is updated to the mention's range`() { - // Given - val mentions = listOf(UIMention(start = 0, length = 5, handler = "@user", userId = TestUser.USER_ID)) - val deletedLength = 1 // Simulate deleting 1 character inside the mention - val text = "@user" - val selection = TextRange(5, 5) - - // When - val (updatedMentions, updatedSelection) = mentionAdjuster.adjustMentionsForDeletion( - mentions = mentions, - deletedLength = deletedLength, - text = text, - selection = selection - ) - - // Then - val expectedMention = UIMention(start = 0, length = 5, handler = "@user", userId = TestUser.USER_ID) - assertEquals(listOf(expectedMention), updatedMentions) - assertEquals(TextRange(0, 5), updatedSelection) - } - - // --- adjustMentionsForInsertion Tests --- - - @Test - fun `Given inserted text does not affect any mentions, When adjustMentionsForInsertion is called, Then mentions remain unchanged`() { - // Given - val mentions = listOf(UIMention(start = 5, length = 5, handler = "@user", userId = TestUser.USER_ID)) - val addedLength = 0 - val text = "Hello world" - val selection = TextRange(0, 5) - - // When - val (updatedMentions, updatedSelection) = mentionAdjuster.adjustMentionsForInsertion( - mentions = mentions, - text = text, - selection = selection, - addedLength = addedLength - ) - - // Then - assertEquals(mentions, updatedMentions) - assertEquals(selection, updatedSelection) - } - - @Test - fun `Given inserted text shifts mentions, When adjustMentionsForInsertion is called, Then mentions are adjusted`() { - // Given - val mentions = listOf(UIMention(start = 5, length = 5, handler = "@user", userId = TestUser.USER_ID)) - val addedLength = 2 // Simulate inserting 2 characters before the mention - val text = "Hello @user" - val selection = TextRange(0, 5) - - // When - val (updatedMentions, updatedSelection) = mentionAdjuster.adjustMentionsForInsertion( - mentions = mentions, - text = text, - selection = selection, - addedLength = addedLength - ) - - // Then - val expectedMention = UIMention(start = 7, length = 5, handler = "@user", userId = TestUser.USER_ID) - assertEquals(listOf(expectedMention), updatedMentions) - assertEquals(selection, updatedSelection) - } - - @Test - fun `Given inserted text shifts mentions, When adjustMentionsForInsertion is called, Then mentions are adjusted accordingly`() { - // Given - val mentions = listOf(UIMention(start = 0, length = 5, handler = "@user", userId = TestUser.USER_ID)) - val addedLength = 3 // Simulate inserting 3 characters before the mention - val text = "Hel world" - val selection = TextRange(0, 5) - - // When - val (updatedMentions, updatedSelection) = mentionAdjuster.adjustMentionsForInsertion( - mentions = mentions, - text = text, - selection = selection, - addedLength = addedLength - ) - - // Then - val expectedMention = UIMention(start = 3, length = 5, handler = "@user", userId = TestUser.USER_ID) - assertEquals(listOf(expectedMention), updatedMentions) - assertEquals(selection, updatedSelection) - } -} diff --git a/app/src/test/kotlin/com/wire/android/ui/common/textfield/mention/MentionSelectionManagerTest.kt b/app/src/test/kotlin/com/wire/android/ui/common/textfield/mention/MentionSelectionManagerTest.kt deleted file mode 100644 index 8daf2c2e19c..00000000000 --- a/app/src/test/kotlin/com/wire/android/ui/common/textfield/mention/MentionSelectionManagerTest.kt +++ /dev/null @@ -1,105 +0,0 @@ -/* - * 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.mention - -import androidx.compose.ui.text.TextRange -import com.wire.android.framework.TestUser -import com.wire.android.ui.home.conversations.model.UIMention -import org.junit.Assert.assertEquals -import org.junit.Test - -class MentionSelectionManagerTest { - - private val selectionManager: MentionSelectionManager = MentionSelectionManager() - - @Test - fun `Given same old and new selection, When updateSelectionForMention is called, Then selection remains unchanged`() { - // Given - val oldSelection = TextRange(0, 5) - val newSelection = TextRange(0, 5) // Same as old selection - val mentions = listOf(UIMention(start = 0, length = 5, handler = "@user", userId = TestUser.USER_ID)) - - // When - val updatedSelection = selectionManager.updateSelectionForMention( - oldSelection = oldSelection, - newSelection = newSelection, - mentions = mentions - ) - - // Then - assertEquals(newSelection, updatedSelection) - } - - @Test - fun `Given new selection inside a mention, When updateSelectionForMention is called, Then selection updates to the mention's range`() { - // Given - val oldSelection = TextRange(0, 5) - val newSelection = TextRange(3, 3) // Inside the mention range - val mentions = listOf(UIMention(start = 0, length = 5, handler = "@user", userId = TestUser.USER_ID)) - - // When - val updatedSelection = selectionManager.updateSelectionForMention( - oldSelection = oldSelection, - newSelection = newSelection, - mentions = mentions - ) - - // Then - assertEquals(TextRange(0, 5), updatedSelection) - } - - @Test - fun `Given new selection outside of any mention, When updateSelectionForMention is called, Then selection remains unchanged`() { - // Given - val oldSelection = TextRange(0, 5) - val newSelection = TextRange(10, 10) // Outside the mention range - val mentions = listOf(UIMention(start = 0, length = 5, handler = "@user", userId = TestUser.USER_ID)) - - // When - val updatedSelection = selectionManager.updateSelectionForMention( - oldSelection = oldSelection, - newSelection = newSelection, - mentions = mentions - ) - - // Then - assertEquals(newSelection, updatedSelection) // Should remain unchanged - } - - @Test - fun `Given multiple mentions, When new selection is inside one of them, Then selection updates to the correct mention's range`() { - // Given - val oldSelection = TextRange(0, 5) - val newSelection = TextRange(8, 8) // Inside the second mention - val mentions = listOf( - UIMention(start = 0, length = 5, handler = "@user1", userId = TestUser.USER_ID), - UIMention(start = 6, length = 5, handler = "@user2", userId = TestUser.SELF_USER_ID), - UIMention(start = 15, length = 5, handler = "@user2", userId = TestUser.SELF_USER_ID) - ) - - // When - val updatedSelection = selectionManager.updateSelectionForMention( - oldSelection = oldSelection, - newSelection = newSelection, - mentions = mentions - ) - - // Then - assertEquals(TextRange(6, 11), updatedSelection) - } -} diff --git a/app/src/test/kotlin/com/wire/android/ui/common/textfield/mention/MentionUpdateCoordinatorTest.kt b/app/src/test/kotlin/com/wire/android/ui/common/textfield/mention/MentionUpdateCoordinatorTest.kt deleted file mode 100644 index 94dd56d4ece..00000000000 --- a/app/src/test/kotlin/com/wire/android/ui/common/textfield/mention/MentionUpdateCoordinatorTest.kt +++ /dev/null @@ -1,181 +0,0 @@ -/* - * 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.mention - -import androidx.compose.ui.text.TextRange -import androidx.compose.ui.text.input.TextFieldValue -import com.wire.android.framework.TestUser -import com.wire.android.ui.home.conversations.model.UIMention -import com.wire.android.util.EMPTY -import io.mockk.MockKAnnotations -import io.mockk.every -import io.mockk.impl.annotations.MockK -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Test - -class MentionUpdateCoordinatorTest { - - @MockK - private lateinit var mentionAdjuster: MentionAdjuster - - @MockK - private lateinit var selectionManager: MentionSelectionManager - - init { - MockKAnnotations.init(this) - } - - private val coordinator = MentionUpdateCoordinator( - mentionAdjuster = mentionAdjuster, - selectionManager = selectionManager - ) - - @Test - fun `Given empty new text, When handle is called, Then mentions should be cleared`() { - // Given - val oldTextFieldValue = TextFieldValue(text = "Hello", selection = TextRange(0, 5)) - val newTextFieldValue = TextFieldValue(text = "", selection = TextRange(0, 0)) - val mentions = listOf(UIMention(start = 0, length = 5, handler = "@user", userId = TestUser.USER_ID)) - var isInvoked = false - // When - val updatedTextFieldValue = coordinator.handle( - oldTextFieldValue, - newTextFieldValue, - mentions, - updateMentions = { isInvoked = true } - ) - - // Then - assertEquals(String.EMPTY, updatedTextFieldValue.text) - assertTrue(isInvoked) - } - - @Test - fun `Given no mention change, When handle is called, Then mentions should remain unchanged`() { - // Given - val oldTextFieldValue = TextFieldValue(text = "Hello", selection = TextRange(0, 5)) - val newTextFieldValue = TextFieldValue(text = "Hello", selection = TextRange(0, 5)) - val mentions = listOf(UIMention(start = 0, length = 5, handler = "@user", userId = TestUser.USER_ID)) - var isInvoked = false - every { - selectionManager.updateSelectionForMention(any(), any(), any()) - } returns TextRange(0, 5) - - // When - val updatedTextFieldValue = coordinator.handle( - oldTextFieldValue, - newTextFieldValue, - mentions, - updateMentions = { isInvoked = true } - ) - - // Then - assertEquals("Hello", updatedTextFieldValue.text) - assertEquals(TextRange(0, 5), updatedTextFieldValue.selection) - assertFalse(isInvoked) - } - - @Test - fun `Given text deletion, When handle is called, Then mentions and selection should adjust`() { - // Given - val oldTextFieldValue = TextFieldValue(text = "Hello @user", selection = TextRange(0, 11)) - val newTextFieldValue = TextFieldValue(text = "Hello", selection = TextRange(0, 5)) - val mentions = listOf(UIMention(start = 6, length = 5, handler = "@user", userId = TestUser.USER_ID)) - var isInvoked = false - - every { - mentionAdjuster.adjustMentionsForDeletion( - mentions = mentions, - deletedLength = 6, - text = "Hello", - selection = newTextFieldValue.selection - ) - } returns Pair(listOf(UIMention(start = 6, length = 5, handler = "@user", userId = TestUser.USER_ID)), TextRange(0, 5)) - - // When - val updatedTextFieldValue = coordinator.handle( - oldTextFieldValue, - newTextFieldValue, - mentions, - updateMentions = { isInvoked = true } - ) - - // Then - assertEquals("Hello", updatedTextFieldValue.text) - assertEquals(TextRange(0, 5), updatedTextFieldValue.selection) - assertTrue(isInvoked) - } - - @Test - fun `Given text insertion, When handle is called, Then mentions should shift accordingly`() { - // Given - val oldTextFieldValue = TextFieldValue(text = "Hello", selection = TextRange(0, 5)) - val newTextFieldValue = TextFieldValue(text = "Hello @user", selection = TextRange(0, 5)) - val mentions = listOf(UIMention(start = 6, length = 5, handler = "@user", userId = TestUser.USER_ID)) - var isInvoked = false - - every { - mentionAdjuster.adjustMentionsForInsertion( - mentions = mentions, - addedLength = 6, - text = any(), - selection = any() - ) - } returns Pair(listOf(UIMention(start = 12, length = 5, handler = "@user", userId = TestUser.USER_ID)), TextRange(0, 5)) - - // When - val updatedTextFieldValue = coordinator.handle( - oldTextFieldValue, - newTextFieldValue, - mentions, - updateMentions = { isInvoked = true } - ) - - // Then - assertEquals("Hello @user", updatedTextFieldValue.text) - assertEquals(TextRange(0, 5), updatedTextFieldValue.selection) - assertTrue(isInvoked) - } - - @Test - fun `Given selection inside mention, When handle is called, Then selection should update`() { - // Given - val oldTextFieldValue = TextFieldValue(text = "Hello @user", selection = TextRange(0, 5)) - val newTextFieldValue = TextFieldValue(text = "Hello @user", selection = TextRange(0, 5)) - val mentions = listOf(UIMention(start = 6, length = 5, handler = "@user", userId = TestUser.USER_ID)) - var isInvoked = false - - every { - selectionManager.updateSelectionForMention(any(), any(), any()) - } returns TextRange(6, 11) - - // When - val updatedTextFieldValue = coordinator.handle( - oldTextFieldValue, - newTextFieldValue, - mentions, - updateMentions = { isInvoked = true } - ) - - // Then - assertEquals(TextRange(6, 11), updatedTextFieldValue.selection) - assertFalse(isInvoked) - } -} diff --git a/app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposerStateHolderTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposerStateHolderTest.kt index 8a55dfb5bd5..7d435d6b520 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposerStateHolderTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposerStateHolderTest.kt @@ -19,12 +19,12 @@ package com.wire.android.ui.home.messagecomposer import android.content.Context +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd import androidx.compose.runtime.MutableState import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.text.TextRange -import androidx.compose.ui.text.input.TextFieldValue import com.wire.android.config.CoroutineTestExtension import com.wire.android.config.SnapshotExtension import com.wire.android.framework.TestConversation @@ -65,7 +65,7 @@ class MessageComposerStateHolderTest { private lateinit var messageCompositionHolder: State private lateinit var additionalOptionStateHolder: AdditionalOptionStateHolder private lateinit var state: MessageComposerStateHolder - private lateinit var messageTextFieldValue: MutableState + private lateinit var messageTextState: TextFieldState @BeforeEach fun before() { @@ -74,16 +74,16 @@ class MessageComposerStateHolderTest { every { focusRequester.captureFocus() } returns true messageComposerViewState = mutableStateOf(MessageComposerViewState()) messageComposition = mutableStateOf(MessageComposition(TestConversation.ID)) - messageTextFieldValue = mutableStateOf(TextFieldValue()) + messageTextState = TextFieldState() messageCompositionInputStateHolder = MessageCompositionInputStateHolder( - messageTextFieldValue = messageTextFieldValue, + messageTextState = messageTextState, keyboardController = null, focusRequester = focusRequester ) messageCompositionHolder = mutableStateOf( MessageCompositionHolder( messageComposition = messageComposition, - messageTextFieldValue = messageTextFieldValue, + messageTextState = messageTextState, onClearDraft = {}, onSaveDraft = {}, onSearchMentionQueryChanged = {}, @@ -134,13 +134,9 @@ class MessageComposerStateHolderTest { editMessageText = "edit_message_text", mentions = listOf() ) - - state.messageCompositionHolder.value.messageTextFieldValue.value = - messageTextFieldValue.value.copy( - text = messageTextFieldValue.value.text + "some text", - selection = TextRange(messageTextFieldValue.value.text.length + "some text".length) - ) - + state.messageCompositionHolder.value.messageTextState.edit { + append("some text") + } assertInstanceOf(InputType.Editing::class.java, messageCompositionInputStateHolder.inputType).also { assertEquals(true, it.isEditButtonEnabled) } @@ -154,7 +150,7 @@ class MessageComposerStateHolderTest { state.toReply(mockMessageWithText) // then - assertEquals(String.EMPTY, messageCompositionHolder.value.messageTextFieldValue.value.text) + assertEquals(String.EMPTY, messageCompositionHolder.value.messageTextState.text.toString()) assertInstanceOf(InputType.Composing::class.java, messageCompositionInputStateHolder.inputType) } @@ -162,15 +158,13 @@ class MessageComposerStateHolderTest { fun `given some message was being composed, when setting toReply, then input continues with the current text`() = runTest { // given val currentText = "Potato" - messageTextFieldValue.value = messageTextFieldValue.value.copy( - text = currentText, - selection = TextRange(currentText.length) - ) + messageCompositionHolder.value.messageTextState.setTextAndPlaceCursorAtEnd(currentText) + // when state.toReply(mockMessageWithText) // then - assertEquals(currentText, messageCompositionHolder.value.messageTextFieldValue.value.text) + assertEquals(currentText, messageCompositionHolder.value.messageTextState.text.toString()) } @Test @@ -197,7 +191,7 @@ class MessageComposerStateHolderTest { // then assertEquals( String.EMPTY, - messageCompositionHolder.value.messageTextFieldValue.value.text + messageCompositionHolder.value.messageTextState.text.toString() ) assertEquals( null, diff --git a/app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageCompositionHolderTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageCompositionHolderTest.kt index e0cab8fb694..91fee977b50 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageCompositionHolderTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageCompositionHolderTest.kt @@ -18,10 +18,10 @@ package com.wire.android.ui.home.messagecomposer.state import android.content.Context +import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.text.TextRange -import androidx.compose.ui.text.input.TextFieldValue import com.wire.android.config.SnapshotExtension import com.wire.android.framework.TestConversation import com.wire.android.ui.home.messagecomposer.model.MessageComposition @@ -47,7 +47,7 @@ class MessageCompositionHolderTest { private lateinit var state: MessageCompositionHolder private lateinit var messageComposition: MutableState - private lateinit var messageTextFieldValue: MutableState + private lateinit var messageTextState: TextFieldState private val dispatcher = StandardTestDispatcher() @BeforeEach @@ -56,10 +56,10 @@ class MessageCompositionHolderTest { Dispatchers.setMain(dispatcher) messageComposition = mutableStateOf(MessageComposition(TestConversation.ID)) - messageTextFieldValue = mutableStateOf(TextFieldValue()) + messageTextState = TextFieldState() state = MessageCompositionHolder( messageComposition = messageComposition, - messageTextFieldValue = messageTextFieldValue, + messageTextState = messageTextState, onClearDraft = {}, onSaveDraft = {}, onSearchMentionQueryChanged = {}, @@ -82,7 +82,7 @@ class MessageCompositionHolderTest { // then assertEquals( "# ", - state.messageTextFieldValue.value.text + state.messageTextState.text.toString() ) } @@ -95,7 +95,7 @@ class MessageCompositionHolderTest { // then assertEquals( "****", - state.messageTextFieldValue.value.text + state.messageTextState.text.toString() ) } @@ -108,52 +108,62 @@ class MessageCompositionHolderTest { // then assertEquals( "__", - state.messageTextFieldValue.value.text + state.messageTextState.text.toString() ) } @Test fun `given non empty text, when adding header markdown on selection, then # is added to the text`() = runTest { // given - val newText = "header" - state.messageTextFieldValue.value = messageTextFieldValue.value.copy( - text = newText, - selection = TextRange(0, 6) - ) + state.messageTextState.edit { + replace(0, length, "header") + selection = TextRange( + start = 0, + end = 6 + ) + } + // when state.addOrRemoveMessageMarkdown(markdown = RichTextMarkdown.Header) // then assertEquals( "# header", - state.messageTextFieldValue.value.text + state.messageTextState.text.toString() ) } @Test fun `given non empty text, when adding bold markdown on selection, then 2x star char is added to the text`() = runTest { // given - state.messageTextFieldValue.value = messageTextFieldValue.value.copy( - text = "bold", // Replace the entire text with "bold" - selection = TextRange(0, 4) - ) + state.messageTextState.edit { + replace(0, length, "bold") + selection = TextRange( + start = 0, + end = 4 + ) + } + // when state.addOrRemoveMessageMarkdown(markdown = RichTextMarkdown.Bold) // then assertEquals( "**bold**", - state.messageTextFieldValue.value.text + state.messageTextState.text.toString() ) } @Test fun `given non empty text, when adding italic markdown on selection, then 2x _ is added to the text`() = runTest { // given - state.messageTextFieldValue.value = messageTextFieldValue.value.copy( - text = "italic", // Replace the entire text with "bold" - selection = TextRange(0, 6) - ) + state.messageTextState.edit { + replace(0, length, "italic") + selection = TextRange( + start = 0, + end = 6 + ) + } // when state.addOrRemoveMessageMarkdown(markdown = RichTextMarkdown.Italic) @@ -161,7 +171,7 @@ class MessageCompositionHolderTest { // then assertEquals( "_italic_", - state.messageTextFieldValue.value.text + state.messageTextState.text.toString() ) } } diff --git a/app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageCompositionInputStateHolderTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageCompositionInputStateHolderTest.kt index 81f3a08a537..9364fc0fd32 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageCompositionInputStateHolderTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageCompositionInputStateHolderTest.kt @@ -20,11 +20,10 @@ package com.wire.android.ui.home.messagecomposer.state -import androidx.compose.runtime.mutableStateOf +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.platform.SoftwareKeyboardController -import androidx.compose.ui.text.TextRange -import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import com.wire.android.config.CoroutineTestExtension import com.wire.android.config.SnapshotExtension @@ -234,14 +233,14 @@ class MessageCompositionInputStateHolderTest { class Arrangement { - private val textFieldValue = mutableStateOf(TextFieldValue()) + private val textFieldState = TextFieldState() val softwareKeyboardController = mockk() private val focusRequester = mockk() private val state by lazy { - MessageCompositionInputStateHolder(textFieldValue, softwareKeyboardController, focusRequester) + MessageCompositionInputStateHolder(textFieldState, softwareKeyboardController, focusRequester) } init { @@ -252,10 +251,7 @@ class MessageCompositionInputStateHolderTest { } fun withText(text: String) = apply { - textFieldValue.value = textFieldValue.value.copy( - text = text, - selection = TextRange(text.length) - ) + textFieldState.setTextAndPlaceCursorAtEnd(text) } fun arrange() = state to this