From 778c5c41b977cea71e9781b899fbf7992c3c3156 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Mon, 25 May 2026 14:53:04 +0200 Subject: [PATCH 1/4] fix: unify detection events between native platforms --- .../textinput/utils/EnrichedSelection.kt | 132 ++++++++++++++---- ios/EnrichedTextInputView.mm | 128 ++++++++++------- 2 files changed, 188 insertions(+), 72 deletions(-) diff --git a/android/src/main/java/com/swmansion/enriched/textinput/utils/EnrichedSelection.kt b/android/src/main/java/com/swmansion/enriched/textinput/utils/EnrichedSelection.kt index 64239d295..54216fd39 100644 --- a/android/src/main/java/com/swmansion/enriched/textinput/utils/EnrichedSelection.kt +++ b/android/src/main/java/com/swmansion/enriched/textinput/utils/EnrichedSelection.kt @@ -190,31 +190,42 @@ class EnrichedSelection( private fun getParametrizedStyleStart(type: Class): Int? { val (start, end) = getInlineSelection() val spannable = view.text as Spannable - val spans = spannable.getSpans(start, end, type) val isLinkType = type == EnrichedInputLinkSpan::class.java val isMentionType = type == EnrichedInputMentionSpan::class.java - if (isLinkType && spans.isEmpty()) { - emitLinkDetectedEvent(spannable, null, start, end) + if (isMentionType) { + val activeMention = findActiveMentionSpan(spannable, start, end) + if (activeMention != null) { + val (span, spanStart, spanEnd) = activeMention + emitMentionDetectedEvent(span, spanStart, spanEnd) + return spanStart + } + if (wasMentionPreviouslyDetected()) { + emitMentionClearedEvent() + } return null } - if (isMentionType && spans.isEmpty()) { - emitMentionDetectedEvent(spannable, null, start, end) + if (isLinkType) { + val activeLink = findActiveLinkSpan(spannable, start, end) + if (activeLink != null) { + val (span, spanStart, spanEnd) = activeLink + emitLinkDetectedEvent(span, spanStart, spanEnd) + return spanStart + } + if (wasLinkPreviouslyDetected()) { + emitLinkClearedEvent() + } return null } + val spans = spannable.getSpans(start, end, type) + for (span in spans) { val spanStart = spannable.getSpanStart(span) val spanEnd = spannable.getSpanEnd(span) if (start >= spanStart && end <= spanEnd) { - if (isLinkType && span is EnrichedInputLinkSpan) { - emitLinkDetectedEvent(spannable, span, spanStart, spanEnd) - } else if (isMentionType && span is EnrichedInputMentionSpan) { - emitMentionDetectedEvent(spannable, span, spanStart, spanEnd) - } - return spanStart } } @@ -222,6 +233,56 @@ class EnrichedSelection( return null } + private fun findActiveLinkSpan( + spannable: Spannable, + start: Int, + end: Int, + ): Triple? { + val spans = spannable.getSpans(start, end, EnrichedInputLinkSpan::class.java) + + for (span in spans) { + val spanStart = spannable.getSpanStart(span) + val spanEnd = spannable.getSpanEnd(span) + + if (start >= spanStart && end <= spanEnd) { + return Triple(span, spanStart, spanEnd) + } + } + + return null + } + + private fun findActiveMentionSpan( + spannable: Spannable, + start: Int, + end: Int, + ): Triple? { + val spans = spannable.getSpans(start, end, EnrichedInputMentionSpan::class.java) + + for (span in spans) { + val spanStart = spannable.getSpanStart(span) + val spanEnd = spannable.getSpanEnd(span) + + if (start >= spanStart && end <= spanEnd) { + return Triple(span, spanStart, spanEnd) + } + } + + return null + } + + private fun wasMentionPreviouslyDetected(): Boolean { + val previousText = previousMentionDetectedEvent["text"] ?: "" + val previousIndicator = previousMentionDetectedEvent["indicator"] ?: "" + return previousText.isNotEmpty() || previousIndicator.isNotEmpty() + } + + private fun wasLinkPreviouslyDetected(): Boolean { + val previousText = previousLinkDetectedEvent["text"] ?: "" + val previousUrl = previousLinkDetectedEvent["url"] ?: "" + return previousText.isNotEmpty() || previousUrl.isNotEmpty() + } + private fun emitSelectionChangeEvent( editable: Editable?, start: Int, @@ -249,15 +310,27 @@ class EnrichedSelection( } private fun emitLinkDetectedEvent( - spannable: Spannable, - span: EnrichedInputLinkSpan?, + span: EnrichedInputLinkSpan, + spanStart: Int, + spanEnd: Int, + ) { + val spannable = view.text as Spannable + val text = spannable.substring(spanStart, spanEnd).replace(EnrichedConstants.ZWS_STRING, "") + dispatchLinkDetectedEvent(text, span.getUrl(), spanStart, spanEnd, spannable) + } + + private fun emitLinkClearedEvent() { + val spannable = view.text as Spannable + dispatchLinkDetectedEvent("", "", 0, 0, spannable) + } + + private fun dispatchLinkDetectedEvent( + text: String, + url: String, start: Int, end: Int, + spannable: Spannable, ) { - val text = spannable.substring(start, end).replace(EnrichedConstants.ZWS_STRING, "") - val url = span?.getUrl() ?: "" - - // Prevents emitting unnecessary events if (text == previousLinkDetectedEvent["text"] && url == previousLinkDetectedEvent["url"]) return previousLinkDetectedEvent.put("text", text) @@ -283,16 +356,27 @@ class EnrichedSelection( } private fun emitMentionDetectedEvent( - spannable: Spannable, - span: EnrichedInputMentionSpan?, - start: Int, - end: Int, + span: EnrichedInputMentionSpan, + spanStart: Int, + spanEnd: Int, ) { - val text = spannable.substring(start, end) - val attributes = span?.getAttributes() ?: emptyMap() - val indicator = span?.getIndicator() ?: "" + val spannable = view.text as Spannable + val text = spannable.substring(spanStart, spanEnd) + val attributes = span.getAttributes() + val indicator = span.getIndicator() val payload = JSONObject(attributes).toString() + dispatchMentionDetectedEvent(text, indicator, payload) + } + + private fun emitMentionClearedEvent() { + dispatchMentionDetectedEvent("", "", "{}") + } + private fun dispatchMentionDetectedEvent( + text: String, + indicator: String, + payload: String, + ) { val previousText = previousMentionDetectedEvent["text"] ?: "" val previousPayload = previousMentionDetectedEvent["payload"] ?: "" val previousIndicator = previousMentionDetectedEvent["indicator"] ?: "" diff --git a/ios/EnrichedTextInputView.mm b/ios/EnrichedTextInputView.mm index 95b8bd5c4..781f876a2 100644 --- a/ios/EnrichedTextInputView.mm +++ b/ios/EnrichedTextInputView.mm @@ -976,10 +976,12 @@ - (void)tryUpdatingActiveStyles { // data for onLinkDetected event LinkData *detectedLinkData; NSRange detectedLinkRange = NSMakeRange(0, 0); + BOOL shouldClearLink = NO; // data for onMentionDetected event MentionParams *detectedMentionParams = nullptr; NSRange detectedMentionRange = NSMakeRange(0, 0); + BOOL shouldClearMention = NO; for (NSNumber *type in stylesDict) { StyleBase *style = stylesDict[type]; @@ -1011,60 +1013,69 @@ - (void)tryUpdatingActiveStyles { } // onLinkDetected event - if (isActive && [type intValue] == [LinkStyle getType]) { - // get the link data - LinkData *candidateLinkData; - NSRange candidateLinkRange = NSMakeRange(0, 0); - LinkStyle *linkStyleClass = - (LinkStyle *)stylesDict[@([LinkStyle getType])]; - if (linkStyleClass != nullptr) { - candidateLinkData = - [linkStyleClass getLinkDataAt:textView.selectedRange.location]; - candidateLinkRange = - [linkStyleClass getFullLinkRangeAt:textView.selectedRange.location]; - } + if ([type intValue] == [LinkStyle getType]) { + if (isActive) { + // get the link data + LinkData *candidateLinkData; + NSRange candidateLinkRange = NSMakeRange(0, 0); + LinkStyle *linkStyleClass = + (LinkStyle *)stylesDict[@([LinkStyle getType])]; + if (linkStyleClass != nullptr) { + candidateLinkData = + [linkStyleClass getLinkDataAt:textView.selectedRange.location]; + candidateLinkRange = [linkStyleClass + getFullLinkRangeAt:textView.selectedRange.location]; + } - if (wasActive == NO) { - // we changed selection from non-link to a link - detectedLinkData = candidateLinkData; - detectedLinkRange = candidateLinkRange; - } else if (![_recentlyActiveLinkData - isEqualToLinkData:candidateLinkData] || - !NSEqualRanges(_recentlyActiveLinkRange, candidateLinkRange)) { - // we changed selection from one link to the other or modified - // current link's text - detectedLinkData = candidateLinkData; - detectedLinkRange = candidateLinkRange; + if (wasActive == NO) { + // we changed selection from non-link to a link + detectedLinkData = candidateLinkData; + detectedLinkRange = candidateLinkRange; + } else if (![_recentlyActiveLinkData + isEqualToLinkData:candidateLinkData] || + !NSEqualRanges(_recentlyActiveLinkRange, + candidateLinkRange)) { + // we changed selection from one link to the other or modified + // current link's text + detectedLinkData = candidateLinkData; + detectedLinkRange = candidateLinkRange; + } + } else if (wasActive && [self wasLinkPreviouslyDetected]) { + shouldClearLink = YES; } } // onMentionDetected event - if (isActive && [type intValue] == [MentionStyle getType]) { - // get mention data - MentionParams *candidateMentionParams; - NSRange candidateMentionRange = NSMakeRange(0, 0); - MentionStyle *mentionStyleClass = - (MentionStyle *)stylesDict[@([MentionStyle getType])]; - if (mentionStyleClass != nullptr) { - candidateMentionParams = [mentionStyleClass - getMentionParamsAt:textView.selectedRange.location]; - candidateMentionRange = [mentionStyleClass - getFullMentionRangeAt:textView.selectedRange.location]; - } + if ([type intValue] == [MentionStyle getType]) { + if (isActive) { + // get mention data + MentionParams *candidateMentionParams; + NSRange candidateMentionRange = NSMakeRange(0, 0); + MentionStyle *mentionStyleClass = + (MentionStyle *)stylesDict[@([MentionStyle getType])]; + if (mentionStyleClass != nullptr) { + candidateMentionParams = [mentionStyleClass + getMentionParamsAt:textView.selectedRange.location]; + candidateMentionRange = [mentionStyleClass + getFullMentionRangeAt:textView.selectedRange.location]; + } - if (wasActive == NO) { - // selection was changed from a non-mention to a mention - detectedMentionParams = candidateMentionParams; - detectedMentionRange = candidateMentionRange; - } else if (![_recentlyActiveMentionParams.text - isEqualToString:candidateMentionParams.text] || - ![_recentlyActiveMentionParams.attributes - isEqualToString:candidateMentionParams.attributes] || - !NSEqualRanges(_recentlyActiveMentionRange, - candidateMentionRange)) { - // selection changed from one mention to another - detectedMentionParams = candidateMentionParams; - detectedMentionRange = candidateMentionRange; + if (wasActive == NO) { + // selection was changed from a non-mention to a mention + detectedMentionParams = candidateMentionParams; + detectedMentionRange = candidateMentionRange; + } else if (![_recentlyActiveMentionParams.text + isEqualToString:candidateMentionParams.text] || + ![_recentlyActiveMentionParams.attributes + isEqualToString:candidateMentionParams.attributes] || + !NSEqualRanges(_recentlyActiveMentionRange, + candidateMentionRange)) { + // selection changed from one mention to another + detectedMentionParams = candidateMentionParams; + detectedMentionRange = candidateMentionRange; + } + } else if (wasActive && [self wasMentionPreviouslyDetected]) { + shouldClearMention = YES; } } } @@ -1111,6 +1122,11 @@ - (void)tryUpdatingActiveStyles { if (detectedLinkData != nullptr) { // emit onLinkeDetected event [self emitOnLinkDetectedEvent:detectedLinkData range:detectedLinkRange]; + } else if (shouldClearLink) { + LinkData *emptyLinkData = [[LinkData alloc] init]; + emptyLinkData.text = @""; + emptyLinkData.url = @""; + [self emitOnLinkDetectedEvent:emptyLinkData range:NSMakeRange(0, 0)]; } if (detectedMentionParams != nullptr) { @@ -1121,6 +1137,10 @@ - (void)tryUpdatingActiveStyles { _recentlyActiveMentionParams = detectedMentionParams; _recentlyActiveMentionRange = detectedMentionRange; + } else if (shouldClearMention) { + [self emitOnMentionDetectedEvent:@"" indicator:@"" attributes:@"{}"]; + _recentlyActiveMentionParams = nullptr; + _recentlyActiveMentionRange = NSMakeRange(0, 0); } // emit onChangeHtml event if needed [self tryEmittingOnChangeHtmlEvent]; @@ -1327,6 +1347,18 @@ - (void)emitOnSubmitEdittingEvent { } } +- (BOOL)wasLinkPreviouslyDetected { + return _recentlyActiveLinkData != nullptr && + (_recentlyActiveLinkData.text.length > 0 || + _recentlyActiveLinkData.url.length > 0); +} + +- (BOOL)wasMentionPreviouslyDetected { + return _recentlyActiveMentionParams != nullptr && + (_recentlyActiveMentionParams.text.length > 0 || + _recentlyActiveMentionParams.indicator.length > 0); +} + - (void)emitOnLinkDetectedEvent:(LinkData *)linkData range:(NSRange)range { auto emitter = [self getEventEmitter]; if (emitter != nullptr) { From d02eb5513a7e49d7f7bef6e5bf074261ceef3a83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Mon, 25 May 2026 15:12:23 +0200 Subject: [PATCH 2/4] fix(web): detection events --- .playwright/tests/mentions.spec.ts | 14 ++++---- .playwright/tests/testLinks.spec.ts | 4 +-- .../mentionPlugin/subscribeMentionEvents.ts | 32 +++++++++++++++---- src/web/useOnLinkDetected.ts | 5 ++- 4 files changed, 37 insertions(+), 18 deletions(-) diff --git a/.playwright/tests/mentions.spec.ts b/.playwright/tests/mentions.spec.ts index d8bc60dc7..5e6d4379d 100644 --- a/.playwright/tests/mentions.spec.ts +++ b/.playwright/tests/mentions.spec.ts @@ -230,7 +230,7 @@ test('moving within the same mention does not re-fire onMentionDetected', async await expect(detectedCount(page)).toHaveText('1'); }); -test('moving out of a mention does not increment detected count', async ({ +test('moving out of a mention fires clear onMentionDetected', async ({ page, }) => { await gotoMentionTest(page); @@ -244,13 +244,15 @@ test('moving out of a mention does not increment detected count', async ({ ) .toBe(true); await editor.click(); - await editor.press('Home'); - await editor.press('ArrowRight'); - await editor.press('ArrowRight'); - await editor.press('ArrowRight'); await editor.press('End'); - await editor.press('Enter'); + await editor.press('ArrowLeft'); // skip trailing space after mention + await editor.press('ArrowLeft'); // caret inside mention text await expect(detectedCount(page)).toHaveText('1'); + await editor.press('End'); + await editor.press('Enter'); + await expect(detectedCount(page)).toHaveText('2'); + await expect(detectedText(page)).toHaveText(''); + await expect(detectedIndicator(page)).toHaveText(''); }); test('mention renders correctly', async ({ page }) => { diff --git a/.playwright/tests/testLinks.spec.ts b/.playwright/tests/testLinks.spec.ts index dd6208f15..82bef7368 100644 --- a/.playwright/tests/testLinks.spec.ts +++ b/.playwright/tests/testLinks.spec.ts @@ -347,8 +347,8 @@ test.describe('test-links onLinkDetected', () => { .toEqual({ text: '', url: '', - start: 8, - end: 8, + start: 0, + end: 0, }); }); }); diff --git a/src/web/pmPlugins/mentionPlugin/subscribeMentionEvents.ts b/src/web/pmPlugins/mentionPlugin/subscribeMentionEvents.ts index 1633ce3a6..334febb91 100644 --- a/src/web/pmPlugins/mentionPlugin/subscribeMentionEvents.ts +++ b/src/web/pmPlugins/mentionPlugin/subscribeMentionEvents.ts @@ -10,6 +10,7 @@ export function subscribeMentionEvents( ): () => void { let prevTriggerState: TriggerState = { active: false }; let prevMentionKey: string | null = null; + let wasInMention = false; const handleTransaction = () => { const cb = callbacksRef.current; @@ -31,14 +32,28 @@ export function subscribeMentionEvents( } prevTriggerState = curr; - const mention = cb.onMentionDetected ? getActiveMention(editor) : null; + if (!cb.onMentionDetected) return; + + const mention = getActiveMention(editor); if (!mention) { - prevMentionKey = null; + if (wasInMention) { + wasInMention = false; + prevMentionKey = null; + cb.onMentionDetected({ + text: '', + indicator: '', + attributes: {}, + }); + } else { + prevMentionKey = null; + } return; } + + wasInMention = true; if (mention.key === prevMentionKey) return; prevMentionKey = mention.key; - cb.onMentionDetected?.({ + cb.onMentionDetected({ text: mention.text, indicator: mention.indicator, attributes: mention.attributes, @@ -67,15 +82,18 @@ function getActiveMention( ): (OnMentionDetected & { key: string }) | null { const { state } = editor; const mentionType = state.schema.marks.mention; - if (!mentionType || !state.selection.empty) return null; + if (!mentionType) return null; - const $pos = state.doc.resolve(state.selection.from); - const mark = mentionType.isInSet($pos.marks()); + const { from: selFrom, to: selTo } = state.selection; + const $from = state.doc.resolve(selFrom); + const mark = mentionType.isInSet($from.marks()); if (!mark) return null; - const range = getMarkRange($pos, mentionType); + const range = getMarkRange($from, mentionType); if (!range) return null; + if (selFrom < range.from || selTo > range.to) return null; + const { text, indicator, attributes } = mark.attrs; return { key: `${range.from}:${range.to}:${text}:${indicator}`, diff --git a/src/web/useOnLinkDetected.ts b/src/web/useOnLinkDetected.ts index c45907f51..daf1fb004 100644 --- a/src/web/useOnLinkDetected.ts +++ b/src/web/useOnLinkDetected.ts @@ -19,7 +19,6 @@ export const useOnLinkDetected = ( const linkType = state.schema.marks.link; if (!linkType) return; - const { from: selFrom, to: selTo } = state.selection; const $pos = state.selection.$from; const range = getMarkRange($pos, linkType); @@ -29,8 +28,8 @@ export const useOnLinkDetected = ( onLinkDetected({ text: '', url: '', - start: tiptapPosToNativePos(state.doc, selFrom), - end: tiptapPosToNativePos(state.doc, selTo), + start: 0, + end: 0, }); } lastEmittedRef.current = null; From 83137168168de099d0167f50bb96671975480d38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Thu, 28 May 2026 14:37:40 +0200 Subject: [PATCH 3/4] fix(ios): remove unnecessary code --- ios/EnrichedTextInputView.mm | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/ios/EnrichedTextInputView.mm b/ios/EnrichedTextInputView.mm index 781f876a2..a0d3d6eb2 100644 --- a/ios/EnrichedTextInputView.mm +++ b/ios/EnrichedTextInputView.mm @@ -1040,7 +1040,7 @@ - (void)tryUpdatingActiveStyles { detectedLinkData = candidateLinkData; detectedLinkRange = candidateLinkRange; } - } else if (wasActive && [self wasLinkPreviouslyDetected]) { + } else if (wasActive) { shouldClearLink = YES; } } @@ -1074,7 +1074,7 @@ - (void)tryUpdatingActiveStyles { detectedMentionParams = candidateMentionParams; detectedMentionRange = candidateMentionRange; } - } else if (wasActive && [self wasMentionPreviouslyDetected]) { + } else if (wasActive) { shouldClearMention = YES; } } @@ -1347,18 +1347,6 @@ - (void)emitOnSubmitEdittingEvent { } } -- (BOOL)wasLinkPreviouslyDetected { - return _recentlyActiveLinkData != nullptr && - (_recentlyActiveLinkData.text.length > 0 || - _recentlyActiveLinkData.url.length > 0); -} - -- (BOOL)wasMentionPreviouslyDetected { - return _recentlyActiveMentionParams != nullptr && - (_recentlyActiveMentionParams.text.length > 0 || - _recentlyActiveMentionParams.indicator.length > 0); -} - - (void)emitOnLinkDetectedEvent:(LinkData *)linkData range:(NSRange)range { auto emitter = [self getEventEmitter]; if (emitter != nullptr) { From 1073ea87919aa36c381864363bd382a5fb4a2409 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Thu, 28 May 2026 15:16:27 +0200 Subject: [PATCH 4/4] fix: simplify android on link detection --- .../textinput/utils/EnrichedSelection.kt | 138 +++++------------- 1 file changed, 35 insertions(+), 103 deletions(-) diff --git a/android/src/main/java/com/swmansion/enriched/textinput/utils/EnrichedSelection.kt b/android/src/main/java/com/swmansion/enriched/textinput/utils/EnrichedSelection.kt index 54216fd39..bce77da1c 100644 --- a/android/src/main/java/com/swmansion/enriched/textinput/utils/EnrichedSelection.kt +++ b/android/src/main/java/com/swmansion/enriched/textinput/utils/EnrichedSelection.kt @@ -190,99 +190,42 @@ class EnrichedSelection( private fun getParametrizedStyleStart(type: Class): Int? { val (start, end) = getInlineSelection() val spannable = view.text as Spannable + val spans = spannable.getSpans(start, end, type) val isLinkType = type == EnrichedInputLinkSpan::class.java val isMentionType = type == EnrichedInputMentionSpan::class.java - if (isMentionType) { - val activeMention = findActiveMentionSpan(spannable, start, end) - if (activeMention != null) { - val (span, spanStart, spanEnd) = activeMention - emitMentionDetectedEvent(span, spanStart, spanEnd) - return spanStart - } - if (wasMentionPreviouslyDetected()) { - emitMentionClearedEvent() - } - return null - } - - if (isLinkType) { - val activeLink = findActiveLinkSpan(spannable, start, end) - if (activeLink != null) { - val (span, spanStart, spanEnd) = activeLink - emitLinkDetectedEvent(span, spanStart, spanEnd) - return spanStart - } + if (isLinkType && spans.isEmpty()) { if (wasLinkPreviouslyDetected()) { - emitLinkClearedEvent() + emitLinkDetectedEvent(spannable, null, 0, 0) } return null } - val spans = spannable.getSpans(start, end, type) - - for (span in spans) { - val spanStart = spannable.getSpanStart(span) - val spanEnd = spannable.getSpanEnd(span) - - if (start >= spanStart && end <= spanEnd) { - return spanStart + if (isMentionType && spans.isEmpty()) { + if (wasMentionPreviouslyDetected()) { + emitMentionDetectedEvent(spannable, null, start, end) } + return null } - return null - } - - private fun findActiveLinkSpan( - spannable: Spannable, - start: Int, - end: Int, - ): Triple? { - val spans = spannable.getSpans(start, end, EnrichedInputLinkSpan::class.java) - for (span in spans) { val spanStart = spannable.getSpanStart(span) val spanEnd = spannable.getSpanEnd(span) if (start >= spanStart && end <= spanEnd) { - return Triple(span, spanStart, spanEnd) - } - } - - return null - } - - private fun findActiveMentionSpan( - spannable: Spannable, - start: Int, - end: Int, - ): Triple? { - val spans = spannable.getSpans(start, end, EnrichedInputMentionSpan::class.java) - - for (span in spans) { - val spanStart = spannable.getSpanStart(span) - val spanEnd = spannable.getSpanEnd(span) + if (isLinkType && span is EnrichedInputLinkSpan) { + emitLinkDetectedEvent(spannable, span, spanStart, spanEnd) + } else if (isMentionType && span is EnrichedInputMentionSpan) { + emitMentionDetectedEvent(spannable, span, spanStart, spanEnd) + } - if (start >= spanStart && end <= spanEnd) { - return Triple(span, spanStart, spanEnd) + return spanStart } } return null } - private fun wasMentionPreviouslyDetected(): Boolean { - val previousText = previousMentionDetectedEvent["text"] ?: "" - val previousIndicator = previousMentionDetectedEvent["indicator"] ?: "" - return previousText.isNotEmpty() || previousIndicator.isNotEmpty() - } - - private fun wasLinkPreviouslyDetected(): Boolean { - val previousText = previousLinkDetectedEvent["text"] ?: "" - val previousUrl = previousLinkDetectedEvent["url"] ?: "" - return previousText.isNotEmpty() || previousUrl.isNotEmpty() - } - private fun emitSelectionChangeEvent( editable: Editable?, start: Int, @@ -309,28 +252,28 @@ class EnrichedSelection( ) } - private fun emitLinkDetectedEvent( - span: EnrichedInputLinkSpan, - spanStart: Int, - spanEnd: Int, - ) { - val spannable = view.text as Spannable - val text = spannable.substring(spanStart, spanEnd).replace(EnrichedConstants.ZWS_STRING, "") - dispatchLinkDetectedEvent(text, span.getUrl(), spanStart, spanEnd, spannable) + private fun wasMentionPreviouslyDetected(): Boolean { + val previousText = previousMentionDetectedEvent["text"] ?: "" + val previousIndicator = previousMentionDetectedEvent["indicator"] ?: "" + return previousText.isNotEmpty() || previousIndicator.isNotEmpty() } - private fun emitLinkClearedEvent() { - val spannable = view.text as Spannable - dispatchLinkDetectedEvent("", "", 0, 0, spannable) + private fun wasLinkPreviouslyDetected(): Boolean { + val previousText = previousLinkDetectedEvent["text"] ?: "" + val previousUrl = previousLinkDetectedEvent["url"] ?: "" + return previousText.isNotEmpty() || previousUrl.isNotEmpty() } - private fun dispatchLinkDetectedEvent( - text: String, - url: String, + private fun emitLinkDetectedEvent( + spannable: Spannable, + span: EnrichedInputLinkSpan?, start: Int, end: Int, - spannable: Spannable, ) { + val text = spannable.substring(start, end).replace(EnrichedConstants.ZWS_STRING, "") + val url = span?.getUrl() ?: "" + + // Prevents emitting unnecessary events if (text == previousLinkDetectedEvent["text"] && url == previousLinkDetectedEvent["url"]) return previousLinkDetectedEvent.put("text", text) @@ -356,27 +299,16 @@ class EnrichedSelection( } private fun emitMentionDetectedEvent( - span: EnrichedInputMentionSpan, - spanStart: Int, - spanEnd: Int, + spannable: Spannable, + span: EnrichedInputMentionSpan?, + start: Int, + end: Int, ) { - val spannable = view.text as Spannable - val text = spannable.substring(spanStart, spanEnd) - val attributes = span.getAttributes() - val indicator = span.getIndicator() + val text = spannable.substring(start, end) + val attributes = span?.getAttributes() ?: emptyMap() + val indicator = span?.getIndicator() ?: "" val payload = JSONObject(attributes).toString() - dispatchMentionDetectedEvent(text, indicator, payload) - } - - private fun emitMentionClearedEvent() { - dispatchMentionDetectedEvent("", "", "{}") - } - private fun dispatchMentionDetectedEvent( - text: String, - indicator: String, - payload: String, - ) { val previousText = previousMentionDetectedEvent["text"] ?: "" val previousPayload = previousMentionDetectedEvent["payload"] ?: "" val previousIndicator = previousMentionDetectedEvent["indicator"] ?: ""