From a2a0b6d765ae07059ec76613b97b2285e1dc5aa1 Mon Sep 17 00:00:00 2001 From: Calvin Walton Date: Fri, 19 Sep 2025 16:20:23 -0400 Subject: [PATCH] Revert "Revert "Merge pull request #288 from Tainan404/add-support-for-layout-swap"" --- src/components/player/content/index.js | 18 ++++++++- src/components/presentation/index.js | 5 ++- src/components/thumbnails/index.js | 53 ++++++++++++++++++++++++-- src/components/tldraw/index.js | 5 ++- src/components/tldraw_v2/index.js | 4 +- src/components/utils/hooks/index.js | 22 ++++++++++- src/config.js | 1 + src/utils/builder.js | 21 +++++++++- src/utils/constants.js | 1 + src/utils/data/storage.js | 5 +++ src/utils/data/validators.js | 21 +++++++++- 11 files changed, 142 insertions(+), 14 deletions(-) diff --git a/src/components/player/content/index.js b/src/components/player/content/index.js index 58aa5596..6502f312 100644 --- a/src/components/player/content/index.js +++ b/src/components/player/content/index.js @@ -4,7 +4,7 @@ import Presentation from 'components/presentation'; import TldrawPresentation from 'components/tldraw'; import TldrawPresentationV2 from 'components/tldraw_v2'; import { getTldrawBbbVersion } from 'utils/tldraw'; -import { useCurrentInterval } from 'components/utils/hooks'; +import { useCurrentInterval, useShouldShowScreenShare } from 'components/utils/hooks'; import Screenshare from 'components/screenshare'; import Thumbnails from 'components/thumbnails'; import FullscreenButton from 'components/player/buttons/fullscreen'; @@ -26,6 +26,8 @@ const Content = ({ index, } = useCurrentInterval(storage.tldraw); + const shouldShowScreenshare = useShouldShowScreenShare(); + if (layout.single) return null; const isTldrawWhiteboard = storage.tldraw.length || @@ -58,7 +60,19 @@ const Content = ({ />
{presentation} - {layout.screenshare ? : null} + {layout.screenshare ? ( + // video-js doesn't mount properly when not mounted in time + + + + ): null}
{ const viewBox = getViewBox(currentPanzoomIndex); const started = currentPanzoomIndex !== -1; - + const shouldShowScreenshare = useShouldShowScreenShare(); return (
diff --git a/src/components/thumbnails/index.js b/src/components/thumbnails/index.js index fb750f4f..a121952c 100644 --- a/src/components/thumbnails/index.js +++ b/src/components/thumbnails/index.js @@ -1,4 +1,4 @@ -import React, { useEffect, useRef } from 'react'; +import React, { useEffect, useMemo, useRef } from 'react'; import PropTypes from 'prop-types'; import { defineMessages, @@ -34,7 +34,7 @@ const propTypes = { }; const defaultProps = { - handleSearch: () => { }, + handleSearch: () => {}, interactive: false, search: [], }; @@ -44,7 +44,6 @@ const Thumbnails = ({ interactive, search, }) => { - const currentIndex = useCurrentIndex(storage.thumbnails); const interaction = useRef(false); const firstNode = useRef(); const currentNode = useRef(); @@ -77,6 +76,52 @@ const Thumbnails = ({ } }); + const items = useMemo(()=> { + const thumbnails = storage.thumbnails; + const layoutSwap = storage.layoutSwap ?? []; + const merged = [...thumbnails, ...layoutSwap]; + const sorted = merged.sort((a, b) => a.timestamp - b.timestamp); + + const addThumbsForSwap = sorted.map((item, index, arr) => { + const previousItem = arr[index - 1]; + const nextItem = arr[index + 1]; + if (item.hasOwnProperty('showScreenshare')) { + if (!item.showScreenshare) { + const previousThumbs = arr.slice(0, index) + const Thumbnail = previousThumbs.find((t) => t.src && t.src !== 'screenshare'); + return { + ...item, + src: Thumbnail?.src ?? '', + alt: Thumbnail?.alt ?? '', + }; + } else if ( + item.showScreenshare + && (nextItem && nextItem.src !== 'screenshare') + && (previousItem && previousItem.src !== 'screenshare') + ) { + return { + ...item, + src: 'screenshare', + alt: 'screenshare', + } + } + return null; + } + return item; + }).filter((item) => item !== null); + + const reworkIds = addThumbsForSwap.map((item, index) => { + return { + ...item, + id: index + 1, + } + }); + + return reworkIds; + }, [storage.thumbnails, storage.layoutSwap]); + + const currentIndex = useCurrentIndex(items); + return (
interaction.current = false} tabIndex="0" > - {storage.thumbnails.reduce((result, item, index) => { + {items.reduce((result, item, index) => { if (!isFiltered(index)) { const active = index === currentIndex; diff --git a/src/components/tldraw/index.js b/src/components/tldraw/index.js index 80ef697d..42191818 100644 --- a/src/components/tldraw/index.js +++ b/src/components/tldraw/index.js @@ -18,6 +18,7 @@ import { useCurrentContent, useCurrentIndex, useCurrentInterval, + useShouldShowScreenShare, } from 'components/utils/hooks'; import { ID } from 'utils/constants'; import storage from 'utils/data/storage'; @@ -117,7 +118,7 @@ const TldrawPresentation = ({ size }) => { const currentPanzoomIndex = useCurrentIndex(storage.panzooms); const currentSlideIndex = useCurrentIndex(storage.slides); const started = currentPanzoomIndex !== -1; - + const shouldShowScreenShare = useShouldShowScreenShare(); const result = SlideData(tldrawAPI); let { assets, shapes, scaleRatio } = result; @@ -156,7 +157,7 @@ const TldrawPresentation = ({ size }) => { return (
{!started diff --git a/src/components/tldraw_v2/index.js b/src/components/tldraw_v2/index.js index ed51fce0..bb183523 100644 --- a/src/components/tldraw_v2/index.js +++ b/src/components/tldraw_v2/index.js @@ -11,6 +11,7 @@ import { useCurrentContent, useCurrentIndex, useCurrentInterval, + useShouldShowScreenShare, } from 'components/utils/hooks'; import { ID } from 'utils/constants'; import storage from 'utils/data/storage'; @@ -99,6 +100,7 @@ const TldrawPresentationV2 = ({ size }) => { const started = currentPanzoomIndex !== -1; const result = SlideData(tldrawAPI); + const shouldShowScreenshare = useShouldShowScreenShare(); let { assets, shapes, scaleRatio } = result; const { @@ -155,7 +157,7 @@ const TldrawPresentationV2 = ({ size }) => { return (
{!started ?
diff --git a/src/components/utils/hooks/index.js b/src/components/utils/hooks/index.js index a68b3068..bc7881c2 100644 --- a/src/components/utils/hooks/index.js +++ b/src/components/utils/hooks/index.js @@ -8,7 +8,8 @@ import { getCurrentDataIndex, getCurrentDataInterval, } from 'utils/data'; -import { isEqual } from 'utils/data/validators'; +import storage from 'utils/data/storage'; +import { isEqual, isShowScreenshareAsContent } from 'utils/data/validators'; const useCurrentContent = () => { const [currentContent, setCurrentContent] = useState(ID.PRESENTATION); @@ -28,6 +29,24 @@ const useCurrentContent = () => { return currentContent; }; +const useShouldShowScreenShare = () => { + const [shouldShowScreenShare, setShouldShowScreenShare] = useState(false); + + useEffect(() => { + const handleTimeUpdate = (event) => { + const nextShouldShowScreenShare = isShowScreenshareAsContent(storage.layoutSwap, event.detail.time); + if (shouldShowScreenShare !== nextShouldShowScreenShare) setShouldShowScreenShare(nextShouldShowScreenShare); + }; + + document.addEventListener(EVENTS.TIME_UPDATE, handleTimeUpdate); + return () => { + document.removeEventListener(EVENTS.TIME_UPDATE, handleTimeUpdate); + }; + }, [shouldShowScreenShare]); + + return shouldShowScreenShare; +} + const useCurrentIndex = (data) => { const [currentIndex, setCurrentIndex] = useState(-1); @@ -77,4 +96,5 @@ export { useCurrentContent, useCurrentIndex, useCurrentInterval, + useShouldShowScreenShare, }; diff --git a/src/config.js b/src/config.js index 83383b6d..4dcf2581 100644 --- a/src/config.js +++ b/src/config.js @@ -27,6 +27,7 @@ const files = { shapes: 'shapes.svg', tldraw: 'tldraw.json', videos: 'external_videos.json', + layout: 'layout.xml', }; const locale = { default: 'en' }; diff --git a/src/utils/builder.js b/src/utils/builder.js index 579f176b..0a39f37e 100644 --- a/src/utils/builder.js +++ b/src/utils/builder.js @@ -336,7 +336,7 @@ const buildShapes = result => { data.thumbnails = buildThumbnails(data.slides); data.canvases = buildCanvases(g, data.slides); } - + data.slides = data.slides.filter(slide => !slide.src.includes(ID.DESKSHARE)); return data; }; @@ -371,6 +371,22 @@ const buildTldraw = result => { return tldraw; } +const buildLayout = result => { + const { recording } = result; + + if (recording?.event) { + const newData = convertToArray(recording.event).map(layout => { + return { + timestamp: parseFloat(layout._timestamp), + showScreenshare: layout._show_screenshare === 'true', + } + }); + return newData; + } + + return []; +}; + const buildPanzooms = result => { let data = []; const { recording } = result; @@ -567,6 +583,9 @@ const build = (filename, value) => { case config.shapes: data = buildShapes(result); break; + case config.layout: + data = buildLayout(result); + break; default: logger.debug('unhandled', 'xml', filename); reject(filename); diff --git a/src/utils/constants.js b/src/utils/constants.js index e87cbb32..6f9606c4 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -52,6 +52,7 @@ const ID = { USERS: 'users', VIDEOS: 'videos', WEBCAMS: 'webcams', + LAYOUT: 'layout', }; const CONTENT = [ diff --git a/src/utils/data/storage.js b/src/utils/data/storage.js index 15151f2c..d8022d5a 100644 --- a/src/utils/data/storage.js +++ b/src/utils/data/storage.js @@ -165,6 +165,7 @@ const storage = { videos: hasProperty(DATA, ID.VIDEOS), presentation: hasProperty(DATA, ID.SHAPES), screenshare: hasProperty(DATA, ID.SCREENSHARE), + layoutSwap: hasProperty(DATA, ID.LAYOUT), }; }, get content() { @@ -176,6 +177,7 @@ const storage = { videos: !isEmpty(this.videos), presentation: hasPresentation(this.slides), screenshare: !isEmpty(this.screenshare), + layoutSwap: !isEmpty(this.layoutSwap), }; }, get alternates() { @@ -238,6 +240,9 @@ const storage = { return DATA[ID.THUMBNAILS]; }, + get layoutSwap() { + return DATA[ID.LAYOUT]; + }, get tldraw() { return DATA[ID.TLDRAW]; }, diff --git a/src/utils/data/validators.js b/src/utils/data/validators.js index 79a5522e..a7d2b466 100644 --- a/src/utils/data/validators.js +++ b/src/utils/data/validators.js @@ -52,7 +52,7 @@ const isContentVisible = (layout, swap) => { let visible; switch (layout) { - case CONTENT: + case CONTENT: visible = !swap; break; case MEDIA: @@ -159,6 +159,24 @@ const isVisible = (time, timestamp) => timestamp <= time; const wasCleared = (time, clear) => clear !== -1 && clear <= time; +function getMostRecentEvent(arr, time) { + return arr + .filter(item => item.timestamp <= time) + .reduce( + (prev, curr) => (prev?.timestamp > curr.timestamp ? prev : curr), + null + ); +} + +const isShowScreenshareAsContent = (data, time) => { + if (isEmpty(data)) return true; + + const event = getMostRecentEvent(data, time); + + if (!event) return false; + return event.showScreenshare; +} + export { hasIndex, hasPresentation, @@ -173,4 +191,5 @@ export { isValid, isVisible, wasCleared, + isShowScreenshareAsContent, };