feat(stage-filmstrip) Added user configurable max (#11324)

The user can set the max number of participants that can be displayed on stage
Send the number on follow me to all participants
This commit is contained in:
Robert Pintilii 2022-04-07 11:31:53 +03:00 committed by GitHub
parent 6687c3f4ab
commit c05a983c98
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 180 additions and 14 deletions

View File

@ -919,6 +919,7 @@
"incomingMessage": "Incoming message", "incomingMessage": "Incoming message",
"language": "Language", "language": "Language",
"loggedIn": "Logged in as {{name}}", "loggedIn": "Logged in as {{name}}",
"maxStageParticipants": "Maximum number of participants who can be pinned to the main stage",
"microphones": "Microphones", "microphones": "Microphones",
"moderator": "Moderator", "moderator": "Moderator",
"more": "More", "more": "More",

View File

@ -159,3 +159,14 @@ export const REMOVE_STAGE_PARTICIPANT = 'REMOVE_STAGE_PARTICIPANT';
* } * }
*/ */
export const SET_STAGE_PARTICIPANTS = 'SET_STAGE_PARTICIPANTS'; export const SET_STAGE_PARTICIPANTS = 'SET_STAGE_PARTICIPANTS';
/**
* The type of Redux action which sets the max number of active participants.
* (the participants displayed on the stage filmstrip).
* {
* type: SET_MAX_STAGE_PARTICIPANTS,
* maxParticipants: Number
* }
*/
export const SET_MAX_STAGE_PARTICIPANTS = 'SET_MAX_STAGE_PARTICIPANTS';

View File

@ -22,7 +22,8 @@ import {
SET_USER_FILMSTRIP_WIDTH, SET_USER_FILMSTRIP_WIDTH,
SET_USER_IS_RESIZING, SET_USER_IS_RESIZING,
SET_VERTICAL_VIEW_DIMENSIONS, SET_VERTICAL_VIEW_DIMENSIONS,
SET_VOLUME SET_VOLUME,
SET_MAX_STAGE_PARTICIPANTS
} from './actionTypes'; } from './actionTypes';
import { import {
HORIZONTAL_FILMSTRIP_MARGIN, HORIZONTAL_FILMSTRIP_MARGIN,
@ -435,3 +436,16 @@ export function setStageParticipants(queue) {
queue queue
}; };
} }
/**
* Sets the max number of participants to be displayed on stage.
*
* @param {number} maxParticipants - Max number of participants.
* @returns {Object}
*/
export function setMaxStageParticipants(maxParticipants) {
return {
type: SET_MAX_STAGE_PARTICIPANTS,
maxParticipants
};
}

View File

@ -293,4 +293,4 @@ export const ACTIVE_PARTICIPANT_TIMEOUT = 1000 * 60;
/** /**
* The max number of participants to be displayed on the stage filmstrip. * The max number of participants to be displayed on the stage filmstrip.
*/ */
export const MAX_ACTIVE_PARTICIPANTS = 4; export const MAX_ACTIVE_PARTICIPANTS = 6;

View File

@ -1,5 +1,7 @@
// @flow // @flow
import { batch } from 'react-redux';
import VideoLayout from '../../../modules/UI/videolayout/VideoLayout'; import VideoLayout from '../../../modules/UI/videolayout/VideoLayout';
import { import {
DOMINANT_SPEAKER_CHANGED, DOMINANT_SPEAKER_CHANGED,
@ -18,7 +20,12 @@ import {
setTileView setTileView
} from '../video-layout'; } from '../video-layout';
import { ADD_STAGE_PARTICIPANT, REMOVE_STAGE_PARTICIPANT, SET_USER_FILMSTRIP_WIDTH } from './actionTypes'; import {
ADD_STAGE_PARTICIPANT,
REMOVE_STAGE_PARTICIPANT,
SET_MAX_STAGE_PARTICIPANTS,
SET_USER_FILMSTRIP_WIDTH
} from './actionTypes';
import { import {
addStageParticipant, addStageParticipant,
removeStageParticipant, removeStageParticipant,
@ -122,7 +129,7 @@ MiddlewareRegistry.register(store => next => action => {
const { dispatch, getState } = store; const { dispatch, getState } = store;
const { participantId, pinned } = action; const { participantId, pinned } = action;
const state = getState(); const state = getState();
const { activeParticipants } = state['features/filmstrip']; const { activeParticipants, maxStageParticipants } = state['features/filmstrip'];
let queue; let queue;
if (activeParticipants.find(p => p.participantId === participantId)) { if (activeParticipants.find(p => p.participantId === participantId)) {
@ -134,7 +141,7 @@ MiddlewareRegistry.register(store => next => action => {
const tid = timers.get(participantId); const tid = timers.get(participantId);
clearTimeout(tid); clearTimeout(tid);
} else if (activeParticipants.length < MAX_ACTIVE_PARTICIPANTS) { } else if (activeParticipants.length < maxStageParticipants) {
queue = [ ...activeParticipants, { queue = [ ...activeParticipants, {
participantId, participantId,
pinned pinned
@ -217,6 +224,22 @@ MiddlewareRegistry.register(store => next => action => {
} }
break; break;
} }
case SET_MAX_STAGE_PARTICIPANTS: {
const { maxParticipants } = action;
const { activeParticipants } = store.getState()['features/filmstrip'];
const newMax = Math.min(MAX_ACTIVE_PARTICIPANTS, maxParticipants);
action.maxParticipants = newMax;
if (newMax < activeParticipants.length) {
const toRemove = activeParticipants.slice(0, activeParticipants.length - newMax);
batch(() => {
toRemove.forEach(p => store.dispatch(removeStageParticipant(p.participantId)));
});
}
break;
}
} }
return result ?? next(action); return result ?? next(action);

View File

@ -17,7 +17,8 @@ import {
SET_USER_IS_RESIZING, SET_USER_IS_RESIZING,
SET_VERTICAL_VIEW_DIMENSIONS, SET_VERTICAL_VIEW_DIMENSIONS,
SET_VISIBLE_REMOTE_PARTICIPANTS, SET_VISIBLE_REMOTE_PARTICIPANTS,
SET_VOLUME SET_VOLUME,
SET_MAX_STAGE_PARTICIPANTS
} from './actionTypes'; } from './actionTypes';
const DEFAULT_STATE = { const DEFAULT_STATE = {
@ -51,6 +52,14 @@ const DEFAULT_STATE = {
*/ */
isResizing: false, isResizing: false,
/**
* The current max number of participants to be displayed on the stage filmstrip.
*
* @public
* @type {Number}
*/
maxStageParticipants: 4,
/** /**
* The custom audio volume levels per participant. * The custom audio volume levels per participant.
* *
@ -258,6 +267,12 @@ ReducerRegistry.register(
activeParticipants: state.activeParticipants.filter(p => p.participantId !== action.participantId) activeParticipants: state.activeParticipants.filter(p => p.participantId !== action.participantId)
}; };
} }
case SET_MAX_STAGE_PARTICIPANTS: {
return {
...state,
maxStageParticipants: action.maxParticipants
};
}
} }
return state; return state;

View File

@ -11,7 +11,7 @@ import {
} from '../base/participants'; } from '../base/participants';
import { MiddlewareRegistry } from '../base/redux'; import { MiddlewareRegistry } from '../base/redux';
import { setFilmstripVisible } from '../filmstrip'; import { setFilmstripVisible } from '../filmstrip';
import { addStageParticipant } from '../filmstrip/actions.web'; import { addStageParticipant, setMaxStageParticipants } from '../filmstrip/actions.web';
import { setTileView } from '../video-layout'; import { setTileView } from '../video-layout';
import { import {
@ -189,6 +189,11 @@ function _onFollowMeCommand(attributes = {}, id, store) {
stageParticipants.forEach(p => store.dispatch(addStageParticipant(p.participantId, true))); stageParticipants.forEach(p => store.dispatch(addStageParticipant(p.participantId, true)));
} }
} }
if (attributes.maxStageParticipants !== undefined
&& oldState.maxStageParticipants !== attributes.maxStageParticipants) {
store.dispatch(setMaxStageParticipants(Number(attributes.maxStageParticipants)));
}
} }
/** /**

View File

@ -70,6 +70,13 @@ StateListenerRegistry.register(
/* selector */ state => state['features/video-layout'].tileViewEnabled, /* selector */ state => state['features/video-layout'].tileViewEnabled,
/* listener */ _sendFollowMeCommand); /* listener */ _sendFollowMeCommand);
/**
* Subscribes to changes to the max number of stage participants setting.
*/
StateListenerRegistry.register(
/* selector */ state => state['features/filmstrip'].maxStageParticipants,
/* listener */ _sendFollowMeCommand);
/** /**
* Private selector for returning state from redux that should be respected by * Private selector for returning state from redux that should be respected by
* other participants while follow me is enabled. * other participants while follow me is enabled.
@ -83,6 +90,7 @@ function _getFollowMeState(state) {
return { return {
filmstripVisible: state['features/filmstrip'].visible, filmstripVisible: state['features/filmstrip'].visible,
maxStageParticipants: stageFilmstrip ? state['features/filmstrip'].maxStageParticipants : undefined,
nextOnStage: stageFilmstrip ? undefined : pinnedParticipant && pinnedParticipant.id, nextOnStage: stageFilmstrip ? undefined : pinnedParticipant && pinnedParticipant.id,
pinnedStageParticipants: stageFilmstrip ? JSON.stringify(getPinnedActiveParticipants(state)) : undefined, pinnedStageParticipants: stageFilmstrip ? JSON.stringify(getPinnedActiveParticipants(state)) : undefined,
sharedDocumentVisible: state['features/etherpad'].editing, sharedDocumentVisible: state['features/etherpad'].editing,

View File

@ -10,6 +10,7 @@ import {
import { openDialog } from '../base/dialog'; import { openDialog } from '../base/dialog';
import { i18next } from '../base/i18n'; import { i18next } from '../base/i18n';
import { updateSettings } from '../base/settings'; import { updateSettings } from '../base/settings';
import { setMaxStageParticipants } from '../filmstrip/actions.web';
import { setScreenshareFramerate } from '../screen-share/actions'; import { setScreenshareFramerate } from '../screen-share/actions';
import { import {
@ -116,6 +117,10 @@ export function submitMoreTab(newState: Object): Function {
if (newState.hideSelfView !== currentState.hideSelfView) { if (newState.hideSelfView !== currentState.hideSelfView) {
dispatch(updateSettings({ disableSelfView: newState.hideSelfView })); dispatch(updateSettings({ disableSelfView: newState.hideSelfView }));
} }
if (Number(newState.maxStageParticipants) !== currentState.maxStageParticipants) {
dispatch(setMaxStageParticipants(Number(newState.maxStageParticipants)));
}
}; };
} }

View File

@ -11,6 +11,7 @@ import { AbstractDialogTab } from '../../../base/dialog';
import type { Props as AbstractDialogTabProps } from '../../../base/dialog'; import type { Props as AbstractDialogTabProps } from '../../../base/dialog';
import { translate } from '../../../base/i18n'; import { translate } from '../../../base/i18n';
import TouchmoveHack from '../../../chat/components/web/TouchmoveHack'; import TouchmoveHack from '../../../chat/components/web/TouchmoveHack';
import { MAX_ACTIVE_PARTICIPANTS } from '../../../filmstrip';
import { SS_DEFAULT_FRAME_RATE } from '../../constants'; import { SS_DEFAULT_FRAME_RATE } from '../../constants';
/** /**
@ -22,7 +23,7 @@ export type Props = {
/** /**
* The currently selected desktop share frame rate in the frame rate select dropdown. * The currently selected desktop share frame rate in the frame rate select dropdown.
*/ */
currentFramerate: string, currentFramerate: string,
/** /**
* The currently selected language to display in the language select * The currently selected language to display in the language select
@ -99,7 +100,7 @@ type State = {
/** /**
* Whether or not the desktop share frame rate select dropdown is open. * Whether or not the desktop share frame rate select dropdown is open.
*/ */
isFramerateSelectOpen: boolean, isFramerateSelectOpen: boolean,
/** /**
* Whether or not the language select dropdown is open. * Whether or not the language select dropdown is open.
@ -124,7 +125,8 @@ class MoreTab extends AbstractDialogTab<Props, State> {
this.state = { this.state = {
isFramerateSelectOpen: false, isFramerateSelectOpen: false,
isLanguageSelectOpen: false isLanguageSelectOpen: false,
isMaxStageParticipantsOpen: false
}; };
// Bind event handler so it is only bound once for every instance. // Bind event handler so it is only bound once for every instance.
@ -136,6 +138,9 @@ class MoreTab extends AbstractDialogTab<Props, State> {
this._onShowPrejoinPageChanged = this._onShowPrejoinPageChanged.bind(this); this._onShowPrejoinPageChanged = this._onShowPrejoinPageChanged.bind(this);
this._onKeyboardShortcutEnableChanged = this._onKeyboardShortcutEnableChanged.bind(this); this._onKeyboardShortcutEnableChanged = this._onKeyboardShortcutEnableChanged.bind(this);
this._onHideSelfViewChanged = this._onHideSelfViewChanged.bind(this); this._onHideSelfViewChanged = this._onHideSelfViewChanged.bind(this);
this._renderMaxStageParticipantsSelect = this._renderMaxStageParticipantsSelect.bind(this);
this._onMaxStageParticipantsSelect = this._onMaxStageParticipantsSelect.bind(this);
this._onMaxStageParticipantsOpenChange = this._onMaxStageParticipantsOpenChange.bind(this);
} }
/** /**
@ -277,6 +282,34 @@ class MoreTab extends AbstractDialogTab<Props, State> {
super._onChange({ hideSelfView: checked }); super._onChange({ hideSelfView: checked });
} }
_onMaxStageParticipantsOpenChange: (Object) => void;
/**
* Callback invoked to toggle display of the max stage participants select dropdown.
*
* @param {Object} event - The event for opening or closing the dropdown.
* @private
* @returns {void}
*/
_onMaxStageParticipantsOpenChange({ isOpen }) {
this.setState({ isMaxStageParticipantsOpen: isOpen });
}
_onMaxStageParticipantsSelect: (Object) => void;
/**
* Callback invoked to select a max number of stage participants from the select dropdown.
*
* @param {Object} e - The key event to handle.
* @private
* @returns {void}
*/
_onMaxStageParticipantsSelect(e) {
const maxParticipants = e.currentTarget.getAttribute('data-maxparticipants');
super._onChange({ maxStageParticipants: maxParticipants });
}
/** /**
* Returns the React Element for the desktop share frame rate dropdown. * Returns the React Element for the desktop share frame rate dropdown.
* *
@ -490,6 +523,54 @@ class MoreTab extends AbstractDialogTab<Props, State> {
); );
} }
_renderMaxStageParticipantsSelect: () => void;
/**
* Returns the React Element for the max stage participants dropdown.
*
* @returns {ReactElement}
*/
_renderMaxStageParticipantsSelect() {
const { maxStageParticipants, t } = this.props;
const maxParticipantsItems = Array(MAX_ACTIVE_PARTICIPANTS).fill(0)
.map((no, index) => (
<DropdownItem
data-maxparticipants = { index + 1 }
key = { index + 1 }
onClick = { this._onMaxStageParticipantsSelect }>
{index + 1}
</DropdownItem>));
return (
<div
className = 'settings-sub-pane-element'
key = 'maxStageParticipants'>
<h2 className = 'mock-atlaskit-label'>
{ t('settings.maxStageParticipants') }
</h2>
<div className = 'dropdown-menu'>
<TouchmoveHack
flex = { true }
isModal = { true }>
<DropdownMenu
isOpen = { this.state.isMaxStageParticipantsOpen }
onOpenChange = { this._onMaxStageParticipantsOpenChange }
shouldFitContainer = { true }
trigger = { maxStageParticipants }
triggerButtonProps = {{
shouldFitContainer: true
}}
triggerType = 'button'>
<DropdownItemGroup>
{ maxParticipantsItems }
</DropdownItemGroup>
</DropdownMenu>
</TouchmoveHack>
</div>
</div>
);
}
/** /**
* Returns the React element that needs to be displayed on the right half of the more tabs. * Returns the React element that needs to be displayed on the right half of the more tabs.
* *
@ -505,6 +586,7 @@ class MoreTab extends AbstractDialogTab<Props, State> {
key = 'settings-sub-pane-right'> key = 'settings-sub-pane-right'>
{ showLanguageSettings && this._renderLanguageSelect() } { showLanguageSettings && this._renderLanguageSelect() }
{ this._renderFramerateSelect() } { this._renderFramerateSelect() }
{ this._renderMaxStageParticipantsSelect() }
</div> </div>
); );
} }

View File

@ -333,8 +333,8 @@ function _mapStateToProps(state, ownProps) {
tabs.push({ tabs.push({
name: SETTINGS_TABS.CALENDAR, name: SETTINGS_TABS.CALENDAR,
component: CalendarTab, component: CalendarTab,
label: 'settings-pane settings.calendar.title', label: 'settings.calendar.title',
styles: `${classes.settingsDialog} calendar-pane` styles: `settings-pane ${classes.settingsDialog} calendar-pane`
}); });
} }
@ -364,7 +364,8 @@ function _mapStateToProps(state, ownProps) {
currentLanguage: tabState?.currentLanguage, currentLanguage: tabState?.currentLanguage,
hideSelfView: tabState?.hideSelfView, hideSelfView: tabState?.hideSelfView,
showPrejoinPage: tabState?.showPrejoinPage, showPrejoinPage: tabState?.showPrejoinPage,
enabledNotifications: tabState?.enabledNotifications enabledNotifications: tabState?.enabledNotifications,
maxStageParticipants: tabState?.maxStageParticipants
}; };
}, },
styles: `settings-pane ${classes.settingsDialog} more-pane`, styles: `settings-pane ${classes.settingsDialog} more-pane`,

View File

@ -130,7 +130,8 @@ export function getMoreTabProps(stateful: Object | Function) {
enabledNotifications, enabledNotifications,
showNotificationsSettings: Object.keys(enabledNotifications).length > 0, showNotificationsSettings: Object.keys(enabledNotifications).length > 0,
showPrejoinPage: !state['features/base/settings'].userSelectedSkipPrejoin, showPrejoinPage: !state['features/base/settings'].userSelectedSkipPrejoin,
showPrejoinSettings: state['features/base/config'].prejoinConfig?.enabled showPrejoinSettings: state['features/base/config'].prejoinConfig?.enabled,
maxStageParticipants: state['features/filmstrip'].maxStageParticipants
}; };
} }