jiti-meet/react/features/base/participants/middleware.ts

841 lines
29 KiB
TypeScript
Raw Normal View History

import i18n from 'i18next';
feat: Participants optimisations (#9515) * fix(participants): Change from array to Map * fix(unload): optimise * feat: Introduces new states for e2ee feature. Stores everyoneSupportsE2EE and everyoneEnabledE2EE to minimize looping through participants list. squash: Uses participants map and go over the elements only once. * feat: Optimizes isEveryoneModerator to do less frequent checks in all participants. * fix: Drops deep equal from participants pane and uses the map. * fix(SharedVideo): isVideoPlaying * fix(participants): Optimise isEveryoneModerator * fix(e2e): Optimise everyoneEnabledE2EE * fix: JS errors. * ref(participants): remove getParticipants * fix(participants): Prepare for PR. * fix: Changes participants pane to be component. The functional component was always rendered: `prev props: {} !== {} :next props`. * feat: Optimization to skip participants list on pane closed. * fix: The participants list shows and the local participant. * fix: Fix wrong action name for av-moderation. * fix: Minimizes the number of render calls of av moderation notification. * fix: Fix iterating over remote participants. * fix: Fixes lint error. * fix: Reflects participant updates for av-moderation. * fix(ParticipantPane): to work with IDs. * fix(av-moderation): on PARTCIPANT_UPDATE * fix(ParticipantPane): close delay. * fix: address code review comments * fix(API): mute-everyone * fix: bugs * fix(Thumbnail): on mobile. * fix(ParticipantPane): Close context menu on click. * fix: Handles few error when local participant is undefined. * feat: Hides AV moderation if not supported. * fix: Show mute all video. * fix: Fixes updating participant for av moderation. Co-authored-by: damencho <damencho@jitsi.org>
2021-07-09 12:36:19 +00:00
import { batch } from 'react-redux';
// @ts-ignore
import UIEvents from '../../../../service/UI/UIEvents';
import { IStore } from '../../app/types';
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';
import { toggleE2EE } from '../../e2ee/actions';
import { MAX_MODE } from '../../e2ee/constants';
import { showNotification } from '../../notifications/actions';
import {
LOCAL_RECORDING_NOTIFICATION_ID,
NOTIFICATION_TIMEOUT_TYPE,
RAISE_HAND_NOTIFICATION_ID
} from '../../notifications/constants';
import { isForceMuted } from '../../participants-pane/functions';
import { CALLING, INVITED } from '../../presence-status/constants';
import { RAISE_HAND_SOUND_ID } from '../../reactions/constants';
import { RECORDING_OFF_SOUND_ID, RECORDING_ON_SOUND_ID } from '../../recording/constants';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app/actionTypes';
import { CONFERENCE_WILL_JOIN } from '../conference/actionTypes';
import { forEachConference, getCurrentConference } from '../conference/functions';
import { IJitsiConference } from '../conference/reducer';
import { SET_CONFIG } from '../config/actionTypes';
import { getDisableRemoveRaisedHandOnFocus } from '../config/functions.any';
import { JitsiConferenceEvents } from '../lib-jitsi-meet';
import { MEDIA_TYPE } from '../media/constants';
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
import StateListenerRegistry from '../redux/StateListenerRegistry';
import { playSound, registerSound, unregisterSound } from '../sounds/actions';
import {
DOMINANT_SPEAKER_CHANGED,
GRANT_MODERATOR,
KICK_PARTICIPANT,
LOCAL_PARTICIPANT_AUDIO_LEVEL_CHANGED,
LOCAL_PARTICIPANT_RAISE_HAND,
MUTE_REMOTE_PARTICIPANT,
2022-05-20 10:45:09 +00:00
OVERWRITE_PARTICIPANTS_NAMES,
OVERWRITE_PARTICIPANT_NAME,
PARTICIPANT_DISPLAY_NAME_CHANGED,
PARTICIPANT_JOINED,
PARTICIPANT_LEFT,
PARTICIPANT_UPDATED,
RAISE_HAND_UPDATED,
SET_LOCAL_PARTICIPANT_RECORDING_STATUS
} 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,
raiseHand,
raiseHandUpdateQueue,
2020-05-20 10:57:03 +00:00
setLoadableAvatarUrl
} from './actions';
import {
LOCAL_PARTICIPANT_DEFAULT_ID,
LOWER_HAND_AUDIO_LEVEL,
PARTICIPANT_JOINED_SOUND_ID,
PARTICIPANT_LEFT_SOUND_ID
} from './constants';
import {
getDominantSpeakerParticipant,
2019-06-26 14:08:23 +00:00
getFirstLoadableAvatarUrl,
getLocalParticipant,
2019-06-26 14:08:23 +00:00
getParticipantById,
getParticipantCount,
feat: Participants optimisations (#9515) * fix(participants): Change from array to Map * fix(unload): optimise * feat: Introduces new states for e2ee feature. Stores everyoneSupportsE2EE and everyoneEnabledE2EE to minimize looping through participants list. squash: Uses participants map and go over the elements only once. * feat: Optimizes isEveryoneModerator to do less frequent checks in all participants. * fix: Drops deep equal from participants pane and uses the map. * fix(SharedVideo): isVideoPlaying * fix(participants): Optimise isEveryoneModerator * fix(e2e): Optimise everyoneEnabledE2EE * fix: JS errors. * ref(participants): remove getParticipants * fix(participants): Prepare for PR. * fix: Changes participants pane to be component. The functional component was always rendered: `prev props: {} !== {} :next props`. * feat: Optimization to skip participants list on pane closed. * fix: The participants list shows and the local participant. * fix: Fix wrong action name for av-moderation. * fix: Minimizes the number of render calls of av moderation notification. * fix: Fix iterating over remote participants. * fix: Fixes lint error. * fix: Reflects participant updates for av-moderation. * fix(ParticipantPane): to work with IDs. * fix(av-moderation): on PARTCIPANT_UPDATE * fix(ParticipantPane): close delay. * fix: address code review comments * fix(API): mute-everyone * fix: bugs * fix(Thumbnail): on mobile. * fix(ParticipantPane): Close context menu on click. * fix: Handles few error when local participant is undefined. * feat: Hides AV moderation if not supported. * fix: Show mute all video. * fix: Fixes updating participant for av moderation. Co-authored-by: damencho <damencho@jitsi.org>
2021-07-09 12:36:19 +00:00
getParticipantDisplayName,
getRaiseHandsQueue,
getRemoteParticipants,
hasRaisedHand,
isLocalParticipantModerator,
isScreenShareParticipant,
isWhiteboardParticipant
} from './functions';
2022-05-20 10:45:09 +00:00
import logger from './logger';
import { PARTICIPANT_JOINED_FILE, PARTICIPANT_LEFT_FILE } from './sounds';
import { FakeParticipant, IJitsiParticipant } from './types';
import './subscriber';
/**
* 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.
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case APP_WILL_MOUNT:
_registerSounds(store);
return _localParticipantJoined(store, next, action);
case APP_WILL_UNMOUNT:
_unregisterSounds(store);
return _localParticipantLeft(store, next, action);
case CONFERENCE_WILL_JOIN:
2016-12-12 00:29:13 +00:00
store.dispatch(localParticipantIdChanged(action.conference.myUserId()));
break;
case DOMINANT_SPEAKER_CHANGED: {
// Lower hand through xmpp when local participant becomes dominant speaker.
const { id } = action.participant;
const state = store.getState();
const participant = getLocalParticipant(state);
2022-09-08 21:14:00 +00:00
const dominantSpeaker = getDominantSpeakerParticipant(state);
const isLocal = participant && participant.id === id;
2022-09-08 21:14:00 +00:00
if (isLocal && dominantSpeaker?.id !== id
&& hasRaisedHand(participant)
&& !getDisableRemoveRaisedHandOnFocus(state)) {
store.dispatch(raiseHand(false));
}
break;
}
case LOCAL_PARTICIPANT_AUDIO_LEVEL_CHANGED: {
const state = store.getState();
const participant = getDominantSpeakerParticipant(state);
if (
participant?.local
&& hasRaisedHand(participant)
&& action.level > LOWER_HAND_AUDIO_LEVEL
&& !getDisableRemoveRaisedHandOnFocus(state)
) {
store.dispatch(raiseHand(false));
}
break;
}
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'];
conference?.kickParticipant(action.id);
break;
2018-05-22 03:42:22 +00:00
}
case LOCAL_PARTICIPANT_RAISE_HAND: {
const { raisedHandTimestamp } = action;
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,
raisedHandTimestamp
}));
store.dispatch(raiseHandUpdateQueue({
id: localId ?? '',
raisedHandTimestamp
}));
if (typeof APP !== 'undefined') {
APP.API.notifyRaiseHandUpdated(localId, raisedHandTimestamp);
}
break;
}
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?.userRegion) {
const localId = getLocalParticipant(state)?.id;
if (localId) {
store.dispatch(participantUpdated({
id: localId,
local: true,
region: deploymentInfo.userRegion
}));
}
}
return result;
}
case SET_LOCAL_PARTICIPANT_RECORDING_STATUS: {
const state = store.getState();
const { recording, onlySelf } = action;
const localId = getLocalParticipant(state)?.id;
const { localRecording } = state['features/base/config'];
if (localRecording?.notifyAllParticipants && !onlySelf && localId) {
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
}));
}
break;
}
2018-05-22 03:42:22 +00:00
case MUTE_REMOTE_PARTICIPANT: {
const { conference } = store.getState()['features/base/conference'];
conference?.muteParticipant(action.id, action.mediaType);
break;
2018-05-22 03:42:22 +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;
}
case RAISE_HAND_UPDATED: {
const { participant } = action;
let queue = getRaiseHandsQueue(store.getState());
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 {
// no need to sort on remove value.
queue = queue.filter(({ id }) => id !== participant.id);
}
action.queue = queue;
break;
}
case PARTICIPANT_JOINED: {
// Do not play sounds when a screenshare or whiteboard participant tile is created for screenshare.
(!isScreenShareParticipant(action.participant)
&& !isWhiteboardParticipant(action.participant)
) && _maybePlaySounds(store, action);
return _participantJoinedOrUpdated(store, next, action);
}
case PARTICIPANT_LEFT: {
// Do not play sounds when a tile for screenshare or whiteboard is removed.
(!isScreenShareParticipant(action.participant)
&& !isWhiteboardParticipant(action.participant)
) && _maybePlaySounds(store, action);
break;
}
2018-05-22 03:42:22 +00:00
case PARTICIPANT_UPDATED:
return _participantJoinedOrUpdated(store, next, action);
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: any = {};
2022-05-20 10:45:09 +00:00
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;
}
}
return next(action);
});
/**
* 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(
/* selector */ state => getCurrentConference(state),
/* listener */ (conference, { dispatch, getState }) => {
feat: Participants optimisations (#9515) * fix(participants): Change from array to Map * fix(unload): optimise * feat: Introduces new states for e2ee feature. Stores everyoneSupportsE2EE and everyoneEnabledE2EE to minimize looping through participants list. squash: Uses participants map and go over the elements only once. * feat: Optimizes isEveryoneModerator to do less frequent checks in all participants. * fix: Drops deep equal from participants pane and uses the map. * fix(SharedVideo): isVideoPlaying * fix(participants): Optimise isEveryoneModerator * fix(e2e): Optimise everyoneEnabledE2EE * fix: JS errors. * ref(participants): remove getParticipants * fix(participants): Prepare for PR. * fix: Changes participants pane to be component. The functional component was always rendered: `prev props: {} !== {} :next props`. * feat: Optimization to skip participants list on pane closed. * fix: The participants list shows and the local participant. * fix: Fix wrong action name for av-moderation. * fix: Minimizes the number of render calls of av moderation notification. * fix: Fix iterating over remote participants. * fix: Fixes lint error. * fix: Reflects participant updates for av-moderation. * fix(ParticipantPane): to work with IDs. * fix(av-moderation): on PARTCIPANT_UPDATE * fix(ParticipantPane): close delay. * fix: address code review comments * fix(API): mute-everyone * fix: bugs * fix(Thumbnail): on mobile. * fix(ParticipantPane): Close context menu on click. * fix: Handles few error when local participant is undefined. * feat: Hides AV moderation if not supported. * fix: Show mute all video. * fix: Fixes updating participant for av moderation. Co-authored-by: damencho <damencho@jitsi.org>
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, {
isReplaced: p.isReplaced
}));
feat: Participants optimisations (#9515) * fix(participants): Change from array to Map * fix(unload): optimise * feat: Introduces new states for e2ee feature. Stores everyoneSupportsE2EE and everyoneEnabledE2EE to minimize looping through participants list. squash: Uses participants map and go over the elements only once. * feat: Optimizes isEveryoneModerator to do less frequent checks in all participants. * fix: Drops deep equal from participants pane and uses the map. * fix(SharedVideo): isVideoPlaying * fix(participants): Optimise isEveryoneModerator * fix(e2e): Optimise everyoneEnabledE2EE * fix: JS errors. * ref(participants): remove getParticipants * fix(participants): Prepare for PR. * fix: Changes participants pane to be component. The functional component was always rendered: `prev props: {} !== {} :next props`. * feat: Optimization to skip participants list on pane closed. * fix: The participants list shows and the local participant. * fix: Fix wrong action name for av-moderation. * fix: Minimizes the number of render calls of av moderation notification. * fix: Fix iterating over remote participants. * fix: Fixes lint error. * fix: Reflects participant updates for av-moderation. * fix(ParticipantPane): to work with IDs. * fix(av-moderation): on PARTCIPANT_UPDATE * fix(ParticipantPane): close delay. * fix: address code review comments * fix(API): mute-everyone * fix: bugs * fix(Thumbnail): on mobile. * fix(ParticipantPane): Close context menu on click. * fix: Handles few error when local participant is undefined. * feat: Hides AV moderation if not supported. * fix: Show mute all video. * fix: Fixes updating participant for av moderation. Co-authored-by: damencho <damencho@jitsi.org>
2021-07-09 12:36:19 +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: string;
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
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));
});
/**
* Registers listeners for participant change events.
*/
StateListenerRegistry.register(
state => state['features/base/conference'].conference,
(conference, store) => {
if (conference) {
const propertyHandlers: {
[key: string]: Function;
} = {
'e2ee.enabled': (participant: IJitsiParticipant, value: string) =>
_e2eeUpdated(store, conference, participant.getId(), value),
'features_e2ee': (participant: IJitsiParticipant, value: boolean) =>
store.dispatch(participantUpdated({
conference,
id: participant.getId(),
e2eeSupported: value
})),
'features_jigasi': (participant: IJitsiParticipant, value: boolean) =>
store.dispatch(participantUpdated({
conference,
id: participant.getId(),
fakeParticipant: value ? FakeParticipant.Jigasi : undefined
})),
// eslint-disable-next-line @typescript-eslint/no-unused-vars
'features_screen-sharing': (participant: IJitsiParticipant, value: string) =>
store.dispatch(participantUpdated({
conference,
id: participant.getId(),
features: { 'screen-sharing': true }
})),
'localRecording': (participant: IJitsiParticipant, value: string) =>
_localRecordingUpdated(store, conference, participant.getId(), value),
'raisedHand': (participant: IJitsiParticipant, value: string) =>
_raiseHandUpdated(store, conference, participant.getId(), value),
'region': (participant: IJitsiParticipant, value: string) =>
2021-12-22 17:51:26 +00:00
store.dispatch(participantUpdated({
conference,
id: participant.getId(),
region: value
})),
'remoteControlSessionStatus': (participant: IJitsiParticipant, value: boolean) =>
store.dispatch(participantUpdated({
conference,
id: participant.getId(),
remoteControlSessionStatus: value
}))
};
// update properties for the participants that are already in the conference
conference.getParticipants().forEach((participant: any) => {
Object.keys(propertyHandlers).forEach(propertyName => {
const value = participant.getProperty(propertyName);
if (value !== undefined) {
propertyHandlers[propertyName as keyof typeof propertyHandlers](participant, value);
}
});
});
// We joined a conference
conference.on(
JitsiConferenceEvents.PARTICIPANT_PROPERTY_CHANGED,
(participant: IJitsiParticipant, propertyName: string, oldValue: string, newValue: string) => {
if (propertyHandlers.hasOwnProperty(propertyName)) {
propertyHandlers[propertyName](participant, newValue);
}
});
} else {
const localParticipantId = getLocalParticipant(store.getState)?.id;
// We left the conference, the local participant must be updated.
_e2eeUpdated(store, conference, localParticipantId ?? '', false);
_raiseHandUpdated(store, conference, localParticipantId ?? '', 0);
}
}
);
/**
* Handles a E2EE enabled status update.
*
* @param {Store} store - The redux store.
* @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 E2EE enabled status.
* @returns {void}
*/
function _e2eeUpdated({ getState, dispatch }: IStore, conference: IJitsiConference,
participantId: string, newValue: string | boolean) {
const e2eeEnabled = newValue === 'true';
const { e2ee = {} } = getState()['features/base/config'];
dispatch(participantUpdated({
conference,
id: participantId,
e2eeEnabled
}));
if (e2ee.externallyManagedKey) {
return;
}
const { maxMode } = getState()['features/e2ee'] || {};
if (maxMode !== MAX_MODE.THRESHOLD_EXCEEDED || !e2eeEnabled) {
dispatch(toggleE2EE(e2eeEnabled));
}
}
/**
* Initializes the local participant and signals that it joined.
*
* @private
2018-05-22 03:42:22 +00:00
* @param {Store} store - The redux store.
* @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 }: IStore, next: Function, action: any) {
const result = next(action);
const settings = getState()['features/base/settings'];
// @ts-ignore
dispatch(localParticipantJoined({
avatarURL: settings.avatarURL,
email: settings.email,
name: settings.displayName
}));
return result;
}
/**
* 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 }: IStore, next: Function, action: any) {
const result = next(action);
dispatch(localParticipantLeft());
return result;
}
/**
* 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
* {@link PARTICIPANT_JOINED} or {@link PARTICIPANT_LEFT}.
* @private
* @returns {void}
*/
function _maybePlaySounds({ getState, dispatch }: IStore, action: any) {
const state = getState();
const { startAudioMuted } = state['features/base/config'];
const { soundsParticipantJoined: joinSound, soundsParticipantLeft: leftSound } = state['features/base/settings'];
// 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)) {
const { isReplacing, isReplaced } = action.participant;
if (action.type === PARTICIPANT_JOINED) {
if (!joinSound) {
return;
}
const { presence } = action.participant;
// The sounds for the poltergeist are handled by features/invite.
if (presence !== INVITED && presence !== CALLING && !isReplacing) {
dispatch(playSound(PARTICIPANT_JOINED_SOUND_ID));
}
} else if (action.type === PARTICIPANT_LEFT && !isReplaced && leftSound) {
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)}.
*/
function _participantJoinedOrUpdated(store: IStore, next: Function, action: any) {
const { dispatch, getState } = store;
2022-05-20 10:45:09 +00:00
const { overwrittenNameList } = store.getState()['features/base/participants'];
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.
if (typeof raisedHandTimestamp !== 'undefined') {
2018-05-22 03:42:22 +00:00
if (local) {
const { conference } = getState()['features/base/conference'];
const rHand = parseInt(raisedHandTimestamp, 10);
2018-05-22 03:42:22 +00:00
// Send raisedHand signalling only if there is a change
if (conference && rHand !== getLocalParticipant(getState())?.raisedHandTimestamp) {
conference.setLocalParticipantProperty('raisedHand', rHand);
}
2018-05-22 03:42:22 +00:00
}
}
2022-05-20 10:45:09 +00:00
if (overwrittenNameList[id]) {
action.participant.name = overwrittenNameList[id];
}
// 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
// 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 ?? { id: '' }, store)
.then((urlData?: { isUsingCORS: boolean; src: string; }) => {
dispatch(setLoadableAvatarUrl(participantId, urlData?.src ?? '', Boolean(urlData?.isUsingCORS)));
});
}
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
}
/**
* 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 }: IStore, conference: IJitsiConference,
participantId: string, newValue: string) {
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));
}
/**
* Handles a raise hand 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 raise hand status.
* @returns {void}
*/
function _raiseHandUpdated({ dispatch, getState }: IStore, conference: IJitsiConference,
participantId: string, newValue: string | number) {
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,
raisedHandTimestamp
}));
dispatch(raiseHandUpdateQueue({
id: participantId,
raisedHandTimestamp
}));
if (typeof APP !== 'undefined') {
APP.API.notifyRaiseHandUpdated(participantId, raisedHandTimestamp);
}
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 ? {
customActionNameKey: [ 'notify.allowAction' ],
customActionHandler: [ () => dispatch(approveParticipant(participantId)) ]
} : {};
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: notificationTitle,
descriptionKey: 'notify.raisedHand',
concatText: true,
uid: RAISE_HAND_NOTIFICATION_ID,
...action
}, shouldDisplayAllowAction ? NOTIFICATION_TIMEOUT_TYPE.MEDIUM : NOTIFICATION_TIMEOUT_TYPE.SHORT));
dispatch(playSound(RAISE_HAND_SOUND_ID));
}
}
/**
* Registers sounds related with the participants feature.
*
2018-05-22 03:42:22 +00:00
* @param {Store} store - The redux store.
* @private
* @returns {void}
*/
function _registerSounds({ dispatch }: IStore) {
dispatch(
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));
}
/**
* Unregisters sounds related with the participants feature.
*
2018-05-22 03:42:22 +00:00
* @param {Store} store - The redux store.
* @private
* @returns {void}
*/
function _unregisterSounds({ dispatch }: IStore) {
2018-05-22 03:42:22 +00:00
dispatch(unregisterSound(PARTICIPANT_JOINED_SOUND_ID));
dispatch(unregisterSound(PARTICIPANT_LEFT_SOUND_ID));
}