Skip to content

Commit 02b8594

Browse files
authored
Merge pull request Sofie-Automation#1311 from nrkno/fix/sofie-3593/show-style-variants-sub
fix: RundownView shows spinner when unMOSing a Rundown from a Playlist (SOFIE-3539)
2 parents 0ae53c4 + 13f0556 commit 02b8594

File tree

2 files changed

+138
-13
lines changed

2 files changed

+138
-13
lines changed

meteor/client/lib/ReactMeteorData/ReactMeteorData.tsx

Lines changed: 127 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable react/prefer-stateless-function */
22

3-
import React, { useState, useEffect, useRef } from 'react'
3+
import React, { useState, useEffect, useRef, useCallback } from 'react'
44
import { Meteor } from 'meteor/meteor'
55
import { Mongo } from 'meteor/mongo'
66
import { Tracker } from 'meteor/tracker'
@@ -13,6 +13,8 @@ const globalTrackerQueue: Array<Function> = []
1313
let globalTrackerTimestamp: number | undefined = undefined
1414
let globalTrackerTimeout: number | undefined = undefined
1515

16+
const SUBSCRIPTION_TIMEOUT = 1000
17+
1618
/**
1719
* Delay an update to be batched with the global tracker invalidation queue
1820
*/
@@ -370,6 +372,61 @@ export function useTracker<T, K extends undefined | T = undefined>(
370372
return meteorData
371373
}
372374

375+
/**
376+
* A hook to track a boolean state with a sort of histeresis, with preference for `true`. `setState` makes the returned
377+
* `state` be `true` immediately, but `false` only after `resetState` is called and `timeout` elapses. If `setState`
378+
* is called with `true` before `timeout` elapses, then `resetState` is aborted and `state` will remain `ture.
379+
*
380+
* Later `resetState` calls replace earlier unelapsed calls and their timeout periods.
381+
*
382+
* @param {boolean} [initialState=false]
383+
* @return {*} {{
384+
* state: boolean
385+
* setState: (value: boolean) => void
386+
* resetState: (timeout: number) => void
387+
* }}
388+
*/
389+
function useDelayState(initialState = false): {
390+
state: boolean
391+
setState: (value: boolean) => void
392+
resetState: (timeout: number) => void
393+
} {
394+
const [state, setState] = useState(initialState)
395+
const [prevState, setPrevState] = useState(initialState)
396+
const prevReadyTimeoutRef = useRef<number | null>(null)
397+
398+
const setStateAndClearResets = useCallback(
399+
(value: boolean) => {
400+
setState(value)
401+
402+
if (value) {
403+
setPrevState(true)
404+
if (prevReadyTimeoutRef.current !== null) {
405+
window.clearTimeout(prevReadyTimeoutRef.current)
406+
prevReadyTimeoutRef.current = null
407+
}
408+
}
409+
},
410+
[setState, setPrevState]
411+
)
412+
413+
const resetStateAfterDelay = useCallback((timeout: number) => {
414+
if (prevReadyTimeoutRef.current !== null) {
415+
window.clearTimeout(prevReadyTimeoutRef.current)
416+
}
417+
prevReadyTimeoutRef.current = window.setTimeout(() => {
418+
prevReadyTimeoutRef.current = null
419+
setPrevState(false)
420+
}, timeout)
421+
}, [])
422+
423+
return {
424+
state: state || prevState,
425+
setState: setStateAndClearResets,
426+
resetState: resetStateAfterDelay,
427+
}
428+
}
429+
373430
/**
374431
* A Meteor Subscription hook that allows using React Functional Components and the Hooks API with Meteor subscriptions.
375432
* Subscriptions will be torn down 1000ms after unmounting the component.
@@ -383,20 +440,28 @@ export function useSubscription<K extends keyof AllPubSubTypes>(
383440
sub: K,
384441
...args: Parameters<AllPubSubTypes[K]>
385442
): boolean {
386-
const [ready, setReady] = useState<boolean>(false)
443+
const { state: ready, setState: setReady, resetState: cancelPreviousReady } = useDelayState()
387444

388445
useEffect(() => {
389446
const subscription = Tracker.nonreactive(() => meteorSubscribe(sub, ...args))
390-
const isReadyComp = Tracker.nonreactive(() => Tracker.autorun(() => setReady(subscription.ready())))
447+
const isReadyComp = Tracker.nonreactive(() =>
448+
Tracker.autorun(() => {
449+
const isNowReady = subscription.ready()
450+
setReady(isNowReady)
451+
})
452+
)
391453
return () => {
392454
isReadyComp.stop()
393455
setTimeout(() => {
394456
subscription.stop()
395-
}, 1000)
457+
}, SUBSCRIPTION_TIMEOUT)
458+
cancelPreviousReady(SUBSCRIPTION_TIMEOUT)
396459
}
397460
}, [sub, stringifyObjects(args)])
398461

399-
return ready
462+
const isReady = ready
463+
464+
return isReady
400465
}
401466

402467
/**
@@ -415,7 +480,7 @@ export function useSubscriptionIfEnabled<K extends keyof AllPubSubTypes>(
415480
enable: boolean,
416481
...args: Parameters<AllPubSubTypes[K]>
417482
): boolean {
418-
const [ready, setReady] = useState<boolean>(false)
483+
const { state: ready, setState: setReady, resetState: cancelPreviousReady } = useDelayState()
419484

420485
useEffect(() => {
421486
if (!enable) {
@@ -424,16 +489,69 @@ export function useSubscriptionIfEnabled<K extends keyof AllPubSubTypes>(
424489
}
425490

426491
const subscription = Tracker.nonreactive(() => meteorSubscribe(sub, ...args))
427-
const isReadyComp = Tracker.nonreactive(() => Tracker.autorun(() => setReady(subscription.ready())))
492+
const isReadyComp = Tracker.nonreactive(() =>
493+
Tracker.autorun(() => {
494+
const isNowReady = subscription.ready()
495+
setReady(isNowReady)
496+
})
497+
)
428498
return () => {
429499
isReadyComp.stop()
430500
setTimeout(() => {
431501
subscription.stop()
432-
}, 1000)
502+
}, SUBSCRIPTION_TIMEOUT)
503+
cancelPreviousReady(SUBSCRIPTION_TIMEOUT)
433504
}
434505
}, [sub, enable, stringifyObjects(args)])
435506

436-
return !enable || ready
507+
const isReady = !enable || ready
508+
509+
return isReady
510+
}
511+
512+
/**
513+
* A Meteor Subscription hook that allows using React Functional Components and the Hooks API with Meteor subscriptions.
514+
* Subscriptions will be torn down 1000ms after unmounting the component.
515+
* If the subscription is not enabled, the subscription will not be created, and the ready state will always be true.
516+
*
517+
* @export
518+
* @param {PubSub} sub The subscription to be subscribed to
519+
* @param {boolean} enable Whether the subscription is enabled
520+
* @param {...any[]} args A list of arugments for the subscription. This is used for optimizing the subscription across
521+
* renders so that it isn't torn down and created for every render.
522+
*/
523+
export function useSubscriptionIfEnabledReadyOnce<K extends keyof AllPubSubTypes>(
524+
sub: K,
525+
enable: boolean,
526+
...args: Parameters<AllPubSubTypes[K]>
527+
): boolean {
528+
const { state: ready, setState: setReady, resetState: cancelPreviousReady } = useDelayState()
529+
530+
useEffect(() => {
531+
if (!enable) {
532+
setReady(false)
533+
return
534+
}
535+
536+
const subscription = Tracker.nonreactive(() => meteorSubscribe(sub, ...args))
537+
const isReadyComp = Tracker.nonreactive(() =>
538+
Tracker.autorun(() => {
539+
const isNowReady = subscription.ready()
540+
if (isNowReady) setReady(true)
541+
})
542+
)
543+
return () => {
544+
isReadyComp.stop()
545+
setTimeout(() => {
546+
subscription.stop()
547+
}, SUBSCRIPTION_TIMEOUT)
548+
cancelPreviousReady(SUBSCRIPTION_TIMEOUT)
549+
}
550+
}, [sub, enable, stringifyObjects(args)])
551+
552+
const isReady = !enable || ready
553+
554+
return isReady
437555
}
438556

439557
/**

meteor/client/ui/RundownView.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
Translated,
88
translateWithTracker,
99
useSubscriptionIfEnabled,
10+
useSubscriptionIfEnabledReadyOnce,
1011
useSubscriptions,
1112
useTracker,
1213
} from '../lib/ReactMeteorData/react-meteor-data'
@@ -1227,9 +1228,6 @@ export function RundownView(props: Readonly<IProps>): JSX.Element {
12271228

12281229
return playlist?.studioId
12291230
}, [playlistId])
1230-
// Load once the playlist is confirmed to exist
1231-
auxSubsReady.push(useSubscriptionIfEnabled(MeteorPubSub.uiSegmentPartNotes, !!playlistStudioId, playlistId))
1232-
auxSubsReady.push(useSubscriptionIfEnabled(MeteorPubSub.uiPieceContentStatuses, !!playlistStudioId, playlistId))
12331231
// Load only when the studio is known
12341232
requiredSubsReady.push(
12351233
useSubscriptionIfEnabled(MeteorPubSub.uiStudio, !!playlistStudioId, playlistStudioId ?? protectString(''))
@@ -1258,7 +1256,12 @@ export function RundownView(props: Readonly<IProps>): JSX.Element {
12581256
)
12591257
)
12601258
requiredSubsReady.push(
1261-
useSubscriptionIfEnabled(CorelibPubSub.showStyleVariants, showStyleVariantIds.length > 0, null, showStyleVariantIds)
1259+
useSubscriptionIfEnabledReadyOnce(
1260+
CorelibPubSub.showStyleVariants,
1261+
showStyleVariantIds.length > 0,
1262+
null,
1263+
showStyleVariantIds
1264+
)
12621265
)
12631266
auxSubsReady.push(
12641267
useSubscriptionIfEnabled(MeteorPubSub.rundownLayouts, showStyleBaseIds.length > 0, showStyleBaseIds)
@@ -1283,6 +1286,10 @@ export function RundownView(props: Readonly<IProps>): JSX.Element {
12831286
)
12841287
)
12851288

1289+
// Load once the playlist is confirmed to exist
1290+
auxSubsReady.push(useSubscriptionIfEnabled(MeteorPubSub.uiSegmentPartNotes, !!playlistStudioId, playlistId))
1291+
auxSubsReady.push(useSubscriptionIfEnabled(MeteorPubSub.uiPieceContentStatuses, !!playlistStudioId, playlistId))
1292+
12861293
useTracker(() => {
12871294
const playlist = RundownPlaylists.findOne(playlistId, {
12881295
fields: {

0 commit comments

Comments
 (0)