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:
@@ -73,6 +73,7 @@ import { livekitSupport } from '../../hooks/useLivekitSupport';
|
||||
import { MessageEvent, StateEvent } from '../../../types/matrix/room';
|
||||
import { webRTCSupported } from '../../utils/rtc';
|
||||
import { useRoomLatestRenderedEvent } from '../../hooks/useRoomLatestRenderedEvent';
|
||||
import { EmojiBoard } from '../../components/emoji-board';
|
||||
|
||||
dayjs.extend(isToday);
|
||||
dayjs.extend(isYesterday);
|
||||
@@ -103,6 +104,15 @@ type RenameRoomDialogProps = {
|
||||
function RenameRoomDialog({ room, onClose }: RenameRoomDialogProps) {
|
||||
const mx = useMatrixClient();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [emojiAnchor, setEmojiAnchor] = useState<RectCords>();
|
||||
|
||||
const handleEmojiSelect = useCallback((unicode: string) => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.value = unicode + inputRef.current.value;
|
||||
inputRef.current.focus();
|
||||
}
|
||||
setEmojiAnchor(undefined);
|
||||
}, []);
|
||||
|
||||
const getCurrentLocalName = useCallback((): string => {
|
||||
const content = getLocalRoomNamesContent(mx);
|
||||
@@ -171,16 +181,52 @@ function RenameRoomDialog({ room, onClose }: RenameRoomDialogProps) {
|
||||
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
|
||||
<Box direction="Column" gap="200">
|
||||
<Text size="L400">Custom name</Text>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
defaultValue={getCurrentLocalName()}
|
||||
placeholder={room.name}
|
||||
variant="Secondary"
|
||||
radii="300"
|
||||
maxLength={255}
|
||||
onKeyDown={handleKeyDown}
|
||||
autoFocus
|
||||
/>
|
||||
<Box direction="Row" gap="100" alignItems="Center">
|
||||
<PopOut
|
||||
anchor={emojiAnchor}
|
||||
position="Top"
|
||||
align="End"
|
||||
content={
|
||||
<EmojiBoard
|
||||
imagePackRooms={[]}
|
||||
returnFocusOnDeactivate={false}
|
||||
onEmojiSelect={handleEmojiSelect}
|
||||
requestClose={() => setEmojiAnchor(undefined)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<IconButton
|
||||
type="button"
|
||||
size="400"
|
||||
radii="400"
|
||||
variant="Surface"
|
||||
fill="Soft"
|
||||
outlined
|
||||
aria-label="Insert emoji"
|
||||
aria-expanded={!!emojiAnchor}
|
||||
aria-haspopup="dialog"
|
||||
onClick={(evt: React.MouseEvent<HTMLButtonElement>) => {
|
||||
const rect = evt.currentTarget.getBoundingClientRect();
|
||||
setEmojiAnchor((prev) => (prev ? undefined : rect));
|
||||
}}
|
||||
>
|
||||
<Icon src={Icons.Smile} size="400" />
|
||||
</IconButton>
|
||||
</PopOut>
|
||||
<Box grow="Yes">
|
||||
<Input
|
||||
ref={inputRef}
|
||||
defaultValue={getCurrentLocalName()}
|
||||
placeholder={room.name}
|
||||
variant="Secondary"
|
||||
radii="300"
|
||||
maxLength={255}
|
||||
onKeyDown={handleKeyDown}
|
||||
autoFocus
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<Text size="T300" priority="300">
|
||||
Only visible to you. Leave blank to use the original name.
|
||||
</Text>
|
||||
@@ -674,7 +720,23 @@ function RoomNavItem_({
|
||||
<Box as="span" direction="Column" grow="Yes" style={{ minWidth: 0 }}>
|
||||
<Box as="span" grow="Yes" alignItems="Center" gap="100">
|
||||
<Text priority={unread ? '500' : '300'} as="span" size="Inherit" truncate>
|
||||
{roomName}
|
||||
{(() => {
|
||||
const emojiMatch = roomName.match(
|
||||
/^(\p{Emoji_Presentation}|\p{Extended_Pictographic})\s*/u,
|
||||
);
|
||||
const emojiPrefix = emojiMatch?.[0] ?? '';
|
||||
const nameRest = emojiPrefix ? roomName.slice(emojiPrefix.length) : roomName;
|
||||
return (
|
||||
<>
|
||||
{emojiPrefix && (
|
||||
<span style={{ fontSize: '1.15em', lineHeight: 1 }}>
|
||||
{emojiPrefix.trim()}
|
||||
</span>
|
||||
)}
|
||||
{emojiPrefix ? ` ${nameRest}` : roomName}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</Text>
|
||||
{hasLocalName && (
|
||||
<Icon
|
||||
|
||||
Reference in New Issue
Block a user