Skip to content

Fix Role Identify Issue in Jitsi react native SDK (for moderator/non-moderator handle) #16954

@Praneethgeethanjana

Description

@Praneethgeethanjana

Hi! đź‘‹

Firstly, thanks for your work on this project! 🙂

Today I used patch-package to patch @jitsi/react-native-sdk@11.5.1 for the project I'm working on.

While working with @jitsi/react-native-sdk@11.5.1, I ran into a few limitations that affected moderator-based UI behavior. I was able to resolve them locally using patch-package, and I wanted to share the changes in case they’re useful to others or can be considered for upstream inclusion.

This issue body was partially generated by patch-package.

Problems Addressed

  1. Missing Role Change Event Support

There was no exposed onParticipantRoleChanged callback in the RN SDK wrapper.
Because of this, it was not possible to detect when a participant (including the local user) was promoted or demoted during an active conference.

  1. Contextless onConferenceJoined

The onConferenceJoined callback was triggered without any payload.
As a result, the app had no way to know the local participant’s role immediately after joining the conference.

  1. Static flags and config Props

The flags, config, and userInfo props were only applied on initial mount.

If the parent component updated these props later (for example, enabling screen sharing after the user becomes a moderator), the SDK did not react to those changes.

  1. Hardcoded UI Element

The “Open Shared Document” button was hardcoded in the overflow menu and could not be easily disabled or hidden via configuration.

Solution

I applied a patch that introduces:

onParticipantRoleChanged event support

Role awareness for the local participant

Payload support for onConferenceJoined

Dynamic updates for flags, config, and userInfo

Ability to hide the “Open Shared Document” menu item

Here is the diff that solved my problem:

diff --git a/node_modules/@jitsi/react-native-sdk/index.tsx b/node_modules/@jitsi/react-native-sdk/index.tsx
index 6923d91..3e7524d 100644
--- a/node_modules/@jitsi/react-native-sdk/index.tsx
+++ b/node_modules/@jitsi/react-native-sdk/index.tsx
@@ -30,10 +30,12 @@ interface IEventListeners {
     onConferenceJoined?: Function;
     onConferenceLeft?: Function;
     onConferenceWillJoin?: Function;
+    onCustomToolbarButtonPressed?: (data: { id: string; text?: string }) => void;
     onEnterPictureInPicture?: Function;
     onEndpointMessageReceived?: Function;
     onParticipantJoined?: Function;
     onParticipantLeft?: ({ id }: { id: string }) => void;
+    onParticipantRoleChanged?: (data: { id: string; role: string }) => void;
     onReadyToClose?: Function;
 }
 
@@ -66,7 +68,7 @@ export interface JitsiRefProps {
  * Main React Native SDK component that displays a Jitsi Meet conference and gets all required params as props
  */
 export const JitsiMeeting = forwardRef<JitsiRefProps, IAppProps>((props, ref) => {
-    const [ appProps, setAppProps ] = useState({});
+    const [appProps, setAppProps] = useState({});
     const app = useRef(null);
     const {
         config,
@@ -140,16 +142,18 @@ export const JitsiMeeting = forwardRef<JitsiRefProps, IAppProps>((props, ref) =>
                     onConferenceJoined: eventListeners?.onConferenceJoined,
                     onConferenceWillJoin: eventListeners?.onConferenceWillJoin,
                     onConferenceLeft: eventListeners?.onConferenceLeft,
+                    onCustomToolbarButtonPressed: eventListeners?.onCustomToolbarButtonPressed,
                     onEnterPictureInPicture: eventListeners?.onEnterPictureInPicture,
                     onEndpointMessageReceived: eventListeners?.onEndpointMessageReceived,
                     onParticipantJoined: eventListeners?.onParticipantJoined,
                     onParticipantLeft: eventListeners?.onParticipantLeft,
+                    onParticipantRoleChanged: eventListeners?.onParticipantRoleChanged,
                     onReadyToClose: eventListeners?.onReadyToClose
                 },
                 'url': urlProps,
                 'userInfo': userInfo
             });
-        }, []
+        }, [config, flags, room, serverURL, token, userInfo, eventListeners]
     );
 
     // eslint-disable-next-line arrow-body-style
@@ -167,10 +171,10 @@ export const JitsiMeeting = forwardRef<JitsiRefProps, IAppProps>((props, ref) =>
     }, []);
 
     return (
-        <View style = { style as ViewStyle }>
+        <View style={style as ViewStyle}>
             <App
-                { ...appProps }
-                ref = { app } />
+                {...appProps}
+                ref={app} />
         </View>
     );
 });
\ No newline at end of file
diff --git a/node_modules/@jitsi/react-native-sdk/react/features/app/components/App.native.tsx b/node_modules/@jitsi/react-native-sdk/react/features/app/components/App.native.tsx
index 93c26fc..ee5b872 100644
--- a/node_modules/@jitsi/react-native-sdk/react/features/app/components/App.native.tsx
+++ b/node_modules/@jitsi/react-native-sdk/react/features/app/components/App.native.tsx
@@ -7,6 +7,7 @@ import { hideSplash } from 'react-native-splash-view';
 
 import BottomSheetContainer from '../../base/dialog/components/native/BottomSheetContainer';
 import DialogContainer from '../../base/dialog/components/native/DialogContainer';
+import { overwriteConfig } from '../../base/config/actions';
 import { updateFlags } from '../../base/flags/actions';
 import { CALL_INTEGRATION_ENABLED } from '../../base/flags/constants';
 import { clientResized, setSafeAreaInsets } from '../../base/responsive-ui/actions';
@@ -101,11 +102,39 @@ export class App extends AbstractApp<IProps> {
     override render() {
         return (
             <JitsiThemePaperProvider>
-                { super.render() }
+                {super.render()}
             </JitsiThemePaperProvider>
         );
     }
 
+    /**
+     * Implements React Component's componentDidUpdate.
+     *
+     * @inheritdoc
+     */
+    override async componentDidUpdate(prevProps: IProps) {
+        await super.componentDidUpdate(prevProps);
+
+        const { dispatch } = this.state.store ?? {};
+        const { flags, url, userInfo } = this.props;
+        // @ts-ignore
+        const config = url?.config;
+        // @ts-ignore
+        const prevConfig = prevProps.url?.config;
+
+        if (config && JSON.stringify(prevConfig) !== JSON.stringify(config)) {
+            dispatch?.(overwriteConfig(config));
+        }
+
+        if (flags && JSON.stringify(prevProps.flags) !== JSON.stringify(flags)) {
+            dispatch?.(updateFlags(flags));
+        }
+
+        if (userInfo && JSON.stringify(prevProps.userInfo) !== JSON.stringify(userInfo)) {
+            dispatch?.(updateSettings(userInfo));
+        }
+    }
+
     /**
      * Initializes feature flags and updates settings.
      *
@@ -185,9 +214,9 @@ export class App extends AbstractApp<IProps> {
         return (
             <SafeAreaProvider>
                 <DimensionsDetector
-                    onDimensionsChanged = { this._onDimensionsChanged }
-                    onSafeAreaInsetsChanged = { this._onSafeAreaInsetsChanged }>
-                    { super._createMainElement(component, props) }
+                    onDimensionsChanged={this._onDimensionsChanged}
+                    onSafeAreaInsetsChanged={this._onSafeAreaInsetsChanged}>
+                    {super._createMainElement(component, props)}
                 </DimensionsDetector>
             </SafeAreaProvider>
         );
@@ -273,8 +302,8 @@ export class App extends AbstractApp<IProps> {
     _renderDialogContainer() {
         return (
             <DialogContainerWrapper
-                pointerEvents = 'box-none'
-                style = { StyleSheet.absoluteFill }>
+                pointerEvents='box-none'
+                style={StyleSheet.absoluteFill}>
                 <BottomSheetContainer />
                 <DialogContainer />
             </DialogContainerWrapper>
diff --git a/node_modules/@jitsi/react-native-sdk/react/features/mobile/react-native-sdk/middleware.js b/node_modules/@jitsi/react-native-sdk/react/features/mobile/react-native-sdk/middleware.js
index 13bf6b3..3091985 100644
--- a/node_modules/@jitsi/react-native-sdk/react/features/mobile/react-native-sdk/middleware.js
+++ b/node_modules/@jitsi/react-native-sdk/react/features/mobile/react-native-sdk/middleware.js
@@ -9,8 +9,10 @@ import {
     CONFERENCE_WILL_JOIN,
     ENDPOINT_MESSAGE_RECEIVED
 } from '../../base/conference/actionTypes';
+import { CUSTOM_BUTTON_PRESSED } from "../../toolbox/actionTypes";
 import { SET_AUDIO_MUTED, SET_VIDEO_MUTED } from '../../base/media/actionTypes';
-import { PARTICIPANT_JOINED, PARTICIPANT_LEFT } from '../../base/participants/actionTypes';
+import { PARTICIPANT_JOINED, PARTICIPANT_LEFT, PARTICIPANT_ROLE_CHANGED, PARTICIPANT_UPDATED } from '../../base/participants/actionTypes';
+import { getLocalParticipant } from '../../base/participants/functions';
 import MiddlewareRegistry from '../../base/redux/MiddlewareRegistry';
 import StateListenerRegistry from '../../base/redux/StateListenerRegistry';
 import { READY_TO_CLOSE } from '../external-api/actionTypes';
@@ -33,57 +35,88 @@ const { JMOngoingConference } = NativeModules;
     const rnSdkHandlers = getAppProp(store, 'rnSdkHandlers');
 
     switch (type) {
-    case SET_AUDIO_MUTED:
-        rnSdkHandlers?.onAudioMutedChanged?.(action.muted);
-        break;
-    case SET_VIDEO_MUTED:
-        rnSdkHandlers?.onVideoMutedChanged?.(Boolean(action.muted));
-        break;
-    case CONFERENCE_BLURRED:
-        rnSdkHandlers?.onConferenceBlurred?.();
-        break;
-    case CONFERENCE_FOCUSED:
-        rnSdkHandlers?.onConferenceFocused?.();
-        break;
-    case CONFERENCE_JOINED:
-        rnSdkHandlers?.onConferenceJoined?.();
-        break;
-    case CONFERENCE_LEFT:
-        //  Props are torn down at this point, perhaps need to leave this one out
-        break;
-    case CONFERENCE_WILL_JOIN:
-        rnSdkHandlers?.onConferenceWillJoin?.();
-        break;
-    case ENTER_PICTURE_IN_PICTURE:
-        rnSdkHandlers?.onEnterPictureInPicture?.();
-        break;
-    case ENDPOINT_MESSAGE_RECEIVED: {
-        const { data, participant } = action;
-
-        rnSdkHandlers?.onEndpointMessageReceived?.({
-            data,
-            participant
-        });
-        break;
-    }
-    case PARTICIPANT_JOINED: {
-        const { participant } = action;
-        const participantInfo = participantToParticipantInfo(participant);
+        case SET_AUDIO_MUTED:
+            rnSdkHandlers?.onAudioMutedChanged?.(action.muted);
+            break;
+        case SET_VIDEO_MUTED:
+            rnSdkHandlers?.onVideoMutedChanged?.(Boolean(action.muted));
+            break;
+        case CONFERENCE_BLURRED:
+            rnSdkHandlers?.onConferenceBlurred?.();
+            break;
+        case CONFERENCE_FOCUSED:
+            rnSdkHandlers?.onConferenceFocused?.();
+            break;
+        case CONFERENCE_JOINED: {
+            const localParticipant = getLocalParticipant(store.getState());
+            const participantInfo = localParticipant ? participantToParticipantInfo(localParticipant) : undefined;
 
-        rnSdkHandlers?.onParticipantJoined?.(participantInfo);
-        break;
-    }
-    case PARTICIPANT_LEFT: {
-        const { participant } = action;
+            rnSdkHandlers?.onConferenceJoined?.(participantInfo);
+            break;
+        }
+        case CONFERENCE_LEFT:
+            //  Props are torn down at this point, perhaps need to leave this one out
+            break;
+        case CONFERENCE_WILL_JOIN:
+            rnSdkHandlers?.onConferenceWillJoin?.();
+            break;
+        case ENTER_PICTURE_IN_PICTURE:
+            rnSdkHandlers?.onEnterPictureInPicture?.();
+            break;
+        case ENDPOINT_MESSAGE_RECEIVED: {
+            const { data, participant } = action;
+
+            rnSdkHandlers?.onEndpointMessageReceived?.({
+                data,
+                participant
+            });
+            break;
+        }
+        case PARTICIPANT_JOINED: {
+            const { participant } = action;
+            const participantInfo = participantToParticipantInfo(participant);
+
+            rnSdkHandlers?.onParticipantJoined?.(participantInfo);
+            break;
+        }
+        case PARTICIPANT_ROLE_CHANGED: {
+            const { participant, role } = action;
 
-        const { id } = participant ?? {};
+            if (role) {
+                const id = participant?.id;
+                const localParticipant = getLocalParticipant(store.getState());
+                const isLocal = localParticipant?.id === id;
 
-        rnSdkHandlers?.onParticipantLeft?.({ id });
-        break;
-    }
-    case READY_TO_CLOSE:
-        rnSdkHandlers?.onReadyToClose?.();
-        break;
+                rnSdkHandlers?.onParticipantRoleChanged?.({ id, role, isLocal });
+            }
+            break;
+        }
+        case PARTICIPANT_UPDATED: {
+            const { participant } = action;
+            const { id, role } = participant ?? {};
+
+            if (role) {
+                const localParticipant = getLocalParticipant(store.getState());
+                const isLocal = localParticipant?.id === id;
+
+                rnSdkHandlers?.onParticipantRoleChanged?.({ id, role, isLocal });
+            }
+            break;
+        }
+        case PARTICIPANT_LEFT: {
+            const { participant } = action;
+
+            const { id } = participant ?? {};
+
+            rnSdkHandlers?.onParticipantLeft?.({ id });
+            break;
+        }
+        case READY_TO_CLOSE:
+            rnSdkHandlers?.onReadyToClose?.();
+            break;
+        case CUSTOM_BUTTON_PRESSED:
+            rnSdkHandlers?.onCustomToolbarButtonPressed?.({ id: action.id, text: action.text });
+            break;
     }
 
     return result;
diff --git a/node_modules/@jitsi/react-native-sdk/react/features/toolbox/components/native/OverflowMenu.tsx b/node_modules/@jitsi/react-native-sdk/react/features/toolbox/components/native/OverflowMenu.tsx
index 6677519..938eaf0 100644
--- a/node_modules/@jitsi/react-native-sdk/react/features/toolbox/components/native/OverflowMenu.tsx
+++ b/node_modules/@jitsi/react-native-sdk/react/features/toolbox/components/native/OverflowMenu.tsx
@@ -153,28 +153,28 @@ class OverflowMenu extends PureComponent<IProps, IState> {
 
         return (
             <BottomSheet
-                renderFooter = { this._renderReactionMenu }>
-                <Divider style = { styles.divider as ViewStyle } />
-                <OpenCarmodeButton { ...topButtonProps } />
-                <AudioOnlyButton { ...buttonProps } />
-                { this._renderRaiseHandButton(buttonProps) }
+                renderFooter={this._renderReactionMenu}>
+                <Divider style={styles.divider as ViewStyle} />
+                <OpenCarmodeButton {...topButtonProps} />
+                <AudioOnlyButton {...buttonProps} />
+                {this._renderRaiseHandButton(buttonProps)}
                 {/* @ts-ignore */}
-                <SecurityDialogButton { ...buttonProps } />
-                <RecordButton { ...buttonProps } />
-                <LiveStreamButton { ...buttonProps } />
-                <LinkToSalesforceButton { ...buttonProps } />
-                <WhiteboardButton { ...buttonProps } />
+                <SecurityDialogButton {...buttonProps} />
+                <RecordButton {...buttonProps} />
+                <LiveStreamButton {...buttonProps} />
+                <LinkToSalesforceButton {...buttonProps} />
+                <WhiteboardButton {...buttonProps} />
                 {/* @ts-ignore */}
-                <Divider style = { styles.divider as ViewStyle } />
-                {_isSharedVideoEnabled && <SharedVideoButton { ...buttonProps } />}
-                { this._renderOverflowMenuButtons(topButtonProps) }
-                {!_isSpeakerStatsDisabled && <SpeakerStatsButton { ...buttonProps } />}
-                {_isBreakoutRoomsSupported && <BreakoutRoomsButton { ...buttonProps } />}
+                <Divider style={styles.divider as ViewStyle} />
+                {_isSharedVideoEnabled && <SharedVideoButton {...buttonProps} />}
+                {this._renderOverflowMenuButtons(topButtonProps)}
+                {!_isSpeakerStatsDisabled && <SpeakerStatsButton {...buttonProps} />}
+                {_isBreakoutRoomsSupported && <BreakoutRoomsButton {...buttonProps} />}
                 {/* @ts-ignore */}
-                <Divider style = { styles.divider as ViewStyle } />
-                <ClosedCaptionButton { ...buttonProps } />
-                <SharedDocumentButton { ...buttonProps } />
-                <SettingsButton { ...buttonProps } />
+                <Divider style={styles.divider as ViewStyle} />
+                <ClosedCaptionButton {...buttonProps} />
+                {/* <SharedDocumentButton { ...buttonProps } /> */}
+                <SettingsButton {...buttonProps} />
             </BottomSheet>
         );
     }
@@ -203,8 +203,8 @@ class OverflowMenu extends PureComponent<IProps, IState> {
         if (_shouldDisplayReactionsButtons && !isRaiseHandInMainMenu) {
             return (
                 <ReactionMenu
-                    onCancel = { this._onCancel }
-                    overflowMenu = { true } />
+                    onCancel={this._onCancel}
+                    overflowMenu={true} />
             );
         }
     }
@@ -223,7 +223,7 @@ class OverflowMenu extends PureComponent<IProps, IState> {
 
         if (!_shouldDisplayReactionsButtons && !isRaiseHandInMainMenu) {
             return (
-                <RaiseHandButton { ...buttonProps } />
+                <RaiseHandButton {...buttonProps} />
             );
         }
     }
@@ -252,17 +252,17 @@ class OverflowMenu extends PureComponent<IProps, IState> {
 
                         return (
                             <Content
-                                { ...topButtonProps }
-                                { ...rest }
+                                {...topButtonProps}
+                                {...rest}
                                 /* eslint-disable react/jsx-no-bind */
-                                handleClick = { () => dispatch(customButtonPressed(key, text)) }
-                                isToolboxButton = { false }
-                                key = { key }
-                                text = { text } />
+                                handleClick={() => dispatch(customButtonPressed(key, text))}
+                                isToolboxButton={false}
+                                key={key}
+                                text={text} />
                         );
                     })
                 }
-                <Divider style = { styles.divider as ViewStyle } />
+                <Divider style={styles.divider as ViewStyle} />
             </>
         );
     }
@@ -309,8 +309,8 @@ export default connect(_mapStateToProps)(props => {
         <OverflowMenu
 
             // @ts-ignore
-            { ... props }
-            _mainMenuButtons = { mainMenuButtons }
-            _overflowMenuButtons = { overflowMenuButtons } />
+            {...props}
+            _mainMenuButtons={mainMenuButtons}
+            _overflowMenuButtons={overflowMenuButtons} />
     );
 });
\ No newline at end of file

This issue body was partially generated by patch-package.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions