UNPKG

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

373 lines (371 loc) 13.7 kB
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { Modal, Platform, Pressable, ScrollView, StyleSheet, Text, TextInput, View, useWindowDimensions, } from 'react-native'; import { FontAwesome } from '@expo/vector-icons'; import { respondToRequests } from '../../methods/requestsMethods/respondToRequests'; import { getModalPosition } from '../../methods/utils/getModalPosition'; import { getModernColors, getModernModalCardStyle, getModernSidePanelStyle, resolveIsDarkMode, } from '../core/modernTheme'; const requestIconMap = { 'fa-microphone': 'microphone', 'fa-desktop': 'desktop', 'fa-video': 'video-camera', 'fa-comments': 'comments', }; export const ModernRequestsModal = ({ isRequestsModalVisible, onRequestClose, requestCounter, onRequestFilterChange, onRequestItemPress = respondToRequests, requestList, updateRequestList, roomName, socket, renderRequestComponent, backgroundColor, isDarkMode, position = 'topRight', parameters, style, renderContent, renderContainer, renderMode = 'modal', }) => { const { width: windowWidth, height: windowHeight } = useWindowDimensions(); const darkMode = resolveIsDarkMode({ isDarkMode, backgroundColor }); const colors = getModernColors(darkMode); const isEmbedded = renderMode === 'sidebar' || renderMode === 'inline'; const useDesktopSidePanel = !isEmbedded && Platform.OS === 'web' && windowWidth >= 1200; const modalWidth = Math.min(Math.max(windowWidth * 0.84, 320), useDesktopSidePanel ? 420 : 460); const modalHeight = Math.min(Math.max(windowHeight * 0.68, 420), 680); const dimensions = { width: modalWidth, height: modalHeight }; const [searchText, setSearchText] = useState(''); const [requestListState, setRequestListState] = useState(requestList); useEffect(() => { const updatedParams = parameters?.getUpdatedAllParams?.(); const nextList = updatedParams?.filteredRequestList ?? requestList; setRequestListState(nextList); }, [parameters, requestList, isRequestsModalVisible]); const filteredRequests = useMemo(() => { if (!searchText.trim()) { return requestListState; } const normalizedSearch = searchText.trim().toLowerCase(); return requestListState.filter((request) => { const haystack = `${request.name ?? ''} ${request.username ?? ''} ${request.icon ?? ''}`.toLowerCase(); return haystack.includes(normalizedSearch); }); }, [requestListState, searchText]); const handleFilterChange = useCallback((value) => { setSearchText(value); onRequestFilterChange(value); }, [onRequestFilterChange]); const handleRequestAction = useCallback(async (request, action) => { await onRequestItemPress({ request, updateRequestList, requestList: requestListState, action, roomName, socket, }); }, [onRequestItemPress, requestListState, roomName, socket, updateRequestList]); if (!isEmbedded && !isRequestsModalVisible) { return null; } const defaultRequestRow = (request, index) => { if (renderRequestComponent) { return renderRequestComponent({ request, onRequestItemPress, requestList: requestListState, updateRequestList, roomName, socket, isDarkMode: darkMode, }); } return (<View key={`${request.id ?? request.name ?? index}`} style={[ styles.requestRow, { backgroundColor: colors.surfaceStrong, borderColor: colors.border, }, ]}> <View style={styles.requestCopy}> <View style={[styles.requestIconBadge, { backgroundColor: colors.accentSoft }]}> <FontAwesome name={requestIconMap[request.icon] ?? 'bell'} size={15} color={colors.accent}/> </View> <View style={styles.requestCopyText}> <Text style={[styles.requestTitle, { color: colors.text }]}>{request.name}</Text> <Text style={[styles.requestMeta, { color: colors.textMuted }]}> {request.username ? `From ${request.username}` : 'Pending approval'} </Text> </View> </View> <View style={styles.requestActions}> <Pressable accessibilityRole="button" accessibilityLabel={`Approve request from ${request.username ?? request.name}`} onPress={() => void handleRequestAction(request, 'accepted')} style={({ pressed }) => [ styles.actionButton, { backgroundColor: pressed ? colors.accentSoft : darkMode ? 'rgba(22, 163, 74, 0.16)' : 'rgba(22, 163, 74, 0.08)', borderColor: darkMode ? 'rgba(74, 222, 128, 0.2)' : 'rgba(22, 163, 74, 0.12)', }, ]}> <FontAwesome name="check" size={16} color={colors.success}/> </Pressable> <Pressable accessibilityRole="button" accessibilityLabel={`Reject request from ${request.username ?? request.name}`} onPress={() => void handleRequestAction(request, 'rejected')} style={({ pressed }) => [ styles.actionButton, { backgroundColor: pressed ? colors.accentSoft : darkMode ? 'rgba(239, 68, 68, 0.16)' : 'rgba(239, 68, 68, 0.08)', borderColor: darkMode ? 'rgba(248, 113, 113, 0.2)' : 'rgba(220, 38, 38, 0.12)', }, ]}> <FontAwesome name="times" size={16} color={colors.danger}/> </Pressable> </View> </View>); }; const contentBody = (<View style={styles.contentShell}> <View style={[styles.header, { borderBottomColor: colors.border }]}> <View style={styles.titleRow}> <View style={[styles.titleIcon, { backgroundColor: colors.accentSoft }]}> <FontAwesome name="hand-paper-o" size={16} color={colors.accent}/> </View> <View> <Text style={[styles.titleText, { color: colors.text }]}>Requests</Text> <Text style={[styles.subtitleText, { color: colors.textMuted }]}>Host approvals for raised actions</Text> </View> </View> <View style={styles.headerActions}> <View style={[styles.counterBadge, { backgroundColor: colors.accentSoft }]}> <Text style={[styles.counterBadgeText, { color: colors.accent }]}>{filteredRequests.length || requestCounter}</Text> </View> <Pressable accessibilityRole="button" accessibilityLabel="Close requests" onPress={onRequestClose} style={({ pressed }) => [ styles.iconButton, { borderColor: colors.border, backgroundColor: pressed ? colors.accentSoft : colors.surfaceStrong, }, ]}> <FontAwesome name="times" size={14} color={colors.textMuted}/> </Pressable> </View> </View> <View style={styles.body}> <View style={[styles.searchShell, { backgroundColor: colors.surfaceStrong, borderColor: colors.border }]}> <FontAwesome name="search" size={13} color={colors.textMuted}/> <TextInput onChangeText={handleFilterChange} placeholder="Search requests" placeholderTextColor={colors.textMuted} style={[styles.searchInput, { color: colors.text }]} value={searchText}/> </View> <View style={[styles.listPanel, { backgroundColor: colors.surfaceStrong, borderColor: colors.border }]}> {filteredRequests.length === 0 ? (<View style={styles.emptyState}> <View style={[styles.emptyStateIcon, { backgroundColor: colors.accentSoft }]}> <FontAwesome name="inbox" size={18} color={colors.accent}/> </View> <Text style={[styles.emptyStateTitle, { color: colors.text }]}>No pending requests</Text> <Text style={[styles.emptyStateBody, { color: colors.textMuted }]}>Incoming requests for mic, camera, chat, or screen share will appear here.</Text> </View>) : (<ScrollView contentContainerStyle={styles.listContent} showsVerticalScrollIndicator={false}> {filteredRequests.map((request, index) => defaultRequestRow(request, index))} </ScrollView>)} </View> </View> </View>); const content = renderContent ? renderContent({ defaultContent: contentBody, dimensions }) : contentBody; if (isEmbedded) { return (<View style={styles.embeddedContainer}> <View style={[styles.embeddedShell, { 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={isRequestsModalVisible} onRequestClose={onRequestClose}> <Pressable onPress={onRequestClose} 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', borderBottomWidth: 1, paddingHorizontal: 20, paddingVertical: 16, }, titleRow: { flexDirection: 'row', alignItems: 'center', flex: 1, marginRight: 12, }, titleIcon: { width: 34, height: 34, borderRadius: 17, alignItems: 'center', justifyContent: 'center', marginRight: 12, }, titleText: { fontSize: 18, fontWeight: '700', }, subtitleText: { fontSize: 12, marginTop: 2, }, headerActions: { flexDirection: 'row', alignItems: 'center', }, counterBadge: { minWidth: 30, height: 30, borderRadius: 15, alignItems: 'center', justifyContent: 'center', marginRight: 10, paddingHorizontal: 8, }, counterBadgeText: { fontSize: 12, fontWeight: '800', }, iconButton: { width: 38, height: 38, borderRadius: 19, borderWidth: 1, alignItems: 'center', justifyContent: 'center', }, body: { flex: 1, minHeight: 0, paddingHorizontal: 20, paddingTop: 16, paddingBottom: 12, }, searchShell: { flexDirection: 'row', alignItems: 'center', borderWidth: 1, borderRadius: 18, paddingHorizontal: 14, paddingVertical: 12, marginBottom: 14, }, searchInput: { flex: 1, fontSize: 14, marginLeft: 10, paddingVertical: 0, }, listPanel: { flex: 1, minHeight: 0, borderWidth: 1, borderRadius: 22, overflow: 'hidden', }, listContent: { padding: 16, }, requestRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', borderWidth: 1, borderRadius: 18, paddingHorizontal: 14, paddingVertical: 12, marginBottom: 12, }, requestCopy: { flexDirection: 'row', alignItems: 'center', flex: 1, marginRight: 12, }, requestIconBadge: { width: 38, height: 38, borderRadius: 19, alignItems: 'center', justifyContent: 'center', marginRight: 12, }, requestCopyText: { flex: 1, }, requestTitle: { fontSize: 14, fontWeight: '700', lineHeight: 20, }, requestMeta: { fontSize: 12, marginTop: 4, }, requestActions: { flexDirection: 'row', alignItems: 'center', }, actionButton: { width: 38, height: 38, borderRadius: 19, borderWidth: 1, alignItems: 'center', justifyContent: 'center', marginLeft: 8, }, emptyState: { flex: 1, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 28, }, emptyStateIcon: { width: 48, height: 48, borderRadius: 24, alignItems: 'center', justifyContent: 'center', marginBottom: 14, }, emptyStateTitle: { fontSize: 15, fontWeight: '700', textAlign: 'center', }, emptyStateBody: { fontSize: 12, lineHeight: 18, textAlign: 'center', marginTop: 6, }, }); export default ModernRequestsModal; //# sourceMappingURL=ModernRequestsModal.js.map