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

289 lines (288 loc) 10.8 kB
// MiniCard.tsx import React, { useCallback, useMemo, useState } from 'react'; import { View, Text, StyleSheet, Image, } from 'react-native'; import { FontAwesome } from '@expo/vector-icons'; /** * MiniCard - Compact participant card displaying avatar or initials with status icons * * MiniCard is a lightweight React Native component designed for displaying participants * in compact grid layouts, sidebars, or minimized views. It intelligently renders either * a participant's avatar image or their initials, with optional status icons for audio/video. * * **Key Features:** * - Avatar image display with automatic fallback to initials * - Optional audio/video status icon overlays * - Rounded or square image corners * - Compact design optimized for grid layouts * - Fully customizable styling * - Lightweight performance footprint * * **UI Customization - Two-Tier Override System:** * * 1. **Custom Render Function** (via `customMiniCard` prop): * Pass a function that receives all MiniCardOptions and returns custom JSX. * Provides complete control over rendering logic and appearance. * * 2. **Component Override** (via `uiOverrides.miniCardComponent`): * Replace the entire MiniCard component while preserving MediaSFU's orchestration. * Useful when you want a different component implementation. * * **Advanced Render Overrides:** * - `renderContent`: Wrap/modify the card's inner content while keeping container * - `renderContainer`: Wrap/modify the entire card container * * @component * @param {MiniCardOptions} props - Configuration options for the MiniCard component * * @returns {JSX.Element} Rendered mini card with avatar/initials and optional status icons * * @example * // Basic usage - Display with initials * import React from 'react'; * import { MiniCard } from 'mediasfu-reactnative-expo'; * * function ParticipantGrid() { * return ( * <MiniCard * initials="AB" * name="Alice Brown" * fontSize={14} * roundedImage={true} * showVideoIcon={false} * showAudioIcon={true} * /> * ); * } * * @example * // With avatar image and custom styling * <MiniCard * name="Charlie Davis" * imageSource="https://example.com/avatars/charlie.jpg" * roundedImage={true} * showVideoIcon={true} * showAudioIcon={false} * customStyle={{ * backgroundColor: '#1a1a2e', * borderRadius: 10, * borderWidth: 2, * borderColor: '#16213e', * padding: 5, * }} * imageStyle={{ width: 50, height: 50 }} * fontSize={16} * /> * * @example * // Custom mini card with custom render function * import { View, Text } from 'react-native'; * * const customMiniCard = (options: MiniCardOptions) => { * const { name, initials, showVideoIcon, showAudioIcon } = options; * * return ( * <View style={{ backgroundColor: '#000', padding: 8, borderRadius: 8 }}> * <Text style={{ color: '#fff', fontSize: 12, fontWeight: 'bold' }}> * {initials || name?.substring(0, 2).toUpperCase()} * </Text> * <View style={{ flexDirection: 'row', marginTop: 4 }}> * {showVideoIcon && <Text style={{ marginRight: 4 }}>📹</Text>} * {showAudioIcon && <Text>🔊</Text>} * </View> * </View> * ); * }; * * <MiniCard * name="Eve Foster" * initials="EF" * showVideoIcon={true} * showAudioIcon={false} * customMiniCard={customMiniCard} * /> * * @example * // Using uiOverrides for component-level customization * import { MyCustomMiniCard } from './MyCustomMiniCard'; * * const sessionConfig = { * credentials: { apiKey: 'your-api-key' }, * uiOverrides: { * miniCardComponent: { * component: MyCustomMiniCard, * injectedProps: { * theme: 'minimal', * size: 'small', * }, * }, * }, * }; * * // MyCustomMiniCard.tsx * export const MyCustomMiniCard = (props: MiniCardOptions & { theme: string; size: string }) => { * const cardSize = props.size === 'small' ? 40 : 60; * * return ( * <View style={{ width: cardSize, height: cardSize, borderRadius: cardSize / 2 }}> * {props.imageSource ? ( * <Image source={{ uri: props.imageSource }} style={{ width: cardSize, height: cardSize }} /> * ) : ( * <Text style={{ fontSize: props.fontSize }}>{props.initials}</Text> * )} * </View> * ); * }; */ const MiniCard = ({ initials, fontSize = 14, textColor, customStyle, imageSource, roundedImage = true, imageStyle, showVideoIcon = false, showAudioIcon = false, name, customMiniCard, parameters, style, renderContent, renderContainer, }) => { const [dimensions, setDimensions] = useState({ width: 0, height: 0 }); const isDarkMode = typeof parameters?.isDarkModeValue === 'boolean' ? parameters.isDarkModeValue : true; // Define the style for the MiniCard const cardStyle = useMemo(() => [styles.miniCard, customStyle, style], [customStyle, style]); const resolvedInitials = useMemo(() => { const rawAvatarLabel = initials?.trim() || name?.trim() || ''; return rawAvatarLabel ? rawAvatarLabel.slice(0, 10) : '?'; }, [initials, name]); const resolvedFontSize = useMemo(() => { if (!dimensions.width || !dimensions.height) { return fontSize; } const labelLength = Math.max(resolvedInitials.length, 1); const availableWidth = Math.max(dimensions.width - 12, 1); const availableHeight = Math.max(dimensions.height - 12, 1); const widthFit = availableWidth / Math.max(labelLength * 0.58, 1); const heightFit = availableHeight * 0.55; return Math.max(8, Math.min(fontSize, Math.floor(widthFit), Math.floor(heightFit))); }, [dimensions.height, dimensions.width, fontSize, resolvedInitials]); const resolvedTextColor = useMemo(() => textColor ?? (isDarkMode ? '#f8fafc' : '#0f172a'), [isDarkMode, textColor]); const resolvedBadgeBackgroundColor = useMemo(() => (isDarkMode ? 'rgba(0, 0, 0, 0.35)' : 'rgba(255, 255, 255, 0.8)'), [isDarkMode]); const resolvedBadgeIconColor = useMemo(() => (isDarkMode ? '#ffffff' : '#0f172a'), [isDarkMode]); const badgeIconSize = useMemo(() => { if (!dimensions.width || !dimensions.height) { return 14; } return Math.max(10, Math.min(14, Math.floor(Math.min(dimensions.width, dimensions.height) * 0.16))); }, [dimensions.height, dimensions.width]); const handleLayout = useCallback((event) => { const { width, height } = event.nativeEvent.layout; setDimensions((current) => { if (current.width === width && current.height === height) { return current; } return { width, height }; }); }, []); const renderDimensions = { width: dimensions.width, height: dimensions.height }; const defaultContent = (<> {customMiniCard ? (customMiniCard({ initials: resolvedInitials, fontSize: resolvedFontSize, textColor: resolvedTextColor, customStyle, name: name || resolvedInitials, showVideoIcon, showAudioIcon, imageSource, roundedImage, imageStyle, parameters, })) : (<View style={styles.defaultContent}> {imageSource ? (<View style={styles.imageContainer}> <Image source={{ uri: imageSource }} style={[ styles.backgroundImage, roundedImage && styles.roundedImage, imageStyle, ]} resizeMode="cover"/> </View>) : (<Text style={[ styles.initials, isDarkMode ? styles.initialsDarkMode : styles.initialsLightMode, { fontSize: resolvedFontSize, color: resolvedTextColor }, ]} numberOfLines={1} adjustsFontSizeToFit minimumFontScale={0.5}> {resolvedInitials} </Text>)} {(showVideoIcon || showAudioIcon) && (<View style={[styles.badgeContainer, { backgroundColor: resolvedBadgeBackgroundColor }]}> {showVideoIcon && (<FontAwesome name="video-camera" size={badgeIconSize} color={resolvedBadgeIconColor} style={[styles.badgeIcon, styles.badgeIconFirst]}/>)} {showAudioIcon && (<FontAwesome name="microphone" size={badgeIconSize} color={resolvedBadgeIconColor} style={[styles.badgeIcon, showVideoIcon ? null : styles.badgeIconFirst]}/>)} </View>)} </View>)} </>); const content = renderContent ? renderContent({ defaultContent, dimensions: renderDimensions }) : defaultContent; const defaultContainer = (<View style={cardStyle} onLayout={handleLayout} accessibilityRole="image" accessibilityLabel={name || resolvedInitials}> {content} </View>); return renderContainer ? renderContainer({ defaultContainer, dimensions: renderDimensions }) : defaultContainer; }; export default MiniCard; /** * Stylesheet for the MiniCard component. */ const styles = StyleSheet.create({ miniCard: { display: 'flex', justifyContent: 'center', alignItems: 'center', borderRadius: 0, // Default border radius; can be overridden via customStyle width: '100%', height: '100%', color: 'white', fontFamily: 'Nunito', overflow: 'hidden', position: 'relative', }, defaultContent: { flex: 1, alignItems: 'center', justifyContent: 'center', width: '100%', height: '100%', }, imageContainer: { justifyContent: 'center', alignItems: 'center', width: '100%', height: '100%', }, backgroundImage: { width: '60%', height: '60%', }, roundedImage: { borderRadius: 50, // Fully rounded; adjust as needed }, initials: { textAlign: 'center', fontWeight: '800', maxWidth: '92%', }, initialsDarkMode: { textShadowColor: 'rgba(15, 23, 42, 0.55)', textShadowOffset: { width: 0, height: 1 }, textShadowRadius: 2, }, initialsLightMode: { textShadowColor: 'rgba(248, 250, 252, 0.85)', textShadowOffset: { width: 0, height: 1 }, textShadowRadius: 1, }, badgeContainer: { position: 'absolute', bottom: 6, right: 6, flexDirection: 'row', paddingHorizontal: 6, paddingVertical: 4, borderRadius: 12, }, badgeIcon: { marginLeft: 6, }, badgeIconFirst: { marginLeft: 0, }, }); //# sourceMappingURL=MiniCard.js.map