mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-02-14 09:50:19 +00:00
Compare commits
2 Commits
dependabot
...
release-73
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93bff0a45e | ||
|
|
250c3d1d70 |
36
config.js
36
config.js
@@ -826,6 +826,42 @@ var config = {
|
||||
// 'whiteboard',
|
||||
// ],
|
||||
|
||||
// Participant context menu buttons which have their click/tap event exposed through the API on
|
||||
// `participantMenuButtonClick`. Passing a string for the button key will
|
||||
// prevent execution of the click/tap routine; passing an object with `key` and
|
||||
// `preventExecution` flag on false will not prevent execution of the click/tap
|
||||
// routine. Below array with mixed mode for passing the buttons.
|
||||
// participantMenuButtonsWithNotifyClick: [
|
||||
// 'allow-video',
|
||||
// {
|
||||
// key: 'ask-unmute',
|
||||
// preventExecution: false
|
||||
// },
|
||||
// 'conn-status',
|
||||
// 'flip-local-video',
|
||||
// 'grant-moderator',
|
||||
// {
|
||||
// key: 'kick',
|
||||
// preventExecution: true
|
||||
// },
|
||||
// {
|
||||
// key: 'hide-self-view',
|
||||
// preventExecution: false
|
||||
// },
|
||||
// 'mute',
|
||||
// 'mute-others',
|
||||
// 'mute-others-video',
|
||||
// 'mute-video',
|
||||
// 'pinToStage',
|
||||
// 'privateMessage',
|
||||
// {
|
||||
// key: 'remote-control',
|
||||
// preventExecution: false
|
||||
// },
|
||||
// 'send-participant-to-room',
|
||||
// 'verify',
|
||||
// ],
|
||||
|
||||
// List of pre meeting screens buttons to hide. The values must be one or more of the 5 allowed buttons:
|
||||
// 'microphone', 'camera', 'select-background', 'invite', 'settings'
|
||||
// hiddenPremeetingButtons: [],
|
||||
|
||||
@@ -2002,14 +2002,16 @@ class API {
|
||||
* Notify external application ( if API is enabled) that a participant menu button was clicked.
|
||||
*
|
||||
* @param {string} key - The key of the participant menu button.
|
||||
* @param {string} participantId - The ID of the participant for with the participant menu button was clicked.
|
||||
* @param {string} participantId - The ID of the participant for whom the participant menu button was clicked.
|
||||
* @param {boolean} preventExecution - Whether execution of the button click was prevented or not.
|
||||
* @returns {void}
|
||||
*/
|
||||
notifyParticipantMenuButtonClicked(key, participantId) {
|
||||
notifyParticipantMenuButtonClicked(key, participantId, preventExecution) {
|
||||
this._sendEvent({
|
||||
name: 'participant-menu-button-clicked',
|
||||
key,
|
||||
participantId
|
||||
participantId,
|
||||
preventExecution
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
37
package-lock.json
generated
37
package-lock.json
generated
@@ -60,7 +60,7 @@
|
||||
"js-md5": "0.6.1",
|
||||
"js-sha512": "0.8.0",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1659.0.0+5d322ea5/lib-jitsi-meet.tgz",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet#9ffab6aa757697840189594a23523cda0c9e3cb6",
|
||||
"lodash": "4.17.21",
|
||||
"moment": "2.29.4",
|
||||
"moment-duration-format": "2.2.2",
|
||||
@@ -5526,6 +5526,11 @@
|
||||
"yarn": ">= 1.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@testrtc/watchrtc-sdk": {
|
||||
"version": "1.36.3",
|
||||
"resolved": "https://registry.npmjs.org/@testrtc/watchrtc-sdk/-/watchrtc-sdk-1.36.3.tgz",
|
||||
"integrity": "sha512-JtcTvvh20t553n8q5ZHpWQeSUTENkQrZbGNvQ05jD8SA2V5PHBok/7my1ZDuA44sgT0y1OfUA8VT7dhcs0VxWg=="
|
||||
},
|
||||
"node_modules/@trysound/sax": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
|
||||
@@ -7111,6 +7116,11 @@
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz",
|
||||
"integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g=="
|
||||
},
|
||||
"node_modules/async-es": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/async-es/-/async-es-3.2.4.tgz",
|
||||
"integrity": "sha512-GFHAAfmW7GzEiHFR6DvE5WMm6+js9pb+RLm+m1UpsyMX+I4j/R4QVw2Te664q+fvDVOz7Y0bORPDNvQS7BJ3Hw=="
|
||||
},
|
||||
"node_modules/async-limiter": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz",
|
||||
@@ -12762,8 +12772,8 @@
|
||||
},
|
||||
"node_modules/lib-jitsi-meet": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1659.0.0+5d322ea5/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-kTBN3NlI3RLxeQeq5uppU557kJPQWwFMq1NweGr0CH8TCJdnKt30Lqx/X+DGP7Js6286JDmpg4EWZa9wW7lLXQ==",
|
||||
"resolved": "git+ssh://git@github.com/jitsi/lib-jitsi-meet.git#9ffab6aa757697840189594a23523cda0c9e3cb6",
|
||||
"integrity": "sha512-OJTzWkFfTWTMcKQ4FdpP/X9RtTnVSOFDFO6SXF3fSByQ3dIXnKvfH5swOc8GuNWX/ODPlML/embKaBZ143Gohg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -12771,7 +12781,8 @@
|
||||
"@jitsi/logger": "2.0.0",
|
||||
"@jitsi/sdp-interop": "git+https://github.com/jitsi/sdp-interop#3d49eb4aa26863a3f8d32d7581cdb4321244266b",
|
||||
"@jitsi/sdp-simulcast": "0.4.0",
|
||||
"async": "3.2.3",
|
||||
"@testrtc/watchrtc-sdk": "1.36.3",
|
||||
"async-es": "3.2.4",
|
||||
"base64-js": "1.3.1",
|
||||
"current-executing-script": "0.1.3",
|
||||
"jquery": "3.6.1",
|
||||
@@ -23719,6 +23730,11 @@
|
||||
"seedrandom": "2.4.3"
|
||||
}
|
||||
},
|
||||
"@testrtc/watchrtc-sdk": {
|
||||
"version": "1.36.3",
|
||||
"resolved": "https://registry.npmjs.org/@testrtc/watchrtc-sdk/-/watchrtc-sdk-1.36.3.tgz",
|
||||
"integrity": "sha512-JtcTvvh20t553n8q5ZHpWQeSUTENkQrZbGNvQ05jD8SA2V5PHBok/7my1ZDuA44sgT0y1OfUA8VT7dhcs0VxWg=="
|
||||
},
|
||||
"@trysound/sax": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
|
||||
@@ -24990,6 +25006,11 @@
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz",
|
||||
"integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g=="
|
||||
},
|
||||
"async-es": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/async-es/-/async-es-3.2.4.tgz",
|
||||
"integrity": "sha512-GFHAAfmW7GzEiHFR6DvE5WMm6+js9pb+RLm+m1UpsyMX+I4j/R4QVw2Te664q+fvDVOz7Y0bORPDNvQS7BJ3Hw=="
|
||||
},
|
||||
"async-limiter": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz",
|
||||
@@ -29269,14 +29290,16 @@
|
||||
}
|
||||
},
|
||||
"lib-jitsi-meet": {
|
||||
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1659.0.0+5d322ea5/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-kTBN3NlI3RLxeQeq5uppU557kJPQWwFMq1NweGr0CH8TCJdnKt30Lqx/X+DGP7Js6286JDmpg4EWZa9wW7lLXQ==",
|
||||
"version": "git+ssh://git@github.com/jitsi/lib-jitsi-meet.git#9ffab6aa757697840189594a23523cda0c9e3cb6",
|
||||
"integrity": "sha512-OJTzWkFfTWTMcKQ4FdpP/X9RtTnVSOFDFO6SXF3fSByQ3dIXnKvfH5swOc8GuNWX/ODPlML/embKaBZ143Gohg==",
|
||||
"from": "lib-jitsi-meet@https://github.com/jitsi/lib-jitsi-meet#9ffab6aa757697840189594a23523cda0c9e3cb6",
|
||||
"requires": {
|
||||
"@jitsi/js-utils": "2.0.0",
|
||||
"@jitsi/logger": "2.0.0",
|
||||
"@jitsi/sdp-interop": "git+https://github.com/jitsi/sdp-interop#3d49eb4aa26863a3f8d32d7581cdb4321244266b",
|
||||
"@jitsi/sdp-simulcast": "0.4.0",
|
||||
"async": "3.2.3",
|
||||
"@testrtc/watchrtc-sdk": "1.36.3",
|
||||
"async-es": "3.2.4",
|
||||
"base64-js": "1.3.1",
|
||||
"current-executing-script": "0.1.3",
|
||||
"jquery": "3.6.1",
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
"js-md5": "0.6.1",
|
||||
"js-sha512": "0.8.0",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1659.0.0+5d322ea5/lib-jitsi-meet.tgz",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet#9ffab6aa757697840189594a23523cda0c9e3cb6",
|
||||
"lodash": "4.17.21",
|
||||
"moment": "2.29.4",
|
||||
"moment-duration-format": "2.2.2",
|
||||
|
||||
@@ -68,6 +68,33 @@ type ButtonsWithNotifyClick = 'camera' |
|
||||
'add-passcode' |
|
||||
'__end';
|
||||
|
||||
type ParticipantMenuButtonsWithNotifyClick = 'allow-video' |
|
||||
'ask-unmute' |
|
||||
'conn-status' |
|
||||
'flip-local-video' |
|
||||
'grant-moderator' |
|
||||
'hide-self-view' |
|
||||
'kick' |
|
||||
'mute' |
|
||||
'mute-others' |
|
||||
'mute-others-video' |
|
||||
'mute-video' |
|
||||
'pinToStage' |
|
||||
'privateMessage' |
|
||||
'remote-control' |
|
||||
'send-participant-to-room' |
|
||||
'verify';
|
||||
|
||||
type NotifyClickButtonKey = string |
|
||||
ButtonsWithNotifyClick |
|
||||
ParticipantMenuButtonsWithNotifyClick;
|
||||
|
||||
export type NotifyClickButton = NotifyClickButtonKey |
|
||||
{
|
||||
key: NotifyClickButtonKey;
|
||||
preventExecution: boolean;
|
||||
};
|
||||
|
||||
export type Sounds = 'ASKED_TO_UNMUTE_SOUND' |
|
||||
'E2EE_OFF_SOUND' |
|
||||
'E2EE_ON_SOUND' |
|
||||
@@ -447,6 +474,10 @@ export interface IConfig {
|
||||
mobileCodecPreferenceOrder?: Array<string>;
|
||||
stunServers?: Array<{ urls: string; }>;
|
||||
};
|
||||
participantMenuButtonsWithNotifyClick?: Array<string | ParticipantMenuButtonsWithNotifyClick | {
|
||||
key: string | ParticipantMenuButtonsWithNotifyClick;
|
||||
preventExecution: boolean;
|
||||
}>;
|
||||
participantsPane?: {
|
||||
hideModeratorSettingsTab?: boolean;
|
||||
hideMoreActionsButton?: boolean;
|
||||
|
||||
@@ -193,6 +193,7 @@ export default [
|
||||
'openSharedDocumentOnJoin',
|
||||
'opusMaxAverageBitrate',
|
||||
'p2p',
|
||||
'participantMenuButtonsWithNotifyClick',
|
||||
'participantsPane',
|
||||
'pcStatsInterval',
|
||||
'prejoinConfig',
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { IReduxState } from '../../app/types';
|
||||
import JitsiMeetJS from '../../base/lib-jitsi-meet';
|
||||
import { NOTIFY_CLICK_MODE } from '../../toolbox/constants';
|
||||
|
||||
import { IConfig, IDeeplinkingConfig, IDeeplinkingMobileConfig, IDeeplinkingPlatformConfig } from './configType';
|
||||
import {
|
||||
IConfig,
|
||||
IDeeplinkingConfig,
|
||||
IDeeplinkingMobileConfig,
|
||||
IDeeplinkingPlatformConfig,
|
||||
NotifyClickButton
|
||||
} from './configType';
|
||||
import { TOOLBAR_BUTTONS } from './constants';
|
||||
|
||||
export * from './functions.any';
|
||||
@@ -120,14 +127,21 @@ export function _setDeeplinkingDefaults(deeplinking: IDeeplinkingConfig) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of buttons that have that notify the api when clicked.
|
||||
* Common logic to gather buttons that have to notify the api when clicked.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {Array} - The list of buttons.
|
||||
* @param {Array} buttonsWithNotifyClick - The array of systme buttons that need to notify the api.
|
||||
* @param {Array} customButtons - The custom buttons.
|
||||
* @returns {Array}
|
||||
*/
|
||||
export function getButtonsWithNotifyClick(state: IReduxState): Array<{ key: string; preventExecution: boolean; }> {
|
||||
const { buttonsWithNotifyClick, customToolbarButtons } = state['features/base/config'];
|
||||
const customButtons = customToolbarButtons?.map(({ id }) => {
|
||||
const buildButtonsArray = (
|
||||
buttonsWithNotifyClick?: NotifyClickButton[],
|
||||
customButtons?: {
|
||||
icon: string;
|
||||
id: string;
|
||||
text: string;
|
||||
}[]
|
||||
): NotifyClickButton[] => {
|
||||
const customButtonsWithNotifyClick = customButtons?.map(({ id }) => {
|
||||
return {
|
||||
key: id,
|
||||
preventExecution: false
|
||||
@@ -135,13 +149,69 @@ export function getButtonsWithNotifyClick(state: IReduxState): Array<{ key: stri
|
||||
});
|
||||
|
||||
const buttons = Array.isArray(buttonsWithNotifyClick)
|
||||
? buttonsWithNotifyClick as Array<{ key: string; preventExecution: boolean; }>
|
||||
? buttonsWithNotifyClick as NotifyClickButton[]
|
||||
: [];
|
||||
|
||||
if (customButtons) {
|
||||
buttons.push(...customButtons);
|
||||
if (customButtonsWithNotifyClick) {
|
||||
buttons.push(...customButtonsWithNotifyClick);
|
||||
}
|
||||
|
||||
return buttons;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the list of toolbar buttons that have to notify the api when clicked.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {Array} - The list of buttons.
|
||||
*/
|
||||
export function getButtonsWithNotifyClick(
|
||||
state: IReduxState
|
||||
): NotifyClickButton[] {
|
||||
const { buttonsWithNotifyClick, customToolbarButtons } = state['features/base/config'];
|
||||
|
||||
return buildButtonsArray(
|
||||
buttonsWithNotifyClick,
|
||||
customToolbarButtons
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of participant menu buttons that have that notify the api when clicked.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {Array} - The list of participant menu buttons.
|
||||
*/
|
||||
export function getParticipantMenuButtonsWithNotifyClick(
|
||||
state: IReduxState
|
||||
): NotifyClickButton[] {
|
||||
const { participantMenuButtonsWithNotifyClick, customParticipantMenuButtons } = state['features/base/config'];
|
||||
|
||||
return buildButtonsArray(
|
||||
participantMenuButtonsWithNotifyClick,
|
||||
customParticipantMenuButtons
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the notify mode for the specified button.
|
||||
*
|
||||
* @param {string} buttonKey - The button key.
|
||||
* @param {Array} buttonsWithNotifyClick - The buttons with notify click.
|
||||
* @returns {string|undefined}
|
||||
*/
|
||||
export const getButtonNotifyMode = (
|
||||
buttonKey: string,
|
||||
buttonsWithNotifyClick?: NotifyClickButton[]
|
||||
): string | undefined => {
|
||||
const notify = buttonsWithNotifyClick?.find(
|
||||
(btn: NotifyClickButton) =>
|
||||
(typeof btn === 'string' && btn === buttonKey) || (typeof btn === 'object' && btn.key === buttonKey)
|
||||
);
|
||||
|
||||
if (notify) {
|
||||
return typeof notify === 'string' || notify.preventExecution
|
||||
? NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY
|
||||
: NOTIFY_CLICK_MODE.ONLY_NOTIFY;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -337,12 +337,12 @@ export default class AbstractButton<P extends IProps, S=any> extends Component<P
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onClick(e?: React.MouseEvent<HTMLElement> | GestureResponderEvent) {
|
||||
const { afterClick, handleClick, notifyMode, buttonKey } = this.props;
|
||||
_onClick(e?: React.MouseEvent | GestureResponderEvent) {
|
||||
const { afterClick, buttonKey, handleClick, notifyMode } = this.props;
|
||||
|
||||
if (typeof APP !== 'undefined' && notifyMode) {
|
||||
APP.API.notifyToolbarButtonClicked(
|
||||
buttonKey, notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY
|
||||
buttonKey, notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,12 +4,18 @@ import { useSelector } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import Avatar from '../../../../../base/avatar/components/Avatar';
|
||||
import {
|
||||
getButtonNotifyMode,
|
||||
getParticipantMenuButtonsWithNotifyClick
|
||||
} from '../../../../../base/config/functions.web';
|
||||
import { isLocalParticipantModerator } from '../../../../../base/participants/functions';
|
||||
import ContextMenu from '../../../../../base/ui/components/web/ContextMenu';
|
||||
import ContextMenuItemGroup from '../../../../../base/ui/components/web/ContextMenuItemGroup';
|
||||
import { getBreakoutRooms } from '../../../../../breakout-rooms/functions';
|
||||
import { NOTIFY_CLICK_MODE } from '../../../../../toolbox/constants';
|
||||
import { showOverflowDrawer } from '../../../../../toolbox/functions.web';
|
||||
import SendToRoomButton from '../../../../../video-menu/components/web/SendToRoomButton';
|
||||
import { PARTICIPANT_MENU_BUTTONS as BUTTONS } from '../../../../../video-menu/constants';
|
||||
import { AVATAR_SIZE } from '../../../../constants';
|
||||
|
||||
|
||||
@@ -72,11 +78,30 @@ export const RoomParticipantContextMenu = ({
|
||||
const lowerMenu = useCallback(() => onSelect(true), [ onSelect ]);
|
||||
const rooms: Object = useSelector(getBreakoutRooms);
|
||||
const overflowDrawer = useSelector(showOverflowDrawer);
|
||||
const buttonsWithNotifyClick = useSelector(getParticipantMenuButtonsWithNotifyClick);
|
||||
|
||||
const notifyClick = useCallback(
|
||||
(buttonKey: string, participantId?: string) => {
|
||||
const notifyMode = getButtonNotifyMode(buttonKey, buttonsWithNotifyClick);
|
||||
|
||||
if (!notifyMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
APP.API.notifyParticipantMenuButtonClicked(
|
||||
buttonKey,
|
||||
participantId,
|
||||
notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY
|
||||
);
|
||||
}, [ buttonsWithNotifyClick, getButtonNotifyMode ]);
|
||||
|
||||
const breakoutRoomsButtons = useMemo(() => Object.values(rooms || {}).map((room: any) => {
|
||||
if (room.id !== entity?.room?.id) {
|
||||
return (<SendToRoomButton
|
||||
key = { room.id }
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
notifyClick = { () => notifyClick(BUTTONS.SEND_PARTICIPANT_TO_ROOM, entity?.jid) }
|
||||
notifyMode = { getButtonNotifyMode(BUTTONS.SEND_PARTICIPANT_TO_ROOM, buttonsWithNotifyClick) }
|
||||
onClick = { lowerMenu }
|
||||
participantID = { entity?.jid ?? '' }
|
||||
room = { room } />);
|
||||
|
||||
@@ -4,8 +4,10 @@ import { connect } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { IReduxState, IStore } from '../../../app/types';
|
||||
import { NotifyClickButton } from '../../../base/config/configType';
|
||||
import { VISITORS_MODE_BUTTONS } from '../../../base/config/constants';
|
||||
import {
|
||||
getButtonNotifyMode,
|
||||
getButtonsWithNotifyClick,
|
||||
getToolbarButtons,
|
||||
isToolbarButtonEnabled
|
||||
@@ -22,7 +24,7 @@ import {
|
||||
setToolbarHovered,
|
||||
showToolbox
|
||||
} from '../../actions.web';
|
||||
import { NOTIFY_CLICK_MODE, NOT_APPLICABLE, THRESHOLDS } from '../../constants';
|
||||
import { NOT_APPLICABLE, THRESHOLDS } from '../../constants';
|
||||
import {
|
||||
getAllToolboxButtons,
|
||||
getJwtDisabledButtons,
|
||||
@@ -46,10 +48,7 @@ interface IProps extends WithTranslation {
|
||||
/**
|
||||
* Toolbar buttons which have their click exposed through the API.
|
||||
*/
|
||||
_buttonsWithNotifyClick?: Array<string | {
|
||||
key: string;
|
||||
preventExecution: boolean;
|
||||
}>;
|
||||
_buttonsWithNotifyClick?: NotifyClickButton[];
|
||||
|
||||
/**
|
||||
* Whether or not the chat feature is currently displayed.
|
||||
@@ -262,26 +261,6 @@ const Toolbox = ({
|
||||
}
|
||||
}, [ _hangupMenuVisible, _overflowMenuVisible ]);
|
||||
|
||||
/**
|
||||
* Returns the notify mode of the given toolbox button.
|
||||
*
|
||||
* @param {string} btnName - The toolbar button's name.
|
||||
* @returns {string|undefined} - The button's notify mode.
|
||||
*/
|
||||
function getButtonNotifyMode(btnName: string) {
|
||||
const notify = _buttonsWithNotifyClick?.find(
|
||||
btn =>
|
||||
(typeof btn === 'string' && btn === btnName)
|
||||
|| (typeof btn === 'object' && btn.key === btnName)
|
||||
);
|
||||
|
||||
if (notify) {
|
||||
return typeof notify === 'string' || notify.preventExecution
|
||||
? NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY
|
||||
: NOTIFY_CLICK_MODE.ONLY_NOTIFY;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the notify click mode for the buttons.
|
||||
*
|
||||
@@ -295,7 +274,7 @@ const Toolbox = ({
|
||||
|
||||
Object.values(buttons).forEach((button: any) => {
|
||||
if (typeof button === 'object') {
|
||||
button.notifyMode = getButtonNotifyMode(button.key);
|
||||
button.notifyMode = getButtonNotifyMode(button.key, _buttonsWithNotifyClick);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -452,7 +431,7 @@ const Toolbox = ({
|
||||
ariaControls = 'hangup-menu'
|
||||
isOpen = { _hangupMenuVisible }
|
||||
key = 'hangup-menu'
|
||||
notifyMode = { getButtonNotifyMode('hangup-menu') }
|
||||
notifyMode = { getButtonNotifyMode('hangup-menu', _buttonsWithNotifyClick) }
|
||||
onVisibilityChange = { onSetHangupVisible }>
|
||||
<ContextMenu
|
||||
accessibilityLabel = { t(toolbarAccLabel) }
|
||||
@@ -462,17 +441,20 @@ const Toolbox = ({
|
||||
onKeyDown = { onEscKey }>
|
||||
<EndConferenceButton
|
||||
buttonKey = 'end-meeting'
|
||||
notifyMode = { getButtonNotifyMode('end-meeting') } />
|
||||
notifyMode = { getButtonNotifyMode(
|
||||
'end-meeting',
|
||||
_buttonsWithNotifyClick
|
||||
) } />
|
||||
<LeaveConferenceButton
|
||||
buttonKey = 'hangup'
|
||||
notifyMode = { getButtonNotifyMode('hangup') } />
|
||||
notifyMode = { getButtonNotifyMode('hangup', _buttonsWithNotifyClick) } />
|
||||
</ContextMenu>
|
||||
</HangupMenuButton>
|
||||
: <HangupButton
|
||||
buttonKey = 'hangup'
|
||||
customClass = 'hangup-button'
|
||||
key = 'hangup-button'
|
||||
notifyMode = { getButtonNotifyMode('hangup') }
|
||||
notifyMode = { getButtonNotifyMode('hangup', _buttonsWithNotifyClick) }
|
||||
visible = { isToolbarButtonEnabled('hangup', _toolbarButtons) } />
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -6,27 +6,38 @@ import { approveParticipantAudio, approveParticipantVideo } from '../../../av-mo
|
||||
import { IconMic, IconVideo } from '../../../base/icons/svg';
|
||||
import { MEDIA_TYPE, MediaType } from '../../../base/media/constants';
|
||||
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
|
||||
import { NOTIFY_CLICK_MODE } from '../../../toolbox/constants';
|
||||
import { IButtonProps } from '../../types';
|
||||
|
||||
interface IProps {
|
||||
|
||||
interface IProps extends IButtonProps {
|
||||
buttonType: MediaType;
|
||||
|
||||
/**
|
||||
* The ID for the participant on which the button will act.
|
||||
*/
|
||||
participantID: string;
|
||||
}
|
||||
|
||||
const AskToUnmuteButton = ({ buttonType, participantID }: IProps) => {
|
||||
/**
|
||||
* Implements a React {@link Component} which displays a button that
|
||||
* allows the moderator to request from a participant to mute themselves.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const AskToUnmuteButton = ({
|
||||
buttonType,
|
||||
notifyMode,
|
||||
notifyClick,
|
||||
participantID
|
||||
}: IProps): JSX.Element => {
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const _onClick = useCallback(() => {
|
||||
notifyClick?.();
|
||||
if (notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
|
||||
return;
|
||||
}
|
||||
if (buttonType === MEDIA_TYPE.AUDIO) {
|
||||
dispatch(approveParticipantAudio(participantID));
|
||||
} else if (buttonType === MEDIA_TYPE.VIDEO) {
|
||||
dispatch(approveParticipantVideo(participantID));
|
||||
}
|
||||
}, [ participantID, buttonType ]);
|
||||
}, [ buttonType, dispatch, notifyClick, notifyMode, participantID ]);
|
||||
|
||||
const text = useMemo(() => {
|
||||
if (buttonType === MEDIA_TYPE.AUDIO) {
|
||||
|
||||
@@ -1,43 +1,42 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { IStore } from '../../../app/types';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { IconInfoCircle } from '../../../base/icons/svg';
|
||||
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
|
||||
import { NOTIFY_CLICK_MODE } from '../../../toolbox/constants';
|
||||
import { renderConnectionStatus } from '../../actions.web';
|
||||
import { IButtonProps } from '../../types';
|
||||
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* The Redux dispatch function.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
|
||||
/**
|
||||
* The ID of the participant for which to show connection stats.
|
||||
*/
|
||||
participantId: string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which displays a button that shows
|
||||
* the connection status for the given participant.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const ConnectionStatusButton = ({
|
||||
dispatch,
|
||||
t
|
||||
}: IProps) => {
|
||||
const onClick = useCallback(e => {
|
||||
notifyClick,
|
||||
notifyMode
|
||||
}: IButtonProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleClick = useCallback(e => {
|
||||
e.stopPropagation();
|
||||
notifyClick?.();
|
||||
if (notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
|
||||
return;
|
||||
}
|
||||
dispatch(renderConnectionStatus(true));
|
||||
}, [ dispatch ]);
|
||||
}, [ dispatch, notifyClick, notifyMode ]);
|
||||
|
||||
return (
|
||||
<ContextMenuItem
|
||||
accessibilityLabel = { t('videothumbnail.connectionInfo') }
|
||||
icon = { IconInfoCircle }
|
||||
onClick = { onClick }
|
||||
onClick = { handleClick }
|
||||
text = { t('videothumbnail.connectionInfo') } />
|
||||
);
|
||||
};
|
||||
|
||||
export default translate(connect()(ConnectionStatusButton));
|
||||
export default ConnectionStatusButton;
|
||||
|
||||
@@ -4,15 +4,18 @@ import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import TogglePinToStageButton from '../../../../features/video-menu/components/web/TogglePinToStageButton';
|
||||
import Avatar from '../../../base/avatar/components/Avatar';
|
||||
import { getButtonNotifyMode, getParticipantMenuButtonsWithNotifyClick } from '../../../base/config/functions.web';
|
||||
import { IconPlay } from '../../../base/icons/svg';
|
||||
import { isWhiteboardParticipant } from '../../../base/participants/functions';
|
||||
import { IParticipant } from '../../../base/participants/types';
|
||||
import ContextMenu from '../../../base/ui/components/web/ContextMenu';
|
||||
import ContextMenuItemGroup from '../../../base/ui/components/web/ContextMenuItemGroup';
|
||||
import { stopSharedVideo } from '../../../shared-video/actions.any';
|
||||
import { NOTIFY_CLICK_MODE } from '../../../toolbox/constants';
|
||||
import { showOverflowDrawer } from '../../../toolbox/functions.web';
|
||||
import { setWhiteboardOpen } from '../../../whiteboard/actions';
|
||||
import { WHITEBOARD_ID } from '../../../whiteboard/constants';
|
||||
import { PARTICIPANT_MENU_BUTTONS as BUTTONS } from '../../constants';
|
||||
|
||||
interface IProps {
|
||||
|
||||
@@ -86,6 +89,23 @@ const FakeParticipantContextMenu = ({
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const _overflowDrawer: boolean = useSelector(showOverflowDrawer);
|
||||
const buttonsWithNotifyClick = useSelector(getParticipantMenuButtonsWithNotifyClick);
|
||||
|
||||
const notifyClick = useCallback(
|
||||
(buttonKey: string, participantId?: string) => {
|
||||
const notifyMode = getButtonNotifyMode(buttonKey, buttonsWithNotifyClick);
|
||||
|
||||
if (!notifyMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
APP.API.notifyParticipantMenuButtonClicked(
|
||||
buttonKey,
|
||||
participantId,
|
||||
notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY
|
||||
);
|
||||
}, [ buttonsWithNotifyClick, getButtonNotifyMode ]);
|
||||
|
||||
|
||||
const clickHandler = useCallback(() => onSelect(true), [ onSelect ]);
|
||||
|
||||
@@ -145,6 +165,9 @@ const FakeParticipantContextMenu = ({
|
||||
{isWhiteboardParticipant(participant) && (
|
||||
<TogglePinToStageButton
|
||||
key = 'pinToStage'
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
notifyClick = { () => notifyClick(BUTTONS.PIN_TO_STAGE, WHITEBOARD_ID) }
|
||||
notifyMode = { getButtonNotifyMode(BUTTONS.PIN_TO_STAGE, buttonsWithNotifyClick) }
|
||||
participantID = { WHITEBOARD_ID } />
|
||||
)}
|
||||
</ContextMenuItemGroup>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { IReduxState, IStore } from '../../../app/types';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { updateSettings } from '../../../base/settings/actions';
|
||||
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
|
||||
import { NOTIFY_CLICK_MODE } from '../../../toolbox/constants';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link FlipLocalVideoButton}.
|
||||
@@ -27,6 +28,17 @@ interface IProps extends WithTranslation {
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
|
||||
/**
|
||||
* Callback to execute when the button is clicked.
|
||||
*/
|
||||
notifyClick?: Function;
|
||||
|
||||
/**
|
||||
* Notify mode for `participantMenuButtonClicked` event -
|
||||
* whether to only notify or to also prevent button click routine.
|
||||
*/
|
||||
notifyMode?: string;
|
||||
|
||||
/**
|
||||
* Click handler executed aside from the main action.
|
||||
*/
|
||||
@@ -82,8 +94,12 @@ class FlipLocalVideoButton extends PureComponent<IProps> {
|
||||
* @returns {void}
|
||||
*/
|
||||
_onClick() {
|
||||
const { _localFlipX, dispatch, onClick } = this.props;
|
||||
const { _localFlipX, dispatch, notifyClick, notifyMode, onClick } = this.props;
|
||||
|
||||
notifyClick?.();
|
||||
if (notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
|
||||
return;
|
||||
}
|
||||
onClick?.();
|
||||
dispatch(updateSettings({
|
||||
localFlipX: !_localFlipX
|
||||
|
||||
@@ -1,52 +1,56 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { openDialog } from '../../../base/dialog/actions';
|
||||
import { IconModerator } from '../../../base/icons/svg';
|
||||
import { PARTICIPANT_ROLE } from '../../../base/participants/constants';
|
||||
import { getLocalParticipant, getParticipantById, isParticipantModerator } from '../../../base/participants/functions';
|
||||
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
|
||||
import AbstractGrantModeratorButton, { IProps, _mapStateToProps } from '../AbstractGrantModeratorButton';
|
||||
import { NOTIFY_CLICK_MODE } from '../../../toolbox/constants';
|
||||
import { IButtonProps } from '../../types';
|
||||
|
||||
import GrantModeratorDialog from './GrantModeratorDialog';
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which displays a button for granting
|
||||
* moderator to a participant.
|
||||
*
|
||||
* @returns {JSX.Element|null}
|
||||
*/
|
||||
class GrantModeratorButton extends AbstractGrantModeratorButton {
|
||||
/**
|
||||
* Instantiates a new {@code GrantModeratorButton}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
const GrantModeratorButton = ({
|
||||
notifyClick,
|
||||
notifyMode,
|
||||
participantID
|
||||
}: IButtonProps): JSX.Element | null => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const localParticipant = useSelector(getLocalParticipant);
|
||||
const targetParticipant = useSelector((state: IReduxState) => getParticipantById(state, participantID));
|
||||
const visible = useMemo(() => Boolean(localParticipant?.role === PARTICIPANT_ROLE.MODERATOR)
|
||||
&& !isParticipantModerator(targetParticipant), [ isParticipantModerator, localParticipant, targetParticipant ]);
|
||||
|
||||
this._handleClick = this._handleClick.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { t, visible } = this.props;
|
||||
|
||||
if (!visible) {
|
||||
return null;
|
||||
const handleClick = useCallback(() => {
|
||||
notifyClick?.();
|
||||
if (notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
|
||||
return;
|
||||
}
|
||||
dispatch(openDialog(GrantModeratorDialog, { participantID }));
|
||||
}, [ dispatch, notifyClick, notifyMode, participantID ]);
|
||||
|
||||
return (
|
||||
<ContextMenuItem
|
||||
accessibilityLabel = { t('toolbar.accessibilityLabel.grantModerator') }
|
||||
className = 'grantmoderatorlink'
|
||||
icon = { IconModerator }
|
||||
// eslint-disable-next-line react/jsx-handler-names
|
||||
onClick = { this._handleClick }
|
||||
text = { t('videothumbnail.grantModerator') } />
|
||||
);
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
_handleClick: () => void;
|
||||
}
|
||||
return (
|
||||
<ContextMenuItem
|
||||
accessibilityLabel = { t('toolbar.accessibilityLabel.grantModerator') }
|
||||
className = 'grantmoderatorlink'
|
||||
icon = { IconModerator }
|
||||
onClick = { handleClick }
|
||||
text = { t('videothumbnail.grantModerator') } />
|
||||
);
|
||||
};
|
||||
|
||||
export default translate(connect(_mapStateToProps)(GrantModeratorButton));
|
||||
export default GrantModeratorButton;
|
||||
|
||||
@@ -7,6 +7,7 @@ import { translate } from '../../../base/i18n/functions';
|
||||
import { updateSettings } from '../../../base/settings/actions';
|
||||
import { getHideSelfView } from '../../../base/settings/functions';
|
||||
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
|
||||
import { NOTIFY_CLICK_MODE } from '../../../toolbox/constants';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link HideSelfViewVideoButton}.
|
||||
@@ -28,6 +29,17 @@ interface IProps extends WithTranslation {
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
|
||||
/**
|
||||
* Callback to execute when the button is clicked.
|
||||
*/
|
||||
notifyClick?: Function;
|
||||
|
||||
/**
|
||||
* Notify mode for `participantMenuButtonClicked` event -
|
||||
* whether to only notify or to also prevent button click routine.
|
||||
*/
|
||||
notifyMode?: string;
|
||||
|
||||
/**
|
||||
* Click handler executed aside from the main action.
|
||||
*/
|
||||
@@ -83,8 +95,12 @@ class HideSelfViewVideoButton extends PureComponent<IProps> {
|
||||
* @returns {void}
|
||||
*/
|
||||
_onClick() {
|
||||
const { disableSelfView, dispatch, onClick } = this.props;
|
||||
const { disableSelfView, dispatch, notifyClick, notifyMode, onClick } = this.props;
|
||||
|
||||
notifyClick?.();
|
||||
if (notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
|
||||
return;
|
||||
}
|
||||
onClick?.();
|
||||
dispatch(updateSettings({
|
||||
disableSelfView: !disableSelfView
|
||||
|
||||
@@ -1,54 +1,46 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { openDialog } from '../../../base/dialog/actions';
|
||||
import { IconUserDeleted } from '../../../base/icons/svg';
|
||||
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
|
||||
import AbstractKickButton, { IProps } from '../AbstractKickButton';
|
||||
import { NOTIFY_CLICK_MODE } from '../../../toolbox/constants';
|
||||
import { IButtonProps } from '../../types';
|
||||
|
||||
import KickRemoteParticipantDialog from './KickRemoteParticipantDialog';
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which displays a button for kicking out
|
||||
* a participant from the conference.
|
||||
*
|
||||
* NOTE: At the time of writing this is a button that doesn't use the
|
||||
* {@code AbstractButton} base component, but is inherited from the same
|
||||
* super class ({@code AbstractKickButton} that extends {@code AbstractButton})
|
||||
* for the sake of code sharing between web and mobile. Once web uses the
|
||||
* {@code AbstractButton} base component, this can be fully removed.
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
class KickButton extends AbstractKickButton {
|
||||
/**
|
||||
* Instantiates a new {@code Component}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
const KickButton = ({
|
||||
notifyClick,
|
||||
notifyMode,
|
||||
participantID
|
||||
}: IButtonProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
this._handleClick = this._handleClick.bind(this);
|
||||
}
|
||||
const handleClick = useCallback(() => {
|
||||
notifyClick?.();
|
||||
if (notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
|
||||
return;
|
||||
}
|
||||
dispatch(openDialog(KickRemoteParticipantDialog, { participantID }));
|
||||
}, [ dispatch, notifyClick, notifyMode, participantID ]);
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { participantID, t } = this.props;
|
||||
return (
|
||||
<ContextMenuItem
|
||||
accessibilityLabel = { t('videothumbnail.kick') }
|
||||
className = 'kicklink'
|
||||
icon = { IconUserDeleted }
|
||||
id = { `ejectlink_${participantID}` }
|
||||
onClick = { handleClick }
|
||||
text = { t('videothumbnail.kick') } />
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ContextMenuItem
|
||||
accessibilityLabel = { t('videothumbnail.kick') }
|
||||
className = 'kicklink'
|
||||
icon = { IconUserDeleted }
|
||||
id = { `ejectlink_${participantID}` }
|
||||
// eslint-disable-next-line react/jsx-handler-names
|
||||
onClick = { this._handleClick }
|
||||
text = { t('videothumbnail.kick') } />
|
||||
);
|
||||
}
|
||||
|
||||
_handleClick: () => void;
|
||||
}
|
||||
export default translate(connect()(KickButton));
|
||||
export default KickButton;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { batch, connect } from 'react-redux';
|
||||
import { batch, connect, useSelector } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { IReduxState, IStore } from '../../../app/types';
|
||||
import { getButtonNotifyMode, getParticipantMenuButtonsWithNotifyClick } from '../../../base/config/functions.web';
|
||||
import { isMobileBrowser } from '../../../base/environment/utils';
|
||||
import { IconDotsHorizontal } from '../../../base/icons/svg';
|
||||
import { getLocalParticipant } from '../../../base/participants/functions';
|
||||
@@ -17,7 +18,9 @@ import ContextMenuItemGroup from '../../../base/ui/components/web/ContextMenuIte
|
||||
import ConnectionIndicatorContent from '../../../connection-indicator/components/web/ConnectionIndicatorContent';
|
||||
import { THUMBNAIL_TYPE } from '../../../filmstrip/constants';
|
||||
import { isStageFilmstripAvailable } from '../../../filmstrip/functions.web';
|
||||
import { NOTIFY_CLICK_MODE } from '../../../toolbox/constants';
|
||||
import { renderConnectionStatus } from '../../actions.web';
|
||||
import { PARTICIPANT_MENU_BUTTONS as BUTTONS } from '../../constants';
|
||||
|
||||
import ConnectionStatusButton from './ConnectionStatusButton';
|
||||
import FlipLocalVideoButton from './FlipLocalVideoButton';
|
||||
@@ -139,6 +142,22 @@ const LocalVideoMenuTriggerButton = ({
|
||||
}: IProps) => {
|
||||
const { classes } = useStyles();
|
||||
const { t } = useTranslation();
|
||||
const buttonsWithNotifyClick = useSelector(getParticipantMenuButtonsWithNotifyClick);
|
||||
|
||||
const notifyClick = useCallback(
|
||||
(buttonKey: string) => {
|
||||
const notifyMode = getButtonNotifyMode(buttonKey, buttonsWithNotifyClick);
|
||||
|
||||
if (!notifyMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
APP.API.notifyParticipantMenuButtonClicked(
|
||||
buttonKey,
|
||||
_localParticipantId,
|
||||
notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY
|
||||
);
|
||||
}, [ buttonsWithNotifyClick, getButtonNotifyMode ]);
|
||||
|
||||
const _onPopoverOpen = useCallback(() => {
|
||||
showPopover?.();
|
||||
@@ -164,22 +183,35 @@ const LocalVideoMenuTriggerButton = ({
|
||||
{_showLocalVideoFlipButton
|
||||
&& <FlipLocalVideoButton
|
||||
className = { _overflowDrawer ? classes.flipText : '' }
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
notifyClick = { () => notifyClick(BUTTONS.FLIP_LOCAL_VIDEO) }
|
||||
notifyMode = { getButtonNotifyMode(BUTTONS.FLIP_LOCAL_VIDEO, buttonsWithNotifyClick) }
|
||||
onClick = { hidePopover } />
|
||||
}
|
||||
{_showHideSelfViewButton
|
||||
&& <HideSelfViewVideoButton
|
||||
className = { _overflowDrawer ? classes.flipText : '' }
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
notifyClick = { () => notifyClick(BUTTONS.HIDE_SELF_VIEW) }
|
||||
notifyMode = { getButtonNotifyMode(BUTTONS.HIDE_SELF_VIEW, buttonsWithNotifyClick) }
|
||||
onClick = { hidePopover } />
|
||||
}
|
||||
{
|
||||
_showPinToStage && <TogglePinToStageButton
|
||||
className = { _overflowDrawer ? classes.flipText : '' }
|
||||
noIcon = { true }
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
notifyClick = { () => notifyClick(BUTTONS.PIN_TO_STAGE) }
|
||||
notifyMode = { getButtonNotifyMode(BUTTONS.PIN_TO_STAGE, buttonsWithNotifyClick) }
|
||||
onClick = { hidePopover }
|
||||
participantID = { _localParticipantId } />
|
||||
}
|
||||
{isMobileBrowser()
|
||||
&& <ConnectionStatusButton participantId = { _localParticipantId } />
|
||||
{
|
||||
isMobileBrowser() && <ConnectionStatusButton
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
notifyClick = { () => notifyClick(BUTTONS.CONN_STATUS) }
|
||||
notifyMode = { getButtonNotifyMode(BUTTONS.CONN_STATUS, buttonsWithNotifyClick) }
|
||||
participantID = { _localParticipantId } />
|
||||
}
|
||||
</ContextMenuItemGroup>
|
||||
</ContextMenu>
|
||||
|
||||
@@ -1,59 +1,65 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { createRemoteVideoMenuButtonEvent } from '../../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../../analytics/functions';
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { rejectParticipantAudio } from '../../../av-moderation/actions';
|
||||
import { IconMicSlash } from '../../../base/icons/svg';
|
||||
import { MEDIA_TYPE } from '../../../base/media/constants';
|
||||
import { isRemoteTrackMuted } from '../../../base/tracks/functions.any';
|
||||
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
|
||||
import AbstractMuteButton, { IProps, _mapStateToProps } from '../AbstractMuteButton';
|
||||
|
||||
import { NOTIFY_CLICK_MODE } from '../../../toolbox/constants';
|
||||
import { muteRemote } from '../../actions.any';
|
||||
import { IButtonProps } from '../../types';
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which displays a button for audio muting
|
||||
* a participant in the conference.
|
||||
*
|
||||
* NOTE: At the time of writing this is a button that doesn't use the
|
||||
* {@code AbstractButton} base component, but is inherited from the same
|
||||
* super class ({@code AbstractMuteButton} that extends {@code AbstractButton})
|
||||
* for the sake of code sharing between web and mobile. Once web uses the
|
||||
* {@code AbstractButton} base component, this can be fully removed.
|
||||
* @returns {JSX.Element|null}
|
||||
*/
|
||||
class MuteButton extends AbstractMuteButton {
|
||||
/**
|
||||
* Instantiates a new {@code Component}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
const MuteButton = ({
|
||||
notifyClick,
|
||||
notifyMode,
|
||||
participantID
|
||||
}: IButtonProps): JSX.Element | null => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const tracks = useSelector((state: IReduxState) => state['features/base/tracks']);
|
||||
const audioTrackMuted = useMemo(
|
||||
() => isRemoteTrackMuted(tracks, MEDIA_TYPE.AUDIO, participantID),
|
||||
[ isRemoteTrackMuted, participantID, tracks ]
|
||||
);
|
||||
|
||||
this._handleClick = this._handleClick.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { _audioTrackMuted, t } = this.props;
|
||||
|
||||
if (_audioTrackMuted) {
|
||||
return null;
|
||||
const handleClick = useCallback(() => {
|
||||
notifyClick?.();
|
||||
if (notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
|
||||
return;
|
||||
}
|
||||
sendAnalytics(createRemoteVideoMenuButtonEvent(
|
||||
'mute',
|
||||
{
|
||||
'participant_id': participantID
|
||||
}));
|
||||
|
||||
return (
|
||||
<ContextMenuItem
|
||||
accessibilityLabel = { t('dialog.muteParticipantButton') }
|
||||
className = 'mutelink'
|
||||
icon = { IconMicSlash }
|
||||
// eslint-disable-next-line react/jsx-handler-names
|
||||
onClick = { this._handleClick }
|
||||
text = { t('dialog.muteParticipantButton') } />
|
||||
);
|
||||
dispatch(muteRemote(participantID, MEDIA_TYPE.AUDIO));
|
||||
dispatch(rejectParticipantAudio(participantID));
|
||||
}, [ dispatch, notifyClick, notifyMode, participantID, sendAnalytics ]);
|
||||
|
||||
if (audioTrackMuted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
_handleClick: () => void;
|
||||
}
|
||||
return (
|
||||
<ContextMenuItem
|
||||
accessibilityLabel = { t('dialog.muteParticipantButton') }
|
||||
className = 'mutelink'
|
||||
icon = { IconMicSlash }
|
||||
onClick = { handleClick }
|
||||
text = { t('dialog.muteParticipantButton') } />
|
||||
);
|
||||
};
|
||||
|
||||
export default translate(connect(_mapStateToProps)(MuteButton));
|
||||
export default MuteButton;
|
||||
|
||||
@@ -1,48 +1,48 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { createToolbarEvent } from '../../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../../analytics/functions';
|
||||
import { openDialog } from '../../../base/dialog/actions';
|
||||
import { IconMicSlash } from '../../../base/icons/svg';
|
||||
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
|
||||
import AbstractMuteEveryoneElseButton, { IProps } from '../AbstractMuteEveryoneElseButton';
|
||||
import { NOTIFY_CLICK_MODE } from '../../../toolbox/constants';
|
||||
import { IButtonProps } from '../../types';
|
||||
|
||||
import MuteEveryoneDialog from './MuteEveryoneDialog';
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which displays a button for audio muting
|
||||
* every participant in the conference except the one with the given
|
||||
* participantID.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
class MuteEveryoneElseButton extends AbstractMuteEveryoneElseButton {
|
||||
/**
|
||||
* Instantiates a new {@code Component}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
const MuteEveryoneElseButton = ({
|
||||
notifyClick,
|
||||
notifyMode,
|
||||
participantID
|
||||
}: IButtonProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
this._handleClick = this._handleClick.bind(this);
|
||||
}
|
||||
const handleClick = useCallback(() => {
|
||||
notifyClick?.();
|
||||
if (notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
|
||||
return;
|
||||
}
|
||||
sendAnalytics(createToolbarEvent('mute.everyoneelse.pressed'));
|
||||
dispatch(openDialog(MuteEveryoneDialog, { exclude: [ participantID ] }));
|
||||
}, [ dispatch, notifyMode, notifyClick, participantID, sendAnalytics ]);
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
return (
|
||||
<ContextMenuItem
|
||||
accessibilityLabel = { t('toolbar.accessibilityLabel.muteEveryoneElse') }
|
||||
icon = { IconMicSlash }
|
||||
onClick = { handleClick }
|
||||
text = { t('videothumbnail.domuteOthers') } />
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ContextMenuItem
|
||||
accessibilityLabel = { t('toolbar.accessibilityLabel.muteEveryoneElse') }
|
||||
icon = { IconMicSlash }
|
||||
// eslint-disable-next-line react/jsx-handler-names
|
||||
onClick = { this._handleClick }
|
||||
text = { t('videothumbnail.domuteOthers') } />
|
||||
);
|
||||
}
|
||||
|
||||
_handleClick: () => void;
|
||||
}
|
||||
|
||||
export default translate(connect()(MuteEveryoneElseButton));
|
||||
export default MuteEveryoneElseButton;
|
||||
|
||||
@@ -1,48 +1,48 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { createToolbarEvent } from '../../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../../analytics/functions';
|
||||
import { openDialog } from '../../../base/dialog/actions';
|
||||
import { IconVideoOff } from '../../../base/icons/svg';
|
||||
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
|
||||
import AbstractMuteEveryoneElsesVideoButton, { IProps } from '../AbstractMuteEveryoneElsesVideoButton';
|
||||
import { NOTIFY_CLICK_MODE } from '../../../toolbox/constants';
|
||||
import { IButtonProps } from '../../types';
|
||||
|
||||
import MuteEveryonesVideoDialog from './MuteEveryonesVideoDialog';
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which displays a button for audio muting
|
||||
* every participant in the conference except the one with the given
|
||||
* participantID.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
class MuteEveryoneElsesVideoButton extends AbstractMuteEveryoneElsesVideoButton {
|
||||
/**
|
||||
* Instantiates a new {@code Component}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
const MuteEveryoneElsesVideoButton = ({
|
||||
notifyClick,
|
||||
notifyMode,
|
||||
participantID
|
||||
}: IButtonProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
this._handleClick = this._handleClick.bind(this);
|
||||
}
|
||||
const handleClick = useCallback(() => {
|
||||
notifyClick?.();
|
||||
if (notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
|
||||
return;
|
||||
}
|
||||
sendAnalytics(createToolbarEvent('mute.everyoneelsesvideo.pressed'));
|
||||
dispatch(openDialog(MuteEveryonesVideoDialog, { exclude: [ participantID ] }));
|
||||
}, [ notifyClick, notifyMode, participantID ]);
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
return (
|
||||
<ContextMenuItem
|
||||
accessibilityLabel = { t('toolbar.accessibilityLabel.muteEveryoneElsesVideoStream') }
|
||||
icon = { IconVideoOff }
|
||||
onClick = { handleClick }
|
||||
text = { t('videothumbnail.domuteVideoOfOthers') } />
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ContextMenuItem
|
||||
accessibilityLabel = { t('toolbar.accessibilityLabel.muteEveryoneElsesVideoStream') }
|
||||
icon = { IconVideoOff }
|
||||
// eslint-disable-next-line react/jsx-handler-names
|
||||
onClick = { this._handleClick }
|
||||
text = { t('videothumbnail.domuteVideoOfOthers') } />
|
||||
);
|
||||
}
|
||||
|
||||
_handleClick: () => void;
|
||||
}
|
||||
|
||||
export default translate(connect()(MuteEveryoneElsesVideoButton));
|
||||
export default MuteEveryoneElsesVideoButton;
|
||||
|
||||
@@ -1,58 +1,66 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { createRemoteVideoMenuButtonEvent } from '../../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../../analytics/functions';
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { openDialog } from '../../../base/dialog/actions';
|
||||
import { IconVideoOff } from '../../../base/icons/svg';
|
||||
import { MEDIA_TYPE } from '../../../base/media/constants';
|
||||
import { isRemoteTrackMuted } from '../../../base/tracks/functions.any';
|
||||
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
|
||||
import AbstractMuteVideoButton, { IProps, _mapStateToProps } from '../AbstractMuteVideoButton';
|
||||
import { NOTIFY_CLICK_MODE } from '../../../toolbox/constants';
|
||||
import { IButtonProps } from '../../types';
|
||||
|
||||
import MuteRemoteParticipantsVideoDialog from './MuteRemoteParticipantsVideoDialog';
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which displays a button for disabling
|
||||
* the camera of a participant in the conference.
|
||||
*
|
||||
* NOTE: At the time of writing this is a button that doesn't use the
|
||||
* {@code AbstractButton} base component, but is inherited from the same
|
||||
* super class ({@code AbstractMuteVideoButton} that extends {@code AbstractButton})
|
||||
* for the sake of code sharing between web and mobile. Once web uses the
|
||||
* {@code AbstractButton} base component, this can be fully removed.
|
||||
* @returns {JSX.Element|null}
|
||||
*/
|
||||
class MuteVideoButton extends AbstractMuteVideoButton {
|
||||
/**
|
||||
* Instantiates a new {@code Component}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
const MuteVideoButton = ({
|
||||
notifyClick,
|
||||
notifyMode,
|
||||
participantID
|
||||
}: IButtonProps): JSX.Element | null => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const tracks = useSelector((state: IReduxState) => state['features/base/tracks']);
|
||||
|
||||
this._handleClick = this._handleClick.bind(this);
|
||||
}
|
||||
const videoTrackMuted = useMemo(
|
||||
() => isRemoteTrackMuted(tracks, MEDIA_TYPE.VIDEO, participantID),
|
||||
[ isRemoteTrackMuted, participantID, tracks ]
|
||||
);
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { _videoTrackMuted, t } = this.props;
|
||||
|
||||
if (_videoTrackMuted) {
|
||||
return null;
|
||||
const handleClick = useCallback(() => {
|
||||
notifyClick?.();
|
||||
if (notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
|
||||
return;
|
||||
}
|
||||
sendAnalytics(createRemoteVideoMenuButtonEvent(
|
||||
'video.mute.button',
|
||||
{
|
||||
'participant_id': participantID
|
||||
}));
|
||||
|
||||
return (
|
||||
<ContextMenuItem
|
||||
accessibilityLabel = { t('participantsPane.actions.stopVideo') }
|
||||
className = 'mutevideolink'
|
||||
icon = { IconVideoOff }
|
||||
// eslint-disable-next-line react/jsx-handler-names
|
||||
onClick = { this._handleClick }
|
||||
text = { t('participantsPane.actions.stopVideo') } />
|
||||
);
|
||||
dispatch(openDialog(MuteRemoteParticipantsVideoDialog, { participantID }));
|
||||
}, [ dispatch, notifyClick, notifyClick, participantID, sendAnalytics ]);
|
||||
|
||||
if (videoTrackMuted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
_handleClick: () => void;
|
||||
}
|
||||
return (
|
||||
<ContextMenuItem
|
||||
accessibilityLabel = { t('participantsPane.actions.stopVideo') }
|
||||
className = 'mutevideolink'
|
||||
icon = { IconVideoOff }
|
||||
onClick = { handleClick }
|
||||
text = { t('participantsPane.actions.stopVideo') } />
|
||||
);
|
||||
};
|
||||
|
||||
export default translate(connect(_mapStateToProps)(MuteVideoButton));
|
||||
export default MuteVideoButton;
|
||||
|
||||
@@ -6,6 +6,7 @@ import { makeStyles } from 'tss-react/mui';
|
||||
import { IReduxState, IStore } from '../../../app/types';
|
||||
import { isSupported as isAvModerationSupported } from '../../../av-moderation/functions';
|
||||
import Avatar from '../../../base/avatar/components/Avatar';
|
||||
import { getButtonNotifyMode, getParticipantMenuButtonsWithNotifyClick } from '../../../base/config/functions.web';
|
||||
import { isIosMobileBrowser, isMobileBrowser } from '../../../base/environment/utils';
|
||||
import { MEDIA_TYPE } from '../../../base/media/constants';
|
||||
import { PARTICIPANT_ROLE } from '../../../base/participants/constants';
|
||||
@@ -15,14 +16,17 @@ import { isParticipantAudioMuted, isParticipantVideoMuted } from '../../../base/
|
||||
import ContextMenu from '../../../base/ui/components/web/ContextMenu';
|
||||
import ContextMenuItemGroup from '../../../base/ui/components/web/ContextMenuItemGroup';
|
||||
import { getBreakoutRooms, getCurrentRoomId, isInBreakoutRoom } from '../../../breakout-rooms/functions';
|
||||
import { IRoom } from '../../../breakout-rooms/types';
|
||||
import { displayVerification } from '../../../e2ee/functions';
|
||||
import { setVolume } from '../../../filmstrip/actions.web';
|
||||
import { isStageFilmstripAvailable } from '../../../filmstrip/functions.web';
|
||||
import { QUICK_ACTION_BUTTON } from '../../../participants-pane/constants';
|
||||
import { getQuickActionButtonType, isForceMuted } from '../../../participants-pane/functions';
|
||||
import { requestRemoteControl, stopController } from '../../../remote-control/actions';
|
||||
import { NOTIFY_CLICK_MODE } from '../../../toolbox/constants';
|
||||
import { showOverflowDrawer } from '../../../toolbox/functions.web';
|
||||
import { iAmVisitor } from '../../../visitors/functions';
|
||||
import { PARTICIPANT_MENU_BUTTONS as BUTTONS } from '../../constants';
|
||||
|
||||
import AskToUnmuteButton from './AskToUnmuteButton';
|
||||
import ConnectionStatusButton from './ConnectionStatusButton';
|
||||
@@ -145,16 +149,15 @@ const ParticipantContextMenu = ({
|
||||
const isModerationSupported = useSelector((state: IReduxState) => isAvModerationSupported()(state));
|
||||
const stageFilmstrip = useSelector(isStageFilmstripAvailable);
|
||||
const shouldDisplayVerification = useSelector((state: IReduxState) => displayVerification(state, participant?.id));
|
||||
const buttonsWithNotifyClick = useSelector(getParticipantMenuButtonsWithNotifyClick);
|
||||
|
||||
const _currentRoomId = useSelector(getCurrentRoomId);
|
||||
const _rooms = Object.values(useSelector(getBreakoutRooms));
|
||||
const _rooms: IRoom[] = Object.values(useSelector(getBreakoutRooms));
|
||||
|
||||
const _onVolumeChange = useCallback(value => {
|
||||
dispatch(setVolume(participant.id, value));
|
||||
}, [ setVolume, dispatch ]);
|
||||
|
||||
const clickHandler = useCallback(() => onSelect(true), [ onSelect ]);
|
||||
|
||||
const _getCurrentParticipantId = useCallback(() => {
|
||||
const drawer = _overflowDrawer && !thumbnailMenu;
|
||||
|
||||
@@ -162,6 +165,25 @@ const ParticipantContextMenu = ({
|
||||
}
|
||||
, [ thumbnailMenu, _overflowDrawer, drawerParticipant, participant ]);
|
||||
|
||||
const notifyClick = useCallback(
|
||||
(buttonKey: string) => {
|
||||
const notifyMode = getButtonNotifyMode(buttonKey, buttonsWithNotifyClick);
|
||||
|
||||
if (!notifyMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
APP.API.notifyParticipantMenuButtonClicked(
|
||||
buttonKey,
|
||||
_getCurrentParticipantId(),
|
||||
notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY
|
||||
);
|
||||
}, [ buttonsWithNotifyClick, getButtonNotifyMode, _getCurrentParticipantId ]);
|
||||
|
||||
const onBreakoutRoomButtonClick = useCallback(() => {
|
||||
onSelect(true);
|
||||
}, [ onSelect ]);
|
||||
|
||||
const isClickedFromParticipantPane = useMemo(
|
||||
() => !_overflowDrawer && !thumbnailMenu,
|
||||
[ _overflowDrawer, thumbnailMenu ]);
|
||||
@@ -177,128 +199,98 @@ const ParticipantContextMenu = ({
|
||||
&& typeof _volume === 'number'
|
||||
&& !isNaN(_volume);
|
||||
|
||||
const getButtonProps = useCallback((key: string) => {
|
||||
const notifyMode = getButtonNotifyMode(key, buttonsWithNotifyClick);
|
||||
const shouldNotifyClick = notifyMode !== NOTIFY_CLICK_MODE.ONLY_NOTIFY
|
||||
|| notifyMode !== NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY;
|
||||
|
||||
return {
|
||||
key,
|
||||
notifyMode,
|
||||
notifyClick: shouldNotifyClick ? () => notifyClick(key) : undefined,
|
||||
participantID: _getCurrentParticipantId()
|
||||
};
|
||||
}, [ _getCurrentParticipantId, buttonsWithNotifyClick, getButtonNotifyMode, notifyClick ]);
|
||||
|
||||
if (_isModerator) {
|
||||
if (isModerationSupported) {
|
||||
if (_isAudioMuted
|
||||
&& !(isClickedFromParticipantPane && quickActionButtonType === QUICK_ACTION_BUTTON.ASK_TO_UNMUTE)) {
|
||||
buttons.push(<AskToUnmuteButton
|
||||
buttonType = { MEDIA_TYPE.AUDIO }
|
||||
key = 'ask-unmute'
|
||||
participantID = { _getCurrentParticipantId() } />
|
||||
{ ...getButtonProps(BUTTONS.ASK_UNMUTE) }
|
||||
buttonType = { MEDIA_TYPE.AUDIO } />
|
||||
);
|
||||
}
|
||||
if (_isVideoForceMuted
|
||||
&& !(isClickedFromParticipantPane && quickActionButtonType === QUICK_ACTION_BUTTON.ALLOW_VIDEO)) {
|
||||
buttons.push(<AskToUnmuteButton
|
||||
buttonType = { MEDIA_TYPE.VIDEO }
|
||||
key = 'allow-video'
|
||||
participantID = { _getCurrentParticipantId() } />
|
||||
{ ...getButtonProps(BUTTONS.ALLOW_VIDEO) }
|
||||
buttonType = { MEDIA_TYPE.VIDEO } />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!disableRemoteMute) {
|
||||
if (!(isClickedFromParticipantPane && quickActionButtonType === QUICK_ACTION_BUTTON.MUTE)) {
|
||||
buttons.push(
|
||||
<MuteButton
|
||||
key = 'mute'
|
||||
participantID = { _getCurrentParticipantId() } />
|
||||
);
|
||||
buttons.push(<MuteButton { ...getButtonProps(BUTTONS.MUTE) } />);
|
||||
}
|
||||
buttons.push(
|
||||
<MuteEveryoneElseButton
|
||||
key = 'mute-others'
|
||||
participantID = { _getCurrentParticipantId() } />
|
||||
);
|
||||
buttons.push(<MuteEveryoneElseButton { ...getButtonProps(BUTTONS.MUTE_OTHERS) } />);
|
||||
if (!(isClickedFromParticipantPane && quickActionButtonType === QUICK_ACTION_BUTTON.STOP_VIDEO)) {
|
||||
buttons.push(
|
||||
<MuteVideoButton
|
||||
key = 'mute-video'
|
||||
participantID = { _getCurrentParticipantId() } />
|
||||
);
|
||||
buttons.push(<MuteVideoButton { ...getButtonProps(BUTTONS.MUTE_VIDEO) } />);
|
||||
}
|
||||
buttons.push(
|
||||
<MuteEveryoneElsesVideoButton
|
||||
key = 'mute-others-video'
|
||||
participantID = { _getCurrentParticipantId() } />
|
||||
);
|
||||
buttons.push(<MuteEveryoneElsesVideoButton { ...getButtonProps(BUTTONS.MUTE_OTHERS_VIDEO) } />);
|
||||
}
|
||||
|
||||
if (!disableGrantModerator && !isBreakoutRoom) {
|
||||
buttons2.push(
|
||||
<GrantModeratorButton
|
||||
key = 'grant-moderator'
|
||||
participantID = { _getCurrentParticipantId() } />
|
||||
);
|
||||
buttons2.push(<GrantModeratorButton { ...getButtonProps(BUTTONS.GRANT_MODERATOR) } />);
|
||||
}
|
||||
|
||||
if (!disableKick) {
|
||||
buttons2.push(
|
||||
<KickButton
|
||||
key = 'kick'
|
||||
participantID = { _getCurrentParticipantId() } />
|
||||
);
|
||||
buttons2.push(<KickButton { ...getButtonProps(BUTTONS.KICK) } />);
|
||||
}
|
||||
|
||||
if (shouldDisplayVerification) {
|
||||
buttons2.push(
|
||||
<VerifyParticipantButton
|
||||
key = 'verify'
|
||||
participantID = { _getCurrentParticipantId() } />
|
||||
);
|
||||
buttons2.push(<VerifyParticipantButton { ...getButtonProps(BUTTONS.VERIFY) } />);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (stageFilmstrip) {
|
||||
buttons2.push(<TogglePinToStageButton
|
||||
key = 'pinToStage'
|
||||
participantID = { _getCurrentParticipantId() } />);
|
||||
buttons2.push(<TogglePinToStageButton { ...getButtonProps(BUTTONS.PIN_TO_STAGE) } />);
|
||||
}
|
||||
|
||||
if (!disablePrivateChat && !visitorsMode) {
|
||||
buttons2.push(<PrivateMessageMenuButton
|
||||
key = 'privateMessage'
|
||||
participantID = { _getCurrentParticipantId() } />
|
||||
);
|
||||
buttons2.push(<PrivateMessageMenuButton { ...getButtonProps(BUTTONS.PRIVATE_MESSAGE) } />);
|
||||
}
|
||||
|
||||
if (thumbnailMenu && isMobileBrowser()) {
|
||||
buttons2.push(
|
||||
<ConnectionStatusButton
|
||||
key = 'conn-status'
|
||||
participantId = { _getCurrentParticipantId() } />
|
||||
);
|
||||
buttons2.push(<ConnectionStatusButton { ...getButtonProps(BUTTONS.CONN_STATUS) } />);
|
||||
}
|
||||
|
||||
if (thumbnailMenu && remoteControlState) {
|
||||
let onRemoteControlToggle = null;
|
||||
const onRemoteControlToggle = useCallback(() => {
|
||||
if (remoteControlState === REMOTE_CONTROL_MENU_STATES.STARTED) {
|
||||
dispatch(stopController(true));
|
||||
} else if (remoteControlState === REMOTE_CONTROL_MENU_STATES.NOT_STARTED) {
|
||||
dispatch(requestRemoteControl(_getCurrentParticipantId()));
|
||||
}
|
||||
}, [ dispatch, remoteControlState, stopController, requestRemoteControl ]);
|
||||
|
||||
if (remoteControlState === REMOTE_CONTROL_MENU_STATES.STARTED) {
|
||||
onRemoteControlToggle = () => dispatch(stopController(true));
|
||||
} else if (remoteControlState === REMOTE_CONTROL_MENU_STATES.NOT_STARTED) {
|
||||
onRemoteControlToggle = () => dispatch(requestRemoteControl(_getCurrentParticipantId()));
|
||||
}
|
||||
|
||||
buttons2.push(
|
||||
<RemoteControlButton
|
||||
key = 'remote-control'
|
||||
onClick = { onRemoteControlToggle }
|
||||
participantID = { _getCurrentParticipantId() }
|
||||
remoteControlState = { remoteControlState } />
|
||||
buttons2.push(<RemoteControlButton
|
||||
{ ...getButtonProps(BUTTONS.REMOTE_CONTROL) }
|
||||
onClick = { onRemoteControlToggle }
|
||||
remoteControlState = { remoteControlState } />
|
||||
);
|
||||
}
|
||||
|
||||
if (customParticipantMenuButtons) {
|
||||
customParticipantMenuButtons.forEach(
|
||||
({ icon, id, text }) => {
|
||||
const onClick = useCallback(
|
||||
() => APP.API.notifyParticipantMenuButtonClicked(id, _getCurrentParticipantId()), []);
|
||||
|
||||
buttons2.push(
|
||||
<CustomOptionButton
|
||||
icon = { icon }
|
||||
key = { id }
|
||||
onClick = { onClick }
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick = { () => notifyClick(id) }
|
||||
text = { text } />
|
||||
);
|
||||
}
|
||||
@@ -312,9 +304,9 @@ const ParticipantContextMenu = ({
|
||||
if (room.id !== _currentRoomId) {
|
||||
breakoutRoomsButtons.push(
|
||||
<SendToRoomButton
|
||||
{ ...getButtonProps(BUTTONS.SEND_PARTICIPANT_TO_ROOM) }
|
||||
key = { room.id }
|
||||
onClick = { clickHandler }
|
||||
participantID = { _getCurrentParticipantId() }
|
||||
onClick = { onBreakoutRoomButtonClick }
|
||||
room = { room } />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,10 +11,11 @@ import { getParticipantById } from '../../../base/participants/functions';
|
||||
import { IParticipant } from '../../../base/participants/types';
|
||||
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
|
||||
import { openChat } from '../../../chat/actions.web';
|
||||
import { IProps as AbstractProps } from '../../../chat/components/web/PrivateMessageButton';
|
||||
import { NOTIFY_CLICK_MODE } from '../../../toolbox/constants';
|
||||
import { isButtonEnabled } from '../../../toolbox/functions.web';
|
||||
import { IButtonProps } from '../../types';
|
||||
|
||||
interface IProps extends AbstractProps, WithTranslation {
|
||||
interface IProps extends IButtonProps, WithTranslation {
|
||||
|
||||
/**
|
||||
* True if the private chat functionality is disabled, hence the button is not visible.
|
||||
@@ -57,7 +58,7 @@ class PrivateMessageMenuButton extends Component<IProps> {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { t, _hidden } = this.props;
|
||||
const { _hidden, t } = this.props;
|
||||
|
||||
if (_hidden) {
|
||||
return null;
|
||||
@@ -75,11 +76,16 @@ class PrivateMessageMenuButton extends Component<IProps> {
|
||||
/**
|
||||
* Callback to be invoked on pressing the button.
|
||||
*
|
||||
* @param {React.MouseEvent|undefined} e - The click event.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onClick() {
|
||||
const { dispatch, _participant } = this.props;
|
||||
const { _participant, dispatch, notifyClick, notifyMode } = this.props;
|
||||
|
||||
notifyClick?.();
|
||||
if (notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
|
||||
return;
|
||||
}
|
||||
dispatch(openChat(_participant));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { sendAnalytics } from '../../../analytics/functions';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { IconRemoteControlStart, IconRemoteControlStop } from '../../../base/icons/svg';
|
||||
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
|
||||
import { NOTIFY_CLICK_MODE } from '../../../toolbox/constants';
|
||||
|
||||
// TODO: Move these enums into the store after further reactification of the
|
||||
// non-react RemoteVideo component.
|
||||
@@ -21,6 +22,17 @@ export const REMOTE_CONTROL_MENU_STATES = {
|
||||
*/
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* Callback to execute when the button is clicked.
|
||||
*/
|
||||
notifyClick?: Function;
|
||||
|
||||
/**
|
||||
* Notify mode for `participantMenuButtonClicked` event -
|
||||
* whether to only notify or to also prevent button click routine.
|
||||
*/
|
||||
notifyMode?: string;
|
||||
|
||||
/**
|
||||
* The callback to invoke when the component is clicked.
|
||||
*/
|
||||
@@ -66,10 +78,7 @@ class RemoteControlButton extends Component<IProps> {
|
||||
* @returns {null|ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
remoteControlState,
|
||||
t
|
||||
} = this.props;
|
||||
const { remoteControlState, t } = this.props;
|
||||
|
||||
let disabled = false, icon;
|
||||
|
||||
@@ -110,7 +119,12 @@ class RemoteControlButton extends Component<IProps> {
|
||||
* @returns {void}
|
||||
*/
|
||||
_onClick() {
|
||||
const { onClick, participantID, remoteControlState } = this.props;
|
||||
const { notifyClick, notifyMode, onClick, participantID, remoteControlState } = this.props;
|
||||
|
||||
notifyClick?.();
|
||||
if (notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: What do we do in case the state is e.g. "requesting"?
|
||||
if (remoteControlState === REMOTE_CONTROL_MENU_STATES.STARTED
|
||||
@@ -126,10 +140,8 @@ class RemoteControlButton extends Component<IProps> {
|
||||
'participant_id': participantID
|
||||
}));
|
||||
}
|
||||
onClick?.();
|
||||
|
||||
if (onClick) {
|
||||
onClick();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,33 +8,40 @@ import { IconRingGroup } from '../../../base/icons/svg';
|
||||
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
|
||||
import { sendParticipantToRoom } from '../../../breakout-rooms/actions';
|
||||
import { IRoom } from '../../../breakout-rooms/types';
|
||||
import { NOTIFY_CLICK_MODE } from '../../../toolbox/constants';
|
||||
import { IButtonProps } from '../../types';
|
||||
|
||||
interface IProps {
|
||||
interface IProps extends IButtonProps {
|
||||
|
||||
/**
|
||||
* Click handler.
|
||||
*/
|
||||
onClick?: Function;
|
||||
|
||||
/**
|
||||
* The ID for the participant on which the button will act.
|
||||
*/
|
||||
participantID: string;
|
||||
|
||||
/**
|
||||
* The room to send the participant to.
|
||||
*/
|
||||
room: IRoom;
|
||||
}
|
||||
|
||||
const SendToRoomButton = ({ onClick, participantID, room }: IProps) => {
|
||||
const SendToRoomButton = ({
|
||||
notifyClick,
|
||||
notifyMode,
|
||||
onClick,
|
||||
participantID,
|
||||
room
|
||||
}: IProps) => {
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const _onClick = useCallback(() => {
|
||||
notifyClick?.();
|
||||
if (notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
|
||||
return;
|
||||
}
|
||||
onClick?.();
|
||||
sendAnalytics(createBreakoutRoomsEvent('send.participant.to.room'));
|
||||
dispatch(sendParticipantToRoom(participantID, room.id));
|
||||
}, [ participantID, room ]);
|
||||
}, [ dispatch, notifyClick, notifyMode, onClick, participantID, room, sendAnalytics ]);
|
||||
|
||||
const roomName = room.name || t('breakoutRooms.mainRoom');
|
||||
|
||||
|
||||
@@ -6,8 +6,10 @@ import { IconPin, IconPinned } from '../../../base/icons/svg';
|
||||
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
|
||||
import { togglePinStageParticipant } from '../../../filmstrip/actions.web';
|
||||
import { getPinnedActiveParticipants } from '../../../filmstrip/functions.web';
|
||||
import { NOTIFY_CLICK_MODE } from '../../../toolbox/constants';
|
||||
import { IButtonProps } from '../../types';
|
||||
|
||||
interface IProps {
|
||||
interface IProps extends IButtonProps {
|
||||
|
||||
/**
|
||||
* Button text class name.
|
||||
@@ -23,22 +25,28 @@ interface IProps {
|
||||
* Click handler executed aside from the main action.
|
||||
*/
|
||||
onClick?: Function;
|
||||
|
||||
/**
|
||||
* The ID for the participant on which the button will act.
|
||||
*/
|
||||
participantID: string;
|
||||
}
|
||||
|
||||
const TogglePinToStageButton = ({ className, noIcon = false, onClick, participantID }: IProps) => {
|
||||
const TogglePinToStageButton = ({
|
||||
className,
|
||||
noIcon = false,
|
||||
notifyClick,
|
||||
notifyMode,
|
||||
onClick,
|
||||
participantID
|
||||
}: IProps): JSX.Element => {
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const isActive = Boolean(useSelector(getPinnedActiveParticipants)
|
||||
.find(p => p.participantId === participantID));
|
||||
const _onClick = useCallback(() => {
|
||||
notifyClick?.();
|
||||
if (notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
|
||||
return;
|
||||
}
|
||||
dispatch(togglePinStageParticipant(participantID));
|
||||
onClick?.();
|
||||
}, [ participantID, isActive ]);
|
||||
}, [ dispatch, isActive, notifyClick, onClick, participantID ]);
|
||||
|
||||
const text = isActive
|
||||
? t('videothumbnail.unpinFromStage')
|
||||
|
||||
@@ -1,38 +1,34 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { IReduxState, IStore } from '../../../app/types';
|
||||
import { IconCheck } from '../../../base/icons/svg';
|
||||
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
|
||||
import { startVerification } from '../../../e2ee/actions';
|
||||
import { NOTIFY_CLICK_MODE } from '../../../toolbox/constants';
|
||||
import { IButtonProps } from '../../types';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of
|
||||
* {@link VerifyParticipantButton}.
|
||||
* Implements a React {@link Component} which displays a button that
|
||||
* verifies the participant.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* The redux {@code dispatch} function.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
|
||||
/**
|
||||
* The ID of the participant that this button is supposed to verified.
|
||||
*/
|
||||
participantID: string;
|
||||
}
|
||||
|
||||
const VerifyParticipantButton = ({
|
||||
dispatch,
|
||||
notifyClick,
|
||||
notifyMode,
|
||||
participantID
|
||||
}: IProps) => {
|
||||
}: IButtonProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const _handleClick = useCallback(() => {
|
||||
notifyClick?.();
|
||||
if (notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
|
||||
return;
|
||||
}
|
||||
dispatch(startVerification(participantID));
|
||||
}, [ participantID ]);
|
||||
}, [ dispatch, notifyClick, notifyMode, participantID ]);
|
||||
|
||||
return (
|
||||
<ContextMenuItem
|
||||
@@ -46,20 +42,4 @@ const VerifyParticipantButton = ({
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated {@code RemoteVideoMenuTriggerButton}'s props.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @param {Object} ownProps - The own props of the component.
|
||||
* @private
|
||||
* @returns {IProps}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState, ownProps: Partial<IProps>) {
|
||||
const { participantID } = ownProps;
|
||||
|
||||
return {
|
||||
_participantID: participantID
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(VerifyParticipantButton);
|
||||
export default VerifyParticipantButton;
|
||||
|
||||
@@ -12,3 +12,25 @@ export const NATIVE_VOLUME_SLIDER_SCALE = 19;
|
||||
* recognizes whole numbers.
|
||||
*/
|
||||
export const VOLUME_SLIDER_SCALE = 100;
|
||||
|
||||
/**
|
||||
* Participant context menu button keys.
|
||||
*/
|
||||
export const PARTICIPANT_MENU_BUTTONS = {
|
||||
ALLOW_VIDEO: 'allow-video',
|
||||
ASK_UNMUTE: 'ask-unmute',
|
||||
CONN_STATUS: 'conn-status',
|
||||
FLIP_LOCAL_VIDEO: 'flip-local-video',
|
||||
GRANT_MODERATOR: 'grant-moderator',
|
||||
HIDE_SELF_VIEW: 'hide-self-view',
|
||||
KICK: 'kick',
|
||||
MUTE: 'mute',
|
||||
MUTE_OTHERS: 'mute-others',
|
||||
MUTE_OTHERS_VIDEO: 'mute-others-video',
|
||||
MUTE_VIDEO: 'mute-video',
|
||||
PIN_TO_STAGE: 'pinToStage',
|
||||
PRIVATE_MESSAGE: 'privateMessage',
|
||||
REMOTE_CONTROL: 'remote-control',
|
||||
SEND_PARTICIPANT_TO_ROOM: 'send-participant-to-room',
|
||||
VERIFY: 'verify'
|
||||
};
|
||||
|
||||
18
react/features/video-menu/types.ts
Normal file
18
react/features/video-menu/types.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export interface IButtonProps {
|
||||
|
||||
/**
|
||||
* Callback to execute when the button is clicked.
|
||||
*/
|
||||
notifyClick?: Function;
|
||||
|
||||
/**
|
||||
* Notify mode for the `participantMenuButtonClicked` event -
|
||||
* whether to only notify or to also prevent button click routine.
|
||||
*/
|
||||
notifyMode?: string;
|
||||
|
||||
/**
|
||||
* The ID of the participant that's linked to the button.
|
||||
*/
|
||||
participantID: string;
|
||||
}
|
||||
Reference in New Issue
Block a user