From f5158ee6cc46f7da5357e053c541235f2ee3da14 Mon Sep 17 00:00:00 2001 From: DIYgod Date: Tue, 14 Apr 2026 12:34:38 +0800 Subject: [PATCH] fix(mobile): restore spotlight highlights --- apps/mobile/ios/Folo/Info.plist | 2 +- .../entry-list/templates/EntryTranslation.tsx | 15 ++++--- .../src/modules/spotlight/HighlightedText.tsx | 39 +++++++++++++++++++ apps/mobile/web-app/html-renderer/src/App.tsx | 9 ++++- .../mobile/web-app/html-renderer/src/HTML.tsx | 12 ++++-- .../web-app/html-renderer/src/parser.tsx | 12 +++++- .../html-renderer/src/spotlight.test.tsx | 28 +++++++++++++ 7 files changed, 104 insertions(+), 13 deletions(-) create mode 100644 apps/mobile/src/modules/spotlight/HighlightedText.tsx diff --git a/apps/mobile/ios/Folo/Info.plist b/apps/mobile/ios/Folo/Info.plist index 7ea05dde3..8c92f272c 100644 --- a/apps/mobile/ios/Folo/Info.plist +++ b/apps/mobile/ios/Folo/Info.plist @@ -54,7 +54,7 @@ CFBundleVersion - 2 + 3 ITSAppUsesNonExemptEncryption LSApplicationCategoryType diff --git a/apps/mobile/src/modules/entry-list/templates/EntryTranslation.tsx b/apps/mobile/src/modules/entry-list/templates/EntryTranslation.tsx index b428e2fe0..819d9a3ca 100644 --- a/apps/mobile/src/modules/entry-list/templates/EntryTranslation.tsx +++ b/apps/mobile/src/modules/entry-list/templates/EntryTranslation.tsx @@ -1,9 +1,11 @@ import { useMemo } from "react" import type { TextProps } from "react-native" -import { View } from "react-native" +import { Text as RNText, View } from "react-native" import { useGeneralSettingKey } from "@/src/atoms/settings/general" +import { useSpotlightSettingKey } from "@/src/atoms/settings/spotlight" import { Text } from "@/src/components/ui/typography/Text" +import { HighlightedText } from "@/src/modules/spotlight/HighlightedText" export const EntryTranslation = ({ source, @@ -22,6 +24,7 @@ export const EntryTranslation = ({ bilingual?: boolean } & TextProps) => { const bilingualFinal = useGeneralSettingKey("translationMode") === "bilingual" || bilingual + const spotlightRules = useSpotlightSettingKey("spotlights") const nextSource = useMemo(() => { if (!source) { return "" @@ -37,25 +40,27 @@ export const EntryTranslation = ({ if (!bilingualFinal) { return ( - {nextTarget || nextSource} + ) } if (inline) { return ( - {`${nextTarget ? `${nextTarget} ⇋ ` : ""}${nextSource}`} + {nextTarget ? : null} + {nextTarget ? {" ⇋ "} : null} + ) } return ( - {nextSource} + {nextTarget && ( - {nextTarget} + )} diff --git a/apps/mobile/src/modules/spotlight/HighlightedText.tsx b/apps/mobile/src/modules/spotlight/HighlightedText.tsx new file mode 100644 index 000000000..e4701122d --- /dev/null +++ b/apps/mobile/src/modules/spotlight/HighlightedText.tsx @@ -0,0 +1,39 @@ +import type { SpotlightRule } from "@follow/shared/spotlight" +import { Fragment, useMemo } from "react" +import { Text as RNText } from "react-native" + +import { getHighlightedTextSegments } from "./highlightedTextSegments" +import { getHighlightedTextSegmentStyle } from "./highlightedTextStyle" + +export const HighlightedText = ({ + text, + rules, +}: { + text?: string | null + rules: SpotlightRule[] +}) => { + const segments = useMemo(() => getHighlightedTextSegments(text, rules), [rules, text]) + const keyedSegments = useMemo(() => { + let offset = 0 + + return segments.map((segment) => { + const key = segment.highlight ? `${offset}-${segment.highlight.ruleId}-${segment.text}` : null + offset += segment.text.length + return { key: key ?? `${offset}-${segment.text}`, segment } + }) + }, [segments]) + + return ( + <> + {keyedSegments.map(({ key, segment }) => + segment.highlight ? ( + + {segment.text} + + ) : ( + {segment.text} + ), + )} + + ) +} diff --git a/apps/mobile/web-app/html-renderer/src/App.tsx b/apps/mobile/web-app/html-renderer/src/App.tsx index e34036a19..922eb7cc7 100644 --- a/apps/mobile/web-app/html-renderer/src/App.tsx +++ b/apps/mobile/web-app/html-renderer/src/App.tsx @@ -1,6 +1,6 @@ import { createStore, Provider, useAtomValue } from "jotai" -import { entryAtom, noMediaAtom, readerRenderInlineStyleAtom } from "./atoms" +import { entryAtom, noMediaAtom, readerRenderInlineStyleAtom, spotlightAtom } from "./atoms" import { HTML } from "./HTML" import { WebViewBridgeManager } from "./managers/webview-bridge" @@ -14,10 +14,15 @@ export const App = () => { const entry = useAtomValue(entryAtom, { store }) const readerRenderInlineStyle = useAtomValue(readerRenderInlineStyleAtom, { store }) const noMedia = useAtomValue(noMediaAtom, { store }) + const spotlightRules = useAtomValue(spotlightAtom, { store }) return ( - + {entry?.content} diff --git a/apps/mobile/web-app/html-renderer/src/HTML.tsx b/apps/mobile/web-app/html-renderer/src/HTML.tsx index 0416abcfa..2f5f9f6c9 100644 --- a/apps/mobile/web-app/html-renderer/src/HTML.tsx +++ b/apps/mobile/web-app/html-renderer/src/HTML.tsx @@ -1,4 +1,5 @@ import { MemoedDangerousHTMLStyle } from "@follow/components/common/MemoedDangerousHTMLStyle.js" +import type { SpotlightRule } from "@follow/shared/spotlight" import { clsx } from "clsx" import katexStyle from "katex/dist/katex.min.css?raw" import * as React from "react" @@ -14,6 +15,7 @@ export type HTMLProps = { accessory?: React.ReactNode noMedia?: boolean + spotlightRules?: SpotlightRule[] } & React.JSX.IntrinsicElements[A] & Partial<{ renderInlineStyle: boolean @@ -25,25 +27,29 @@ export const HTML = (props: as = "article", accessory, noMedia, + spotlightRules, ...rest } = props const [remarkOptions, setRemarkOptions] = useState({ renderInlineStyle, noMedia, + spotlightRules, }) const [shouldForceReMountKey, setShouldForceReMountKey] = useState(0) useEffect(() => { setRemarkOptions((options) => { - if (JSON.stringify(options) === JSON.stringify({ renderInlineStyle, noMedia })) { + if ( + JSON.stringify(options) === JSON.stringify({ renderInlineStyle, noMedia, spotlightRules }) + ) { return options } setShouldForceReMountKey((key) => key + 1) - return { ...options, renderInlineStyle, noMedia } + return { ...options, renderInlineStyle, noMedia, spotlightRules } }) - }, [renderInlineStyle, noMedia]) + }, [renderInlineStyle, noMedia, spotlightRules]) const [refElement, setRefElement] = useState(null) diff --git a/apps/mobile/web-app/html-renderer/src/parser.tsx b/apps/mobile/web-app/html-renderer/src/parser.tsx index f94859b90..b20560a66 100644 --- a/apps/mobile/web-app/html-renderer/src/parser.tsx +++ b/apps/mobile/web-app/html-renderer/src/parser.tsx @@ -1,5 +1,6 @@ import { MemoedDangerousHTMLStyle } from "@follow/components/common/MemoedDangerousHTMLStyle.jsx" import { Checkbox } from "@follow/components/ui/checkbox/index.jsx" +import type { SpotlightRule } from "@follow/shared/spotlight" import { parseHtml as parseHtmlGeneral } from "@follow/utils/html" import type { Components } from "hast-util-to-jsx-runtime" import { createElement } from "react" @@ -9,8 +10,9 @@ import { createHeadingRenderer, MarkdownLink, MarkdownP } from "./components" import { MarkdownImage } from "./components/image" import { Math } from "./components/math" import { ShikiHighLighter } from "./components/shiki" +import { applySpotlightToHtmlRendererTree } from "./spotlight" -const Style: Components["style"] = ({ node, ...props }) => { +const renderStyleTag: Components["style"] = ({ node, ...props }) => { if (typeof props.children === "string") { return createElement(MemoedDangerousHTMLStyle, null, props.children) } @@ -22,10 +24,16 @@ export const parseHtml = ( options?: Partial<{ renderInlineStyle: boolean noMedia?: boolean + spotlightRules?: SpotlightRule[] }>, ) => { + const spotlightRules = options?.spotlightRules ?? [] + return parseHtmlGeneral(content, { ...options, + hastTransform: (tree) => { + applySpotlightToHtmlRendererTree(tree, spotlightRules) + }, components: { a: ({ node, ...props }) => { // Ignore link wrapper when child is an image to ensure image preview @@ -62,7 +70,7 @@ export const parseHtml = ( h6: (props) => { return createHeadingRenderer(6)(props) }, - style: Style, + style: renderStyleTag, img: ({ node, ...props }) => { return createElement(MarkdownImage, props as any) }, diff --git a/apps/mobile/web-app/html-renderer/src/spotlight.test.tsx b/apps/mobile/web-app/html-renderer/src/spotlight.test.tsx index 78cbcc27f..3c6a33d66 100644 --- a/apps/mobile/web-app/html-renderer/src/spotlight.test.tsx +++ b/apps/mobile/web-app/html-renderer/src/spotlight.test.tsx @@ -5,6 +5,7 @@ import { beforeEach, describe, expect, test, vi } from "vitest" import { entryAtom, spotlightAtom } from "./atoms" import { WebViewBridgeManager } from "./managers/webview-bridge" +import { parseHtml } from "./parser" import { applySpotlightToHtmlRendererTree } from "./spotlight" const spotlightRules: SpotlightRule[] = [ @@ -212,3 +213,30 @@ describe("WebViewBridgeManager", () => { expect(store.get(spotlightAtom)).toEqual(spotlightRules) }) }) + +describe("parseHtml", () => { + test("injects spotlight markup when parser receives spotlight rules", () => { + const { hastTree } = parseHtml("

alpha beta

", { spotlightRules }) + + expect(hastTree).toMatchObject({ + children: [ + { + type: "element", + tagName: "p", + children: [ + { + type: "element", + tagName: "span", + properties: expect.objectContaining({ + "data-spotlight-rule-id": "alpha-rule", + style: "background-color:#ffcc00CC;border-radius:5px;padding-inline:2px;", + }), + children: [{ type: "text", value: "alpha" }], + }, + { type: "text", value: " beta" }, + ], + }, + ], + }) + }) +})