History

feat(mobile): make chat text selectable on Android (#8779)

Dara Adedeji committed 2 months ago

Commit 09d13de1 parent 5f878d210 files · +246 −23Browse files

Unified · Split

10 files
  1. apps/mobile/modules/t3-markdown-text/android/build.gradle +20 −0
  2. apps/mobile/modules/t3-markdown-text/android/src/main/java/expo/modules/t3markdowntext/T3MarkdownTextSelectionModule.kt +93 −0
  3. apps/mobile/modules/t3-markdown-text/expo-module.config.json +6 −0
  4. apps/mobile/modules/t3-markdown-text/package.json +3 −0
  5. apps/mobile/modules/t3-markdown-text/src/MarkdownTextPrimitive.tsx +12 −5
  6. apps/mobile/modules/t3-markdown-text/src/NativeMarkdownBlock.ios.tsx +17 −6
  7. apps/mobile/modules/t3-markdown-text/src/NativeMarkdownSelectableText.ios.tsx +55 −10
  8. apps/mobile/modules/t3-markdown-text/src/T3MarkdownTextSelectionModule.ts +12 −0
  9. apps/mobile/src/native/SelectableMarkdownText.android.tsx +24 −0
  10. pnpm-lock.yaml +4 −2
apps/mobile/modules/t3-markdown-text/android/build.gradle+20−0
@@ -0,0 +1,20 @@
1apply plugin: 'com.android.library'
2apply plugin: 'org.jetbrains.kotlin.android'
3
4group = 'com.t3tools.markdowntext'
5version = '0.0.0'
6
7android {
8 namespace 'expo.modules.t3markdowntext'
9 compileSdk rootProject.ext.compileSdkVersion
10
11 defaultConfig {
12 minSdkVersion rootProject.ext.minSdkVersion
13 targetSdkVersion rootProject.ext.targetSdkVersion
14 }
15}
16
17dependencies {
18 implementation project(':expo-modules-core')
19 implementation 'com.facebook.react:react-android'
20}
apps/mobile/modules/t3-markdown-text/android/src/main/java/expo/modules/t3markdowntext/T3MarkdownTextSelectionModule.kt+93−0
@@ -0,0 +1,93 @@
1package expo.modules.t3markdowntext
2
3import android.content.ClipData
4import android.content.ClipboardManager
5import android.content.Context
6import android.text.Spanned
7import android.text.style.ReplacementSpan
8import android.view.ActionMode
9import android.view.Menu
10import android.view.MenuItem
11import android.widget.TextView
12import com.facebook.react.bridge.ReactContext
13import com.facebook.react.uimanager.UIManagerHelper
14import expo.modules.kotlin.modules.Module
15import expo.modules.kotlin.modules.ModuleDefinition
16import kotlin.math.max
17import kotlin.math.min
18
19private const val OBJECT_REPLACEMENT_CHARACTER = "\uFFFC"
20
21private fun copyTextWithoutInlineImages(
22 text: CharSequence,
23 start: Int,
24 end: Int
25): String {
26 if (text !is Spanned) return text.subSequence(start, end).toString()
27
28 return buildString {
29 for (index in start until end) {
30 val isInlineImage =
31 text[index].toString() == OBJECT_REPLACEMENT_CHARACTER &&
32 text.getSpans(index, index + 1, ReplacementSpan::class.java).isNotEmpty()
33 if (!isInlineImage) append(text[index])
34 }
35 }
36}
37
38private class SanitizingSelectionActionModeCallback(
39 private val textView: TextView,
40 private val delegate: ActionMode.Callback?
41) : ActionMode.Callback {
42 override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean =
43 delegate?.onCreateActionMode(mode, menu) ?: true
44
45 override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean =
46 delegate?.onPrepareActionMode(mode, menu) ?: false
47
48 override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
49 if (item.itemId == android.R.id.copy) {
50 val start = min(textView.selectionStart, textView.selectionEnd)
51 val end = max(textView.selectionStart, textView.selectionEnd)
52 if (start >= 0 && end > start) {
53 val originalText = textView.text.subSequence(start, end).toString()
54 val selectedText = copyTextWithoutInlineImages(textView.text, start, end)
55 if (selectedText != originalText) {
56 val clipboard =
57 textView.context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
58 clipboard.setPrimaryClip(ClipData.newPlainText(null, selectedText))
59 mode.finish()
60 return true
61 }
62 }
63 }
64 return delegate?.onActionItemClicked(mode, item) ?: false
65 }
66
67 override fun onDestroyActionMode(mode: ActionMode) {
68 delegate?.onDestroyActionMode(mode)
69 }
70}
71
72class T3MarkdownTextSelectionModule : Module() {
73 override fun definition() = ModuleDefinition {
74 Name("T3MarkdownTextSelection")
75
76 Function("installCopySanitizer") { reactTag: Int ->
77 val reactContext = appContext.reactContext as? ReactContext ?: return@Function
78 reactContext.runOnUiQueueThread {
79 val textView =
80 runCatching {
81 UIManagerHelper.getUIManagerForReactTag(reactContext, reactTag)?.resolveView(reactTag)
82 }
83 .getOrNull() as? TextView ?: return@runOnUiQueueThread
84 val currentCallback = textView.customSelectionActionModeCallback
85 if (currentCallback is SanitizingSelectionActionModeCallback) {
86 return@runOnUiQueueThread
87 }
88 textView.customSelectionActionModeCallback =
89 SanitizingSelectionActionModeCallback(textView, currentCallback)
90 }
91 }
92 }
93}
apps/mobile/modules/t3-markdown-text/expo-module.config.json+6−0
@@ -0,0 +1,6 @@
1{
2 "platforms": ["android"],
3 "android": {
4 "modules": ["expo.modules.t3markdowntext.T3MarkdownTextSelectionModule"]
5 }
6}
apps/mobile/modules/t3-markdown-text/package.json+3−0
@@ -4,7 +4,9 @@ · 3 unmodified lines
4 "private": true,4 "private": true,
5 "source": "./index.ts",5 "source": "./index.ts",
6 "files": [6 "files": [
7 "android",
7 "assets",8 "assets",
9 "expo-module.config.json",
8 "ios",10 "ios",
9 "src",11 "src",
10 "index.ts",12 "index.ts",
@@ -28,6 +30,7 @@ · 17 unmodified lines
28 "peerDependencies": {30 "peerDependencies": {
29 "@t3tools/client-runtime": "*",31 "@t3tools/client-runtime": "*",
30 "@t3tools/shared": "*",32 "@t3tools/shared": "*",
33 "expo": "*",
31 "expo-asset": "*",34 "expo-asset": "*",
32 "expo-clipboard": "*",35 "expo-clipboard": "*",
33 "expo-haptics": "*",36 "expo-haptics": "*",
apps/mobile/modules/t3-markdown-text/src/MarkdownTextPrimitive.tsx+12−5
@@ -1,4 +1,4 @@
1import React from "react";1import React, { type Ref } from "react";
2import { Platform, StyleSheet, Text as RNText, type TextProps, type ViewStyle } from "react-native";2import { Platform, StyleSheet, Text as RNText, type TextProps, type ViewStyle } from "react-native";
3import T3MarkdownTextRunNativeComponent from "./T3MarkdownTextRunNativeComponent";3import T3MarkdownTextRunNativeComponent from "./T3MarkdownTextRunNativeComponent";
4import T3MarkdownTextNativeComponent from "./T3MarkdownTextNativeComponent";4import T3MarkdownTextNativeComponent from "./T3MarkdownTextNativeComponent";
@@ -33,6 +33,7 @@ export type ContextMenuActionEvent = { · 28 unmodified lines
33 * while the React Native Text fallback reports measured `TextLayoutLine`s.33 * while the React Native Text fallback reports measured `TextLayoutLine`s.
34 */34 */
35export type MarkdownTextPrimitiveProps = Omit<TextProps, "onTextLayout"> & {35export type MarkdownTextPrimitiveProps = Omit<TextProps, "onTextLayout"> & {
36 nativeTextRef?: Ref<RNText>;
36 uiTextView?: boolean;37 uiTextView?: boolean;
37 contextMenuConfig?: string;38 contextMenuConfig?: string;
38 onContextMenuAction?: (event: ContextMenuActionEvent) => void;39 onContextMenuAction?: (event: ContextMenuActionEvent) => void;
@@ -45,7 +46,12 @@ export type MarkdownTextPrimitiveProps = Omit<TextProps, "onTextLayout"> & { · 6 unmodified lines
45 onSelectionChange?: (event: SelectionChangeEvent) => void;46 onSelectionChange?: (event: SelectionChangeEvent) => void;
46};47};
47 48
48function MarkdownTextPrimitiveChild({ style, children, ...rest }: MarkdownTextPrimitiveProps) {49function MarkdownTextPrimitiveChild({
50 style,
51 children,
52 nativeTextRef: _nativeTextRef,
53 ...rest
54}: MarkdownTextPrimitiveProps) {
49 const [isAncestor, rootStyle] = useTextAncestorContext();55 const [isAncestor, rootStyle] = useTextAncestorContext();
50 56
51 // Flatten the styles, and apply the root styles when needed57 // Flatten the styles, and apply the root styles when needed
@@ -97,21 +103,22 @@ function MarkdownTextPrimitiveChild({ style, children, ...rest }: MarkdownTextPr · 45 unmodified lines
97 return <>{nativeChildren}</>;103 return <>{nativeChildren}</>;
98}104}
99 105
100function MarkdownTextPrimitiveInner(props: MarkdownTextPrimitiveProps) {106function MarkdownTextPrimitiveInner({ nativeTextRef, ...props }: MarkdownTextPrimitiveProps) {
101 const [isAncestor] = useTextAncestorContext();107 const [isAncestor] = useTextAncestorContext();
102 108
103 // Even if the uiTextView prop is set, we can still default to using109 // Even if the uiTextView prop is set, we can still default to using
104 // normal selection (i.e. base RN text) if the text doesn't need to be110 // normal selection (i.e. base RN text) if the text doesn't need to be
105 // selectable111 // selectable
106 if ((!props.selectable || !props.uiTextView) && !isAncestor) {112 if ((!props.selectable || !props.uiTextView) && !isAncestor) {
107 return <RNText {...props} />;113 return <RNText ref={nativeTextRef} {...props} />;
108 }114 }
109 return <MarkdownTextPrimitiveChild {...props} />;115 return <MarkdownTextPrimitiveChild {...props} />;
110}116}
111 117
112export function MarkdownTextPrimitive(props: MarkdownTextPrimitiveProps) {118export function MarkdownTextPrimitive(props: MarkdownTextPrimitiveProps) {
113 if (Platform.OS !== "ios") {119 if (Platform.OS !== "ios") {
114 return <RNText {...props} />;120 const { nativeTextRef, ...textProps } = props;
121 return <RNText ref={nativeTextRef} {...textProps} />;
115 }122 }
116 return <MarkdownTextPrimitiveInner {...props} />;123 return <MarkdownTextPrimitiveInner {...props} />;
117}124}
apps/mobile/modules/t3-markdown-text/src/NativeMarkdownBlock.ios.tsx+17−6
@@ -1,5 +1,5 @@
1import { createContext, useContext, useEffect, useState } from "react";1import { createContext, useContext, useEffect, useState } from "react";
2import { Image, ScrollView, Text, useColorScheme, View } from "react-native";2import { Image, Platform, ScrollView, Text, useColorScheme, View } from "react-native";
3import type { MarkdownNode } from "react-native-nitro-markdown/headless";3import type { MarkdownNode } from "react-native-nitro-markdown/headless";
4 4
5import { CopyTextButton } from "./CopyTextButton";5import { CopyTextButton } from "./CopyTextButton";
@@ -22,6 +22,11 @@ type HighlightedCode = ReadonlyArray<ReadonlyArray<MarkdownHighlightedToken>>; · 16 unmodified lines
22const highlightedCodeCache = new Map<string, HighlightedCode>();22const highlightedCodeCache = new Map<string, HighlightedCode>();
23const highlightedCodePromiseCache = new Map<string, Promise<HighlightedCode>>();23const highlightedCodePromiseCache = new Map<string, Promise<HighlightedCode>>();
24const HIGHLIGHTED_CODE_CACHE_LIMIT = 64;24const HIGHLIGHTED_CODE_CACHE_LIMIT = 64;
25const MONO_FONT_FAMILY = Platform.select({
26 ios: "ui-monospace",
27 android: "monospace",
28 default: "monospace",
29});
25 30
26function nodeKey(node: MarkdownNode, index: number): string {31function nodeKey(node: MarkdownNode, index: number): string {
27 return `${node.type}:${node.beg ?? index}:${node.end ?? index}`;32 return `${node.type}:${node.beg ?? index}:${node.end ?? index}`;
@@ -171,7 +176,7 @@ function HighlightedCodeText(props: { · 143 unmodified lines
171 selectable176 selectable
172 style={{177 style={{
173 color: props.textStyle.codeColor,178 color: props.textStyle.codeColor,
174 fontFamily: "ui-monospace",179 fontFamily: MONO_FONT_FAMILY,
175 fontSize: codeBlockFontSize(props.textStyle),180 fontSize: codeBlockFontSize(props.textStyle),
176 lineHeight: codeBlockLineHeight(props.textStyle),181 lineHeight: codeBlockLineHeight(props.textStyle),
177 }}182 }}
@@ -206,7 +211,7 @@ function HighlightedCodeText(props: { · 28 unmodified lines
206 selectable211 selectable
207 style={{212 style={{
208 color: props.textStyle.codeColor,213 color: props.textStyle.codeColor,
209 fontFamily: "ui-monospace",214 fontFamily: MONO_FONT_FAMILY,
210 fontSize: codeBlockFontSize(props.textStyle),215 fontSize: codeBlockFontSize(props.textStyle),
211 lineHeight: codeBlockLineHeight(props.textStyle),216 lineHeight: codeBlockLineHeight(props.textStyle),
212 }}217 }}
@@ -218,7 +223,7 @@ function HighlightedCodeText(props: { · 5 unmodified lines
218 key={key}223 key={key}
219 style={{224 style={{
220 color: token.color ?? props.textStyle.codeColor,225 color: token.color ?? props.textStyle.codeColor,
221 fontFamily: "ui-monospace",226 fontFamily: MONO_FONT_FAMILY,
222 fontStyle:227 fontStyle:
223 token.fontStyle !== null && (token.fontStyle & 1) === 1 ? "italic" : "normal",228 token.fontStyle !== null && (token.fontStyle & 1) === 1 ? "italic" : "normal",
224 fontWeight: token.fontStyle !== null && (token.fontStyle & 2) === 2 ? "700" : "400",229 fontWeight: token.fontStyle !== null && (token.fontStyle & 2) === 2 ? "700" : "400",
@@ -274,7 +279,7 @@ function NativeCodeBlock(props: { · 49 unmodified lines
274 style={{279 style={{
275 flex: 1,280 flex: 1,
276 color: props.textStyle.mutedColor,281 color: props.textStyle.mutedColor,
277 fontFamily: "ui-monospace",282 fontFamily: MONO_FONT_FAMILY,
278 fontSize: codeBlockFontSize(props.textStyle),283 fontSize: codeBlockFontSize(props.textStyle),
279 }}284 }}
280 >285 >
@@ -294,6 +299,7 @@ function NativeCodeBlock(props: { · 13 unmodified lines
294 <ScrollView299 <ScrollView
295 horizontal300 horizontal
296 bounces={false}301 bounces={false}
302 nestedScrollEnabled={Platform.OS === "android"}
297 showsHorizontalScrollIndicator={false}303 showsHorizontalScrollIndicator={false}
298 contentContainerStyle={{ paddingHorizontal: 14, paddingVertical: 12 }}304 contentContainerStyle={{ paddingHorizontal: 14, paddingVertical: 12 }}
299 >305 >
@@ -330,7 +336,12 @@ function NativeTable(props: { · 30 unmodified lines
330}) {336}) {
331 const rows = collectTableRows(props.node);337 const rows = collectTableRows(props.node);
332 return (338 return (
333 <ScrollView horizontal bounces={false} showsHorizontalScrollIndicator={false}>339 <ScrollView
340 horizontal
341 bounces={false}
342 nestedScrollEnabled={Platform.OS === "android"}
343 showsHorizontalScrollIndicator={false}
344 >
334 <View345 <View
335 style={{346 style={{
336 borderColor: props.textStyle.dividerColor,347 borderColor: props.textStyle.dividerColor,
apps/mobile/modules/t3-markdown-text/src/NativeMarkdownSelectableText.ios.tsx+55−10
@@ -1,5 +1,14 @@
1import { createContext, useContext } from "react";1import { createContext, useCallback, useContext } from "react";
2import { Image, Linking, type TextStyle, useColorScheme } from "react-native";2import {
3 findNodeHandle,
4 Image,
5 Linking,
6 Platform,
7 StyleSheet,
8 Text as RNText,
9 type TextStyle,
10 useColorScheme,
11} from "react-native";
3 12
4import { MarkdownTextPrimitive } from "./MarkdownTextPrimitive";13import { MarkdownTextPrimitive } from "./MarkdownTextPrimitive";
5import { markdownFileIconSource } from "./markdownFileIcons";14import { markdownFileIconSource } from "./markdownFileIcons";
@@ -8,6 +17,7 @@ import type { · 2 unmodified lines
8 MarkdownFileContextMenu,17 MarkdownFileContextMenu,
9 NativeMarkdownTextStyle,18 NativeMarkdownTextStyle,
10} from "./SelectableMarkdownText.types";19} from "./SelectableMarkdownText.types";
20import { installMarkdownCopySanitizer } from "./T3MarkdownTextSelectionModule";
11 21
12export interface MarkdownFileContextMenuHandlers {22export interface MarkdownFileContextMenuHandlers {
13 readonly fileContextMenu: (href: string) => MarkdownFileContextMenu | undefined;23 readonly fileContextMenu: (href: string) => MarkdownFileContextMenu | undefined;
@@ -23,6 +33,19 @@ const EXTERNAL_LINK_PREFIX = "◉ "; · 9 unmodified lines
23const INLINE_ATTACHMENT_PREFIX = "\uFFFC\u00A0";33const INLINE_ATTACHMENT_PREFIX = "\uFFFC\u00A0";
24const SKILL_ICON_PLACEHOLDER = "\uFFFC";34const SKILL_ICON_PLACEHOLDER = "\uFFFC";
25const PARAGRAPH_STYLE_ENCODING_OFFSET = 1000;35const PARAGRAPH_STYLE_ENCODING_OFFSET = 1000;
36const MONO_FONT_FAMILY = Platform.select({
37 ios: "ui-monospace",
38 android: "monospace",
39 default: "monospace",
40});
41const styles = StyleSheet.create({
42 inlineIcon: {
43 width: 14,
44 height: 14,
45 marginHorizontal: 3,
46 transform: [{ translateY: 2 }],
47 },
48});
26 49
27function runKeySignature(run: NativeMarkdownTextRun): string {50function runKeySignature(run: NativeMarkdownTextRun): string {
28 return [51 return [
@@ -102,7 +125,7 @@ function runStyle(run: NativeMarkdownTextRun, textStyle: NativeMarkdownTextStyle · 73 unmodified lines
102 isFile || isSkill125 isFile || isSkill
103 ? textStyle.boldFontFamily126 ? textStyle.boldFontFamily
104 : run.code || isCodeBlock127 : run.code || isCodeBlock
105 ? "ui-monospace"128 ? MONO_FONT_FAMILY
106 : isHeading129 : isHeading
107 ? textStyle.headingFontFamily130 ? textStyle.headingFontFamily
108 : run.bold131 : run.bold
@@ -154,6 +177,19 @@ export function NativeMarkdownSelectableText(props: { · 45 unmodified lines
154}) {177}) {
155 const colorScheme = useColorScheme();178 const colorScheme = useColorScheme();
156 const menu = useContext(MarkdownFileContextMenuContext);179 const menu = useContext(MarkdownFileContextMenuContext);
180 const containsInlineFileIcon = props.runs.some((run) => run.fileIcon != null);
181 const attachAndroidText = useCallback(
182 (textView: RNText | null) => {
183 if (Platform.OS !== "android" || !containsInlineFileIcon || textView === null) {
184 return;
185 }
186 const reactTag = findNodeHandle(textView);
187 if (reactTag !== null) {
188 installMarkdownCopySanitizer(reactTag);
189 }
190 },
191 [containsInlineFileIcon],
192 );
157 const occurrences = new Map<string, number>();193 const occurrences = new Map<string, number>();
158 const prefixedExternalLinks = new Set<string>();194 const prefixedExternalLinks = new Set<string>();
159 const keyedRuns = props.runs.map((run) => {195 const keyedRuns = props.runs.map((run) => {
@@ -162,10 +198,13 @@ export function NativeMarkdownSelectableText(props: { · 2 unmodified lines
162 occurrences.set(signature, occurrence + 1);198 occurrences.set(signature, occurrence + 1);
163 199
164 let text = run.text;200 let text = run.text;
165 if (run.fileIcon) {201 if (run.fileIcon && Platform.OS === "ios") {
166 text = `${INLINE_ATTACHMENT_PREFIX}${text}`;202 text = `${INLINE_ATTACHMENT_PREFIX}${text}`;
167 } else if (run.skillName && run.skillLabel) {203 } else if (run.skillName && run.skillLabel) {
168 text = `${SKILL_ICON_PLACEHOLDER}\u00A0${run.skillLabel}`;204 text =
205 Platform.OS === "ios"
206 ? `${SKILL_ICON_PLACEHOLDER}\u00A0${run.skillLabel}`
207 : `$${run.skillName}`;
169 } else if (run.externalHost && run.href && !prefixedExternalLinks.has(run.href)) {208 } else if (run.externalHost && run.href && !prefixedExternalLinks.has(run.href)) {
170 prefixedExternalLinks.add(run.href);209 prefixedExternalLinks.add(run.href);
171 text = `${EXTERNAL_LINK_PREFIX}${text}`;210 text = `${EXTERNAL_LINK_PREFIX}${text}`;
@@ -197,6 +236,7 @@ export function NativeMarkdownSelectableText(props: { · 25 unmodified lines
197 return (236 return (
198 <MarkdownTextPrimitive237 <MarkdownTextPrimitive
199 key={appearanceKey}238 key={appearanceKey}
239 nativeTextRef={attachAndroidText}
200 uiTextView240 uiTextView
201 selectable241 selectable
202 style={{242 style={{
@@ -215,11 +255,13 @@ export function NativeMarkdownSelectableText(props: { · 12 unmodified lines
215 <MarkdownTextPrimitive255 <MarkdownTextPrimitive
216 key={key}256 key={key}
217 nativeID={257 nativeID={
218 run.fileIcon258 Platform.OS === "ios"
219 ? `t3-file:${Image.resolveAssetSource(markdownFileIconSource(run.fileIcon)).uri}`259 ? run.fileIcon
220 : run.skillName260 ? `t3-file:${Image.resolveAssetSource(markdownFileIconSource(run.fileIcon)).uri}`
221 ? "t3-skill:sf:cube"261 : run.skillName
222 : undefined262 ? "t3-skill:sf:cube"
263 : undefined
264 : undefined
223 }265 }
224 contextMenuConfig={contextMenu ? JSON.stringify(contextMenu) : undefined}266 contextMenuConfig={contextMenu ? JSON.stringify(contextMenu) : undefined}
225 style={runStyle(run, props.textStyle)}267 style={runStyle(run, props.textStyle)}
@@ -240,6 +282,9 @@ export function NativeMarkdownSelectableText(props: { · 14 unmodified lines
240 : undefined282 : undefined
241 }283 }
242 >284 >
285 {Platform.OS === "android" && run.fileIcon ? (
286 <Image source={markdownFileIconSource(run.fileIcon)} style={styles.inlineIcon} />
287 ) : null}
243 {text}288 {text}
244 </MarkdownTextPrimitive>289 </MarkdownTextPrimitive>
245 );290 );
apps/mobile/modules/t3-markdown-text/src/T3MarkdownTextSelectionModule.ts+12−0
@@ -0,0 +1,12 @@
1import { requireOptionalNativeModule } from "expo";
2
3interface T3MarkdownTextSelectionNativeModule {
4 readonly installCopySanitizer: (reactTag: number) => void;
5}
6
7const nativeModule =
8 requireOptionalNativeModule<T3MarkdownTextSelectionNativeModule>("T3MarkdownTextSelection");
9
10export function installMarkdownCopySanitizer(reactTag: number): void {
11 nativeModule?.installCopySanitizer(reactTag);
12}
apps/mobile/src/native/SelectableMarkdownText.android.tsx+24−0
@@ -0,0 +1,24 @@
1import {
2 SelectableMarkdownText as T3SelectableMarkdownText,
3 type SelectableMarkdownTextProps,
4} from "@t3tools/mobile-markdown-text/renderer";
5
6import { highlightCodeSnippet } from "../features/review/shikiReviewHighlighter";
7
8type MobileSelectableMarkdownTextProps = Omit<SelectableMarkdownTextProps, "highlightCode">;
9
10export type {
11 MarkdownImageRequest,
12 NativeMarkdownTextStyle,
13 SelectableMarkdownSkill,
14} from "@t3tools/mobile-markdown-text/types";
15
16// The renderer falls back to React Native Text outside iOS, so Android can use
17// the same Markdown chunking while retaining native text selection.
18export function hasNativeSelectableMarkdownText(): boolean {
19 return true;
20}
21
22export function SelectableMarkdownText(props: MobileSelectableMarkdownTextProps) {
23 return <T3SelectableMarkdownText {...props} highlightCode={highlightCodeSnippet} />;
24}
pnpm-lock.yaml+4−2
@@ -278,7 +278,7 @@ importers: · 277 unmodified lines
278 version: link:../../packages/contracts278 version: link:../../packages/contracts
279 '@t3tools/mobile-markdown-text':279 '@t3tools/mobile-markdown-text':
280 specifier: file:./modules/t3-markdown-text280 specifier: file:./modules/t3-markdown-text
281 version: file:apps/mobile/modules/t3-markdown-text(cd0d4cdec0d5bee3af406af520908919)281 version: file:apps/mobile/modules/t3-markdown-text(eb96cd030e772ab91fe21f812240338a)
282 '@t3tools/mobile-review-diff-native':282 '@t3tools/mobile-review-diff-native':
283 specifier: file:./modules/t3-review-diff283 specifier: file:./modules/t3-review-diff
284 version: file:apps/mobile/modules/t3-review-diff284 version: file:apps/mobile/modules/t3-review-diff
@@ -4513,6 +4513,7 @@ packages: · 4228 unmodified lines
4513 peerDependencies:4513 peerDependencies:
4514 '@t3tools/client-runtime': '*'4514 '@t3tools/client-runtime': '*'
4515 '@t3tools/shared': '*'4515 '@t3tools/shared': '*'
4516 expo: '*'
4516 expo-asset: '*'4517 expo-asset: '*'
4517 expo-clipboard: '*'4518 expo-clipboard: '*'
4518 expo-haptics: '*'4519 expo-haptics: '*'
@@ -14372,10 +14373,11 @@ snapshots: · 9853 unmodified lines
14372 dependencies:14373 dependencies:
14373 defer-to-connect: 2.0.114374 defer-to-connect: 2.0.1
14374 14375
14375 '@t3tools/mobile-markdown-text@file:apps/mobile/modules/t3-markdown-text(cd0d4cdec0d5bee3af406af520908919)':14376 '@t3tools/mobile-markdown-text@file:apps/mobile/modules/t3-markdown-text(eb96cd030e772ab91fe21f812240338a)':
14376 dependencies:14377 dependencies:
14377 '@t3tools/client-runtime': link:packages/client-runtime14378 '@t3tools/client-runtime': link:packages/client-runtime
14378 '@t3tools/shared': link:packages/shared14379 '@t3tools/shared': link:packages/shared
14380 expo: 57.0.18(f9c992a5d7c53d81398568d3950992dc)
14379 expo-asset: 57.0.15(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)14381 expo-asset: 57.0.15(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)
14380 expo-clipboard: 57.0.1(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)14382 expo-clipboard: 57.0.1(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
14381 expo-haptics: 57.0.2(expo@57.0.18)14383 expo-haptics: 57.0.2(expo@57.0.18)