Files
glchat/web/src/components/ui/Avatar.tsx
T
grendervill 2e038a1d3f feat(webhooks): вебхуки комнат — бэкенд, клиент и настройки комнаты
- миграция 00012: таблица webhooks, снимок имени и аватара в messages (AGENT.md 7.11)
- API: список/создание/правка/удаление и пересоздание токена (MANAGE_WEBHOOKS,
  step-up при создании, аудит), загрузка аватара отдельной multipart-ручкой
- исполнение POST /webhooks/{id}/{token} без сессии: content, username,
  avatar_url, файлы создателя вебхука, лимит 30/мин на вебхук
- клиент: пункт настроек «Комната» со списком вебхуков, копированием ссылки,
  пересозданием токена и удалением
- сообщения вебхуков: имя, аватар и значок в ленте вместо «неизвестного автора»
- fix: неполный ответ REST больше не затирает данные READY-снапшота
2026-09-21 00:26:50 +03:00

69 lines
2.4 KiB
TypeScript

import { avatarUrl, hueFromId, initials } from '@/lib/identity';
/**
* Безопасная ссылка на картинку аватара: файл инстанса или внешний http(s).
* Всё остальное (в том числе `javascript:`) игнорируется.
*/
function safeImageUrl(url: string | undefined): string | null {
if (url === undefined || url === '') {
return null;
}
return url.startsWith('/files/') || url.startsWith('https://') || url.startsWith('http://')
? url
: null;
}
interface AvatarProps {
name: string;
/** Стабильный id для оттенка заглушки. */
seed: string;
fileId?: string | undefined;
/** Готовая ссылка (аватар вебхука) вместо `fileId`. */
url?: string | undefined;
/** Локальный предпросмотр (свежевыбранный файл) вместо `fileId`. */
preview?: string | undefined;
size?: 'sm' | 'md' | 'lg' | 'xl';
/** Квадратная заглушка для серверов, круглая — для пользователей. */
shape?: 'circle' | 'square';
}
const sizes = {
sm: 'h-7 w-7 text-[11px]',
md: 'h-9 w-9 text-xs',
lg: 'h-14 w-14 text-lg',
/** Крупный аватар в настройках профиля: занимает родителя целиком. */
xl: 'h-full w-full text-4xl',
} as const;
/**
* Аватар: картинка файлового сервиса либо инициалы на цветной заглушке.
* `preview` перекрывает `fileId` — так выбранный файл видно до ответа сервера.
*/
export function Avatar({
name,
seed,
fileId,
url,
preview,
size = 'md',
shape = 'circle',
}: AvatarProps) {
const src = preview ?? safeImageUrl(url) ?? avatarUrl(fileId);
const hue = hueFromId(seed);
const radius = shape === 'circle' ? 'rounded-full' : 'rounded-[var(--radius-lg)]';
return (
<span
aria-hidden="true"
className={`inline-flex shrink-0 items-center justify-center overflow-hidden font-semibold text-white ${radius} ${sizes[size]}`}
style={src === null ? { backgroundColor: `oklch(0.55 0.13 ${String(hue)})` } : undefined}
>
{src === null ? (
initials(name)
) : (
<img src={src} alt="" className="h-full w-full object-cover" />
)}
</span>
);
}