mediasfu-reactnative-expo
Version:
mediasfu-reactnative-expo – Expo-managed React Native WebRTC SDK for video conferencing, webinars, live streaming, broadcast, screen sharing, whiteboard, chat, recording, live subtitles, translation, and AI agent rooms on iOS, Android, and web. Prebuilt r
641 lines • 27.1 kB
JavaScript
// BreakoutRoomsModal.tsx
import React, { useState, useEffect, useRef } from 'react';
import { Modal, View, Text, StyleSheet, TextInput, Pressable, FlatList, ScrollView, Dimensions, } from 'react-native';
import RNPickerSelect from 'react-native-picker-select';
import { getModalPosition } from '../../methods/utils/getModalPosition';
import { FontAwesome5 } from "@expo/vector-icons";
import { createThemedPickerSelectStyles, getModalBodyTheme } from '../../components_modern/core/modalBodyTheme';
// EditRoomModal component with types
const EditRoomModal = ({ editRoomModalVisible, setEditRoomModalVisible, currentRoom, participantsRef, handleAddParticipant, handleRemoveParticipant, currentRoomIndex, isDarkMode, }) => {
const theme = getModalBodyTheme(isDarkMode);
const shouldUseModernTheme = typeof isDarkMode === 'boolean';
const renderAssignedParticipant = ({ item, index, }) => (<View style={[styles.listItem, { borderBottomColor: theme.dividerColor }]} key={index}>
<Text style={{ color: theme.textColor }}>{item.name}</Text>
<Pressable onPress={() => handleRemoveParticipant(currentRoomIndex, item)} style={styles.iconButton}>
<FontAwesome5 name="times" size={20} color={theme.dangerColor}/>
</Pressable>
</View>);
const renderUnassignedParticipant = ({ item, index, }) => (<View style={[styles.listItem, { borderBottomColor: theme.dividerColor }]} key={index}>
<Text style={{ color: theme.textColor }}>{item.name}</Text>
<Pressable onPress={() => handleAddParticipant(currentRoomIndex, item)} style={styles.iconButton}>
<FontAwesome5 name="plus" size={20} color={theme.successColor}/>
</Pressable>
</View>);
return (<Modal transparent={true} animationType="slide" visible={editRoomModalVisible} onRequestClose={() => setEditRoomModalVisible(false)}>
<View style={styles.modalContainer}>
<View style={[styles.modalContent, shouldUseModernTheme ? { backgroundColor: '#0f172a', borderColor: theme.borderColor } : null]}>
<View style={styles.modalHeader}>
<Text style={[styles.modalTitle, { color: theme.textColor }]}>
Edit Room {currentRoomIndex + 1} <FontAwesome5 name="pen" color={theme.iconColor}/>
</Text>
<Pressable onPress={() => setEditRoomModalVisible(false)}>
<FontAwesome5 name="times" size={20} color={theme.iconColor}/>
</Pressable>
</View>
<FlatList data={currentRoom} renderItem={renderAssignedParticipant} keyExtractor={(item, index) => `${item.name}-${index}`} ListHeaderComponent={<Text style={[styles.listTitle, { color: theme.textColor }]}>
Assigned Participants <FontAwesome5 name="users" color={theme.iconColor}/>
</Text>} ListEmptyComponent={<View style={[styles.listItem, { borderBottomColor: theme.dividerColor }]}>
<Text style={{ color: theme.mutedTextColor }}>None assigned</Text>
</View>}/>
<FlatList data={participantsRef.current.filter((participant) => participant.breakRoom == null)} renderItem={renderUnassignedParticipant} keyExtractor={(item, index) => `${item.name}-${index}`} ListHeaderComponent={<Text style={[styles.listTitle, { color: theme.textColor }]}>
Unassigned Participants <FontAwesome5 name="users" color={theme.iconColor}/>
</Text>} ListEmptyComponent={<View style={[styles.listItem, { borderBottomColor: theme.dividerColor }]}>
<Text style={{ color: theme.mutedTextColor }}>None pending</Text>
</View>}/>
</View>
</View>
</Modal>);
};
/**
* BreakoutRoomsModal - Breakout room creation and management interface
*
* BreakoutRoomsModal is a React Native component that provides hosts with controls
* to create multiple breakout rooms, assign participants to rooms, and manage the
* breakout session lifecycle (start/stop). Participants can be manually assigned or
* randomly distributed across rooms.
*
* **Key Features:**
* - Create multiple breakout rooms (customizable count)
* - Manual participant assignment to rooms
* - Random participant distribution option
* - Edit room assignments (add/remove participants)
* - Duplicate participant prevention
* - Start/stop breakout session controls
* - Real-time Socket.io synchronization
* - Display mode switching for breakout view
* - Validation for starting (all participants assigned)
*
* **UI Customization:**
* This component can be replaced via `uiOverrides.breakoutRoomsModal` to
* provide a completely custom breakout room management interface.
*
* @component
* @param {BreakoutRoomsModalOptions} props - Configuration options
*
* @returns {JSX.Element} Rendered breakout rooms modal
*
* @example
* ```tsx
* // Basic usage with manual assignment
* import React, { useState } from 'react';
* import { BreakoutRoomsModal } from 'mediasfu-reactnative-expo';
* import { io } from 'socket.io-client';
*
* const socket = io('https://your-server.com');
* const [showBreakout, setShowBreakout] = useState(false);
*
* const breakoutParams = {
* participants: [
* { name: 'John', id: '1', islevel: '0' },
* { name: 'Jane', id: '2', islevel: '0' },
* { name: 'Bob', id: '3', islevel: '0' },
* ],
* breakoutRooms: [[], []], // 2 empty rooms
* breakOutRoomStarted: false,
* breakOutRoomEnded: false,
* currentRoomIndex: null,
* canStartBreakout: false,
* roomName: 'main-meeting',
* socket: socket,
* updateBreakoutRooms: (rooms) => console.log('Updated:', rooms),
* updateBreakOutRoomStarted: (started) => console.log('Started:', started),
* // ... other required parameters
* };
*
* return (
* <BreakoutRoomsModal
* isVisible={showBreakout}
* onBreakoutRoomsClose={() => setShowBreakout(false)}
* parameters={breakoutParams}
* />
* );
* ```
*
* @example
* ```tsx
* // With random assignment and custom positioning
* return (
* <BreakoutRoomsModal
* isVisible={isVisible}
* onBreakoutRoomsClose={handleClose}
* parameters={breakoutParameters}
* position="center"
* backgroundColor="#2c3e50"
* />
* );
* ```
*
* @example
* ```tsx
* // Using custom UI via uiOverrides
* const config = {
* uiOverrides: {
* breakoutRoomsModal: {
* component: MyCustomBreakoutManager,
* injectedProps: {
* theme: 'dark',
* allowAutoAssign: true,
* },
* },
* },
* };
*
* return <MyMeetingComponent config={config} />;
* ```
*/
const BreakoutRoomsModal = ({ isVisible, onBreakoutRoomsClose, parameters, position = 'topRight', backgroundColor = '#83c0e9', isDarkMode, style, renderContent, renderContainer, }) => {
const { participants, showAlert, socket, localSocket, itemPageLimit, meetingDisplayType, prevMeetingDisplayType, roomName, shareScreenStarted, shared, breakOutRoomStarted, breakOutRoomEnded, currentRoomIndex, canStartBreakout, breakoutRooms, updateBreakOutRoomStarted, updateBreakOutRoomEnded, updateCurrentRoomIndex, updateCanStartBreakout, updateBreakoutRooms, updateMeetingDisplayType, } = parameters;
const participantsRef = useRef(participants);
const breakoutRoomsRef = useRef(breakoutRooms && breakoutRooms.length > 0 ? [...breakoutRooms] : []);
const [numRooms, setNumRooms] = useState('');
const [newParticipantAction, setNewParticipantAction] = useState('autoAssignNewRoom');
const [currentRoom, setCurrentRoom] = useState([]);
const [editRoomModalVisible, setEditRoomModalVisible] = useState(false);
const [startBreakoutButtonVisible, setStartBreakoutButtonVisible] = useState(false);
const [stopBreakoutButtonVisible, setStopBreakoutButtonVisible] = useState(false);
const screenWidth = Dimensions.get('window').width;
let modalWidth = 0.9 * screenWidth;
if (modalWidth > 600) {
modalWidth = 600;
}
const checkCanStartBreakout = () => {
if (canStartBreakout) {
setStartBreakoutButtonVisible(true);
setStopBreakoutButtonVisible(breakOutRoomStarted && !breakOutRoomEnded);
}
else {
setStartBreakoutButtonVisible(false);
setStopBreakoutButtonVisible(false);
}
};
useEffect(() => {
if (isVisible) {
const filteredParticipants = participants.filter((participant) => participant.islevel !== '2');
participantsRef.current = filteredParticipants;
breakoutRoomsRef.current =
breakoutRooms && breakoutRooms.length > 0 ? [...breakoutRooms] : [];
checkCanStartBreakout();
}
}, [isVisible]);
const handleRandomAssign = () => {
const numRoomsInt = parseInt(numRooms, 10);
if (!numRoomsInt || numRoomsInt <= 0) {
showAlert?.({
message: 'Please enter a valid number of rooms',
type: 'danger',
});
return;
}
const newBreakoutRooms = Array.from({ length: numRoomsInt }, () => []);
const shuffledParticipants = [...participantsRef.current].sort(() => 0.5 - Math.random());
shuffledParticipants.forEach((participant, index) => {
const roomIndex = index % numRoomsInt;
if (newBreakoutRooms[roomIndex].length < itemPageLimit) {
const participant_ = {
name: participant.name,
breakRoom: roomIndex,
};
newBreakoutRooms[roomIndex].push(participant_);
participant.breakRoom = roomIndex;
}
else {
for (let i = 0; i < numRoomsInt; i++) {
if (newBreakoutRooms[i].length < itemPageLimit) {
newBreakoutRooms[i].push(participant);
participant.breakRoom = i;
break;
}
}
}
});
breakoutRoomsRef.current = newBreakoutRooms;
checkCanStartBreakout();
};
const handleManualAssign = () => {
const numRoomsInt = parseInt(numRooms, 10);
if (!numRoomsInt || numRoomsInt <= 0) {
showAlert?.({
message: 'Please enter a valid number of rooms',
type: 'danger',
});
return;
}
breakoutRoomsRef.current = Array.from({ length: numRoomsInt }, () => []);
updateCanStartBreakout(false);
checkCanStartBreakout();
};
const handleAddRoom = () => {
breakoutRoomsRef.current = [...breakoutRoomsRef.current, []];
updateCanStartBreakout(false);
checkCanStartBreakout();
};
const handleSaveRooms = () => {
if (validateRooms()) {
updateBreakoutRooms(breakoutRoomsRef.current);
updateCanStartBreakout(true);
checkCanStartBreakout();
showAlert?.({ message: 'Rooms saved successfully', type: 'success' });
}
else {
showAlert?.({ message: 'Rooms validation failed', type: 'danger' });
}
};
const validateRooms = () => {
if (breakoutRoomsRef.current.length === 0) {
showAlert?.({
message: 'There must be at least one room',
type: 'danger',
});
return false;
}
for (let room of breakoutRoomsRef.current) {
if (room.length === 0) {
showAlert?.({ message: 'Rooms must not be empty', type: 'danger' });
return false;
}
const participantNames = room.map((p) => p.name);
const uniqueNames = new Set(participantNames);
if (participantNames.length !== uniqueNames.size) {
showAlert?.({
message: 'Duplicate participant names in a room',
type: 'danger',
});
return false;
}
if (room.length > itemPageLimit) {
showAlert?.({
message: 'A room exceeds the participant limit',
type: 'danger',
});
return false;
}
}
return true;
};
const handleStartBreakout = () => {
if (shareScreenStarted || shared) {
showAlert?.({
message: 'You cannot start breakout rooms while screen sharing is active',
type: 'danger',
});
return;
}
if (canStartBreakout) {
const emitName = breakOutRoomStarted && !breakOutRoomEnded
? 'updateBreakout'
: 'startBreakout';
const filteredBreakoutRooms = breakoutRoomsRef.current.map((room) => room.map(({ name, breakRoom }) => ({ name, breakRoom })));
socket.emit(emitName, {
breakoutRooms: filteredBreakoutRooms,
newParticipantAction,
roomName,
}, (response) => {
if (response.success) {
showAlert?.({ message: 'Breakout rooms active', type: 'success' });
updateBreakOutRoomStarted(true);
updateBreakOutRoomEnded(false);
onBreakoutRoomsClose();
if (meetingDisplayType !== 'all') {
updateMeetingDisplayType('all');
}
}
else {
showAlert?.({ message: response.reason, type: 'danger' });
}
});
if (localSocket && localSocket.id) {
try {
localSocket.emit(emitName, { breakoutRooms: filteredBreakoutRooms, newParticipantAction, roomName }, (response) => {
if (response.success) {
// do nothing
}
});
}
catch {
console.log('Error starting local breakout rooms:');
}
}
}
};
const handleStopBreakout = () => {
socket.emit('stopBreakout', { roomName }, (response) => {
if (response.success) {
showAlert?.({ message: 'Breakout rooms stopped', type: 'success' });
updateBreakOutRoomStarted(false);
updateBreakOutRoomEnded(true);
onBreakoutRoomsClose();
if (meetingDisplayType !== prevMeetingDisplayType) {
updateMeetingDisplayType(prevMeetingDisplayType);
}
}
else {
showAlert?.({ message: response.reason, type: 'danger' });
}
});
if (localSocket && localSocket.id) {
try {
localSocket.emit('stopBreakout', { roomName }, (response) => {
if (response.success) {
// do nothing
}
});
}
catch {
console.log('Error stopping local breakout rooms:');
}
}
};
const handleEditRoom = (roomIndex) => {
updateCurrentRoomIndex(roomIndex);
setCurrentRoom(breakoutRoomsRef.current[roomIndex]);
setEditRoomModalVisible(true);
updateCanStartBreakout(false);
checkCanStartBreakout();
};
const handleDeleteRoom = (roomIndex) => {
const room = breakoutRoomsRef.current[roomIndex];
room.forEach((participant) => (participant.breakRoom = null));
const newBreakoutRooms = [...breakoutRoomsRef.current];
newBreakoutRooms.splice(roomIndex, 1);
newBreakoutRooms.forEach((room, index) => {
room.forEach((participant) => (participant.breakRoom = index));
});
breakoutRoomsRef.current = newBreakoutRooms;
checkCanStartBreakout();
};
const handleAddParticipant = (roomIndex, participant) => {
if (breakoutRoomsRef.current[roomIndex].length < itemPageLimit) {
const newBreakoutRooms = [...breakoutRoomsRef.current];
const participant_ = {
name: participant.name,
breakRoom: roomIndex,
};
newBreakoutRooms[roomIndex].push(participant_);
breakoutRoomsRef.current = newBreakoutRooms;
participant.breakRoom = roomIndex;
if (currentRoomIndex != null) {
handleEditRoom(currentRoomIndex);
}
}
else {
showAlert?.({ message: 'Room is full', type: 'danger' });
}
};
const handleRemoveParticipant = (roomIndex, participant) => {
const newBreakoutRooms = [...breakoutRoomsRef.current];
newBreakoutRooms[roomIndex] = newBreakoutRooms[roomIndex].filter((p) => p !== participant);
breakoutRoomsRef.current = newBreakoutRooms;
participant.breakRoom = null;
if (currentRoomIndex != null) {
handleEditRoom(currentRoomIndex);
}
};
const dimensions = { width: modalWidth, height: 0 };
const theme = getModalBodyTheme(isDarkMode);
const shouldUseModernTheme = typeof isDarkMode === 'boolean';
const themedPickerSelectStyles = createThemedPickerSelectStyles(theme);
const defaultContent = (<>
<View style={styles.modalHeader}>
<Text style={[styles.modalTitle, { color: theme.textColor }]}>
Breakout Rooms <FontAwesome5 name="door-open" color={theme.iconColor}/>
</Text>
<Pressable onPress={onBreakoutRoomsClose}>
<FontAwesome5 name="times" size={20} color={theme.iconColor}/>
</Pressable>
</View>
<FlatList ListHeaderComponent={<View>
<View style={styles.formGroup}>
<Text style={{ color: theme.textColor }}>
Number of Rooms <FontAwesome5 name="users" color={theme.iconColor}/>
</Text>
<TextInput style={[
styles.input,
{ backgroundColor: theme.inputBackgroundColor, borderColor: theme.borderColor, color: theme.inputTextColor },
]} value={numRooms} onChangeText={setNumRooms} inputMode="numeric" placeholderTextColor={theme.placeholderTextColor}/>
</View>
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
<View style={styles.buttonGroup}>
<Pressable style={styles.button} onPress={handleRandomAssign}>
<FontAwesome5 name="random" size={20} color="#fff"/>
<Text style={styles.buttonText}>Random</Text>
</Pressable>
<Pressable style={styles.button} onPress={handleManualAssign}>
<FontAwesome5 name="hand-pointer" size={20} color="#fff"/>
<Text style={styles.buttonText}>Manual</Text>
</Pressable>
<Pressable style={styles.button} onPress={handleAddRoom}>
<FontAwesome5 name="plus" size={20} color="#fff"/>
<Text style={styles.buttonText}>Add Room</Text>
</Pressable>
<Pressable style={styles.button} onPress={handleSaveRooms}>
<FontAwesome5 name="save" size={20} color="#fff"/>
<Text style={styles.buttonText}>Save Rooms</Text>
</Pressable>
</View>
</ScrollView>
<View style={styles.formGroup}>
<Text style={{ color: theme.textColor }}>
New Participant Action <FontAwesome5 name="users" color={theme.iconColor}/>
</Text>
<RNPickerSelect style={themedPickerSelectStyles} value={newParticipantAction} onValueChange={(value) => setNewParticipantAction(value)} items={[
{ label: 'Add to new room', value: 'autoAssignNewRoom' },
{
label: 'Add to open room',
value: 'autoAssignAvailableRoom',
},
{ label: 'No action', value: 'manualAssign' },
]} placeholder={{}} useNativeAndroidPickerStyle={false}/>
</View>
</View>} data={breakoutRoomsRef.current} keyExtractor={(item, index) => `room-${index}`} renderItem={({ item, index: roomIndex }) => (<View style={[styles.card, { backgroundColor: theme.rowBackgroundColor }, shouldUseModernTheme ? { borderColor: theme.borderColor, borderWidth: 1 } : null]}>
<View style={styles.cardHeader}>
<Text style={{ color: theme.textColor }}>
Room {roomIndex + 1} <FontAwesome5 name="users" color={theme.iconColor}/>
</Text>
<View style={styles.cardHeaderButtons}>
<Pressable onPress={() => handleEditRoom(roomIndex)} style={styles.iconButton}>
<FontAwesome5 name="pen" size={20} color={theme.iconColor}/>
</Pressable>
<Pressable onPress={() => handleDeleteRoom(roomIndex)} style={styles.iconButton}>
<FontAwesome5 name="times" size={20} color={theme.dangerColor}/>
</Pressable>
</View>
</View>
<View style={[styles.cardBody, { borderTopColor: theme.dividerColor }]}>
{item.map((participant, index) => (<View key={index} style={[styles.listItem, { borderBottomColor: theme.dividerColor }]}>
<Text style={{ color: theme.textColor }}>{participant.name}</Text>
<Pressable onPress={() => handleRemoveParticipant(roomIndex, participant)} style={styles.iconButton}>
<FontAwesome5 name="times" size={20} color={theme.dangerColor}/>
</Pressable>
</View>))}
</View>
</View>)} ListFooterComponent={<View style={styles.buttonGroup}>
{startBreakoutButtonVisible && (<Pressable style={styles.button} onPress={handleStartBreakout}>
<Text style={styles.buttonText}>
{breakOutRoomStarted && !breakOutRoomEnded
? 'Update Breakout'
: 'Start Breakout'}
</Text>
<FontAwesome5 name={breakOutRoomStarted && !breakOutRoomEnded
? 'sync'
: 'play'} size={16} color={breakOutRoomStarted && !breakOutRoomEnded
? 'yellow'
: 'green'}/>
</Pressable>)}
{stopBreakoutButtonVisible && (<Pressable style={styles.button} onPress={handleStopBreakout}>
<Text style={styles.buttonText}>Stop Breakout</Text>
<FontAwesome5 name="stop" size={16} color="red"/>
</Pressable>)}
</View>}/>
<EditRoomModal editRoomModalVisible={editRoomModalVisible} setEditRoomModalVisible={setEditRoomModalVisible} currentRoom={currentRoom} participantsRef={participantsRef} handleAddParticipant={handleAddParticipant} handleRemoveParticipant={handleRemoveParticipant} currentRoomIndex={currentRoomIndex} isDarkMode={isDarkMode}/>
</>);
const content = renderContent
? renderContent({ defaultContent, dimensions })
: defaultContent;
const defaultContainer = (<Modal transparent={true} animationType="slide" visible={isVisible} onRequestClose={onBreakoutRoomsClose}>
<View style={[styles.modalContainer, getModalPosition({ position })]}>
<View style={[
styles.modalContent,
{ backgroundColor: backgroundColor, width: modalWidth },
shouldUseModernTheme ? { borderColor: theme.borderColor } : null,
style,
]}>
{content}
</View>
</View>
</Modal>);
return renderContainer
? renderContainer({ defaultContainer, dimensions })
: defaultContainer;
};
const pickerSelectStyles = StyleSheet.create({
inputIOS: {
fontSize: 16,
paddingVertical: 2,
paddingHorizontal: 10,
borderWidth: 1,
borderColor: 'black',
borderRadius: 4,
color: 'black',
paddingRight: 30,
},
inputAndroid: {
fontSize: 16,
paddingHorizontal: 10,
paddingVertical: 2,
borderWidth: 0.5,
borderColor: 'black',
borderRadius: 8,
color: 'black',
paddingRight: 30,
},
inputWeb: {
fontSize: 14,
paddingHorizontal: 5,
paddingVertical: 1,
borderWidth: 0.5,
borderColor: 'purple',
borderRadius: 8,
color: 'black',
paddingRight: 30, // To ensure the text is never behind the icon
backgroundColor: 'white',
marginBottom: 10,
},
});
const styles = StyleSheet.create({
modalContainer: {
flex: 1,
justifyContent: 'flex-end',
alignItems: 'flex-end',
zIndex: 9,
elevation: 9,
},
modalContent: {
height: '70%',
backgroundColor: '#83c0e9',
borderRadius: 0,
borderWidth: 2,
borderColor: '#00000',
padding: 20,
maxHeight: '75%',
maxWidth: '90%',
zIndex: 9,
elevation: 9,
},
modalHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 15,
},
modalTitle: {
fontSize: 18,
fontWeight: 'bold',
color: 'black',
},
formGroup: {
marginBottom: 15,
},
input: {
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 5,
padding: 10,
marginTop: 5,
},
button: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#8D9BAB',
padding: 5,
margin: 5,
borderRadius: 5,
},
buttonText: {
color: '#fff',
marginLeft: 5,
marginRight: 5,
},
buttonGroup: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 15,
},
card: {
backgroundColor: '#fff',
borderRadius: 10,
marginBottom: 10,
padding: 10,
},
cardHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 10,
},
cardHeaderButtons: {
flexDirection: 'row',
},
cardBody: {
borderTopWidth: 1,
borderTopColor: '#ccc',
paddingTop: 10,
},
listItem: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
borderBottomWidth: 1,
borderBottomColor: '#ccc',
paddingVertical: 5,
},
iconButton: {
padding: 5,
},
listTitle: {
fontSize: 16,
fontWeight: 'bold',
marginBottom: 10,
},
});
export default BreakoutRoomsModal;
//# sourceMappingURL=BreakoutRoomsModal.js.map