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
246 lines (242 loc) • 12.5 kB
JavaScript
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Dimensions, Modal, ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, View, } from 'react-native';
import { FontAwesome5 } from '@expo/vector-icons';
import { addPanelist, removePanelist } from '../../methods/panelistsMethods/updatePanelists';
import { focusPanelists } from '../../methods/panelistsMethods/focusPanelists';
import { getModalPosition } from '../../methods/utils/getModalPosition';
import { getModalBodyTheme } from '../../components_modern/core/modalBodyTheme';
const { width, height } = Dimensions.get('window');
const PanelistsModal = ({ isPanelistsModalVisible, onPanelistsClose, parameters, backgroundColor = '#1e293b', isDarkMode, position = 'center', style, renderMode = 'modal', renderContainer, }) => {
const theme = getModalBodyTheme(isDarkMode);
const { participants: initialParticipants, panelists: initialPanelists, member, islevel, socket, roomName, showAlert, itemPageLimit, panelistsFocused: initialFocused = false, updatePanelists, updatePanelistsFocused, getUpdatedAllParams, } = parameters;
const [searchFilter, setSearchFilter] = useState('');
const [participants, setParticipants] = useState(initialParticipants || []);
const [localPanelists, setLocalPanelists] = useState(initialPanelists || []);
const [isFocused, setIsFocused] = useState(initialFocused);
const [muteOthersMic, setMuteOthersMic] = useState(false);
const [muteOthersCamera, setMuteOthersCamera] = useState(false);
useEffect(() => {
if (!isPanelistsModalVisible)
return;
const fresh = getUpdatedAllParams?.() || parameters;
setParticipants(fresh.participants || []);
setLocalPanelists(fresh.panelists || []);
setIsFocused(!!fresh.panelistsFocused);
}, [getUpdatedAllParams, isPanelistsModalVisible, parameters]);
const isHost = islevel === '2';
const panelistIds = useMemo(() => new Set(localPanelists.map((p) => p.id)), [localPanelists]);
const availableParticipants = useMemo(() => participants
.filter((p) => p.islevel !== '2' && p.name !== member)
.filter((p) => !panelistIds.has(p.id))
.filter((p) => (searchFilter ? p.name.toLowerCase().includes(searchFilter.toLowerCase()) : true)), [member, panelistIds, participants, searchFilter]);
const onAdd = useCallback(async (participant) => {
const success = await addPanelist({
socket,
participant,
currentPanelists: localPanelists,
maxPanelists: itemPageLimit,
roomName,
member,
islevel,
showAlert,
});
if (!success)
return;
const next = [...localPanelists, participant];
setLocalPanelists(next);
updatePanelists?.(next);
}, [islevel, itemPageLimit, localPanelists, member, roomName, showAlert, socket, updatePanelists]);
const onRemove = useCallback(async (participant) => {
await removePanelist({
socket,
participant,
roomName,
member,
islevel,
showAlert,
});
const next = localPanelists.filter((p) => p.id !== participant.id);
setLocalPanelists(next);
updatePanelists?.(next);
}, [islevel, localPanelists, member, roomName, showAlert, socket, updatePanelists]);
const onToggleFocus = useCallback(async () => {
const next = !isFocused;
await focusPanelists({
socket,
roomName,
member,
islevel,
focusEnabled: next,
muteOthersMic: next ? muteOthersMic : false,
muteOthersCamera: next ? muteOthersCamera : false,
showAlert,
});
setIsFocused(next);
updatePanelistsFocused?.(next);
}, [
isFocused,
islevel,
member,
muteOthersCamera,
muteOthersMic,
roomName,
showAlert,
socket,
updatePanelistsFocused,
]);
if (!isPanelistsModalVisible)
return null;
const isEmbedded = renderMode === 'sidebar' || renderMode === 'inline';
const content = (<>
<View style={isEmbedded ? styles.embeddedHeader : styles.header}>
<View style={styles.headerLeft}>
<View style={[styles.headerIcon, isEmbedded && styles.embeddedHeaderIcon]}>
<FontAwesome5 name="user-tie" size={16} color={isEmbedded ? theme.accentColor : '#fff'}/>
</View>
<View>
<Text style={[isEmbedded ? styles.embeddedTitle : styles.headerTitle, { color: isEmbedded ? theme.textColor : '#fff' }]}>Panelists</Text>
{isEmbedded && <Text style={[styles.embeddedSubtitle, { color: theme.mutedTextColor }]}>Focus and presenter access</Text>}
</View>
</View>
{!isEmbedded && (<TouchableOpacity style={styles.closeButton} onPress={onPanelistsClose}>
<FontAwesome5 name="times" size={14} color="#fff"/>
</TouchableOpacity>)}
</View>
<ScrollView style={styles.content} contentContainerStyle={styles.contentInner}>
{isHost && (<View style={styles.section}>
<Text style={[styles.sectionTitle, { color: theme.textColor }]}>Focus mode</Text>
<TouchableOpacity style={styles.switchRow} onPress={onToggleFocus}>
<Text style={[styles.lightText, { color: theme.mutedTextColor }]}>{isFocused ? 'Disable focus on panelists' : 'Enable focus on panelists'}</Text>
</TouchableOpacity>
{isFocused && (<View>
<TouchableOpacity style={styles.switchRow} onPress={() => setMuteOthersMic((v) => !v)}>
<Text style={[styles.lightText, { color: theme.mutedTextColor }]}>Mute others' microphone</Text>
<Text style={[styles.lightText, { color: theme.mutedTextColor }]}>{muteOthersMic ? 'ON' : 'OFF'}</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.switchRow} onPress={() => setMuteOthersCamera((v) => !v)}>
<Text style={[styles.lightText, { color: theme.mutedTextColor }]}>Mute others' camera</Text>
<Text style={[styles.lightText, { color: theme.mutedTextColor }]}>{muteOthersCamera ? 'ON' : 'OFF'}</Text>
</TouchableOpacity>
</View>)}
</View>)}
<View style={styles.section}>
<Text style={[styles.sectionTitle, { color: theme.textColor }]}>Current panelists ({localPanelists.length}/{itemPageLimit})</Text>
{localPanelists.length === 0 ? (<Text style={[styles.emptyText, { color: theme.mutedTextColor }]}>No panelists selected</Text>) : (localPanelists.map((p) => (<View key={p.id || p.name} style={[styles.participantItem, { backgroundColor: theme.rowBackgroundColor }]}>
<Text style={[styles.participantName, { color: theme.textColor }]}>{p.name}</Text>
{isHost && (<TouchableOpacity style={styles.actionBtn} onPress={() => onRemove(p)}>
<FontAwesome5 name="minus" size={12} color="#f472b6"/>
</TouchableOpacity>)}
</View>)))}
</View>
{isHost && (<View style={styles.section}>
<Text style={[styles.sectionTitle, { color: theme.textColor }]}>Add participants</Text>
<View style={[styles.searchContainer, { backgroundColor: theme.inputBackgroundColor, borderColor: theme.borderColor, borderWidth: 1 }]}>
<FontAwesome5 name="search" size={13} color={theme.mutedTextColor}/>
<TextInput style={[styles.searchInput, { color: theme.inputTextColor }]} placeholder="Search participants..." placeholderTextColor={theme.placeholderTextColor} value={searchFilter} onChangeText={setSearchFilter}/>
</View>
{availableParticipants.length === 0 ? (<Text style={[styles.emptyText, { color: theme.mutedTextColor }]}>No participants found</Text>) : (availableParticipants.map((p) => (<View key={p.id || p.name} style={[styles.participantItem, { backgroundColor: theme.rowBackgroundColor }]}>
<Text style={[styles.participantName, { color: theme.textColor }]}>{p.name}</Text>
<TouchableOpacity style={styles.actionBtn} onPress={() => onAdd(p)}>
<FontAwesome5 name="plus" size={12} color="#22d3ee"/>
</TouchableOpacity>
</View>)))}
</View>)}
</ScrollView>
</>);
if (isEmbedded) {
return (<View style={[styles.embeddedContainer, { backgroundColor, borderColor: theme.borderColor }, style]}>
{content}
</View>);
}
const defaultContainer = (<Modal visible={isPanelistsModalVisible} transparent animationType="fade" onRequestClose={onPanelistsClose}>
<TouchableOpacity style={[styles.overlay, getModalPosition({ position })]} activeOpacity={1} onPress={onPanelistsClose}>
<TouchableOpacity style={[styles.modalContainer, { backgroundColor, borderColor: theme.borderColor }, style]} activeOpacity={1}>
{content}
</TouchableOpacity>
</TouchableOpacity>
</Modal>);
return renderContainer
? renderContainer({ defaultContainer, dimensions: { width, height } })
: defaultContainer;
};
const styles = StyleSheet.create({
overlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.55)', justifyContent: 'center', alignItems: 'center' },
modalContainer: {
width: Math.min(460, width * 0.92),
maxHeight: height * 0.88,
borderRadius: 16,
overflow: 'hidden',
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.08)',
},
embeddedContainer: {
flex: 1,
width: '100%',
height: '100%',
borderWidth: 0,
borderRadius: 0,
overflow: 'hidden',
},
header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 14, backgroundColor: '#7c3aed' },
embeddedHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 16,
paddingVertical: 14,
borderBottomWidth: 1,
borderBottomColor: 'rgba(148,163,184,0.22)',
},
headerLeft: { flexDirection: 'row', alignItems: 'center' },
headerIcon: { alignItems: 'center', justifyContent: 'center' },
embeddedHeaderIcon: {
width: 34,
height: 34,
borderRadius: 17,
backgroundColor: 'rgba(56,189,248,0.12)',
marginRight: 10,
},
headerTitle: { color: '#fff', fontSize: 17, fontWeight: '700', marginLeft: 10 },
embeddedTitle: { fontSize: 17, fontWeight: '700' },
embeddedSubtitle: { fontSize: 12, marginTop: 2 },
closeButton: { backgroundColor: 'rgba(255,255,255,0.2)', padding: 8, borderRadius: 8 },
content: { flex: 1 },
contentInner: { padding: 14, paddingBottom: 22 },
section: { marginBottom: 18 },
sectionTitle: { color: '#fff', fontSize: 15, fontWeight: '600', marginBottom: 8 },
lightText: { color: '#e2e8f0', fontSize: 13 },
emptyText: { color: 'rgba(255,255,255,0.55)', fontSize: 13 },
switchRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
backgroundColor: 'rgba(255,255,255,0.06)',
borderRadius: 8,
paddingHorizontal: 12,
paddingVertical: 10,
marginBottom: 8,
},
searchContainer: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'rgba(255,255,255,0.08)',
borderRadius: 8,
paddingHorizontal: 10,
marginBottom: 10,
},
searchInput: { flex: 1, color: '#fff', paddingVertical: 10, paddingHorizontal: 8 },
participantItem: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
backgroundColor: 'rgba(255,255,255,0.06)',
borderRadius: 8,
paddingHorizontal: 12,
paddingVertical: 10,
marginBottom: 8,
},
participantName: { color: '#fff', fontSize: 14 },
actionBtn: { backgroundColor: 'rgba(15,23,42,0.6)', borderRadius: 6, padding: 8 },
});
export default PanelistsModal;
//# sourceMappingURL=PanelistsModal.js.map