feat(raised-hand) Change `raisedHand` to a timestamp instead of boole… (#10167)

- this was needed for sorting the raised hand participants in participants pane in
the order they raised their hand also for participants joining late
This commit is contained in:
Horatiu Muresan 2021-10-21 12:40:57 +03:00 committed by GitHub
parent f435fc4ade
commit 4b7a6741fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 97 additions and 78 deletions

View File

@ -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));

View File

@ -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));
});
}

View File

@ -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
};
}

View File

@ -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<string>}
* @returns {Array<Object>}
*/
export function getRaiseHandsQueue(stateful: Object | Function): Array<string> {
export function getRaiseHandsQueue(stateful: Object | Function): Array<Object> {
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);
}

View File

@ -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),

View File

@ -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)
};
}

View File

@ -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 }>
<Button
children = { t('lobby.admit') }

View File

@ -7,6 +7,7 @@ import {
getLocalParticipant,
getParticipantByIdOrUndefined,
getParticipantDisplayName,
hasRaisedHand,
isParticipantModerator
} from '../../../base/participants';
import { connect } from '../../../base/redux';
@ -190,7 +191,7 @@ function mapStateToProps(state, ownProps): Object {
_local: Boolean(participant?.local),
_localVideoOwner: Boolean(ownerId === localParticipantId),
_participantID: participant?.id,
_raisedHand: Boolean(participant?.raisedHand),
_raisedHand: hasRaisedHand(participant),
_videoMediaState: videoMediaState
};
}

View File

@ -3,6 +3,7 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { hasRaisedHand } from '../../../base/participants';
import { ACTION_TRIGGER, MEDIA_STATE } from '../../constants';
import { useLobbyActions } from '../../hooks';
@ -45,7 +46,7 @@ export const LobbyParticipantItem = ({
openDrawerForParticipant = { openDrawerForParticipant }
overflowDrawer = { overflowDrawer }
participantID = { id }
raisedHand = { p.raisedHand }
raisedHand = { hasRaisedHand(p) }
videoMediaState = { MEDIA_STATE.NONE }
youText = { t('chat.you') }>
<ParticipantActionButton

View File

@ -9,6 +9,7 @@ import {
getLocalParticipant,
getParticipantByIdOrUndefined,
getParticipantDisplayName,
hasRaisedHand,
isParticipantModerator
} from '../../../base/participants';
import { connect } from '../../../base/redux';
@ -307,7 +308,7 @@ function _mapStateToProps(state, ownProps): Object {
_participant: participant,
_participantID: participant?.id,
_quickActionButtonType,
_raisedHand: Boolean(participant?.raisedHand),
_raisedHand: hasRaisedHand(participant),
_videoMediaState
};
}

View File

@ -211,7 +211,7 @@ export function getSortedParticipantIds(stateful: Object | Function): Array<stri
const { id } = getLocalParticipant(stateful);
const remoteParticipants = getRemoteParticipantsSorted(stateful);
const reorderedParticipants = new Set(remoteParticipants);
const raisedHandParticipants = getRaiseHandsQueue(stateful);
const raisedHandParticipants = getRaiseHandsQueue(stateful).map(({ id: particId }) => particId);
const remoteRaisedHandParticipants = new Set(raisedHandParticipants || []);
const dominantSpeaker = getDominantSpeakerParticipant(stateful);
@ -219,15 +219,11 @@ export function getSortedParticipantIds(stateful: Object | Function): Array<stri
// Avoid duplicates.
if (reorderedParticipants.has(participant)) {
reorderedParticipants.delete(participant);
} else {
remoteRaisedHandParticipants.delete(participant);
}
}
// Remove self.
remoteRaisedHandParticipants.delete(id);
const dominant = [];
const local = remoteRaisedHandParticipants.has(id) ? [] : [ id ];
// Remove dominant speaker.
if (dominantSpeaker && dominantSpeaker.id !== id) {
@ -239,7 +235,7 @@ export function getSortedParticipantIds(stateful: Object | Function): Array<stri
// Move self and participants with raised hand to the top of the list.
return [
...dominant,
id,
...local,
...Array.from(remoteRaisedHandParticipants.keys()),
...Array.from(reorderedParticipants.keys())
];

View File

@ -12,6 +12,7 @@ import { ColorSchemeRegistry } from '../../../base/color-scheme';
import { translate } from '../../../base/i18n';
import {
getLocalParticipant,
hasRaisedHand,
raiseHand
} from '../../../base/participants';
import { connect } from '../../../base/redux';
@ -157,7 +158,7 @@ function _mapStateToProps(state): Object {
return {
_localParticipant,
_raisedHand: _localParticipant.raisedHand,
_raisedHand: hasRaisedHand(_localParticipant),
_styles: ColorSchemeRegistry.get(state, 'Toolbox').raiseHandButton
};
}

View File

@ -7,7 +7,7 @@ import { RAISE_HAND_ENABLED, getFeatureFlag } from '../../../base/flags';
import { translate } from '../../../base/i18n';
import { IconRaisedHand } from '../../../base/icons';
import {
getLocalParticipant
getLocalParticipant, hasRaisedHand
} from '../../../base/participants';
import { connect } from '../../../base/redux';
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
@ -81,7 +81,7 @@ function _mapStateToProps(state, ownProps): Object {
const { visible = enabled } = ownProps;
return {
_raisedHand: _localParticipant.raisedHand,
_raisedHand: hasRaisedHand(_localParticipant),
_reactionsOpen: isDialogOpen(state, ReactionMenuDialog),
visible
};

View File

@ -10,7 +10,7 @@ import {
} from '../../../analytics';
import { isMobileBrowser } from '../../../base/environment/utils';
import { translate } from '../../../base/i18n';
import { getLocalParticipant, participantUpdated } from '../../../base/participants';
import { getLocalParticipant, hasRaisedHand, raiseHand } from '../../../base/participants';
import { connect } from '../../../base/redux';
import { dockToolbox } from '../../../toolbox/actions.web';
import { addReactionToBuffer } from '../../actions.any';
@ -123,22 +123,9 @@ class ReactionsMenu extends Component<Props> {
* @returns {void}
*/
_doToggleRaiseHand() {
const { _localParticipantID, _raisedHand } = this.props;
const newRaisedStatus = !_raisedHand;
const { _raisedHand } = this.props;
this.props.dispatch(participantUpdated({
// XXX Only the local participant is allowed to update without
// stating the JitsiConference instance (i.e. participant property
// `conference` for a remote participant) because the local
// participant is uniquely identified by the very fact that there is
// only one local participant.
id: _localParticipantID,
local: true,
raisedHand: newRaisedStatus
}));
APP.API.notifyRaiseHandUpdated(_localParticipantID, newRaisedStatus);
this.props.dispatch(raiseHand(!_raisedHand));
}
/**
@ -221,7 +208,7 @@ function mapStateToProps(state) {
return {
_localParticipantID: localParticipant.id,
_isMobile: isMobileBrowser(),
_raisedHand: localParticipant.raisedHand
_raisedHand: hasRaisedHand(localParticipant)
};
}

View File

@ -5,7 +5,7 @@ import React from 'react';
import { isMobileBrowser } from '../../../base/environment/utils';
import { translate } from '../../../base/i18n';
import { IconArrowUp, IconRaisedHand } from '../../../base/icons';
import { getLocalParticipant } from '../../../base/participants';
import { getLocalParticipant, hasRaisedHand } from '../../../base/participants';
import { connect } from '../../../base/redux';
import { ToolboxButtonWithIcon } from '../../../base/toolbox/components';
import ToolbarButton from '../../../toolbox/components/web/ToolbarButton';
@ -138,7 +138,7 @@ function mapStateToProps(state) {
isOpen: getReactionsMenuVisibility(state),
isMobile: isMobileBrowser(),
reactionsQueue: getReactionsQueue(state),
raisedHand: localParticipant?.raisedHand
raisedHand: hasRaisedHand(localParticipant)
};
}

View File

@ -11,6 +11,7 @@ import { translate } from '../../../base/i18n';
import { IconRaisedHand } from '../../../base/icons';
import {
getLocalParticipant,
hasRaisedHand,
raiseHand
} from '../../../base/participants';
import { connect } from '../../../base/redux';
@ -97,7 +98,7 @@ function _mapStateToProps(state, ownProps): Object {
return {
_localParticipant,
_raisedHand: _localParticipant.raisedHand,
_raisedHand: hasRaisedHand(_localParticipant),
visible
};
}

View File

@ -17,6 +17,7 @@ import { translate } from '../../../base/i18n';
import JitsiMeetJS from '../../../base/lib-jitsi-meet';
import {
getLocalParticipant,
hasRaisedHand,
haveParticipantWithScreenSharingFeature,
raiseHand
} from '../../../base/participants';
@ -488,12 +489,9 @@ class Toolbox extends Component<Props> {
* @returns {void}
*/
_doToggleRaiseHand() {
const { _localParticipantID, _raisedHand } = this.props;
const newRaisedStatus = !_raisedHand;
const { _raisedHand } = this.props;
this.props.dispatch(raiseHand(newRaisedStatus));
APP.API.notifyRaiseHandUpdated(_localParticipantID, newRaisedStatus);
this.props.dispatch(raiseHand(!_raisedHand));
}
/**
@ -1338,7 +1336,7 @@ function _mapStateToProps(state, ownProps) {
_localVideo: localVideo,
_overflowMenuVisible: overflowMenuVisible,
_participantsPaneOpen: getParticipantsPaneOpen(state),
_raisedHand: localParticipant?.raisedHand,
_raisedHand: hasRaisedHand(localParticipant),
_reactionsEnabled: isReactionsEnabled(state),
_screenSharing: isScreenVideoShared(state),
_tileViewEnabled: shouldDisplayTileView(state),