feat(external-api) extend event to listen to system buttons and add config to prevent execution

pull/13596/head jitsi-meet_8833
Mihaela Dumitru 1 year ago committed by GitHub
parent 470e987fad
commit 1b7a81afa5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 36
      config.js
  2. 8
      modules/API/API.js
  3. 31
      react/features/base/config/configType.ts
  4. 1
      react/features/base/config/configWhitelist.ts
  5. 92
      react/features/base/config/functions.web.ts
  6. 6
      react/features/base/toolbox/components/AbstractButton.tsx
  7. 25
      react/features/participants-pane/components/breakout-rooms/components/web/RoomParticipantContextMenu.tsx
  8. 42
      react/features/toolbox/components/web/Toolbox.tsx
  9. 29
      react/features/video-menu/components/web/AskToUnmuteButton.tsx
  10. 51
      react/features/video-menu/components/web/ConnectionStatusButton.tsx
  11. 23
      react/features/video-menu/components/web/FakeParticipantContextMenu.tsx
  12. 18
      react/features/video-menu/components/web/FlipLocalVideoButton.tsx
  13. 84
      react/features/video-menu/components/web/GrantModeratorButton.tsx
  14. 18
      react/features/video-menu/components/web/HideSelfViewVideoButton.tsx
  15. 76
      react/features/video-menu/components/web/KickButton.tsx
  16. 38
      react/features/video-menu/components/web/LocalVideoMenuTriggerButton.tsx
  17. 94
      react/features/video-menu/components/web/MuteButton.tsx
  18. 72
      react/features/video-menu/components/web/MuteEveryoneElseButton.tsx
  19. 72
      react/features/video-menu/components/web/MuteEveryoneElsesVideoButton.tsx
  20. 94
      react/features/video-menu/components/web/MuteVideoButton.tsx
  21. 144
      react/features/video-menu/components/web/ParticipantContextMenu.tsx
  22. 14
      react/features/video-menu/components/web/PrivateMessageMenuButton.tsx
  23. 28
      react/features/video-menu/components/web/RemoteControlButton.tsx
  24. 23
      react/features/video-menu/components/web/SendToRoomButton.tsx
  25. 24
      react/features/video-menu/components/web/TogglePinToStageButton.tsx
  26. 54
      react/features/video-menu/components/web/VerifyParticipantButton.tsx
  27. 22
      react/features/video-menu/constants.ts
  28. 18
      react/features/video-menu/types.ts

@ -826,6 +826,42 @@ var config = {
// 'whiteboard', // '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: // 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' // 'microphone', 'camera', 'select-background', 'invite', 'settings'
// hiddenPremeetingButtons: [], // hiddenPremeetingButtons: [],

@ -2002,14 +2002,16 @@ class API {
* Notify external application ( if API is enabled) that a participant menu button was clicked. * 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} 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} * @returns {void}
*/ */
notifyParticipantMenuButtonClicked(key, participantId) { notifyParticipantMenuButtonClicked(key, participantId, preventExecution) {
this._sendEvent({ this._sendEvent({
name: 'participant-menu-button-clicked', name: 'participant-menu-button-clicked',
key, key,
participantId participantId,
preventExecution
}); });
} }

@ -68,6 +68,33 @@ type ButtonsWithNotifyClick = 'camera' |
'add-passcode' | 'add-passcode' |
'__end'; '__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' | export type Sounds = 'ASKED_TO_UNMUTE_SOUND' |
'E2EE_OFF_SOUND' | 'E2EE_OFF_SOUND' |
'E2EE_ON_SOUND' | 'E2EE_ON_SOUND' |
@ -468,6 +495,10 @@ export interface IConfig {
mobileCodecPreferenceOrder?: Array<string>; mobileCodecPreferenceOrder?: Array<string>;
stunServers?: Array<{ urls: string; }>; stunServers?: Array<{ urls: string; }>;
}; };
participantMenuButtonsWithNotifyClick?: Array<string | ParticipantMenuButtonsWithNotifyClick | {
key: string | ParticipantMenuButtonsWithNotifyClick;
preventExecution: boolean;
}>;
participantsPane?: { participantsPane?: {
hideModeratorSettingsTab?: boolean; hideModeratorSettingsTab?: boolean;
hideMoreActionsButton?: boolean; hideMoreActionsButton?: boolean;

@ -194,6 +194,7 @@ export default [
'openSharedDocumentOnJoin', 'openSharedDocumentOnJoin',
'opusMaxAverageBitrate', 'opusMaxAverageBitrate',
'p2p', 'p2p',
'participantMenuButtonsWithNotifyClick',
'participantsPane', 'participantsPane',
'pcStatsInterval', 'pcStatsInterval',
'prejoinConfig', 'prejoinConfig',

@ -1,7 +1,14 @@
import { IReduxState } from '../../app/types'; import { IReduxState } from '../../app/types';
import JitsiMeetJS from '../../base/lib-jitsi-meet'; 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'; import { TOOLBAR_BUTTONS } from './constants';
export * from './functions.any'; 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. * @param {Array} buttonsWithNotifyClick - The array of systme buttons that need to notify the api.
* @returns {Array} - The list of buttons. * @param {Array} customButtons - The custom buttons.
* @returns {Array}
*/ */
export function getButtonsWithNotifyClick(state: IReduxState): Array<{ key: string; preventExecution: boolean; }> { const buildButtonsArray = (
const { buttonsWithNotifyClick, customToolbarButtons } = state['features/base/config']; buttonsWithNotifyClick?: NotifyClickButton[],
const customButtons = customToolbarButtons?.map(({ id }) => { customButtons?: {
icon: string;
id: string;
text: string;
}[]
): NotifyClickButton[] => {
const customButtonsWithNotifyClick = customButtons?.map(({ id }) => {
return { return {
key: id, key: id,
preventExecution: false preventExecution: false
@ -135,13 +149,69 @@ export function getButtonsWithNotifyClick(state: IReduxState): Array<{ key: stri
}); });
const buttons = Array.isArray(buttonsWithNotifyClick) const buttons = Array.isArray(buttonsWithNotifyClick)
? buttonsWithNotifyClick as Array<{ key: string; preventExecution: boolean; }> ? buttonsWithNotifyClick as NotifyClickButton[]
: []; : [];
if (customButtons) { if (customButtonsWithNotifyClick) {
buttons.push(...customButtons); buttons.push(...customButtonsWithNotifyClick);
} }
return buttons; 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 * @private
* @returns {void} * @returns {void}
*/ */
_onClick(e?: React.MouseEvent<HTMLElement> | GestureResponderEvent) { _onClick(e?: React.MouseEvent | GestureResponderEvent) {
const { afterClick, handleClick, notifyMode, buttonKey } = this.props; const { afterClick, buttonKey, handleClick, notifyMode } = this.props;
if (typeof APP !== 'undefined' && notifyMode) { if (typeof APP !== 'undefined' && notifyMode) {
APP.API.notifyToolbarButtonClicked( 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 { makeStyles } from 'tss-react/mui';
import Avatar from '../../../../../base/avatar/components/Avatar'; import Avatar from '../../../../../base/avatar/components/Avatar';
import {
getButtonNotifyMode,
getParticipantMenuButtonsWithNotifyClick
} from '../../../../../base/config/functions.web';
import { isLocalParticipantModerator } from '../../../../../base/participants/functions'; import { isLocalParticipantModerator } from '../../../../../base/participants/functions';
import ContextMenu from '../../../../../base/ui/components/web/ContextMenu'; import ContextMenu from '../../../../../base/ui/components/web/ContextMenu';
import ContextMenuItemGroup from '../../../../../base/ui/components/web/ContextMenuItemGroup'; import ContextMenuItemGroup from '../../../../../base/ui/components/web/ContextMenuItemGroup';
import { getBreakoutRooms } from '../../../../../breakout-rooms/functions'; import { getBreakoutRooms } from '../../../../../breakout-rooms/functions';
import { NOTIFY_CLICK_MODE } from '../../../../../toolbox/constants';
import { showOverflowDrawer } from '../../../../../toolbox/functions.web'; import { showOverflowDrawer } from '../../../../../toolbox/functions.web';
import SendToRoomButton from '../../../../../video-menu/components/web/SendToRoomButton'; import SendToRoomButton from '../../../../../video-menu/components/web/SendToRoomButton';
import { PARTICIPANT_MENU_BUTTONS as BUTTONS } from '../../../../../video-menu/constants';
import { AVATAR_SIZE } from '../../../../constants'; import { AVATAR_SIZE } from '../../../../constants';
@ -72,11 +78,30 @@ export const RoomParticipantContextMenu = ({
const lowerMenu = useCallback(() => onSelect(true), [ onSelect ]); const lowerMenu = useCallback(() => onSelect(true), [ onSelect ]);
const rooms: Object = useSelector(getBreakoutRooms); const rooms: Object = useSelector(getBreakoutRooms);
const overflowDrawer = useSelector(showOverflowDrawer); 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) => { const breakoutRoomsButtons = useMemo(() => Object.values(rooms || {}).map((room: any) => {
if (room.id !== entity?.room?.id) { if (room.id !== entity?.room?.id) {
return (<SendToRoomButton return (<SendToRoomButton
key = { room.id } 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 } onClick = { lowerMenu }
participantID = { entity?.jid ?? '' } participantID = { entity?.jid ?? '' }
room = { room } />); room = { room } />);

@ -4,8 +4,10 @@ import { connect } from 'react-redux';
import { makeStyles } from 'tss-react/mui'; import { makeStyles } from 'tss-react/mui';
import { IReduxState, IStore } from '../../../app/types'; import { IReduxState, IStore } from '../../../app/types';
import { NotifyClickButton } from '../../../base/config/configType';
import { VISITORS_MODE_BUTTONS } from '../../../base/config/constants'; import { VISITORS_MODE_BUTTONS } from '../../../base/config/constants';
import { import {
getButtonNotifyMode,
getButtonsWithNotifyClick, getButtonsWithNotifyClick,
getToolbarButtons, getToolbarButtons,
isToolbarButtonEnabled isToolbarButtonEnabled
@ -22,7 +24,7 @@ import {
setToolbarHovered, setToolbarHovered,
showToolbox showToolbox
} from '../../actions.web'; } from '../../actions.web';
import { NOTIFY_CLICK_MODE, NOT_APPLICABLE, THRESHOLDS } from '../../constants'; import { NOT_APPLICABLE, THRESHOLDS } from '../../constants';
import { import {
getAllToolboxButtons, getAllToolboxButtons,
getJwtDisabledButtons, getJwtDisabledButtons,
@ -46,10 +48,7 @@ interface IProps extends WithTranslation {
/** /**
* Toolbar buttons which have their click exposed through the API. * Toolbar buttons which have their click exposed through the API.
*/ */
_buttonsWithNotifyClick?: Array<string | { _buttonsWithNotifyClick?: NotifyClickButton[];
key: string;
preventExecution: boolean;
}>;
/** /**
* Whether or not the chat feature is currently displayed. * Whether or not the chat feature is currently displayed.
@ -262,26 +261,6 @@ const Toolbox = ({
} }
}, [ _hangupMenuVisible, _overflowMenuVisible ]); }, [ _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. * Sets the notify click mode for the buttons.
* *
@ -295,7 +274,7 @@ const Toolbox = ({
Object.values(buttons).forEach((button: any) => { Object.values(buttons).forEach((button: any) => {
if (typeof button === 'object') { if (typeof button === 'object') {
button.notifyMode = getButtonNotifyMode(button.key); button.notifyMode = getButtonNotifyMode(button.key, _buttonsWithNotifyClick);
} }
}); });
} }
@ -452,7 +431,7 @@ const Toolbox = ({
ariaControls = 'hangup-menu' ariaControls = 'hangup-menu'
isOpen = { _hangupMenuVisible } isOpen = { _hangupMenuVisible }
key = 'hangup-menu' key = 'hangup-menu'
notifyMode = { getButtonNotifyMode('hangup-menu') } notifyMode = { getButtonNotifyMode('hangup-menu', _buttonsWithNotifyClick) }
onVisibilityChange = { onSetHangupVisible }> onVisibilityChange = { onSetHangupVisible }>
<ContextMenu <ContextMenu
accessibilityLabel = { t(toolbarAccLabel) } accessibilityLabel = { t(toolbarAccLabel) }
@ -462,17 +441,20 @@ const Toolbox = ({
onKeyDown = { onEscKey }> onKeyDown = { onEscKey }>
<EndConferenceButton <EndConferenceButton
buttonKey = 'end-meeting' buttonKey = 'end-meeting'
notifyMode = { getButtonNotifyMode('end-meeting') } /> notifyMode = { getButtonNotifyMode(
'end-meeting',
_buttonsWithNotifyClick
) } />
<LeaveConferenceButton <LeaveConferenceButton
buttonKey = 'hangup' buttonKey = 'hangup'
notifyMode = { getButtonNotifyMode('hangup') } /> notifyMode = { getButtonNotifyMode('hangup', _buttonsWithNotifyClick) } />
</ContextMenu> </ContextMenu>
</HangupMenuButton> </HangupMenuButton>
: <HangupButton : <HangupButton
buttonKey = 'hangup' buttonKey = 'hangup'
customClass = 'hangup-button' customClass = 'hangup-button'
key = 'hangup-button' key = 'hangup-button'
notifyMode = { getButtonNotifyMode('hangup') } notifyMode = { getButtonNotifyMode('hangup', _buttonsWithNotifyClick) }
visible = { isToolbarButtonEnabled('hangup', _toolbarButtons) } /> visible = { isToolbarButtonEnabled('hangup', _toolbarButtons) } />
)} )}
</div> </div>

@ -6,27 +6,38 @@ import { approveParticipantAudio, approveParticipantVideo } from '../../../av-mo
import { IconMic, IconVideo } from '../../../base/icons/svg'; import { IconMic, IconVideo } from '../../../base/icons/svg';
import { MEDIA_TYPE, MediaType } from '../../../base/media/constants'; import { MEDIA_TYPE, MediaType } from '../../../base/media/constants';
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem'; 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; 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 dispatch = useDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const _onClick = useCallback(() => { const _onClick = useCallback(() => {
notifyClick?.();
if (notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
return;
}
if (buttonType === MEDIA_TYPE.AUDIO) { if (buttonType === MEDIA_TYPE.AUDIO) {
dispatch(approveParticipantAudio(participantID)); dispatch(approveParticipantAudio(participantID));
} else if (buttonType === MEDIA_TYPE.VIDEO) { } else if (buttonType === MEDIA_TYPE.VIDEO) {
dispatch(approveParticipantVideo(participantID)); dispatch(approveParticipantVideo(participantID));
} }
}, [ participantID, buttonType ]); }, [ buttonType, dispatch, notifyClick, notifyMode, participantID ]);
const text = useMemo(() => { const text = useMemo(() => {
if (buttonType === MEDIA_TYPE.AUDIO) { if (buttonType === MEDIA_TYPE.AUDIO) {

@ -1,43 +1,42 @@
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { WithTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { connect } from 'react-redux'; import { useDispatch } from 'react-redux';
import { IStore } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import { IconInfoCircle } from '../../../base/icons/svg'; import { IconInfoCircle } from '../../../base/icons/svg';
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem'; import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
import { NOTIFY_CLICK_MODE } from '../../../toolbox/constants';
import { renderConnectionStatus } from '../../actions.web'; import { renderConnectionStatus } from '../../actions.web';
import { IButtonProps } from '../../types';
interface IProps extends WithTranslation {
/**
/** * Implements a React {@link Component} which displays a button that shows
* The Redux dispatch function. * the connection status for the given participant.
*/ *
dispatch: IStore['dispatch']; * @returns {JSX.Element}
*/
/**
* The ID of the participant for which to show connection stats.
*/
participantId: string;
}
const ConnectionStatusButton = ({ const ConnectionStatusButton = ({
dispatch, notifyClick,
t notifyMode
}: IProps) => { }: IButtonProps): JSX.Element => {
const onClick = useCallback(e => { const { t } = useTranslation();
const dispatch = useDispatch();
const handleClick = useCallback(e => {
e.stopPropagation(); e.stopPropagation();
notifyClick?.();
if (notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
return;
}
dispatch(renderConnectionStatus(true)); dispatch(renderConnectionStatus(true));
}, [ dispatch ]); }, [ dispatch, notifyClick, notifyMode ]);
return ( return (
<ContextMenuItem <ContextMenuItem
accessibilityLabel = { t('videothumbnail.connectionInfo') } accessibilityLabel = { t('videothumbnail.connectionInfo') }
icon = { IconInfoCircle } icon = { IconInfoCircle }
onClick = { onClick } onClick = { handleClick }
text = { t('videothumbnail.connectionInfo') } /> 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 TogglePinToStageButton from '../../../../features/video-menu/components/web/TogglePinToStageButton';
import Avatar from '../../../base/avatar/components/Avatar'; import Avatar from '../../../base/avatar/components/Avatar';
import { getButtonNotifyMode, getParticipantMenuButtonsWithNotifyClick } from '../../../base/config/functions.web';
import { IconPlay } from '../../../base/icons/svg'; import { IconPlay } from '../../../base/icons/svg';
import { isWhiteboardParticipant } from '../../../base/participants/functions'; import { isWhiteboardParticipant } from '../../../base/participants/functions';
import { IParticipant } from '../../../base/participants/types'; import { IParticipant } from '../../../base/participants/types';
import ContextMenu from '../../../base/ui/components/web/ContextMenu'; import ContextMenu from '../../../base/ui/components/web/ContextMenu';
import ContextMenuItemGroup from '../../../base/ui/components/web/ContextMenuItemGroup'; import ContextMenuItemGroup from '../../../base/ui/components/web/ContextMenuItemGroup';
import { stopSharedVideo } from '../../../shared-video/actions.any'; import { stopSharedVideo } from '../../../shared-video/actions.any';
import { NOTIFY_CLICK_MODE } from '../../../toolbox/constants';
import { showOverflowDrawer } from '../../../toolbox/functions.web'; import { showOverflowDrawer } from '../../../toolbox/functions.web';
import { setWhiteboardOpen } from '../../../whiteboard/actions'; import { setWhiteboardOpen } from '../../../whiteboard/actions';
import { WHITEBOARD_ID } from '../../../whiteboard/constants'; import { WHITEBOARD_ID } from '../../../whiteboard/constants';
import { PARTICIPANT_MENU_BUTTONS as BUTTONS } from '../../constants';
interface IProps { interface IProps {
@ -86,6 +89,23 @@ const FakeParticipantContextMenu = ({
const dispatch = useDispatch(); const dispatch = useDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const _overflowDrawer: boolean = useSelector(showOverflowDrawer); 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 ]); const clickHandler = useCallback(() => onSelect(true), [ onSelect ]);
@ -145,6 +165,9 @@ const FakeParticipantContextMenu = ({
{isWhiteboardParticipant(participant) && ( {isWhiteboardParticipant(participant) && (
<TogglePinToStageButton <TogglePinToStageButton
key = 'pinToStage' 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 } /> participantID = { WHITEBOARD_ID } />
)} )}
</ContextMenuItemGroup> </ContextMenuItemGroup>

@ -6,6 +6,7 @@ import { IReduxState, IStore } from '../../../app/types';
import { translate } from '../../../base/i18n/functions'; import { translate } from '../../../base/i18n/functions';
import { updateSettings } from '../../../base/settings/actions'; import { updateSettings } from '../../../base/settings/actions';
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem'; 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}. * The type of the React {@code Component} props of {@link FlipLocalVideoButton}.
@ -27,6 +28,17 @@ interface IProps extends WithTranslation {
*/ */
dispatch: IStore['dispatch']; 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. * Click handler executed aside from the main action.
*/ */
@ -82,8 +94,12 @@ class FlipLocalVideoButton extends PureComponent<IProps> {
* @returns {void} * @returns {void}
*/ */
_onClick() { _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?.(); onClick?.();
dispatch(updateSettings({ dispatch(updateSettings({
localFlipX: !_localFlipX localFlipX: !_localFlipX

@ -1,52 +1,56 @@
import React from 'react'; import React, { useCallback, useMemo } from 'react';
import { connect } from 'react-redux'; 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 { 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 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 * Implements a React {@link Component} which displays a button for granting
* moderator to a participant. * moderator to a participant.
*
* @returns {JSX.Element|null}
*/ */
class GrantModeratorButton extends AbstractGrantModeratorButton { const GrantModeratorButton = ({
/** notifyClick,
* Instantiates a new {@code GrantModeratorButton}. notifyMode,
* participantID
* @inheritdoc }: IButtonProps): JSX.Element | null => {
*/ const { t } = useTranslation();
constructor(props: IProps) { const dispatch = useDispatch();
super(props); const localParticipant = useSelector(getLocalParticipant);
const targetParticipant = useSelector((state: IReduxState) => getParticipantById(state, participantID));
this._handleClick = this._handleClick.bind(this); const visible = useMemo(() => Boolean(localParticipant?.role === PARTICIPANT_ROLE.MODERATOR)
} && !isParticipantModerator(targetParticipant), [ isParticipantModerator, localParticipant, targetParticipant ]);
/** const handleClick = useCallback(() => {
* Implements React's {@link Component#render()}. notifyClick?.();
* if (notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
* @inheritdoc return;
* @returns {ReactElement}
*/
render() {
const { t, visible } = this.props;
if (!visible) {
return null;
} }
dispatch(openDialog(GrantModeratorDialog, { participantID }));
}, [ dispatch, notifyClick, notifyMode, participantID ]);
return ( if (!visible) {
<ContextMenuItem return null;
accessibilityLabel = { t('toolbar.accessibilityLabel.grantModerator') }
className = 'grantmoderatorlink'
icon = { IconModerator }
// eslint-disable-next-line react/jsx-handler-names
onClick = { this._handleClick }
text = { t('videothumbnail.grantModerator') } />
);
} }
_handleClick: () => void; return (
} <ContextMenuItem
accessibilityLabel = { t('toolbar.accessibilityLabel.grantModerator') }
export default translate(connect(_mapStateToProps)(GrantModeratorButton)); className = 'grantmoderatorlink'
icon = { IconModerator }
onClick = { handleClick }
text = { t('videothumbnail.grantModerator') } />
);
};
export default GrantModeratorButton;

@ -7,6 +7,7 @@ import { translate } from '../../../base/i18n/functions';
import { updateSettings } from '../../../base/settings/actions'; import { updateSettings } from '../../../base/settings/actions';
import { getHideSelfView } from '../../../base/settings/functions'; import { getHideSelfView } from '../../../base/settings/functions';
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem'; 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}. * The type of the React {@code Component} props of {@link HideSelfViewVideoButton}.
@ -28,6 +29,17 @@ interface IProps extends WithTranslation {
*/ */
dispatch: IStore['dispatch']; 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. * Click handler executed aside from the main action.
*/ */
@ -83,8 +95,12 @@ class HideSelfViewVideoButton extends PureComponent<IProps> {
* @returns {void} * @returns {void}
*/ */
_onClick() { _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?.(); onClick?.();
dispatch(updateSettings({ dispatch(updateSettings({
disableSelfView: !disableSelfView disableSelfView: !disableSelfView

@ -1,54 +1,46 @@
import React from 'react'; import React, { useCallback } from 'react';
import { connect } from 'react-redux'; 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 { IconUserDeleted } from '../../../base/icons/svg';
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem'; 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 * Implements a React {@link Component} which displays a button for kicking out
* a participant from the conference. * a participant from the conference.
* *
* NOTE: At the time of writing this is a button that doesn't use the * @returns {JSX.Element}
* {@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.
*/ */
class KickButton extends AbstractKickButton { const KickButton = ({
/** notifyClick,
* Instantiates a new {@code Component}. notifyMode,
* participantID
* @inheritdoc }: IButtonProps): JSX.Element => {
*/ const { t } = useTranslation();
constructor(props: IProps) { const dispatch = useDispatch();
super(props);
this._handleClick = this._handleClick.bind(this);
}
/** const handleClick = useCallback(() => {
* Implements React's {@link Component#render()}. notifyClick?.();
* if (notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
* @inheritdoc return;
* @returns {ReactElement} }
*/ dispatch(openDialog(KickRemoteParticipantDialog, { participantID }));
render() { }, [ dispatch, notifyClick, notifyMode, participantID ]);
const { participantID, t } = this.props;
return ( return (
<ContextMenuItem <ContextMenuItem
accessibilityLabel = { t('videothumbnail.kick') } accessibilityLabel = { t('videothumbnail.kick') }
className = 'kicklink' className = 'kicklink'
icon = { IconUserDeleted } icon = { IconUserDeleted }
id = { `ejectlink_${participantID}` } id = { `ejectlink_${participantID}` }
// eslint-disable-next-line react/jsx-handler-names onClick = { handleClick }
onClick = { this._handleClick } text = { t('videothumbnail.kick') } />
text = { t('videothumbnail.kick') } /> );
); };
}
_handleClick: () => void; export default KickButton;
}
export default translate(connect()(KickButton));

@ -1,9 +1,10 @@
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next'; 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 { makeStyles } from 'tss-react/mui';
import { IReduxState, IStore } from '../../../app/types'; import { IReduxState, IStore } from '../../../app/types';
import { getButtonNotifyMode, getParticipantMenuButtonsWithNotifyClick } from '../../../base/config/functions.web';
import { isMobileBrowser } from '../../../base/environment/utils'; import { isMobileBrowser } from '../../../base/environment/utils';
import { IconDotsHorizontal } from '../../../base/icons/svg'; import { IconDotsHorizontal } from '../../../base/icons/svg';
import { getLocalParticipant } from '../../../base/participants/functions'; 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 ConnectionIndicatorContent from '../../../connection-indicator/components/web/ConnectionIndicatorContent';
import { THUMBNAIL_TYPE } from '../../../filmstrip/constants'; import { THUMBNAIL_TYPE } from '../../../filmstrip/constants';
import { isStageFilmstripAvailable } from '../../../filmstrip/functions.web'; import { isStageFilmstripAvailable } from '../../../filmstrip/functions.web';
import { NOTIFY_CLICK_MODE } from '../../../toolbox/constants';
import { renderConnectionStatus } from '../../actions.web'; import { renderConnectionStatus } from '../../actions.web';
import { PARTICIPANT_MENU_BUTTONS as BUTTONS } from '../../constants';
import ConnectionStatusButton from './ConnectionStatusButton'; import ConnectionStatusButton from './ConnectionStatusButton';
import FlipLocalVideoButton from './FlipLocalVideoButton'; import FlipLocalVideoButton from './FlipLocalVideoButton';
@ -139,6 +142,22 @@ const LocalVideoMenuTriggerButton = ({
}: IProps) => { }: IProps) => {
const { classes } = useStyles(); const { classes } = useStyles();
const { t } = useTranslation(); 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(() => { const _onPopoverOpen = useCallback(() => {
showPopover?.(); showPopover?.();
@ -164,22 +183,35 @@ const LocalVideoMenuTriggerButton = ({
{_showLocalVideoFlipButton {_showLocalVideoFlipButton
&& <FlipLocalVideoButton && <FlipLocalVideoButton
className = { _overflowDrawer ? classes.flipText : '' } 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 } /> onClick = { hidePopover } />
} }
{_showHideSelfViewButton {_showHideSelfViewButton
&& <HideSelfViewVideoButton && <HideSelfViewVideoButton
className = { _overflowDrawer ? classes.flipText : '' } 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 } /> onClick = { hidePopover } />
} }
{ {
_showPinToStage && <TogglePinToStageButton _showPinToStage && <TogglePinToStageButton
className = { _overflowDrawer ? classes.flipText : '' } className = { _overflowDrawer ? classes.flipText : '' }
noIcon = { true } 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 } onClick = { hidePopover }
participantID = { _localParticipantId } /> 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> </ContextMenuItemGroup>
</ContextMenu> </ContextMenu>

@ -1,59 +1,65 @@
import React from 'react'; import React, { useCallback, useMemo } from 'react';
import { connect } from 'react-redux'; 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 { 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 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 * Implements a React {@link Component} which displays a button for audio muting
* a participant in the conference. * a participant in the conference.
* *
* NOTE: At the time of writing this is a button that doesn't use the * @returns {JSX.Element|null}
* {@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.
*/ */
class MuteButton extends AbstractMuteButton { const MuteButton = ({
/** notifyClick,
* Instantiates a new {@code Component}. notifyMode,
* participantID
* @inheritdoc }: IButtonProps): JSX.Element | null => {
*/ const { t } = useTranslation();
constructor(props: IProps) { const dispatch = useDispatch();
super(props); const tracks = useSelector((state: IReduxState) => state['features/base/tracks']);
const audioTrackMuted = useMemo(
this._handleClick = this._handleClick.bind(this); () => isRemoteTrackMuted(tracks, MEDIA_TYPE.AUDIO, participantID),
} [ isRemoteTrackMuted, participantID, tracks ]
);
/** const handleClick = useCallback(() => {
* Implements React's {@link Component#render()}. notifyClick?.();
* if (notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
* @inheritdoc return;
* @returns {ReactElement}
*/
render() {
const { _audioTrackMuted, t } = this.props;
if (_audioTrackMuted) {
return null;
} }
sendAnalytics(createRemoteVideoMenuButtonEvent(
'mute',
{
'participant_id': participantID
}));
dispatch(muteRemote(participantID, MEDIA_TYPE.AUDIO));
dispatch(rejectParticipantAudio(participantID));
}, [ dispatch, notifyClick, notifyMode, participantID, sendAnalytics ]);
return ( if (audioTrackMuted) {
<ContextMenuItem return null;
accessibilityLabel = { t('dialog.muteParticipantButton') }
className = 'mutelink'
icon = { IconMicSlash }
// eslint-disable-next-line react/jsx-handler-names
onClick = { this._handleClick }
text = { t('dialog.muteParticipantButton') } />
);
} }
_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 React, { useCallback } from 'react';
import { connect } from 'react-redux'; 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 { IconMicSlash } from '../../../base/icons/svg';
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem'; 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 * Implements a React {@link Component} which displays a button for audio muting
* every participant in the conference except the one with the given * every participant in the conference except the one with the given
* participantID. * participantID.
*
* @returns {JSX.Element}
*/ */
class MuteEveryoneElseButton extends AbstractMuteEveryoneElseButton { const MuteEveryoneElseButton = ({
/** notifyClick,
* Instantiates a new {@code Component}. notifyMode,
* participantID
* @inheritdoc }: IButtonProps): JSX.Element => {
*/ const { t } = useTranslation();
constructor(props: IProps) { const dispatch = useDispatch();
super(props);
this._handleClick = this._handleClick.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { t } = this.props;
return ( const handleClick = useCallback(() => {
<ContextMenuItem notifyClick?.();
accessibilityLabel = { t('toolbar.accessibilityLabel.muteEveryoneElse') } if (notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
icon = { IconMicSlash } return;
// eslint-disable-next-line react/jsx-handler-names }
onClick = { this._handleClick } sendAnalytics(createToolbarEvent('mute.everyoneelse.pressed'));
text = { t('videothumbnail.domuteOthers') } /> dispatch(openDialog(MuteEveryoneDialog, { exclude: [ participantID ] }));
); }, [ dispatch, notifyMode, notifyClick, participantID, sendAnalytics ]);
}
_handleClick: () => void; return (
} <ContextMenuItem
accessibilityLabel = { t('toolbar.accessibilityLabel.muteEveryoneElse') }
icon = { IconMicSlash }
onClick = { handleClick }
text = { t('videothumbnail.domuteOthers') } />
);
};
export default translate(connect()(MuteEveryoneElseButton)); export default MuteEveryoneElseButton;

@ -1,48 +1,48 @@
import React from 'react'; import React, { useCallback } from 'react';
import { connect } from 'react-redux'; 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 { IconVideoOff } from '../../../base/icons/svg';
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem'; 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 * Implements a React {@link Component} which displays a button for audio muting
* every participant in the conference except the one with the given * every participant in the conference except the one with the given
* participantID. * participantID.
*
* @returns {JSX.Element}
*/ */
class MuteEveryoneElsesVideoButton extends AbstractMuteEveryoneElsesVideoButton { const MuteEveryoneElsesVideoButton = ({
/** notifyClick,
* Instantiates a new {@code Component}. notifyMode,
* participantID
* @inheritdoc }: IButtonProps): JSX.Element => {
*/ const { t } = useTranslation();
constructor(props: IProps) { const dispatch = useDispatch();
super(props);
this._handleClick = this._handleClick.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { t } = this.props;
return ( const handleClick = useCallback(() => {
<ContextMenuItem notifyClick?.();
accessibilityLabel = { t('toolbar.accessibilityLabel.muteEveryoneElsesVideoStream') } if (notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
icon = { IconVideoOff } return;
// eslint-disable-next-line react/jsx-handler-names }
onClick = { this._handleClick } sendAnalytics(createToolbarEvent('mute.everyoneelsesvideo.pressed'));
text = { t('videothumbnail.domuteVideoOfOthers') } /> dispatch(openDialog(MuteEveryonesVideoDialog, { exclude: [ participantID ] }));
); }, [ notifyClick, notifyMode, participantID ]);
}
_handleClick: () => void; return (
} <ContextMenuItem
accessibilityLabel = { t('toolbar.accessibilityLabel.muteEveryoneElsesVideoStream') }
icon = { IconVideoOff }
onClick = { handleClick }
text = { t('videothumbnail.domuteVideoOfOthers') } />
);
};
export default translate(connect()(MuteEveryoneElsesVideoButton)); export default MuteEveryoneElsesVideoButton;

@ -1,58 +1,66 @@
import React from 'react'; import React, { useCallback, useMemo } from 'react';
import { connect } from 'react-redux'; 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 { 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 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 * Implements a React {@link Component} which displays a button for disabling
* the camera of a participant in the conference. * the camera of a participant in the conference.
* *
* NOTE: At the time of writing this is a button that doesn't use the * @returns {JSX.Element|null}
* {@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.
*/ */
class MuteVideoButton extends AbstractMuteVideoButton { const MuteVideoButton = ({
/** notifyClick,
* Instantiates a new {@code Component}. notifyMode,
* participantID
* @inheritdoc }: IButtonProps): JSX.Element | null => {
*/ const { t } = useTranslation();
constructor(props: IProps) { const dispatch = useDispatch();
super(props); const tracks = useSelector((state: IReduxState) => state['features/base/tracks']);
this._handleClick = this._handleClick.bind(this);
}
/** const videoTrackMuted = useMemo(
* Implements React's {@link Component#render()}. () => isRemoteTrackMuted(tracks, MEDIA_TYPE.VIDEO, participantID),
* [ isRemoteTrackMuted, participantID, tracks ]
* @inheritdoc );
* @returns {ReactElement}
*/ const handleClick = useCallback(() => {
render() { notifyClick?.();
const { _videoTrackMuted, t } = this.props; if (notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
return;
if (_videoTrackMuted) {
return null;
} }
sendAnalytics(createRemoteVideoMenuButtonEvent(
'video.mute.button',
{
'participant_id': participantID
}));
dispatch(openDialog(MuteRemoteParticipantsVideoDialog, { participantID }));
}, [ dispatch, notifyClick, notifyClick, participantID, sendAnalytics ]);
return ( if (videoTrackMuted) {
<ContextMenuItem return null;
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') } />
);
} }
_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 { IReduxState, IStore } from '../../../app/types';
import { isSupported as isAvModerationSupported } from '../../../av-moderation/functions'; import { isSupported as isAvModerationSupported } from '../../../av-moderation/functions';
import Avatar from '../../../base/avatar/components/Avatar'; import Avatar from '../../../base/avatar/components/Avatar';
import { getButtonNotifyMode, getParticipantMenuButtonsWithNotifyClick } from '../../../base/config/functions.web';
import { isIosMobileBrowser, isMobileBrowser } from '../../../base/environment/utils'; import { isIosMobileBrowser, isMobileBrowser } from '../../../base/environment/utils';
import { MEDIA_TYPE } from '../../../base/media/constants'; import { MEDIA_TYPE } from '../../../base/media/constants';
import { PARTICIPANT_ROLE } from '../../../base/participants/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 ContextMenu from '../../../base/ui/components/web/ContextMenu';
import ContextMenuItemGroup from '../../../base/ui/components/web/ContextMenuItemGroup'; import ContextMenuItemGroup from '../../../base/ui/components/web/ContextMenuItemGroup';
import { getBreakoutRooms, getCurrentRoomId, isInBreakoutRoom } from '../../../breakout-rooms/functions'; import { getBreakoutRooms, getCurrentRoomId, isInBreakoutRoom } from '../../../breakout-rooms/functions';
import { IRoom } from '../../../breakout-rooms/types';
import { displayVerification } from '../../../e2ee/functions'; import { displayVerification } from '../../../e2ee/functions';
import { setVolume } from '../../../filmstrip/actions.web'; import { setVolume } from '../../../filmstrip/actions.web';
import { isStageFilmstripAvailable } from '../../../filmstrip/functions.web'; import { isStageFilmstripAvailable } from '../../../filmstrip/functions.web';
import { QUICK_ACTION_BUTTON } from '../../../participants-pane/constants'; import { QUICK_ACTION_BUTTON } from '../../../participants-pane/constants';
import { getQuickActionButtonType, isForceMuted } from '../../../participants-pane/functions'; import { getQuickActionButtonType, isForceMuted } from '../../../participants-pane/functions';
import { requestRemoteControl, stopController } from '../../../remote-control/actions'; import { requestRemoteControl, stopController } from '../../../remote-control/actions';
import { NOTIFY_CLICK_MODE } from '../../../toolbox/constants';
import { showOverflowDrawer } from '../../../toolbox/functions.web'; import { showOverflowDrawer } from '../../../toolbox/functions.web';
import { iAmVisitor } from '../../../visitors/functions'; import { iAmVisitor } from '../../../visitors/functions';
import { PARTICIPANT_MENU_BUTTONS as BUTTONS } from '../../constants';
import AskToUnmuteButton from './AskToUnmuteButton'; import AskToUnmuteButton from './AskToUnmuteButton';
import ConnectionStatusButton from './ConnectionStatusButton'; import ConnectionStatusButton from './ConnectionStatusButton';
@ -145,16 +149,15 @@ const ParticipantContextMenu = ({
const isModerationSupported = useSelector((state: IReduxState) => isAvModerationSupported()(state)); const isModerationSupported = useSelector((state: IReduxState) => isAvModerationSupported()(state));
const stageFilmstrip = useSelector(isStageFilmstripAvailable); const stageFilmstrip = useSelector(isStageFilmstripAvailable);
const shouldDisplayVerification = useSelector((state: IReduxState) => displayVerification(state, participant?.id)); const shouldDisplayVerification = useSelector((state: IReduxState) => displayVerification(state, participant?.id));
const buttonsWithNotifyClick = useSelector(getParticipantMenuButtonsWithNotifyClick);
const _currentRoomId = useSelector(getCurrentRoomId); const _currentRoomId = useSelector(getCurrentRoomId);
const _rooms = Object.values(useSelector(getBreakoutRooms)); const _rooms: IRoom[] = Object.values(useSelector(getBreakoutRooms));
const _onVolumeChange = useCallback(value => { const _onVolumeChange = useCallback(value => {
dispatch(setVolume(participant.id, value)); dispatch(setVolume(participant.id, value));
}, [ setVolume, dispatch ]); }, [ setVolume, dispatch ]);
const clickHandler = useCallback(() => onSelect(true), [ onSelect ]);
const _getCurrentParticipantId = useCallback(() => { const _getCurrentParticipantId = useCallback(() => {
const drawer = _overflowDrawer && !thumbnailMenu; const drawer = _overflowDrawer && !thumbnailMenu;
@ -162,6 +165,25 @@ const ParticipantContextMenu = ({
} }
, [ thumbnailMenu, _overflowDrawer, drawerParticipant, participant ]); , [ 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( const isClickedFromParticipantPane = useMemo(
() => !_overflowDrawer && !thumbnailMenu, () => !_overflowDrawer && !thumbnailMenu,
[ _overflowDrawer, thumbnailMenu ]); [ _overflowDrawer, thumbnailMenu ]);
@ -177,128 +199,98 @@ const ParticipantContextMenu = ({
&& typeof _volume === 'number' && typeof _volume === 'number'
&& !isNaN(_volume); && !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 (_isModerator) {
if (isModerationSupported) { if (isModerationSupported) {
if (_isAudioMuted if (_isAudioMuted
&& !(isClickedFromParticipantPane && quickActionButtonType === QUICK_ACTION_BUTTON.ASK_TO_UNMUTE)) { && !(isClickedFromParticipantPane && quickActionButtonType === QUICK_ACTION_BUTTON.ASK_TO_UNMUTE)) {
buttons.push(<AskToUnmuteButton buttons.push(<AskToUnmuteButton
buttonType = { MEDIA_TYPE.AUDIO } { ...getButtonProps(BUTTONS.ASK_UNMUTE) }
key = 'ask-unmute' buttonType = { MEDIA_TYPE.AUDIO } />
participantID = { _getCurrentParticipantId() } />
); );
} }
if (_isVideoForceMuted if (_isVideoForceMuted
&& !(isClickedFromParticipantPane && quickActionButtonType === QUICK_ACTION_BUTTON.ALLOW_VIDEO)) { && !(isClickedFromParticipantPane && quickActionButtonType === QUICK_ACTION_BUTTON.ALLOW_VIDEO)) {
buttons.push(<AskToUnmuteButton buttons.push(<AskToUnmuteButton
buttonType = { MEDIA_TYPE.VIDEO } { ...getButtonProps(BUTTONS.ALLOW_VIDEO) }
key = 'allow-video' buttonType = { MEDIA_TYPE.VIDEO } />
participantID = { _getCurrentParticipantId() } />
); );
} }
} }
if (!disableRemoteMute) { if (!disableRemoteMute) {
if (!(isClickedFromParticipantPane && quickActionButtonType === QUICK_ACTION_BUTTON.MUTE)) { if (!(isClickedFromParticipantPane && quickActionButtonType === QUICK_ACTION_BUTTON.MUTE)) {
buttons.push( buttons.push(<MuteButton { ...getButtonProps(BUTTONS.MUTE) } />);
<MuteButton
key = 'mute'
participantID = { _getCurrentParticipantId() } />
);
} }
buttons.push( buttons.push(<MuteEveryoneElseButton { ...getButtonProps(BUTTONS.MUTE_OTHERS) } />);
<MuteEveryoneElseButton
key = 'mute-others'
participantID = { _getCurrentParticipantId() } />
);
if (!(isClickedFromParticipantPane && quickActionButtonType === QUICK_ACTION_BUTTON.STOP_VIDEO)) { if (!(isClickedFromParticipantPane && quickActionButtonType === QUICK_ACTION_BUTTON.STOP_VIDEO)) {
buttons.push( buttons.push(<MuteVideoButton { ...getButtonProps(BUTTONS.MUTE_VIDEO) } />);
<MuteVideoButton
key = 'mute-video'
participantID = { _getCurrentParticipantId() } />
);
} }
buttons.push( buttons.push(<MuteEveryoneElsesVideoButton { ...getButtonProps(BUTTONS.MUTE_OTHERS_VIDEO) } />);
<MuteEveryoneElsesVideoButton
key = 'mute-others-video'
participantID = { _getCurrentParticipantId() } />
);
} }
if (!disableGrantModerator && !isBreakoutRoom) { if (!disableGrantModerator && !isBreakoutRoom) {
buttons2.push( buttons2.push(<GrantModeratorButton { ...getButtonProps(BUTTONS.GRANT_MODERATOR) } />);
<GrantModeratorButton
key = 'grant-moderator'
participantID = { _getCurrentParticipantId() } />
);
} }
if (!disableKick) { if (!disableKick) {
buttons2.push( buttons2.push(<KickButton { ...getButtonProps(BUTTONS.KICK) } />);
<KickButton
key = 'kick'
participantID = { _getCurrentParticipantId() } />
);
} }
if (shouldDisplayVerification) { if (shouldDisplayVerification) {
buttons2.push( buttons2.push(<VerifyParticipantButton { ...getButtonProps(BUTTONS.VERIFY) } />);
<VerifyParticipantButton
key = 'verify'
participantID = { _getCurrentParticipantId() } />
);
} }
} }
if (stageFilmstrip) { if (stageFilmstrip) {
buttons2.push(<TogglePinToStageButton buttons2.push(<TogglePinToStageButton { ...getButtonProps(BUTTONS.PIN_TO_STAGE) } />);
key = 'pinToStage'
participantID = { _getCurrentParticipantId() } />);
} }
if (!disablePrivateChat && !visitorsMode) { if (!disablePrivateChat && !visitorsMode) {
buttons2.push(<PrivateMessageMenuButton buttons2.push(<PrivateMessageMenuButton { ...getButtonProps(BUTTONS.PRIVATE_MESSAGE) } />);
key = 'privateMessage'
participantID = { _getCurrentParticipantId() } />
);
} }
if (thumbnailMenu && isMobileBrowser()) { if (thumbnailMenu && isMobileBrowser()) {
buttons2.push( buttons2.push(<ConnectionStatusButton { ...getButtonProps(BUTTONS.CONN_STATUS) } />);
<ConnectionStatusButton
key = 'conn-status'
participantId = { _getCurrentParticipantId() } />
);
} }
if (thumbnailMenu && remoteControlState) { if (thumbnailMenu && remoteControlState) {
let onRemoteControlToggle = null; const onRemoteControlToggle = useCallback(() => {
if (remoteControlState === REMOTE_CONTROL_MENU_STATES.STARTED) {
if (remoteControlState === REMOTE_CONTROL_MENU_STATES.STARTED) { dispatch(stopController(true));
onRemoteControlToggle = () => dispatch(stopController(true)); } else if (remoteControlState === REMOTE_CONTROL_MENU_STATES.NOT_STARTED) {
} else if (remoteControlState === REMOTE_CONTROL_MENU_STATES.NOT_STARTED) { dispatch(requestRemoteControl(_getCurrentParticipantId()));
onRemoteControlToggle = () => dispatch(requestRemoteControl(_getCurrentParticipantId())); }
} }, [ dispatch, remoteControlState, stopController, requestRemoteControl ]);
buttons2.push( buttons2.push(<RemoteControlButton
<RemoteControlButton { ...getButtonProps(BUTTONS.REMOTE_CONTROL) }
key = 'remote-control' onClick = { onRemoteControlToggle }
onClick = { onRemoteControlToggle } remoteControlState = { remoteControlState } />
participantID = { _getCurrentParticipantId() }
remoteControlState = { remoteControlState } />
); );
} }
if (customParticipantMenuButtons) { if (customParticipantMenuButtons) {
customParticipantMenuButtons.forEach( customParticipantMenuButtons.forEach(
({ icon, id, text }) => { ({ icon, id, text }) => {
const onClick = useCallback(
() => APP.API.notifyParticipantMenuButtonClicked(id, _getCurrentParticipantId()), []);
buttons2.push( buttons2.push(
<CustomOptionButton <CustomOptionButton
icon = { icon } icon = { icon }
key = { id } key = { id }
onClick = { onClick } // eslint-disable-next-line react/jsx-no-bind
onClick = { () => notifyClick(id) }
text = { text } /> text = { text } />
); );
} }
@ -312,9 +304,9 @@ const ParticipantContextMenu = ({
if (room.id !== _currentRoomId) { if (room.id !== _currentRoomId) {
breakoutRoomsButtons.push( breakoutRoomsButtons.push(
<SendToRoomButton <SendToRoomButton
{ ...getButtonProps(BUTTONS.SEND_PARTICIPANT_TO_ROOM) }
key = { room.id } key = { room.id }
onClick = { clickHandler } onClick = { onBreakoutRoomButtonClick }
participantID = { _getCurrentParticipantId() }
room = { room } /> room = { room } />
); );
} }

@ -11,10 +11,11 @@ import { getParticipantById } from '../../../base/participants/functions';
import { IParticipant } from '../../../base/participants/types'; import { IParticipant } from '../../../base/participants/types';
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem'; import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
import { openChat } from '../../../chat/actions.web'; 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 { 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. * 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} * @returns {ReactElement}
*/ */
render() { render() {
const { t, _hidden } = this.props; const { _hidden, t } = this.props;
if (_hidden) { if (_hidden) {
return null; return null;
@ -75,11 +76,16 @@ class PrivateMessageMenuButton extends Component<IProps> {
/** /**
* Callback to be invoked on pressing the button. * Callback to be invoked on pressing the button.
* *
* @param {React.MouseEvent|undefined} e - The click event.
* @returns {void} * @returns {void}
*/ */
_onClick() { _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)); dispatch(openChat(_participant));
} }
} }

@ -6,6 +6,7 @@ import { sendAnalytics } from '../../../analytics/functions';
import { translate } from '../../../base/i18n/functions'; import { translate } from '../../../base/i18n/functions';
import { IconRemoteControlStart, IconRemoteControlStop } from '../../../base/icons/svg'; import { IconRemoteControlStart, IconRemoteControlStop } from '../../../base/icons/svg';
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem'; 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 // TODO: Move these enums into the store after further reactification of the
// non-react RemoteVideo component. // non-react RemoteVideo component.
@ -21,6 +22,17 @@ export const REMOTE_CONTROL_MENU_STATES = {
*/ */
interface IProps extends WithTranslation { 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. * The callback to invoke when the component is clicked.
*/ */
@ -66,10 +78,7 @@ class RemoteControlButton extends Component<IProps> {
* @returns {null|ReactElement} * @returns {null|ReactElement}
*/ */
render() { render() {
const { const { remoteControlState, t } = this.props;
remoteControlState,
t
} = this.props;
let disabled = false, icon; let disabled = false, icon;
@ -110,7 +119,12 @@ class RemoteControlButton extends Component<IProps> {
* @returns {void} * @returns {void}
*/ */
_onClick() { _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"? // TODO: What do we do in case the state is e.g. "requesting"?
if (remoteControlState === REMOTE_CONTROL_MENU_STATES.STARTED if (remoteControlState === REMOTE_CONTROL_MENU_STATES.STARTED
@ -126,10 +140,8 @@ class RemoteControlButton extends Component<IProps> {
'participant_id': participantID '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 ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
import { sendParticipantToRoom } from '../../../breakout-rooms/actions'; import { sendParticipantToRoom } from '../../../breakout-rooms/actions';
import { IRoom } from '../../../breakout-rooms/types'; 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. * Click handler.
*/ */
onClick?: Function; onClick?: Function;
/**
* The ID for the participant on which the button will act.
*/
participantID: string;
/** /**
* The room to send the participant to. * The room to send the participant to.
*/ */
room: IRoom; room: IRoom;
} }
const SendToRoomButton = ({ onClick, participantID, room }: IProps) => { const SendToRoomButton = ({
notifyClick,
notifyMode,
onClick,
participantID,
room
}: IProps) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const _onClick = useCallback(() => { const _onClick = useCallback(() => {
notifyClick?.();
if (notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
return;
}
onClick?.(); onClick?.();
sendAnalytics(createBreakoutRoomsEvent('send.participant.to.room')); sendAnalytics(createBreakoutRoomsEvent('send.participant.to.room'));
dispatch(sendParticipantToRoom(participantID, room.id)); dispatch(sendParticipantToRoom(participantID, room.id));
}, [ participantID, room ]); }, [ dispatch, notifyClick, notifyMode, onClick, participantID, room, sendAnalytics ]);
const roomName = room.name || t('breakoutRooms.mainRoom'); 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 ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
import { togglePinStageParticipant } from '../../../filmstrip/actions.web'; import { togglePinStageParticipant } from '../../../filmstrip/actions.web';
import { getPinnedActiveParticipants } from '../../../filmstrip/functions.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. * Button text class name.
@ -23,22 +25,28 @@ interface IProps {
* Click handler executed aside from the main action. * Click handler executed aside from the main action.
*/ */
onClick?: Function; 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 dispatch = useDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const isActive = Boolean(useSelector(getPinnedActiveParticipants) const isActive = Boolean(useSelector(getPinnedActiveParticipants)
.find(p => p.participantId === participantID)); .find(p => p.participantId === participantID));
const _onClick = useCallback(() => { const _onClick = useCallback(() => {
notifyClick?.();
if (notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
return;
}
dispatch(togglePinStageParticipant(participantID)); dispatch(togglePinStageParticipant(participantID));
onClick?.(); onClick?.();
}, [ participantID, isActive ]); }, [ dispatch, isActive, notifyClick, onClick, participantID ]);
const text = isActive const text = isActive
? t('videothumbnail.unpinFromStage') ? t('videothumbnail.unpinFromStage')

@ -1,38 +1,34 @@
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next'; 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 { IconCheck } from '../../../base/icons/svg';
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem'; import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
import { startVerification } from '../../../e2ee/actions'; 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 * Implements a React {@link Component} which displays a button that
* {@link VerifyParticipantButton}. * 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 = ({ const VerifyParticipantButton = ({
dispatch, notifyClick,
notifyMode,
participantID participantID
}: IProps) => { }: IButtonProps): JSX.Element => {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useDispatch();
const _handleClick = useCallback(() => { const _handleClick = useCallback(() => {
notifyClick?.();
if (notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
return;
}
dispatch(startVerification(participantID)); dispatch(startVerification(participantID));
}, [ participantID ]); }, [ dispatch, notifyClick, notifyMode, participantID ]);
return ( return (
<ContextMenuItem <ContextMenuItem
@ -46,20 +42,4 @@ const VerifyParticipantButton = ({
); );
}; };
/** export default 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);

@ -12,3 +12,25 @@ export const NATIVE_VOLUME_SLIDER_SCALE = 19;
* recognizes whole numbers. * recognizes whole numbers.
*/ */
export const VOLUME_SLIDER_SCALE = 100; 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'
};

@ -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;
}
Loading…
Cancel
Save