From 42e01b6d20b08f5b7b1fad982f44bc949ae8348f Mon Sep 17 00:00:00 2001 From: Igor Furgala Date: Mon, 13 Oct 2025 09:47:02 +0200 Subject: [PATCH] fix: mentions detection - android --- .../com/swmansion/enriched/EnrichedTextInputView.kt | 13 ++++++++----- .../com/swmansion/enriched/events/MentionHandler.kt | 5 +++++ .../swmansion/enriched/styles/ParametrizedStyles.kt | 3 +++ .../swmansion/enriched/utils/EnrichedSelection.kt | 2 +- .../enriched/watchers/EnrichedTextWatcher.kt | 4 ++-- example/src/App.tsx | 9 ++++++++- 6 files changed, 27 insertions(+), 9 deletions(-) diff --git a/android/src/main/java/com/swmansion/enriched/EnrichedTextInputView.kt b/android/src/main/java/com/swmansion/enriched/EnrichedTextInputView.kt index 99d834ac1..ca98ba4f6 100644 --- a/android/src/main/java/com/swmansion/enriched/EnrichedTextInputView.kt +++ b/android/src/main/java/com/swmansion/enriched/EnrichedTextInputView.kt @@ -51,7 +51,10 @@ class EnrichedTextInputView : AppCompatEditText { val paragraphStyles: ParagraphStyles? = ParagraphStyles(this) val listStyles: ListStyles? = ListStyles(this) val parametrizedStyles: ParametrizedStyles? = ParametrizedStyles(this) - var isSettingValue: Boolean = false + // Sometimes setting up style triggers many changes in sequence + // Eg. removing conflicting styles -> changing text -> applying spans + // In such scenario we want to prevent from handling side effects (eg. onTextChanged) + var isDuringTransaction: Boolean = false var isRemovingMany: Boolean = false val mentionHandler: MentionHandler? = MentionHandler(this) @@ -233,7 +236,7 @@ class EnrichedTextInputView : AppCompatEditText { fun setValue(value: CharSequence?) { if (value == null) return - isSettingValue = true + isDuringTransaction = true val newText = parseText(value) setText(newText) @@ -244,7 +247,7 @@ class EnrichedTextInputView : AppCompatEditText { // Scroll to the last line of text setSelection(text?.length ?: 0) - isSettingValue = false + isDuringTransaction = false } fun setAutoFocus(autoFocus: Boolean) { @@ -452,13 +455,13 @@ class EnrichedTextInputView : AppCompatEditText { val end = selection?.end ?: 0 val lengthBefore = text?.length ?: 0 - isSettingValue = true + isDuringTransaction = true val targetRange = getTargetRange(name) val removed = removeStyle(style, targetRange.first, targetRange.second) if (removed) { spanState?.setStart(style, null) } - isSettingValue = false + isDuringTransaction = false val lengthAfter = text?.length ?: 0 val charactersRemoved = lengthBefore - lengthAfter diff --git a/android/src/main/java/com/swmansion/enriched/events/MentionHandler.kt b/android/src/main/java/com/swmansion/enriched/events/MentionHandler.kt index 4a47ec6b3..874b3126e 100644 --- a/android/src/main/java/com/swmansion/enriched/events/MentionHandler.kt +++ b/android/src/main/java/com/swmansion/enriched/events/MentionHandler.kt @@ -8,6 +8,11 @@ class MentionHandler(private val view: EnrichedTextInputView) { private var previousText: String? = null private var previousIndicator: String? = null + fun reset() { + previousText = null + previousIndicator = null + } + fun endMention() { val indicator = previousIndicator if (indicator == null) return diff --git a/android/src/main/java/com/swmansion/enriched/styles/ParametrizedStyles.kt b/android/src/main/java/com/swmansion/enriched/styles/ParametrizedStyles.kt index 4cf6d7139..1716923ff 100644 --- a/android/src/main/java/com/swmansion/enriched/styles/ParametrizedStyles.kt +++ b/android/src/main/java/com/swmansion/enriched/styles/ParametrizedStyles.kt @@ -201,6 +201,7 @@ class ParametrizedStyles(private val view: EnrichedTextInputView) { } val start = mentionStart ?: return + view.isDuringTransaction = true spannable.replace(start, selectionEnd, text) val span = EnrichedMentionSpan(text, indicator, attributes, view.htmlStyle) @@ -213,6 +214,8 @@ class ParametrizedStyles(private val view: EnrichedTextInputView) { spannable.insert(safeEnd, " ") } + view.isDuringTransaction = false + view.mentionHandler?.reset() view.selection.validateStyles() } diff --git a/android/src/main/java/com/swmansion/enriched/utils/EnrichedSelection.kt b/android/src/main/java/com/swmansion/enriched/utils/EnrichedSelection.kt index f78536f3a..774a4e342 100644 --- a/android/src/main/java/com/swmansion/enriched/utils/EnrichedSelection.kt +++ b/android/src/main/java/com/swmansion/enriched/utils/EnrichedSelection.kt @@ -39,7 +39,7 @@ class EnrichedSelection(private val view: EnrichedTextInputView) { val finalStart = newStart.coerceAtMost(newEnd).coerceAtLeast(0).coerceAtMost(textLength) val finalEnd = newEnd.coerceAtLeast(newStart).coerceAtLeast(0).coerceAtMost(textLength) - if (isZeroWidthSelection(finalStart, finalEnd) && !view.isSettingValue) { + if (isZeroWidthSelection(finalStart, finalEnd) && !view.isDuringTransaction) { view.setSelection(finalStart + 1) shouldValidateStyles = false } diff --git a/android/src/main/java/com/swmansion/enriched/watchers/EnrichedTextWatcher.kt b/android/src/main/java/com/swmansion/enriched/watchers/EnrichedTextWatcher.kt index 9c26f6857..aaafcb2a6 100644 --- a/android/src/main/java/com/swmansion/enriched/watchers/EnrichedTextWatcher.kt +++ b/android/src/main/java/com/swmansion/enriched/watchers/EnrichedTextWatcher.kt @@ -18,14 +18,14 @@ class EnrichedTextWatcher(private val view: EnrichedTextInputView) : TextWatcher override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { endCursorPosition = start + count view.layoutManager.measureSize(s ?: "") - view.isRemovingMany = !view.isSettingValue && before > count + 1 + view.isRemovingMany = !view.isDuringTransaction && before > count + 1 } override fun afterTextChanged(s: Editable?) { if (s == null) return emitEvents(s) - if (view.isSettingValue) return + if (view.isDuringTransaction) return applyStyles(s) } diff --git a/example/src/App.tsx b/example/src/App.tsx index 3b514eedb..7d93a2863 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -149,7 +149,14 @@ export default function App() { }; const handleStartMention = (indicator: string) => { - indicator === '@' ? openUserMentionPopup() : openChannelMentionPopup(); + if (indicator === '@') { + userMention.onMentionChange(''); + openUserMentionPopup(); + return; + } + + channelMention.onMentionChange(''); + openChannelMentionPopup(); }; const handleEndMention = (indicator: string) => {