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>
This commit is contained in:
parent
d87a40e77e
commit
0bdc7d42c5
|
@ -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));
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
|||
</div>
|
||||
<NotificationWithParticipants
|
||||
approveButtonText = { t('notify.unmute') }
|
||||
onApprove = { approveAudio }
|
||||
onApprove = { approveParticipant }
|
||||
onReject = { dismissPendingAudioParticipant }
|
||||
participants = { participants }
|
||||
rejectButtonText = { t('dialog.dismiss') }
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// @flow
|
||||
|
||||
import { MEDIA_TYPE, type MediaType } from '../base/media/constants';
|
||||
import { getParticipantById, isLocalParticipantModerator } from '../base/participants/functions';
|
||||
import { isLocalParticipantModerator } from '../base/participants/functions';
|
||||
|
||||
import { MEDIA_TYPE_TO_WHITELIST_STORE_KEY, MEDIA_TYPE_TO_PENDING_STORE_KEY } from './constants';
|
||||
|
||||
|
@ -13,6 +13,14 @@ import { MEDIA_TYPE_TO_WHITELIST_STORE_KEY, MEDIA_TYPE_TO_PENDING_STORE_KEY } fr
|
|||
*/
|
||||
const getState = state => 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;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -56,12 +56,15 @@ export function getToolbarButtons(state: Object): Array<string> {
|
|||
}
|
||||
|
||||
/**
|
||||
* 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<string>} 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<string>) {
|
||||
const buttons = Array.isArray(state) ? state : getToolbarButtons(state);
|
||||
|
||||
return buttons.includes(buttonName);
|
||||
}
|
||||
|
|
|
@ -79,16 +79,15 @@ export function getFirstLoadableAvatarUrl(participant: Object, store: Store<any,
|
|||
/**
|
||||
* Returns local participant 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.
|
||||
* @returns {(Participant|undefined)}
|
||||
*/
|
||||
export function getLocalParticipant(stateful: Object | Function) {
|
||||
const participants = _getAllParticipants(stateful);
|
||||
const state = toState(stateful)['features/base/participants'];
|
||||
|
||||
return participants.find(p => 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<string, Participant>} - 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<string, Object>}
|
||||
*/
|
||||
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;
|
||||
|
|
|
@ -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,12 +185,13 @@ 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));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Reset the ID of the local participant to
|
||||
|
|
|
@ -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,38 +350,10 @@ 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;
|
||||
}
|
||||
|
||||
if (state.id === id) {
|
||||
const newState = { ...state };
|
||||
|
||||
for (const key in participant) {
|
||||
|
@ -156,12 +366,6 @@ function _participant(state: Object = {}, action) {
|
|||
|
||||
return newState;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case PIN_PARTICIPANT:
|
||||
// Currently, only one pinned participant is allowed.
|
||||
return set(state, 'pinned', state.id === action.participant.id);
|
||||
}
|
||||
|
||||
return state;
|
||||
|
|
|
@ -21,24 +21,16 @@ 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.
|
||||
*/
|
||||
export const getIsParticipantMediaMuted = (participant, mediaType) =>
|
||||
|
||||
/**
|
||||
* Bound selector.
|
||||
*
|
||||
* @param {Object} state - Global state.
|
||||
* @returns {boolean} Is the media type muted for the participant.
|
||||
* @returns {boolean} - Is the media type muted for the participant.
|
||||
*/
|
||||
state => {
|
||||
export function isParticipantMediaMuted(participant, mediaType, state) {
|
||||
if (!participant) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
const tracks = getTrackState(state);
|
||||
|
@ -50,39 +42,29 @@ export const getIsParticipantMediaMuted = (participant, mediaType) =>
|
|||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
export const getIsParticipantAudioMuted = participant =>
|
||||
|
||||
/**
|
||||
* Bound selector.
|
||||
*
|
||||
* @param {Object} state - Global state.
|
||||
* @returns {boolean} Is audio muted for the participant.
|
||||
* @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.
|
||||
*/
|
||||
export const getIsParticipantVideoMuted = participant =>
|
||||
|
||||
/**
|
||||
* Bound selector.
|
||||
*
|
||||
* @param {Object} state - Global state.
|
||||
* @returns {boolean} Is video muted for the participant.
|
||||
* @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
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
|
@ -288,8 +288,7 @@ function _mapStateToProps(state, ownProps) {
|
|||
|
||||
return {
|
||||
_configuredDisplayName: participant && participant.name,
|
||||
_nameToDisplay: getParticipantDisplayName(
|
||||
state, participantID)
|
||||
_nameToDisplay: getParticipantDisplayName(state, participantID)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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<Props, State> {
|
|||
* @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<Props, State> {
|
|||
</span> }
|
||||
</p>
|
||||
{
|
||||
!_everyoneSupportsE2EE
|
||||
!_everyoneSupportE2EE
|
||||
&& <span className = 'warning'>
|
||||
{ t('dialog.e2eeWarning') }
|
||||
</span>
|
||||
|
@ -195,12 +194,11 @@ class E2EESection extends Component<Props, State> {
|
|||
* @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
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -109,10 +109,10 @@ class Filmstrip extends Component<Props> {
|
|||
{
|
||||
|
||||
this._sort(_participants, isNarrowAspectRatio)
|
||||
.map(p => (
|
||||
.map(id => (
|
||||
<Thumbnail
|
||||
key = { p.id }
|
||||
participant = { p } />))
|
||||
key = { id }
|
||||
participantID = { id } />))
|
||||
|
||||
}
|
||||
{
|
||||
|
@ -166,12 +166,11 @@ class Filmstrip extends Component<Props> {
|
|||
* @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)
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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<Props> {
|
||||
/**
|
||||
* Implements React Component's render.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { _localParticipant } = this.props;
|
||||
|
||||
export default function LocalThumbnail() {
|
||||
return (
|
||||
<View style = { styles.localThumbnail }>
|
||||
<Thumbnail participant = { _localParticipant } />
|
||||
<Thumbnail />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the associated {@code LocalThumbnail}'s
|
||||
* props.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _localParticipant: Participant
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
return {
|
||||
/**
|
||||
* The local participant.
|
||||
*
|
||||
* @private
|
||||
* @type {Participant}
|
||||
*/
|
||||
_localParticipant: getLocalParticipant(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(LocalThumbnail);
|
||||
|
|
|
@ -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<any>,
|
||||
|
||||
/**
|
||||
* 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 (
|
||||
<Container
|
||||
onClick = { _onClick }
|
||||
onLongPress = { _onThumbnailLongPress }
|
||||
onClick = { onClick }
|
||||
onLongPress = { onThumbnailLongPress }
|
||||
style = { [
|
||||
styles.thumbnail,
|
||||
participant.pinned && !tileView
|
||||
|
@ -198,55 +211,6 @@ function Thumbnail(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps part of redux actions to component's props.
|
||||
*
|
||||
* @param {Function} dispatch - Redux's {@code dispatch} function.
|
||||
* @param {Props} ownProps - The own props of the component.
|
||||
* @returns {{
|
||||
* _onClick: Function,
|
||||
* _onShowRemoteVideoMenu: Function
|
||||
* }}
|
||||
*/
|
||||
function _mapDispatchToProps(dispatch: Function, ownProps): Object {
|
||||
return {
|
||||
/**
|
||||
* Handles click/tap event on the thumbnail.
|
||||
*
|
||||
* @protected
|
||||
* @returns {void}
|
||||
*/
|
||||
_onClick() {
|
||||
const { participant, tileView } = ownProps;
|
||||
|
||||
if (tileView) {
|
||||
dispatch(toggleToolboxVisible());
|
||||
} else {
|
||||
dispatch(pinParticipant(participant.pinned ? null : participant.id));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles long press on the thumbnail.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onThumbnailLongPress() {
|
||||
const { participant } = ownProps;
|
||||
|
||||
if (participant.local) {
|
||||
dispatch(openDialog(ConnectionStatusComponent, {
|
||||
participantID: participant.id
|
||||
}));
|
||||
} else {
|
||||
dispatch(openDialog(RemoteVideoMenu, {
|
||||
participant
|
||||
}));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Function that maps parts of Redux state tree into component props.
|
||||
*
|
||||
|
@ -260,20 +224,23 @@ function _mapStateToProps(state, ownProps) {
|
|||
// the stage i.e. as a large video.
|
||||
const largeVideo = state['features/large-video'];
|
||||
const tracks = state['features/base/tracks'];
|
||||
const { participant } = ownProps;
|
||||
const id = participant.id;
|
||||
const { participantID } = ownProps;
|
||||
const participant = getParticipantByIdOrUndefined(state, participantID);
|
||||
const id = participant?.id;
|
||||
const audioTrack
|
||||
= getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.AUDIO, id);
|
||||
const videoTrack
|
||||
= getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, id);
|
||||
const participantCount = getParticipantCount(state);
|
||||
const renderDominantSpeakerIndicator = participant.dominantSpeaker && participantCount > 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);
|
||||
|
|
|
@ -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<Object>,
|
||||
_localParticipant: Object,
|
||||
|
||||
/**
|
||||
* The number of participants in the conference.
|
||||
*/
|
||||
_participantCount: number,
|
||||
|
||||
/**
|
||||
* An array with the IDs of the remote participants in the conference.
|
||||
*/
|
||||
_remoteParticipants: Array<string>,
|
||||
|
||||
/**
|
||||
* Application's viewport height.
|
||||
|
@ -131,7 +143,7 @@ class TileView extends Component<Props> {
|
|||
* @private
|
||||
*/
|
||||
_getColumnCount() {
|
||||
const participantCount = this.props._participants.length;
|
||||
const participantCount = this.props._participantCount;
|
||||
|
||||
// For narrow view, tiles should stack on top of each other for a lonely
|
||||
// call and a 1:1 call. Otherwise tiles should be grouped into rows of
|
||||
|
@ -155,18 +167,10 @@ class TileView extends Component<Props> {
|
|||
* @returns {Participant[]}
|
||||
*/
|
||||
_getSortedParticipants() {
|
||||
const participants = [];
|
||||
let localParticipant;
|
||||
const { _localParticipant, _remoteParticipants } = this.props;
|
||||
const participants = [ ..._remoteParticipants ];
|
||||
|
||||
for (const participant of this.props._participants) {
|
||||
if (participant.local) {
|
||||
localParticipant = participant;
|
||||
} else {
|
||||
participants.push(participant);
|
||||
}
|
||||
}
|
||||
|
||||
localParticipant && participants.push(localParticipant);
|
||||
_localParticipant && participants.push(_localParticipant.id);
|
||||
|
||||
return participants;
|
||||
}
|
||||
|
@ -178,16 +182,15 @@ class TileView extends Component<Props> {
|
|||
* @returns {Object}
|
||||
*/
|
||||
_getTileDimensions() {
|
||||
const { _height, _participants, _width } = this.props;
|
||||
const { _height, _participantCount, _width } = this.props;
|
||||
const columns = this._getColumnCount();
|
||||
const participantCount = _participants.length;
|
||||
const heightToUse = _height - (MARGIN * 2);
|
||||
const widthToUse = _width - (MARGIN * 2);
|
||||
let tileWidth;
|
||||
|
||||
// If there is going to be at least two rows, ensure that at least two
|
||||
// rows display fully on screen.
|
||||
if (participantCount / columns > 1) {
|
||||
if (_participantCount / columns > 1) {
|
||||
tileWidth = Math.min(widthToUse / columns, heightToUse / 2);
|
||||
} else {
|
||||
tileWidth = Math.min(widthToUse / columns, heightToUse);
|
||||
|
@ -247,11 +250,11 @@ class TileView extends Component<Props> {
|
|||
};
|
||||
|
||||
return this._getSortedParticipants()
|
||||
.map(participant => (
|
||||
.map(id => (
|
||||
<Thumbnail
|
||||
disableTint = { true }
|
||||
key = { participant.id }
|
||||
participant = { participant }
|
||||
key = { id }
|
||||
participantID = { id }
|
||||
renderDisplayName = { true }
|
||||
styleOverrides = { styleOverrides }
|
||||
tileView = { true } />));
|
||||
|
@ -285,11 +288,14 @@ class TileView extends Component<Props> {
|
|||
*/
|
||||
function _mapStateToProps(state) {
|
||||
const responsiveUi = state['features/base/responsive-ui'];
|
||||
const { remoteParticipants } = state['features/filmstrip'];
|
||||
|
||||
return {
|
||||
_aspectRatio: responsiveUi.aspectRatio,
|
||||
_height: responsiveUi.clientHeight,
|
||||
_participants: state['features/base/participants'],
|
||||
_localParticipant: getLocalParticipant(state),
|
||||
_participantCount: getParticipantCountWithFake(state),
|
||||
_remoteParticipants: remoteParticipants,
|
||||
_width: responsiveUi.clientWidth
|
||||
};
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import React, { Component } from 'react';
|
||||
|
||||
import { MEDIA_TYPE } from '../../../base/media';
|
||||
import { getLocalParticipant, getParticipantById, PARTICIPANT_ROLE } from '../../../base/participants';
|
||||
import { getParticipantByIdOrUndefined, PARTICIPANT_ROLE } from '../../../base/participants';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { getTrackByMediaTypeAndParticipant, isLocalTrackMuted, isRemoteTrackMuted } from '../../../base/tracks';
|
||||
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
|
||||
|
@ -111,7 +111,7 @@ function _mapStateToProps(state, ownProps) {
|
|||
const { participantID } = ownProps;
|
||||
|
||||
// Only the local participant won't have id for the time when the conference is not yet joined.
|
||||
const participant = participantID ? getParticipantById(state, participantID) : getLocalParticipant(state);
|
||||
const participant = getParticipantByIdOrUndefined(state, participantID);
|
||||
|
||||
const tracks = state['features/base/tracks'];
|
||||
let isVideoMuted = true;
|
||||
|
|
|
@ -9,8 +9,7 @@ import { Avatar } from '../../../base/avatar';
|
|||
import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
|
||||
import { MEDIA_TYPE, VideoTrack } from '../../../base/media';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
getParticipantById,
|
||||
getParticipantByIdOrUndefined,
|
||||
getParticipantCount,
|
||||
pinParticipant
|
||||
} from '../../../base/participants';
|
||||
|
@ -1012,9 +1011,8 @@ class Thumbnail extends Component<Props, State> {
|
|||
function _mapStateToProps(state, ownProps): Object {
|
||||
const { participantID } = ownProps;
|
||||
|
||||
// Only the local participant won't have id for the time when the conference is not yet joined.
|
||||
const participant = participantID ? getParticipantById(state, participantID) : getLocalParticipant(state);
|
||||
const { id } = participant;
|
||||
const participant = getParticipantByIdOrUndefined(state, participantID);
|
||||
const id = participant?.id;
|
||||
const isLocal = participant?.local ?? true;
|
||||
const tracks = state['features/base/tracks'];
|
||||
const { participantsVolume } = state['features/filmstrip'];
|
||||
|
@ -1085,14 +1083,14 @@ function _mapStateToProps(state, ownProps): Object {
|
|||
_isDominantSpeakerDisabled: interfaceConfig.DISABLE_DOMINANT_SPEAKER_INDICATOR,
|
||||
_isScreenSharing: _videoTrack?.videoType === 'desktop',
|
||||
_isTestModeEnabled: isTestModeEnabled(state),
|
||||
_isVideoPlayable: isVideoPlayable(state, id),
|
||||
_isVideoPlayable: id && isVideoPlayable(state, id),
|
||||
_indicatorIconSize: NORMAL,
|
||||
_localFlipX: Boolean(localFlipX),
|
||||
_participant: participant,
|
||||
_participantCountMoreThan2: getParticipantCount(state) > 2,
|
||||
_startSilent: Boolean(startSilent),
|
||||
_videoTrack,
|
||||
_volume: isLocal ? undefined : participantsVolume[id],
|
||||
_volume: isLocal ? undefined : id ? participantsVolume[id] : undefined,
|
||||
...size
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// @flow
|
||||
|
||||
import { getFeatureFlag, FILMSTRIP_ENABLED } from '../base/flags';
|
||||
import { getParticipantCountWithFake } from '../base/participants';
|
||||
import { toState } from '../base/redux';
|
||||
|
||||
/**
|
||||
|
@ -22,7 +23,5 @@ export function isFilmstripVisible(stateful: Object | Function) {
|
|||
return false;
|
||||
}
|
||||
|
||||
const { length: participantCount } = state['features/base/participants'];
|
||||
|
||||
return participantCount > 1;
|
||||
return getParticipantCountWithFake(state) > 1;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
// @flow
|
||||
|
||||
import { getParticipantCountWithFake } from '../base/participants';
|
||||
import { StateListenerRegistry, equals } from '../base/redux';
|
||||
import { clientResized } from '../base/responsive-ui';
|
||||
import { setFilmstripVisible } from '../filmstrip/actions';
|
||||
|
@ -19,7 +20,7 @@ import {
|
|||
* Listens for changes in the number of participants to calculate the dimensions of the tile view grid and the tiles.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ state => state['features/base/participants'].length,
|
||||
/* selector */ getParticipantCountWithFake,
|
||||
/* listener */ (numberOfParticipants, store) => {
|
||||
const state = store.getState();
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import type { Dispatch } from 'redux';
|
||||
|
||||
import { getInviteURL } from '../base/connection';
|
||||
import { getLocalParticipant, getParticipants } from '../base/participants';
|
||||
import { getLocalParticipant, getParticipantCount } from '../base/participants';
|
||||
import { inviteVideoRooms } from '../videosipgw';
|
||||
|
||||
import {
|
||||
|
@ -71,14 +71,14 @@ export function invite(
|
|||
dispatch: Dispatch<any>,
|
||||
getState: Function): Promise<Array<Object>> => {
|
||||
const state = getState();
|
||||
const participants = getParticipants(state);
|
||||
const participantsCount = getParticipantCount(state);
|
||||
const { calleeInfoVisible } = state['features/invite'];
|
||||
|
||||
if (showCalleeInfo
|
||||
&& !calleeInfoVisible
|
||||
&& invitees.length === 1
|
||||
&& invitees[0].type === INVITE_TYPES.USER
|
||||
&& participants.length === 1) {
|
||||
&& participantsCount === 1) {
|
||||
dispatch(setCalleeInfoVisible(true, invitees[0]));
|
||||
}
|
||||
|
||||
|
|
|
@ -5,9 +5,9 @@ import React, { Component } from 'react';
|
|||
import { Avatar } from '../../../base/avatar';
|
||||
import { MEDIA_TYPE } from '../../../base/media';
|
||||
import {
|
||||
getParticipants,
|
||||
getParticipantDisplayName,
|
||||
getParticipantPresenceStatus
|
||||
getParticipantPresenceStatus,
|
||||
getRemoteParticipants
|
||||
} from '../../../base/participants';
|
||||
import { Container, Text } from '../../../base/react';
|
||||
import { connect } from '../../../base/redux';
|
||||
|
@ -135,12 +135,11 @@ class CalleeInfo extends Component<Props> {
|
|||
function _mapStateToProps(state) {
|
||||
const _isVideoMuted
|
||||
= isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.VIDEO);
|
||||
const poltergeist
|
||||
= getParticipants(state).find(p => p.botType === 'poltergeist');
|
||||
|
||||
if (poltergeist) {
|
||||
const { id } = poltergeist;
|
||||
|
||||
// This would be expensive for big calls but the component will be mounted only when there are up
|
||||
// to 3 participants in the call.
|
||||
for (const [ id, p ] of getRemoteParticipants(state)) {
|
||||
if (p.botType === 'poltergeist') {
|
||||
return {
|
||||
_callee: {
|
||||
id,
|
||||
|
@ -150,6 +149,7 @@ function _mapStateToProps(state) {
|
|||
_isVideoMuted
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
_callee: state['features/invite'].initialCalleeInfo,
|
||||
|
|
|
@ -6,8 +6,9 @@ import {
|
|||
} from '../base/conference';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
getParticipantCount,
|
||||
getParticipantPresenceStatus,
|
||||
getParticipants,
|
||||
getRemoteParticipants,
|
||||
PARTICIPANT_JOINED,
|
||||
PARTICIPANT_JOINED_SOUND_ID,
|
||||
PARTICIPANT_LEFT,
|
||||
|
@ -167,13 +168,19 @@ function _maybeHideCalleeInfo(action, store) {
|
|||
if (!state['features/invite'].calleeInfoVisible) {
|
||||
return;
|
||||
}
|
||||
const participants = getParticipants(state);
|
||||
const numberOfPoltergeists
|
||||
= participants.filter(p => p.botType === 'poltergeist').length;
|
||||
const numberOfRealParticipants = participants.length - numberOfPoltergeists;
|
||||
const participants = getRemoteParticipants(state);
|
||||
const participantCount = getParticipantCount(state);
|
||||
let numberOfPoltergeists = 0;
|
||||
|
||||
participants.forEach(p => {
|
||||
if (p.botType === 'poltergeist') {
|
||||
numberOfPoltergeists++;
|
||||
}
|
||||
});
|
||||
const numberOfRealParticipants = participantCount - numberOfPoltergeists;
|
||||
|
||||
if ((numberOfPoltergeists > 1 || numberOfRealParticipants > 1)
|
||||
|| (action.type === PARTICIPANT_LEFT && participants.length === 1)) {
|
||||
|| (action.type === PARTICIPANT_LEFT && participantCount === 1)) {
|
||||
store.dispatch(setCalleeInfoVisible(false));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,12 @@
|
|||
import type { Dispatch } from 'redux';
|
||||
|
||||
import { MEDIA_TYPE } from '../base/media';
|
||||
import {
|
||||
getDominantSpeakerParticipant,
|
||||
getLocalParticipant,
|
||||
getPinnedParticipant,
|
||||
getRemoteParticipants
|
||||
} from '../base/participants';
|
||||
|
||||
import {
|
||||
SELECT_LARGE_VIDEO_PARTICIPANT,
|
||||
|
@ -92,8 +98,7 @@ function _electLastVisibleRemoteVideo(tracks) {
|
|||
function _electParticipantInLargeVideo(state) {
|
||||
// 1. If a participant is pinned, they will be shown in the LargeVideo
|
||||
// (regardless of whether they are local or remote).
|
||||
const participants = state['features/base/participants'];
|
||||
let participant = participants.find(p => p.pinned);
|
||||
let participant = getPinnedParticipant(state);
|
||||
|
||||
if (participant) {
|
||||
return participant.id;
|
||||
|
@ -107,11 +112,14 @@ function _electParticipantInLargeVideo(state) {
|
|||
}
|
||||
|
||||
// 3. Next, pick the dominant speaker (other than self).
|
||||
participant = participants.find(p => p.dominantSpeaker && !p.local);
|
||||
if (participant) {
|
||||
participant = getDominantSpeakerParticipant(state);
|
||||
if (participant && !participant.local) {
|
||||
return participant.id;
|
||||
}
|
||||
|
||||
// In case this is the local participant.
|
||||
participant = undefined;
|
||||
|
||||
// 4. Next, pick the most recent participant with video.
|
||||
const tracks = state['features/base/tracks'];
|
||||
const videoTrack = _electLastVisibleRemoteVideo(tracks);
|
||||
|
@ -122,6 +130,9 @@ function _electParticipantInLargeVideo(state) {
|
|||
|
||||
// 5. As a last resort, select the participant that joined last (other than poltergist or other bot type
|
||||
// participants).
|
||||
|
||||
const participants = [ ...getRemoteParticipants(state).values() ];
|
||||
|
||||
for (let i = participants.length; i > 0 && !participant; i--) {
|
||||
const p = participants[i - 1];
|
||||
|
||||
|
@ -131,5 +142,5 @@ function _electParticipantInLargeVideo(state) {
|
|||
return participant.id;
|
||||
}
|
||||
|
||||
return participants.find(p => p.local)?.id;
|
||||
return getLocalParticipant(state)?.id;
|
||||
}
|
||||
|
|
|
@ -28,7 +28,13 @@ import {
|
|||
import { JitsiConferenceEvents } from '../../base/lib-jitsi-meet';
|
||||
import { MEDIA_TYPE } from '../../base/media';
|
||||
import { SET_AUDIO_MUTED, SET_VIDEO_MUTED } from '../../base/media/actionTypes';
|
||||
import { PARTICIPANT_JOINED, PARTICIPANT_LEFT, getParticipants, getParticipantById } from '../../base/participants';
|
||||
import {
|
||||
PARTICIPANT_JOINED,
|
||||
PARTICIPANT_LEFT,
|
||||
getParticipantById,
|
||||
getRemoteParticipants,
|
||||
getLocalParticipant
|
||||
} from '../../base/participants';
|
||||
import { MiddlewareRegistry, StateListenerRegistry } from '../../base/redux';
|
||||
import { toggleScreensharing } from '../../base/tracks';
|
||||
import { OPEN_CHAT, CLOSE_CHAT } from '../../chat';
|
||||
|
@ -268,6 +274,24 @@ StateListenerRegistry.register(
|
|||
|
||||
}, 100));
|
||||
|
||||
/**
|
||||
* Returns a participant info object based on the passed participant object from redux.
|
||||
*
|
||||
* @param {Participant} participant - The participant object from the redux store.
|
||||
* @returns {Object} - The participant info object.
|
||||
*/
|
||||
function _participantToParticipantInfo(participant) {
|
||||
return {
|
||||
isLocal: participant.local,
|
||||
email: participant.email,
|
||||
name: participant.name,
|
||||
participantId: participant.id,
|
||||
displayName: participant.displayName,
|
||||
avatarUrl: participant.avatarURL,
|
||||
role: participant.role
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers for events sent from the native side via NativeEventEmitter.
|
||||
*
|
||||
|
@ -309,16 +333,15 @@ function _registerForNativeEvents(store) {
|
|||
|
||||
eventEmitter.addListener(ExternalAPI.RETRIEVE_PARTICIPANTS_INFO, ({ requestId }) => {
|
||||
|
||||
const participantsInfo = getParticipants(store).map(participant => {
|
||||
return {
|
||||
isLocal: participant.local,
|
||||
email: participant.email,
|
||||
name: participant.name,
|
||||
participantId: participant.id,
|
||||
displayName: participant.displayName,
|
||||
avatarUrl: participant.avatarURL,
|
||||
role: participant.role
|
||||
};
|
||||
const participantsInfo = [];
|
||||
const remoteParticipants = getRemoteParticipants(store);
|
||||
const localParticipant = getLocalParticipant(store);
|
||||
|
||||
participantsInfo.push(_participantToParticipantInfo(localParticipant));
|
||||
remoteParticipants.forEach(participant => {
|
||||
if (!participant.isFakeParticipant) {
|
||||
participantsInfo.push(_participantToParticipantInfo(participant));
|
||||
}
|
||||
});
|
||||
|
||||
sendEvent(
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
// @flow
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { approveParticipant } from '../../av-moderation/actions';
|
||||
|
@ -10,6 +9,11 @@ import { QuickActionButton } from './styled';
|
|||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The translated ask unmute text.
|
||||
*/
|
||||
askUnmuteText: string,
|
||||
|
||||
/**
|
||||
* Participant id.
|
||||
*/
|
||||
|
@ -22,10 +26,8 @@ type Props = {
|
|||
* @param {Object} participant - Participant reference.
|
||||
* @returns {React$Element<'button'>}
|
||||
*/
|
||||
export default function({ id }: Props) {
|
||||
export default function AskToUnmuteButton({ id, askUnmuteText }: Props) {
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const askToUnmute = useCallback(() => {
|
||||
dispatch(approveParticipant(id));
|
||||
}, [ dispatch, id ]);
|
||||
|
@ -37,7 +39,7 @@ export default function({ id }: Props) {
|
|||
theme = {{
|
||||
panePadding: 16
|
||||
}}>
|
||||
{t('participantsPane.actions.askUnmute')}
|
||||
{ askUnmuteText }
|
||||
</QuickActionButton>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -6,11 +6,17 @@ import { useTranslation } from 'react-i18next';
|
|||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { requestDisableModeration, requestEnableModeration } from '../../av-moderation/actions';
|
||||
import { isEnabled as isAvModerationEnabled } from '../../av-moderation/functions';
|
||||
import {
|
||||
isEnabled as isAvModerationEnabled,
|
||||
isSupported as isAvModerationSupported
|
||||
} from '../../av-moderation/functions';
|
||||
import { openDialog } from '../../base/dialog';
|
||||
import { Icon, IconCheck, IconVideoOff } from '../../base/icons';
|
||||
import { MEDIA_TYPE } from '../../base/media';
|
||||
import { getLocalParticipant } from '../../base/participants';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
isEveryoneModerator
|
||||
} from '../../base/participants';
|
||||
import { MuteEveryonesVideoDialog } from '../../video-menu/components';
|
||||
|
||||
import {
|
||||
|
@ -49,6 +55,8 @@ type Props = {
|
|||
|
||||
export const FooterContextMenu = ({ onMouseLeave }: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
const isModerationSupported = useSelector(isAvModerationSupported());
|
||||
const allModerators = useSelector(isEveryoneModerator);
|
||||
const isModerationEnabled = useSelector(isAvModerationEnabled(MEDIA_TYPE.AUDIO));
|
||||
const { id } = useSelector(getLocalParticipant);
|
||||
const { t } = useTranslation();
|
||||
|
@ -75,6 +83,8 @@ export const FooterContextMenu = ({ onMouseLeave }: Props) => {
|
|||
<span>{ t('participantsPane.actions.stopEveryonesVideo') }</span>
|
||||
</ContextMenuItem>
|
||||
|
||||
{ isModerationSupported && !allModerators ? (
|
||||
<>
|
||||
<div className = { classes.text }>
|
||||
{t('participantsPane.actions.allow')}
|
||||
</div>
|
||||
|
@ -96,6 +106,9 @@ export const FooterContextMenu = ({ onMouseLeave }: Props) => {
|
|||
<span>{ t('participantsPane.actions.startModeration') }</span>
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
</>
|
||||
) : undefined
|
||||
}
|
||||
</ContextMenu>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -7,7 +7,7 @@ import { useDispatch } from 'react-redux';
|
|||
import { approveKnockingParticipant, rejectKnockingParticipant } from '../../lobby/actions';
|
||||
import { ACTION_TRIGGER, MEDIA_STATE } from '../constants';
|
||||
|
||||
import { ParticipantItem } from './ParticipantItem';
|
||||
import ParticipantItem from './ParticipantItem';
|
||||
import { ParticipantActionButton } from './styled';
|
||||
|
||||
type Props = {
|
||||
|
@ -28,9 +28,12 @@ export const LobbyParticipantItem = ({ participant: p }: Props) => {
|
|||
<ParticipantItem
|
||||
actionsTrigger = { ACTION_TRIGGER.PERMANENT }
|
||||
audioMediaState = { MEDIA_STATE.NONE }
|
||||
name = { p.name }
|
||||
participant = { p }
|
||||
videoMuteState = { MEDIA_STATE.NONE }>
|
||||
displayName = { p.name }
|
||||
local = { p.local }
|
||||
participantID = { p.id }
|
||||
raisedHand = { p.raisedHand }
|
||||
videoMuteState = { MEDIA_STATE.NONE }
|
||||
youText = { t('chat.you') }>
|
||||
<ParticipantActionButton
|
||||
onClick = { reject }>
|
||||
{t('lobby.reject')}
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
// @flow
|
||||
|
||||
import React, { useCallback, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { isToolbarButtonEnabled } from '../../base/config/functions.web';
|
||||
import { openDialog } from '../../base/dialog';
|
||||
import { translate } from '../../base/i18n';
|
||||
import {
|
||||
IconCloseCircle,
|
||||
IconCrown,
|
||||
|
@ -14,8 +13,13 @@ import {
|
|||
IconMuteEveryoneElse,
|
||||
IconVideoOff
|
||||
} from '../../base/icons';
|
||||
import { isLocalParticipantModerator, isParticipantModerator } from '../../base/participants';
|
||||
import { getIsParticipantAudioMuted, getIsParticipantVideoMuted } from '../../base/tracks';
|
||||
import {
|
||||
getParticipantByIdOrUndefined,
|
||||
isLocalParticipantModerator,
|
||||
isParticipantModerator
|
||||
} from '../../base/participants';
|
||||
import { connect } from '../../base/redux';
|
||||
import { isParticipantAudioMuted, isParticipantVideoMuted } from '../../base/tracks';
|
||||
import { openChat } from '../../chat/actions';
|
||||
import { GrantModeratorDialog, KickRemoteParticipantDialog, MuteEveryoneDialog } from '../../video-menu';
|
||||
import MuteRemoteParticipantsVideoDialog from '../../video-menu/components/web/MuteRemoteParticipantsVideoDialog';
|
||||
|
@ -31,6 +35,41 @@ import {
|
|||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* True if the local participant is moderator and false otherwise.
|
||||
*/
|
||||
_isLocalModerator: boolean,
|
||||
|
||||
/**
|
||||
* True if the chat button is enabled and false otherwise.
|
||||
*/
|
||||
_isChatButtonEnabled: boolean,
|
||||
|
||||
/**
|
||||
* True if the participant is moderator and false otherwise.
|
||||
*/
|
||||
_isParticipantModerator: boolean,
|
||||
|
||||
/**
|
||||
* True if the participant is video muted and false otherwise.
|
||||
*/
|
||||
_isParticipantVideoMuted: boolean,
|
||||
|
||||
/**
|
||||
* True if the participant is audio muted and false otherwise.
|
||||
*/
|
||||
_isParticipantAudioMuted: boolean,
|
||||
|
||||
/**
|
||||
* Participant reference
|
||||
*/
|
||||
_participant: Object,
|
||||
|
||||
/**
|
||||
* The dispatch function from redux.
|
||||
*/
|
||||
dispatch: Function,
|
||||
|
||||
/**
|
||||
* Callback used to open a confirmation dialog for audio muting.
|
||||
*/
|
||||
|
@ -57,35 +96,145 @@ type Props = {
|
|||
onSelect: Function,
|
||||
|
||||
/**
|
||||
* Participant reference
|
||||
* The ID of the participant.
|
||||
*/
|
||||
participant: Object
|
||||
participantID: string,
|
||||
|
||||
/**
|
||||
* The translate function.
|
||||
*/
|
||||
t: Function
|
||||
};
|
||||
|
||||
export const MeetingParticipantContextMenu = ({
|
||||
offsetTarget,
|
||||
onEnter,
|
||||
onLeave,
|
||||
onSelect,
|
||||
muteAudio,
|
||||
participant
|
||||
}: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
const containerRef = useRef(null);
|
||||
const isLocalModerator = useSelector(isLocalParticipantModerator);
|
||||
const isChatButtonEnabled = useSelector(isToolbarButtonEnabled('chat'));
|
||||
const isParticipantVideoMuted = useSelector(getIsParticipantVideoMuted(participant));
|
||||
const isParticipantAudioMuted = useSelector(getIsParticipantAudioMuted(participant));
|
||||
const [ isHidden, setIsHidden ] = useState(true);
|
||||
const { t } = useTranslation();
|
||||
type State = {
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (participant
|
||||
&& containerRef.current
|
||||
/**
|
||||
* If true the context menu will be hidden.
|
||||
*/
|
||||
isHidden: boolean
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements the MeetingParticipantContextMenu component.
|
||||
*/
|
||||
class MeetingParticipantContextMenu extends Component<Props, State> {
|
||||
|
||||
/**
|
||||
* Reference to the context menu container div.
|
||||
*/
|
||||
_containerRef: Object;
|
||||
|
||||
/**
|
||||
* Creates new instance of MeetingParticipantContextMenu.
|
||||
*
|
||||
* @param {Props} props - The props.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isHidden: true
|
||||
};
|
||||
|
||||
this._containerRef = React.createRef();
|
||||
|
||||
this._onGrantModerator = this._onGrantModerator.bind(this);
|
||||
this._onKick = this._onKick.bind(this);
|
||||
this._onMuteEveryoneElse = this._onMuteEveryoneElse.bind(this);
|
||||
this._onMuteVideo = this._onMuteVideo.bind(this);
|
||||
this._onSendPrivateMessage = this._onSendPrivateMessage.bind(this);
|
||||
this._position = this._position.bind(this);
|
||||
}
|
||||
|
||||
_onGrantModerator: () => void;
|
||||
|
||||
/**
|
||||
* Grant moderator permissions.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onGrantModerator() {
|
||||
const { _participant, dispatch } = this.props;
|
||||
|
||||
dispatch(openDialog(GrantModeratorDialog, {
|
||||
participantID: _participant?.id
|
||||
}));
|
||||
}
|
||||
|
||||
_onKick: () => void;
|
||||
|
||||
/**
|
||||
* Kicks the participant.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onKick() {
|
||||
const { _participant, dispatch } = this.props;
|
||||
|
||||
dispatch(openDialog(KickRemoteParticipantDialog, {
|
||||
participantID: _participant?.id
|
||||
}));
|
||||
}
|
||||
|
||||
_onMuteEveryoneElse: () => void;
|
||||
|
||||
/**
|
||||
* Mutes everyone else.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onMuteEveryoneElse() {
|
||||
const { _participant, dispatch } = this.props;
|
||||
|
||||
dispatch(openDialog(MuteEveryoneDialog, {
|
||||
exclude: [ _participant?.id ]
|
||||
}));
|
||||
}
|
||||
|
||||
_onMuteVideo: () => void;
|
||||
|
||||
/**
|
||||
* Mutes the video of the selected participant.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onMuteVideo() {
|
||||
const { _participant, dispatch } = this.props;
|
||||
|
||||
dispatch(openDialog(MuteRemoteParticipantsVideoDialog, {
|
||||
participantID: _participant?.id
|
||||
}));
|
||||
}
|
||||
|
||||
_onSendPrivateMessage: () => void;
|
||||
|
||||
/**
|
||||
* Sends private message.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onSendPrivateMessage() {
|
||||
const { _participant, dispatch } = this.props;
|
||||
|
||||
dispatch(openChat(_participant));
|
||||
}
|
||||
|
||||
_position: () => void;
|
||||
|
||||
/**
|
||||
* Positions the context menu.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_position() {
|
||||
const { _participant, offsetTarget } = this.props;
|
||||
|
||||
if (_participant
|
||||
&& this._containerRef.current
|
||||
&& offsetTarget?.offsetParent
|
||||
&& offsetTarget.offsetParent instanceof HTMLElement
|
||||
) {
|
||||
const { current: container } = containerRef;
|
||||
const { current: container } = this._containerRef;
|
||||
const { offsetTop, offsetParent: { offsetHeight, scrollTop } } = offsetTarget;
|
||||
const outerHeight = getComputedOuterHeight(container);
|
||||
|
||||
|
@ -93,97 +242,158 @@ export const MeetingParticipantContextMenu = ({
|
|||
? offsetTop - outerHeight
|
||||
: offsetTop;
|
||||
|
||||
setIsHidden(false);
|
||||
this.setState({ isHidden: false });
|
||||
} else {
|
||||
setIsHidden(true);
|
||||
this.setState({ isHidden: true });
|
||||
}
|
||||
}
|
||||
}, [ participant, offsetTarget ]);
|
||||
|
||||
const grantModerator = useCallback(() => {
|
||||
dispatch(openDialog(GrantModeratorDialog, {
|
||||
participantID: participant.id
|
||||
}));
|
||||
}, [ dispatch, participant ]);
|
||||
/**
|
||||
* Implements React Component's componentDidMount.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
componentDidMount() {
|
||||
this._position();
|
||||
}
|
||||
|
||||
const kick = useCallback(() => {
|
||||
dispatch(openDialog(KickRemoteParticipantDialog, {
|
||||
participantID: participant.id
|
||||
}));
|
||||
}, [ dispatch, participant ]);
|
||||
/**
|
||||
* Implements React Component's componentDidUpdate.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
if (prevProps.offsetTarget !== this.props.offsetTarget || prevProps._participant !== this.props._participant) {
|
||||
this._position();
|
||||
}
|
||||
}
|
||||
|
||||
const muteEveryoneElse = useCallback(() => {
|
||||
dispatch(openDialog(MuteEveryoneDialog, {
|
||||
exclude: [ participant.id ]
|
||||
}));
|
||||
}, [ dispatch, participant ]);
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
_isLocalModerator,
|
||||
_isChatButtonEnabled,
|
||||
_isParticipantModerator,
|
||||
_isParticipantVideoMuted,
|
||||
_isParticipantAudioMuted,
|
||||
_participant,
|
||||
onEnter,
|
||||
onLeave,
|
||||
onSelect,
|
||||
muteAudio,
|
||||
t
|
||||
} = this.props;
|
||||
|
||||
const muteVideo = useCallback(() => {
|
||||
dispatch(openDialog(MuteRemoteParticipantsVideoDialog, {
|
||||
participantID: participant.id
|
||||
}));
|
||||
}, [ dispatch, participant ]);
|
||||
|
||||
const sendPrivateMessage = useCallback(() => {
|
||||
dispatch(openChat(participant));
|
||||
}, [ dispatch, participant ]);
|
||||
|
||||
if (!participant) {
|
||||
if (!_participant) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ContextMenu
|
||||
className = { ignoredChildClassName }
|
||||
innerRef = { containerRef }
|
||||
isHidden = { isHidden }
|
||||
innerRef = { this._containerRef }
|
||||
isHidden = { this.state.isHidden }
|
||||
onClick = { onSelect }
|
||||
onMouseEnter = { onEnter }
|
||||
onMouseLeave = { onLeave }>
|
||||
<ContextMenuItemGroup>
|
||||
{isLocalModerator && (
|
||||
{
|
||||
_isLocalModerator && (
|
||||
<>
|
||||
{!isParticipantAudioMuted
|
||||
&& <ContextMenuItem onClick = { muteAudio(participant) }>
|
||||
{
|
||||
!_isParticipantAudioMuted
|
||||
&& <ContextMenuItem onClick = { muteAudio(_participant) }>
|
||||
<ContextMenuIcon src = { IconMicDisabled } />
|
||||
<span>{t('dialog.muteParticipantButton')}</span>
|
||||
</ContextMenuItem>}
|
||||
</ContextMenuItem>
|
||||
}
|
||||
|
||||
<ContextMenuItem onClick = { muteEveryoneElse }>
|
||||
<ContextMenuItem onClick = { this._onMuteEveryoneElse }>
|
||||
<ContextMenuIcon src = { IconMuteEveryoneElse } />
|
||||
<span>{t('toolbar.accessibilityLabel.muteEveryoneElse')}</span>
|
||||
</ContextMenuItem>
|
||||
</>
|
||||
)}
|
||||
)
|
||||
}
|
||||
|
||||
{isLocalModerator && (isParticipantVideoMuted || (
|
||||
<ContextMenuItem onClick = { muteVideo }>
|
||||
{
|
||||
_isLocalModerator && (
|
||||
_isParticipantVideoMuted || (
|
||||
<ContextMenuItem onClick = { this._onMuteVideo }>
|
||||
<ContextMenuIcon src = { IconVideoOff } />
|
||||
<span>{t('participantsPane.actions.stopVideo')}</span>
|
||||
</ContextMenuItem>
|
||||
))}
|
||||
)
|
||||
)
|
||||
}
|
||||
</ContextMenuItemGroup>
|
||||
|
||||
<ContextMenuItemGroup>
|
||||
{isLocalModerator && (
|
||||
{
|
||||
_isLocalModerator && (
|
||||
<>
|
||||
{!isParticipantModerator(participant)
|
||||
&& <ContextMenuItem onClick = { grantModerator }>
|
||||
{
|
||||
!_isParticipantModerator && (
|
||||
<ContextMenuItem onClick = { this._onGrantModerator }>
|
||||
<ContextMenuIcon src = { IconCrown } />
|
||||
<span>{t('toolbar.accessibilityLabel.grantModerator')}</span>
|
||||
</ContextMenuItem>}
|
||||
<ContextMenuItem onClick = { kick }>
|
||||
</ContextMenuItem>
|
||||
)
|
||||
}
|
||||
<ContextMenuItem onClick = { this._onKick }>
|
||||
<ContextMenuIcon src = { IconCloseCircle } />
|
||||
<span>{t('videothumbnail.kick')}</span>
|
||||
<span>{ t('videothumbnail.kick') }</span>
|
||||
</ContextMenuItem>
|
||||
</>
|
||||
)}
|
||||
{isChatButtonEnabled && (
|
||||
<ContextMenuItem onClick = { sendPrivateMessage }>
|
||||
)
|
||||
}
|
||||
{
|
||||
_isChatButtonEnabled && (
|
||||
<ContextMenuItem onClick = { this._onSendPrivateMessage }>
|
||||
<ContextMenuIcon src = { IconMessage } />
|
||||
<span>{t('toolbar.accessibilityLabel.privateMessage')}</span>
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
)
|
||||
}
|
||||
</ContextMenuItemGroup>
|
||||
</ContextMenu>
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the associated props for this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @param {Object} ownProps - The own props of the component.
|
||||
* @private
|
||||
* @returns {Props}
|
||||
*/
|
||||
function _mapStateToProps(state, ownProps): Object {
|
||||
const { participantID } = ownProps;
|
||||
|
||||
const participant = getParticipantByIdOrUndefined(state, participantID);
|
||||
|
||||
const _isLocalModerator = isLocalParticipantModerator(state);
|
||||
const _isChatButtonEnabled = isToolbarButtonEnabled('chat', state);
|
||||
const _isParticipantVideoMuted = isParticipantVideoMuted(participant, state);
|
||||
const _isParticipantAudioMuted = isParticipantAudioMuted(participant, state);
|
||||
const _isParticipantModerator = isParticipantModerator(participant);
|
||||
|
||||
return {
|
||||
_isLocalModerator,
|
||||
_isChatButtonEnabled,
|
||||
_isParticipantModerator,
|
||||
_isParticipantVideoMuted,
|
||||
_isParticipantAudioMuted,
|
||||
_participant: participant
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(MeetingParticipantContextMenu));
|
||||
|
|
|
@ -1,19 +1,62 @@
|
|||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { getIsParticipantAudioMuted, getIsParticipantVideoMuted } from '../../base/tracks';
|
||||
import { ACTION_TRIGGER, MEDIA_STATE } from '../constants';
|
||||
import { getParticipantAudioMediaState } from '../functions';
|
||||
import { getParticipantByIdOrUndefined, getParticipantDisplayName } from '../../base/participants';
|
||||
import { connect } from '../../base/redux';
|
||||
import { isParticipantAudioMuted, isParticipantVideoMuted } from '../../base/tracks';
|
||||
import { ACTION_TRIGGER, MEDIA_STATE, type MediaState } from '../constants';
|
||||
import { getParticipantAudioMediaState, getQuickActionButtonType } from '../functions';
|
||||
|
||||
import { ParticipantItem } from './ParticipantItem';
|
||||
import ParticipantItem from './ParticipantItem';
|
||||
import ParticipantQuickAction from './ParticipantQuickAction';
|
||||
import { ParticipantActionEllipsis } from './styled';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Media state for audio.
|
||||
*/
|
||||
_audioMediaState: MediaState,
|
||||
|
||||
/**
|
||||
* The display name of the participant.
|
||||
*/
|
||||
_displayName: string,
|
||||
|
||||
/**
|
||||
* True if the participant is video muted.
|
||||
*/
|
||||
_isVideoMuted: boolean,
|
||||
|
||||
/**
|
||||
* True if the participant is the local participant.
|
||||
*/
|
||||
_local: boolean,
|
||||
|
||||
/**
|
||||
* The participant ID.
|
||||
*
|
||||
* NOTE: This ID may be different from participantID prop in the case when we pass undefined for the local
|
||||
* participant. In this case the local participant ID will be filled trough _participantID prop.
|
||||
*/
|
||||
_participantID: string,
|
||||
|
||||
/**
|
||||
* The type of button to be rendered for the quick action.
|
||||
*/
|
||||
_quickActionButtonType: string,
|
||||
|
||||
/**
|
||||
* True if the participant have raised hand.
|
||||
*/
|
||||
_raisedHand: boolean,
|
||||
|
||||
/**
|
||||
* The translated ask unmute text for the qiuck action buttons.
|
||||
*/
|
||||
askUnmuteText: string,
|
||||
|
||||
/**
|
||||
* Is this item highlighted
|
||||
*/
|
||||
|
@ -24,6 +67,11 @@ type Props = {
|
|||
*/
|
||||
muteAudio: Function,
|
||||
|
||||
/**
|
||||
* The translated text for the mute participant button.
|
||||
*/
|
||||
muteParticipantButtonText: string,
|
||||
|
||||
/**
|
||||
* Callback for the activation of this item's context menu
|
||||
*/
|
||||
|
@ -35,38 +83,97 @@ type Props = {
|
|||
onLeave: Function,
|
||||
|
||||
/**
|
||||
* Participant reference
|
||||
* The aria-label for the ellipsis action.
|
||||
*/
|
||||
participant: Object
|
||||
participantActionEllipsisLabel: string,
|
||||
|
||||
/**
|
||||
* The ID of the participant.
|
||||
*/
|
||||
participantID: ?string,
|
||||
|
||||
/**
|
||||
* The translated "you" text.
|
||||
*/
|
||||
youText: string
|
||||
};
|
||||
|
||||
export const MeetingParticipantItem = ({
|
||||
/**
|
||||
* Implements the MeetingParticipantItem component.
|
||||
*
|
||||
* @param {Props} props - The props of the component.
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
function MeetingParticipantItem({
|
||||
_audioMediaState,
|
||||
_displayName,
|
||||
_isVideoMuted,
|
||||
_local,
|
||||
_participantID,
|
||||
_quickActionButtonType,
|
||||
_raisedHand,
|
||||
askUnmuteText,
|
||||
isHighlighted,
|
||||
onContextMenu,
|
||||
onLeave,
|
||||
muteAudio,
|
||||
participant
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const isAudioMuted = useSelector(getIsParticipantAudioMuted(participant));
|
||||
const isVideoMuted = useSelector(getIsParticipantVideoMuted(participant));
|
||||
const audioMediaState = useSelector(getParticipantAudioMediaState(participant, isAudioMuted));
|
||||
|
||||
muteParticipantButtonText,
|
||||
participantActionEllipsisLabel,
|
||||
youText
|
||||
}: Props) {
|
||||
return (
|
||||
<ParticipantItem
|
||||
actionsTrigger = { ACTION_TRIGGER.HOVER }
|
||||
audioMediaState = { audioMediaState }
|
||||
audioMediaState = { _audioMediaState }
|
||||
displayName = { _displayName }
|
||||
isHighlighted = { isHighlighted }
|
||||
local = { _local }
|
||||
onLeave = { onLeave }
|
||||
participant = { participant }
|
||||
videoMuteState = { isVideoMuted ? MEDIA_STATE.MUTED : MEDIA_STATE.UNMUTED }>
|
||||
participantID = { _participantID }
|
||||
raisedHand = { _raisedHand }
|
||||
videoMuteState = { _isVideoMuted ? MEDIA_STATE.MUTED : MEDIA_STATE.UNMUTED }
|
||||
youText = { youText }>
|
||||
<ParticipantQuickAction
|
||||
isAudioMuted = { isAudioMuted }
|
||||
askUnmuteText = { askUnmuteText }
|
||||
buttonType = { _quickActionButtonType }
|
||||
muteAudio = { muteAudio }
|
||||
participant = { participant } />
|
||||
muteParticipantButtonText = { muteParticipantButtonText }
|
||||
participantID = { _participantID } />
|
||||
<ParticipantActionEllipsis
|
||||
aria-label = { t('MeetingParticipantItem.ParticipantActionEllipsis.options') }
|
||||
aria-label = { participantActionEllipsisLabel }
|
||||
onClick = { onContextMenu } />
|
||||
</ParticipantItem>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the associated props for this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @param {Object} ownProps - The own props of the component.
|
||||
* @private
|
||||
* @returns {Props}
|
||||
*/
|
||||
function _mapStateToProps(state, ownProps): Object {
|
||||
const { participantID } = ownProps;
|
||||
|
||||
const participant = getParticipantByIdOrUndefined(state, participantID);
|
||||
|
||||
const _isAudioMuted = isParticipantAudioMuted(participant, state);
|
||||
const _isVideoMuted = isParticipantVideoMuted(participant, state);
|
||||
const _audioMediaState = getParticipantAudioMediaState(participant, _isAudioMuted, state);
|
||||
const _quickActionButtonType = getQuickActionButtonType(participant, _isAudioMuted, state);
|
||||
|
||||
return {
|
||||
_audioMediaState,
|
||||
_displayName: getParticipantDisplayName(state, participant?.id),
|
||||
_isAudioMuted,
|
||||
_isVideoMuted,
|
||||
_local: Boolean(participant?.local),
|
||||
_participantID: participant?.id,
|
||||
_quickActionButtonType,
|
||||
_raisedHand: Boolean(participant?.raisedHand)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(MeetingParticipantItem);
|
||||
|
|
|
@ -1,18 +1,21 @@
|
|||
// @flow
|
||||
|
||||
import _ from 'lodash';
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
|
||||
import { openDialog } from '../../base/dialog';
|
||||
import { getParticipants } from '../../base/participants';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
getParticipantCountWithFake,
|
||||
getRemoteParticipants
|
||||
} from '../../base/participants';
|
||||
import MuteRemoteParticipantDialog from '../../video-menu/components/web/MuteRemoteParticipantDialog';
|
||||
import { findStyledAncestor, shouldRenderInviteButton } from '../functions';
|
||||
|
||||
import { InviteButton } from './InviteButton';
|
||||
import { MeetingParticipantContextMenu } from './MeetingParticipantContextMenu';
|
||||
import { MeetingParticipantItem } from './MeetingParticipantItem';
|
||||
import MeetingParticipantContextMenu from './MeetingParticipantContextMenu';
|
||||
import MeetingParticipantItem from './MeetingParticipantItem';
|
||||
import { Heading, ParticipantContainer } from './styled';
|
||||
|
||||
type NullProto = {
|
||||
|
@ -20,7 +23,7 @@ type NullProto = {
|
|||
__proto__: null
|
||||
};
|
||||
|
||||
type RaiseContext = NullProto | {
|
||||
type RaiseContext = NullProto | {|
|
||||
|
||||
/**
|
||||
* Target elements against which positioning calculations are made
|
||||
|
@ -28,17 +31,28 @@ type RaiseContext = NullProto | {
|
|||
offsetTarget?: HTMLElement,
|
||||
|
||||
/**
|
||||
* Participant reference
|
||||
* The ID of the participant.
|
||||
*/
|
||||
participant?: Object,
|
||||
};
|
||||
participantID?: String,
|
||||
|};
|
||||
|
||||
const initialState = Object.freeze(Object.create(null));
|
||||
|
||||
export const MeetingParticipantList = () => {
|
||||
/**
|
||||
* Renders the MeetingParticipantList component.
|
||||
*
|
||||
* @returns {ReactNode} - The component.
|
||||
*/
|
||||
export function MeetingParticipantList() {
|
||||
const dispatch = useDispatch();
|
||||
const isMouseOverMenu = useRef(false);
|
||||
const participants = useSelector(getParticipants, _.isEqual);
|
||||
const participants = useSelector(getRemoteParticipants);
|
||||
const localParticipant = useSelector(getLocalParticipant);
|
||||
|
||||
// This is very important as getRemoteParticipants is not changing its reference object
|
||||
// and we will not re-render on change, but if count changes we will do
|
||||
const participantsCount = useSelector(getParticipantCountWithFake);
|
||||
|
||||
const showInviteButton = useSelector(shouldRenderInviteButton);
|
||||
const [ raiseContext, setRaiseContext ] = useState<RaiseContext>(initialState);
|
||||
const { t } = useTranslation();
|
||||
|
@ -61,20 +75,20 @@ export const MeetingParticipantList = () => {
|
|||
});
|
||||
}, [ raiseContext ]);
|
||||
|
||||
const raiseMenu = useCallback((participant, target) => {
|
||||
const raiseMenu = useCallback((participantID, target) => {
|
||||
setRaiseContext({
|
||||
participant,
|
||||
participantID,
|
||||
offsetTarget: findStyledAncestor(target, ParticipantContainer)
|
||||
});
|
||||
}, [ raiseContext ]);
|
||||
|
||||
const toggleMenu = useCallback(participant => e => {
|
||||
const { participant: raisedParticipant } = raiseContext;
|
||||
const toggleMenu = useCallback(participantID => e => {
|
||||
const { participantID: raisedParticipant } = raiseContext;
|
||||
|
||||
if (raisedParticipant && raisedParticipant === participant) {
|
||||
if (raisedParticipant && raisedParticipant === participantID) {
|
||||
lowerMenu();
|
||||
} else {
|
||||
raiseMenu(participant, e.target);
|
||||
raiseMenu(participantID, e.target);
|
||||
}
|
||||
}, [ raiseContext ]);
|
||||
|
||||
|
@ -91,20 +105,44 @@ export const MeetingParticipantList = () => {
|
|||
dispatch(openDialog(MuteRemoteParticipantDialog, { participantID: id }));
|
||||
});
|
||||
|
||||
// FIXME:
|
||||
// It seems that useTranslation is not very scallable. Unmount 500 components that have the useTranslation hook is
|
||||
// taking more than 10s. To workaround the issue we need to pass the texts as props. This is temporary and dirty
|
||||
// solution!!!
|
||||
// One potential proper fix would be to use react-window component in order to lower the number of components
|
||||
// mounted.
|
||||
const participantActionEllipsisLabel = t('MeetingParticipantItem.ParticipantActionEllipsis.options');
|
||||
const youText = t('chat.you');
|
||||
const askUnmuteText = t('participantsPane.actions.askUnmute');
|
||||
const muteParticipantButtonText = t('dialog.muteParticipantButton');
|
||||
|
||||
const renderParticipant = id => (
|
||||
<MeetingParticipantItem
|
||||
askUnmuteText = { askUnmuteText }
|
||||
isHighlighted = { raiseContext.participantID === id }
|
||||
key = { id }
|
||||
muteAudio = { muteAudio }
|
||||
muteParticipantButtonText = { muteParticipantButtonText }
|
||||
onContextMenu = { toggleMenu(id) }
|
||||
onLeave = { lowerMenu }
|
||||
participantActionEllipsisLabel = { participantActionEllipsisLabel }
|
||||
participantID = { id }
|
||||
youText = { youText } />
|
||||
);
|
||||
|
||||
const items = [];
|
||||
|
||||
localParticipant && items.push(renderParticipant(localParticipant?.id));
|
||||
participants.forEach(p => {
|
||||
items.push(renderParticipant(p?.id));
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Heading>{t('participantsPane.headings.participantsList', { count: participants.length })}</Heading>
|
||||
<Heading>{t('participantsPane.headings.participantsList', { count: participantsCount })}</Heading>
|
||||
{showInviteButton && <InviteButton />}
|
||||
<div>
|
||||
{participants.map(p => (
|
||||
<MeetingParticipantItem
|
||||
isHighlighted = { raiseContext.participant === p }
|
||||
key = { p.id }
|
||||
muteAudio = { muteAudio }
|
||||
onContextMenu = { toggleMenu(p) }
|
||||
onLeave = { lowerMenu }
|
||||
participant = { p } />
|
||||
))}
|
||||
{ items }
|
||||
</div>
|
||||
<MeetingParticipantContextMenu
|
||||
muteAudio = { muteAudio }
|
||||
|
@ -114,4 +152,4 @@ export const MeetingParticipantList = () => {
|
|||
{ ...raiseContext } />
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
// @flow
|
||||
|
||||
import React, { type Node } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { Avatar } from '../../base/avatar';
|
||||
import {
|
||||
|
@ -12,7 +10,6 @@ import {
|
|||
IconMicrophoneEmpty,
|
||||
IconMicrophoneEmptySlash
|
||||
} from '../../base/icons';
|
||||
import { getParticipantDisplayNameWithId } from '../../base/participants';
|
||||
import { ACTION_TRIGGER, MEDIA_STATE, type ActionTrigger, type MediaState } from '../constants';
|
||||
|
||||
import { RaisedHandIndicator } from './RaisedHandIndicator';
|
||||
|
@ -100,15 +97,20 @@ type Props = {
|
|||
*/
|
||||
children: Node,
|
||||
|
||||
/**
|
||||
* The name of the participant. Used for showing lobby names.
|
||||
*/
|
||||
displayName: string,
|
||||
|
||||
/**
|
||||
* Is this item highlighted/raised
|
||||
*/
|
||||
isHighlighted?: boolean,
|
||||
|
||||
/**
|
||||
* The name of the participant. Used for showing lobby names.
|
||||
* True if the participant is local.
|
||||
*/
|
||||
name?: string,
|
||||
local: boolean,
|
||||
|
||||
/**
|
||||
* Callback for when the mouse leaves this component
|
||||
|
@ -116,29 +118,46 @@ type Props = {
|
|||
onLeave?: Function,
|
||||
|
||||
/**
|
||||
* Participant reference
|
||||
* The ID of the participant.
|
||||
*/
|
||||
participant: Object,
|
||||
participantID: string,
|
||||
|
||||
/**
|
||||
* True if the participant have raised hand.
|
||||
*/
|
||||
raisedHand: boolean,
|
||||
|
||||
/**
|
||||
* Media state for video
|
||||
*/
|
||||
videoMuteState: MediaState
|
||||
videoMuteState: MediaState,
|
||||
|
||||
/**
|
||||
* The translated "you" text.
|
||||
*/
|
||||
youText: string
|
||||
}
|
||||
|
||||
export const ParticipantItem = ({
|
||||
/**
|
||||
* A component representing a participant entry in ParticipantPane and Lobby.
|
||||
*
|
||||
* @param {Props} props - The props of the component.
|
||||
* @returns {ReactNode}
|
||||
*/
|
||||
export default function ParticipantItem({
|
||||
children,
|
||||
isHighlighted,
|
||||
onLeave,
|
||||
actionsTrigger = ACTION_TRIGGER.HOVER,
|
||||
audioMediaState = MEDIA_STATE.NONE,
|
||||
videoMuteState = MEDIA_STATE.NONE,
|
||||
name,
|
||||
participant: p
|
||||
}: Props) => {
|
||||
displayName,
|
||||
participantID,
|
||||
local,
|
||||
raisedHand,
|
||||
youText
|
||||
}: Props) {
|
||||
const ParticipantActions = Actions[actionsTrigger];
|
||||
const { t } = useTranslation();
|
||||
const displayName = name || useSelector(getParticipantDisplayNameWithId(p.id));
|
||||
|
||||
return (
|
||||
<ParticipantContainer
|
||||
|
@ -147,22 +166,22 @@ export const ParticipantItem = ({
|
|||
trigger = { actionsTrigger }>
|
||||
<Avatar
|
||||
className = 'participant-avatar'
|
||||
participantId = { p.id }
|
||||
participantId = { participantID }
|
||||
size = { 32 } />
|
||||
<ParticipantContent>
|
||||
<ParticipantNameContainer>
|
||||
<ParticipantName>
|
||||
{ displayName }
|
||||
</ParticipantName>
|
||||
{ p.local ? <span> ({t('chat.you')})</span> : null }
|
||||
{ local ? <span> ({ youText })</span> : null }
|
||||
</ParticipantNameContainer>
|
||||
{ !p.local && <ParticipantActions children = { children } /> }
|
||||
{ !local && <ParticipantActions children = { children } /> }
|
||||
<ParticipantStates>
|
||||
{p.raisedHand && <RaisedHandIndicator />}
|
||||
{VideoStateIcons[videoMuteState]}
|
||||
{AudioStateIcons[audioMediaState]}
|
||||
{ raisedHand && <RaisedHandIndicator /> }
|
||||
{ VideoStateIcons[videoMuteState] }
|
||||
{ AudioStateIcons[audioMediaState] }
|
||||
</ParticipantStates>
|
||||
</ParticipantContent>
|
||||
</ParticipantContainer>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { QUICK_ACTION_BUTTON } from '../constants';
|
||||
import { getQuickActionButtonType } from '../functions';
|
||||
|
||||
import AskToUnmuteButton from './AskToUnmuteButton';
|
||||
import { QuickActionButton } from './styled';
|
||||
|
@ -13,19 +10,26 @@ import { QuickActionButton } from './styled';
|
|||
type Props = {
|
||||
|
||||
/**
|
||||
* If audio is muted for the current participant.
|
||||
* The translated "ask unmute" text.
|
||||
*/
|
||||
isAudioMuted: Boolean,
|
||||
askUnmuteText: string,
|
||||
|
||||
/**
|
||||
* The type of button to be displayed.
|
||||
*/
|
||||
buttonType: string,
|
||||
|
||||
/**
|
||||
* Callback used to open a confirmation dialog for audio muting.
|
||||
*/
|
||||
muteAudio: Function,
|
||||
|
||||
muteParticipantButtonText: string,
|
||||
|
||||
/**
|
||||
* Participant.
|
||||
* The ID of the participant.
|
||||
*/
|
||||
participant: Object,
|
||||
participantID: string,
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -34,23 +38,29 @@ type Props = {
|
|||
* @param {Props} props - The props of the component.
|
||||
* @returns {React$Element<'button'>}
|
||||
*/
|
||||
export default function({ isAudioMuted, muteAudio, participant }: Props) {
|
||||
const buttonType = useSelector(getQuickActionButtonType(participant, isAudioMuted));
|
||||
const { id } = participant;
|
||||
const { t } = useTranslation();
|
||||
|
||||
export default function ParticipantQuickAction({
|
||||
askUnmuteText,
|
||||
buttonType,
|
||||
muteAudio,
|
||||
muteParticipantButtonText,
|
||||
participantID
|
||||
}: Props) {
|
||||
switch (buttonType) {
|
||||
case QUICK_ACTION_BUTTON.MUTE: {
|
||||
return (
|
||||
<QuickActionButton
|
||||
onClick = { muteAudio(id) }
|
||||
onClick = { muteAudio(participantID) }
|
||||
primary = { true }>
|
||||
{t('dialog.muteParticipantButton')}
|
||||
{ muteParticipantButtonText }
|
||||
</QuickActionButton>
|
||||
);
|
||||
}
|
||||
case QUICK_ACTION_BUTTON.ASK_TO_UNMUTE: {
|
||||
return <AskToUnmuteButton id = { id } />;
|
||||
return (
|
||||
<AskToUnmuteButton
|
||||
askUnmuteText = { askUnmuteText }
|
||||
id = { participantID } />
|
||||
);
|
||||
}
|
||||
default: {
|
||||
return null;
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
// @flow
|
||||
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import React, { Component } from 'react';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
|
||||
import { openDialog } from '../../base/dialog';
|
||||
import { translate } from '../../base/i18n';
|
||||
import {
|
||||
getParticipantCount,
|
||||
isEveryoneModerator,
|
||||
isLocalParticipantModerator
|
||||
} from '../../base/participants';
|
||||
import { connect } from '../../base/redux';
|
||||
import { MuteEveryoneDialog } from '../../video-menu/components/';
|
||||
import { close } from '../actions';
|
||||
import { classList, findStyledAncestor, getParticipantsPaneOpen } from '../functions';
|
||||
|
@ -30,49 +29,119 @@ import {
|
|||
Header
|
||||
} from './styled';
|
||||
|
||||
export const ParticipantsPane = () => {
|
||||
const dispatch = useDispatch();
|
||||
const paneOpen = useSelector(getParticipantsPaneOpen);
|
||||
const isLocalModerator = useSelector(isLocalParticipantModerator);
|
||||
const participantsCount = useSelector(getParticipantCount);
|
||||
const everyoneModerator = useSelector(isEveryoneModerator);
|
||||
const showContextMenu = !everyoneModerator && participantsCount > 2;
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link ParticipantsPane}.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
const [ contextOpen, setContextOpen ] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
/**
|
||||
* Is the participants pane open.
|
||||
*/
|
||||
_paneOpen: boolean,
|
||||
|
||||
const closePane = useCallback(() => dispatch(close(), [ dispatch ]));
|
||||
const closePaneKeyPress = useCallback(e => {
|
||||
if (closePane && (e.key === ' ' || e.key === 'Enter')) {
|
||||
e.preventDefault();
|
||||
closePane();
|
||||
/**
|
||||
* Whether to show context menu.
|
||||
*/
|
||||
_showContextMenu: boolean,
|
||||
|
||||
/**
|
||||
* Whether to show the footer menu.
|
||||
*/
|
||||
_showFooter: boolean,
|
||||
|
||||
/**
|
||||
* The Redux dispatch function.
|
||||
*/
|
||||
dispatch: Function,
|
||||
|
||||
/**
|
||||
* The i18n translate function.
|
||||
*/
|
||||
t: Function
|
||||
};
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} state of {@link ParticipantsPane}.
|
||||
*/
|
||||
type State = {
|
||||
|
||||
/**
|
||||
* Indicates if the footer context menu is open.
|
||||
*/
|
||||
contextOpen: boolean,
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements the participants list.
|
||||
*/
|
||||
class ParticipantsPane extends Component<Props, State> {
|
||||
/**
|
||||
* Initializes a new {@code ParticipantsPane} instance.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
contextOpen: false
|
||||
};
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onClosePane = this._onClosePane.bind(this);
|
||||
this._onKeyPress = this._onKeyPress.bind(this);
|
||||
this._onMuteAll = this._onMuteAll.bind(this);
|
||||
this._onToggleContext = this._onToggleContext.bind(this);
|
||||
this._onWindowClickListener = this._onWindowClickListener.bind(this);
|
||||
}
|
||||
}, [ closePane ]);
|
||||
const muteAll = useCallback(() => dispatch(openDialog(MuteEveryoneDialog)), [ dispatch ]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = [ 'click', e => {
|
||||
if (!findStyledAncestor(e.target, FooterEllipsisContainer)) {
|
||||
setContextOpen(false);
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#componentDidMount()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidMount() {
|
||||
window.addEventListener('click', this._onWindowClickListener);
|
||||
}
|
||||
} ];
|
||||
|
||||
window.addEventListener(...handler);
|
||||
/**
|
||||
* Implements React's {@link Component#componentWillUnmount()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('click', this._onWindowClickListener);
|
||||
}
|
||||
|
||||
return () => window.removeEventListener(...handler);
|
||||
}, [ contextOpen ]);
|
||||
/**
|
||||
* Implements React's {@link Component#render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
_paneOpen,
|
||||
_showContextMenu,
|
||||
_showFooter,
|
||||
t
|
||||
} = this.props;
|
||||
|
||||
const toggleContext = useCallback(() => setContextOpen(!contextOpen), [ contextOpen, setContextOpen ]);
|
||||
// when the pane is not open optimize to not
|
||||
// execute the MeetingParticipantList render for large list of participants
|
||||
if (!_paneOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeProvider theme = { theme }>
|
||||
<div className = { classList('participants_pane', !paneOpen && 'participants_pane--closed') }>
|
||||
<div className = { classList('participants_pane', !_paneOpen && 'participants_pane--closed') }>
|
||||
<div className = 'participants_pane-content'>
|
||||
<Header>
|
||||
<Close
|
||||
aria-label = { t('participantsPane.close', 'Close') }
|
||||
onClick = { closePane }
|
||||
onKeyPress = { closePaneKeyPress }
|
||||
onClick = { this._onClosePane }
|
||||
onKeyPress = { this._onKeyPress }
|
||||
role = 'button'
|
||||
tabIndex = { 0 } />
|
||||
</Header>
|
||||
|
@ -81,17 +150,18 @@ export const ParticipantsPane = () => {
|
|||
<AntiCollapse />
|
||||
<MeetingParticipantList />
|
||||
</Container>
|
||||
{isLocalModerator && (
|
||||
{_showFooter && (
|
||||
<Footer>
|
||||
<FooterButton onClick = { muteAll }>
|
||||
<FooterButton onClick = { this._onMuteAll }>
|
||||
{t('participantsPane.actions.muteAll')}
|
||||
</FooterButton>
|
||||
{showContextMenu && (
|
||||
{_showContextMenu && (
|
||||
<FooterEllipsisContainer>
|
||||
<FooterEllipsisButton
|
||||
id = 'participants-pane-context-menu'
|
||||
onClick = { toggleContext } />
|
||||
{contextOpen && <FooterContextMenu onMouseLeave = { toggleContext } />}
|
||||
onClick = { this._onToggleContext } />
|
||||
{this.state.contextOpen
|
||||
&& <FooterContextMenu onMouseLeave = { this._onToggleContext } />}
|
||||
</FooterEllipsisContainer>
|
||||
)}
|
||||
</Footer>
|
||||
|
@ -100,4 +170,97 @@ export const ParticipantsPane = () => {
|
|||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
_onClosePane: () => void;
|
||||
|
||||
/**
|
||||
* Callback for closing the participant pane.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onClosePane() {
|
||||
this.props.dispatch(close());
|
||||
}
|
||||
|
||||
_onKeyPress: (Object) => void;
|
||||
|
||||
/**
|
||||
* KeyPress handler for accessibility for closing the participants pane.
|
||||
*
|
||||
* @param {Object} e - The key event to handle.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onKeyPress(e) {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
this._onClosePane();
|
||||
}
|
||||
}
|
||||
|
||||
_onMuteAll: () => void;
|
||||
|
||||
/**
|
||||
* The handler for clicking mute all button.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onMuteAll() {
|
||||
this.props.dispatch(openDialog(MuteEveryoneDialog));
|
||||
}
|
||||
|
||||
_onToggleContext: () => void;
|
||||
|
||||
/**
|
||||
* Handler for toggling open/close of the footer context menu.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onToggleContext() {
|
||||
this.setState({
|
||||
contextOpen: !this.state.contextOpen
|
||||
});
|
||||
}
|
||||
|
||||
_onWindowClickListener: (event: Object) => void;
|
||||
|
||||
/**
|
||||
* Window click event listener.
|
||||
*
|
||||
* @param {Event} e - The click event.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onWindowClickListener(e) {
|
||||
if (this.state.contextOpen && !findStyledAncestor(e.target, FooterEllipsisContainer)) {
|
||||
this.setState({
|
||||
contextOpen: false
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the React {@code Component} props of
|
||||
* {@code ParticipantsPane}.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @protected
|
||||
* @returns {{
|
||||
* _paneOpen: boolean,
|
||||
* _showContextMenu: boolean,
|
||||
* _showFooter: boolean
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state: Object) {
|
||||
const isPaneOpen = getParticipantsPaneOpen(state);
|
||||
|
||||
return {
|
||||
_paneOpen: isPaneOpen,
|
||||
_showContextMenu: isPaneOpen && getParticipantCount(state) > 2,
|
||||
_showFooter: isPaneOpen && isLocalParticipantModerator(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(ParticipantsPane));
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
export * from './InviteButton';
|
||||
export * from './LobbyParticipantItem';
|
||||
export * from './LobbyParticipantList';
|
||||
export * from './MeetingParticipantContextMenu';
|
||||
export * from './MeetingParticipantItem';
|
||||
export * from './MeetingParticipantList';
|
||||
export * from './ParticipantItem';
|
||||
export * from './ParticipantsPane';
|
||||
export { default as ParticipantsPane } from './ParticipantsPane';
|
||||
export * from './ParticipantsPaneButton';
|
||||
export * from './RaisedHandIndicator';
|
||||
|
|
|
@ -41,13 +41,14 @@ export const findStyledAncestor = (target: Object, component: any) => {
|
|||
};
|
||||
|
||||
/**
|
||||
* Returns a selector used to determine if a participant is force muted.
|
||||
* Checks if a participant is force muted.
|
||||
*
|
||||
* @param {Object} participant - The participant id.
|
||||
* @param {Object} participant - The participant.
|
||||
* @param {MediaType} mediaType - The media type.
|
||||
* @returns {MediaState}.
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {MediaState}
|
||||
*/
|
||||
export const isForceMuted = (participant: Object, mediaType: MediaType) => (state: Object) => {
|
||||
export function isForceMuted(participant: Object, mediaType: MediaType, state: Object) {
|
||||
if (getParticipantCount(state) > 2 && isEnabledFromState(mediaType, state)) {
|
||||
if (participant.local) {
|
||||
return !isLocalParticipantApprovedFromState(mediaType, state);
|
||||
|
@ -62,18 +63,19 @@ export const isForceMuted = (participant: Object, mediaType: MediaType) => (stat
|
|||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a selector used to determine the audio media state (the mic icon) for a participant.
|
||||
* Determines the audio media state (the mic icon) for a participant.
|
||||
*
|
||||
* @param {Object} participant - The participant.
|
||||
* @param {boolean} muted - The mute state of the participant.
|
||||
* @returns {MediaState}.
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {MediaState}
|
||||
*/
|
||||
export const getParticipantAudioMediaState = (participant: Object, muted: Boolean) => (state: Object) => {
|
||||
export function getParticipantAudioMediaState(participant: Object, muted: Boolean, state: Object) {
|
||||
if (muted) {
|
||||
if (isForceMuted(participant, MEDIA_TYPE.AUDIO)(state)) {
|
||||
if (isForceMuted(participant, MEDIA_TYPE.AUDIO, state)) {
|
||||
return MEDIA_STATE.FORCE_MUTED;
|
||||
}
|
||||
|
||||
|
@ -81,7 +83,7 @@ export const getParticipantAudioMediaState = (participant: Object, muted: Boolea
|
|||
}
|
||||
|
||||
return MEDIA_STATE.UNMUTED;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
|
@ -125,17 +127,18 @@ const getState = (state: Object) => state[REDUCER_KEY];
|
|||
export const getParticipantsPaneOpen = (state: Object) => Boolean(getState(state)?.isOpen);
|
||||
|
||||
/**
|
||||
* Returns a selector used to determine the type of quick action button to be displayed for a participant.
|
||||
* Returns the type of quick action button to be displayed for a participant.
|
||||
* The button is displayed when hovering a participant from the participant list.
|
||||
*
|
||||
* @param {Object} participant - The participant.
|
||||
* @param {boolean} isAudioMuted - If audio is muted for the participant.
|
||||
* @returns {Function}
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {string} - The type of the quick action button.
|
||||
*/
|
||||
export const getQuickActionButtonType = (participant: Object, isAudioMuted: Boolean) => (state: Object) => {
|
||||
export function getQuickActionButtonType(participant: Object, isAudioMuted: Boolean, state: Object) {
|
||||
// handled only by moderators
|
||||
if (isLocalParticipantModerator(state)) {
|
||||
if (isForceMuted(participant, MEDIA_TYPE.AUDIO)(state)) {
|
||||
if (isForceMuted(participant, MEDIA_TYPE.AUDIO, state)) {
|
||||
return QUICK_ACTION_BUTTON.ASK_TO_UNMUTE;
|
||||
}
|
||||
if (!isAudioMuted) {
|
||||
|
@ -144,7 +147,7 @@ export const getQuickActionButtonType = (participant: Object, isAudioMuted: Bool
|
|||
}
|
||||
|
||||
return QUICK_ACTION_BUTTON.NONE;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the invite button should be rendered.
|
||||
|
|
|
@ -144,7 +144,7 @@ function _mapStateToProps(state) {
|
|||
clientHeight,
|
||||
clientWidth,
|
||||
filmstripVisible: visible,
|
||||
isOwner: ownerId === localParticipant.id,
|
||||
isOwner: ownerId === localParticipant?.id,
|
||||
videoUrl
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// @flow
|
||||
|
||||
import { getParticipants } from '../base/participants';
|
||||
import { getFakeParticipants } from '../base/participants';
|
||||
|
||||
import { VIDEO_PLAYER_PARTICIPANT_NAME, YOUTUBE_PLAYER_PARTICIPANT_NAME } from './constants';
|
||||
|
||||
|
@ -41,7 +41,15 @@ export function isSharingStatus(status: string) {
|
|||
* @returns {boolean}
|
||||
*/
|
||||
export function isVideoPlaying(stateful: Object | Function): boolean {
|
||||
return Boolean(getParticipants(stateful).find(p => p.isFakeParticipant
|
||||
&& (p.name === VIDEO_PLAYER_PARTICIPANT_NAME || p.name === YOUTUBE_PLAYER_PARTICIPANT_NAME))
|
||||
);
|
||||
let videoPlaying = false;
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
for (const [ id, p ] of getFakeParticipants(stateful)) {
|
||||
if (p.name === VIDEO_PLAYER_PARTICIPANT_NAME || p.name === YOUTUBE_PLAYER_PARTICIPANT_NAME) {
|
||||
videoPlaying = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return videoPlaying;
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import { getParticipantCountWithFake } from '../../base/participants';
|
||||
import { connect } from '../../base/redux';
|
||||
|
||||
import {
|
||||
|
@ -75,7 +76,7 @@ class Captions
|
|||
function mapStateToProps(state) {
|
||||
return {
|
||||
..._abstractMapStateToProps(state),
|
||||
_isLifted: state['features/base/participants'].length < 2
|
||||
_isLifted: getParticipantCountWithFake(state) < 2
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ import { translate } from '../../../base/i18n';
|
|||
import JitsiMeetJS from '../../../base/lib-jitsi-meet';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
getParticipants,
|
||||
haveParticipantWithScreenSharingFeature,
|
||||
raiseHand
|
||||
} from '../../../base/participants';
|
||||
import { connect } from '../../../base/redux';
|
||||
|
@ -183,15 +183,20 @@ type Props = {
|
|||
_raisedHand: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not the local participant is screensharing.
|
||||
* Whether or not the local participant is screenSharing.
|
||||
*/
|
||||
_screensharing: boolean,
|
||||
_screenSharing: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not the local participant is sharing a YouTube video.
|
||||
*/
|
||||
_sharingVideo: boolean,
|
||||
|
||||
/**
|
||||
* The enabled buttons.
|
||||
*/
|
||||
_toolbarButtons: Array<string>,
|
||||
|
||||
/**
|
||||
* Flag showing whether toolbar is visible.
|
||||
*/
|
||||
|
@ -202,11 +207,6 @@ type Props = {
|
|||
*/
|
||||
_visibleButtons: Array<string>,
|
||||
|
||||
/**
|
||||
* Handler to check if a button is enabled.
|
||||
*/
|
||||
_shouldShowButton: Function,
|
||||
|
||||
/**
|
||||
* Returns the selected virtual source object.
|
||||
*/
|
||||
|
@ -269,38 +269,39 @@ class Toolbox extends Component<Props> {
|
|||
* @returns {void}
|
||||
*/
|
||||
componentDidMount() {
|
||||
const { _toolbarButtons } = this.props;
|
||||
const KEYBOARD_SHORTCUTS = [
|
||||
this.props._shouldShowButton('videoquality') && {
|
||||
isToolbarButtonEnabled('videoquality', _toolbarButtons) && {
|
||||
character: 'A',
|
||||
exec: this._onShortcutToggleVideoQuality,
|
||||
helpDescription: 'toolbar.callQuality'
|
||||
},
|
||||
this.props._shouldShowButton('chat') && {
|
||||
isToolbarButtonEnabled('chat', _toolbarButtons) && {
|
||||
character: 'C',
|
||||
exec: this._onShortcutToggleChat,
|
||||
helpDescription: 'keyboardShortcuts.toggleChat'
|
||||
},
|
||||
this.props._shouldShowButton('desktop') && {
|
||||
isToolbarButtonEnabled('desktop', _toolbarButtons) && {
|
||||
character: 'D',
|
||||
exec: this._onShortcutToggleScreenshare,
|
||||
helpDescription: 'keyboardShortcuts.toggleScreensharing'
|
||||
},
|
||||
this.props._shouldShowButton('participants-pane') && {
|
||||
isToolbarButtonEnabled('participants-pane', _toolbarButtons) && {
|
||||
character: 'P',
|
||||
exec: this._onShortcutToggleParticipantsPane,
|
||||
helpDescription: 'keyboardShortcuts.toggleParticipantsPane'
|
||||
},
|
||||
this.props._shouldShowButton('raisehand') && {
|
||||
isToolbarButtonEnabled('raisehand', _toolbarButtons) && {
|
||||
character: 'R',
|
||||
exec: this._onShortcutToggleRaiseHand,
|
||||
helpDescription: 'keyboardShortcuts.raiseHand'
|
||||
},
|
||||
this.props._shouldShowButton('fullscreen') && {
|
||||
isToolbarButtonEnabled('fullscreen', _toolbarButtons) && {
|
||||
character: 'S',
|
||||
exec: this._onShortcutToggleFullScreen,
|
||||
helpDescription: 'keyboardShortcuts.fullScreen'
|
||||
},
|
||||
this.props._shouldShowButton('tileview') && {
|
||||
isToolbarButtonEnabled('tileview', _toolbarButtons) && {
|
||||
character: 'W',
|
||||
exec: this._onShortcutToggleTileView,
|
||||
helpDescription: 'toolbar.tileViewToggle'
|
||||
|
@ -509,7 +510,7 @@ class Toolbox extends Component<Props> {
|
|||
const {
|
||||
_feedbackConfigured,
|
||||
_isMobile,
|
||||
_screensharing
|
||||
_screenSharing
|
||||
} = this.props;
|
||||
|
||||
const microphone = {
|
||||
|
@ -644,7 +645,7 @@ class Toolbox extends Component<Props> {
|
|||
group: 3
|
||||
};
|
||||
|
||||
const virtualBackground = !_screensharing && checkBlurSupport() && {
|
||||
const virtualBackground = !_screenSharing && checkBlurSupport() && {
|
||||
key: 'select-background',
|
||||
Content: VideoBackgroundButton,
|
||||
group: 3
|
||||
|
@ -734,12 +735,12 @@ class Toolbox extends Component<Props> {
|
|||
_getVisibleButtons() {
|
||||
const {
|
||||
_clientWidth,
|
||||
_shouldShowButton
|
||||
_toolbarButtons
|
||||
} = this.props;
|
||||
|
||||
|
||||
const buttons = this._getAllButtons();
|
||||
const isHangupVisible = _shouldShowButton('hangup');
|
||||
const isHangupVisible = isToolbarButtonEnabled('hangup', _toolbarButtons);
|
||||
const { order } = THRESHOLDS.find(({ width }) => _clientWidth > width)
|
||||
|| THRESHOLDS[THRESHOLDS.length - 1];
|
||||
let sliceIndex = order.length + 2;
|
||||
|
@ -749,7 +750,7 @@ class Toolbox extends Component<Props> {
|
|||
const filtered = [
|
||||
...order.map(key => buttons[key]),
|
||||
...Object.values(buttons).filter((button, index) => !order.includes(keys[index]))
|
||||
].filter(Boolean).filter(({ key }) => _shouldShowButton(key));
|
||||
].filter(Boolean).filter(({ key }) => isToolbarButtonEnabled(key, _toolbarButtons));
|
||||
|
||||
if (isHangupVisible) {
|
||||
sliceIndex -= 1;
|
||||
|
@ -934,7 +935,7 @@ class Toolbox extends Component<Props> {
|
|||
'toggle.screen.sharing',
|
||||
ACTION_SHORTCUT_TRIGGERED,
|
||||
{
|
||||
enable: !this.props._screensharing
|
||||
enable: !this.props._screenSharing
|
||||
}));
|
||||
|
||||
this._doToggleScreenshare();
|
||||
|
@ -1053,7 +1054,7 @@ class Toolbox extends Component<Props> {
|
|||
sendAnalytics(createToolbarEvent(
|
||||
'toggle.screen.sharing',
|
||||
ACTION_SHORTCUT_TRIGGERED,
|
||||
{ enable: !this.props._screensharing }));
|
||||
{ enable: !this.props._screenSharing }));
|
||||
|
||||
this._closeOverflowMenuIfOpen();
|
||||
this._doToggleScreenshare();
|
||||
|
@ -1116,6 +1117,7 @@ class Toolbox extends Component<Props> {
|
|||
const {
|
||||
_isMobile,
|
||||
_overflowMenuVisible,
|
||||
_toolbarButtons,
|
||||
t
|
||||
} = this.props;
|
||||
|
||||
|
@ -1169,7 +1171,7 @@ class Toolbox extends Component<Props> {
|
|||
<HangupButton
|
||||
customClass = 'hangup-button'
|
||||
key = 'hangup-button'
|
||||
visible = { this.props._shouldShowButton('hangup') } />
|
||||
visible = { isToolbarButtonEnabled('hangup', _toolbarButtons) } />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1203,13 +1205,13 @@ function _mapStateToProps(state) {
|
|||
let desktopSharingDisabledTooltipKey;
|
||||
|
||||
if (enableFeaturesBasedOnToken) {
|
||||
if (desktopSharingEnabled) {
|
||||
// we enable desktop sharing if any participant already have this
|
||||
// feature enabled
|
||||
desktopSharingEnabled = getParticipants(state)
|
||||
.find(({ features = {} }) =>
|
||||
String(features['screen-sharing']) === 'true') !== undefined;
|
||||
// feature enabled and if the user supports it.
|
||||
desktopSharingEnabled = haveParticipantWithScreenSharingFeature(state);
|
||||
desktopSharingDisabledTooltipKey = 'dialog.shareYourScreenDisabled';
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
_chatOpen: state['features/chat'].isOpen,
|
||||
|
@ -1226,13 +1228,13 @@ function _mapStateToProps(state) {
|
|||
_isVpaasMeeting: isVpaasMeeting(state),
|
||||
_fullScreen: fullScreen,
|
||||
_tileViewEnabled: shouldDisplayTileView(state),
|
||||
_localParticipantID: localParticipant.id,
|
||||
_localParticipantID: localParticipant?.id,
|
||||
_localVideo: localVideo,
|
||||
_overflowMenuVisible: overflowMenuVisible,
|
||||
_participantsPaneOpen: getParticipantsPaneOpen(state),
|
||||
_raisedHand: localParticipant.raisedHand,
|
||||
_screensharing: isScreenVideoShared(state),
|
||||
_shouldShowButton: buttonName => isToolbarButtonEnabled(buttonName)(state),
|
||||
_raisedHand: localParticipant?.raisedHand,
|
||||
_screenSharing: isScreenVideoShared(state),
|
||||
_toolbarButtons: getToolbarButtons(state),
|
||||
_visible: isToolboxVisible(state),
|
||||
_visibleButtons: getToolbarButtons(state)
|
||||
};
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { hasAvailableDevices } from '../base/devices';
|
||||
import { TOOLBOX_ALWAYS_VISIBLE, getFeatureFlag, TOOLBOX_ENABLED } from '../base/flags';
|
||||
import { getParticipantCountWithFake } from '../base/participants';
|
||||
import { toState } from '../base/redux';
|
||||
import { isLocalVideoTrackDesktop } from '../base/tracks';
|
||||
|
||||
|
@ -60,7 +61,7 @@ export function getMovableButtons(width: number): Set<string> {
|
|||
export function isToolboxVisible(stateful: Object | Function) {
|
||||
const state = toState(stateful);
|
||||
const { alwaysVisible, enabled, visible } = state['features/toolbox'];
|
||||
const { length: participantCount } = state['features/base/participants'];
|
||||
const participantCount = getParticipantCountWithFake(state);
|
||||
const alwaysVisibleFlag = getFeatureFlag(state, TOOLBOX_ALWAYS_VISIBLE, false);
|
||||
const enabledFlag = getFeatureFlag(state, TOOLBOX_ENABLED, true);
|
||||
|
||||
|
|
|
@ -5,7 +5,8 @@ import { getFeatureFlag, TILE_VIEW_ENABLED } from '../base/flags';
|
|||
import {
|
||||
getPinnedParticipant,
|
||||
getParticipantCount,
|
||||
pinParticipant
|
||||
pinParticipant,
|
||||
getParticipantCountWithFake
|
||||
} from '../base/participants';
|
||||
import {
|
||||
ASPECT_RATIO_BREAKPOINT,
|
||||
|
@ -101,7 +102,7 @@ export function getTileViewGridDimensions(state: Object) {
|
|||
// When in tile view mode, we must discount ourselves (the local participant) because our
|
||||
// tile is not visible.
|
||||
const { iAmRecorder } = state['features/base/config'];
|
||||
const numberOfParticipants = state['features/base/participants'].length - (iAmRecorder ? 1 : 0);
|
||||
const numberOfParticipants = getParticipantCountWithFake(state) - (iAmRecorder ? 1 : 0);
|
||||
|
||||
const columnsToMaintainASquare = Math.ceil(Math.sqrt(numberOfParticipants));
|
||||
const columns = Math.min(columnsToMaintainASquare, maxColumns);
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
} from '../base/media';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
getRemoteParticipants,
|
||||
muteRemoteParticipant
|
||||
} from '../base/participants';
|
||||
|
||||
|
@ -91,14 +92,17 @@ export function muteAllParticipants(exclude: Array<string>, mediaType: MEDIA_TYP
|
|||
return (dispatch: Dispatch<any>, getState: Function) => {
|
||||
const state = getState();
|
||||
const localId = getLocalParticipant(state).id;
|
||||
const participantIds = state['features/base/participants']
|
||||
.map(p => p.id);
|
||||
|
||||
/* eslint-disable no-confusing-arrow */
|
||||
participantIds
|
||||
.filter(id => !exclude.includes(id))
|
||||
.map(id => id === localId ? muteLocal(true, mediaType) : muteRemote(id, mediaType))
|
||||
.map(dispatch);
|
||||
/* eslint-enable no-confusing-arrow */
|
||||
if (!exclude.includes(localId)) {
|
||||
dispatch(muteLocal(true, mediaType));
|
||||
}
|
||||
|
||||
getRemoteParticipants(state).forEach((p, id) => {
|
||||
if (exclude.includes(id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(muteRemote(id, mediaType));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue