From c4f8fe5f3c95af8df82120ea4ca612d24fb09eea Mon Sep 17 00:00:00 2001 From: Kelvin Amoaba <97001695+AmoabaKelvin@users.noreply.github.com> Date: Mon, 26 Oct 2026 20:00:10 +0000 Subject: [PATCH] feat(mobile): copy button on each code block in chat (#34039) Co-authored-by: Radu Toader --- .../MobileMarkdown.file-links.test.ts | 1 + .../MobileMarkdown.rendering.test.tsx | 7 +++++++ ...mobile-markdown-android-selection.test.tsx | 1 + .../src/components/mobile-markdown-blocks.tsx | 20 ++++++++++++------- .../mobile-markdown-mermaid-routing.test.ts | 1 + .../mobile-markdown-selectable.test.tsx | 1 + .../src/components/mobile-markdown-styles.ts | 6 +++++- .../components/mobile-markdown-table.test.tsx | 1 + 8 files changed, 30 insertions(+), 8 deletions(-) diff --git a/mobile/src/components/MobileMarkdown.file-links.test.ts b/mobile/src/components/MobileMarkdown.file-links.test.ts index 2f1976c7b9e..746095fa89d 100644 --- a/mobile/src/components/MobileMarkdown.file-links.test.ts +++ b/mobile/src/components/MobileMarkdown.file-links.test.ts @@ -15,6 +15,7 @@ vi.mock('react-native', () => ({ View: 'View' })) vi.mock('./pr-sidebar/MermaidDiagram', () => ({ MermaidDiagram: 'MermaidDiagram' })) +vi.mock('./MobileSourceCopyButton', () => ({ MobileSourceCopyButton: 'MobileSourceCopyButton' })) function flattenText(node: ReactTestInstance): string { return node.children diff --git a/mobile/src/components/MobileMarkdown.rendering.test.tsx b/mobile/src/components/MobileMarkdown.rendering.test.tsx index a6d25a7ff55..a11b566a2ef 100644 --- a/mobile/src/components/MobileMarkdown.rendering.test.tsx +++ b/mobile/src/components/MobileMarkdown.rendering.test.tsx @@ -16,6 +16,7 @@ vi.mock('react-native', () => ({ View: 'View' })) vi.mock('./pr-sidebar/MermaidDiagram', () => ({ MermaidDiagram: 'MermaidDiagram' })) +vi.mock('./MobileSourceCopyButton', () => ({ MobileSourceCopyButton: 'MobileSourceCopyButton' })) let renderer: ReactTestRenderer | null = null @@ -127,6 +128,12 @@ describe('MobileMarkdown rendering', () => { expect(texts(render(content))).toEqual(expected) }) + it('gives each code block its own copy button holding only that code', () => { + const tree = render('Run this:\n\n```sh\npnpm test\n```\n\nthen:\n\n```\nexit\n```') + const buttons = tree.root.findAll((node) => String(node.type) === 'MobileSourceCopyButton') + expect(buttons.map((button) => button.props.text)).toEqual(['pnpm test', 'exit']) + }) + it('C12: opens a link with brackets in it, and one with a title, at their own address', () => { const tree = render( '[wiki](https://en.wikipedia.org/wiki/Foo_(bar)) and [docs](https://x.com/a "The docs")' diff --git a/mobile/src/components/mobile-markdown-android-selection.test.tsx b/mobile/src/components/mobile-markdown-android-selection.test.tsx index c2983b75d84..ffd00098a89 100644 --- a/mobile/src/components/mobile-markdown-android-selection.test.tsx +++ b/mobile/src/components/mobile-markdown-android-selection.test.tsx @@ -15,6 +15,7 @@ vi.mock('react-native', () => ({ })) vi.mock('./pr-sidebar/MermaidDiagram', () => ({ MermaidDiagram: 'MermaidDiagram' })) +vi.mock('./MobileSourceCopyButton', () => ({ MobileSourceCopyButton: 'MobileSourceCopyButton' })) const CONTENT = [ '# Heading', diff --git a/mobile/src/components/mobile-markdown-blocks.tsx b/mobile/src/components/mobile-markdown-blocks.tsx index 99d93d29763..fced4c1c260 100644 --- a/mobile/src/components/mobile-markdown-blocks.tsx +++ b/mobile/src/components/mobile-markdown-blocks.tsx @@ -21,6 +21,7 @@ import { MARKDOWN_TONES, scaledMarkdownText, styles } from './mobile-markdown-st import { MarkdownText } from './mobile-markdown-text' import { codeFenceClosed, plainText } from './mobile-markdown-tree' import { MermaidDiagram } from './pr-sidebar/MermaidDiagram' +import { MobileSourceCopyButton } from './MobileSourceCopyButton' /** Prose base size — passed to MermaidDiagram fallback mono text. */ const MERMAID_BASE = 13 @@ -54,13 +55,18 @@ function renderCode(node: Code, key: number, ctx: MarkdownBlockContext): ReactNo } return ( - {node.lang ? {node.lang} : null} - - {node.value} - + + {node.lang ? {node.lang} : null} + + {node.value} + + + {node.value ? ( + + ) : null} ) } diff --git a/mobile/src/components/mobile-markdown-mermaid-routing.test.ts b/mobile/src/components/mobile-markdown-mermaid-routing.test.ts index e6e305edcf9..58851169252 100644 --- a/mobile/src/components/mobile-markdown-mermaid-routing.test.ts +++ b/mobile/src/components/mobile-markdown-mermaid-routing.test.ts @@ -14,6 +14,7 @@ vi.mock('react-native', () => ({ View: 'View' })) vi.mock('./pr-sidebar/MermaidDiagram', () => ({ MermaidDiagram: 'MermaidDiagram' })) +vi.mock('./MobileSourceCopyButton', () => ({ MobileSourceCopyButton: 'MobileSourceCopyButton' })) let renderer: ReactTestRenderer | undefined diff --git a/mobile/src/components/mobile-markdown-selectable.test.tsx b/mobile/src/components/mobile-markdown-selectable.test.tsx index 84b8824f251..e0cec0e2e32 100644 --- a/mobile/src/components/mobile-markdown-selectable.test.tsx +++ b/mobile/src/components/mobile-markdown-selectable.test.tsx @@ -14,6 +14,7 @@ vi.mock('react-native', () => ({ })) vi.mock('./pr-sidebar/MermaidDiagram', () => ({ MermaidDiagram: 'MermaidDiagram' })) +vi.mock('./MobileSourceCopyButton', () => ({ MobileSourceCopyButton: 'MobileSourceCopyButton' })) type TestNode = { type: string diff --git a/mobile/src/components/mobile-markdown-styles.ts b/mobile/src/components/mobile-markdown-styles.ts index 5784f5a3b8c..631cebbf559 100644 --- a/mobile/src/components/mobile-markdown-styles.ts +++ b/mobile/src/components/mobile-markdown-styles.ts @@ -62,8 +62,12 @@ export const styles = StyleSheet.create({ borderWidth: 1, borderColor: colors.borderSubtle, borderRadius: radii.input, - padding: spacing.sm + padding: spacing.sm, + flexDirection: 'row', + alignItems: 'flex-start', + gap: spacing.xs }, + codeBody: { flex: 1 }, codeLanguage: { fontSize: 10, color: colors.textMuted, diff --git a/mobile/src/components/mobile-markdown-table.test.tsx b/mobile/src/components/mobile-markdown-table.test.tsx index 8a80c8d92ef..d718eeb80e0 100644 --- a/mobile/src/components/mobile-markdown-table.test.tsx +++ b/mobile/src/components/mobile-markdown-table.test.tsx @@ -15,6 +15,7 @@ vi.mock('react-native', () => ({ })) vi.mock('./pr-sidebar/MermaidDiagram', () => ({ MermaidDiagram: 'MermaidDiagram' })) +vi.mock('./MobileSourceCopyButton', () => ({ MobileSourceCopyButton: 'MobileSourceCopyButton' })) /** Each rendered table row's cell styles, header row first. */ function renderTableCellStyles(content: string): unknown[][] {