diff --git a/modules/API/API.js b/modules/API/API.js index 9975ad729..2e20ff44c 100644 --- a/modules/API/API.js +++ b/modules/API/API.js @@ -37,7 +37,8 @@ import { kickParticipant, raiseHand, isParticipantModerator, - isLocalParticipantModerator + isLocalParticipantModerator, + hasRaisedHand } from '../../react/features/base/participants'; import { updateSettings } from '../../react/features/base/settings'; import { isToggleCameraEnabled, toggleCamera } from '../../react/features/base/tracks'; @@ -281,7 +282,7 @@ function initCommands() { if (!localParticipant) { return; } - const { raisedHand } = localParticipant; + const raisedHand = hasRaisedHand(localParticipant); sendAnalytics(createApiEvent('raise-hand.toggled')); APP.store.dispatch(raiseHand(!raisedHand)); diff --git a/react/features/av-moderation/middleware.js b/react/features/av-moderation/middleware.js index 1dad96117..dd8f3d7eb 100644 --- a/react/features/av-moderation/middleware.js +++ b/react/features/av-moderation/middleware.js @@ -8,6 +8,7 @@ import { MEDIA_TYPE } from '../base/media'; import { getLocalParticipant, getRemoteParticipants, + hasRaisedHand, isLocalParticipantModerator, isParticipantModerator, PARTICIPANT_UPDATED, @@ -134,7 +135,7 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => { if (isLocalParticipantModerator(state)) { // this is handled only by moderators - if (participant.raisedHand) { + if (hasRaisedHand(participant)) { // if participant raises hand show notification !isParticipantApproved(participant.id, MEDIA_TYPE.AUDIO)(state) && dispatch(participantPendingAudio(participant)); @@ -148,7 +149,7 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => { // this is the granted moderator case getRemoteParticipants(state).forEach(p => { - p.raisedHand && !isParticipantApproved(p.id, MEDIA_TYPE.AUDIO)(state) + hasRaisedHand(p) && !isParticipantApproved(p.id, MEDIA_TYPE.AUDIO)(state) && dispatch(participantPendingAudio(p)); }); } diff --git a/react/features/base/participants/actions.js b/react/features/base/participants/actions.js index bbd0c0227..910190aa5 100644 --- a/react/features/base/participants/actions.js +++ b/react/features/base/participants/actions.js @@ -564,13 +564,13 @@ export function setLoadableAvatarUrl(participantId, url) { * @param {boolean} enabled - Raise or lower hand. * @returns {{ * type: LOCAL_PARTICIPANT_RAISE_HAND, - * enabled: boolean + * raisedHandTimestamp: number * }} */ export function raiseHand(enabled) { return { type: LOCAL_PARTICIPANT_RAISE_HAND, - enabled + raisedHandTimestamp: enabled ? Date.now() : 0 }; } diff --git a/react/features/base/participants/functions.js b/react/features/base/participants/functions.js index beefae849..b0c8e740f 100644 --- a/react/features/base/participants/functions.js +++ b/react/features/base/participants/functions.js @@ -461,10 +461,20 @@ async function _getFirstLoadableAvatarUrl(participant, store) { * @param {(Function|Object)} stateful - The (whole) redux state, or redux's * {@code getState} function to be used to retrieve the state * features/base/participants. - * @returns {Array} + * @returns {Array} */ -export function getRaiseHandsQueue(stateful: Object | Function): Array { +export function getRaiseHandsQueue(stateful: Object | Function): Array { const { raisedHandsQueue } = toState(stateful)['features/base/participants']; return raisedHandsQueue; } + +/** + * Returns whether the given participant has his hand raised or not. + * + * @param {Object} participant - The participant. + * @returns {boolean} - Whether participant has raise hand or not. + */ +export function hasRaisedHand(participant: Object): boolean { + return Boolean(participant && participant.raisedHandTimestamp); +} diff --git a/react/features/base/participants/middleware.js b/react/features/base/participants/middleware.js index 384185e77..d985f431e 100644 --- a/react/features/base/participants/middleware.js +++ b/react/features/base/participants/middleware.js @@ -94,7 +94,7 @@ MiddlewareRegistry.register(store => next => action => { const participant = getLocalParticipant(state); const isLocal = participant && participant.id === id; - if (isLocal && participant.raisedHand === undefined) { + if (isLocal && participant.raisedHandTimestamp === undefined) { // if local was undefined, let's leave it like that // avoids sending unnecessary presence updates break; @@ -105,7 +105,7 @@ MiddlewareRegistry.register(store => next => action => { conference, id, local: isLocal, - raisedHand: false + raisedHandTimestamp: 0 })); } @@ -127,14 +127,9 @@ MiddlewareRegistry.register(store => next => action => { } case LOCAL_PARTICIPANT_RAISE_HAND: { - const { enabled } = action; + const { raisedHandTimestamp } = action; const localId = getLocalParticipant(store.getState())?.id; - store.dispatch(raiseHandUpdateQueue({ - id: localId, - raisedHand: enabled - })); - store.dispatch(participantUpdated({ // XXX Only the local participant is allowed to update without // stating the JitsiConference instance (i.e. participant property @@ -144,11 +139,16 @@ MiddlewareRegistry.register(store => next => action => { id: localId, local: true, - raisedHand: enabled + raisedHandTimestamp + })); + + store.dispatch(raiseHandUpdateQueue({ + id: localId, + raisedHandTimestamp })); if (typeof APP !== 'undefined') { - APP.API.notifyRaiseHandUpdated(localId, enabled); + APP.API.notifyRaiseHandUpdated(localId, raisedHandTimestamp); } break; @@ -177,16 +177,22 @@ MiddlewareRegistry.register(store => next => action => { case RAISE_HAND_UPDATED: { const { participant } = action; - const queue = getRaiseHandsQueue(store.getState()); + let queue = getRaiseHandsQueue(store.getState()); - if (participant.raisedHand) { - queue.push(participant.id); - action.queue = queue; + if (participant.raisedHandTimestamp) { + queue.push({ + id: participant.id, + raisedHandTimestamp: participant.raisedHandTimestamp + }); + + // sort the queue before adding to store. + queue = queue.sort(({ raisedHandTimestamp: a }, { raisedHandTimestamp: b }) => a - b); } else { - const filteredQueue = queue.filter(id => id !== participant.id); - - action.queue = filteredQueue; + // no need to sort on remove value. + queue = queue.filter(({ id }) => id !== participant.id); } + + action.queue = queue; break; } @@ -287,7 +293,8 @@ StateListenerRegistry.register( id: participant.getId(), features: { 'screen-sharing': true } })), - 'raisedHand': (participant, value) => _raiseHandUpdated(store, conference, participant.getId(), value), + 'raisedHand': (participant, value) => + _raiseHandUpdated(store, conference, participant.getId(), value), 'remoteControlSessionStatus': (participant, value) => store.dispatch(participantUpdated({ conference, @@ -320,7 +327,7 @@ StateListenerRegistry.register( // We left the conference, the local participant must be updated. _e2eeUpdated(store, conference, localParticipantId, false); - _raiseHandUpdated(store, conference, localParticipantId, false); + _raiseHandUpdated(store, conference, localParticipantId, 0); } } ); @@ -451,18 +458,19 @@ function _maybePlaySounds({ getState, dispatch }, action) { */ function _participantJoinedOrUpdated(store, next, action) { const { dispatch, getState } = store; - const { participant: { avatarURL, email, id, local, name, raisedHand } } = action; + const { participant: { avatarURL, email, id, local, name, raisedHandTimestamp } } = action; // Send an external update of the local participant's raised hand state // if a new raised hand state is defined in the action. - if (typeof raisedHand !== 'undefined') { + if (typeof raisedHandTimestamp !== 'undefined') { if (local) { const { conference } = getState()['features/base/conference']; + const rHand = parseInt(raisedHandTimestamp, 10); // Send raisedHand signalling only if there is a change - if (conference && raisedHand !== getLocalParticipant(getState()).raisedHand) { - conference.setLocalParticipantProperty('raisedHand', raisedHand); + if (conference && rHand !== getLocalParticipant(getState()).raisedHandTimestamp) { + conference.setLocalParticipantProperty('raisedHand', rHand); } } } @@ -508,22 +516,34 @@ function _participantJoinedOrUpdated(store, next, action) { * @returns {void} */ function _raiseHandUpdated({ dispatch, getState }, conference, participantId, newValue) { - const raisedHand = newValue === 'true'; + let raisedHandTimestamp; + + switch (newValue) { + case undefined: + case 'false': + raisedHandTimestamp = 0; + break; + case 'true': + raisedHandTimestamp = Date.now(); + break; + default: + raisedHandTimestamp = parseInt(newValue, 10); + } const state = getState(); dispatch(participantUpdated({ conference, id: participantId, - raisedHand + raisedHandTimestamp })); dispatch(raiseHandUpdateQueue({ id: participantId, - raisedHand + raisedHandTimestamp })); if (typeof APP !== 'undefined') { - APP.API.notifyRaiseHandUpdated(participantId, raisedHand); + APP.API.notifyRaiseHandUpdated(participantId, raisedHandTimestamp); } const isModerator = isLocalParticipantModerator(state); @@ -540,7 +560,7 @@ function _raiseHandUpdated({ dispatch, getState }, conference, participantId, ne customActionHandler: () => dispatch(approveParticipant(participantId)) } : {}; - if (raisedHand) { + if (raisedHandTimestamp) { dispatch(showNotification({ titleKey: 'notify.somebody', title: getParticipantDisplayName(state, participantId), diff --git a/react/features/filmstrip/components/AbstractRaisedHandIndicator.js b/react/features/filmstrip/components/AbstractRaisedHandIndicator.js index 2afbb8941..6c95801f4 100644 --- a/react/features/filmstrip/components/AbstractRaisedHandIndicator.js +++ b/react/features/filmstrip/components/AbstractRaisedHandIndicator.js @@ -2,7 +2,7 @@ import { Component } from 'react'; -import { getParticipantById } from '../../base/participants'; +import { getParticipantById, hasRaisedHand } from '../../base/participants'; export type Props = { @@ -57,6 +57,6 @@ export function _mapStateToProps(state: Object, ownProps: Props): Object { const participant = getParticipantById(state, ownProps.participantId); return { - _raisedHand: participant && participant.raisedHand + _raisedHand: hasRaisedHand(participant) }; } diff --git a/react/features/participants-pane/components/native/LobbyParticipantItem.js b/react/features/participants-pane/components/native/LobbyParticipantItem.js index 198f1247d..43bcc16c1 100644 --- a/react/features/participants-pane/components/native/LobbyParticipantItem.js +++ b/react/features/participants-pane/components/native/LobbyParticipantItem.js @@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next'; import { Button } from 'react-native-paper'; import { useDispatch } from 'react-redux'; +import { hasRaisedHand } from '../../../base/participants'; import { approveKnockingParticipant } from '../../../lobby/actions.native'; import { showContextMenuReject } from '../../actions.native'; import { MEDIA_STATE } from '../../constants'; @@ -35,7 +36,7 @@ export const LobbyParticipantItem = ({ participant: p }: Props) => { onPress = { openContextMenuReject } participant = { p } participantID = { p.id } - raisedHand = { p.raisedHand } + raisedHand = { hasRaisedHand(p) } videoMediaState = { MEDIA_STATE.NONE }>