import { useEffect, useMemo, useState, type ComponentType } from 'react' import { Image, View, type ImageProps, type StyleProp, type ViewProps, type ViewStyle } from 'react-native' import { HightideIconRegistry } from '../../icons/HightideIconRegistry' import { ThemedIcon } from './ThemedIcon' import { ThemedText } from './ThemedText' import { avatarSizes, type AvatarSize as AvatarSizeToken } from '@helpwave/hightide-design/component-token-resolvers' import type { ColorPairToken } from '@helpwave/hightide-design/theme-tokens' import { useTheme } from '../../global-contexts/theme/ThemeContext' import type { AvatarGroupContainerStyle, AvatarGroupStackStyle, AvatarGroupTextStyle, AvatarImageStyle, AvatarIconStyle, AvatarState, AvatarStatus, AvatarStyle, AvatarTextStyle, AvatarStatusDotStyle, AvatarWithStatusState } from '../../theme/types/components/avatar' import { avatarStatuses } from '../../theme/types/components/avatar' import type { StyleOverwrite } from '../../theme/types/resolver' export type AvatarSize = AvatarSizeToken | number type ImageConfig = { avatarUrl: string, alt: string, } export type AvatarImageProps = Omit & { source: { uri: string }, alt?: string, } const DefaultAvatarImage: ComponentType = ({ alt, ...props }) => ( ) export const AvatarUtil = { sizes: avatarSizes, statuses: avatarStatuses, } export type AvatarProps = Omit & { image?: ImageConfig, name?: string, size?: AvatarSize, color?: ColorPairToken, ImageComponent?: ComponentType, style?: StyleProp, avatarStyle?: StyleOverwrite, imageStyle?: StyleOverwrite, textStyle?: StyleOverwrite, iconStyle?: StyleOverwrite, isGrouped?: boolean, groupIndex?: number, } export const Avatar = ({ image: initialImage, name, size = 'md', color, ImageComponent = DefaultAvatarImage, style, avatarStyle, imageStyle, textStyle, iconStyle, isGrouped = false, groupIndex, ...props }: AvatarProps) => { const { theme } = useTheme() const [hasError, setHasError] = useState(false) const [hasLoaded, setHasLoaded] = useState(false) const [image, setImage] = useState(initialImage) const ImageElement = ImageComponent const state = useMemo((): AvatarState => ({ size, isGrouped, groupIndex, color, }), [size, isGrouped, groupIndex, color]) const displayName = useMemo(() => { const maxLetters = size === 'sm' ? 1 : 2 return (name ?? '') .split(' ') .filter((_, index) => index < maxLetters) .map(value => value[0]) .join('') .toUpperCase() }, [name, size]) const isShowingImage = !!image && (!hasError || !hasLoaded) const isShowingFallback = !hasLoaded || hasError useEffect(() => { if (initialImage?.avatarUrl !== image?.avatarUrl) { setHasError(false) setHasLoaded(false) } setImage(initialImage) }, [image?.avatarUrl, initialImage]) const resolvedAvatar = theme.components.avatar.container(state, avatarStyle) const resolvedImage = theme.components.avatar.image(state, imageStyle) const resolvedText = theme.components.avatar.text(state, textStyle) const resolvedIcon = theme.components.avatar.icon(state, iconStyle) return ( {isShowingImage && ( setHasLoaded(true)} onError={() => setHasError(true)} /> )} {isShowingFallback && ( name ? ( {displayName} ) : ( ) )} ) } export type AvatarGroupProps = Omit & { avatars: Omit[], showTotalNumber?: boolean, size?: AvatarSize, color?: ColorPairToken, ImageComponent?: ComponentType, style?: StyleProp, containerStyle?: StyleOverwrite<{ size?: AvatarSize, count?: number }, AvatarGroupContainerStyle>, avatarStackStyle?: StyleOverwrite<{ size?: AvatarSize, count?: number }, AvatarGroupStackStyle>, textStyle?: StyleOverwrite<{ size?: AvatarSize, count?: number }, AvatarGroupTextStyle>, } export const AvatarGroup = ({ avatars, showTotalNumber = true, size = 'md', color, ImageComponent, style, containerStyle, avatarStackStyle, textStyle, ...props }: AvatarGroupProps) => { const { theme } = useTheme() const maxShownProfiles = 5 const displayedProfiles = avatars.length < maxShownProfiles ? avatars : avatars.slice(0, maxShownProfiles) const notDisplayedProfiles = avatars.length - maxShownProfiles const state = useMemo(() => ({ size, color, count: displayedProfiles.length, }), [size, color, displayedProfiles.length]) const avatarResolvers = useMemo( () => theme.components.avatarGroup.avatar(state), [theme, state] ) const resolvedContainer = theme.components.avatarGroup.container(state, containerStyle) const resolvedAvatarStack = theme.components.avatarGroup.avatarStack(state, avatarStackStyle) const resolvedText = theme.components.avatarGroup.text(state, textStyle) return ( {displayedProfiles.map((avatar, index) => { const avatarState: AvatarState = { size, isGrouped: true, groupIndex: index, color: avatar.color ?? color, } return ( avatarResolvers.container({ ...state, ...avatarState, })} imageStyle={(state) => avatarResolvers.image({ ...state, ...avatarState, })} textStyle={(state) => avatarResolvers.text({ ...state, ...avatarState, })} iconStyle={(state) => avatarResolvers.icon({ ...state, ...avatarState, })} /> ) })} {showTotalNumber && notDisplayedProfiles > 0 && ( {`+${notDisplayedProfiles}`} )} ) } export type { AvatarStatus } export type AvatarWithStatusProps = AvatarProps & { status?: AvatarStatus, statusDotStyle?: StyleOverwrite, } export const AvatarWithStatus = ({ status = 'unknown', size = 'md', color, style, avatarStyle, imageStyle, textStyle, iconStyle, statusDotStyle, ...avatarProps }: AvatarWithStatusProps) => { const { theme } = useTheme() const state = useMemo((): AvatarWithStatusState => ({ size, color, status, }), [size, color, status]) const avatarResolvers = useMemo( () => theme.components.avatarWithStatus.avatar(state), [theme, state] ) const resolvedStatusDot = theme.components.avatarWithStatus.statusDot(state, statusDotStyle) return ( avatarResolvers.container(state, avatarStyle)} imageStyle={(state) => avatarResolvers.image(state, imageStyle)} textStyle={(state) => avatarResolvers.text(state, textStyle)} iconStyle={(state) => avatarResolvers.icon(state, iconStyle)} /> ) }