2018-03-07 00:28:19 +00:00
|
|
|
// @flow
|
2017-10-02 23:08:07 +00:00
|
|
|
|
2021-12-02 13:17:07 +00:00
|
|
|
import i18n from 'i18next';
|
2021-07-09 12:36:19 +00:00
|
|
|
import { batch } from 'react-redux';
|
|
|
|
|
2019-03-26 12:11:09 +00:00
|
|
|
import UIEvents from '../../../../service/UI/UIEvents';
|
2021-09-10 11:05:16 +00:00
|
|
|
import { approveParticipant } from '../../av-moderation/actions';
|
2022-05-20 10:45:09 +00:00
|
|
|
import { UPDATE_BREAKOUT_ROOMS } from '../../breakout-rooms/actionTypes';
|
|
|
|
import { getBreakoutRooms } from '../../breakout-rooms/functions';
|
2021-04-01 09:34:01 +00:00
|
|
|
import { toggleE2EE } from '../../e2ee/actions';
|
2021-09-21 11:00:23 +00:00
|
|
|
import { MAX_MODE } from '../../e2ee/constants';
|
2021-12-02 13:17:07 +00:00
|
|
|
import {
|
2022-06-03 11:45:27 +00:00
|
|
|
LOCAL_RECORDING_NOTIFICATION_ID,
|
2021-12-02 13:17:07 +00:00
|
|
|
NOTIFICATION_TIMEOUT_TYPE,
|
|
|
|
RAISE_HAND_NOTIFICATION_ID,
|
|
|
|
showNotification
|
|
|
|
} from '../../notifications';
|
2021-09-10 11:05:16 +00:00
|
|
|
import { isForceMuted } from '../../participants-pane/functions';
|
2019-03-26 12:11:09 +00:00
|
|
|
import { CALLING, INVITED } from '../../presence-status';
|
2021-08-31 11:00:27 +00:00
|
|
|
import { RAISE_HAND_SOUND_ID } from '../../reactions/constants';
|
2022-06-03 11:45:27 +00:00
|
|
|
import { RECORDING_OFF_SOUND_ID, RECORDING_ON_SOUND_ID } from '../../recording';
|
2018-07-11 09:42:43 +00:00
|
|
|
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app';
|
2018-05-24 20:15:32 +00:00
|
|
|
import {
|
|
|
|
CONFERENCE_WILL_JOIN,
|
|
|
|
forEachConference,
|
|
|
|
getCurrentConference
|
|
|
|
} from '../conference';
|
2022-08-10 21:19:48 +00:00
|
|
|
import { SET_CONFIG } from '../config';
|
2021-09-09 21:53:25 +00:00
|
|
|
import { getDisableRemoveRaisedHandOnFocus } from '../config/functions.any';
|
2019-03-26 12:11:09 +00:00
|
|
|
import { JitsiConferenceEvents } from '../lib-jitsi-meet';
|
2021-09-10 11:05:16 +00:00
|
|
|
import { MEDIA_TYPE } from '../media';
|
2018-05-22 03:48:51 +00:00
|
|
|
import { MiddlewareRegistry, StateListenerRegistry } from '../redux';
|
2018-02-26 19:37:12 +00:00
|
|
|
import { playSound, registerSound, unregisterSound } from '../sounds';
|
2016-10-05 14:36:59 +00:00
|
|
|
|
2018-03-07 00:28:19 +00:00
|
|
|
import {
|
|
|
|
DOMINANT_SPEAKER_CHANGED,
|
2020-07-15 10:13:28 +00:00
|
|
|
GRANT_MODERATOR,
|
2017-08-14 15:02:58 +00:00
|
|
|
KICK_PARTICIPANT,
|
2022-01-21 08:07:55 +00:00
|
|
|
LOCAL_PARTICIPANT_AUDIO_LEVEL_CHANGED,
|
2021-06-23 11:23:44 +00:00
|
|
|
LOCAL_PARTICIPANT_RAISE_HAND,
|
2017-08-14 15:02:58 +00:00
|
|
|
MUTE_REMOTE_PARTICIPANT,
|
2022-05-20 10:45:09 +00:00
|
|
|
OVERWRITE_PARTICIPANTS_NAMES,
|
|
|
|
OVERWRITE_PARTICIPANT_NAME,
|
2017-12-19 23:11:54 +00:00
|
|
|
PARTICIPANT_DISPLAY_NAME_CHANGED,
|
|
|
|
PARTICIPANT_JOINED,
|
2018-02-26 19:37:12 +00:00
|
|
|
PARTICIPANT_LEFT,
|
2021-09-10 11:05:16 +00:00
|
|
|
PARTICIPANT_UPDATED,
|
2022-06-03 11:45:27 +00:00
|
|
|
RAISE_HAND_UPDATED,
|
|
|
|
SET_LOCAL_PARTICIPANT_RECORDING_STATUS
|
2017-08-14 15:02:58 +00:00
|
|
|
} from './actionTypes';
|
2020-05-20 10:57:03 +00:00
|
|
|
import {
|
|
|
|
localParticipantIdChanged,
|
|
|
|
localParticipantJoined,
|
|
|
|
localParticipantLeft,
|
2022-05-20 10:45:09 +00:00
|
|
|
overwriteParticipantName,
|
2020-05-20 10:57:03 +00:00
|
|
|
participantLeft,
|
|
|
|
participantUpdated,
|
2022-01-20 10:59:50 +00:00
|
|
|
raiseHand,
|
2021-09-10 11:05:16 +00:00
|
|
|
raiseHandUpdateQueue,
|
2020-05-20 10:57:03 +00:00
|
|
|
setLoadableAvatarUrl
|
|
|
|
} from './actions';
|
2018-02-26 19:37:12 +00:00
|
|
|
import {
|
|
|
|
LOCAL_PARTICIPANT_DEFAULT_ID,
|
2022-01-21 08:07:55 +00:00
|
|
|
LOWER_HAND_AUDIO_LEVEL,
|
2018-02-26 19:37:12 +00:00
|
|
|
PARTICIPANT_JOINED_SOUND_ID,
|
|
|
|
PARTICIPANT_LEFT_SOUND_ID
|
|
|
|
} from './constants';
|
2017-12-19 23:11:54 +00:00
|
|
|
import {
|
2022-01-21 08:07:55 +00:00
|
|
|
getDominantSpeakerParticipant,
|
2019-06-26 14:08:23 +00:00
|
|
|
getFirstLoadableAvatarUrl,
|
2018-02-26 19:37:12 +00:00
|
|
|
getLocalParticipant,
|
2019-06-26 14:08:23 +00:00
|
|
|
getParticipantById,
|
2019-03-27 15:29:41 +00:00
|
|
|
getParticipantCount,
|
2021-07-09 12:36:19 +00:00
|
|
|
getParticipantDisplayName,
|
2021-09-10 11:05:16 +00:00
|
|
|
getRaiseHandsQueue,
|
|
|
|
getRemoteParticipants,
|
2022-01-20 10:59:50 +00:00
|
|
|
hasRaisedHand,
|
2021-09-10 11:05:16 +00:00
|
|
|
isLocalParticipantModerator
|
2017-12-19 23:11:54 +00:00
|
|
|
} from './functions';
|
2022-05-20 10:45:09 +00:00
|
|
|
import logger from './logger';
|
2018-04-05 19:12:24 +00:00
|
|
|
import { PARTICIPANT_JOINED_FILE, PARTICIPANT_LEFT_FILE } from './sounds';
|
2022-04-29 14:32:16 +00:00
|
|
|
import './subscriber';
|
2017-06-29 03:35:43 +00:00
|
|
|
|
|
|
|
declare var APP: Object;
|
2016-10-05 14:36:59 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Middleware that captures CONFERENCE_JOINED and CONFERENCE_LEFT actions and
|
|
|
|
* updates respectively ID of local participant.
|
|
|
|
*
|
2018-05-22 03:42:22 +00:00
|
|
|
* @param {Store} store - The redux store.
|
2016-10-05 14:36:59 +00:00
|
|
|
* @returns {Function}
|
|
|
|
*/
|
|
|
|
MiddlewareRegistry.register(store => next => action => {
|
|
|
|
switch (action.type) {
|
2018-02-26 19:37:12 +00:00
|
|
|
case APP_WILL_MOUNT:
|
|
|
|
_registerSounds(store);
|
2018-05-03 13:30:07 +00:00
|
|
|
|
|
|
|
return _localParticipantJoined(store, next, action);
|
2018-05-18 10:27:36 +00:00
|
|
|
|
2018-02-26 19:37:12 +00:00
|
|
|
case APP_WILL_UNMOUNT:
|
|
|
|
_unregisterSounds(store);
|
2018-07-10 09:11:45 +00:00
|
|
|
|
|
|
|
return _localParticipantLeft(store, next, action);
|
2018-05-18 10:27:36 +00:00
|
|
|
|
|
|
|
case CONFERENCE_WILL_JOIN:
|
2016-12-12 00:29:13 +00:00
|
|
|
store.dispatch(localParticipantIdChanged(action.conference.myUserId()));
|
2016-10-05 14:36:59 +00:00
|
|
|
break;
|
|
|
|
|
2018-03-07 00:28:19 +00:00
|
|
|
case DOMINANT_SPEAKER_CHANGED: {
|
2021-10-21 14:52:22 +00:00
|
|
|
// Lower hand through xmpp when local participant becomes dominant speaker.
|
|
|
|
const { id } = action.participant;
|
2021-09-09 21:53:25 +00:00
|
|
|
const state = store.getState();
|
|
|
|
const participant = getLocalParticipant(state);
|
2020-12-15 16:00:07 +00:00
|
|
|
const isLocal = participant && participant.id === id;
|
|
|
|
|
2021-10-21 14:52:22 +00:00
|
|
|
if (isLocal && hasRaisedHand(participant) && !getDisableRemoveRaisedHandOnFocus(state)) {
|
|
|
|
store.dispatch(raiseHand(false));
|
2021-09-09 21:53:25 +00:00
|
|
|
}
|
2018-03-07 00:28:19 +00:00
|
|
|
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2022-01-21 08:07:55 +00:00
|
|
|
case LOCAL_PARTICIPANT_AUDIO_LEVEL_CHANGED: {
|
|
|
|
const state = store.getState();
|
|
|
|
const participant = getDominantSpeakerParticipant(state);
|
|
|
|
|
|
|
|
if (
|
|
|
|
participant
|
|
|
|
&& participant.local
|
|
|
|
&& hasRaisedHand(participant)
|
|
|
|
&& action.level > LOWER_HAND_AUDIO_LEVEL
|
|
|
|
&& !getDisableRemoveRaisedHandOnFocus(state)
|
|
|
|
) {
|
|
|
|
store.dispatch(raiseHand(false));
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2020-07-15 10:13:28 +00:00
|
|
|
case GRANT_MODERATOR: {
|
|
|
|
const { conference } = store.getState()['features/base/conference'];
|
|
|
|
|
|
|
|
conference.grantOwner(action.id);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2018-05-22 03:42:22 +00:00
|
|
|
case KICK_PARTICIPANT: {
|
|
|
|
const { conference } = store.getState()['features/base/conference'];
|
|
|
|
|
2017-11-09 17:23:17 +00:00
|
|
|
conference.kickParticipant(action.id);
|
2017-08-14 15:02:58 +00:00
|
|
|
break;
|
2018-05-22 03:42:22 +00:00
|
|
|
}
|
|
|
|
|
2021-06-23 11:23:44 +00:00
|
|
|
case LOCAL_PARTICIPANT_RAISE_HAND: {
|
2021-10-21 09:40:57 +00:00
|
|
|
const { raisedHandTimestamp } = action;
|
2021-06-23 11:23:44 +00:00
|
|
|
const localId = getLocalParticipant(store.getState())?.id;
|
|
|
|
|
|
|
|
store.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: localId,
|
|
|
|
local: true,
|
2021-10-21 09:40:57 +00:00
|
|
|
raisedHandTimestamp
|
|
|
|
}));
|
|
|
|
|
|
|
|
store.dispatch(raiseHandUpdateQueue({
|
|
|
|
id: localId,
|
|
|
|
raisedHandTimestamp
|
2021-06-23 11:23:44 +00:00
|
|
|
}));
|
|
|
|
|
|
|
|
if (typeof APP !== 'undefined') {
|
2021-10-21 09:40:57 +00:00
|
|
|
APP.API.notifyRaiseHandUpdated(localId, raisedHandTimestamp);
|
2021-06-23 11:23:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2022-08-10 21:19:48 +00:00
|
|
|
case SET_CONFIG: {
|
|
|
|
const result = next(action);
|
|
|
|
|
|
|
|
const state = store.getState();
|
|
|
|
const { deploymentInfo } = state['features/base/config'];
|
|
|
|
|
|
|
|
// if there userRegion set let's use it for the local participant
|
|
|
|
if (deploymentInfo && deploymentInfo.userRegion) {
|
|
|
|
const localId = getLocalParticipant(state)?.id;
|
|
|
|
|
|
|
|
store.dispatch(participantUpdated({
|
|
|
|
id: localId,
|
|
|
|
local: true,
|
|
|
|
region: deploymentInfo.userRegion
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2022-06-03 11:45:27 +00:00
|
|
|
case SET_LOCAL_PARTICIPANT_RECORDING_STATUS: {
|
2022-06-22 09:52:22 +00:00
|
|
|
const state = store.getState();
|
2022-06-24 12:07:40 +00:00
|
|
|
const { recording, onlySelf } = action;
|
2022-06-22 09:52:22 +00:00
|
|
|
const localId = getLocalParticipant(state)?.id;
|
|
|
|
const { localRecording } = state['features/base/config'];
|
|
|
|
|
2022-06-24 12:07:40 +00:00
|
|
|
if (localRecording?.notifyAllParticipants && !onlySelf) {
|
2022-06-22 09:52:22 +00:00
|
|
|
store.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: localId,
|
|
|
|
local: true,
|
|
|
|
localRecording: recording
|
|
|
|
}));
|
|
|
|
}
|
2022-06-03 11:45:27 +00:00
|
|
|
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2018-05-22 03:42:22 +00:00
|
|
|
case MUTE_REMOTE_PARTICIPANT: {
|
|
|
|
const { conference } = store.getState()['features/base/conference'];
|
2017-08-14 15:02:58 +00:00
|
|
|
|
2021-02-24 21:45:07 +00:00
|
|
|
conference.muteParticipant(action.id, action.mediaType);
|
2017-08-14 15:02:58 +00:00
|
|
|
break;
|
2018-05-22 03:42:22 +00:00
|
|
|
}
|
2017-08-14 15:02:58 +00:00
|
|
|
|
2017-06-29 03:35:43 +00:00
|
|
|
// TODO Remove this middleware when the local display name update flow is
|
|
|
|
// fully brought into redux.
|
|
|
|
case PARTICIPANT_DISPLAY_NAME_CHANGED: {
|
|
|
|
if (typeof APP !== 'undefined') {
|
|
|
|
const participant = getLocalParticipant(store.getState());
|
|
|
|
|
|
|
|
if (participant && participant.id === action.id) {
|
|
|
|
APP.UI.emitEvent(UIEvents.NICKNAME_CHANGED, action.name);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
}
|
2017-12-19 23:11:54 +00:00
|
|
|
|
2021-09-10 11:05:16 +00:00
|
|
|
case RAISE_HAND_UPDATED: {
|
|
|
|
const { participant } = action;
|
2021-10-21 09:40:57 +00:00
|
|
|
let queue = getRaiseHandsQueue(store.getState());
|
2021-09-10 11:05:16 +00:00
|
|
|
|
2021-10-21 09:40:57 +00:00
|
|
|
if (participant.raisedHandTimestamp) {
|
|
|
|
queue.push({
|
|
|
|
id: participant.id,
|
|
|
|
raisedHandTimestamp: participant.raisedHandTimestamp
|
|
|
|
});
|
2021-09-10 11:05:16 +00:00
|
|
|
|
2021-10-21 09:40:57 +00:00
|
|
|
// sort the queue before adding to store.
|
|
|
|
queue = queue.sort(({ raisedHandTimestamp: a }, { raisedHandTimestamp: b }) => a - b);
|
|
|
|
} else {
|
|
|
|
// no need to sort on remove value.
|
|
|
|
queue = queue.filter(({ id }) => id !== participant.id);
|
2021-09-10 11:05:16 +00:00
|
|
|
}
|
2021-10-21 09:40:57 +00:00
|
|
|
|
|
|
|
action.queue = queue;
|
2021-09-10 11:05:16 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2019-02-07 15:30:20 +00:00
|
|
|
case PARTICIPANT_JOINED: {
|
2022-04-29 14:32:16 +00:00
|
|
|
const { isVirtualScreenshareParticipant } = action.participant;
|
2022-04-18 16:34:07 +00:00
|
|
|
|
2022-04-29 14:32:16 +00:00
|
|
|
// Do not play sounds when a virtual participant tile is created for screenshare.
|
|
|
|
!isVirtualScreenshareParticipant && _maybePlaySounds(store, action);
|
2017-12-19 23:11:54 +00:00
|
|
|
|
2019-06-25 22:25:43 +00:00
|
|
|
return _participantJoinedOrUpdated(store, next, action);
|
2019-02-07 15:30:20 +00:00
|
|
|
}
|
2017-12-19 23:11:54 +00:00
|
|
|
|
2022-04-18 16:34:07 +00:00
|
|
|
case PARTICIPANT_LEFT: {
|
2022-04-29 14:32:16 +00:00
|
|
|
const { isVirtualScreenshareParticipant } = action.participant;
|
2022-04-18 16:34:07 +00:00
|
|
|
|
|
|
|
// Do not play sounds when a tile for screenshare is removed.
|
2022-04-29 14:32:16 +00:00
|
|
|
!isVirtualScreenshareParticipant && _maybePlaySounds(store, action);
|
2022-04-18 16:34:07 +00:00
|
|
|
|
2017-12-19 23:11:54 +00:00
|
|
|
break;
|
2022-04-18 16:34:07 +00:00
|
|
|
}
|
2018-05-22 03:42:22 +00:00
|
|
|
|
|
|
|
case PARTICIPANT_UPDATED:
|
|
|
|
return _participantJoinedOrUpdated(store, next, action);
|
2020-10-29 16:11:15 +00:00
|
|
|
|
2022-05-20 10:45:09 +00:00
|
|
|
case OVERWRITE_PARTICIPANTS_NAMES: {
|
|
|
|
const { participantList } = action;
|
|
|
|
|
|
|
|
if (!Array.isArray(participantList)) {
|
|
|
|
logger.error('Overwrite names failed. Argument is not an array.');
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
batch(() => {
|
|
|
|
participantList.forEach(p => {
|
|
|
|
store.dispatch(overwriteParticipantName(p.id, p.name));
|
|
|
|
});
|
|
|
|
});
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
case OVERWRITE_PARTICIPANT_NAME: {
|
|
|
|
const { dispatch, getState } = store;
|
|
|
|
const state = getState();
|
|
|
|
const { id, name } = action;
|
|
|
|
|
|
|
|
let breakoutRoom = false, identifier = id;
|
|
|
|
|
|
|
|
if (id.indexOf('@') !== -1) {
|
|
|
|
identifier = id.slice(id.indexOf('/') + 1);
|
|
|
|
breakoutRoom = true;
|
|
|
|
action.id = identifier;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (breakoutRoom) {
|
|
|
|
const rooms = getBreakoutRooms(state);
|
|
|
|
const roomCounter = state['features/breakout-rooms'].roomCounter;
|
|
|
|
const newRooms = {};
|
|
|
|
|
|
|
|
Object.entries(rooms).forEach(([ key, r ]) => {
|
|
|
|
const participants = r?.participants || {};
|
|
|
|
const jid = Object.keys(participants).find(p =>
|
|
|
|
p.slice(p.indexOf('/') + 1) === identifier);
|
|
|
|
|
|
|
|
if (jid) {
|
|
|
|
newRooms[key] = {
|
|
|
|
...r,
|
|
|
|
participants: {
|
|
|
|
...participants,
|
|
|
|
[jid]: {
|
|
|
|
...participants[jid],
|
|
|
|
displayName: name
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
newRooms[key] = r;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
dispatch({
|
|
|
|
type: UPDATE_BREAKOUT_ROOMS,
|
|
|
|
rooms,
|
|
|
|
roomCounter,
|
|
|
|
updatedNames: true
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
dispatch(participantUpdated({
|
|
|
|
id: identifier,
|
|
|
|
name
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
2016-10-05 14:36:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return next(action);
|
|
|
|
});
|
2018-02-26 19:37:12 +00:00
|
|
|
|
2018-05-22 03:48:51 +00:00
|
|
|
/**
|
|
|
|
* Syncs the redux state features/base/participants up with the redux state
|
|
|
|
* features/base/conference by ensuring that the former does not contain remote
|
|
|
|
* participants no longer relevant to the latter. Introduced to address an issue
|
|
|
|
* with multiplying thumbnails in the filmstrip.
|
|
|
|
*/
|
|
|
|
StateListenerRegistry.register(
|
2018-05-24 20:15:32 +00:00
|
|
|
/* selector */ state => getCurrentConference(state),
|
2018-05-22 03:48:51 +00:00
|
|
|
/* listener */ (conference, { dispatch, getState }) => {
|
2021-07-09 12:36:19 +00:00
|
|
|
batch(() => {
|
|
|
|
for (const [ id, p ] of getRemoteParticipants(getState())) {
|
|
|
|
(!conference || p.conference !== conference)
|
|
|
|
&& dispatch(participantLeft(id, p.conference, p.isReplaced));
|
|
|
|
}
|
|
|
|
});
|
2018-05-22 03:48:51 +00:00
|
|
|
});
|
|
|
|
|
fix(base/participants): ensure default local id outside of conference
Makes sure that whenever a conference is left or switched, the local
participant's id will be equal to the default value.
The problem fixed by this commit is a situation where the local
participant may end up sharing the same ID with it's "ghost" when
rejoining a disconnected conference. The most important and easiest to
hit case is when the conference is left after the CONFERENCE_FAILED
event.
Another rare and harder to encounter in the real world issue is
where CONFERENCE_LEFT may come with the delay due to it's asynchronous
nature. The step by step scenario is as follows: trying to leave a
conference, but the network is not doing well, so it takes time,
requests are timing out. After getting back to the welcome page the
the CONFERENCE_LEFT has not arrived yet. The same conference is joined
again and the load config may timeout, but it will be read from the
cache. Now the network gets better and conference is joining which
results in our ghost participant added to the redux state. At this point
there's the root issue: two participants with the same id, because the
local one was neither cleared nor set to the new one yet
(PARTICIPANT_JOINED come, before CONFERENCE_JOINED where we adjust the
id). Then comes CONFERENCE_JOINED and we try to update our local id.
We're updating the ID of both ghost and local participant. It could be
also that the delayed CONFERENCE_LEFT comes for the old conference, but
it's too late and it would update the id for both participants.
The approach here reasons that the ID of the local participant
may be reset as soon as the local participant and, respectively, her ID
is no longer involved in a recoverable JitsiConference of interest to
the user and, consequently, the app.
Co-authored-by: Pawel Domas <pawel.domas@jitsi.org>
Co-authored-by: Lyubo Marinov <lmarinov@atlassian.com>
2018-05-16 20:08:34 +00:00
|
|
|
/**
|
|
|
|
* Reset the ID of the local participant to
|
|
|
|
* {@link LOCAL_PARTICIPANT_DEFAULT_ID}. Such a reset is deemed possible only if
|
|
|
|
* the local participant and, respectively, her ID is not involved in a
|
|
|
|
* conference which is still of interest to the user and, consequently, the app.
|
|
|
|
* For example, a conference which is in the process of leaving is no longer of
|
|
|
|
* interest the user, is unrecoverable from the perspective of the user and,
|
|
|
|
* consequently, the app.
|
|
|
|
*/
|
|
|
|
StateListenerRegistry.register(
|
|
|
|
/* selector */ state => state['features/base/conference'],
|
|
|
|
/* listener */ ({ leaving }, { dispatch, getState }) => {
|
|
|
|
const state = getState();
|
|
|
|
const localParticipant = getLocalParticipant(state);
|
|
|
|
let id;
|
|
|
|
|
|
|
|
if (!localParticipant
|
|
|
|
|| (id = localParticipant.id)
|
|
|
|
=== LOCAL_PARTICIPANT_DEFAULT_ID) {
|
|
|
|
// The ID of the local participant has been reset already.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// The ID of the local may be reset only if it is not in use.
|
|
|
|
const dispatchLocalParticipantIdChanged
|
|
|
|
= forEachConference(
|
|
|
|
state,
|
|
|
|
conference =>
|
|
|
|
conference === leaving || conference.myUserId() !== id);
|
|
|
|
|
|
|
|
dispatchLocalParticipantIdChanged
|
|
|
|
&& dispatch(
|
|
|
|
localParticipantIdChanged(LOCAL_PARTICIPANT_DEFAULT_ID));
|
|
|
|
});
|
|
|
|
|
2019-03-26 12:11:09 +00:00
|
|
|
/**
|
|
|
|
* Registers listeners for participant change events.
|
|
|
|
*/
|
|
|
|
StateListenerRegistry.register(
|
|
|
|
state => state['features/base/conference'].conference,
|
2019-03-27 15:29:41 +00:00
|
|
|
(conference, store) => {
|
2019-03-26 12:11:09 +00:00
|
|
|
if (conference) {
|
2021-01-05 19:14:19 +00:00
|
|
|
const propertyHandlers = {
|
2021-04-01 09:34:01 +00:00
|
|
|
'e2ee.enabled': (participant, value) => _e2eeUpdated(store, conference, participant.getId(), value),
|
2021-01-05 19:14:19 +00:00
|
|
|
'features_e2ee': (participant, value) =>
|
|
|
|
store.dispatch(participantUpdated({
|
|
|
|
conference,
|
|
|
|
id: participant.getId(),
|
|
|
|
e2eeSupported: value
|
|
|
|
})),
|
|
|
|
'features_jigasi': (participant, value) =>
|
|
|
|
store.dispatch(participantUpdated({
|
|
|
|
conference,
|
|
|
|
id: participant.getId(),
|
|
|
|
isJigasi: value
|
|
|
|
})),
|
|
|
|
'features_screen-sharing': (participant, value) => // eslint-disable-line no-unused-vars
|
|
|
|
store.dispatch(participantUpdated({
|
|
|
|
conference,
|
|
|
|
id: participant.getId(),
|
|
|
|
features: { 'screen-sharing': true }
|
|
|
|
})),
|
2022-06-03 11:45:27 +00:00
|
|
|
'localRecording': (participant, value) =>
|
|
|
|
_localRecordingUpdated(store, conference, participant.getId(), value),
|
2021-10-21 09:40:57 +00:00
|
|
|
'raisedHand': (participant, value) =>
|
|
|
|
_raiseHandUpdated(store, conference, participant.getId(), value),
|
2021-12-22 17:51:26 +00:00
|
|
|
'region': (participant, value) =>
|
|
|
|
store.dispatch(participantUpdated({
|
|
|
|
conference,
|
|
|
|
id: participant.getId(),
|
|
|
|
region: value
|
|
|
|
})),
|
2021-01-05 19:14:19 +00:00
|
|
|
'remoteControlSessionStatus': (participant, value) =>
|
|
|
|
store.dispatch(participantUpdated({
|
|
|
|
conference,
|
|
|
|
id: participant.getId(),
|
|
|
|
remoteControlSessionStatus: value
|
|
|
|
}))
|
|
|
|
};
|
|
|
|
|
|
|
|
// update properties for the participants that are already in the conference
|
|
|
|
conference.getParticipants().forEach(participant => {
|
|
|
|
Object.keys(propertyHandlers).forEach(propertyName => {
|
|
|
|
const value = participant.getProperty(propertyName);
|
|
|
|
|
|
|
|
if (value !== undefined) {
|
|
|
|
propertyHandlers[propertyName](participant, value);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2019-03-26 12:11:09 +00:00
|
|
|
// We joined a conference
|
|
|
|
conference.on(
|
|
|
|
JitsiConferenceEvents.PARTICIPANT_PROPERTY_CHANGED,
|
|
|
|
(participant, propertyName, oldValue, newValue) => {
|
2021-01-05 19:14:19 +00:00
|
|
|
if (propertyHandlers.hasOwnProperty(propertyName)) {
|
|
|
|
propertyHandlers[propertyName](participant, newValue);
|
2019-03-27 15:29:41 +00:00
|
|
|
}
|
2019-03-26 12:11:09 +00:00
|
|
|
});
|
2019-04-01 15:24:57 +00:00
|
|
|
} else {
|
2021-08-24 18:30:51 +00:00
|
|
|
const localParticipantId = getLocalParticipant(store.getState).id;
|
2020-04-23 12:12:30 +00:00
|
|
|
|
2021-08-24 18:30:51 +00:00
|
|
|
// We left the conference, the local participant must be updated.
|
|
|
|
_e2eeUpdated(store, conference, localParticipantId, false);
|
2021-10-21 09:40:57 +00:00
|
|
|
_raiseHandUpdated(store, conference, localParticipantId, 0);
|
2019-03-26 12:11:09 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2020-04-23 12:12:30 +00:00
|
|
|
/**
|
|
|
|
* Handles a E2EE enabled status update.
|
|
|
|
*
|
2021-09-21 11:00:23 +00:00
|
|
|
* @param {Store} store - The redux store.
|
2020-04-23 12:12:30 +00:00
|
|
|
* @param {Object} conference - The conference for which we got an update.
|
|
|
|
* @param {string} participantId - The ID of the participant from which we got an update.
|
2021-04-01 09:34:01 +00:00
|
|
|
* @param {boolean} newValue - The new value of the E2EE enabled status.
|
2020-04-23 12:12:30 +00:00
|
|
|
* @returns {void}
|
|
|
|
*/
|
2021-09-21 11:00:23 +00:00
|
|
|
function _e2eeUpdated({ getState, dispatch }, conference, participantId, newValue) {
|
2020-04-23 12:12:30 +00:00
|
|
|
const e2eeEnabled = newValue === 'true';
|
2021-11-16 11:12:10 +00:00
|
|
|
const { e2ee = {} } = getState()['features/base/config'];
|
2021-04-01 09:34:01 +00:00
|
|
|
|
2020-04-23 12:12:30 +00:00
|
|
|
dispatch(participantUpdated({
|
|
|
|
conference,
|
|
|
|
id: participantId,
|
|
|
|
e2eeEnabled
|
|
|
|
}));
|
2021-11-16 11:12:10 +00:00
|
|
|
|
|
|
|
if (e2ee.externallyManagedKey) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const { maxMode } = getState()['features/e2ee'] || {};
|
|
|
|
|
|
|
|
if (maxMode !== MAX_MODE.THRESHOLD_EXCEEDED || !e2eeEnabled) {
|
|
|
|
dispatch(toggleE2EE(e2eeEnabled));
|
|
|
|
}
|
2020-04-23 12:12:30 +00:00
|
|
|
}
|
|
|
|
|
2018-05-03 13:30:07 +00:00
|
|
|
/**
|
|
|
|
* Initializes the local participant and signals that it joined.
|
|
|
|
*
|
|
|
|
* @private
|
2018-05-22 03:42:22 +00:00
|
|
|
* @param {Store} store - The redux store.
|
2018-05-03 13:30:07 +00:00
|
|
|
* @param {Dispatch} next - The redux dispatch function to dispatch the
|
|
|
|
* specified action to the specified store.
|
|
|
|
* @param {Action} action - The redux action which is being dispatched
|
|
|
|
* in the specified store.
|
|
|
|
* @private
|
|
|
|
* @returns {Object} The value returned by {@code next(action)}.
|
|
|
|
*/
|
|
|
|
function _localParticipantJoined({ getState, dispatch }, next, action) {
|
|
|
|
const result = next(action);
|
2018-05-22 19:30:51 +00:00
|
|
|
|
2018-05-03 13:30:07 +00:00
|
|
|
const settings = getState()['features/base/settings'];
|
2018-05-22 19:30:51 +00:00
|
|
|
|
|
|
|
dispatch(localParticipantJoined({
|
2018-05-03 13:30:07 +00:00
|
|
|
avatarURL: settings.avatarURL,
|
|
|
|
email: settings.email,
|
|
|
|
name: settings.displayName
|
2018-05-22 19:30:51 +00:00
|
|
|
}));
|
2018-05-03 13:30:07 +00:00
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2018-07-10 09:11:45 +00:00
|
|
|
/**
|
|
|
|
* Signals that the local participant has left.
|
|
|
|
*
|
|
|
|
* @param {Store} store - The redux store.
|
|
|
|
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
|
|
|
|
* specified {@code action} into the specified {@code store}.
|
|
|
|
* @param {Action} action - The redux action which is being dispatched in the
|
|
|
|
* specified {@code store}.
|
|
|
|
* @private
|
|
|
|
* @returns {Object} The value returned by {@code next(action)}.
|
|
|
|
*/
|
|
|
|
function _localParticipantLeft({ dispatch }, next, action) {
|
|
|
|
const result = next(action);
|
|
|
|
|
|
|
|
dispatch(localParticipantLeft());
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2018-02-26 19:37:12 +00:00
|
|
|
/**
|
|
|
|
* Plays sounds when participants join/leave conference.
|
|
|
|
*
|
2018-05-22 03:42:22 +00:00
|
|
|
* @param {Store} store - The redux store.
|
|
|
|
* @param {Action} action - The redux action. Should be either
|
2018-02-26 19:37:12 +00:00
|
|
|
* {@link PARTICIPANT_JOINED} or {@link PARTICIPANT_LEFT}.
|
|
|
|
* @private
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
|
|
|
function _maybePlaySounds({ getState, dispatch }, action) {
|
|
|
|
const state = getState();
|
2021-09-09 14:18:26 +00:00
|
|
|
const { startAudioMuted } = state['features/base/config'];
|
2021-07-20 11:56:57 +00:00
|
|
|
const { soundsParticipantJoined: joinSound, soundsParticipantLeft: leftSound } = state['features/base/settings'];
|
2021-03-22 16:28:34 +00:00
|
|
|
|
2018-02-26 19:37:12 +00:00
|
|
|
// We're not playing sounds for local participant
|
|
|
|
// nor when the user is joining past the "startAudioMuted" limit.
|
|
|
|
// The intention there was to not play user joined notification in big
|
|
|
|
// conferences where 100th person is joining.
|
|
|
|
if (!action.participant.local
|
2018-05-22 03:42:22 +00:00
|
|
|
&& (!startAudioMuted
|
|
|
|
|| getParticipantCount(state) < startAudioMuted)) {
|
2021-06-11 08:58:45 +00:00
|
|
|
const { isReplacing, isReplaced } = action.participant;
|
|
|
|
|
2018-02-26 19:37:12 +00:00
|
|
|
if (action.type === PARTICIPANT_JOINED) {
|
2021-07-20 11:56:57 +00:00
|
|
|
if (!joinSound) {
|
|
|
|
return;
|
|
|
|
}
|
2018-05-21 21:40:15 +00:00
|
|
|
const { presence } = action.participant;
|
|
|
|
|
|
|
|
// The sounds for the poltergeist are handled by features/invite.
|
2021-06-11 08:58:45 +00:00
|
|
|
if (presence !== INVITED && presence !== CALLING && !isReplacing) {
|
2018-05-21 21:40:15 +00:00
|
|
|
dispatch(playSound(PARTICIPANT_JOINED_SOUND_ID));
|
|
|
|
}
|
2021-07-20 11:56:57 +00:00
|
|
|
} else if (action.type === PARTICIPANT_LEFT && !isReplaced && leftSound) {
|
2018-02-26 19:37:12 +00:00
|
|
|
dispatch(playSound(PARTICIPANT_LEFT_SOUND_ID));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-05-22 03:42:22 +00:00
|
|
|
/**
|
|
|
|
* Notifies the feature base/participants that the action
|
|
|
|
* {@code PARTICIPANT_JOINED} or {@code PARTICIPANT_UPDATED} is being dispatched
|
|
|
|
* within a specific redux store.
|
|
|
|
*
|
|
|
|
* @param {Store} store - The redux store in which the specified {@code action}
|
|
|
|
* is being dispatched.
|
|
|
|
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
|
|
|
|
* specified {@code action} in the specified {@code store}.
|
|
|
|
* @param {Action} action - The redux action {@code PARTICIPANT_JOINED} or
|
|
|
|
* {@code PARTICIPANT_UPDATED} which is being dispatched in the specified
|
|
|
|
* {@code store}.
|
|
|
|
* @private
|
|
|
|
* @returns {Object} The value returned by {@code next(action)}.
|
|
|
|
*/
|
2020-11-15 21:33:55 +00:00
|
|
|
function _participantJoinedOrUpdated(store, next, action) {
|
|
|
|
const { dispatch, getState } = store;
|
2022-05-20 10:45:09 +00:00
|
|
|
const { overwrittenNameList } = store.getState()['features/base/participants'];
|
2022-06-03 11:45:27 +00:00
|
|
|
const { participant: {
|
|
|
|
avatarURL,
|
|
|
|
email,
|
|
|
|
id,
|
|
|
|
local,
|
|
|
|
localRecording,
|
|
|
|
name,
|
|
|
|
raisedHandTimestamp
|
|
|
|
} } = action;
|
2018-05-22 03:42:22 +00:00
|
|
|
|
|
|
|
// Send an external update of the local participant's raised hand state
|
|
|
|
// if a new raised hand state is defined in the action.
|
2021-10-21 09:40:57 +00:00
|
|
|
if (typeof raisedHandTimestamp !== 'undefined') {
|
2021-09-10 11:05:16 +00:00
|
|
|
|
2018-05-22 03:42:22 +00:00
|
|
|
if (local) {
|
|
|
|
const { conference } = getState()['features/base/conference'];
|
2021-10-21 09:40:57 +00:00
|
|
|
const rHand = parseInt(raisedHandTimestamp, 10);
|
2018-05-22 03:42:22 +00:00
|
|
|
|
2020-12-15 16:00:07 +00:00
|
|
|
// Send raisedHand signalling only if there is a change
|
2021-10-21 09:40:57 +00:00
|
|
|
if (conference && rHand !== getLocalParticipant(getState()).raisedHandTimestamp) {
|
|
|
|
conference.setLocalParticipantProperty('raisedHand', rHand);
|
2020-12-15 16:00:07 +00:00
|
|
|
}
|
2018-05-22 03:42:22 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-20 10:45:09 +00:00
|
|
|
if (overwrittenNameList[id]) {
|
|
|
|
action.participant.name = overwrittenNameList[id];
|
|
|
|
}
|
|
|
|
|
2022-06-03 11:45:27 +00:00
|
|
|
// Send an external update of the local participant's local recording state
|
|
|
|
// if a new local recording state is defined in the action.
|
|
|
|
if (typeof localRecording !== 'undefined') {
|
|
|
|
if (local) {
|
|
|
|
const conference = getCurrentConference(getState);
|
|
|
|
|
|
|
|
// Send localRecording signalling only if there is a change
|
|
|
|
if (conference
|
|
|
|
&& localRecording !== getLocalParticipant(getState()).localRecording) {
|
|
|
|
conference.setLocalParticipantProperty('localRecording', localRecording);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-06-26 14:08:23 +00:00
|
|
|
// Allow the redux update to go through and compare the old avatar
|
|
|
|
// to the new avatar and emit out change events if necessary.
|
|
|
|
const result = next(action);
|
2018-05-22 03:42:22 +00:00
|
|
|
|
2021-06-01 08:28:36 +00:00
|
|
|
// Only run this if the config is populated, otherwise we preload external resources
|
|
|
|
// even if disableThirdPartyRequests is set to true in config
|
|
|
|
if (Object.keys(getState()['features/base/config']).length) {
|
|
|
|
const { disableThirdPartyRequests } = getState()['features/base/config'];
|
|
|
|
|
|
|
|
if (!disableThirdPartyRequests && (avatarURL || email || id || name)) {
|
|
|
|
const participantId = !id && local ? getLocalParticipant(getState()).id : id;
|
|
|
|
const updatedParticipant = getParticipantById(getState(), participantId);
|
|
|
|
|
|
|
|
getFirstLoadableAvatarUrl(updatedParticipant, store)
|
2021-12-17 00:16:24 +00:00
|
|
|
.then(urlData => {
|
|
|
|
dispatch(setLoadableAvatarUrl(participantId, urlData?.src, urlData?.isUsingCORS));
|
2021-06-01 08:28:36 +00:00
|
|
|
});
|
|
|
|
}
|
2019-06-26 14:08:23 +00:00
|
|
|
}
|
2018-05-22 03:42:22 +00:00
|
|
|
|
2019-06-26 14:08:23 +00:00
|
|
|
// Notify external listeners of potential avatarURL changes.
|
|
|
|
if (typeof APP === 'object') {
|
|
|
|
const currentKnownId = local ? APP.conference.getMyUserId() : id;
|
2018-05-22 03:42:22 +00:00
|
|
|
|
2019-06-26 14:08:23 +00:00
|
|
|
// Force update of local video getting a new id.
|
|
|
|
APP.UI.refreshAvatarDisplay(currentKnownId);
|
2018-05-22 03:42:22 +00:00
|
|
|
}
|
|
|
|
|
2019-06-26 14:08:23 +00:00
|
|
|
return result;
|
2018-05-22 03:42:22 +00:00
|
|
|
}
|
|
|
|
|
2022-06-03 11:45:27 +00:00
|
|
|
/**
|
|
|
|
* Handles a local recording status update.
|
|
|
|
*
|
|
|
|
* @param {Function} dispatch - The Redux dispatch function.
|
|
|
|
* @param {Object} conference - The conference for which we got an update.
|
|
|
|
* @param {string} participantId - The ID of the participant from which we got an update.
|
|
|
|
* @param {boolean} newValue - The new value of the local recording status.
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
|
|
|
function _localRecordingUpdated({ dispatch, getState }, conference, participantId, newValue) {
|
|
|
|
const state = getState();
|
|
|
|
|
|
|
|
dispatch(participantUpdated({
|
|
|
|
conference,
|
|
|
|
id: participantId,
|
|
|
|
localRecording: newValue
|
|
|
|
}));
|
|
|
|
const participantName = getParticipantDisplayName(state, participantId);
|
|
|
|
|
|
|
|
dispatch(showNotification({
|
|
|
|
titleKey: 'notify.somebody',
|
|
|
|
title: participantName,
|
|
|
|
descriptionKey: newValue ? 'notify.localRecordingStarted' : 'notify.localRecordingStopped',
|
|
|
|
uid: LOCAL_RECORDING_NOTIFICATION_ID
|
|
|
|
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
|
|
|
|
dispatch(playSound(newValue ? RECORDING_ON_SOUND_ID : RECORDING_OFF_SOUND_ID));
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2019-03-27 15:29:41 +00:00
|
|
|
/**
|
|
|
|
* Handles a raise hand status update.
|
|
|
|
*
|
|
|
|
* @param {Function} dispatch - The Redux dispatch function.
|
|
|
|
* @param {Object} conference - The conference for which we got an update.
|
2020-04-23 12:12:30 +00:00
|
|
|
* @param {string} participantId - The ID of the participant from which we got an update.
|
2019-04-01 15:24:57 +00:00
|
|
|
* @param {boolean} newValue - The new value of the raise hand status.
|
2019-03-27 15:29:41 +00:00
|
|
|
* @returns {void}
|
|
|
|
*/
|
2019-04-01 15:24:57 +00:00
|
|
|
function _raiseHandUpdated({ dispatch, getState }, conference, participantId, newValue) {
|
2021-10-21 09:40:57 +00:00
|
|
|
let raisedHandTimestamp;
|
|
|
|
|
|
|
|
switch (newValue) {
|
|
|
|
case undefined:
|
|
|
|
case 'false':
|
|
|
|
raisedHandTimestamp = 0;
|
|
|
|
break;
|
|
|
|
case 'true':
|
|
|
|
raisedHandTimestamp = Date.now();
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
raisedHandTimestamp = parseInt(newValue, 10);
|
|
|
|
}
|
2021-09-10 11:05:16 +00:00
|
|
|
const state = getState();
|
2019-03-27 15:29:41 +00:00
|
|
|
|
|
|
|
dispatch(participantUpdated({
|
|
|
|
conference,
|
2020-04-23 12:12:30 +00:00
|
|
|
id: participantId,
|
2021-10-21 09:40:57 +00:00
|
|
|
raisedHandTimestamp
|
2019-03-27 15:29:41 +00:00
|
|
|
}));
|
|
|
|
|
2021-09-10 11:05:16 +00:00
|
|
|
dispatch(raiseHandUpdateQueue({
|
|
|
|
id: participantId,
|
2021-10-21 09:40:57 +00:00
|
|
|
raisedHandTimestamp
|
2021-09-10 11:05:16 +00:00
|
|
|
}));
|
|
|
|
|
2021-01-06 14:49:10 +00:00
|
|
|
if (typeof APP !== 'undefined') {
|
2021-10-21 09:40:57 +00:00
|
|
|
APP.API.notifyRaiseHandUpdated(participantId, raisedHandTimestamp);
|
2021-01-06 14:49:10 +00:00
|
|
|
}
|
|
|
|
|
2021-09-10 11:05:16 +00:00
|
|
|
const isModerator = isLocalParticipantModerator(state);
|
|
|
|
const participant = getParticipantById(state, participantId);
|
|
|
|
let shouldDisplayAllowAction = false;
|
|
|
|
|
|
|
|
if (isModerator) {
|
|
|
|
shouldDisplayAllowAction = isForceMuted(participant, MEDIA_TYPE.AUDIO, state)
|
|
|
|
|| isForceMuted(participant, MEDIA_TYPE.VIDEO, state);
|
|
|
|
}
|
|
|
|
|
|
|
|
const action = shouldDisplayAllowAction ? {
|
2021-11-10 11:19:40 +00:00
|
|
|
customActionNameKey: [ 'notify.allowAction' ],
|
|
|
|
customActionHandler: [ () => dispatch(approveParticipant(participantId)) ]
|
2021-09-10 11:05:16 +00:00
|
|
|
} : {};
|
|
|
|
|
2021-10-21 09:40:57 +00:00
|
|
|
if (raisedHandTimestamp) {
|
2021-12-02 13:17:07 +00:00
|
|
|
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;
|
|
|
|
}
|
2019-03-27 15:29:41 +00:00
|
|
|
dispatch(showNotification({
|
2021-09-10 11:05:16 +00:00
|
|
|
titleKey: 'notify.somebody',
|
2021-12-02 13:17:07 +00:00
|
|
|
title: notificationTitle,
|
2021-09-10 11:05:16 +00:00
|
|
|
descriptionKey: 'notify.raisedHand',
|
2021-09-22 14:05:42 +00:00
|
|
|
concatText: true,
|
2021-12-02 13:17:07 +00:00
|
|
|
uid: RAISE_HAND_NOTIFICATION_ID,
|
2021-09-10 11:05:16 +00:00
|
|
|
...action
|
2021-11-24 11:05:27 +00:00
|
|
|
}, shouldDisplayAllowAction ? NOTIFICATION_TIMEOUT_TYPE.MEDIUM : NOTIFICATION_TIMEOUT_TYPE.SHORT));
|
2021-08-31 11:00:27 +00:00
|
|
|
dispatch(playSound(RAISE_HAND_SOUND_ID));
|
2019-03-27 15:29:41 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-02-26 19:37:12 +00:00
|
|
|
/**
|
|
|
|
* Registers sounds related with the participants feature.
|
|
|
|
*
|
2018-05-22 03:42:22 +00:00
|
|
|
* @param {Store} store - The redux store.
|
2018-02-26 19:37:12 +00:00
|
|
|
* @private
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
|
|
|
function _registerSounds({ dispatch }) {
|
|
|
|
dispatch(
|
2018-04-05 19:12:24 +00:00
|
|
|
registerSound(PARTICIPANT_JOINED_SOUND_ID, PARTICIPANT_JOINED_FILE));
|
2018-05-22 03:42:22 +00:00
|
|
|
dispatch(registerSound(PARTICIPANT_LEFT_SOUND_ID, PARTICIPANT_LEFT_FILE));
|
2018-02-26 19:37:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Unregisters sounds related with the participants feature.
|
|
|
|
*
|
2018-05-22 03:42:22 +00:00
|
|
|
* @param {Store} store - The redux store.
|
2018-02-26 19:37:12 +00:00
|
|
|
* @private
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
|
|
|
function _unregisterSounds({ dispatch }) {
|
2018-05-22 03:42:22 +00:00
|
|
|
dispatch(unregisterSound(PARTICIPANT_JOINED_SOUND_ID));
|
|
|
|
dispatch(unregisterSound(PARTICIPANT_LEFT_SOUND_ID));
|
2018-02-26 19:37:12 +00:00
|
|
|
}
|