Files
upstream-jitsi-meet/react/features/notifications/components/NotificationsTransition.tsx
upstream-mirror 1d2bfcac0c
Some checks failed
Prosody Plugin Tests / test (push) Failing after 10m11s
Close stale issues and PRs / stale (push) Failing after 1s
snapshot of https://github.com/jitsi/jitsi-meet.git @ fd1749dd28f4138aa80234fc7f311b19aaffa0e7 (2026-09-15)
2026-09-15 15:56:32 +05:00

96 lines
3.4 KiB
TypeScript

import React, { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from 'react';
type TimeoutType = ReturnType<typeof setTimeout>;
export const NotificationsTransitionContext = React.createContext({
unmounting: new Map<string, TimeoutType>()
});
/**
* How long a removed notification is kept mounted so that its exit animation can play.
*/
const UNMOUNT_ANIMATION_TIMEOUT = 250;
const NotificationsTransition = ({ children }: { children: ReactElement[]; }) => {
const [ childrenToRender, setChildrenToRender ] = useState(children);
/**
* The pending removals, by uid. Held in a ref so that the timeout callbacks always act on the current
* timers instead of the ones captured when they were scheduled, and mirrored into state so that a render
* is triggered and the notifications which are animating out can be marked as such.
*/
const timers = useRef(new Map<string, TimeoutType>());
const [ unmounting, setUnmounting ] = useState(timers.current);
const removeChild = useCallback((uid: string) => {
timers.current.delete(uid);
setUnmounting(new Map(timers.current));
setChildrenToRender(current => current.filter(child => child.props.uid !== uid));
}, []);
useEffect(() => {
const uids = new Set(children.map(child => child.props.uid));
let timersChanged = false;
// A notification that was re-added while it was animating out is displayed again, so its pending
// removal has to be cancelled. Notifications are keyed by uid and are reused (the lobby one, for
// example), so without this the removal would fire and drop a notification that is still current.
timers.current.forEach((timeoutId, uid) => {
if (uids.has(uid)) {
clearTimeout(timeoutId);
timers.current.delete(uid);
timersChanged = true;
}
});
childrenToRender.forEach(child => {
const { uid } = child.props;
if (!uids.has(uid) && !timers.current.has(uid)) {
timers.current.set(uid, setTimeout(() => removeChild(uid), UNMOUNT_ANIMATION_TIMEOUT));
timersChanged = true;
}
});
if (timersChanged) {
setUnmounting(new Map(timers.current));
}
setChildrenToRender(current => {
const toMount = children.filter(child =>
current.findIndex(c => c.props.uid === child.props.uid) === -1);
/**
* Update current notifications.
* In some cases the UID is the same but the other props change.
* This way we make sure the notification displays the latest info.
*/
const updated = current.map(child =>
children.find(c => c.props.uid === child.props.uid) ?? child);
return toMount.concat(updated);
});
}, [ children ]);
useEffect(() => {
const pending = timers.current;
return () => {
pending.forEach(timeoutId => clearTimeout(timeoutId));
pending.clear();
};
}, []);
const context = useMemo(() => {
return { unmounting };
}, [ unmounting ]);
return (
<NotificationsTransitionContext.Provider value = { context }>
{childrenToRender}
</NotificationsTransitionContext.Provider>
);
};
export default NotificationsTransition;