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',
// ],
// 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
});
}

@ -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' |
@ -468,6 +495,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;

@ -194,6 +194,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 { IConfig, IDeeplinkingConfig, IDeeplinkingMobileConfig, IDeeplinkingPlatformConfig } from './configType';
import { NOTIFY_CLICK_MODE } from '../../toolbox/constants';
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';
interface IProps extends WithTranslation {
/**
* The Redux dispatch function.
*/
dispatch: IStore['dispatch'];
/**
* The ID of the participant for which to show connection stats.
*/
participantId: string;
}
import { IButtonProps } from '../../types';
/**
* 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);
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 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 ]);
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;
}
export default translate(connect(_mapStateToProps)(GrantModeratorButton));
return (
<ContextMenuItem
accessibilityLabel = { t('toolbar.accessibilityLabel.grantModerator') }
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 { 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);
this._handleClick = this._handleClick.bind(this);
}
const KickButton = ({
notifyClick,
notifyMode,
participantID
}: IButtonProps): JSX.Element => {
const { t } = useTranslation();
const dispatch = useDispatch();
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { participantID, t } = this.props;
const handleClick = useCallback(() => {
notifyClick?.();
if (notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
return;
}
dispatch(openDialog(KickRemoteParticipantDialog, { participantID }));
}, [ dispatch, notifyClick, notifyMode, participantID ]);
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') } />
);
}
return (
<ContextMenuItem
accessibilityLabel = { t('videothumbnail.kick') }
className = 'kicklink'
icon = { IconUserDeleted }
id = { `ejectlink_${participantID}` }
onClick = { 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);
this._handleClick = this._handleClick.bind(this);
}
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 ]
);
/**
* 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
}));
dispatch(muteRemote(participantID, MEDIA_TYPE.AUDIO));
dispatch(rejectParticipantAudio(participantID));
}, [ dispatch, notifyClick, notifyMode, participantID, sendAnalytics ]);
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') } />
);
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);
this._handleClick = this._handleClick.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { t } = this.props;
const MuteEveryoneElseButton = ({
notifyClick,
notifyMode,
participantID
}: IButtonProps): JSX.Element => {
const { t } = useTranslation();
const dispatch = useDispatch();
return (
<ContextMenuItem
accessibilityLabel = { t('toolbar.accessibilityLabel.muteEveryoneElse') }
icon = { IconMicSlash }
// eslint-disable-next-line react/jsx-handler-names
onClick = { this._handleClick }
text = { t('videothumbnail.domuteOthers') } />
);
}
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 ]);
_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 { 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);
this._handleClick = this._handleClick.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { t } = this.props;
const MuteEveryoneElsesVideoButton = ({
notifyClick,
notifyMode,
participantID
}: IButtonProps): JSX.Element => {
const { t } = useTranslation();
const dispatch = useDispatch();
return (
<ContextMenuItem
accessibilityLabel = { t('toolbar.accessibilityLabel.muteEveryoneElsesVideoStream') }
icon = { IconVideoOff }
// eslint-disable-next-line react/jsx-handler-names
onClick = { this._handleClick }
text = { t('videothumbnail.domuteVideoOfOthers') } />
);
}
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 ]);
_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 { 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);
this._handleClick = this._handleClick.bind(this);
}
const MuteVideoButton = ({
notifyClick,
notifyMode,
participantID
}: IButtonProps): JSX.Element | null => {
const { t } = useTranslation();
const dispatch = useDispatch();
const tracks = useSelector((state: IReduxState) => state['features/base/tracks']);
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { _videoTrackMuted, t } = this.props;
if (_videoTrackMuted) {
return null;
const videoTrackMuted = useMemo(
() => isRemoteTrackMuted(tracks, MEDIA_TYPE.VIDEO, participantID),
[ isRemoteTrackMuted, participantID, tracks ]
);
const handleClick = useCallback(() => {
notifyClick?.();
if (notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
return;
}
sendAnalytics(createRemoteVideoMenuButtonEvent(
'video.mute.button',
{
'participant_id': participantID
}));
dispatch(openDialog(MuteRemoteParticipantsVideoDialog, { participantID }));
}, [ dispatch, notifyClick, notifyClick, participantID, sendAnalytics ]);
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') } />
);
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;
if (remoteControlState === REMOTE_CONTROL_MENU_STATES.STARTED) {
onRemoteControlToggle = () => dispatch(stopController(true));
} else if (remoteControlState === REMOTE_CONTROL_MENU_STATES.NOT_STARTED) {
onRemoteControlToggle = () => dispatch(requestRemoteControl(_getCurrentParticipantId()));
}
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 ]);
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'
};

@ -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