fix(app): render inline links via UITextView span on iOS (#1257)

AssistantMarkdownLink's native branch wrapped its children in a plain RN
<Text>. On iOS that <Text> sits inside the paragraph's native UITextView
and is not hoisted into a UITextViewChild, so link text (markdown links,
bare URLs, and inline-code path links) was silently dropped — the same
root cause as the bold/italic/strikethrough fix in this PR.

Route the native link through MarkdownTextSpan so it composes as a
UITextViewChild and stays visible + selectable. MarkdownTextSpan now
forwards onPress/accessibilityRole across platforms: tappable on
Android/web; on iOS react-native-uitextview nulls onPress on the root
native view, so reliable tap-to-open is still tracked by #21 — but
visible, selectable, copyable link text strictly beats an invisible one.

Refs #1253

Co-authored-by: Geoffrey <geoffrey.marc@consulting-for.edenred.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Geoffrey Marc
2026-06-01 11:29:59 +08:00
committed by GitHub
co-authored by Geoffrey Claude Opus 4.8
parent 632c48fde3
commit a3b0af9ac0
4 changed files with 76 additions and 13 deletions
@@ -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<TextStyle>;
monoSurface?: boolean;
children: ReactNode;
// Links route through this span too (see assistant-file-links/link.tsx). A
// plain <Text> 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 (
<UITextView uiTextView selectable style={style}>
<UITextView
uiTextView
selectable
style={style}
onPress={onPress}
accessibilityRole={accessibilityRole}
>
{children}
</UITextView>
);