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
558 lines (552 loc) • 20.6 kB
JavaScript
import React, { useEffect, useState } from 'react';
import { Modal, View, Text, TextInput, ScrollView, Pressable, StyleSheet, Dimensions, } from 'react-native';
import { FontAwesome5 } from '@expo/vector-icons';
import RNPickerSelect from 'react-native-picker-select';
import { getModalPosition } from '../../methods/utils/getModalPosition';
/**
* PollModal - Interactive polling and voting interface
*
* PollModal is a React Native component that provides a complete polling system
* for meetings. Hosts/co-hosts can create polls with multiple options, participants
* can vote, and results are displayed in real-time with vote counts and percentages.
*
* **Key Features:**
* - Poll creation with custom question and multiple options (host/co-host only)
* - Real-time voting for participants
* - Live vote count and percentage display
* - Poll history view (all active and inactive polls)
* - End poll functionality (host/co-host only)
* - Single vote per participant enforcement
* - Socket.io synchronization for instant updates
* - Position-configurable modal (5 positions)
*
* **UI Customization:**
* This component can be replaced via `uiOverrides.pollModal` to
* provide a completely custom polling interface.
*
* @component
* @param {PollModalOptions} props - Configuration options
*
* @returns {JSX.Element} Rendered poll modal
*
* @example
* ```tsx
* // Host creating and managing polls
* import React, { useState } from 'react';
* import { PollModal } from 'mediasfu-reactnative-expo';
* import { io } from 'socket.io-client';
*
* const socket = io('https://your-server.com');
* const [showPolls, setShowPolls] = useState(false);
*
* const activePoll = {
* id: 'poll-1',
* question: 'What time works best?',
* options: ['Morning', 'Afternoon', 'Evening'],
* status: 'active',
* voters: { 'user1': 0, 'user2': 2 },
* votes: [1, 0, 1],
* };
*
* return (
* <PollModal
* isPollModalVisible={showPolls}
* onClose={() => setShowPolls(false)}
* member="host-user"
* islevel="2" // Host - can create/end polls
* polls={[activePoll]}
* poll={activePoll}
* socket={socket}
* roomName="meeting-room-123"
* handleCreatePoll={handleCreatePoll}
* handleEndPoll={handleEndPoll}
* handleVotePoll={handleVotePoll}
* updateIsPollModalVisible={setShowPolls}
* showAlert={showCustomAlert}
* />
* );
* ```
*
* @example
* ```tsx
* // Participant voting in polls
* return (
* <PollModal
* isPollModalVisible={isVisible}
* onClose={handleClose}
* member="participant-user"
* islevel="0" // Participant - can only vote
* polls={allPolls}
* poll={currentPoll}
* socket={socketConnection}
* roomName={roomId}
* handleCreatePoll={handleCreatePoll}
* handleEndPoll={handleEndPoll}
* handleVotePoll={handleVotePoll}
* updateIsPollModalVisible={setIsVisible}
* position="center"
* backgroundColor="#e8f5e9"
* />
* );
* ```
*
* @example
* ```tsx
* // Using custom UI via uiOverrides
* const config = {
* uiOverrides: {
* pollModal: {
* component: MyCustomPollInterface,
* injectedProps: {
* theme: 'dark',
* allowAnonymousVoting: false,
* },
* },
* },
* };
*
* return <MyMeetingComponent config={config} />;
* ```
*/
const PollModal = ({ isPollModalVisible, onClose, position = 'topRight', backgroundColor = '#f5f5f5', isDarkMode, member, islevel, polls, poll, socket, roomName, showAlert, updateIsPollModalVisible, handleCreatePoll, handleEndPoll, handleVotePoll, style, renderContent, renderContainer, }) => {
const [newPoll, setNewPoll] = useState({
question: '',
type: '',
options: [],
});
const screenWidth = Dimensions.get('window').width;
let modalWidth = 0.9 * screenWidth; // Adjusted for better mobile view
if (modalWidth > 350) {
modalWidth = 350;
}
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') : 'transparent';
const inputBorderColor = themed ? (isDarkMode ? 'rgba(226, 232, 240, 0.22)' : 'rgba(71, 85, 105, 0.28)') : 'gray';
const controlColor = themed ? (isDarkMode ? '#60a5fa' : '#2563eb') : '#000';
const pickerTheme = themed ? createPickerSelectStyles(isDarkMode) : pickerSelectStyles;
/**
* Renders polls based on the user's level and poll status.
*/
const renderPolls = () => {
let activePollCount = 0;
polls.forEach((polled) => {
if (polled.status === 'active' && poll && polled.id === poll.id) {
activePollCount++;
}
});
if (islevel === '2' && activePollCount === 0) {
if (poll && poll.status === 'active') {
// Ideally, you should handle state immutably
// This is just a placeholder; consider using state management
poll.status = 'inactive';
}
}
};
useEffect(() => {
if (isPollModalVisible) {
renderPolls();
}
}, [isPollModalVisible, polls, poll]);
/**
* Calculates the percentage of votes for a given option.
* @param votes Array of vote counts.
* @param optionIndex Index of the option.
* @returns Percentage string.
*/
const calculatePercentage = (votes, optionIndex) => {
const totalVotes = votes.reduce((a, b) => a + b, 0);
return totalVotes > 0
? ((votes[optionIndex] / totalVotes) * 100).toFixed(2)
: '0.00';
};
/**
* Handles the change in poll type and initializes options accordingly.
* @param type Selected poll type.
*/
const handlePollTypeChange = (type) => {
let options = [];
switch (type) {
case 'trueFalse':
options = ['True', 'False'];
break;
case 'yesNo':
options = ['Yes', 'No'];
break;
case 'custom':
options = [];
break;
default:
options = [];
break;
}
setNewPoll({ ...newPoll, type, options });
};
/**
* Renders poll options based on the selected poll type.
*/
const renderPollOptions = () => {
switch (newPoll?.type) {
case 'trueFalse':
case 'yesNo':
return (<View>
{newPoll.options.map((option, index) => (<View style={styles.formCheck} key={index}>
<View style={[styles.radioButton, { borderColor: controlColor }]}>
<View style={styles.radioButtonIcon}/>
</View>
<Text style={[styles.formCheckLabel, { color: textColor }]}>{option}</Text>
</View>))}
</View>);
case 'custom':
return (<>
{newPoll.options?.map((option, index) => (<View style={styles.formGroup} key={index}>
<TextInput style={[styles.formControl, { color: textColor, borderColor: inputBorderColor, backgroundColor: inputBackgroundColor }]} placeholder={`Option ${index + 1}`} placeholderTextColor={mutedTextColor} maxLength={50} value={option || ''} onChangeText={(text) => {
const newOptions = [...newPoll.options];
newOptions[index] = text;
setNewPoll({ ...newPoll, options: newOptions });
}}/>
</View>))}
{[...Array(5 - (newPoll.options?.length || 0))].map((_, index) => (<View style={styles.formGroup} key={(newPoll.options?.length || 0) + index}>
<TextInput style={[styles.formControl, { color: textColor, borderColor: inputBorderColor, backgroundColor: inputBackgroundColor }]} placeholder={`Option ${(newPoll.options?.length || 0) + index + 1}`} placeholderTextColor={mutedTextColor} maxLength={50} value="" onChangeText={(text) => {
const newOptions = [...(newPoll.options || []), text];
setNewPoll({ ...newPoll, options: newOptions });
}}/>
</View>))}
</>);
default:
return null;
}
};
/**
* Renders the options for the current active poll.
*/
const renderCurrentPollOptions = () => poll?.options.map((option, i) => (<Pressable key={i} style={styles.formCheck} onPress={() => handleVotePoll({
pollId: poll.id,
optionIndex: i,
socket,
showAlert,
member,
roomName,
updateIsPollModalVisible,
})}>
<View style={[
styles.radioButton,
{ borderColor: controlColor },
poll.voters
&& poll.voters[member] === i
&& [styles.radioButtonSelected, { borderColor: controlColor, backgroundColor: controlColor }],
]}>
{poll.voters && poll.voters[member] === i && (<View style={styles.radioButtonIcon}/>)}
</View>
<Text style={[styles.formCheckLabel, { color: textColor }]}>{option}</Text>
</Pressable>));
const dimensions = { width: modalWidth, height: 0 };
const defaultContent = (<>
{/* Header */}
<View style={styles.header}>
<Text style={[styles.headerText, { color: textColor }]}>Polls</Text>
<Pressable onPress={onClose} style={styles.closeButton}>
<FontAwesome5 name="times" size={24} color={textColor}/>
</Pressable>
</View>
<View style={[styles.separator, { backgroundColor: borderColor }]}/>
<ScrollView>
{islevel === '2' && (<>
{/* Previous Polls */}
<View style={styles.section}>
<Text style={[styles.sectionHeader, { color: textColor }]}>Previous Polls</Text>
{polls.length === 0 && (<Text style={[styles.noPollText, { color: mutedTextColor }]}>No polls available</Text>)}
{polls.map((polled, index) => polled
&& (!poll
|| (poll
&& (poll.status !== 'active'
|| polled.id !== poll.id))) && (<View key={index} style={styles.poll}>
<Text style={[styles.pollLabel, { color: textColor }]}>Question:</Text>
<TextInput style={[styles.textarea, { color: textColor, borderColor: inputBorderColor, backgroundColor: inputBackgroundColor }]} multiline editable={false} value={polled.question}/>
<Text style={[styles.pollLabel, { color: textColor }]}>Options:</Text>
{polled.options.map((option, i) => (<Text key={i} style={[styles.optionText, { color: textColor }]}>
{`${option}: ${polled.votes[i]} votes (${calculatePercentage(polled.votes, i)}%)`}
</Text>))}
{polled.status === 'active' && (<Pressable style={[styles.button, styles.buttonDanger]} onPress={() => handleEndPoll({
pollId: polled.id,
socket,
showAlert,
roomName,
updateIsPollModalVisible,
})}>
<Text style={styles.buttonText}>End Poll</Text>
</Pressable>)}
</View>))}
</View>
<View style={[styles.separator, { backgroundColor: borderColor }]}/>
{/* Create New Poll */}
<View style={styles.section}>
<Text style={[styles.sectionHeader, { color: textColor }]}>Create a New Poll</Text>
<View style={styles.formGroup}>
<Text style={[styles.formLabel, { color: textColor }]}>Poll Question</Text>
<TextInput style={[styles.textarea, { color: textColor, borderColor: inputBorderColor, backgroundColor: inputBackgroundColor }]} multiline maxLength={300} value={newPoll.question} onChangeText={(text) => setNewPoll({ ...newPoll, question: text })} placeholder="Enter your question here..." placeholderTextColor={mutedTextColor}/>
</View>
<View style={styles.formGroup}>
<Text style={[styles.formLabel, { color: textColor }]}>
Select Poll Answer Type
</Text>
<RNPickerSelect onValueChange={handlePollTypeChange} items={[
{ label: 'Choose...', value: '' },
{ label: 'True/False', value: 'trueFalse' },
{ label: 'Yes/No', value: 'yesNo' },
{ label: 'Custom', value: 'custom' },
]} placeholder={{}} style={pickerTheme} value={newPoll.type} useNativeAndroidPickerStyle={false}/>
</View>
{renderPollOptions()}
<Pressable style={[styles.button, styles.buttonPrimary]} onPress={() => handleCreatePoll({
poll: newPoll,
socket,
roomName,
showAlert,
updateIsPollModalVisible,
})}>
<Text style={styles.buttonText}>Create Poll</Text>
</Pressable>
</View>
<View style={[styles.separator, { backgroundColor: borderColor }]}/>
</>)}
{/* Current Poll */}
<View style={styles.section}>
<Text style={[styles.sectionHeader, { color: textColor }]}>Current Poll</Text>
{poll && poll.status === 'active' ? (<View style={styles.poll}>
<Text style={[styles.pollLabel, { color: textColor }]}>Question:</Text>
<TextInput style={[styles.textarea, { color: textColor, borderColor: inputBorderColor, backgroundColor: inputBackgroundColor }]} multiline editable={false} value={poll.question}/>
<Text style={[styles.pollLabel, { color: textColor }]}>Options:</Text>
{renderCurrentPollOptions()}
{poll.status === 'active' && islevel === '2' && (<Pressable style={[styles.button, styles.buttonDanger]} onPress={() => handleEndPoll({
pollId: poll.id,
socket,
showAlert,
roomName,
updateIsPollModalVisible,
})}>
<Text style={styles.buttonText}>End Poll</Text>
</Pressable>)}
</View>) : (<Text style={[styles.noPollText, { color: mutedTextColor }]}>No active poll</Text>)}
</View>
</ScrollView>
</>);
const content = renderContent
? renderContent({ defaultContent, dimensions })
: defaultContent;
const defaultContainer = (<Modal visible={isPollModalVisible} transparent animationType="slide" onRequestClose={onClose}>
<View style={[styles.modalContainer, getModalPosition({ position })]}>
<View style={[styles.modalContent, { backgroundColor, width: modalWidth }, style]}>
{content}
</View>
</View>
</Modal>);
return renderContainer
? renderContainer({ defaultContainer, dimensions })
: defaultContainer;
};
const styles = StyleSheet.create({
modalContainer: {
flex: 1,
borderRadius: 10,
padding: 10,
zIndex: 9,
elevation: 9,
},
modalContent: {
backgroundColor: '#ffffff',
borderRadius: 10,
padding: 15,
maxHeight: '70%',
maxWidth: '80%',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.25,
shadowRadius: 4,
elevation: 9,
zIndex: 9,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 15,
},
headerText: {
fontSize: 24,
fontWeight: 'bold',
color: 'black',
},
closeButton: {
padding: 5,
},
separator: {
height: 1,
backgroundColor: '#000000',
marginVertical: 10,
},
section: {
marginBottom: 15,
},
sectionHeader: {
fontSize: 20,
fontWeight: 'bold',
marginBottom: 10,
color: 'black',
},
noPollText: {
fontSize: 16,
color: 'gray',
textAlign: 'center',
marginVertical: 10,
},
poll: {
marginBottom: 10,
},
pollLabel: {
fontWeight: 'bold',
fontSize: 16,
marginBottom: 5,
},
optionText: {
fontSize: 14,
marginLeft: 10,
marginBottom: 5,
color: 'black',
},
textarea: {
borderWidth: 1,
borderColor: 'gray',
borderRadius: 5,
padding: 10,
marginBottom: 10,
textAlignVertical: 'top',
fontSize: 16,
color: 'black',
},
formGroup: {
marginBottom: 15,
},
formControl: {
fontSize: 16,
paddingVertical: 8,
paddingHorizontal: 10,
borderWidth: 1,
borderColor: 'gray',
borderRadius: 5,
color: 'black',
},
formLabel: {
fontSize: 16,
marginBottom: 5,
color: 'black',
},
formCheck: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 10,
},
radioButton: {
height: 20,
width: 20,
borderRadius: 10,
borderWidth: 1,
borderColor: '#000',
alignItems: 'center',
justifyContent: 'center',
marginRight: 10,
},
radioButtonSelected: {
borderColor: '#000',
backgroundColor: '#000',
},
radioButtonIcon: {
height: 10,
width: 10,
borderRadius: 5,
backgroundColor: '#fff',
},
formCheckLabel: {
fontSize: 16,
color: 'black',
},
button: {
padding: 10,
borderRadius: 5,
alignItems: 'center',
justifyContent: 'center',
marginTop: 10,
},
buttonText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
},
buttonPrimary: {
backgroundColor: '#000000',
},
buttonDanger: {
backgroundColor: '#dc3545',
},
});
/**
* Styles for the RNPickerSelect component.
*/
const pickerSelectStyles = StyleSheet.create({
inputIOS: {
fontSize: 16,
paddingVertical: 4,
paddingHorizontal: 10,
borderWidth: 1,
borderColor: 'gray',
borderRadius: 5,
color: 'black',
paddingRight: 30, // to ensure the text is never behind the icon
},
inputAndroid: {
fontSize: 16,
paddingHorizontal: 10,
paddingVertical: 4,
borderWidth: 0.5,
borderColor: 'gray',
borderRadius: 5,
color: 'black',
paddingRight: 30, // to ensure the text is never behind the icon
},
inputWeb: {
fontSize: 14,
paddingHorizontal: 10,
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 createPickerSelectStyles = (isDarkMode) => StyleSheet.create({
inputIOS: {
...pickerSelectStyles.inputIOS,
color: isDarkMode ? '#f8fafc' : '#0f172a',
borderColor: isDarkMode ? 'rgba(226, 232, 240, 0.22)' : 'rgba(71, 85, 105, 0.28)',
backgroundColor: isDarkMode ? '#1e293b' : '#ffffff',
},
inputAndroid: {
...pickerSelectStyles.inputAndroid,
color: isDarkMode ? '#f8fafc' : '#0f172a',
borderColor: isDarkMode ? 'rgba(226, 232, 240, 0.22)' : 'rgba(71, 85, 105, 0.28)',
backgroundColor: isDarkMode ? '#1e293b' : '#ffffff',
},
inputWeb: {
...pickerSelectStyles.inputWeb,
color: isDarkMode ? '#f8fafc' : '#0f172a',
borderColor: isDarkMode ? 'rgba(226, 232, 240, 0.22)' : 'rgba(71, 85, 105, 0.28)',
backgroundColor: isDarkMode ? '#1e293b' : '#ffffff',
},
});
export default PollModal;
//# sourceMappingURL=PollModal.js.map