From c05a983c982b0d9a8653196b473d8267d48b2c80 Mon Sep 17 00:00:00 2001 From: Robert Pintilii Date: Thu, 7 Apr 2022 11:31:53 +0300 Subject: [PATCH] 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 --- lang/main.json | 1 + react/features/filmstrip/actionTypes.js | 11 +++ react/features/filmstrip/actions.web.js | 16 +++- react/features/filmstrip/constants.js | 2 +- react/features/filmstrip/middleware.web.js | 29 +++++- react/features/filmstrip/reducer.js | 17 +++- react/features/follow-me/middleware.js | 7 +- react/features/follow-me/subscriber.js | 8 ++ react/features/settings/actions.js | 5 ++ .../settings/components/web/MoreTab.js | 88 ++++++++++++++++++- .../settings/components/web/SettingsDialog.js | 7 +- react/features/settings/functions.js | 3 +- 12 files changed, 180 insertions(+), 14 deletions(-) diff --git a/lang/main.json b/lang/main.json index fdd75a335..b96bf1be2 100644 --- a/lang/main.json +++ b/lang/main.json @@ -919,6 +919,7 @@ "incomingMessage": "Incoming message", "language": "Language", "loggedIn": "Logged in as {{name}}", + "maxStageParticipants": "Maximum number of participants who can be pinned to the main stage", "microphones": "Microphones", "moderator": "Moderator", "more": "More", diff --git a/react/features/filmstrip/actionTypes.js b/react/features/filmstrip/actionTypes.js index b639c1e23..4cc16019c 100644 --- a/react/features/filmstrip/actionTypes.js +++ b/react/features/filmstrip/actionTypes.js @@ -159,3 +159,14 @@ export const REMOVE_STAGE_PARTICIPANT = 'REMOVE_STAGE_PARTICIPANT'; * } */ 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'; diff --git a/react/features/filmstrip/actions.web.js b/react/features/filmstrip/actions.web.js index 597f43d7e..4518ccf7c 100644 --- a/react/features/filmstrip/actions.web.js +++ b/react/features/filmstrip/actions.web.js @@ -22,7 +22,8 @@ import { SET_USER_FILMSTRIP_WIDTH, SET_USER_IS_RESIZING, SET_VERTICAL_VIEW_DIMENSIONS, - SET_VOLUME + SET_VOLUME, + SET_MAX_STAGE_PARTICIPANTS } from './actionTypes'; import { HORIZONTAL_FILMSTRIP_MARGIN, @@ -435,3 +436,16 @@ export function setStageParticipants(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 + }; +} diff --git a/react/features/filmstrip/constants.js b/react/features/filmstrip/constants.js index 09c4b8573..f99834247 100644 --- a/react/features/filmstrip/constants.js +++ b/react/features/filmstrip/constants.js @@ -293,4 +293,4 @@ export const ACTIVE_PARTICIPANT_TIMEOUT = 1000 * 60; /** * The max number of participants to be displayed on the stage filmstrip. */ -export const MAX_ACTIVE_PARTICIPANTS = 4; +export const MAX_ACTIVE_PARTICIPANTS = 6; diff --git a/react/features/filmstrip/middleware.web.js b/react/features/filmstrip/middleware.web.js index be5218983..306503d04 100644 --- a/react/features/filmstrip/middleware.web.js +++ b/react/features/filmstrip/middleware.web.js @@ -1,5 +1,7 @@ // @flow +import { batch } from 'react-redux'; + import VideoLayout from '../../../modules/UI/videolayout/VideoLayout'; import { DOMINANT_SPEAKER_CHANGED, @@ -18,7 +20,12 @@ import { setTileView } 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 { addStageParticipant, removeStageParticipant, @@ -122,7 +129,7 @@ MiddlewareRegistry.register(store => next => action => { const { dispatch, getState } = store; const { participantId, pinned } = action; const state = getState(); - const { activeParticipants } = state['features/filmstrip']; + const { activeParticipants, maxStageParticipants } = state['features/filmstrip']; let queue; if (activeParticipants.find(p => p.participantId === participantId)) { @@ -134,7 +141,7 @@ MiddlewareRegistry.register(store => next => action => { const tid = timers.get(participantId); clearTimeout(tid); - } else if (activeParticipants.length < MAX_ACTIVE_PARTICIPANTS) { + } else if (activeParticipants.length < maxStageParticipants) { queue = [ ...activeParticipants, { participantId, pinned @@ -217,6 +224,22 @@ MiddlewareRegistry.register(store => next => action => { } 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); diff --git a/react/features/filmstrip/reducer.js b/react/features/filmstrip/reducer.js index a0e6f1e84..8446f7f8d 100644 --- a/react/features/filmstrip/reducer.js +++ b/react/features/filmstrip/reducer.js @@ -17,7 +17,8 @@ import { SET_USER_IS_RESIZING, SET_VERTICAL_VIEW_DIMENSIONS, SET_VISIBLE_REMOTE_PARTICIPANTS, - SET_VOLUME + SET_VOLUME, + SET_MAX_STAGE_PARTICIPANTS } from './actionTypes'; const DEFAULT_STATE = { @@ -51,6 +52,14 @@ const DEFAULT_STATE = { */ 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. * @@ -258,6 +267,12 @@ ReducerRegistry.register( activeParticipants: state.activeParticipants.filter(p => p.participantId !== action.participantId) }; } + case SET_MAX_STAGE_PARTICIPANTS: { + return { + ...state, + maxStageParticipants: action.maxParticipants + }; + } } return state; diff --git a/react/features/follow-me/middleware.js b/react/features/follow-me/middleware.js index 3f2bacc1b..dd959786c 100644 --- a/react/features/follow-me/middleware.js +++ b/react/features/follow-me/middleware.js @@ -11,7 +11,7 @@ import { } from '../base/participants'; import { MiddlewareRegistry } from '../base/redux'; import { setFilmstripVisible } from '../filmstrip'; -import { addStageParticipant } from '../filmstrip/actions.web'; +import { addStageParticipant, setMaxStageParticipants } from '../filmstrip/actions.web'; import { setTileView } from '../video-layout'; import { @@ -189,6 +189,11 @@ function _onFollowMeCommand(attributes = {}, id, store) { stageParticipants.forEach(p => store.dispatch(addStageParticipant(p.participantId, true))); } } + + if (attributes.maxStageParticipants !== undefined + && oldState.maxStageParticipants !== attributes.maxStageParticipants) { + store.dispatch(setMaxStageParticipants(Number(attributes.maxStageParticipants))); + } } /** diff --git a/react/features/follow-me/subscriber.js b/react/features/follow-me/subscriber.js index c2dcc0fb6..c3c027d30 100644 --- a/react/features/follow-me/subscriber.js +++ b/react/features/follow-me/subscriber.js @@ -70,6 +70,13 @@ StateListenerRegistry.register( /* selector */ state => state['features/video-layout'].tileViewEnabled, /* 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 * other participants while follow me is enabled. @@ -83,6 +90,7 @@ function _getFollowMeState(state) { return { filmstripVisible: state['features/filmstrip'].visible, + maxStageParticipants: stageFilmstrip ? state['features/filmstrip'].maxStageParticipants : undefined, nextOnStage: stageFilmstrip ? undefined : pinnedParticipant && pinnedParticipant.id, pinnedStageParticipants: stageFilmstrip ? JSON.stringify(getPinnedActiveParticipants(state)) : undefined, sharedDocumentVisible: state['features/etherpad'].editing, diff --git a/react/features/settings/actions.js b/react/features/settings/actions.js index f57803256..4b88c0000 100644 --- a/react/features/settings/actions.js +++ b/react/features/settings/actions.js @@ -10,6 +10,7 @@ import { import { openDialog } from '../base/dialog'; import { i18next } from '../base/i18n'; import { updateSettings } from '../base/settings'; +import { setMaxStageParticipants } from '../filmstrip/actions.web'; import { setScreenshareFramerate } from '../screen-share/actions'; import { @@ -116,6 +117,10 @@ export function submitMoreTab(newState: Object): Function { if (newState.hideSelfView !== currentState.hideSelfView) { dispatch(updateSettings({ disableSelfView: newState.hideSelfView })); } + + if (Number(newState.maxStageParticipants) !== currentState.maxStageParticipants) { + dispatch(setMaxStageParticipants(Number(newState.maxStageParticipants))); + } }; } diff --git a/react/features/settings/components/web/MoreTab.js b/react/features/settings/components/web/MoreTab.js index a57758ace..11aed76bd 100644 --- a/react/features/settings/components/web/MoreTab.js +++ b/react/features/settings/components/web/MoreTab.js @@ -11,6 +11,7 @@ import { AbstractDialogTab } from '../../../base/dialog'; import type { Props as AbstractDialogTabProps } from '../../../base/dialog'; import { translate } from '../../../base/i18n'; import TouchmoveHack from '../../../chat/components/web/TouchmoveHack'; +import { MAX_ACTIVE_PARTICIPANTS } from '../../../filmstrip'; 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. */ - currentFramerate: string, + currentFramerate: string, /** * 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. */ - isFramerateSelectOpen: boolean, + isFramerateSelectOpen: boolean, /** * Whether or not the language select dropdown is open. @@ -124,7 +125,8 @@ class MoreTab extends AbstractDialogTab { this.state = { isFramerateSelectOpen: false, - isLanguageSelectOpen: false + isLanguageSelectOpen: false, + isMaxStageParticipantsOpen: false }; // Bind event handler so it is only bound once for every instance. @@ -136,6 +138,9 @@ class MoreTab extends AbstractDialogTab { this._onShowPrejoinPageChanged = this._onShowPrejoinPageChanged.bind(this); this._onKeyboardShortcutEnableChanged = this._onKeyboardShortcutEnableChanged.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 { 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. * @@ -490,6 +523,54 @@ class MoreTab extends AbstractDialogTab { ); } + _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) => ( + + {index + 1} + )); + + return ( +
+

+ { t('settings.maxStageParticipants') } +

+
+ + + + { maxParticipantsItems } + + + +
+
+ ); + } + /** * 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 { key = 'settings-sub-pane-right'> { showLanguageSettings && this._renderLanguageSelect() } { this._renderFramerateSelect() } + { this._renderMaxStageParticipantsSelect() } ); } diff --git a/react/features/settings/components/web/SettingsDialog.js b/react/features/settings/components/web/SettingsDialog.js index 921b2a1fd..63e35bda0 100644 --- a/react/features/settings/components/web/SettingsDialog.js +++ b/react/features/settings/components/web/SettingsDialog.js @@ -333,8 +333,8 @@ function _mapStateToProps(state, ownProps) { tabs.push({ name: SETTINGS_TABS.CALENDAR, component: CalendarTab, - label: 'settings-pane settings.calendar.title', - styles: `${classes.settingsDialog} calendar-pane` + label: 'settings.calendar.title', + styles: `settings-pane ${classes.settingsDialog} calendar-pane` }); } @@ -364,7 +364,8 @@ function _mapStateToProps(state, ownProps) { currentLanguage: tabState?.currentLanguage, hideSelfView: tabState?.hideSelfView, showPrejoinPage: tabState?.showPrejoinPage, - enabledNotifications: tabState?.enabledNotifications + enabledNotifications: tabState?.enabledNotifications, + maxStageParticipants: tabState?.maxStageParticipants }; }, styles: `settings-pane ${classes.settingsDialog} more-pane`, diff --git a/react/features/settings/functions.js b/react/features/settings/functions.js index d61a9937b..dee806a63 100644 --- a/react/features/settings/functions.js +++ b/react/features/settings/functions.js @@ -130,7 +130,8 @@ export function getMoreTabProps(stateful: Object | Function) { enabledNotifications, showNotificationsSettings: Object.keys(enabledNotifications).length > 0, 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 }; }