From c172a27f24ba646668330d477a3a4640af1eafa7 Mon Sep 17 00:00:00 2001 From: "Tudor D. Pop" Date: Thu, 2 Dec 2021 15:17:07 +0200 Subject: [PATCH] feat(notifications) coalesce participant left and raised hand notifications --- lang/main.json | 4 + .../features/base/participants/middleware.js | 24 +++++- react/features/notifications/actions.js | 83 ++++++++++++++++++- react/features/notifications/constants.js | 12 +++ react/features/notifications/middleware.js | 15 ++-- 5 files changed, 126 insertions(+), 12 deletions(-) diff --git a/lang/main.json b/lang/main.json index 3216ed36e..2964fe8ae 100644 --- a/lang/main.json +++ b/lang/main.json @@ -585,6 +585,9 @@ "invitedThreePlusMembers": "{{name}} and {{count}} others have been invited", "invitedTwoMembers": "{{first}} and {{second}} have been invited", "kickParticipant": "{{kicked}} was kicked by {{kicker}}", + "leftOneMember": "{{name}} left the meeting", + "leftThreePlusMembers": "{{name}} and many others left the meeting", + "leftTwoMembers": "{{first}} and {{second}} left the meeting", "me": "Me", "moderator": "You're now a moderator", "muted": "You have started the conversation muted.", @@ -596,6 +599,7 @@ "passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) removed by another participant", "passwordSetRemotely": "$t(lockRoomPasswordUppercase) set by another participant", "raisedHand": "Would like to speak.", + "raisedHands": "{{participantName}} and {{raisedHands}} more people", "screenShareNoAudio": " Share audio box was not checked in the window selection screen.", "screenShareNoAudioTitle": "Couldn't share system audio!", "somebody": "Somebody", diff --git a/react/features/base/participants/middleware.js b/react/features/base/participants/middleware.js index 6d7d7194b..30ea47d70 100644 --- a/react/features/base/participants/middleware.js +++ b/react/features/base/participants/middleware.js @@ -1,12 +1,17 @@ // @flow +import i18n from 'i18next'; import { batch } from 'react-redux'; import UIEvents from '../../../../service/UI/UIEvents'; import { approveParticipant } from '../../av-moderation/actions'; import { toggleE2EE } from '../../e2ee/actions'; import { MAX_MODE } from '../../e2ee/constants'; -import { NOTIFICATION_TIMEOUT_TYPE, showNotification } from '../../notifications'; +import { + NOTIFICATION_TIMEOUT_TYPE, + RAISE_HAND_NOTIFICATION_ID, + showNotification +} from '../../notifications'; import { isForceMuted } from '../../participants-pane/functions'; import { CALLING, INVITED } from '../../presence-status'; import { RAISE_HAND_SOUND_ID } from '../../reactions/constants'; @@ -555,12 +560,27 @@ function _raiseHandUpdated({ dispatch, getState }, conference, participantId, ne } : {}; if (raisedHandTimestamp) { + let notificationTitle; + const participantName = getParticipantDisplayName(state, participantId); + const { raisedHandsQueue } = state['features/base/participants']; + + if (raisedHandsQueue.length > 1) { + const raisedHands = raisedHandsQueue.length - 1; + + notificationTitle = i18n.t('notify.raisedHands', { + participantName, + raisedHands + }); + } else { + notificationTitle = participantName; + } dispatch(showNotification({ titleKey: 'notify.somebody', - title: getParticipantDisplayName(state, participantId), + title: notificationTitle, descriptionKey: 'notify.raisedHand', raiseHandNotification: true, concatText: true, + uid: RAISE_HAND_NOTIFICATION_ID, ...action }, shouldDisplayAllowAction ? NOTIFICATION_TIMEOUT_TYPE.MEDIUM : NOTIFICATION_TIMEOUT_TYPE.SHORT)); dispatch(playSound(RAISE_HAND_SOUND_ID)); diff --git a/react/features/notifications/actions.js b/react/features/notifications/actions.js index 732d46ac6..b565eee0e 100644 --- a/react/features/notifications/actions.js +++ b/react/features/notifications/actions.js @@ -17,7 +17,8 @@ import { NOTIFICATION_TIMEOUT_TYPE, NOTIFICATION_TIMEOUT, NOTIFICATION_TYPE, - SILENT_JOIN_THRESHOLD + SILENT_JOIN_THRESHOLD, + SILENT_LEFT_THRESHOLD } from './constants'; /** @@ -219,6 +220,70 @@ const _throttledNotifyParticipantConnected = throttle((dispatch: Dispatch, }, 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 = []; + +/** + * 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: Dispatch, getState: Function) => { + 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 @@ -228,7 +293,21 @@ const _throttledNotifyParticipantConnected = throttle((dispatch: Dispatch, * @returns {Function} */ export function showParticipantJoinedNotification(displayName: string) { - joinedParticipantsNames.push(displayName); + leftParticipantsNames.push(displayName); return (dispatch: Dispatch, getState: Function) => _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) { + joinedParticipantsNames.push(displayName); + + return (dispatch: Dispatch, getState: Function) => _throttledNotifyParticipantLeft(dispatch, getState); +} diff --git a/react/features/notifications/constants.js b/react/features/notifications/constants.js index cfe31cf0b..9b4fcacab 100644 --- a/react/features/notifications/constants.js +++ b/react/features/notifications/constants.js @@ -46,7 +46,19 @@ export const NOTIFICATION_TYPE_PRIORITIES = { [NOTIFICATION_TYPE.WARNING]: 4 }; +/** + * The identifier of the raise hand notification. + * + * @type {string} + */ +export const RAISE_HAND_NOTIFICATION_ID = 'RAISE_HAND_NOTIFICATION'; + /** * Amount of participants beyond which no join notification will be emitted. */ export const SILENT_JOIN_THRESHOLD = 30; + +/** + * Amount of participants beyond which no left notification will be emitted. + */ +export const SILENT_LEFT_THRESHOLD = 30; diff --git a/react/features/notifications/middleware.js b/react/features/notifications/middleware.js index 5bb853f23..fcc2c1138 100644 --- a/react/features/notifications/middleware.js +++ b/react/features/notifications/middleware.js @@ -17,13 +17,12 @@ import { clearNotifications, hideRaiseHandNotifications, showNotification, - showParticipantJoinedNotification + showParticipantJoinedNotification, + showParticipantLeftNotification } from './actions'; import { NOTIFICATION_TIMEOUT_TYPE } from './constants'; import { joinLeaveNotificationsDisabled } from './functions'; -declare var interfaceConfig: Object; - /** * Middleware that captures actions to display notifications. * @@ -49,17 +48,17 @@ MiddlewareRegistry.register(store => next => action => { } case PARTICIPANT_LEFT: { if (!joinLeaveNotificationsDisabled()) { + const { dispatch, getState } = store; + const state = getState(); const participant = getParticipantById( store.getState(), action.participant.id ); if (participant && !participant.local && !action.participant.isReplaced) { - store.dispatch(showNotification({ - descriptionKey: 'notify.disconnected', - titleKey: 'notify.somebody', - title: participant.name - }, NOTIFICATION_TIMEOUT_TYPE.SHORT)); + dispatch(showParticipantLeftNotification( + getParticipantDisplayName(state, participant.id) + )); } }