Summary
Assistant markdown links in the mobile app currently get a custom JavaScript overlay for the long-press menu (mobile/src/components/chat/linkContextMenu.tsx → LinkContextMenuHost) that approximates the native iOS link context menu. We should eventually replace this approximation with the genuine native iOS UIContextMenu (the blurred, system-rendered "Peek and Pop" menu with link preview), which requires rendering the assistant text in a real native UITextView.
This is tracked as a future/polish item — the current JS overlay works and ships in #262. This issue captures why the native menu isn't trivially available and what the real fix looks like, so we don't relitigate it.
Background: why we have a JS overlay instead of the native menu
The native iOS link menu is produced by UITextView (via UITextViewDelegate.textView(_:menuConfigurationFor:defaultMenu:), iOS 17+, on UITextItems backed by NSLinkAttributeName/UITextItemTag ranges). React Native's <Text> is not a UITextView — on the New Architecture it renders as RCTParagraphComponentView, which draws glyphs with CoreText. There is no UITextView in the tree, so there is nothing to host the per-link UIContextMenuInteraction. That is why the long-press menu had to be re-implemented as a JS overlay.
Why expo-router Link Preview / Link.Menu does not solve this
expo-router (we're on 56.2.8) ships a real native context-menu API — Link.Trigger / Link.Preview / Link.Menu / Link.MenuAction (docs: https://docs.expo.dev/router/reference/link-preview/). It is the genuine native UIContextMenu. But it is built for in-app route navigation, not inline external hyperlinks:
- Preview is hard-disabled for external URLs. In
node_modules/expo-router/build/link/LinkWithPreview.js the preview element is nulled when the href is external:
const preview = useMemo(
() => (shouldLinkExternally(String(rest.href)) || !previewElement ? null : previewElement),
[previewElement, rest.href]
);
HrefPreview resolves the href through the router (store.getStateForHref) and renders the destination screen as the preview; an https:// URL has no route, so it falls to an "Invalid preview" placeholder.
- The trigger is a native block-host view, not an inline text run. It composes for a standalone/block link (a row, a card, a button) but not for a hyperlink mid-paragraph that must wrap across lines like text.
- Verified on the iOS 26.5 simulator (iPhone 17 Pro): rendering an inline
<Link href="https://docs.anthropic.com"><Link.Trigger>…</Link.Trigger><Link.Menu>…</Link.Menu></Link> and long-pressing it produced no context menu — the press just navigated out to the external browser. The native menu does not attach for external-href links.
So expo-router's API is the right tool for navigation links, and the wrong tool for inline external markdown links.
Proposed fix (the real one)
Render assistant markdown (or at least link-bearing inline text) through a custom Fabric native component that wraps a UITextView:
- Back link ranges with
NSLinkAttributeName + UITextItemTag.
- Implement
textView(_:menuConfigurationFor:defaultMenu:) (iOS 17+) to return our actions (Open Link → default browser, Copy Link, Share…), getting the system blur + preview for free.
- Implement
textView(_:primaryActionFor:defaultAction:) for tap (→ in-app browser, to match current behavior).
- Bridge the same scheme allowlist + host-sanitization currently in
linkContextMenu.tsx so behavior/security parity is preserved.
Alternatives considered
bluesky-social/react-native-uitextview gives a real UITextView (selection/translation) but does not expose the link-item menu delegate — it would need extending to add menuConfigurationFor.
- Block-level links via
expo-router Link.Trigger+Link.Menu (e.g. a "Sources" chip list under a message) would get the native menu for free, at the cost of inline flow. Viable for a sources UI, not for inline body links.
- Keep the JS overlay (current state). Acceptable; it just isn't the system menu (no system blur/preview, must track theme manually).
Acceptance criteria
References
Summary
Assistant markdown links in the mobile app currently get a custom JavaScript overlay for the long-press menu (
mobile/src/components/chat/linkContextMenu.tsx→LinkContextMenuHost) that approximates the native iOS link context menu. We should eventually replace this approximation with the genuine native iOSUIContextMenu(the blurred, system-rendered "Peek and Pop" menu with link preview), which requires rendering the assistant text in a real nativeUITextView.This is tracked as a future/polish item — the current JS overlay works and ships in #262. This issue captures why the native menu isn't trivially available and what the real fix looks like, so we don't relitigate it.
Background: why we have a JS overlay instead of the native menu
The native iOS link menu is produced by
UITextView(viaUITextViewDelegate.textView(_:menuConfigurationFor:defaultMenu:), iOS 17+, onUITextItems backed byNSLinkAttributeName/UITextItemTagranges). React Native's<Text>is not aUITextView— on the New Architecture it renders asRCTParagraphComponentView, which draws glyphs with CoreText. There is noUITextViewin the tree, so there is nothing to host the per-linkUIContextMenuInteraction. That is why the long-press menu had to be re-implemented as a JS overlay.Why
expo-routerLink Preview /Link.Menudoes not solve thisexpo-router(we're on56.2.8) ships a real native context-menu API —Link.Trigger/Link.Preview/Link.Menu/Link.MenuAction(docs: https://docs.expo.dev/router/reference/link-preview/). It is the genuine nativeUIContextMenu. But it is built for in-app route navigation, not inline external hyperlinks:node_modules/expo-router/build/link/LinkWithPreview.jsthe preview element is nulled when the href is external:HrefPreviewresolves the href through the router (store.getStateForHref) and renders the destination screen as the preview; anhttps://URL has no route, so it falls to an "Invalid preview" placeholder.<Link href="https://docs.anthropic.com"><Link.Trigger>…</Link.Trigger><Link.Menu>…</Link.Menu></Link>and long-pressing it produced no context menu — the press just navigated out to the external browser. The native menu does not attach for external-href links.So
expo-router's API is the right tool for navigation links, and the wrong tool for inline external markdown links.Proposed fix (the real one)
Render assistant markdown (or at least link-bearing inline text) through a custom Fabric native component that wraps a
UITextView:NSLinkAttributeName+UITextItemTag.textView(_:menuConfigurationFor:defaultMenu:)(iOS 17+) to return our actions (Open Link → default browser, Copy Link, Share…), getting the system blur + preview for free.textView(_:primaryActionFor:defaultAction:)for tap (→ in-app browser, to match current behavior).linkContextMenu.tsxso behavior/security parity is preserved.Alternatives considered
bluesky-social/react-native-uitextviewgives a realUITextView(selection/translation) but does not expose the link-item menu delegate — it would need extending to addmenuConfigurationFor.expo-routerLink.Trigger+Link.Menu(e.g. a "Sources" chip list under a message) would get the native menu for free, at the cost of inline flow. Viable for a sources UI, not for inline body links.Acceptance criteria
UIContextMenu(system blur + link preview), not the JS overlay.References
mobile/src/components/chat/linkContextMenu.tsx,BlockAssistantText.tsx,SelectableBlockText.tsxUITextViewtext item menus /menuConfigurationFor