mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-11-11 01:30:29 +03:00
* WIP - add room settings dialog * join rule setting - WIP * show emojis & stickers in room settings - WIP * restyle join rule switcher * Merge branch 'dev' into new-room-settings * add join rule hook * open room settings from global state * open new room settings from all places * rearrange settings menu item * add option for creating new image pack * room devtools - WIP * render room state events as list * add option to open state event * add option to edit state event * refactor text area code editor into hook * add option to send message and state event * add cutout card component * add hook for room account data * display room account data - WIP * refactor global account data editor component * add account data editor in room * fix font style in devtool * show state events in compact form * add option to delete room image pack * add server badge component * add member tile component * render members in room settings * add search in room settings member * add option to reset member search * add filter in room members * fix member virtual item key * remove color from serve badge in room members * show room in settings * fix loading indicator position * power level tags in room setting - WIP * generate fallback tag in backward compatible way * add color picker * add powers editor - WIP * add props to stop adding emoji to recent usage * add beta feature notice badge * add types for power level tag icon * refactor image pack rooms code to hook * option for adding new power levels tags * remove console log * refactor power icon * add option to edit power level tags * remove power level from powers pill * fix power level labels * add option to delete power levels * fix long power level name shrinks power integer * room permissions - WIP * add power level selector component * add room permissions * move user default permission setting to other group * add power permission peek menu * fix weigh of power switch text * hide above for max power in permission switcher * improve beta badge description * render room profile in room settings * add option to edit room profile * make room topic input text area * add option to enable room encryption in room settings * add option to change message history visibility * add option to change join rule * add option for addresses in room settings * close encryption dialog after enabling
322 lines
8.5 KiB
TypeScript
322 lines
8.5 KiB
TypeScript
import React, { FormEventHandler, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
import {
|
|
Box,
|
|
Text,
|
|
Icon,
|
|
Icons,
|
|
IconButton,
|
|
Input,
|
|
Button,
|
|
TextArea as TextAreaComponent,
|
|
color,
|
|
Spinner,
|
|
Chip,
|
|
Scroll,
|
|
config,
|
|
} from 'folds';
|
|
import { MatrixError } from 'matrix-js-sdk';
|
|
import { Cursor } from '../plugins/text-area';
|
|
import { syntaxErrorPosition } from '../utils/dom';
|
|
import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback';
|
|
import { Page, PageHeader } from './page';
|
|
import { useAlive } from '../hooks/useAlive';
|
|
import { SequenceCard } from './sequence-card';
|
|
import { TextViewerContent } from './text-viewer';
|
|
import { useTextAreaCodeEditor } from '../hooks/useTextAreaCodeEditor';
|
|
|
|
const EDITOR_INTENT_SPACE_COUNT = 2;
|
|
|
|
export type AccountDataSubmitCallback = (type: string, content: object) => Promise<void>;
|
|
|
|
type AccountDataInfo = {
|
|
type: string;
|
|
content: object;
|
|
};
|
|
|
|
type AccountDataEditProps = {
|
|
type: string;
|
|
defaultContent: string;
|
|
submitChange: AccountDataSubmitCallback;
|
|
onCancel: () => void;
|
|
onSave: (info: AccountDataInfo) => void;
|
|
};
|
|
function AccountDataEdit({
|
|
type,
|
|
defaultContent,
|
|
submitChange,
|
|
onCancel,
|
|
onSave,
|
|
}: AccountDataEditProps) {
|
|
const alive = useAlive();
|
|
|
|
const textAreaRef = useRef<HTMLTextAreaElement>(null);
|
|
const [jsonError, setJSONError] = useState<SyntaxError>();
|
|
|
|
const { handleKeyDown, operations, getTarget } = useTextAreaCodeEditor(
|
|
textAreaRef,
|
|
EDITOR_INTENT_SPACE_COUNT
|
|
);
|
|
|
|
const [submitState, submit] = useAsyncCallback<void, MatrixError, [string, object]>(submitChange);
|
|
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 contentTextArea = target?.contentTextArea as HTMLTextAreaElement | undefined;
|
|
if (!typeInput || !contentTextArea) return;
|
|
|
|
const typeStr = typeInput.value.trim();
|
|
const contentStr = contentTextArea.value.trim();
|
|
|
|
let parsedContent: object;
|
|
try {
|
|
parsedContent = JSON.parse(contentStr);
|
|
} catch (e) {
|
|
setJSONError(e as SyntaxError);
|
|
return;
|
|
}
|
|
setJSONError(undefined);
|
|
|
|
if (
|
|
!typeStr ||
|
|
parsedContent === null ||
|
|
defaultContent === JSON.stringify(parsedContent, null, EDITOR_INTENT_SPACE_COUNT)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
submit(typeStr, parsedContent).then(() => {
|
|
if (alive()) {
|
|
onSave({
|
|
type: typeStr,
|
|
content: parsedContent,
|
|
});
|
|
}
|
|
});
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (jsonError) {
|
|
const errorPosition = syntaxErrorPosition(jsonError) ?? 0;
|
|
const cursor = new Cursor(errorPosition, errorPosition, 'none');
|
|
operations.select(cursor);
|
|
getTarget()?.focus();
|
|
}
|
|
}, [jsonError, operations, getTarget]);
|
|
|
|
return (
|
|
<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">Account Data</Text>
|
|
<Box gap="300">
|
|
<Box grow="Yes" direction="Column">
|
|
<Input
|
|
variant={type.length > 0 || submitting ? 'SurfaceVariant' : 'Background'}
|
|
name="typeInput"
|
|
size="400"
|
|
radii="300"
|
|
readOnly={type.length > 0 || 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">Save</Text>
|
|
</Button>
|
|
<Button
|
|
variant="Secondary"
|
|
fill="Soft"
|
|
size="400"
|
|
radii="300"
|
|
type="button"
|
|
onClick={onCancel}
|
|
disabled={submitting}
|
|
>
|
|
<Text size="B400">Cancel</Text>
|
|
</Button>
|
|
</Box>
|
|
|
|
{submitState.status === AsyncStatus.Error && (
|
|
<Text size="T200" style={{ color: color.Critical.Main }}>
|
|
<b>{submitState.error.message}</b>
|
|
</Text>
|
|
)}
|
|
</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}
|
|
defaultValue={defaultContent}
|
|
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>
|
|
);
|
|
}
|
|
|
|
type AccountDataViewProps = {
|
|
type: string;
|
|
defaultContent: string;
|
|
onEdit: () => void;
|
|
};
|
|
function AccountDataView({ type, defaultContent, onEdit }: AccountDataViewProps) {
|
|
return (
|
|
<Box
|
|
direction="Column"
|
|
style={{
|
|
padding: config.space.S400,
|
|
}}
|
|
gap="400"
|
|
>
|
|
<Box shrink="No" gap="300" alignItems="End">
|
|
<Box grow="Yes" direction="Column" gap="100">
|
|
<Text size="L400">Account Data</Text>
|
|
<Input
|
|
variant="SurfaceVariant"
|
|
size="400"
|
|
radii="300"
|
|
readOnly
|
|
defaultValue={type}
|
|
required
|
|
/>
|
|
</Box>
|
|
<Button variant="Secondary" size="400" radii="300" onClick={onEdit}>
|
|
<Text size="B400">Edit</Text>
|
|
</Button>
|
|
</Box>
|
|
<Box grow="Yes" direction="Column" gap="100">
|
|
<Text size="L400">JSON Content</Text>
|
|
<SequenceCard variant="SurfaceVariant">
|
|
<Scroll visibility="Always" size="300" hideTrack>
|
|
<TextViewerContent
|
|
size="T300"
|
|
style={{
|
|
padding: `${config.space.S300} ${config.space.S100} ${config.space.S300} ${config.space.S300}`,
|
|
}}
|
|
text={defaultContent}
|
|
langName="JSON"
|
|
/>
|
|
</Scroll>
|
|
</SequenceCard>
|
|
</Box>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
export type AccountDataEditorProps = {
|
|
type?: string;
|
|
content?: object;
|
|
submitChange: AccountDataSubmitCallback;
|
|
requestClose: () => void;
|
|
};
|
|
|
|
export function AccountDataEditor({
|
|
type,
|
|
content,
|
|
submitChange,
|
|
requestClose,
|
|
}: AccountDataEditorProps) {
|
|
const [data, setData] = useState<AccountDataInfo>({
|
|
type: type ?? '',
|
|
content: content ?? {},
|
|
});
|
|
|
|
const [edit, setEdit] = useState(!type);
|
|
|
|
const closeEdit = useCallback(() => {
|
|
if (!type) {
|
|
requestClose();
|
|
return;
|
|
}
|
|
setEdit(false);
|
|
}, [type, requestClose]);
|
|
|
|
const handleSave = useCallback((info: AccountDataInfo) => {
|
|
setData(info);
|
|
setEdit(false);
|
|
}, []);
|
|
|
|
const contentJSONStr = useMemo(
|
|
() => JSON.stringify(data.content, null, EDITOR_INTENT_SPACE_COUNT),
|
|
[data.content]
|
|
);
|
|
|
|
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">
|
|
{edit ? (
|
|
<AccountDataEdit
|
|
type={data.type}
|
|
defaultContent={contentJSONStr}
|
|
submitChange={submitChange}
|
|
onCancel={closeEdit}
|
|
onSave={handleSave}
|
|
/>
|
|
) : (
|
|
<AccountDataView
|
|
type={data.type}
|
|
defaultContent={contentJSONStr}
|
|
onEdit={() => setEdit(true)}
|
|
/>
|
|
)}
|
|
</Box>
|
|
</Page>
|
|
);
|
|
}
|