import { Image } from 'expo-image'; import { LinearGradient } from 'expo-linear-gradient'; import { User } from 'lucide-react-native'; import { useEffect, useMemo, useState } from 'react'; import { Pressable, StyleSheet, Text, View, type ImageSourcePropType, type StyleProp, type ViewStyle, } from 'react-native'; import { colors, gradientDirection, gradients } from '@/theme/colors'; import { fontFamily } from '@/theme/typography'; export type AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'; export const AVATAR_SIZES: Record = { xs: 20, sm: 26, md: 34, lg: 48, xl: 72, }; export type AvatarProps = { source?: ImageSourcePropType | string | null; name?: string; size?: AvatarSize; shape?: 'circle' | 'rounded'; ring?: 'none' | 'border' | 'gradient'; ringColor?: string; badge?: 'none' | 'online' | 'live'; onPress?: () => void; style?: StyleProp; }; function getInitials(name?: string): string { if (!name) { return ''; } const words = name .trim() .split(/\s+/) .filter(Boolean) .slice(0, 2); if (words.length === 0) { return ''; } return words .map((word) => word.charAt(0)) .join('') .toUpperCase(); } export function Avatar({ source, name, size = 'md', shape = 'circle', ring = 'none', ringColor, badge = 'none', onPress, style, }: AvatarProps) { const [hasImageError, setHasImageError] = useState(false); const diameter = AVATAR_SIZES[size]; const radius = shape === 'rounded' ? Math.round(diameter * 0.3) : diameter / 2; const innerRadius = ring === 'gradient' ? Math.max(0, radius - 2) : radius; const fallbackInitials = useMemo(() => getInitials(name), [name]); const normalizedSource = typeof source === 'string' ? { uri: source } : (source ?? null); useEffect(() => { setHasImageError(false); }, [source]); const shouldShowImage = normalizedSource !== null && !hasImageError; const badgeSize = Math.max(8, Math.round(diameter * 0.26)); const fallbackTextSize = Math.round(diameter * 0.38); const iconSize = Math.round(diameter * 0.5); const minTouchSize = 44; const hitSlopPadding = diameter < minTouchSize ? Math.ceil((minTouchSize - diameter) / 2) : 0; const frameStyle: ViewStyle = { width: diameter, height: diameter, borderRadius: radius, }; const innerStyle: ViewStyle = { flex: 1, borderRadius: innerRadius, overflow: 'hidden', backgroundColor: colors.surfaceElevated, alignItems: 'center', justifyContent: 'center', }; const borderStyle: ViewStyle = ring === 'border' ? { borderWidth: 2, borderColor: ringColor ?? colors.border, } : {}; const badgeColor = badge === 'online' ? colors.success : colors.primary; const innerContentNode = shouldShowImage ? ( setHasImageError(true)} /> ) : fallbackInitials ? ( {fallbackInitials} ) : ( ); const avatarNode = ring === 'gradient' ? ( {innerContentNode} {badge !== 'none' ? ( ) : null} ) : ( {innerContentNode} {badge !== 'none' ? ( ) : null} ); if (!onPress) { return ( {avatarNode} ); } return ( 0 ? { top: hitSlopPadding, right: hitSlopPadding, bottom: hitSlopPadding, left: hitSlopPadding, } : undefined } style={style} > {avatarNode} ); } const styles = StyleSheet.create({ media: { width: '100%', height: '100%', }, gradientFrame: { padding: 2, }, frame: { backgroundColor: colors.surfaceElevated, }, badge: { position: 'absolute', right: 0, bottom: 0, borderWidth: 2, borderColor: colors.surface, }, }); export default Avatar;