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
492 lines (488 loc) • 18.7 kB
JavaScript
import React, { useEffect, useState } from 'react';
import { Modal, Platform, Pressable, ScrollView, StyleSheet, Text, TextInput, View, useWindowDimensions, } from 'react-native';
import { FontAwesome5 } from '@expo/vector-icons';
import RNPickerSelect from 'react-native-picker-select';
import { getModalPosition } from '../../methods/utils/getModalPosition';
import { getModernColors, getModernModalCardStyle, getModernSidePanelStyle, resolveIsDarkMode, } from '../core/modernTheme';
import { createThemedPickerSelectStyles, getModalBodyTheme } from '../core/modalBodyTheme';
export const ModernPollModal = ({ isPollModalVisible, onClose, position = 'topRight', backgroundColor, isDarkMode, member, islevel, polls, poll, socket, roomName, showAlert, updateIsPollModalVisible, handleCreatePoll, handleEndPoll, handleVotePoll, style, renderContent, renderContainer, renderMode = 'modal', }) => {
const { width: windowWidth, height: windowHeight } = useWindowDimensions();
const darkMode = resolveIsDarkMode({ isDarkMode, backgroundColor });
const colors = getModernColors(darkMode);
const modalTheme = getModalBodyTheme(darkMode);
const pickerTheme = createThemedPickerSelectStyles(modalTheme);
const isEmbedded = renderMode === 'sidebar' || renderMode === 'inline';
const useDesktopSidePanel = !isEmbedded && Platform.OS === 'web' && windowWidth >= 1200;
const modalWidth = Math.min(Math.max(windowWidth * 0.88, 320), useDesktopSidePanel ? 420 : 500);
const modalHeight = Math.min(Math.max(windowHeight * 0.78, 520), 820);
const dimensions = { width: modalWidth, height: modalHeight };
const [newPoll, setNewPoll] = useState({
question: '',
type: '',
options: [],
});
useEffect(() => {
if (!isPollModalVisible) {
return;
}
let activePollCount = 0;
polls.forEach((polled) => {
if (polled.status === 'active' && poll && polled.id === poll.id) {
activePollCount += 1;
}
});
if (islevel === '2' && activePollCount === 0 && poll && poll.status === 'active') {
poll.status = 'inactive';
}
}, [isPollModalVisible, islevel, poll, polls]);
if (!isEmbedded && !isPollModalVisible) {
return null;
}
const calculatePercentage = (votes, optionIndex) => {
const totalVotes = votes.reduce((sum, value) => sum + value, 0);
return totalVotes > 0 ? ((votes[optionIndex] / totalVotes) * 100).toFixed(2) : '0.00';
};
const handlePollTypeChange = (type) => {
let options = [];
switch (type) {
case 'trueFalse':
options = ['True', 'False'];
break;
case 'yesNo':
options = ['Yes', 'No'];
break;
case 'custom':
default:
options = [];
break;
}
setNewPoll({ ...newPoll, type, options });
};
const renderCreateOptions = () => {
if (newPoll.type === 'trueFalse' || newPoll.type === 'yesNo') {
return newPoll.options.map((option, index) => (<View key={`${option}-${index}`} style={[styles.staticOptionRow, { borderColor: colors.border, backgroundColor: colors.surfaceMuted }]}>
<View style={[styles.optionBullet, { backgroundColor: colors.accent }]}/>
<Text style={[styles.optionTitle, { color: colors.text }]}>{option}</Text>
</View>));
}
if (newPoll.type === 'custom') {
return [...Array(5)].map((_, index) => {
const value = newPoll.options[index] ?? '';
return (<TextInput key={`custom-option-${index}`} style={[
styles.input,
{
color: modalTheme.inputTextColor,
borderColor: modalTheme.borderColor,
backgroundColor: modalTheme.inputBackgroundColor,
},
]} placeholder={`Option ${index + 1}`} placeholderTextColor={modalTheme.placeholderTextColor} maxLength={50} value={value} onChangeText={(text) => {
const nextOptions = [...newPoll.options];
nextOptions[index] = text;
setNewPoll({ ...newPoll, options: nextOptions });
}}/>);
});
}
return null;
};
const renderCurrentPollOptions = () => poll?.options.map((option, index) => {
const selected = !!poll.voters && poll.voters[member] === index;
return (<Pressable key={`${option}-${index}`} accessibilityRole="button" accessibilityLabel={`Vote for ${option}`} onPress={() => void handleVotePoll({
pollId: poll.id,
optionIndex: index,
socket,
showAlert,
member,
roomName,
updateIsPollModalVisible,
})} style={[
styles.voteOption,
{
borderColor: selected ? colors.borderStrong : colors.border,
backgroundColor: selected ? colors.accentSoft : colors.surfaceStrong,
},
]}>
<View style={[styles.radioOuter, { borderColor: selected ? colors.accent : colors.border }]}>
{selected ? <View style={[styles.radioInner, { backgroundColor: colors.accent }]}/> : null}
</View>
<View style={styles.voteOptionCopy}>
<Text style={[styles.optionTitle, { color: colors.text }]}>{option}</Text>
</View>
</Pressable>);
});
const contentBody = (<View style={styles.contentShell}>
<View style={[styles.header, { borderBottomColor: colors.border }]}>
<View style={styles.titleRow}>
<View style={[styles.titleIcon, { backgroundColor: colors.accentSoft }]}>
<FontAwesome5 name="poll" size={15} color={colors.accent}/>
</View>
<View style={styles.titleCopy}>
<Text style={[styles.titleText, { color: colors.text }]}>Polls</Text>
<Text style={[styles.subtitleText, { color: colors.textMuted }]}>Create polls, capture votes, and review results</Text>
</View>
</View>
<Pressable accessibilityRole="button" accessibilityLabel="Close polls" onPress={onClose} style={({ pressed }) => [
styles.iconButton,
{
borderColor: colors.border,
backgroundColor: pressed ? colors.accentSoft : colors.surfaceStrong,
},
]}>
<FontAwesome5 name="times" size={14} color={colors.textMuted}/>
</Pressable>
</View>
<ScrollView contentContainerStyle={styles.bodyContent} showsVerticalScrollIndicator={false} style={styles.body}>
{islevel === '2' ? (<View style={[styles.panel, { backgroundColor: colors.surfaceStrong, borderColor: colors.border }]}>
<Text style={[styles.panelEyebrow, { color: colors.textMuted }]}>Archive</Text>
<Text style={[styles.panelTitle, { color: colors.text }]}>Previous polls</Text>
{polls.length === 0 ? (<Text style={[styles.emptyStateText, { color: colors.textMuted }]}>No polls available yet.</Text>) : (polls.map((polled, index) => {
if (poll && poll.status === 'active' && polled.id === poll.id) {
return null;
}
return (<View key={polled.id ?? index} style={[styles.pollCard, { borderColor: colors.border, backgroundColor: colors.surfaceMuted }]}>
<Text style={[styles.pollQuestion, { color: colors.text }]}>{polled.question}</Text>
{polled.options.map((option, optionIndex) => (<Text key={`${polled.id}-${optionIndex}`} style={[styles.optionMeta, { color: colors.textMuted }]}>
{`${option}: ${polled.votes[optionIndex]} votes (${calculatePercentage(polled.votes, optionIndex)}%)`}
</Text>))}
{polled.status === 'active' ? (<Pressable accessibilityRole="button" accessibilityLabel={`End poll ${polled.question}`} onPress={() => void handleEndPoll({
pollId: polled.id,
socket,
showAlert,
roomName,
updateIsPollModalVisible,
})} style={({ pressed }) => [
styles.ghostButton,
{
borderColor: colors.danger,
backgroundColor: pressed ? 'rgba(239, 68, 68, 0.18)' : 'rgba(239, 68, 68, 0.08)',
},
]}>
<Text style={[styles.ghostButtonText, { color: colors.danger }]}>End Poll</Text>
</Pressable>) : null}
</View>);
}))}
</View>) : null}
{islevel === '2' ? (<View style={[styles.panel, { backgroundColor: colors.surfaceStrong, borderColor: colors.border }]}>
<Text style={[styles.panelEyebrow, { color: colors.textMuted }]}>Composer</Text>
<Text style={[styles.panelTitle, { color: colors.text }]}>Create a poll</Text>
<TextInput style={[
styles.textArea,
{
color: modalTheme.inputTextColor,
borderColor: modalTheme.borderColor,
backgroundColor: modalTheme.inputBackgroundColor,
},
]} multiline maxLength={300} value={newPoll.question} onChangeText={(text) => setNewPoll({ ...newPoll, question: text })} placeholder="Ask a question for the room" placeholderTextColor={modalTheme.placeholderTextColor}/>
<RNPickerSelect onValueChange={handlePollTypeChange} items={[
{ label: 'True / False', value: 'trueFalse' },
{ label: 'Yes / No', value: 'yesNo' },
{ label: 'Custom', value: 'custom' },
]} placeholder={{ label: 'Choose answer type', value: '' }} style={pickerTheme} value={newPoll.type} useNativeAndroidPickerStyle={false}/>
<View style={styles.createOptions}>{renderCreateOptions()}</View>
<Pressable accessibilityRole="button" accessibilityLabel="Create poll" onPress={() => void handleCreatePoll({
poll: newPoll,
socket,
roomName,
showAlert,
updateIsPollModalVisible,
})} style={({ pressed }) => [
styles.primaryButton,
{
backgroundColor: pressed ? colors.accentAlt : colors.accent,
borderColor: pressed ? colors.accentAlt : colors.accent,
},
]}>
<Text style={[styles.primaryButtonText, { color: colors.invertedText }]}>Create Poll</Text>
</Pressable>
</View>) : null}
<View style={[styles.panel, { backgroundColor: colors.surfaceStrong, borderColor: colors.border }]}>
<View style={styles.currentPollHeader}>
<View>
<Text style={[styles.panelEyebrow, { color: colors.textMuted }]}>Live</Text>
<Text style={[styles.panelTitle, { color: colors.text }]}>Current poll</Text>
</View>
{poll?.status === 'active' ? (<View style={[styles.badge, { backgroundColor: colors.accentSoft }]}>
<Text style={[styles.badgeText, { color: colors.accent }]}>Active</Text>
</View>) : null}
</View>
{poll && poll.status === 'active' ? (<>
<View style={[styles.questionCard, { backgroundColor: colors.surfaceMuted, borderColor: colors.border }]}>
<Text style={[styles.pollQuestion, { color: colors.text }]}>{poll.question}</Text>
</View>
<View style={styles.voteOptions}>{renderCurrentPollOptions()}</View>
{islevel === '2' ? (<Pressable accessibilityRole="button" accessibilityLabel="End current poll" onPress={() => void handleEndPoll({
pollId: poll.id,
socket,
showAlert,
roomName,
updateIsPollModalVisible,
})} style={({ pressed }) => [
styles.ghostButton,
{
borderColor: colors.danger,
backgroundColor: pressed ? 'rgba(239, 68, 68, 0.18)' : 'rgba(239, 68, 68, 0.08)',
},
]}>
<Text style={[styles.ghostButtonText, { color: colors.danger }]}>End Poll</Text>
</Pressable>) : null}
</>) : (<Text style={[styles.emptyStateText, { color: colors.textMuted }]}>No active poll.</Text>)}
</View>
</ScrollView>
</View>);
const content = renderContent ? renderContent({ defaultContent: contentBody, dimensions }) : contentBody;
if (isEmbedded) {
return (<View style={styles.embeddedContainer}>
<View style={[styles.embeddedShell, { backgroundColor: backgroundColor ?? colors.surface }, style]}>{content}</View>
</View>);
}
const overlayStyle = [
styles.modalContainer,
useDesktopSidePanel ? styles.desktopSidebarContainer : getModalPosition({ position }),
{ backgroundColor: useDesktopSidePanel ? 'rgba(15, 23, 42, 0.14)' : 'rgba(15, 23, 42, 0.28)' },
];
const shellStyle = [
styles.modalShell,
getModernModalCardStyle(darkMode),
useDesktopSidePanel ? getModernSidePanelStyle(darkMode) : { width: modalWidth, height: modalHeight },
{ backgroundColor: backgroundColor ?? colors.surface },
style,
];
const defaultContainer = (<Modal animationType="fade" transparent visible={isPollModalVisible} onRequestClose={onClose}>
<Pressable onPress={onClose} style={overlayStyle}>
<Pressable onPress={() => undefined} style={shellStyle}>
{content}
</Pressable>
</Pressable>
</Modal>);
return renderContainer
? renderContainer({ defaultContainer, dimensions })
: defaultContainer;
};
const styles = StyleSheet.create({
modalContainer: {
flex: 1,
padding: 12,
},
desktopSidebarContainer: {
justifyContent: 'flex-start',
alignItems: 'flex-end',
padding: 0,
},
modalShell: {
overflow: 'hidden',
},
embeddedContainer: {
flex: 1,
minHeight: 0,
width: '100%',
},
embeddedShell: {
flex: 1,
minHeight: 0,
width: '100%',
},
contentShell: {
flex: 1,
minHeight: 0,
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 18,
paddingVertical: 16,
borderBottomWidth: 1,
},
titleRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
flexShrink: 1,
},
titleCopy: {
flexShrink: 1,
},
titleIcon: {
width: 34,
height: 34,
borderRadius: 17,
alignItems: 'center',
justifyContent: 'center',
},
titleText: {
fontSize: 18,
fontWeight: '800',
},
subtitleText: {
fontSize: 12,
lineHeight: 18,
marginTop: 2,
},
iconButton: {
width: 34,
height: 34,
borderRadius: 17,
borderWidth: 1,
alignItems: 'center',
justifyContent: 'center',
},
body: {
flex: 1,
},
bodyContent: {
padding: 18,
gap: 14,
},
panel: {
borderWidth: 1,
borderRadius: 18,
padding: 16,
},
panelEyebrow: {
fontSize: 11,
fontWeight: '700',
letterSpacing: 0.6,
textTransform: 'uppercase',
marginBottom: 4,
},
panelTitle: {
fontSize: 16,
fontWeight: '800',
},
emptyStateText: {
fontSize: 13,
lineHeight: 19,
marginTop: 10,
},
pollCard: {
borderWidth: 1,
borderRadius: 14,
padding: 14,
gap: 8,
marginTop: 12,
},
pollQuestion: {
fontSize: 14,
fontWeight: '800',
lineHeight: 20,
},
optionMeta: {
fontSize: 12,
lineHeight: 18,
},
textArea: {
minHeight: 100,
borderWidth: 1,
borderRadius: 14,
paddingHorizontal: 12,
paddingVertical: 12,
textAlignVertical: 'top',
marginTop: 12,
marginBottom: 12,
},
input: {
minHeight: 44,
borderWidth: 1,
borderRadius: 12,
paddingHorizontal: 12,
marginTop: 10,
},
createOptions: {
marginTop: 4,
},
staticOptionRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 10,
borderWidth: 1,
borderRadius: 12,
paddingHorizontal: 12,
paddingVertical: 12,
marginTop: 10,
},
optionBullet: {
width: 8,
height: 8,
borderRadius: 4,
},
optionTitle: {
fontSize: 14,
fontWeight: '700',
},
primaryButton: {
minHeight: 46,
borderRadius: 14,
borderWidth: 1,
alignItems: 'center',
justifyContent: 'center',
marginTop: 14,
},
primaryButtonText: {
fontSize: 14,
fontWeight: '800',
},
currentPollHeader: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
gap: 12,
},
badge: {
paddingHorizontal: 10,
paddingVertical: 6,
borderRadius: 999,
},
badgeText: {
fontSize: 11,
fontWeight: '800',
},
questionCard: {
borderWidth: 1,
borderRadius: 14,
padding: 14,
marginTop: 12,
},
voteOptions: {
marginTop: 12,
gap: 10,
},
voteOption: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
borderWidth: 1,
borderRadius: 14,
paddingHorizontal: 14,
paddingVertical: 12,
},
radioOuter: {
width: 20,
height: 20,
borderRadius: 10,
borderWidth: 1,
alignItems: 'center',
justifyContent: 'center',
},
radioInner: {
width: 10,
height: 10,
borderRadius: 5,
},
voteOptionCopy: {
flex: 1,
gap: 2,
},
ghostButton: {
minHeight: 42,
borderRadius: 12,
borderWidth: 1,
alignItems: 'center',
justifyContent: 'center',
marginTop: 12,
},
ghostButtonText: {
fontSize: 13,
fontWeight: '800',
},
});
export default ModernPollModal;
//# sourceMappingURL=ModernPollModal.js.map