From d114576eee1bcec632433a4a2b89e3f68ad21a68 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Sun, 8 Nov 2026 23:39:51 +0000 Subject: [PATCH 1/5] fix(mobile): Android chip taps open the chip from either half A chip is one replacement span, so a tap on its right half reported the offset at its end and moved the cursor instead of opening the chip. The tap now also checks the chip's horizontal bounds. --- .../t3composereditor/T3ComposerEditorView.kt | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/apps/mobile/modules/t3-composer-editor/android/src/main/java/expo/modules/t3composereditor/T3ComposerEditorView.kt b/apps/mobile/modules/t3-composer-editor/android/src/main/java/expo/modules/t3composereditor/T3ComposerEditorView.kt index 4d2a5e3a744f6..a132e19c3e86b 100644 --- a/apps/mobile/modules/t3-composer-editor/android/src/main/java/expo/modules/t3composereditor/T3ComposerEditorView.kt +++ b/apps/mobile/modules/t3-composer-editor/android/src/main/java/expo/modules/t3composereditor/T3ComposerEditorView.kt @@ -132,7 +132,7 @@ class T3ComposerEditorView(context: Context, appContext: AppContext) : ExpoView( tokens.firstOrNull { (it.type == "context" || it.type == "mention" || it.type == "skill") && offset >= it.start && - offset < it.end + (offset < it.end || (offset == it.end && chipContainsX(it, event.x))) } ?: return false if (token.end <= editor.length() && @@ -450,6 +450,21 @@ class T3ComposerEditorView(context: Context, appContext: AppContext) : ExpoView( ) } + // A chip is one replacement span, so a tap on its right half reports the offset at its end. + private fun chipContainsX(token: ComposerToken, viewX: Float): Boolean { + val layout = editor.layout ?: return false + val x = viewX - editor.totalPaddingLeft + editor.scrollX + val line = layout.getLineForOffset(token.start) + val left = layout.getPrimaryHorizontal(token.start) + val right = + if (layout.getLineForOffset(token.end) == line) { + layout.getPrimaryHorizontal(token.end) + } else { + layout.getLineRight(line) + } + return x >= minOf(left, right) && x <= maxOf(left, right) + } + private fun applyTokenSpans() { val editable = editor.text ?: return editable.getSpans( From faca193e817ac7cead35806078a02d8d6a049224 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Sun, 8 Nov 2026 23:52:21 +0000 Subject: [PATCH 2/5] fix(mobile): Android chip end taps must land on the chip's line --- .../expo/modules/t3composereditor/T3ComposerEditorView.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/mobile/modules/t3-composer-editor/android/src/main/java/expo/modules/t3composereditor/T3ComposerEditorView.kt b/apps/mobile/modules/t3-composer-editor/android/src/main/java/expo/modules/t3composereditor/T3ComposerEditorView.kt index a132e19c3e86b..8d4493aa6e738 100644 --- a/apps/mobile/modules/t3-composer-editor/android/src/main/java/expo/modules/t3composereditor/T3ComposerEditorView.kt +++ b/apps/mobile/modules/t3-composer-editor/android/src/main/java/expo/modules/t3composereditor/T3ComposerEditorView.kt @@ -132,7 +132,7 @@ class T3ComposerEditorView(context: Context, appContext: AppContext) : ExpoView( tokens.firstOrNull { (it.type == "context" || it.type == "mention" || it.type == "skill") && offset >= it.start && - (offset < it.end || (offset == it.end && chipContainsX(it, event.x))) + (offset < it.end || (offset == it.end && chipContains(it, event.x, event.y))) } ?: return false if (token.end <= editor.length() && @@ -451,10 +451,13 @@ class T3ComposerEditorView(context: Context, appContext: AppContext) : ExpoView( } // A chip is one replacement span, so a tap on its right half reports the offset at its end. - private fun chipContainsX(token: ComposerToken, viewX: Float): Boolean { + // Taps below the last line clamp to it, so the drawn line's bounds are checked too. + private fun chipContains(token: ComposerToken, viewX: Float, viewY: Float): Boolean { val layout = editor.layout ?: return false val x = viewX - editor.totalPaddingLeft + editor.scrollX + val y = viewY - editor.totalPaddingTop + editor.scrollY val line = layout.getLineForOffset(token.start) + if (y < layout.getLineTop(line) || y > layout.getLineBottom(line)) return false val left = layout.getPrimaryHorizontal(token.start) val right = if (layout.getLineForOffset(token.end) == line) { From 271e95de7ab2da0701a2aeca01fa3d3d6ef431c5 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Sun, 8 Nov 2026 23:55:10 +0000 Subject: [PATCH 3/5] fix(mobile): keep the chip hit test within detekt's return limit --- .../expo/modules/t3composereditor/T3ComposerEditorView.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/mobile/modules/t3-composer-editor/android/src/main/java/expo/modules/t3composereditor/T3ComposerEditorView.kt b/apps/mobile/modules/t3-composer-editor/android/src/main/java/expo/modules/t3composereditor/T3ComposerEditorView.kt index 8d4493aa6e738..98ef464c56fdd 100644 --- a/apps/mobile/modules/t3-composer-editor/android/src/main/java/expo/modules/t3composereditor/T3ComposerEditorView.kt +++ b/apps/mobile/modules/t3-composer-editor/android/src/main/java/expo/modules/t3composereditor/T3ComposerEditorView.kt @@ -457,7 +457,6 @@ class T3ComposerEditorView(context: Context, appContext: AppContext) : ExpoView( val x = viewX - editor.totalPaddingLeft + editor.scrollX val y = viewY - editor.totalPaddingTop + editor.scrollY val line = layout.getLineForOffset(token.start) - if (y < layout.getLineTop(line) || y > layout.getLineBottom(line)) return false val left = layout.getPrimaryHorizontal(token.start) val right = if (layout.getLineForOffset(token.end) == line) { @@ -465,7 +464,8 @@ class T3ComposerEditorView(context: Context, appContext: AppContext) : ExpoView( } else { layout.getLineRight(line) } - return x >= minOf(left, right) && x <= maxOf(left, right) + return y >= layout.getLineTop(line) && y <= layout.getLineBottom(line) && + x >= minOf(left, right) && x <= maxOf(left, right) } private fun applyTokenSpans() { From c81e5f78ba7318d2e4a7dd5d0a91af34f94326f0 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Mon, 9 Nov 2026 00:52:13 +0000 Subject: [PATCH 4/5] fix(mobile): every Android chip tap must land inside the chip --- .../expo/modules/t3composereditor/T3ComposerEditorView.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/mobile/modules/t3-composer-editor/android/src/main/java/expo/modules/t3composereditor/T3ComposerEditorView.kt b/apps/mobile/modules/t3-composer-editor/android/src/main/java/expo/modules/t3composereditor/T3ComposerEditorView.kt index 98ef464c56fdd..ae1228fc8990c 100644 --- a/apps/mobile/modules/t3-composer-editor/android/src/main/java/expo/modules/t3composereditor/T3ComposerEditorView.kt +++ b/apps/mobile/modules/t3-composer-editor/android/src/main/java/expo/modules/t3composereditor/T3ComposerEditorView.kt @@ -131,8 +131,8 @@ class T3ComposerEditorView(context: Context, appContext: AppContext) : ExpoView( val token = tokens.firstOrNull { (it.type == "context" || it.type == "mention" || it.type == "skill") && - offset >= it.start && - (offset < it.end || (offset == it.end && chipContains(it, event.x, event.y))) + offset in it.start..it.end && + chipContains(it, event.x, event.y) } ?: return false if (token.end <= editor.length() && @@ -450,8 +450,8 @@ class T3ComposerEditorView(context: Context, appContext: AppContext) : ExpoView( ) } - // A chip is one replacement span, so a tap on its right half reports the offset at its end. - // Taps below the last line clamp to it, so the drawn line's bounds are checked too. + // A chip is one replacement span, so a tap on its right half reports the offset at its end, + // and a tap below the last line clamps to it; only the chip's drawn bounds count. private fun chipContains(token: ComposerToken, viewX: Float, viewY: Float): Boolean { val layout = editor.layout ?: return false val x = viewX - editor.totalPaddingLeft + editor.scrollX From 88191209924f5589ed13b8516b2fb83dde76a86c Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Mon, 9 Nov 2026 00:56:48 +0000 Subject: [PATCH 5/5] fix(mobile): skip stale chip tokens before measuring their bounds --- .../java/expo/modules/t3composereditor/T3ComposerEditorView.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/mobile/modules/t3-composer-editor/android/src/main/java/expo/modules/t3composereditor/T3ComposerEditorView.kt b/apps/mobile/modules/t3-composer-editor/android/src/main/java/expo/modules/t3composereditor/T3ComposerEditorView.kt index ae1228fc8990c..28b28306f2f6f 100644 --- a/apps/mobile/modules/t3-composer-editor/android/src/main/java/expo/modules/t3composereditor/T3ComposerEditorView.kt +++ b/apps/mobile/modules/t3-composer-editor/android/src/main/java/expo/modules/t3composereditor/T3ComposerEditorView.kt @@ -132,6 +132,7 @@ class T3ComposerEditorView(context: Context, appContext: AppContext) : ExpoView( tokens.firstOrNull { (it.type == "context" || it.type == "mention" || it.type == "skill") && offset in it.start..it.end && + it.end <= editor.length() && chipContains(it, event.x, event.y) } ?: return false