From 36671d7c4f35d2ffaa021d366d7d931319f028ae Mon Sep 17 00:00:00 2001 From: Calinteodor Date: Wed, 10 Apr 2024 14:51:10 +0300 Subject: [PATCH] feat(toolbox/native): custom overflow menu buttons (#14594) * feat(toolbox/native): custom buttons for the OverflowMenu --- .../org/jitsi/meet/sdk/BroadcastEvent.java | 6 ++- .../org/jitsi/meet/sdk/JitsiMeetActivity.java | 7 +++ ios/app/src/ViewController.m | 4 ++ ios/sdk/src/JitsiMeetViewDelegate.h | 7 +++ .../mobile/external-api/actionTypes.ts | 11 +++++ react/features/mobile/external-api/actions.ts | 21 +++++++++ .../mobile/external-api/middleware.ts | 25 +++++++++- .../components/native/CustomOptionButton.tsx | 42 +++++++++++++++++ .../components/native/OverflowMenu.tsx | 47 ++++++++++++++++++- .../toolbox/components/native/styles.ts | 5 ++ 10 files changed, 172 insertions(+), 3 deletions(-) create mode 100644 react/features/toolbox/components/native/CustomOptionButton.tsx diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/BroadcastEvent.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/BroadcastEvent.java index 14a3e6e9a127..d90bf07f60d7 100644 --- a/android/sdk/src/main/java/org/jitsi/meet/sdk/BroadcastEvent.java +++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/BroadcastEvent.java @@ -90,7 +90,8 @@ public enum Type { CHAT_TOGGLED("org.jitsi.meet.CHAT_TOGGLED"), VIDEO_MUTED_CHANGED("org.jitsi.meet.VIDEO_MUTED_CHANGED"), READY_TO_CLOSE("org.jitsi.meet.READY_TO_CLOSE"), - TRANSCRIPTION_CHUNK_RECEIVED("org.jitsi.meet.TRANSCRIPTION_CHUNK_RECEIVED"); + TRANSCRIPTION_CHUNK_RECEIVED("org.jitsi.meet.TRANSCRIPTION_CHUNK_RECEIVED"), + CUSTOM_OVERFLOW_MENU_BUTTON_PRESSED("org.jitsi.meet.CUSTOM_OVERFLOW_MENU_BUTTON_PRESSED"); private static final String CONFERENCE_BLURRED_NAME = "CONFERENCE_BLURRED"; private static final String CONFERENCE_FOCUSED_NAME = "CONFERENCE_FOCUSED"; @@ -108,6 +109,7 @@ public enum Type { private static final String VIDEO_MUTED_CHANGED_NAME = "VIDEO_MUTED_CHANGED"; private static final String READY_TO_CLOSE_NAME = "READY_TO_CLOSE"; private static final String TRANSCRIPTION_CHUNK_RECEIVED_NAME = "TRANSCRIPTION_CHUNK_RECEIVED"; + private static final String CUSTOM_OVERFLOW_MENU_BUTTON_PRESSED_NAME = "CUSTOM_OVERFLOW_MENU_BUTTON_PRESSED"; private final String action; @@ -162,6 +164,8 @@ private static Type buildTypeFromName(String name) { return READY_TO_CLOSE; case TRANSCRIPTION_CHUNK_RECEIVED_NAME: return TRANSCRIPTION_CHUNK_RECEIVED; + case CUSTOM_OVERFLOW_MENU_BUTTON_PRESSED_NAME: + return CUSTOM_OVERFLOW_MENU_BUTTON_PRESSED; } return null; diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetActivity.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetActivity.java index 047954d36fcf..816d5a1dd42f 100644 --- a/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetActivity.java +++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetActivity.java @@ -257,6 +257,10 @@ protected void onReadyToClose() { // protected void onTranscriptionChunkReceived(HashMap extraData) { // JitsiMeetLogger.i("Transcription chunk received: " + extraData); +// } + +// protected void onCustomOverflowMenuButtonPressed(HashMap extraData) { +// JitsiMeetLogger.i("Custom overflow menu button pressed: " + extraData); // } // Activity lifecycle methods @@ -344,6 +348,9 @@ private void onBroadcastReceived(Intent intent) { break; // case TRANSCRIPTION_CHUNK_RECEIVED: // onTranscriptionChunkReceived(event.getData()); +// break; +// case CUSTOM_OVERFLOW_MENU_BUTTON_PRESSED: +// onCustomOverflowMenuButtonPressed(event.getData()); // break; } } diff --git a/ios/app/src/ViewController.m b/ios/app/src/ViewController.m index 9845817411a4..a169c17d3cef 100644 --- a/ios/app/src/ViewController.m +++ b/ios/app/src/ViewController.m @@ -88,6 +88,10 @@ - (void)conferenceWillJoin:(NSDictionary *)data { [self _onJitsiMeetViewDelegateEvent:@"CONFERENCE_WILL_JOIN" withData:data]; } +// - (void)customOverflowMenuButtonPressed:(NSDictionary *)data { +// [self _onJitsiMeetViewDelegateEvent:@"CUSTOM_OVERFLOW_MENU_BUTTON_PRESSED" withData:data]; +// } + #if 0 - (void)enterPictureInPicture:(NSDictionary *)data { [self _onJitsiMeetViewDelegateEvent:@"ENTER_PICTURE_IN_PICTURE" withData:data]; diff --git a/ios/sdk/src/JitsiMeetViewDelegate.h b/ios/sdk/src/JitsiMeetViewDelegate.h index 5818730a68e2..56f388eaeb6c 100644 --- a/ios/sdk/src/JitsiMeetViewDelegate.h +++ b/ios/sdk/src/JitsiMeetViewDelegate.h @@ -123,4 +123,11 @@ */ - (void)transcriptionChunkReceived:(NSDictionary *)data; +/** + * Called when the custom overflow menu button is pressed. + * + * The `data` dictionary contains a `id`, `text` key. + */ +- (void)customOverflowMenuButtonPressed:(NSDictionary *)data; + @end diff --git a/react/features/mobile/external-api/actionTypes.ts b/react/features/mobile/external-api/actionTypes.ts index 8f1964170db9..a672e62177e3 100644 --- a/react/features/mobile/external-api/actionTypes.ts +++ b/react/features/mobile/external-api/actionTypes.ts @@ -18,3 +18,14 @@ export const READY_TO_CLOSE = 'READY_TO_CLOSE'; */ export const SCREEN_SHARE_PARTICIPANTS_UPDATED = 'SCREEN_SHARE_PARTICIPANTS_UPDATED'; + +/** + * The type of (redux) action which signals that a custom button from the overflow menu was pressed. + * + * @returns {{ + * type: CUSTOM_OVERFLOW_MENU_BUTTON_PRESSED, + * id: string, + * text: string + * }} + */ +export const CUSTOM_OVERFLOW_MENU_BUTTON_PRESSED = 'CUSTOM_OVERFLOW_MENU_BUTTON_PRESSED'; diff --git a/react/features/mobile/external-api/actions.ts b/react/features/mobile/external-api/actions.ts index a4fdf6d634ec..7614062e222c 100644 --- a/react/features/mobile/external-api/actions.ts +++ b/react/features/mobile/external-api/actions.ts @@ -1,8 +1,10 @@ import { + CUSTOM_OVERFLOW_MENU_BUTTON_PRESSED, READY_TO_CLOSE, SCREEN_SHARE_PARTICIPANTS_UPDATED } from './actionTypes'; + /** * Creates a (redux) action which signals that the SDK is ready to be closed. * @@ -33,3 +35,22 @@ export function setParticipantsWithScreenShare(participantIds: Array) { participantIds }; } + +/** + * Creates a (redux) action which that a custom overflow menu button was pressed. + * + * @param {string} id - The id for the custom button. + * @param {string} text - The label for the custom button. + * @returns {{ + * type: CUSTOM_OVERFLOW_MENU_BUTTON_PRESSED, + * id: string, + * text: string + * }} + */ +export function customOverflowMenuButtonPressed(id: string, text: string) { + return { + type: CUSTOM_OVERFLOW_MENU_BUTTON_PRESSED, + id, + text + }; +} diff --git a/react/features/mobile/external-api/middleware.ts b/react/features/mobile/external-api/middleware.ts index 55a7a24d3d4f..7c1c1f6798dd 100644 --- a/react/features/mobile/external-api/middleware.ts +++ b/react/features/mobile/external-api/middleware.ts @@ -57,7 +57,10 @@ import { ENTER_PICTURE_IN_PICTURE } from '../picture-in-picture/actionTypes'; // @ts-ignore import { isExternalAPIAvailable } from '../react-native-sdk/functions'; -import { READY_TO_CLOSE } from './actionTypes'; +import { + CUSTOM_OVERFLOW_MENU_BUTTON_PRESSED, + READY_TO_CLOSE +} from './actionTypes'; import { setParticipantsWithScreenShare } from './actions'; import { participantToParticipantInfo, sendEvent } from './functions'; import logger from './logger'; @@ -79,6 +82,12 @@ const CHAT_TOGGLED = 'CHAT_TOGGLED'; */ const CONFERENCE_TERMINATED = 'CONFERENCE_TERMINATED'; +/** + * Event which will be emitted on the native side to indicate that the custom overflow menu button was pressed. + */ +const CUSTOM_MENU_BUTTON_PRESSED = 'CUSTOM_MENU_BUTTON_PRESSED'; + + /** * Event which will be emitted on the native side to indicate a message was received * through the channel. @@ -185,6 +194,20 @@ externalAPIEnabled && MiddlewareRegistry.register(store => next => action => { break; } + case CUSTOM_OVERFLOW_MENU_BUTTON_PRESSED: { + const { id, text } = action; + + sendEvent( + store, + CUSTOM_MENU_BUTTON_PRESSED, + { + id, + text + }); + + break; + } + case ENDPOINT_MESSAGE_RECEIVED: { const { participant, data } = action; diff --git a/react/features/toolbox/components/native/CustomOptionButton.tsx b/react/features/toolbox/components/native/CustomOptionButton.tsx new file mode 100644 index 000000000000..357d20d3e9a4 --- /dev/null +++ b/react/features/toolbox/components/native/CustomOptionButton.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { Image } from 'react-native'; +import { connect } from 'react-redux'; + +import { translate } from '../../../base/i18n/functions'; +import AbstractButton, { IProps as AbstractButtonProps } + from '../../../base/toolbox/components/AbstractButton'; + +import styles from './styles'; + + +interface IProps extends AbstractButtonProps { + icon: any; + id?: string; + text: string; +} + +/** + * Component that renders a custom button. + * + * @returns {Component} + */ +class CustomOptionButton extends AbstractButton { + iconSrc = this.props.icon; + id = this.props.id; + text = this.props.text; + + /** + * Custom icon component. + * + * @returns {React.Component} + */ + icon = () => ( + + ); + + label = this.text; +} + +export default translate(connect()(CustomOptionButton)); diff --git a/react/features/toolbox/components/native/OverflowMenu.tsx b/react/features/toolbox/components/native/OverflowMenu.tsx index 9e607f1f72c0..480ba5dbeca4 100644 --- a/react/features/toolbox/components/native/OverflowMenu.tsx +++ b/react/features/toolbox/components/native/OverflowMenu.tsx @@ -11,6 +11,7 @@ import SettingsButton from '../../../base/settings/components/native/SettingsBut import BreakoutRoomsButton from '../../../breakout-rooms/components/native/BreakoutRoomsButton'; import SharedDocumentButton from '../../../etherpad/components/SharedDocumentButton.native'; +import { customOverflowMenuButtonPressed } from '../../../mobile/external-api/actions'; import ReactionMenu from '../../../reactions/components/native/ReactionMenu'; import { shouldDisplayReactionsButtons } from '../../../reactions/functions.any'; import LiveStreamButton from '../../../recording/components/LiveStream/native/LiveStreamButton'; @@ -27,6 +28,7 @@ import WhiteboardButton from '../../../whiteboard/components/native/WhiteboardBu import { getMovableButtons } from '../../functions.native'; import AudioOnlyButton from './AudioOnlyButton'; +import CustomOptionButton from './CustomOptionButton'; import LinkToSalesforceButton from './LinkToSalesforceButton'; import OpenCarmodeButton from './OpenCarmodeButton'; import RaiseHandButton from './RaiseHandButton'; @@ -38,6 +40,11 @@ import ScreenSharingButton from './ScreenSharingButton'; */ interface IProps { + /** + * Custom Toolbar buttons. + */ + _customToolbarButtons?: Array<{ backgroundColor?: string; icon: string; id: string; text: string; }>; + /** * True if breakout rooms feature is available, false otherwise. */ @@ -145,6 +152,7 @@ class OverflowMenu extends PureComponent { renderFooter = { _shouldDisplayReactionsButtons && !toolbarButtons.has('raisehand') ? this._renderReactionMenu : undefined }> + { this._renderCustomOverflowMenuButtons(topButtonProps) } { @@ -187,7 +195,7 @@ class OverflowMenu extends PureComponent { /** * Function to render the reaction menu as the footer of the bottom sheet. * - * @returns {React$Element} + * @returns {React.ReactElement} */ _renderReactionMenu() { return ( @@ -196,6 +204,41 @@ class OverflowMenu extends PureComponent { overflowMenu = { true } /> ); } + + /** + * Function to render the custom buttons for the overflow menu. + * + * @param {Object} topButtonProps - Button properties. + * @returns {React.ReactElement} + */ + _renderCustomOverflowMenuButtons(topButtonProps: Object) { + const { _customToolbarButtons, dispatch } = this.props; + + if (!_customToolbarButtons?.length) { + return; + } + + return ( + <> + { + _customToolbarButtons.map(({ id, text, icon, ...rest }) => ( + + dispatch(customOverflowMenuButtonPressed(id, text)) + } + icon = { icon } + key = { id } + text = { text } /> + )) + } + + + ); + } } /** @@ -207,8 +250,10 @@ class OverflowMenu extends PureComponent { */ function _mapStateToProps(state: IReduxState) { const { conference } = state['features/base/conference']; + const { customToolbarButtons } = state['features/base/config']; return { + _customToolbarButtons: customToolbarButtons, _isBreakoutRoomsSupported: conference?.getBreakoutRooms()?.isSupported(), _isSpeakerStatsDisabled: isSpeakerStatsDisabled(state), _shouldDisplayReactionsButtons: shouldDisplayReactionsButtons(state), diff --git a/react/features/toolbox/components/native/styles.ts b/react/features/toolbox/components/native/styles.ts index f70ec13a7f1d..da33b77ac7ec 100644 --- a/react/features/toolbox/components/native/styles.ts +++ b/react/features/toolbox/components/native/styles.ts @@ -103,6 +103,11 @@ const styles = { marginLeft: 'auto', marginRight: 'auto', width: '100%' + }, + + iconImageStyles: { + height: BaseTheme.spacing[5], + width: BaseTheme.spacing[5] } };