CI / Build & Quality Checks (push) Successful in 1m41s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 11s
CI / Trigger Desktop Build (push) Successful in 9s
CI / Playwright smoke (e2e) (push) Successful in 13m14s
Follow-up to ce8ed89f for the modals that have no visible heading to
point at: the three "Seen by" reader lists (read-receipt pill, "is
following", message menu), View source, the text/PDF file viewers, the
room-card join error and the user-profile modal get role="dialog",
aria-modal and an aria-label; their traps move focus in (fallbackFocus
on the dialog) where the trap is local.
Verified: the receipt pill opens a "Seen by" dialog with focus on
Close; Escape closes it and focus returns to the pill.
The remaining unnamed Modal/Dialog uses are startup/loading and error
screens (config, feature check, spec versions, client root, password
reset) and wrappers around components that carry their own role (image
viewer).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
154 lines
5.2 KiB
TypeScript
154 lines
5.2 KiB
TypeScript
import React, { useState } from 'react';
|
|
import {
|
|
Box,
|
|
Icon,
|
|
Icons,
|
|
Modal,
|
|
Overlay,
|
|
OverlayBackdrop,
|
|
OverlayCenter,
|
|
Text,
|
|
as,
|
|
config,
|
|
} from 'folds';
|
|
import { Room } from 'matrix-js-sdk';
|
|
import classNames from 'classnames';
|
|
import FocusTrap from 'focus-trap-react';
|
|
|
|
import { getMemberName } from '../../utils/room';
|
|
import * as css from './RoomViewFollowing.css';
|
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
|
import { useRoomLatestRenderedEvent } from '../../hooks/useRoomLatestRenderedEvent';
|
|
import { useRoomEventReaders } from '../../hooks/useRoomEventReaders';
|
|
import { EventReaders } from '../../components/event-readers';
|
|
import { useModalStyle } from '../../hooks/useModalStyle';
|
|
import { stopPropagation } from '../../utils/keyboard';
|
|
|
|
export function RoomViewFollowingPlaceholder() {
|
|
return <div className={css.RoomViewFollowingPlaceholder} />;
|
|
}
|
|
|
|
export type RoomViewFollowingProps = {
|
|
room: Room;
|
|
};
|
|
export const RoomViewFollowing = as<'div', RoomViewFollowingProps>(
|
|
({ className, room, ...props }, ref) => {
|
|
const mx = useMatrixClient();
|
|
const [open, setOpen] = useState(false);
|
|
const modalStyle = useModalStyle(360);
|
|
const latestEvent = useRoomLatestRenderedEvent(room);
|
|
const latestEventReaders = useRoomEventReaders(room, latestEvent?.getId());
|
|
const names = latestEventReaders
|
|
.filter((readerId) => readerId !== mx.getUserId())
|
|
.map((readerId) => getMemberName(room, readerId));
|
|
|
|
const eventId = latestEvent?.getId();
|
|
|
|
return (
|
|
<>
|
|
{eventId && (
|
|
<Overlay open={open} backdrop={<OverlayBackdrop />}>
|
|
<OverlayCenter>
|
|
<FocusTrap
|
|
focusTrapOptions={{
|
|
fallbackFocus: '#following-seenby-dialog',
|
|
onDeactivate: () => setOpen(false),
|
|
clickOutsideDeactivates: true,
|
|
escapeDeactivates: stopPropagation,
|
|
}}
|
|
>
|
|
<Modal
|
|
id="following-seenby-dialog"
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-label="Seen by"
|
|
tabIndex={-1}
|
|
variant="Surface"
|
|
size="300"
|
|
style={modalStyle}
|
|
>
|
|
<EventReaders room={room} eventId={eventId} requestClose={() => setOpen(false)} />
|
|
</Modal>
|
|
</FocusTrap>
|
|
</OverlayCenter>
|
|
</Overlay>
|
|
)}
|
|
<Box
|
|
as={names.length > 0 ? 'button' : 'div'}
|
|
onClick={names.length > 0 ? () => setOpen(true) : undefined}
|
|
className={classNames(css.RoomViewFollowing({ clickable: names.length > 0 }), className)}
|
|
alignItems="Center"
|
|
justifyContent="End"
|
|
gap="200"
|
|
{...props}
|
|
ref={ref}
|
|
>
|
|
{names.length > 0 && (
|
|
<>
|
|
<Icon style={{ opacity: config.opacity.P300 }} size="100" src={Icons.CheckTwice} />
|
|
<Text size="T300" truncate>
|
|
{names.length === 1 && (
|
|
<>
|
|
<b>{names[0]}</b>
|
|
<Text as="span" size="Inherit" priority="300">
|
|
{' is following the conversation.'}
|
|
</Text>
|
|
</>
|
|
)}
|
|
{names.length === 2 && (
|
|
<>
|
|
<b>{names[0]}</b>
|
|
<Text as="span" size="Inherit" priority="300">
|
|
{' and '}
|
|
</Text>
|
|
<b>{names[1]}</b>
|
|
<Text as="span" size="Inherit" priority="300">
|
|
{' are following the conversation.'}
|
|
</Text>
|
|
</>
|
|
)}
|
|
{names.length === 3 && (
|
|
<>
|
|
<b>{names[0]}</b>
|
|
<Text as="span" size="Inherit" priority="300">
|
|
{', '}
|
|
</Text>
|
|
<b>{names[1]}</b>
|
|
<Text as="span" size="Inherit" priority="300">
|
|
{' and '}
|
|
</Text>
|
|
<b>{names[2]}</b>
|
|
<Text as="span" size="Inherit" priority="300">
|
|
{' are following the conversation.'}
|
|
</Text>
|
|
</>
|
|
)}
|
|
{names.length > 3 && (
|
|
<>
|
|
<b>{names[0]}</b>
|
|
<Text as="span" size="Inherit" priority="300">
|
|
{', '}
|
|
</Text>
|
|
<b>{names[1]}</b>
|
|
<Text as="span" size="Inherit" priority="300">
|
|
{', '}
|
|
</Text>
|
|
<b>{names[2]}</b>
|
|
<Text as="span" size="Inherit" priority="300">
|
|
{' and '}
|
|
</Text>
|
|
<b>{names.length - 3} others</b>
|
|
<Text as="span" size="Inherit" priority="300">
|
|
{' are following the conversation.'}
|
|
</Text>
|
|
</>
|
|
)}
|
|
</Text>
|
|
</>
|
|
)}
|
|
</Box>
|
|
</>
|
|
);
|
|
},
|
|
);
|