mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-11-06 07:20:29 +03:00
208 lines
6.4 KiB
TypeScript
208 lines
6.4 KiB
TypeScript
import React, { useCallback, useRef, useState, FormEventHandler, useEffect } from 'react';
|
|
import { MatrixError } from 'matrix-js-sdk';
|
|
import {
|
|
Box,
|
|
Chip,
|
|
Icon,
|
|
Icons,
|
|
IconButton,
|
|
Text,
|
|
config,
|
|
Button,
|
|
Spinner,
|
|
color,
|
|
TextArea as TextAreaComponent,
|
|
Input,
|
|
} from 'folds';
|
|
import { Page, PageHeader } from '../../../components/page';
|
|
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
|
import { useRoom } from '../../../hooks/useRoom';
|
|
import { useAlive } from '../../../hooks/useAlive';
|
|
import { useTextAreaCodeEditor } from '../../../hooks/useTextAreaCodeEditor';
|
|
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
|
import { syntaxErrorPosition } from '../../../utils/dom';
|
|
import { Cursor } from '../../../plugins/text-area';
|
|
|
|
const EDITOR_INTENT_SPACE_COUNT = 2;
|
|
|
|
export type SendRoomEventProps = {
|
|
type?: string;
|
|
stateKey?: string;
|
|
requestClose: () => void;
|
|
};
|
|
export function SendRoomEvent({ type, stateKey, requestClose }: SendRoomEventProps) {
|
|
const mx = useMatrixClient();
|
|
const room = useRoom();
|
|
const alive = useAlive();
|
|
const composeStateEvent = typeof stateKey === 'string';
|
|
|
|
const textAreaRef = useRef<HTMLTextAreaElement>(null);
|
|
const [jsonError, setJSONError] = useState<SyntaxError>();
|
|
const { handleKeyDown, operations, getTarget } = useTextAreaCodeEditor(
|
|
textAreaRef,
|
|
EDITOR_INTENT_SPACE_COUNT
|
|
);
|
|
|
|
const [submitState, submit] = useAsyncCallback<
|
|
object,
|
|
MatrixError,
|
|
[string, string | undefined, object]
|
|
>(
|
|
useCallback(
|
|
(evtType, evtStateKey, evtContent) => {
|
|
if (typeof evtStateKey === 'string') {
|
|
return mx.sendStateEvent(room.roomId, evtType as any, evtContent, evtStateKey);
|
|
}
|
|
return mx.sendEvent(room.roomId, evtType as any, evtContent);
|
|
},
|
|
[mx, room]
|
|
)
|
|
);
|
|
const submitting = submitState.status === AsyncStatus.Loading;
|
|
|
|
const handleSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
|
|
evt.preventDefault();
|
|
if (submitting) return;
|
|
|
|
const target = evt.target as HTMLFormElement | undefined;
|
|
const typeInput = target?.typeInput as HTMLInputElement | undefined;
|
|
const stateKeyInput = target?.stateKeyInput as HTMLInputElement | undefined;
|
|
const contentTextArea = target?.contentTextArea as HTMLTextAreaElement | undefined;
|
|
if (!typeInput || !contentTextArea) return;
|
|
|
|
const evtType = typeInput.value;
|
|
const evtStateKey = stateKeyInput?.value;
|
|
const contentStr = contentTextArea.value.trim();
|
|
|
|
let parsedContent: object;
|
|
try {
|
|
parsedContent = JSON.parse(contentStr);
|
|
} catch (e) {
|
|
setJSONError(e as SyntaxError);
|
|
return;
|
|
}
|
|
setJSONError(undefined);
|
|
|
|
if (parsedContent === null) {
|
|
return;
|
|
}
|
|
|
|
submit(evtType, evtStateKey, parsedContent).then(() => {
|
|
if (alive()) {
|
|
requestClose();
|
|
}
|
|
});
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (jsonError) {
|
|
const errorPosition = syntaxErrorPosition(jsonError) ?? 0;
|
|
const cursor = new Cursor(errorPosition, errorPosition, 'none');
|
|
operations.select(cursor);
|
|
getTarget()?.focus();
|
|
}
|
|
}, [jsonError, operations, getTarget]);
|
|
|
|
return (
|
|
<Page>
|
|
<PageHeader outlined={false} balance>
|
|
<Box alignItems="Center" grow="Yes" gap="200">
|
|
<Box alignItems="Inherit" grow="Yes" gap="200">
|
|
<Chip
|
|
size="500"
|
|
radii="Pill"
|
|
onClick={requestClose}
|
|
before={<Icon size="100" src={Icons.ArrowLeft} />}
|
|
>
|
|
<Text size="T300">Developer Tools</Text>
|
|
</Chip>
|
|
</Box>
|
|
<Box shrink="No">
|
|
<IconButton onClick={requestClose} variant="Surface">
|
|
<Icon src={Icons.Cross} />
|
|
</IconButton>
|
|
</Box>
|
|
</Box>
|
|
</PageHeader>
|
|
<Box grow="Yes" direction="Column">
|
|
<Box
|
|
as="form"
|
|
onSubmit={handleSubmit}
|
|
grow="Yes"
|
|
style={{ padding: config.space.S400 }}
|
|
direction="Column"
|
|
gap="400"
|
|
aria-disabled={submitting}
|
|
>
|
|
<Box shrink="No" direction="Column" gap="100">
|
|
<Text size="L400">{composeStateEvent ? 'State Event Type' : 'Message Event Type'}</Text>
|
|
<Box gap="300">
|
|
<Box grow="Yes" direction="Column">
|
|
<Input
|
|
variant="Background"
|
|
name="typeInput"
|
|
size="400"
|
|
radii="300"
|
|
readOnly={submitting}
|
|
defaultValue={type}
|
|
required
|
|
/>
|
|
</Box>
|
|
<Button
|
|
variant="Success"
|
|
size="400"
|
|
radii="300"
|
|
type="submit"
|
|
disabled={submitting}
|
|
before={submitting && <Spinner variant="Primary" fill="Solid" size="300" />}
|
|
>
|
|
<Text size="B400">Send</Text>
|
|
</Button>
|
|
</Box>
|
|
|
|
{submitState.status === AsyncStatus.Error && (
|
|
<Text size="T200" style={{ color: color.Critical.Main }}>
|
|
<b>{submitState.error.message}</b>
|
|
</Text>
|
|
)}
|
|
</Box>
|
|
{composeStateEvent && (
|
|
<Box shrink="No" direction="Column" gap="100">
|
|
<Text size="L400">State Key (Optional)</Text>
|
|
<Input
|
|
variant="Background"
|
|
name="stateKeyInput"
|
|
size="400"
|
|
radii="300"
|
|
readOnly={submitting}
|
|
defaultValue={stateKey}
|
|
/>
|
|
</Box>
|
|
)}
|
|
<Box grow="Yes" direction="Column" gap="100">
|
|
<Box shrink="No">
|
|
<Text size="L400">JSON Content</Text>
|
|
</Box>
|
|
<TextAreaComponent
|
|
ref={textAreaRef}
|
|
name="contentTextArea"
|
|
style={{ fontFamily: 'monospace' }}
|
|
onKeyDown={handleKeyDown}
|
|
resize="None"
|
|
spellCheck="false"
|
|
required
|
|
readOnly={submitting}
|
|
/>
|
|
{jsonError && (
|
|
<Text size="T200" style={{ color: color.Critical.Main }}>
|
|
<b>
|
|
{jsonError.name}: {jsonError.message}
|
|
</b>
|
|
</Text>
|
|
)}
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
</Page>
|
|
);
|
|
}
|