feat: add custom buttons for participant menu and toolbar via config (#12832)

* add custom remote menu button

* add config for custom buttons

* whitelist custom buttons flag

* add toolbox custom button

* fix notify toolbox buttons

* whitelist toolbar custom buttons

* rename and fix notify

* rename participant remote menu

* revert some flag wrong changes

* fix some formatings

* add undefined type to custom buttons toolbox

* code review

* code review 2

* fix linting issue
This commit is contained in:
Gabriel Borlea 2023-02-09 13:12:00 +02:00 committed by GitHub
parent 3a5833829c
commit 1a113ba733
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 180 additions and 8 deletions

View File

@ -799,6 +799,14 @@ var config = {
// 'microphone', 'camera', 'select-background', 'invite', 'settings'
// hiddenPremeetingButtons: [],
// An array with custom option buttons for the participant context menu
// type: Array<{ icon: string; id: string; text: string; }>
// customParticipantMenuButtons: [],
// An array with custom option buttons for the toolbar
// type: Array<{ icon: string; id: string; text: string; }>
// customToolbarButtons: [],
// Stats
//

View File

@ -1941,6 +1941,21 @@ 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.
* @returns {void}
*/
notifyParticipantMenuButtonClicked(key, participantId) {
this._sendEvent({
name: 'participant-menu-button-clicked',
key,
participantId
});
}
/**
* Disposes the allocated resources.
*

View File

@ -140,6 +140,7 @@ const events = {
'raise-hand-updated': 'raiseHandUpdated',
'recording-link-available': 'recordingLinkAvailable',
'recording-status-changed': 'recordingStatusChanged',
'participant-menu-button-clicked': 'participantMenuButtonClick',
'video-ready-to-close': 'readyToClose',
'video-conference-joined': 'videoConferenceJoined',
'video-conference-left': 'videoConferenceLeft',

View File

@ -206,6 +206,8 @@ export interface IConfig {
};
};
corsAvatarURLs?: Array<string>;
customParticipantMenuButtons?: Array<{ icon: string; id: string; text: string; }>;
customToolbarButtons?: Array<{ icon: string; id: string; text: string; }>;
deeplinking?: IDeeplinkingConfig;
defaultLanguage?: string;
defaultLocalDisplayName?: string;

View File

@ -32,9 +32,16 @@ export function getReplaceParticipant(state: IReduxState): string | undefined {
* @returns {Array<string>} - The list of enabled toolbar buttons.
*/
export function getToolbarButtons(state: IReduxState): Array<string> {
const { toolbarButtons } = state['features/base/config'];
const { toolbarButtons, customToolbarButtons } = state['features/base/config'];
const customButtons = customToolbarButtons?.map(({ id }) => id);
return Array.isArray(toolbarButtons) ? toolbarButtons : TOOLBAR_BUTTONS;
const buttons = Array.isArray(toolbarButtons) ? toolbarButtons : TOOLBAR_BUTTONS;
if (customButtons) {
buttons.push(...customButtons);
}
return buttons;
}
/**
@ -101,3 +108,30 @@ export function _setDeeplinkingDefaults(deeplinking: IDeeplinkingConfig) {
android.dynamicLink.isi = android.dynamicLink.isi || '1165103905';
}
}
/**
* Returns the list of buttons that have that notify the api when clicked.
*
* @param {Object} state - The redux state.
* @returns {Array} - The list of buttons.
*/
export function getButtonsWithNotifyClick(state: IReduxState): Array<{ key: string; preventExecution: boolean; }> {
const { buttonsWithNotifyClick, customToolbarButtons } = state['features/base/config'];
const customButtons = customToolbarButtons?.map(({ id }) => {
return {
key: id,
preventExecution: false
};
});
const buttons = Array.isArray(buttonsWithNotifyClick)
? buttonsWithNotifyClick as Array<{ key: string; preventExecution: boolean; }>
: [];
if (customButtons) {
buttons.push(...customButtons);
}
return buttons;
}

View File

@ -0,0 +1,44 @@
import React from 'react';
// eslint-disable-next-line lines-around-comment
// @ts-ignore
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
type Props = AbstractButtonProps & {
icon: string;
text: string;
};
/**
* Component that renders a custom toolbox button.
*
* @returns {Component}
*/
class CustomOptionButton extends AbstractButton<Props, any, any> {
// @ts-ignore
iconSrc = this.props.icon;
// @ts-ignore
id = this.props.id;
// @ts-ignore
text = this.props.text;
accessibilityLabel = this.text;
/**
* Custom icon component.
*
* @param {any} props - Icon's props.
* @returns {img}
*/
icon = (props: any) => (<img
src = { this.iconSrc }
{ ...props } />);
label = this.text;
tooltip = this.text;
}
export default CustomOptionButton;

View File

@ -12,6 +12,7 @@ import { sendAnalytics } from '../../../analytics/functions';
import { IReduxState } from '../../../app/types';
import { IJitsiConference } from '../../../base/conference/reducer';
import {
getButtonsWithNotifyClick,
getMultipleVideoSendingSupportFeatureFlag,
getToolbarButtons,
isToolbarButtonEnabled
@ -120,6 +121,7 @@ import HelpButton from '../HelpButton';
// @ts-ignore
import AudioSettingsButton from './AudioSettingsButton';
import CustomOptionButton from './CustomOptionButton';
import { EndConferenceButton } from './EndConferenceButton';
// @ts-ignore
import FullscreenButton from './FullscreenButton';
@ -174,6 +176,11 @@ interface IProps extends WithTranslation {
*/
_conference?: IJitsiConference;
/**
* Custom Toolbar buttons.
*/
_customToolbarButtons?: Array<{ icon: string; id: string; text: string; }>;
/**
* Whether or not screensharing button is disabled.
*/
@ -715,6 +722,7 @@ class Toolbox extends Component<IProps> {
*/
_getAllButtons() {
const {
_customToolbarButtons,
_feedbackConfigured,
_hasSalesforce,
_isIosMobile,
@ -915,6 +923,19 @@ class Toolbox extends Component<IProps> {
group: 4
};
const customButtons = _customToolbarButtons?.reduce((prev, { icon, id, text }) => {
return {
...prev,
[id]: {
key: id,
Content: CustomOptionButton,
group: 4,
icon,
text
}
};
}, {});
return {
microphone,
camera,
@ -945,7 +966,8 @@ class Toolbox extends Component<IProps> {
embed,
feedback,
download,
help
help,
...customButtons
};
}
@ -1525,8 +1547,8 @@ function _mapStateToProps(state: IReduxState, ownProps: Partial<IProps>) {
const endConferenceSupported = conference?.isEndConferenceSupported() && isLocalParticipantModerator(state);
const {
buttonsWithNotifyClick,
callStatsID,
customToolbarButtons,
disableProfile,
iAmRecorder,
iAmSipGateway
@ -1544,10 +1566,11 @@ function _mapStateToProps(state: IReduxState, ownProps: Partial<IProps>) {
return {
_backgroundType: state['features/virtual-background'].backgroundType ?? '',
_buttonsWithNotifyClick: buttonsWithNotifyClick,
_buttonsWithNotifyClick: getButtonsWithNotifyClick(state),
_chatOpen: state['features/chat'].isOpen,
_clientWidth: clientWidth,
_conference: conference,
_customToolbarButtons: customToolbarButtons,
_desktopSharingEnabled: JitsiMeetJS.isDesktopSharingEnabled(),
_desktopSharingButtonDisabled: isDesktopShareButtonDisabled(state),
_dialog: Boolean(state['features/base/dialog'].component),

View File

@ -0,0 +1,27 @@
import React, { useCallback } from 'react';
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
const CustomOptionButton = (
{ icon: iconSrc, onClick, text }:
{
icon: string;
onClick: (e?: React.MouseEvent<Element, MouseEvent> | undefined) => void;
text: string;
}
) => {
const icon = useCallback(props => (<img
src = { iconSrc }
{ ...props } />), [ iconSrc ]);
return (
<ContextMenuItem
accessibilityLabel = { text }
icon = { icon }
onClick = { onClick }
text = { text } />
);
};
export default CustomOptionButton;

View File

@ -26,6 +26,7 @@ import { isForceMuted } from '../../../participants-pane/functions';
import { requestRemoteControl, stopController } from '../../../remote-control';
import { showOverflowDrawer } from '../../../toolbox/functions.web';
import CustomOptionButton from './CustomOptionButton';
// @ts-ignore
import { REMOTE_CONTROL_MENU_STATES } from './RemoteControlButton';
// @ts-ignore
@ -144,7 +145,7 @@ const ParticipantContextMenu = ({
isForceMuted(participant, MEDIA_TYPE.VIDEO, state));
const _isAudioMuted = useSelector((state: IReduxState) => isParticipantAudioMuted(participant, state));
const _overflowDrawer: boolean = useSelector(showOverflowDrawer);
const { remoteVideoMenu = {}, disableRemoteMute, startSilent }
const { remoteVideoMenu = {}, disableRemoteMute, startSilent, customParticipantMenuButtons }
= useSelector((state: IReduxState) => state['features/base/config']);
const { disableKick, disableGrantModerator, disablePrivateChat } = remoteVideoMenu;
const { participantsVolume } = useSelector((state: IReduxState) => state['features/filmstrip']);
@ -171,8 +172,8 @@ const ParticipantContextMenu = ({
}
, [ thumbnailMenu, _overflowDrawer, drawerParticipant, participant ]);
const buttons = [];
const buttons2 = [];
const buttons: JSX.Element[] = [];
const buttons2: JSX.Element[] = [];
const showVolumeSlider = !startSilent
&& !isIosMobileBrowser()
@ -277,6 +278,23 @@ const ParticipantContextMenu = ({
);
}
if (customParticipantMenuButtons) {
customParticipantMenuButtons.forEach(
({ icon, id, text }) => {
const onClick = useCallback(
() => APP.API.notifyParticipantMenuButtonClicked(id, _getCurrentParticipantId()), []);
buttons2.push(
<CustomOptionButton
icon = { icon }
key = { id }
onClick = { onClick }
text = { text } />
);
}
);
}
const breakoutRoomsButtons: any = [];
if (!thumbnailMenu && _isModerator) {