2026-06-14 11:24:04 -04:00
|
|
|
import React from 'react';
|
|
|
|
|
import { useAvatarDecoration } from '../../hooks/useAvatarDecoration';
|
2026-07-19 00:00:07 -04:00
|
|
|
import { useReducedMotion } from '../../hooks/useReducedMotion';
|
2026-06-14 11:24:04 -04:00
|
|
|
import { decorationUrl } from '../../features/lotus/avatarDecorations';
|
|
|
|
|
|
2026-06-14 16:19:47 -04:00
|
|
|
const DEFAULT_INSET = 8;
|
2026-06-14 11:24:04 -04:00
|
|
|
|
|
|
|
|
type AvatarDecorationProps = {
|
|
|
|
|
userId: string;
|
|
|
|
|
children: React.ReactNode;
|
2026-06-14 16:19:47 -04:00
|
|
|
inset?: number;
|
2026-06-14 11:24:04 -04:00
|
|
|
};
|
|
|
|
|
|
2026-06-15 20:50:00 -04:00
|
|
|
export function AvatarDecoration({
|
|
|
|
|
userId,
|
|
|
|
|
children,
|
|
|
|
|
inset = DEFAULT_INSET,
|
|
|
|
|
}: AvatarDecorationProps) {
|
2026-06-14 11:24:04 -04:00
|
|
|
const slug = useAvatarDecoration(userId);
|
2026-07-19 00:00:07 -04:00
|
|
|
const reducedMotion = useReducedMotion();
|
2026-06-14 11:24:04 -04:00
|
|
|
|
2026-07-19 00:00:07 -04:00
|
|
|
// Decorations are animated APNGs with no static asset to freeze to, so honor
|
|
|
|
|
// prefers-reduced-motion by not rendering the animation at all (consistent
|
|
|
|
|
// with the rest of the theming stack — chat backgrounds / seasonal overlays —
|
|
|
|
|
// which all suppress motion under this preference; also avoids dozens of live
|
|
|
|
|
// APNGs animating in scrolling mobile lists).
|
|
|
|
|
if (!slug || reducedMotion) {
|
2026-06-14 11:24:04 -04:00
|
|
|
return <>{children}</>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
position: 'relative',
|
|
|
|
|
display: 'inline-flex',
|
|
|
|
|
flexShrink: 0,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{children}
|
|
|
|
|
<img
|
2026-07-02 21:40:07 -04:00
|
|
|
// Force a fresh element per slug so a recycled node whose previous slug
|
|
|
|
|
// 404'd (and was hidden in onError) can't leak `display:none` onto a
|
|
|
|
|
// valid decoration.
|
|
|
|
|
key={slug}
|
2026-06-14 11:24:04 -04:00
|
|
|
src={decorationUrl(slug)}
|
|
|
|
|
style={{
|
|
|
|
|
position: 'absolute',
|
2026-06-14 16:19:47 -04:00
|
|
|
top: -inset,
|
|
|
|
|
left: -inset,
|
|
|
|
|
right: -inset,
|
|
|
|
|
bottom: -inset,
|
|
|
|
|
width: `calc(100% + ${inset * 2}px)`,
|
|
|
|
|
height: `calc(100% + ${inset * 2}px)`,
|
2026-06-14 11:24:04 -04:00
|
|
|
pointerEvents: 'none',
|
|
|
|
|
zIndex: 10,
|
|
|
|
|
objectFit: 'contain',
|
|
|
|
|
}}
|
|
|
|
|
alt=""
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
loading="lazy"
|
|
|
|
|
decoding="async"
|
2026-07-02 21:40:07 -04:00
|
|
|
onLoad={(e) => {
|
|
|
|
|
(e.currentTarget as HTMLImageElement).style.removeProperty('display');
|
|
|
|
|
}}
|
2026-06-14 12:02:50 -04:00
|
|
|
onError={(e) => {
|
|
|
|
|
(e.currentTarget as HTMLImageElement).style.display = 'none';
|
|
|
|
|
}}
|
2026-06-14 11:24:04 -04:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|