diff --git a/modules/API/API.js b/modules/API/API.js
index e29d3d69f..4c4fcbb01 100644
--- a/modules/API/API.js
+++ b/modules/API/API.js
@@ -23,7 +23,8 @@ import {
getParticipantById,
pinParticipant,
kickParticipant,
- raiseHand
+ raiseHand,
+ isParticipantModerator
} from '../../react/features/base/participants';
import { updateSettings } from '../../react/features/base/settings';
import { isToggleCameraEnabled, toggleCamera } from '../../react/features/base/tracks';
@@ -105,13 +106,14 @@ function initCommands() {
const muteMediaType = mediaType ? mediaType : MEDIA_TYPE.AUDIO;
sendAnalytics(createApiEvent('muted-everyone'));
- const participants = APP.store.getState()['features/base/participants'];
- const localIds = participants
- .filter(participant => participant.local)
- .filter(participant => participant.role === 'moderator')
- .map(participant => participant.id);
+ const localParticipant = getLocalParticipant(APP.store.getState());
+ const exclude = [];
- APP.store.dispatch(muteAllParticipants(localIds, muteMediaType));
+ if (localParticipant && isParticipantModerator(localParticipant)) {
+ exclude.push(localParticipant.id);
+ }
+
+ APP.store.dispatch(muteAllParticipants(exclude, muteMediaType));
},
'toggle-lobby': isLobbyEnabled => {
APP.store.dispatch(toggleLobbyMode(isLobbyEnabled));
diff --git a/react/features/av-moderation/actions.js b/react/features/av-moderation/actions.js
index 917c30388..2c9b6e6f9 100644
--- a/react/features/av-moderation/actions.js
+++ b/react/features/av-moderation/actions.js
@@ -49,24 +49,24 @@ export const disableModeration = (mediaType: MediaType, actor: Object) => {
/**
* Hides the notification with the participant that asked to unmute audio.
*
- * @param {string} id - The participant id.
+ * @param {Object} participant - The participant for which the notification to be hidden.
* @returns {Object}
*/
-export function dismissPendingAudioParticipant(id: string) {
- return dismissPendingParticipant(id, MEDIA_TYPE.AUDIO);
+export function dismissPendingAudioParticipant(participant: Object) {
+ return dismissPendingParticipant(participant, MEDIA_TYPE.AUDIO);
}
/**
* Hides the notification with the participant that asked to unmute.
*
- * @param {string} id - The participant id.
+ * @param {Object} participant - The participant for which the notification to be hidden.
* @param {MediaType} mediaType - The media type.
* @returns {Object}
*/
-export function dismissPendingParticipant(id: string, mediaType: MediaType) {
+export function dismissPendingParticipant(participant: Object, mediaType: MediaType) {
return {
type: DISMISS_PENDING_PARTICIPANT,
- id,
+ participant,
mediaType
};
}
@@ -145,13 +145,13 @@ export function showModeratedNotification(mediaType: MediaType) {
/**
* Shows a notification with the participant that asked to audio unmute.
*
- * @param {string} id - The participant id.
+ * @param {Object} participant - The participant for which is the notification.
* @returns {Object}
*/
-export function participantPendingAudio(id: string) {
+export function participantPendingAudio(participant: Object) {
return {
type: PARTICIPANT_PENDING_AUDIO,
- id
+ participant
};
}
diff --git a/react/features/av-moderation/components/AudioModerationNotifications.js b/react/features/av-moderation/components/AudioModerationNotifications.js
index a282f8bd1..cd15831fd 100644
--- a/react/features/av-moderation/components/AudioModerationNotifications.js
+++ b/react/features/av-moderation/components/AudioModerationNotifications.js
@@ -3,7 +3,10 @@ import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import NotificationWithParticipants from '../../notifications/components/web/NotificationWithParticipants';
-import { approveAudio, dismissPendingAudioParticipant } from '../actions';
+import {
+ approveParticipant,
+ dismissPendingAudioParticipant
+} from '../actions';
import { getParticipantsAskingToAudioUnmute } from '../functions';
@@ -25,7 +28,7 @@ export default function() {
state['features/av-moderation'];
+/**
+ * We use to construct once the empty array so we can keep the same instance between calls
+ * of getParticipantsAskingToAudioUnmute.
+ *
+ * @type {*[]}
+ */
+const EMPTY_ARRAY = [];
+
/**
* Returns whether moderation is enabled per media type.
*
@@ -33,6 +41,17 @@ export const isEnabledFromState = (mediaType: MediaType, state: Object) =>
*/
export const isEnabled = (mediaType: MediaType) => (state: Object) => isEnabledFromState(mediaType, state);
+/**
+ * Returns whether moderation is supported by the backend.
+ *
+ * @returns {null|boolean}
+ */
+export const isSupported = () => (state: Object) => {
+ const { conference } = state['features/base/conference'];
+
+ return conference ? conference.isAVModerationSupported() : false;
+};
+
/**
* Returns whether local participant is approved to unmute a media type.
*
@@ -74,15 +93,15 @@ export const isParticipantApproved = (id: string, mediaType: MediaType) => (stat
/**
* Returns a selector creator which determines if the participant is pending or not for a media type.
*
- * @param {string} id - The participant id.
+ * @param {Participant} participant - The participant.
* @param {MEDIA_TYPE} mediaType - The media type to check.
* @returns {boolean}
*/
-export const isParticipantPending = (id: string, mediaType: MediaType) => (state: Object) => {
+export const isParticipantPending = (participant: Object, mediaType: MediaType) => (state: Object) => {
const storeKey = MEDIA_TYPE_TO_PENDING_STORE_KEY[mediaType];
const arr = getState(state)[storeKey];
- return Boolean(arr.find(pending => pending === id));
+ return Boolean(arr.find(pending => pending.id === participant.id));
};
/**
@@ -94,12 +113,10 @@ export const isParticipantPending = (id: string, mediaType: MediaType) => (state
*/
export const getParticipantsAskingToAudioUnmute = (state: Object) => {
if (isLocalParticipantModerator(state)) {
- const ids = getState(state).pendingAudio;
-
- return ids.map(id => getParticipantById(state, id)).filter(Boolean);
+ return getState(state).pendingAudio;
}
- return [];
+ return EMPTY_ARRAY;
};
/**
diff --git a/react/features/av-moderation/middleware.js b/react/features/av-moderation/middleware.js
index 863491691..748494c4d 100644
--- a/react/features/av-moderation/middleware.js
+++ b/react/features/av-moderation/middleware.js
@@ -127,14 +127,16 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
// this is handled only by moderators
if (audioModerationEnabled && isLocalParticipantModerator(state)) {
- const { participant: { id, raisedHand } } = action;
+ const participant = action.participant;
- if (raisedHand) {
+ if (participant.raisedHand) {
// if participant raises hand show notification
- !isParticipantApproved(id, MEDIA_TYPE.AUDIO)(state) && dispatch(participantPendingAudio(id));
+ !isParticipantApproved(participant.id, MEDIA_TYPE.AUDIO)(state)
+ && dispatch(participantPendingAudio(participant));
} else {
// if participant lowers hand hide notification
- isParticipantPending(id, MEDIA_TYPE.AUDIO)(state) && dispatch(dismissPendingAudioParticipant(id));
+ isParticipantPending(participant, MEDIA_TYPE.AUDIO)(state)
+ && dispatch(dismissPendingAudioParticipant(participant));
}
}
diff --git a/react/features/av-moderation/reducer.js b/react/features/av-moderation/reducer.js
index d78695201..e5b501ccc 100644
--- a/react/features/av-moderation/reducer.js
+++ b/react/features/av-moderation/reducer.js
@@ -1,6 +1,11 @@
/* @flow */
import { MEDIA_TYPE } from '../base/media/constants';
+import type { MediaType } from '../base/media/constants';
+import {
+ PARTICIPANT_LEFT,
+ PARTICIPANT_UPDATED
+} from '../base/participants';
import { ReducerRegistry } from '../base/redux';
import {
@@ -11,6 +16,7 @@ import {
PARTICIPANT_APPROVED,
PARTICIPANT_PENDING_AUDIO
} from './actionTypes';
+import { MEDIA_TYPE_TO_PENDING_STORE_KEY } from './constants';
const initialState = {
audioModerationEnabled: false,
@@ -21,6 +27,41 @@ const initialState = {
pendingVideo: []
};
+/**
+ Updates a participant in the state for the specified media type.
+ *
+ * @param {MediaType} mediaType - The media type.
+ * @param {Object} participant - Information about participant to be modified.
+ * @param {Object} state - The current state.
+ * @private
+ * @returns {boolean} - Whether state instance was modified.
+ */
+function _updatePendingParticipant(mediaType: MediaType, participant, state: Object = {}) {
+ let arrayItemChanged = false;
+ const storeKey = MEDIA_TYPE_TO_PENDING_STORE_KEY[mediaType];
+ const arr = state[storeKey];
+ const newArr = arr.map(pending => {
+ if (pending.id === participant.id) {
+ arrayItemChanged = true;
+
+ return {
+ ...pending,
+ ...participant
+ };
+ }
+
+ return pending;
+ });
+
+ if (arrayItemChanged) {
+ state[storeKey] = newArr;
+
+ return true;
+ }
+
+ return false;
+}
+
ReducerRegistry.register('features/av-moderation', (state = initialState, action) => {
switch (action.type) {
@@ -65,13 +106,13 @@ ReducerRegistry.register('features/av-moderation', (state = initialState, action
}
case PARTICIPANT_PENDING_AUDIO: {
- const { id } = action;
+ const { participant } = action;
- // Add participant to pendigAudio array only if it's not already added
- if (!state.pendingAudio.find(pending => pending === id)) {
+ // Add participant to pendingAudio array only if it's not already added
+ if (!state.pendingAudio.find(pending => pending.id === participant.id)) {
const updated = [ ...state.pendingAudio ];
- updated.push(id);
+ updated.push(participant);
return {
...state,
@@ -82,20 +123,79 @@ ReducerRegistry.register('features/av-moderation', (state = initialState, action
return state;
}
+ case PARTICIPANT_UPDATED: {
+ const participant = action.participant;
+ const { audioModerationEnabled, videoModerationEnabled } = state;
+ let hasStateChanged = false;
+
+ // skips changing the reference of pendingAudio or pendingVideo,
+ // if there is no change in the elements
+ if (audioModerationEnabled) {
+ hasStateChanged = _updatePendingParticipant(MEDIA_TYPE.AUDIO, participant, state);
+ }
+
+ if (videoModerationEnabled) {
+ hasStateChanged = _updatePendingParticipant(MEDIA_TYPE.VIDEO, participant, state);
+ }
+
+ // If the state has changed we need to return a new object reference in order to trigger subscriber updates.
+ if (hasStateChanged) {
+ return {
+ ...state
+ };
+ }
+
+ return state;
+ }
+ case PARTICIPANT_LEFT: {
+ const participant = action.participant;
+ const { audioModerationEnabled, videoModerationEnabled } = state;
+ let hasStateChanged = false;
+
+ // skips changing the reference of pendingAudio or pendingVideo,
+ // if there is no change in the elements
+ if (audioModerationEnabled) {
+ const newPendingAudio = state.pendingAudio.filter(pending => pending.id !== participant.id);
+
+ if (state.pendingAudio.length !== newPendingAudio.length) {
+ state.pendingAudio = newPendingAudio;
+ hasStateChanged = true;
+ }
+ }
+
+ if (videoModerationEnabled) {
+ const newPendingVideo = state.pendingVideo.filter(pending => pending.id !== participant.id);
+
+ if (state.pendingVideo.length !== newPendingVideo.length) {
+ state.pendingVideo = newPendingVideo;
+ hasStateChanged = true;
+ }
+ }
+
+ // If the state has changed we need to return a new object reference in order to trigger subscriber updates.
+ if (hasStateChanged) {
+ return {
+ ...state
+ };
+ }
+
+ return state;
+ }
+
case DISMISS_PENDING_PARTICIPANT: {
- const { id, mediaType } = action;
+ const { participant, mediaType } = action;
if (mediaType === MEDIA_TYPE.AUDIO) {
return {
...state,
- pendingAudio: state.pendingAudio.filter(pending => pending !== id)
+ pendingAudio: state.pendingAudio.filter(pending => pending.id !== participant.id)
};
}
if (mediaType === MEDIA_TYPE.VIDEO) {
return {
...state,
- pendingAudio: state.pendingVideo.filter(pending => pending !== id)
+ pendingVideo: state.pendingVideo.filter(pending => pending.id !== participant.id)
};
}
diff --git a/react/features/base/conference/middleware.any.js b/react/features/base/conference/middleware.any.js
index 692fb39c4..e864f203f 100644
--- a/react/features/base/conference/middleware.any.js
+++ b/react/features/base/conference/middleware.any.js
@@ -398,10 +398,9 @@ function _pinParticipant({ getState }, next, action) {
return next(action);
}
- const participants = state['features/base/participants'];
const id = action.participant.id;
- const participantById = getParticipantById(participants, id);
- const pinnedParticipant = getPinnedParticipant(participants);
+ const participantById = getParticipantById(state, id);
+ const pinnedParticipant = getPinnedParticipant(state);
const actionName = id ? ACTION_PINNED : ACTION_UNPINNED;
const local
= (participantById && participantById.local)
diff --git a/react/features/base/config/functions.web.js b/react/features/base/config/functions.web.js
index 224d4f5b0..f78d9c37a 100644
--- a/react/features/base/config/functions.web.js
+++ b/react/features/base/config/functions.web.js
@@ -56,12 +56,15 @@ export function getToolbarButtons(state: Object): Array {
}
/**
- * Curried selector to check if the specified button is enabled.
+ * Checks if the specified button is enabled.
*
* @param {string} buttonName - The name of the button.
* {@link interfaceConfig}.
- * @returns {Function} - Selector that returns a boolean.
+ * @param {Object|Array} state - The redux state or the array with the enabled buttons.
+ * @returns {boolean} - True if the button is enabled and false otherwise.
*/
-export const isToolbarButtonEnabled = (buttonName: string) =>
- (state: Object): boolean =>
- getToolbarButtons(state).includes(buttonName);
+export function isToolbarButtonEnabled(buttonName: string, state: Object | Array) {
+ const buttons = Array.isArray(state) ? state : getToolbarButtons(state);
+
+ return buttons.includes(buttonName);
+}
diff --git a/react/features/base/participants/functions.js b/react/features/base/participants/functions.js
index 4b361256c..b859ca479 100644
--- a/react/features/base/participants/functions.js
+++ b/react/features/base/participants/functions.js
@@ -79,16 +79,15 @@ export function getFirstLoadableAvatarUrl(participant: Object, store: Store p.local);
+ return state.local;
}
/**
@@ -109,8 +108,7 @@ export function getNormalizedDisplayName(name: string) {
/**
* Returns participant by ID from Redux state.
*
- * @param {(Function|Object|Participant[])} stateful - The redux state
- * features/base/participants, the (whole) redux state, or redux's
+ * @param {(Function|Object)} stateful - The (whole) redux state, or redux's
* {@code getState} function to be used to retrieve the state
* features/base/participants.
* @param {string} id - The ID of the participant to retrieve.
@@ -119,37 +117,82 @@ export function getNormalizedDisplayName(name: string) {
*/
export function getParticipantById(
stateful: Object | Function, id: string): ?Object {
- const participants = _getAllParticipants(stateful);
+ const state = toState(stateful)['features/base/participants'];
+ const { local, remote } = state;
- return participants.find(p => p.id === id);
+ return remote.get(id) || (local?.id === id ? local : undefined);
+}
+
+/**
+ * Returns the participant with the ID matching the passed ID or the local participant if the ID is
+ * undefined.
+ *
+ * @param {(Function|Object)} stateful - The (whole) redux state, or redux's
+ * {@code getState} function to be used to retrieve the state
+ * features/base/participants.
+ * @param {string|undefined} [participantID] - An optional partipantID argument.
+ * @returns {Participant|undefined}
+ */
+export function getParticipantByIdOrUndefined(stateful: Object | Function, participantID: ?string) {
+ return participantID ? getParticipantById(stateful, participantID) : getLocalParticipant(stateful);
}
/**
* Returns a count of the known participants in the passed in redux state,
* excluding any fake participants.
*
- * @param {(Function|Object|Participant[])} stateful - The redux state
- * features/base/participants, the (whole) redux state, or redux's
+ * @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 {number}
*/
export function getParticipantCount(stateful: Object | Function) {
- return getParticipants(stateful).length;
+ const state = toState(stateful)['features/base/participants'];
+ const { local, remote, fakeParticipants } = state;
+
+ return remote.size - fakeParticipants.size + (local ? 1 : 0);
+}
+
+/**
+ * Returns the Map with fake participants.
+ *
+ * @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 {Map} - The Map with fake participants.
+ */
+export function getFakeParticipants(stateful: Object | Function) {
+ return toState(stateful)['features/base/participants'].fakeParticipants;
+}
+
+/**
+ * Returns a count of the known remote participants in the passed in redux state.
+ *
+ * @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 {number}
+ */
+export function getRemoteParticipantCount(stateful: Object | Function) {
+ const state = toState(stateful)['features/base/participants'];
+
+ return state.remote.size;
}
/**
* Returns a count of the known participants in the passed in redux state,
* including fake participants.
*
- * @param {(Function|Object|Participant[])} stateful - The redux state
- * features/base/participants, the (whole) redux state, or redux's
+ * @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 {number}
*/
export function getParticipantCountWithFake(stateful: Object | Function) {
- return _getAllParticipants(stateful).length;
+ const state = toState(stateful)['features/base/participants'];
+ const { local, remote } = state;
+
+ return remote.size + (local ? 1 : 0);
}
/**
@@ -185,17 +228,6 @@ export function getParticipantDisplayName(
: 'Fellow Jitster';
}
-/**
- * Curried version of getParticipantDisplayName.
- *
- * @see {@link getParticipantDisplayName}
- * @param {string} id - The ID of the participant's display name to retrieve.
- * @returns {Function}
- */
-export const getParticipantDisplayNameWithId = (id: string) =>
- (state: Object | Function) =>
- getParticipantDisplayName(state, id);
-
/**
* Returns the presence status of a participant associated with the passed id.
*
@@ -219,64 +251,45 @@ export function getParticipantPresenceStatus(
}
/**
- * Selectors for getting all known participants with fake participants filtered
- * out.
+ * Returns true if there is at least 1 participant with screen sharing feature and false otherwise.
*
- * @param {(Function|Object|Participant[])} stateful - The redux state
- * features/base/participants, the (whole) redux state, or redux's
+ * @param {(Function|Object)} stateful - The (whole) redux state, or redux's
+ * {@code getState} function to be used to retrieve the state.
+ * @returns {boolean}
+ */
+export function haveParticipantWithScreenSharingFeature(stateful: Object | Function) {
+ return toState(stateful)['features/base/participants'].haveParticipantWithScreenSharingFeature;
+}
+
+/**
+ * Selectors for getting all remote participants.
+ *
+ * @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 {Participant[]}
+ * @returns {Map}
*/
-export function getParticipants(stateful: Object | Function) {
- return _getAllParticipants(stateful).filter(p => !p.isFakeParticipant);
+export function getRemoteParticipants(stateful: Object | Function) {
+ return toState(stateful)['features/base/participants'].remote;
}
/**
* Returns the participant which has its pinned state set to truthy.
*
- * @param {(Function|Object|Participant[])} stateful - The redux state
- * features/base/participants, the (whole) redux state, or redux's
+ * @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 {(Participant|undefined)}
*/
export function getPinnedParticipant(stateful: Object | Function) {
- return _getAllParticipants(stateful).find(p => p.pinned);
-}
+ const state = toState(stateful)['features/base/participants'];
+ const { pinnedParticipant } = state;
-/**
- * Returns array of participants from Redux state.
- *
- * @param {(Function|Object|Participant[])} stateful - The redux state
- * features/base/participants, the (whole) redux state, or redux's
- * {@code getState} function to be used to retrieve the state
- * features/base/participants.
- * @private
- * @returns {Participant[]}
- */
-function _getAllParticipants(stateful) {
- return (
- Array.isArray(stateful)
- ? stateful
- : toState(stateful)['features/base/participants'] || []);
-}
+ if (!pinnedParticipant) {
+ return undefined;
+ }
-/**
- * Returns the youtube fake participant.
- * At the moment it is considered the youtube participant the only fake participant in the list.
- *
- * @param {(Function|Object|Participant[])} stateful - The redux state
- * features/base/participants, the (whole) redux state, or redux's
- * {@code getState} function to be used to retrieve the state
- * features/base/participants.
- * @private
- * @returns {Participant}
- */
-export function getYoutubeParticipant(stateful: Object | Function) {
- const participants = _getAllParticipants(stateful);
-
- return participants.filter(p => p.isFakeParticipant)[0];
+ return getParticipantById(stateful, pinnedParticipant);
}
/**
@@ -289,6 +302,24 @@ export function isParticipantModerator(participant: Object) {
return participant?.role === PARTICIPANT_ROLE.MODERATOR;
}
+/**
+ * Returns the dominant speaker participant.
+ *
+ * @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 {Participant} - The participant from the redux store.
+ */
+export function getDominantSpeakerParticipant(stateful: Object | Function) {
+ const state = toState(stateful)['features/base/participants'];
+ const { dominantSpeaker } = state;
+
+ if (!dominantSpeaker) {
+ return undefined;
+ }
+
+ return getParticipantById(stateful, dominantSpeaker);
+}
+
/**
* Returns true if all of the meeting participants are moderators.
*
@@ -297,9 +328,9 @@ export function isParticipantModerator(participant: Object) {
* @returns {boolean}
*/
export function isEveryoneModerator(stateful: Object | Function) {
- const participants = _getAllParticipants(stateful);
+ const state = toState(stateful)['features/base/participants'];
- return participants.every(isParticipantModerator);
+ return state.everyoneIsModerator === true;
}
/**
@@ -321,14 +352,15 @@ export function isIconUrl(icon: ?string | ?Object) {
* @returns {boolean}
*/
export function isLocalParticipantModerator(stateful: Object | Function) {
- const state = toState(stateful);
- const localParticipant = getLocalParticipant(state);
+ const state = toState(stateful)['features/base/participants'];
- if (!localParticipant) {
+ const { local } = state;
+
+ if (!local) {
return false;
}
- return isParticipantModerator(localParticipant);
+ return isParticipantModerator(local);
}
/**
@@ -390,7 +422,7 @@ async function _getFirstLoadableAvatarUrl(participant, store) {
for (let i = 0; i < AVATAR_CHECKER_FUNCTIONS.length; i++) {
const url = AVATAR_CHECKER_FUNCTIONS[i](participant, store);
- if (url) {
+ if (url !== null) {
if (AVATAR_CHECKED_URLS.has(url)) {
if (AVATAR_CHECKED_URLS.get(url)) {
return url;
diff --git a/react/features/base/participants/middleware.js b/react/features/base/participants/middleware.js
index 2d0be9927..5c2b986aa 100644
--- a/react/features/base/participants/middleware.js
+++ b/react/features/base/participants/middleware.js
@@ -1,5 +1,7 @@
// @flow
+import { batch } from 'react-redux';
+
import UIEvents from '../../../../service/UI/UIEvents';
import { toggleE2EE } from '../../e2ee/actions';
import { NOTIFICATION_TIMEOUT, showNotification } from '../../notifications';
@@ -43,7 +45,8 @@ import {
getLocalParticipant,
getParticipantById,
getParticipantCount,
- getParticipantDisplayName
+ getParticipantDisplayName,
+ getRemoteParticipants
} from './functions';
import { PARTICIPANT_JOINED_FILE, PARTICIPANT_LEFT_FILE } from './sounds';
@@ -182,11 +185,12 @@ MiddlewareRegistry.register(store => next => action => {
StateListenerRegistry.register(
/* selector */ state => getCurrentConference(state),
/* listener */ (conference, { dispatch, getState }) => {
- for (const p of getState()['features/base/participants']) {
- !p.local
- && (!conference || p.conference !== conference)
- && dispatch(participantLeft(p.id, p.conference, p.isReplaced));
- }
+ batch(() => {
+ for (const [ id, p ] of getRemoteParticipants(getState())) {
+ (!conference || p.conference !== conference)
+ && dispatch(participantLeft(id, p.conference, p.isReplaced));
+ }
+ });
});
/**
diff --git a/react/features/base/participants/reducer.js b/react/features/base/participants/reducer.js
index 7467fbf55..e3c304653 100644
--- a/react/features/base/participants/reducer.js
+++ b/react/features/base/participants/reducer.js
@@ -12,6 +12,7 @@ import {
SET_LOADABLE_AVATAR_URL
} from './actionTypes';
import { LOCAL_PARTICIPANT_DEFAULT_ID, PARTICIPANT_ROLE } from './constants';
+import { isParticipantModerator } from './functions';
/**
* Participant object.
@@ -51,6 +52,16 @@ const PARTICIPANT_PROPS_TO_OMIT_WHEN_UPDATE = [
'pinned'
];
+const DEFAULT_STATE = {
+ haveParticipantWithScreenSharingFeature: false,
+ dominantSpeaker: undefined,
+ everyoneIsModerator: false,
+ pinnedParticipant: undefined,
+ local: undefined,
+ remote: new Map(),
+ fakeParticipants: new Map()
+};
+
/**
* Listen for actions which add, remove, or update the set of participants in
* the conference.
@@ -62,18 +73,157 @@ const PARTICIPANT_PROPS_TO_OMIT_WHEN_UPDATE = [
* added/removed/modified.
* @returns {Participant[]}
*/
-ReducerRegistry.register('features/base/participants', (state = [], action) => {
+ReducerRegistry.register('features/base/participants', (state = DEFAULT_STATE, action) => {
switch (action.type) {
+ case PARTICIPANT_ID_CHANGED: {
+ const { local } = state;
+
+ if (local) {
+ state.local = {
+ ...local,
+ id: action.newValue
+ };
+
+ return {
+ ...state
+ };
+ }
+
+ return state;
+ }
+ case DOMINANT_SPEAKER_CHANGED: {
+ const { participant } = action;
+ const { id } = participant;
+ const { dominantSpeaker } = state;
+
+ // Only one dominant speaker is allowed.
+ if (dominantSpeaker) {
+ _updateParticipantProperty(state, dominantSpeaker, 'dominantSpeaker', false);
+ }
+
+ if (_updateParticipantProperty(state, id, 'dominantSpeaker', true)) {
+ return {
+ ...state,
+ dominantSpeaker: id
+ };
+ }
+
+ delete state.dominantSpeaker;
+
+ return {
+ ...state
+ };
+ }
+ case PIN_PARTICIPANT: {
+ const { participant } = action;
+ const { id } = participant;
+ const { pinnedParticipant } = state;
+
+ // Only one pinned participant is allowed.
+ if (pinnedParticipant) {
+ _updateParticipantProperty(state, pinnedParticipant, 'pinned', false);
+ }
+
+ if (_updateParticipantProperty(state, id, 'pinned', true)) {
+ return {
+ ...state,
+ pinnedParticipant: id
+ };
+ }
+
+ delete state.pinnedParticipant;
+
+ return {
+ ...state
+ };
+ }
case SET_LOADABLE_AVATAR_URL:
- case DOMINANT_SPEAKER_CHANGED:
- case PARTICIPANT_ID_CHANGED:
- case PARTICIPANT_UPDATED:
- case PIN_PARTICIPANT:
- return state.map(p => _participant(p, action));
+ case PARTICIPANT_UPDATED: {
+ const { participant } = action;
+ let { id } = participant;
+ const { local } = participant;
- case PARTICIPANT_JOINED:
- return [ ...state, _participantJoined(action) ];
+ if (!id && local) {
+ id = LOCAL_PARTICIPANT_DEFAULT_ID;
+ }
+ let newParticipant;
+
+ if (state.remote.has(id)) {
+ newParticipant = _participant(state.remote.get(id), action);
+ state.remote.set(id, newParticipant);
+ } else if (id === state.local?.id) {
+ newParticipant = state.local = _participant(state.local, action);
+ }
+
+ if (newParticipant) {
+
+ // everyoneIsModerator calculation:
+ const isModerator = isParticipantModerator(newParticipant);
+
+ if (state.everyoneIsModerator && !isModerator) {
+ state.everyoneIsModerator = false;
+ } else if (!state.everyoneIsModerator && isModerator) {
+ state.everyoneIsModerator = _isEveryoneModerator(state);
+ }
+
+ // haveParticipantWithScreenSharingFeature calculation:
+ const { features = {} } = participant;
+
+ // Currently we use only PARTICIPANT_UPDATED to set a feature to enabled and we never disable it.
+ if (String(features['screen-sharing']) === 'true') {
+ state.haveParticipantWithScreenSharingFeature = true;
+ }
+ }
+
+ return {
+ ...state
+ };
+ }
+ case PARTICIPANT_JOINED: {
+ const participant = _participantJoined(action);
+ const { pinnedParticipant, dominantSpeaker } = state;
+
+ if (participant.pinned) {
+ if (pinnedParticipant) {
+ _updateParticipantProperty(state, pinnedParticipant, 'pinned', false);
+ }
+
+ state.pinnedParticipant = participant.id;
+ }
+
+ if (participant.dominantSpeaker) {
+ if (dominantSpeaker) {
+ _updateParticipantProperty(state, dominantSpeaker, 'dominantSpeaker', false);
+ }
+ state.dominantSpeaker = participant.id;
+ }
+
+ const isModerator = isParticipantModerator(participant);
+ const { local, remote } = state;
+
+ if (state.everyoneIsModerator && !isModerator) {
+ state.everyoneIsModerator = false;
+ } else if (!local && remote.size === 0 && isModerator) {
+ state.everyoneIsModerator = true;
+ }
+
+ if (participant.local) {
+ return {
+ ...state,
+ local: participant
+ };
+ }
+
+ state.remote.set(participant.id, participant);
+
+ if (participant.isFakeParticipant) {
+ state.fakeParticipants.set(participant.id, participant);
+ }
+
+ return { ...state };
+
+ }
case PARTICIPANT_LEFT: {
// XXX A remote participant is uniquely identified by their id in a
// specific JitsiConference instance. The local participant is uniquely
@@ -81,23 +231,111 @@ ReducerRegistry.register('features/base/participants', (state = [], action) => {
// (and the fact that the local participant "joins" at the beginning of
// the app and "leaves" at the end of the app).
const { conference, id } = action.participant;
+ const { fakeParticipants, remote, local, dominantSpeaker, pinnedParticipant } = state;
+ let oldParticipant = remote.get(id);
- return state.filter(p =>
- !(
- p.id === id
+ if (oldParticipant && oldParticipant.conference === conference) {
+ remote.delete(id);
+ } else if (local?.id === id) {
+ oldParticipant = state.local;
+ delete state.local;
+ } else {
+ // no participant found
+ return state;
+ }
- // XXX Do not allow collisions in the IDs of the local
- // participant and a remote participant cause the removal of
- // the local participant when the remote participant's
- // removal is requested.
- && p.conference === conference
- && (conference || p.local)));
+ if (!state.everyoneIsModerator && !isParticipantModerator(oldParticipant)) {
+ state.everyoneIsModerator = _isEveryoneModerator(state);
+ }
+
+ const { features = {} } = oldParticipant || {};
+
+ if (state.haveParticipantWithScreenSharingFeature && String(features['screen-sharing']) === 'true') {
+ const { features: localFeatures = {} } = state.local || {};
+
+ if (String(localFeatures['screen-sharing']) !== 'true') {
+ state.haveParticipantWithScreenSharingFeature = false;
+
+ // eslint-disable-next-line no-unused-vars
+ for (const [ key, participant ] of state.remote) {
+ const { features: f = {} } = participant;
+
+ if (String(f['screen-sharing']) === 'true') {
+ state.haveParticipantWithScreenSharingFeature = true;
+ break;
+ }
+ }
+ }
+
+
+ }
+
+ if (dominantSpeaker === id) {
+ state.dominantSpeaker = undefined;
+ }
+
+ if (pinnedParticipant === id) {
+ state.pinnedParticipant = undefined;
+ }
+
+ if (fakeParticipants.has(id)) {
+ fakeParticipants.delete(id);
+ }
+
+ return { ...state };
}
}
return state;
});
+/**
+ * Loops trough the participants in the state in order to check if all participants are moderators.
+ *
+ * @param {Object} state - The local participant redux state.
+ * @returns {boolean}
+ */
+function _isEveryoneModerator(state) {
+ if (isParticipantModerator(state.local)) {
+ // eslint-disable-next-line no-unused-vars
+ for (const [ k, p ] of state.remote) {
+ if (!isParticipantModerator(p)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ return false;
+}
+
+
+/**
+ * Updates a specific property for a participant.
+ *
+ * @param {State} state - The redux state.
+ * @param {string} id - The ID of the participant.
+ * @param {string} property - The property to update.
+ * @param {*} value - The new value.
+ * @returns {boolean} - True if a participant was updated and false otherwise.
+ */
+function _updateParticipantProperty(state, id, property, value) {
+ const { remote, local } = state;
+
+ if (remote.has(id)) {
+ remote.set(id, set(remote.get(id), property, value));
+
+ return true;
+ } else if (local?.id === id) {
+ state.local = set(local, property, value);
+
+ return true;
+ }
+
+ return false;
+}
+
/**
* Reducer function for a single participant.
*
@@ -112,56 +350,22 @@ ReducerRegistry.register('features/base/participants', (state = [], action) => {
*/
function _participant(state: Object = {}, action) {
switch (action.type) {
- case DOMINANT_SPEAKER_CHANGED:
- // Only one dominant speaker is allowed.
- return (
- set(state, 'dominantSpeaker', state.id === action.participant.id));
-
- case PARTICIPANT_ID_CHANGED: {
- // A participant is identified by an id-conference pair. Only the local
- // participant is with an undefined conference.
- const { conference } = action;
-
- if (state.id === action.oldValue
- && state.conference === conference
- && (conference || state.local)) {
- return {
- ...state,
- id: action.newValue
- };
- }
- break;
- }
-
case SET_LOADABLE_AVATAR_URL:
case PARTICIPANT_UPDATED: {
const { participant } = action; // eslint-disable-line no-shadow
- let { id } = participant;
- const { local } = participant;
- if (!id && local) {
- id = LOCAL_PARTICIPANT_DEFAULT_ID;
- }
+ const newState = { ...state };
- if (state.id === id) {
- const newState = { ...state };
-
- for (const key in participant) {
- if (participant.hasOwnProperty(key)
- && PARTICIPANT_PROPS_TO_OMIT_WHEN_UPDATE.indexOf(key)
- === -1) {
- newState[key] = participant[key];
- }
+ for (const key in participant) {
+ if (participant.hasOwnProperty(key)
+ && PARTICIPANT_PROPS_TO_OMIT_WHEN_UPDATE.indexOf(key)
+ === -1) {
+ newState[key] = participant[key];
}
-
- return newState;
}
- break;
- }
- case PIN_PARTICIPANT:
- // Currently, only one pinned participant is allowed.
- return set(state, 'pinned', state.id === action.participant.id);
+ return newState;
+ }
}
return state;
diff --git a/react/features/base/tracks/functions.js b/react/features/base/tracks/functions.js
index ff1135c2e..9cc66a0d1 100644
--- a/react/features/base/tracks/functions.js
+++ b/react/features/base/tracks/functions.js
@@ -21,68 +21,50 @@ import logger from './logger';
export const getTrackState = state => state['features/base/tracks'];
/**
- * Higher-order function that returns a selector for a specific participant
- * and media type.
+ * Checks if the passed media type is muted for the participant.
*
* @param {Object} participant - Participant reference.
* @param {MEDIA_TYPE} mediaType - Media type.
- * @returns {Function} Selector.
+ * @param {Object} state - Global state.
+ * @returns {boolean} - Is the media type muted for the participant.
*/
-export const getIsParticipantMediaMuted = (participant, mediaType) =>
+export function isParticipantMediaMuted(participant, mediaType, state) {
+ if (!participant) {
+ return false;
+ }
- /**
- * Bound selector.
- *
- * @param {Object} state - Global state.
- * @returns {boolean} Is the media type muted for the participant.
- */
- state => {
- if (!participant) {
- return;
- }
+ const tracks = getTrackState(state);
- const tracks = getTrackState(state);
+ if (participant?.local) {
+ return isLocalTrackMuted(tracks, mediaType);
+ } else if (!participant?.isFakeParticipant) {
+ return isRemoteTrackMuted(tracks, mediaType, participant.id);
+ }
- if (participant?.local) {
- return isLocalTrackMuted(tracks, mediaType);
- } else if (!participant?.isFakeParticipant) {
- return isRemoteTrackMuted(tracks, mediaType, participant.id);
- }
-
- return true;
- };
+ return true;
+}
/**
- * Higher-order function that returns a selector for a specific participant.
+ * Checks if the participant is audio muted.
*
* @param {Object} participant - Participant reference.
- * @returns {Function} Selector.
+ * @param {Object} state - Global state.
+ * @returns {boolean} - Is audio muted for the participant.
*/
-export const getIsParticipantAudioMuted = participant =>
-
- /**
- * Bound selector.
- *
- * @param {Object} state - Global state.
- * @returns {boolean} Is audio muted for the participant.
- */
- state => getIsParticipantMediaMuted(participant, MEDIA_TYPE.AUDIO)(state);
+export function isParticipantAudioMuted(participant, state) {
+ return isParticipantMediaMuted(participant, MEDIA_TYPE.AUDIO, state);
+}
/**
- * Higher-order function that returns a selector for a specific participant.
+ * Checks if the participant is video muted.
*
* @param {Object} participant - Participant reference.
- * @returns {Function} Selector.
+ * @param {Object} state - Global state.
+ * @returns {boolean} - Is video muted for the participant.
*/
-export const getIsParticipantVideoMuted = participant =>
-
- /**
- * Bound selector.
- *
- * @param {Object} state - Global state.
- * @returns {boolean} Is video muted for the participant.
- */
- state => getIsParticipantMediaMuted(participant, MEDIA_TYPE.VIDEO)(state);
+export function isParticipantVideoMuted(participant, state) {
+ return isParticipantMediaMuted(participant, MEDIA_TYPE.VIDEO, state);
+}
/**
* Creates a local video track for presenter. The constraints are computed based
diff --git a/react/features/chat/components/AbstractChat.js b/react/features/chat/components/AbstractChat.js
index 22e6d5f87..7bbb1cc82 100644
--- a/react/features/chat/components/AbstractChat.js
+++ b/react/features/chat/components/AbstractChat.js
@@ -108,6 +108,6 @@ export function _mapStateToProps(state: Object) {
_isModal: window.innerWidth <= SMALL_WIDTH_THRESHOLD,
_isOpen: isOpen,
_messages: messages,
- _showNamePrompt: !_localParticipant.name
+ _showNamePrompt: !_localParticipant?.name
};
}
diff --git a/react/features/display-name/components/web/DisplayName.js b/react/features/display-name/components/web/DisplayName.js
index f02ef5a26..7588e7cd1 100644
--- a/react/features/display-name/components/web/DisplayName.js
+++ b/react/features/display-name/components/web/DisplayName.js
@@ -288,8 +288,7 @@ function _mapStateToProps(state, ownProps) {
return {
_configuredDisplayName: participant && participant.name,
- _nameToDisplay: getParticipantDisplayName(
- state, participantID)
+ _nameToDisplay: getParticipantDisplayName(state, participantID)
};
}
diff --git a/react/features/e2ee/actionTypes.js b/react/features/e2ee/actionTypes.js
index 25051106a..14fcdaa6d 100644
--- a/react/features/e2ee/actionTypes.js
+++ b/react/features/e2ee/actionTypes.js
@@ -6,3 +6,22 @@
* }
*/
export const TOGGLE_E2EE = 'TOGGLE_E2EE';
+
+/**
+ * The type of the action which signals to set new value whether everyone has E2EE enabled.
+ *
+ * {
+ * type: SET_EVERYONE_ENABLED_E2EE,
+ * everyoneEnabledE2EE: boolean
+ * }
+ */
+export const SET_EVERYONE_ENABLED_E2EE = 'SET_EVERYONE_ENABLED_E2EE';
+
+/**
+ * The type of the action which signals to set new value whether everyone supports E2EE.
+ *
+ * {
+ * type: SET_EVERYONE_SUPPORT_E2EE
+ * }
+ */
+export const SET_EVERYONE_SUPPORT_E2EE = 'SET_EVERYONE_SUPPORT_E2EE';
diff --git a/react/features/e2ee/actions.js b/react/features/e2ee/actions.js
index 4beb1629f..a9b737559 100644
--- a/react/features/e2ee/actions.js
+++ b/react/features/e2ee/actions.js
@@ -1,6 +1,6 @@
// @flow
-import { TOGGLE_E2EE } from './actionTypes';
+import { SET_EVERYONE_ENABLED_E2EE, SET_EVERYONE_SUPPORT_E2EE, TOGGLE_E2EE } from './actionTypes';
/**
* Dispatches an action to enable / disable E2EE.
@@ -14,3 +14,35 @@ export function toggleE2EE(enabled: boolean) {
enabled
};
}
+
+/**
+ * Set new value whether everyone has E2EE enabled.
+ *
+ * @param {boolean} everyoneEnabledE2EE - The new value.
+ * @returns {{
+ * type: SET_EVERYONE_ENABLED_E2EE,
+ * everyoneEnabledE2EE: boolean
+ * }}
+ */
+export function setEveryoneEnabledE2EE(everyoneEnabledE2EE: boolean) {
+ return {
+ type: SET_EVERYONE_ENABLED_E2EE,
+ everyoneEnabledE2EE
+ };
+}
+
+/**
+ * Set new value whether everyone support E2EE.
+ *
+ * @param {boolean} everyoneSupportE2EE - The new value.
+ * @returns {{
+ * type: SET_EVERYONE_SUPPORT_E2EE,
+ * everyoneSupportE2EE: boolean
+ * }}
+ */
+export function setEveryoneSupportE2EE(everyoneSupportE2EE: boolean) {
+ return {
+ type: SET_EVERYONE_SUPPORT_E2EE,
+ everyoneSupportE2EE
+ };
+}
diff --git a/react/features/e2ee/components/AbstractE2EELabel.js b/react/features/e2ee/components/AbstractE2EELabel.js
index e3af399aa..c668d7675 100644
--- a/react/features/e2ee/components/AbstractE2EELabel.js
+++ b/react/features/e2ee/components/AbstractE2EELabel.js
@@ -22,9 +22,7 @@ export type Props = {
* @returns {Props}
*/
export function _mapStateToProps(state: Object) {
- const participants = state['features/base/participants'];
-
return {
- _showLabel: participants.every(p => p.e2eeEnabled)
+ _showLabel: state['features/e2ee'].everyoneEnabledE2EE
};
}
diff --git a/react/features/e2ee/components/E2EESection.js b/react/features/e2ee/components/E2EESection.js
index be6bd5502..4e5f54c60 100644
--- a/react/features/e2ee/components/E2EESection.js
+++ b/react/features/e2ee/components/E2EESection.js
@@ -5,7 +5,6 @@ import type { Dispatch } from 'redux';
import { createE2EEEvent, sendAnalytics } from '../../analytics';
import { translate } from '../../base/i18n';
-import { getParticipants } from '../../base/participants';
import { Switch } from '../../base/react';
import { connect } from '../../base/redux';
import { toggleE2EE } from '../actions';
@@ -21,7 +20,7 @@ type Props = {
/**
* Indicates whether all participants in the conference currently support E2EE.
*/
- _everyoneSupportsE2EE: boolean,
+ _everyoneSupportE2EE: boolean,
/**
* The redux {@code dispatch} function.
@@ -96,7 +95,7 @@ class E2EESection extends Component {
* @returns {ReactElement}
*/
render() {
- const { _everyoneSupportsE2EE, t } = this.props;
+ const { _everyoneSupportE2EE, t } = this.props;
const { enabled, expand } = this.state;
const description = t('dialog.e2eeDescription');
@@ -120,7 +119,7 @@ class E2EESection extends Component {
}
{
- !_everyoneSupportsE2EE
+ !_everyoneSupportE2EE
&&
{ t('dialog.e2eeWarning') }
@@ -195,12 +194,11 @@ class E2EESection extends Component {
* @returns {Props}
*/
function mapStateToProps(state) {
- const { enabled } = state['features/e2ee'];
- const participants = getParticipants(state).filter(p => !p.local);
+ const { enabled, everyoneSupportE2EE } = state['features/e2ee'];
return {
_enabled: enabled,
- _everyoneSupportsE2EE: participants.every(p => Boolean(p.e2eeSupported))
+ _everyoneSupportE2EE: everyoneSupportE2EE
};
}
diff --git a/react/features/e2ee/middleware.js b/react/features/e2ee/middleware.js
index 1941f7c92..065e573c0 100644
--- a/react/features/e2ee/middleware.js
+++ b/react/features/e2ee/middleware.js
@@ -1,13 +1,24 @@
// @flow
+import { batch } from 'react-redux';
+
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app';
import { getCurrentConference } from '../base/conference';
-import { getLocalParticipant, participantUpdated } from '../base/participants';
+import {
+ getLocalParticipant,
+ getParticipantById,
+ getParticipantCount,
+ PARTICIPANT_JOINED,
+ PARTICIPANT_LEFT,
+ PARTICIPANT_UPDATED,
+ participantUpdated,
+ getRemoteParticipants
+} from '../base/participants';
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
import { playSound, registerSound, unregisterSound } from '../base/sounds';
import { TOGGLE_E2EE } from './actionTypes';
-import { toggleE2EE } from './actions';
+import { setEveryoneEnabledE2EE, setEveryoneSupportE2EE, toggleE2EE } from './actions';
import { E2EE_OFF_SOUND_ID, E2EE_ON_SOUND_ID } from './constants';
import logger from './logger';
import { E2EE_OFF_SOUND_FILE, E2EE_ON_SOUND_FILE } from './sounds';
@@ -35,6 +46,128 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
dispatch(unregisterSound(E2EE_ON_SOUND_ID));
break;
+ case PARTICIPANT_UPDATED: {
+ const { id, e2eeEnabled, e2eeSupported } = action.participant;
+ const oldParticipant = getParticipantById(getState(), id);
+ const result = next(action);
+
+ if (e2eeEnabled !== oldParticipant?.e2eeEnabled
+ || e2eeSupported !== oldParticipant?.e2eeSupported) {
+ const state = getState();
+ let newEveryoneSupportE2EE = true;
+ let newEveryoneEnabledE2EE = true;
+
+ // eslint-disable-next-line no-unused-vars
+ for (const [ key, p ] of getRemoteParticipants(state)) {
+ if (!p.e2eeEnabled) {
+ newEveryoneEnabledE2EE = false;
+ }
+
+ if (!p.e2eeSupported) {
+ newEveryoneSupportE2EE = false;
+ }
+
+ if (!newEveryoneEnabledE2EE && !newEveryoneSupportE2EE) {
+ break;
+ }
+ }
+
+ if (!getLocalParticipant(state)?.e2eeEnabled) {
+ newEveryoneEnabledE2EE = false;
+ }
+
+ batch(() => {
+ dispatch(setEveryoneEnabledE2EE(newEveryoneEnabledE2EE));
+ dispatch(setEveryoneSupportE2EE(newEveryoneSupportE2EE));
+ });
+ }
+
+ return result;
+ }
+ case PARTICIPANT_JOINED: {
+ const result = next(action);
+ const { e2eeEnabled, e2eeSupported, local } = action.participant;
+ const { everyoneEnabledE2EE } = getState()['features/e2ee'];
+ const participantCount = getParticipantCount(getState());
+
+ // the initial values
+ if (participantCount === 1) {
+ batch(() => {
+ dispatch(setEveryoneEnabledE2EE(e2eeEnabled));
+ dispatch(setEveryoneSupportE2EE(e2eeSupported));
+ });
+ }
+
+ // if all had it enabled and this one disabled it, change value in store
+ // otherwise there is no change in the value we store
+ if (everyoneEnabledE2EE && !e2eeEnabled) {
+ dispatch(setEveryoneEnabledE2EE(false));
+ }
+
+ if (local) {
+ return result;
+ }
+
+ const { everyoneSupportE2EE } = getState()['features/e2ee'];
+
+ // if all supported it and this one does not, change value in store
+ // otherwise there is no change in the value we store
+ if (everyoneSupportE2EE && !e2eeSupported) {
+ dispatch(setEveryoneSupportE2EE(false));
+ }
+
+ return result;
+ }
+
+ case PARTICIPANT_LEFT: {
+ const previosState = getState();
+ const participant = getParticipantById(previosState, action.participant?.id) || {};
+ const result = next(action);
+ const newState = getState();
+ const { e2eeEnabled = false, e2eeSupported = false } = participant;
+
+ const { everyoneEnabledE2EE, everyoneSupportE2EE } = newState['features/e2ee'];
+
+
+ // if it was not enabled by everyone, and the participant leaving had it disabled, or if it was not supported
+ // by everyone, and the participant leaving had it not supported let's check is it enabled for all that stay
+ if ((!everyoneEnabledE2EE && !e2eeEnabled) || (!everyoneSupportE2EE && !e2eeSupported)) {
+ let latestEveryoneEnabledE2EE = true;
+ let latestEveryoneSupportE2EE = true;
+
+ // eslint-disable-next-line no-unused-vars
+ for (const [ key, p ] of getRemoteParticipants(newState)) {
+ if (!p.e2eeEnabled) {
+ latestEveryoneEnabledE2EE = false;
+ }
+
+ if (!p.e2eeSupported) {
+ latestEveryoneSupportE2EE = false;
+ }
+
+ if (!latestEveryoneEnabledE2EE && !latestEveryoneSupportE2EE) {
+ break;
+ }
+ }
+
+ if (!getLocalParticipant(newState)?.e2eeEnabled) {
+ latestEveryoneEnabledE2EE = false;
+ }
+
+ batch(() => {
+ if (!everyoneEnabledE2EE && latestEveryoneEnabledE2EE) {
+ dispatch(setEveryoneEnabledE2EE(true));
+ }
+
+ if (!everyoneSupportE2EE && latestEveryoneSupportE2EE) {
+ dispatch(setEveryoneSupportE2EE(true));
+ }
+ });
+ }
+
+ return result;
+ }
+
case TOGGLE_E2EE: {
const conference = getCurrentConference(getState);
diff --git a/react/features/e2ee/reducer.js b/react/features/e2ee/reducer.js
index cba87327b..abb587947 100644
--- a/react/features/e2ee/reducer.js
+++ b/react/features/e2ee/reducer.js
@@ -2,7 +2,11 @@
import { ReducerRegistry } from '../base/redux';
-import { TOGGLE_E2EE } from './actionTypes';
+import {
+ SET_EVERYONE_ENABLED_E2EE,
+ SET_EVERYONE_SUPPORT_E2EE,
+ TOGGLE_E2EE
+} from './actionTypes';
const DEFAULT_STATE = {
enabled: false
@@ -18,6 +22,16 @@ ReducerRegistry.register('features/e2ee', (state = DEFAULT_STATE, action) => {
...state,
enabled: action.enabled
};
+ case SET_EVERYONE_ENABLED_E2EE:
+ return {
+ ...state,
+ everyoneEnabledE2EE: action.everyoneEnabledE2EE
+ };
+ case SET_EVERYONE_SUPPORT_E2EE:
+ return {
+ ...state,
+ everyoneSupportE2EE: action.everyoneSupportE2EE
+ };
default:
return state;
diff --git a/react/features/filmstrip/actions.web.js b/react/features/filmstrip/actions.web.js
index a3bfb8211..631573ee3 100644
--- a/react/features/filmstrip/actions.web.js
+++ b/react/features/filmstrip/actions.web.js
@@ -1,7 +1,7 @@
// @flow
import type { Dispatch } from 'redux';
-import { pinParticipant } from '../base/participants';
+import { getLocalParticipant, getRemoteParticipants, pinParticipant } from '../base/participants';
import {
SET_HORIZONTAL_VIEW_DIMENSIONS,
@@ -127,7 +127,8 @@ export function setHorizontalViewDimensions() {
*/
export function clickOnVideo(n: number) {
return (dispatch: Function, getState: Function) => {
- const participants = getState()['features/base/participants'];
+ const state = getState();
+ const participants = [ getLocalParticipant(state), ...getRemoteParticipants(state).values() ];
const nThParticipant = participants[n];
const { id, pinned } = nThParticipant;
diff --git a/react/features/filmstrip/components/native/Filmstrip.js b/react/features/filmstrip/components/native/Filmstrip.js
index 9b5a84138..1f569c11d 100644
--- a/react/features/filmstrip/components/native/Filmstrip.js
+++ b/react/features/filmstrip/components/native/Filmstrip.js
@@ -109,10 +109,10 @@ class Filmstrip extends Component {
{
this._sort(_participants, isNarrowAspectRatio)
- .map(p => (
+ .map(id => (
))
+ key = { id }
+ participantID = { id } />))
}
{
@@ -166,12 +166,11 @@ class Filmstrip extends Component {
* @returns {Props}
*/
function _mapStateToProps(state) {
- const participants = state['features/base/participants'];
- const { enabled } = state['features/filmstrip'];
+ const { enabled, remoteParticipants } = state['features/filmstrip'];
return {
_aspectRatio: state['features/base/responsive-ui'].aspectRatio,
- _participants: participants.filter(p => !p.local),
+ _participants: remoteParticipants,
_visible: enabled && isFilmstripVisible(state)
};
}
diff --git a/react/features/filmstrip/components/native/LocalThumbnail.js b/react/features/filmstrip/components/native/LocalThumbnail.js
index c519ec302..b30cc43ce 100644
--- a/react/features/filmstrip/components/native/LocalThumbnail.js
+++ b/react/features/filmstrip/components/native/LocalThumbnail.js
@@ -1,63 +1,21 @@
// @flow
-import React, { Component } from 'react';
+import React from 'react';
import { View } from 'react-native';
-import { getLocalParticipant } from '../../../base/participants';
-import { connect } from '../../../base/redux';
-
import Thumbnail from './Thumbnail';
import styles from './styles';
-type Props = {
-
- /**
- * The local participant.
- */
- _localParticipant: Object
-};
-
/**
* Component to render a local thumbnail that can be separated from the
* remote thumbnails later.
- */
-class LocalThumbnail extends Component {
- /**
- * Implements React Component's render.
- *
- * @inheritdoc
- */
- render() {
- const { _localParticipant } = this.props;
-
- return (
-
-
-
- );
- }
-}
-
-/**
- * Maps (parts of) the redux state to the associated {@code LocalThumbnail}'s
- * props.
*
- * @param {Object} state - The redux state.
- * @private
- * @returns {{
- * _localParticipant: Participant
- * }}
+ * @returns {ReactElement}
*/
-function _mapStateToProps(state) {
- return {
- /**
- * The local participant.
- *
- * @private
- * @type {Participant}
- */
- _localParticipant: getLocalParticipant(state)
- };
+export default function LocalThumbnail() {
+ return (
+
+
+
+ );
}
-
-export default connect(_mapStateToProps)(LocalThumbnail);
diff --git a/react/features/filmstrip/components/native/Thumbnail.js b/react/features/filmstrip/components/native/Thumbnail.js
index 9026ad48c..eb3f145e9 100644
--- a/react/features/filmstrip/components/native/Thumbnail.js
+++ b/react/features/filmstrip/components/native/Thumbnail.js
@@ -1,6 +1,6 @@
// @flow
-import React from 'react';
+import React, { useCallback } from 'react';
import { View } from 'react-native';
import type { Dispatch } from 'redux';
@@ -12,7 +12,8 @@ import {
ParticipantView,
getParticipantCount,
isEveryoneModerator,
- pinParticipant
+ pinParticipant,
+ getParticipantByIdOrUndefined
} from '../../../base/participants';
import { Container } from '../../../base/react';
import { connect } from '../../../base/redux';
@@ -48,14 +49,9 @@ type Props = {
_largeVideo: Object,
/**
- * Handles click/tap event on the thumbnail.
+ * The Redux representation of the participant to display.
*/
- _onClick: ?Function,
-
- /**
- * Handles long press on the thumbnail.
- */
- _onThumbnailLongPress: ?Function,
+ _participant: Object,
/**
* Whether to show the dominant speaker indicator or not.
@@ -90,9 +86,9 @@ type Props = {
dispatch: Dispatch,
/**
- * The Redux representation of the participant to display.
+ * The ID of the participant related to the thumbnail.
*/
- participant: Object,
+ participantID: ?string,
/**
* Whether to display or hide the display name of the participant in the thumbnail.
@@ -120,14 +116,13 @@ function Thumbnail(props: Props) {
const {
_audioMuted: audioMuted,
_largeVideo: largeVideo,
- _onClick,
- _onThumbnailLongPress,
_renderDominantSpeakerIndicator: renderDominantSpeakerIndicator,
_renderModeratorIndicator: renderModeratorIndicator,
+ _participant: participant,
_styles,
_videoTrack: videoTrack,
+ dispatch,
disableTint,
- participant,
renderDisplayName,
tileView
} = props;
@@ -137,11 +132,29 @@ function Thumbnail(props: Props) {
= participantId === largeVideo.participantId;
const videoMuted = !videoTrack || videoTrack.muted;
const isScreenShare = videoTrack && videoTrack.videoType === VIDEO_TYPE.DESKTOP;
+ const onClick = useCallback(() => {
+ if (tileView) {
+ dispatch(toggleToolboxVisible());
+ } else {
+ dispatch(pinParticipant(participant.pinned ? null : participant.id));
+ }
+ }, [ participant, tileView, dispatch ]);
+ const onThumbnailLongPress = useCallback(() => {
+ if (participant.local) {
+ dispatch(openDialog(ConnectionStatusComponent, {
+ participantID: participant.id
+ }));
+ } else {
+ dispatch(openDialog(RemoteVideoMenu, {
+ participant
+ }));
+ }
+ }, [ participant, dispatch ]);
return (
2;
+ const renderDominantSpeakerIndicator = participant && participant.dominantSpeaker && participantCount > 2;
const _isEveryoneModerator = isEveryoneModerator(state);
- const renderModeratorIndicator = !_isEveryoneModerator && participant.role === PARTICIPANT_ROLE.MODERATOR;
+ const renderModeratorIndicator = !_isEveryoneModerator
+ && participant && participant.role === PARTICIPANT_ROLE.MODERATOR;
return {
_audioMuted: audioTrack?.muted ?? true,
_largeVideo: largeVideo,
+ _participant: participant,
_renderDominantSpeakerIndicator: renderDominantSpeakerIndicator,
_renderModeratorIndicator: renderModeratorIndicator,
_styles: ColorSchemeRegistry.get(state, 'Thumbnail'),
@@ -281,4 +248,4 @@ function _mapStateToProps(state, ownProps) {
};
}
-export default connect(_mapStateToProps, _mapDispatchToProps)(Thumbnail);
+export default connect(_mapStateToProps)(Thumbnail);
diff --git a/react/features/filmstrip/components/native/TileView.js b/react/features/filmstrip/components/native/TileView.js
index 4e39ca82a..7ed8fed53 100644
--- a/react/features/filmstrip/components/native/TileView.js
+++ b/react/features/filmstrip/components/native/TileView.js
@@ -8,6 +8,7 @@ import {
} from 'react-native';
import type { Dispatch } from 'redux';
+import { getLocalParticipant, getParticipantCountWithFake } from '../../../base/participants';
import { connect } from '../../../base/redux';
import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui/constants';
import { setTileViewDimensions } from '../../actions.native';
@@ -15,6 +16,7 @@ import { setTileViewDimensions } from '../../actions.native';
import Thumbnail from './Thumbnail';
import styles from './styles';
+
/**
* The type of the React {@link Component} props of {@link TileView}.
*/
@@ -31,9 +33,19 @@ type Props = {
_height: number,
/**
- * The participants in the conference.
+ * The local participant.
*/
- _participants: Array