Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 127 additions & 9 deletions meteor/client/lib/ReactMeteorData/ReactMeteorData.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable react/prefer-stateless-function */

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

const SUBSCRIPTION_TIMEOUT = 1000

/**
* Delay an update to be batched with the global tracker invalidation queue
*/
Expand Down Expand Up @@ -370,6 +372,61 @@ export function useTracker<T, K extends undefined | T = undefined>(
return meteorData
}

/**
* A hook to track a boolean state with a sort of histeresis, with preference for `true`. `setState` makes the returned
* `state` be `true` immediately, but `false` only after `resetState` is called and `timeout` elapses. If `setState`
* is called with `true` before `timeout` elapses, then `resetState` is aborted and `state` will remain `ture.
*
* Later `resetState` calls replace earlier unelapsed calls and their timeout periods.
*
* @param {boolean} [initialState=false]
* @return {*} {{
* state: boolean
* setState: (value: boolean) => void
* resetState: (timeout: number) => void
* }}
*/
function useDelayState(initialState = false): {
state: boolean
setState: (value: boolean) => void
resetState: (timeout: number) => void
} {
const [state, setState] = useState(initialState)
const [prevState, setPrevState] = useState(initialState)
const prevReadyTimeoutRef = useRef<number | null>(null)

const setStateAndClearResets = useCallback(
(value: boolean) => {
setState(value)

if (value) {
setPrevState(true)
if (prevReadyTimeoutRef.current !== null) {
window.clearTimeout(prevReadyTimeoutRef.current)
prevReadyTimeoutRef.current = null
}
}
},
[setState, setPrevState]
)

const resetStateAfterDelay = useCallback((timeout: number) => {
if (prevReadyTimeoutRef.current !== null) {
window.clearTimeout(prevReadyTimeoutRef.current)
}
prevReadyTimeoutRef.current = window.setTimeout(() => {
prevReadyTimeoutRef.current = null
setPrevState(false)
}, timeout)
}, [])

return {
state: state || prevState,
setState: setStateAndClearResets,
resetState: resetStateAfterDelay,
}
}

/**
* A Meteor Subscription hook that allows using React Functional Components and the Hooks API with Meteor subscriptions.
* Subscriptions will be torn down 1000ms after unmounting the component.
Expand All @@ -383,20 +440,28 @@ export function useSubscription<K extends keyof AllPubSubTypes>(
sub: K,
...args: Parameters<AllPubSubTypes[K]>
): boolean {
const [ready, setReady] = useState<boolean>(false)
const { state: ready, setState: setReady, resetState: cancelPreviousReady } = useDelayState()

useEffect(() => {
const subscription = Tracker.nonreactive(() => meteorSubscribe(sub, ...args))
const isReadyComp = Tracker.nonreactive(() => Tracker.autorun(() => setReady(subscription.ready())))
const isReadyComp = Tracker.nonreactive(() =>
Tracker.autorun(() => {
const isNowReady = subscription.ready()
setReady(isNowReady)
})
)
return () => {
isReadyComp.stop()
setTimeout(() => {
subscription.stop()
}, 1000)
}, SUBSCRIPTION_TIMEOUT)
cancelPreviousReady(SUBSCRIPTION_TIMEOUT)
}
}, [sub, stringifyObjects(args)])

return ready
const isReady = ready

return isReady
}

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

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

const subscription = Tracker.nonreactive(() => meteorSubscribe(sub, ...args))
const isReadyComp = Tracker.nonreactive(() => Tracker.autorun(() => setReady(subscription.ready())))
const isReadyComp = Tracker.nonreactive(() =>
Tracker.autorun(() => {
const isNowReady = subscription.ready()
setReady(isNowReady)
})
)
return () => {
isReadyComp.stop()
setTimeout(() => {
subscription.stop()
}, 1000)
}, SUBSCRIPTION_TIMEOUT)
cancelPreviousReady(SUBSCRIPTION_TIMEOUT)
}
}, [sub, enable, stringifyObjects(args)])

return !enable || ready
const isReady = !enable || ready

return isReady
}

/**
* A Meteor Subscription hook that allows using React Functional Components and the Hooks API with Meteor subscriptions.
* Subscriptions will be torn down 1000ms after unmounting the component.
* If the subscription is not enabled, the subscription will not be created, and the ready state will always be true.
*
* @export
* @param {PubSub} sub The subscription to be subscribed to
* @param {boolean} enable Whether the subscription is enabled
* @param {...any[]} args A list of arugments for the subscription. This is used for optimizing the subscription across
* renders so that it isn't torn down and created for every render.
*/
export function useSubscriptionIfEnabledReadyOnce<K extends keyof AllPubSubTypes>(
sub: K,
enable: boolean,
...args: Parameters<AllPubSubTypes[K]>
): boolean {
const { state: ready, setState: setReady, resetState: cancelPreviousReady } = useDelayState()

useEffect(() => {
if (!enable) {
setReady(false)
return
}

const subscription = Tracker.nonreactive(() => meteorSubscribe(sub, ...args))
const isReadyComp = Tracker.nonreactive(() =>
Tracker.autorun(() => {
const isNowReady = subscription.ready()
if (isNowReady) setReady(true)
})
)
return () => {
isReadyComp.stop()
setTimeout(() => {
subscription.stop()
}, SUBSCRIPTION_TIMEOUT)
cancelPreviousReady(SUBSCRIPTION_TIMEOUT)
}
}, [sub, enable, stringifyObjects(args)])

const isReady = !enable || ready

return isReady
}

/**
Expand Down
15 changes: 11 additions & 4 deletions meteor/client/ui/RundownView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
Translated,
translateWithTracker,
useSubscriptionIfEnabled,
useSubscriptionIfEnabledReadyOnce,
useSubscriptions,
useTracker,
} from '../lib/ReactMeteorData/react-meteor-data'
Expand Down Expand Up @@ -1227,9 +1228,6 @@ export function RundownView(props: Readonly<IProps>): JSX.Element {

return playlist?.studioId
}, [playlistId])
// Load once the playlist is confirmed to exist
auxSubsReady.push(useSubscriptionIfEnabled(MeteorPubSub.uiSegmentPartNotes, !!playlistStudioId, playlistId))
auxSubsReady.push(useSubscriptionIfEnabled(MeteorPubSub.uiPieceContentStatuses, !!playlistStudioId, playlistId))
// Load only when the studio is known
requiredSubsReady.push(
useSubscriptionIfEnabled(MeteorPubSub.uiStudio, !!playlistStudioId, playlistStudioId ?? protectString(''))
Expand Down Expand Up @@ -1258,7 +1256,12 @@ export function RundownView(props: Readonly<IProps>): JSX.Element {
)
)
requiredSubsReady.push(
useSubscriptionIfEnabled(CorelibPubSub.showStyleVariants, showStyleVariantIds.length > 0, null, showStyleVariantIds)
useSubscriptionIfEnabledReadyOnce(
CorelibPubSub.showStyleVariants,
showStyleVariantIds.length > 0,
null,
showStyleVariantIds
)
)
auxSubsReady.push(
useSubscriptionIfEnabled(MeteorPubSub.rundownLayouts, showStyleBaseIds.length > 0, showStyleBaseIds)
Expand All @@ -1283,6 +1286,10 @@ export function RundownView(props: Readonly<IProps>): JSX.Element {
)
)

// Load once the playlist is confirmed to exist
auxSubsReady.push(useSubscriptionIfEnabled(MeteorPubSub.uiSegmentPartNotes, !!playlistStudioId, playlistId))
auxSubsReady.push(useSubscriptionIfEnabled(MeteorPubSub.uiPieceContentStatuses, !!playlistStudioId, playlistId))

useTracker(() => {
const playlist = RundownPlaylists.findOne(playlistId, {
fields: {
Expand Down
Loading