157 lines
5.3 KiB
TypeScript
157 lines
5.3 KiB
TypeScript
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>
|
||
|
|
);
|
||
|
|
}
|