diff --git a/apps/client/src/views/teleprompter/Teleprompter.tsx b/apps/client/src/views/teleprompter/Teleprompter.tsx index 7d3675ac2..d2d000d87 100644 --- a/apps/client/src/views/teleprompter/Teleprompter.tsx +++ b/apps/client/src/views/teleprompter/Teleprompter.tsx @@ -1,5 +1,5 @@ import { OntimeView } from 'ontime-types'; -import { type CSSProperties, useCallback, useMemo, useState } from 'react'; +import { type CSSProperties, useState } from 'react'; import EmptyPage from '../../common/components/state/EmptyPage'; import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor'; @@ -38,6 +38,8 @@ export default function TeleprompterLoader() { } function Teleprompter({ rundown, rundownMetadata, customFields }: TeleprompterData) { + 'use memo'; + const options = useTeleprompterOptions(); const selectedEventId = useSelectedEventId(); // the shared "Flip Screen" toggle from the navigation menu @@ -48,28 +50,15 @@ function Teleprompter({ rundown, rundownMetadata, customFields }: TeleprompterDa const [flipV, setFlipV] = useState(options.flipV); const [showHelp, setShowHelp] = useState(false); - const viewOptions = useMemo(() => getTeleprompterOptions(customFields), [customFields]); + const viewOptions = getTeleprompterOptions(customFields); - const blocks = useMemo( - () => - buildScript(rundown, rundownMetadata, customFields, { - scriptSource: options.scriptSource, - heading: options.heading, - hideEmpty: options.hideEmpty, - hidePast: options.hidePast, - showGroups: options.showGroups, - }), - [ - rundown, - rundownMetadata, - customFields, - options.scriptSource, - options.heading, - options.hideEmpty, - options.hidePast, - options.showGroups, - ], - ); + const blocks = buildScript(rundown, rundownMetadata, customFields, { + scriptSource: options.scriptSource, + heading: options.heading, + hideEmpty: options.hideEmpty, + hidePast: options.hidePast, + showGroups: options.showGroups, + }); const { scrollerRef, @@ -90,20 +79,17 @@ function Teleprompter({ rundown, rundownMetadata, customFields }: TeleprompterDa blocks, }); - const handleFlip = useCallback((axis: 'h' | 'v') => { + const handleFlip = (axis: 'h' | 'v') => { if (axis === 'h') { setFlipH((current) => !current); } else { setFlipV((current) => !current); } - }, []); + }; - const handleFontSize = useCallback((delta: number) => { - setFontScale((current) => clampFontScale(current + delta)); - }, []); - - const handleResetFontSize = useCallback(() => setFontScale(1), []); - const handleToggleHelp = useCallback(() => setShowHelp((current) => !current), []); + const handleFontSize = (delta: number) => setFontScale((current) => clampFontScale(current + delta)); + const handleResetFontSize = () => setFontScale(1); + const handleToggleHelp = () => setShowHelp((current) => !current); useTeleprompterControls({ controller, diff --git a/apps/client/src/views/teleprompter/script-block/ScriptBlock.tsx b/apps/client/src/views/teleprompter/script-block/ScriptBlock.tsx index 7d573a568..6e418e67f 100644 --- a/apps/client/src/views/teleprompter/script-block/ScriptBlock.tsx +++ b/apps/client/src/views/teleprompter/script-block/ScriptBlock.tsx @@ -1,5 +1,3 @@ -import { memo, useCallback } from 'react'; - import type { ScriptBlock } from '../teleprompter.types'; interface ScriptBlockProps { @@ -7,16 +5,18 @@ interface ScriptBlockProps { registerRef: (id: string, element: HTMLElement | null) => void; } -export default memo(ScriptBlockView); - -function ScriptBlockView({ block, registerRef }: ScriptBlockProps) { - // the id is bound here rather than by the parent so that the callback stays - // stable: React re-runs a ref callback whenever its identity changes, which - // would unregister and re-register every block on every parent render - const setRef = useCallback((element: HTMLElement | null) => registerRef(block.id, element), [block.id, registerRef]); +export default function ScriptBlockView({ block, registerRef }: ScriptBlockProps) { + 'use memo'; return ( -
+ // the id is bound here rather than by the parent so the compiler can keep + // the callback stable: React re-runs a ref callback whenever its identity + // changes, which would unregister every block on every parent render +
registerRef(block.id, element)} + data-loaded={block.isLoaded || undefined} + > {block.groupTitle &&
{block.groupTitle}
} {block.heading &&

{block.heading}

} {/* the script is user data, it is rendered as text and never as markup */} diff --git a/apps/client/src/views/teleprompter/useTeleprompterScroll.ts b/apps/client/src/views/teleprompter/useTeleprompterScroll.ts index a8aee786e..c148cbaf9 100644 --- a/apps/client/src/views/teleprompter/useTeleprompterScroll.ts +++ b/apps/client/src/views/teleprompter/useTeleprompterScroll.ts @@ -25,7 +25,7 @@ interface UseTeleprompterScrollArgs { selectedEventId: string | null; /** percentage from the top of the screen */ readingLinePos: number; - /** the rendered document, watched so a follow can retarget once blocks remount */ + /** the rendered document, watched so a follow can retarget once its block exists */ blocks: ScriptBlock[]; } @@ -234,6 +234,16 @@ export function useTeleprompterScroll({ // eslint-disable-next-line react-hooks/exhaustive-deps -- autoplay is a starting condition, not a live one }, []); + /** + * Whether the loaded event currently has a block to scroll to. + * + * The follow watches this rather than the blocks array, because memoisation is + * a performance hint that React is free to discard: keying off array identity + * would let a rebuilt but otherwise identical list re-fire the jump and yank + * the script back while it is being read. + */ + const hasSelectedBlock = selectedEventId !== null && blocks.some((block) => block.id === selectedEventId); + // follow the loaded event useEffect(() => { if (!followLoaded || followLocked || !selectedEventId) return; @@ -250,7 +260,7 @@ export function useTeleprompterScroll({ catchUpTargetRef.current = clamp(top, 0, Math.max(maxScrollRef.current, 0)); setAtEnd(false); ensureLoop(); - }, [selectedEventId, followLoaded, followLocked, readingLinePos, blocks, ensureLoop]); + }, [selectedEventId, followLoaded, followLocked, readingLinePos, hasSelectedBlock, ensureLoop]); const lockFollow = useMemo(() => throttle(() => setFollowLocked(true), FOLLOW_LOCK_THROTTLE), []);