Files
cinny/src/app/features/room/ReportRoomModal.tsx
T
2026-06-01 21:38:35 -04:00

215 lines
7.1 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 { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { stopPropagation } from '../../utils/keyboard';
type ReportCategory = 'spam' | 'harassment' | 'inappropriate' | 'other';
const CATEGORY_LABELS: Record<ReportCategory, string> = {
spam: 'Spam',
harassment: 'Harassment',
inappropriate: 'Inappropriate Content',
other: 'Other',
};
type ReportRoomModalProps = {
roomId: string;
onClose: () => void;
};
export function ReportRoomModal({ roomId, onClose }: ReportRoomModalProps) {
const mx = useMatrixClient();
const [category, setCategory] = useState<ReportCategory>('spam');
const [reportState, submitReport] = useAsyncCallback(
useCallback(
async (reason: string) => {
await mx.reportRoom(roomId, reason);
},
[mx, roomId],
),
);
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 errcode =
reportState.status === AsyncStatus.Error
? (reportState.error as { errcode?: string })?.errcode
: undefined;
const errorMsg =
errcode === 'M_LIMIT_EXCEEDED'
? 'You are being rate limited. Please wait before reporting again.'
: errcode === 'M_FORBIDDEN'
? 'You cannot report this room.'
: '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-room-dialog-title"
onSubmit={handleSubmit}
direction="Column"
style={{
background: color.Surface.Container,
borderRadius: config.radii.R400,
boxShadow: color.Other.Shadow,
width: '100%',
maxWidth: 420,
overflow: 'hidden',
}}
>
<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-room-dialog-title" size="H4">
Report Room
</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 room to your homeserver admins. Please describe the issue below.
</Text>
<Box direction="Column" gap="100">
<Text as="label" htmlFor="report-category" size="L400">
Category
</Text>
<Box
as="select"
id="report-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-reason-input" size="L400">
Reason
</Text>
<Input
id="report-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">
Room has been reported to the server.
</Text>
)}
</Box>
<Box gap="200" justifyContent="End">
<Button type="button" variant="Secondary" fill="None" radii="300" onClick={onClose}>
<Text size="B400">Cancel</Text>
</Button>
<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 Room'}
</Text>
</Button>
</Box>
</Box>
</Box>
</FocusTrap>
</OverlayCenter>
</Overlay>
);
}