diff --git a/config.js b/config.js index 4b075768c..31f94af0c 100644 --- a/config.js +++ b/config.js @@ -41,6 +41,10 @@ var config = { // issues related to insertable streams. // disableE2EE: false, + // Enables/disables thumbnail reordering in the filmstrip. It is enabled by default unless explicitly + // disabled by the below option. + // enableThumbnailReordering: true, + // P2P test mode disables automatic switching to P2P when there are 2 // participants in the conference. p2pTestMode: false diff --git a/react/features/base/participants/functions.js b/react/features/base/participants/functions.js index c3df4f2ca..ed35f819a 100644 --- a/react/features/base/participants/functions.js +++ b/react/features/base/participants/functions.js @@ -273,6 +273,17 @@ export function getRemoteParticipants(stateful: Object | Function) { return toState(stateful)['features/base/participants'].remote; } +/** + * Selectors for the getting the remote participants in the order that they are displayed in the filmstrip. + * +@param {(Function|Object)} stateful - The (whole) redux state, or redux's {@code getState} function to be used to + * retrieve the state features/filmstrip. + * @returns {Array} + */ +export function getRemoteParticipantsSorted(stateful: Object | Function) { + return toState(stateful)['features/filmstrip'].remoteParticipants; +} + /** * Returns the participant which has its pinned state set to truthy. * @@ -444,67 +455,45 @@ async function _getFirstLoadableAvatarUrl(participant, store) { return undefined; } - /** - * Selector for retrieving sorted participants by display name. + * Selector for retrieving ids of participants in the order that they are displayed in the filmstrip (with the + * exception of participants with raised hand). The participants are reordered as follows. + * 1. Local participant. + * 2. Participants with raised hand. + * 3. Participants with screenshare sorted alphabetically by their display name. + * 4. Shared video participants. + * 5. Recent speakers sorted alphabetically by their display name. + * 6. Rest of the participants sorted alphabetically by their display name. * * @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 {Array} - */ -export function getSortedParticipants(stateful: Object | Function) { - const localParticipant = getLocalParticipant(stateful); - const remoteParticipants = getRemoteParticipants(stateful); - const raisedHandParticipantIds = getRaiseHandsQueue(stateful); - - const items = []; - const dominantSpeaker = getDominantSpeakerParticipant(stateful); - const raisedHandParticipants = []; - - raisedHandParticipantIds - .map(id => remoteParticipants.get(id) || localParticipant) - .forEach(p => { - if (p !== dominantSpeaker) { - raisedHandParticipants.push(p); - } - }); - - remoteParticipants.forEach(p => { - if (p !== dominantSpeaker && !raisedHandParticipantIds.find(id => p.id === id)) { - items.push(p); - } - }); - - if (!raisedHandParticipantIds.find(id => localParticipant.id === id)) { - items.push(localParticipant); - } - - items.sort((a, b) => - getParticipantDisplayName(stateful, a.id).localeCompare(getParticipantDisplayName(stateful, b.id)) - ); - - items.unshift(...raisedHandParticipants); - - if (dominantSpeaker && dominantSpeaker !== localParticipant) { - items.unshift(dominantSpeaker); - } - - return items; -} - -/** - * Selector for retrieving ids of alphabetically sorted participants by name. - * - * @param {(Function|Object)} stateful - The (whole) redux state, or redux's - * {@code getState} function to be used to retrieve the state - * features/base/participants. + * {@code getState} function to be used to retrieve the state features/base/participants. * @returns {Array} */ export function getSortedParticipantIds(stateful: Object | Function): Array { - const participantIds = getSortedParticipants(stateful).map((p): Object => p.id); + const { id } = getLocalParticipant(stateful); + const remoteParticipants = getRemoteParticipantsSorted(stateful); + const reorderedParticipants = new Set(remoteParticipants); + const raisedHandParticipants = getRaiseHandsQueue(stateful); + const remoteRaisedHandParticipants = new Set(raisedHandParticipants || []); - return participantIds; + for (const participant of remoteRaisedHandParticipants.keys()) { + // Avoid duplicates. + if (reorderedParticipants.has(participant)) { + reorderedParticipants.delete(participant); + } else { + remoteRaisedHandParticipants.delete(participant); + } + } + + // Remove self. + remoteRaisedHandParticipants.has(id) && remoteRaisedHandParticipants.delete(id); + + // Move self and participants with raised hand to the top of the list. + return [ + id, + ...Array.from(remoteRaisedHandParticipants.keys()), + ...Array.from(reorderedParticipants.keys()) + ]; } /** diff --git a/react/features/base/participants/reducer.js b/react/features/base/participants/reducer.js index 82697e195..04e9d959b 100644 --- a/react/features/base/participants/reducer.js +++ b/react/features/base/participants/reducer.js @@ -61,11 +61,11 @@ const DEFAULT_STATE = { haveParticipantWithScreenSharingFeature: false, local: undefined, pinnedParticipant: undefined, + raisedHandsQueue: [], remote: new Map(), sortedRemoteParticipants: new Map(), sortedRemoteScreenshares: new Map(), - speakersList: new Map(), - raisedHandsQueue: [] + speakersList: new Map() }; /** diff --git a/react/features/filmstrip/components/web/Filmstrip.js b/react/features/filmstrip/components/web/Filmstrip.js index 56b992a60..96da47d21 100644 --- a/react/features/filmstrip/components/web/Filmstrip.js +++ b/react/features/filmstrip/components/web/Filmstrip.js @@ -240,10 +240,10 @@ class Filmstrip extends PureComponent { let stop = stopIndex; if (_thumbnailsReordered) { - // In tile view, the start index needs to be offset by 1 because the first thumbnail is that of the local + // In tile view, the indices needs to be offset by 1 because the first thumbnail is that of the local // endpoint. The remote participants start from index 1. if (_currentLayout === LAYOUTS.TILE_VIEW) { - start = startIndex > 0 ? startIndex - 1 : 0; + start = Math.max(startIndex - 1, 0); stop = stopIndex - 1; } } @@ -528,7 +528,8 @@ class Filmstrip extends PureComponent { */ function _mapStateToProps(state) { const toolbarButtons = getToolbarButtons(state); - const { enableThumbnailReordering = true } = state['features/base/config']; + const { testing = {} } = state['features/base/config']; + const enableThumbnailReordering = testing.enableThumbnailReordering ?? true; const { visible, remoteParticipants } = state['features/filmstrip']; const reduceHeight = state['features/toolbox'].visible && toolbarButtons.length; const remoteVideosVisible = shouldRemoteVideosBeVisible(state); diff --git a/react/features/filmstrip/components/web/ThumbnailWrapper.js b/react/features/filmstrip/components/web/ThumbnailWrapper.js index b5af35b59..dd3525a0c 100644 --- a/react/features/filmstrip/components/web/ThumbnailWrapper.js +++ b/react/features/filmstrip/components/web/ThumbnailWrapper.js @@ -104,7 +104,8 @@ function _mapStateToProps(state, ownProps) { const _currentLayout = getCurrentLayout(state); const { remoteParticipants } = state['features/filmstrip']; const remoteParticipantsLength = remoteParticipants.length; - const { enableThumbnailReordering = true } = state['features/base/config']; + const { testing = {} } = state['features/base/config']; + const enableThumbnailReordering = testing.enableThumbnailReordering ?? true; if (_currentLayout === LAYOUTS.TILE_VIEW) { const { columnIndex, rowIndex } = ownProps; diff --git a/react/features/filmstrip/functions.any.js b/react/features/filmstrip/functions.any.js index 8d707c4db..c355b8965 100644 --- a/react/features/filmstrip/functions.any.js +++ b/react/features/filmstrip/functions.any.js @@ -12,7 +12,8 @@ import { setRemoteParticipants } from './actions'; */ export function updateRemoteParticipants(store: Object, participantId: ?number) { const state = store.getState(); - const { enableThumbnailReordering = true } = state['features/base/config']; + const { testing = {} } = state['features/base/config']; + const enableThumbnailReordering = testing.enableThumbnailReordering ?? true; let reorderedParticipants = []; if (!enableThumbnailReordering) { diff --git a/react/features/filmstrip/middleware.web.js b/react/features/filmstrip/middleware.web.js index aa81251f3..1c5e4e734 100644 --- a/react/features/filmstrip/middleware.web.js +++ b/react/features/filmstrip/middleware.web.js @@ -22,6 +22,15 @@ import './subscriber'; * The middleware of the feature Filmstrip. */ MiddlewareRegistry.register(store => next => action => { + if (action.type === PARTICIPANT_LEFT) { + // This has to be executed before we remove the participant from features/base/participants state in order to + // remove the related thumbnail component before we need to re-render it. If we do this after next() + // we will be in sitation where the participant exists in the remoteParticipants array in features/filmstrip + // but doesn't exist in features/base/participants state which will lead to rendering a thumbnail for + // non-existing participant. + updateRemoteParticipantsOnLeave(store, action.participant?.id); + } + const result = next(action); switch (action.type) { @@ -50,10 +59,6 @@ MiddlewareRegistry.register(store => next => action => { updateRemoteParticipants(store, action.participant?.id); break; } - case PARTICIPANT_LEFT: { - updateRemoteParticipantsOnLeave(store, action.participant?.id); - break; - } case SETTINGS_UPDATED: { if (typeof action.settings?.localFlipX === 'boolean') { // TODO: This needs to be removed once the large video is Reactified. diff --git a/react/features/participants-pane/components/web/MeetingParticipantContextMenu.js b/react/features/participants-pane/components/web/MeetingParticipantContextMenu.js index c871a0924..77df53725 100644 --- a/react/features/participants-pane/components/web/MeetingParticipantContextMenu.js +++ b/react/features/participants-pane/components/web/MeetingParticipantContextMenu.js @@ -396,7 +396,7 @@ class MeetingParticipantContextMenu extends Component { } const actions - = _participant.isFakeParticipant ? ( + = _participant?.isFakeParticipant ? ( <> {_localVideoOwner && ( diff --git a/react/features/participants-pane/components/web/MeetingParticipantItem.js b/react/features/participants-pane/components/web/MeetingParticipantItem.js index 94f5293e5..72a9d0b4a 100644 --- a/react/features/participants-pane/components/web/MeetingParticipantItem.js +++ b/react/features/participants-pane/components/web/MeetingParticipantItem.js @@ -170,7 +170,7 @@ function MeetingParticipantItem({ videoMediaState = { _videoMediaState } youText = { youText }> - {!overflowDrawer && !_participant.isFakeParticipant + {!overflowDrawer && !_participant?.isFakeParticipant && <> } - {!overflowDrawer && _localVideoOwner && _participant.isFakeParticipant && ( + {!overflowDrawer && _localVideoOwner && _participant?.isFakeParticipant && (