Files
cinny/src/app/features/room/ReportUserModal.tsx
T
jared b361d43088 fix(ui): native inputs/checkboxes, QR fallback, focus + report modal cleanup
- 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>
2026-06-19 18:12:25 -04:00

221 lines
7.3 KiB
TypeScript

import React, { FormEventHandler, useCallback, useEffect, useState } from 'react';
import FocusTrap from 'focus-trap-react';
import {
Box,
Text,
Input,
Button,
IconButton,
Icon,
Icons,
Overlay,
OverlayBackdrop,
OverlayCenter,
Header,
config,
color,
Spinner,
} from 'folds';
import { Method } from 'matrix-js-sdk';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { stopPropagation } from '../../utils/keyboard';
import { useModalStyle } from '../../hooks/useModalStyle';
type ReportCategory = 'spam' | 'harassment' | 'inappropriate' | 'other';
const CATEGORY_LABELS: Record<ReportCategory, string> = {
spam: 'Spam',
harassment: 'Harassment',
inappropriate: 'Inappropriate Content',
other: 'Other',
};
type ReportUserModalProps = {
userId: string;
onClose: () => void;
};
export function ReportUserModal({ userId, onClose }: ReportUserModalProps) {
const mx = useMatrixClient();
const modalStyle = useModalStyle(420);
const [category, setCategory] = useState<ReportCategory>('spam');
const [reportState, submitReport] = useAsyncCallback(
useCallback(
async (reason: string) => {
await mx.http.authedRequest(
Method.Post,
`/users/${encodeURIComponent(userId)}/report`,
undefined,
{ reason },
);
},
[mx, userId],
),
);
useEffect(() => {
if (reportState.status === AsyncStatus.Success) {
const timer = setTimeout(onClose, 1500);
return () => clearTimeout(timer);
}
return undefined;
}, [reportState.status, onClose]);
const handleSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
evt.preventDefault();
if (reportState.status === AsyncStatus.Loading || reportState.status === AsyncStatus.Success) {
return;
}
const target = evt.target as HTMLFormElement;
const reasonInput = target.elements.namedItem('reasonInput') as HTMLInputElement | null;
const reasonText = reasonInput?.value.trim() ?? '';
const fullReason = `[${CATEGORY_LABELS[category]}] ${reasonText}`;
submitReport(fullReason);
};
const reportError =
reportState.status === AsyncStatus.Error
? (reportState.error as { errcode?: string; httpStatus?: number })
: undefined;
const errcode = reportError?.errcode;
const errorMsg =
errcode === 'M_LIMIT_EXCEEDED'
? 'You are being rate limited. Please wait before reporting again.'
: errcode === 'M_FORBIDDEN'
? 'You cannot report this user.'
: errcode === 'M_UNRECOGNIZED' || reportError?.httpStatus === 404
? 'User reporting is not supported by your homeserver.'
: 'Failed to submit report. Please try again.';
return (
<Overlay open backdrop={<OverlayBackdrop />}>
<OverlayCenter>
<FocusTrap
focusTrapOptions={{
initialFocus: false,
onDeactivate: onClose,
clickOutsideDeactivates: true,
escapeDeactivates: stopPropagation,
}}
>
<Box
as="form"
role="dialog"
aria-modal="true"
aria-labelledby="report-user-dialog-title"
onSubmit={handleSubmit}
direction="Column"
style={{
background: color.Surface.Container,
borderRadius: config.radii.R400,
boxShadow: color.Other.Shadow,
...modalStyle,
}}
>
<Header
style={{
padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
borderBottomWidth: config.borderWidth.B300,
}}
variant="Surface"
size="500"
>
<Box grow="Yes">
<Text id="report-user-dialog-title" size="H4">
Report User
</Text>
</Box>
<IconButton size="300" onClick={onClose} radii="300" aria-label="Close">
<Icon src={Icons.Cross} />
</IconButton>
</Header>
<Box direction="Column" gap="400" style={{ padding: config.space.S400 }}>
<Text priority="400">
Report this user to your homeserver admins. Please describe the issue below.
</Text>
<Box direction="Column" gap="100">
<Text as="label" htmlFor="report-user-category" size="L400">
Category
</Text>
<Box
as="select"
id="report-user-category"
aria-label="Report category"
value={category}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) =>
setCategory(e.target.value as ReportCategory)
}
style={{
padding: `${config.space.S200} ${config.space.S300}`,
borderRadius: config.radii.R300,
border: `1px solid ${color.Surface.ContainerLine}`,
background: color.Surface.Container,
color: color.Surface.OnContainer,
fontSize: 'inherit',
fontFamily: 'inherit',
width: '100%',
}}
>
{(Object.keys(CATEGORY_LABELS) as ReportCategory[]).map((key) => (
<option key={key} value={key}>
{CATEGORY_LABELS[key]}
</option>
))}
</Box>
</Box>
<Box direction="Column" gap="100">
<Text as="label" htmlFor="report-user-reason-input" size="L400">
Reason
</Text>
<Input
id="report-user-reason-input"
name="reasonInput"
aria-label="Reason for report"
variant="Background"
required
/>
{reportState.status === AsyncStatus.Error && (
<Text style={{ color: color.Critical.Main }} size="T300">
{errorMsg}
</Text>
)}
{reportState.status === AsyncStatus.Success && (
<Text style={{ color: color.Success.Main }} size="T300">
User has been reported to the server.
</Text>
)}
</Box>
<Box gap="200" justifyContent="End">
<Button
type="submit"
variant="Critical"
radii="300"
before={
reportState.status === AsyncStatus.Loading ? (
<Spinner fill="Solid" variant="Critical" size="200" />
) : undefined
}
aria-disabled={
reportState.status === AsyncStatus.Loading ||
reportState.status === AsyncStatus.Success
}
>
<Text size="B400">
{reportState.status === AsyncStatus.Loading ? 'Reporting...' : 'Report User'}
</Text>
</Button>
</Box>
</Box>
</Box>
</FocusTrap>
</OverlayCenter>
</Overlay>
);
}