import React, { FunctionComponent, MutableRefObject, useEffect, useRef, useState } from 'react'; import { Event, ScheduledView } from '../Models/ScheduledView'; import { View } from '../Models/View'; import { extractScheduledViewsFromPlaylists } from '../Services/extractScheduledViewsFromPlaylists'; import ViewRenderer from '../Components/ViewRenderer'; export interface SolarCastProps { mouseInteraction?: boolean; events: Event[]; views: View[]; guestMode?: boolean; } interface ActiveViewHelper { current?: ScheduledView; upcoming?: ScheduledView; } const SolarCast: FunctionComponent = ({ children, guestMode, mouseInteraction, events, views }) => { const eventsRef = useRef(events); eventsRef.current = events; const viewsRef = useRef(views); viewsRef.current = views; const activeViewIndexRef = useRef(0); const nextEventTimeoutRef = useRef(null); const activeViewTimeoutRef = useRef(null); // Currently displaying this view const [availableScheduledViews, setAvailableScheduledViews] = useState([]); const [activeScheduledView, setActiveScheduledView] = useState({}); const availableScheduledViewsRef = useRef(availableScheduledViews); availableScheduledViewsRef.current = availableScheduledViews; const activeScheduledViewRef = useRef(activeScheduledView); activeScheduledViewRef.current = activeScheduledView; const setTimer = (ref: MutableRefObject, callback: () => void, ms: number): void => { ref.current = setTimeout(callback, ms); }; const clearTimer = (ref: MutableRefObject): void => { ref.current && clearTimeout(ref.current); ref.current = null; }; const setNextActiveScheduledView = (nextScheduledView?: ScheduledView): void => { if (activeScheduledViewRef.current.current?.id === nextScheduledView?.id && activeScheduledViewRef.current.current?.view_id === nextScheduledView?.view_id) { return; } if (nextScheduledView === undefined) { setActiveScheduledView({}); return; } setActiveScheduledView({ ...activeScheduledViewRef.current, upcoming: nextScheduledView, }); }; /** Clean-up timers before unmounting */ useEffect(() => () => { clearTimer(nextEventTimeoutRef); clearTimer(activeViewTimeoutRef); }, []); const onDisplayChange = (): void => { clearTimer(activeViewTimeoutRef); switch (availableScheduledViewsRef.current.length) { case 0: return setNextActiveScheduledView(); case 1: return setNextActiveScheduledView(availableScheduledViewsRef.current[0]); } const displayNow = availableScheduledViewsRef.current[activeViewIndexRef.current % availableScheduledViewsRef.current.length]; setNextActiveScheduledView(displayNow); const currentViewLengthInSeconds = viewsRef.current.find(view => view.id === displayNow.view_id)?.length; setTimer(activeViewTimeoutRef, onDisplayChange, (currentViewLengthInSeconds || 1) * 1000); activeViewIndexRef.current++; }; const onEventsChange = (): void => { const now = new Date(); let currentEvent = null; let convertedViews: ScheduledView[] = []; clearTimer(nextEventTimeoutRef); for (let index = 0; index < eventsRef.current.length; index++) { const event = eventsRef.current[index]; if (now >= event.start && now < event.end) { currentEvent = event; convertedViews = extractScheduledViewsFromPlaylists(currentEvent.scheduled_views, viewsRef.current); if (availableScheduledViewsRef.current.length !== convertedViews.length || !availableScheduledViewsRef.current.every((value, i) => value.id === convertedViews[i].id)) { activeViewIndexRef.current = 0; setAvailableScheduledViews(convertedViews); } } if (now < event.start) { setTimer(nextEventTimeoutRef, onEventsChange, (currentEvent?.end || event.start).getTime() - now.getTime() + 100); break; } } if (!currentEvent) { activeViewIndexRef.current = 0; return setAvailableScheduledViews([]); } if (!nextEventTimeoutRef.current) { setTimer(nextEventTimeoutRef, onEventsChange, currentEvent.end.getTime() - now.getTime() + 100); } }; const addDynamicPayloadToView = (view: View, scheduledView: ScheduledView): View => { if (typeof scheduledView?.payload !== 'object') { return view; } return { ...view, payload: { ...(typeof view.payload === 'object' ? view.payload : {}), ...scheduledView.payload, } }; }; const getViewRendererKey = (view: View, scheduledView?: ScheduledView): string => scheduledView?.payload ? `sv-${scheduledView.id}-${scheduledView.updated_at}-${view.updated_at}` : `v-${view.id}-${view.updated_at}`; useEffect(onEventsChange, [events]); useEffect(onDisplayChange, [availableScheduledViews]); useEffect(() => { if (children) { clearTimer(nextEventTimeoutRef); clearTimer(activeViewTimeoutRef); } }, [children]); if (children) { return <>{ children }; } return ( <> { /** if the active view is not ready, continue displaying the previous view until onready is called. */ views.map((view) => { if (view.id === activeScheduledView.upcoming?.view_id) { return ( setActiveScheduledView({ current: activeScheduledView.upcoming }) } /> ); } if (view.id === activeScheduledView.current?.view_id) { return ( ); } }) } ); }; export default SolarCast;