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:
parent
ba0c9119dc
commit
5d340ec0ca
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
3031
pnpm-lock.yaml
3031
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue