mediasfu-reactnative
Version:
MediaSFU Prebuilt React Native SDK
559 lines • 24.2 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 'react-native-vector-icons/FontAwesome5';
// EditRoomModal component with types
const EditRoomModal = ({ editRoomModalVisible, setEditRoomModalVisible, currentRoom, participantsRef, handleAddParticipant, handleRemoveParticipant, currentRoomIndex, }) => {
const renderAssignedParticipant = ({ item, index, }) => (<View style={styles.listItem} key={index}>
<Text>{item.name}</Text>
<Pressable onPress={() => handleRemoveParticipant(currentRoomIndex, item)} style={styles.iconButton}>
<FontAwesome5 name="times" size={20} color="#000"/>
</Pressable>
</View>);
const renderUnassignedParticipant = ({ item, index, }) => (<View style={styles.listItem} key={index}>
<Text>{item.name}</Text>
<Pressable onPress={() => handleAddParticipant(currentRoomIndex, item)} style={styles.iconButton}>
<FontAwesome5 name="plus" size={20} color="#000"/>
</Pressable>
</View>);
return (<Modal transparent={true} animationType="slide" visible={editRoomModalVisible} onRequestClose={() => setEditRoomModalVisible(false)}>
<View style={styles.modalContainer}>
<View style={styles.modalContent}>
<View style={styles.modalHeader}>
<Text style={styles.modalTitle}>
Edit Room {currentRoomIndex + 1} <FontAwesome5 name="pen"/>
</Text>
<Pressable onPress={() => setEditRoomModalVisible(false)}>
<FontAwesome5 name="times" size={20} color="#000"/>
</Pressable>
</View>
<FlatList data={currentRoom} renderItem={renderAssignedParticipant} keyExtractor={(item, index) => `${item.name}-${index}`} ListHeaderComponent={<Text style={styles.listTitle}>
Assigned Participants <FontAwesome5 name="users"/>
</Text>} ListEmptyComponent={<View style={styles.listItem}>
<Text>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}>
Unassigned Participants <FontAwesome5 name="users"/>
</Text>} ListEmptyComponent={<View style={styles.listItem}>
<Text>None pending</Text>
</View>}/>
</View>
</View>
</Modal>);
};
/**
* BreakoutRoomsModal component is a React functional component that manages the breakout rooms modal.
* It allows users to create, edit, and manage breakout rooms for participants in a meeting.
*
* @component
* @param {BreakoutRoomsModalOptions} props - The properties for the BreakoutRoomsModal component.
* @param {boolean} props.isVisible - Determines if the modal is visible.
* @param {Function} props.onBreakoutRoomsClose - Callback function to close the modal.
* @param {Object} props.parameters - Parameters for managing breakout rooms.
* @param {string} [props.position='topRight'] - Position of the modal on the screen.
* @param {string} [props.backgroundColor='#83c0e9'] - Background color of the modal.
*
* @returns {JSX.Element} The rendered BreakoutRoomsModal component.
*
* @example
* ```tsx
* import React from 'react';
* import { BreakoutRoomsModal } from 'mediasfu-reactnative';
*
* function App() {
* const [modalVisible, setModalVisible] = useState(true);
*
* return (
* <BreakoutRoomsModal
* isVisible={modalVisible}
* onBreakoutRoomsClose={() => setModalVisible(false)}
* parameters={breakoutParameters}
* position="topRight"
* backgroundColor="#83c0e9"
* />
* );
* }
*
* export default App;
* ```
*/
const BreakoutRoomsModal = ({ isVisible, onBreakoutRoomsClose, parameters, position = 'topRight', backgroundColor = '#83c0e9', }) => {
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 === null || showAlert === void 0 ? void 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 === null || showAlert === void 0 ? void 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 === null || showAlert === void 0 ? void 0 : showAlert({ message: 'Rooms saved successfully', type: 'success' });
}
else {
showAlert === null || showAlert === void 0 ? void 0 : showAlert({ message: 'Rooms validation failed', type: 'danger' });
}
};
const validateRooms = () => {
if (breakoutRoomsRef.current.length === 0) {
showAlert === null || showAlert === void 0 ? void 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 === null || showAlert === void 0 ? void 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 === null || showAlert === void 0 ? void 0 : showAlert({
message: 'Duplicate participant names in a room',
type: 'danger',
});
return false;
}
if (room.length > itemPageLimit) {
showAlert === null || showAlert === void 0 ? void 0 : showAlert({
message: 'A room exceeds the participant limit',
type: 'danger',
});
return false;
}
}
return true;
};
const handleStartBreakout = () => {
if (shareScreenStarted || shared) {
showAlert === null || showAlert === void 0 ? void 0 : 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 === null || showAlert === void 0 ? void 0 : showAlert({ message: 'Breakout rooms active', type: 'success' });
updateBreakOutRoomStarted(true);
updateBreakOutRoomEnded(false);
onBreakoutRoomsClose();
if (meetingDisplayType !== 'all') {
updateMeetingDisplayType('all');
}
}
else {
showAlert === null || showAlert === void 0 ? void 0 : 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 (_a) {
console.log('Error starting local breakout rooms:');
}
}
}
};
const handleStopBreakout = () => {
socket.emit('stopBreakout', { roomName }, (response) => {
if (response.success) {
showAlert === null || showAlert === void 0 ? void 0 : showAlert({ message: 'Breakout rooms stopped', type: 'success' });
updateBreakOutRoomStarted(false);
updateBreakOutRoomEnded(true);
onBreakoutRoomsClose();
if (meetingDisplayType !== prevMeetingDisplayType) {
updateMeetingDisplayType(prevMeetingDisplayType);
}
}
else {
showAlert === null || showAlert === void 0 ? void 0 : showAlert({ message: response.reason, type: 'danger' });
}
});
if (localSocket && localSocket.id) {
try {
localSocket.emit('stopBreakout', { roomName }, (response) => {
if (response.success) {
// do nothing
}
});
}
catch (_a) {
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 === null || showAlert === void 0 ? void 0 : 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);
}
};
return (<Modal transparent={true} animationType="slide" visible={isVisible} onRequestClose={onBreakoutRoomsClose}>
<View style={[styles.modalContainer, getModalPosition({ position })]}>
<View style={[
styles.modalContent,
{ backgroundColor: backgroundColor, width: modalWidth },
]}>
<View style={styles.modalHeader}>
<Text style={styles.modalTitle}>
Breakout Rooms <FontAwesome5 name="door-open"/>
</Text>
<Pressable onPress={onBreakoutRoomsClose}>
<FontAwesome5 name="times" size={20} color="#000"/>
</Pressable>
</View>
<FlatList ListHeaderComponent={<View>
<View style={styles.formGroup}>
<Text>
Number of Rooms <FontAwesome5 name="users"/>
</Text>
<TextInput style={styles.input} value={numRooms} onChangeText={setNumRooms} inputMode="numeric"/>
</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>
New Participant Action <FontAwesome5 name="users"/>
</Text>
<RNPickerSelect style={pickerSelectStyles} 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}>
<View style={styles.cardHeader}>
<Text>
Room {roomIndex + 1} <FontAwesome5 name="users"/>
</Text>
<View style={styles.cardHeaderButtons}>
<Pressable onPress={() => handleEditRoom(roomIndex)} style={styles.iconButton}>
<FontAwesome5 name="pen" size={20} color="#000"/>
</Pressable>
<Pressable onPress={() => handleDeleteRoom(roomIndex)} style={styles.iconButton}>
<FontAwesome5 name="times" size={20} color="#000"/>
</Pressable>
</View>
</View>
<View style={styles.cardBody}>
{item.map((participant, index) => (<View key={index} style={styles.listItem}>
<Text>{participant.name}</Text>
<Pressable onPress={() => handleRemoveParticipant(roomIndex, participant)} style={styles.iconButton}>
<FontAwesome5 name="times" size={20} color="#000"/>
</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>}/>
</View>
</View>
<EditRoomModal editRoomModalVisible={editRoomModalVisible} setEditRoomModalVisible={setEditRoomModalVisible} currentRoom={currentRoom} participantsRef={participantsRef} handleAddParticipant={handleAddParticipant} handleRemoveParticipant={handleRemoveParticipant} currentRoomIndex={currentRoomIndex}/>
</Modal>);
};
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