mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-11-14 19:20:28 +03:00
Redesign space/room creation panel (#2408)
* add new create room * rename create room modal file * default restrict access for space children in room create modal * move create room kind selector to components * add radii variant to sequence card component * more more reusable create room logic to components * add create space * update address input description * add new space modal * fix add room button visible on left room in space lobby
This commit is contained in:
parent
e9798a22c3
commit
faa952295f
33 changed files with 1637 additions and 53 deletions
|
|
@ -27,6 +27,11 @@ import {
|
|||
} from '../../../state/hooks/roomList';
|
||||
import { allRoomsAtom } from '../../../state/room-list/roomList';
|
||||
import { roomToParentsAtom } from '../../../state/room/roomToParents';
|
||||
import {
|
||||
knockRestrictedSupported,
|
||||
knockSupported,
|
||||
restrictedSupported,
|
||||
} from '../../../utils/matrix';
|
||||
|
||||
type RestrictedRoomAllowContent = {
|
||||
room_id: string;
|
||||
|
|
@ -39,10 +44,9 @@ type RoomJoinRulesProps = {
|
|||
export function RoomJoinRules({ powerLevels }: RoomJoinRulesProps) {
|
||||
const mx = useMatrixClient();
|
||||
const room = useRoom();
|
||||
const roomVersion = parseInt(room.getVersion(), 10);
|
||||
const allowKnockRestricted = roomVersion >= 10;
|
||||
const allowRestricted = roomVersion >= 8;
|
||||
const allowKnock = roomVersion >= 7;
|
||||
const allowKnockRestricted = knockRestrictedSupported(room.getVersion());
|
||||
const allowRestricted = restrictedSupported(room.getVersion());
|
||||
const allowKnock = knockSupported(room.getVersion());
|
||||
|
||||
const roomIdToParents = useAtomValue(roomToParentsAtom);
|
||||
const space = useSpaceOptionally();
|
||||
|
|
|
|||
277
src/app/features/create-room/CreateRoom.tsx
Normal file
277
src/app/features/create-room/CreateRoom.tsx
Normal file
|
|
@ -0,0 +1,277 @@
|
|||
import React, { FormEventHandler, useCallback, useState } from 'react';
|
||||
import { MatrixError, Room } from 'matrix-js-sdk';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Chip,
|
||||
color,
|
||||
config,
|
||||
Icon,
|
||||
Icons,
|
||||
Input,
|
||||
Spinner,
|
||||
Switch,
|
||||
Text,
|
||||
TextArea,
|
||||
} from 'folds';
|
||||
import { SettingTile } from '../../components/setting-tile';
|
||||
import { SequenceCard } from '../../components/sequence-card';
|
||||
import { knockRestrictedSupported, knockSupported, restrictedSupported } from '../../utils/matrix';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { millisecondsToMinutes, replaceSpaceWithDash } from '../../utils/common';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||
import { useCapabilities } from '../../hooks/useCapabilities';
|
||||
import { useAlive } from '../../hooks/useAlive';
|
||||
import { ErrorCode } from '../../cs-errorcode';
|
||||
import {
|
||||
createRoom,
|
||||
CreateRoomAliasInput,
|
||||
CreateRoomData,
|
||||
CreateRoomKind,
|
||||
CreateRoomKindSelector,
|
||||
RoomVersionSelector,
|
||||
} from '../../components/create-room';
|
||||
|
||||
const getCreateRoomKindToIcon = (kind: CreateRoomKind) => {
|
||||
if (kind === CreateRoomKind.Private) return Icons.HashLock;
|
||||
if (kind === CreateRoomKind.Restricted) return Icons.Hash;
|
||||
return Icons.HashGlobe;
|
||||
};
|
||||
|
||||
type CreateRoomFormProps = {
|
||||
defaultKind?: CreateRoomKind;
|
||||
space?: Room;
|
||||
onCreate?: (roomId: string) => void;
|
||||
};
|
||||
export function CreateRoomForm({ defaultKind, space, onCreate }: CreateRoomFormProps) {
|
||||
const mx = useMatrixClient();
|
||||
const alive = useAlive();
|
||||
|
||||
const capabilities = useCapabilities();
|
||||
const roomVersions = capabilities['m.room_versions'];
|
||||
const [selectedRoomVersion, selectRoomVersion] = useState(roomVersions?.default ?? '1');
|
||||
|
||||
const allowRestricted = space && restrictedSupported(selectedRoomVersion);
|
||||
|
||||
const [kind, setKind] = useState(
|
||||
defaultKind ?? allowRestricted ? CreateRoomKind.Restricted : CreateRoomKind.Private
|
||||
);
|
||||
const [federation, setFederation] = useState(true);
|
||||
const [encryption, setEncryption] = useState(false);
|
||||
const [knock, setKnock] = useState(false);
|
||||
const [advance, setAdvance] = useState(false);
|
||||
|
||||
const allowKnock = kind === CreateRoomKind.Private && knockSupported(selectedRoomVersion);
|
||||
const allowKnockRestricted =
|
||||
kind === CreateRoomKind.Restricted && knockRestrictedSupported(selectedRoomVersion);
|
||||
|
||||
const handleRoomVersionChange = (version: string) => {
|
||||
if (!restrictedSupported(version)) {
|
||||
setKind(CreateRoomKind.Private);
|
||||
}
|
||||
selectRoomVersion(version);
|
||||
};
|
||||
|
||||
const [createState, create] = useAsyncCallback<string, Error | MatrixError, [CreateRoomData]>(
|
||||
useCallback((data) => createRoom(mx, data), [mx])
|
||||
);
|
||||
const loading = createState.status === AsyncStatus.Loading;
|
||||
const error = createState.status === AsyncStatus.Error ? createState.error : undefined;
|
||||
const disabled = createState.status === AsyncStatus.Loading;
|
||||
|
||||
const handleSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
|
||||
evt.preventDefault();
|
||||
if (disabled) return;
|
||||
const form = evt.currentTarget;
|
||||
|
||||
const nameInput = form.nameInput as HTMLInputElement | undefined;
|
||||
const topicTextArea = form.topicTextAria as HTMLTextAreaElement | undefined;
|
||||
const aliasInput = form.aliasInput as HTMLInputElement | undefined;
|
||||
const roomName = nameInput?.value.trim();
|
||||
const roomTopic = topicTextArea?.value.trim();
|
||||
const aliasLocalPart =
|
||||
aliasInput && aliasInput.value ? replaceSpaceWithDash(aliasInput.value) : undefined;
|
||||
|
||||
if (!roomName) return;
|
||||
const publicRoom = kind === CreateRoomKind.Public;
|
||||
let roomKnock = false;
|
||||
if (allowKnock && kind === CreateRoomKind.Private) {
|
||||
roomKnock = knock;
|
||||
}
|
||||
if (allowKnockRestricted && kind === CreateRoomKind.Restricted) {
|
||||
roomKnock = knock;
|
||||
}
|
||||
|
||||
create({
|
||||
version: selectedRoomVersion,
|
||||
parent: space,
|
||||
kind,
|
||||
name: roomName,
|
||||
topic: roomTopic || undefined,
|
||||
aliasLocalPart: publicRoom ? aliasLocalPart : undefined,
|
||||
encryption: publicRoom ? false : encryption,
|
||||
knock: roomKnock,
|
||||
allowFederation: federation,
|
||||
}).then((roomId) => {
|
||||
if (alive()) {
|
||||
onCreate?.(roomId);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Box as="form" onSubmit={handleSubmit} grow="Yes" direction="Column" gap="500">
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">Access</Text>
|
||||
<CreateRoomKindSelector
|
||||
value={kind}
|
||||
onSelect={setKind}
|
||||
canRestrict={allowRestricted}
|
||||
disabled={disabled}
|
||||
getIcon={getCreateRoomKindToIcon}
|
||||
/>
|
||||
</Box>
|
||||
<Box shrink="No" direction="Column" gap="100">
|
||||
<Text size="L400">Name</Text>
|
||||
<Input
|
||||
required
|
||||
before={<Icon size="100" src={getCreateRoomKindToIcon(kind)} />}
|
||||
name="nameInput"
|
||||
autoFocus
|
||||
size="500"
|
||||
variant="SurfaceVariant"
|
||||
radii="400"
|
||||
autoComplete="off"
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Box>
|
||||
<Box shrink="No" direction="Column" gap="100">
|
||||
<Text size="L400">Topic (Optional)</Text>
|
||||
<TextArea
|
||||
name="topicTextAria"
|
||||
size="500"
|
||||
variant="SurfaceVariant"
|
||||
radii="400"
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{kind === CreateRoomKind.Public && <CreateRoomAliasInput disabled={disabled} />}
|
||||
|
||||
<Box shrink="No" direction="Column" gap="100">
|
||||
<Box gap="200" alignItems="End">
|
||||
<Text size="L400">Options</Text>
|
||||
<Box grow="Yes" justifyContent="End">
|
||||
<Chip
|
||||
radii="Pill"
|
||||
before={<Icon src={advance ? Icons.ChevronTop : Icons.ChevronBottom} size="50" />}
|
||||
onClick={() => setAdvance(!advance)}
|
||||
type="button"
|
||||
>
|
||||
<Text size="T200">Advance Options</Text>
|
||||
</Chip>
|
||||
</Box>
|
||||
</Box>
|
||||
{kind !== CreateRoomKind.Public && (
|
||||
<>
|
||||
<SequenceCard
|
||||
style={{ padding: config.space.S300 }}
|
||||
variant="SurfaceVariant"
|
||||
direction="Column"
|
||||
gap="500"
|
||||
>
|
||||
<SettingTile
|
||||
title="End-to-End Encryption"
|
||||
description="Once this feature is enabled, it can't be disabled after the room is created."
|
||||
after={
|
||||
<Switch
|
||||
variant="Primary"
|
||||
value={encryption}
|
||||
onChange={setEncryption}
|
||||
disabled={disabled}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SequenceCard>
|
||||
{advance && (allowKnock || allowKnockRestricted) && (
|
||||
<SequenceCard
|
||||
style={{ padding: config.space.S300 }}
|
||||
variant="SurfaceVariant"
|
||||
direction="Column"
|
||||
gap="500"
|
||||
>
|
||||
<SettingTile
|
||||
title="Knock to Join"
|
||||
description="Anyone can send request to join this room."
|
||||
after={
|
||||
<Switch
|
||||
variant="Primary"
|
||||
value={knock}
|
||||
onChange={setKnock}
|
||||
disabled={disabled}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SequenceCard>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<SequenceCard
|
||||
style={{ padding: config.space.S300 }}
|
||||
variant="SurfaceVariant"
|
||||
direction="Column"
|
||||
gap="500"
|
||||
>
|
||||
<SettingTile
|
||||
title="Allow Federation"
|
||||
description="Users from other servers can join."
|
||||
after={
|
||||
<Switch
|
||||
variant="Primary"
|
||||
value={federation}
|
||||
onChange={setFederation}
|
||||
disabled={disabled}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SequenceCard>
|
||||
{advance && (
|
||||
<RoomVersionSelector
|
||||
versions={roomVersions?.available ? Object.keys(roomVersions.available) : ['1']}
|
||||
value={selectedRoomVersion}
|
||||
onChange={handleRoomVersionChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{error && (
|
||||
<Box style={{ color: color.Critical.Main }} alignItems="Center" gap="200">
|
||||
<Icon src={Icons.Warning} filled size="100" />
|
||||
<Text size="T300" style={{ color: color.Critical.Main }}>
|
||||
<b>
|
||||
{error instanceof MatrixError && error.name === ErrorCode.M_LIMIT_EXCEEDED
|
||||
? `Server rate-limited your request for ${millisecondsToMinutes(
|
||||
(error.data.retry_after_ms as number | undefined) ?? 0
|
||||
)} minutes!`
|
||||
: error.message}
|
||||
</b>
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
<Box shrink="No" direction="Column" gap="200">
|
||||
<Button
|
||||
type="submit"
|
||||
size="500"
|
||||
variant="Primary"
|
||||
radii="400"
|
||||
disabled={disabled}
|
||||
before={loading && <Spinner variant="Primary" fill="Solid" size="200" />}
|
||||
>
|
||||
<Text size="B500">Create</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
95
src/app/features/create-room/CreateRoomModal.tsx
Normal file
95
src/app/features/create-room/CreateRoomModal.tsx
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
config,
|
||||
Header,
|
||||
Icon,
|
||||
IconButton,
|
||||
Icons,
|
||||
Modal,
|
||||
Overlay,
|
||||
OverlayBackdrop,
|
||||
OverlayCenter,
|
||||
Scroll,
|
||||
Text,
|
||||
} from 'folds';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import { useAllJoinedRoomsSet, useGetRoom } from '../../hooks/useGetRoom';
|
||||
import { SpaceProvider } from '../../hooks/useSpace';
|
||||
import { CreateRoomForm } from './CreateRoom';
|
||||
import {
|
||||
useCloseCreateRoomModal,
|
||||
useCreateRoomModalState,
|
||||
} from '../../state/hooks/createRoomModal';
|
||||
import { CreateRoomModalState } from '../../state/createRoomModal';
|
||||
import { stopPropagation } from '../../utils/keyboard';
|
||||
|
||||
type CreateRoomModalProps = {
|
||||
state: CreateRoomModalState;
|
||||
};
|
||||
function CreateRoomModal({ state }: CreateRoomModalProps) {
|
||||
const { spaceId } = state;
|
||||
const closeDialog = useCloseCreateRoomModal();
|
||||
|
||||
const allJoinedRooms = useAllJoinedRoomsSet();
|
||||
const getRoom = useGetRoom(allJoinedRooms);
|
||||
const space = spaceId ? getRoom(spaceId) : undefined;
|
||||
|
||||
return (
|
||||
<SpaceProvider value={space ?? null}>
|
||||
<Overlay open backdrop={<OverlayBackdrop />}>
|
||||
<OverlayCenter>
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
clickOutsideDeactivates: true,
|
||||
onDeactivate: closeDialog,
|
||||
escapeDeactivates: stopPropagation,
|
||||
}}
|
||||
>
|
||||
<Modal size="300" flexHeight>
|
||||
<Box direction="Column">
|
||||
<Header
|
||||
size="500"
|
||||
style={{
|
||||
padding: config.space.S200,
|
||||
paddingLeft: config.space.S400,
|
||||
borderBottomWidth: config.borderWidth.B300,
|
||||
}}
|
||||
>
|
||||
<Box grow="Yes">
|
||||
<Text size="H4">New Room</Text>
|
||||
</Box>
|
||||
<Box shrink="No">
|
||||
<IconButton size="300" radii="300" onClick={closeDialog}>
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Header>
|
||||
<Scroll size="300" hideTrack>
|
||||
<Box
|
||||
style={{
|
||||
padding: config.space.S400,
|
||||
paddingRight: config.space.S200,
|
||||
}}
|
||||
direction="Column"
|
||||
gap="500"
|
||||
>
|
||||
<CreateRoomForm space={space} onCreate={closeDialog} />
|
||||
</Box>
|
||||
</Scroll>
|
||||
</Box>
|
||||
</Modal>
|
||||
</FocusTrap>
|
||||
</OverlayCenter>
|
||||
</Overlay>
|
||||
</SpaceProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export function CreateRoomModalRenderer() {
|
||||
const state = useCreateRoomModalState();
|
||||
|
||||
if (!state) return null;
|
||||
return <CreateRoomModal state={state} />;
|
||||
}
|
||||
2
src/app/features/create-room/index.ts
Normal file
2
src/app/features/create-room/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from './CreateRoom';
|
||||
export * from './CreateRoomModal';
|
||||
249
src/app/features/create-space/CreateSpace.tsx
Normal file
249
src/app/features/create-space/CreateSpace.tsx
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
import React, { FormEventHandler, useCallback, useState } from 'react';
|
||||
import { MatrixError, Room } from 'matrix-js-sdk';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Chip,
|
||||
color,
|
||||
config,
|
||||
Icon,
|
||||
Icons,
|
||||
Input,
|
||||
Spinner,
|
||||
Switch,
|
||||
Text,
|
||||
TextArea,
|
||||
} from 'folds';
|
||||
import { SettingTile } from '../../components/setting-tile';
|
||||
import { SequenceCard } from '../../components/sequence-card';
|
||||
import { knockRestrictedSupported, knockSupported, restrictedSupported } from '../../utils/matrix';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { millisecondsToMinutes, replaceSpaceWithDash } from '../../utils/common';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||
import { useCapabilities } from '../../hooks/useCapabilities';
|
||||
import { useAlive } from '../../hooks/useAlive';
|
||||
import { ErrorCode } from '../../cs-errorcode';
|
||||
import {
|
||||
createRoom,
|
||||
CreateRoomAliasInput,
|
||||
CreateRoomData,
|
||||
CreateRoomKind,
|
||||
CreateRoomKindSelector,
|
||||
RoomVersionSelector,
|
||||
} from '../../components/create-room';
|
||||
import { RoomType } from '../../../types/matrix/room';
|
||||
|
||||
const getCreateSpaceKindToIcon = (kind: CreateRoomKind) => {
|
||||
if (kind === CreateRoomKind.Private) return Icons.SpaceLock;
|
||||
if (kind === CreateRoomKind.Restricted) return Icons.Space;
|
||||
return Icons.SpaceGlobe;
|
||||
};
|
||||
|
||||
type CreateSpaceFormProps = {
|
||||
defaultKind?: CreateRoomKind;
|
||||
space?: Room;
|
||||
onCreate?: (roomId: string) => void;
|
||||
};
|
||||
export function CreateSpaceForm({ defaultKind, space, onCreate }: CreateSpaceFormProps) {
|
||||
const mx = useMatrixClient();
|
||||
const alive = useAlive();
|
||||
|
||||
const capabilities = useCapabilities();
|
||||
const roomVersions = capabilities['m.room_versions'];
|
||||
const [selectedRoomVersion, selectRoomVersion] = useState(roomVersions?.default ?? '1');
|
||||
|
||||
const allowRestricted = space && restrictedSupported(selectedRoomVersion);
|
||||
|
||||
const [kind, setKind] = useState(
|
||||
defaultKind ?? allowRestricted ? CreateRoomKind.Restricted : CreateRoomKind.Private
|
||||
);
|
||||
const [federation, setFederation] = useState(true);
|
||||
const [knock, setKnock] = useState(false);
|
||||
const [advance, setAdvance] = useState(false);
|
||||
|
||||
const allowKnock = kind === CreateRoomKind.Private && knockSupported(selectedRoomVersion);
|
||||
const allowKnockRestricted =
|
||||
kind === CreateRoomKind.Restricted && knockRestrictedSupported(selectedRoomVersion);
|
||||
|
||||
const handleRoomVersionChange = (version: string) => {
|
||||
if (!restrictedSupported(version)) {
|
||||
setKind(CreateRoomKind.Private);
|
||||
}
|
||||
selectRoomVersion(version);
|
||||
};
|
||||
|
||||
const [createState, create] = useAsyncCallback<string, Error | MatrixError, [CreateRoomData]>(
|
||||
useCallback((data) => createRoom(mx, data), [mx])
|
||||
);
|
||||
const loading = createState.status === AsyncStatus.Loading;
|
||||
const error = createState.status === AsyncStatus.Error ? createState.error : undefined;
|
||||
const disabled = createState.status === AsyncStatus.Loading;
|
||||
|
||||
const handleSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
|
||||
evt.preventDefault();
|
||||
if (disabled) return;
|
||||
const form = evt.currentTarget;
|
||||
|
||||
const nameInput = form.nameInput as HTMLInputElement | undefined;
|
||||
const topicTextArea = form.topicTextAria as HTMLTextAreaElement | undefined;
|
||||
const aliasInput = form.aliasInput as HTMLInputElement | undefined;
|
||||
const roomName = nameInput?.value.trim();
|
||||
const roomTopic = topicTextArea?.value.trim();
|
||||
const aliasLocalPart =
|
||||
aliasInput && aliasInput.value ? replaceSpaceWithDash(aliasInput.value) : undefined;
|
||||
|
||||
if (!roomName) return;
|
||||
const publicRoom = kind === CreateRoomKind.Public;
|
||||
let roomKnock = false;
|
||||
if (allowKnock && kind === CreateRoomKind.Private) {
|
||||
roomKnock = knock;
|
||||
}
|
||||
if (allowKnockRestricted && kind === CreateRoomKind.Restricted) {
|
||||
roomKnock = knock;
|
||||
}
|
||||
|
||||
create({
|
||||
version: selectedRoomVersion,
|
||||
type: RoomType.Space,
|
||||
parent: space,
|
||||
kind,
|
||||
name: roomName,
|
||||
topic: roomTopic || undefined,
|
||||
aliasLocalPart: publicRoom ? aliasLocalPart : undefined,
|
||||
knock: roomKnock,
|
||||
allowFederation: federation,
|
||||
}).then((roomId) => {
|
||||
if (alive()) {
|
||||
onCreate?.(roomId);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Box as="form" onSubmit={handleSubmit} grow="Yes" direction="Column" gap="500">
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">Access</Text>
|
||||
<CreateRoomKindSelector
|
||||
value={kind}
|
||||
onSelect={setKind}
|
||||
canRestrict={allowRestricted}
|
||||
disabled={disabled}
|
||||
getIcon={getCreateSpaceKindToIcon}
|
||||
/>
|
||||
</Box>
|
||||
<Box shrink="No" direction="Column" gap="100">
|
||||
<Text size="L400">Name</Text>
|
||||
<Input
|
||||
required
|
||||
before={<Icon size="100" src={getCreateSpaceKindToIcon(kind)} />}
|
||||
name="nameInput"
|
||||
autoFocus
|
||||
size="500"
|
||||
variant="SurfaceVariant"
|
||||
radii="400"
|
||||
autoComplete="off"
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Box>
|
||||
<Box shrink="No" direction="Column" gap="100">
|
||||
<Text size="L400">Topic (Optional)</Text>
|
||||
<TextArea
|
||||
name="topicTextAria"
|
||||
size="500"
|
||||
variant="SurfaceVariant"
|
||||
radii="400"
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{kind === CreateRoomKind.Public && <CreateRoomAliasInput disabled={disabled} />}
|
||||
|
||||
<Box shrink="No" direction="Column" gap="100">
|
||||
<Box gap="200" alignItems="End">
|
||||
<Text size="L400">Options</Text>
|
||||
<Box grow="Yes" justifyContent="End">
|
||||
<Chip
|
||||
radii="Pill"
|
||||
before={<Icon src={advance ? Icons.ChevronTop : Icons.ChevronBottom} size="50" />}
|
||||
onClick={() => setAdvance(!advance)}
|
||||
type="button"
|
||||
>
|
||||
<Text size="T200">Advance Options</Text>
|
||||
</Chip>
|
||||
</Box>
|
||||
</Box>
|
||||
{kind !== CreateRoomKind.Public && advance && (allowKnock || allowKnockRestricted) && (
|
||||
<SequenceCard
|
||||
style={{ padding: config.space.S300 }}
|
||||
variant="SurfaceVariant"
|
||||
direction="Column"
|
||||
gap="500"
|
||||
>
|
||||
<SettingTile
|
||||
title="Knock to Join"
|
||||
description="Anyone can send request to join this space."
|
||||
after={
|
||||
<Switch variant="Primary" value={knock} onChange={setKnock} disabled={disabled} />
|
||||
}
|
||||
/>
|
||||
</SequenceCard>
|
||||
)}
|
||||
|
||||
<SequenceCard
|
||||
style={{ padding: config.space.S300 }}
|
||||
variant="SurfaceVariant"
|
||||
direction="Column"
|
||||
gap="500"
|
||||
>
|
||||
<SettingTile
|
||||
title="Allow Federation"
|
||||
description="Users from other servers can join."
|
||||
after={
|
||||
<Switch
|
||||
variant="Primary"
|
||||
value={federation}
|
||||
onChange={setFederation}
|
||||
disabled={disabled}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SequenceCard>
|
||||
{advance && (
|
||||
<RoomVersionSelector
|
||||
versions={roomVersions?.available ? Object.keys(roomVersions.available) : ['1']}
|
||||
value={selectedRoomVersion}
|
||||
onChange={handleRoomVersionChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{error && (
|
||||
<Box style={{ color: color.Critical.Main }} alignItems="Center" gap="200">
|
||||
<Icon src={Icons.Warning} filled size="100" />
|
||||
<Text size="T300" style={{ color: color.Critical.Main }}>
|
||||
<b>
|
||||
{error instanceof MatrixError && error.name === ErrorCode.M_LIMIT_EXCEEDED
|
||||
? `Server rate-limited your request for ${millisecondsToMinutes(
|
||||
(error.data.retry_after_ms as number | undefined) ?? 0
|
||||
)} minutes!`
|
||||
: error.message}
|
||||
</b>
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
<Box shrink="No" direction="Column" gap="200">
|
||||
<Button
|
||||
type="submit"
|
||||
size="500"
|
||||
variant="Primary"
|
||||
radii="400"
|
||||
disabled={disabled}
|
||||
before={loading && <Spinner variant="Primary" fill="Solid" size="200" />}
|
||||
>
|
||||
<Text size="B500">Create</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
95
src/app/features/create-space/CreateSpaceModal.tsx
Normal file
95
src/app/features/create-space/CreateSpaceModal.tsx
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
config,
|
||||
Header,
|
||||
Icon,
|
||||
IconButton,
|
||||
Icons,
|
||||
Modal,
|
||||
Overlay,
|
||||
OverlayBackdrop,
|
||||
OverlayCenter,
|
||||
Scroll,
|
||||
Text,
|
||||
} from 'folds';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import { useAllJoinedRoomsSet, useGetRoom } from '../../hooks/useGetRoom';
|
||||
import { SpaceProvider } from '../../hooks/useSpace';
|
||||
import { CreateSpaceForm } from './CreateSpace';
|
||||
import {
|
||||
useCloseCreateSpaceModal,
|
||||
useCreateSpaceModalState,
|
||||
} from '../../state/hooks/createSpaceModal';
|
||||
import { CreateSpaceModalState } from '../../state/createSpaceModal';
|
||||
import { stopPropagation } from '../../utils/keyboard';
|
||||
|
||||
type CreateSpaceModalProps = {
|
||||
state: CreateSpaceModalState;
|
||||
};
|
||||
function CreateSpaceModal({ state }: CreateSpaceModalProps) {
|
||||
const { spaceId } = state;
|
||||
const closeDialog = useCloseCreateSpaceModal();
|
||||
|
||||
const allJoinedRooms = useAllJoinedRoomsSet();
|
||||
const getRoom = useGetRoom(allJoinedRooms);
|
||||
const space = spaceId ? getRoom(spaceId) : undefined;
|
||||
|
||||
return (
|
||||
<SpaceProvider value={space ?? null}>
|
||||
<Overlay open backdrop={<OverlayBackdrop />}>
|
||||
<OverlayCenter>
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
clickOutsideDeactivates: true,
|
||||
onDeactivate: closeDialog,
|
||||
escapeDeactivates: stopPropagation,
|
||||
}}
|
||||
>
|
||||
<Modal size="300" flexHeight>
|
||||
<Box direction="Column">
|
||||
<Header
|
||||
size="500"
|
||||
style={{
|
||||
padding: config.space.S200,
|
||||
paddingLeft: config.space.S400,
|
||||
borderBottomWidth: config.borderWidth.B300,
|
||||
}}
|
||||
>
|
||||
<Box grow="Yes">
|
||||
<Text size="H4">New Space</Text>
|
||||
</Box>
|
||||
<Box shrink="No">
|
||||
<IconButton size="300" radii="300" onClick={closeDialog}>
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Header>
|
||||
<Scroll size="300" hideTrack>
|
||||
<Box
|
||||
style={{
|
||||
padding: config.space.S400,
|
||||
paddingRight: config.space.S200,
|
||||
}}
|
||||
direction="Column"
|
||||
gap="500"
|
||||
>
|
||||
<CreateSpaceForm space={space} onCreate={closeDialog} />
|
||||
</Box>
|
||||
</Scroll>
|
||||
</Box>
|
||||
</Modal>
|
||||
</FocusTrap>
|
||||
</OverlayCenter>
|
||||
</Overlay>
|
||||
</SpaceProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export function CreateSpaceModalRenderer() {
|
||||
const state = useCreateSpaceModalState();
|
||||
|
||||
if (!state) return null;
|
||||
return <CreateSpaceModal state={state} />;
|
||||
}
|
||||
2
src/app/features/create-space/index.ts
Normal file
2
src/app/features/create-space/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from './CreateSpace';
|
||||
export * from './CreateSpaceModal';
|
||||
|
|
@ -220,14 +220,12 @@ export function Lobby() {
|
|||
() =>
|
||||
hierarchy
|
||||
.flatMap((i) => {
|
||||
const childRooms = Array.isArray(i.rooms)
|
||||
? i.rooms.map((r) => mx.getRoom(r.roomId))
|
||||
: [];
|
||||
const childRooms = Array.isArray(i.rooms) ? i.rooms.map((r) => getRoom(r.roomId)) : [];
|
||||
|
||||
return [mx.getRoom(i.space.roomId), ...childRooms];
|
||||
return [getRoom(i.space.roomId), ...childRooms];
|
||||
})
|
||||
.filter((r) => !!r) as Room[],
|
||||
[mx, hierarchy]
|
||||
[hierarchy, getRoom]
|
||||
)
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -30,10 +30,12 @@ import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
|||
import * as css from './SpaceItem.css';
|
||||
import * as styleCss from './style.css';
|
||||
import { useDraggableItem } from './DnD';
|
||||
import { openCreateRoom, openSpaceAddExisting } from '../../../client/action/navigation';
|
||||
import { openSpaceAddExisting } from '../../../client/action/navigation';
|
||||
import { stopPropagation } from '../../utils/keyboard';
|
||||
import { mxcUrlToHttp } from '../../utils/matrix';
|
||||
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
||||
import { useOpenCreateRoomModal } from '../../state/hooks/createRoomModal';
|
||||
import { useOpenCreateSpaceModal } from '../../state/hooks/createSpaceModal';
|
||||
|
||||
function SpaceProfileLoading() {
|
||||
return (
|
||||
|
|
@ -240,13 +242,14 @@ function RootSpaceProfile({ closed, categoryId, handleClose }: RootSpaceProfileP
|
|||
|
||||
function AddRoomButton({ item }: { item: HierarchyItem }) {
|
||||
const [cords, setCords] = useState<RectCords>();
|
||||
const openCreateRoomModal = useOpenCreateRoomModal();
|
||||
|
||||
const handleAddRoom: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||
setCords(evt.currentTarget.getBoundingClientRect());
|
||||
};
|
||||
|
||||
const handleCreateRoom = () => {
|
||||
openCreateRoom(false, item.roomId as any);
|
||||
openCreateRoomModal(item.roomId);
|
||||
setCords(undefined);
|
||||
};
|
||||
|
||||
|
|
@ -303,13 +306,14 @@ function AddRoomButton({ item }: { item: HierarchyItem }) {
|
|||
|
||||
function AddSpaceButton({ item }: { item: HierarchyItem }) {
|
||||
const [cords, setCords] = useState<RectCords>();
|
||||
const openCreateSpaceModal = useOpenCreateSpaceModal();
|
||||
|
||||
const handleAddSpace: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||
setCords(evt.currentTarget.getBoundingClientRect());
|
||||
};
|
||||
|
||||
const handleCreateSpace = () => {
|
||||
openCreateRoom(true, item.roomId as any);
|
||||
openCreateSpaceModal(item.roomId as any);
|
||||
setCords(undefined);
|
||||
};
|
||||
|
||||
|
|
@ -470,7 +474,7 @@ export const SpaceItemCard = as<'div', SpaceItemCardProps>(
|
|||
</>
|
||||
)}
|
||||
</Box>
|
||||
{canEditChild && (
|
||||
{space && canEditChild && (
|
||||
<Box shrink="No" alignItems="Inherit" gap="200">
|
||||
<AddRoomButton item={item} />
|
||||
{item.parentId === undefined && <AddSpaceButton item={item} />}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue