This commit is contained in:
168
react/features/toolbox/actionTypes.ts
Normal file
168
react/features/toolbox/actionTypes.ts
Normal 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';
|
||||
189
react/features/toolbox/actions.any.ts
Normal file
189
react/features/toolbox/actions.any.ts
Normal 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
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
45
react/features/toolbox/actions.native.ts
Normal file
45
react/features/toolbox/actions.native.ts
Normal 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
|
||||
};
|
||||
}
|
||||
280
react/features/toolbox/actions.web.ts
Normal file
280
react/features/toolbox/actions.web.ts
Normal 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));
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
59
react/features/toolbox/components/DownloadButton.ts
Normal file
59
react/features/toolbox/components/DownloadButton.ts
Normal 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));
|
||||
50
react/features/toolbox/components/HangupButton.ts
Normal file
50
react/features/toolbox/components/HangupButton.ts
Normal 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));
|
||||
62
react/features/toolbox/components/HelpButton.ts
Normal file
62
react/features/toolbox/components/HelpButton.ts
Normal 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));
|
||||
1
react/features/toolbox/components/index.native.ts
Normal file
1
react/features/toolbox/components/index.native.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as CustomOptionButton } from './native/CustomOptionButton';
|
||||
1
react/features/toolbox/components/index.web.ts
Normal file
1
react/features/toolbox/components/index.web.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as CustomOptionButton } from './web/CustomOptionButton';
|
||||
@@ -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>));
|
||||
96
react/features/toolbox/components/native/AudioOnlyButton.ts
Normal file
96
react/features/toolbox/components/native/AudioOnlyButton.ts
Normal 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));
|
||||
@@ -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));
|
||||
@@ -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;
|
||||
77
react/features/toolbox/components/native/HangupMenu.tsx
Normal file
77
react/features/toolbox/components/native/HangupMenu.tsx
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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));
|
||||
@@ -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));
|
||||
316
react/features/toolbox/components/native/OverflowMenu.tsx
Normal file
316
react/features/toolbox/components/native/OverflowMenu.tsx
Normal 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 } />
|
||||
);
|
||||
});
|
||||
@@ -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));
|
||||
99
react/features/toolbox/components/native/RaiseHandButton.ts
Normal file
99
react/features/toolbox/components/native/RaiseHandButton.ts
Normal 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));
|
||||
@@ -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));
|
||||
@@ -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);
|
||||
@@ -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));
|
||||
@@ -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));
|
||||
@@ -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));
|
||||
142
react/features/toolbox/components/native/Toolbox.tsx
Normal file
142
react/features/toolbox/components/native/Toolbox.tsx
Normal 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);
|
||||
@@ -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>));
|
||||
216
react/features/toolbox/components/native/styles.ts
Normal file
216
react/features/toolbox/components/native/styles.ts
Normal 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'
|
||||
}
|
||||
});
|
||||
205
react/features/toolbox/components/web/AudioMuteButton.tsx
Normal file
205
react/features/toolbox/components/web/AudioMuteButton.tsx
Normal 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);
|
||||
180
react/features/toolbox/components/web/AudioSettingsButton.tsx
Normal file
180
react/features/toolbox/components/web/AudioSettingsButton.tsx
Normal 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));
|
||||
40
react/features/toolbox/components/web/CustomOptionButton.tsx
Normal file
40
react/features/toolbox/components/web/CustomOptionButton.tsx
Normal 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;
|
||||
126
react/features/toolbox/components/web/DialogPortal.ts
Normal file
126
react/features/toolbox/components/web/DialogPortal.ts
Normal 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;
|
||||
172
react/features/toolbox/components/web/Drawer.tsx
Normal file
172
react/features/toolbox/components/web/Drawer.tsx
Normal 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;
|
||||
@@ -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 } /> }
|
||||
</>);
|
||||
};
|
||||
77
react/features/toolbox/components/web/FullscreenButton.ts
Normal file
77
react/features/toolbox/components/web/FullscreenButton.ts
Normal 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));
|
||||
@@ -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 } />
|
||||
);
|
||||
};
|
||||
|
||||
134
react/features/toolbox/components/web/HangupMenuButton.tsx
Normal file
134
react/features/toolbox/components/web/HangupMenuButton.tsx
Normal 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);
|
||||
57
react/features/toolbox/components/web/HangupToggleButton.tsx
Normal file
57
react/features/toolbox/components/web/HangupToggleButton.tsx
Normal 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));
|
||||
58
react/features/toolbox/components/web/JitsiPortal.tsx
Normal file
58
react/features/toolbox/components/web/JitsiPortal.tsx
Normal 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;
|
||||
@@ -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 } />
|
||||
);
|
||||
};
|
||||
@@ -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));
|
||||
262
react/features/toolbox/components/web/OverflowMenuButton.tsx
Normal file
262
react/features/toolbox/components/web/OverflowMenuButton.tsx
Normal 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;
|
||||
@@ -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));
|
||||
116
react/features/toolbox/components/web/ProfileButton.ts
Normal file
116
react/features/toolbox/components/web/ProfileButton.ts
Normal 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));
|
||||
@@ -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);
|
||||
3
react/features/toolbox/components/web/Separator.tsx
Normal file
3
react/features/toolbox/components/web/Separator.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
import React from 'react';
|
||||
|
||||
export default () => <hr className = 'overflow-menu-hr' />;
|
||||
116
react/features/toolbox/components/web/ShareDesktopButton.ts
Normal file
116
react/features/toolbox/components/web/ShareDesktopButton.ts
Normal 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));
|
||||
76
react/features/toolbox/components/web/ToggleCameraButton.ts
Normal file
76
react/features/toolbox/components/web/ToggleCameraButton.ts
Normal 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));
|
||||
334
react/features/toolbox/components/web/Toolbox.tsx
Normal file
334
react/features/toolbox/components/web/Toolbox.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
203
react/features/toolbox/components/web/VideoMuteButton.tsx
Normal file
203
react/features/toolbox/components/web/VideoMuteButton.tsx
Normal 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);
|
||||
197
react/features/toolbox/components/web/VideoSettingsButton.tsx
Normal file
197
react/features/toolbox/components/web/VideoSettingsButton.tsx
Normal 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));
|
||||
214
react/features/toolbox/constants.ts
Normal file
214
react/features/toolbox/constants.ts
Normal 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'
|
||||
];
|
||||
95
react/features/toolbox/functions.any.ts
Normal file
95
react/features/toolbox/functions.any.ts
Normal 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);
|
||||
}
|
||||
125
react/features/toolbox/functions.native.ts
Normal file
125
react/features/toolbox/functions.native.ts
Normal 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
|
||||
};
|
||||
}
|
||||
251
react/features/toolbox/functions.web.ts
Normal file
251
react/features/toolbox/functions.web.ts
Normal 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`;
|
||||
}
|
||||
181
react/features/toolbox/hooks.native.ts
Normal file
181
react/features/toolbox/hooks.native.ts
Normal 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
|
||||
};
|
||||
}
|
||||
642
react/features/toolbox/hooks.web.ts
Normal file
642
react/features/toolbox/hooks.web.ts
Normal 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
|
||||
]);
|
||||
};
|
||||
46
react/features/toolbox/middleware.native.ts
Normal file
46
react/features/toolbox/middleware.native.ts
Normal 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);
|
||||
});
|
||||
172
react/features/toolbox/middleware.web.ts
Normal file
172
react/features/toolbox/middleware.web.ts
Normal 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 ]);
|
||||
}
|
||||
215
react/features/toolbox/reducer.ts
Normal file
215
react/features/toolbox/reducer.ts
Normal 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;
|
||||
});
|
||||
106
react/features/toolbox/subscriber.web.ts
Normal file
106
react/features/toolbox/subscriber.web.ts
Normal 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
|
||||
});
|
||||
107
react/features/toolbox/types.ts
Normal file
107
react/features/toolbox/types.ts
Normal 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[];
|
||||
}
|
||||
Reference in New Issue
Block a user