Files
ncd-fe/packages/pl-fe/src/components/ui/avatar.tsx
nicole mikołajczyk f50ac8d73e pl-fe: default avatar/header detection cleanup
Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
2025-10-27 11:43:19 +01:00

133 lines
4.0 KiB
TypeScript

import clsx from 'clsx';
import { FastAverageColor } from 'fast-average-color';
import React, { useEffect, useState } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import StillImage, { IStillImage } from 'pl-fe/components/still-image';
import { useSettings } from 'pl-fe/stores/settings';
import AltIndicator from '../alt-indicator';
import Icon from './icon';
import Popover from './popover';
import Stack from './stack';
import Text from './text';
const COLOR_CACHE = new Map<string, string>();
const AVATAR_SIZE = 42;
const messages = defineMessages({
avatar: { id: 'account.avatar.alt', defaultMessage: 'Avatar' },
avatar_with_username: { id: 'account.avatar.with_username', defaultMessage: 'Avatar for {username}' },
avatar_with_content: { id: 'account.avatar.with_content', defaultMessage: 'Avatar for {username}: {alt}' },
});
interface IAvatar extends Pick<IStillImage, 'alt' | 'src' | 'staticSrc' | 'onError' | 'className'> {
/** Width and height of the avatar in pixels. */
size?: number;
/** Whether the user is a cat. */
isCat?: boolean;
username?: string;
showAlt?: boolean;
}
const fac = new FastAverageColor();
/** Round profile avatar for accounts. */
const Avatar = (props: IAvatar) => {
const intl = useIntl();
const { disableUserProvidedMedia } = useSettings();
const { alt, src, size = AVATAR_SIZE, className, isCat } = props;
const [color, setColor] = useState<string | undefined>(undefined);
const [isAvatarMissing, setIsAvatarMissing] = useState(false);
const handleLoadFailure = () => setIsAvatarMissing(true);
useEffect(() => {
if (!isCat) return;
if (COLOR_CACHE.has(src)) {
setColor(COLOR_CACHE.get(src));
return;
}
fac.getColorAsync(src).then(color => {
if (!color.error) {
COLOR_CACHE.set(src, color.hex);
setColor(color.hex);
}
}).catch(() => setColor(undefined));
}, [src, isCat]);
const style: React.CSSProperties = React.useMemo(() => {
const value = `${size / 16}rem`;
return {
width: value,
height: value,
fontSize: value,
color,
};
}, [size, color]);
if (disableUserProvidedMedia) {
if (isAvatarMissing || !alt || isDefaultAvatar(src)) return null;
return (
<Popover
interaction='hover'
referenceElementClassName='cursor-pointer'
content={
<Stack space={1} className='max-h-[32rem] max-w-96 overflow-auto p-4'>
<Text weight='semibold'>
<FormattedMessage id='account.avatar.description' defaultMessage='Avatar description' />
</Text>
<Text className='whitespace-pre-wrap'>
{alt}
</Text>
</Stack>
}
isFlush
>
<AltIndicator message={<FormattedMessage id='account.avatar.alt' defaultMessage='Avatar' />} />
</Popover>
);
}
if (isAvatarMissing) {
return (
<div
style={style}
className={clsx('relative rounded-lg bg-gray-200 leading-[0] dark:bg-gray-900', isCat && 'avatar__cat', className)}
>
<div className='absolute inset-0 z-[1] flex items-center justify-center rounded-[inherit] bg-gray-200 dark:bg-gray-900'>
<Icon
src={require('@phosphor-icons/core/regular/image-square.svg')}
className='size-4 text-gray-500 dark:text-gray-700'
/>
</div>
</div>
);
}
const altText = props.showAlt && alt
? intl.formatMessage(messages.avatar_with_content, { username: props.username, alt })
: props.username
? intl.formatMessage(messages.avatar_with_username, { username: props.username })
: intl.formatMessage(messages.avatar);
return (
<StillImage
className={clsx('rounded-lg leading-[0]', isCat && 'avatar__cat bg-gray-200 dark:bg-gray-900', className)}
innerClassName='rounded-[inherit] text-sm'
style={style}
src={src}
alt={altText}
onError={handleLoadFailure}
/>
);
};
export { Avatar as default };