Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: improve mention while typing (WPB-1895) #3690

Merged
merged 27 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
539cc46
feat: hide personal to team migration feature if not supported by bac…
ohassine Nov 20, 2024
d468507
feat: update kalium reference
ohassine Nov 20, 2024
6c5f913
Merge branch 'develop' into hide-personal-to-team-migration-feature-i…
ohassine Nov 20, 2024
1bc8443
feat: unit test
ohassine Nov 20, 2024
4417ddc
Merge remote-tracking branch 'origin/hide-personal-to-team-migration-…
ohassine Nov 20, 2024
66162d1
chore: address comments
ohassine Nov 21, 2024
af49833
chore: kalium reference
ohassine Nov 21, 2024
3109f20
Merge remote-tracking branch 'origin/develop' into hide-personal-to-t…
ohassine Nov 22, 2024
abe3a39
chore: kalium reference
ohassine Nov 22, 2024
ef4ce7e
chore: kalium reference
ohassine Nov 22, 2024
2c4163c
chore: update kalium reference
ohassine Nov 26, 2024
90330bf
Merge remote-tracking branch 'origin/develop' into hide-personal-to-t…
ohassine Nov 26, 2024
1e77436
chore: Empty-Commit
ohassine Nov 26, 2024
4ac44fd
chore: unit test
ohassine Nov 26, 2024
c10922d
Merge branch 'develop' into hide-personal-to-team-migration-feature-i…
ohassine Nov 26, 2024
0adfb18
chore: unit test
ohassine Nov 26, 2024
9298370
chore: detekt
ohassine Nov 26, 2024
d8512cc
Merge branch 'develop' into hide-personal-to-team-migration-feature-i…
ohassine Nov 26, 2024
2337d1d
chore: update kalium reference
ohassine Nov 26, 2024
56ad181
Merge remote-tracking branch 'origin/hide-personal-to-team-migration-…
ohassine Nov 26, 2024
a1ce79b
feat: mention while typing
ohassine Nov 29, 2024
31610ef
Merge remote-tracking branch 'origin/develop' into mention-while-typing
ohassine Dec 2, 2024
4680d96
feat: detekt
ohassine Dec 2, 2024
1e11108
feat: kalium reference
ohassine Dec 2, 2024
4517782
feat: kalium reference
ohassine Dec 2, 2024
5ebe739
chore: documentation
ohassine Dec 2, 2024
ac8f978
Merge branch 'develop' into mention-while-typing
ohassine Dec 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

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,49 @@
/*
* 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 {
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
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,6 @@ class MessageCompositionHolder(
.distinctUntilChanged()
.collectLatest { (messageText, selection) ->
updateTypingEvent(messageText)
updateMentionsIfNeeded(messageText)
requestMentionSuggestionIfNeeded(messageText, selection)
onSaveDraft(messageComposition.value.toDraft(messageText))
}
Expand All @@ -125,10 +124,6 @@ class MessageCompositionHolder(
}
}

private fun updateMentionsIfNeeded(messageText: String) {
messageComposition.update { it.copy(selectedMentions = it.getSelectedMentions(messageText)) }
}

private fun requestMentionSuggestionIfNeeded(messageText: String, selection: TextRange) {
if (selection.min != selection.max) {
onClearMentionSearchResult()
Expand Down Expand Up @@ -192,17 +187,24 @@ class MessageCompositionHolder(
}

fun addMention(contact: Contact) {
val mention = UIMention(
val mentionToAdd = UIMention(
start = currentMentionStartIndex(messageTextFieldValue.value.text, messageTextFieldValue.value.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
)
insertMentionIntoText(mention)
val updatedList = mutableListOf<UIMention>()
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)
messageComposition.update {
it.copy(
selectedMentions = it.selectedMentions.plus(mention).sortedBy { it.start }
)
it.copy(selectedMentions = updatedList.sortedBy { it.start })
}
}

Expand Down
Loading
Loading