diff --git a/packages/app/src/assistant-file-links/link.tsx b/packages/app/src/assistant-file-links/link.tsx index 584e71a5c..4c19322a6 100644 --- a/packages/app/src/assistant-file-links/link.tsx +++ b/packages/app/src/assistant-file-links/link.tsx @@ -9,6 +9,7 @@ import { } from "react-native"; import { StyleSheet } from "react-native-unistyles"; import { isNative, isWeb } from "@/constants/platform"; +import { MarkdownTextSpan } from "@/components/markdown-text"; import { Shortcut } from "@/components/ui/shortcut"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { useStableEvent } from "@/hooks/use-stable-event"; @@ -57,16 +58,21 @@ export function AssistantMarkdownLink({ ); if (isNative) { + // Must be a MarkdownTextSpan, not a plain : on iOS the link renders + // inside the paragraph's native UITextView, and a plain nested there + // is not hoisted into a UITextViewChild, so its text is silently dropped + // (the link disappears). The span composes correctly and stays selectable; + // onPress is forwarded (reliable tap-to-open on iOS is tracked by #21). return ( - {children} - + ); } diff --git a/packages/app/src/components/markdown-text.android.tsx b/packages/app/src/components/markdown-text.android.tsx index f6f03c645..7733afb82 100644 --- a/packages/app/src/components/markdown-text.android.tsx +++ b/packages/app/src/components/markdown-text.android.tsx @@ -1,18 +1,33 @@ import { useMemo, type ReactNode } from "react"; -import { Text, View, type StyleProp, type TextStyle, type ViewStyle } from "react-native"; +import { + Text, + View, + type StyleProp, + type TextProps, + type TextStyle, + type ViewStyle, +} from "react-native"; interface MarkdownTextSpanProps { style?: StyleProp; monoSurface?: boolean; children: ReactNode; + onPress?: TextProps["onPress"]; + accessibilityRole?: TextProps["accessibilityRole"]; } // Android's enables per-text-node selection natively. Each // sibling Text is its own selection scope — drag can't span across siblings -// (that requires a single UITextView ancestor and is iOS-only). -export function MarkdownTextSpan({ style, children }: MarkdownTextSpanProps) { +// (that requires a single UITextView ancestor and is iOS-only). onPress works +// natively here, so links routed through this span stay tappable on Android. +export function MarkdownTextSpan({ + style, + children, + onPress, + accessibilityRole, +}: MarkdownTextSpanProps) { return ( - + {children} ); diff --git a/packages/app/src/components/markdown-text.ios.tsx b/packages/app/src/components/markdown-text.ios.tsx index a66cc7708..8bd3b1046 100644 --- a/packages/app/src/components/markdown-text.ios.tsx +++ b/packages/app/src/components/markdown-text.ios.tsx @@ -1,20 +1,39 @@ import { useMemo, type ReactNode } from "react"; -import type { StyleProp, TextStyle, ViewStyle } from "react-native"; +import type { StyleProp, TextProps, TextStyle, ViewStyle } from "react-native"; import { UITextView } from "react-native-uitextview"; interface MarkdownTextSpanProps { style?: StyleProp; monoSurface?: boolean; children: ReactNode; + // Links route through this span too (see assistant-file-links/link.tsx). A + // plain nested in the paragraph UITextView is dropped, so the link + // must be a UITextView span to be visible. onPress is forwarded best-effort: + // react-native-uitextview nulls onPress on the root native view, so reliable + // tap-to-open is still tracked by #21 — but visible+selectable text beats an + // invisible link. + onPress?: TextProps["onPress"]; + accessibilityRole?: TextProps["accessibilityRole"]; } // Inline span backed by UITextView so iOS gets native word-selection handles. // Used inside MarkdownParagraphView (which is also a UITextView on iOS); the // library's TextAncestorContext hoists these into UITextViewChild nodes so // selection drags can cross sibling spans (e.g. plain text → **bold** → code). -export function MarkdownTextSpan({ style, children }: MarkdownTextSpanProps) { +export function MarkdownTextSpan({ + style, + children, + onPress, + accessibilityRole, +}: MarkdownTextSpanProps) { return ( - + {children} ); diff --git a/packages/app/src/components/markdown-text.web.tsx b/packages/app/src/components/markdown-text.web.tsx index f918b2868..0d3a74753 100644 --- a/packages/app/src/components/markdown-text.web.tsx +++ b/packages/app/src/components/markdown-text.web.tsx @@ -1,11 +1,23 @@ import { useMemo, type ReactNode } from "react"; -import { Text, View, type StyleProp, type TextStyle, type ViewStyle } from "react-native"; +import { + Text, + View, + type StyleProp, + type TextProps, + type TextStyle, + type ViewStyle, +} from "react-native"; import { CODE_SURFACE_DATASET } from "@/styles/code-surface"; interface MarkdownTextSpanProps { style?: StyleProp; monoSurface?: boolean; children: ReactNode; + // Web links use the /Pressable path in link.tsx, not this span, so these + // are accepted for prop-shape parity with the native variants and forwarded + // harmlessly. + onPress?: TextProps["onPress"]; + accessibilityRole?: TextProps["accessibilityRole"]; } // react-native-web renders Text as /
with `user-select: text` @@ -13,9 +25,20 @@ interface MarkdownTextSpanProps { // react-native-uitextview: its transitive import of codegenNativeComponent // pulls in setUpReactDevTools, which doesn't resolve under Metro's web // target in dev mode. -export function MarkdownTextSpan({ style, monoSurface, children }: MarkdownTextSpanProps) { +export function MarkdownTextSpan({ + style, + monoSurface, + children, + onPress, + accessibilityRole, +}: MarkdownTextSpanProps) { return ( - + {children} );