Unified · Split
| @@ -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 | +} | |
| @@ -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 | +} | |
| @@ -0,0 +1,6 @@ | ||
| 1 | +{ | |
| 2 | + "platforms": ["android"], | |
| 3 | + "android": { | |
| 4 | + "modules": ["expo.modules.t3markdowntext.T3MarkdownTextSelectionModule"] | |
| 5 | + } | |
| 6 | +} | |
| @@ -4,7 +4,9 @@ · 3 unmodified lines | ||
| 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 @@ · 17 unmodified lines | ||
| 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": "*", |
| @@ -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 = { · 28 unmodified lines | ||
| 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"> & { · 6 unmodified lines | ||
| 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 · 45 unmodified lines | ||
| 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 | } |
| @@ -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>>; · 16 unmodified lines | ||
| 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: { · 143 unmodified lines | ||
| 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: { · 28 unmodified lines | ||
| 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: { · 5 unmodified lines | ||
| 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: { · 49 unmodified lines | ||
| 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: { · 13 unmodified lines | ||
| 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: { · 30 unmodified lines | ||
| 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, |
| @@ -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 { · 2 unmodified lines | ||
| 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 = "◉ "; · 9 unmodified lines | ||
| 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 · 73 unmodified lines | ||
| 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: { · 45 unmodified lines | ||
| 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: { · 2 unmodified lines | ||
| 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: { · 25 unmodified lines | ||
| 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: { · 12 unmodified lines | ||
| 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: { · 14 unmodified lines | ||
| 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 | ); |
| @@ -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 | +} | |
| @@ -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 | +} | |
| @@ -278,7 +278,7 @@ importers: · 277 unmodified lines | ||
| 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: · 4228 unmodified lines | ||
| 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: · 9853 unmodified lines | ||
| 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) |