diff --git a/front/packages/primitives/src/image/blurhash.web.tsx b/front/packages/primitives/src/image/blurhash.web.tsx index 41fed85a..27e8726d 100644 --- a/front/packages/primitives/src/image/blurhash.web.tsx +++ b/front/packages/primitives/src/image/blurhash.web.tsx @@ -19,7 +19,17 @@ */ import { decode } from "blurhash"; -import { ReactElement } from "react"; +import { + HTMLAttributes, + ReactElement, + createElement, + forwardRef, + useEffect, + useImperativeHandle, + useLayoutEffect, + useRef, + useState, +} from "react"; import { useYoshiki } from "yoshiki"; import { Stylable, nativeStyleToCss } from "yoshiki/native"; import { StyleList, processStyleList } from "yoshiki/src/type"; @@ -191,28 +201,102 @@ function generatePng(width: number, height: number, rgbaString: string) { return pngString; } -export const BlurhashContainer = ({ - blurhash, - blurhashUrl, - children, - ...props -}: { - blurhash?: string; - blurhashUrl?: string; - children?: ReactElement | ReactElement[]; -}) => { +const BlurhashCanvas = forwardRef< + HTMLCanvasElement, + { + blurhash: string; + } & HTMLAttributes +>(function BlurhashCanvas({ blurhash, ...props }, forwardedRef) { + const ref = useRef(null); + const { css } = useYoshiki(); + + useImperativeHandle(forwardedRef, () => ref.current!, []); + + useLayoutEffect(() => { + if (!ref.current) return; + const pixels = decode(blurhash, 32, 32); + const ctx = ref.current.getContext("2d"); + if (!ctx) return; + const imageData = ctx.createImageData(32, 32); + imageData.data.set(pixels); + ctx.putImageData(imageData, 0, 0); + }, [blurhash]); + + return ( + + ); +}); + +const BlurhashDiv = forwardRef< + HTMLDivElement, + { blurhash: string } & HTMLAttributes +>(function BlurhashDiv({ blurhash, ...props }, ref) { const { css } = useYoshiki(); return (
+ ); +}); + +export const BlurhashContainer = ({ + blurhash, + children, + ...props +}: { + blurhash: string; + children?: ReactElement | ReactElement[]; +}) => { + const { css } = useYoshiki(); + const ref = useRef(null); + const [renderType, setRenderType] = useState<"ssr" | "hydratation" | "client">( + typeof window === "undefined" ? "ssr" : "hydratation", + ); + + useLayoutEffect(() => { + // If the html is empty, it was not SSRed. + if (ref.current?.innerHTML === '') setRenderType("client"); + }, []); + + return ( +
+ {renderType === "ssr" && } + {renderType === "client" && } + {renderType === "hydratation" && ( +
+ )} {children}
); diff --git a/front/packages/primitives/src/image/image.web.tsx b/front/packages/primitives/src/image/image.web.tsx index 2424d415..cebb2d24 100644 --- a/front/packages/primitives/src/image/image.web.tsx +++ b/front/packages/primitives/src/image/image.web.tsx @@ -37,13 +37,9 @@ export const Image = ({ }: Props & { style?: ImageStyle } & { layout: ImageLayout }) => { const { css } = useYoshiki(); const [state, setState] = useState<"loading" | "errored" | "finished">( - src ? "finished" : "errored", + src ? (typeof window === "undefined" ? "finished" : "loading") : "errored", ); - useLayoutEffect(() => { - setState("loading"); - }, []); - const border = { borderRadius: 6 } satisfies ViewStyle; if (forcedLoading) return ; @@ -55,9 +51,8 @@ export const Image = ({ ); } - const blurhashUrl = blurHashToDataURL(src.blurhash); return ( - + setState("finished")} onError={() => setState("errored")} + suppressHydrationWarning /> );