feat(conference): apply reduce ui for web (#16763)

* Change stage view and use newly reducedUImainToolbarButtons config to show different custom buttons as main toolbar buttons for when web is in reduced UI.
This commit is contained in:
Calinteodor
2025-12-17 12:17:06 +02:00
committed by GitHub
parent 4b2b85bd12
commit a574d5ec79
12 changed files with 148 additions and 8 deletions

View File

@@ -924,6 +924,11 @@ var config = {
// [ 'microphone', 'camera' ] // [ 'microphone', 'camera' ]
// ], // ],
// Overrides the buttons displayed in the main toolbar for reduced UI.
// When there isn't an override for a certain configuration the default jitsi-meet configuration will be used.
// The order of the buttons in the array is preserved.
// reducedUImainToolbarButtons: [ 'microphone', 'camera' ],
// Toolbar buttons which have their click/tap event exposed through the API on // Toolbar buttons which have their click/tap event exposed through the API on
// `toolbarButtonClicked`. Passing a string for the button key will // `toolbarButtonClicked`. Passing a string for the button key will
// prevent execution of the click/tap routine; passing an object with `key` and // prevent execution of the click/tap routine; passing an object with `key` and

View File

@@ -559,6 +559,7 @@ export interface IConfig {
skipConsentInMeeting?: boolean; skipConsentInMeeting?: boolean;
suggestRecording?: boolean; suggestRecording?: boolean;
}; };
reducedUImainToolbarButtons?: Array<string>;
remoteVideoMenu?: { remoteVideoMenu?: {
disableDemote?: boolean; disableDemote?: boolean;
disableGrantModerator?: boolean; disableGrantModerator?: boolean;

View File

@@ -215,6 +215,7 @@ export default [
'recordings.showPrejoinWarning', 'recordings.showPrejoinWarning',
'recordings.showRecordingLink', 'recordings.showRecordingLink',
'recordings.suggestRecording', 'recordings.suggestRecording',
'reducedUImainToolbarButtons',
'replaceParticipant', 'replaceParticipant',
'resolution', 'resolution',
'screenshotCapture', 'screenshotCapture',

View File

@@ -24,6 +24,7 @@ import { ASPECT_RATIO_NARROW, ASPECT_RATIO_WIDE } from './constants';
* determine whether and how to render it. * determine whether and how to render it.
*/ */
const REDUCED_UI_THRESHOLD = 300; const REDUCED_UI_THRESHOLD = 300;
const WEB_REDUCED_UI_THRESHOLD = 320;
/** /**
* Indicates a resize of the window. * Indicates a resize of the window.
@@ -49,6 +50,8 @@ export function clientResized(clientWidth: number, clientHeight: number) {
} }
availableWidth -= getParticipantsPaneWidth(state); availableWidth -= getParticipantsPaneWidth(state);
dispatch(setReducedUI(availableWidth, clientHeight));
} }
batch(() => { batch(() => {
@@ -106,7 +109,10 @@ export function setAspectRatio(width: number, height: number) {
*/ */
export function setReducedUI(width: number, height: number) { export function setReducedUI(width: number, height: number) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => { return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const reducedUI = Math.min(width, height) < REDUCED_UI_THRESHOLD; const threshold = navigator.product === 'ReactNative'
? REDUCED_UI_THRESHOLD
: WEB_REDUCED_UI_THRESHOLD;
const reducedUI = Math.min(width, height) < threshold;
if (reducedUI !== getState()['features/base/responsive-ui'].reducedUI) { if (reducedUI !== getState()['features/base/responsive-ui'].reducedUI) {
return dispatch({ return dispatch({

View File

@@ -78,6 +78,11 @@ interface IProps extends AbstractProps {
*/ */
_isResizing: boolean; _isResizing: boolean;
/**
* The indicator which determines whether the UI is reduced.
*/
_reducedUI: boolean;
/** /**
* Whether or not to block chat access with a nickname input form. * Whether or not to block chat access with a nickname input form.
*/ */
@@ -227,6 +232,7 @@ const Chat = ({
_focusedTab, _focusedTab,
_isResizing, _isResizing,
_messages, _messages,
_reducedUI,
_unreadMessagesCount, _unreadMessagesCount,
_unreadPollsCount, _unreadPollsCount,
_unreadFilesCount, _unreadFilesCount,
@@ -567,6 +573,10 @@ const Chat = ({
); );
} }
if (_reducedUI) {
return null;
}
return ( return (
_isOpen ? <div _isOpen ? <div
className = { classes.container } className = { classes.container }
@@ -623,6 +633,7 @@ function _mapStateToProps(state: IReduxState, _ownProps: any) {
const { isOpen, messages, unreadMessagesCount, unreadFilesCount, width, isResizing } = state['features/chat']; const { isOpen, messages, unreadMessagesCount, unreadFilesCount, width, isResizing } = state['features/chat'];
const { unreadPollsCount } = state['features/polls']; const { unreadPollsCount } = state['features/polls'];
const _localParticipant = getLocalParticipant(state); const _localParticipant = getLocalParticipant(state);
const { reducedUI } = state['features/base/responsive-ui'];
return { return {
_isModal: window.innerWidth <= SMALL_WIDTH_THRESHOLD, _isModal: window.innerWidth <= SMALL_WIDTH_THRESHOLD,
@@ -633,6 +644,7 @@ function _mapStateToProps(state: IReduxState, _ownProps: any) {
_isFileSharingTabEnabled: isFileSharingEnabled(state), _isFileSharingTabEnabled: isFileSharingEnabled(state),
_focusedTab: getFocusedTab(state), _focusedTab: getFocusedTab(state),
_messages: messages, _messages: messages,
_reducedUI: reducedUI,
_unreadMessagesCount: unreadMessagesCount, _unreadMessagesCount: unreadMessagesCount,
_unreadPollsCount: unreadPollsCount, _unreadPollsCount: unreadPollsCount,
_unreadFilesCount: unreadFilesCount, _unreadFilesCount: unreadFilesCount,

View File

@@ -90,6 +90,11 @@ interface IProps extends AbstractProps, WithTranslation {
*/ */
_overflowDrawer: boolean; _overflowDrawer: boolean;
/**
* The indicator which determines whether the UI is reduced.
*/
_reducedUI: boolean;
/** /**
* Name for this conference room. * Name for this conference room.
*/ */
@@ -226,12 +231,45 @@ class Conference extends AbstractConference<IProps, any> {
_layoutClassName, _layoutClassName,
_notificationsVisible, _notificationsVisible,
_overflowDrawer, _overflowDrawer,
_reducedUI,
_showLobby, _showLobby,
_showPrejoin, _showPrejoin,
_showVisitorsQueue, _showVisitorsQueue,
t t
} = this.props; } = this.props;
if (_reducedUI) {
return (
<div
id = 'layout_wrapper'
onMouseEnter = { this._onMouseEnter }
onMouseLeave = { this._onMouseLeave }
onMouseMove = { this._onMouseMove }
ref = { this._setBackground }>
<Chat />
<div
className = { _layoutClassName }
id = 'videoconference_page'
onMouseMove = { isMobileBrowser() ? undefined : this._onShowToolbar }>
<ConferenceInfo />
<Notice />
<div
id = 'videospace'
onTouchStart = { this._onVideospaceTouchStart }>
<LargeVideo />
</div>
<span
aria-level = { 1 }
className = 'sr-only'
role = 'heading'>
{ t('toolbar.accessibilityLabel.heading') }
</span>
<Toolbox />
</div>
</div>
);
}
return ( return (
<div <div
id = 'layout_wrapper' id = 'layout_wrapper'
@@ -418,6 +456,7 @@ class Conference extends AbstractConference<IProps, any> {
function _mapStateToProps(state: IReduxState) { function _mapStateToProps(state: IReduxState) {
const { backgroundAlpha, mouseMoveCallbackInterval } = state['features/base/config']; const { backgroundAlpha, mouseMoveCallbackInterval } = state['features/base/config'];
const { overflowDrawer } = state['features/toolbox']; const { overflowDrawer } = state['features/toolbox'];
const { reducedUI } = state['features/base/responsive-ui'];
return { return {
...abstractMapStateToProps(state), ...abstractMapStateToProps(state),
@@ -426,6 +465,7 @@ function _mapStateToProps(state: IReduxState) {
_layoutClassName: LAYOUT_CLASSNAMES[getCurrentLayout(state) ?? ''], _layoutClassName: LAYOUT_CLASSNAMES[getCurrentLayout(state) ?? ''],
_mouseMoveCallbackInterval: mouseMoveCallbackInterval, _mouseMoveCallbackInterval: mouseMoveCallbackInterval,
_overflowDrawer: overflowDrawer, _overflowDrawer: overflowDrawer,
_reducedUI: reducedUI,
_roomName: getConferenceNameForTitle(state), _roomName: getConferenceNameForTitle(state),
_showLobby: getIsLobbyVisible(state), _showLobby: getIsLobbyVisible(state),
_showPrejoin: isPrejoinPageVisible(state), _showPrejoin: isPrejoinPageVisible(state),

View File

@@ -34,6 +34,11 @@ interface IProps {
autoHide?: string[]; autoHide?: string[];
}; };
/**
* The indicator which determines whether the UI is reduced.
*/
_reducedUI: boolean;
/** /**
* Indicates whether the component should be visible or not. * Indicates whether the component should be visible or not.
*/ */
@@ -194,6 +199,12 @@ class ConferenceInfo extends Component<IProps> {
* @returns {ReactElement} * @returns {ReactElement}
*/ */
override render() { override render() {
const { _reducedUI } = this.props;
if (_reducedUI) {
return null;
}
return ( return (
<div <div
className = 'details-container' className = 'details-container'
@@ -217,9 +228,12 @@ class ConferenceInfo extends Component<IProps> {
* }} * }}
*/ */
function _mapStateToProps(state: IReduxState) { function _mapStateToProps(state: IReduxState) {
const { reducedUI } = state['features/base/responsive-ui'];
return { return {
_conferenceInfo: getConferenceInfo(state),
_reducedUI: reducedUI,
_visible: isToolboxVisible(state), _visible: isToolboxVisible(state),
_conferenceInfo: getConferenceInfo(state)
}; };
} }

View File

@@ -25,9 +25,10 @@ const useStyles = makeStyles()(theme => {
const Notice = () => { const Notice = () => {
const message = useSelector((state: IReduxState) => state['features/base/config'].noticeMessage); const message = useSelector((state: IReduxState) => state['features/base/config'].noticeMessage);
const { reducedUI } = useSelector((state: IReduxState) => state['features/base/responsive-ui']);
const { classes } = useStyles(); const { classes } = useStyles();
if (!message) { if (!message || reducedUI) {
return null; return null;
} }

View File

@@ -19,6 +19,7 @@ import {
import { import {
getJwtDisabledButtons, getJwtDisabledButtons,
getVisibleButtons, getVisibleButtons,
getVisibleButtonsForReducedUI,
isButtonEnabled, isButtonEnabled,
isToolboxVisible isToolboxVisible
} from '../../functions.web'; } from '../../functions.web';
@@ -82,8 +83,7 @@ export default function Toolbox({
const isNarrowLayout = useSelector((state: IReduxState) => state['features/base/responsive-ui'].isNarrowLayout); const isNarrowLayout = useSelector((state: IReduxState) => state['features/base/responsive-ui'].isNarrowLayout);
const videoSpaceWidth = useSelector((state: IReduxState) => state['features/base/responsive-ui'].videoSpaceWidth); const videoSpaceWidth = useSelector((state: IReduxState) => state['features/base/responsive-ui'].videoSpaceWidth);
const isModerator = useSelector(isLocalParticipantModerator); const isModerator = useSelector(isLocalParticipantModerator);
const customToolbarButtons = useSelector( const customToolbarButtons = useSelector((state: IReduxState) => state['features/base/config'].customToolbarButtons);
(state: IReduxState) => state['features/base/config'].customToolbarButtons);
const iAmRecorder = useSelector((state: IReduxState) => state['features/base/config'].iAmRecorder); const iAmRecorder = useSelector((state: IReduxState) => state['features/base/config'].iAmRecorder);
const iAmSipGateway = useSelector((state: IReduxState) => state['features/base/config'].iAmSipGateway); const iAmSipGateway = useSelector((state: IReduxState) => state['features/base/config'].iAmSipGateway);
const overflowDrawer = useSelector((state: IReduxState) => state['features/toolbox'].overflowDrawer); const overflowDrawer = useSelector((state: IReduxState) => state['features/toolbox'].overflowDrawer);
@@ -110,6 +110,8 @@ export default function Toolbox({
const toolbarVisible = useSelector(isToolboxVisible); const toolbarVisible = useSelector(isToolboxVisible);
const mainToolbarButtonsThresholds const mainToolbarButtonsThresholds
= useSelector((state: IReduxState) => state['features/toolbox'].mainToolbarButtonsThresholds); = useSelector((state: IReduxState) => state['features/toolbox'].mainToolbarButtonsThresholds);
const { reducedUImainToolbarButtons } = useSelector((state: IReduxState) => state['features/base/config']);
const reducedUI = useSelector((state: IReduxState) => state['features/base/responsive-ui'].reducedUI);
const allButtons = useToolboxButtons(customToolbarButtons); const allButtons = useToolboxButtons(customToolbarButtons);
const isMobile = isMobileBrowser(); const isMobile = isMobileBrowser();
const endConferenceSupported = Boolean(conference?.isEndConferenceSupported() && isModerator); const endConferenceSupported = Boolean(conference?.isEndConferenceSupported() && isModerator);
@@ -233,7 +235,7 @@ export default function Toolbox({
const toolbarAccLabel = 'toolbar.accessibilityLabel.moreActionsMenu'; const toolbarAccLabel = 'toolbar.accessibilityLabel.moreActionsMenu';
const containerClassName = `toolbox-content${isMobile || isNarrowLayout ? ' toolbox-content-mobile' : ''}`; const containerClassName = `toolbox-content${isMobile || isNarrowLayout ? ' toolbox-content-mobile' : ''}`;
const { mainMenuButtons, overflowMenuButtons } = getVisibleButtons({ const normalUIButtons = getVisibleButtons({
allButtons, allButtons,
buttonsWithNotifyClick, buttonsWithNotifyClick,
toolbarButtons: toolbarButtonsToUse, toolbarButtons: toolbarButtonsToUse,
@@ -241,6 +243,20 @@ export default function Toolbox({
jwtDisabledButtons, jwtDisabledButtons,
mainToolbarButtonsThresholds mainToolbarButtonsThresholds
}); });
const reducedUIButtons = getVisibleButtonsForReducedUI({
allButtons,
buttonsWithNotifyClick,
jwtDisabledButtons,
reducedUImainToolbarButtons,
});
const mainMenuButtons = reducedUI
? reducedUIButtons.mainMenuButtons
: normalUIButtons.mainMenuButtons;
const overflowMenuButtons = reducedUI
? []
: normalUIButtons.overflowMenuButtons;
const raiseHandInOverflowMenu = overflowMenuButtons.some(({ key }) => key === 'raisehand'); const raiseHandInOverflowMenu = overflowMenuButtons.some(({ key }) => key === 'raisehand');
const showReactionsInOverflowMenu = _shouldDisplayReactionsButtons const showReactionsInOverflowMenu = _shouldDisplayReactionsButtons
&& ( && (

View File

@@ -12,6 +12,8 @@ export const DUMMY_9_BUTTONS_THRESHOLD_VALUE = Symbol('9_BUTTONS_THRESHOLD_VALUE
*/ */
export const DUMMY_10_BUTTONS_THRESHOLD_VALUE = Symbol('10_BUTTONS_THRESHOLD_VALUE'); export const DUMMY_10_BUTTONS_THRESHOLD_VALUE = Symbol('10_BUTTONS_THRESHOLD_VALUE');
export const DEFAULT_REDUCED_UI_MAIN_TOOLBAR_BUTTONS = [ 'microphone', 'camera' ];
/** /**
* Thresholds for displaying toolbox buttons. * Thresholds for displaying toolbox buttons.
*/ */

View File

@@ -6,9 +6,9 @@ import { IGUMPendingState } from '../base/media/types';
import { isScreenMediaShared } from '../screen-share/functions'; import { isScreenMediaShared } from '../screen-share/functions';
import { isWhiteboardVisible } from '../whiteboard/functions'; import { isWhiteboardVisible } from '../whiteboard/functions';
import { MAIN_TOOLBAR_BUTTONS_PRIORITY, TOOLBAR_TIMEOUT } from './constants'; import { DEFAULT_REDUCED_UI_MAIN_TOOLBAR_BUTTONS, MAIN_TOOLBAR_BUTTONS_PRIORITY, TOOLBAR_TIMEOUT } from './constants';
import { isButtonEnabled } from './functions.any'; import { isButtonEnabled } from './functions.any';
import { IGetVisibleButtonsParams, IToolboxButton, NOTIFY_CLICK_MODE } from './types'; import { IGetVisibleButtonsForReducedUIParams, IGetVisibleButtonsParams, IToolboxButton, NOTIFY_CLICK_MODE } from './types';
export * from './functions.any'; export * from './functions.any';
@@ -201,6 +201,41 @@ export function getVisibleButtons({
}; };
} }
/**
* Returns buttons that need to be rendered for reduced UI mode.
*
* @param {IGetVisibleButtonsForReducedUIParams} params - The parameters needed to extract the visible buttons.
* @returns {Object} - The visible buttons for reduced ui.
*/
export function getVisibleButtonsForReducedUI({
allButtons,
buttonsWithNotifyClick,
jwtDisabledButtons,
reducedUImainToolbarButtons
}: IGetVisibleButtonsForReducedUIParams) {
setButtonsNotifyClickMode(allButtons, buttonsWithNotifyClick);
if (!Array.isArray(reducedUImainToolbarButtons) || reducedUImainToolbarButtons.length === 0) {
const defaultButtons = DEFAULT_REDUCED_UI_MAIN_TOOLBAR_BUTTONS.map(key => allButtons[key]);
return {
mainMenuButtons: defaultButtons
};
}
const filteredButtons = reducedUImainToolbarButtons.filter(key =>
typeof key !== 'undefined'
&& !jwtDisabledButtons.includes(key)
&& isButtonEnabled(key, reducedUImainToolbarButtons)
&& allButtons[key]);
const mainMenuButtons = filteredButtons.map(key => allButtons[key]);
return {
mainMenuButtons
};
}
/** /**
* Returns the list of participant menu buttons that have that notify the api when clicked. * Returns the list of participant menu buttons that have that notify the api when clicked.
* *

View File

@@ -107,3 +107,10 @@ export interface IGetVisibleButtonsParams {
mainToolbarButtonsThresholds: IMainToolbarButtonThresholds; mainToolbarButtonsThresholds: IMainToolbarButtonThresholds;
toolbarButtons: string[]; toolbarButtons: string[];
} }
export interface IGetVisibleButtonsForReducedUIParams {
allButtons: { [key: string]: IToolboxButton; };
buttonsWithNotifyClick: Map<string, NOTIFY_CLICK_MODE>;
jwtDisabledButtons: string[];
reducedUImainToolbarButtons?: string[];
}