| author | |
| committer | |
| log | 09d13de4381925fa2a6dea74eff8185fa301e905 |
| tree | ee52e1e7c05918e59cb8b1390fd47e93ae3d2e50 |
| parent | 5f878d2a85807618a4c8571cdef5daa3124672d6 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: shivam <91240327+shivamhwp@users.noreply.github.com>10 files changed, 246 insertions(+), 23 deletions(-)
apps/mobile/modules/t3-markdown-text/android/build.gradle created+20| ... | ... | @@ -0,0 +1,20 @@ |
| 1 | apply plugin: 'com.android.library' | |
| 2 | apply plugin: 'org.jetbrains.kotlin.android' | |
| 3 | ||
| 4 | group = 'com.t3tools.markdowntext' | |
| 5 | version = '0.0.0' | |
| 6 | ||
| 7 | android { | |
| 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 | ||
| 17 | dependencies { | |
| 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 created+93| ... | ... | @@ -0,0 +1,93 @@ |
| 1 | package expo.modules.t3markdowntext | |
| 2 | ||
| 3 | import android.content.ClipData | |
| 4 | import android.content.ClipboardManager | |
| 5 | import android.content.Context | |
| 6 | import android.text.Spanned | |
| 7 | import android.text.style.ReplacementSpan | |
| 8 | import android.view.ActionMode | |
| 9 | import android.view.Menu | |
| 10 | import android.view.MenuItem | |
| 11 | import android.widget.TextView | |
| 12 | import com.facebook.react.bridge.ReactContext | |
| 13 | import com.facebook.react.uimanager.UIManagerHelper | |
| 14 | import expo.modules.kotlin.modules.Module | |
| 15 | import expo.modules.kotlin.modules.ModuleDefinition | |
| 16 | import kotlin.math.max | |
| 17 | import kotlin.math.min | |
| 18 | ||
| 19 | private const val OBJECT_REPLACEMENT_CHARACTER = "\uFFFC" | |
| 20 | ||
| 21 | private 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 | ||
| 38 | private 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 | ||
| 72 | class 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 created+6| ... | ... | @@ -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| ... | ... | @@ -4,7 +4,9 @@ |
| 4 | 4 | "private": true, |
| 5 | 5 | "source": "./index.ts", |
| 6 | 6 | "files": [ |
| 7 | "android", | |
| 7 | 8 | "assets", |
| 9 | "expo-module.config.json", | |
| 8 | 10 | "ios", |
| 9 | 11 | "src", |
| 10 | 12 | "index.ts", |
| ... | ... | @@ -28,6 +30,7 @@ |
| 28 | 30 | "peerDependencies": { |
| 29 | 31 | "@t3tools/client-runtime": "*", |
| 30 | 32 | "@t3tools/shared": "*", |
| 33 | "expo": "*", | |
| 31 | 34 | "expo-asset": "*", |
| 32 | 35 | "expo-clipboard": "*", |
| 33 | 36 | "expo-haptics": "*", |
apps/mobile/modules/t3-markdown-text/src/MarkdownTextPrimitive.tsx+12-5| ... | ... | @@ -1,4 +1,4 @@ |
| 1 | import React from "react"; | |
| 1 | import React, { type Ref } from "react"; | |
| 2 | 2 | import { Platform, StyleSheet, Text as RNText, type TextProps, type ViewStyle } from "react-native"; |
| 3 | 3 | import T3MarkdownTextRunNativeComponent from "./T3MarkdownTextRunNativeComponent"; |
| 4 | 4 | import T3MarkdownTextNativeComponent from "./T3MarkdownTextNativeComponent"; |
| ... | ... | @@ -33,6 +33,7 @@ export type ContextMenuActionEvent = { |
| 33 | 33 | * while the React Native Text fallback reports measured `TextLayoutLine`s. |
| 34 | 34 | */ |
| 35 | 35 | export type MarkdownTextPrimitiveProps = Omit<TextProps, "onTextLayout"> & { |
| 36 | nativeTextRef?: Ref<RNText>; | |
| 36 | 37 | uiTextView?: boolean; |
| 37 | 38 | contextMenuConfig?: string; |
| 38 | 39 | onContextMenuAction?: (event: ContextMenuActionEvent) => void; |
| ... | ... | @@ -45,7 +46,12 @@ export type MarkdownTextPrimitiveProps = Omit<TextProps, "onTextLayout"> & { |
| 45 | 46 | onSelectionChange?: (event: SelectionChangeEvent) => void; |
| 46 | 47 | }; |
| 47 | 48 | |
| 48 | function MarkdownTextPrimitiveChild({ style, children, ...rest }: MarkdownTextPrimitiveProps) { | |
| 49 | function MarkdownTextPrimitiveChild({ | |
| 50 | style, | |
| 51 | children, | |
| 52 | nativeTextRef: _nativeTextRef, | |
| 53 | ...rest | |
| 54 | }: MarkdownTextPrimitiveProps) { | |
| 49 | 55 | const [isAncestor, rootStyle] = useTextAncestorContext(); |
| 50 | 56 | |
| 51 | 57 | // Flatten the styles, and apply the root styles when needed |
| ... | ... | @@ -97,21 +103,22 @@ function MarkdownTextPrimitiveChild({ style, children, ...rest }: MarkdownTextPr |
| 97 | 103 | return <>{nativeChildren}</>; |
| 98 | 104 | } |
| 99 | 105 | |
| 100 | function MarkdownTextPrimitiveInner(props: MarkdownTextPrimitiveProps) { | |
| 106 | function MarkdownTextPrimitiveInner({ nativeTextRef, ...props }: MarkdownTextPrimitiveProps) { | |
| 101 | 107 | const [isAncestor] = useTextAncestorContext(); |
| 102 | 108 | |
| 103 | 109 | // Even if the uiTextView prop is set, we can still default to using |
| 104 | 110 | // normal selection (i.e. base RN text) if the text doesn't need to be |
| 105 | 111 | // selectable |
| 106 | 112 | if ((!props.selectable || !props.uiTextView) && !isAncestor) { |
| 107 | return <RNText {...props} />; | |
| 113 | return <RNText ref={nativeTextRef} {...props} />; | |
| 108 | 114 | } |
| 109 | 115 | return <MarkdownTextPrimitiveChild {...props} />; |
| 110 | 116 | } |
| 111 | 117 | |
| 112 | 118 | export function MarkdownTextPrimitive(props: MarkdownTextPrimitiveProps) { |
| 113 | 119 | if (Platform.OS !== "ios") { |
| 114 | return <RNText {...props} />; | |
| 120 | const { nativeTextRef, ...textProps } = props; | |
| 121 | return <RNText ref={nativeTextRef} {...textProps} />; | |
| 115 | 122 | } |
| 116 | 123 | return <MarkdownTextPrimitiveInner {...props} />; |
| 117 | 124 | } |
apps/mobile/modules/t3-markdown-text/src/NativeMarkdownBlock.ios.tsx+17-6| ... | ... | @@ -1,5 +1,5 @@ |
| 1 | 1 | import { createContext, useContext, useEffect, useState } from "react"; |
| 2 | import { Image, ScrollView, Text, useColorScheme, View } from "react-native"; | |
| 2 | import { Image, Platform, ScrollView, Text, useColorScheme, View } from "react-native"; | |
| 3 | 3 | import type { MarkdownNode } from "react-native-nitro-markdown/headless"; |
| 4 | 4 | |
| 5 | 5 | import { CopyTextButton } from "./CopyTextButton"; |
| ... | ... | @@ -22,6 +22,11 @@ type HighlightedCode = ReadonlyArray<ReadonlyArray<MarkdownHighlightedToken>>; |
| 22 | 22 | const highlightedCodeCache = new Map<string, HighlightedCode>(); |
| 23 | 23 | const highlightedCodePromiseCache = new Map<string, Promise<HighlightedCode>>(); |
| 24 | 24 | const HIGHLIGHTED_CODE_CACHE_LIMIT = 64; |
| 25 | const MONO_FONT_FAMILY = Platform.select({ | |
| 26 | ios: "ui-monospace", | |
| 27 | android: "monospace", | |
| 28 | default: "monospace", | |
| 29 | }); | |
| 25 | 30 | |
| 26 | 31 | function nodeKey(node: MarkdownNode, index: number): string { |
| 27 | 32 | return `${node.type}:${node.beg ?? index}:${node.end ?? index}`; |
| ... | ... | @@ -171,7 +176,7 @@ function HighlightedCodeText(props: { |
| 171 | 176 | selectable |
| 172 | 177 | style={{ |
| 173 | 178 | color: props.textStyle.codeColor, |
| 174 | fontFamily: "ui-monospace", | |
| 179 | fontFamily: MONO_FONT_FAMILY, | |
| 175 | 180 | fontSize: codeBlockFontSize(props.textStyle), |
| 176 | 181 | lineHeight: codeBlockLineHeight(props.textStyle), |
| 177 | 182 | }} |
| ... | ... | @@ -206,7 +211,7 @@ function HighlightedCodeText(props: { |
| 206 | 211 | selectable |
| 207 | 212 | style={{ |
| 208 | 213 | color: props.textStyle.codeColor, |
| 209 | fontFamily: "ui-monospace", | |
| 214 | fontFamily: MONO_FONT_FAMILY, | |
| 210 | 215 | fontSize: codeBlockFontSize(props.textStyle), |
| 211 | 216 | lineHeight: codeBlockLineHeight(props.textStyle), |
| 212 | 217 | }} |
| ... | ... | @@ -218,7 +223,7 @@ function HighlightedCodeText(props: { |
| 218 | 223 | key={key} |
| 219 | 224 | style={{ |
| 220 | 225 | color: token.color ?? props.textStyle.codeColor, |
| 221 | fontFamily: "ui-monospace", | |
| 226 | fontFamily: MONO_FONT_FAMILY, | |
| 222 | 227 | fontStyle: |
| 223 | 228 | token.fontStyle !== null && (token.fontStyle & 1) === 1 ? "italic" : "normal", |
| 224 | 229 | fontWeight: token.fontStyle !== null && (token.fontStyle & 2) === 2 ? "700" : "400", |
| ... | ... | @@ -274,7 +279,7 @@ function NativeCodeBlock(props: { |
| 274 | 279 | style={{ |
| 275 | 280 | flex: 1, |
| 276 | 281 | color: props.textStyle.mutedColor, |
| 277 | fontFamily: "ui-monospace", | |
| 282 | fontFamily: MONO_FONT_FAMILY, | |
| 278 | 283 | fontSize: codeBlockFontSize(props.textStyle), |
| 279 | 284 | }} |
| 280 | 285 | > |
| ... | ... | @@ -294,6 +299,7 @@ function NativeCodeBlock(props: { |
| 294 | 299 | <ScrollView |
| 295 | 300 | horizontal |
| 296 | 301 | bounces={false} |
| 302 | nestedScrollEnabled={Platform.OS === "android"} | |
| 297 | 303 | showsHorizontalScrollIndicator={false} |
| 298 | 304 | contentContainerStyle={{ paddingHorizontal: 14, paddingVertical: 12 }} |
| 299 | 305 | > |
| ... | ... | @@ -330,7 +336,12 @@ function NativeTable(props: { |
| 330 | 336 | }) { |
| 331 | 337 | const rows = collectTableRows(props.node); |
| 332 | 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 | 345 | <View |
| 335 | 346 | style={{ |
| 336 | 347 | borderColor: props.textStyle.dividerColor, |
apps/mobile/modules/t3-markdown-text/src/NativeMarkdownSelectableText.ios.tsx+55-10| ... | ... | @@ -1,5 +1,14 @@ |
| 1 | import { createContext, useContext } from "react"; | |
| 2 | import { Image, Linking, type TextStyle, useColorScheme } from "react-native"; | |
| 1 | import { createContext, useCallback, useContext } from "react"; | |
| 2 | import { | |
| 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 | |
| 4 | 13 | import { MarkdownTextPrimitive } from "./MarkdownTextPrimitive"; |
| 5 | 14 | import { markdownFileIconSource } from "./markdownFileIcons"; |
| ... | ... | @@ -8,6 +17,7 @@ import type { |
| 8 | 17 | MarkdownFileContextMenu, |
| 9 | 18 | NativeMarkdownTextStyle, |
| 10 | 19 | } from "./SelectableMarkdownText.types"; |
| 20 | import { installMarkdownCopySanitizer } from "./T3MarkdownTextSelectionModule"; | |
| 11 | 21 | |
| 12 | 22 | export interface MarkdownFileContextMenuHandlers { |
| 13 | 23 | readonly fileContextMenu: (href: string) => MarkdownFileContextMenu | undefined; |
| ... | ... | @@ -23,6 +33,19 @@ const EXTERNAL_LINK_PREFIX = "◉ "; |
| 23 | 33 | const INLINE_ATTACHMENT_PREFIX = "\uFFFC\u00A0"; |
| 24 | 34 | const SKILL_ICON_PLACEHOLDER = "\uFFFC"; |
| 25 | 35 | const PARAGRAPH_STYLE_ENCODING_OFFSET = 1000; |
| 36 | const MONO_FONT_FAMILY = Platform.select({ | |
| 37 | ios: "ui-monospace", | |
| 38 | android: "monospace", | |
| 39 | default: "monospace", | |
| 40 | }); | |
| 41 | const styles = StyleSheet.create({ | |
| 42 | inlineIcon: { | |
| 43 | width: 14, | |
| 44 | height: 14, | |
| 45 | marginHorizontal: 3, | |
| 46 | transform: [{ translateY: 2 }], | |
| 47 | }, | |
| 48 | }); | |
| 26 | 49 | |
| 27 | 50 | function runKeySignature(run: NativeMarkdownTextRun): string { |
| 28 | 51 | return [ |
| ... | ... | @@ -102,7 +125,7 @@ function runStyle(run: NativeMarkdownTextRun, textStyle: NativeMarkdownTextStyle |
| 102 | 125 | isFile || isSkill |
| 103 | 126 | ? textStyle.boldFontFamily |
| 104 | 127 | : run.code || isCodeBlock |
| 105 | ? "ui-monospace" | |
| 128 | ? MONO_FONT_FAMILY | |
| 106 | 129 | : isHeading |
| 107 | 130 | ? textStyle.headingFontFamily |
| 108 | 131 | : run.bold |
| ... | ... | @@ -154,6 +177,19 @@ export function NativeMarkdownSelectableText(props: { |
| 154 | 177 | }) { |
| 155 | 178 | const colorScheme = useColorScheme(); |
| 156 | 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 | 193 | const occurrences = new Map<string, number>(); |
| 158 | 194 | const prefixedExternalLinks = new Set<string>(); |
| 159 | 195 | const keyedRuns = props.runs.map((run) => { |
| ... | ... | @@ -162,10 +198,13 @@ export function NativeMarkdownSelectableText(props: { |
| 162 | 198 | occurrences.set(signature, occurrence + 1); |
| 163 | 199 | |
| 164 | 200 | let text = run.text; |
| 165 | if (run.fileIcon) { | |
| 201 | if (run.fileIcon && Platform.OS === "ios") { | |
| 166 | 202 | text = `${INLINE_ATTACHMENT_PREFIX}${text}`; |
| 167 | 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 | 208 | } else if (run.externalHost && run.href && !prefixedExternalLinks.has(run.href)) { |
| 170 | 209 | prefixedExternalLinks.add(run.href); |
| 171 | 210 | text = `${EXTERNAL_LINK_PREFIX}${text}`; |
| ... | ... | @@ -197,6 +236,7 @@ export function NativeMarkdownSelectableText(props: { |
| 197 | 236 | return ( |
| 198 | 237 | <MarkdownTextPrimitive |
| 199 | 238 | key={appearanceKey} |
| 239 | nativeTextRef={attachAndroidText} | |
| 200 | 240 | uiTextView |
| 201 | 241 | selectable |
| 202 | 242 | style={{ |
| ... | ... | @@ -215,11 +255,13 @@ export function NativeMarkdownSelectableText(props: { |
| 215 | 255 | <MarkdownTextPrimitive |
| 216 | 256 | key={key} |
| 217 | 257 | nativeID={ |
| 218 | run.fileIcon | |
| 219 | ? `t3-file:${Image.resolveAssetSource(markdownFileIconSource(run.fileIcon)).uri}` | |
| 220 | : run.skillName | |
| 221 | ? "t3-skill:sf:cube" | |
| 222 | : undefined | |
| 258 | Platform.OS === "ios" | |
| 259 | ? run.fileIcon | |
| 260 | ? `t3-file:${Image.resolveAssetSource(markdownFileIconSource(run.fileIcon)).uri}` | |
| 261 | : run.skillName | |
| 262 | ? "t3-skill:sf:cube" | |
| 263 | : undefined | |
| 264 | : undefined | |
| 223 | 265 | } |
| 224 | 266 | contextMenuConfig={contextMenu ? JSON.stringify(contextMenu) : undefined} |
| 225 | 267 | style={runStyle(run, props.textStyle)} |
| ... | ... | @@ -240,6 +282,9 @@ export function NativeMarkdownSelectableText(props: { |
| 240 | 282 | : undefined |
| 241 | 283 | } |
| 242 | 284 | > |
| 285 | {Platform.OS === "android" && run.fileIcon ? ( | |
| 286 | <Image source={markdownFileIconSource(run.fileIcon)} style={styles.inlineIcon} /> | |
| 287 | ) : null} | |
| 243 | 288 | {text} |
| 244 | 289 | </MarkdownTextPrimitive> |
| 245 | 290 | ); |
apps/mobile/modules/t3-markdown-text/src/T3MarkdownTextSelectionModule.ts created+12| ... | ... | @@ -0,0 +1,12 @@ |
| 1 | import { requireOptionalNativeModule } from "expo"; | |
| 2 | ||
| 3 | interface T3MarkdownTextSelectionNativeModule { | |
| 4 | readonly installCopySanitizer: (reactTag: number) => void; | |
| 5 | } | |
| 6 | ||
| 7 | const nativeModule = | |
| 8 | requireOptionalNativeModule<T3MarkdownTextSelectionNativeModule>("T3MarkdownTextSelection"); | |
| 9 | ||
| 10 | export function installMarkdownCopySanitizer(reactTag: number): void { | |
| 11 | nativeModule?.installCopySanitizer(reactTag); | |
| 12 | } |
apps/mobile/src/native/SelectableMarkdownText.android.tsx created+24| ... | ... | @@ -0,0 +1,24 @@ |
| 1 | import { | |
| 2 | SelectableMarkdownText as T3SelectableMarkdownText, | |
| 3 | type SelectableMarkdownTextProps, | |
| 4 | } from "@t3tools/mobile-markdown-text/renderer"; | |
| 5 | ||
| 6 | import { highlightCodeSnippet } from "../features/review/shikiReviewHighlighter"; | |
| 7 | ||
| 8 | type MobileSelectableMarkdownTextProps = Omit<SelectableMarkdownTextProps, "highlightCode">; | |
| 9 | ||
| 10 | export 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. | |
| 18 | export function hasNativeSelectableMarkdownText(): boolean { | |
| 19 | return true; | |
| 20 | } | |
| 21 | ||
| 22 | export function SelectableMarkdownText(props: MobileSelectableMarkdownTextProps) { | |
| 23 | return <T3SelectableMarkdownText {...props} highlightCode={highlightCodeSnippet} />; | |
| 24 | } |
pnpm-lock.yaml+4-2| ... | ... | @@ -278,7 +278,7 @@ importers: |
| 278 | 278 | version: link:../../packages/contracts |
| 279 | 279 | '@t3tools/mobile-markdown-text': |
| 280 | 280 | 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 | 282 | '@t3tools/mobile-review-diff-native': |
| 283 | 283 | specifier: file:./modules/t3-review-diff |
| 284 | 284 | version: file:apps/mobile/modules/t3-review-diff |
| ... | ... | @@ -4513,6 +4513,7 @@ packages: |
| 4513 | 4513 | peerDependencies: |
| 4514 | 4514 | '@t3tools/client-runtime': '*' |
| 4515 | 4515 | '@t3tools/shared': '*' |
| 4516 | expo: '*' | |
| 4516 | 4517 | expo-asset: '*' |
| 4517 | 4518 | expo-clipboard: '*' |
| 4518 | 4519 | expo-haptics: '*' |
| ... | ... | @@ -14372,10 +14373,11 @@ snapshots: |
| 14372 | 14373 | dependencies: |
| 14373 | 14374 | 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 | 14377 | dependencies: |
| 14377 | 14378 | '@t3tools/client-runtime': link:packages/client-runtime |
| 14378 | 14379 | '@t3tools/shared': link:packages/shared |
| 14380 | expo: 57.0.18(f9c992a5d7c53d81398568d3950992dc) | |
| 14379 | 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 | 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 | 14383 | expo-haptics: 57.0.2(expo@57.0.18) |