feat: presence avatar border rings (P5-18) + room emoji prefix support (P5-6)

P5-18: PresenceRingAvatar wrapper component applies a 2px box-shadow
ring to user avatars — green (online), yellow (idle/unavailable), red
(DND via status_msg='dnd'), no ring (offline). Applied to: message
timeline sender avatars, members drawer (members + knock requests),
@mention autocomplete, and inbox notification senders.

P5-6: Leading emoji in room names renders at 1.15× in the sidebar via
Unicode emoji regex detection in RoomNavItem. Emoji picker (EmojiBoard
in PopOut) added to all three room-name inputs: Create Room dialog
(converted to controlled input), Room Settings name field (shown only
when canEditName), and the "Rename for me" local rename dialog.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-04 21:56:19 -04:00
parent 4876c2e4ca
commit c5fbc20394
11 changed files with 326 additions and 103 deletions
@@ -20,6 +20,7 @@ import { getMemberDisplayName, getMemberSearchStr } from '../../../utils/room';
import { UserAvatar } from '../../user-avatar';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
import { Membership } from '../../../../types/matrix/room';
import { PresenceRingAvatar } from '../../presence';
type MentionAutoCompleteHandler = (userId: string, name: string) => void;
@@ -47,12 +48,14 @@ function UnknownMentionItem({
}
onClick={() => handleAutocomplete(userId, name)}
before={
<Avatar size="200">
<UserAvatar
userId={userId}
renderFallback={() => <Icon size="50" src={Icons.User} filled />}
/>
</Avatar>
<PresenceRingAvatar userId={userId}>
<Avatar size="200">
<UserAvatar
userId={userId}
renderFallback={() => <Icon size="50" src={Icons.User} filled />}
/>
</Avatar>
</PresenceRingAvatar>
}
>
<Text style={{ flexGrow: 1 }} size="B400">
@@ -174,14 +177,16 @@ export function UserMentionAutocomplete({
</Text>
}
before={
<Avatar size="200">
<UserAvatar
userId={roomMember.userId}
src={avatarUrl ?? undefined}
alt={getName(roomMember)}
renderFallback={() => <Icon size="50" src={Icons.User} filled />}
/>
</Avatar>
<PresenceRingAvatar userId={roomMember.userId}>
<Avatar size="200">
<UserAvatar
userId={roomMember.userId}
src={avatarUrl ?? undefined}
alt={getName(roomMember)}
renderFallback={() => <Icon size="50" src={Icons.User} filled />}
/>
</Avatar>
</PresenceRingAvatar>
}
>
<Text style={{ flexGrow: 1 }} size="B400" truncate>