diff --git a/packages/app/src/components/markdown-text-selection.test.ts b/packages/app/src/components/markdown-text-selection.test.ts new file mode 100644 index 000000000..6aa42afa0 --- /dev/null +++ b/packages/app/src/components/markdown-text-selection.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from "vitest"; +import { iosMarkdownTextIsSelectable } from "./markdown-text-selection"; + +describe("markdown text selection", () => { + it("uses plain text only for iOS table cells", () => { + expect({ + tableCell: iosMarkdownTextIsSelectable("table-cell"), + prose: iosMarkdownTextIsSelectable("prose"), + }).toEqual({ + tableCell: false, + prose: true, + }); + }); +}); diff --git a/packages/app/src/components/markdown-text-selection.tsx b/packages/app/src/components/markdown-text-selection.tsx new file mode 100644 index 000000000..f83ff6a25 --- /dev/null +++ b/packages/app/src/components/markdown-text-selection.tsx @@ -0,0 +1,17 @@ +import { createContext, type ReactNode, useContext } from "react"; + +export type MarkdownTextSurface = "prose" | "table-cell"; + +const MarkdownTextSurfaceContext = createContext("prose"); + +export function MarkdownTableCellText({ children }: { children: ReactNode }) { + return {children}; +} + +export function useMarkdownTextSurface(): MarkdownTextSurface { + return useContext(MarkdownTextSurfaceContext); +} + +export function iosMarkdownTextIsSelectable(surface: MarkdownTextSurface): boolean { + return surface !== "table-cell"; +} diff --git a/packages/app/src/components/markdown-text.ios.tsx b/packages/app/src/components/markdown-text.ios.tsx index 7fc07ae09..b3b6607e0 100644 --- a/packages/app/src/components/markdown-text.ios.tsx +++ b/packages/app/src/components/markdown-text.ios.tsx @@ -1,7 +1,18 @@ import { useMemo, type ReactNode } from "react"; -import { View, type StyleProp, type TextProps, type TextStyle, type ViewStyle } from "react-native"; +import { + Text, + View, + type StyleProp, + type TextProps, + type TextStyle, + type ViewStyle, +} from "react-native"; import { UITextView } from "react-native-uitextview"; import { resolvePlainMarkdownTextStyle } from "@/components/markdown-text-style"; +import { + iosMarkdownTextIsSelectable, + useMarkdownTextSurface, +} from "@/components/markdown-text-selection"; interface MarkdownTextSpanProps { style?: StyleProp; @@ -29,6 +40,22 @@ export function MarkdownTextSpan({ accessibilityRole, }: MarkdownTextSpanProps) { const plainStyle = useMemo(() => resolvePlainMarkdownTextStyle(style), [style]); + const surface = useMarkdownTextSurface(); + + // Each selectable span creates a UIKit UITextView with a window-level tap recognizer. + // A large table would create one per cell and make every app touch fan out across them. + if (!iosMarkdownTextIsSelectable(surface)) { + return ( + + {children} + + ); + } return ( ); }, + th: (node: ASTNode, children: ReactNode[], _parent: ASTNode[], styles: MarkdownStyles) => ( + + {children} + + ), + td: (node: ASTNode, children: ReactNode[], _parent: ASTNode[], styles: MarkdownStyles) => ( + + {children} + + ), paragraph: ( node: ASTNode, children: ReactNode[], diff --git a/packages/app/src/components/message.tsx b/packages/app/src/components/message.tsx index 033aa477e..af51a82ca 100644 --- a/packages/app/src/components/message.tsx +++ b/packages/app/src/components/message.tsx @@ -12,6 +12,7 @@ import { } from "react-native"; import { useTranslation } from "react-i18next"; import { MarkdownParagraphView, MarkdownTextSpan } from "@/components/markdown-text"; +import { MarkdownTableCellText } from "@/components/markdown-text-selection"; import * as React from "react"; import { useState, @@ -1825,6 +1826,16 @@ export const AssistantMessage = memo(function AssistantMessage({ ); }, + th: (node: ASTNode, children: ReactNode[], _parent: ASTNode[], styles: MarkdownStyles) => ( + + {children} + + ), + td: (node: ASTNode, children: ReactNode[], _parent: ASTNode[], styles: MarkdownStyles) => ( + + {children} + + ), paragraph: ( node: ASTNode, children: ReactNode[],