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,
|
getParticipantById,
|
||||||
pinParticipant,
|
pinParticipant,
|
||||||
kickParticipant,
|
kickParticipant,
|
||||||
raiseHand
|
raiseHand,
|
||||||
|
isParticipantModerator
|
||||||
} from '../../react/features/base/participants';
|
} from '../../react/features/base/participants';
|
||||||
import { updateSettings } from '../../react/features/base/settings';
|
import { updateSettings } from '../../react/features/base/settings';
|
||||||
import { isToggleCameraEnabled, toggleCamera } from '../../react/features/base/tracks';
|
import { isToggleCameraEnabled, toggleCamera } from '../../react/features/base/tracks';
|
||||||
|
@ -105,13 +106,14 @@ function initCommands() {
|
||||||
const muteMediaType = mediaType ? mediaType : MEDIA_TYPE.AUDIO;
|
const muteMediaType = mediaType ? mediaType : MEDIA_TYPE.AUDIO;
|
||||||
|
|
||||||
sendAnalytics(createApiEvent('muted-everyone'));
|
sendAnalytics(createApiEvent('muted-everyone'));
|
||||||
const participants = APP.store.getState()['features/base/participants'];
|
const localParticipant = getLocalParticipant(APP.store.getState());
|
||||||
const localIds = participants
|
const exclude = [];
|
||||||
.filter(participant => participant.local)
|
|
||||||
.filter(participant => participant.role === 'moderator')
|
|
||||||
.map(participant => participant.id);
|
|
||||||
|
|
||||||
APP.store.dispatch(muteAllParticipants(localIds, muteMediaType));
|
if (localParticipant && isParticipantModerator(localParticipant)) {
|
||||||
|
exclude.push(localParticipant.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
APP.store.dispatch(muteAllParticipants(exclude, muteMediaType));
|
||||||
},
|
},
|
||||||
'toggle-lobby': isLobbyEnabled => {
|
'toggle-lobby': isLobbyEnabled => {
|
||||||
APP.store.dispatch(toggleLobbyMode(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.
|
* 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}
|
* @returns {Object}
|
||||||
*/
|
*/
|
||||||
export function dismissPendingAudioParticipant(id: string) {
|
export function dismissPendingAudioParticipant(participant: Object) {
|
||||||
return dismissPendingParticipant(id, MEDIA_TYPE.AUDIO);
|
return dismissPendingParticipant(participant, MEDIA_TYPE.AUDIO);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hides the notification with the participant that asked to unmute.
|
* 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.
|
* @param {MediaType} mediaType - The media type.
|
||||||
* @returns {Object}
|
* @returns {Object}
|
||||||
*/
|
*/
|
||||||
export function dismissPendingParticipant(id: string, mediaType: MediaType) {
|
export function dismissPendingParticipant(participant: Object, mediaType: MediaType) {
|
||||||
return {
|
return {
|
||||||
type: DISMISS_PENDING_PARTICIPANT,
|
type: DISMISS_PENDING_PARTICIPANT,
|
||||||
id,
|
participant,
|
||||||
mediaType
|
mediaType
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -145,13 +145,13 @@ export function showModeratedNotification(mediaType: MediaType) {
|
||||||
/**
|
/**
|
||||||
* Shows a notification with the participant that asked to audio unmute.
|
* 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}
|
* @returns {Object}
|
||||||
*/
|
*/
|
||||||
export function participantPendingAudio(id: string) {
|
export function participantPendingAudio(participant: Object) {
|
||||||
return {
|
return {
|
||||||
type: PARTICIPANT_PENDING_AUDIO,
|
type: PARTICIPANT_PENDING_AUDIO,
|
||||||
id
|
participant
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,10 @@ import { useTranslation } from 'react-i18next';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
import NotificationWithParticipants from '../../notifications/components/web/NotificationWithParticipants';
|
import NotificationWithParticipants from '../../notifications/components/web/NotificationWithParticipants';
|
||||||
import { approveAudio, dismissPendingAudioParticipant } from '../actions';
|
import {
|
||||||
|
approveParticipant,
|
||||||
|
dismissPendingAudioParticipant
|
||||||
|
} from '../actions';
|
||||||
import { getParticipantsAskingToAudioUnmute } from '../functions';
|
import { getParticipantsAskingToAudioUnmute } from '../functions';
|
||||||
|
|
||||||
|
|
||||||
|
@ -25,7 +28,7 @@ export default function() {
|
||||||
</div>
|
</div>
|
||||||
<NotificationWithParticipants
|
<NotificationWithParticipants
|
||||||
approveButtonText = { t('notify.unmute') }
|
approveButtonText = { t('notify.unmute') }
|
||||||
onApprove = { approveAudio }
|
onApprove = { approveParticipant }
|
||||||
onReject = { dismissPendingAudioParticipant }
|
onReject = { dismissPendingAudioParticipant }
|
||||||
participants = { participants }
|
participants = { participants }
|
||||||
rejectButtonText = { t('dialog.dismiss') }
|
rejectButtonText = { t('dialog.dismiss') }
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import { MEDIA_TYPE, type MediaType } from '../base/media/constants';
|
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';
|
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'];
|
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.
|
* 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);
|
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.
|
* 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.
|
* 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.
|
* @param {MEDIA_TYPE} mediaType - The media type to check.
|
||||||
* @returns {boolean}
|
* @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 storeKey = MEDIA_TYPE_TO_PENDING_STORE_KEY[mediaType];
|
||||||
const arr = getState(state)[storeKey];
|
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) => {
|
export const getParticipantsAskingToAudioUnmute = (state: Object) => {
|
||||||
if (isLocalParticipantModerator(state)) {
|
if (isLocalParticipantModerator(state)) {
|
||||||
const ids = getState(state).pendingAudio;
|
return getState(state).pendingAudio;
|
||||||
|
|
||||||
return ids.map(id => getParticipantById(state, id)).filter(Boolean);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return [];
|
return EMPTY_ARRAY;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -127,14 +127,16 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||||
|
|
||||||
// this is handled only by moderators
|
// this is handled only by moderators
|
||||||
if (audioModerationEnabled && isLocalParticipantModerator(state)) {
|
if (audioModerationEnabled && isLocalParticipantModerator(state)) {
|
||||||
const { participant: { id, raisedHand } } = action;
|
const participant = action.participant;
|
||||||
|
|
||||||
if (raisedHand) {
|
if (participant.raisedHand) {
|
||||||
// if participant raises hand show notification
|
// 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 {
|
} else {
|
||||||
// if participant lowers hand hide notification
|
// 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 */
|
/* @flow */
|
||||||
|
|
||||||
import { MEDIA_TYPE } from '../base/media/constants';
|
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 { ReducerRegistry } from '../base/redux';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -11,6 +16,7 @@ import {
|
||||||
PARTICIPANT_APPROVED,
|
PARTICIPANT_APPROVED,
|
||||||
PARTICIPANT_PENDING_AUDIO
|
PARTICIPANT_PENDING_AUDIO
|
||||||
} from './actionTypes';
|
} from './actionTypes';
|
||||||
|
import { MEDIA_TYPE_TO_PENDING_STORE_KEY } from './constants';
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
audioModerationEnabled: false,
|
audioModerationEnabled: false,
|
||||||
|
@ -21,6 +27,41 @@ const initialState = {
|
||||||
pendingVideo: []
|
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) => {
|
ReducerRegistry.register('features/av-moderation', (state = initialState, action) => {
|
||||||
|
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
|
@ -65,13 +106,13 @@ ReducerRegistry.register('features/av-moderation', (state = initialState, action
|
||||||
}
|
}
|
||||||
|
|
||||||
case PARTICIPANT_PENDING_AUDIO: {
|
case PARTICIPANT_PENDING_AUDIO: {
|
||||||
const { id } = action;
|
const { participant } = action;
|
||||||
|
|
||||||
// Add participant to pendigAudio array only if it's not already added
|
// Add participant to pendingAudio array only if it's not already added
|
||||||
if (!state.pendingAudio.find(pending => pending === id)) {
|
if (!state.pendingAudio.find(pending => pending.id === participant.id)) {
|
||||||
const updated = [ ...state.pendingAudio ];
|
const updated = [ ...state.pendingAudio ];
|
||||||
|
|
||||||
updated.push(id);
|
updated.push(participant);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
@ -82,20 +123,79 @@ ReducerRegistry.register('features/av-moderation', (state = initialState, action
|
||||||
return state;
|
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: {
|
case DISMISS_PENDING_PARTICIPANT: {
|
||||||
const { id, mediaType } = action;
|
const { participant, mediaType } = action;
|
||||||
|
|
||||||
if (mediaType === MEDIA_TYPE.AUDIO) {
|
if (mediaType === MEDIA_TYPE.AUDIO) {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
pendingAudio: state.pendingAudio.filter(pending => pending !== id)
|
pendingAudio: state.pendingAudio.filter(pending => pending.id !== participant.id)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mediaType === MEDIA_TYPE.VIDEO) {
|
if (mediaType === MEDIA_TYPE.VIDEO) {
|
||||||
return {
|
return {
|
||||||
...state,
|
...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);
|
return next(action);
|
||||||
}
|
}
|
||||||
|
|
||||||
const participants = state['features/base/participants'];
|
|
||||||
const id = action.participant.id;
|
const id = action.participant.id;
|
||||||
const participantById = getParticipantById(participants, id);
|
const participantById = getParticipantById(state, id);
|
||||||
const pinnedParticipant = getPinnedParticipant(participants);
|
const pinnedParticipant = getPinnedParticipant(state);
|
||||||
const actionName = id ? ACTION_PINNED : ACTION_UNPINNED;
|
const actionName = id ? ACTION_PINNED : ACTION_UNPINNED;
|
||||||
const local
|
const local
|
||||||
= (participantById && participantById.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.
|
* @param {string} buttonName - The name of the button.
|
||||||
* {@link interfaceConfig}.
|
* {@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) =>
|
export function isToolbarButtonEnabled(buttonName: string, state: Object | Array<string>) {
|
||||||
(state: Object): boolean =>
|
const buttons = Array.isArray(state) ? state : getToolbarButtons(state);
|
||||||
getToolbarButtons(state).includes(buttonName);
|
|
||||||
|
return buttons.includes(buttonName);
|
||||||
|
}
|
||||||
|
|
|
@ -79,16 +79,15 @@ export function getFirstLoadableAvatarUrl(participant: Object, store: Store<any,
|
||||||
/**
|
/**
|
||||||
* Returns local participant from Redux state.
|
* Returns local participant from Redux state.
|
||||||
*
|
*
|
||||||
* @param {(Function|Object|Participant[])} stateful - The redux state
|
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's
|
||||||
* features/base/participants, the (whole) redux state, or redux's
|
|
||||||
* {@code getState} function to be used to retrieve the state
|
* {@code getState} function to be used to retrieve the state
|
||||||
* features/base/participants.
|
* features/base/participants.
|
||||||
* @returns {(Participant|undefined)}
|
* @returns {(Participant|undefined)}
|
||||||
*/
|
*/
|
||||||
export function getLocalParticipant(stateful: Object | Function) {
|
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.
|
* Returns participant by ID from Redux state.
|
||||||
*
|
*
|
||||||
* @param {(Function|Object|Participant[])} stateful - The redux state
|
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's
|
||||||
* features/base/participants, the (whole) redux state, or redux's
|
|
||||||
* {@code getState} function to be used to retrieve the state
|
* {@code getState} function to be used to retrieve the state
|
||||||
* features/base/participants.
|
* features/base/participants.
|
||||||
* @param {string} id - The ID of the participant to retrieve.
|
* @param {string} id - The ID of the participant to retrieve.
|
||||||
|
@ -119,37 +117,82 @@ export function getNormalizedDisplayName(name: string) {
|
||||||
*/
|
*/
|
||||||
export function getParticipantById(
|
export function getParticipantById(
|
||||||
stateful: Object | Function, id: string): ?Object {
|
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,
|
* Returns a count of the known participants in the passed in redux state,
|
||||||
* excluding any fake participants.
|
* excluding any fake participants.
|
||||||
*
|
*
|
||||||
* @param {(Function|Object|Participant[])} stateful - The redux state
|
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's
|
||||||
* features/base/participants, the (whole) redux state, or redux's
|
|
||||||
* {@code getState} function to be used to retrieve the state
|
* {@code getState} function to be used to retrieve the state
|
||||||
* features/base/participants.
|
* features/base/participants.
|
||||||
* @returns {number}
|
* @returns {number}
|
||||||
*/
|
*/
|
||||||
export function getParticipantCount(stateful: Object | Function) {
|
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,
|
* Returns a count of the known participants in the passed in redux state,
|
||||||
* including fake participants.
|
* including fake participants.
|
||||||
*
|
*
|
||||||
* @param {(Function|Object|Participant[])} stateful - The redux state
|
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's
|
||||||
* features/base/participants, the (whole) redux state, or redux's
|
|
||||||
* {@code getState} function to be used to retrieve the state
|
* {@code getState} function to be used to retrieve the state
|
||||||
* features/base/participants.
|
* features/base/participants.
|
||||||
* @returns {number}
|
* @returns {number}
|
||||||
*/
|
*/
|
||||||
export function getParticipantCountWithFake(stateful: Object | Function) {
|
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';
|
: '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.
|
* 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
|
* Returns true if there is at least 1 participant with screen sharing feature and false otherwise.
|
||||||
* out.
|
|
||||||
*
|
*
|
||||||
* @param {(Function|Object|Participant[])} stateful - The redux state
|
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's
|
||||||
* features/base/participants, 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
|
* {@code getState} function to be used to retrieve the state
|
||||||
* features/base/participants.
|
* features/base/participants.
|
||||||
* @returns {Participant[]}
|
* @returns {Map<string, Object>}
|
||||||
*/
|
*/
|
||||||
export function getParticipants(stateful: Object | Function) {
|
export function getRemoteParticipants(stateful: Object | Function) {
|
||||||
return _getAllParticipants(stateful).filter(p => !p.isFakeParticipant);
|
return toState(stateful)['features/base/participants'].remote;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the participant which has its pinned state set to truthy.
|
* Returns the participant which has its pinned state set to truthy.
|
||||||
*
|
*
|
||||||
* @param {(Function|Object|Participant[])} stateful - The redux state
|
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's
|
||||||
* features/base/participants, the (whole) redux state, or redux's
|
|
||||||
* {@code getState} function to be used to retrieve the state
|
* {@code getState} function to be used to retrieve the state
|
||||||
* features/base/participants.
|
* features/base/participants.
|
||||||
* @returns {(Participant|undefined)}
|
* @returns {(Participant|undefined)}
|
||||||
*/
|
*/
|
||||||
export function getPinnedParticipant(stateful: Object | Function) {
|
export function getPinnedParticipant(stateful: Object | Function) {
|
||||||
return _getAllParticipants(stateful).find(p => p.pinned);
|
const state = toState(stateful)['features/base/participants'];
|
||||||
}
|
const { pinnedParticipant } = state;
|
||||||
|
|
||||||
/**
|
if (!pinnedParticipant) {
|
||||||
* Returns array of participants from Redux state.
|
return undefined;
|
||||||
*
|
}
|
||||||
* @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'] || []);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
return getParticipantById(stateful, pinnedParticipant);
|
||||||
* 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];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -289,6 +302,24 @@ export function isParticipantModerator(participant: Object) {
|
||||||
return participant?.role === PARTICIPANT_ROLE.MODERATOR;
|
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.
|
* Returns true if all of the meeting participants are moderators.
|
||||||
*
|
*
|
||||||
|
@ -297,9 +328,9 @@ export function isParticipantModerator(participant: Object) {
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
export function isEveryoneModerator(stateful: Object | Function) {
|
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}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
export function isLocalParticipantModerator(stateful: Object | Function) {
|
export function isLocalParticipantModerator(stateful: Object | Function) {
|
||||||
const state = toState(stateful);
|
const state = toState(stateful)['features/base/participants'];
|
||||||
const localParticipant = getLocalParticipant(state);
|
|
||||||
|
|
||||||
if (!localParticipant) {
|
const { local } = state;
|
||||||
|
|
||||||
|
if (!local) {
|
||||||
return false;
|
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++) {
|
for (let i = 0; i < AVATAR_CHECKER_FUNCTIONS.length; i++) {
|
||||||
const url = AVATAR_CHECKER_FUNCTIONS[i](participant, store);
|
const url = AVATAR_CHECKER_FUNCTIONS[i](participant, store);
|
||||||
|
|
||||||
if (url) {
|
if (url !== null) {
|
||||||
if (AVATAR_CHECKED_URLS.has(url)) {
|
if (AVATAR_CHECKED_URLS.has(url)) {
|
||||||
if (AVATAR_CHECKED_URLS.get(url)) {
|
if (AVATAR_CHECKED_URLS.get(url)) {
|
||||||
return url;
|
return url;
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
|
import { batch } from 'react-redux';
|
||||||
|
|
||||||
import UIEvents from '../../../../service/UI/UIEvents';
|
import UIEvents from '../../../../service/UI/UIEvents';
|
||||||
import { toggleE2EE } from '../../e2ee/actions';
|
import { toggleE2EE } from '../../e2ee/actions';
|
||||||
import { NOTIFICATION_TIMEOUT, showNotification } from '../../notifications';
|
import { NOTIFICATION_TIMEOUT, showNotification } from '../../notifications';
|
||||||
|
@ -43,7 +45,8 @@ import {
|
||||||
getLocalParticipant,
|
getLocalParticipant,
|
||||||
getParticipantById,
|
getParticipantById,
|
||||||
getParticipantCount,
|
getParticipantCount,
|
||||||
getParticipantDisplayName
|
getParticipantDisplayName,
|
||||||
|
getRemoteParticipants
|
||||||
} from './functions';
|
} from './functions';
|
||||||
import { PARTICIPANT_JOINED_FILE, PARTICIPANT_LEFT_FILE } from './sounds';
|
import { PARTICIPANT_JOINED_FILE, PARTICIPANT_LEFT_FILE } from './sounds';
|
||||||
|
|
||||||
|
@ -182,11 +185,12 @@ MiddlewareRegistry.register(store => next => action => {
|
||||||
StateListenerRegistry.register(
|
StateListenerRegistry.register(
|
||||||
/* selector */ state => getCurrentConference(state),
|
/* selector */ state => getCurrentConference(state),
|
||||||
/* listener */ (conference, { dispatch, getState }) => {
|
/* listener */ (conference, { dispatch, getState }) => {
|
||||||
for (const p of getState()['features/base/participants']) {
|
batch(() => {
|
||||||
!p.local
|
for (const [ id, p ] of getRemoteParticipants(getState())) {
|
||||||
&& (!conference || p.conference !== conference)
|
(!conference || p.conference !== conference)
|
||||||
&& dispatch(participantLeft(p.id, p.conference, p.isReplaced));
|
&& dispatch(participantLeft(id, p.conference, p.isReplaced));
|
||||||
}
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -12,6 +12,7 @@ import {
|
||||||
SET_LOADABLE_AVATAR_URL
|
SET_LOADABLE_AVATAR_URL
|
||||||
} from './actionTypes';
|
} from './actionTypes';
|
||||||
import { LOCAL_PARTICIPANT_DEFAULT_ID, PARTICIPANT_ROLE } from './constants';
|
import { LOCAL_PARTICIPANT_DEFAULT_ID, PARTICIPANT_ROLE } from './constants';
|
||||||
|
import { isParticipantModerator } from './functions';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Participant object.
|
* Participant object.
|
||||||
|
@ -51,6 +52,16 @@ const PARTICIPANT_PROPS_TO_OMIT_WHEN_UPDATE = [
|
||||||
'pinned'
|
'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
|
* Listen for actions which add, remove, or update the set of participants in
|
||||||
* the conference.
|
* the conference.
|
||||||
|
@ -62,18 +73,157 @@ const PARTICIPANT_PROPS_TO_OMIT_WHEN_UPDATE = [
|
||||||
* added/removed/modified.
|
* added/removed/modified.
|
||||||
* @returns {Participant[]}
|
* @returns {Participant[]}
|
||||||
*/
|
*/
|
||||||
ReducerRegistry.register('features/base/participants', (state = [], action) => {
|
ReducerRegistry.register('features/base/participants', (state = DEFAULT_STATE, action) => {
|
||||||
switch (action.type) {
|
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 SET_LOADABLE_AVATAR_URL:
|
||||||
case DOMINANT_SPEAKER_CHANGED:
|
case PARTICIPANT_UPDATED: {
|
||||||
case PARTICIPANT_ID_CHANGED:
|
const { participant } = action;
|
||||||
case PARTICIPANT_UPDATED:
|
let { id } = participant;
|
||||||
case PIN_PARTICIPANT:
|
const { local } = participant;
|
||||||
return state.map(p => _participant(p, action));
|
|
||||||
|
|
||||||
case PARTICIPANT_JOINED:
|
if (!id && local) {
|
||||||
return [ ...state, _participantJoined(action) ];
|
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: {
|
case PARTICIPANT_LEFT: {
|
||||||
// XXX A remote participant is uniquely identified by their id in a
|
// XXX A remote participant is uniquely identified by their id in a
|
||||||
// specific JitsiConference instance. The local participant is uniquely
|
// 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
|
// (and the fact that the local participant "joins" at the beginning of
|
||||||
// the app and "leaves" at the end of the app).
|
// the app and "leaves" at the end of the app).
|
||||||
const { conference, id } = action.participant;
|
const { conference, id } = action.participant;
|
||||||
|
const { fakeParticipants, remote, local, dominantSpeaker, pinnedParticipant } = state;
|
||||||
|
let oldParticipant = remote.get(id);
|
||||||
|
|
||||||
return state.filter(p =>
|
if (oldParticipant && oldParticipant.conference === conference) {
|
||||||
!(
|
remote.delete(id);
|
||||||
p.id === 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
|
if (!state.everyoneIsModerator && !isParticipantModerator(oldParticipant)) {
|
||||||
// participant and a remote participant cause the removal of
|
state.everyoneIsModerator = _isEveryoneModerator(state);
|
||||||
// the local participant when the remote participant's
|
}
|
||||||
// removal is requested.
|
|
||||||
&& p.conference === conference
|
const { features = {} } = oldParticipant || {};
|
||||||
&& (conference || p.local)));
|
|
||||||
|
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;
|
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.
|
* Reducer function for a single participant.
|
||||||
*
|
*
|
||||||
|
@ -112,56 +350,22 @@ ReducerRegistry.register('features/base/participants', (state = [], action) => {
|
||||||
*/
|
*/
|
||||||
function _participant(state: Object = {}, action) {
|
function _participant(state: Object = {}, action) {
|
||||||
switch (action.type) {
|
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 SET_LOADABLE_AVATAR_URL:
|
||||||
case PARTICIPANT_UPDATED: {
|
case PARTICIPANT_UPDATED: {
|
||||||
const { participant } = action; // eslint-disable-line no-shadow
|
const { participant } = action; // eslint-disable-line no-shadow
|
||||||
let { id } = participant;
|
|
||||||
const { local } = participant;
|
|
||||||
|
|
||||||
if (!id && local) {
|
const newState = { ...state };
|
||||||
id = LOCAL_PARTICIPANT_DEFAULT_ID;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.id === id) {
|
for (const key in participant) {
|
||||||
const newState = { ...state };
|
if (participant.hasOwnProperty(key)
|
||||||
|
&& PARTICIPANT_PROPS_TO_OMIT_WHEN_UPDATE.indexOf(key)
|
||||||
for (const key in participant) {
|
=== -1) {
|
||||||
if (participant.hasOwnProperty(key)
|
newState[key] = participant[key];
|
||||||
&& PARTICIPANT_PROPS_TO_OMIT_WHEN_UPDATE.indexOf(key)
|
|
||||||
=== -1) {
|
|
||||||
newState[key] = participant[key];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return newState;
|
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case PIN_PARTICIPANT:
|
return newState;
|
||||||
// Currently, only one pinned participant is allowed.
|
}
|
||||||
return set(state, 'pinned', state.id === action.participant.id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
|
|
|
@ -21,68 +21,50 @@ import logger from './logger';
|
||||||
export const getTrackState = state => state['features/base/tracks'];
|
export const getTrackState = state => state['features/base/tracks'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Higher-order function that returns a selector for a specific participant
|
* Checks if the passed media type is muted for the participant.
|
||||||
* and media type.
|
|
||||||
*
|
*
|
||||||
* @param {Object} participant - Participant reference.
|
* @param {Object} participant - Participant reference.
|
||||||
* @param {MEDIA_TYPE} mediaType - Media type.
|
* @param {MEDIA_TYPE} mediaType - Media type.
|
||||||
* @returns {Function} Selector.
|
* @param {Object} state - Global state.
|
||||||
|
* @returns {boolean} - Is the media type muted for the participant.
|
||||||
*/
|
*/
|
||||||
export const getIsParticipantMediaMuted = (participant, mediaType) =>
|
export function isParticipantMediaMuted(participant, mediaType, state) {
|
||||||
|
if (!participant) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
const tracks = getTrackState(state);
|
||||||
* Bound selector.
|
|
||||||
*
|
|
||||||
* @param {Object} state - Global state.
|
|
||||||
* @returns {boolean} Is the media type muted for the participant.
|
|
||||||
*/
|
|
||||||
state => {
|
|
||||||
if (!participant) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tracks = getTrackState(state);
|
if (participant?.local) {
|
||||||
|
return isLocalTrackMuted(tracks, mediaType);
|
||||||
|
} else if (!participant?.isFakeParticipant) {
|
||||||
|
return isRemoteTrackMuted(tracks, mediaType, participant.id);
|
||||||
|
}
|
||||||
|
|
||||||
if (participant?.local) {
|
return true;
|
||||||
return isLocalTrackMuted(tracks, mediaType);
|
}
|
||||||
} else if (!participant?.isFakeParticipant) {
|
|
||||||
return isRemoteTrackMuted(tracks, mediaType, participant.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
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.
|
* @param {Object} participant - Participant reference.
|
||||||
* @returns {Function} Selector.
|
* @param {Object} state - Global state.
|
||||||
|
* @returns {boolean} - Is audio muted for the participant.
|
||||||
*/
|
*/
|
||||||
export const getIsParticipantAudioMuted = participant =>
|
export function isParticipantAudioMuted(participant, state) {
|
||||||
|
return isParticipantMediaMuted(participant, MEDIA_TYPE.AUDIO, state);
|
||||||
/**
|
}
|
||||||
* Bound selector.
|
|
||||||
*
|
|
||||||
* @param {Object} state - Global state.
|
|
||||||
* @returns {boolean} Is audio muted for the participant.
|
|
||||||
*/
|
|
||||||
state => getIsParticipantMediaMuted(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.
|
* @param {Object} participant - Participant reference.
|
||||||
* @returns {Function} Selector.
|
* @param {Object} state - Global state.
|
||||||
|
* @returns {boolean} - Is video muted for the participant.
|
||||||
*/
|
*/
|
||||||
export const getIsParticipantVideoMuted = participant =>
|
export function isParticipantVideoMuted(participant, state) {
|
||||||
|
return isParticipantMediaMuted(participant, MEDIA_TYPE.VIDEO, state);
|
||||||
/**
|
}
|
||||||
* Bound selector.
|
|
||||||
*
|
|
||||||
* @param {Object} state - Global state.
|
|
||||||
* @returns {boolean} Is video muted for the participant.
|
|
||||||
*/
|
|
||||||
state => getIsParticipantMediaMuted(participant, MEDIA_TYPE.VIDEO)(state);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a local video track for presenter. The constraints are computed based
|
* 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,
|
_isModal: window.innerWidth <= SMALL_WIDTH_THRESHOLD,
|
||||||
_isOpen: isOpen,
|
_isOpen: isOpen,
|
||||||
_messages: messages,
|
_messages: messages,
|
||||||
_showNamePrompt: !_localParticipant.name
|
_showNamePrompt: !_localParticipant?.name
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -288,8 +288,7 @@ function _mapStateToProps(state, ownProps) {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
_configuredDisplayName: participant && participant.name,
|
_configuredDisplayName: participant && participant.name,
|
||||||
_nameToDisplay: getParticipantDisplayName(
|
_nameToDisplay: getParticipantDisplayName(state, participantID)
|
||||||
state, participantID)
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,3 +6,22 @@
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
export const TOGGLE_E2EE = 'TOGGLE_E2EE';
|
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
|
// @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.
|
* Dispatches an action to enable / disable E2EE.
|
||||||
|
@ -14,3 +14,35 @@ export function toggleE2EE(enabled: boolean) {
|
||||||
enabled
|
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}
|
* @returns {Props}
|
||||||
*/
|
*/
|
||||||
export function _mapStateToProps(state: Object) {
|
export function _mapStateToProps(state: Object) {
|
||||||
const participants = state['features/base/participants'];
|
|
||||||
|
|
||||||
return {
|
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 { createE2EEEvent, sendAnalytics } from '../../analytics';
|
||||||
import { translate } from '../../base/i18n';
|
import { translate } from '../../base/i18n';
|
||||||
import { getParticipants } from '../../base/participants';
|
|
||||||
import { Switch } from '../../base/react';
|
import { Switch } from '../../base/react';
|
||||||
import { connect } from '../../base/redux';
|
import { connect } from '../../base/redux';
|
||||||
import { toggleE2EE } from '../actions';
|
import { toggleE2EE } from '../actions';
|
||||||
|
@ -21,7 +20,7 @@ type Props = {
|
||||||
/**
|
/**
|
||||||
* Indicates whether all participants in the conference currently support E2EE.
|
* Indicates whether all participants in the conference currently support E2EE.
|
||||||
*/
|
*/
|
||||||
_everyoneSupportsE2EE: boolean,
|
_everyoneSupportE2EE: boolean,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The redux {@code dispatch} function.
|
* The redux {@code dispatch} function.
|
||||||
|
@ -96,7 +95,7 @@ class E2EESection extends Component<Props, State> {
|
||||||
* @returns {ReactElement}
|
* @returns {ReactElement}
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
const { _everyoneSupportsE2EE, t } = this.props;
|
const { _everyoneSupportE2EE, t } = this.props;
|
||||||
const { enabled, expand } = this.state;
|
const { enabled, expand } = this.state;
|
||||||
const description = t('dialog.e2eeDescription');
|
const description = t('dialog.e2eeDescription');
|
||||||
|
|
||||||
|
@ -120,7 +119,7 @@ class E2EESection extends Component<Props, State> {
|
||||||
</span> }
|
</span> }
|
||||||
</p>
|
</p>
|
||||||
{
|
{
|
||||||
!_everyoneSupportsE2EE
|
!_everyoneSupportE2EE
|
||||||
&& <span className = 'warning'>
|
&& <span className = 'warning'>
|
||||||
{ t('dialog.e2eeWarning') }
|
{ t('dialog.e2eeWarning') }
|
||||||
</span>
|
</span>
|
||||||
|
@ -195,12 +194,11 @@ class E2EESection extends Component<Props, State> {
|
||||||
* @returns {Props}
|
* @returns {Props}
|
||||||
*/
|
*/
|
||||||
function mapStateToProps(state) {
|
function mapStateToProps(state) {
|
||||||
const { enabled } = state['features/e2ee'];
|
const { enabled, everyoneSupportE2EE } = state['features/e2ee'];
|
||||||
const participants = getParticipants(state).filter(p => !p.local);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
_enabled: enabled,
|
_enabled: enabled,
|
||||||
_everyoneSupportsE2EE: participants.every(p => Boolean(p.e2eeSupported))
|
_everyoneSupportE2EE: everyoneSupportE2EE
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,24 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
|
import { batch } from 'react-redux';
|
||||||
|
|
||||||
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app';
|
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app';
|
||||||
import { getCurrentConference } from '../base/conference';
|
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 { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
|
||||||
import { playSound, registerSound, unregisterSound } from '../base/sounds';
|
import { playSound, registerSound, unregisterSound } from '../base/sounds';
|
||||||
|
|
||||||
import { TOGGLE_E2EE } from './actionTypes';
|
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 { E2EE_OFF_SOUND_ID, E2EE_ON_SOUND_ID } from './constants';
|
||||||
import logger from './logger';
|
import logger from './logger';
|
||||||
import { E2EE_OFF_SOUND_FILE, E2EE_ON_SOUND_FILE } from './sounds';
|
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));
|
dispatch(unregisterSound(E2EE_ON_SOUND_ID));
|
||||||
break;
|
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: {
|
case TOGGLE_E2EE: {
|
||||||
const conference = getCurrentConference(getState);
|
const conference = getCurrentConference(getState);
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,11 @@
|
||||||
|
|
||||||
import { ReducerRegistry } from '../base/redux';
|
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 = {
|
const DEFAULT_STATE = {
|
||||||
enabled: false
|
enabled: false
|
||||||
|
@ -18,6 +22,16 @@ ReducerRegistry.register('features/e2ee', (state = DEFAULT_STATE, action) => {
|
||||||
...state,
|
...state,
|
||||||
enabled: action.enabled
|
enabled: action.enabled
|
||||||
};
|
};
|
||||||
|
case SET_EVERYONE_ENABLED_E2EE:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
everyoneEnabledE2EE: action.everyoneEnabledE2EE
|
||||||
|
};
|
||||||
|
case SET_EVERYONE_SUPPORT_E2EE:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
everyoneSupportE2EE: action.everyoneSupportE2EE
|
||||||
|
};
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
import type { Dispatch } from 'redux';
|
import type { Dispatch } from 'redux';
|
||||||
|
|
||||||
import { pinParticipant } from '../base/participants';
|
import { getLocalParticipant, getRemoteParticipants, pinParticipant } from '../base/participants';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
SET_HORIZONTAL_VIEW_DIMENSIONS,
|
SET_HORIZONTAL_VIEW_DIMENSIONS,
|
||||||
|
@ -127,7 +127,8 @@ export function setHorizontalViewDimensions() {
|
||||||
*/
|
*/
|
||||||
export function clickOnVideo(n: number) {
|
export function clickOnVideo(n: number) {
|
||||||
return (dispatch: Function, getState: Function) => {
|
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 nThParticipant = participants[n];
|
||||||
const { id, pinned } = nThParticipant;
|
const { id, pinned } = nThParticipant;
|
||||||
|
|
||||||
|
|
|
@ -109,10 +109,10 @@ class Filmstrip extends Component<Props> {
|
||||||
{
|
{
|
||||||
|
|
||||||
this._sort(_participants, isNarrowAspectRatio)
|
this._sort(_participants, isNarrowAspectRatio)
|
||||||
.map(p => (
|
.map(id => (
|
||||||
<Thumbnail
|
<Thumbnail
|
||||||
key = { p.id }
|
key = { id }
|
||||||
participant = { p } />))
|
participantID = { id } />))
|
||||||
|
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
|
@ -166,12 +166,11 @@ class Filmstrip extends Component<Props> {
|
||||||
* @returns {Props}
|
* @returns {Props}
|
||||||
*/
|
*/
|
||||||
function _mapStateToProps(state) {
|
function _mapStateToProps(state) {
|
||||||
const participants = state['features/base/participants'];
|
const { enabled, remoteParticipants } = state['features/filmstrip'];
|
||||||
const { enabled } = state['features/filmstrip'];
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
_aspectRatio: state['features/base/responsive-ui'].aspectRatio,
|
_aspectRatio: state['features/base/responsive-ui'].aspectRatio,
|
||||||
_participants: participants.filter(p => !p.local),
|
_participants: remoteParticipants,
|
||||||
_visible: enabled && isFilmstripVisible(state)
|
_visible: enabled && isFilmstripVisible(state)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,63 +1,21 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import React, { Component } from 'react';
|
import React from 'react';
|
||||||
import { View } from 'react-native';
|
import { View } from 'react-native';
|
||||||
|
|
||||||
import { getLocalParticipant } from '../../../base/participants';
|
|
||||||
import { connect } from '../../../base/redux';
|
|
||||||
|
|
||||||
import Thumbnail from './Thumbnail';
|
import Thumbnail from './Thumbnail';
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
|
|
||||||
type Props = {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The local participant.
|
|
||||||
*/
|
|
||||||
_localParticipant: Object
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component to render a local thumbnail that can be separated from the
|
* Component to render a local thumbnail that can be separated from the
|
||||||
* remote thumbnails later.
|
* remote thumbnails later.
|
||||||
*/
|
|
||||||
class LocalThumbnail extends Component<Props> {
|
|
||||||
/**
|
|
||||||
* Implements React Component's render.
|
|
||||||
*
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
render() {
|
|
||||||
const { _localParticipant } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style = { styles.localThumbnail }>
|
|
||||||
<Thumbnail participant = { _localParticipant } />
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maps (parts of) the redux state to the associated {@code LocalThumbnail}'s
|
|
||||||
* props.
|
|
||||||
*
|
*
|
||||||
* @param {Object} state - The redux state.
|
* @returns {ReactElement}
|
||||||
* @private
|
|
||||||
* @returns {{
|
|
||||||
* _localParticipant: Participant
|
|
||||||
* }}
|
|
||||||
*/
|
*/
|
||||||
function _mapStateToProps(state) {
|
export default function LocalThumbnail() {
|
||||||
return {
|
return (
|
||||||
/**
|
<View style = { styles.localThumbnail }>
|
||||||
* The local participant.
|
<Thumbnail />
|
||||||
*
|
</View>
|
||||||
* @private
|
);
|
||||||
* @type {Participant}
|
|
||||||
*/
|
|
||||||
_localParticipant: getLocalParticipant(state)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(_mapStateToProps)(LocalThumbnail);
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { View } from 'react-native';
|
import { View } from 'react-native';
|
||||||
import type { Dispatch } from 'redux';
|
import type { Dispatch } from 'redux';
|
||||||
|
|
||||||
|
@ -12,7 +12,8 @@ import {
|
||||||
ParticipantView,
|
ParticipantView,
|
||||||
getParticipantCount,
|
getParticipantCount,
|
||||||
isEveryoneModerator,
|
isEveryoneModerator,
|
||||||
pinParticipant
|
pinParticipant,
|
||||||
|
getParticipantByIdOrUndefined
|
||||||
} from '../../../base/participants';
|
} from '../../../base/participants';
|
||||||
import { Container } from '../../../base/react';
|
import { Container } from '../../../base/react';
|
||||||
import { connect } from '../../../base/redux';
|
import { connect } from '../../../base/redux';
|
||||||
|
@ -48,14 +49,9 @@ type Props = {
|
||||||
_largeVideo: Object,
|
_largeVideo: Object,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles click/tap event on the thumbnail.
|
* The Redux representation of the participant to display.
|
||||||
*/
|
*/
|
||||||
_onClick: ?Function,
|
_participant: Object,
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles long press on the thumbnail.
|
|
||||||
*/
|
|
||||||
_onThumbnailLongPress: ?Function,
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether to show the dominant speaker indicator or not.
|
* Whether to show the dominant speaker indicator or not.
|
||||||
|
@ -90,9 +86,9 @@ type Props = {
|
||||||
dispatch: Dispatch<any>,
|
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.
|
* Whether to display or hide the display name of the participant in the thumbnail.
|
||||||
|
@ -120,14 +116,13 @@ function Thumbnail(props: Props) {
|
||||||
const {
|
const {
|
||||||
_audioMuted: audioMuted,
|
_audioMuted: audioMuted,
|
||||||
_largeVideo: largeVideo,
|
_largeVideo: largeVideo,
|
||||||
_onClick,
|
|
||||||
_onThumbnailLongPress,
|
|
||||||
_renderDominantSpeakerIndicator: renderDominantSpeakerIndicator,
|
_renderDominantSpeakerIndicator: renderDominantSpeakerIndicator,
|
||||||
_renderModeratorIndicator: renderModeratorIndicator,
|
_renderModeratorIndicator: renderModeratorIndicator,
|
||||||
|
_participant: participant,
|
||||||
_styles,
|
_styles,
|
||||||
_videoTrack: videoTrack,
|
_videoTrack: videoTrack,
|
||||||
|
dispatch,
|
||||||
disableTint,
|
disableTint,
|
||||||
participant,
|
|
||||||
renderDisplayName,
|
renderDisplayName,
|
||||||
tileView
|
tileView
|
||||||
} = props;
|
} = props;
|
||||||
|
@ -137,11 +132,29 @@ function Thumbnail(props: Props) {
|
||||||
= participantId === largeVideo.participantId;
|
= participantId === largeVideo.participantId;
|
||||||
const videoMuted = !videoTrack || videoTrack.muted;
|
const videoMuted = !videoTrack || videoTrack.muted;
|
||||||
const isScreenShare = videoTrack && videoTrack.videoType === VIDEO_TYPE.DESKTOP;
|
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 (
|
return (
|
||||||
<Container
|
<Container
|
||||||
onClick = { _onClick }
|
onClick = { onClick }
|
||||||
onLongPress = { _onThumbnailLongPress }
|
onLongPress = { onThumbnailLongPress }
|
||||||
style = { [
|
style = { [
|
||||||
styles.thumbnail,
|
styles.thumbnail,
|
||||||
participant.pinned && !tileView
|
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.
|
* 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.
|
// the stage i.e. as a large video.
|
||||||
const largeVideo = state['features/large-video'];
|
const largeVideo = state['features/large-video'];
|
||||||
const tracks = state['features/base/tracks'];
|
const tracks = state['features/base/tracks'];
|
||||||
const { participant } = ownProps;
|
const { participantID } = ownProps;
|
||||||
const id = participant.id;
|
const participant = getParticipantByIdOrUndefined(state, participantID);
|
||||||
|
const id = participant?.id;
|
||||||
const audioTrack
|
const audioTrack
|
||||||
= getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.AUDIO, id);
|
= getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.AUDIO, id);
|
||||||
const videoTrack
|
const videoTrack
|
||||||
= getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, id);
|
= getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, id);
|
||||||
const participantCount = getParticipantCount(state);
|
const participantCount = getParticipantCount(state);
|
||||||
const renderDominantSpeakerIndicator = participant.dominantSpeaker && participantCount > 2;
|
const renderDominantSpeakerIndicator = participant && participant.dominantSpeaker && participantCount > 2;
|
||||||
const _isEveryoneModerator = isEveryoneModerator(state);
|
const _isEveryoneModerator = isEveryoneModerator(state);
|
||||||
const renderModeratorIndicator = !_isEveryoneModerator && participant.role === PARTICIPANT_ROLE.MODERATOR;
|
const renderModeratorIndicator = !_isEveryoneModerator
|
||||||
|
&& participant && participant.role === PARTICIPANT_ROLE.MODERATOR;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
_audioMuted: audioTrack?.muted ?? true,
|
_audioMuted: audioTrack?.muted ?? true,
|
||||||
_largeVideo: largeVideo,
|
_largeVideo: largeVideo,
|
||||||
|
_participant: participant,
|
||||||
_renderDominantSpeakerIndicator: renderDominantSpeakerIndicator,
|
_renderDominantSpeakerIndicator: renderDominantSpeakerIndicator,
|
||||||
_renderModeratorIndicator: renderModeratorIndicator,
|
_renderModeratorIndicator: renderModeratorIndicator,
|
||||||
_styles: ColorSchemeRegistry.get(state, 'Thumbnail'),
|
_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';
|
} from 'react-native';
|
||||||
import type { Dispatch } from 'redux';
|
import type { Dispatch } from 'redux';
|
||||||
|
|
||||||
|
import { getLocalParticipant, getParticipantCountWithFake } from '../../../base/participants';
|
||||||
import { connect } from '../../../base/redux';
|
import { connect } from '../../../base/redux';
|
||||||
import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui/constants';
|
import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui/constants';
|
||||||
import { setTileViewDimensions } from '../../actions.native';
|
import { setTileViewDimensions } from '../../actions.native';
|
||||||
|
@ -15,6 +16,7 @@ import { setTileViewDimensions } from '../../actions.native';
|
||||||
import Thumbnail from './Thumbnail';
|
import Thumbnail from './Thumbnail';
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The type of the React {@link Component} props of {@link TileView}.
|
* The type of the React {@link Component} props of {@link TileView}.
|
||||||
*/
|
*/
|
||||||
|
@ -31,9 +33,19 @@ type Props = {
|
||||||
_height: number,
|
_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.
|
* Application's viewport height.
|
||||||
|
@ -131,7 +143,7 @@ class TileView extends Component<Props> {
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_getColumnCount() {
|
_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
|
// 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
|
// 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[]}
|
* @returns {Participant[]}
|
||||||
*/
|
*/
|
||||||
_getSortedParticipants() {
|
_getSortedParticipants() {
|
||||||
const participants = [];
|
const { _localParticipant, _remoteParticipants } = this.props;
|
||||||
let localParticipant;
|
const participants = [ ..._remoteParticipants ];
|
||||||
|
|
||||||
for (const participant of this.props._participants) {
|
_localParticipant && participants.push(_localParticipant.id);
|
||||||
if (participant.local) {
|
|
||||||
localParticipant = participant;
|
|
||||||
} else {
|
|
||||||
participants.push(participant);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
localParticipant && participants.push(localParticipant);
|
|
||||||
|
|
||||||
return participants;
|
return participants;
|
||||||
}
|
}
|
||||||
|
@ -178,16 +182,15 @@ class TileView extends Component<Props> {
|
||||||
* @returns {Object}
|
* @returns {Object}
|
||||||
*/
|
*/
|
||||||
_getTileDimensions() {
|
_getTileDimensions() {
|
||||||
const { _height, _participants, _width } = this.props;
|
const { _height, _participantCount, _width } = this.props;
|
||||||
const columns = this._getColumnCount();
|
const columns = this._getColumnCount();
|
||||||
const participantCount = _participants.length;
|
|
||||||
const heightToUse = _height - (MARGIN * 2);
|
const heightToUse = _height - (MARGIN * 2);
|
||||||
const widthToUse = _width - (MARGIN * 2);
|
const widthToUse = _width - (MARGIN * 2);
|
||||||
let tileWidth;
|
let tileWidth;
|
||||||
|
|
||||||
// If there is going to be at least two rows, ensure that at least two
|
// If there is going to be at least two rows, ensure that at least two
|
||||||
// rows display fully on screen.
|
// rows display fully on screen.
|
||||||
if (participantCount / columns > 1) {
|
if (_participantCount / columns > 1) {
|
||||||
tileWidth = Math.min(widthToUse / columns, heightToUse / 2);
|
tileWidth = Math.min(widthToUse / columns, heightToUse / 2);
|
||||||
} else {
|
} else {
|
||||||
tileWidth = Math.min(widthToUse / columns, heightToUse);
|
tileWidth = Math.min(widthToUse / columns, heightToUse);
|
||||||
|
@ -247,11 +250,11 @@ class TileView extends Component<Props> {
|
||||||
};
|
};
|
||||||
|
|
||||||
return this._getSortedParticipants()
|
return this._getSortedParticipants()
|
||||||
.map(participant => (
|
.map(id => (
|
||||||
<Thumbnail
|
<Thumbnail
|
||||||
disableTint = { true }
|
disableTint = { true }
|
||||||
key = { participant.id }
|
key = { id }
|
||||||
participant = { participant }
|
participantID = { id }
|
||||||
renderDisplayName = { true }
|
renderDisplayName = { true }
|
||||||
styleOverrides = { styleOverrides }
|
styleOverrides = { styleOverrides }
|
||||||
tileView = { true } />));
|
tileView = { true } />));
|
||||||
|
@ -285,11 +288,14 @@ class TileView extends Component<Props> {
|
||||||
*/
|
*/
|
||||||
function _mapStateToProps(state) {
|
function _mapStateToProps(state) {
|
||||||
const responsiveUi = state['features/base/responsive-ui'];
|
const responsiveUi = state['features/base/responsive-ui'];
|
||||||
|
const { remoteParticipants } = state['features/filmstrip'];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
_aspectRatio: responsiveUi.aspectRatio,
|
_aspectRatio: responsiveUi.aspectRatio,
|
||||||
_height: responsiveUi.clientHeight,
|
_height: responsiveUi.clientHeight,
|
||||||
_participants: state['features/base/participants'],
|
_localParticipant: getLocalParticipant(state),
|
||||||
|
_participantCount: getParticipantCountWithFake(state),
|
||||||
|
_remoteParticipants: remoteParticipants,
|
||||||
_width: responsiveUi.clientWidth
|
_width: responsiveUi.clientWidth
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
import { MEDIA_TYPE } from '../../../base/media';
|
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 { connect } from '../../../base/redux';
|
||||||
import { getTrackByMediaTypeAndParticipant, isLocalTrackMuted, isRemoteTrackMuted } from '../../../base/tracks';
|
import { getTrackByMediaTypeAndParticipant, isLocalTrackMuted, isRemoteTrackMuted } from '../../../base/tracks';
|
||||||
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
|
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
|
||||||
|
@ -111,7 +111,7 @@ function _mapStateToProps(state, ownProps) {
|
||||||
const { participantID } = ownProps;
|
const { participantID } = ownProps;
|
||||||
|
|
||||||
// Only the local participant won't have id for the time when the conference is not yet joined.
|
// 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'];
|
const tracks = state['features/base/tracks'];
|
||||||
let isVideoMuted = true;
|
let isVideoMuted = true;
|
||||||
|
|
|
@ -9,8 +9,7 @@ import { Avatar } from '../../../base/avatar';
|
||||||
import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
|
import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
|
||||||
import { MEDIA_TYPE, VideoTrack } from '../../../base/media';
|
import { MEDIA_TYPE, VideoTrack } from '../../../base/media';
|
||||||
import {
|
import {
|
||||||
getLocalParticipant,
|
getParticipantByIdOrUndefined,
|
||||||
getParticipantById,
|
|
||||||
getParticipantCount,
|
getParticipantCount,
|
||||||
pinParticipant
|
pinParticipant
|
||||||
} from '../../../base/participants';
|
} from '../../../base/participants';
|
||||||
|
@ -1012,9 +1011,8 @@ class Thumbnail extends Component<Props, State> {
|
||||||
function _mapStateToProps(state, ownProps): Object {
|
function _mapStateToProps(state, ownProps): Object {
|
||||||
const { participantID } = ownProps;
|
const { participantID } = ownProps;
|
||||||
|
|
||||||
// Only the local participant won't have id for the time when the conference is not yet joined.
|
const participant = getParticipantByIdOrUndefined(state, participantID);
|
||||||
const participant = participantID ? getParticipantById(state, participantID) : getLocalParticipant(state);
|
const id = participant?.id;
|
||||||
const { id } = participant;
|
|
||||||
const isLocal = participant?.local ?? true;
|
const isLocal = participant?.local ?? true;
|
||||||
const tracks = state['features/base/tracks'];
|
const tracks = state['features/base/tracks'];
|
||||||
const { participantsVolume } = state['features/filmstrip'];
|
const { participantsVolume } = state['features/filmstrip'];
|
||||||
|
@ -1085,14 +1083,14 @@ function _mapStateToProps(state, ownProps): Object {
|
||||||
_isDominantSpeakerDisabled: interfaceConfig.DISABLE_DOMINANT_SPEAKER_INDICATOR,
|
_isDominantSpeakerDisabled: interfaceConfig.DISABLE_DOMINANT_SPEAKER_INDICATOR,
|
||||||
_isScreenSharing: _videoTrack?.videoType === 'desktop',
|
_isScreenSharing: _videoTrack?.videoType === 'desktop',
|
||||||
_isTestModeEnabled: isTestModeEnabled(state),
|
_isTestModeEnabled: isTestModeEnabled(state),
|
||||||
_isVideoPlayable: isVideoPlayable(state, id),
|
_isVideoPlayable: id && isVideoPlayable(state, id),
|
||||||
_indicatorIconSize: NORMAL,
|
_indicatorIconSize: NORMAL,
|
||||||
_localFlipX: Boolean(localFlipX),
|
_localFlipX: Boolean(localFlipX),
|
||||||
_participant: participant,
|
_participant: participant,
|
||||||
_participantCountMoreThan2: getParticipantCount(state) > 2,
|
_participantCountMoreThan2: getParticipantCount(state) > 2,
|
||||||
_startSilent: Boolean(startSilent),
|
_startSilent: Boolean(startSilent),
|
||||||
_videoTrack,
|
_videoTrack,
|
||||||
_volume: isLocal ? undefined : participantsVolume[id],
|
_volume: isLocal ? undefined : id ? participantsVolume[id] : undefined,
|
||||||
...size
|
...size
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import { getFeatureFlag, FILMSTRIP_ENABLED } from '../base/flags';
|
import { getFeatureFlag, FILMSTRIP_ENABLED } from '../base/flags';
|
||||||
|
import { getParticipantCountWithFake } from '../base/participants';
|
||||||
import { toState } from '../base/redux';
|
import { toState } from '../base/redux';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -22,7 +23,5 @@ export function isFilmstripVisible(stateful: Object | Function) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { length: participantCount } = state['features/base/participants'];
|
return getParticipantCountWithFake(state) > 1;
|
||||||
|
|
||||||
return participantCount > 1;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
|
import { getParticipantCountWithFake } from '../base/participants';
|
||||||
import { StateListenerRegistry, equals } from '../base/redux';
|
import { StateListenerRegistry, equals } from '../base/redux';
|
||||||
import { clientResized } from '../base/responsive-ui';
|
import { clientResized } from '../base/responsive-ui';
|
||||||
import { setFilmstripVisible } from '../filmstrip/actions';
|
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.
|
* Listens for changes in the number of participants to calculate the dimensions of the tile view grid and the tiles.
|
||||||
*/
|
*/
|
||||||
StateListenerRegistry.register(
|
StateListenerRegistry.register(
|
||||||
/* selector */ state => state['features/base/participants'].length,
|
/* selector */ getParticipantCountWithFake,
|
||||||
/* listener */ (numberOfParticipants, store) => {
|
/* listener */ (numberOfParticipants, store) => {
|
||||||
const state = store.getState();
|
const state = store.getState();
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
import type { Dispatch } from 'redux';
|
import type { Dispatch } from 'redux';
|
||||||
|
|
||||||
import { getInviteURL } from '../base/connection';
|
import { getInviteURL } from '../base/connection';
|
||||||
import { getLocalParticipant, getParticipants } from '../base/participants';
|
import { getLocalParticipant, getParticipantCount } from '../base/participants';
|
||||||
import { inviteVideoRooms } from '../videosipgw';
|
import { inviteVideoRooms } from '../videosipgw';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -71,14 +71,14 @@ export function invite(
|
||||||
dispatch: Dispatch<any>,
|
dispatch: Dispatch<any>,
|
||||||
getState: Function): Promise<Array<Object>> => {
|
getState: Function): Promise<Array<Object>> => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const participants = getParticipants(state);
|
const participantsCount = getParticipantCount(state);
|
||||||
const { calleeInfoVisible } = state['features/invite'];
|
const { calleeInfoVisible } = state['features/invite'];
|
||||||
|
|
||||||
if (showCalleeInfo
|
if (showCalleeInfo
|
||||||
&& !calleeInfoVisible
|
&& !calleeInfoVisible
|
||||||
&& invitees.length === 1
|
&& invitees.length === 1
|
||||||
&& invitees[0].type === INVITE_TYPES.USER
|
&& invitees[0].type === INVITE_TYPES.USER
|
||||||
&& participants.length === 1) {
|
&& participantsCount === 1) {
|
||||||
dispatch(setCalleeInfoVisible(true, invitees[0]));
|
dispatch(setCalleeInfoVisible(true, invitees[0]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,9 +5,9 @@ import React, { Component } from 'react';
|
||||||
import { Avatar } from '../../../base/avatar';
|
import { Avatar } from '../../../base/avatar';
|
||||||
import { MEDIA_TYPE } from '../../../base/media';
|
import { MEDIA_TYPE } from '../../../base/media';
|
||||||
import {
|
import {
|
||||||
getParticipants,
|
|
||||||
getParticipantDisplayName,
|
getParticipantDisplayName,
|
||||||
getParticipantPresenceStatus
|
getParticipantPresenceStatus,
|
||||||
|
getRemoteParticipants
|
||||||
} from '../../../base/participants';
|
} from '../../../base/participants';
|
||||||
import { Container, Text } from '../../../base/react';
|
import { Container, Text } from '../../../base/react';
|
||||||
import { connect } from '../../../base/redux';
|
import { connect } from '../../../base/redux';
|
||||||
|
@ -135,20 +135,20 @@ class CalleeInfo extends Component<Props> {
|
||||||
function _mapStateToProps(state) {
|
function _mapStateToProps(state) {
|
||||||
const _isVideoMuted
|
const _isVideoMuted
|
||||||
= isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.VIDEO);
|
= isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.VIDEO);
|
||||||
const poltergeist
|
|
||||||
= getParticipants(state).find(p => p.botType === 'poltergeist');
|
|
||||||
|
|
||||||
if (poltergeist) {
|
// This would be expensive for big calls but the component will be mounted only when there are up
|
||||||
const { id } = poltergeist;
|
// to 3 participants in the call.
|
||||||
|
for (const [ id, p ] of getRemoteParticipants(state)) {
|
||||||
return {
|
if (p.botType === 'poltergeist') {
|
||||||
_callee: {
|
return {
|
||||||
id,
|
_callee: {
|
||||||
name: getParticipantDisplayName(state, id),
|
id,
|
||||||
status: getParticipantPresenceStatus(state, id)
|
name: getParticipantDisplayName(state, id),
|
||||||
},
|
status: getParticipantPresenceStatus(state, id)
|
||||||
_isVideoMuted
|
},
|
||||||
};
|
_isVideoMuted
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -6,8 +6,9 @@ import {
|
||||||
} from '../base/conference';
|
} from '../base/conference';
|
||||||
import {
|
import {
|
||||||
getLocalParticipant,
|
getLocalParticipant,
|
||||||
|
getParticipantCount,
|
||||||
getParticipantPresenceStatus,
|
getParticipantPresenceStatus,
|
||||||
getParticipants,
|
getRemoteParticipants,
|
||||||
PARTICIPANT_JOINED,
|
PARTICIPANT_JOINED,
|
||||||
PARTICIPANT_JOINED_SOUND_ID,
|
PARTICIPANT_JOINED_SOUND_ID,
|
||||||
PARTICIPANT_LEFT,
|
PARTICIPANT_LEFT,
|
||||||
|
@ -167,13 +168,19 @@ function _maybeHideCalleeInfo(action, store) {
|
||||||
if (!state['features/invite'].calleeInfoVisible) {
|
if (!state['features/invite'].calleeInfoVisible) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const participants = getParticipants(state);
|
const participants = getRemoteParticipants(state);
|
||||||
const numberOfPoltergeists
|
const participantCount = getParticipantCount(state);
|
||||||
= participants.filter(p => p.botType === 'poltergeist').length;
|
let numberOfPoltergeists = 0;
|
||||||
const numberOfRealParticipants = participants.length - numberOfPoltergeists;
|
|
||||||
|
participants.forEach(p => {
|
||||||
|
if (p.botType === 'poltergeist') {
|
||||||
|
numberOfPoltergeists++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const numberOfRealParticipants = participantCount - numberOfPoltergeists;
|
||||||
|
|
||||||
if ((numberOfPoltergeists > 1 || numberOfRealParticipants > 1)
|
if ((numberOfPoltergeists > 1 || numberOfRealParticipants > 1)
|
||||||
|| (action.type === PARTICIPANT_LEFT && participants.length === 1)) {
|
|| (action.type === PARTICIPANT_LEFT && participantCount === 1)) {
|
||||||
store.dispatch(setCalleeInfoVisible(false));
|
store.dispatch(setCalleeInfoVisible(false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,12 @@
|
||||||
import type { Dispatch } from 'redux';
|
import type { Dispatch } from 'redux';
|
||||||
|
|
||||||
import { MEDIA_TYPE } from '../base/media';
|
import { MEDIA_TYPE } from '../base/media';
|
||||||
|
import {
|
||||||
|
getDominantSpeakerParticipant,
|
||||||
|
getLocalParticipant,
|
||||||
|
getPinnedParticipant,
|
||||||
|
getRemoteParticipants
|
||||||
|
} from '../base/participants';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
SELECT_LARGE_VIDEO_PARTICIPANT,
|
SELECT_LARGE_VIDEO_PARTICIPANT,
|
||||||
|
@ -92,8 +98,7 @@ function _electLastVisibleRemoteVideo(tracks) {
|
||||||
function _electParticipantInLargeVideo(state) {
|
function _electParticipantInLargeVideo(state) {
|
||||||
// 1. If a participant is pinned, they will be shown in the LargeVideo
|
// 1. If a participant is pinned, they will be shown in the LargeVideo
|
||||||
// (regardless of whether they are local or remote).
|
// (regardless of whether they are local or remote).
|
||||||
const participants = state['features/base/participants'];
|
let participant = getPinnedParticipant(state);
|
||||||
let participant = participants.find(p => p.pinned);
|
|
||||||
|
|
||||||
if (participant) {
|
if (participant) {
|
||||||
return participant.id;
|
return participant.id;
|
||||||
|
@ -107,11 +112,14 @@ function _electParticipantInLargeVideo(state) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Next, pick the dominant speaker (other than self).
|
// 3. Next, pick the dominant speaker (other than self).
|
||||||
participant = participants.find(p => p.dominantSpeaker && !p.local);
|
participant = getDominantSpeakerParticipant(state);
|
||||||
if (participant) {
|
if (participant && !participant.local) {
|
||||||
return participant.id;
|
return participant.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// In case this is the local participant.
|
||||||
|
participant = undefined;
|
||||||
|
|
||||||
// 4. Next, pick the most recent participant with video.
|
// 4. Next, pick the most recent participant with video.
|
||||||
const tracks = state['features/base/tracks'];
|
const tracks = state['features/base/tracks'];
|
||||||
const videoTrack = _electLastVisibleRemoteVideo(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
|
// 5. As a last resort, select the participant that joined last (other than poltergist or other bot type
|
||||||
// participants).
|
// participants).
|
||||||
|
|
||||||
|
const participants = [ ...getRemoteParticipants(state).values() ];
|
||||||
|
|
||||||
for (let i = participants.length; i > 0 && !participant; i--) {
|
for (let i = participants.length; i > 0 && !participant; i--) {
|
||||||
const p = participants[i - 1];
|
const p = participants[i - 1];
|
||||||
|
|
||||||
|
@ -131,5 +142,5 @@ function _electParticipantInLargeVideo(state) {
|
||||||
return participant.id;
|
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 { JitsiConferenceEvents } from '../../base/lib-jitsi-meet';
|
||||||
import { MEDIA_TYPE } from '../../base/media';
|
import { MEDIA_TYPE } from '../../base/media';
|
||||||
import { SET_AUDIO_MUTED, SET_VIDEO_MUTED } from '../../base/media/actionTypes';
|
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 { MiddlewareRegistry, StateListenerRegistry } from '../../base/redux';
|
||||||
import { toggleScreensharing } from '../../base/tracks';
|
import { toggleScreensharing } from '../../base/tracks';
|
||||||
import { OPEN_CHAT, CLOSE_CHAT } from '../../chat';
|
import { OPEN_CHAT, CLOSE_CHAT } from '../../chat';
|
||||||
|
@ -268,6 +274,24 @@ StateListenerRegistry.register(
|
||||||
|
|
||||||
}, 100));
|
}, 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.
|
* Registers for events sent from the native side via NativeEventEmitter.
|
||||||
*
|
*
|
||||||
|
@ -309,16 +333,15 @@ function _registerForNativeEvents(store) {
|
||||||
|
|
||||||
eventEmitter.addListener(ExternalAPI.RETRIEVE_PARTICIPANTS_INFO, ({ requestId }) => {
|
eventEmitter.addListener(ExternalAPI.RETRIEVE_PARTICIPANTS_INFO, ({ requestId }) => {
|
||||||
|
|
||||||
const participantsInfo = getParticipants(store).map(participant => {
|
const participantsInfo = [];
|
||||||
return {
|
const remoteParticipants = getRemoteParticipants(store);
|
||||||
isLocal: participant.local,
|
const localParticipant = getLocalParticipant(store);
|
||||||
email: participant.email,
|
|
||||||
name: participant.name,
|
participantsInfo.push(_participantToParticipantInfo(localParticipant));
|
||||||
participantId: participant.id,
|
remoteParticipants.forEach(participant => {
|
||||||
displayName: participant.displayName,
|
if (!participant.isFakeParticipant) {
|
||||||
avatarUrl: participant.avatarURL,
|
participantsInfo.push(_participantToParticipantInfo(participant));
|
||||||
role: participant.role
|
}
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
sendEvent(
|
sendEvent(
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
|
|
||||||
import { approveParticipant } from '../../av-moderation/actions';
|
import { approveParticipant } from '../../av-moderation/actions';
|
||||||
|
@ -10,6 +9,11 @@ import { QuickActionButton } from './styled';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The translated ask unmute text.
|
||||||
|
*/
|
||||||
|
askUnmuteText: string,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Participant id.
|
* Participant id.
|
||||||
*/
|
*/
|
||||||
|
@ -22,10 +26,8 @@ type Props = {
|
||||||
* @param {Object} participant - Participant reference.
|
* @param {Object} participant - Participant reference.
|
||||||
* @returns {React$Element<'button'>}
|
* @returns {React$Element<'button'>}
|
||||||
*/
|
*/
|
||||||
export default function({ id }: Props) {
|
export default function AskToUnmuteButton({ id, askUnmuteText }: Props) {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const askToUnmute = useCallback(() => {
|
const askToUnmute = useCallback(() => {
|
||||||
dispatch(approveParticipant(id));
|
dispatch(approveParticipant(id));
|
||||||
}, [ dispatch, id ]);
|
}, [ dispatch, id ]);
|
||||||
|
@ -37,7 +39,7 @@ export default function({ id }: Props) {
|
||||||
theme = {{
|
theme = {{
|
||||||
panePadding: 16
|
panePadding: 16
|
||||||
}}>
|
}}>
|
||||||
{t('participantsPane.actions.askUnmute')}
|
{ askUnmuteText }
|
||||||
</QuickActionButton>
|
</QuickActionButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,11 +6,17 @@ import { useTranslation } from 'react-i18next';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
|
||||||
import { requestDisableModeration, requestEnableModeration } from '../../av-moderation/actions';
|
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 { openDialog } from '../../base/dialog';
|
||||||
import { Icon, IconCheck, IconVideoOff } from '../../base/icons';
|
import { Icon, IconCheck, IconVideoOff } from '../../base/icons';
|
||||||
import { MEDIA_TYPE } from '../../base/media';
|
import { MEDIA_TYPE } from '../../base/media';
|
||||||
import { getLocalParticipant } from '../../base/participants';
|
import {
|
||||||
|
getLocalParticipant,
|
||||||
|
isEveryoneModerator
|
||||||
|
} from '../../base/participants';
|
||||||
import { MuteEveryonesVideoDialog } from '../../video-menu/components';
|
import { MuteEveryonesVideoDialog } from '../../video-menu/components';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -49,6 +55,8 @@ type Props = {
|
||||||
|
|
||||||
export const FooterContextMenu = ({ onMouseLeave }: Props) => {
|
export const FooterContextMenu = ({ onMouseLeave }: Props) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const isModerationSupported = useSelector(isAvModerationSupported());
|
||||||
|
const allModerators = useSelector(isEveryoneModerator);
|
||||||
const isModerationEnabled = useSelector(isAvModerationEnabled(MEDIA_TYPE.AUDIO));
|
const isModerationEnabled = useSelector(isAvModerationEnabled(MEDIA_TYPE.AUDIO));
|
||||||
const { id } = useSelector(getLocalParticipant);
|
const { id } = useSelector(getLocalParticipant);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
@ -75,27 +83,32 @@ export const FooterContextMenu = ({ onMouseLeave }: Props) => {
|
||||||
<span>{ t('participantsPane.actions.stopEveryonesVideo') }</span>
|
<span>{ t('participantsPane.actions.stopEveryonesVideo') }</span>
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
|
|
||||||
<div className = { classes.text }>
|
{ isModerationSupported && !allModerators ? (
|
||||||
{t('participantsPane.actions.allow')}
|
<>
|
||||||
</div>
|
<div className = { classes.text }>
|
||||||
{ isModerationEnabled ? (
|
{t('participantsPane.actions.allow')}
|
||||||
<ContextMenuItem
|
</div>
|
||||||
id = 'participants-pane-context-menu-start-moderation'
|
{ isModerationEnabled ? (
|
||||||
onClick = { disable }>
|
<ContextMenuItem
|
||||||
<span className = { classes.paddedAction }>
|
id = 'participants-pane-context-menu-start-moderation'
|
||||||
{ t('participantsPane.actions.startModeration') }
|
onClick = { disable }>
|
||||||
</span>
|
<span className = { classes.paddedAction }>
|
||||||
</ContextMenuItem>
|
{ t('participantsPane.actions.startModeration') }
|
||||||
) : (
|
</span>
|
||||||
<ContextMenuItem
|
</ContextMenuItem>
|
||||||
id = 'participants-pane-context-menu-stop-moderation'
|
) : (
|
||||||
onClick = { enable }>
|
<ContextMenuItem
|
||||||
<Icon
|
id = 'participants-pane-context-menu-stop-moderation'
|
||||||
size = { 20 }
|
onClick = { enable }>
|
||||||
src = { IconCheck } />
|
<Icon
|
||||||
<span>{ t('participantsPane.actions.startModeration') }</span>
|
size = { 20 }
|
||||||
</ContextMenuItem>
|
src = { IconCheck } />
|
||||||
)}
|
<span>{ t('participantsPane.actions.startModeration') }</span>
|
||||||
|
</ContextMenuItem>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { useDispatch } from 'react-redux';
|
||||||
import { approveKnockingParticipant, rejectKnockingParticipant } from '../../lobby/actions';
|
import { approveKnockingParticipant, rejectKnockingParticipant } from '../../lobby/actions';
|
||||||
import { ACTION_TRIGGER, MEDIA_STATE } from '../constants';
|
import { ACTION_TRIGGER, MEDIA_STATE } from '../constants';
|
||||||
|
|
||||||
import { ParticipantItem } from './ParticipantItem';
|
import ParticipantItem from './ParticipantItem';
|
||||||
import { ParticipantActionButton } from './styled';
|
import { ParticipantActionButton } from './styled';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -28,9 +28,12 @@ export const LobbyParticipantItem = ({ participant: p }: Props) => {
|
||||||
<ParticipantItem
|
<ParticipantItem
|
||||||
actionsTrigger = { ACTION_TRIGGER.PERMANENT }
|
actionsTrigger = { ACTION_TRIGGER.PERMANENT }
|
||||||
audioMediaState = { MEDIA_STATE.NONE }
|
audioMediaState = { MEDIA_STATE.NONE }
|
||||||
name = { p.name }
|
displayName = { p.name }
|
||||||
participant = { p }
|
local = { p.local }
|
||||||
videoMuteState = { MEDIA_STATE.NONE }>
|
participantID = { p.id }
|
||||||
|
raisedHand = { p.raisedHand }
|
||||||
|
videoMuteState = { MEDIA_STATE.NONE }
|
||||||
|
youText = { t('chat.you') }>
|
||||||
<ParticipantActionButton
|
<ParticipantActionButton
|
||||||
onClick = { reject }>
|
onClick = { reject }>
|
||||||
{t('lobby.reject')}
|
{t('lobby.reject')}
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import React, { useCallback, useLayoutEffect, useRef, useState } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
|
||||||
|
|
||||||
import { isToolbarButtonEnabled } from '../../base/config/functions.web';
|
import { isToolbarButtonEnabled } from '../../base/config/functions.web';
|
||||||
import { openDialog } from '../../base/dialog';
|
import { openDialog } from '../../base/dialog';
|
||||||
|
import { translate } from '../../base/i18n';
|
||||||
import {
|
import {
|
||||||
IconCloseCircle,
|
IconCloseCircle,
|
||||||
IconCrown,
|
IconCrown,
|
||||||
|
@ -14,8 +13,13 @@ import {
|
||||||
IconMuteEveryoneElse,
|
IconMuteEveryoneElse,
|
||||||
IconVideoOff
|
IconVideoOff
|
||||||
} from '../../base/icons';
|
} from '../../base/icons';
|
||||||
import { isLocalParticipantModerator, isParticipantModerator } from '../../base/participants';
|
import {
|
||||||
import { getIsParticipantAudioMuted, getIsParticipantVideoMuted } from '../../base/tracks';
|
getParticipantByIdOrUndefined,
|
||||||
|
isLocalParticipantModerator,
|
||||||
|
isParticipantModerator
|
||||||
|
} from '../../base/participants';
|
||||||
|
import { connect } from '../../base/redux';
|
||||||
|
import { isParticipantAudioMuted, isParticipantVideoMuted } from '../../base/tracks';
|
||||||
import { openChat } from '../../chat/actions';
|
import { openChat } from '../../chat/actions';
|
||||||
import { GrantModeratorDialog, KickRemoteParticipantDialog, MuteEveryoneDialog } from '../../video-menu';
|
import { GrantModeratorDialog, KickRemoteParticipantDialog, MuteEveryoneDialog } from '../../video-menu';
|
||||||
import MuteRemoteParticipantsVideoDialog from '../../video-menu/components/web/MuteRemoteParticipantsVideoDialog';
|
import MuteRemoteParticipantsVideoDialog from '../../video-menu/components/web/MuteRemoteParticipantsVideoDialog';
|
||||||
|
@ -31,6 +35,41 @@ import {
|
||||||
|
|
||||||
type Props = {
|
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.
|
* Callback used to open a confirmation dialog for audio muting.
|
||||||
*/
|
*/
|
||||||
|
@ -57,35 +96,145 @@ type Props = {
|
||||||
onSelect: Function,
|
onSelect: Function,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Participant reference
|
* The ID of the participant.
|
||||||
*/
|
*/
|
||||||
participant: Object
|
participantID: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The translate function.
|
||||||
|
*/
|
||||||
|
t: Function
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MeetingParticipantContextMenu = ({
|
type State = {
|
||||||
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();
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
/**
|
||||||
if (participant
|
* If true the context menu will be hidden.
|
||||||
&& containerRef.current
|
*/
|
||||||
|
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
|
||||||
&& offsetTarget.offsetParent instanceof HTMLElement
|
&& offsetTarget.offsetParent instanceof HTMLElement
|
||||||
) {
|
) {
|
||||||
const { current: container } = containerRef;
|
const { current: container } = this._containerRef;
|
||||||
const { offsetTop, offsetParent: { offsetHeight, scrollTop } } = offsetTarget;
|
const { offsetTop, offsetParent: { offsetHeight, scrollTop } } = offsetTarget;
|
||||||
const outerHeight = getComputedOuterHeight(container);
|
const outerHeight = getComputedOuterHeight(container);
|
||||||
|
|
||||||
|
@ -93,97 +242,158 @@ export const MeetingParticipantContextMenu = ({
|
||||||
? offsetTop - outerHeight
|
? offsetTop - outerHeight
|
||||||
: offsetTop;
|
: offsetTop;
|
||||||
|
|
||||||
setIsHidden(false);
|
this.setState({ isHidden: false });
|
||||||
} else {
|
} else {
|
||||||
setIsHidden(true);
|
this.setState({ isHidden: true });
|
||||||
}
|
}
|
||||||
}, [ participant, offsetTarget ]);
|
|
||||||
|
|
||||||
const grantModerator = useCallback(() => {
|
|
||||||
dispatch(openDialog(GrantModeratorDialog, {
|
|
||||||
participantID: participant.id
|
|
||||||
}));
|
|
||||||
}, [ dispatch, participant ]);
|
|
||||||
|
|
||||||
const kick = useCallback(() => {
|
|
||||||
dispatch(openDialog(KickRemoteParticipantDialog, {
|
|
||||||
participantID: participant.id
|
|
||||||
}));
|
|
||||||
}, [ dispatch, participant ]);
|
|
||||||
|
|
||||||
const muteEveryoneElse = useCallback(() => {
|
|
||||||
dispatch(openDialog(MuteEveryoneDialog, {
|
|
||||||
exclude: [ participant.id ]
|
|
||||||
}));
|
|
||||||
}, [ dispatch, participant ]);
|
|
||||||
|
|
||||||
const muteVideo = useCallback(() => {
|
|
||||||
dispatch(openDialog(MuteRemoteParticipantsVideoDialog, {
|
|
||||||
participantID: participant.id
|
|
||||||
}));
|
|
||||||
}, [ dispatch, participant ]);
|
|
||||||
|
|
||||||
const sendPrivateMessage = useCallback(() => {
|
|
||||||
dispatch(openChat(participant));
|
|
||||||
}, [ dispatch, participant ]);
|
|
||||||
|
|
||||||
if (!participant) {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
/**
|
||||||
<ContextMenu
|
* Implements React Component's componentDidMount.
|
||||||
className = { ignoredChildClassName }
|
*
|
||||||
innerRef = { containerRef }
|
* @inheritdoc
|
||||||
isHidden = { isHidden }
|
* @returns {void}
|
||||||
onClick = { onSelect }
|
*/
|
||||||
onMouseEnter = { onEnter }
|
componentDidMount() {
|
||||||
onMouseLeave = { onLeave }>
|
this._position();
|
||||||
<ContextMenuItemGroup>
|
}
|
||||||
{isLocalModerator && (
|
|
||||||
<>
|
|
||||||
{!isParticipantAudioMuted
|
|
||||||
&& <ContextMenuItem onClick = { muteAudio(participant) }>
|
|
||||||
<ContextMenuIcon src = { IconMicDisabled } />
|
|
||||||
<span>{t('dialog.muteParticipantButton')}</span>
|
|
||||||
</ContextMenuItem>}
|
|
||||||
|
|
||||||
<ContextMenuItem onClick = { muteEveryoneElse }>
|
/**
|
||||||
<ContextMenuIcon src = { IconMuteEveryoneElse } />
|
* Implements React Component's componentDidUpdate.
|
||||||
<span>{t('toolbar.accessibilityLabel.muteEveryoneElse')}</span>
|
*
|
||||||
</ContextMenuItem>
|
* @inheritdoc
|
||||||
</>
|
*/
|
||||||
)}
|
componentDidUpdate(prevProps: Props) {
|
||||||
|
if (prevProps.offsetTarget !== this.props.offsetTarget || prevProps._participant !== this.props._participant) {
|
||||||
|
this._position();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
{isLocalModerator && (isParticipantVideoMuted || (
|
/**
|
||||||
<ContextMenuItem onClick = { muteVideo }>
|
* Implements React's {@link Component#render()}.
|
||||||
<ContextMenuIcon src = { IconVideoOff } />
|
*
|
||||||
<span>{t('participantsPane.actions.stopVideo')}</span>
|
* @inheritdoc
|
||||||
</ContextMenuItem>
|
* @returns {ReactElement}
|
||||||
))}
|
*/
|
||||||
</ContextMenuItemGroup>
|
render() {
|
||||||
|
const {
|
||||||
|
_isLocalModerator,
|
||||||
|
_isChatButtonEnabled,
|
||||||
|
_isParticipantModerator,
|
||||||
|
_isParticipantVideoMuted,
|
||||||
|
_isParticipantAudioMuted,
|
||||||
|
_participant,
|
||||||
|
onEnter,
|
||||||
|
onLeave,
|
||||||
|
onSelect,
|
||||||
|
muteAudio,
|
||||||
|
t
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
<ContextMenuItemGroup>
|
if (!_participant) {
|
||||||
{isLocalModerator && (
|
return null;
|
||||||
<>
|
}
|
||||||
{!isParticipantModerator(participant)
|
|
||||||
&& <ContextMenuItem onClick = { grantModerator }>
|
return (
|
||||||
<ContextMenuIcon src = { IconCrown } />
|
<ContextMenu
|
||||||
<span>{t('toolbar.accessibilityLabel.grantModerator')}</span>
|
className = { ignoredChildClassName }
|
||||||
</ContextMenuItem>}
|
innerRef = { this._containerRef }
|
||||||
<ContextMenuItem onClick = { kick }>
|
isHidden = { this.state.isHidden }
|
||||||
<ContextMenuIcon src = { IconCloseCircle } />
|
onClick = { onSelect }
|
||||||
<span>{t('videothumbnail.kick')}</span>
|
onMouseEnter = { onEnter }
|
||||||
</ContextMenuItem>
|
onMouseLeave = { onLeave }>
|
||||||
</>
|
<ContextMenuItemGroup>
|
||||||
)}
|
{
|
||||||
{isChatButtonEnabled && (
|
_isLocalModerator && (
|
||||||
<ContextMenuItem onClick = { sendPrivateMessage }>
|
<>
|
||||||
<ContextMenuIcon src = { IconMessage } />
|
{
|
||||||
<span>{t('toolbar.accessibilityLabel.privateMessage')}</span>
|
!_isParticipantAudioMuted
|
||||||
</ContextMenuItem>
|
&& <ContextMenuItem onClick = { muteAudio(_participant) }>
|
||||||
)}
|
<ContextMenuIcon src = { IconMicDisabled } />
|
||||||
</ContextMenuItemGroup>
|
<span>{t('dialog.muteParticipantButton')}</span>
|
||||||
</ContextMenu>
|
</ContextMenuItem>
|
||||||
);
|
}
|
||||||
};
|
|
||||||
|
<ContextMenuItem onClick = { this._onMuteEveryoneElse }>
|
||||||
|
<ContextMenuIcon src = { IconMuteEveryoneElse } />
|
||||||
|
<span>{t('toolbar.accessibilityLabel.muteEveryoneElse')}</span>
|
||||||
|
</ContextMenuItem>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
_isLocalModerator && (
|
||||||
|
_isParticipantVideoMuted || (
|
||||||
|
<ContextMenuItem onClick = { this._onMuteVideo }>
|
||||||
|
<ContextMenuIcon src = { IconVideoOff } />
|
||||||
|
<span>{t('participantsPane.actions.stopVideo')}</span>
|
||||||
|
</ContextMenuItem>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</ContextMenuItemGroup>
|
||||||
|
|
||||||
|
<ContextMenuItemGroup>
|
||||||
|
{
|
||||||
|
_isLocalModerator && (
|
||||||
|
<>
|
||||||
|
{
|
||||||
|
!_isParticipantModerator && (
|
||||||
|
<ContextMenuItem onClick = { this._onGrantModerator }>
|
||||||
|
<ContextMenuIcon src = { IconCrown } />
|
||||||
|
<span>{t('toolbar.accessibilityLabel.grantModerator')}</span>
|
||||||
|
</ContextMenuItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<ContextMenuItem onClick = { this._onKick }>
|
||||||
|
<ContextMenuIcon src = { IconCloseCircle } />
|
||||||
|
<span>{ t('videothumbnail.kick') }</span>
|
||||||
|
</ContextMenuItem>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
_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
|
// @flow
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
|
|
||||||
import { getIsParticipantAudioMuted, getIsParticipantVideoMuted } from '../../base/tracks';
|
import { getParticipantByIdOrUndefined, getParticipantDisplayName } from '../../base/participants';
|
||||||
import { ACTION_TRIGGER, MEDIA_STATE } from '../constants';
|
import { connect } from '../../base/redux';
|
||||||
import { getParticipantAudioMediaState } from '../functions';
|
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 ParticipantQuickAction from './ParticipantQuickAction';
|
||||||
import { ParticipantActionEllipsis } from './styled';
|
import { ParticipantActionEllipsis } from './styled';
|
||||||
|
|
||||||
type Props = {
|
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
|
* Is this item highlighted
|
||||||
*/
|
*/
|
||||||
|
@ -24,6 +67,11 @@ type Props = {
|
||||||
*/
|
*/
|
||||||
muteAudio: Function,
|
muteAudio: Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The translated text for the mute participant button.
|
||||||
|
*/
|
||||||
|
muteParticipantButtonText: string,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback for the activation of this item's context menu
|
* Callback for the activation of this item's context menu
|
||||||
*/
|
*/
|
||||||
|
@ -35,38 +83,97 @@ type Props = {
|
||||||
onLeave: Function,
|
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,
|
isHighlighted,
|
||||||
onContextMenu,
|
onContextMenu,
|
||||||
onLeave,
|
onLeave,
|
||||||
muteAudio,
|
muteAudio,
|
||||||
participant
|
muteParticipantButtonText,
|
||||||
}: Props) => {
|
participantActionEllipsisLabel,
|
||||||
const { t } = useTranslation();
|
youText
|
||||||
const isAudioMuted = useSelector(getIsParticipantAudioMuted(participant));
|
}: Props) {
|
||||||
const isVideoMuted = useSelector(getIsParticipantVideoMuted(participant));
|
|
||||||
const audioMediaState = useSelector(getParticipantAudioMediaState(participant, isAudioMuted));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ParticipantItem
|
<ParticipantItem
|
||||||
actionsTrigger = { ACTION_TRIGGER.HOVER }
|
actionsTrigger = { ACTION_TRIGGER.HOVER }
|
||||||
audioMediaState = { audioMediaState }
|
audioMediaState = { _audioMediaState }
|
||||||
|
displayName = { _displayName }
|
||||||
isHighlighted = { isHighlighted }
|
isHighlighted = { isHighlighted }
|
||||||
|
local = { _local }
|
||||||
onLeave = { onLeave }
|
onLeave = { onLeave }
|
||||||
participant = { participant }
|
participantID = { _participantID }
|
||||||
videoMuteState = { isVideoMuted ? MEDIA_STATE.MUTED : MEDIA_STATE.UNMUTED }>
|
raisedHand = { _raisedHand }
|
||||||
|
videoMuteState = { _isVideoMuted ? MEDIA_STATE.MUTED : MEDIA_STATE.UNMUTED }
|
||||||
|
youText = { youText }>
|
||||||
<ParticipantQuickAction
|
<ParticipantQuickAction
|
||||||
isAudioMuted = { isAudioMuted }
|
askUnmuteText = { askUnmuteText }
|
||||||
|
buttonType = { _quickActionButtonType }
|
||||||
muteAudio = { muteAudio }
|
muteAudio = { muteAudio }
|
||||||
participant = { participant } />
|
muteParticipantButtonText = { muteParticipantButtonText }
|
||||||
|
participantID = { _participantID } />
|
||||||
<ParticipantActionEllipsis
|
<ParticipantActionEllipsis
|
||||||
aria-label = { t('MeetingParticipantItem.ParticipantActionEllipsis.options') }
|
aria-label = { participantActionEllipsisLabel }
|
||||||
onClick = { onContextMenu } />
|
onClick = { onContextMenu } />
|
||||||
</ParticipantItem>
|
</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
|
// @flow
|
||||||
|
|
||||||
import _ from 'lodash';
|
|
||||||
import React, { useCallback, useRef, useState } from 'react';
|
import React, { useCallback, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
|
|
||||||
import { openDialog } from '../../base/dialog';
|
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 MuteRemoteParticipantDialog from '../../video-menu/components/web/MuteRemoteParticipantDialog';
|
||||||
import { findStyledAncestor, shouldRenderInviteButton } from '../functions';
|
import { findStyledAncestor, shouldRenderInviteButton } from '../functions';
|
||||||
|
|
||||||
import { InviteButton } from './InviteButton';
|
import { InviteButton } from './InviteButton';
|
||||||
import { MeetingParticipantContextMenu } from './MeetingParticipantContextMenu';
|
import MeetingParticipantContextMenu from './MeetingParticipantContextMenu';
|
||||||
import { MeetingParticipantItem } from './MeetingParticipantItem';
|
import MeetingParticipantItem from './MeetingParticipantItem';
|
||||||
import { Heading, ParticipantContainer } from './styled';
|
import { Heading, ParticipantContainer } from './styled';
|
||||||
|
|
||||||
type NullProto = {
|
type NullProto = {
|
||||||
|
@ -20,7 +23,7 @@ type NullProto = {
|
||||||
__proto__: null
|
__proto__: null
|
||||||
};
|
};
|
||||||
|
|
||||||
type RaiseContext = NullProto | {
|
type RaiseContext = NullProto | {|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Target elements against which positioning calculations are made
|
* Target elements against which positioning calculations are made
|
||||||
|
@ -28,17 +31,28 @@ type RaiseContext = NullProto | {
|
||||||
offsetTarget?: HTMLElement,
|
offsetTarget?: HTMLElement,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Participant reference
|
* The ID of the participant.
|
||||||
*/
|
*/
|
||||||
participant?: Object,
|
participantID?: String,
|
||||||
};
|
|};
|
||||||
|
|
||||||
const initialState = Object.freeze(Object.create(null));
|
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 dispatch = useDispatch();
|
||||||
const isMouseOverMenu = useRef(false);
|
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 showInviteButton = useSelector(shouldRenderInviteButton);
|
||||||
const [ raiseContext, setRaiseContext ] = useState<RaiseContext>(initialState);
|
const [ raiseContext, setRaiseContext ] = useState<RaiseContext>(initialState);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
@ -61,20 +75,20 @@ export const MeetingParticipantList = () => {
|
||||||
});
|
});
|
||||||
}, [ raiseContext ]);
|
}, [ raiseContext ]);
|
||||||
|
|
||||||
const raiseMenu = useCallback((participant, target) => {
|
const raiseMenu = useCallback((participantID, target) => {
|
||||||
setRaiseContext({
|
setRaiseContext({
|
||||||
participant,
|
participantID,
|
||||||
offsetTarget: findStyledAncestor(target, ParticipantContainer)
|
offsetTarget: findStyledAncestor(target, ParticipantContainer)
|
||||||
});
|
});
|
||||||
}, [ raiseContext ]);
|
}, [ raiseContext ]);
|
||||||
|
|
||||||
const toggleMenu = useCallback(participant => e => {
|
const toggleMenu = useCallback(participantID => e => {
|
||||||
const { participant: raisedParticipant } = raiseContext;
|
const { participantID: raisedParticipant } = raiseContext;
|
||||||
|
|
||||||
if (raisedParticipant && raisedParticipant === participant) {
|
if (raisedParticipant && raisedParticipant === participantID) {
|
||||||
lowerMenu();
|
lowerMenu();
|
||||||
} else {
|
} else {
|
||||||
raiseMenu(participant, e.target);
|
raiseMenu(participantID, e.target);
|
||||||
}
|
}
|
||||||
}, [ raiseContext ]);
|
}, [ raiseContext ]);
|
||||||
|
|
||||||
|
@ -91,20 +105,44 @@ export const MeetingParticipantList = () => {
|
||||||
dispatch(openDialog(MuteRemoteParticipantDialog, { participantID: id }));
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Heading>{t('participantsPane.headings.participantsList', { count: participants.length })}</Heading>
|
<Heading>{t('participantsPane.headings.participantsList', { count: participantsCount })}</Heading>
|
||||||
{showInviteButton && <InviteButton />}
|
{showInviteButton && <InviteButton />}
|
||||||
<div>
|
<div>
|
||||||
{participants.map(p => (
|
{ items }
|
||||||
<MeetingParticipantItem
|
|
||||||
isHighlighted = { raiseContext.participant === p }
|
|
||||||
key = { p.id }
|
|
||||||
muteAudio = { muteAudio }
|
|
||||||
onContextMenu = { toggleMenu(p) }
|
|
||||||
onLeave = { lowerMenu }
|
|
||||||
participant = { p } />
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
<MeetingParticipantContextMenu
|
<MeetingParticipantContextMenu
|
||||||
muteAudio = { muteAudio }
|
muteAudio = { muteAudio }
|
||||||
|
@ -114,4 +152,4 @@ export const MeetingParticipantList = () => {
|
||||||
{ ...raiseContext } />
|
{ ...raiseContext } />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import React, { type Node } from 'react';
|
import React, { type Node } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
|
|
||||||
import { Avatar } from '../../base/avatar';
|
import { Avatar } from '../../base/avatar';
|
||||||
import {
|
import {
|
||||||
|
@ -12,7 +10,6 @@ import {
|
||||||
IconMicrophoneEmpty,
|
IconMicrophoneEmpty,
|
||||||
IconMicrophoneEmptySlash
|
IconMicrophoneEmptySlash
|
||||||
} from '../../base/icons';
|
} from '../../base/icons';
|
||||||
import { getParticipantDisplayNameWithId } from '../../base/participants';
|
|
||||||
import { ACTION_TRIGGER, MEDIA_STATE, type ActionTrigger, type MediaState } from '../constants';
|
import { ACTION_TRIGGER, MEDIA_STATE, type ActionTrigger, type MediaState } from '../constants';
|
||||||
|
|
||||||
import { RaisedHandIndicator } from './RaisedHandIndicator';
|
import { RaisedHandIndicator } from './RaisedHandIndicator';
|
||||||
|
@ -100,15 +97,20 @@ type Props = {
|
||||||
*/
|
*/
|
||||||
children: Node,
|
children: Node,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the participant. Used for showing lobby names.
|
||||||
|
*/
|
||||||
|
displayName: string,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is this item highlighted/raised
|
* Is this item highlighted/raised
|
||||||
*/
|
*/
|
||||||
isHighlighted?: boolean,
|
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
|
* Callback for when the mouse leaves this component
|
||||||
|
@ -116,29 +118,46 @@ type Props = {
|
||||||
onLeave?: Function,
|
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
|
* 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,
|
children,
|
||||||
isHighlighted,
|
isHighlighted,
|
||||||
onLeave,
|
onLeave,
|
||||||
actionsTrigger = ACTION_TRIGGER.HOVER,
|
actionsTrigger = ACTION_TRIGGER.HOVER,
|
||||||
audioMediaState = MEDIA_STATE.NONE,
|
audioMediaState = MEDIA_STATE.NONE,
|
||||||
videoMuteState = MEDIA_STATE.NONE,
|
videoMuteState = MEDIA_STATE.NONE,
|
||||||
name,
|
displayName,
|
||||||
participant: p
|
participantID,
|
||||||
}: Props) => {
|
local,
|
||||||
|
raisedHand,
|
||||||
|
youText
|
||||||
|
}: Props) {
|
||||||
const ParticipantActions = Actions[actionsTrigger];
|
const ParticipantActions = Actions[actionsTrigger];
|
||||||
const { t } = useTranslation();
|
|
||||||
const displayName = name || useSelector(getParticipantDisplayNameWithId(p.id));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ParticipantContainer
|
<ParticipantContainer
|
||||||
|
@ -147,22 +166,22 @@ export const ParticipantItem = ({
|
||||||
trigger = { actionsTrigger }>
|
trigger = { actionsTrigger }>
|
||||||
<Avatar
|
<Avatar
|
||||||
className = 'participant-avatar'
|
className = 'participant-avatar'
|
||||||
participantId = { p.id }
|
participantId = { participantID }
|
||||||
size = { 32 } />
|
size = { 32 } />
|
||||||
<ParticipantContent>
|
<ParticipantContent>
|
||||||
<ParticipantNameContainer>
|
<ParticipantNameContainer>
|
||||||
<ParticipantName>
|
<ParticipantName>
|
||||||
{ displayName }
|
{ displayName }
|
||||||
</ParticipantName>
|
</ParticipantName>
|
||||||
{ p.local ? <span> ({t('chat.you')})</span> : null }
|
{ local ? <span> ({ youText })</span> : null }
|
||||||
</ParticipantNameContainer>
|
</ParticipantNameContainer>
|
||||||
{ !p.local && <ParticipantActions children = { children } /> }
|
{ !local && <ParticipantActions children = { children } /> }
|
||||||
<ParticipantStates>
|
<ParticipantStates>
|
||||||
{p.raisedHand && <RaisedHandIndicator />}
|
{ raisedHand && <RaisedHandIndicator /> }
|
||||||
{VideoStateIcons[videoMuteState]}
|
{ VideoStateIcons[videoMuteState] }
|
||||||
{AudioStateIcons[audioMediaState]}
|
{ AudioStateIcons[audioMediaState] }
|
||||||
</ParticipantStates>
|
</ParticipantStates>
|
||||||
</ParticipantContent>
|
</ParticipantContent>
|
||||||
</ParticipantContainer>
|
</ParticipantContainer>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
|
@ -1,11 +1,8 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
|
|
||||||
import { QUICK_ACTION_BUTTON } from '../constants';
|
import { QUICK_ACTION_BUTTON } from '../constants';
|
||||||
import { getQuickActionButtonType } from '../functions';
|
|
||||||
|
|
||||||
import AskToUnmuteButton from './AskToUnmuteButton';
|
import AskToUnmuteButton from './AskToUnmuteButton';
|
||||||
import { QuickActionButton } from './styled';
|
import { QuickActionButton } from './styled';
|
||||||
|
@ -13,19 +10,26 @@ import { QuickActionButton } from './styled';
|
||||||
type Props = {
|
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.
|
* Callback used to open a confirmation dialog for audio muting.
|
||||||
*/
|
*/
|
||||||
muteAudio: Function,
|
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.
|
* @param {Props} props - The props of the component.
|
||||||
* @returns {React$Element<'button'>}
|
* @returns {React$Element<'button'>}
|
||||||
*/
|
*/
|
||||||
export default function({ isAudioMuted, muteAudio, participant }: Props) {
|
export default function ParticipantQuickAction({
|
||||||
const buttonType = useSelector(getQuickActionButtonType(participant, isAudioMuted));
|
askUnmuteText,
|
||||||
const { id } = participant;
|
buttonType,
|
||||||
const { t } = useTranslation();
|
muteAudio,
|
||||||
|
muteParticipantButtonText,
|
||||||
|
participantID
|
||||||
|
}: Props) {
|
||||||
switch (buttonType) {
|
switch (buttonType) {
|
||||||
case QUICK_ACTION_BUTTON.MUTE: {
|
case QUICK_ACTION_BUTTON.MUTE: {
|
||||||
return (
|
return (
|
||||||
<QuickActionButton
|
<QuickActionButton
|
||||||
onClick = { muteAudio(id) }
|
onClick = { muteAudio(participantID) }
|
||||||
primary = { true }>
|
primary = { true }>
|
||||||
{t('dialog.muteParticipantButton')}
|
{ muteParticipantButtonText }
|
||||||
</QuickActionButton>
|
</QuickActionButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
case QUICK_ACTION_BUTTON.ASK_TO_UNMUTE: {
|
case QUICK_ACTION_BUTTON.ASK_TO_UNMUTE: {
|
||||||
return <AskToUnmuteButton id = { id } />;
|
return (
|
||||||
|
<AskToUnmuteButton
|
||||||
|
askUnmuteText = { askUnmuteText }
|
||||||
|
id = { participantID } />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
|
||||||
import { ThemeProvider } from 'styled-components';
|
import { ThemeProvider } from 'styled-components';
|
||||||
|
|
||||||
import { openDialog } from '../../base/dialog';
|
import { openDialog } from '../../base/dialog';
|
||||||
|
import { translate } from '../../base/i18n';
|
||||||
import {
|
import {
|
||||||
getParticipantCount,
|
getParticipantCount,
|
||||||
isEveryoneModerator,
|
|
||||||
isLocalParticipantModerator
|
isLocalParticipantModerator
|
||||||
} from '../../base/participants';
|
} from '../../base/participants';
|
||||||
|
import { connect } from '../../base/redux';
|
||||||
import { MuteEveryoneDialog } from '../../video-menu/components/';
|
import { MuteEveryoneDialog } from '../../video-menu/components/';
|
||||||
import { close } from '../actions';
|
import { close } from '../actions';
|
||||||
import { classList, findStyledAncestor, getParticipantsPaneOpen } from '../functions';
|
import { classList, findStyledAncestor, getParticipantsPaneOpen } from '../functions';
|
||||||
|
@ -30,74 +29,238 @@ import {
|
||||||
Header
|
Header
|
||||||
} from './styled';
|
} from './styled';
|
||||||
|
|
||||||
export const ParticipantsPane = () => {
|
/**
|
||||||
const dispatch = useDispatch();
|
* The type of the React {@code Component} props of {@link ParticipantsPane}.
|
||||||
const paneOpen = useSelector(getParticipantsPaneOpen);
|
*/
|
||||||
const isLocalModerator = useSelector(isLocalParticipantModerator);
|
type Props = {
|
||||||
const participantsCount = useSelector(getParticipantCount);
|
|
||||||
const everyoneModerator = useSelector(isEveryoneModerator);
|
|
||||||
const showContextMenu = !everyoneModerator && participantsCount > 2;
|
|
||||||
|
|
||||||
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 => {
|
* Whether to show context menu.
|
||||||
if (closePane && (e.key === ' ' || e.key === 'Enter')) {
|
*/
|
||||||
e.preventDefault();
|
_showContextMenu: boolean,
|
||||||
closePane();
|
|
||||||
}
|
|
||||||
}, [ closePane ]);
|
|
||||||
const muteAll = useCallback(() => dispatch(openDialog(MuteEveryoneDialog)), [ dispatch ]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
/**
|
||||||
const handler = [ 'click', e => {
|
* Whether to show the footer menu.
|
||||||
if (!findStyledAncestor(e.target, FooterEllipsisContainer)) {
|
*/
|
||||||
setContextOpen(false);
|
_showFooter: boolean,
|
||||||
}
|
|
||||||
} ];
|
|
||||||
|
|
||||||
window.addEventListener(...handler);
|
/**
|
||||||
|
* The Redux dispatch function.
|
||||||
|
*/
|
||||||
|
dispatch: Function,
|
||||||
|
|
||||||
return () => window.removeEventListener(...handler);
|
/**
|
||||||
}, [ contextOpen ]);
|
* The i18n translate function.
|
||||||
|
*/
|
||||||
const toggleContext = useCallback(() => setContextOpen(!contextOpen), [ contextOpen, setContextOpen ]);
|
t: Function
|
||||||
|
|
||||||
return (
|
|
||||||
<ThemeProvider theme = { theme }>
|
|
||||||
<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 }
|
|
||||||
role = 'button'
|
|
||||||
tabIndex = { 0 } />
|
|
||||||
</Header>
|
|
||||||
<Container>
|
|
||||||
<LobbyParticipantList />
|
|
||||||
<AntiCollapse />
|
|
||||||
<MeetingParticipantList />
|
|
||||||
</Container>
|
|
||||||
{isLocalModerator && (
|
|
||||||
<Footer>
|
|
||||||
<FooterButton onClick = { muteAll }>
|
|
||||||
{t('participantsPane.actions.muteAll')}
|
|
||||||
</FooterButton>
|
|
||||||
{showContextMenu && (
|
|
||||||
<FooterEllipsisContainer>
|
|
||||||
<FooterEllipsisButton
|
|
||||||
id = 'participants-pane-context-menu'
|
|
||||||
onClick = { toggleContext } />
|
|
||||||
{contextOpen && <FooterContextMenu onMouseLeave = { toggleContext } />}
|
|
||||||
</FooterEllipsisContainer>
|
|
||||||
)}
|
|
||||||
</Footer>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ThemeProvider>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements React's {@link Component#componentDidMount()}.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
componentDidMount() {
|
||||||
|
window.addEventListener('click', this._onWindowClickListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements React's {@link Component#componentWillUnmount()}.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
componentWillUnmount() {
|
||||||
|
window.removeEventListener('click', this._onWindowClickListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements React's {@link Component#render}.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
_paneOpen,
|
||||||
|
_showContextMenu,
|
||||||
|
_showFooter,
|
||||||
|
t
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
// 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 = 'participants_pane-content'>
|
||||||
|
<Header>
|
||||||
|
<Close
|
||||||
|
aria-label = { t('participantsPane.close', 'Close') }
|
||||||
|
onClick = { this._onClosePane }
|
||||||
|
onKeyPress = { this._onKeyPress }
|
||||||
|
role = 'button'
|
||||||
|
tabIndex = { 0 } />
|
||||||
|
</Header>
|
||||||
|
<Container>
|
||||||
|
<LobbyParticipantList />
|
||||||
|
<AntiCollapse />
|
||||||
|
<MeetingParticipantList />
|
||||||
|
</Container>
|
||||||
|
{_showFooter && (
|
||||||
|
<Footer>
|
||||||
|
<FooterButton onClick = { this._onMuteAll }>
|
||||||
|
{t('participantsPane.actions.muteAll')}
|
||||||
|
</FooterButton>
|
||||||
|
{_showContextMenu && (
|
||||||
|
<FooterEllipsisContainer>
|
||||||
|
<FooterEllipsisButton
|
||||||
|
id = 'participants-pane-context-menu'
|
||||||
|
onClick = { this._onToggleContext } />
|
||||||
|
{this.state.contextOpen
|
||||||
|
&& <FooterContextMenu onMouseLeave = { this._onToggleContext } />}
|
||||||
|
</FooterEllipsisContainer>
|
||||||
|
)}
|
||||||
|
</Footer>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</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 './InviteButton';
|
||||||
export * from './LobbyParticipantItem';
|
export * from './LobbyParticipantItem';
|
||||||
export * from './LobbyParticipantList';
|
export * from './LobbyParticipantList';
|
||||||
export * from './MeetingParticipantContextMenu';
|
|
||||||
export * from './MeetingParticipantItem';
|
|
||||||
export * from './MeetingParticipantList';
|
export * from './MeetingParticipantList';
|
||||||
export * from './ParticipantItem';
|
export { default as ParticipantsPane } from './ParticipantsPane';
|
||||||
export * from './ParticipantsPane';
|
|
||||||
export * from './ParticipantsPaneButton';
|
export * from './ParticipantsPaneButton';
|
||||||
export * from './RaisedHandIndicator';
|
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.
|
* @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 (getParticipantCount(state) > 2 && isEnabledFromState(mediaType, state)) {
|
||||||
if (participant.local) {
|
if (participant.local) {
|
||||||
return !isLocalParticipantApprovedFromState(mediaType, state);
|
return !isLocalParticipantApprovedFromState(mediaType, state);
|
||||||
|
@ -62,18 +63,19 @@ export const isForceMuted = (participant: Object, mediaType: MediaType) => (stat
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
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 {Object} participant - The participant.
|
||||||
* @param {boolean} muted - The mute state of 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 (muted) {
|
||||||
if (isForceMuted(participant, MEDIA_TYPE.AUDIO)(state)) {
|
if (isForceMuted(participant, MEDIA_TYPE.AUDIO, state)) {
|
||||||
return MEDIA_STATE.FORCE_MUTED;
|
return MEDIA_STATE.FORCE_MUTED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,7 +83,7 @@ export const getParticipantAudioMediaState = (participant: Object, muted: Boolea
|
||||||
}
|
}
|
||||||
|
|
||||||
return MEDIA_STATE.UNMUTED;
|
return MEDIA_STATE.UNMUTED;
|
||||||
};
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -125,17 +127,18 @@ const getState = (state: Object) => state[REDUCER_KEY];
|
||||||
export const getParticipantsPaneOpen = (state: Object) => Boolean(getState(state)?.isOpen);
|
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.
|
* The button is displayed when hovering a participant from the participant list.
|
||||||
*
|
*
|
||||||
* @param {Object} participant - The participant.
|
* @param {Object} participant - The participant.
|
||||||
* @param {boolean} isAudioMuted - If audio is muted for 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
|
// handled only by moderators
|
||||||
if (isLocalParticipantModerator(state)) {
|
if (isLocalParticipantModerator(state)) {
|
||||||
if (isForceMuted(participant, MEDIA_TYPE.AUDIO)(state)) {
|
if (isForceMuted(participant, MEDIA_TYPE.AUDIO, state)) {
|
||||||
return QUICK_ACTION_BUTTON.ASK_TO_UNMUTE;
|
return QUICK_ACTION_BUTTON.ASK_TO_UNMUTE;
|
||||||
}
|
}
|
||||||
if (!isAudioMuted) {
|
if (!isAudioMuted) {
|
||||||
|
@ -144,7 +147,7 @@ export const getQuickActionButtonType = (participant: Object, isAudioMuted: Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
return QUICK_ACTION_BUTTON.NONE;
|
return QUICK_ACTION_BUTTON.NONE;
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the invite button should be rendered.
|
* Returns true if the invite button should be rendered.
|
||||||
|
|
|
@ -144,7 +144,7 @@ function _mapStateToProps(state) {
|
||||||
clientHeight,
|
clientHeight,
|
||||||
clientWidth,
|
clientWidth,
|
||||||
filmstripVisible: visible,
|
filmstripVisible: visible,
|
||||||
isOwner: ownerId === localParticipant.id,
|
isOwner: ownerId === localParticipant?.id,
|
||||||
videoUrl
|
videoUrl
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import { getParticipants } from '../base/participants';
|
import { getFakeParticipants } from '../base/participants';
|
||||||
|
|
||||||
import { VIDEO_PLAYER_PARTICIPANT_NAME, YOUTUBE_PLAYER_PARTICIPANT_NAME } from './constants';
|
import { VIDEO_PLAYER_PARTICIPANT_NAME, YOUTUBE_PLAYER_PARTICIPANT_NAME } from './constants';
|
||||||
|
|
||||||
|
@ -41,7 +41,15 @@ export function isSharingStatus(status: string) {
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
export function isVideoPlaying(stateful: Object | Function): boolean {
|
export function isVideoPlaying(stateful: Object | Function): boolean {
|
||||||
return Boolean(getParticipants(stateful).find(p => p.isFakeParticipant
|
let videoPlaying = false;
|
||||||
&& (p.name === VIDEO_PLAYER_PARTICIPANT_NAME || p.name === YOUTUBE_PLAYER_PARTICIPANT_NAME))
|
|
||||||
);
|
// 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 React from 'react';
|
||||||
|
|
||||||
|
import { getParticipantCountWithFake } from '../../base/participants';
|
||||||
import { connect } from '../../base/redux';
|
import { connect } from '../../base/redux';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -75,7 +76,7 @@ class Captions
|
||||||
function mapStateToProps(state) {
|
function mapStateToProps(state) {
|
||||||
return {
|
return {
|
||||||
..._abstractMapStateToProps(state),
|
..._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 JitsiMeetJS from '../../../base/lib-jitsi-meet';
|
||||||
import {
|
import {
|
||||||
getLocalParticipant,
|
getLocalParticipant,
|
||||||
getParticipants,
|
haveParticipantWithScreenSharingFeature,
|
||||||
raiseHand
|
raiseHand
|
||||||
} from '../../../base/participants';
|
} from '../../../base/participants';
|
||||||
import { connect } from '../../../base/redux';
|
import { connect } from '../../../base/redux';
|
||||||
|
@ -183,15 +183,20 @@ type Props = {
|
||||||
_raisedHand: boolean,
|
_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.
|
* Whether or not the local participant is sharing a YouTube video.
|
||||||
*/
|
*/
|
||||||
_sharingVideo: boolean,
|
_sharingVideo: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The enabled buttons.
|
||||||
|
*/
|
||||||
|
_toolbarButtons: Array<string>,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flag showing whether toolbar is visible.
|
* Flag showing whether toolbar is visible.
|
||||||
*/
|
*/
|
||||||
|
@ -202,11 +207,6 @@ type Props = {
|
||||||
*/
|
*/
|
||||||
_visibleButtons: Array<string>,
|
_visibleButtons: Array<string>,
|
||||||
|
|
||||||
/**
|
|
||||||
* Handler to check if a button is enabled.
|
|
||||||
*/
|
|
||||||
_shouldShowButton: Function,
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the selected virtual source object.
|
* Returns the selected virtual source object.
|
||||||
*/
|
*/
|
||||||
|
@ -269,38 +269,39 @@ class Toolbox extends Component<Props> {
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
const { _toolbarButtons } = this.props;
|
||||||
const KEYBOARD_SHORTCUTS = [
|
const KEYBOARD_SHORTCUTS = [
|
||||||
this.props._shouldShowButton('videoquality') && {
|
isToolbarButtonEnabled('videoquality', _toolbarButtons) && {
|
||||||
character: 'A',
|
character: 'A',
|
||||||
exec: this._onShortcutToggleVideoQuality,
|
exec: this._onShortcutToggleVideoQuality,
|
||||||
helpDescription: 'toolbar.callQuality'
|
helpDescription: 'toolbar.callQuality'
|
||||||
},
|
},
|
||||||
this.props._shouldShowButton('chat') && {
|
isToolbarButtonEnabled('chat', _toolbarButtons) && {
|
||||||
character: 'C',
|
character: 'C',
|
||||||
exec: this._onShortcutToggleChat,
|
exec: this._onShortcutToggleChat,
|
||||||
helpDescription: 'keyboardShortcuts.toggleChat'
|
helpDescription: 'keyboardShortcuts.toggleChat'
|
||||||
},
|
},
|
||||||
this.props._shouldShowButton('desktop') && {
|
isToolbarButtonEnabled('desktop', _toolbarButtons) && {
|
||||||
character: 'D',
|
character: 'D',
|
||||||
exec: this._onShortcutToggleScreenshare,
|
exec: this._onShortcutToggleScreenshare,
|
||||||
helpDescription: 'keyboardShortcuts.toggleScreensharing'
|
helpDescription: 'keyboardShortcuts.toggleScreensharing'
|
||||||
},
|
},
|
||||||
this.props._shouldShowButton('participants-pane') && {
|
isToolbarButtonEnabled('participants-pane', _toolbarButtons) && {
|
||||||
character: 'P',
|
character: 'P',
|
||||||
exec: this._onShortcutToggleParticipantsPane,
|
exec: this._onShortcutToggleParticipantsPane,
|
||||||
helpDescription: 'keyboardShortcuts.toggleParticipantsPane'
|
helpDescription: 'keyboardShortcuts.toggleParticipantsPane'
|
||||||
},
|
},
|
||||||
this.props._shouldShowButton('raisehand') && {
|
isToolbarButtonEnabled('raisehand', _toolbarButtons) && {
|
||||||
character: 'R',
|
character: 'R',
|
||||||
exec: this._onShortcutToggleRaiseHand,
|
exec: this._onShortcutToggleRaiseHand,
|
||||||
helpDescription: 'keyboardShortcuts.raiseHand'
|
helpDescription: 'keyboardShortcuts.raiseHand'
|
||||||
},
|
},
|
||||||
this.props._shouldShowButton('fullscreen') && {
|
isToolbarButtonEnabled('fullscreen', _toolbarButtons) && {
|
||||||
character: 'S',
|
character: 'S',
|
||||||
exec: this._onShortcutToggleFullScreen,
|
exec: this._onShortcutToggleFullScreen,
|
||||||
helpDescription: 'keyboardShortcuts.fullScreen'
|
helpDescription: 'keyboardShortcuts.fullScreen'
|
||||||
},
|
},
|
||||||
this.props._shouldShowButton('tileview') && {
|
isToolbarButtonEnabled('tileview', _toolbarButtons) && {
|
||||||
character: 'W',
|
character: 'W',
|
||||||
exec: this._onShortcutToggleTileView,
|
exec: this._onShortcutToggleTileView,
|
||||||
helpDescription: 'toolbar.tileViewToggle'
|
helpDescription: 'toolbar.tileViewToggle'
|
||||||
|
@ -509,7 +510,7 @@ class Toolbox extends Component<Props> {
|
||||||
const {
|
const {
|
||||||
_feedbackConfigured,
|
_feedbackConfigured,
|
||||||
_isMobile,
|
_isMobile,
|
||||||
_screensharing
|
_screenSharing
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const microphone = {
|
const microphone = {
|
||||||
|
@ -644,7 +645,7 @@ class Toolbox extends Component<Props> {
|
||||||
group: 3
|
group: 3
|
||||||
};
|
};
|
||||||
|
|
||||||
const virtualBackground = !_screensharing && checkBlurSupport() && {
|
const virtualBackground = !_screenSharing && checkBlurSupport() && {
|
||||||
key: 'select-background',
|
key: 'select-background',
|
||||||
Content: VideoBackgroundButton,
|
Content: VideoBackgroundButton,
|
||||||
group: 3
|
group: 3
|
||||||
|
@ -734,12 +735,12 @@ class Toolbox extends Component<Props> {
|
||||||
_getVisibleButtons() {
|
_getVisibleButtons() {
|
||||||
const {
|
const {
|
||||||
_clientWidth,
|
_clientWidth,
|
||||||
_shouldShowButton
|
_toolbarButtons
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
|
||||||
const buttons = this._getAllButtons();
|
const buttons = this._getAllButtons();
|
||||||
const isHangupVisible = _shouldShowButton('hangup');
|
const isHangupVisible = isToolbarButtonEnabled('hangup', _toolbarButtons);
|
||||||
const { order } = THRESHOLDS.find(({ width }) => _clientWidth > width)
|
const { order } = THRESHOLDS.find(({ width }) => _clientWidth > width)
|
||||||
|| THRESHOLDS[THRESHOLDS.length - 1];
|
|| THRESHOLDS[THRESHOLDS.length - 1];
|
||||||
let sliceIndex = order.length + 2;
|
let sliceIndex = order.length + 2;
|
||||||
|
@ -749,7 +750,7 @@ class Toolbox extends Component<Props> {
|
||||||
const filtered = [
|
const filtered = [
|
||||||
...order.map(key => buttons[key]),
|
...order.map(key => buttons[key]),
|
||||||
...Object.values(buttons).filter((button, index) => !order.includes(keys[index]))
|
...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) {
|
if (isHangupVisible) {
|
||||||
sliceIndex -= 1;
|
sliceIndex -= 1;
|
||||||
|
@ -934,7 +935,7 @@ class Toolbox extends Component<Props> {
|
||||||
'toggle.screen.sharing',
|
'toggle.screen.sharing',
|
||||||
ACTION_SHORTCUT_TRIGGERED,
|
ACTION_SHORTCUT_TRIGGERED,
|
||||||
{
|
{
|
||||||
enable: !this.props._screensharing
|
enable: !this.props._screenSharing
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this._doToggleScreenshare();
|
this._doToggleScreenshare();
|
||||||
|
@ -1053,7 +1054,7 @@ class Toolbox extends Component<Props> {
|
||||||
sendAnalytics(createToolbarEvent(
|
sendAnalytics(createToolbarEvent(
|
||||||
'toggle.screen.sharing',
|
'toggle.screen.sharing',
|
||||||
ACTION_SHORTCUT_TRIGGERED,
|
ACTION_SHORTCUT_TRIGGERED,
|
||||||
{ enable: !this.props._screensharing }));
|
{ enable: !this.props._screenSharing }));
|
||||||
|
|
||||||
this._closeOverflowMenuIfOpen();
|
this._closeOverflowMenuIfOpen();
|
||||||
this._doToggleScreenshare();
|
this._doToggleScreenshare();
|
||||||
|
@ -1116,6 +1117,7 @@ class Toolbox extends Component<Props> {
|
||||||
const {
|
const {
|
||||||
_isMobile,
|
_isMobile,
|
||||||
_overflowMenuVisible,
|
_overflowMenuVisible,
|
||||||
|
_toolbarButtons,
|
||||||
t
|
t
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
@ -1169,7 +1171,7 @@ class Toolbox extends Component<Props> {
|
||||||
<HangupButton
|
<HangupButton
|
||||||
customClass = 'hangup-button'
|
customClass = 'hangup-button'
|
||||||
key = 'hangup-button'
|
key = 'hangup-button'
|
||||||
visible = { this.props._shouldShowButton('hangup') } />
|
visible = { isToolbarButtonEnabled('hangup', _toolbarButtons) } />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1203,12 +1205,12 @@ function _mapStateToProps(state) {
|
||||||
let desktopSharingDisabledTooltipKey;
|
let desktopSharingDisabledTooltipKey;
|
||||||
|
|
||||||
if (enableFeaturesBasedOnToken) {
|
if (enableFeaturesBasedOnToken) {
|
||||||
// we enable desktop sharing if any participant already have this
|
if (desktopSharingEnabled) {
|
||||||
// feature enabled
|
// we enable desktop sharing if any participant already have this
|
||||||
desktopSharingEnabled = getParticipants(state)
|
// feature enabled and if the user supports it.
|
||||||
.find(({ features = {} }) =>
|
desktopSharingEnabled = haveParticipantWithScreenSharingFeature(state);
|
||||||
String(features['screen-sharing']) === 'true') !== undefined;
|
desktopSharingDisabledTooltipKey = 'dialog.shareYourScreenDisabled';
|
||||||
desktopSharingDisabledTooltipKey = 'dialog.shareYourScreenDisabled';
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -1226,13 +1228,13 @@ function _mapStateToProps(state) {
|
||||||
_isVpaasMeeting: isVpaasMeeting(state),
|
_isVpaasMeeting: isVpaasMeeting(state),
|
||||||
_fullScreen: fullScreen,
|
_fullScreen: fullScreen,
|
||||||
_tileViewEnabled: shouldDisplayTileView(state),
|
_tileViewEnabled: shouldDisplayTileView(state),
|
||||||
_localParticipantID: localParticipant.id,
|
_localParticipantID: localParticipant?.id,
|
||||||
_localVideo: localVideo,
|
_localVideo: localVideo,
|
||||||
_overflowMenuVisible: overflowMenuVisible,
|
_overflowMenuVisible: overflowMenuVisible,
|
||||||
_participantsPaneOpen: getParticipantsPaneOpen(state),
|
_participantsPaneOpen: getParticipantsPaneOpen(state),
|
||||||
_raisedHand: localParticipant.raisedHand,
|
_raisedHand: localParticipant?.raisedHand,
|
||||||
_screensharing: isScreenVideoShared(state),
|
_screenSharing: isScreenVideoShared(state),
|
||||||
_shouldShowButton: buttonName => isToolbarButtonEnabled(buttonName)(state),
|
_toolbarButtons: getToolbarButtons(state),
|
||||||
_visible: isToolboxVisible(state),
|
_visible: isToolboxVisible(state),
|
||||||
_visibleButtons: getToolbarButtons(state)
|
_visibleButtons: getToolbarButtons(state)
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import { hasAvailableDevices } from '../base/devices';
|
import { hasAvailableDevices } from '../base/devices';
|
||||||
import { TOOLBOX_ALWAYS_VISIBLE, getFeatureFlag, TOOLBOX_ENABLED } from '../base/flags';
|
import { TOOLBOX_ALWAYS_VISIBLE, getFeatureFlag, TOOLBOX_ENABLED } from '../base/flags';
|
||||||
|
import { getParticipantCountWithFake } from '../base/participants';
|
||||||
import { toState } from '../base/redux';
|
import { toState } from '../base/redux';
|
||||||
import { isLocalVideoTrackDesktop } from '../base/tracks';
|
import { isLocalVideoTrackDesktop } from '../base/tracks';
|
||||||
|
|
||||||
|
@ -60,7 +61,7 @@ export function getMovableButtons(width: number): Set<string> {
|
||||||
export function isToolboxVisible(stateful: Object | Function) {
|
export function isToolboxVisible(stateful: Object | Function) {
|
||||||
const state = toState(stateful);
|
const state = toState(stateful);
|
||||||
const { alwaysVisible, enabled, visible } = state['features/toolbox'];
|
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 alwaysVisibleFlag = getFeatureFlag(state, TOOLBOX_ALWAYS_VISIBLE, false);
|
||||||
const enabledFlag = getFeatureFlag(state, TOOLBOX_ENABLED, true);
|
const enabledFlag = getFeatureFlag(state, TOOLBOX_ENABLED, true);
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,8 @@ import { getFeatureFlag, TILE_VIEW_ENABLED } from '../base/flags';
|
||||||
import {
|
import {
|
||||||
getPinnedParticipant,
|
getPinnedParticipant,
|
||||||
getParticipantCount,
|
getParticipantCount,
|
||||||
pinParticipant
|
pinParticipant,
|
||||||
|
getParticipantCountWithFake
|
||||||
} from '../base/participants';
|
} from '../base/participants';
|
||||||
import {
|
import {
|
||||||
ASPECT_RATIO_BREAKPOINT,
|
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
|
// When in tile view mode, we must discount ourselves (the local participant) because our
|
||||||
// tile is not visible.
|
// tile is not visible.
|
||||||
const { iAmRecorder } = state['features/base/config'];
|
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 columnsToMaintainASquare = Math.ceil(Math.sqrt(numberOfParticipants));
|
||||||
const columns = Math.min(columnsToMaintainASquare, maxColumns);
|
const columns = Math.min(columnsToMaintainASquare, maxColumns);
|
||||||
|
|
|
@ -20,6 +20,7 @@ import {
|
||||||
} from '../base/media';
|
} from '../base/media';
|
||||||
import {
|
import {
|
||||||
getLocalParticipant,
|
getLocalParticipant,
|
||||||
|
getRemoteParticipants,
|
||||||
muteRemoteParticipant
|
muteRemoteParticipant
|
||||||
} from '../base/participants';
|
} from '../base/participants';
|
||||||
|
|
||||||
|
@ -91,14 +92,17 @@ export function muteAllParticipants(exclude: Array<string>, mediaType: MEDIA_TYP
|
||||||
return (dispatch: Dispatch<any>, getState: Function) => {
|
return (dispatch: Dispatch<any>, getState: Function) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const localId = getLocalParticipant(state).id;
|
const localId = getLocalParticipant(state).id;
|
||||||
const participantIds = state['features/base/participants']
|
|
||||||
.map(p => p.id);
|
|
||||||
|
|
||||||
/* eslint-disable no-confusing-arrow */
|
if (!exclude.includes(localId)) {
|
||||||
participantIds
|
dispatch(muteLocal(true, mediaType));
|
||||||
.filter(id => !exclude.includes(id))
|
}
|
||||||
.map(id => id === localId ? muteLocal(true, mediaType) : muteRemote(id, mediaType))
|
|
||||||
.map(dispatch);
|
getRemoteParticipants(state).forEach((p, id) => {
|
||||||
/* eslint-enable no-confusing-arrow */
|
if (exclude.includes(id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(muteRemote(id, mediaType));
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue