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",
"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",

View File

@ -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';

View File

@ -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
};
}

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.
*/
export const MAX_ACTIVE_PARTICIPANTS = 4;
export const MAX_ACTIVE_PARTICIPANTS = 6;

View File

@ -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);

View File

@ -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;

View File

@ -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)));
}
}
/**

View File

@ -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,

View File

@ -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)));
}
};
}

View File

@ -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';
/**
@ -124,7 +125,8 @@ class MoreTab extends AbstractDialogTab<Props, State> {
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<Props, State> {
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<Props, State> {
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<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.
*
@ -505,6 +586,7 @@ class MoreTab extends AbstractDialogTab<Props, State> {
key = 'settings-sub-pane-right'>
{ showLanguageSettings && this._renderLanguageSelect() }
{ this._renderFramerateSelect() }
{ this._renderMaxStageParticipantsSelect() }
</div>
);
}

View File

@ -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`,

View File

@ -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
};
}