Files
cinny/src/app/components/read-receipt-avatars/ReadReceiptAvatars.tsx
T
root 4c4d61600d feat: reaction TDS styling, debounce read receipts, Escape to skip boot, type fixes
- lotus-terminal.css.ts: add reaction chip styles for dark + light TDS modes
  (cyan border/bg for unselected, orange accent for own/pressed reactions)
- useRoomReadPositions: debounce receipt handler at 150ms (M-3)
- lotus-boot.ts: Escape key skips boot animation (I-3)
- RoomInput.tsx: replace (uploadRes as any) with typed assertion (M-7)
- CallEmbedProvider: call mention detection, audio cleanup, display name (C-1, C-2, M-5)
- EventReaders: timestamps in seen-by modal, filter self, TDS styling
- ReadReceiptAvatars: StackedAvatar pill, TDS visual treatment
- chatBackground: add waves/neon/aurora backgrounds
- RoomView: auto-apply tactical bg when TDS active and bg is none
- settings: extend ChatBackground union type
2026-05-16 01:34:20 -04:00

127 lines
4.1 KiB
TypeScript

import React, { useState } from 'react';
import { Room } from 'matrix-js-sdk';
import { Icon, Icons, Modal, Overlay, OverlayBackdrop, OverlayCenter, Text, color } 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';
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');
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">
<EventReaders room={room} eventId={eventId} requestClose={() => setOpen(false)} />
</Modal>
</FocusTrap>
</OverlayCenter>
</Overlay>
<button
type="button"
onClick={() => setOpen(true)}
title={tooltipNames}
aria-label={tooltipNames}
style={{
background: 'none',
border: 'none',
cursor: 'pointer',
padding: 0,
marginLeft: 'auto',
marginTop: '4px',
display: 'flex',
alignItems: 'center',
gap: '4px',
}}
>
{/* 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 ? '1px solid rgba(0,212,255,0.30)' : '1px solid transparent',
boxShadow: lotusTerminal ? '0 0 10px rgba(0,212,255,0.12)' : 'none',
borderRadius: '999px',
padding: '2px 6px 2px 2px',
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="T100"
style={{ paddingLeft: '4px', color: color.SurfaceVariant.OnContainer }}
>
+{extra}
</Text>
)}
</span>
</button>
</>
);
}