Skip to content

Commit

Permalink
feat: improve mention while typing (WPB-1895) (#3690)
Browse files Browse the repository at this point in the history
  • Loading branch information
ohassine authored Dec 2, 2024
1 parent 0a8549d commit d5b89ca
Show file tree
Hide file tree
Showing 13 changed files with 719 additions and 173 deletions.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -222,17 +222,7 @@ internal fun WireTextField(
innerBasicTextField = { _, textFieldModifier, decoratorBox ->
BasicTextField(
value = textFieldValue.value,
onValueChange = { newText ->
val mentionsByName = mentions.map { it.handler }
val updatedText =
MentionDeletionHandler.handle(
textFieldValue.value.text,
newText.text,
textFieldValue.value.selection,
mentionsByName
)
onValueChange(TextFieldValue(updatedText, newText.selection))
},
onValueChange = onValueChange,
textStyle = textStyle.copy(
color = colors.textColor(state = state).value,
textDirection = TextDirection.ContentOrLtr
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* 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<UIMention>,
deletedLength: Int,
text: String,
selection: TextRange
): Pair<List<UIMention>, TextRange> {
val updatedMentions = mutableListOf<UIMention>()
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<UIMention>,
text: String,
selection: TextRange,
addedLength: Int
): Pair<List<UIMention>, TextRange> {
val updatedMentions = mutableListOf<UIMention>()
// 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)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* 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<UIMention>
): 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
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* 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<UIMention>,
updateMentions: (List<UIMention>) -> 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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,11 @@ 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
Expand Down Expand Up @@ -206,10 +208,18 @@ fun EnabledMessageComposer(
conversationId = conversationId,
messageComposition = messageComposition.value,
messageTextFieldValue = inputStateHolder.messageTextFieldValue,
onValueChange = {
inputStateHolder.messageTextFieldValue.value = it
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),
mentions = messageComposition.value.selectedMentions,
isTextExpanded = inputStateHolder.isTextExpanded,
inputType = messageCompositionInputStateHolder.inputType,
focusRequester = messageCompositionInputStateHolder.focusRequester,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ private fun InputContent(
viewModel: SelfDeletingMessageActionViewModel =
hiltViewModelScoped<SelfDeletingMessageActionViewModelImpl, SelfDeletingMessageActionViewModel, SelfDeletingMessageActionArgs>(
SelfDeletingMessageActionArgs(conversationId = conversationId)
),
)
) {
ConstraintLayout(modifier = modifier) {
val (additionalOptionButton, input, actions) = createRefs()
Expand Down
Loading

0 comments on commit d5b89ca

Please sign in to comment.