Files
cinny/src/app/features/room/widgets/WidgetPermissionPrompt.tsx
T

157 lines
5.3 KiB
TypeScript
Raw Normal View History

import React, { useState } from 'react';
import FocusTrap from 'focus-trap-react';
import {
Box,
Button,
Checkbox,
Dialog,
Header,
Overlay,
OverlayBackdrop,
OverlayCenter,
Text,
color,
config,
} from 'folds';
import type { Capability } from 'matrix-widget-api';
import type { WidgetPermissionRequest } from './widgetUtils';
import { useModalStyle } from '../../../hooks/useModalStyle';
type WidgetPermissionPromptProps = {
widgetName: string;
/** Hostname the widget is served from — who actually gets the access. */
origin: string;
/** Who added the widget to the room, if known. */
addedBy?: string;
requests: WidgetPermissionRequest[];
onDone: (allowed: Set<Capability>, remember: boolean) => void;
};
/**
* Asks before a room widget may read or send events in this room as you.
* Reading is pre-ticked, sending is not; "Deny" (or Escape / clicking away)
* grants nothing beyond display-only access.
*/
export function WidgetPermissionPrompt({
widgetName,
origin,
addedBy,
requests,
onDone,
}: WidgetPermissionPromptProps) {
const modalStyle = useModalStyle(460);
const [checked, setChecked] = useState<Set<Capability>>(
() => new Set(requests.filter((r) => !r.sends).map((r) => r.capability)),
);
const [remember, setRemember] = useState(true);
const toggle = (cap: Capability, on: boolean) =>
setChecked((prev) => {
const next = new Set(prev);
if (on) next.add(cap);
else next.delete(cap);
return next;
});
return (
<Overlay open backdrop={<OverlayBackdrop />}>
<OverlayCenter>
<FocusTrap
focusTrapOptions={{
initialFocus: '#widget-perm-deny',
onDeactivate: () => onDone(new Set(), false),
clickOutsideDeactivates: true,
escapeDeactivates: true,
}}
>
<Dialog
variant="Surface"
role="dialog"
aria-modal="true"
aria-labelledby="widget-perm-title"
aria-describedby="widget-perm-body"
style={modalStyle}
>
<Header
style={{
padding: `0 ${config.space.S400}`,
borderBottomWidth: config.borderWidth.B300,
}}
variant="Surface"
size="500"
>
<Text as="h2" size="H4" id="widget-perm-title" truncate>
Allow “{widgetName}” to access this room?
</Text>
</Header>
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
<Text priority="400" id="widget-perm-body">
This widget is run by <b>{origin}</b>
{addedBy ? `, added to the room by ${addedBy}` : ''}. It is asking to act for you in
this room. Only allow what you trust {origin} with.
</Text>
<Box as="ul" direction="Column" gap="200" style={{ margin: 0, padding: 0 }}>
{requests.map((r) => (
<Box
as="li"
key={r.capability}
alignItems="Center"
gap="200"
style={{ listStyle: 'none' }}
>
<Box as="label" alignItems="Center" gap="200" style={{ cursor: 'pointer' }}>
<Checkbox
size="300"
variant={r.sends ? 'Warning' : 'Primary'}
checked={checked.has(r.capability)}
onClick={(e: React.MouseEvent<HTMLInputElement>) =>
toggle(r.capability, e.currentTarget.checked)
}
/>
<Text size="T300">
{r.label}
{r.sends && (
<Text as="span" size="T200" style={{ color: color.Warning.Main }}>
{' '}
· as you
</Text>
)}
</Text>
</Box>
</Box>
))}
</Box>
<Box as="label" alignItems="Center" gap="200" style={{ cursor: 'pointer' }}>
<Checkbox
size="300"
variant="Secondary"
checked={remember}
onClick={(e: React.MouseEvent<HTMLInputElement>) =>
setRemember(e.currentTarget.checked)
}
/>
<Text size="T300">Remember my choice for this widget on this device</Text>
</Box>
<Box gap="200" justifyContent="End">
<Button
id="widget-perm-deny"
variant="Secondary"
fill="Soft"
onClick={() => onDone(new Set(), remember)}
>
<Text size="B400">Deny</Text>
</Button>
<Button variant="Primary" onClick={() => onDone(new Set(checked), remember)}>
<Text size="B400">
{checked.size === 0 ? 'Continue without access' : 'Allow selected'}
</Text>
</Button>
</Box>
</Box>
</Dialog>
</FocusTrap>
</OverlayCenter>
</Overlay>
);
}