From abf129f56eb67ef954d0a8fd57f078a7d9998476 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 5 Jun 2026 19:11:29 +0700 Subject: [PATCH] Fix iOS markdown style metadata warnings Resolve markdown UITextView styles to plain React Native text styles before crossing the third-party boundary. react-native-uitextview flattens parent and child styles internally, so Unistyles metadata must not be carried into that handoff. --- .../components/markdown-text-style.test.ts | 32 +++++++++++++++++++ .../app/src/components/markdown-text-style.ts | 25 +++++++++++++++ .../app/src/components/markdown-text.ios.tsx | 11 +++++-- 3 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 packages/app/src/components/markdown-text-style.test.ts create mode 100644 packages/app/src/components/markdown-text-style.ts diff --git a/packages/app/src/components/markdown-text-style.test.ts b/packages/app/src/components/markdown-text-style.test.ts new file mode 100644 index 000000000..8094cca38 --- /dev/null +++ b/packages/app/src/components/markdown-text-style.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from "vitest"; +import { StyleSheet, type TextStyle } from "react-native"; +import { resolvePlainMarkdownTextStyle } from "@/components/markdown-text-style"; + +function unistylesStyle(id: string, style: Record) { + return { + ...style, + [`unistyles_${id}`]: { id }, + }; +} + +function uiTextViewFlatten(rootStyle: TextStyle, style: TextStyle): Record { + // react-native-uitextview/src/util.ts:8 flattens [rootStyle, style] + // before passing the result to its native View-backed components. + return { ...(StyleSheet.flatten([rootStyle, style]) as Record) }; +} + +function unistylesMetadataKeys(style: Record) { + return Object.keys(style).filter((key) => key.startsWith("unistyles_")); +} + +describe("resolvePlainMarkdownTextStyle", () => { + it("keeps UITextView from collapsing parent and child Unistyles styles into one native View style object", () => { + const merged = uiTextViewFlatten( + resolvePlainMarkdownTextStyle(unistylesStyle("paragraph", { color: "#111" })), + resolvePlainMarkdownTextStyle(unistylesStyle("text", { fontWeight: "600" })), + ); + + expect(unistylesMetadataKeys(merged)).toHaveLength(0); + expect(merged).toMatchObject({ color: "#111", fontWeight: "600" }); + }); +}); diff --git a/packages/app/src/components/markdown-text-style.ts b/packages/app/src/components/markdown-text-style.ts new file mode 100644 index 000000000..ab1ac4abf --- /dev/null +++ b/packages/app/src/components/markdown-text-style.ts @@ -0,0 +1,25 @@ +import { StyleSheet, type StyleProp, type TextStyle } from "react-native"; + +export function resolvePlainMarkdownTextStyle(style: StyleProp): TextStyle { + return stripUnistylesMetadata(StyleSheet.flatten(style) ?? {}); +} + +function stripUnistylesMetadata(style: TextStyle): TextStyle { + // iOS markdown text goes through react-native-uitextview. That library + // inherits text styles by flattening [parentStyle, childStyle] before handing + // the result to native View-backed components. If both entries are Unistyles + // styles, flattening preserves both `unistyles_*` metadata keys in one object, + // and Unistyles correctly warns that the style should have stayed array-shaped. + // + // `UITextView` is a third-party boundary, not a Unistyles-tracked component in + // our ownership model. Resolve the concrete style values before crossing that + // boundary and drop only Unistyles' private tracking metadata. This preserves + // iOS paragraph/spanning text selection while avoiding the metadata merge. + const plainStyle: Record = { ...style }; + for (const key of Object.keys(plainStyle)) { + if (key.startsWith("unistyles_")) { + delete plainStyle[key]; + } + } + return plainStyle as TextStyle; +} diff --git a/packages/app/src/components/markdown-text.ios.tsx b/packages/app/src/components/markdown-text.ios.tsx index 8b925cebd..8d50abbda 100644 --- a/packages/app/src/components/markdown-text.ios.tsx +++ b/packages/app/src/components/markdown-text.ios.tsx @@ -1,6 +1,7 @@ import { useMemo, type ReactNode } from "react"; import { 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"; interface MarkdownTextSpanProps { style?: StyleProp; @@ -26,11 +27,13 @@ export function MarkdownTextSpan({ onPress, accessibilityRole, }: MarkdownTextSpanProps) { + const plainStyle = useMemo(() => resolvePlainMarkdownTextStyle(style), [style]); + return ( @@ -59,7 +62,11 @@ export function MarkdownParagraphView({ children, }: MarkdownParagraphViewProps) { const textStyle = useMemo( - () => [paragraphStyle, MARKDOWN_PARAGRAPH_RESET] as StyleProp, + () => + resolvePlainMarkdownTextStyle([ + paragraphStyle, + MARKDOWN_PARAGRAPH_RESET, + ] as StyleProp), [paragraphStyle], ); const viewStyle = useMemo(() => [paragraphStyle, MARKDOWN_PARAGRAPH_RESET], [paragraphStyle]);