feat(Filmstrip): Reorder the visible participants in the filmstrip. (#9707)

* feat(Filmstrip): Reorder the visible participants in the filmstrip.
The participants are ordered alphabetically and the endpoints with screenshares, shared-videos and dominant speakers (in that order) are bumped to the top of the list. The local participant is also moved to the top left corner as opposed to the bottom right corner.

* squash: Implement review comments.

* squash: store alphabetically sorted list in redux and move shared videos to top.

* squash: Use the DEFAULT_REMOTE_DISPLAY_NAME from interfaceConfig for users without a display name.
This commit is contained in:
Jaya Allamsetty 2021-08-18 18:34:01 -04:00 committed by GitHub
parent a7a44902ec
commit 40099e97ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 228 additions and 85 deletions

View File

@ -2106,7 +2106,7 @@ export default {
room.on( room.on(
JitsiConferenceEvents.DOMINANT_SPEAKER_CHANGED, JitsiConferenceEvents.DOMINANT_SPEAKER_CHANGED,
id => APP.store.dispatch(dominantSpeakerChanged(id, room))); (dominant, previous) => APP.store.dispatch(dominantSpeakerChanged(dominant, previous, room)));
room.on( room.on(
JitsiConferenceEvents.CONFERENCE_CREATED_TIMESTAMP, JitsiConferenceEvents.CONFERENCE_CREATED_TIMESTAMP,

View File

@ -170,7 +170,7 @@ function _addConferenceListeners(conference, dispatch, state) {
conference.on( conference.on(
JitsiConferenceEvents.DOMINANT_SPEAKER_CHANGED, JitsiConferenceEvents.DOMINANT_SPEAKER_CHANGED,
id => dispatch(dominantSpeakerChanged(id, conference))); (dominant, previous) => dispatch(dominantSpeakerChanged(dominant, previous, conference)));
conference.on( conference.on(
JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,

View File

@ -6,7 +6,9 @@
* { * {
* type: DOMINANT_SPEAKER_CHANGED, * type: DOMINANT_SPEAKER_CHANGED,
* participant: { * participant: {
* id: string * conference: JitsiConference,
* id: string,
* previousSpeakers: Array<string>
* } * }
* } * }
*/ */

View File

@ -31,7 +31,8 @@ import logger from './logger';
/** /**
* Create an action for when dominant speaker changes. * Create an action for when dominant speaker changes.
* *
* @param {string} id - Participant's ID. * @param {string} dominantSpeaker - Participant ID of the dominant speaker.
* @param {Array<string>} previousSpeakers - Participant IDs of the previous speakers.
* @param {JitsiConference} conference - The {@code JitsiConference} associated * @param {JitsiConference} conference - The {@code JitsiConference} associated
* with the participant identified by the specified {@code id}. Only the local * with the participant identified by the specified {@code id}. Only the local
* participant is allowed to not specify an associated {@code JitsiConference} * participant is allowed to not specify an associated {@code JitsiConference}
@ -40,16 +41,18 @@ import logger from './logger';
* type: DOMINANT_SPEAKER_CHANGED, * type: DOMINANT_SPEAKER_CHANGED,
* participant: { * participant: {
* conference: JitsiConference, * conference: JitsiConference,
* id: string * id: string,
* previousSpeakers: Array<string>
* } * }
* }} * }}
*/ */
export function dominantSpeakerChanged(id, conference) { export function dominantSpeakerChanged(dominantSpeaker, previousSpeakers, conference) {
return { return {
type: DOMINANT_SPEAKER_CHANGED, type: DOMINANT_SPEAKER_CHANGED,
participant: { participant: {
conference, conference,
id id: dominantSpeaker,
previousSpeakers
} }
}; };
} }

View File

@ -14,6 +14,8 @@ import {
import { LOCAL_PARTICIPANT_DEFAULT_ID, PARTICIPANT_ROLE } from './constants'; import { LOCAL_PARTICIPANT_DEFAULT_ID, PARTICIPANT_ROLE } from './constants';
import { isParticipantModerator } from './functions'; import { isParticipantModerator } from './functions';
declare var interfaceConfig: Object;
/** /**
* Participant object. * Participant object.
* @typedef {Object} Participant * @typedef {Object} Participant
@ -51,13 +53,15 @@ const PARTICIPANT_PROPS_TO_OMIT_WHEN_UPDATE = [
]; ];
const DEFAULT_STATE = { const DEFAULT_STATE = {
haveParticipantWithScreenSharingFeature: false,
dominantSpeaker: undefined, dominantSpeaker: undefined,
everyoneIsModerator: false, everyoneIsModerator: false,
pinnedParticipant: undefined, fakeParticipants: new Map(),
haveParticipantWithScreenSharingFeature: false,
local: undefined, local: undefined,
pinnedParticipant: undefined,
remote: new Map(), remote: new Map(),
fakeParticipants: new Map() sortedRemoteParticipants: new Map(),
speakersList: []
}; };
/** /**
@ -91,8 +95,13 @@ ReducerRegistry.register('features/base/participants', (state = DEFAULT_STATE, a
} }
case DOMINANT_SPEAKER_CHANGED: { case DOMINANT_SPEAKER_CHANGED: {
const { participant } = action; const { participant } = action;
const { id } = participant; const { id, previousSpeakers = [] } = participant;
const { dominantSpeaker } = state; const { dominantSpeaker, local } = state;
const speakersList = [];
// Update the speakers list.
id !== local?.id && speakersList.push(id);
speakersList.push(...previousSpeakers.filter(p => p !== local?.id));
// Only one dominant speaker is allowed. // Only one dominant speaker is allowed.
if (dominantSpeaker) { if (dominantSpeaker) {
@ -102,7 +111,8 @@ ReducerRegistry.register('features/base/participants', (state = DEFAULT_STATE, a
if (_updateParticipantProperty(state, id, 'dominantSpeaker', true)) { if (_updateParticipantProperty(state, id, 'dominantSpeaker', true)) {
return { return {
...state, ...state,
dominantSpeaker: id dominantSpeaker: id,
speakersList
}; };
} }
@ -180,21 +190,22 @@ ReducerRegistry.register('features/base/participants', (state = DEFAULT_STATE, a
} }
case PARTICIPANT_JOINED: { case PARTICIPANT_JOINED: {
const participant = _participantJoined(action); const participant = _participantJoined(action);
const { id, isFakeParticipant, name, pinned } = participant;
const { pinnedParticipant, dominantSpeaker } = state; const { pinnedParticipant, dominantSpeaker } = state;
if (participant.pinned) { if (pinned) {
if (pinnedParticipant) { if (pinnedParticipant) {
_updateParticipantProperty(state, pinnedParticipant, 'pinned', false); _updateParticipantProperty(state, pinnedParticipant, 'pinned', false);
} }
state.pinnedParticipant = participant.id; state.pinnedParticipant = id;
} }
if (participant.dominantSpeaker) { if (participant.dominantSpeaker) {
if (dominantSpeaker) { if (dominantSpeaker) {
_updateParticipantProperty(state, dominantSpeaker, 'dominantSpeaker', false); _updateParticipantProperty(state, dominantSpeaker, 'dominantSpeaker', false);
} }
state.dominantSpeaker = participant.id; state.dominantSpeaker = id;
} }
const isModerator = isParticipantModerator(participant); const isModerator = isParticipantModerator(participant);
@ -213,10 +224,21 @@ ReducerRegistry.register('features/base/participants', (state = DEFAULT_STATE, a
}; };
} }
state.remote.set(participant.id, participant); state.remote.set(id, participant);
if (participant.isFakeParticipant) { // Insert the new participant.
state.fakeParticipants.set(participant.id, participant); const displayName = name
?? (typeof interfaceConfig === 'object' ? interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME : 'Fellow Jitser');
const sortedRemoteParticipants = Array.from(state.sortedRemoteParticipants);
sortedRemoteParticipants.push([ id, displayName ]);
sortedRemoteParticipants.sort((a, b) => a[1].localeCompare(b[1]));
// The sort order of participants is preserved since Map remembers the original insertion order of the keys.
state.sortedRemoteParticipants = new Map(sortedRemoteParticipants);
if (isFakeParticipant) {
state.fakeParticipants.set(id, participant);
} }
return { ...state }; return { ...state };
@ -242,6 +264,8 @@ ReducerRegistry.register('features/base/participants', (state = DEFAULT_STATE, a
return state; return state;
} }
state.sortedRemoteParticipants.delete(id);
if (!state.everyoneIsModerator && !isParticipantModerator(oldParticipant)) { if (!state.everyoneIsModerator && !isParticipantModerator(oldParticipant)) {
state.everyoneIsModerator = _isEveryoneModerator(state); state.everyoneIsModerator = _isEveryoneModerator(state);
} }
@ -272,6 +296,9 @@ ReducerRegistry.register('features/base/participants', (state = DEFAULT_STATE, a
state.dominantSpeaker = undefined; state.dominantSpeaker = undefined;
} }
// Remove the participant from the list of speakers.
state.speakersList = state.speakersList.filter(speaker => speaker !== id);
if (pinnedParticipant === id) { if (pinnedParticipant === id) {
state.pinnedParticipant = undefined; state.pinnedParticipant = undefined;
} }

View File

@ -50,6 +50,15 @@ export const SET_TILE_VIEW_DIMENSIONS = 'SET_TILE_VIEW_DIMENSIONS';
*/ */
export const SET_HORIZONTAL_VIEW_DIMENSIONS = 'SET_HORIZONTAL_VIEW_DIMENSIONS'; export const SET_HORIZONTAL_VIEW_DIMENSIONS = 'SET_HORIZONTAL_VIEW_DIMENSIONS';
/**
* The type of (redux) action which sets the reordered list of the remote participants in the filmstrip.
* {
* type: SET_REMOTE_PARTICIPANTS,
* participants: Array<string>
* }
*/
export const SET_REMOTE_PARTICIPANTS = 'SET_REMOTE_PARTICIPANTS';
/** /**
* The type of (redux) action which sets the dimensions of the thumbnails in vertical view. * The type of (redux) action which sets the dimensions of the thumbnails in vertical view.
* *

View File

@ -5,6 +5,7 @@ import { getLocalParticipant, getRemoteParticipants, pinParticipant } from '../b
import { import {
SET_HORIZONTAL_VIEW_DIMENSIONS, SET_HORIZONTAL_VIEW_DIMENSIONS,
SET_REMOTE_PARTICIPANTS,
SET_TILE_VIEW_DIMENSIONS, SET_TILE_VIEW_DIMENSIONS,
SET_VERTICAL_VIEW_DIMENSIONS, SET_VERTICAL_VIEW_DIMENSIONS,
SET_VISIBLE_REMOTE_PARTICIPANTS, SET_VISIBLE_REMOTE_PARTICIPANTS,
@ -25,6 +26,23 @@ import {
calculateThumbnailSizeForVerticalView calculateThumbnailSizeForVerticalView
} from './functions'; } from './functions';
/**
* Sets the list of the reordered remote participants based on which the visible participants in the filmstrip will be
* determined.
*
* @param {Array<string>} participants - The list of the remote participant endpoint IDs.
* @returns {{
type: SET_REMOTE_PARTICIPANTS,
participants: Array<string>
}}
*/
export function setRemoteParticipants(participants: Array<string>) {
return {
type: SET_REMOTE_PARTICIPANTS,
participants
};
}
/** /**
* Sets the dimensions of the tile view grid. * Sets the dimensions of the tile view grid.
* *

View File

@ -269,11 +269,11 @@ class Filmstrip extends PureComponent <Props> {
return `empty-${index}`; return `empty-${index}`;
} }
if (index === _remoteParticipantsLength) { if (index === 0) {
return 'local'; return 'local';
} }
return _remoteParticipants[index]; return _remoteParticipants[index - 1];
} }
_onListItemsRendered: Object => void; _onListItemsRendered: Object => void;
@ -287,7 +287,7 @@ class Filmstrip extends PureComponent <Props> {
_onListItemsRendered({ visibleStartIndex, visibleStopIndex }) { _onListItemsRendered({ visibleStartIndex, visibleStopIndex }) {
const { dispatch } = this.props; const { dispatch } = this.props;
dispatch(setVisibleRemoteParticipants(visibleStartIndex, visibleStopIndex)); dispatch(setVisibleRemoteParticipants(visibleStartIndex, visibleStopIndex + 1));
} }
_onGridItemsRendered: Object => void; _onGridItemsRendered: Object => void;
@ -305,9 +305,12 @@ class Filmstrip extends PureComponent <Props> {
visibleRowStopIndex visibleRowStopIndex
}) { }) {
const { _columns, dispatch } = this.props; const { _columns, dispatch } = this.props;
const startIndex = (visibleRowStartIndex * _columns) + visibleColumnStartIndex; let startIndex = (visibleRowStartIndex * _columns) + visibleColumnStartIndex;
const endIndex = (visibleRowStopIndex * _columns) + visibleColumnStopIndex; const endIndex = (visibleRowStopIndex * _columns) + visibleColumnStopIndex;
// In tile view, the start index needs to be offset by 1 because the first participant is the local
// participant.
startIndex = startIndex > 0 ? startIndex - 1 : 0;
dispatch(setVisibleRemoteParticipants(startIndex, endIndex)); dispatch(setVisibleRemoteParticipants(startIndex, endIndex));
} }

View File

@ -126,7 +126,8 @@ function _mapStateToProps(state, ownProps) {
return {}; return {};
} }
if (index === remoteParticipantsLength) { // Make the local participant as the first thumbnail (top left corner) in tile view.
if (index === 0) {
return { return {
_participantID: 'local', _participantID: 'local',
_horizontalOffset: horizontalOffset _horizontalOffset: horizontalOffset
@ -134,7 +135,7 @@ function _mapStateToProps(state, ownProps) {
} }
return { return {
_participantID: remoteParticipants[index], _participantID: remoteParticipants[index - 1],
_horizontalOffset: horizontalOffset _horizontalOffset: horizontalOffset
}; };
} }

View File

@ -16,6 +16,7 @@ import {
isRemoteTrackMuted isRemoteTrackMuted
} from '../base/tracks/functions'; } from '../base/tracks/functions';
import { setRemoteParticipants } from './actions.web';
import { import {
ASPECT_RATIO_BREAKPOINT, ASPECT_RATIO_BREAKPOINT,
DISPLAY_AVATAR, DISPLAY_AVATAR,
@ -265,3 +266,36 @@ export function computeDisplayMode(input: Object) {
// check hovering and change state to avatar with name // check hovering and change state to avatar with name
return isHovered ? DISPLAY_AVATAR_WITH_NAME : DISPLAY_AVATAR; return isHovered ? DISPLAY_AVATAR_WITH_NAME : DISPLAY_AVATAR;
} }
/**
* Computes the reorderd list of the remote participants.
*
* @param {*} store - The redux store.
* @returns {void}
* @private
*/
export function updateRemoteParticipants(store: Object) {
const state = store.getState();
const { fakeParticipants, sortedRemoteParticipants, speakersList } = state['features/base/participants'];
const { remoteScreenShares } = state['features/video-layout'];
const screenShares = (remoteScreenShares || []).slice();
let speakers = (speakersList || []).slice();
const remoteParticipants = new Map(sortedRemoteParticipants);
const sharedVideos = fakeParticipants ? Array.from(fakeParticipants.keys()) : [];
for (const screenshare of screenShares) {
remoteParticipants.delete(screenshare);
speakers = speakers.filter(speaker => speaker !== screenshare);
}
for (const sharedVideo of sharedVideos) {
remoteParticipants.delete(sharedVideo);
speakers = speakers.filter(speaker => speaker !== sharedVideo);
}
for (const speaker of speakers) {
remoteParticipants.delete(speaker);
}
const reorderedParticipants
= [ ...screenShares.reverse(), ...sharedVideos, ...speakers, ...Array.from(remoteParticipants.keys()) ];
store.dispatch(setRemoteParticipants(reorderedParticipants));
}

View File

@ -1,6 +1,7 @@
// @flow // @flow
import VideoLayout from '../../../modules/UI/videolayout/VideoLayout'; import VideoLayout from '../../../modules/UI/videolayout/VideoLayout';
import { PARTICIPANT_JOINED, PARTICIPANT_LEFT } from '../base/participants';
import { MiddlewareRegistry } from '../base/redux'; import { MiddlewareRegistry } from '../base/redux';
import { CLIENT_RESIZED } from '../base/responsive-ui'; import { CLIENT_RESIZED } from '../base/responsive-ui';
import { SETTINGS_UPDATED } from '../base/settings'; import { SETTINGS_UPDATED } from '../base/settings';
@ -9,8 +10,13 @@ import {
LAYOUTS LAYOUTS
} from '../video-layout'; } from '../video-layout';
import { setHorizontalViewDimensions, setTileViewDimensions, setVerticalViewDimensions } from './actions.web'; import {
setHorizontalViewDimensions,
setRemoteParticipants,
setTileViewDimensions,
setVerticalViewDimensions
} from './actions.web';
import { updateRemoteParticipants } from './functions.web';
import './subscriber.web'; import './subscriber.web';
/** /**
@ -41,6 +47,14 @@ MiddlewareRegistry.register(store => next => action => {
} }
break; break;
} }
case PARTICIPANT_JOINED: {
updateRemoteParticipants(store);
break;
}
case PARTICIPANT_LEFT: {
_updateRemoteParticipantsOnLeave(store, action.participant?.id);
break;
}
case SETTINGS_UPDATED: { case SETTINGS_UPDATED: {
if (typeof action.settings?.localFlipX === 'boolean') { if (typeof action.settings?.localFlipX === 'boolean') {
// TODO: This needs to be removed once the large video is Reactified. // TODO: This needs to be removed once the large video is Reactified.
@ -53,3 +67,22 @@ MiddlewareRegistry.register(store => next => action => {
return result; return result;
}); });
/**
* Private helper to calculate the reordered list of remote participants when a participant leaves.
*
* @param {*} store - The redux store.
* @param {string} participantId - The endpoint id of the participant leaving the call.
* @returns {void}
* @private
*/
function _updateRemoteParticipantsOnLeave(store, participantId = null) {
if (!participantId) {
return;
}
const state = store.getState();
const { remoteParticipants } = state['features/filmstrip'];
const reorderedParticipants = new Set(remoteParticipants);
reorderedParticipants.delete(participantId)
&& store.dispatch(setRemoteParticipants(Array.from(reorderedParticipants)));
}

View File

@ -1,12 +1,13 @@
// @flow // @flow
import { PARTICIPANT_JOINED, PARTICIPANT_LEFT } from '../base/participants'; import { PARTICIPANT_LEFT } from '../base/participants';
import { ReducerRegistry } from '../base/redux'; import { ReducerRegistry } from '../base/redux';
import { import {
SET_FILMSTRIP_ENABLED, SET_FILMSTRIP_ENABLED,
SET_FILMSTRIP_VISIBLE, SET_FILMSTRIP_VISIBLE,
SET_HORIZONTAL_VIEW_DIMENSIONS, SET_HORIZONTAL_VIEW_DIMENSIONS,
SET_REMOTE_PARTICIPANTS,
SET_TILE_VIEW_DIMENSIONS, SET_TILE_VIEW_DIMENSIONS,
SET_VERTICAL_VIEW_DIMENSIONS, SET_VERTICAL_VIEW_DIMENSIONS,
SET_VISIBLE_REMOTE_PARTICIPANTS, SET_VISIBLE_REMOTE_PARTICIPANTS,
@ -40,8 +41,8 @@ const DEFAULT_STATE = {
/** /**
* The ordered IDs of the remote participants displayed in the filmstrip. * The ordered IDs of the remote participants displayed in the filmstrip.
* *
* NOTE: Currently the order will match the one from the base/participants array. But this is good initial step for * @public
* reordering the remote participants. * @type {Array<string>}
*/ */
remoteParticipants: [], remoteParticipants: [],
@ -77,22 +78,21 @@ const DEFAULT_STATE = {
*/ */
visibleParticipantsEndIndex: 0, visibleParticipantsEndIndex: 0,
/**
* The visible participants in the filmstrip.
*
* @public
* @type {Array<string>}
*/
visibleParticipants: [],
/** /**
* The start index in the remote participants array that is visible in the filmstrip. * The start index in the remote participants array that is visible in the filmstrip.
* *
* @public * @public
* @type {number} * @type {number}
*/ */
visibleParticipantsStartIndex: 0 visibleParticipantsStartIndex: 0,
/**
* The visible remote participants in the filmstrip.
*
* @public
* @type {Set<string>}
*/
visibleRemoteParticipants: new Set()
}; };
ReducerRegistry.register( ReducerRegistry.register(
@ -116,6 +116,14 @@ ReducerRegistry.register(
...state, ...state,
horizontalViewDimensions: action.dimensions horizontalViewDimensions: action.dimensions
}; };
case SET_REMOTE_PARTICIPANTS: {
const { visibleParticipantsStartIndex: startIndex, visibleParticipantsEndIndex: endIndex } = state;
state.remoteParticipants = action.participants;
state.visibleRemoteParticipants = new Set(state.remoteParticipants.slice(startIndex, endIndex));
return { ...state };
}
case SET_TILE_VIEW_DIMENSIONS: case SET_TILE_VIEW_DIMENSIONS:
return { return {
...state, ...state,
@ -138,27 +146,13 @@ ReducerRegistry.register(
[action.participantId]: action.volume [action.participantId]: action.volume
} }
}; };
case SET_VISIBLE_REMOTE_PARTICIPANTS: case SET_VISIBLE_REMOTE_PARTICIPANTS: {
return { return {
...state, ...state,
visibleParticipantsStartIndex: action.startIndex, visibleParticipantsStartIndex: action.startIndex,
visibleParticipantsEndIndex: action.endIndex, visibleParticipantsEndIndex: action.endIndex,
visibleParticipants: state.remoteParticipants.slice(action.startIndex, action.endIndex + 1) visibleRemoteParticipants: new Set(state.remoteParticipants.slice(action.startIndex, action.endIndex))
}; };
case PARTICIPANT_JOINED: {
const { id, local } = action.participant;
if (!local) {
state.remoteParticipants = [ ...state.remoteParticipants, id ];
const { visibleParticipantsStartIndex: startIndex, visibleParticipantsEndIndex: endIndex } = state;
if (state.remoteParticipants.length - 1 <= endIndex) {
state.visibleParticipants = state.remoteParticipants.slice(startIndex, endIndex + 1);
}
}
return state;
} }
case PARTICIPANT_LEFT: { case PARTICIPANT_LEFT: {
const { id, local } = action.participant; const { id, local } = action.participant;
@ -166,25 +160,6 @@ ReducerRegistry.register(
if (local) { if (local) {
return state; return state;
} }
let removedParticipantIndex = 0;
state.remoteParticipants = state.remoteParticipants.filter((participantId, index) => {
if (participantId === id) {
removedParticipantIndex = index;
return false;
}
return true;
});
const { visibleParticipantsStartIndex: startIndex, visibleParticipantsEndIndex: endIndex } = state;
if (removedParticipantIndex >= startIndex && removedParticipantIndex <= endIndex) {
state.visibleParticipants = state.remoteParticipants.slice(startIndex, endIndex + 1);
}
delete state.participantsVolume[id]; delete state.participantsVolume[id];
return state; return state;

View File

@ -8,13 +8,18 @@ import { getParticipantsPaneOpen } from '../participants-pane/functions';
import { setOverflowDrawer } from '../toolbox/actions.web'; import { setOverflowDrawer } from '../toolbox/actions.web';
import { getCurrentLayout, getTileViewGridDimensions, shouldDisplayTileView, LAYOUTS } from '../video-layout'; import { getCurrentLayout, getTileViewGridDimensions, shouldDisplayTileView, LAYOUTS } from '../video-layout';
import { setHorizontalViewDimensions, setTileViewDimensions, setVerticalViewDimensions } from './actions.web'; import {
setHorizontalViewDimensions,
setTileViewDimensions,
setVerticalViewDimensions
} from './actions.web';
import { import {
ASPECT_RATIO_BREAKPOINT, ASPECT_RATIO_BREAKPOINT,
DISPLAY_DRAWER_THRESHOLD, DISPLAY_DRAWER_THRESHOLD,
SINGLE_COLUMN_BREAKPOINT, SINGLE_COLUMN_BREAKPOINT,
TWO_COLUMN_BREAKPOINT TWO_COLUMN_BREAKPOINT
} from './constants'; } from './constants';
import { updateRemoteParticipants } from './functions.web';
/** /**
* 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.
@ -153,3 +158,36 @@ StateListenerRegistry.register(
store.dispatch(setTileViewDimensions(gridDimensions)); store.dispatch(setTileViewDimensions(gridDimensions));
} }
}); });
/**
* Listens for changes to the screensharing status of the remote participants to recompute the reordered list of the
* remote endpoints.
*/
StateListenerRegistry.register(
/* selector */ state => state['features/video-layout'].remoteScreenShares,
/* listener */ (remoteScreenShares, store) => updateRemoteParticipants(store));
/**
* Listens for changes to the dominant speaker to recompute the reordered list of the remote endpoints.
*/
StateListenerRegistry.register(
/* selector */ state => state['features/base/participants'].dominantSpeaker,
/* listener */ (dominantSpeaker, store) => _reorderDominantSpeakers(store));
/**
* Private helper function that reorders the remote participants based on dominant speaker changes.
*
* @param {*} store - The redux store.
* @returns {void}
* @private
*/
function _reorderDominantSpeakers(store) {
const state = store.getState();
const { dominantSpeaker, local } = state['features/base/participants'];
const { visibleRemoteParticipants } = state['features/filmstrip'];
// Reorder the participants if the new dominant speaker is currently not visible.
if (dominantSpeaker !== local?.id && !visibleRemoteParticipants.has(dominantSpeaker)) {
updateRemoteParticipants(store);
}
}

View File

@ -22,8 +22,8 @@ declare var APP: Object;
* scrolling through the thumbnails prompting updates to the selected endpoints. * scrolling through the thumbnails prompting updates to the selected endpoints.
*/ */
StateListenerRegistry.register( StateListenerRegistry.register(
/* selector */ state => state['features/filmstrip'].visibleParticipants, /* selector */ state => state['features/filmstrip'].visibleRemoteParticipants,
/* listener */ debounce((visibleParticipants, store) => { /* listener */ debounce((visibleRemoteParticipants, store) => {
_updateReceiverVideoConstraints(store); _updateReceiverVideoConstraints(store);
}, 100)); }, 100));
@ -191,11 +191,11 @@ function _updateReceiverVideoConstraints({ getState }) {
const { maxReceiverVideoQuality, preferredVideoQuality } = state['features/video-quality']; const { maxReceiverVideoQuality, preferredVideoQuality } = state['features/video-quality'];
const { participantId: largeVideoParticipantId } = state['features/large-video']; const { participantId: largeVideoParticipantId } = state['features/large-video'];
const maxFrameHeight = Math.min(maxReceiverVideoQuality, preferredVideoQuality); const maxFrameHeight = Math.min(maxReceiverVideoQuality, preferredVideoQuality);
let { visibleParticipants } = state['features/filmstrip']; let { visibleRemoteParticipants } = state['features/filmstrip'];
// TODO: implement this on mobile. // TODO: implement this on mobile.
if (navigator.product === 'ReactNative') { if (navigator.product === 'ReactNative') {
visibleParticipants = Array.from(state['features/base/participants'].remote.keys()); visibleRemoteParticipants = new Set(Array.from(state['features/base/participants'].remote.keys()));
} }
const receiverConstraints = { const receiverConstraints = {
@ -208,22 +208,22 @@ function _updateReceiverVideoConstraints({ getState }) {
// Tile view. // Tile view.
if (shouldDisplayTileView(state)) { if (shouldDisplayTileView(state)) {
if (!visibleParticipants?.length) { if (!visibleRemoteParticipants?.size) {
return; return;
} }
visibleParticipants.forEach(participantId => { visibleRemoteParticipants.forEach(participantId => {
receiverConstraints.constraints[participantId] = { 'maxHeight': maxFrameHeight }; receiverConstraints.constraints[participantId] = { 'maxHeight': maxFrameHeight };
}); });
// Stage view. // Stage view.
} else { } else {
if (!visibleParticipants?.length && !largeVideoParticipantId) { if (!visibleRemoteParticipants?.size && !largeVideoParticipantId) {
return; return;
} }
if (visibleParticipants?.length > 0) { if (visibleRemoteParticipants?.size > 0) {
visibleParticipants.forEach(participantId => { visibleRemoteParticipants.forEach(participantId => {
receiverConstraints.constraints[participantId] = { 'maxHeight': VIDEO_QUALITY_LEVELS.LOW }; receiverConstraints.constraints[participantId] = { 'maxHeight': VIDEO_QUALITY_LEVELS.LOW };
}); });
} }