mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-11-16 12:10:28 +03:00
Add a panel in Developer Tools for editing profile fields
This commit is contained in:
parent
4e7b64eb5f
commit
5bc9654d32
5 changed files with 287 additions and 201 deletions
|
|
@ -27,6 +27,7 @@ import { useTextAreaCodeEditor } from '../hooks/useTextAreaCodeEditor';
|
|||
const EDITOR_INTENT_SPACE_COUNT = 2;
|
||||
|
||||
export type AccountDataSubmitCallback = (type: string, content: object) => Promise<void>;
|
||||
export type AccountDataDeleteCallback = (type: string) => Promise<void>;
|
||||
|
||||
type AccountDataInfo = {
|
||||
type: string;
|
||||
|
|
@ -83,8 +84,7 @@ function AccountDataEdit({
|
|||
|
||||
if (
|
||||
!typeStr ||
|
||||
parsedContent === null ||
|
||||
defaultContent === JSON.stringify(parsedContent, null, EDITOR_INTENT_SPACE_COUNT)
|
||||
parsedContent === null
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -121,7 +121,7 @@ function AccountDataEdit({
|
|||
aria-disabled={submitting}
|
||||
>
|
||||
<Box shrink="No" direction="Column" gap="100">
|
||||
<Text size="L400">Account Data</Text>
|
||||
<Text size="L400">Field Name</Text>
|
||||
<Box gap="300">
|
||||
<Box grow="Yes" direction="Column">
|
||||
<Input
|
||||
|
|
@ -196,8 +196,21 @@ type AccountDataViewProps = {
|
|||
type: string;
|
||||
defaultContent: string;
|
||||
onEdit: () => void;
|
||||
requestClose: () => void;
|
||||
submitDelete?: AccountDataDeleteCallback;
|
||||
};
|
||||
function AccountDataView({ type, defaultContent, onEdit }: AccountDataViewProps) {
|
||||
function AccountDataView({ type, defaultContent, onEdit, requestClose, submitDelete }: AccountDataViewProps) {
|
||||
const [deleteState, deleteCallback] = useAsyncCallback<void, MatrixError, []>(useCallback(
|
||||
async () => {
|
||||
if (submitDelete !== undefined) {
|
||||
await submitDelete(type);
|
||||
requestClose();
|
||||
}
|
||||
},
|
||||
[type, submitDelete, requestClose],
|
||||
));
|
||||
const deleting = deleteState.status === AsyncStatus.Loading;
|
||||
|
||||
return (
|
||||
<Box
|
||||
direction="Column"
|
||||
|
|
@ -208,7 +221,7 @@ function AccountDataView({ type, defaultContent, onEdit }: AccountDataViewProps)
|
|||
>
|
||||
<Box shrink="No" gap="300" alignItems="End">
|
||||
<Box grow="Yes" direction="Column" gap="100">
|
||||
<Text size="L400">Account Data</Text>
|
||||
<Text size="L400">Field Name</Text>
|
||||
<Input
|
||||
variant="SurfaceVariant"
|
||||
size="400"
|
||||
|
|
@ -221,6 +234,18 @@ function AccountDataView({ type, defaultContent, onEdit }: AccountDataViewProps)
|
|||
<Button variant="Secondary" size="400" radii="300" onClick={onEdit}>
|
||||
<Text size="B400">Edit</Text>
|
||||
</Button>
|
||||
{submitDelete && (
|
||||
<Button
|
||||
variant="Critical"
|
||||
size="400"
|
||||
radii="300"
|
||||
disabled={deleting}
|
||||
before={deleting && <Spinner variant="Critical" fill="Solid" size="300" />}
|
||||
onClick={deleteCallback}
|
||||
>
|
||||
<Text size="B400">Delete</Text>
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
<Box grow="Yes" direction="Column" gap="100">
|
||||
<Text size="L400">JSON Content</Text>
|
||||
|
|
@ -243,8 +268,9 @@ function AccountDataView({ type, defaultContent, onEdit }: AccountDataViewProps)
|
|||
|
||||
export type AccountDataEditorProps = {
|
||||
type?: string;
|
||||
content?: object;
|
||||
content?: unknown;
|
||||
submitChange: AccountDataSubmitCallback;
|
||||
submitDelete?: AccountDataDeleteCallback;
|
||||
requestClose: () => void;
|
||||
};
|
||||
|
||||
|
|
@ -252,6 +278,7 @@ export function AccountDataEditor({
|
|||
type,
|
||||
content,
|
||||
submitChange,
|
||||
submitDelete,
|
||||
requestClose,
|
||||
}: AccountDataEditorProps) {
|
||||
const [data, setData] = useState<AccountDataInfo>({
|
||||
|
|
@ -314,6 +341,8 @@ export function AccountDataEditor({
|
|||
type={data.type}
|
||||
defaultContent={contentJSONStr}
|
||||
onEdit={() => setEdit(true)}
|
||||
requestClose={requestClose}
|
||||
submitDelete={submitDelete}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ export function Profile() {
|
|||
// once the profile request completes
|
||||
await refreshExtendedProfile();
|
||||
|
||||
// synthesise a profile update for ourselves to update our name and avatr in the rest
|
||||
// synthesize a profile update for ourselves to update our name and avatar in the rest
|
||||
// of the UI. code copied from matrix-js-sdk
|
||||
const user = mx.getUser(userId);
|
||||
if (user) {
|
||||
|
|
|
|||
|
|
@ -1,100 +0,0 @@
|
|||
import React, { useCallback, useState } from 'react';
|
||||
import { Box, Text, Icon, Icons, Button, MenuItem } from 'folds';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { SequenceCardStyle } from '../styles.css';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { useAccountDataCallback } from '../../../hooks/useAccountDataCallback';
|
||||
import { CutoutCard } from '../../../components/cutout-card';
|
||||
|
||||
type AccountDataProps = {
|
||||
expand: boolean;
|
||||
onExpandToggle: (expand: boolean) => void;
|
||||
onSelect: (type: string | null) => void;
|
||||
};
|
||||
export function AccountData({ expand, onExpandToggle, onSelect }: AccountDataProps) {
|
||||
const mx = useMatrixClient();
|
||||
const [accountDataTypes, setAccountDataKeys] = useState(() =>
|
||||
Array.from(mx.store.accountData.keys())
|
||||
);
|
||||
|
||||
useAccountDataCallback(
|
||||
mx,
|
||||
useCallback(() => {
|
||||
setAccountDataKeys(Array.from(mx.store.accountData.keys()));
|
||||
}, [mx])
|
||||
);
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">Account Data</Text>
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
variant="SurfaceVariant"
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<SettingTile
|
||||
title="Global"
|
||||
description="Data stored in your global account data."
|
||||
after={
|
||||
<Button
|
||||
onClick={() => onExpandToggle(!expand)}
|
||||
variant="Secondary"
|
||||
fill="Soft"
|
||||
size="300"
|
||||
radii="300"
|
||||
outlined
|
||||
before={
|
||||
<Icon src={expand ? Icons.ChevronTop : Icons.ChevronBottom} size="100" filled />
|
||||
}
|
||||
>
|
||||
<Text size="B300">{expand ? 'Collapse' : 'Expand'}</Text>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
{expand && (
|
||||
<Box direction="Column" gap="100">
|
||||
<Box justifyContent="SpaceBetween">
|
||||
<Text size="L400">Events</Text>
|
||||
<Text size="L400">Total: {accountDataTypes.length}</Text>
|
||||
</Box>
|
||||
<CutoutCard>
|
||||
<MenuItem
|
||||
variant="Surface"
|
||||
fill="None"
|
||||
size="300"
|
||||
radii="0"
|
||||
before={<Icon size="50" src={Icons.Plus} />}
|
||||
onClick={() => onSelect(null)}
|
||||
>
|
||||
<Box grow="Yes">
|
||||
<Text size="T200" truncate>
|
||||
Add New
|
||||
</Text>
|
||||
</Box>
|
||||
</MenuItem>
|
||||
{accountDataTypes.sort().map((type) => (
|
||||
<MenuItem
|
||||
key={type}
|
||||
variant="Surface"
|
||||
fill="None"
|
||||
size="300"
|
||||
radii="0"
|
||||
after={<Icon size="50" src={Icons.ChevronRight} />}
|
||||
onClick={() => onSelect(type)}
|
||||
>
|
||||
<Box grow="Yes">
|
||||
<Text size="T200" truncate>
|
||||
{type}
|
||||
</Text>
|
||||
</Box>
|
||||
</MenuItem>
|
||||
))}
|
||||
</CutoutCard>
|
||||
</Box>
|
||||
)}
|
||||
</SequenceCard>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
import React from 'react';
|
||||
import { Box, Text, Icon, Icons, Button, MenuItem } from 'folds';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { SequenceCardStyle } from '../styles.css';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
import { CutoutCard } from '../../../components/cutout-card';
|
||||
|
||||
type AccountDataListProps = {
|
||||
title?: string;
|
||||
description?: string;
|
||||
expand: boolean;
|
||||
setExpand: (expand: boolean) => void;
|
||||
types: string[];
|
||||
onSelect: (type: string | null) => void;
|
||||
};
|
||||
export function AccountDataList({ types, onSelect, expand, setExpand, title, description }: AccountDataListProps) {
|
||||
return (
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
variant="SurfaceVariant"
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<SettingTile
|
||||
title={title}
|
||||
description={description}
|
||||
after={
|
||||
<Button
|
||||
onClick={() => setExpand(!expand)}
|
||||
variant="Secondary"
|
||||
fill="Soft"
|
||||
size="300"
|
||||
radii="300"
|
||||
outlined
|
||||
before={
|
||||
<Icon src={expand ? Icons.ChevronTop : Icons.ChevronBottom} size="100" filled />
|
||||
}
|
||||
>
|
||||
<Text size="B300">{expand ? 'Collapse' : 'Expand'}</Text>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
{expand && (
|
||||
<Box direction="Column" gap="100">
|
||||
<Box justifyContent="SpaceBetween">
|
||||
<Text size="L400">Fields</Text>
|
||||
<Text size="L400">Total: {types.length}</Text>
|
||||
</Box>
|
||||
<CutoutCard>
|
||||
<MenuItem
|
||||
variant="Surface"
|
||||
fill="None"
|
||||
size="300"
|
||||
radii="0"
|
||||
before={<Icon size="50" src={Icons.Plus} />}
|
||||
onClick={() => onSelect(null)}
|
||||
>
|
||||
<Box grow="Yes">
|
||||
<Text size="T200" truncate>
|
||||
Add New
|
||||
</Text>
|
||||
</Box>
|
||||
</MenuItem>
|
||||
{types.sort().map((type) => (
|
||||
<MenuItem
|
||||
key={type}
|
||||
variant="Surface"
|
||||
fill="None"
|
||||
size="300"
|
||||
radii="0"
|
||||
after={<Icon size="50" src={Icons.ChevronRight} />}
|
||||
onClick={() => onSelect(type)}
|
||||
>
|
||||
<Box grow="Yes">
|
||||
<Text size="T200" truncate>
|
||||
{type}
|
||||
</Text>
|
||||
</Box>
|
||||
</MenuItem>
|
||||
))}
|
||||
</CutoutCard>
|
||||
</Box>
|
||||
)}
|
||||
</SequenceCard>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useCallback, useState } from 'react';
|
||||
import { Box, Text, IconButton, Icon, Icons, Scroll, Switch, Button } from 'folds';
|
||||
import { AccountDataEvents } from 'matrix-js-sdk';
|
||||
import { Page, PageContent, PageHeader } from '../../../components/page';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { SequenceCardStyle } from '../styles.css';
|
||||
|
|
@ -8,39 +9,92 @@ import { useSetting } from '../../../state/hooks/settings';
|
|||
import { settingsAtom } from '../../../state/settings';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import {
|
||||
AccountDataDeleteCallback,
|
||||
AccountDataEditor,
|
||||
AccountDataSubmitCallback,
|
||||
} from '../../../components/AccountDataEditor';
|
||||
import { copyToClipboard } from '../../../utils/dom';
|
||||
import { AccountData } from './AccountData';
|
||||
import { AccountDataList } from './AccountDataList';
|
||||
import { useExtendedProfile } from '../../../hooks/useExtendedProfile';
|
||||
import { useAccountDataCallback } from '../../../hooks/useAccountDataCallback';
|
||||
|
||||
type DeveloperToolsPage =
|
||||
| { name: 'index' }
|
||||
| { name: 'account-data'; type: string | null }
|
||||
| { name: 'profile-field'; type: string | null };
|
||||
|
||||
type DeveloperToolsProps = {
|
||||
requestClose: () => void;
|
||||
};
|
||||
export function DeveloperTools({ requestClose }: DeveloperToolsProps) {
|
||||
const mx = useMatrixClient();
|
||||
const userId = mx.getUserId() as string;
|
||||
|
||||
const [accountDataTypes, setAccountDataKeys] = useState(() =>
|
||||
Array.from(mx.store.accountData.keys())
|
||||
);
|
||||
|
||||
useAccountDataCallback(
|
||||
mx,
|
||||
useCallback(() => {
|
||||
setAccountDataKeys(Array.from(mx.store.accountData.keys()));
|
||||
}, [mx])
|
||||
);
|
||||
const [extendedProfile, refreshExtendedProfile] = useExtendedProfile(userId);
|
||||
|
||||
const [developerTools, setDeveloperTools] = useSetting(settingsAtom, 'developerTools');
|
||||
const [expand, setExpend] = useState(false);
|
||||
const [accountDataType, setAccountDataType] = useState<string | null>();
|
||||
const [page, setPage] = useState<DeveloperToolsPage>({ name: 'index' });
|
||||
const [globalExpand, setGlobalExpand] = useState(false);
|
||||
const [profileExpand, setProfileExpand] = useState(false);
|
||||
|
||||
const submitAccountData: AccountDataSubmitCallback = useCallback(
|
||||
async (type, content) => {
|
||||
await mx.setAccountData(type, content);
|
||||
await mx.setAccountData(type as keyof AccountDataEvents, content);
|
||||
},
|
||||
[mx]
|
||||
);
|
||||
|
||||
if (accountDataType !== undefined) {
|
||||
const submitProfileField: AccountDataSubmitCallback = useCallback(
|
||||
async (type, content) => {
|
||||
await mx.setExtendedProfileProperty(type, content);
|
||||
await refreshExtendedProfile();
|
||||
},
|
||||
[mx, refreshExtendedProfile]
|
||||
);
|
||||
|
||||
const deleteProfileField: AccountDataDeleteCallback = useCallback(
|
||||
async (type) => {
|
||||
await mx.deleteExtendedProfileProperty(type);
|
||||
await refreshExtendedProfile();
|
||||
},
|
||||
[mx, refreshExtendedProfile]
|
||||
);
|
||||
|
||||
const handleClose = useCallback(() => setPage({ name: 'index' }), [setPage]);
|
||||
|
||||
switch (page.name) {
|
||||
case 'account-data':
|
||||
return (
|
||||
<AccountDataEditor
|
||||
type={accountDataType ?? undefined}
|
||||
content={accountDataType ? mx.getAccountData(accountDataType)?.getContent() : undefined}
|
||||
type={page.type ?? undefined}
|
||||
content={page.type ? mx.getAccountData(page.type as keyof AccountDataEvents)?.getContent() : undefined}
|
||||
submitChange={submitAccountData}
|
||||
requestClose={() => setAccountDataType(undefined)}
|
||||
requestClose={handleClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
case 'profile-field':
|
||||
return (
|
||||
<AccountDataEditor
|
||||
type={page.type ?? undefined}
|
||||
content={page.type ? extendedProfile?.[page.type] : undefined}
|
||||
submitChange={submitProfileField}
|
||||
submitDelete={deleteProfileField}
|
||||
requestClose={handleClose}
|
||||
/>
|
||||
);
|
||||
|
||||
default:
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader outlined={false}>
|
||||
|
|
@ -109,11 +163,27 @@ export function DeveloperTools({ requestClose }: DeveloperToolsProps) {
|
|||
)}
|
||||
</Box>
|
||||
{developerTools && (
|
||||
<AccountData
|
||||
expand={expand}
|
||||
onExpandToggle={setExpend}
|
||||
onSelect={setAccountDataType}
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">Account Data</Text>
|
||||
<AccountDataList
|
||||
title="Account"
|
||||
description="Private data stored in your account."
|
||||
expand={globalExpand}
|
||||
setExpand={setGlobalExpand}
|
||||
types={accountDataTypes}
|
||||
onSelect={(type) => setPage({ name: 'account-data', type })}
|
||||
/>
|
||||
{extendedProfile && (
|
||||
<AccountDataList
|
||||
title="Profile"
|
||||
description="Public data attached to your Matrix profile."
|
||||
expand={profileExpand}
|
||||
setExpand={setProfileExpand}
|
||||
types={Object.keys(extendedProfile)}
|
||||
onSelect={(type) => setPage({ name: 'profile-field', type })}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</PageContent>
|
||||
|
|
@ -122,3 +192,4 @@ export function DeveloperTools({ requestClose }: DeveloperToolsProps) {
|
|||
</Page>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue