Skip to content

Commit

Permalink
feat(mobile/html-renderer): Add WrappedElementProvider for dynamic el…
Browse files Browse the repository at this point in the history
…ement 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>
  • Loading branch information
Innei committed Feb 19, 2025
1 parent ba0c911 commit 5d340ec
Show file tree
Hide file tree
Showing 8 changed files with 1,009 additions and 2,224 deletions.
2 changes: 2 additions & 0 deletions apps/mobile/web-app/html-renderer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
22 changes: 13 additions & 9 deletions apps/mobile/web-app/html-renderer/src/HTML.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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>
)
Expand Down
10 changes: 10 additions & 0 deletions apps/mobile/web-app/html-renderer/src/common/ProviderComposer.tsx
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
@@ -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 }
11 changes: 6 additions & 5 deletions apps/mobile/web-app/html-renderer/src/components/image.tsx
Original file line number Diff line number Diff line change
@@ -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">) => {
Expand All @@ -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 (
Expand Down
6 changes: 2 additions & 4 deletions apps/mobile/web-app/html-renderer/vite.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
4 changes: 1 addition & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -147,16 +148,13 @@
"@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",
"expo-modules-core": "2.2.0",
"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"
Expand Down
Loading

0 comments on commit 5d340ec

Please sign in to comment.