diff --git a/index.tsx b/index.tsx
index b8be536..62f8430 100644
--- a/index.tsx
+++ b/index.tsx
@@ -1,10 +1,14 @@
-import {customize} from 'customization-api';
-import AttendeeLayout from './layout/AttendeeLayout';
-import {CUSTOM_LAYOUT_NAME, VideoCallWrapper} from './wrapper/VideoCallWrapper';
-import {AppRootWrapper} from './wrapper/AppRootWrapper';
-import {BottomToolbarOverride} from './toolbar/BottomToolbarOverride';
-import {TopToolbarOverride} from './toolbar/TopToolbarOverride';
-import {PrecallWrapper} from './wrapper/PrecallWrapper';
+import { customize } from "customization-api";
+import AttendeeLayout from "./layout/AttendeeLayout";
+import {
+ CUSTOM_LAYOUT_NAME,
+ CustomWrapperProvider,
+} from "./wrapper/VideoCallWrapper";
+import { AppRootWrapper } from "./wrapper/AppRootWrapper";
+import { BottomToolbarOverride } from "./toolbar/BottomToolbarOverride";
+import { TopToolbarOverride } from "./toolbar/TopToolbarOverride";
+import { PrecallWrapper } from "./wrapper/PrecallWrapper";
+import CustomSidePanel from "./sidePanel/CustomSidePanel";
const config = customize({
components: {
@@ -20,18 +24,29 @@ const config = customize({
//hiding the unnessary controls
bottomToolBar: BottomToolbarOverride,
//to update layout for attendee and hide the share tile for the host and inject chat overlay
- wrapper: VideoCallWrapper,
+ wrapper: CustomWrapperProvider,
//adding custom layout
- customLayout: defaultLayouts => {
+ customLayout: (defaultLayouts) => {
return defaultLayouts.concat([
{
component: AttendeeLayout,
- icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABABAMAAABYR2ztAAAAA3NCSVQICAjb4U/gAAAACXBIWXMAABrYAAAa2AGP9iLXAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAABJQTFRF////AAAAAAAAAAAAAAAAAAAAZJzAmgAAAAV0Uk5TADhWeLi9mUy9AAAAQUlEQVRIS2MIhYJgBgYWGNuBgcEUxmYYVTCqAFmBMRQYMjAwwdgKDAzCMDbDkAAuBAA8HHCBUQWjCkYVjCoY4QoAw0lL0bRtNkoAAAAASUVORK5CYII=',
- label: 'Custom Layout',
+ icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABABAMAAABYR2ztAAAAA3NCSVQICAjb4U/gAAAACXBIWXMAABrYAAAa2AGP9iLXAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAABJQTFRF////AAAAAAAAAAAAAAAAAAAAZJzAmgAAAAV0Uk5TADhWeLi9mUy9AAAAQUlEQVRIS2MIhYJgBgYWGNuBgcEUxmYYVTCqAFmBMRQYMjAwwdgKDAzCMDbDkAAuBAA8HHCBUQWjCkYVjCoY4QoAw0lL0bRtNkoAAAAASUVORK5CYII=",
+ label: "Custom Layout",
name: CUSTOM_LAYOUT_NAME,
},
]);
},
+ // side panel to show raised hand users
+ customSidePanel: () => {
+ return [
+ {
+ name: "raise-hand-panel",
+ component: CustomSidePanel,
+ title: "Raised Hand Users",
+ onClose: () => {},
+ },
+ ];
+ },
},
},
});
diff --git a/sidePanel/CustomSidePanel.tsx b/sidePanel/CustomSidePanel.tsx
new file mode 100644
index 0000000..9fc068f
--- /dev/null
+++ b/sidePanel/CustomSidePanel.tsx
@@ -0,0 +1,86 @@
+import {
+ ScrollView,
+ StyleSheet,
+ Text,
+ TouchableOpacity,
+ View,
+} from "react-native";
+import React, { useContext } from "react";
+import { CustomWrapperContext } from "../wrapper/VideoCallWrapper";
+import { customEvents, UidType, useContent } from "customization-api";
+
+const CustomSidePanel = () => {
+ const { raisedHandUsers, setRaisedHandUsers } =
+ useContext(CustomWrapperContext);
+ const { defaultContent } = useContent();
+
+ // Handler to lower a user's raised hand
+ const handleLowerHand = (uid: UidType) => {
+ const data = {
+ hand_raised: false,
+ user_uid: uid,
+ };
+ // Emit custom event to notify other Hosts
+ customEvents.send("LowerHandEvent", JSON.stringify({ data }), uid);
+ // Remove the uid from the raiseHandUsers List
+ setRaisedHandUsers((prev) => prev.filter((user) => user.user_uid !== uid));
+ };
+
+ return (
+
+
+ {/* Filter and display only users with raised hands */}
+ {raisedHandUsers
+ .filter((user) => user.hand_raised)
+ .map((user) => (
+
+
+ {defaultContent[user.user_uid].name}
+
+ {/* Hand raised emoji indicator */}
+ ✋
+ {/* Button to lower user's hand */}
+ handleLowerHand(user.user_uid)}
+ style={styles.crossButton}
+ >
+ ❌
+
+
+ ))}
+
+
+ );
+};
+
+export default CustomSidePanel;
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: "#1A1A1A",
+ },
+ userRow: {
+ flexDirection: "row",
+ alignItems: "center",
+ padding: 12,
+ borderBottomWidth: 1,
+ borderBottomColor: "#333333",
+ },
+ userName: {
+ color: "white",
+ flex: 1,
+ fontSize: 16,
+ },
+ handStatus: {
+ fontSize: 18,
+ marginLeft: 8,
+ },
+ crossButton: {
+ marginLeft: 8,
+ padding: 4,
+ },
+ crossIcon: {
+ fontSize: 14,
+ },
+});
diff --git a/toolbar/BottomToolbarOverride.tsx b/toolbar/BottomToolbarOverride.tsx
index a552475..e27b014 100644
--- a/toolbar/BottomToolbarOverride.tsx
+++ b/toolbar/BottomToolbarOverride.tsx
@@ -1,5 +1,14 @@
-import React from 'react';
-import {ToolbarPreset} from 'customization-api';
+import React from "react";
+import {
+ ToolbarPreset,
+ ToolbarItem,
+ IconButton,
+ customEvents,
+ PersistanceLevel,
+ useLocalUid,
+ useContent,
+ useIsAttendee,
+} from "customization-api";
export const BottomToolbarOverride = () => {
/**
@@ -9,14 +18,78 @@ export const BottomToolbarOverride = () => {
);
};
+
+const RaiseHandOption = () => {
+ const [isHandRaised, setIsHandRaised] = React.useState(false);
+ const localUid = useLocalUid();
+ const { defaultContent } = useContent();
+ const isAttendee = useIsAttendee()(localUid);
+
+ React.useEffect(() => {
+ customEvents.on("LowerHandEvent", handleLowerHandCallback);
+ }, []);
+
+ // When Host / Self (Attendee) lowers raised hand
+ const handleLowerHandCallback = (data) => {
+ const payload = JSON.parse(data?.payload);
+ if (payload.data.user_uid === localUid) {
+ setIsHandRaised(false);
+ }
+ };
+
+ // When Self (Attendee) toggle raises Hand Option
+ const handleRaiseHand = () => {
+ setIsHandRaised((prev) => {
+ const newHandRaisedState = !prev;
+
+ const data = {
+ hand_raised: newHandRaisedState,
+ user_uid: localUid,
+ };
+
+ customEvents.send(
+ "RaiseHandEvent",
+ JSON.stringify({ data }),
+ PersistanceLevel.Session
+ );
+
+ return newHandRaisedState;
+ });
+ };
+
+ // Raise Hand Option is only for attendee
+ return isAttendee ? (
+
+ {
+ handleRaiseHand();
+ }}
+ />
+
+ ) : null;
+};
diff --git a/toolbar/TopToolbarOverride.tsx b/toolbar/TopToolbarOverride.tsx
index b6d32db..2ed2f45 100644
--- a/toolbar/TopToolbarOverride.tsx
+++ b/toolbar/TopToolbarOverride.tsx
@@ -1,11 +1,79 @@
-import React from 'react';
-import {ToolbarPreset, isMobileUA} from 'customization-api';
+import React, { useContext } from "react";
+import {
+ ToolbarPreset,
+ isMobileUA,
+ useLocalUid,
+ useIsHost,
+ useSidePanel,
+ ToolbarItem,
+ IconButton,
+} from "customization-api";
+import { CustomWrapperContext } from "../wrapper/VideoCallWrapper";
+import { View } from "react-native";
export const TopToolbarOverride = () => {
+ const { raisedHandUsers } = useContext(CustomWrapperContext);
+ const localUid = useLocalUid();
+ const isHost = useIsHost()(localUid);
+ const { sidePanel, setSidePanel } = useSidePanel();
+ const hasRaisedHands = raisedHandUsers?.some(
+ (user) => user.hand_raised === true
+ );
+
+ const NewToolBarItem = () => {
+ const isOpen = sidePanel === "raise-hand-panel";
+
+ const handlePress = () => {
+ setSidePanel(isOpen ? null : "raise-hand-panel");
+ };
+
+ return (
+
+
+ {hasRaisedHands && (
+
+ )}
+
+ );
+ };
+
//mobile web we are hiding the top toolbar so we will have extra space to view host video
if (isMobileUA()) {
return null;
}
- return ;
+ return (
+
+ );
};
diff --git a/wrapper/VideoCallWrapper.tsx b/wrapper/VideoCallWrapper.tsx
index eaf15f8..c6da834 100644
--- a/wrapper/VideoCallWrapper.tsx
+++ b/wrapper/VideoCallWrapper.tsx
@@ -5,23 +5,86 @@ import {
PersistanceLevel,
useRtm,
useHideShareTitle,
-} from 'customization-api';
-import React, {useEffect} from 'react';
-import {ChatOverlayUI} from '../chat/ChatOverlayUI';
+ UidType,
+} from "customization-api";
+import React, { useEffect, SetStateAction, useState } from "react";
+import { ChatOverlayUI } from "../chat/ChatOverlayUI";
-export const CUSTOM_EVENT_NAME_FOR_HOST_JOINED = 'custom-event-host-joined';
-export const CUSTOM_LAYOUT_NAME = 'attendee-layout';
+export const CUSTOM_EVENT_NAME_FOR_HOST_JOINED = "custom-event-host-joined";
+export const CUSTOM_LAYOUT_NAME = "attendee-layout";
-export const VideoCallWrapper = props => {
+interface RaisedHandUser {
+ hand_raised: boolean;
+ user_uid: UidType;
+}
+
+export interface CustomWrapperContextInterface {
+ raisedHandUsers: RaisedHandUser[];
+ setRaisedHandUsers: React.Dispatch>;
+}
+
+interface CustomWrapperProviderProps {
+ children: React.ReactNode;
+}
+
+const CustomWrapperContext = React.createContext(
+ {
+ raisedHandUsers: [],
+ setRaisedHandUsers: () => {},
+ }
+);
+
+const CustomWrapperProvider = (props: CustomWrapperProviderProps) => {
const {
- data: {isHost, uid},
+ data: { isHost, uid },
isJoinDataFetched,
- roomPreference: {disableShareTile},
+ roomPreference: { disableShareTile },
} = useRoomInfo();
- const {hasUserJoinedRTM} = useRtm();
- const {setLayout, currentLayout} = useLayout();
+ const { hasUserJoinedRTM } = useRtm();
+ const { setLayout, currentLayout } = useLayout();
const hideShareTile = useHideShareTitle();
+ // List of users who have raised their hands
+ const [raisedHandUsers, setRaisedHandUsers] = useState([]);
+
+ // Event listener for handling raised hand events
+ React.useEffect(() => {
+ customEvents.on("RaiseHandEvent", handleRaiseHandCallback);
+ }, []);
+
+ // Event listener for handling lowered hand events
+ React.useEffect(() => {
+ customEvents.on("LowerHandEvent", handleLowerHandCallback);
+ }, []);
+
+ // Remove user from raised hands list when they lower their hand
+ const handleLowerHandCallback = (data) => {
+ const payload = JSON.parse(data?.payload);
+ setRaisedHandUsers((prev) =>
+ prev.filter((user) => user.user_uid !== payload.data.user_uid)
+ );
+ };
+
+ // Add or update user in raised hands list when they raise their hand
+ const handleRaiseHandCallback = (data) => {
+ const payload = JSON.parse(data?.payload);
+ setRaisedHandUsers((prev) => {
+ const existingUserIndex = prev.findIndex(
+ (user) => user.user_uid === payload.data.user_uid
+ );
+
+ if (existingUserIndex === -1) {
+ // User doesn't exist
+ return [...prev, payload.data];
+ } else {
+ // User exists
+ const newArray = [...prev];
+ newArray[existingUserIndex] = payload.data;
+ return newArray;
+ }
+ });
+ };
+
useEffect(() => {
if (!isJoinDataFetched) {
return;
@@ -34,7 +97,7 @@ export const VideoCallWrapper = props => {
customEvents.send(
CUSTOM_EVENT_NAME_FOR_HOST_JOINED,
uid?.toString(),
- PersistanceLevel.Channel,
+ PersistanceLevel.Channel
);
}
} else {
@@ -53,9 +116,13 @@ export const VideoCallWrapper = props => {
hideShareTile,
]);
return (
- <>
+
{props.children}
- >
+
);
};
+
+export { CustomWrapperProvider, CustomWrapperContext };