feat(mobile/html-renderer): Add WrappedElementProvider for dynamic element tracking

- Introduce WrappedElementProvider to track element size and position
- Create ProviderComposer for composing multiple context providers
- Add hooks for accessing wrapped element details
- Update HTML component to use new provider
- Enhance image rendering with dynamic width calculation

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei 2025-02-19 21:23:18 +08:00
parent ba0c9119dc
commit 5d340ec0ca
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
8 changed files with 987 additions and 2202 deletions

View File

@ -12,7 +12,9 @@
"@follow/components": "workspace:*",
"@follow/types": "workspace:*",
"clsx": "2.1.1",
"foxact": "0.2.44",
"jotai": "2.11.3",
"react": "18.3.1",
"react-blurhash": "0.3.0"
}
}

View File

@ -3,6 +3,7 @@ import { clsx } from "clsx"
import katexStyle from "katex/dist/katex.min.css?raw"
import { createElement, Fragment, useEffect, useMemo, useState } from "react"
import { WrappedElementProvider } from "./common/WrappedElementProvider"
import { MarkdownRenderContainerRefContext } from "./components/__internal/ctx"
import { parseHtml } from "./parser"
@ -58,15 +59,18 @@ export const HTML = <A extends keyof JSX.IntrinsicElements = "div">(props: HTMLP
return (
<MarkdownRenderContainerRefContext.Provider value={refElement}>
<MemoedDangerousHTMLStyle>{katexStyle}</MemoedDangerousHTMLStyle>
{createElement(
as,
{
...rest,
ref: setRefElement,
className: clsx("prose mx-auto px-3", "dark:prose-invert"),
},
markdownElement,
)}
<WrappedElementProvider>
{createElement(
as,
{
...rest,
ref: setRefElement,
className: clsx("prose mx-auto px-3", "dark:prose-invert"),
},
markdownElement,
)}
</WrappedElementProvider>
{!!accessory && <Fragment key={shouldForceReMountKey}>{accessory}</Fragment>}
</MarkdownRenderContainerRefContext.Provider>
)

View File

@ -0,0 +1,10 @@
import type { JSX } from "react"
import * as React from "react"
export const ProviderComposer: Component<{
contexts: JSX.Element[]
}> = ({ contexts, children }) => {
return contexts.reduceRight((kids: any, parent: any) => {
return React.cloneElement(parent, { children: kids })
}, children)
}

View File

@ -0,0 +1,103 @@
import { cn } from "@follow/utils/utils"
import { createContextState } from "foxact/create-context-state"
import { useIsomorphicLayoutEffect } from "foxact/use-isomorphic-layout-effect"
import type * as React from "react"
import { memo } from "react"
import { ProviderComposer } from "./ProviderComposer"
const [WrappedElementProviderInternal, useWrappedElement, useSetWrappedElement] =
createContextState<HTMLDivElement | null>(undefined as any)
const [ElementSizeProviderInternal, useWrappedElementSize, useSetWrappedElementSize] =
createContextState({
h: 0,
w: 0,
})
const [ElementPositionProviderInternal, useWrappedElementPosition, useSetElementPosition] =
createContextState({
x: 0,
y: 0,
})
const Providers = [
<WrappedElementProviderInternal key="ArticleElementProviderInternal" />,
<ElementSizeProviderInternal key="ElementSizeProviderInternal" />,
<ElementPositionProviderInternal key="ElementPositionProviderInternal" />,
]
interface WrappedElementProviderProps {
as?: keyof React.JSX.IntrinsicElements
}
export const WrappedElementProvider: Component<WrappedElementProviderProps> = ({
children,
className,
...props
}) => (
<ProviderComposer contexts={Providers}>
<ElementResizeObserver />
<Content {...props} className={className}>
{children}
</Content>
</ProviderComposer>
)
const ElementResizeObserver = () => {
const setSize = useSetWrappedElementSize()
const setPos = useSetElementPosition()
const $element = useWrappedElement()
useIsomorphicLayoutEffect(() => {
if (!$element) return
const { height, width, left, top } = $element.getBoundingClientRect()
setSize({ h: height, w: width })
const pageX = window.scrollX + left
const pageY = window.scrollY + top
setPos({ x: pageX, y: pageY })
const observer = new ResizeObserver((entries) => {
const entry = entries[0]
if (!entry) return
const { height, width } = entry.contentRect
const { left, top } = $element.getBoundingClientRect()
const pageX = window.scrollX + left
const pageY = window.scrollY + top
setSize((size) => {
if (size.h === height && size.w === width) return size
return { h: height, w: width }
})
setPos((pos) => {
if (pos.x === pageX && pos.y === pageY) return pos
return { x: pageX, y: pageY }
})
})
observer.observe($element)
return () => {
observer.unobserve($element)
observer.disconnect()
}
}, [$element])
return null
}
const Content: Component<WrappedElementProviderProps> = memo(
({ children, className, as = "div" }) => {
const setElement = useSetWrappedElement()
const As = as as any
return (
<As className={cn("relative", className)} ref={setElement}>
{children}
</As>
)
},
)
Content.displayName = "ArticleElementProviderContent"
export { useSetWrappedElement, useWrappedElement, useWrappedElementPosition, useWrappedElementSize }

View File

@ -1,13 +1,13 @@
import clsx from "clsx"
import { useAtomValue } from "jotai"
import { useContext, useMemo, useRef, useState } from "react"
import { useMemo, useRef, useState } from "react"
import { Blurhash } from "react-blurhash"
import { entryAtom } from "~/atoms"
import { useWrappedElementSize } from "~/common/WrappedElementProvider"
import type { HTMLProps } from "~/HTML"
import { calculateDimensions } from "./__internal/calculateDimensions"
import { MarkdownRenderContainerRefContext } from "./__internal/ctx"
const protocol = "follow-xhr"
export const MarkdownImage = (props: HTMLProps<"img">) => {
@ -23,20 +23,21 @@ export const MarkdownImage = (props: HTMLProps<"img">) => {
const entry = useAtomValue(entryAtom)
const [isLoading, setIsLoading] = useState(true)
const ref = useContext(MarkdownRenderContainerRefContext)
const image = entry?.media.find((media) => media.url === src)
const { w } = useWrappedElementSize()
const { height: scaleHeight, width: scaleWidth } = useMemo(
() =>
calculateDimensions({
width: image?.width,
height: image?.height,
max: {
width: ref?.clientWidth ?? 0,
width: w,
height: window.innerHeight,
},
}),
[image?.width, image?.height, ref?.clientWidth],
[image?.width, image?.height, w],
)
return (

View File

@ -7,14 +7,12 @@ import { viteRenderBaseConfig } from "../../../../configs/vite.render.config"
import { astPlugin } from "../../../../plugins/vite/ast"
// const isDev = process.env.NODE_ENV === "development"
const isCI = !!process.env.CI
// const isCI = !!process.env.CI
export default defineConfig({
...viteRenderBaseConfig,
base: "",
build: {
outDir: !isCI
? path.resolve(import.meta.dirname, "../../../../out/rn-web/html-renderer")
: path.resolve("/tmp/rn-web/html-renderer"),
outDir: path.resolve(import.meta.dirname, "../../../../out/rn-web/html-renderer"),
},
resolve: {
alias: {

View File

@ -41,6 +41,7 @@
"polyfill-optimize": "pnpx nolyfill install",
"prepare": "simple-git-hooks",
"publish": "electron-vite build && electron-forge publish",
"reinstall": "rm -rf node_modules && rm -rf apps/**/node_modules && rm -rf packages/**/node_modules && pnpm install",
"start": "electron-vite preview",
"sync:ab": "tsx scripts/pull-ab-flags.ts",
"sync:icons": "tsx scripts/svg-to-rn.ts && prettier --write apps/mobile/src/icons/**/*.tsx",
@ -147,8 +148,6 @@
"@microflash/remark-callout-directives": "patches/@microflash__remark-callout-directives.patch"
},
"overrides": {
"@types/react": "18.3.12",
"@types/react-dom": "18.3.1",
"app-builder-lib": "25.1.8",
"electron": "34.2.0",
"esbuild": "0.24.2",
@ -156,7 +155,6 @@
"is-core-module": "npm:@nolyfill/is-core-module@1",
"isarray": "npm:@nolyfill/isarray@1",
"lightningcss": "1.29.1",
"react": "18.3.1",
"typescript": "5.7.3",
"unist-util-visit-parents": "5.1.3",
"vfile": "5.3.7"

File diff suppressed because it is too large Load Diff