From the 6-agent deep per-feature audit. Mobile-gated / consistency fixes; desktop unchanged except two intentional dialog-width normalizations noted below. - In-call control bar: wrap="Wrap" on the SequenceCard so the compact two-group row wraps on the narrowest phones (<=390px) instead of pushing End off-screen (M1 fixed the 500-750px band; this covers narrower). Desktop stays one row. - In-call soundboard popout: clamp maxWidth to the viewport (like M5's screenshare popover) so it can't overflow a narrow phone. - Report-Message dialog + "Seen by" (EventReaders) modals (Message.tsx x2 + RoomViewFollowing): add useModalStyle so they go full-screen on mobile like their sibling report/receipt modals (they floated as fixed cards before). - In-app toast container: full-width toasts inset from both edges on mobile (ScreenSize.Mobile); a fixed 280-340px card previously overflowed a narrow phone. Desktop byte-identical (bottom-right floating card). - Policy-list tabs + audio-controls rows: wrap="Wrap" (inert on desktop). Intentional desktop deltas (normalizing to existing sibling modals, verified by two review passes as consistent, not regressions): Report dialog max-width 380->480px; EventReaders modals 460->360px. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
145 lines
4.9 KiB
TypeScript
145 lines
4.9 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={{
|
|
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>
|
|
)}
|
|
<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>
|
|
</>
|
|
);
|
|
},
|
|
);
|