diff --git a/src/renderer/src/components/common/ErrorElement.tsx b/src/renderer/src/components/common/ErrorElement.tsx index 81411b7e90..1c9eda009e 100644 --- a/src/renderer/src/components/common/ErrorElement.tsx +++ b/src/renderer/src/components/common/ErrorElement.tsx @@ -55,9 +55,9 @@ export function ErrorElement() {

{message}

{import.meta.env.DEV && stack ? ( -
+
           {attachOpenInEditor(stack)}
-        
+ ) : null}

diff --git a/src/renderer/src/components/errors/ModalError.tsx b/src/renderer/src/components/errors/ModalError.tsx index e92c2f5235..f1b7c5c0c6 100644 --- a/src/renderer/src/components/errors/ModalError.tsx +++ b/src/renderer/src/components/errors/ModalError.tsx @@ -25,9 +25,9 @@ export const ModalErrorFallback: FC = (props) => {

{message}
{import.meta.env.DEV && stack ? ( -
+
             {attachOpenInEditor(stack)}
-          
+ ) : null}

diff --git a/src/renderer/src/components/errors/PageError.tsx b/src/renderer/src/components/errors/PageError.tsx index 1254bb6377..d0f55da0e3 100644 --- a/src/renderer/src/components/errors/PageError.tsx +++ b/src/renderer/src/components/errors/PageError.tsx @@ -17,9 +17,9 @@ export const PageErrorFallback: FC = (props) => {

{message}
{import.meta.env.DEV && stack ? ( -
+
             {attachOpenInEditor(stack)}
-          
+ ) : null}

diff --git a/src/renderer/src/components/ui/code-highlighter/shiki/Shiki.tsx b/src/renderer/src/components/ui/code-highlighter/shiki/Shiki.tsx index 66c4914d4a..907e84a099 100644 --- a/src/renderer/src/components/ui/code-highlighter/shiki/Shiki.tsx +++ b/src/renderer/src/components/ui/code-highlighter/shiki/Shiki.tsx @@ -5,10 +5,10 @@ import { import { isElectronBuild } from "@renderer/constants" import { tipcClient } from "@renderer/lib/client" import { cn } from "@renderer/lib/utils" +import { useIsomorphicLayoutEffect } from "foxact/use-isomorphic-layout-effect" import type { FC } from "react" import { useInsertionEffect, - useLayoutEffect, useMemo, useRef, useState, @@ -95,7 +95,7 @@ export const ShikiHighLighter: FC = (props) => { const codeTheme = useUISettingSelector( (s) => overrideTheme || s.codeHighlightTheme, ) - useLayoutEffect(() => { + useIsomorphicLayoutEffect(() => { let isMounted = true setLoaded(false) @@ -214,7 +214,10 @@ const ShikiCode: FC< className, )} > -

+
) } + +export const HTML: Component< + { + children: string | null | undefined + } & Partial<{ + renderInlineStyle: boolean + }> +> = ({ children, renderInlineStyle }) => { + const stableRemarkOptions = useState({ renderInlineStyle })[0] + + const markdownElement = useMemo( + () => children && parseHtml(children, { ...stableRemarkOptions }).content, + [children, stableRemarkOptions], + ) + + return markdownElement +} diff --git a/src/renderer/src/components/ui/markdown/renderers/BlockErrorBoundary.tsx b/src/renderer/src/components/ui/markdown/renderers/BlockErrorBoundary.tsx new file mode 100644 index 0000000000..06f12df4d6 --- /dev/null +++ b/src/renderer/src/components/ui/markdown/renderers/BlockErrorBoundary.tsx @@ -0,0 +1,15 @@ +import { captureException } from "@sentry/react" +import { useEffect } from "react" + +export const BlockError = (props: { error: any, message: string }) => { + useEffect(() => { + captureException(props.error) + }, []) + return ( +
+ {props.message} + +
{props.error?.message}
+
+ ) +} diff --git a/src/renderer/src/components/ui/markdown/renderers/MarkdownLink.tsx b/src/renderer/src/components/ui/markdown/renderers/MarkdownLink.tsx index 8a7fd65ebd..7d2019cc68 100644 --- a/src/renderer/src/components/ui/markdown/renderers/MarkdownLink.tsx +++ b/src/renderer/src/components/ui/markdown/renderers/MarkdownLink.tsx @@ -39,6 +39,7 @@ export const MarkdownLink = (props: LinkProps) => { }, [feedSiteUrl, props]) const entryId = isBizId(props.href) ? props.href : null const entry = useEntry(entryId) + useAuthQuery(Queries.entries.byId(entryId!), { enabled: !!entryId && !entry, staleTime: 1000 * 60 * 5, diff --git a/src/renderer/src/lib/parse-html.ts b/src/renderer/src/lib/parse-html.ts index dac2fc7506..1400b87e96 100644 --- a/src/renderer/src/lib/parse-html.ts +++ b/src/renderer/src/lib/parse-html.ts @@ -5,6 +5,7 @@ import { MarkdownLink, MarkdownP, } from "@renderer/components/ui/markdown/renderers" +import { BlockError } from "@renderer/components/ui/markdown/renderers/BlockErrorBoundary" import { Media } from "@renderer/components/ui/media" import type { Components } from "hast-util-to-jsx-runtime" import { toJsxRuntime } from "hast-util-to-jsx-runtime" @@ -18,11 +19,11 @@ import rehypeStringify from "rehype-stringify" import { unified } from "unified" import { VFile } from "vfile" -export const parseHtml = async ( +export const parseHtml = ( content: string, - options?: { + options?: Partial<{ renderInlineStyle: boolean - }, + }>, ) => { const file = new VFile(content) const { renderInlineStyle = false } = options || {} @@ -94,6 +95,7 @@ export const parseHtml = async ( if (!props.children) return null let language = "" + let codeString = null as string | null if (props.className?.includes("language-")) { language = props.className.replace("language-", "") @@ -116,7 +118,14 @@ export const parseHtml = async ( "props" in props.children && props.children.props.children if (!code) return null - codeString = extractCodeFromHtml(renderToString(code)) + try { + codeString = extractCodeFromHtml(renderToString(code)) + } catch (error) { + return createElement(BlockError, { + error, + message: "Code Block Render Error", + }) + } } if (!codeString) return null diff --git a/src/renderer/src/modules/entry-content/index.tsx b/src/renderer/src/modules/entry-content/index.tsx index 880c7520b0..3e7208d45a 100644 --- a/src/renderer/src/modules/entry-content/index.tsx +++ b/src/renderer/src/modules/entry-content/index.tsx @@ -9,6 +9,7 @@ import { useUISettingKey } from "@renderer/atoms/settings/ui" import { useWhoami } from "@renderer/atoms/user" import { m } from "@renderer/components/common/Motion" import { AutoResizeHeight } from "@renderer/components/ui/auto-resize-height" +import { HTML } from "@renderer/components/ui/markdown" import { ScrollArea } from "@renderer/components/ui/scroll-area" import { isWebBuild, ROUTE_FEED_PENDING } from "@renderer/constants" import { useEntryReadabilityToggle } from "@renderer/hooks/biz/useEntryActions" @@ -19,7 +20,6 @@ import { import { useAuthQuery, useTitle } from "@renderer/hooks/common" import { stopPropagation } from "@renderer/lib/dom" import { FeedViewType } from "@renderer/lib/enum" -import { parseHtml } from "@renderer/lib/parse-html" import type { ActiveEntryId } from "@renderer/models" import { useIsSoFWrappedElement, @@ -28,8 +28,8 @@ import { import { Queries } from "@renderer/queries" import { useEntry, useEntryReadHistory } from "@renderer/store/entry" import { useFeedById, useFeedHeaderTitle } from "@renderer/store/feed" -import type { FC, ReactNode } from "react" -import { useEffect, useLayoutEffect, useRef, useState } from "react" +import type { FC } from "react" +import { useEffect, useLayoutEffect, useRef } from "react" import { LoadingCircle } from "../../components/ui/loading" import { EntryPlaceholderDaily } from "../ai/ai-daily/EntryPlaceholderDaily" @@ -79,27 +79,7 @@ function EntryContentRender({ entryId }: { entryId: string }) { const entryHistory = useEntryReadHistory(entryId) - const [content, setContent] = useState() const readerRenderInlineStyle = useUISettingKey("readerRenderInlineStyle") - useLayoutEffect(() => { - // Fallback data, if local data is broken should fallback to cached query data. - const processContent = entry?.entries.content ?? data?.entries.content - if (processContent) { - parseHtml(processContent, { - renderInlineStyle: readerRenderInlineStyle, - }).then((parsed) => { - setContent(parsed.content) - }) - } else { - setContent(undefined) - } - }, [ - data?.entries.content, - entry?.entries.content, - readerRenderInlineStyle, - // Only for dx, hmr - parseHtml, - ]) const translation = useAuthQuery( Queries.ai.translation({ @@ -137,12 +117,13 @@ function EntryContentRender({ entryId }: { entryId: string }) { const isInReadabilityMode = useEntryIsInReadability(entryId) const scrollerRef = useRef(null) - useEffect(() => { scrollerRef.current?.scrollTo(0, 0) }, [entryId]) if (!entry) return null + const content = entry?.entries.content ?? data?.entries.content + return (
)} - {!isInReadabilityMode ? ( - content - ) : ( - - )} +
+ {!isInReadabilityMode ? ( + {content} + ) : ( + + )} +
{!content && ( @@ -305,22 +288,6 @@ const TitleMetaHandler: Component<{ const ReadabilityContent = ({ entryId }: { entryId: string }) => { const result = useEntryReadabilityContent(entryId) - const [renderer, setRenderer] = useState(null) - useLayoutEffect(() => { - if (!result) return - const { content: processContent } = result - - if (processContent) { - parseHtml(processContent, { - renderInlineStyle: true, - }).then((parsed) => { - setRenderer(parsed.content) - }) - } else { - setRenderer(null) - } - }, [result, parseHtml]) - return (
{result ? ( @@ -337,7 +304,12 @@ const ReadabilityContent = ({ entryId }: { entryId: string }) => {
)} - {renderer} +
+ + {result?.content ?? ""} + +
+ ) }