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
440 lines (434 loc) • 18.3 kB
JavaScript
import React, { useEffect, useMemo, useState } from 'react';
import { Modal, ScrollView, StyleSheet, Text, TouchableOpacity, View, } from 'react-native';
import { handleStartWhiteboard as sharedHandleStartWhiteboard, handleStopWhiteboard as sharedHandleStopWhiteboard } from 'mediasfu-shared';
import { getModalPosition } from '../../methods/utils/getModalPosition';
import { getModalBodyTheme } from '../../components_modern/core/modalBodyTheme';
const ConfigureWhiteboardModal = (props) => {
const { isConfigureWhiteboardModalVisible, isVisible, onConfigureWhiteboardClose, backgroundColor, isDarkMode, position = 'center', renderMode = 'modal', style, renderContainer, parameters = {}, } = props;
const visible = Boolean(isConfigureWhiteboardModalVisible ?? isVisible);
const { participants = [], itemPageLimit = 0, socket, islevel, roomName, shareScreenStarted, shared, updateShareScreenStarted, breakOutRoomStarted, breakOutRoomEnded, recordStarted, recordResumed, recordPaused, recordStopped, recordingMediaOptions, whiteboardStarted, whiteboardEnded, hostLabel, showAlert, updateWhiteboardStarted, updateWhiteboardEnded, updateWhiteboardUsers, updateCanStartWhiteboard, updateIsConfigureWhiteboardModalVisible, onScreenChanges, captureCanvasStream, prepopulateUserMedia, rePort, } = parameters;
const [participantsCopy, setParticipantsCopy] = useState([]);
const [isEditing, setIsEditing] = useState(false);
const theme = getModalBodyTheme(isDarkMode);
const shouldUseModernTheme = typeof isDarkMode === 'boolean';
const isEmbedded = renderMode === 'sidebar' || renderMode === 'inline';
const whiteboardLimit = itemPageLimit || 0;
const notify = (message, type = 'info') => {
if (typeof showAlert === 'function') {
showAlert({ message, type });
}
};
const closeConfigureModal = () => {
updateIsConfigureWhiteboardModalVisible?.(false);
onConfigureWhiteboardClose?.();
};
const validateWhiteboard = (workingList = participantsCopy) => {
const selectedParticipants = workingList.filter((participant) => participant?.useBoard);
return selectedParticipants.length <= whiteboardLimit;
};
const checkCanStartWhiteboard = (workingList = participantsCopy) => {
const isValid = validateWhiteboard(workingList);
if (typeof updateCanStartWhiteboard === 'function') {
updateCanStartWhiteboard(isValid);
}
return isValid;
};
useEffect(() => {
if (!visible)
return;
const filteredParticipants = (participants || []).filter((participant) => participant?.islevel !== '2');
setParticipantsCopy(filteredParticipants);
checkCanStartWhiteboard(filteredParticipants);
}, [visible, participants]);
useEffect(() => {
if (!socket || typeof socket.on !== 'function')
return;
const handleWhiteboardUpdated = async (data) => {
try {
if (islevel === '2' && data?.members) {
const filteredParticipants = data.members.filter((participant) => !participant?.isBanned);
setParticipantsCopy(filteredParticipants);
checkCanStartWhiteboard(filteredParticipants);
}
if (typeof updateWhiteboardUsers === 'function') {
updateWhiteboardUsers(data?.whiteboardUsers || []);
}
if (data?.status === 'started') {
updateWhiteboardStarted?.(true);
updateWhiteboardEnded?.(false);
updateCanStartWhiteboard?.(false);
closeConfigureModal();
if (islevel !== '2') {
updateShareScreenStarted?.(true);
await onScreenChanges?.({ changed: true, parameters });
}
else if (parameters?.mainHeightWidth !== 84) {
parameters?.updateMainHeightWidth?.(84);
await onScreenChanges?.({ changed: true, parameters });
await prepopulateUserMedia?.({ name: hostLabel, parameters });
}
}
else if (data?.status === 'ended') {
updateWhiteboardStarted?.(false);
updateWhiteboardEnded?.(true);
updateShareScreenStarted?.(false);
await onScreenChanges?.({ changed: true, parameters });
await prepopulateUserMedia?.({ name: hostLabel, parameters });
await rePort?.({ restart: true, parameters });
}
}
catch (error) {
console.error('Error in whiteboardUpdated listener:', error);
}
};
socket.on('whiteboardUpdated', handleWhiteboardUpdated);
return () => {
if (typeof socket.off === 'function') {
socket.off('whiteboardUpdated', handleWhiteboardUpdated);
}
};
}, [socket, islevel, hostLabel, onScreenChanges, prepopulateUserMedia, rePort, parameters]);
const assignedParticipants = useMemo(() => participantsCopy.filter((participant) => participant?.useBoard), [participantsCopy]);
const pendingParticipants = useMemo(() => participantsCopy.filter((participant) => !participant?.useBoard), [participantsCopy]);
const selectedCount = assignedParticipants.length;
const participantLimitLabel = whiteboardLimit > 0
? `${selectedCount}/${whiteboardLimit}`
: `${selectedCount}`;
const modalBackgroundColor = backgroundColor || (shouldUseModernTheme
? isDarkMode
? '#020617'
: '#f8fafc'
: '#1e293b');
const renderEmptyState = (label) => (<View style={[styles.emptyState, { borderColor: theme.borderColor, backgroundColor: theme.rowBackgroundColor }]}>
<Text style={[styles.emptyText, { color: theme.mutedTextColor }]}>{label}</Text>
</View>);
const renderParticipantRow = (participant, add) => (<TouchableOpacity key={`${add ? 'pending' : 'assigned'}-${participant.name}`} onPress={() => toggleParticipant(participant, add)} style={[styles.item, { backgroundColor: theme.inputBackgroundColor, borderColor: theme.borderColor }]}>
<Text style={[styles.itemText, { color: theme.textColor }]} numberOfLines={1}>
{participant.name}
</Text>
<View style={[
styles.actionPill,
{
backgroundColor: add ? theme.badgeBackgroundColor : 'rgba(248, 113, 113, 0.14)',
borderColor: add ? theme.accentColor : theme.dangerColor,
},
]}>
<Text style={[styles.actionText, { color: add ? theme.accentColor : theme.dangerColor }]}>
{add ? 'Add' : 'Remove'}
</Text>
</View>
</TouchableOpacity>);
const toggleParticipant = (participant, add) => {
setIsEditing(true);
const selectedParticipants = participantsCopy.filter((p) => p?.useBoard);
if (add && selectedParticipants.length >= Math.max(whiteboardLimit - 1, 0)) {
notify(`Participant limit exceeded - you can only add ${Math.max(whiteboardLimit - 1, 0)} other participants`, 'danger');
return;
}
const updatedParticipants = participantsCopy.map((p) => p?.name === participant?.name ? { ...p, useBoard: add } : p);
setParticipantsCopy(updatedParticipants);
checkCanStartWhiteboard(updatedParticipants);
};
const handleSaveWhiteboard = () => {
if (validateWhiteboard()) {
setIsEditing(false);
updateCanStartWhiteboard?.(true);
notify('Whiteboard saved successfully', 'success');
return;
}
notify('Whiteboard validation failed', 'danger');
};
const handleStartWhiteboard = async () => {
if ((shareScreenStarted || shared) && !whiteboardStarted) {
notify('You cannot start whiteboard while screen sharing is active', 'danger');
return;
}
if (breakOutRoomStarted && !breakOutRoomEnded) {
notify('You cannot start whiteboard while breakout rooms are active', 'danger');
return;
}
if (!checkCanStartWhiteboard() || !socket || typeof socket.emit !== 'function') {
notify('Whiteboard validation failed', 'danger');
return;
}
const filteredWhiteboardUsers = participantsCopy
.filter((participant) => participant?.useBoard)
.map(({ name, useBoard }) => ({ name, useBoard }));
const started = await sharedHandleStartWhiteboard({
socket,
whiteboardUsers: filteredWhiteboardUsers,
roomName,
whiteboardStarted,
whiteboardEnded,
showAlert,
updateWhiteboardStarted: updateWhiteboardStarted ?? (() => { }),
updateWhiteboardEnded: updateWhiteboardEnded ?? (() => { }),
updateIsConfigureWhiteboardModalVisible: updateIsConfigureWhiteboardModalVisible ?? (() => { }),
});
if (!started) {
return;
}
updateWhiteboardStarted?.(true);
updateWhiteboardEnded?.(false);
updateWhiteboardUsers?.(filteredWhiteboardUsers);
updateCanStartWhiteboard?.(false);
closeConfigureModal();
if (islevel !== '2') {
updateShareScreenStarted?.(true);
await onScreenChanges?.({ changed: true, parameters });
}
if (islevel === '2' &&
(recordStarted || recordResumed) &&
!(recordPaused || recordStopped) &&
recordingMediaOptions === 'video') {
await captureCanvasStream?.({ parameters });
}
};
const handleStopWhiteboard = async () => {
if (!socket || typeof socket.emit !== 'function')
return;
const stopped = await sharedHandleStopWhiteboard({
socket,
roomName,
showAlert,
updateWhiteboardStarted: updateWhiteboardStarted ?? (() => { }),
updateWhiteboardEnded: updateWhiteboardEnded ?? (() => { }),
updateIsConfigureWhiteboardModalVisible: updateIsConfigureWhiteboardModalVisible ?? (() => { }),
});
if (!stopped) {
return;
}
updateShareScreenStarted?.(false);
await onScreenChanges?.({ changed: true, parameters });
await prepopulateUserMedia?.({ name: hostLabel, parameters });
await rePort?.({ restart: true, parameters });
closeConfigureModal();
};
const dimensions = { width: 520, height: 0 };
const content = (<View style={[isEmbedded ? styles.embeddedContainer : styles.container, { backgroundColor: isEmbedded ? 'transparent' : modalBackgroundColor }, shouldUseModernTheme ? { borderColor: theme.borderColor, borderWidth: isEmbedded ? 0 : 1 } : null, style]}>
<View style={styles.header}>
<View style={styles.headerTextGroup}>
<Text style={[styles.title, { color: theme.textColor }]}>Configure Whiteboard</Text>
<Text style={[styles.subtitle, { color: theme.mutedTextColor }]}>Participants with board access</Text>
</View>
<View style={[styles.countBadge, { backgroundColor: theme.badgeBackgroundColor, borderColor: theme.borderColor }]}>
<Text style={[styles.countBadgeText, { color: theme.badgeTextColor }]}>{participantLimitLabel}</Text>
</View>
</View>
<ScrollView style={styles.listWrapper} contentContainerStyle={styles.listContent}>
<View style={[styles.sectionCard, { borderColor: theme.borderColor, backgroundColor: theme.rowBackgroundColor }]}>
<View style={styles.sectionHeader}>
<Text style={[styles.sectionTitle, { color: theme.textColor }]}>Assigned</Text>
<Text style={[styles.sectionCount, { color: theme.mutedTextColor }]}>{assignedParticipants.length}</Text>
</View>
{assignedParticipants.length > 0
? assignedParticipants.map((participant) => renderParticipantRow(participant, false))
: renderEmptyState('No one is assigned yet')}
</View>
<View style={[styles.sectionCard, { borderColor: theme.borderColor, backgroundColor: theme.rowBackgroundColor }]}>
<View style={styles.sectionHeader}>
<Text style={[styles.sectionTitle, { color: theme.textColor }]}>Pending</Text>
<Text style={[styles.sectionCount, { color: theme.mutedTextColor }]}>{pendingParticipants.length}</Text>
</View>
{pendingParticipants.length > 0
? pendingParticipants.map((participant) => renderParticipantRow(participant, true))
: renderEmptyState('No pending participants')}
</View>
</ScrollView>
<View style={styles.actionsRow}>
<TouchableOpacity onPress={handleSaveWhiteboard} style={[styles.buttonSecondary, { backgroundColor: theme.inputBackgroundColor, borderColor: theme.borderColor }]}>
<Text style={[styles.buttonText, { color: theme.textColor }]}>Save</Text>
</TouchableOpacity>
<TouchableOpacity onPress={handleStartWhiteboard} style={styles.buttonPrimary}>
<Text style={styles.buttonText}>{whiteboardStarted && !whiteboardEnded ? 'Update' : 'Start'}</Text>
</TouchableOpacity>
<TouchableOpacity onPress={handleStopWhiteboard} style={styles.buttonDanger}>
<Text style={styles.buttonText}>Stop</Text>
</TouchableOpacity>
</View>
{!isEmbedded && (<TouchableOpacity style={[styles.buttonSecondary, styles.closeButton, { backgroundColor: theme.inputBackgroundColor, borderColor: theme.borderColor }]} onPress={closeConfigureModal}>
<Text style={[styles.buttonText, { color: theme.textColor }]}>{isEditing ? 'Close (Unsaved edits)' : 'Close'}</Text>
</TouchableOpacity>)}
</View>);
if (isEmbedded) {
return visible ? content : null;
}
const defaultContainer = (<Modal visible={visible} transparent animationType="slide" onRequestClose={onConfigureWhiteboardClose}>
<View style={[styles.overlay, getModalPosition({ position })]}>
{content}
</View>
</Modal>);
return renderContainer
? renderContainer({ defaultContainer, dimensions })
: defaultContainer;
};
const styles = StyleSheet.create({
container: {
backgroundColor: '#1e293b',
width: '100%',
maxWidth: 520,
alignSelf: 'center',
margin: 16,
borderRadius: 16,
padding: 18,
maxHeight: '88%'
},
embeddedContainer: {
flex: 1,
width: '100%',
paddingHorizontal: 2,
paddingBottom: 12,
},
overlay: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.45)',
justifyContent: 'center',
alignItems: 'center',
padding: 12
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
gap: 12,
marginBottom: 14
},
headerTextGroup: {
flex: 1,
minWidth: 0
},
title: {
color: '#fff',
fontSize: 20,
fontWeight: '700',
marginBottom: 4
},
subtitle: {
fontSize: 13,
fontWeight: '600'
},
countBadge: {
minWidth: 58,
borderRadius: 999,
borderWidth: 1,
paddingHorizontal: 10,
paddingVertical: 6,
alignItems: 'center'
},
countBadgeText: {
fontSize: 12,
fontWeight: '800'
},
listWrapper: {
maxHeight: 420,
marginBottom: 14
},
listContent: {
gap: 12,
paddingBottom: 2
},
sectionCard: {
borderRadius: 12,
borderWidth: 1,
padding: 12
},
sectionHeader: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: 10
},
sectionTitle: {
color: '#cbd5e1',
fontSize: 15,
fontWeight: '800'
},
sectionCount: {
fontSize: 12,
fontWeight: '800'
},
emptyState: {
borderWidth: 1,
borderStyle: 'dashed',
borderRadius: 10,
paddingVertical: 14,
paddingHorizontal: 12,
alignItems: 'center'
},
emptyText: {
fontSize: 13,
fontWeight: '600'
},
item: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
backgroundColor: '#334155',
borderWidth: 1,
borderRadius: 10,
paddingHorizontal: 12,
paddingVertical: 10,
marginBottom: 8,
gap: 10
},
actionPill: {
borderWidth: 1,
borderRadius: 999,
paddingHorizontal: 10,
paddingVertical: 4
},
itemText: {
color: '#fff',
flex: 1,
fontSize: 14,
fontWeight: '700'
},
actionText: {
color: '#93c5fd',
fontSize: 12,
fontWeight: '800'
},
actionsRow: {
flexDirection: 'row',
justifyContent: 'space-between',
gap: 8,
marginBottom: 10
},
buttonPrimary: {
flex: 1,
backgroundColor: '#2563eb',
borderRadius: 8,
paddingVertical: 10,
alignItems: 'center'
},
buttonSecondary: {
flex: 1,
backgroundColor: '#475569',
borderWidth: 1,
borderRadius: 8,
paddingVertical: 10,
alignItems: 'center'
},
buttonDanger: {
flex: 1,
backgroundColor: '#dc2626',
borderRadius: 8,
paddingVertical: 10,
alignItems: 'center'
},
closeButton: {
flex: 0
},
buttonText: {
color: '#fff',
fontWeight: '700',
fontSize: 13
},
text: {
color: '#fff',
fontSize: 16,
textAlign: 'center'
}
});
export default ConfigureWhiteboardModal;
//# sourceMappingURL=ConfigureWhiteboardModal.js.map