authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-10 10:35:34-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-10 10:35:34-07:00
log3836890e4484406813997259efc69065b25ce698
tree6056ee76648e1cb353a46a1cfd0230f63e45325c
parent444fd8badd265ddde33babf2a21bf3f527334e98
signature Signed by PGP key B5690EEEBB952194

Revert "fix(mobile): keep Android markdown icons aligned with text" (#11098)


9 files changed, 22 insertions(+), 120 deletions(-)

apps/mobile/modules/t3-markdown-text/android/build.gradle-12
...@@ -8,10 +8,6 @@ android {...@@ -8,10 +8,6 @@ android {
8 namespace 'expo.modules.t3markdowntext'8 namespace 'expo.modules.t3markdowntext'
9 compileSdk rootProject.ext.compileSdkVersion9 compileSdk rootProject.ext.compileSdkVersion
1010
11 testOptions {
12 unitTests.includeAndroidResources = true
13 }
14
15 defaultConfig {11 defaultConfig {
16 minSdkVersion rootProject.ext.minSdkVersion12 minSdkVersion rootProject.ext.minSdkVersion
17 targetSdkVersion rootProject.ext.targetSdkVersion13 targetSdkVersion rootProject.ext.targetSdkVersion
...@@ -21,12 +17,4 @@ android {...@@ -21,12 +17,4 @@ android {
21dependencies {17dependencies {
22 implementation project(':expo-modules-core')18 implementation project(':expo-modules-core')
23 implementation 'com.facebook.react:react-android'19 implementation 'com.facebook.react:react-android'
24 testImplementation 'junit:junit:4.13.2'
25 testImplementation 'org.robolectric:robolectric:4.16.1'
26}
27
28tasks.withType(Test).configureEach {
29 javaLauncher = javaToolchains.launcherFor {
30 languageVersion = JavaLanguageVersion.of(21)
31 }
32}20}
apps/mobile/modules/t3-markdown-text/android/src/main/java/expo/modules/t3markdowntext/T3MarkdownTextSelectionModule.kt+5-9
...@@ -18,23 +18,19 @@ import kotlin.math.min...@@ -18,23 +18,19 @@ import kotlin.math.min
1818
19private const val OBJECT_REPLACEMENT_CHARACTER = "\uFFFC"19private const val OBJECT_REPLACEMENT_CHARACTER = "\uFFFC"
2020
21internal fun copyTextWithoutInlineImages(21private fun copyTextWithoutInlineImages(
22 text: CharSequence,22 text: CharSequence,
23 start: Int,23 start: Int,
24 end: Int24 end: Int
25): String {25): String {
26 if (text !is Spanned) return text.subSequence(start, end).toString()26 if (text !is Spanned) return text.subSequence(start, end).toString()
2727
28 fun isInlineImage(index: Int): Boolean =
29 index >= 0 && text[index].toString() == OBJECT_REPLACEMENT_CHARACTER &&
30 text.getSpans(index, index + 1, ReplacementSpan::class.java).isNotEmpty()
31
32 return buildString {28 return buildString {
33 for (index in start until end) {29 for (index in start until end) {
34 // The renderer inserts one NBSP after each image to keep its label on the same line.30 val isInlineImage =
35 // Inspect the original text even when selection starts after the image.31 text[index].toString() == OBJECT_REPLACEMENT_CHARACTER &&
36 val isIconSpacer = text[index] == '\u00A0' && isInlineImage(index - 1)32 text.getSpans(index, index + 1, ReplacementSpan::class.java).isNotEmpty()
37 if (!isInlineImage(index) && !isIconSpacer) append(text[index])33 if (!isInlineImage) append(text[index])
38 }34 }
39 }35 }
40}36}
apps/mobile/modules/t3-markdown-text/android/src/test/java/expo/modules/t3markdowntext/MarkdownSelectionCopyTest.kt deleted-48
...@@ -1,48 +0,0 @@
1package expo.modules.t3markdowntext
2
3import android.graphics.drawable.ColorDrawable
4import android.text.SpannableString
5import android.text.Spanned
6import android.text.style.ImageSpan
7import org.junit.Assert.assertEquals
8import org.junit.Test
9import org.junit.runner.RunWith
10import org.robolectric.RobolectricTestRunner
11import org.robolectric.annotation.Config
12
13@RunWith(RobolectricTestRunner::class)
14@Config(sdk = [36], manifest = Config.NONE)
15class MarkdownSelectionCopyTest {
16 private fun withIcon(value: String): SpannableString = SpannableString(value).apply {
17 val index = value.indexOf('\uFFFC')
18 setSpan(ImageSpan(ColorDrawable()), index, index + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
19 }
20
21 @Test
22 fun removesIconAndInjectedSpacer() {
23 val text = withIcon("\uFFFC\u00A0main.go:12 starts the server.")
24 assertEquals("main.go:12 starts the server.", copyTextWithoutInlineImages(text, 0, text.length))
25 }
26
27 @Test
28 fun removesSpacerWhenSelectionStartsAfterIcon() {
29 val text = withIcon("\uFFFC\u00A0main.go:12 starts the server.")
30 assertEquals("main.go:12", copyTextWithoutInlineImages(text, 1, 12))
31 }
32
33 @Test
34 fun preservesAuthoredWhitespaceAndLiteralObjectCharacters() {
35 val text = withIcon("before\u00A0 \uFFFC\u00A0\u00A0 main.go after\u00A0\uFFFC\u00A0")
36 assertEquals(
37 "before\u00A0 \u00A0 main.go after\u00A0\uFFFC\u00A0",
38 copyTextWithoutInlineImages(text, 0, text.length)
39 )
40 }
41
42 @Test
43 fun preservesTextWithoutImageSpans() {
44 val text = "\uFFFC\u00A0main.go"
45 assertEquals(text, copyTextWithoutInlineImages(text, 0, text.length))
46 assertEquals(text, copyTextWithoutInlineImages(SpannableString(text), 0, text.length))
47 }
48}
apps/mobile/modules/t3-markdown-text/src/NativeMarkdownBlock.ios.tsx+10-25
...@@ -4,11 +4,7 @@ import type { MarkdownNode } from "react-native-nitro-markdown/headless";...@@ -4,11 +4,7 @@ import type { MarkdownNode } from "react-native-nitro-markdown/headless";
44
5import { CopyTextButton } from "./CopyTextButton";5import { CopyTextButton } from "./CopyTextButton";
6import { MarkdownTextPrimitive } from "./MarkdownTextPrimitive";6import { MarkdownTextPrimitive } from "./MarkdownTextPrimitive";
7import {7import { nativeMarkdownDocumentRuns, nativeMarkdownListItemBlocks } from "./nativeMarkdownText";
8 nativeMarkdownBlockSpacing,
9 nativeMarkdownDocumentRuns,
10 nativeMarkdownListItemBlocks,
11} from "./nativeMarkdownText";
12import { NativeMarkdownSelectableText } from "./NativeMarkdownSelectableText.ios";8import { NativeMarkdownSelectableText } from "./NativeMarkdownSelectableText.ios";
13import type {9import type {
14 MarkdownCodeHighlighter,10 MarkdownCodeHighlighter,
...@@ -599,28 +595,17 @@ export function NativeMarkdownBlock(props: {...@@ -599,28 +595,17 @@ export function NativeMarkdownBlock(props: {
599 switch (props.node.type) {595 switch (props.node.type) {
600 case "document":596 case "document":
601 return (597 return (
602 <View>598 <View style={{ gap: 8 }}>
603 {(props.node.children ?? []).map((child, index) => (599 {(props.node.children ?? []).map((child, index) => (
604 <View600 <NativeMarkdownBlock
605 key={nodeKey(child, index)}601 key={nodeKey(child, index)}
606 style={{602 node={child}
607 paddingTop:603 skills={props.skills}
608 Platform.OS === "android"604 textStyle={props.textStyle}
609 ? nativeMarkdownBlockSpacing(props.node.children?.[index - 1], child)605 highlightCode={props.highlightCode}
610 : index > 0606 onLinkPress={props.onLinkPress}
611 ? 8607 depth={depth}
612 : 0,608 />
613 }}
614 >
615 <NativeMarkdownBlock
616 node={child}
617 skills={props.skills}
618 textStyle={props.textStyle}
619 highlightCode={props.highlightCode}
620 onLinkPress={props.onLinkPress}
621 depth={depth}
622 />
623 </View>
624 ))}609 ))}
625 </View>610 </View>
626 );611 );
apps/mobile/modules/t3-markdown-text/src/NativeMarkdownSelectableText.ios.tsx-4
...@@ -222,10 +222,6 @@ export function NativeMarkdownSelectableText(props: {...@@ -222,10 +222,6 @@ export function NativeMarkdownSelectableText(props: {
222 }222 }
223 }223 }
224224
225 if (Platform.OS === "android" && (run.fileIcon || linkIcon)) {
226 text = `\u00A0${text}`;
227 }
228
229 return { key: `${signature}:${occurrence}`, run, text, linkIcon };225 return { key: `${signature}:${occurrence}`, run, text, linkIcon };
230 });226 });
231 // T3MarkdownText only rebuilds its attributed string during native layout. A227 // T3MarkdownText only rebuilds its attributed string during native layout. A
apps/mobile/modules/t3-markdown-text/src/SelectableMarkdownText.ios.tsx+2-4
...@@ -1,5 +1,5 @@...@@ -1,5 +1,5 @@
1import { useMemo } from "react";1import { useMemo } from "react";
2import { Platform, View } from "react-native";2import { View } from "react-native";
3import { parseMarkdownWithOptions } from "react-native-nitro-markdown/headless";3import { parseMarkdownWithOptions } from "react-native-nitro-markdown/headless";
44
5import {5import {
...@@ -84,10 +84,8 @@ export function SelectableMarkdownText({...@@ -84,10 +84,8 @@ export function SelectableMarkdownText({
84 the resulting single-line width instead of reflowing it. */}84 the resulting single-line width instead of reflowing it. */}
85 <View style={{ flexShrink: 1, minWidth: 0, marginTop, marginBottom }}>85 <View style={{ flexShrink: 1, minWidth: 0, marginTop, marginBottom }}>
86 {chunks.map((chunk, index) => {86 {chunks.map((chunk, index) => {
87 // Android inline images drift when one Text mixes paragraph and list
88 // spacer line heights. Keep those layouts in separate native blocks.
89 const content =87 const content =
90 chunk.kind === "rich" || Platform.OS === "android" ? (88 chunk.kind === "rich" ? (
91 <NativeMarkdownBlock89 <NativeMarkdownBlock
92 node={chunk.node}90 node={chunk.node}
93 skills={skills}91 skills={skills}
apps/mobile/modules/t3-markdown-text/src/nativeMarkdownText.ts+5-9
...@@ -589,14 +589,6 @@ function appendTable(...@@ -589,14 +589,6 @@ function appendTable(
589 return runs;589 return runs;
590}590}
591591
592export function nativeMarkdownBlockSpacing(
593 previous: MarkdownNode | undefined,
594 current: MarkdownNode,
595): number {
596 if (!previous) return 0;
597 return current.type === "heading" ? 20 : previous.type === "heading" ? 10 : 12;
598}
599
600function appendDocumentBlock(592function appendDocumentBlock(
601 runs: NativeMarkdownTextRun[],593 runs: NativeMarkdownTextRun[],
602 node: MarkdownNode,594 node: MarkdownNode,
...@@ -607,7 +599,11 @@ function appendDocumentBlock(...@@ -607,7 +599,11 @@ function appendDocumentBlock(
607 const children = node.children ?? [];599 const children = node.children ?? [];
608 for (const [index, child] of children.entries()) {600 for (const [index, child] of children.entries()) {
609 if (index > 0) {601 if (index > 0) {
610 appendSpacer(runs, nativeMarkdownBlockSpacing(children[index - 1], child));602 const previous = children[index - 1];
603 appendSpacer(
604 runs,
605 child.type === "heading" ? 20 : previous?.type === "heading" ? 10 : 12,
606 );
611 }607 }
612 appendDocumentBlock(runs, child, depth);608 appendDocumentBlock(runs, child, depth);
613 }609 }
apps/mobile/src/lib/nativeMarkdownText.test.ts-6
...@@ -2,7 +2,6 @@ import { describe, expect, it } from "vite-plus/test";...@@ -2,7 +2,6 @@ import { describe, expect, it } from "vite-plus/test";
2import type { MarkdownNode } from "react-native-nitro-markdown/headless";2import type { MarkdownNode } from "react-native-nitro-markdown/headless";
33
4import {4import {
5 nativeMarkdownBlockSpacing,
6 nativeMarkdownChunkSpacing,5 nativeMarkdownChunkSpacing,
7 nativeMarkdownDocumentChunks,6 nativeMarkdownDocumentChunks,
8 nativeMarkdownDocumentRuns,7 nativeMarkdownDocumentRuns,
...@@ -382,11 +381,6 @@ describe("nativeMarkdownDocumentRuns", () => {...@@ -382,11 +381,6 @@ describe("nativeMarkdownDocumentRuns", () => {
382 .filter((run) => run.role === "spacer")381 .filter((run) => run.role === "spacer")
383 .map((run) => run.spacing),382 .map((run) => run.spacing),
384 ).toEqual([20, 10, 12]);383 ).toEqual([20, 10, 12]);
385 expect(
386 node.children?.map((child, index) =>
387 nativeMarkdownBlockSpacing(node.children?.[index - 1], child),
388 ),
389 ).toEqual([0, 20, 10, 12]);
390 });384 });
391385
392 it("renders tight list items whose inline nodes are direct children", () => {386 it("renders tight list items whose inline nodes are direct children", () => {
docs/user/composer.md-3
...@@ -6,9 +6,6 @@ include a skill when the task needs more context....@@ -6,9 +6,6 @@ include a skill when the task needs more context.
6Messages can contain up to 120,000 characters. Longer drafts stay in the composer6Messages can contain up to 120,000 characters. Longer drafts stay in the composer
7so you can shorten them or split them into several messages.7so you can shorten them or split them into several messages.
88
9On Android, long-press message text to select within a paragraph or list item.
10Use the message’s copy button to copy the whole message.
11
12## Attach files9## Attach files
1310
14Attach up to eight files per message. Images can be up to 10 MB; other files can11Attach up to eight files per message. Images can be up to 10 MB; other files can