diff --git a/mobile/src/components/MobileMarkdown.file-links.test.ts b/mobile/src/components/MobileMarkdown.file-links.test.ts index 345a8f4786c..67e9141f13d 100644 --- a/mobile/src/components/MobileMarkdown.file-links.test.ts +++ b/mobile/src/components/MobileMarkdown.file-links.test.ts @@ -132,18 +132,20 @@ describe('MobileMarkdown file links', () => { }) describe('MobileMarkdown text selection', () => { - let renderer: ReactTestRenderer | null = null + // Why: definite assignment, not `| null` - react-test-renderer's types are + // unavailable to the type-aware lint pass, so a written union trips + // no-redundant-type-constituents. + let renderer!: ReactTestRenderer afterEach(() => { act(() => renderer?.unmount()) - renderer = null }) function renderMarkdown(content: string): ReactTestRenderer { act(() => { renderer = create(createElement(MobileMarkdown, { content })) }) - return renderer! + return renderer } it('makes paragraph text selectable for long-press copy', () => { diff --git a/mobile/src/components/MobileMarkdown.tsx b/mobile/src/components/MobileMarkdown.tsx index 75dfe6c19fd..39b836ae5c1 100644 --- a/mobile/src/components/MobileMarkdown.tsx +++ b/mobile/src/components/MobileMarkdown.tsx @@ -192,6 +192,17 @@ function MobileMarkdownInner({ content, fallback = '', textScale = 1, onOpenFile const text = content?.trim() ?? '' const previewText = useMemo(() => normalizeMobileMarkdownPreviewHtml(text), [text]) const blocks = useMemo(() => parseMobileMarkdown(previewText), [previewText]) + // Stable per-block keys: content-derived, with an occurrence suffix so repeated + // identical blocks stay distinct across re-parses. + const blockKeys = useMemo(() => { + const seen = new Map() + return blocks.map((block) => { + const base = `${block.type}:${'text' in block ? block.text : JSON.stringify(block)}` + const occurrence = seen.get(base) ?? 0 + seen.set(base, occurrence + 1) + return `${base}#${occurrence}` + }) + }, [blocks]) // Scale prose sizes; inline spans inherit fontSize from the wrapping Text. const scaled = (size: number): { fontSize: number; lineHeight: number } | null => textScale !== 1 ? { fontSize: size * textScale, lineHeight: (size + 6) * textScale } : null @@ -208,7 +219,7 @@ function MobileMarkdownInner({ content, fallback = '', textScale = 1, onOpenFile if (block.type === 'heading') { return ( {renderInline(block.text, onOpenFile)} @@ -217,7 +228,7 @@ function MobileMarkdownInner({ content, fallback = '', textScale = 1, onOpenFile } if (block.type === 'quote') { return ( - + {renderInline(block.text, onOpenFile)} @@ -240,7 +251,7 @@ function MobileMarkdownInner({ content, fallback = '', textScale = 1, onOpenFile ) } return ( - + {block.language ? ( {block.language} ) : null} @@ -251,7 +262,7 @@ function MobileMarkdownInner({ content, fallback = '', textScale = 1, onOpenFile if (block.type === 'image') { return ( openMarkdownHref(block.url, onOpenFile)} > @@ -268,7 +279,7 @@ function MobileMarkdownInner({ content, fallback = '', textScale = 1, onOpenFile const hiddenRows = Math.max(0, block.rows.length - visibleRows.length) const hiddenColumns = Math.max(0, block.headers.length - visibleHeaders.length) return ( - + {visibleHeaders.map((header, cellIndex) => ( @@ -299,7 +310,7 @@ function MobileMarkdownInner({ content, fallback = '', textScale = 1, onOpenFile } if (block.type === 'list') { return ( - + {block.items.map((item, itemIndex) => ( @@ -320,10 +331,10 @@ function MobileMarkdownInner({ content, fallback = '', textScale = 1, onOpenFile ) } if (block.type === 'rule') { - return + return } return ( - + {block.text.split('\n').map((line, lineIndex) => ( {lineIndex > 0 ? '\n' : null} diff --git a/mobile/src/session/MobileNativeChatMessage.test.ts b/mobile/src/session/MobileNativeChatMessage.test.ts index 27a608ee8e3..b75158e6550 100644 --- a/mobile/src/session/MobileNativeChatMessage.test.ts +++ b/mobile/src/session/MobileNativeChatMessage.test.ts @@ -182,7 +182,7 @@ describe('MobileNativeChatMessage', () => { }) function isInsidePressable(node: ReactTestInstance): boolean { - let current: ReactTestInstance | null = node.parent + let current = node.parent while (current) { if (current.type === ('Pressable' as never)) { return true