import * as React from 'react'; import { memo, FC, useCallback, useState, useLayoutEffect } from 'react'; import { style } from 'typestyle'; import { createPortal } from 'react-dom'; import usePortal from '../../hooks/usePortal'; import useResponsive from '../../hooks/useResponsive'; import Carousel from '../Layouts/Carousel'; import Wall from '../Layouts/Wall'; import { Mosaic } from '../Layouts/Mosaic'; import PostViewer from '../PostViewer'; import { useStore } from '../../services/store'; import { IMedia } from '../../types'; import { Thumbnail } from '../Thumbnail'; import Visibility from '../Visibility'; import { Helpers } from '../../services/helpers'; import usePostViewerEvents from '../../hooks/usePostViewerEvents'; interface WidgetProps { root: HTMLElement; startDate: Date; postViewerPortalId?: string; } const Widget: FC = memo(({ root, startDate, postViewerPortalId }) => { const store = useStore(); const [hasLoaded, setHasLoaded] = useState(false); const portalElement = usePortal(postViewerPortalId, !!store.post); usePostViewerEvents(); // Measure the widget width before the first paint so carousel dimension calculations use the real value. const setStoreState = store.setStoreState; useLayoutEffect(() => { const width = Math.floor(root.getBoundingClientRect().width); setStoreState((state) => { const isMobile = state.forceMobile || Helpers.isMobileDevice(); if (state.widgetWidth === width && state.isMobile === isMobile) return state; return { ...state, isMobile, widgetWidth: width }; }); }, [root, setStoreState]); // Stable callback so useResponsive doesn't disconnect/reconnect the ResizeObserver on every render. const onResize = useCallback( (width: number) => { setStoreState((state) => { const isMobile = state.forceMobile || Helpers.isMobileDevice(); if (state.widgetWidth === width && state.isMobile === isMobile) return state; return { ...state, isMobile, widgetWidth: width }; }); }, [setStoreState], ); useResponsive(root, onResize); // Event handlers const handlePostChange = useCallback( (post: IMedia | null) => { if (post) { store.setStoreState((state) => ({ ...state, post })); Helpers.replacePostInHistory(post); } }, [store], ); const widgetLoaded = useCallback(() => { if (hasLoaded) return; const elapsedSeconds = startDate ? (new Date().getTime() - startDate.getTime()) / 1000 : undefined; setHasLoaded(true); store.triggerEvent( 'widgetLoaded', { elapsedSeconds, posts: store.data.content.medias, layout: store.data.settings.type, }, store.data.id, ); }, [hasLoaded, startDate, store]); const widgetLoadingFailed = useCallback(() => { if (hasLoaded) return; setHasLoaded(true); store.triggerEvent('widgetLoadingFailed', {}, store.data.id); }, [hasLoaded, store]); const mediaRender = useCallback( (media: IMedia, position: number) => , [], ); if (!store.data) { widgetLoadingFailed(); return null; } widgetLoaded(); const currentType = store.data.settings.type || 'wall'; return ( {(ref) => (
{ { carousel: ( mediaRender(m, i))} onPostChange={handlePostChange} /> ), mosaic: , wall: , }[currentType] }
{portalElement && store.post && createPortal(, portalElement)}
)}
); }); const widgetClass = style({ all: 'initial', $nest: { '*': { boxSizing: 'initial', }, }, }); export default Widget;