init
Some checks failed
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
2025-09-02 14:49:16 +08:00
commit 38ba663466
2885 changed files with 391107 additions and 0 deletions

View File

@@ -0,0 +1,168 @@
/**
* The type of the action which clears the Toolbox visibility timeout.
*
* {
* type: CLEAR_TOOLBOX_TIMEOUT
* }
*/
export const CLEAR_TOOLBOX_TIMEOUT = 'CLEAR_TOOLBOX_TIMEOUT';
/**
* The type of (redux) action which signals that a custom button was pressed.
*
* @returns {{
* type: CUSTOM_BUTTON_PRESSED,
* id: string,
* text: string
* }}
*/
export const CUSTOM_BUTTON_PRESSED = 'CUSTOM_BUTTON_PRESSED';
/**
* The type of (redux) action which updates whether the conference is or is not
* currently in full screen view.
*
* {
* type: FULL_SCREEN_CHANGED,
* fullScreen: boolean
* }
*/
export const FULL_SCREEN_CHANGED = 'FULL_SCREEN_CHANGED';
/**
* The type of (redux) action which sets the buttonsWithNotifyClick redux property.
*
* {
* type: SET_BUTTONS_WITH_NOTIFY_CLICK,
* buttonsWithNotifyClick: Map<string, NOTIFY_CLICK_MODE>
* }
*/
export const SET_BUTTONS_WITH_NOTIFY_CLICK = 'SET_BUTTONS_WITH_NOTIFY_CLICK';
/**
* The type of (redux) action which sets the participantMenuButtonsWithNotifyClick redux property.
*
* {
* type: SET_BUTTONS_WITH_NOTIFY_CLICK,
* participantMenuButtonsWithNotifyClick: Map<string, NOTIFY_CLICK_MODE>
* }
*/
export const SET_PARTICIPANT_MENU_BUTTONS_WITH_NOTIFY_CLICK = 'SET_PARTICIPANT_MENU_BUTTONS_WITH_NOTIFY_CLICK';
/**
* The type of (redux) action which requests full screen mode be entered or
* exited.
*
* {
* type: SET_FULL_SCREEN,
* fullScreen: boolean
* }
*/
export const SET_FULL_SCREEN = 'SET_FULL_SCREEN';
/**
* The type of the (redux) action which shows/hides the hangup menu.
*
* {
* type: SET_HANGUP_MENU_VISIBLE,
* visible: boolean
* }
*/
export const SET_HANGUP_MENU_VISIBLE = 'SET_HANGUP_MENU_VISIBLE';
/**
* The type of the (redux) action which sets the main toolbar thresholds.
*
* {
* type: SET_MAIN_TOOLBAR_BUTTONS_THRESHOLDS,
* mainToolbarButtonsThresholds: IMainToolbarButtonThresholds
* }
*/
export const SET_MAIN_TOOLBAR_BUTTONS_THRESHOLDS = 'SET_MAIN_TOOLBAR_BUTTONS_THRESHOLDS';
/**
* The type of the redux action that toggles whether the overflow menu(s) should be shown as drawers.
*/
export const SET_OVERFLOW_DRAWER = 'SET_OVERFLOW_DRAWER';
/**
* The type of the (redux) action which shows/hides the OverflowMenu.
*
* {
* type: SET_OVERFLOW_MENU_VISIBLE,
* visible: boolean
* }
*/
export const SET_OVERFLOW_MENU_VISIBLE = 'SET_OVERFLOW_MENU_VISIBLE';
/**
* The type of the action which sets enabled toolbar buttons.
*
* {
* type: SET_TOOLBAR_BUTTONS,
* toolbarButtons: Array<string>
* }
*/
export const SET_TOOLBAR_BUTTONS = 'SET_TOOLBAR_BUTTONS';
/**
* The type of the action which sets the indicator which determines whether a
* fToolbar in the Toolbox is hovered.
*
* {
* type: SET_TOOLBAR_HOVERED,
* hovered: boolean
* }
*/
export const SET_TOOLBAR_HOVERED = 'SET_TOOLBAR_HOVERED';
/**
* The type of the (redux) action which enables/disables the Toolbox.
*
* {
* type: SET_TOOLBOX_ENABLED,
* enabled: boolean
* }
*/
export const SET_TOOLBOX_ENABLED = 'SET_TOOLBOX_ENABLED';
/**
* The type of the action which sets a new Toolbox visibility timeout and its
* delay.
*
* {
* type: SET_TOOLBOX_TIMEOUT,
* handler: Function,
* timeoutMS: number
* }
*/
export const SET_TOOLBOX_TIMEOUT = 'SET_TOOLBOX_TIMEOUT';
/**
* The type of the (redux) action which shows/hides the Toolbox.
*
* {
* type: SET_TOOLBOX_VISIBLE,
* visible: boolean
* }
*/
export const SET_TOOLBOX_VISIBLE = 'SET_TOOLBOX_VISIBLE';
/**
* The type of the redux action which toggles the toolbox visibility regardless of it's current state.
*
* {
* type: TOGGLE_TOOLBOX_VISIBLE
* }
*/
export const TOGGLE_TOOLBOX_VISIBLE = 'TOGGLE_TOOLBOX_VISIBLE';
/**
* The type of the redux action which sets whether the toolbox should be shifted up or not.
*
* {
* type: SET_TOOLBOX_SHIFT_UP
* }
*/
export const SET_TOOLBOX_SHIFT_UP = 'SET_TOOLBOX_SHIFT_UP';

View File

@@ -0,0 +1,189 @@
import { VIDEO_MUTE, createToolbarEvent } from '../analytics/AnalyticsEvents';
import { sendAnalytics } from '../analytics/functions';
import { IStore } from '../app/types';
import { setAudioOnly } from '../base/audio-only/actions';
import { setVideoMuted } from '../base/media/actions';
import { VIDEO_MUTISM_AUTHORITY } from '../base/media/constants';
import {
SET_MAIN_TOOLBAR_BUTTONS_THRESHOLDS,
SET_TOOLBOX_ENABLED,
SET_TOOLBOX_SHIFT_UP,
SET_TOOLBOX_VISIBLE,
TOGGLE_TOOLBOX_VISIBLE
} from './actionTypes';
import { DUMMY_10_BUTTONS_THRESHOLD_VALUE, DUMMY_9_BUTTONS_THRESHOLD_VALUE } from './constants';
import { IMainToolbarButtonThresholds, IMainToolbarButtonThresholdsUnfiltered } from './types';
/**
* Enables/disables the toolbox.
*
* @param {boolean} enabled - True to enable the toolbox or false to disable it.
* @returns {{
* type: SET_TOOLBOX_ENABLED,
* enabled: boolean
* }}
*/
export function setToolboxEnabled(enabled: boolean) {
return {
type: SET_TOOLBOX_ENABLED,
enabled
};
}
/**
* Shows/hides the toolbox.
*
* @param {boolean} visible - True to show the toolbox or false to hide it.
* @returns {Function}
*/
export function setToolboxVisible(visible: boolean) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const { toolbarConfig } = getState()['features/base/config'];
const alwaysVisible = toolbarConfig?.alwaysVisible;
if (!visible && alwaysVisible) {
return;
}
dispatch({
type: SET_TOOLBOX_VISIBLE,
visible
});
};
}
/**
* Action to toggle the toolbox visibility.
*
* @returns {Function}
*/
export function toggleToolboxVisible() {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const state = getState();
const { toolbarConfig } = getState()['features/base/config'];
const alwaysVisible = toolbarConfig?.alwaysVisible;
const { visible } = state['features/toolbox'];
if (visible && alwaysVisible) {
return;
}
dispatch({
type: TOGGLE_TOOLBOX_VISIBLE
});
};
}
/**
* Action to handle toggle video from toolbox's video buttons.
*
* @param {boolean} muted - Whether to mute or unmute.
* @param {boolean} showUI - When set to false will not display any error.
* @param {boolean} ensureTrack - True if we want to ensure that a new track is
* created if missing.
* @returns {Function}
*/
export function handleToggleVideoMuted(muted: boolean, showUI: boolean, ensureTrack: boolean) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const state = getState();
const { enabled: audioOnly } = state['features/base/audio-only'];
sendAnalytics(createToolbarEvent(VIDEO_MUTE, { enable: muted }));
if (audioOnly) {
dispatch(setAudioOnly(false));
}
dispatch(
setVideoMuted(
muted,
VIDEO_MUTISM_AUTHORITY.USER,
ensureTrack));
// FIXME: The old conference logic still relies on this event being
// emitted.
typeof APP === 'undefined'
|| APP.conference.muteVideo(muted, showUI);
};
}
/**
* Sets whether the toolbox should be shifted up or not.
*
* @param {boolean} shiftUp - Whether the toolbox should shift up or not.
* @returns {Object}
*/
export function setShiftUp(shiftUp: boolean) {
return {
type: SET_TOOLBOX_SHIFT_UP,
shiftUp
};
}
/**
* Sets the mainToolbarButtonsThresholds.
*
* @param {IMainToolbarButtonThresholds} thresholds - Thresholds for screen size and visible main toolbar buttons.
* @returns {Function}
*/
export function setMainToolbarThresholds(thresholds: IMainToolbarButtonThresholdsUnfiltered) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const { mainToolbarButtons } = getState()['features/base/config'];
if (!Array.isArray(mainToolbarButtons) || mainToolbarButtons.length === 0) {
return;
}
const mainToolbarButtonsThresholds: IMainToolbarButtonThresholds = [];
const mainToolbarButtonsLengthMap = new Map();
let orderIsChanged = false;
mainToolbarButtons.forEach(buttons => {
if (!Array.isArray(buttons) || buttons.length === 0) {
return;
}
mainToolbarButtonsLengthMap.set(buttons.length, buttons);
});
thresholds.forEach(({ width, order }) => {
let numberOfButtons = 0;
if (Array.isArray(order)) {
numberOfButtons = order.length;
} else if (order === DUMMY_9_BUTTONS_THRESHOLD_VALUE) {
numberOfButtons = 9;
} else if (order === DUMMY_10_BUTTONS_THRESHOLD_VALUE) {
numberOfButtons = 10;
} else { // Unexpected value. Ignore it.
return;
}
let finalOrder = mainToolbarButtonsLengthMap.get(numberOfButtons);
if (finalOrder) {
orderIsChanged = true;
} else if (Array.isArray(order)) {
finalOrder = order;
} else {
// Ignore dummy (symbol) values.
return;
}
mainToolbarButtonsThresholds.push({
order: finalOrder,
width
});
});
if (orderIsChanged) {
dispatch({
type: SET_MAIN_TOOLBAR_BUTTONS_THRESHOLDS,
mainToolbarButtonsThresholds
});
}
};
}

View File

@@ -0,0 +1,45 @@
import { CUSTOM_BUTTON_PRESSED } from './actionTypes';
export * from './actions.any';
/**
* Shows the toolbox for specified timeout.
*
* @param {number} _timeout - Timeout for showing the toolbox.
* @returns {Function}
*/
export function showToolbox(_timeout?: number): any {
return {};
}
/**
* Shows/hides the overflow menu.
*
* @param {boolean} _visible - True to show it or false to hide it.
* @returns {{
* type: SET_OVERFLOW_MENU_VISIBLE,
* visible: boolean
* }}
*/
export function setOverflowMenuVisible(_visible: boolean): any {
return {};
}
/**
* Creates a (redux) action which that a custom button was pressed.
*
* @param {string} id - The id for the custom button.
* @param {string} text - The label for the custom button.
* @returns {{
* type: CUSTOM_BUTTON_PRESSED,
* id: string,
* text: string
* }}
*/
export function customButtonPressed(id: string, text: string | undefined) {
return {
type: CUSTOM_BUTTON_PRESSED,
id,
text
};
}

View File

@@ -0,0 +1,280 @@
import { IStore } from '../app/types';
import { overwriteConfig } from '../base/config/actions';
import { isMobileBrowser } from '../base/environment/utils';
import { isLayoutTileView } from '../video-layout/functions.any';
import {
CLEAR_TOOLBOX_TIMEOUT,
FULL_SCREEN_CHANGED,
SET_FULL_SCREEN,
SET_HANGUP_MENU_VISIBLE,
SET_OVERFLOW_DRAWER,
SET_OVERFLOW_MENU_VISIBLE,
SET_TOOLBAR_HOVERED,
SET_TOOLBOX_TIMEOUT
} from './actionTypes';
import { setToolboxVisible } from './actions.web';
import { getToolbarTimeout } from './functions.web';
export * from './actions.any';
/**
* Docks/undocks the Toolbox.
*
* @param {boolean} dock - True if dock, false otherwise.
* @returns {Function}
*/
export function dockToolbox(dock: boolean) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const state = getState();
const { visible } = state['features/toolbox'];
const toolbarTimeout = getToolbarTimeout(state);
if (dock) {
// First make sure the toolbox is shown.
visible || dispatch(showToolbox());
dispatch(clearToolboxTimeout());
} else if (visible) {
dispatch(
setToolboxTimeout(
() => dispatch(hideToolbox()),
toolbarTimeout));
} else {
dispatch(showToolbox());
}
};
}
/**
* Signals that full screen mode has been entered or exited.
*
* @param {boolean} fullScreen - Whether or not full screen mode is currently
* enabled.
* @returns {{
* type: FULL_SCREEN_CHANGED,
* fullScreen: boolean
* }}
*/
export function fullScreenChanged(fullScreen: boolean) {
return {
type: FULL_SCREEN_CHANGED,
fullScreen
};
}
/**
* Hides the toolbox.
*
* @param {boolean} force - True to force the hiding of the toolbox without
* caring about the extended toolbar side panels.
* @returns {Function}
*/
export function hideToolbox(force = false) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const state = getState();
const { toolbarConfig } = state['features/base/config'];
const alwaysVisible = toolbarConfig?.alwaysVisible;
const autoHideWhileChatIsOpen = toolbarConfig?.autoHideWhileChatIsOpen;
const { hovered } = state['features/toolbox'];
const toolbarTimeout = getToolbarTimeout(state);
if (alwaysVisible) {
return;
}
dispatch(clearToolboxTimeout());
const hoverSelector = isLayoutTileView(state)
? '.remotevideomenu:hover'
: '.filmstrip:hover,.remotevideomenu:hover';
const hoveredElem = document.querySelector(hoverSelector);
if (!force
&& (hovered
|| state['features/invite'].calleeInfoVisible
|| (state['features/chat'].isOpen && !autoHideWhileChatIsOpen)
|| hoveredElem)) {
dispatch(
setToolboxTimeout(
() => dispatch(hideToolbox()),
toolbarTimeout));
} else {
dispatch(setToolboxVisible(false));
}
};
}
/**
* Signals a request to enter or exit full screen mode.
*
* @param {boolean} fullScreen - True to enter full screen mode, false to exit.
* @returns {{
* type: SET_FULL_SCREEN,
* fullScreen: boolean
* }}
*/
export function setFullScreen(fullScreen: boolean) {
return {
type: SET_FULL_SCREEN,
fullScreen
};
}
/**
* Shows the toolbox for specified timeout.
*
* @param {number} timeout - Timeout for showing the toolbox.
* @returns {Function}
*/
export function showToolbox(timeout = 0) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const state = getState();
const { toolbarConfig } = state['features/base/config'];
const toolbarTimeout = getToolbarTimeout(state);
const initialTimeout = toolbarConfig?.initialTimeout;
const alwaysVisible = toolbarConfig?.alwaysVisible;
const {
enabled,
visible
} = state['features/toolbox'];
if (enabled && !visible) {
dispatch(setToolboxVisible(true));
// If the Toolbox is always visible, there's no need for a timeout
// to toggle its visibility.
if (!alwaysVisible) {
if (typeof initialTimeout === 'number') {
// reset `initialTimeout` once it is consumed once
dispatch(overwriteConfig({ toolbarConfig: {
...toolbarConfig,
initialTimeout: null
} }));
}
dispatch(
setToolboxTimeout(
() => dispatch(hideToolbox()),
timeout || initialTimeout || toolbarTimeout));
}
}
};
}
/**
* Signals a request to display overflow as drawer.
*
* @param {boolean} displayAsDrawer - True to display overflow as drawer, false to preserve original behaviour.
* @returns {{
* type: SET_OVERFLOW_DRAWER,
* displayAsDrawer: boolean
* }}
*/
export function setOverflowDrawer(displayAsDrawer: boolean) {
return {
type: SET_OVERFLOW_DRAWER,
displayAsDrawer
};
}
/**
* Signals that toolbox timeout should be cleared.
*
* @returns {{
* type: CLEAR_TOOLBOX_TIMEOUT
* }}
*/
export function clearToolboxTimeout() {
return {
type: CLEAR_TOOLBOX_TIMEOUT
};
}
/**
* Shows/hides the hangup menu.
*
* @param {boolean} visible - True to show it or false to hide it.
* @returns {{
* type: SET_HANGUP_MENU_VISIBLE,
* visible: boolean
* }}
*/
export function setHangupMenuVisible(visible: boolean) {
return {
type: SET_HANGUP_MENU_VISIBLE,
visible
};
}
/**
* Shows/hides the overflow menu.
*
* @param {boolean} visible - True to show it or false to hide it.
* @returns {{
* type: SET_OVERFLOW_MENU_VISIBLE,
* visible: boolean
* }}
*/
export function setOverflowMenuVisible(visible: boolean) {
return {
type: SET_OVERFLOW_MENU_VISIBLE,
visible
};
}
/**
* Signals that toolbar is hovered value should be changed.
*
* @param {boolean} hovered - Flag showing whether toolbar is hovered.
* @returns {{
* type: SET_TOOLBAR_HOVERED,
* hovered: boolean
* }}
*/
export function setToolbarHovered(hovered: boolean) {
return {
type: SET_TOOLBAR_HOVERED,
hovered
};
}
/**
* Dispatches an action which sets new timeout for the toolbox visibility and clears the previous one.
* On mobile browsers the toolbox does not hide on timeout. It is toggled on simple tap.
*
* @param {Function} handler - Function to be invoked after the timeout.
* @param {number} timeoutMS - Delay.
* @returns {{
* type: SET_TOOLBOX_TIMEOUT,
* handler: Function,
* timeoutMS: number
* }}
*/
export function setToolboxTimeout(handler: Function, timeoutMS: number) {
return function(dispatch: IStore['dispatch']) {
if (isMobileBrowser()) {
return;
}
dispatch({
type: SET_TOOLBOX_TIMEOUT,
handler,
timeoutMS
});
};
}
/**
* Closes the overflow menu if opened.
*
* @private
* @returns {void}
*/
export function closeOverflowMenuIfOpen() {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const { overflowMenuVisible } = getState()['features/toolbox'];
overflowMenuVisible && dispatch(setOverflowMenuVisible(false));
};
}

View File

@@ -0,0 +1,95 @@
import { IReduxState } from '../../app/types';
import { AUDIO_MUTE_BUTTON_ENABLED } from '../../base/flags/constants';
import { getFeatureFlag } from '../../base/flags/functions';
import { MEDIA_TYPE } from '../../base/media/constants';
import { IProps as AbstractButtonProps } from '../../base/toolbox/components/AbstractButton';
import BaseAudioMuteButton from '../../base/toolbox/components/BaseAudioMuteButton';
import { isLocalTrackMuted } from '../../base/tracks/functions';
import { muteLocal } from '../../video-menu/actions';
import { isAudioMuteButtonDisabled } from '../functions';
/**
* The type of the React {@code Component} props of {@link AbstractAudioMuteButton}.
*/
export interface IProps extends AbstractButtonProps {
/**
* Whether audio is currently muted or not.
*/
_audioMuted: boolean;
/**
* Whether the button is disabled.
*/
_disabled: boolean;
}
/**
* Component that renders a toolbar button for toggling audio mute.
*
* @augments BaseAudioMuteButton
*/
export default class AbstractAudioMuteButton<P extends IProps> extends BaseAudioMuteButton<P> {
override accessibilityLabel = 'toolbar.accessibilityLabel.mute';
override toggledAccessibilityLabel = 'toolbar.accessibilityLabel.unmute';
override label = 'toolbar.mute';
override toggledLabel = 'toolbar.unmute';
override tooltip = 'toolbar.mute';
override toggledTooltip = 'toolbar.unmute';
/**
* Indicates if audio is currently muted or not.
*
* @override
* @protected
* @returns {boolean}
*/
override _isAudioMuted() {
return this.props._audioMuted;
}
/**
* Changes the muted state.
*
* @param {boolean} audioMuted - Whether audio should be muted or not.
* @protected
* @returns {void}
*/
override _setAudioMuted(audioMuted: boolean) {
this.props.dispatch(muteLocal(audioMuted, MEDIA_TYPE.AUDIO));
}
/**
* Return a boolean value indicating if this button is disabled or not.
*
* @returns {boolean}
*/
override _isDisabled() {
return this.props._disabled;
}
}
/**
* Maps (parts of) the redux state to the associated props for the
* {@code AbstractAudioMuteButton} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _audioMuted: boolean,
* _disabled: boolean
* }}
*/
export function mapStateToProps(state: IReduxState) {
const _audioMuted = isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.AUDIO);
const _disabled = isAudioMuteButtonDisabled(state);
const enabledFlag = getFeatureFlag(state, AUDIO_MUTE_BUTTON_ENABLED, true);
return {
_audioMuted,
_disabled,
visible: enabledFlag
};
}

View File

@@ -0,0 +1,94 @@
import { IReduxState } from '../../app/types';
import { VIDEO_MUTE_BUTTON_ENABLED } from '../../base/flags/constants';
import { getFeatureFlag } from '../../base/flags/functions';
import { MEDIA_TYPE } from '../../base/media/constants';
import { IProps as AbstractButtonProps } from '../../base/toolbox/components/AbstractButton';
import BaseVideoMuteButton from '../../base/toolbox/components/BaseVideoMuteButton';
import { isLocalTrackMuted } from '../../base/tracks/functions';
import { handleToggleVideoMuted } from '../actions.any';
import { isVideoMuteButtonDisabled } from '../functions';
/**
* The type of the React {@code Component} props of {@link AbstractVideoMuteButton}.
*/
export interface IProps extends AbstractButtonProps {
/**
* Whether video button is disabled or not.
*/
_videoDisabled: boolean;
/**
* Whether video is currently muted or not.
*/
_videoMuted: boolean;
}
/**
* Component that renders a toolbar button for toggling video mute.
*
* @augments BaseVideoMuteButton
*/
export default class AbstractVideoMuteButton<P extends IProps> extends BaseVideoMuteButton<P> {
override accessibilityLabel = 'toolbar.accessibilityLabel.videomute';
override toggledAccessibilityLabel = 'toolbar.accessibilityLabel.videounmute';
override label = 'toolbar.videomute';
override toggledLabel = 'toolbar.videounmute';
override tooltip = 'toolbar.videomute';
override toggledTooltip = 'toolbar.videounmute';
/**
* Indicates if video is currently disabled or not.
*
* @override
* @protected
* @returns {boolean}
*/
override _isDisabled() {
return this.props._videoDisabled;
}
/**
* Indicates if video is currently muted or not.
*
* @override
* @protected
* @returns {boolean}
*/
override _isVideoMuted() {
return this.props._videoMuted;
}
/**
* Changes the muted state.
*
* @override
* @param {boolean} videoMuted - Whether video should be muted or not.
* @protected
* @returns {void}
*/
override _setVideoMuted(videoMuted: boolean) {
this.props.dispatch(handleToggleVideoMuted(videoMuted, true, true));
}
}
/**
* Maps (parts of) the redux state to the associated props for the
* {@code VideoMuteButton} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _videoMuted: boolean
* }}
*/
export function mapStateToProps(state: IReduxState) {
const tracks = state['features/base/tracks'];
const enabledFlag = getFeatureFlag(state, VIDEO_MUTE_BUTTON_ENABLED, true);
return {
_videoDisabled: isVideoMuteButtonDisabled(state),
_videoMuted: isLocalTrackMuted(tracks, MEDIA_TYPE.VIDEO),
visible: enabledFlag
};
}

View File

@@ -0,0 +1,59 @@
import { connect } from 'react-redux';
import { createToolbarEvent } from '../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../analytics/functions';
import { IReduxState } from '../../app/types';
import { translate } from '../../base/i18n/functions';
import { IconDownload } from '../../base/icons/svg';
import AbstractButton, { IProps as AbstractButtonProps } from '../../base/toolbox/components/AbstractButton';
import { openURLInBrowser } from '../../base/util/openURLInBrowser';
interface IProps extends AbstractButtonProps {
/**
* The URL to the applications page.
*/
_downloadAppsUrl: string;
}
/**
* Implements an {@link AbstractButton} to open the applications page in a new window.
*/
class DownloadButton extends AbstractButton<IProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.download';
override icon = IconDownload;
override label = 'toolbar.download';
override tooltip = 'toolbar.download';
/**
* Handles clicking / pressing the button, and opens a new window with the user documentation.
*
* @private
* @returns {void}
*/
override _handleClick() {
const { _downloadAppsUrl } = this.props;
sendAnalytics(createToolbarEvent('download.pressed'));
openURLInBrowser(_downloadAppsUrl);
}
}
/**
* Maps part of the redux state to the component's props.
*
* @param {Object} state - The redux store/state.
* @returns {Object}
*/
function _mapStateToProps(state: IReduxState) {
const { downloadAppsUrl } = state['features/base/config'].deploymentUrls || {};
const visible = typeof downloadAppsUrl === 'string';
return {
_downloadAppsUrl: downloadAppsUrl ?? '',
visible
};
}
export default translate(connect(_mapStateToProps)(DownloadButton));

View File

@@ -0,0 +1,50 @@
import { once } from 'lodash-es';
import { connect } from 'react-redux';
import { createToolbarEvent } from '../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../analytics/functions';
import { leaveConference } from '../../base/conference/actions';
import { translate } from '../../base/i18n/functions';
import { IProps as AbstractButtonProps } from '../../base/toolbox/components/AbstractButton';
import AbstractHangupButton from '../../base/toolbox/components/AbstractHangupButton';
/**
* Component that renders a toolbar button for leaving the current conference.
*
* @augments AbstractHangupButton
*/
class HangupButton extends AbstractHangupButton<AbstractButtonProps> {
_hangup: Function;
override accessibilityLabel = 'toolbar.accessibilityLabel.hangup';
override label = 'toolbar.hangup';
override tooltip = 'toolbar.hangup';
/**
* Initializes a new HangupButton instance.
*
* @param {Props} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: AbstractButtonProps) {
super(props);
this._hangup = once(() => {
sendAnalytics(createToolbarEvent('hangup'));
this.props.dispatch(leaveConference());
});
}
/**
* Helper function to perform the actual hangup action.
*
* @override
* @protected
* @returns {void}
*/
override _doHangup() {
this._hangup();
}
}
export default translate(connect()(HangupButton));

View File

@@ -0,0 +1,62 @@
import { connect } from 'react-redux';
import { createToolbarEvent } from '../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../analytics/functions';
import { IReduxState } from '../../app/types';
import { HELP_BUTTON_ENABLED } from '../../base/flags/constants';
import { getFeatureFlag } from '../../base/flags/functions';
import { translate } from '../../base/i18n/functions';
import { IconHelp } from '../../base/icons/svg';
import AbstractButton, { IProps as AbstractButtonProps } from '../../base/toolbox/components/AbstractButton';
import { openURLInBrowser } from '../../base/util/openURLInBrowser';
interface IProps extends AbstractButtonProps {
/**
* The URL to the user documentation.
*/
_userDocumentationURL: string;
}
/**
* Implements an {@link AbstractButton} to open the user documentation in a new window.
*/
class HelpButton extends AbstractButton<IProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.help';
override icon = IconHelp;
override label = 'toolbar.help';
override tooltip = 'toolbar.help';
/**
* Handles clicking / pressing the button, and opens a new window with the user documentation.
*
* @private
* @returns {void}
*/
override _handleClick() {
const { _userDocumentationURL } = this.props;
sendAnalytics(createToolbarEvent('help.pressed'));
openURLInBrowser(_userDocumentationURL);
}
}
/**
* Maps part of the redux state to the component's props.
*
* @param {Object} state - The redux store/state.
* @returns {Object}
*/
function _mapStateToProps(state: IReduxState) {
const { userDocumentationURL } = state['features/base/config'].deploymentUrls || {};
const enabled = getFeatureFlag(state, HELP_BUTTON_ENABLED, true);
const visible = typeof userDocumentationURL === 'string' && enabled;
return {
_userDocumentationURL: userDocumentationURL ?? '',
visible
};
}
export default translate(connect(_mapStateToProps)(HelpButton));

View File

@@ -0,0 +1 @@
export { default as CustomOptionButton } from './native/CustomOptionButton';

View File

@@ -0,0 +1 @@
export { default as CustomOptionButton } from './web/CustomOptionButton';

View File

@@ -0,0 +1,6 @@
import { connect } from 'react-redux';
import { translate } from '../../../base/i18n/functions';
import AbstractAudioMuteButton, { IProps, mapStateToProps } from '../AbstractAudioMuteButton';
export default translate(connect(mapStateToProps)(AbstractAudioMuteButton<IProps>));

View File

@@ -0,0 +1,96 @@
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { setAudioOnly, toggleAudioOnly } from '../../../base/audio-only/actions';
import { AUDIO_ONLY_BUTTON_ENABLED } from '../../../base/flags/constants';
import { getFeatureFlag } from '../../../base/flags/functions';
import { translate } from '../../../base/i18n/functions';
import { IconAudioOnly, IconAudioOnlyOff } from '../../../base/icons/svg';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import {
navigate
} from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
import { screen } from '../../../mobile/navigation/routes';
/**
* The type of the React {@code Component} props of {@link AudioOnlyButton}.
*/
interface IProps extends AbstractButtonProps {
/**
* Whether the current conference is in audio only mode or not.
*/
_audioOnly: boolean;
/**
* Indicates whether the car mode is enabled.
*/
_startCarMode?: boolean;
}
/**
* An implementation of a button for toggling the audio-only mode.
*/
class AudioOnlyButton extends AbstractButton<IProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.audioOnly';
override icon = IconAudioOnly;
override label = 'toolbar.audioOnlyOn';
override toggledIcon = IconAudioOnlyOff;
override toggledLabel = 'toolbar.audioOnlyOff';
/**
* Handles clicking / pressing the button.
*
* @override
* @protected
* @returns {void}
*/
override _handleClick() {
const { _audioOnly, _startCarMode, dispatch } = this.props;
if (!_audioOnly && _startCarMode) {
dispatch(setAudioOnly(true));
navigate(screen.conference.carmode);
} else {
dispatch(toggleAudioOnly());
}
}
/**
* Indicates whether this button is in toggled state or not.
*
* @override
* @protected
* @returns {boolean}
*/
override _isToggled() {
return this.props._audioOnly;
}
}
/**
* Maps (parts of) the redux state to the associated props for the
* {@code AudioOnlyButton} component.
*
* @param {Object} state - The Redux state.
* @param {Object} ownProps - The properties explicitly passed to the component instance.
* @private
* @returns {{
* _audioOnly: boolean
* }}
*/
function _mapStateToProps(state: IReduxState, ownProps: any) {
const { enabled: audioOnly } = state['features/base/audio-only'];
const enabledInFeatureFlags = getFeatureFlag(state, AUDIO_ONLY_BUTTON_ENABLED, true);
const { startCarMode } = state['features/base/settings'];
const { visible = enabledInFeatureFlags } = ownProps;
return {
_audioOnly: Boolean(audioOnly),
_startCarMode: startCarMode,
visible
};
}
export default translate(connect(_mapStateToProps)(AudioOnlyButton));

View File

@@ -0,0 +1,75 @@
import React from 'react';
import { Image, View, ViewStyle } from 'react-native';
import { SvgCssUri } from 'react-native-svg/css';
import { connect } from 'react-redux';
import { translate } from '../../../base/i18n/functions';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
import styles from './styles';
export interface ICustomOptionButton extends AbstractButtonProps {
backgroundColor?: string;
icon: any;
id?: string;
isToolboxButton?: boolean;
text: string;
}
/**
* Component that renders a custom button.
*
* @returns {Component}
*/
class CustomOptionButton extends AbstractButton<ICustomOptionButton> {
backgroundColor = this.props.backgroundColor;
iconSrc = this.props.icon;
id = this.props.id;
text = this.props.text;
/**
* Custom icon component.
*
* @returns {React.Component}
*/
icon = () => {
let iconComponent;
if (!this.iconSrc) {
return null;
}
if (this.iconSrc?.includes('svg')) {
iconComponent = (
<SvgCssUri
// @ts-ignore
height = { BaseTheme.spacing[4] }
uri = { this.iconSrc }
width = { BaseTheme.spacing[4] } />
);
} else {
iconComponent = (
<Image
height = { BaseTheme.spacing[4] }
resizeMode = { 'contain' }
source = {{ uri: this.iconSrc }}
width = { BaseTheme.spacing[4] } />
);
}
return (
<View
style = { this.props.isToolboxButton && [
styles.toolboxButtonIconContainer,
{ backgroundColor: this.backgroundColor } ] as ViewStyle[] }>
{ iconComponent }
</View>
);
};
label = this.text || '';
}
export default translate(connect()(CustomOptionButton));

View File

@@ -0,0 +1,21 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import HangupButton from '../HangupButton';
import HangupMenuButton from './HangupMenuButton';
const HangupContainerButtons = (props: AbstractButtonProps) => {
const { conference } = useSelector((state: IReduxState) => state['features/base/conference']);
const endConferenceSupported = conference?.isEndConferenceSupported();
return endConferenceSupported
// @ts-ignore
? <HangupMenuButton { ...props } />
: <HangupButton { ...props } />;
};
export default HangupContainerButtons;

View File

@@ -0,0 +1,77 @@
import React, { useCallback } from 'react';
import { View } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import { createBreakoutRoomsEvent, createToolbarEvent } from '../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../analytics/functions';
import { appNavigate } from '../../../app/actions';
import { IReduxState } from '../../../app/types';
import ColorSchemeRegistry from '../../../base/color-scheme/ColorSchemeRegistry';
import { endConference } from '../../../base/conference/actions';
import { hideSheet } from '../../../base/dialog/actions';
import BottomSheet from '../../../base/dialog/components/native/BottomSheet';
import { PARTICIPANT_ROLE } from '../../../base/participants/constants';
import { getLocalParticipant } from '../../../base/participants/functions';
import Button from '../../../base/ui/components/native/Button';
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
import { moveToRoom } from '../../../breakout-rooms/actions';
import { isInBreakoutRoom } from '../../../breakout-rooms/functions';
/**
* Menu presenting options to leave a room or meeting and to end meeting.
*
* @returns {JSX.Element} - The hangup menu.
*/
function HangupMenu() {
const dispatch = useDispatch();
const _styles: any = useSelector((state: IReduxState) => ColorSchemeRegistry.get(state, 'Toolbox'));
const inBreakoutRoom = useSelector(isInBreakoutRoom);
const isModerator = useSelector((state: IReduxState) =>
getLocalParticipant(state)?.role === PARTICIPANT_ROLE.MODERATOR);
const { DESTRUCTIVE, SECONDARY } = BUTTON_TYPES;
const handleEndConference = useCallback(() => {
dispatch(hideSheet());
sendAnalytics(createToolbarEvent('endmeeting'));
dispatch(endConference());
}, [ hideSheet ]);
const handleLeaveConference = useCallback(() => {
dispatch(hideSheet());
sendAnalytics(createToolbarEvent('hangup'));
dispatch(appNavigate(undefined));
}, [ hideSheet ]);
const handleLeaveBreakoutRoom = useCallback(() => {
dispatch(hideSheet());
sendAnalytics(createBreakoutRoomsEvent('leave'));
dispatch(moveToRoom());
}, [ hideSheet ]);
return (
<BottomSheet>
<View style = { _styles.hangupMenuContainer }>
{ isModerator && <Button
accessibilityLabel = 'toolbar.endConference'
labelKey = 'toolbar.endConference'
onClick = { handleEndConference }
style = { _styles.hangupButton }
type = { DESTRUCTIVE } /> }
<Button
accessibilityLabel = 'toolbar.leaveConference'
labelKey = 'toolbar.leaveConference'
onClick = { handleLeaveConference }
style = { _styles.hangupButton }
type = { SECONDARY } />
{ inBreakoutRoom && <Button
accessibilityLabel = 'breakoutRooms.actions.leaveBreakoutRoom'
labelKey = 'breakoutRooms.actions.leaveBreakoutRoom'
onClick = { handleLeaveBreakoutRoom }
style = { _styles.hangupButton }
type = { SECONDARY } /> }
</View>
</BottomSheet>
);
}
export default HangupMenu;

View File

@@ -0,0 +1,32 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { openSheet } from '../../../base/dialog/actions';
import { IconHangup } from '../../../base/icons/svg';
import IconButton from '../../../base/ui/components/native/IconButton';
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
import HangupMenu from './HangupMenu';
/**
* Button for showing the hangup menu.
*
* @returns {JSX.Element} - The hangup menu button.
*/
const HangupMenuButton = (): JSX.Element => {
const dispatch = useDispatch();
const onSelect = useCallback(() => {
dispatch(openSheet(HangupMenu));
}, [ dispatch ]);
return (
<IconButton
accessibilityLabel = 'toolbar.accessibilityLabel.hangup'
onPress = { onSelect }
src = { IconHangup }
type = { BUTTON_TYPES.PRIMARY } />
);
};
export default HangupMenuButton;

View File

@@ -0,0 +1,48 @@
import { connect } from 'react-redux';
import { createToolbarEvent } from '../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../analytics/functions';
import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import { IconCloudUpload } from '../../../base/icons/svg';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { navigate }
from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
import { screen } from '../../../mobile/navigation/routes';
import { isSalesforceEnabled } from '../../../salesforce/functions';
/**
* Implementation of a button for opening the Salesforce link dialog.
*/
class LinkToSalesforceButton extends AbstractButton<AbstractButtonProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.linkToSalesforce';
override icon = IconCloudUpload;
override label = 'toolbar.linkToSalesforce';
/**
* Handles clicking / pressing the button, and opens the Salesforce link dialog.
*
* @protected
* @returns {void}
*/
override _handleClick() {
sendAnalytics(createToolbarEvent('link.to.salesforce'));
return navigate(screen.conference.salesforce);
}
}
/**
* Function that maps parts of Redux state tree into component props.
*
* @param {Object} state - Redux state.
* @private
* @returns {Props}
*/
function mapStateToProps(state: IReduxState) {
return {
visible: isSalesforceEnabled(state)
};
}
export default translate(connect(mapStateToProps)(LinkToSalesforceButton));

View File

@@ -0,0 +1,49 @@
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { CAR_MODE_ENABLED } from '../../../base/flags/constants';
import { getFeatureFlag } from '../../../base/flags/functions';
import { translate } from '../../../base/i18n/functions';
import { IconCar } from '../../../base/icons/svg';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { navigate }
from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
import { screen } from '../../../mobile/navigation/routes';
/**
* Implements an {@link AbstractButton} to open the carmode.
*/
class OpenCarmodeButton extends AbstractButton<AbstractButtonProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.carmode';
override icon = IconCar;
override label = 'carmode.labels.buttonLabel';
/**
* Handles clicking / pressing the button, and opens the carmode mode.
*
* @private
* @returns {void}
*/
override _handleClick() {
return navigate(screen.conference.carmode);
}
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {IReduxState} state - The Redux state.
* @param {AbstractButtonProps} ownProps - The properties explicitly passed to the component instance.
* @private
* @returns {Object}
*/
function _mapStateToProps(state: IReduxState, ownProps: AbstractButtonProps): Object {
const enabled = getFeatureFlag(state, CAR_MODE_ENABLED, true);
const { visible = enabled } = ownProps;
return {
visible
};
}
export default translate(connect(_mapStateToProps)(OpenCarmodeButton));

View File

@@ -0,0 +1,316 @@
import React, { PureComponent } from 'react';
import { ViewStyle } from 'react-native';
import { Divider } from 'react-native-paper';
import { connect, useSelector } from 'react-redux';
import { IReduxState, IStore } from '../../../app/types';
import { hideSheet } from '../../../base/dialog/actions';
import BottomSheet from '../../../base/dialog/components/native/BottomSheet';
import { bottomSheetStyles } from '../../../base/dialog/components/native/styles';
import SettingsButton from '../../../base/settings/components/native/SettingsButton';
import BreakoutRoomsButton
from '../../../breakout-rooms/components/native/BreakoutRoomsButton';
import SharedDocumentButton from '../../../etherpad/components/SharedDocumentButton.native';
import ReactionMenu from '../../../reactions/components/native/ReactionMenu';
import { shouldDisplayReactionsButtons } from '../../../reactions/functions.any';
import LiveStreamButton from '../../../recording/components/LiveStream/native/LiveStreamButton';
import RecordButton from '../../../recording/components/Recording/native/RecordButton';
import SecurityDialogButton
from '../../../security/components/security-dialog/native/SecurityDialogButton';
import SharedVideoButton from '../../../shared-video/components/native/SharedVideoButton';
import { isSharedVideoEnabled } from '../../../shared-video/functions';
import SpeakerStatsButton from '../../../speaker-stats/components/native/SpeakerStatsButton';
import { isSpeakerStatsDisabled } from '../../../speaker-stats/functions';
import ClosedCaptionButton from '../../../subtitles/components/native/ClosedCaptionButton';
import styles from '../../../video-menu/components/native/styles';
import { iAmVisitor } from '../../../visitors/functions';
import WhiteboardButton from '../../../whiteboard/components/native/WhiteboardButton';
import { customButtonPressed } from '../../actions.native';
import { getVisibleNativeButtons } from '../../functions.native';
import { useNativeToolboxButtons } from '../../hooks.native';
import { IToolboxNativeButton } from '../../types';
import AudioOnlyButton from './AudioOnlyButton';
import LinkToSalesforceButton from './LinkToSalesforceButton';
import OpenCarmodeButton from './OpenCarmodeButton';
import RaiseHandButton from './RaiseHandButton';
/**
* The type of the React {@code Component} props of {@link OverflowMenu}.
*/
interface IProps {
/**
* True if breakout rooms feature is available, false otherwise.
*/
_isBreakoutRoomsSupported?: boolean;
/**
* True if the overflow menu is currently visible, false otherwise.
*/
_isOpen: boolean;
/**
* Whether the shared video is enabled or not.
*/
_isSharedVideoEnabled: boolean;
/**
* Whether or not speaker stats is disable.
*/
_isSpeakerStatsDisabled?: boolean;
/**
* Toolbar buttons.
*/
_mainMenuButtons?: Array<IToolboxNativeButton>;
/**
* Overflow menu buttons.
*/
_overflowMenuButtons?: Array<IToolboxNativeButton>;
/**
* Whether the recoding button should be enabled or not.
*/
_recordingEnabled: boolean;
/**
* Whether or not any reactions buttons should be displayed.
*/
_shouldDisplayReactionsButtons: boolean;
/**
* Used for hiding the dialog when the selection was completed.
*/
dispatch: IStore['dispatch'];
}
interface IState {
/**
* True if the bottom sheet is scrolled to the top.
*/
scrolledToTop: boolean;
}
/**
* Implements a React {@code Component} with some extra actions in addition to
* those in the toolbar.
*/
class OverflowMenu extends PureComponent<IProps, IState> {
/**
* Initializes a new {@code OverflowMenu} instance.
*
* @inheritdoc
*/
constructor(props: IProps) {
super(props);
this.state = {
scrolledToTop: true
};
// Bind event handlers so they are only bound once per instance.
this._onCancel = this._onCancel.bind(this);
this._renderReactionMenu = this._renderReactionMenu.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
override render() {
const {
_isBreakoutRoomsSupported,
_isSpeakerStatsDisabled,
_isSharedVideoEnabled,
dispatch
} = this.props;
const buttonProps = {
afterClick: this._onCancel,
showLabel: true,
styles: bottomSheetStyles.buttons
};
const topButtonProps = {
afterClick: this._onCancel,
dispatch,
showLabel: true,
styles: {
...bottomSheetStyles.buttons,
style: {
...bottomSheetStyles.buttons.style,
borderTopLeftRadius: 16,
borderTopRightRadius: 16
}
}
};
return (
<BottomSheet
renderFooter = { this._renderReactionMenu }>
<Divider style = { styles.divider as ViewStyle } />
<OpenCarmodeButton { ...topButtonProps } />
<AudioOnlyButton { ...buttonProps } />
{ this._renderRaiseHandButton(buttonProps) }
{/* @ts-ignore */}
<SecurityDialogButton { ...buttonProps } />
<RecordButton { ...buttonProps } />
<LiveStreamButton { ...buttonProps } />
<LinkToSalesforceButton { ...buttonProps } />
<WhiteboardButton { ...buttonProps } />
{/* @ts-ignore */}
<Divider style = { styles.divider as ViewStyle } />
{_isSharedVideoEnabled && <SharedVideoButton { ...buttonProps } />}
{ this._renderOverflowMenuButtons(topButtonProps) }
{!_isSpeakerStatsDisabled && <SpeakerStatsButton { ...buttonProps } />}
{_isBreakoutRoomsSupported && <BreakoutRoomsButton { ...buttonProps } />}
{/* @ts-ignore */}
<Divider style = { styles.divider as ViewStyle } />
<ClosedCaptionButton { ...buttonProps } />
<SharedDocumentButton { ...buttonProps } />
<SettingsButton { ...buttonProps } />
</BottomSheet>
);
}
/**
* Hides this {@code OverflowMenu}.
*
* @private
* @returns {void}
*/
_onCancel() {
this.props.dispatch(hideSheet());
}
/**
* Function to render the reaction menu as the footer of the bottom sheet.
*
* @returns {React.ReactElement}
*/
_renderReactionMenu() {
const { _mainMenuButtons, _shouldDisplayReactionsButtons } = this.props;
// @ts-ignore
const isRaiseHandInMainMenu = _mainMenuButtons?.some(item => item.key === 'raisehand');
if (_shouldDisplayReactionsButtons && !isRaiseHandInMainMenu) {
return (
<ReactionMenu
onCancel = { this._onCancel }
overflowMenu = { true } />
);
}
}
/**
* Function to render the reaction menu as the footer of the bottom sheet.
*
* @param {Object} buttonProps - Styling button properties.
* @returns {React.ReactElement}
*/
_renderRaiseHandButton(buttonProps: Object) {
const { _mainMenuButtons, _shouldDisplayReactionsButtons } = this.props;
// @ts-ignore
const isRaiseHandInMainMenu = _mainMenuButtons?.some(item => item.key === 'raisehand');
if (!_shouldDisplayReactionsButtons && !isRaiseHandInMainMenu) {
return (
<RaiseHandButton { ...buttonProps } />
);
}
}
/**
* Function to render the custom buttons for the overflow menu.
*
* @param {Object} topButtonProps - Styling button properties.
* @returns {React.ReactElement}
*/
_renderOverflowMenuButtons(topButtonProps: Object) {
const { _overflowMenuButtons, dispatch } = this.props;
if (!_overflowMenuButtons?.length) {
return;
}
return (
<>
{
_overflowMenuButtons?.map(({ Content, key, text, ...rest }: IToolboxNativeButton) => {
if (key === 'raisehand') {
return null;
}
return (
<Content
{ ...topButtonProps }
{ ...rest }
/* eslint-disable react/jsx-no-bind */
handleClick = { () => dispatch(customButtonPressed(key, text)) }
isToolboxButton = { false }
key = { key }
text = { text } />
);
})
}
<Divider style = { styles.divider as ViewStyle } />
</>
);
}
}
/**
* Function that maps parts of Redux state tree into component props.
*
* @param {Object} state - Redux state.
* @private
* @returns {IProps}
*/
function _mapStateToProps(state: IReduxState) {
const { conference } = state['features/base/conference'];
return {
_isBreakoutRoomsSupported: conference?.getBreakoutRooms()?.isSupported(),
_isSharedVideoEnabled: isSharedVideoEnabled(state),
_isSpeakerStatsDisabled: isSpeakerStatsDisabled(state),
_shouldDisplayReactionsButtons: shouldDisplayReactionsButtons(state)
};
}
export default connect(_mapStateToProps)(props => {
const { clientWidth } = useSelector((state: IReduxState) => state['features/base/responsive-ui']);
const { customToolbarButtons } = useSelector((state: IReduxState) => state['features/base/config']);
const {
mainToolbarButtonsThresholds,
toolbarButtons
} = useSelector((state: IReduxState) => state['features/toolbox']);
const _iAmVisitor = useSelector(iAmVisitor);
const allButtons = useNativeToolboxButtons(customToolbarButtons);
const { mainMenuButtons, overflowMenuButtons } = getVisibleNativeButtons({
allButtons,
clientWidth,
mainToolbarButtonsThresholds,
toolbarButtons,
iAmVisitor: _iAmVisitor
});
return (
<OverflowMenu
// @ts-ignore
{ ... props }
_mainMenuButtons = { mainMenuButtons }
_overflowMenuButtons = { overflowMenuButtons } />
);
});

View File

@@ -0,0 +1,50 @@
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { openSheet } from '../../../base/dialog/actions';
import { OVERFLOW_MENU_ENABLED } from '../../../base/flags/constants';
import { getFeatureFlag } from '../../../base/flags/functions';
import { translate } from '../../../base/i18n/functions';
import { IconDotsHorizontal } from '../../../base/icons/svg';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import OverflowMenu from './OverflowMenu';
/**
* An implementation of a button for showing the {@code OverflowMenu}.
*/
class OverflowMenuButton extends AbstractButton<AbstractButtonProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.moreActions';
override icon = IconDotsHorizontal;
override label = 'toolbar.moreActions';
/**
* Handles clicking / pressing this {@code OverflowMenuButton}.
*
* @protected
* @returns {void}
*/
override _handleClick() {
// @ts-ignore
this.props.dispatch(openSheet(OverflowMenu));
}
}
/**
* Maps (parts of) the redux state to the associated props for the
* {@code OverflowMenuButton} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {Props}
*/
function _mapStateToProps(state: IReduxState) {
const enabledFlag = getFeatureFlag(state, OVERFLOW_MENU_ENABLED, true);
return {
visible: enabledFlag
};
}
export default translate(connect(_mapStateToProps)(OverflowMenuButton));

View File

@@ -0,0 +1,99 @@
import { connect } from 'react-redux';
import { createToolbarEvent } from '../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../analytics/functions';
import { IReduxState } from '../../../app/types';
import { RAISE_HAND_ENABLED } from '../../../base/flags/constants';
import { getFeatureFlag } from '../../../base/flags/functions';
import { translate } from '../../../base/i18n/functions';
import { IconRaiseHand } from '../../../base/icons/svg';
import { raiseHand } from '../../../base/participants/actions';
import {
getLocalParticipant,
hasRaisedHand
} from '../../../base/participants/functions';
import { ILocalParticipant } from '../../../base/participants/types';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
/**
* The type of the React {@code Component} props of {@link RaiseHandButton}.
*/
interface IProps extends AbstractButtonProps {
/**
* The local participant.
*/
_localParticipant?: ILocalParticipant;
/**
* Whether the participant raised their hand or not.
*/
_raisedHand: boolean;
}
/**
* An implementation of a button to raise or lower hand.
*/
class RaiseHandButton extends AbstractButton<IProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.raiseHand';
override icon = IconRaiseHand;
override label = 'toolbar.raiseYourHand';
override toggledLabel = 'toolbar.lowerYourHand';
/**
* Handles clicking / pressing the button.
*
* @override
* @protected
* @returns {void}
*/
override _handleClick() {
this._toggleRaisedHand();
}
/**
* Indicates whether this button is in toggled state or not.
*
* @override
* @protected
* @returns {boolean}
*/
override _isToggled() {
return this.props._raisedHand;
}
/**
* Toggles the rased hand status of the local participant.
*
* @returns {void}
*/
_toggleRaisedHand() {
const enable = !this.props._raisedHand;
sendAnalytics(createToolbarEvent('raise.hand', { enable }));
this.props.dispatch(raiseHand(enable));
}
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @param {Object} ownProps - The properties explicitly passed to the component instance.
* @private
* @returns {IProps}
*/
function _mapStateToProps(state: IReduxState, ownProps: any) {
const _localParticipant = getLocalParticipant(state);
const enabled = getFeatureFlag(state, RAISE_HAND_ENABLED, true);
const { visible = enabled } = ownProps;
return {
_localParticipant,
_raisedHand: hasRaisedHand(_localParticipant),
visible
};
}
export default translate(connect(_mapStateToProps)(RaiseHandButton));

View File

@@ -0,0 +1,91 @@
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { ANDROID_SCREENSHARING_ENABLED } from '../../../base/flags/constants';
import { getFeatureFlag } from '../../../base/flags/functions';
import { translate } from '../../../base/i18n/functions';
import { IconScreenshare } from '../../../base/icons/svg';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { toggleScreensharing } from '../../../base/tracks/actions.native';
import { isLocalVideoTrackDesktop } from '../../../base/tracks/functions.native';
/**
* The type of the React {@code Component} props of {@link ScreenSharingAndroidButton}.
*/
interface IProps extends AbstractButtonProps {
/**
* True if the button needs to be disabled.
*/
_disabled: boolean;
/**
* Whether video is currently muted or not.
*/
_screensharing: boolean;
}
/**
* An implementation of a button for toggling screen sharing.
*/
class ScreenSharingAndroidButton extends AbstractButton<IProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.shareYourScreen';
override icon = IconScreenshare;
override label = 'toolbar.startScreenSharing';
override toggledLabel = 'toolbar.stopScreenSharing';
/**
* Handles clicking / pressing the button.
*
* @override
* @protected
* @returns {void}
*/
override _handleClick() {
const enable = !this._isToggled();
this.props.dispatch(toggleScreensharing(enable));
}
/**
* Returns a boolean value indicating if this button is disabled or not.
*
* @protected
* @returns {boolean}
*/
_isDisabled() {
return this.props._disabled;
}
/**
* Indicates whether this button is in toggled state or not.
*
* @override
* @protected
* @returns {boolean}
*/
override _isToggled() {
return this.props._screensharing;
}
}
/**
* Maps (parts of) the redux state to the associated props for the
* {@code ToggleCameraButton} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _screensharing: boolean
* }}
*/
function _mapStateToProps(state: IReduxState) {
const enabled = getFeatureFlag(state, ANDROID_SCREENSHARING_ENABLED, true);
return {
_screensharing: isLocalVideoTrackDesktop(state),
visible: enabled
};
}
export default translate(connect(_mapStateToProps)(ScreenSharingAndroidButton));

View File

@@ -0,0 +1,36 @@
import React from 'react';
import { Platform } from 'react-native';
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { isDesktopShareButtonDisabled } from '../../functions.native';
import ScreenSharingAndroidButton from './ScreenSharingAndroidButton';
import ScreenSharingIosButton from './ScreenSharingIosButton';
const ScreenSharingButton = (props: any) => (
<>
{Platform.OS === 'android'
&& <ScreenSharingAndroidButton { ...props } />
}
{Platform.OS === 'ios'
&& <ScreenSharingIosButton { ...props } />
}
</>
);
/**
* Maps (parts of) the redux state to the associated props for the
* {@code ScreenSharingButton} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {Object}
*/
function _mapStateToProps(state: IReduxState) {
return {
_disabled: isDesktopShareButtonDisabled(state)
};
}
export default connect(_mapStateToProps)(ScreenSharingButton);

View File

@@ -0,0 +1,148 @@
import React from 'react';
import { NativeModules, Platform, findNodeHandle } from 'react-native';
import { ScreenCapturePickerView } from 'react-native-webrtc';
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { IOS_SCREENSHARING_ENABLED } from '../../../base/flags/constants';
import { getFeatureFlag } from '../../../base/flags/functions';
import { translate } from '../../../base/i18n/functions';
import { IconScreenshare } from '../../../base/icons/svg';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { isLocalVideoTrackDesktop } from '../../../base/tracks/functions.native';
/**
* The type of the React {@code Component} props of {@link ScreenSharingIosButton}.
*/
interface IProps extends AbstractButtonProps {
/**
* True if the button needs to be disabled.
*/
_disabled: boolean;
/**
* Whether video is currently muted or not.
*/
_screensharing: boolean;
}
const styles = {
screenCapturePickerView: {
display: 'none'
}
};
/**
* An implementation of a button for toggling screen sharing on iOS.
*/
class ScreenSharingIosButton extends AbstractButton<IProps> {
_nativeComponent: React.Component<any, any> | null;
override accessibilityLabel = 'toolbar.accessibilityLabel.shareYourScreen';
override icon = IconScreenshare;
override label = 'toolbar.startScreenSharing';
override toggledLabel = 'toolbar.stopScreenSharing';
/**
* Initializes a new {@code ScreenSharingIosButton} instance.
*
* @param {Object} props - The React {@code Component} props to initialize
* the new {@code ScreenSharingIosButton} instance with.
*/
constructor(props: IProps) {
super(props);
this._nativeComponent = null;
// Bind event handlers so they are only bound once per instance.
this._setNativeComponent = this._setNativeComponent.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {React$Node}
*/
override render() {
return (
<>
{ super.render() }
<ScreenCapturePickerView
ref = { this._setNativeComponent } // @ts-ignore
style = { styles.screenCapturePickerView } />
</>
);
}
/**
* Sets the internal reference to the React Component wrapping the
* {@code RPSystemBroadcastPickerView} component.
*
* @param {ReactComponent} component - React Component.
* @returns {void}
*/
_setNativeComponent(component: React.Component<any, any> | null) {
this._nativeComponent = component;
}
/**
* Handles clicking / pressing the button.
*
* @override
* @protected
* @returns {void}
*/
override _handleClick() {
const handle = findNodeHandle(this._nativeComponent);
NativeModules.ScreenCapturePickerViewManager.show(handle);
}
/**
* Returns a boolean value indicating if this button is disabled or not.
*
* @protected
* @returns {boolean}
*/
_isDisabled() {
return this.props._disabled;
}
/**
* Indicates whether this button is in toggled state or not.
*
* @override
* @protected
* @returns {boolean}
*/
override _isToggled() {
return this.props._screensharing;
}
}
/**
* Maps (parts of) the redux state to the associated props for the
* {@code ScreenSharingIosButton} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _disabled: boolean,
* }}
*/
function _mapStateToProps(state: IReduxState) {
const enabled = getFeatureFlag(state, IOS_SCREENSHARING_ENABLED, false);
return {
_screensharing: isLocalVideoTrackDesktop(state),
// TODO: this should work on iOS 12 too, but our trick to show the picker doesn't work.
visible: enabled
&& Platform.OS === 'ios'
&& Number.parseInt(Platform.Version.split('.')[0], 10) >= 14
};
}
export default translate(connect(_mapStateToProps)(ScreenSharingIosButton));

View File

@@ -0,0 +1,79 @@
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import { IconCameraRefresh } from '../../../base/icons/svg';
import { toggleCameraFacingMode } from '../../../base/media/actions';
import { MEDIA_TYPE } from '../../../base/media/constants';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { isLocalTrackMuted } from '../../../base/tracks/functions.native';
/**
* The type of the React {@code Component} props of {@link ToggleCameraButton}.
*/
interface IProps extends AbstractButtonProps {
/**
* Whether the current conference is in audio only mode or not.
*/
_audioOnly: boolean;
/**
* Whether video is currently muted or not.
*/
_videoMuted: boolean;
}
/**
* An implementation of a button for toggling the camera facing mode.
*/
class ToggleCameraButton extends AbstractButton<IProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.toggleCamera';
override icon = IconCameraRefresh;
override label = 'toolbar.toggleCamera';
/**
* Handles clicking / pressing the button.
*
* @override
* @protected
* @returns {void}
*/
override _handleClick() {
this.props.dispatch(toggleCameraFacingMode());
}
/**
* Indicates whether this button is disabled or not.
*
* @override
* @protected
* @returns {boolean}
*/
_isDisabled() {
return this.props._audioOnly || this.props._videoMuted;
}
}
/**
* Maps (parts of) the redux state to the associated props for the
* {@code ToggleCameraButton} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _audioOnly: boolean,
* _videoMuted: boolean
* }}
*/
function _mapStateToProps(state: IReduxState) {
const { enabled: audioOnly } = state['features/base/audio-only'];
const tracks = state['features/base/tracks'];
return {
_audioOnly: Boolean(audioOnly),
_videoMuted: isLocalTrackMuted(tracks, MEDIA_TYPE.VIDEO)
};
}
export default translate(connect(_mapStateToProps)(ToggleCameraButton));

View File

@@ -0,0 +1,74 @@
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import { IconAudioOnlyOff } from '../../../base/icons/svg';
import { updateSettings } from '../../../base/settings/actions';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
/**
* The type of the React {@code Component} props of {@link ToggleSelfViewButton}.
*/
interface IProps extends AbstractButtonProps {
/**
* Whether the self view is disabled or not.
*/
_disableSelfView: boolean;
}
/**
* An implementation of a button for toggling the self view.
*/
class ToggleSelfViewButton extends AbstractButton<IProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.selfView';
override icon = IconAudioOnlyOff;
override label = 'videothumbnail.hideSelfView';
override toggledLabel = 'videothumbnail.showSelfView';
/**
* Handles clicking / pressing the button.
*
* @override
* @protected
* @returns {void}
*/
override _handleClick() {
const { _disableSelfView, dispatch } = this.props;
dispatch(updateSettings({
disableSelfView: !_disableSelfView
}));
}
/**
* Indicates whether this button is in toggled state or not.
*
* @override
* @protected
* @returns {boolean}
*/
override _isToggled() {
return this.props._disableSelfView;
}
}
/**
* Maps (parts of) the redux state to the associated props for the
* {@code ToggleSelfViewButton} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _disableSelfView: boolean
* }}
*/
function _mapStateToProps(state: IReduxState) {
const { disableSelfView } = state['features/base/settings'];
return {
_disableSelfView: Boolean(disableSelfView)
};
}
export default translate(connect(_mapStateToProps)(ToggleSelfViewButton));

View File

@@ -0,0 +1,142 @@
import React from 'react';
import { View, ViewStyle } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { connect, useSelector } from 'react-redux';
import { IReduxState, IStore } from '../../../app/types';
import ColorSchemeRegistry from '../../../base/color-scheme/ColorSchemeRegistry';
import Platform from '../../../base/react/Platform.native';
import { iAmVisitor } from '../../../visitors/functions';
import { customButtonPressed } from '../../actions.native';
import { getVisibleNativeButtons, isToolboxVisible } from '../../functions.native';
import { useNativeToolboxButtons } from '../../hooks.native';
import { IToolboxNativeButton } from '../../types';
import styles from './styles';
/**
* The type of {@link Toolbox}'s React {@code Component} props.
*/
interface IProps {
/**
* Whether we are in visitors mode.
*/
_iAmVisitor: boolean;
/**
* The color-schemed stylesheet of the feature.
*/
_styles: any;
/**
* The indicator which determines whether the toolbox is visible.
*/
_visible: boolean;
/**
* Redux store dispatch method.
*/
dispatch: IStore['dispatch'];
}
/**
* Implements the conference Toolbox on React Native.
*
* @param {Object} props - The props of the component.
* @returns {React$Element}
*/
function Toolbox(props: IProps) {
const {
_iAmVisitor,
_styles,
_visible,
dispatch
} = props;
if (!_visible) {
return null;
}
const { clientWidth } = useSelector((state: IReduxState) => state['features/base/responsive-ui']);
const { customToolbarButtons } = useSelector((state: IReduxState) => state['features/base/config']);
const {
mainToolbarButtonsThresholds,
toolbarButtons
} = useSelector((state: IReduxState) => state['features/toolbox']);
const allButtons = useNativeToolboxButtons(customToolbarButtons);
const { mainMenuButtons } = getVisibleNativeButtons({
allButtons,
clientWidth,
iAmVisitor: _iAmVisitor,
mainToolbarButtonsThresholds,
toolbarButtons
});
const bottomEdge = Platform.OS === 'ios' && _visible;
const { buttonStylesBorderless, hangupButtonStyles } = _styles;
const style = { ...styles.toolbox };
// We have only hangup and raisehand button in _iAmVisitor mode
if (_iAmVisitor) {
style.justifyContent = 'center';
}
const renderToolboxButtons = () => {
if (!mainMenuButtons?.length) {
return;
}
return (
<>
{
mainMenuButtons?.map(({ Content, key, text, ...rest }: IToolboxNativeButton) => (
<Content
{ ...rest }
/* eslint-disable react/jsx-no-bind */
handleClick = { () => dispatch(customButtonPressed(key, text)) }
isToolboxButton = { true }
key = { key }
styles = { key === 'hangup' ? hangupButtonStyles : buttonStylesBorderless } />
))
}
</>
);
};
return (
<View
style = { styles.toolboxContainer as ViewStyle }>
<SafeAreaView
accessibilityRole = 'toolbar'
// @ts-ignore
edges = { [ bottomEdge && 'bottom' ].filter(Boolean) }
pointerEvents = 'box-none'
style = { style as ViewStyle }>
{ renderToolboxButtons() }
</SafeAreaView>
</View>
);
}
/**
* Maps parts of the redux state to {@link Toolbox} (React {@code Component})
* props.
*
* @param {Object} state - The redux state of which parts are to be mapped to
* {@code Toolbox} props.
* @private
* @returns {IProps}
*/
function _mapStateToProps(state: IReduxState) {
return {
_iAmVisitor: iAmVisitor(state),
_styles: ColorSchemeRegistry.get(state, 'Toolbox'),
_visible: isToolboxVisible(state),
};
}
export default connect(_mapStateToProps)(Toolbox);

View File

@@ -0,0 +1,7 @@
import { connect } from 'react-redux';
import { translate } from '../../../base/i18n/functions';
import AbstractVideoMuteButton, { IProps, mapStateToProps } from '../AbstractVideoMuteButton';
export default translate(connect(mapStateToProps)(AbstractVideoMuteButton<IProps>));

View File

@@ -0,0 +1,216 @@
import ColorSchemeRegistry from '../../../base/color-scheme/ColorSchemeRegistry';
import { schemeColor } from '../../../base/color-scheme/functions';
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
const BUTTON_SIZE = 48;
// Toolbox, toolbar:
/**
* The style of toolbar buttons.
*/
const toolbarButton = {
borderRadius: BaseTheme.shape.borderRadius,
borderWidth: 0,
flex: 0,
flexDirection: 'row',
height: BUTTON_SIZE,
justifyContent: 'center',
marginHorizontal: 6,
marginVertical: 6,
width: BUTTON_SIZE
};
/**
* The icon style of the toolbar buttons.
*/
const toolbarButtonIcon = {
alignSelf: 'center',
color: BaseTheme.palette.icon04,
fontSize: 24
};
/**
* The icon style of toolbar buttons which display white icons.
*/
const whiteToolbarButtonIcon = {
...toolbarButtonIcon,
color: BaseTheme.palette.icon01
};
/**
* The style of reaction buttons.
*/
const reactionButton = {
...toolbarButton,
backgroundColor: 'transparent',
alignItems: 'center',
marginTop: 0,
marginHorizontal: 0
};
const gifButton = {
...reactionButton,
backgroundColor: '#000'
};
/**
* The style of the emoji on the reaction buttons.
*/
const reactionEmoji = {
fontSize: 20,
color: BaseTheme.palette.icon01
};
const reactionMenu = {
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: BaseTheme.palette.ui01
};
/**
* The Toolbox and toolbar related styles.
*/
const styles = {
sheetGestureRecognizer: {
alignItems: 'stretch',
flexDirection: 'column'
},
/**
* The style of the toolbar.
*/
toolbox: {
alignItems: 'center',
backgroundColor: BaseTheme.palette.uiBackground,
borderTopLeftRadius: 3,
borderTopRightRadius: 3,
flexDirection: 'row',
justifyContent: 'space-between'
},
/**
* The style of the root/top-level container of {@link Toolbox}.
*/
toolboxContainer: {
backgroundColor: BaseTheme.palette.uiBackground,
flexDirection: 'column',
maxWidth: 580,
marginHorizontal: 'auto',
marginVertical: BaseTheme.spacing[0],
paddingHorizontal: BaseTheme.spacing[2],
width: '100%'
},
toolboxButtonIconContainer: {
alignItems: 'center',
borderRadius: BaseTheme.shape.borderRadius,
height: BaseTheme.spacing[7],
justifyContent: 'center',
width: BaseTheme.spacing[7]
}
};
export default styles;
/**
* Color schemed styles for the @{Toolbox} component.
*/
ColorSchemeRegistry.register('Toolbox', {
/**
* Styles for buttons in the toolbar.
*/
buttonStyles: {
iconStyle: toolbarButtonIcon,
style: toolbarButton
},
buttonStylesBorderless: {
iconStyle: whiteToolbarButtonIcon,
style: {
...toolbarButton,
backgroundColor: 'transparent'
},
underlayColor: 'transparent'
},
backgroundToggle: {
backgroundColor: BaseTheme.palette.ui04
},
hangupMenuContainer: {
marginHorizontal: BaseTheme.spacing[2],
marginVertical: BaseTheme.spacing[2]
},
hangupButton: {
flex: 1,
marginHorizontal: BaseTheme.spacing[2],
marginVertical: BaseTheme.spacing[2]
},
hangupButtonStyles: {
iconStyle: whiteToolbarButtonIcon,
style: {
...toolbarButton,
backgroundColor: schemeColor('hangup')
},
underlayColor: BaseTheme.palette.ui04
},
reactionDialog: {
position: 'absolute',
width: '100%',
height: '100%',
backgroundColor: 'transparent'
},
overflowReactionMenu: {
...reactionMenu,
padding: BaseTheme.spacing[3]
},
reactionMenu: {
...reactionMenu,
paddingHorizontal: BaseTheme.spacing[3],
borderRadius: 3,
width: 360
},
reactionRow: {
alignItems: 'center',
flexDirection: 'row',
justifyContent: 'space-between'
},
reactionButton: {
gifButton,
style: reactionButton,
underlayColor: BaseTheme.palette.ui04,
emoji: reactionEmoji
},
emojiAnimation: {
color: BaseTheme.palette.icon01,
position: 'absolute',
zIndex: 1001,
elevation: 2,
fontSize: 20,
left: '50%',
top: '100%'
},
/**
* Styles for toggled buttons in the toolbar.
*/
toggledButtonStyles: {
iconStyle: whiteToolbarButtonIcon,
style: {
...toolbarButton
},
underlayColor: 'transparent'
}
});

View File

@@ -0,0 +1,205 @@
import React, { ReactElement } from 'react';
import { connect } from 'react-redux';
import { withStyles } from 'tss-react/mui';
import { ACTION_SHORTCUT_TRIGGERED, AUDIO_MUTE, createShortcutEvent } from '../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../analytics/functions';
import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import { IGUMPendingState } from '../../../base/media/types';
import AbstractButton from '../../../base/toolbox/components/AbstractButton';
import Spinner from '../../../base/ui/components/web/Spinner';
import { registerShortcut, unregisterShortcut } from '../../../keyboard-shortcuts/actions';
import { SPINNER_COLOR } from '../../constants';
import AbstractAudioMuteButton, {
IProps as AbstractAudioMuteButtonProps,
mapStateToProps as abstractMapStateToProps
} from '../AbstractAudioMuteButton';
const styles = () => {
return {
pendingContainer: {
position: 'absolute' as const,
bottom: '3px',
right: '3px'
}
};
};
/**
* The type of the React {@code Component} props of {@link AudioMuteButton}.
*/
interface IProps extends AbstractAudioMuteButtonProps {
/**
* The gumPending state from redux.
*/
_gumPending: IGUMPendingState;
/**
* An object containing the CSS classes.
*/
classes?: Partial<Record<keyof ReturnType<typeof styles>, string>>;
}
/**
* Component that renders a toolbar button for toggling audio mute.
*
* @augments AbstractAudioMuteButton
*/
class AudioMuteButton extends AbstractAudioMuteButton<IProps> {
/**
* Initializes a new {@code AudioMuteButton} instance.
*
* @param {IProps} props - The read-only React {@code Component} props with
* which the new instance is to be initialized.
*/
constructor(props: IProps) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._onKeyboardShortcut = this._onKeyboardShortcut.bind(this);
this._getTooltip = this._getLabel;
}
/**
* Registers the keyboard shortcut that toggles the audio muting.
*
* @inheritdoc
* @returns {void}
*/
override componentDidMount() {
this.props.dispatch(registerShortcut({
character: 'M',
helpDescription: 'keyboardShortcuts.mute',
handler: this._onKeyboardShortcut
}));
}
/**
* Unregisters the keyboard shortcut that toggles the audio muting.
*
* @inheritdoc
* @returns {void}
*/
override componentWillUnmount() {
this.props.dispatch(unregisterShortcut('M'));
}
/**
* Gets the current accessibility label, taking the toggled and GUM pending state into account. If no toggled label
* is provided, the regular accessibility label will also be used in the toggled state.
*
* The accessibility label is not visible in the UI, it is meant to be used by assistive technologies, mainly screen
* readers.
*
* @private
* @returns {string}
*/
override _getAccessibilityLabel() {
const { _gumPending } = this.props;
if (_gumPending === IGUMPendingState.NONE) {
return super._getAccessibilityLabel();
}
return 'toolbar.accessibilityLabel.muteGUMPending';
}
/**
* Gets the current label, taking the toggled and GUM pending state into account. If no
* toggled label is provided, the regular label will also be used in the toggled state.
*
* @private
* @returns {string}
*/
override _getLabel() {
const { _gumPending } = this.props;
if (_gumPending === IGUMPendingState.NONE) {
return super._getLabel();
}
return 'toolbar.muteGUMPending';
}
/**
* Indicates if audio is currently muted or not.
*
* @override
* @protected
* @returns {boolean}
*/
override _isAudioMuted() {
if (this.props._gumPending === IGUMPendingState.PENDING_UNMUTE) {
return false;
}
return super._isAudioMuted();
}
/**
* Creates an analytics keyboard shortcut event and dispatches an action to
* toggle the audio muting.
*
* @private
* @returns {void}
*/
_onKeyboardShortcut() {
// Ignore keyboard shortcuts if the audio button is disabled.
if (this._isDisabled()) {
return;
}
sendAnalytics(
createShortcutEvent(
AUDIO_MUTE,
ACTION_SHORTCUT_TRIGGERED,
{ enable: !this._isAudioMuted() }));
AbstractButton.prototype._onClick.call(this);
}
/**
* Returns a spinner if there is pending GUM.
*
* @returns {ReactElement | null}
*/
override _getElementAfter(): ReactElement | null {
const { _gumPending } = this.props;
const classes = withStyles.getClasses(this.props);
return _gumPending === IGUMPendingState.NONE ? null
: (
<div className = { classes.pendingContainer }>
<Spinner
color = { SPINNER_COLOR }
size = 'small' />
</div>
);
}
}
/**
* Maps (parts of) the redux state to the associated props for the
* {@code AudioMuteButton} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _audioMuted: boolean,
* _disabled: boolean
* }}
*/
function _mapStateToProps(state: IReduxState) {
const { gumPending } = state['features/base/media'].audio;
return {
...abstractMapStateToProps(state),
_gumPending: gumPending
};
}
export default withStyles(translate(connect(_mapStateToProps)(AudioMuteButton)), styles);

View File

@@ -0,0 +1,180 @@
import React, { Component } from 'react';
import { WithTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { isMobileBrowser } from '../../../base/environment/utils';
import { translate } from '../../../base/i18n/functions';
import { IconArrowUp } from '../../../base/icons/svg';
import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
import { IGUMPendingState } from '../../../base/media/types';
import ToolboxButtonWithIcon from '../../../base/toolbox/components/web/ToolboxButtonWithIcon';
import { toggleAudioSettings } from '../../../settings/actions.web';
import AudioSettingsPopup from '../../../settings/components/web/audio/AudioSettingsPopup';
import { getAudioSettingsVisibility } from '../../../settings/functions.web';
import { isAudioSettingsButtonDisabled } from '../../functions.web';
import AudioMuteButton from './AudioMuteButton';
interface IProps extends WithTranslation {
/**
* The button's key.
*/
buttonKey?: string;
/**
* The gumPending state from redux.
*/
gumPending: IGUMPendingState;
/**
* External handler for click action.
*/
handleClick: Function;
/**
* Indicates whether audio permissions have been granted or denied.
*/
hasPermissions: boolean;
/**
* If the button should be disabled.
*/
isDisabled: boolean;
/**
* Defines is popup is open.
*/
isOpen: boolean;
/**
* Notify mode for `toolbarButtonClicked` event -
* whether to only notify or to also prevent button click routine.
*/
notifyMode?: string;
/**
* Click handler for the small icon. Opens audio options.
*/
onAudioOptionsClick: Function;
/**
* Flag controlling the visibility of the button.
* AudioSettings popup is disabled on mobile browsers.
*/
visible: boolean;
}
/**
* Button used for audio & audio settings.
*
* @returns {ReactElement}
*/
class AudioSettingsButton extends Component<IProps> {
/**
* Initializes a new {@code AudioSettingsButton} instance.
*
* @inheritdoc
*/
constructor(props: IProps) {
super(props);
this._onEscClick = this._onEscClick.bind(this);
this._onClick = this._onClick.bind(this);
}
/**
* Click handler for the more actions entries.
*
* @param {KeyboardEvent} event - Esc key click to close the popup.
* @returns {void}
*/
_onEscClick(event: React.KeyboardEvent) {
if (event.key === 'Escape' && this.props.isOpen) {
event.preventDefault();
event.stopPropagation();
this._onClick();
}
}
/**
* Click handler for the more actions entries.
*
* @param {MouseEvent} e - Mouse event.
* @returns {void}
*/
_onClick(e?: React.MouseEvent) {
const { onAudioOptionsClick, isOpen } = this.props;
if (isOpen) {
e?.stopPropagation();
}
onAudioOptionsClick();
}
/**
* Implements React's {@link Component#render}.
*
* @inheritdoc
*/
override render() {
const { gumPending, hasPermissions, isDisabled, visible, isOpen, buttonKey, notifyMode, t } = this.props;
const settingsDisabled = !hasPermissions
|| isDisabled
|| !JitsiMeetJS.mediaDevices.isMultipleAudioInputSupported();
return visible ? (
<AudioSettingsPopup>
<ToolboxButtonWithIcon
ariaControls = 'audio-settings-dialog'
ariaExpanded = { isOpen }
ariaHasPopup = { true }
ariaLabel = { t('toolbar.audioSettings') }
buttonKey = { buttonKey }
icon = { IconArrowUp }
iconDisabled = { settingsDisabled || gumPending !== IGUMPendingState.NONE }
iconId = 'audio-settings-button'
iconTooltip = { t('toolbar.audioSettings') }
notifyMode = { notifyMode }
onIconClick = { this._onClick }
onIconKeyDown = { this._onEscClick }>
<AudioMuteButton
buttonKey = { buttonKey }
notifyMode = { notifyMode } />
</ToolboxButtonWithIcon>
</AudioSettingsPopup>
) : <AudioMuteButton
buttonKey = { buttonKey }
notifyMode = { notifyMode } />;
}
}
/**
* Function that maps parts of Redux state tree into component props.
*
* @param {Object} state - Redux state.
* @returns {Object}
*/
function mapStateToProps(state: IReduxState) {
const { permissions = { audio: false } } = state['features/base/devices'];
const { isNarrowLayout } = state['features/base/responsive-ui'];
const { gumPending } = state['features/base/media'].audio;
return {
gumPending,
hasPermissions: permissions.audio,
isDisabled: Boolean(isAudioSettingsButtonDisabled(state)),
isOpen: Boolean(getAudioSettingsVisibility(state)),
visible: !isMobileBrowser() && !isNarrowLayout
};
}
const mapDispatchToProps = {
onAudioOptionsClick: toggleAudioSettings
};
export default translate(connect(
mapStateToProps,
mapDispatchToProps
)(AudioSettingsButton));

View File

@@ -0,0 +1,40 @@
import React from 'react';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
interface IProps extends AbstractButtonProps {
backgroundColor?: string;
icon: string;
id?: string;
text: string;
}
/**
* Component that renders a custom toolbox button.
*
* @returns {Component}
*/
class CustomOptionButton extends AbstractButton<IProps> {
iconSrc = this.props.icon;
id = this.props.id;
text = this.props.text;
override backgroundColor = this.props.backgroundColor;
override accessibilityLabel = this.text;
/**
* Custom icon component.
*
* @param {any} props - Icon's props.
* @returns {img}
*/
override icon = (props: any) => (<img
src = { this.iconSrc }
{ ...props } />);
override label = this.text;
override tooltip = this.text;
}
export default CustomOptionButton;

View File

@@ -0,0 +1,126 @@
import { ReactNode, useEffect, useRef, useState } from 'react';
import ReactDOM from 'react-dom';
import { useSelector } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { debounce } from '../../../base/config/functions.any';
import { ZINDEX_DIALOG_PORTAL } from '../../constants';
interface IProps {
/**
* The component(s) to be displayed within the drawer portal.
*/
children: ReactNode;
/**
* Custom class name to apply on the container div.
*/
className?: string;
/**
* Function used to get the reference to the container div.
*/
getRef?: Function;
/**
* Function called when the portal target becomes actually visible.
*/
onVisible?: Function;
/**
* Function used to get the updated size info of the container on it's resize.
*/
setSize?: Function;
/**
* Custom style to apply to the container div.
*/
style?: any;
/**
* The selector for the element we consider the content container.
* This is used to determine the correct size of the portal content.
*/
targetSelector?: string;
}
/**
* Component meant to render a drawer at the bottom of the screen,
* by creating a portal containing the component's children.
*
* @returns {ReactElement}
*/
function DialogPortal({ children, className, style, getRef, setSize, targetSelector, onVisible }: IProps) {
const videoSpaceWidth = useSelector((state: IReduxState) => state['features/base/responsive-ui'].videoSpaceWidth);
const [ portalTarget ] = useState(() => {
const portalDiv = document.createElement('div');
portalDiv.style.visibility = 'hidden';
return portalDiv;
});
const timerRef = useRef<number>();
useEffect(() => {
if (style) {
for (const styleProp of Object.keys(style)) {
const objStyle: any = portalTarget.style;
objStyle[styleProp] = style[styleProp];
}
}
if (className) {
portalTarget.className = className;
}
}, [ style, className ]);
useEffect(() => {
if (portalTarget && getRef) {
getRef(portalTarget);
portalTarget.style.zIndex = `${ZINDEX_DIALOG_PORTAL}`;
}
}, [ portalTarget, getRef ]);
useEffect(() => {
const size = {
width: 1,
height: 1
};
const debouncedResizeCallback = debounce((entries: ResizeObserverEntry[]) => {
const { contentRect } = entries[0];
if (contentRect.width !== size.width || contentRect.height !== size.height) {
setSize?.(contentRect);
clearTimeout(timerRef.current);
timerRef.current = window.setTimeout(() => {
portalTarget.style.visibility = 'visible';
onVisible?.();
}, 100);
}
}, 20); // 20ms delay
// Create and observe ResizeObserver
const observer = new ResizeObserver(debouncedResizeCallback);
const target = targetSelector ? portalTarget.querySelector(targetSelector) : portalTarget;
if (document.body) {
document.body.appendChild(portalTarget);
observer.observe(target ?? portalTarget);
}
return () => {
observer.unobserve(target ?? portalTarget);
if (document.body) {
document.body.removeChild(portalTarget);
}
};
}, [ videoSpaceWidth ]);
return ReactDOM.createPortal(
children,
portalTarget
);
}
export default DialogPortal;

View File

@@ -0,0 +1,172 @@
import React, { KeyboardEvent, ReactNode, useCallback } from 'react';
import { FocusOn } from 'react-focus-on';
import { makeStyles } from 'tss-react/mui';
import { isElementInTheViewport } from '../../../base/ui/functions.web';
import { DRAWER_MAX_HEIGHT } from '../../constants';
interface IProps {
/**
* The component(s) to be displayed within the drawer menu.
*/
children: ReactNode;
/**
* Class name for custom styles.
*/
className?: string;
/**
* The id of the dom element acting as the Drawer label.
*/
headingId?: string;
/**
* Whether the drawer should be shown or not.
*/
isOpen: boolean;
/**
* Function that hides the drawer.
*/
onClose?: Function;
}
const useStyles = makeStyles()(theme => {
return {
drawerMenuContainer: {
backgroundColor: 'rgba(0,0,0,0.6)',
height: '100dvh',
display: 'flex',
alignItems: 'flex-end'
},
drawer: {
backgroundColor: theme.palette.ui01,
maxHeight: `calc(${DRAWER_MAX_HEIGHT})`,
borderRadius: '24px 24px 0 0',
overflowY: 'auto',
marginBottom: 'env(safe-area-inset-bottom, 0)',
width: '100%',
'& .overflow-menu': {
margin: 'auto',
fontSize: '1.2em',
listStyleType: 'none',
padding: 0,
height: 'calc(80vh - 144px - 64px)',
overflowY: 'auto',
'& .overflow-menu-item': {
boxSizing: 'border-box',
height: '48px',
padding: '12px 16px',
alignItems: 'center',
color: theme.palette.text01,
cursor: 'pointer',
display: 'flex',
fontSize: '1rem',
'& div': {
display: 'flex',
flexDirection: 'row',
alignItems: 'center'
},
'&.disabled': {
cursor: 'initial',
color: '#3b475c'
}
}
}
}
};
});
/**
* Component that displays the mobile friendly drawer on web.
*
* @returns {ReactElement}
*/
function Drawer({
children,
className = '',
headingId,
isOpen,
onClose
}: IProps) {
const { classes, cx } = useStyles();
/**
* Handles clicks within the menu, preventing the propagation of the click event.
*
* @param {Object} event - The click event.
* @returns {void}
*/
const handleInsideClick = useCallback(event => {
event.stopPropagation();
}, []);
/**
* Handles clicks outside of the menu, closing it, and also stopping further propagation.
*
* @param {Object} event - The click event.
* @returns {void}
*/
const handleOutsideClick = useCallback(event => {
event.stopPropagation();
onClose?.();
}, [ onClose ]);
/**
* Handles pressing the escape key, closing the drawer.
*
* @param {KeyboardEvent<HTMLDivElement>} event - The keydown event.
* @returns {void}
*/
const handleEscKey = useCallback((event: KeyboardEvent<HTMLDivElement>) => {
if (event.key === 'Escape') {
event.preventDefault();
event.stopPropagation();
onClose?.();
}
}, [ onClose ]);
return (
isOpen ? (
<div
className = { classes.drawerMenuContainer }
onClick = { handleOutsideClick }
onKeyDown = { handleEscKey }>
<div
className = { cx(classes.drawer, className) }
onClick = { handleInsideClick }>
<FocusOn
returnFocus = {
// If we return the focus to an element outside the viewport the page will scroll to
// this element which in our case is undesirable and the element is outside of the
// viewport on purpose (to be hidden). For example if we return the focus to the toolbox
// when it is hidden the whole page will move up in order to show the toolbox. This is
// usually followed up with displaying the toolbox (because now it is on focus) but
// because of the animation the whole scenario looks like jumping large video.
isElementInTheViewport
}>
<div
aria-labelledby = { headingId ? `#${headingId}` : undefined }
aria-modal = { true }
data-autofocus = { true }
role = 'dialog'
tabIndex = { -1 }>
{children}
</div>
</FocusOn>
</div>
</div>
) : null
);
}
export default Drawer;

View File

@@ -0,0 +1,55 @@
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { endConference } from '../../../base/conference/actions';
import { isLocalParticipantModerator } from '../../../base/participants/functions';
import { BUTTON_TYPES } from '../../../base/ui/constants.web';
import { isInBreakoutRoom } from '../../../breakout-rooms/functions';
import { HangupContextMenuItem } from './HangupContextMenuItem';
/**
* The type of the React {@code Component} props of {@link EndConferenceButton}.
*/
interface IProps {
/**
* Key to use for toolbarButtonClicked event.
*/
buttonKey: string;
/**
* Notify mode for `toolbarButtonClicked` event -
* whether to only notify or to also prevent button click routine.
*/
notifyMode?: string;
}
/**
* Button to end the conference for all participants.
*
* @param {Object} props - Component's props.
* @returns {JSX.Element} - The end conference button.
*/
export const EndConferenceButton = (props: IProps) => {
const { t } = useTranslation();
const dispatch = useDispatch();
const _isLocalParticipantModerator = useSelector(isLocalParticipantModerator);
const _isInBreakoutRoom = useSelector(isInBreakoutRoom);
const onEndConference = useCallback(() => {
dispatch(endConference());
}, [ dispatch ]);
return (<>
{ !_isInBreakoutRoom && _isLocalParticipantModerator && <HangupContextMenuItem
accessibilityLabel = { t('toolbar.accessibilityLabel.endConference') }
buttonKey = { props.buttonKey }
buttonType = { BUTTON_TYPES.DESTRUCTIVE }
label = { t('toolbar.endConference') }
notifyMode = { props.notifyMode }
onClick = { onEndConference } /> }
</>);
};

View File

@@ -0,0 +1,77 @@
import { connect } from 'react-redux';
import { createToolbarEvent } from '../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../analytics/functions';
import { IReduxState } from '../../../app/types';
import { isIosMobileBrowser } from '../../../base/environment/utils';
import { translate } from '../../../base/i18n/functions';
import { IconEnterFullscreen, IconExitFullscreen } from '../../../base/icons/svg';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { closeOverflowMenuIfOpen, setFullScreen } from '../../actions.web';
interface IProps extends AbstractButtonProps {
/**
* Whether or not the app is currently in full screen.
*/
_fullScreen?: boolean;
}
/**
* Implementation of a button for toggling fullscreen state.
*/
class FullscreenButton extends AbstractButton<IProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.enterFullScreen';
override toggledAccessibilityLabel = 'toolbar.accessibilityLabel.exitFullScreen';
override label = 'toolbar.enterFullScreen';
override toggledLabel = 'toolbar.exitFullScreen';
override tooltip = 'toolbar.enterFullScreen';
override toggledTooltip = 'toolbar.exitFullScreen';
override toggledIcon = IconExitFullscreen;
override icon = IconEnterFullscreen;
/**
* Indicates whether this button is in toggled state or not.
*
* @override
* @protected
* @returns {boolean}
*/
override _isToggled() {
return this.props._fullScreen;
}
/**
* Handles clicking the button, and toggles fullscreen.
*
* @private
* @returns {void}
*/
override _handleClick() {
const { dispatch, _fullScreen } = this.props;
sendAnalytics(createToolbarEvent(
'toggle.fullscreen',
{
enable: !_fullScreen
}));
dispatch(closeOverflowMenuIfOpen());
dispatch(setFullScreen(!_fullScreen));
}
}
/**
* Function that maps parts of Redux state tree into component props.
*
* @param {Object} state - Redux state.
* @returns {Object}
*/
const mapStateToProps = (state: IReduxState) => {
return {
_fullScreen: state['features/toolbox'].fullScreen,
visible: !isIosMobileBrowser()
};
};
export default translate(connect(mapStateToProps)(FullscreenButton));

View File

@@ -0,0 +1,73 @@
import React, { useCallback } from 'react';
import Button from '../../../base/ui/components/web/Button';
import { NOTIFY_CLICK_MODE } from '../../types';
/**
* The type of the React {@code Component} props of {@link HangupContextMenuItem}.
*/
interface IProps {
/**
* Accessibility label for the button.
*/
accessibilityLabel: string;
/**
* Key to use for toolbarButtonClicked event.
*/
buttonKey: string;
/**
* Type of button to display.
*/
buttonType: string;
/**
* Text associated with the button.
*/
label: string;
/**
* Notify mode for `toolbarButtonClicked` event -
* whether to only notify or to also prevent button click routine.
*/
notifyMode?: string;
/**
* Callback that performs the actual hangup action.
*/
onClick: Function;
}
/**
* Implementation of a button to be rendered within Hangup context menu.
*
* @param {Object} props - Component's props.
* @returns {JSX.Element} - Button that would trigger the hangup action.
*/
export const HangupContextMenuItem = (props: IProps) => {
const shouldNotify = props.notifyMode !== undefined;
const shouldPreventExecution = props.notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY;
const _onClick = useCallback(() => {
if (shouldNotify) {
APP.API.notifyToolbarButtonClicked(props.buttonKey, shouldPreventExecution);
}
if (!shouldPreventExecution) {
props.onClick();
}
}, []);
return (
<Button
accessibilityLabel = { props.accessibilityLabel }
fullWidth = { true }
label = { props.label }
onClick = { _onClick }
type = { props.buttonType } />
);
};

View File

@@ -0,0 +1,134 @@
import React, { Component } from 'react';
import { WithTranslation } from 'react-i18next';
import { createToolbarEvent } from '../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../analytics/functions';
import { translate } from '../../../base/i18n/functions';
import Popover from '../../../base/popover/components/Popover.web';
import HangupToggleButton from './HangupToggleButton';
/**
* The type of the React {@code Component} props of {@link HangupMenuButton}.
*/
interface IProps extends WithTranslation {
/**
* ID of the menu that is controlled by this button.
*/
ariaControls: String;
/**
* A child React Element to display within {@code InlineDialog}.
*/
children: React.ReactNode;
/**
* Whether or not the HangupMenu popover should display.
*/
isOpen: boolean;
/**
* Notify mode for `toolbarButtonClicked` event -
* whether to only notify or to also prevent button click routine.
*/
notifyMode?: string;
/**
* Callback to change the visibility of the hangup menu.
*/
onVisibilityChange: Function;
}
/**
* A React {@code Component} for opening or closing the {@code HangupMenu}.
*
* @augments Component
*/
class HangupMenuButton extends Component<IProps> {
/**
* Initializes a new {@code HangupMenuButton} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: IProps) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._onCloseDialog = this._onCloseDialog.bind(this);
this._toggleDialogVisibility
= this._toggleDialogVisibility.bind(this);
this._onEscClick = this._onEscClick.bind(this);
}
/**
* Click handler for the more actions entries.
*
* @param {KeyboardEvent} event - Esc key click to close the popup.
* @returns {void}
*/
_onEscClick(event: KeyboardEvent) {
if (event.key === 'Escape' && this.props.isOpen) {
event.preventDefault();
event.stopPropagation();
this._onCloseDialog();
}
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
override render() {
const { children, isOpen, t } = this.props;
return (
<div className = 'toolbox-button-wth-dialog context-menu'>
<Popover
content = { children }
headingLabel = { t('toolbar.accessibilityLabel.hangup') }
onPopoverClose = { this._onCloseDialog }
position = 'top'
trigger = 'click'
visible = { isOpen }>
<HangupToggleButton
buttonKey = 'hangup-menu'
customClass = 'hangup-menu-button'
handleClick = { this._toggleDialogVisibility }
isOpen = { isOpen }
notifyMode = { this.props.notifyMode }
onKeyDown = { this._onEscClick } />
</Popover>
</div>
);
}
/**
* Callback invoked when {@code InlineDialog} signals that it should be
* close.
*
* @private
* @returns {void}
*/
_onCloseDialog() {
this.props.onVisibilityChange(false);
}
/**
* Callback invoked to signal that an event has occurred that should change
* the visibility of the {@code InlineDialog} component.
*
* @private
* @returns {void}
*/
_toggleDialogVisibility() {
sendAnalytics(createToolbarEvent('hangup'));
this.props.onVisibilityChange(!this.props.isOpen);
}
}
export default translate(HangupMenuButton);

View File

@@ -0,0 +1,57 @@
import { connect } from 'react-redux';
import { translate } from '../../../base/i18n/functions';
import { IconCloseLarge, IconHangup } from '../../../base/icons/svg';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
/**
* The type of the React {@code Component} props of {@link HangupToggleButton}.
*/
interface IProps extends AbstractButtonProps {
/**
* Whether the more options menu is open.
*/
isOpen: boolean;
/**
* External handler for key down action.
*/
onKeyDown: Function;
}
/**
* Implementation of a button for toggling the hangup menu.
*/
class HangupToggleButton extends AbstractButton<IProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.hangup';
override icon = IconHangup;
override label = 'toolbar.hangup';
override toggledIcon = IconCloseLarge;
override toggledLabel = 'toolbar.hangup';
override tooltip = 'toolbar.hangup';
/**
* Indicates whether this button is in toggled state or not.
*
* @override
* @protected
* @returns {boolean}
*/
override _isToggled() {
return this.props.isOpen;
}
/**
* Indicates whether a key was pressed.
*
* @override
* @protected
* @returns {boolean}
*/
override _onKeyDown() {
this.props.onKeyDown();
}
}
export default connect()(translate(HangupToggleButton));

View File

@@ -0,0 +1,58 @@
import React, { ReactNode } from 'react';
import { makeStyles } from 'tss-react/mui';
import DialogPortal from './DialogPortal';
interface IProps {
/**
* The component(s) to be displayed within the drawer portal.
*/
children: ReactNode;
/**
* Class name used to add custom styles to the portal.
*/
className?: string;
}
const useStyles = makeStyles()(theme => {
return {
portal: {
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
zIndex: 351,
borderRadius: '16px 16px 0 0',
'&.notification-portal': {
zIndex: 901
},
'&::after': {
content: '""',
backgroundColor: theme.palette.ui01,
marginBottom: 'env(safe-area-inset-bottom, 0)'
}
}
};
});
/**
* Component meant to render a drawer at the bottom of the screen,
* by creating a portal containing the component's children.
*
* @returns {ReactElement}
*/
function JitsiPortal({ children, className }: IProps) {
const { classes, cx } = useStyles();
return (
<DialogPortal className = { cx(classes.portal, className) }>
{ children }
</DialogPortal>
);
}
export default JitsiPortal;

View File

@@ -0,0 +1,54 @@
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { createToolbarEvent } from '../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../analytics/functions';
import { leaveConference } from '../../../base/conference/actions';
import { BUTTON_TYPES } from '../../../base/ui/constants.web';
import { HangupContextMenuItem } from './HangupContextMenuItem';
/**
* The type of the React {@code Component} props of {@link LeaveConferenceButton}.
*/
interface IProps {
/**
* Key to use for toolbarButtonClicked event.
*/
buttonKey: string;
/**
* Notify mode for `toolbarButtonClicked` event -
* whether to only notify or to also prevent button click routine.
*/
notifyMode?: string;
}
/**
* Button to leave the conference.
*
* @param {Object} props - Component's props.
* @returns {JSX.Element} - The leave conference button.
*/
export const LeaveConferenceButton = (props: IProps) => {
const { t } = useTranslation();
const dispatch = useDispatch();
const onLeaveConference = useCallback(() => {
sendAnalytics(createToolbarEvent('hangup'));
dispatch(leaveConference());
}, [ dispatch ]);
return (
<HangupContextMenuItem
accessibilityLabel = { t('toolbar.accessibilityLabel.leaveConference') }
buttonKey = { props.buttonKey }
buttonType = { BUTTON_TYPES.SECONDARY }
label = { t('toolbar.leaveConference') }
notifyMode = { props.notifyMode }
onClick = { onLeaveConference } />
);
};

View File

@@ -0,0 +1,48 @@
import { connect } from 'react-redux';
import { createToolbarEvent } from '../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../analytics/functions';
import { IReduxState } from '../../../app/types';
import { openDialog } from '../../../base/dialog/actions';
import { translate } from '../../../base/i18n/functions';
import { IconCloudUpload } from '../../../base/icons/svg';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import SalesforceLinkDialog from '../../../salesforce/components/web/SalesforceLinkDialog';
import { isSalesforceEnabled } from '../../../salesforce/functions';
/**
* Implementation of a button for opening the Salesforce link dialog.
*/
class LinkToSalesforce extends AbstractButton<AbstractButtonProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.linkToSalesforce';
override icon = IconCloudUpload;
override label = 'toolbar.linkToSalesforce';
override tooltip = 'toolbar.linkToSalesforce';
/**
* Handles clicking / pressing the button, and opens the Salesforce link dialog.
*
* @protected
* @returns {void}
*/
override _handleClick() {
const { dispatch } = this.props;
sendAnalytics(createToolbarEvent('link.to.salesforce'));
dispatch(openDialog(SalesforceLinkDialog));
}
}
/**
* Function that maps parts of Redux state tree into component props.
*
* @param {Object} state - Redux state.
* @returns {Object}
*/
const mapStateToProps = (state: IReduxState) => {
return {
visible: isSalesforceEnabled(state)
};
};
export default translate(connect(mapStateToProps)(LinkToSalesforce));

View File

@@ -0,0 +1,262 @@
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { createToolbarEvent } from '../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../analytics/functions';
import Popover from '../../../base/popover/components/Popover.web';
import ContextMenu from '../../../base/ui/components/web/ContextMenu';
import ContextMenuItemGroup from '../../../base/ui/components/web/ContextMenuItemGroup';
import { setGifMenuVisibility } from '../../../gifs/actions';
import { isGifsMenuOpen } from '../../../gifs/functions.web';
import ReactionEmoji from '../../../reactions/components/web/ReactionEmoji';
import ReactionsMenu from '../../../reactions/components/web/ReactionsMenu';
import {
GIFS_MENU_HEIGHT_IN_OVERFLOW_MENU,
RAISE_HAND_ROW_HEIGHT,
REACTIONS_MENU_HEIGHT_DRAWER,
REACTIONS_MENU_HEIGHT_IN_OVERFLOW_MENU
} from '../../../reactions/constants';
import { getReactionsQueue } from '../../../reactions/functions.any';
import { IReactionsMenuParent } from '../../../reactions/types';
import { DRAWER_MAX_HEIGHT } from '../../constants';
import { showOverflowDrawer } from '../../functions.web';
import Drawer from './Drawer';
import JitsiPortal from './JitsiPortal';
import OverflowToggleButton from './OverflowToggleButton';
/**
* The type of the React {@code Component} props of {@link OverflowMenuButton}.
*/
interface IProps {
/**
* ID of the menu that is controlled by this button.
*/
ariaControls: string;
/**
* Information about the buttons that need to be rendered in the overflow menu.
*/
buttons: Object[];
/**
* Whether or not the OverflowMenu popover should display.
*/
isOpen: boolean;
/**
* Esc key handler.
*/
onToolboxEscKey: (e?: React.KeyboardEvent) => void;
/**
* Callback to change the visibility of the overflow menu.
*/
onVisibilityChange: Function;
/**
* Whether to show the raise hand in the reactions menu or not.
*/
showRaiseHandInReactionsMenu: boolean;
/**
* Whether or not to display the reactions menu.
*/
showReactionsMenu: boolean;
}
const useStyles = makeStyles<{ overflowDrawer: boolean; reactionsMenuHeight: number; }>()(
(_theme, { reactionsMenuHeight, overflowDrawer }) => {
return {
overflowMenuDrawer: {
overflowY: 'scroll',
height: `calc(${DRAWER_MAX_HEIGHT})`
},
contextMenu: {
position: 'relative' as const,
right: 'auto',
margin: 0,
marginBottom: '8px',
maxHeight: overflowDrawer ? undefined : 'calc(100dvh - 100px)',
paddingBottom: overflowDrawer ? undefined : 0,
minWidth: '240px',
overflow: 'hidden'
},
content: {
position: 'relative',
maxHeight: overflowDrawer
? `calc(100% - ${reactionsMenuHeight}px - 16px)` : `calc(100dvh - 100px - ${reactionsMenuHeight}px)`,
overflowY: 'auto'
},
footer: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0
},
reactionsPadding: {
height: `${reactionsMenuHeight}px`
}
};
});
const OverflowMenuButton = ({
buttons,
isOpen,
onToolboxEscKey,
onVisibilityChange,
showRaiseHandInReactionsMenu,
showReactionsMenu
}: IProps) => {
const overflowDrawer = useSelector(showOverflowDrawer);
const reactionsQueue = useSelector(getReactionsQueue);
const isGiphyVisible = useSelector(isGifsMenuOpen);
const dispatch = useDispatch();
const onCloseDialog = useCallback(() => {
onVisibilityChange(false);
if (isGiphyVisible && !overflowDrawer) {
dispatch(setGifMenuVisibility(false));
}
}, [ onVisibilityChange, setGifMenuVisibility, isGiphyVisible, overflowDrawer, dispatch ]);
const onOpenDialog = useCallback(() => {
onVisibilityChange(true);
}, [ onVisibilityChange ]);
const onEscClick = useCallback((event: React.KeyboardEvent) => {
if (event.key === 'Escape' && isOpen) {
event.preventDefault();
event.stopPropagation();
onCloseDialog();
}
}, [ onCloseDialog ]);
const toggleDialogVisibility = useCallback(() => {
sendAnalytics(createToolbarEvent('overflow'));
onVisibilityChange(!isOpen);
}, [ isOpen, onVisibilityChange ]);
const toolbarAccLabel = 'toolbar.accessibilityLabel.moreActionsMenu';
const { t } = useTranslation();
let reactionsMenuHeight = 0;
if (showReactionsMenu) {
reactionsMenuHeight = REACTIONS_MENU_HEIGHT_DRAWER;
if (!overflowDrawer) {
reactionsMenuHeight = REACTIONS_MENU_HEIGHT_IN_OVERFLOW_MENU;
}
if (!showRaiseHandInReactionsMenu) {
reactionsMenuHeight -= RAISE_HAND_ROW_HEIGHT;
}
if (!overflowDrawer && isGiphyVisible) {
reactionsMenuHeight += GIFS_MENU_HEIGHT_IN_OVERFLOW_MENU;
}
}
const { classes } = useStyles({
reactionsMenuHeight,
overflowDrawer
});
const groupsJSX = buttons.map((buttonGroup: any) => (
<ContextMenuItemGroup key = { `group-${buttonGroup[0].group}` }>
{buttonGroup.map(({ key, Content, ...rest }: { Content: React.ElementType; key: string; }) => {
const props: { buttonKey?: string; contextMenu?: boolean; showLabel?: boolean; } = { ...rest };
if (key !== 'reactions') {
props.buttonKey = key;
props.contextMenu = true;
props.showLabel = true;
}
return (
<Content
{ ...props }
key = { key } />);
})}
</ContextMenuItemGroup>));
const overflowMenu = groupsJSX && (
<ContextMenu
accessibilityLabel = { t(toolbarAccLabel) }
className = { classes.contextMenu }
hidden = { false }
id = 'overflow-context-menu'
inDrawer = { overflowDrawer }
onKeyDown = { onToolboxEscKey }>
<div className = { classes.content }>
{ groupsJSX }
</div>
{
showReactionsMenu && (<div className = { classes.footer }>
<ReactionsMenu
parent = {
overflowDrawer ? IReactionsMenuParent.OverflowDrawer : IReactionsMenuParent.OverflowMenu }
showRaisedHand = { showRaiseHandInReactionsMenu } />
</div>)
}
</ContextMenu>);
if (overflowDrawer) {
return (
<div className = 'toolbox-button-wth-dialog context-menu'>
<>
<OverflowToggleButton
handleClick = { toggleDialogVisibility }
isOpen = { isOpen }
onKeyDown = { onEscClick } />
<JitsiPortal>
<Drawer
isOpen = { isOpen }
onClose = { onCloseDialog }>
<>
<div className = { classes.overflowMenuDrawer }>
{ overflowMenu }
<div className = { classes.reactionsPadding } />
</div>
</>
</Drawer>
{showReactionsMenu && <div className = 'reactions-animations-overflow-container'>
{reactionsQueue.map(({ reaction, uid }, index) => (<ReactionEmoji
index = { index }
key = { uid }
reaction = { reaction }
uid = { uid } />))}
</div>}
</JitsiPortal>
</>
</div>
);
}
return (
<div className = 'toolbox-button-wth-dialog context-menu'>
<Popover
content = { overflowMenu }
headingId = 'overflow-context-menu'
onPopoverClose = { onCloseDialog }
onPopoverOpen = { onOpenDialog }
position = 'top'
trigger = 'click'
visible = { isOpen }>
<OverflowToggleButton
isMenuButton = { true }
isOpen = { isOpen }
onKeyDown = { onEscClick } />
</Popover>
{showReactionsMenu && <div className = 'reactions-animations-container'>
{reactionsQueue.map(({ reaction, uid }, index) => (<ReactionEmoji
index = { index }
key = { uid }
reaction = { reaction }
uid = { uid } />))}
</div>}
</div>
);
};
export default OverflowMenuButton;

View File

@@ -0,0 +1,59 @@
import { connect } from 'react-redux';
import { translate } from '../../../base/i18n/functions';
import { IconDotsHorizontal } from '../../../base/icons/svg';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
/**
* The type of the React {@code Component} props of {@link OverflowToggleButton}.
*/
interface IProps extends AbstractButtonProps {
/**
* Whether the more options menu is open.
*/
isOpen: boolean;
/**
* External handler for key down action.
*/
onKeyDown: Function;
}
/**
* Implementation of a button for toggling the overflow menu.
*/
class OverflowToggleButton extends AbstractButton<IProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.moreActions';
override toggledAccessibilityLabel = 'toolbar.accessibilityLabel.closeMoreActions';
override icon = IconDotsHorizontal;
override label = 'toolbar.moreActions';
override toggledLabel = 'toolbar.moreActions';
override tooltip = 'toolbar.moreActions';
/**
* Indicates whether this button is in toggled state or not.
*
* @override
* @protected
* @returns {boolean}
*/
override _isToggled() {
return this.props.isOpen;
}
/**
* Indicates whether a key was pressed.
*
* @override
* @protected
* @returns {boolean}
*/
override _onKeyDown() {
this.props.onKeyDown();
}
}
export default connect()(translate(OverflowToggleButton));

View File

@@ -0,0 +1,116 @@
import { connect } from 'react-redux';
import { createToolbarEvent } from '../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../analytics/functions';
import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import { getLocalParticipant } from '../../../base/participants/functions';
import { ILocalParticipant } from '../../../base/participants/types';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { openSettingsDialog } from '../../../settings/actions';
import { SETTINGS_TABS } from '../../../settings/constants';
import ProfileButtonAvatar from './ProfileButtonAvatar';
/**
* The type of the React {@code Component} props of {@link ProfileButton}.
*/
interface IProps extends AbstractButtonProps {
/**
* Default displayed name for local participant.
*/
_defaultLocalDisplayName: string;
/**
* The redux representation of the local participant.
*/
_localParticipant?: ILocalParticipant;
/**
* Whether the button support clicking or not.
*/
_unclickable: boolean;
}
/**
* Implementation of a button for opening profile dialog.
*/
class ProfileButton extends AbstractButton<IProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.profile';
override icon = ProfileButtonAvatar;
/**
* Retrieves the label.
*
* @returns {string}
*/
override _getLabel() {
const {
_defaultLocalDisplayName,
_localParticipant
} = this.props;
let displayName;
if (_localParticipant?.name) {
displayName = _localParticipant.name;
} else {
displayName = _defaultLocalDisplayName;
}
return displayName;
}
/**
* Retrieves the tooltip.
*
* @returns {string}
*/
override _getTooltip() {
return this._getLabel();
}
/**
* Handles clicking / pressing the button, and opens the appropriate dialog.
*
* @protected
* @returns {void}
*/
override _handleClick() {
const { dispatch, _unclickable } = this.props;
if (!_unclickable) {
sendAnalytics(createToolbarEvent('profile'));
dispatch(openSettingsDialog(SETTINGS_TABS.PROFILE));
}
}
/**
* Indicates whether the button should be disabled or not.
*
* @protected
* @returns {void}
*/
override _isDisabled() {
return this.props._unclickable;
}
}
/**
* Function that maps parts of Redux state tree into component props.
*
* @param {Object} state - Redux state.
* @returns {Object}
*/
const mapStateToProps = (state: IReduxState) => {
const { defaultLocalDisplayName } = state['features/base/config'];
return {
_defaultLocalDisplayName: defaultLocalDisplayName ?? '',
_localParticipant: getLocalParticipant(state),
_unclickable: !interfaceConfig.SETTINGS_SECTIONS.includes('profile'),
customClass: 'profile-button-avatar'
};
};
export default translate(connect(mapStateToProps)(ProfileButton));

View File

@@ -0,0 +1,62 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import Avatar from '../../../base/avatar/components/Avatar';
import { getLocalParticipant } from '../../../base/participants/functions';
import { ILocalParticipant } from '../../../base/participants/types';
/**
* The type of the React {@code Component} props of
* {@link ProfileButtonAvatar}.
*/
interface IProps {
/**
* The redux representation of the local participant.
*/
_localParticipant?: ILocalParticipant;
}
/**
* A React {@code Component} for displaying a profile avatar as an
* icon.
*
* @augments Component
*/
class ProfileButtonAvatar extends Component<IProps> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
override render() {
const { _localParticipant } = this.props;
return (
<Avatar
participantId = { _localParticipant?.id }
size = { 20 } />
);
}
}
/**
* Maps (parts of) the Redux state to the associated
* {@code ProfileButtonAvatar} component's props.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _localParticipant: Object,
* }}
*/
function _mapStateToProps(state: IReduxState) {
return {
_localParticipant: getLocalParticipant(state)
};
}
export default connect(_mapStateToProps)(ProfileButtonAvatar);

View File

@@ -0,0 +1,3 @@
import React from 'react';
export default () => <hr className = 'overflow-menu-hr' />;

View File

@@ -0,0 +1,116 @@
import { connect } from 'react-redux';
import { createToolbarEvent } from '../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../analytics/functions';
import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import { IconScreenshare } from '../../../base/icons/svg';
import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { startScreenShareFlow } from '../../../screen-share/actions.web';
import { isScreenVideoShared } from '../../../screen-share/functions';
import { closeOverflowMenuIfOpen } from '../../actions.web';
import { isDesktopShareButtonDisabled } from '../../functions.web';
interface IProps extends AbstractButtonProps {
/**
* Whether or not screen-sharing is initialized.
*/
_desktopSharingEnabled: boolean;
/**
* Whether or not the local participant is screen-sharing.
*/
_screensharing: boolean;
}
/**
* Implementation of a button for sharing desktop / windows.
*/
class ShareDesktopButton extends AbstractButton<IProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.shareYourScreen';
override toggledAccessibilityLabel = 'toolbar.accessibilityLabel.stopScreenSharing';
override label = 'toolbar.startScreenSharing';
override icon = IconScreenshare;
override toggledLabel = 'toolbar.stopScreenSharing';
/**
* Retrieves tooltip dynamically.
*
* @returns {string}
*/
override _getTooltip() {
const { _desktopSharingEnabled, _screensharing } = this.props;
if (_desktopSharingEnabled) {
if (_screensharing) {
return 'toolbar.stopScreenSharing';
}
return 'toolbar.startScreenSharing';
}
return 'dialog.shareYourScreenDisabled';
}
/**
* Indicates whether this button is in toggled state or not.
*
* @override
* @protected
* @returns {boolean}
*/
override _isToggled() {
return this.props._screensharing;
}
/**
* Indicates whether this button is in disabled state or not.
*
* @override
* @protected
* @returns {boolean}
*/
override _isDisabled() {
return !this.props._desktopSharingEnabled;
}
/**
* Handles clicking the button, and toggles the chat.
*
* @private
* @returns {void}
*/
override _handleClick() {
const { dispatch, _screensharing } = this.props;
sendAnalytics(createToolbarEvent(
'toggle.screen.sharing',
{ enable: !_screensharing }));
dispatch(closeOverflowMenuIfOpen());
dispatch(startScreenShareFlow(!_screensharing));
}
}
/**
* Function that maps parts of Redux state tree into component props.
*
* @param {Object} state - Redux state.
* @returns {Object}
*/
const mapStateToProps = (state: IReduxState) => {
// Disable the screen-share button if the video sender limit is reached and there is no video or media share in
// progress.
const desktopSharingEnabled
= JitsiMeetJS.isDesktopSharingEnabled() && !isDesktopShareButtonDisabled(state);
return {
_desktopSharingEnabled: desktopSharingEnabled,
_screensharing: isScreenVideoShared(state),
visible: JitsiMeetJS.isDesktopSharingEnabled()
};
};
export default translate(connect(mapStateToProps)(ShareDesktopButton));

View File

@@ -0,0 +1,76 @@
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import { IconCameraRefresh } from '../../../base/icons/svg';
import { MEDIA_TYPE } from '../../../base/media/constants';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { toggleCamera } from '../../../base/tracks/actions';
import { isLocalTrackMuted, isToggleCameraEnabled } from '../../../base/tracks/functions';
import { setOverflowMenuVisible } from '../../actions.web';
/**
* The type of the React {@code Component} props of {@link ToggleCameraButton}.
*/
interface IProps extends AbstractButtonProps {
/**
* Whether the current conference is in audio only mode or not.
*/
_audioOnly: boolean;
/**
* Whether video is currently muted or not.
*/
_videoMuted: boolean;
}
/**
* An implementation of a button for toggling the camera facing mode.
*/
class ToggleCameraButton extends AbstractButton<IProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.toggleCamera';
override icon = IconCameraRefresh;
override label = 'toolbar.toggleCamera';
/**
* Handles clicking/pressing the button.
*
* @returns {void}
*/
override _handleClick() {
const { dispatch } = this.props;
dispatch(toggleCamera());
dispatch(setOverflowMenuVisible(false));
}
/**
* Whether this button is disabled or not.
*
* @returns {boolean}
*/
override _isDisabled() {
return this.props._audioOnly || this.props._videoMuted;
}
}
/**
* Maps (parts of) the redux state to the associated props for the
* {@code ToggleCameraButton} component.
*
* @param {Object} state - The Redux state.
* @returns {IProps}
*/
function mapStateToProps(state: IReduxState) {
const { enabled: audioOnly } = state['features/base/audio-only'];
const tracks = state['features/base/tracks'];
return {
_audioOnly: Boolean(audioOnly),
_videoMuted: isLocalTrackMuted(tracks, MEDIA_TYPE.VIDEO),
visible: isToggleCameraEnabled(state)
};
}
export default translate(connect(mapStateToProps)(ToggleCameraButton));

View File

@@ -0,0 +1,334 @@
import React, { useCallback, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { IReduxState } from '../../../app/types';
import { isMobileBrowser } from '../../../base/environment/utils';
import { getLocalParticipant, isLocalParticipantModerator } from '../../../base/participants/functions';
import ContextMenu from '../../../base/ui/components/web/ContextMenu';
import { isReactionsButtonEnabled, shouldDisplayReactionsButtons } from '../../../reactions/functions.web';
import { isCCTabEnabled } from '../../../subtitles/functions.any';
import { isTranscribing } from '../../../transcribing/functions';
import {
setHangupMenuVisible,
setOverflowMenuVisible,
setToolbarHovered,
setToolboxVisible
} from '../../actions.web';
import {
getJwtDisabledButtons,
getVisibleButtons,
isButtonEnabled,
isToolboxVisible
} from '../../functions.web';
import { useKeyboardShortcuts, useToolboxButtons } from '../../hooks.web';
import { IToolboxButton } from '../../types';
import HangupButton from '../HangupButton';
import { EndConferenceButton } from './EndConferenceButton';
import HangupMenuButton from './HangupMenuButton';
import { LeaveConferenceButton } from './LeaveConferenceButton';
import OverflowMenuButton from './OverflowMenuButton';
import Separator from './Separator';
/**
* The type of the React {@code Component} props of {@link Toolbox}.
*/
interface IProps {
/**
* Explicitly passed array with the buttons which this Toolbox should display.
*/
toolbarButtons?: Array<string>;
}
const useStyles = makeStyles()(() => {
return {
hangupMenu: {
position: 'relative',
right: 'auto',
display: 'flex',
flexDirection: 'column',
rowGap: '8px',
margin: 0,
padding: '16px',
marginBottom: '8px'
}
};
});
/**
* A component that renders the main toolbar.
*
* @param {IProps} props - The props of the component.
* @returns {ReactElement}
*/
export default function Toolbox({
toolbarButtons
}: IProps) {
const { classes, cx } = useStyles();
const { t } = useTranslation();
const dispatch = useDispatch();
const _toolboxRef = useRef<HTMLDivElement>(null);
const conference = useSelector((state: IReduxState) => state['features/base/conference'].conference);
const isNarrowLayout = useSelector((state: IReduxState) => state['features/base/responsive-ui'].isNarrowLayout);
const videoSpaceWidth = useSelector((state: IReduxState) => state['features/base/responsive-ui'].videoSpaceWidth);
const isModerator = useSelector(isLocalParticipantModerator);
const customToolbarButtons = useSelector(
(state: IReduxState) => state['features/base/config'].customToolbarButtons);
const iAmRecorder = useSelector((state: IReduxState) => state['features/base/config'].iAmRecorder);
const iAmSipGateway = useSelector((state: IReduxState) => state['features/base/config'].iAmSipGateway);
const overflowDrawer = useSelector((state: IReduxState) => state['features/toolbox'].overflowDrawer);
const shiftUp = useSelector((state: IReduxState) => state['features/toolbox'].shiftUp);
const overflowMenuVisible = useSelector((state: IReduxState) => state['features/toolbox'].overflowMenuVisible);
const hangupMenuVisible = useSelector((state: IReduxState) => state['features/toolbox'].hangupMenuVisible);
const buttonsWithNotifyClick
= useSelector((state: IReduxState) => state['features/toolbox'].buttonsWithNotifyClick);
const reduxToolbarButtons = useSelector((state: IReduxState) => state['features/toolbox'].toolbarButtons);
const toolbarButtonsToUse = toolbarButtons || reduxToolbarButtons;
const isDialogVisible = useSelector((state: IReduxState) => Boolean(state['features/base/dialog'].component));
const localParticipant = useSelector(getLocalParticipant);
const transcribing = useSelector(isTranscribing);
const _isCCTabEnabled = useSelector(isCCTabEnabled);
// Do not convert to selector, it returns new array and will cause re-rendering of toolbox on every action.
const jwtDisabledButtons = getJwtDisabledButtons(transcribing, _isCCTabEnabled, localParticipant?.features);
const reactionsButtonEnabled = useSelector(isReactionsButtonEnabled);
const _shouldDisplayReactionsButtons = useSelector(shouldDisplayReactionsButtons);
const toolbarVisible = useSelector(isToolboxVisible);
const mainToolbarButtonsThresholds
= useSelector((state: IReduxState) => state['features/toolbox'].mainToolbarButtonsThresholds);
const allButtons = useToolboxButtons(customToolbarButtons);
const isMobile = isMobileBrowser();
const endConferenceSupported = Boolean(conference?.isEndConferenceSupported() && isModerator);
useKeyboardShortcuts(toolbarButtonsToUse);
useEffect(() => {
if (!toolbarVisible) {
if (document.activeElement instanceof HTMLElement
&& _toolboxRef.current?.contains(document.activeElement)) {
document.activeElement.blur();
}
}
}, [ toolbarVisible ]);
/**
* Sets the visibility of the hangup menu.
*
* @param {boolean} visible - Whether or not the hangup menu should be
* displayed.
* @private
* @returns {void}
*/
const onSetHangupVisible = useCallback((visible: boolean) => {
dispatch(setHangupMenuVisible(visible));
dispatch(setToolbarHovered(visible));
}, [ dispatch ]);
/**
* Sets the visibility of the overflow menu.
*
* @param {boolean} visible - Whether or not the overflow menu should be
* displayed.
* @private
* @returns {void}
*/
const onSetOverflowVisible = useCallback((visible: boolean) => {
dispatch(setOverflowMenuVisible(visible));
dispatch(setToolbarHovered(visible));
}, [ dispatch ]);
useEffect(() => {
// On mobile web we want to keep both toolbox and hang up menu visible
// because they depend on each other.
if (endConferenceSupported && isMobile) {
hangupMenuVisible && dispatch(setToolboxVisible(true));
} else if (hangupMenuVisible && !toolbarVisible) {
onSetHangupVisible(false);
dispatch(setToolbarHovered(false));
}
}, [ dispatch, hangupMenuVisible, toolbarVisible, onSetHangupVisible ]);
useEffect(() => {
if (overflowMenuVisible && isDialogVisible) {
onSetOverflowVisible(false);
dispatch(setToolbarHovered(false));
}
}, [ dispatch, overflowMenuVisible, isDialogVisible, onSetOverflowVisible ]);
/**
* Key handler for overflow/hangup menus.
*
* @param {KeyboardEvent} e - Esc key click to close the popup.
* @returns {void}
*/
const onEscKey = useCallback((e?: React.KeyboardEvent) => {
if (e?.key === 'Escape') {
e?.stopPropagation();
hangupMenuVisible && dispatch(setHangupMenuVisible(false));
overflowMenuVisible && dispatch(setOverflowMenuVisible(false));
}
}, [ dispatch, hangupMenuVisible, overflowMenuVisible ]);
/**
* Dispatches an action signaling the toolbar is not being hovered.
*
* @private
* @returns {void}
*/
const onMouseOut = useCallback(() => {
!overflowMenuVisible && dispatch(setToolbarHovered(false));
}, [ dispatch, overflowMenuVisible ]);
/**
* Dispatches an action signaling the toolbar is being hovered.
*
* @private
* @returns {void}
*/
const onMouseOver = useCallback(() => {
dispatch(setToolbarHovered(true));
}, [ dispatch ]);
/**
* Handle focus on the toolbar.
*
* @returns {void}
*/
const handleFocus = useCallback(() => {
dispatch(setToolboxVisible(true));
}, [ dispatch ]);
/**
* Handle blur the toolbar..
*
* @returns {void}
*/
const handleBlur = useCallback(() => {
dispatch(setToolboxVisible(false));
}, [ dispatch ]);
if (iAmRecorder || iAmSipGateway) {
return null;
}
const rootClassNames = `new-toolbox ${toolbarVisible ? 'visible' : ''} ${
toolbarButtonsToUse.length ? '' : 'no-buttons'}`;
const toolbarAccLabel = 'toolbar.accessibilityLabel.moreActionsMenu';
const containerClassName = `toolbox-content${isMobile || isNarrowLayout ? ' toolbox-content-mobile' : ''}`;
const { mainMenuButtons, overflowMenuButtons } = getVisibleButtons({
allButtons,
buttonsWithNotifyClick,
toolbarButtons: toolbarButtonsToUse,
clientWidth: videoSpaceWidth,
jwtDisabledButtons,
mainToolbarButtonsThresholds
});
const raiseHandInOverflowMenu = overflowMenuButtons.some(({ key }) => key === 'raisehand');
const showReactionsInOverflowMenu = _shouldDisplayReactionsButtons
&& (
(!reactionsButtonEnabled && (raiseHandInOverflowMenu || isNarrowLayout || isMobile))
|| overflowMenuButtons.some(({ key }) => key === 'reactions'));
const showRaiseHandInReactionsMenu = showReactionsInOverflowMenu && raiseHandInOverflowMenu;
return (
<div
className = { cx(rootClassNames, shiftUp && 'shift-up') }
id = 'new-toolbox'>
<div className = { containerClassName }>
<div
className = 'toolbox-content-wrapper'
onBlur = { handleBlur }
onFocus = { handleFocus }
{ ...(isMobile ? {} : {
onMouseOut,
onMouseOver
}) }>
<div
className = 'toolbox-content-items'
ref = { _toolboxRef }>
{mainMenuButtons.map(({ Content, key, ...rest }) => Content !== Separator && (
<Content
{ ...rest }
buttonKey = { key }
key = { key } />))}
{Boolean(overflowMenuButtons.length) && (
<OverflowMenuButton
ariaControls = 'overflow-menu'
buttons = { overflowMenuButtons.reduce<Array<IToolboxButton[]>>((acc, val) => {
if (val.key === 'reactions' && showReactionsInOverflowMenu) {
return acc;
}
if (val.key === 'raisehand' && showRaiseHandInReactionsMenu) {
return acc;
}
if (acc.length) {
const prev = acc[acc.length - 1];
const group = prev[prev.length - 1].group;
if (group === val.group) {
prev.push(val);
} else {
acc.push([ val ]);
}
} else {
acc.push([ val ]);
}
return acc;
}, []) }
isOpen = { overflowMenuVisible }
key = 'overflow-menu'
onToolboxEscKey = { onEscKey }
onVisibilityChange = { onSetOverflowVisible }
showRaiseHandInReactionsMenu = { showRaiseHandInReactionsMenu }
showReactionsMenu = { showReactionsInOverflowMenu } />
)}
{isButtonEnabled('hangup', toolbarButtonsToUse) && (
endConferenceSupported
? <HangupMenuButton
ariaControls = 'hangup-menu'
isOpen = { hangupMenuVisible }
key = 'hangup-menu'
notifyMode = { buttonsWithNotifyClick?.get('hangup-menu') }
onVisibilityChange = { onSetHangupVisible }>
<ContextMenu
accessibilityLabel = { t(toolbarAccLabel) }
className = { classes.hangupMenu }
hidden = { false }
inDrawer = { overflowDrawer }
onKeyDown = { onEscKey }>
<EndConferenceButton
buttonKey = 'end-meeting'
notifyMode = { buttonsWithNotifyClick?.get('end-meeting') } />
<LeaveConferenceButton
buttonKey = 'hangup'
notifyMode = { buttonsWithNotifyClick?.get('hangup') } />
</ContextMenu>
</HangupMenuButton>
: <HangupButton
buttonKey = 'hangup'
customClass = 'hangup-button'
key = 'hangup-button'
notifyMode = { buttonsWithNotifyClick.get('hangup') }
visible = { isButtonEnabled('hangup', toolbarButtonsToUse) } />
)}
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,203 @@
import React, { ReactElement } from 'react';
import { connect } from 'react-redux';
import { withStyles } from 'tss-react/mui';
import { ACTION_SHORTCUT_TRIGGERED, VIDEO_MUTE, createShortcutEvent } from '../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../analytics/functions';
import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import { IGUMPendingState } from '../../../base/media/types';
import AbstractButton from '../../../base/toolbox/components/AbstractButton';
import Spinner from '../../../base/ui/components/web/Spinner';
import { registerShortcut, unregisterShortcut } from '../../../keyboard-shortcuts/actions';
import { SPINNER_COLOR } from '../../constants';
import AbstractVideoMuteButton, {
IProps as AbstractVideoMuteButtonProps,
mapStateToProps as abstractMapStateToProps
} from '../AbstractVideoMuteButton';
const styles = () => {
return {
pendingContainer: {
position: 'absolute' as const,
bottom: '3px',
right: '3px'
}
};
};
/**
* The type of the React {@code Component} props of {@link VideoMuteButton}.
*/
export interface IProps extends AbstractVideoMuteButtonProps {
/**
* The gumPending state from redux.
*/
_gumPending: IGUMPendingState;
/**
* An object containing the CSS classes.
*/
classes?: Partial<Record<keyof ReturnType<typeof styles>, string>>;
}
/**
* Component that renders a toolbar button for toggling video mute.
*
* @augments AbstractVideoMuteButton
*/
class VideoMuteButton extends AbstractVideoMuteButton<IProps> {
/**
* Initializes a new {@code VideoMuteButton} instance.
*
* @param {IProps} props - The read-only React {@code Component} props with
* which the new instance is to be initialized.
*/
constructor(props: IProps) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._onKeyboardShortcut = this._onKeyboardShortcut.bind(this);
this._getTooltip = this._getLabel;
}
/**
* Registers the keyboard shortcut that toggles the video muting.
*
* @inheritdoc
* @returns {void}
*/
override componentDidMount() {
this.props.dispatch(registerShortcut({
character: 'V',
helpDescription: 'keyboardShortcuts.videoMute',
handler: this._onKeyboardShortcut
}));
}
/**
* Unregisters the keyboard shortcut that toggles the video muting.
*
* @inheritdoc
* @returns {void}
*/
override componentWillUnmount() {
this.props.dispatch(unregisterShortcut('V'));
}
/**
* Gets the current accessibility label, taking the toggled and GUM pending state into account. If no toggled label
* is provided, the regular accessibility label will also be used in the toggled state.
*
* The accessibility label is not visible in the UI, it is meant to be used by assistive technologies, mainly screen
* readers.
*
* @private
* @returns {string}
*/
override _getAccessibilityLabel() {
const { _gumPending } = this.props;
if (_gumPending === IGUMPendingState.NONE) {
return super._getAccessibilityLabel();
}
return 'toolbar.accessibilityLabel.videomuteGUMPending';
}
/**
* Gets the current label, taking the toggled and GUM pending state into account. If no
* toggled label is provided, the regular label will also be used in the toggled state.
*
* @private
* @returns {string}
*/
override _getLabel() {
const { _gumPending } = this.props;
if (_gumPending === IGUMPendingState.NONE) {
return super._getLabel();
}
return 'toolbar.videomuteGUMPending';
}
/**
* Indicates if video is currently muted or not.
*
* @override
* @protected
* @returns {boolean}
*/
override _isVideoMuted() {
if (this.props._gumPending === IGUMPendingState.PENDING_UNMUTE) {
return false;
}
return super._isVideoMuted();
}
/**
* Returns a spinner if there is pending GUM.
*
* @returns {ReactElement | null}
*/
override _getElementAfter(): ReactElement | null {
const { _gumPending } = this.props;
const classes = withStyles.getClasses(this.props);
return _gumPending === IGUMPendingState.NONE ? null
: (
<div className = { classes.pendingContainer }>
<Spinner
color = { SPINNER_COLOR }
size = 'small' />
</div>
);
}
/**
* Creates an analytics keyboard shortcut event and dispatches an action to
* toggle the video muting.
*
* @private
* @returns {void}
*/
_onKeyboardShortcut() {
// Ignore keyboard shortcuts if the video button is disabled.
if (this._isDisabled()) {
return;
}
sendAnalytics(
createShortcutEvent(
VIDEO_MUTE,
ACTION_SHORTCUT_TRIGGERED,
{ enable: !this._isVideoMuted() }));
AbstractButton.prototype._onClick.call(this);
}
}
/**
* Maps (parts of) the redux state to the associated props for the
* {@code VideoMuteButton} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _videoMuted: boolean
* }}
*/
function _mapStateToProps(state: IReduxState) {
const { gumPending } = state['features/base/media'].video;
return {
...abstractMapStateToProps(state),
_gumPending: gumPending
};
}
export default withStyles(translate(connect(_mapStateToProps)(VideoMuteButton)), styles);

View File

@@ -0,0 +1,197 @@
import React, { Component } from 'react';
import { WithTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { isMobileBrowser } from '../../../base/environment/utils';
import { translate } from '../../../base/i18n/functions';
import { IconArrowUp } from '../../../base/icons/svg';
import { IGUMPendingState } from '../../../base/media/types';
import ToolboxButtonWithIcon from '../../../base/toolbox/components/web/ToolboxButtonWithIcon';
import { getLocalJitsiVideoTrack } from '../../../base/tracks/functions.web';
import { toggleVideoSettings } from '../../../settings/actions';
import VideoSettingsPopup from '../../../settings/components/web/video/VideoSettingsPopup';
import { getVideoSettingsVisibility } from '../../../settings/functions.web';
import { isVideoSettingsButtonDisabled } from '../../functions.web';
import VideoMuteButton from './VideoMuteButton';
interface IProps extends WithTranslation {
/**
* The button's key.
*/
buttonKey?: string;
/**
* The gumPending state from redux.
*/
gumPending: IGUMPendingState;
/**
* External handler for click action.
*/
handleClick: Function;
/**
* Indicates whether video permissions have been granted or denied.
*/
hasPermissions: boolean;
/**
* Whether there is a video track or not.
*/
hasVideoTrack: boolean;
/**
* If the button should be disabled.
*/
isDisabled: boolean;
/**
* Defines is popup is open.
*/
isOpen: boolean;
/**
* Notify mode for `toolbarButtonClicked` event -
* whether to only notify or to also prevent button click routine.
*/
notifyMode?: string;
/**
* Click handler for the small icon. Opens video options.
*/
onVideoOptionsClick: Function;
/**
* Flag controlling the visibility of the button.
* VideoSettings popup is currently disabled on mobile browsers
* as mobile devices do not support capture of more than one
* camera at a time.
*/
visible: boolean;
}
/**
* Button used for video & video settings.
*
* @returns {ReactElement}
*/
class VideoSettingsButton extends Component<IProps> {
/**
* Initializes a new {@code VideoSettingsButton} instance.
*
* @inheritdoc
*/
constructor(props: IProps) {
super(props);
this._onEscClick = this._onEscClick.bind(this);
this._onClick = this._onClick.bind(this);
}
/**
* Returns true if the settings icon is disabled.
*
* @returns {boolean}
*/
_isIconDisabled() {
const { gumPending, hasPermissions, hasVideoTrack, isDisabled } = this.props;
return ((!hasPermissions || isDisabled) && !hasVideoTrack) || gumPending !== IGUMPendingState.NONE;
}
/**
* Click handler for the more actions entries.
*
* @param {KeyboardEvent} event - Esc key click to close the popup.
* @returns {void}
*/
_onEscClick(event: React.KeyboardEvent) {
if (event.key === 'Escape' && this.props.isOpen) {
event.preventDefault();
event.stopPropagation();
this._onClick();
}
}
/**
* Click handler for the more actions entries.
*
* @param {MouseEvent} e - Mousw event.
* @returns {void}
*/
_onClick(e?: React.MouseEvent) {
const { onVideoOptionsClick, isOpen } = this.props;
if (isOpen) {
e?.stopPropagation();
}
onVideoOptionsClick();
}
/**
* Implements React's {@link Component#render}.
*
* @inheritdoc
*/
override render() {
const { gumPending, t, visible, isOpen, buttonKey, notifyMode } = this.props;
return visible ? (
<VideoSettingsPopup>
<ToolboxButtonWithIcon
ariaControls = 'video-settings-dialog'
ariaExpanded = { isOpen }
ariaHasPopup = { true }
ariaLabel = { this.props.t('toolbar.videoSettings') }
buttonKey = { buttonKey }
icon = { IconArrowUp }
iconDisabled = { this._isIconDisabled() || gumPending !== IGUMPendingState.NONE }
iconId = 'video-settings-button'
iconTooltip = { t('toolbar.videoSettings') }
notifyMode = { notifyMode }
onIconClick = { this._onClick }
onIconKeyDown = { this._onEscClick }>
<VideoMuteButton
buttonKey = { buttonKey }
notifyMode = { notifyMode } />
</ToolboxButtonWithIcon>
</VideoSettingsPopup>
) : <VideoMuteButton
buttonKey = { buttonKey }
notifyMode = { notifyMode } />;
}
}
/**
* Function that maps parts of Redux state tree into component props.
*
* @param {Object} state - Redux state.
* @returns {Object}
*/
function mapStateToProps(state: IReduxState) {
const { permissions = { video: false } } = state['features/base/devices'];
const { isNarrowLayout } = state['features/base/responsive-ui'];
const { gumPending } = state['features/base/media'].video;
return {
gumPending,
hasPermissions: permissions.video,
hasVideoTrack: Boolean(getLocalJitsiVideoTrack(state)),
isDisabled: isVideoSettingsButtonDisabled(state),
isOpen: Boolean(getVideoSettingsVisibility(state)),
visible: !isMobileBrowser() && !isNarrowLayout
};
}
const mapDispatchToProps = {
onVideoOptionsClick: toggleVideoSettings
};
export default translate(connect(
mapStateToProps,
mapDispatchToProps
)(VideoSettingsButton));

View File

@@ -0,0 +1,214 @@
import { NativeToolbarButton, ToolbarButton } from './types';
/**
* Dummy toolbar threschold value for 9 buttons. It is used as a placeholder in THRESHOLDS that would work only when
* this value is overiden.
*/
export const DUMMY_9_BUTTONS_THRESHOLD_VALUE = Symbol('9_BUTTONS_THRESHOLD_VALUE');
/**
* Dummy toolbar threschold value for 10 buttons. It is used as a placeholder in THRESHOLDS that would work only when
* this value is overiden.
*/
export const DUMMY_10_BUTTONS_THRESHOLD_VALUE = Symbol('10_BUTTONS_THRESHOLD_VALUE');
/**
* Thresholds for displaying toolbox buttons.
*/
export const THRESHOLDS = [
// This entry won't be used unless the order is overridden trough the mainToolbarButtons config prop.
{
width: 675,
order: DUMMY_10_BUTTONS_THRESHOLD_VALUE
},
// This entry won't be used unless the order is overridden trough the mainToolbarButtons config prop.
{
width: 625,
order: DUMMY_9_BUTTONS_THRESHOLD_VALUE
},
{
width: 565,
order: [ 'microphone', 'camera', 'desktop', 'chat', 'raisehand', 'reactions', 'participants-pane', 'tileview' ]
},
{
width: 520,
order: [ 'microphone', 'camera', 'desktop', 'chat', 'raisehand', 'participants-pane', 'tileview' ]
},
{
width: 470,
order: [ 'microphone', 'camera', 'desktop', 'chat', 'raisehand', 'participants-pane' ]
},
{
width: 420,
order: [ 'microphone', 'camera', 'desktop', 'chat', 'participants-pane' ]
},
{
width: 370,
order: [ 'microphone', 'camera', 'chat', 'participants-pane' ]
},
{
width: 225,
order: [ 'microphone', 'camera', 'chat' ]
},
{
width: 200,
order: [ 'microphone', 'camera' ]
}
];
/**
* Thresholds for displaying native toolbox buttons.
*/
export const NATIVE_THRESHOLDS = [
{
width: 560,
order: [ 'microphone', 'camera', 'chat', 'desktop', 'raisehand', 'tileview', 'overflowmenu', 'hangup' ]
},
{
width: 500,
order: [ 'microphone', 'camera', 'chat', 'raisehand', 'tileview', 'overflowmenu', 'hangup' ]
},
{
width: 440,
order: [ 'microphone', 'camera', 'chat', 'raisehand', 'overflowmenu', 'hangup' ]
},
{
width: 380,
order: [ 'microphone', 'camera', 'chat', 'overflowmenu', 'hangup' ]
},
{
width: 320,
order: [ 'microphone', 'camera', 'overflowmenu', 'hangup' ]
}
];
/**
* Main toolbar buttons priority used to determine which button should be picked to fill empty spaces for disabled
* buttons.
*/
export const MAIN_TOOLBAR_BUTTONS_PRIORITY = [
'microphone',
'camera',
'desktop',
'chat',
'raisehand',
'reactions',
'participants-pane',
'tileview',
'overflowmenu',
'hangup',
'invite',
'toggle-camera',
'videoquality',
'fullscreen',
'security',
'closedcaptions',
'recording',
'livestreaming',
'linktosalesforce',
'sharedvideo',
'shareaudio',
'noisesuppression',
'whiteboard',
'etherpad',
'select-background',
'stats',
'settings',
'shortcuts',
'profile',
'embedmeeting',
'feedback',
'download',
'help'
];
export const TOOLBAR_TIMEOUT = 4000;
export const DRAWER_MAX_HEIGHT = '80dvh - 64px';
// Around 300 to be displayed above components like chat
export const ZINDEX_DIALOG_PORTAL = 302;
/**
* Color for spinner displayed in the toolbar.
*/
export const SPINNER_COLOR = '#929292';
/**
* The list of all possible UI buttons.
*
* @protected
* @type Array<string>
*/
export const TOOLBAR_BUTTONS: ToolbarButton[] = [
'camera',
'chat',
'closedcaptions',
'desktop',
'download',
'embedmeeting',
'etherpad',
'feedback',
'filmstrip',
'fullscreen',
'hangup',
'help',
'highlight',
'invite',
'linktosalesforce',
'livestreaming',
'microphone',
'mute-everyone',
'mute-video-everyone',
'participants-pane',
'profile',
'raisehand',
'recording',
'security',
'select-background',
'settings',
'shareaudio',
'noisesuppression',
'sharedvideo',
'shortcuts',
'stats',
'tileview',
'toggle-camera',
'videoquality',
'whiteboard'
];
/**
* The list of all possible native buttons.
*
* @protected
* @type Array<string>
*/
export const NATIVE_TOOLBAR_BUTTONS: NativeToolbarButton[] = [
'camera',
'chat',
'hangup',
'microphone',
'overflowmenu',
'raisehand',
'desktop',
'tileview'
];
/**
* The toolbar buttons to show when in visitors mode.
*/
export const VISITORS_MODE_BUTTONS: ToolbarButton[] = [
'chat',
'closedcaptions',
'fullscreen',
'hangup',
'participants-pane',
'raisehand',
'settings',
'stats',
'tileview',
'videoquality'
];

View File

@@ -0,0 +1,95 @@
import { IReduxState } from '../app/types';
import { IStateful } from '../base/app/types';
import { isJwtFeatureEnabledStateless } from '../base/jwt/functions';
import { IGUMPendingState } from '../base/media/types';
import { IParticipantFeatures } from '../base/participants/types';
import { toState } from '../base/redux/functions';
import { iAmVisitor } from '../visitors/functions';
import { VISITORS_MODE_BUTTONS } from './constants';
/**
* Indicates if the audio mute button is disabled or not.
*
* @param {IReduxState} state - The state from the Redux store.
* @returns {boolean}
*/
export function isAudioMuteButtonDisabled(state: IReduxState) {
const { available, muted, unmuteBlocked, gumPending } = state['features/base/media'].audio;
const { startSilent } = state['features/base/config'];
return Boolean(!available || startSilent || (muted && unmuteBlocked) || gumPending !== IGUMPendingState.NONE
|| iAmVisitor(state));
}
/**
* Returns the buttons corresponding to features disabled through jwt.
* This function is stateless as it returns a new array and may cause re-rendering.
*
* @param {boolean} isTranscribing - Whether there is currently a transcriber in the meeting.
* @param {boolean} isCCTabEnabled - Whether the closed captions tab is enabled.
* @param {ILocalParticipant} localParticipantFeatures - The features of the local participant.
* @returns {string[]} - The disabled by jwt buttons array.
*/
export function getJwtDisabledButtons(
isTranscribing: boolean,
isCCTabEnabled: boolean,
localParticipantFeatures?: IParticipantFeatures) {
const acc = [];
if (!isJwtFeatureEnabledStateless({
localParticipantFeatures,
feature: 'livestreaming',
ifNotInFeatures: false
})) {
acc.push('livestreaming');
}
if (!isTranscribing && !isCCTabEnabled && !isJwtFeatureEnabledStateless({
localParticipantFeatures,
feature: 'transcription',
ifNotInFeatures: false
})) {
acc.push('closedcaptions');
}
return acc;
}
/**
* Returns the list of enabled toolbar buttons.
*
* @param {Object|Function} stateful - Either the whole Redux state object or the Redux store's {@code getState} method.
* @param {string[]} definedToolbarButtons - The list of all possible buttons.
*
* @returns {Array<string>} - The list of enabled toolbar buttons.
*/
export function getToolbarButtons(stateful: IStateful, definedToolbarButtons: string[]): Array<string> {
const state = toState(stateful);
const { toolbarButtons, customToolbarButtons } = state['features/base/config'];
const customButtons = customToolbarButtons?.map(({ id }) => id);
let buttons = Array.isArray(toolbarButtons) ? toolbarButtons : definedToolbarButtons;
if (iAmVisitor(state)) {
buttons = VISITORS_MODE_BUTTONS.filter(button => buttons.indexOf(button) > -1);
}
if (customButtons) {
return [ ...buttons, ...customButtons ];
}
return buttons;
}
/**
* Checks if the specified button is enabled.
*
* @param {string} buttonName - The name of the button. See {@link interfaceConfig}.
* @param {Object|Array<string>} state - The redux state or the array with the enabled buttons.
* @returns {boolean} - True if the button is enabled and false otherwise.
*/
export function isButtonEnabled(buttonName: string, state: IReduxState | Array<string>) {
const buttons = Array.isArray(state) ? state : state['features/toolbox'].toolbarButtons || [];
return buttons.includes(buttonName);
}

View File

@@ -0,0 +1,125 @@
import { IReduxState } from '../app/types';
import { IStateful } from '../base/app/types';
import { hasAvailableDevices } from '../base/devices/functions.native';
import { TOOLBOX_ALWAYS_VISIBLE, TOOLBOX_ENABLED } from '../base/flags/constants';
import { getFeatureFlag } from '../base/flags/functions';
import { getParticipantCountWithFake } from '../base/participants/functions';
import { toState } from '../base/redux/functions';
import { isLocalVideoTrackDesktop } from '../base/tracks/functions.native';
import { MAIN_TOOLBAR_BUTTONS_PRIORITY, VISITORS_MODE_BUTTONS } from './constants';
import { isButtonEnabled } from './functions.any';
import { IGetVisibleNativeButtonsParams, IToolboxNativeButton } from './types';
export * from './functions.any';
/**
* Indicates if the desktop share button is disabled or not.
*
* @param {IReduxState} state - The state from the Redux store.
* @returns {boolean}
*/
export function isDesktopShareButtonDisabled(state: IReduxState) {
const { muted, unmuteBlocked } = state['features/base/media'].video;
const videoOrShareInProgress = !muted || isLocalVideoTrackDesktop(state);
return unmuteBlocked && !videoOrShareInProgress;
}
/**
* Returns true if the toolbox is visible.
*
* @param {IStateful} stateful - A function or object that can be
* resolved to Redux state by the function {@code toState}.
* @returns {boolean}
*/
export function isToolboxVisible(stateful: IStateful) {
const state = toState(stateful);
const { toolbarConfig } = state['features/base/config'];
const { alwaysVisible } = toolbarConfig || {};
const { enabled, visible } = state['features/toolbox'];
const participantCount = getParticipantCountWithFake(state);
const alwaysVisibleFlag = getFeatureFlag(state, TOOLBOX_ALWAYS_VISIBLE, false);
const enabledFlag = getFeatureFlag(state, TOOLBOX_ENABLED, true);
return enabledFlag && enabled
&& (alwaysVisible || visible || participantCount === 1 || alwaysVisibleFlag);
}
/**
* Indicates if the video mute button is disabled or not.
*
* @param {IReduxState} state - The state from the Redux store.
* @returns {boolean}
*/
export function isVideoMuteButtonDisabled(state: IReduxState) {
const { muted, unmuteBlocked } = state['features/base/media'].video;
return !hasAvailableDevices(state, 'videoInput')
|| (unmuteBlocked && Boolean(muted));
}
/**
* Returns all buttons that need to be rendered.
*
* @param {IGetVisibleButtonsParams} params - The parameters needed to extract the visible buttons.
* @returns {Object} - The visible buttons arrays .
*/
export function getVisibleNativeButtons(
{ allButtons, clientWidth, iAmVisitor, mainToolbarButtonsThresholds, toolbarButtons }: IGetVisibleNativeButtonsParams) {
let filteredButtons = Object.keys(allButtons).filter(key =>
typeof key !== 'undefined' // filter invalid buttons that may be coming from config.mainToolbarButtons override
&& isButtonEnabled(key, toolbarButtons));
if (iAmVisitor) {
filteredButtons = VISITORS_MODE_BUTTONS.filter(button => filteredButtons.indexOf(button) > -1);
}
const { order } = mainToolbarButtonsThresholds.find(({ width }) => clientWidth > width)
|| mainToolbarButtonsThresholds[mainToolbarButtonsThresholds.length - 1];
const mainToolbarButtonKeysOrder = [
...order.filter(key => filteredButtons.includes(key)),
...MAIN_TOOLBAR_BUTTONS_PRIORITY.filter(key => !order.includes(key) && filteredButtons.includes(key)),
...filteredButtons.filter(key => !order.includes(key) && !MAIN_TOOLBAR_BUTTONS_PRIORITY.includes(key))
];
const mainButtonsKeys = mainToolbarButtonKeysOrder.slice(0, order.length);
const overflowMenuButtons = filteredButtons.reduce((acc, key) => {
if (!mainButtonsKeys.includes(key)) {
acc.push(allButtons[key]);
}
return acc;
}, [] as IToolboxNativeButton[]);
// if we have 1 button in the overflow menu it is better to directly display it in the main toolbar by replacing
// the "More" menu button with it.
if (overflowMenuButtons.length === 1) {
const button = overflowMenuButtons.shift()?.key;
button && mainButtonsKeys.push(button);
}
const mainMenuButtons
= mainButtonsKeys.map(key => allButtons[key]).sort((a, b) => {
// Native toolbox includes hangup and overflowmenu button keys, too
// hangup goes last, overflowmenu goes second-to-last
if (a.key === 'hangup' || a.key === 'overflowmenu') {
return 1;
}
if (b.key === 'hangup' || b.key === 'overflowmenu') {
return -1;
}
return 0; // other buttons are sorted by priority
});
return {
mainMenuButtons,
overflowMenuButtons
};
}

View File

@@ -0,0 +1,251 @@
import { IReduxState } from '../app/types';
import { hasAvailableDevices } from '../base/devices/functions.web';
import { MEET_FEATURES } from '../base/jwt/constants';
import { isJwtFeatureEnabled } from '../base/jwt/functions';
import { IGUMPendingState } from '../base/media/types';
import { isScreenMediaShared } from '../screen-share/functions';
import { isWhiteboardVisible } from '../whiteboard/functions';
import { MAIN_TOOLBAR_BUTTONS_PRIORITY, TOOLBAR_TIMEOUT } from './constants';
import { isButtonEnabled } from './functions.any';
import { IGetVisibleButtonsParams, IToolboxButton, NOTIFY_CLICK_MODE } from './types';
export * from './functions.any';
/**
* Helper for getting the height of the toolbox.
*
* @returns {number} The height of the toolbox.
*/
export function getToolboxHeight() {
const toolbox = document.getElementById('new-toolbox');
return toolbox?.clientHeight || 0;
}
/**
* Indicates if the toolbox is visible or not.
*
* @param {IReduxState} state - The state from the Redux store.
* @returns {boolean} - True to indicate that the toolbox is visible, false -
* otherwise.
*/
export function isToolboxVisible(state: IReduxState) {
const { iAmRecorder, iAmSipGateway, toolbarConfig } = state['features/base/config'];
const { alwaysVisible } = toolbarConfig || {};
const {
timeoutID,
visible
} = state['features/toolbox'];
const { audioSettingsVisible, videoSettingsVisible } = state['features/settings'];
const whiteboardVisible = isWhiteboardVisible(state);
return Boolean(!iAmRecorder && !iAmSipGateway
&& (
timeoutID
|| visible
|| alwaysVisible
|| audioSettingsVisible
|| videoSettingsVisible
|| whiteboardVisible
));
}
/**
* Indicates if the audio settings button is disabled or not.
*
* @param {IReduxState} state - The state from the Redux store.
* @returns {boolean}
*/
export function isAudioSettingsButtonDisabled(state: IReduxState) {
return !(hasAvailableDevices(state, 'audioInput')
|| hasAvailableDevices(state, 'audioOutput'))
|| state['features/base/config'].startSilent;
}
/**
* Indicates if the desktop share button is disabled or not.
*
* @param {IReduxState} state - The state from the Redux store.
* @returns {boolean}
*/
export function isDesktopShareButtonDisabled(state: IReduxState) {
const { muted, unmuteBlocked } = state['features/base/media'].video;
const videoOrShareInProgress = !muted || isScreenMediaShared(state);
const enabledInJwt = isJwtFeatureEnabled(state, MEET_FEATURES.SCREEN_SHARING, true);
return !enabledInJwt || (unmuteBlocked && !videoOrShareInProgress);
}
/**
* Indicates if the video settings button is disabled or not.
*
* @param {IReduxState} state - The state from the Redux store.
* @returns {boolean}
*/
export function isVideoSettingsButtonDisabled(state: IReduxState) {
return !hasAvailableDevices(state, 'videoInput');
}
/**
* Indicates if the video mute button is disabled or not.
*
* @param {IReduxState} state - The state from the Redux store.
* @returns {boolean}
*/
export function isVideoMuteButtonDisabled(state: IReduxState) {
const { muted, unmuteBlocked, gumPending } = state['features/base/media'].video;
return !hasAvailableDevices(state, 'videoInput')
|| (unmuteBlocked && Boolean(muted))
|| gumPending !== IGUMPendingState.NONE;
}
/**
* If an overflow drawer should be displayed or not.
* This is usually done for mobile devices or on narrow screens.
*
* @param {IReduxState} state - The state from the Redux store.
* @returns {boolean}
*/
export function showOverflowDrawer(state: IReduxState) {
return state['features/toolbox'].overflowDrawer;
}
/**
* Returns the toolbar timeout from config or the default value.
*
* @param {IReduxState} state - The state from the Redux store.
* @returns {number} - Toolbar timeout in milliseconds.
*/
export function getToolbarTimeout(state: IReduxState) {
const { toolbarConfig } = state['features/base/config'];
return toolbarConfig?.timeout || TOOLBAR_TIMEOUT;
}
/**
* Sets the notify click mode for the buttons.
*
* @param {Object} buttons - The list of toolbar buttons.
* @param {Map} buttonsWithNotifyClick - The buttons notify click configuration.
* @returns {void}
*/
function setButtonsNotifyClickMode(buttons: Object, buttonsWithNotifyClick: Map<string, NOTIFY_CLICK_MODE>) {
if (typeof APP === 'undefined' || (buttonsWithNotifyClick?.size ?? 0) <= 0) {
return;
}
Object.values(buttons).forEach((button: any) => {
if (typeof button === 'object') {
button.notifyMode = buttonsWithNotifyClick.get(button.key);
}
});
}
/**
* Returns all buttons that need to be rendered.
*
* @param {IGetVisibleButtonsParams} params - The parameters needed to extract the visible buttons.
* @returns {Object} - The visible buttons arrays .
*/
export function getVisibleButtons({
allButtons,
buttonsWithNotifyClick,
toolbarButtons,
clientWidth,
jwtDisabledButtons,
mainToolbarButtonsThresholds
}: IGetVisibleButtonsParams) {
setButtonsNotifyClickMode(allButtons, buttonsWithNotifyClick);
const filteredButtons = Object.keys(allButtons).filter(key =>
typeof key !== 'undefined' // filter invalid buttons that may be coming from config.mainToolbarButtons
// override
&& !jwtDisabledButtons.includes(key)
&& isButtonEnabled(key, toolbarButtons));
const { order } = mainToolbarButtonsThresholds.find(({ width }) => clientWidth > width)
|| mainToolbarButtonsThresholds[mainToolbarButtonsThresholds.length - 1];
const mainToolbarButtonKeysOrder = [
...order.filter(key => filteredButtons.includes(key)),
...MAIN_TOOLBAR_BUTTONS_PRIORITY.filter(key => !order.includes(key) && filteredButtons.includes(key)),
...filteredButtons.filter(key => !order.includes(key) && !MAIN_TOOLBAR_BUTTONS_PRIORITY.includes(key))
];
const mainButtonsKeys = mainToolbarButtonKeysOrder.slice(0, order.length);
const overflowMenuButtons = filteredButtons.reduce((acc, key) => {
if (!mainButtonsKeys.includes(key)) {
acc.push(allButtons[key]);
}
return acc;
}, [] as IToolboxButton[]);
// if we have 1 button in the overflow menu it is better to directly display it in the main toolbar by replacing
// the "More" menu button with it.
if (overflowMenuButtons.length === 1) {
const button = overflowMenuButtons.shift()?.key;
button && mainButtonsKeys.push(button);
}
const mainMenuButtons = mainButtonsKeys.map(key => allButtons[key]);
return {
mainMenuButtons,
overflowMenuButtons
};
}
/**
* Returns the list of participant menu buttons that have that notify the api when clicked.
*
* @param {Object} state - The redux state.
* @returns {Map<string, NOTIFY_CLICK_MODE>} - The list of participant menu buttons.
*/
export function getParticipantMenuButtonsWithNotifyClick(state: IReduxState): Map<string, NOTIFY_CLICK_MODE> {
return state['features/toolbox'].participantMenuButtonsWithNotifyClick;
}
interface ICSSTransitionObject {
delay: number;
duration: number;
easingFunction: string;
}
/**
* Returns the time, timing function and delay for elements that are position above the toolbar and need to move along
* with the toolbar.
*
* @param {boolean} isToolbarVisible - Whether the toolbar is visible or not.
* @returns {ICSSTransitionObject}
*/
export function getTransitionParamsForElementsAboveToolbox(isToolbarVisible: boolean): ICSSTransitionObject {
// The transition time and delay is different to account for the time when the toolbar is about to hide/show but
// the elements don't have to move.
return isToolbarVisible ? {
duration: 0.15,
easingFunction: 'ease-in',
delay: 0.15
} : {
duration: 0.24,
easingFunction: 'ease-in',
delay: 0
};
}
/**
* Converts a given object to a css transition value string.
*
* @param {ICSSTransitionObject} object - The object.
* @returns {string}
*/
export function toCSSTransitionValue(object: ICSSTransitionObject) {
const { delay, duration, easingFunction } = object;
return `${duration}s ${easingFunction} ${delay}s`;
}

View File

@@ -0,0 +1,181 @@
import { useSelector } from 'react-redux';
import ChatButton from '../chat/components/native/ChatButton';
import RaiseHandContainerButtons from '../reactions/components/native/RaiseHandContainerButtons';
import TileViewButton from '../video-layout/components/TileViewButton';
import { iAmVisitor } from '../visitors/functions';
import AudioMuteButton from './components/native/AudioMuteButton';
import CustomOptionButton from './components/native/CustomOptionButton';
import HangupContainerButtons from './components/native/HangupContainerButtons';
import OverflowMenuButton from './components/native/OverflowMenuButton';
import ScreenSharingButton from './components/native/ScreenSharingButton';
import VideoMuteButton from './components/native/VideoMuteButton';
import { isDesktopShareButtonDisabled } from './functions.native';
import { ICustomToolbarButton, IToolboxNativeButton, NativeToolbarButton } from './types';
const microphone = {
key: 'microphone',
Content: AudioMuteButton,
group: 0
};
const camera = {
key: 'camera',
Content: VideoMuteButton,
group: 0
};
const chat = {
key: 'chat',
Content: ChatButton,
group: 1
};
const screensharing = {
key: 'desktop',
Content: ScreenSharingButton,
group: 1
};
const raisehand = {
key: 'raisehand',
Content: RaiseHandContainerButtons,
group: 2
};
const tileview = {
key: 'tileview',
Content: TileViewButton,
group: 2
};
const overflowmenu = {
key: 'overflowmenu',
Content: OverflowMenuButton,
group: 3
};
const hangup = {
key: 'hangup',
Content: HangupContainerButtons,
group: 3
};
/**
* A hook that returns the audio mute button.
*
* @returns {Object | undefined}
*/
function getAudioMuteButton() {
const _iAmVisitor = useSelector(iAmVisitor);
if (!_iAmVisitor) {
return microphone;
}
}
/**
* A hook that returns the video mute button.
*
* @returns {Object | undefined}
*/
function getVideoMuteButton() {
const _iAmVisitor = useSelector(iAmVisitor);
if (!_iAmVisitor) {
return camera;
}
}
/**
* A hook that returns the chat button.
*
* @returns {Object | undefined}
*/
function getChatButton() {
return chat;
}
/**
* A hook that returns the screen sharing button.
*
* @returns {Object | undefined}
*/
function getScreenSharingButton() {
const _iAmVisitor = useSelector(iAmVisitor);
const _isScreenShareButtonDisabled = useSelector(isDesktopShareButtonDisabled);
if (!_isScreenShareButtonDisabled && !_iAmVisitor) {
return screensharing;
}
}
/**
* A hook that returns the tile view button.
*
* @returns {Object | undefined}
*/
function getTileViewButton() {
return tileview;
}
/**
* A hook that returns the overflow menu button.
*
* @returns {Object | undefined}
*/
function getOverflowMenuButton() {
return overflowmenu;
}
/**
* Returns all buttons that could be rendered.
*
* @param {Object} _customToolbarButtons - An array containing custom buttons objects.
* @returns {Object} The button maps mainMenuButtons and overflowMenuButtons.
*/
export function useNativeToolboxButtons(
_customToolbarButtons?: ICustomToolbarButton[]): { [key: string]: IToolboxNativeButton; } {
const audioMuteButton = getAudioMuteButton();
const videoMuteButton = getVideoMuteButton();
const chatButton = getChatButton();
const screenSharingButton = getScreenSharingButton();
const tileViewButton = getTileViewButton();
const overflowMenuButton = getOverflowMenuButton();
const buttons: { [key in NativeToolbarButton]?: IToolboxNativeButton; } = {
microphone: audioMuteButton,
camera: videoMuteButton,
chat: chatButton,
desktop: screenSharingButton,
raisehand,
tileview: tileViewButton,
overflowmenu: overflowMenuButton,
hangup
};
const buttonKeys = Object.keys(buttons) as NativeToolbarButton[];
buttonKeys.forEach(
key => typeof buttons[key] === 'undefined' && delete buttons[key]);
const customButtons = _customToolbarButtons?.reduce((prev, { backgroundColor, icon, id, text }) => {
prev[id] = {
backgroundColor,
key: id,
id,
Content: CustomOptionButton,
group: 4,
icon,
text
};
return prev;
}, {} as { [key: string]: ICustomToolbarButton; });
return {
...buttons,
...customButtons
};
}

View File

@@ -0,0 +1,642 @@
import { useEffect } from 'react';
import { batch, useDispatch, useSelector } from 'react-redux';
import { ACTION_SHORTCUT_TRIGGERED, createShortcutEvent } from '../analytics/AnalyticsEvents';
import { sendAnalytics } from '../analytics/functions';
import { IReduxState } from '../app/types';
import { toggleDialog } from '../base/dialog/actions';
import { isIosMobileBrowser, isIpadMobileBrowser } from '../base/environment/utils';
import { HELP_BUTTON_ENABLED } from '../base/flags/constants';
import { getFeatureFlag } from '../base/flags/functions';
import JitsiMeetJS from '../base/lib-jitsi-meet';
import { raiseHand } from '../base/participants/actions';
import { getLocalParticipant, hasRaisedHand } from '../base/participants/functions';
import { isToggleCameraEnabled } from '../base/tracks/functions.web';
import { toggleChat } from '../chat/actions.web';
import ChatButton from '../chat/components/web/ChatButton';
import { useEmbedButton } from '../embed-meeting/hooks';
import { useEtherpadButton } from '../etherpad/hooks';
import { useFeedbackButton } from '../feedback/hooks.web';
import { setGifMenuVisibility } from '../gifs/actions';
import { isGifEnabled } from '../gifs/function.any';
import InviteButton from '../invite/components/add-people-dialog/web/InviteButton';
import { registerShortcut, unregisterShortcut } from '../keyboard-shortcuts/actions';
import { useKeyboardShortcutsButton } from '../keyboard-shortcuts/hooks';
import NoiseSuppressionButton from '../noise-suppression/components/NoiseSuppressionButton';
import {
close as closeParticipantsPane,
open as openParticipantsPane
} from '../participants-pane/actions.web';
import {
getParticipantsPaneOpen,
isParticipantsPaneEnabled
} from '../participants-pane/functions';
import { useParticipantPaneButton } from '../participants-pane/hooks.web';
import { addReactionToBuffer } from '../reactions/actions.any';
import { toggleReactionsMenuVisibility } from '../reactions/actions.web';
import RaiseHandContainerButton from '../reactions/components/web/RaiseHandContainerButtons';
import { REACTIONS } from '../reactions/constants';
import { shouldDisplayReactionsButtons } from '../reactions/functions.any';
import { useReactionsButton } from '../reactions/hooks.web';
import { useLiveStreamingButton, useRecordingButton } from '../recording/hooks.web';
import { isSalesforceEnabled } from '../salesforce/functions';
import { startScreenShareFlow } from '../screen-share/actions.web';
import ShareAudioButton from '../screen-share/components/web/ShareAudioButton';
import { isScreenAudioSupported, isScreenVideoShared } from '../screen-share/functions';
import { useSecurityDialogButton } from '../security/hooks.web';
import SettingsButton from '../settings/components/web/SettingsButton';
import { useSharedVideoButton } from '../shared-video/hooks';
import SpeakerStats from '../speaker-stats/components/web/SpeakerStats';
import { isSpeakerStatsDisabled } from '../speaker-stats/functions';
import { useSpeakerStatsButton } from '../speaker-stats/hooks.web';
import { useClosedCaptionButton } from '../subtitles/hooks.web';
import { toggleTileView } from '../video-layout/actions.any';
import { shouldDisplayTileView } from '../video-layout/functions.web';
import { useTileViewButton } from '../video-layout/hooks';
import VideoQualityButton from '../video-quality/components/VideoQualityButton.web';
import VideoQualityDialog from '../video-quality/components/VideoQualityDialog.web';
import { useVirtualBackgroundButton } from '../virtual-background/hooks';
import { useWhiteboardButton } from '../whiteboard/hooks';
import { setFullScreen } from './actions.web';
import DownloadButton from './components/DownloadButton';
import HelpButton from './components/HelpButton';
import AudioSettingsButton from './components/web/AudioSettingsButton';
import CustomOptionButton from './components/web/CustomOptionButton';
import FullscreenButton from './components/web/FullscreenButton';
import LinkToSalesforceButton from './components/web/LinkToSalesforceButton';
import ProfileButton from './components/web/ProfileButton';
import ShareDesktopButton from './components/web/ShareDesktopButton';
import ToggleCameraButton from './components/web/ToggleCameraButton';
import VideoSettingsButton from './components/web/VideoSettingsButton';
import { isButtonEnabled, isDesktopShareButtonDisabled } from './functions.web';
import { ICustomToolbarButton, IToolboxButton, ToolbarButton } from './types';
const microphone = {
key: 'microphone',
Content: AudioSettingsButton,
group: 0
};
const camera = {
key: 'camera',
Content: VideoSettingsButton,
group: 0
};
const profile = {
key: 'profile',
Content: ProfileButton,
group: 1
};
const chat = {
key: 'chat',
Content: ChatButton,
group: 2
};
const desktop = {
key: 'desktop',
Content: ShareDesktopButton,
group: 2
};
// In Narrow layout and mobile web we are using drawer for popups and that is why it is better to include
// all forms of reactions in the overflow menu. Otherwise the toolbox will be hidden and the reactions popup
// misaligned.
const raisehand = {
key: 'raisehand',
Content: RaiseHandContainerButton,
group: 2
};
const invite = {
key: 'invite',
Content: InviteButton,
group: 2
};
const toggleCamera = {
key: 'toggle-camera',
Content: ToggleCameraButton,
group: 2
};
const videoQuality = {
key: 'videoquality',
Content: VideoQualityButton,
group: 2
};
const fullscreen = {
key: 'fullscreen',
Content: FullscreenButton,
group: 2
};
const linkToSalesforce = {
key: 'linktosalesforce',
Content: LinkToSalesforceButton,
group: 2
};
const shareAudio = {
key: 'shareaudio',
Content: ShareAudioButton,
group: 3
};
const noiseSuppression = {
key: 'noisesuppression',
Content: NoiseSuppressionButton,
group: 3
};
const settings = {
key: 'settings',
Content: SettingsButton,
group: 4
};
const download = {
key: 'download',
Content: DownloadButton,
group: 4
};
const help = {
key: 'help',
Content: HelpButton,
group: 4
};
/**
* A hook that returns the toggle camera button if it is enabled and undefined otherwise.
*
* @returns {Object | undefined}
*/
function useToggleCameraButton() {
const toggleCameraEnabled = useSelector(isToggleCameraEnabled);
if (toggleCameraEnabled) {
return toggleCamera;
}
}
/**
* A hook that returns the desktop sharing button if it is enabled and undefined otherwise.
*
* @returns {Object | undefined}
*/
function getDesktopSharingButton() {
if (JitsiMeetJS.isDesktopSharingEnabled()) {
return desktop;
}
}
/**
* A hook that returns the fullscreen button if it is enabled and undefined otherwise.
*
* @returns {Object | undefined}
*/
function getFullscreenButton() {
if (!isIosMobileBrowser() || isIpadMobileBrowser()) {
return fullscreen;
}
}
/**
* A hook that returns the "link to salesforce" button if it is enabled and undefined otherwise.
*
* @returns {Object | undefined}
*/
function useLinkToSalesforceButton() {
const _isSalesforceEnabled = useSelector(isSalesforceEnabled);
if (_isSalesforceEnabled) {
return linkToSalesforce;
}
}
/**
* A hook that returns the share audio button if it is enabled and undefined otherwise.
*
* @returns {Object | undefined}
*/
function getShareAudioButton() {
if (JitsiMeetJS.isDesktopSharingEnabled() && isScreenAudioSupported()) {
return shareAudio;
}
}
/**
* A hook that returns the download button if it is enabled and undefined otherwise.
*
* @returns {Object | undefined}
*/
function useDownloadButton() {
const visible = useSelector(
(state: IReduxState) => typeof state['features/base/config'].deploymentUrls?.downloadAppsUrl === 'string');
if (visible) {
return download;
}
}
/**
* A hook that returns the help button if it is enabled and undefined otherwise.
*
* @returns {Object | undefined}
*/
function useHelpButton() {
const visible = useSelector(
(state: IReduxState) =>
typeof state['features/base/config'].deploymentUrls?.userDocumentationURL === 'string'
&& getFeatureFlag(state, HELP_BUTTON_ENABLED, true));
if (visible) {
return help;
}
}
/**
* Returns all buttons that could be rendered.
*
* @param {Object} _customToolbarButtons - An array containing custom buttons objects.
* @returns {Object} The button maps mainMenuButtons and overflowMenuButtons.
*/
export function useToolboxButtons(
_customToolbarButtons?: ICustomToolbarButton[]): { [key: string]: IToolboxButton; } {
const desktopSharing = getDesktopSharingButton();
const toggleCameraButton = useToggleCameraButton();
const _fullscreen = getFullscreenButton();
const security = useSecurityDialogButton();
const reactions = useReactionsButton();
const participants = useParticipantPaneButton();
const tileview = useTileViewButton();
const cc = useClosedCaptionButton();
const recording = useRecordingButton();
const liveStreaming = useLiveStreamingButton();
const linktosalesforce = useLinkToSalesforceButton();
const shareaudio = getShareAudioButton();
const shareVideo = useSharedVideoButton();
const whiteboard = useWhiteboardButton();
const etherpad = useEtherpadButton();
const virtualBackground = useVirtualBackgroundButton();
const speakerStats = useSpeakerStatsButton();
const shortcuts = useKeyboardShortcutsButton();
const embed = useEmbedButton();
const feedback = useFeedbackButton();
const _download = useDownloadButton();
const _help = useHelpButton();
const buttons: { [key in ToolbarButton]?: IToolboxButton; } = {
microphone,
camera,
profile,
desktop: desktopSharing,
chat,
raisehand,
reactions,
'participants-pane': participants,
invite,
tileview,
'toggle-camera': toggleCameraButton,
videoquality: videoQuality,
fullscreen: _fullscreen,
security,
closedcaptions: cc,
recording,
livestreaming: liveStreaming,
linktosalesforce,
sharedvideo: shareVideo,
shareaudio,
noisesuppression: noiseSuppression,
whiteboard,
etherpad,
'select-background': virtualBackground,
stats: speakerStats,
settings,
shortcuts,
embedmeeting: embed,
feedback,
download: _download,
help: _help
};
const buttonKeys = Object.keys(buttons) as ToolbarButton[];
buttonKeys.forEach(
key => typeof buttons[key] === 'undefined' && delete buttons[key]);
const customButtons = _customToolbarButtons?.reduce((prev, { backgroundColor, icon, id, text }) => {
prev[id] = {
backgroundColor,
key: id,
id,
Content: CustomOptionButton,
group: 4,
icon,
text
};
return prev;
}, {} as { [key: string]: ICustomToolbarButton; });
return {
...buttons,
...customButtons
};
}
export const useKeyboardShortcuts = (toolbarButtons: Array<string>) => {
const dispatch = useDispatch();
const _isSpeakerStatsDisabled = useSelector(isSpeakerStatsDisabled);
const _isParticipantsPaneEnabled = useSelector(isParticipantsPaneEnabled);
const _shouldDisplayReactionsButtons = useSelector(shouldDisplayReactionsButtons);
const _toolbarButtons = useSelector(
(state: IReduxState) => toolbarButtons || state['features/toolbox'].toolbarButtons);
const chatOpen = useSelector((state: IReduxState) => state['features/chat'].isOpen);
const desktopSharingButtonDisabled = useSelector(isDesktopShareButtonDisabled);
const desktopSharingEnabled = JitsiMeetJS.isDesktopSharingEnabled();
const fullScreen = useSelector((state: IReduxState) => state['features/toolbox'].fullScreen);
const gifsEnabled = useSelector(isGifEnabled);
const participantsPaneOpen = useSelector(getParticipantsPaneOpen);
const raisedHand = useSelector((state: IReduxState) => hasRaisedHand(getLocalParticipant(state)));
const screenSharing = useSelector(isScreenVideoShared);
const tileViewEnabled = useSelector(shouldDisplayTileView);
/**
* Creates an analytics keyboard shortcut event and dispatches an action for
* toggling the display of chat.
*
* @private
* @returns {void}
*/
function onToggleChat() {
sendAnalytics(createShortcutEvent(
'toggle.chat',
ACTION_SHORTCUT_TRIGGERED,
{
enable: !chatOpen
}));
// Checks if there was any text selected by the user.
// Used for when we press simultaneously keys for copying
// text messages from the chat board
if (window.getSelection()?.toString() !== '') {
return false;
}
dispatch(toggleChat());
}
/**
* Creates an analytics keyboard shortcut event and dispatches an action for
* toggling the display of the participants pane.
*
* @private
* @returns {void}
*/
function onToggleParticipantsPane() {
sendAnalytics(createShortcutEvent(
'toggle.participants-pane',
ACTION_SHORTCUT_TRIGGERED,
{
enable: !participantsPaneOpen
}));
if (participantsPaneOpen) {
dispatch(closeParticipantsPane());
} else {
dispatch(openParticipantsPane());
}
}
/**
* Creates an analytics keyboard shortcut event and dispatches an action for
* toggling the display of Video Quality.
*
* @private
* @returns {void}
*/
function onToggleVideoQuality() {
sendAnalytics(createShortcutEvent('video.quality'));
dispatch(toggleDialog(VideoQualityDialog));
}
/**
* Dispatches an action for toggling the tile view.
*
* @private
* @returns {void}
*/
function onToggleTileView() {
sendAnalytics(createShortcutEvent(
'toggle.tileview',
ACTION_SHORTCUT_TRIGGERED,
{
enable: !tileViewEnabled
}));
dispatch(toggleTileView());
}
/**
* Creates an analytics keyboard shortcut event and dispatches an action for
* toggling full screen mode.
*
* @private
* @returns {void}
*/
function onToggleFullScreen() {
sendAnalytics(createShortcutEvent(
'toggle.fullscreen',
ACTION_SHORTCUT_TRIGGERED,
{
enable: !fullScreen
}));
dispatch(setFullScreen(!fullScreen));
}
/**
* Creates an analytics keyboard shortcut event and dispatches an action for
* toggling raise hand.
*
* @private
* @returns {void}
*/
function onToggleRaiseHand() {
sendAnalytics(createShortcutEvent(
'toggle.raise.hand',
ACTION_SHORTCUT_TRIGGERED,
{ enable: !raisedHand }));
dispatch(raiseHand(!raisedHand));
}
/**
* Creates an analytics keyboard shortcut event and dispatches an action for
* toggling screensharing.
*
* @private
* @returns {void}
*/
function onToggleScreenshare() {
// Ignore the shortcut if the button is disabled.
if (desktopSharingButtonDisabled) {
return;
}
sendAnalytics(createShortcutEvent(
'toggle.screen.sharing',
ACTION_SHORTCUT_TRIGGERED,
{
enable: !screenSharing
}));
if (desktopSharingEnabled && !desktopSharingButtonDisabled) {
dispatch(startScreenShareFlow(!screenSharing));
}
}
/**
* Creates an analytics keyboard shortcut event and dispatches an action for
* toggling speaker stats.
*
* @private
* @returns {void}
*/
function onSpeakerStats() {
sendAnalytics(createShortcutEvent(
'speaker.stats'
));
dispatch(toggleDialog(SpeakerStats, {
conference: APP.conference
}));
}
useEffect(() => {
const KEYBOARD_SHORTCUTS = [
isButtonEnabled('videoquality', _toolbarButtons) && {
character: 'A',
exec: onToggleVideoQuality,
helpDescription: 'toolbar.callQuality'
},
isButtonEnabled('chat', _toolbarButtons) && {
character: 'C',
exec: onToggleChat,
helpDescription: 'keyboardShortcuts.toggleChat'
},
isButtonEnabled('desktop', _toolbarButtons) && {
character: 'D',
exec: onToggleScreenshare,
helpDescription: 'keyboardShortcuts.toggleScreensharing'
},
_isParticipantsPaneEnabled && isButtonEnabled('participants-pane', _toolbarButtons) && {
character: 'P',
exec: onToggleParticipantsPane,
helpDescription: 'keyboardShortcuts.toggleParticipantsPane'
},
isButtonEnabled('raisehand', _toolbarButtons) && {
character: 'R',
exec: onToggleRaiseHand,
helpDescription: 'keyboardShortcuts.raiseHand'
},
isButtonEnabled('fullscreen', _toolbarButtons) && {
character: 'S',
exec: onToggleFullScreen,
helpDescription: 'keyboardShortcuts.fullScreen'
},
isButtonEnabled('tileview', _toolbarButtons) && {
character: 'W',
exec: onToggleTileView,
helpDescription: 'toolbar.tileViewToggle'
},
!_isSpeakerStatsDisabled && isButtonEnabled('stats', _toolbarButtons) && {
character: 'T',
exec: onSpeakerStats,
helpDescription: 'keyboardShortcuts.showSpeakerStats'
}
];
KEYBOARD_SHORTCUTS.forEach(shortcut => {
if (typeof shortcut === 'object') {
dispatch(registerShortcut({
character: shortcut.character,
handler: shortcut.exec,
helpDescription: shortcut.helpDescription
}));
}
});
// If the buttons for sending reactions are not displayed we should disable the shortcuts too.
if (_shouldDisplayReactionsButtons) {
const REACTION_SHORTCUTS = Object.keys(REACTIONS).map(key => {
const onShortcutSendReaction = () => {
dispatch(addReactionToBuffer(key));
sendAnalytics(createShortcutEvent(
`reaction.${key}`
));
};
return {
character: REACTIONS[key].shortcutChar,
exec: onShortcutSendReaction,
helpDescription: `toolbar.reaction${key.charAt(0).toUpperCase()}${key.slice(1)}`,
altKey: true
};
});
REACTION_SHORTCUTS.forEach(shortcut => {
dispatch(registerShortcut({
alt: shortcut.altKey,
character: shortcut.character,
handler: shortcut.exec,
helpDescription: shortcut.helpDescription
}));
});
if (gifsEnabled) {
const onGifShortcut = () => {
batch(() => {
dispatch(toggleReactionsMenuVisibility());
dispatch(setGifMenuVisibility(true));
});
};
dispatch(registerShortcut({
character: 'G',
handler: onGifShortcut,
helpDescription: 'keyboardShortcuts.giphyMenu'
}));
}
}
return () => {
[ 'A', 'C', 'D', 'P', 'R', 'S', 'W', 'T', 'G' ].forEach(letter =>
dispatch(unregisterShortcut(letter)));
if (_shouldDisplayReactionsButtons) {
Object.keys(REACTIONS).map(key => REACTIONS[key].shortcutChar)
.forEach(letter =>
dispatch(unregisterShortcut(letter, true)));
}
};
}, [
_shouldDisplayReactionsButtons,
chatOpen,
desktopSharingButtonDisabled,
desktopSharingEnabled,
fullScreen,
gifsEnabled,
participantsPaneOpen,
raisedHand,
screenSharing,
tileViewEnabled
]);
};

View File

@@ -0,0 +1,46 @@
import { OVERWRITE_CONFIG, SET_CONFIG, UPDATE_CONFIG } from '../base/config/actionTypes';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import { I_AM_VISITOR_MODE } from '../visitors/actionTypes';
import { SET_TOOLBAR_BUTTONS } from './actionTypes';
import { setMainToolbarThresholds } from './actions.native';
import { NATIVE_THRESHOLDS, NATIVE_TOOLBAR_BUTTONS } from './constants';
import { getToolbarButtons } from './functions.native';
/**
* Middleware which intercepts Toolbox actions to handle changes to the
* visibility timeout of the Toolbox.
*
* @param {Store} store - The redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case UPDATE_CONFIG:
case OVERWRITE_CONFIG:
case I_AM_VISITOR_MODE:
case SET_CONFIG: {
const result = next(action);
const { dispatch } = store;
const state = store.getState();
const toolbarButtons = getToolbarButtons(state, NATIVE_TOOLBAR_BUTTONS);
if (action.type !== I_AM_VISITOR_MODE) {
dispatch(setMainToolbarThresholds(NATIVE_THRESHOLDS));
}
dispatch({
type: SET_TOOLBAR_BUTTONS,
toolbarButtons
});
return result;
}
}
return next(action);
});

View File

@@ -0,0 +1,172 @@
import { batch } from 'react-redux';
import { AnyAction } from 'redux';
import { OVERWRITE_CONFIG, SET_CONFIG, UPDATE_CONFIG } from '../base/config/actionTypes';
import { NotifyClickButton } from '../base/config/configType';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import { I_AM_VISITOR_MODE } from '../visitors/actionTypes';
import {
CLEAR_TOOLBOX_TIMEOUT,
SET_BUTTONS_WITH_NOTIFY_CLICK,
SET_FULL_SCREEN,
SET_PARTICIPANT_MENU_BUTTONS_WITH_NOTIFY_CLICK,
SET_TOOLBAR_BUTTONS,
SET_TOOLBOX_TIMEOUT
} from './actionTypes';
import { setMainToolbarThresholds } from './actions.web';
import { THRESHOLDS, TOOLBAR_BUTTONS } from './constants';
import { getToolbarButtons } from './functions.web';
import { NOTIFY_CLICK_MODE } from './types';
import './subscriber.web';
/**
* Middleware which intercepts Toolbox actions to handle changes to the
* visibility timeout of the Toolbox.
*
* @param {Store} store - The redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case CLEAR_TOOLBOX_TIMEOUT: {
const { timeoutID } = store.getState()['features/toolbox'];
clearTimeout(timeoutID ?? undefined);
break;
}
case UPDATE_CONFIG:
case OVERWRITE_CONFIG:
case I_AM_VISITOR_MODE:
case SET_CONFIG: {
const result = next(action);
const { dispatch, getState } = store;
const state = getState();
if (action.type !== I_AM_VISITOR_MODE) {
const {
customToolbarButtons,
buttonsWithNotifyClick,
participantMenuButtonsWithNotifyClick,
customParticipantMenuButtons
} = state['features/base/config'];
batch(() => {
if (action.type !== I_AM_VISITOR_MODE) {
dispatch(setMainToolbarThresholds(THRESHOLDS));
}
dispatch({
type: SET_BUTTONS_WITH_NOTIFY_CLICK,
buttonsWithNotifyClick: _buildButtonsArray(buttonsWithNotifyClick, customToolbarButtons)
});
dispatch({
type: SET_PARTICIPANT_MENU_BUTTONS_WITH_NOTIFY_CLICK,
participantMenuButtonsWithNotifyClick:
_buildButtonsArray(participantMenuButtonsWithNotifyClick, customParticipantMenuButtons)
});
});
}
const toolbarButtons = getToolbarButtons(state, TOOLBAR_BUTTONS);
dispatch({
type: SET_TOOLBAR_BUTTONS,
toolbarButtons
});
return result;
}
case SET_FULL_SCREEN:
return _setFullScreen(next, action);
case SET_TOOLBOX_TIMEOUT: {
const { timeoutID } = store.getState()['features/toolbox'];
const { handler, timeoutMS }: { handler: Function; timeoutMS: number; } = action;
clearTimeout(timeoutID ?? undefined);
action.timeoutID = setTimeout(handler, timeoutMS);
break;
}
}
return next(action);
});
type DocumentElement = {
requestFullscreen?: Function;
webkitRequestFullscreen?: Function;
};
/**
* Makes an external request to enter or exit full screen mode.
*
* @param {Dispatch} next - The redux dispatch function to dispatch the
* specified action to the specified store.
* @param {Action} action - The redux action SET_FULL_SCREEN which is being
* dispatched in the specified store.
* @private
* @returns {Object} The value returned by {@code next(action)}.
*/
function _setFullScreen(next: Function, action: AnyAction) {
const result = next(action);
const { fullScreen } = action;
if (fullScreen) {
const documentElement: DocumentElement
= document.documentElement || {};
if (typeof documentElement.requestFullscreen === 'function') {
documentElement.requestFullscreen();
} else if (
typeof documentElement.webkitRequestFullscreen === 'function') {
documentElement.webkitRequestFullscreen();
}
return result;
}
if (typeof document.exitFullscreen === 'function') {
document.exitFullscreen();
} else if (typeof document.webkitExitFullscreen === 'function') {
document.webkitExitFullscreen();
}
return result;
}
/**
* Common logic to gather buttons that have to notify the api when clicked.
*
* @param {Array} buttonsWithNotifyClick - The array of system buttons that need to notify the api.
* @param {Array} customButtons - The custom buttons.
* @returns {Array}
*/
function _buildButtonsArray(
buttonsWithNotifyClick?: NotifyClickButton[],
customButtons?: {
icon: string;
id: string;
text: string;
}[]
): Map<string, NOTIFY_CLICK_MODE> {
const customButtonsWithNotifyClick = customButtons?.map(
({ id }) => ([ id, NOTIFY_CLICK_MODE.ONLY_NOTIFY ]) as [string, NOTIFY_CLICK_MODE]) ?? [];
const buttons = (Array.isArray(buttonsWithNotifyClick) ? buttonsWithNotifyClick : [])
.filter(button => typeof button === 'string' || (typeof button === 'object' && typeof button.key === 'string'))
.map(button => {
if (typeof button === 'string') {
return [ button, NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY ] as [string, NOTIFY_CLICK_MODE];
}
return [
button.key,
button.preventExecution ? NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY : NOTIFY_CLICK_MODE.ONLY_NOTIFY
] as [string, NOTIFY_CLICK_MODE];
});
return new Map([ ...customButtonsWithNotifyClick, ...buttons ]);
}

View File

@@ -0,0 +1,215 @@
import ReducerRegistry from '../base/redux/ReducerRegistry';
import { set } from '../base/redux/functions';
import {
CLEAR_TOOLBOX_TIMEOUT,
FULL_SCREEN_CHANGED,
SET_BUTTONS_WITH_NOTIFY_CLICK,
SET_HANGUP_MENU_VISIBLE,
SET_MAIN_TOOLBAR_BUTTONS_THRESHOLDS,
SET_OVERFLOW_DRAWER,
SET_OVERFLOW_MENU_VISIBLE,
SET_PARTICIPANT_MENU_BUTTONS_WITH_NOTIFY_CLICK,
SET_TOOLBAR_BUTTONS,
SET_TOOLBAR_HOVERED,
SET_TOOLBOX_ENABLED,
SET_TOOLBOX_SHIFT_UP,
SET_TOOLBOX_TIMEOUT,
SET_TOOLBOX_VISIBLE,
TOGGLE_TOOLBOX_VISIBLE
} from './actionTypes';
import { NATIVE_THRESHOLDS, THRESHOLDS } from './constants';
import { IMainToolbarButtonThresholds, NOTIFY_CLICK_MODE } from './types';
/**
* Array of thresholds for the main toolbar buttons that will inlude only the usable entries from THRESHOLDS array.
*
* Note: THRESHOLDS array includes some dummy values that enables users of the iframe API to override and use.
* Note2: Casting is needed because it seems isArray guard is not working well in TS. See:
* https://github.com/microsoft/TypeScript/issues/17002.
*/
const FILTERED_THRESHOLDS = THRESHOLDS.filter(({ order }) => Array.isArray(order)) as IMainToolbarButtonThresholds;
/**
* Initial state of toolbox's part of Redux store.
*/
const INITIAL_STATE = {
buttonsWithNotifyClick: new Map(),
/**
* The indicator which determines whether the Toolbox is enabled.
*
* @type {boolean}
*/
enabled: true,
/**
* The indicator which determines whether the hangup menu is visible.
*
* @type {boolean}
*/
hangupMenuVisible: false,
/**
* The indicator which determines whether a Toolbar in the Toolbox is
* hovered.
*
* @type {boolean}
*/
hovered: false,
/**
* The thresholds for screen size and visible main toolbar buttons.
*/
mainToolbarButtonsThresholds: navigator.product === 'ReactNative' ? NATIVE_THRESHOLDS : FILTERED_THRESHOLDS,
participantMenuButtonsWithNotifyClick: new Map(),
/**
* The indicator which determines whether the overflow menu(s) are to be displayed as drawers.
*
* @type {boolean}
*/
overflowDrawer: false,
/**
* The indicator which determines whether the OverflowMenu is visible.
*
* @type {boolean}
*/
overflowMenuVisible: false,
/**
* Whether to shift the toolbar up (in case it overlaps the tiles names).
*/
shiftUp: false,
/**
* A number, non-zero value which identifies the timer created by a call
* to setTimeout().
*
* @type {number|null}
*/
timeoutID: null,
/**
* The list of enabled toolbar buttons.
*
* @type {Array<string>}
*/
toolbarButtons: [],
/**
* The indicator that determines whether the Toolbox is visible.
*
* @type {boolean}
*/
visible: false
};
export interface IToolboxState {
buttonsWithNotifyClick: Map<string, NOTIFY_CLICK_MODE>;
enabled: boolean;
fullScreen?: boolean;
hangupMenuVisible: boolean;
hovered: boolean;
mainToolbarButtonsThresholds: IMainToolbarButtonThresholds;
overflowDrawer: boolean;
overflowMenuVisible: boolean;
participantMenuButtonsWithNotifyClick: Map<string, NOTIFY_CLICK_MODE>;
shiftUp: boolean;
timeoutID?: number | null;
toolbarButtons: Array<string>;
visible: boolean;
}
ReducerRegistry.register<IToolboxState>(
'features/toolbox',
(state = INITIAL_STATE, action): IToolboxState => {
switch (action.type) {
case CLEAR_TOOLBOX_TIMEOUT:
return {
...state,
timeoutID: undefined
};
case FULL_SCREEN_CHANGED:
return {
...state,
fullScreen: action.fullScreen
};
case SET_HANGUP_MENU_VISIBLE:
return {
...state,
hangupMenuVisible: action.visible
};
case SET_OVERFLOW_DRAWER:
return {
...state,
overflowDrawer: action.displayAsDrawer
};
case SET_OVERFLOW_MENU_VISIBLE:
return {
...state,
overflowMenuVisible: action.visible
};
case SET_TOOLBAR_BUTTONS:
return {
...state,
toolbarButtons: action.toolbarButtons
};
case SET_BUTTONS_WITH_NOTIFY_CLICK:
return {
...state,
buttonsWithNotifyClick: action.buttonsWithNotifyClick
};
case SET_MAIN_TOOLBAR_BUTTONS_THRESHOLDS:
return {
...state,
mainToolbarButtonsThresholds: action.mainToolbarButtonsThresholds
};
case SET_TOOLBAR_HOVERED:
return {
...state,
hovered: action.hovered
};
case SET_TOOLBOX_ENABLED:
return {
...state,
enabled: action.enabled
};
case SET_TOOLBOX_TIMEOUT:
return {
...state,
timeoutID: action.timeoutID
};
case SET_TOOLBOX_SHIFT_UP:
return {
...state,
shiftUp: action.shiftUp
};
case SET_TOOLBOX_VISIBLE:
return set(state, 'visible', action.visible);
case SET_PARTICIPANT_MENU_BUTTONS_WITH_NOTIFY_CLICK:
return {
...state,
participantMenuButtonsWithNotifyClick: action.participantMenuButtonsWithNotifyClick
};
case TOGGLE_TOOLBOX_VISIBLE:
return set(state, 'visible', !state.visible);
}
return state;
});

View File

@@ -0,0 +1,106 @@
import { throttle } from 'lodash-es';
import { IReduxState, IStore } from '../app/types';
import { getParticipantCount } from '../base/participants/functions';
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
import { DEFAULT_MAX_COLUMNS } from '../filmstrip/constants';
import { isLayoutTileView } from '../video-layout/functions.any';
import { setShiftUp } from './actions.any';
import { isAudioMuteButtonDisabled } from './functions.any';
/**
* Notifies when audio availability changes.
*/
StateListenerRegistry.register(
/* selector */ (state: IReduxState) => isAudioMuteButtonDisabled(state),
/* listener */ (disabled: boolean, store: IStore, previousDisabled: boolean) => {
if (disabled !== previousDisabled) {
APP.API.notifyAudioAvailabilityChanged(!disabled);
}
}
);
const checkToolboxOverlap = (clientHeight: number, store: IStore) => {
let toolboxRect = document.querySelector('.toolbox-content-items')?.getBoundingClientRect();
if (!toolboxRect) {
return;
}
const tiles = document.querySelectorAll('span.videocontainer');
if (!tiles.length) {
return;
}
const toolboxHeight = 48 + 12; // height + padding
const bottomMargin = 16;
// Set top and bottom manually to avoid wrong coordinates
// caused by the hiding/ showing of the toolbox.
toolboxRect = {
...toolboxRect,
top: clientHeight - toolboxHeight - bottomMargin,
bottom: clientHeight - bottomMargin,
left: toolboxRect.left,
right: toolboxRect.right
};
let isIntersecting = false;
const rows = store.getState()['features/filmstrip'].tileViewDimensions?.gridDimensions?.rows;
const noOfTilesToCheck = rows === 1 ? tiles.length : DEFAULT_MAX_COLUMNS - 1;
for (let i = 1; i < Math.max(noOfTilesToCheck, tiles.length); i++) {
const tile = tiles[tiles.length - i];
const indicatorsRect = tile?.querySelector('.bottom-indicators')?.getBoundingClientRect();
if (!indicatorsRect) {
continue;
}
if (indicatorsRect.top <= toolboxRect.bottom
&& indicatorsRect.right >= toolboxRect.left
&& indicatorsRect.bottom >= toolboxRect.top
&& indicatorsRect.left <= toolboxRect.right
) {
isIntersecting = true;
break;
}
}
store.dispatch(setShiftUp(isIntersecting));
};
const throttledCheckOverlap = throttle(checkToolboxOverlap, 100, {
leading: false,
trailing: true
});
/**
* Listens for changes in the selected layout to calculate the dimensions of the tile view grid and horizontal view.
*/
StateListenerRegistry.register(
/* selector */ state => {
const { clientHeight, videoSpaceWidth } = state['features/base/responsive-ui'];
return {
participantCount: getParticipantCount(state),
clientHeight,
clientWidth: videoSpaceWidth,
isTileView: isLayoutTileView(state)
};
},
/* listener */({ clientHeight, isTileView }, store, previousState) => {
if (!isTileView) {
if (previousState?.isTileView) {
store.dispatch(setShiftUp(false));
}
return;
}
throttledCheckOverlap(clientHeight, store);
}, {
deepEquals: true
});

View File

@@ -0,0 +1,107 @@
import { ComponentType } from 'react';
export interface IToolboxButton {
Content: ComponentType<any>;
group: number;
key: string;
}
export interface IToolboxNativeButton {
Content: ComponentType<any>;
backgroundColor?: string;
group: number;
icon?: string;
id?: string;
key: string;
text?: string;
}
export type ToolbarButton = 'camera' |
'chat' |
'closedcaptions' |
'desktop' |
'download' |
'embedmeeting' |
'etherpad' |
'feedback' |
'filmstrip' |
'fullscreen' |
'hangup' |
'help' |
'highlight' |
'invite' |
'linktosalesforce' |
'livestreaming' |
'microphone' |
'mute-everyone' |
'mute-video-everyone' |
'noisesuppression' |
'overflowmenu' |
'participants-pane' |
'profile' |
'raisehand' |
'reactions' |
'recording' |
'security' |
'select-background' |
'settings' |
'shareaudio' |
'sharedvideo' |
'shortcuts' |
'stats' |
'tileview' |
'toggle-camera' |
'videoquality' |
'whiteboard' |
'__end';
export enum NOTIFY_CLICK_MODE {
ONLY_NOTIFY = 'ONLY_NOTIFY',
PREVENT_AND_NOTIFY = 'PREVENT_AND_NOTIFY'
}
export type IMainToolbarButtonThresholds = Array<{
order: Array<ToolbarButton | NativeToolbarButton | string>;
width: number;
}>;
export type IMainToolbarButtonThresholdsUnfiltered = Array<{
order: Array<ToolbarButton | NativeToolbarButton | string> | Symbol;
width: number;
}>;
export interface ICustomToolbarButton {
Content?: ComponentType<any>;
backgroundColor?: string;
group?: number;
icon: string;
id: string;
key?: string;
text: string;
}
export type NativeToolbarButton = 'camera' |
'chat' |
'microphone' |
'raisehand' |
'desktop' |
'tileview' |
'overflowmenu' |
'hangup';
export interface IGetVisibleNativeButtonsParams {
allButtons: { [key: string]: IToolboxNativeButton; };
clientWidth: number;
iAmVisitor: boolean;
mainToolbarButtonsThresholds: IMainToolbarButtonThresholds;
toolbarButtons: string[];
}
export interface IGetVisibleButtonsParams {
allButtons: { [key: string]: IToolboxButton; };
buttonsWithNotifyClick: Map<string, NOTIFY_CLICK_MODE>;
clientWidth: number;
jwtDisabledButtons: string[];
mainToolbarButtonsThresholds: IMainToolbarButtonThresholds;
toolbarButtons: string[];
}