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 };