feat: avatar decorations (P5-13)
256×256 APNG overlays from avatardecoration.com (open CORS CDN). Stored in the user's Matrix profile as io.lotus.avatar_decoration via MSC4133 so all Lotus Chat users see each other's decorations. - avatarDecorations.ts: curated catalog of 110 original-IP decorations across 9 categories (Gaming, Cyber, Space, Fantasy, Elements, Japanese, Nature, Spooky, Cozy) - useAvatarDecoration: per-user profile fetch with module-level cache and in-flight deduplication so concurrent renders for the same userId share one HTTP request - AvatarDecoration: position:relative wrapper that overlays the APNG 8px beyond the avatar on all sides; renders nothing when no decoration is set (zero cost for undecorated users) - ProfileDecoration: scrollable grid picker in Settings → Profile, grouped by category with live preview; Save button appears only when the selection differs from what's saved - Applied at all five avatar display sites: message timeline, members drawer, knock list, @mention autocomplete, notifications inbox Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -83,6 +83,7 @@ import { getPowerTagIconSrc } from '../../../hooks/useMemberPowerTag';
|
||||
import { ForwardMessageDialog } from './ForwardMessageDialog';
|
||||
import { useBookmarks } from '../../../hooks/useBookmarks';
|
||||
import { PresenceRingAvatar } from '../../../components/presence';
|
||||
import { AvatarDecoration } from '../../../components/avatar-decoration/AvatarDecoration';
|
||||
|
||||
// Delivery status indicator for own messages
|
||||
function DeliveryStatus({
|
||||
@@ -874,27 +875,29 @@ export const Message = React.memo(
|
||||
<AvatarBase
|
||||
className={messageLayout === MessageLayout.Bubble ? css.BubbleAvatarBase : undefined}
|
||||
>
|
||||
<PresenceRingAvatar userId={senderId}>
|
||||
<Avatar
|
||||
className={css.MessageAvatar}
|
||||
as="button"
|
||||
size="300"
|
||||
data-user-id={senderId}
|
||||
onClick={onUserClick}
|
||||
>
|
||||
<UserAvatar
|
||||
userId={senderId}
|
||||
src={
|
||||
senderAvatarMxc
|
||||
? (mxcUrlToHttp(mx, senderAvatarMxc, useAuthentication, 48, 48, 'crop') ??
|
||||
undefined)
|
||||
: undefined
|
||||
}
|
||||
alt={senderDisplayName}
|
||||
renderFallback={() => <Icon size="200" src={Icons.User} filled />}
|
||||
/>
|
||||
</Avatar>
|
||||
</PresenceRingAvatar>
|
||||
<AvatarDecoration userId={senderId}>
|
||||
<PresenceRingAvatar userId={senderId}>
|
||||
<Avatar
|
||||
className={css.MessageAvatar}
|
||||
as="button"
|
||||
size="300"
|
||||
data-user-id={senderId}
|
||||
onClick={onUserClick}
|
||||
>
|
||||
<UserAvatar
|
||||
userId={senderId}
|
||||
src={
|
||||
senderAvatarMxc
|
||||
? (mxcUrlToHttp(mx, senderAvatarMxc, useAuthentication, 48, 48, 'crop') ??
|
||||
undefined)
|
||||
: undefined
|
||||
}
|
||||
alt={senderDisplayName}
|
||||
renderFallback={() => <Icon size="200" src={Icons.User} filled />}
|
||||
/>
|
||||
</Avatar>
|
||||
</PresenceRingAvatar>
|
||||
</AvatarDecoration>
|
||||
</AvatarBase>
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user