b361d43088
- N23 RoomServerACL: raw text input -> folds Input; raw checkbox -> folds Checkbox
- N24 PolicyListViewer: raw room-id input -> folds Input (Critical variant on error)
- N25 ExportRoomHistory: raw <input type="date"> x2 -> folds Input
- N26 RoomShareInvite: QR <img> gets loading="lazy" + onError fallback card
("QR code unavailable") instead of a broken-image icon
- N27 GifPicker: FocusTrap returnFocusOnDeactivate:false (matches EmojiBoard)
- N76 Report modals: drop redundant Cancel button (dismiss via header x /
click-outside, like MessageReportItem)
- N5 ReadReceiptAvatars: hover/focus moved to co-located css :hover/:focus-visible
(removed JS onMouseEnter/Leave .style mutation)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
133 lines
4.2 KiB
TypeScript
133 lines
4.2 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { Room } from 'matrix-js-sdk';
|
|
import {
|
|
Icon,
|
|
Icons,
|
|
Modal,
|
|
Overlay,
|
|
OverlayBackdrop,
|
|
OverlayCenter,
|
|
Text,
|
|
color,
|
|
config,
|
|
} from 'folds';
|
|
import FocusTrap from 'focus-trap-react';
|
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
|
import { useSetting } from '../../state/hooks/settings';
|
|
import { settingsAtom } from '../../state/settings';
|
|
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
|
import { getMemberDisplayName } from '../../utils/room';
|
|
import { getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix';
|
|
import { UserAvatar } from '../user-avatar';
|
|
import { StackedAvatar } from '../stacked-avatar';
|
|
import { EventReaders } from '../event-readers';
|
|
import { stopPropagation } from '../../utils/keyboard';
|
|
import { useModalStyle } from '../../hooks/useModalStyle';
|
|
import * as css from './ReadReceiptAvatars.css';
|
|
|
|
const MAX_DISPLAY = 5;
|
|
|
|
export function ReadReceiptAvatars({
|
|
room,
|
|
eventId,
|
|
userIds,
|
|
}: {
|
|
room: Room;
|
|
eventId: string;
|
|
userIds: string[];
|
|
}) {
|
|
const mx = useMatrixClient();
|
|
const useAuthentication = useMediaAuthentication();
|
|
const [open, setOpen] = useState(false);
|
|
const [lotusTerminal] = useSetting(settingsAtom, 'lotusTerminal');
|
|
const modalStyle = useModalStyle(360);
|
|
|
|
if (userIds.length === 0) return null;
|
|
|
|
const displayed = userIds.slice(0, MAX_DISPLAY);
|
|
const extra = userIds.length - MAX_DISPLAY;
|
|
const tooltipNames =
|
|
userIds
|
|
.slice(0, 5)
|
|
.map((id) => getMemberDisplayName(room, id) ?? getMxIdLocalPart(id) ?? id)
|
|
.join(', ') + (extra > 0 ? ` +${extra} more` : '');
|
|
|
|
return (
|
|
<>
|
|
<Overlay open={open} backdrop={<OverlayBackdrop />}>
|
|
<OverlayCenter>
|
|
<FocusTrap
|
|
focusTrapOptions={{
|
|
initialFocus: false,
|
|
onDeactivate: () => setOpen(false),
|
|
clickOutsideDeactivates: true,
|
|
escapeDeactivates: stopPropagation,
|
|
}}
|
|
>
|
|
<Modal variant="Surface" size="300" style={modalStyle}>
|
|
<EventReaders room={room} eventId={eventId} requestClose={() => setOpen(false)} />
|
|
</Modal>
|
|
</FocusTrap>
|
|
</OverlayCenter>
|
|
</Overlay>
|
|
<button
|
|
type="button"
|
|
onClick={() => setOpen(true)}
|
|
title={tooltipNames}
|
|
aria-label={tooltipNames}
|
|
className={css.ReceiptTrigger}
|
|
>
|
|
{/* Pill wrapper ensures visibility on any wallpaper/background */}
|
|
<span
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
backgroundColor: lotusTerminal
|
|
? 'rgba(0,212,255,0.07)'
|
|
: color.SurfaceVariant.Container,
|
|
border: lotusTerminal
|
|
? `${config.borderWidth.B300} solid rgba(0,212,255,0.30)`
|
|
: `${config.borderWidth.B300} solid transparent`,
|
|
boxShadow: lotusTerminal ? '0 0 10px rgba(0,212,255,0.12)' : 'none',
|
|
borderRadius: config.radii.Pill,
|
|
padding: `${config.space.S100} ${config.space.S200}`,
|
|
gap: '0px',
|
|
}}
|
|
>
|
|
{displayed.map((userId) => {
|
|
const name = getMemberDisplayName(room, userId) ?? getMxIdLocalPart(userId) ?? userId;
|
|
const avatarMxc = room.getMember(userId)?.getMxcAvatarUrl();
|
|
const avatarUrl = avatarMxc
|
|
? (mxcUrlToHttp(mx, avatarMxc, useAuthentication, 32, 32, 'crop') ?? undefined)
|
|
: undefined;
|
|
return (
|
|
<StackedAvatar
|
|
key={userId}
|
|
title={name}
|
|
variant="SurfaceVariant"
|
|
size="200"
|
|
radii="Pill"
|
|
>
|
|
<UserAvatar
|
|
userId={userId}
|
|
src={avatarUrl}
|
|
alt={name}
|
|
renderFallback={() => <Icon size="50" src={Icons.User} filled />}
|
|
/>
|
|
</StackedAvatar>
|
|
);
|
|
})}
|
|
{extra > 0 && (
|
|
<Text
|
|
size="T200"
|
|
style={{ paddingLeft: '4px', color: color.SurfaceVariant.OnContainer }}
|
|
>
|
|
+{extra}
|
|
</Text>
|
|
)}
|
|
</span>
|
|
</button>
|
|
</>
|
|
);
|
|
}
|