import throttle from 'lodash/throttle'; import { IStore } from '../app/types'; import { NOTIFICATIONS_ENABLED } from '../base/flags/constants'; import { getFeatureFlag } from '../base/flags/functions'; import { getParticipantCount } from '../base/participants/functions'; import { CLEAR_NOTIFICATIONS, HIDE_NOTIFICATION, SET_NOTIFICATIONS_ENABLED, SHOW_NOTIFICATION } from './actionTypes'; import { NOTIFICATION_ICON, NOTIFICATION_TIMEOUT, NOTIFICATION_TIMEOUT_TYPE, NOTIFICATION_TYPE, SILENT_JOIN_THRESHOLD, SILENT_LEFT_THRESHOLD } from './constants'; /** * Function that returns notification timeout value based on notification timeout type. * * @param {string} type - Notification type. * @param {Object} notificationTimeouts - Config notification timeouts. * @returns {number} */ function getNotificationTimeout(type?: string, notificationTimeouts?: { long?: number; medium?: number; short?: number; }) { if (type === NOTIFICATION_TIMEOUT_TYPE.SHORT) { return notificationTimeouts?.short ?? NOTIFICATION_TIMEOUT.SHORT; } else if (type === NOTIFICATION_TIMEOUT_TYPE.MEDIUM) { return notificationTimeouts?.medium ?? NOTIFICATION_TIMEOUT.MEDIUM; } else if (type === NOTIFICATION_TIMEOUT_TYPE.LONG) { return notificationTimeouts?.long ?? NOTIFICATION_TIMEOUT.LONG; } return NOTIFICATION_TIMEOUT.STICKY; } /** * Clears (removes) all the notifications. * * @returns {{ * type: CLEAR_NOTIFICATIONS * }} */ export function clearNotifications() { return { type: CLEAR_NOTIFICATIONS }; } /** * Removes the notification with the passed in id. * * @param {string} uid - The unique identifier for the notification to be * removed. * @returns {{ * type: HIDE_NOTIFICATION, * uid: string * }} */ export function hideNotification(uid: string) { return { type: HIDE_NOTIFICATION, uid }; } /** * Stops notifications from being displayed. * * @param {boolean} enabled - Whether or not notifications should display. * @returns {{ * type: SET_NOTIFICATIONS_ENABLED, * enabled: boolean * }} */ export function setNotificationsEnabled(enabled: boolean) { return { type: SET_NOTIFICATIONS_ENABLED, enabled }; } interface INotificationProps { appearance?: string; concatText?: boolean; customActionHandler?: Function[]; customActionNameKey?: string[]; description?: string; descriptionKey?: string; icon?: string; title?: string; titleArguments?: { [key: string]: string; }; titleKey?: string; uid?: string; } /** * Queues an error notification for display. * * @param {Object} props - The props needed to show the notification component. * @param {string} type - Notification type. * @returns {Object} */ export function showErrorNotification(props: INotificationProps, type?: string) { return showNotification({ ...props, appearance: NOTIFICATION_TYPE.ERROR }, type); } /** * Queues a notification for display. * * @param {Object} props - The props needed to show the notification component. * @param {string} type - Timeout type. * @returns {Function} */ export function showNotification(props: INotificationProps = {}, type?: string) { return function(dispatch: IStore['dispatch'], getState: IStore['getState']) { const { disabledNotifications = [], notifications, notificationTimeouts } = getState()['features/base/config']; const enabledFlag = getFeatureFlag(getState(), NOTIFICATIONS_ENABLED, true); const shouldDisplay = enabledFlag && !(disabledNotifications.includes(props.descriptionKey ?? '') || disabledNotifications.includes(props.titleKey ?? '')) && (!notifications || notifications.includes(props.descriptionKey ?? '') || notifications.includes(props.titleKey ?? '')); if (shouldDisplay) { return dispatch({ type: SHOW_NOTIFICATION, props, timeout: getNotificationTimeout(type, notificationTimeouts), uid: props.uid || window.Date.now().toString() }); } }; } /** * Queues a warning notification for display. * * @param {Object} props - The props needed to show the notification component. * @param {string} type - Notification type. * @returns {Object} */ export function showWarningNotification(props: INotificationProps, type?: string) { return showNotification({ ...props, appearance: NOTIFICATION_TYPE.WARNING }, type); } /** * Queues a message notification for display. * * @param {Object} props - The props needed to show the notification component. * @param {string} type - Notification type. * @returns {Object} */ export function showMessageNotification(props: INotificationProps, type?: string) { return showNotification({ ...props, concatText: true, titleKey: 'notify.chatMessages', appearance: NOTIFICATION_TYPE.NORMAL, icon: NOTIFICATION_ICON.MESSAGE }, type); } /** * An array of names of participants that have joined the conference. The array * is replaced with an empty array as notifications are displayed. * * @private * @type {string[]} */ let joinedParticipantsNames: string[] = []; /** * A throttled internal function that takes the internal list of participant * names, {@code joinedParticipantsNames}, and triggers the display of a * notification informing of their joining. * * @private * @type {Function} */ const _throttledNotifyParticipantConnected = throttle((dispatch: IStore['dispatch'], getState: IStore['getState']) => { const participantCount = getParticipantCount(getState()); // Skip join notifications altogether for large meetings. if (participantCount > SILENT_JOIN_THRESHOLD) { joinedParticipantsNames = []; return; } const joinedParticipantsCount = joinedParticipantsNames.length; let notificationProps; if (joinedParticipantsCount >= 3) { notificationProps = { titleArguments: { name: joinedParticipantsNames[0] }, titleKey: 'notify.connectedThreePlusMembers' }; } else if (joinedParticipantsCount === 2) { notificationProps = { titleArguments: { first: joinedParticipantsNames[0], second: joinedParticipantsNames[1] }, titleKey: 'notify.connectedTwoMembers' }; } else if (joinedParticipantsCount) { notificationProps = { titleArguments: { name: joinedParticipantsNames[0] }, titleKey: 'notify.connectedOneMember' }; } if (notificationProps) { dispatch( showNotification(notificationProps, NOTIFICATION_TIMEOUT_TYPE.SHORT)); } joinedParticipantsNames = []; }, 2000, { leading: false }); /** * An array of names of participants that have left the conference. The array * is replaced with an empty array as notifications are displayed. * * @private * @type {string[]} */ let leftParticipantsNames: string[] = []; /** * A throttled internal function that takes the internal list of participant * names, {@code leftParticipantsNames}, and triggers the display of a * notification informing of their leaving. * * @private * @type {Function} */ const _throttledNotifyParticipantLeft = throttle((dispatch: IStore['dispatch'], getState: IStore['getState']) => { const participantCount = getParticipantCount(getState()); // Skip left notifications altogether for large meetings. if (participantCount > SILENT_LEFT_THRESHOLD) { leftParticipantsNames = []; return; } const leftParticipantsCount = leftParticipantsNames.length; let notificationProps; if (leftParticipantsCount >= 3) { notificationProps = { titleArguments: { name: leftParticipantsNames[0] }, titleKey: 'notify.leftThreePlusMembers' }; } else if (leftParticipantsCount === 2) { notificationProps = { titleArguments: { first: leftParticipantsNames[0], second: leftParticipantsNames[1] }, titleKey: 'notify.leftTwoMembers' }; } else if (leftParticipantsCount) { notificationProps = { titleArguments: { name: leftParticipantsNames[0] }, titleKey: 'notify.leftOneMember' }; } if (notificationProps) { dispatch( showNotification(notificationProps, NOTIFICATION_TIMEOUT_TYPE.SHORT)); } leftParticipantsNames = []; }, 2000, { leading: false }); /** * Queues the display of a notification of a participant having connected to * the meeting. The notifications are batched so that quick consecutive * connection events are shown in one notification. * * @param {string} displayName - The name of the participant that connected. * @returns {Function} */ export function showParticipantJoinedNotification(displayName: string) { joinedParticipantsNames.push(displayName); return (dispatch: IStore['dispatch'], getState: IStore['getState']) => _throttledNotifyParticipantConnected(dispatch, getState); } /** * Queues the display of a notification of a participant having left to * the meeting. The notifications are batched so that quick consecutive * connection events are shown in one notification. * * @param {string} displayName - The name of the participant that left. * @returns {Function} */ export function showParticipantLeftNotification(displayName: string) { leftParticipantsNames.push(displayName); return (dispatch: IStore['dispatch'], getState: IStore['getState']) => _throttledNotifyParticipantLeft(dispatch, getState); }