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
334 lines (331 loc) • 13.3 kB
JavaScript
// ParticipantsModal.tsx
import React, { useEffect, useState } from 'react';
import { Modal, View, Text, Pressable, StyleSheet, ScrollView, TextInput, Dimensions, } from 'react-native';
import { FontAwesome } from '@expo/vector-icons';
import { getModalPosition } from '../../methods/utils/getModalPosition';
import ParticipantList from './ParticipantList';
import ParticipantListOthers from './ParticipantListOthers';
import { muteParticipants } from '../../methods/participantsMethods/muteParticipants';
import { messageParticipants } from '../../methods/participantsMethods/messageParticipants';
import { removeParticipants } from '../../methods/participantsMethods/removeParticipants';
/**
* ParticipantsModal - Participant list with search, filtering, and management actions
*
* ParticipantsModal is a comprehensive React Native modal for viewing and managing
* session participants. It provides search/filter functionality, participant lists
* (segmented by role/type), and action buttons for muting, messaging, or removing
* participants based on user permissions.
*
* **Key Features:**
* - Real-time participant list with count display
* - Search/filter functionality for finding participants
* - Segmented lists (main participants vs. others)
* - Permission-based actions (mute, message, remove)
* - Role-based visibility (host, co-host, participant)
* - Custom renderers for participant list items
* - Responsive design with flexible positioning
*
* **UI Customization:**
* This component can be replaced via `uiOverrides.participantsModalComponent` to
* provide a completely custom participants modal implementation.
*
* @component
* @param {ParticipantsModalOptions} props - Configuration options for the ParticipantsModal component
*
* @returns {JSX.Element} Rendered participants modal with list and management actions
*
* @example
* // Basic usage - Display participants modal with default settings
* import React, { useState } from 'react';
* import { ParticipantsModal } from 'mediasfu-reactnative-expo';
* import { Socket } from 'socket.io-client';
*
* function MeetingControls() {
* const [isModalVisible, setIsModalVisible] = useState(false);
* const [participants, setParticipants] = useState([
* { name: 'Alice', id: '1', islevel: '1', muted: false },
* { name: 'Bob', id: '2', islevel: '1', muted: true },
* ]);
*
* const participantsParameters = {
* position: 'topRight',
* backgroundColor: '#83c0e9',
* coHostResponsibility: [{ name: 'participants', value: true }],
* coHost: 'JaneDoe',
* member: 'JohnDoe',
* islevel: '2',
* participants,
* eventType: 'conference',
* filteredParticipants: participants,
* socket: socketInstance,
* roomName: 'MainRoom',
* updateIsMessagesModalVisible: (visible) => console.log('Messages:', visible),
* updateDirectMessageDetails: (participant) => console.log('DM:', participant),
* updateStartDirectMessage: (start) => console.log('Start DM:', start),
* updateParticipants: setParticipants,
* getUpdatedAllParams: () => participantsParameters,
* };
*
* return (
* <>
* <Button title="Participants" onPress={() => setIsModalVisible(true)} />
* <ParticipantsModal
* isParticipantsModalVisible={isModalVisible}
* onParticipantsClose={() => setIsModalVisible(false)}
* onParticipantsFilterChange={(filter) => console.log('Filter:', filter)}
* participantsCounter={participants.length}
* parameters={participantsParameters}
* />
* </>
* );
* }
*
* @example
* // With custom action handlers and positioning
* <ParticipantsModal
* isParticipantsModalVisible={showParticipants}
* onParticipantsClose={() => setShowParticipants(false)}
* onParticipantsFilterChange={(filter) => handleFilterChange(filter)}
* participantsCounter={filteredParticipants.length}
* onMuteParticipants={async (options) => {
* console.log('Custom mute logic');
* await muteParticipants(options);
* }}
* onMessageParticipants={(options) => {
* console.log('Messaging participant:', options.participant.name);
* messageParticipants(options);
* }}
* onRemoveParticipants={async (options) => {
* if (confirm('Remove participant?')) {
* await removeParticipants(options);
* }
* }}
* parameters={{
* ...participantsParameters,
* position: 'center',
* backgroundColor: '#1a1a2e',
* }}
* backgroundColor="#1a1a2e"
* position="center"
* />
*
* @example
* // Using uiOverrides for complete modal replacement
* import { MyCustomParticipantsModal } from './MyCustomParticipantsModal';
*
* const sessionConfig = {
* credentials: { apiKey: 'your-api-key' },
* uiOverrides: {
* participantsModalComponent: {
* component: MyCustomParticipantsModal,
* injectedProps: {
* theme: 'compact',
* showAvatars: true,
* },
* },
* },
* };
*
* // MyCustomParticipantsModal.tsx
* export const MyCustomParticipantsModal = (props: ParticipantsModalOptions & { theme: string; showAvatars: boolean }) => {
* return (
* <Modal visible={props.isParticipantsModalVisible} onRequestClose={props.onParticipantsClose}>
* <View>
* <Text>Participants ({props.participantsCounter})</Text>
* {props.parameters.participants.map(p => (
* <View key={p.id}>
* {props.showAvatars && <Image source={{ uri: p.avatar }} />}
* <Text>{p.name}</Text>
* </View>
* ))}
* </View>
* </Modal>
* );
* };
*/
const ParticipantsModal = ({ isParticipantsModalVisible, onParticipantsClose, onParticipantsFilterChange, participantsCounter, onMuteParticipants = muteParticipants, onMessageParticipants = messageParticipants, onRemoveParticipants = removeParticipants, RenderParticipantList = ParticipantList, RenderParticipantListOthers = ParticipantListOthers, position = 'topRight', backgroundColor = '#83c0e9', isDarkMode, parameters, style, renderContent, renderContainer, }) => {
const { coHostResponsibility, coHost, member, islevel, showAlert, participants, roomName, eventType, socket, updateIsMessagesModalVisible, updateDirectMessageDetails, updateStartDirectMessage, updateParticipants, } = parameters;
const [participantList, setParticipantList] = useState(participants);
const [participantsCounter_s, setParticipantsCounter_s] = useState(participantsCounter);
const [filterText, setFilterText] = useState('');
const screenWidth = Dimensions.get('window').width;
let modalWidth = 0.8 * screenWidth;
if (modalWidth > 400) {
modalWidth = 400;
}
let participantsValue = false;
try {
participantsValue = coHostResponsibility?.find((item) => item.name === 'participants')?.value ?? false;
}
catch {
// Default to false if not found
}
useEffect(() => {
if (!isParticipantsModalVisible) {
return;
}
const updatedParams = parameters.getUpdatedAllParams();
setParticipantList(updatedParams.filteredParticipants);
setParticipantsCounter_s(updatedParams.filteredParticipants.length);
}, [isParticipantsModalVisible, participants, parameters]);
if (!isParticipantsModalVisible) {
return null;
}
const dimensions = { width: modalWidth, height: 0 };
const themed = typeof isDarkMode === 'boolean';
const textColor = themed ? (isDarkMode ? '#f8fafc' : '#0f172a') : 'black';
const mutedTextColor = themed ? (isDarkMode ? '#cbd5e1' : '#475569') : 'gray';
const borderColor = themed ? (isDarkMode ? 'rgba(226, 232, 240, 0.18)' : 'rgba(71, 85, 105, 0.22)') : '#000000';
const inputBackgroundColor = themed ? (isDarkMode ? '#1e293b' : '#ffffff') : 'white';
const inputBorderColor = themed ? (isDarkMode ? 'rgba(226, 232, 240, 0.22)' : 'rgba(71, 85, 105, 0.28)') : 'gray';
const badgeBackgroundColor = themed ? (isDarkMode ? '#38bdf8' : '#2563eb') : '#fff';
const badgeTextColor = themed ? '#ffffff' : '#000';
const defaultContent = (<ScrollView style={styles.scrollView}>
<View style={styles.modalHeader}>
<Text style={[styles.modalTitle, { color: textColor }]}>
Participants
{' '}
<Text style={[styles.badge, { backgroundColor: badgeBackgroundColor, color: badgeTextColor }]}>{participantsCounter_s}</Text>
</Text>
<Pressable onPress={onParticipantsClose} style={styles.closeButton}>
<FontAwesome name="times" size={24} color={textColor}/>
</Pressable>
</View>
<View style={[styles.separator, { backgroundColor: borderColor }]}/>
<View style={styles.modalBody}>
{/* Search Input */}
<View style={styles.formGroup}>
<TextInput style={[styles.input, { color: textColor, borderColor: inputBorderColor, backgroundColor: inputBackgroundColor }]} placeholder="Search ..." placeholderTextColor={mutedTextColor} value={filterText} onChangeText={(text) => {
setFilterText(text);
onParticipantsFilterChange(text);
}}/>
</View>
{/* Participant List */}
{(participantList && islevel === '2')
|| (coHost === member && participantsValue === true) ? (<RenderParticipantList participants={participantList} isBroadcast={eventType === 'broadcast'} onMuteParticipants={onMuteParticipants} onMessageParticipants={onMessageParticipants} onRemoveParticipants={onRemoveParticipants} socket={socket} coHostResponsibility={coHostResponsibility} member={member} islevel={islevel} showAlert={showAlert} coHost={coHost} roomName={roomName} updateIsMessagesModalVisible={updateIsMessagesModalVisible} updateDirectMessageDetails={updateDirectMessageDetails} updateStartDirectMessage={updateStartDirectMessage} updateParticipants={updateParticipants} isDarkMode={isDarkMode}/>) : participantList ? (<RenderParticipantListOthers participants={participantList} coHost={coHost} member={member} isDarkMode={isDarkMode}/>) : (<Text style={[styles.noParticipantsText, { color: mutedTextColor }]}>No participants</Text>)}
</View>
</ScrollView>);
const content = renderContent
? renderContent({ defaultContent, dimensions })
: defaultContent;
const defaultContainer = (<Modal transparent animationType="slide" visible={isParticipantsModalVisible} onRequestClose={onParticipantsClose}>
<View style={[styles.modalContainer, getModalPosition({ position })]}>
<View style={[styles.modalContent, { backgroundColor, width: modalWidth }, style]}>
{content}
</View>
</View>
</Modal>);
return renderContainer
? renderContainer({ defaultContainer, dimensions })
: defaultContainer;
};
export default ParticipantsModal;
/**
* Stylesheet for the ParticipantsModal component.
*/
const styles = StyleSheet.create({
modalContainer: {
flex: 1,
justifyContent: 'flex-end',
alignItems: 'flex-end',
zIndex: 9,
elevation: 9,
},
modalContent: {
height: '65%',
backgroundColor: '#83c0e9',
borderRadius: 10,
padding: 15,
maxHeight: '65%',
maxWidth: '70%',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.25,
shadowRadius: 4,
elevation: 9,
zIndex: 9,
},
modalHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
modalTitle: {
fontSize: 18,
fontWeight: 'bold',
color: 'black',
flexDirection: 'row',
alignItems: 'center',
},
badge: {
backgroundColor: '#fff',
color: '#000',
borderRadius: 12,
paddingHorizontal: 5,
paddingVertical: 2,
marginLeft: 5,
fontSize: 14,
},
closeButton: {
padding: 5,
},
separator: {
height: 1,
backgroundColor: '#000000',
marginVertical: 10,
},
modalBody: {
flex: 1,
},
formGroup: {
marginBottom: 10,
},
input: {
fontSize: 14,
paddingVertical: 8,
paddingHorizontal: 8,
borderWidth: 1,
borderColor: 'gray',
borderRadius: 4,
color: 'black',
paddingRight: 20,
backgroundColor: 'white',
},
scrollView: {
flexGrow: 1,
},
waitingItem: {
flexDirection: 'row',
alignItems: 'center',
marginVertical: 5,
paddingVertical: 10,
borderBottomWidth: 1,
borderBottomColor: '#ddd',
},
participantName: {
flex: 5,
},
participantText: {
fontSize: 16,
color: 'black',
},
actionButtons: {
flex: 2,
flexDirection: 'row',
justifyContent: 'space-around',
alignItems: 'center',
},
acceptButton: {
padding: 5,
},
rejectButton: {
padding: 5,
},
noParticipantsText: {
textAlign: 'center',
color: 'gray',
fontSize: 16,
marginTop: 20,
},
});
//# sourceMappingURL=ParticipantsModal.js.map