mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-09-13 14:22:25 +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
118
src/app/components/create-room/CreateRoomAliasInput.tsx
Normal file
118
src/app/components/create-room/CreateRoomAliasInput.tsx
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
import React, {
|
||||||
|
FormEventHandler,
|
||||||
|
KeyboardEventHandler,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import { MatrixError } from 'matrix-js-sdk';
|
||||||
|
import { Box, color, Icon, Icons, Input, Spinner, Text, toRem } from 'folds';
|
||||||
|
import { isKeyHotkey } from 'is-hotkey';
|
||||||
|
import { getMxIdServer } from '../../utils/matrix';
|
||||||
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
|
import { replaceSpaceWithDash } from '../../utils/common';
|
||||||
|
import { AsyncState, AsyncStatus, useAsync } from '../../hooks/useAsyncCallback';
|
||||||
|
import { useDebounce } from '../../hooks/useDebounce';
|
||||||
|
|
||||||
|
export function CreateRoomAliasInput({ disabled }: { disabled?: boolean }) {
|
||||||
|
const mx = useMatrixClient();
|
||||||
|
const aliasInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const [aliasAvail, setAliasAvail] = useState<AsyncState<boolean, Error>>({
|
||||||
|
status: AsyncStatus.Idle,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (aliasAvail.status === AsyncStatus.Success && aliasInputRef.current?.value === '') {
|
||||||
|
setAliasAvail({ status: AsyncStatus.Idle });
|
||||||
|
}
|
||||||
|
}, [aliasAvail]);
|
||||||
|
|
||||||
|
const checkAliasAvail = useAsync(
|
||||||
|
useCallback(
|
||||||
|
async (aliasLocalPart: string) => {
|
||||||
|
const roomAlias = `#${aliasLocalPart}:${getMxIdServer(mx.getSafeUserId())}`;
|
||||||
|
try {
|
||||||
|
const result = await mx.getRoomIdForAlias(roomAlias);
|
||||||
|
return typeof result.room_id !== 'string';
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof MatrixError && e.httpStatus === 404) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[mx]
|
||||||
|
),
|
||||||
|
setAliasAvail
|
||||||
|
);
|
||||||
|
const aliasAvailable: boolean | undefined =
|
||||||
|
aliasAvail.status === AsyncStatus.Success ? aliasAvail.data : undefined;
|
||||||
|
|
||||||
|
const debounceCheckAliasAvail = useDebounce(checkAliasAvail, { wait: 500 });
|
||||||
|
|
||||||
|
const handleAliasChange: FormEventHandler<HTMLInputElement> = (evt) => {
|
||||||
|
const aliasInput = evt.currentTarget;
|
||||||
|
const aliasLocalPart = replaceSpaceWithDash(aliasInput.value);
|
||||||
|
if (aliasLocalPart) {
|
||||||
|
aliasInput.value = aliasLocalPart;
|
||||||
|
debounceCheckAliasAvail(aliasLocalPart);
|
||||||
|
} else {
|
||||||
|
setAliasAvail({ status: AsyncStatus.Idle });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAliasKeyDown: KeyboardEventHandler<HTMLInputElement> = (evt) => {
|
||||||
|
if (isKeyHotkey('enter', evt)) {
|
||||||
|
evt.preventDefault();
|
||||||
|
|
||||||
|
const aliasInput = evt.currentTarget;
|
||||||
|
const aliasLocalPart = replaceSpaceWithDash(aliasInput.value);
|
||||||
|
if (aliasLocalPart) {
|
||||||
|
checkAliasAvail(aliasLocalPart);
|
||||||
|
} else {
|
||||||
|
setAliasAvail({ status: AsyncStatus.Idle });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box shrink="No" direction="Column" gap="100">
|
||||||
|
<Text size="L400">Address (Optional)</Text>
|
||||||
|
<Text size="T200" priority="300">
|
||||||
|
Pick an unique address to make it discoverable.
|
||||||
|
</Text>
|
||||||
|
<Input
|
||||||
|
ref={aliasInputRef}
|
||||||
|
onChange={handleAliasChange}
|
||||||
|
before={
|
||||||
|
aliasAvail.status === AsyncStatus.Loading ? (
|
||||||
|
<Spinner size="100" variant="Secondary" />
|
||||||
|
) : (
|
||||||
|
<Icon size="100" src={Icons.Hash} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
after={
|
||||||
|
<Text style={{ maxWidth: toRem(150) }} truncate>
|
||||||
|
:{getMxIdServer(mx.getSafeUserId())}
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
onKeyDown={handleAliasKeyDown}
|
||||||
|
name="aliasInput"
|
||||||
|
size="500"
|
||||||
|
variant={aliasAvailable === true ? 'Success' : 'SurfaceVariant'}
|
||||||
|
radii="400"
|
||||||
|
autoComplete="off"
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
{aliasAvailable === false && (
|
||||||
|
<Box style={{ color: color.Critical.Main }} alignItems="Center" gap="100">
|
||||||
|
<Icon src={Icons.Warning} filled size="50" />
|
||||||
|
<Text size="T200">
|
||||||
|
<b>This address is already taken. Please select a different one.</b>
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
94
src/app/components/create-room/CreateRoomKindSelector.tsx
Normal file
94
src/app/components/create-room/CreateRoomKindSelector.tsx
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Box, Text, Icon, Icons, config, IconSrc } from 'folds';
|
||||||
|
import { SequenceCard } from '../sequence-card';
|
||||||
|
import { SettingTile } from '../setting-tile';
|
||||||
|
|
||||||
|
export enum CreateRoomKind {
|
||||||
|
Private = 'private',
|
||||||
|
Restricted = 'restricted',
|
||||||
|
Public = 'public',
|
||||||
|
}
|
||||||
|
type CreateRoomKindSelectorProps = {
|
||||||
|
value?: CreateRoomKind;
|
||||||
|
onSelect: (value: CreateRoomKind) => void;
|
||||||
|
canRestrict?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
getIcon: (kind: CreateRoomKind) => IconSrc;
|
||||||
|
};
|
||||||
|
export function CreateRoomKindSelector({
|
||||||
|
value,
|
||||||
|
onSelect,
|
||||||
|
canRestrict,
|
||||||
|
disabled,
|
||||||
|
getIcon,
|
||||||
|
}: CreateRoomKindSelectorProps) {
|
||||||
|
return (
|
||||||
|
<Box shrink="No" direction="Column" gap="100">
|
||||||
|
{canRestrict && (
|
||||||
|
<SequenceCard
|
||||||
|
style={{ padding: config.space.S300 }}
|
||||||
|
variant={value === CreateRoomKind.Restricted ? 'Primary' : 'SurfaceVariant'}
|
||||||
|
direction="Column"
|
||||||
|
gap="100"
|
||||||
|
as="button"
|
||||||
|
type="button"
|
||||||
|
aria-pressed={value === CreateRoomKind.Restricted}
|
||||||
|
onClick={() => onSelect(CreateRoomKind.Restricted)}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<SettingTile
|
||||||
|
before={<Icon size="400" src={getIcon(CreateRoomKind.Restricted)} />}
|
||||||
|
after={value === CreateRoomKind.Restricted && <Icon src={Icons.Check} />}
|
||||||
|
>
|
||||||
|
<Text size="H6">Restricted</Text>
|
||||||
|
<Text size="T300" priority="300">
|
||||||
|
Only member of parent space can join.
|
||||||
|
</Text>
|
||||||
|
</SettingTile>
|
||||||
|
</SequenceCard>
|
||||||
|
)}
|
||||||
|
<SequenceCard
|
||||||
|
style={{ padding: config.space.S300 }}
|
||||||
|
variant={value === CreateRoomKind.Private ? 'Primary' : 'SurfaceVariant'}
|
||||||
|
direction="Column"
|
||||||
|
gap="100"
|
||||||
|
as="button"
|
||||||
|
type="button"
|
||||||
|
aria-pressed={value === CreateRoomKind.Private}
|
||||||
|
onClick={() => onSelect(CreateRoomKind.Private)}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<SettingTile
|
||||||
|
before={<Icon size="400" src={getIcon(CreateRoomKind.Private)} />}
|
||||||
|
after={value === CreateRoomKind.Private && <Icon src={Icons.Check} />}
|
||||||
|
>
|
||||||
|
<Text size="H6">Private</Text>
|
||||||
|
<Text size="T300" priority="300">
|
||||||
|
Only people with invite can join.
|
||||||
|
</Text>
|
||||||
|
</SettingTile>
|
||||||
|
</SequenceCard>
|
||||||
|
<SequenceCard
|
||||||
|
style={{ padding: config.space.S300 }}
|
||||||
|
variant={value === CreateRoomKind.Public ? 'Primary' : 'SurfaceVariant'}
|
||||||
|
direction="Column"
|
||||||
|
gap="100"
|
||||||
|
as="button"
|
||||||
|
type="button"
|
||||||
|
aria-pressed={value === CreateRoomKind.Public}
|
||||||
|
onClick={() => onSelect(CreateRoomKind.Public)}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<SettingTile
|
||||||
|
before={<Icon size="400" src={getIcon(CreateRoomKind.Public)} />}
|
||||||
|
after={value === CreateRoomKind.Public && <Icon src={Icons.Check} />}
|
||||||
|
>
|
||||||
|
<Text size="H6">Public</Text>
|
||||||
|
<Text size="T300" priority="300">
|
||||||
|
Anyone with the address can join.
|
||||||
|
</Text>
|
||||||
|
</SettingTile>
|
||||||
|
</SequenceCard>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
117
src/app/components/create-room/RoomVersionSelector.tsx
Normal file
117
src/app/components/create-room/RoomVersionSelector.tsx
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
import React, { MouseEventHandler, useState } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Chip,
|
||||||
|
config,
|
||||||
|
Icon,
|
||||||
|
Icons,
|
||||||
|
Menu,
|
||||||
|
PopOut,
|
||||||
|
RectCords,
|
||||||
|
Text,
|
||||||
|
toRem,
|
||||||
|
} from 'folds';
|
||||||
|
import FocusTrap from 'focus-trap-react';
|
||||||
|
import { SettingTile } from '../setting-tile';
|
||||||
|
import { SequenceCard } from '../sequence-card';
|
||||||
|
import { stopPropagation } from '../../utils/keyboard';
|
||||||
|
|
||||||
|
export function RoomVersionSelector({
|
||||||
|
versions,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
disabled,
|
||||||
|
}: {
|
||||||
|
versions: string[];
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}) {
|
||||||
|
const [menuCords, setMenuCords] = useState<RectCords>();
|
||||||
|
|
||||||
|
const handleMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||||
|
setMenuCords(evt.currentTarget.getBoundingClientRect());
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelect = (version: string) => {
|
||||||
|
setMenuCords(undefined);
|
||||||
|
onChange(version);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SequenceCard
|
||||||
|
style={{ padding: config.space.S300 }}
|
||||||
|
variant="SurfaceVariant"
|
||||||
|
direction="Column"
|
||||||
|
gap="500"
|
||||||
|
>
|
||||||
|
<SettingTile
|
||||||
|
title="Room Version"
|
||||||
|
after={
|
||||||
|
<PopOut
|
||||||
|
anchor={menuCords}
|
||||||
|
offset={5}
|
||||||
|
position="Bottom"
|
||||||
|
align="End"
|
||||||
|
content={
|
||||||
|
<FocusTrap
|
||||||
|
focusTrapOptions={{
|
||||||
|
initialFocus: false,
|
||||||
|
onDeactivate: () => setMenuCords(undefined),
|
||||||
|
clickOutsideDeactivates: true,
|
||||||
|
isKeyForward: (evt: KeyboardEvent) =>
|
||||||
|
evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
|
||||||
|
isKeyBackward: (evt: KeyboardEvent) =>
|
||||||
|
evt.key === 'ArrowUp' || evt.key === 'ArrowLeft',
|
||||||
|
escapeDeactivates: stopPropagation,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Menu>
|
||||||
|
<Box
|
||||||
|
direction="Column"
|
||||||
|
gap="200"
|
||||||
|
style={{ padding: config.space.S200, maxWidth: toRem(300) }}
|
||||||
|
>
|
||||||
|
<Text size="L400">Versions</Text>
|
||||||
|
<Box wrap="Wrap" gap="100">
|
||||||
|
{versions.map((version) => (
|
||||||
|
<Chip
|
||||||
|
key={version}
|
||||||
|
variant={value === version ? 'Primary' : 'SurfaceVariant'}
|
||||||
|
aria-pressed={value === version}
|
||||||
|
outlined={value === version}
|
||||||
|
radii="300"
|
||||||
|
onClick={() => handleSelect(version)}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<Text truncate size="T300">
|
||||||
|
{version}
|
||||||
|
</Text>
|
||||||
|
</Chip>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Menu>
|
||||||
|
</FocusTrap>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={handleMenu}
|
||||||
|
size="300"
|
||||||
|
variant="Secondary"
|
||||||
|
fill="Soft"
|
||||||
|
radii="300"
|
||||||
|
aria-pressed={!!menuCords}
|
||||||
|
before={<Icon size="50" src={menuCords ? Icons.ChevronTop : Icons.ChevronBottom} />}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<Text size="B300">{value}</Text>
|
||||||
|
</Button>
|
||||||
|
</PopOut>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</SequenceCard>
|
||||||
|
);
|
||||||
|
}
|
4
src/app/components/create-room/index.ts
Normal file
4
src/app/components/create-room/index.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export * from './CreateRoomKindSelector';
|
||||||
|
export * from './CreateRoomAliasInput';
|
||||||
|
export * from './RoomVersionSelector';
|
||||||
|
export * from './utils';
|
131
src/app/components/create-room/utils.ts
Normal file
131
src/app/components/create-room/utils.ts
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
import {
|
||||||
|
ICreateRoomOpts,
|
||||||
|
ICreateRoomStateEvent,
|
||||||
|
JoinRule,
|
||||||
|
MatrixClient,
|
||||||
|
RestrictedAllowType,
|
||||||
|
Room,
|
||||||
|
} from 'matrix-js-sdk';
|
||||||
|
import { RoomJoinRulesEventContent } from 'matrix-js-sdk/lib/types';
|
||||||
|
import { CreateRoomKind } from './CreateRoomKindSelector';
|
||||||
|
import { RoomType, StateEvent } from '../../../types/matrix/room';
|
||||||
|
import { getViaServers } from '../../plugins/via-servers';
|
||||||
|
import { getMxIdServer } from '../../utils/matrix';
|
||||||
|
|
||||||
|
export const createRoomCreationContent = (
|
||||||
|
type: RoomType | undefined,
|
||||||
|
allowFederation: boolean
|
||||||
|
): object => {
|
||||||
|
const content: Record<string, any> = {};
|
||||||
|
if (typeof type === 'string') {
|
||||||
|
content.type = type;
|
||||||
|
}
|
||||||
|
if (allowFederation === false) {
|
||||||
|
content['m.federate'] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return content;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createRoomJoinRulesState = (
|
||||||
|
kind: CreateRoomKind,
|
||||||
|
parent: Room | undefined,
|
||||||
|
knock: boolean
|
||||||
|
) => {
|
||||||
|
let content: RoomJoinRulesEventContent = {
|
||||||
|
join_rule: knock ? JoinRule.Knock : JoinRule.Invite,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (kind === CreateRoomKind.Public) {
|
||||||
|
content = {
|
||||||
|
join_rule: JoinRule.Public,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kind === CreateRoomKind.Restricted && parent) {
|
||||||
|
content = {
|
||||||
|
join_rule: knock ? ('knock_restricted' as JoinRule) : JoinRule.Restricted,
|
||||||
|
allow: [
|
||||||
|
{
|
||||||
|
type: RestrictedAllowType.RoomMembership,
|
||||||
|
room_id: parent.roomId,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: StateEvent.RoomJoinRules,
|
||||||
|
state_key: '',
|
||||||
|
content,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createRoomParentState = (parent: Room) => ({
|
||||||
|
type: StateEvent.SpaceParent,
|
||||||
|
state_key: parent.roomId,
|
||||||
|
content: {
|
||||||
|
canonical: true,
|
||||||
|
via: getViaServers(parent),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createRoomEncryptionState = () => ({
|
||||||
|
type: 'm.room.encryption',
|
||||||
|
state_key: '',
|
||||||
|
content: {
|
||||||
|
algorithm: 'm.megolm.v1.aes-sha2',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CreateRoomData = {
|
||||||
|
version: string;
|
||||||
|
type?: RoomType;
|
||||||
|
parent?: Room;
|
||||||
|
kind: CreateRoomKind;
|
||||||
|
name: string;
|
||||||
|
topic?: string;
|
||||||
|
aliasLocalPart?: string;
|
||||||
|
encryption?: boolean;
|
||||||
|
knock: boolean;
|
||||||
|
allowFederation: boolean;
|
||||||
|
};
|
||||||
|
export const createRoom = async (mx: MatrixClient, data: CreateRoomData): Promise<string> => {
|
||||||
|
const initialState: ICreateRoomStateEvent[] = [];
|
||||||
|
|
||||||
|
if (data.encryption) {
|
||||||
|
initialState.push(createRoomEncryptionState());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.parent) {
|
||||||
|
initialState.push(createRoomParentState(data.parent));
|
||||||
|
}
|
||||||
|
|
||||||
|
initialState.push(createRoomJoinRulesState(data.kind, data.parent, data.knock));
|
||||||
|
|
||||||
|
const options: ICreateRoomOpts = {
|
||||||
|
room_version: data.version,
|
||||||
|
name: data.name,
|
||||||
|
topic: data.topic,
|
||||||
|
room_alias_name: data.aliasLocalPart,
|
||||||
|
creation_content: createRoomCreationContent(data.type, data.allowFederation),
|
||||||
|
initial_state: initialState,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await mx.createRoom(options);
|
||||||
|
|
||||||
|
if (data.parent) {
|
||||||
|
await mx.sendStateEvent(
|
||||||
|
data.parent.roomId,
|
||||||
|
StateEvent.SpaceChild as any,
|
||||||
|
{
|
||||||
|
auto_join: false,
|
||||||
|
suggested: false,
|
||||||
|
via: [getMxIdServer(mx.getUserId() ?? '') ?? ''],
|
||||||
|
},
|
||||||
|
result.room_id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.room_id;
|
||||||
|
};
|
|
@ -7,12 +7,31 @@ import * as css from './style.css';
|
||||||
export const SequenceCard = as<
|
export const SequenceCard = as<
|
||||||
'div',
|
'div',
|
||||||
ComponentProps<typeof Box> & ContainerColorVariants & css.SequenceCardVariants
|
ComponentProps<typeof Box> & ContainerColorVariants & css.SequenceCardVariants
|
||||||
>(({ className, variant, firstChild, lastChild, outlined, ...props }, ref) => (
|
>(
|
||||||
<Box
|
(
|
||||||
className={classNames(css.SequenceCard({ outlined }), ContainerColor({ variant }), className)}
|
{
|
||||||
data-first-child={firstChild}
|
as: AsSequenceCard = 'div',
|
||||||
data-last-child={lastChild}
|
className,
|
||||||
{...props}
|
variant,
|
||||||
ref={ref}
|
radii,
|
||||||
/>
|
firstChild,
|
||||||
));
|
lastChild,
|
||||||
|
outlined,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) => (
|
||||||
|
<Box
|
||||||
|
as={AsSequenceCard}
|
||||||
|
className={classNames(
|
||||||
|
css.SequenceCard({ radii, outlined }),
|
||||||
|
ContainerColor({ variant }),
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
data-first-child={firstChild}
|
||||||
|
data-last-child={lastChild}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { RecipeVariants, recipe } from '@vanilla-extract/recipes';
|
||||||
import { config } from 'folds';
|
import { config } from 'folds';
|
||||||
|
|
||||||
const outlinedWidth = createVar('0');
|
const outlinedWidth = createVar('0');
|
||||||
|
const radii = createVar(config.radii.R400);
|
||||||
export const SequenceCard = recipe({
|
export const SequenceCard = recipe({
|
||||||
base: {
|
base: {
|
||||||
vars: {
|
vars: {
|
||||||
|
@ -13,33 +14,59 @@ export const SequenceCard = recipe({
|
||||||
borderBottomWidth: 0,
|
borderBottomWidth: 0,
|
||||||
selectors: {
|
selectors: {
|
||||||
'&:first-child, :not(&) + &': {
|
'&:first-child, :not(&) + &': {
|
||||||
borderTopLeftRadius: config.radii.R400,
|
borderTopLeftRadius: [radii],
|
||||||
borderTopRightRadius: config.radii.R400,
|
borderTopRightRadius: [radii],
|
||||||
},
|
},
|
||||||
'&:last-child, &:not(:has(+&))': {
|
'&:last-child, &:not(:has(+&))': {
|
||||||
borderBottomLeftRadius: config.radii.R400,
|
borderBottomLeftRadius: [radii],
|
||||||
borderBottomRightRadius: config.radii.R400,
|
borderBottomRightRadius: [radii],
|
||||||
borderBottomWidth: outlinedWidth,
|
borderBottomWidth: outlinedWidth,
|
||||||
},
|
},
|
||||||
[`&[data-first-child="true"]`]: {
|
[`&[data-first-child="true"]`]: {
|
||||||
borderTopLeftRadius: config.radii.R400,
|
borderTopLeftRadius: [radii],
|
||||||
borderTopRightRadius: config.radii.R400,
|
borderTopRightRadius: [radii],
|
||||||
},
|
},
|
||||||
[`&[data-first-child="false"]`]: {
|
[`&[data-first-child="false"]`]: {
|
||||||
borderTopLeftRadius: 0,
|
borderTopLeftRadius: 0,
|
||||||
borderTopRightRadius: 0,
|
borderTopRightRadius: 0,
|
||||||
},
|
},
|
||||||
[`&[data-last-child="true"]`]: {
|
[`&[data-last-child="true"]`]: {
|
||||||
borderBottomLeftRadius: config.radii.R400,
|
borderBottomLeftRadius: [radii],
|
||||||
borderBottomRightRadius: config.radii.R400,
|
borderBottomRightRadius: [radii],
|
||||||
},
|
},
|
||||||
[`&[data-last-child="false"]`]: {
|
[`&[data-last-child="false"]`]: {
|
||||||
borderBottomLeftRadius: 0,
|
borderBottomLeftRadius: 0,
|
||||||
borderBottomRightRadius: 0,
|
borderBottomRightRadius: 0,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'button&': {
|
||||||
|
cursor: 'pointer',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
variants: {
|
variants: {
|
||||||
|
radii: {
|
||||||
|
'0': {
|
||||||
|
vars: {
|
||||||
|
[radii]: config.radii.R0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'300': {
|
||||||
|
vars: {
|
||||||
|
[radii]: config.radii.R300,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'400': {
|
||||||
|
vars: {
|
||||||
|
[radii]: config.radii.R400,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'500': {
|
||||||
|
vars: {
|
||||||
|
[radii]: config.radii.R500,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
outlined: {
|
outlined: {
|
||||||
true: {
|
true: {
|
||||||
vars: {
|
vars: {
|
||||||
|
@ -48,5 +75,8 @@ export const SequenceCard = recipe({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
radii: '400',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
export type SequenceCardVariants = RecipeVariants<typeof SequenceCard>;
|
export type SequenceCardVariants = RecipeVariants<typeof SequenceCard>;
|
||||||
|
|
|
@ -27,6 +27,11 @@ import {
|
||||||
} from '../../../state/hooks/roomList';
|
} from '../../../state/hooks/roomList';
|
||||||
import { allRoomsAtom } from '../../../state/room-list/roomList';
|
import { allRoomsAtom } from '../../../state/room-list/roomList';
|
||||||
import { roomToParentsAtom } from '../../../state/room/roomToParents';
|
import { roomToParentsAtom } from '../../../state/room/roomToParents';
|
||||||
|
import {
|
||||||
|
knockRestrictedSupported,
|
||||||
|
knockSupported,
|
||||||
|
restrictedSupported,
|
||||||
|
} from '../../../utils/matrix';
|
||||||
|
|
||||||
type RestrictedRoomAllowContent = {
|
type RestrictedRoomAllowContent = {
|
||||||
room_id: string;
|
room_id: string;
|
||||||
|
@ -39,10 +44,9 @@ type RoomJoinRulesProps = {
|
||||||
export function RoomJoinRules({ powerLevels }: RoomJoinRulesProps) {
|
export function RoomJoinRules({ powerLevels }: RoomJoinRulesProps) {
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const room = useRoom();
|
const room = useRoom();
|
||||||
const roomVersion = parseInt(room.getVersion(), 10);
|
const allowKnockRestricted = knockRestrictedSupported(room.getVersion());
|
||||||
const allowKnockRestricted = roomVersion >= 10;
|
const allowRestricted = restrictedSupported(room.getVersion());
|
||||||
const allowRestricted = roomVersion >= 8;
|
const allowKnock = knockSupported(room.getVersion());
|
||||||
const allowKnock = roomVersion >= 7;
|
|
||||||
|
|
||||||
const roomIdToParents = useAtomValue(roomToParentsAtom);
|
const roomIdToParents = useAtomValue(roomToParentsAtom);
|
||||||
const space = useSpaceOptionally();
|
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
|
hierarchy
|
||||||
.flatMap((i) => {
|
.flatMap((i) => {
|
||||||
const childRooms = Array.isArray(i.rooms)
|
const childRooms = Array.isArray(i.rooms) ? i.rooms.map((r) => getRoom(r.roomId)) : [];
|
||||||
? i.rooms.map((r) => mx.getRoom(r.roomId))
|
|
||||||
: [];
|
|
||||||
|
|
||||||
return [mx.getRoom(i.space.roomId), ...childRooms];
|
return [getRoom(i.space.roomId), ...childRooms];
|
||||||
})
|
})
|
||||||
.filter((r) => !!r) as Room[],
|
.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 css from './SpaceItem.css';
|
||||||
import * as styleCss from './style.css';
|
import * as styleCss from './style.css';
|
||||||
import { useDraggableItem } from './DnD';
|
import { useDraggableItem } from './DnD';
|
||||||
import { openCreateRoom, openSpaceAddExisting } from '../../../client/action/navigation';
|
import { openSpaceAddExisting } from '../../../client/action/navigation';
|
||||||
import { stopPropagation } from '../../utils/keyboard';
|
import { stopPropagation } from '../../utils/keyboard';
|
||||||
import { mxcUrlToHttp } from '../../utils/matrix';
|
import { mxcUrlToHttp } from '../../utils/matrix';
|
||||||
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
||||||
|
import { useOpenCreateRoomModal } from '../../state/hooks/createRoomModal';
|
||||||
|
import { useOpenCreateSpaceModal } from '../../state/hooks/createSpaceModal';
|
||||||
|
|
||||||
function SpaceProfileLoading() {
|
function SpaceProfileLoading() {
|
||||||
return (
|
return (
|
||||||
|
@ -240,13 +242,14 @@ function RootSpaceProfile({ closed, categoryId, handleClose }: RootSpaceProfileP
|
||||||
|
|
||||||
function AddRoomButton({ item }: { item: HierarchyItem }) {
|
function AddRoomButton({ item }: { item: HierarchyItem }) {
|
||||||
const [cords, setCords] = useState<RectCords>();
|
const [cords, setCords] = useState<RectCords>();
|
||||||
|
const openCreateRoomModal = useOpenCreateRoomModal();
|
||||||
|
|
||||||
const handleAddRoom: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
const handleAddRoom: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||||
setCords(evt.currentTarget.getBoundingClientRect());
|
setCords(evt.currentTarget.getBoundingClientRect());
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateRoom = () => {
|
const handleCreateRoom = () => {
|
||||||
openCreateRoom(false, item.roomId as any);
|
openCreateRoomModal(item.roomId);
|
||||||
setCords(undefined);
|
setCords(undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -303,13 +306,14 @@ function AddRoomButton({ item }: { item: HierarchyItem }) {
|
||||||
|
|
||||||
function AddSpaceButton({ item }: { item: HierarchyItem }) {
|
function AddSpaceButton({ item }: { item: HierarchyItem }) {
|
||||||
const [cords, setCords] = useState<RectCords>();
|
const [cords, setCords] = useState<RectCords>();
|
||||||
|
const openCreateSpaceModal = useOpenCreateSpaceModal();
|
||||||
|
|
||||||
const handleAddSpace: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
const handleAddSpace: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||||
setCords(evt.currentTarget.getBoundingClientRect());
|
setCords(evt.currentTarget.getBoundingClientRect());
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateSpace = () => {
|
const handleCreateSpace = () => {
|
||||||
openCreateRoom(true, item.roomId as any);
|
openCreateSpaceModal(item.roomId as any);
|
||||||
setCords(undefined);
|
setCords(undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -470,7 +474,7 @@ export const SpaceItemCard = as<'div', SpaceItemCardProps>(
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
{canEditChild && (
|
{space && canEditChild && (
|
||||||
<Box shrink="No" alignItems="Inherit" gap="200">
|
<Box shrink="No" alignItems="Inherit" gap="200">
|
||||||
<AddRoomButton item={item} />
|
<AddRoomButton item={item} />
|
||||||
{item.parentId === undefined && <AddSpaceButton item={item} />}
|
{item.parentId === undefined && <AddSpaceButton item={item} />}
|
||||||
|
|
12
src/app/hooks/router/useCreateSelected.ts
Normal file
12
src/app/hooks/router/useCreateSelected.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { useMatch } from 'react-router-dom';
|
||||||
|
import { getCreatePath } from '../../pages/pathUtils';
|
||||||
|
|
||||||
|
export const useCreateSelected = (): boolean => {
|
||||||
|
const match = useMatch({
|
||||||
|
path: getCreatePath(),
|
||||||
|
caseSensitive: true,
|
||||||
|
end: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return !!match;
|
||||||
|
};
|
|
@ -28,6 +28,7 @@ import {
|
||||||
_ROOM_PATH,
|
_ROOM_PATH,
|
||||||
_SEARCH_PATH,
|
_SEARCH_PATH,
|
||||||
_SERVER_PATH,
|
_SERVER_PATH,
|
||||||
|
CREATE_PATH,
|
||||||
} from './paths';
|
} from './paths';
|
||||||
import { isAuthenticated } from '../../client/state/auth';
|
import { isAuthenticated } from '../../client/state/auth';
|
||||||
import {
|
import {
|
||||||
|
@ -61,6 +62,10 @@ import { AutoRestoreBackupOnVerification } from '../components/BackupRestore';
|
||||||
import { RoomSettingsRenderer } from '../features/room-settings';
|
import { RoomSettingsRenderer } from '../features/room-settings';
|
||||||
import { ClientRoomsNotificationPreferences } from './client/ClientRoomsNotificationPreferences';
|
import { ClientRoomsNotificationPreferences } from './client/ClientRoomsNotificationPreferences';
|
||||||
import { SpaceSettingsRenderer } from '../features/space-settings';
|
import { SpaceSettingsRenderer } from '../features/space-settings';
|
||||||
|
import { CreateRoomModalRenderer } from '../features/create-room';
|
||||||
|
import { HomeCreateRoom } from './client/home/CreateRoom';
|
||||||
|
import { Create } from './client/create';
|
||||||
|
import { CreateSpaceModalRenderer } from '../features/create-space';
|
||||||
|
|
||||||
export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize) => {
|
export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize) => {
|
||||||
const { hashRouter } = clientConfig;
|
const { hashRouter } = clientConfig;
|
||||||
|
@ -125,6 +130,8 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
|
||||||
>
|
>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</ClientLayout>
|
</ClientLayout>
|
||||||
|
<CreateRoomModalRenderer />
|
||||||
|
<CreateSpaceModalRenderer />
|
||||||
<RoomSettingsRenderer />
|
<RoomSettingsRenderer />
|
||||||
<SpaceSettingsRenderer />
|
<SpaceSettingsRenderer />
|
||||||
<ReceiveSelfDeviceVerification />
|
<ReceiveSelfDeviceVerification />
|
||||||
|
@ -152,7 +159,7 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{mobile ? null : <Route index element={<WelcomePage />} />}
|
{mobile ? null : <Route index element={<WelcomePage />} />}
|
||||||
<Route path={_CREATE_PATH} element={<p>create</p>} />
|
<Route path={_CREATE_PATH} element={<HomeCreateRoom />} />
|
||||||
<Route path={_JOIN_PATH} element={<p>join</p>} />
|
<Route path={_JOIN_PATH} element={<p>join</p>} />
|
||||||
<Route path={_SEARCH_PATH} element={<HomeSearch />} />
|
<Route path={_SEARCH_PATH} element={<HomeSearch />} />
|
||||||
<Route
|
<Route
|
||||||
|
@ -253,6 +260,7 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
|
||||||
<Route path={_FEATURED_PATH} element={<FeaturedRooms />} />
|
<Route path={_FEATURED_PATH} element={<FeaturedRooms />} />
|
||||||
<Route path={_SERVER_PATH} element={<PublicRooms />} />
|
<Route path={_SERVER_PATH} element={<PublicRooms />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route path={CREATE_PATH} element={<Create />} />
|
||||||
<Route
|
<Route
|
||||||
path={INBOX_PATH}
|
path={INBOX_PATH}
|
||||||
element={
|
element={
|
||||||
|
|
|
@ -19,7 +19,8 @@ import {
|
||||||
SettingsTab,
|
SettingsTab,
|
||||||
UnverifiedTab,
|
UnverifiedTab,
|
||||||
} from './sidebar';
|
} from './sidebar';
|
||||||
import { openCreateRoom, openSearch } from '../../../client/action/navigation';
|
import { openSearch } from '../../../client/action/navigation';
|
||||||
|
import { CreateTab } from './sidebar/CreateTab';
|
||||||
|
|
||||||
export function SidebarNav() {
|
export function SidebarNav() {
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
@ -37,20 +38,7 @@ export function SidebarNav() {
|
||||||
<SidebarStackSeparator />
|
<SidebarStackSeparator />
|
||||||
<SidebarStack>
|
<SidebarStack>
|
||||||
<ExploreTab />
|
<ExploreTab />
|
||||||
<SidebarItem>
|
<CreateTab />
|
||||||
<SidebarItemTooltip tooltip="Create Space">
|
|
||||||
{(triggerRef) => (
|
|
||||||
<SidebarAvatar
|
|
||||||
as="button"
|
|
||||||
ref={triggerRef}
|
|
||||||
outlined
|
|
||||||
onClick={() => openCreateRoom(true)}
|
|
||||||
>
|
|
||||||
<Icon src={Icons.Plus} />
|
|
||||||
</SidebarAvatar>
|
|
||||||
)}
|
|
||||||
</SidebarItemTooltip>
|
|
||||||
</SidebarItem>
|
|
||||||
</SidebarStack>
|
</SidebarStack>
|
||||||
</Scroll>
|
</Scroll>
|
||||||
}
|
}
|
||||||
|
|
38
src/app/pages/client/create/Create.tsx
Normal file
38
src/app/pages/client/create/Create.tsx
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Box, Icon, Icons, Scroll } from 'folds';
|
||||||
|
import {
|
||||||
|
Page,
|
||||||
|
PageContent,
|
||||||
|
PageContentCenter,
|
||||||
|
PageHero,
|
||||||
|
PageHeroSection,
|
||||||
|
} from '../../../components/page';
|
||||||
|
import { CreateSpaceForm } from '../../../features/create-space';
|
||||||
|
import { useRoomNavigate } from '../../../hooks/useRoomNavigate';
|
||||||
|
|
||||||
|
export function Create() {
|
||||||
|
const { navigateSpace } = useRoomNavigate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<Box grow="Yes">
|
||||||
|
<Scroll hideTrack visibility="Hover">
|
||||||
|
<PageContent>
|
||||||
|
<PageContentCenter>
|
||||||
|
<PageHeroSection>
|
||||||
|
<Box direction="Column" gap="700">
|
||||||
|
<PageHero
|
||||||
|
icon={<Icon size="600" src={Icons.Space} />}
|
||||||
|
title="Create Space"
|
||||||
|
subTitle="Build a space for your community."
|
||||||
|
/>
|
||||||
|
<CreateSpaceForm onCreate={navigateSpace} />
|
||||||
|
</Box>
|
||||||
|
</PageHeroSection>
|
||||||
|
</PageContentCenter>
|
||||||
|
</PageContent>
|
||||||
|
</Scroll>
|
||||||
|
</Box>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
1
src/app/pages/client/create/index.ts
Normal file
1
src/app/pages/client/create/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './Create';
|
56
src/app/pages/client/home/CreateRoom.tsx
Normal file
56
src/app/pages/client/home/CreateRoom.tsx
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Box, Icon, Icons, Scroll, IconButton } from 'folds';
|
||||||
|
import {
|
||||||
|
Page,
|
||||||
|
PageContent,
|
||||||
|
PageContentCenter,
|
||||||
|
PageHeader,
|
||||||
|
PageHero,
|
||||||
|
PageHeroSection,
|
||||||
|
} from '../../../components/page';
|
||||||
|
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
|
||||||
|
import { BackRouteHandler } from '../../../components/BackRouteHandler';
|
||||||
|
import { CreateRoomForm } from '../../../features/create-room';
|
||||||
|
import { useRoomNavigate } from '../../../hooks/useRoomNavigate';
|
||||||
|
|
||||||
|
export function HomeCreateRoom() {
|
||||||
|
const screenSize = useScreenSizeContext();
|
||||||
|
|
||||||
|
const { navigateRoom } = useRoomNavigate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
{screenSize === ScreenSize.Mobile && (
|
||||||
|
<PageHeader balance outlined={false}>
|
||||||
|
<Box grow="Yes" alignItems="Center" gap="200">
|
||||||
|
<BackRouteHandler>
|
||||||
|
{(onBack) => (
|
||||||
|
<IconButton onClick={onBack}>
|
||||||
|
<Icon src={Icons.ArrowLeft} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</BackRouteHandler>
|
||||||
|
</Box>
|
||||||
|
</PageHeader>
|
||||||
|
)}
|
||||||
|
<Box grow="Yes">
|
||||||
|
<Scroll hideTrack visibility="Hover">
|
||||||
|
<PageContent>
|
||||||
|
<PageContentCenter>
|
||||||
|
<PageHeroSection>
|
||||||
|
<Box direction="Column" gap="700">
|
||||||
|
<PageHero
|
||||||
|
icon={<Icon size="600" src={Icons.Hash} />}
|
||||||
|
title="Create Room"
|
||||||
|
subTitle="Build a Room for Real-Time Conversations"
|
||||||
|
/>
|
||||||
|
<CreateRoomForm onCreate={navigateRoom} />
|
||||||
|
</Box>
|
||||||
|
</PageHeroSection>
|
||||||
|
</PageContentCenter>
|
||||||
|
</PageContent>
|
||||||
|
</Scroll>
|
||||||
|
</Box>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
|
@ -29,10 +29,18 @@ import {
|
||||||
NavItemContent,
|
NavItemContent,
|
||||||
NavLink,
|
NavLink,
|
||||||
} from '../../../components/nav';
|
} from '../../../components/nav';
|
||||||
import { getExplorePath, getHomeRoomPath, getHomeSearchPath } from '../../pathUtils';
|
import {
|
||||||
|
getExplorePath,
|
||||||
|
getHomeCreatePath,
|
||||||
|
getHomeRoomPath,
|
||||||
|
getHomeSearchPath,
|
||||||
|
} from '../../pathUtils';
|
||||||
import { getCanonicalAliasOrRoomId } from '../../../utils/matrix';
|
import { getCanonicalAliasOrRoomId } from '../../../utils/matrix';
|
||||||
import { useSelectedRoom } from '../../../hooks/router/useSelectedRoom';
|
import { useSelectedRoom } from '../../../hooks/router/useSelectedRoom';
|
||||||
import { useHomeSearchSelected } from '../../../hooks/router/useHomeSelected';
|
import {
|
||||||
|
useHomeCreateSelected,
|
||||||
|
useHomeSearchSelected,
|
||||||
|
} from '../../../hooks/router/useHomeSelected';
|
||||||
import { useHomeRooms } from './useHomeRooms';
|
import { useHomeRooms } from './useHomeRooms';
|
||||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||||
import { VirtualTile } from '../../../components/virtualizer';
|
import { VirtualTile } from '../../../components/virtualizer';
|
||||||
|
@ -41,7 +49,7 @@ import { makeNavCategoryId } from '../../../state/closedNavCategories';
|
||||||
import { roomToUnreadAtom } from '../../../state/room/roomToUnread';
|
import { roomToUnreadAtom } from '../../../state/room/roomToUnread';
|
||||||
import { useCategoryHandler } from '../../../hooks/useCategoryHandler';
|
import { useCategoryHandler } from '../../../hooks/useCategoryHandler';
|
||||||
import { useNavToActivePathMapper } from '../../../hooks/useNavToActivePathMapper';
|
import { useNavToActivePathMapper } from '../../../hooks/useNavToActivePathMapper';
|
||||||
import { openCreateRoom, openJoinAlias } from '../../../../client/action/navigation';
|
import { openJoinAlias } from '../../../../client/action/navigation';
|
||||||
import { PageNav, PageNavHeader, PageNavContent } from '../../../components/page';
|
import { PageNav, PageNavHeader, PageNavContent } from '../../../components/page';
|
||||||
import { useRoomsUnread } from '../../../state/hooks/unread';
|
import { useRoomsUnread } from '../../../state/hooks/unread';
|
||||||
import { markAsRead } from '../../../../client/action/notifications';
|
import { markAsRead } from '../../../../client/action/notifications';
|
||||||
|
@ -174,7 +182,7 @@ function HomeEmpty() {
|
||||||
}
|
}
|
||||||
options={
|
options={
|
||||||
<>
|
<>
|
||||||
<Button onClick={() => openCreateRoom()} variant="Secondary" size="300">
|
<Button onClick={() => navigate(getHomeCreatePath())} variant="Secondary" size="300">
|
||||||
<Text size="B300" truncate>
|
<Text size="B300" truncate>
|
||||||
Create Room
|
Create Room
|
||||||
</Text>
|
</Text>
|
||||||
|
@ -204,8 +212,10 @@ export function Home() {
|
||||||
const rooms = useHomeRooms();
|
const rooms = useHomeRooms();
|
||||||
const notificationPreferences = useRoomsNotificationPreferencesContext();
|
const notificationPreferences = useRoomsNotificationPreferencesContext();
|
||||||
const roomToUnread = useAtomValue(roomToUnreadAtom);
|
const roomToUnread = useAtomValue(roomToUnreadAtom);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const selectedRoomId = useSelectedRoom();
|
const selectedRoomId = useSelectedRoom();
|
||||||
|
const createRoomSelected = useHomeCreateSelected();
|
||||||
const searchSelected = useHomeSearchSelected();
|
const searchSelected = useHomeSearchSelected();
|
||||||
const noRoomToDisplay = rooms.length === 0;
|
const noRoomToDisplay = rooms.length === 0;
|
||||||
const [closedCategories, setClosedCategories] = useAtom(useClosedNavCategoriesAtom());
|
const [closedCategories, setClosedCategories] = useAtom(useClosedNavCategoriesAtom());
|
||||||
|
@ -242,8 +252,8 @@ export function Home() {
|
||||||
<PageNavContent scrollRef={scrollRef}>
|
<PageNavContent scrollRef={scrollRef}>
|
||||||
<Box direction="Column" gap="300">
|
<Box direction="Column" gap="300">
|
||||||
<NavCategory>
|
<NavCategory>
|
||||||
<NavItem variant="Background" radii="400">
|
<NavItem variant="Background" radii="400" aria-selected={createRoomSelected}>
|
||||||
<NavButton onClick={() => openCreateRoom()}>
|
<NavButton onClick={() => navigate(getHomeCreatePath())}>
|
||||||
<NavItemContent>
|
<NavItemContent>
|
||||||
<Box as="span" grow="Yes" alignItems="Center" gap="200">
|
<Box as="span" grow="Yes" alignItems="Center" gap="200">
|
||||||
<Avatar size="200" radii="400">
|
<Avatar size="200" radii="400">
|
||||||
|
|
111
src/app/pages/client/sidebar/CreateTab.tsx
Normal file
111
src/app/pages/client/sidebar/CreateTab.tsx
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
import React, { MouseEventHandler, useState } from 'react';
|
||||||
|
import { Box, config, Icon, Icons, Menu, PopOut, RectCords, Text } from 'folds';
|
||||||
|
import FocusTrap from 'focus-trap-react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { SidebarAvatar, SidebarItem, SidebarItemTooltip } from '../../../components/sidebar';
|
||||||
|
import { stopPropagation } from '../../../utils/keyboard';
|
||||||
|
import { SequenceCard } from '../../../components/sequence-card';
|
||||||
|
import { SettingTile } from '../../../components/setting-tile';
|
||||||
|
import { ContainerColor } from '../../../styles/ContainerColor.css';
|
||||||
|
import { openJoinAlias } from '../../../../client/action/navigation';
|
||||||
|
import { getCreatePath } from '../../pathUtils';
|
||||||
|
import { useCreateSelected } from '../../../hooks/router/useCreateSelected';
|
||||||
|
|
||||||
|
export function CreateTab() {
|
||||||
|
const createSelected = useCreateSelected();
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [menuCords, setMenuCords] = useState<RectCords>();
|
||||||
|
|
||||||
|
const handleMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||||
|
setMenuCords(menuCords ? undefined : evt.currentTarget.getBoundingClientRect());
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateSpace = () => {
|
||||||
|
navigate(getCreatePath());
|
||||||
|
setMenuCords(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleJoinWithAddress = () => {
|
||||||
|
openJoinAlias();
|
||||||
|
setMenuCords(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SidebarItem active={createSelected}>
|
||||||
|
<SidebarItemTooltip tooltip="Add Space">
|
||||||
|
{(triggerRef) => (
|
||||||
|
<PopOut
|
||||||
|
anchor={menuCords}
|
||||||
|
position="Right"
|
||||||
|
align="Center"
|
||||||
|
content={
|
||||||
|
<FocusTrap
|
||||||
|
focusTrapOptions={{
|
||||||
|
returnFocusOnDeactivate: false,
|
||||||
|
initialFocus: false,
|
||||||
|
onDeactivate: () => setMenuCords(undefined),
|
||||||
|
clickOutsideDeactivates: true,
|
||||||
|
isKeyForward: (evt: KeyboardEvent) =>
|
||||||
|
evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
|
||||||
|
isKeyBackward: (evt: KeyboardEvent) =>
|
||||||
|
evt.key === 'ArrowUp' || evt.key === 'ArrowLeft',
|
||||||
|
escapeDeactivates: stopPropagation,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Menu>
|
||||||
|
<Box direction="Column">
|
||||||
|
<SequenceCard
|
||||||
|
style={{ padding: config.space.S300 }}
|
||||||
|
variant="Surface"
|
||||||
|
direction="Column"
|
||||||
|
gap="100"
|
||||||
|
radii="0"
|
||||||
|
as="button"
|
||||||
|
type="button"
|
||||||
|
onClick={handleCreateSpace}
|
||||||
|
>
|
||||||
|
<SettingTile before={<Icon size="400" src={Icons.Space} />}>
|
||||||
|
<Text size="H6">Create Space</Text>
|
||||||
|
<Text size="T300" priority="300">
|
||||||
|
Build a space for your community.
|
||||||
|
</Text>
|
||||||
|
</SettingTile>
|
||||||
|
</SequenceCard>
|
||||||
|
<SequenceCard
|
||||||
|
style={{ padding: config.space.S300 }}
|
||||||
|
variant="Surface"
|
||||||
|
direction="Column"
|
||||||
|
gap="100"
|
||||||
|
radii="0"
|
||||||
|
as="button"
|
||||||
|
type="button"
|
||||||
|
onClick={handleJoinWithAddress}
|
||||||
|
>
|
||||||
|
<SettingTile before={<Icon size="400" src={Icons.Link} />}>
|
||||||
|
<Text size="H6">Join with Address</Text>
|
||||||
|
<Text size="T300" priority="300">
|
||||||
|
Become a part of existing community.
|
||||||
|
</Text>
|
||||||
|
</SettingTile>
|
||||||
|
</SequenceCard>
|
||||||
|
</Box>
|
||||||
|
</Menu>
|
||||||
|
</FocusTrap>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SidebarAvatar
|
||||||
|
className={menuCords ? ContainerColor({ variant: 'Surface' }) : undefined}
|
||||||
|
as="button"
|
||||||
|
ref={triggerRef}
|
||||||
|
outlined
|
||||||
|
onClick={handleMenu}
|
||||||
|
>
|
||||||
|
<Icon src={Icons.Plus} />
|
||||||
|
</SidebarAvatar>
|
||||||
|
</PopOut>
|
||||||
|
)}
|
||||||
|
</SidebarItemTooltip>
|
||||||
|
</SidebarItem>
|
||||||
|
);
|
||||||
|
}
|
|
@ -22,6 +22,7 @@ import {
|
||||||
SPACE_PATH,
|
SPACE_PATH,
|
||||||
SPACE_ROOM_PATH,
|
SPACE_ROOM_PATH,
|
||||||
SPACE_SEARCH_PATH,
|
SPACE_SEARCH_PATH,
|
||||||
|
CREATE_PATH,
|
||||||
} from './paths';
|
} from './paths';
|
||||||
import { trimLeadingSlash, trimTrailingSlash } from '../utils/common';
|
import { trimLeadingSlash, trimTrailingSlash } from '../utils/common';
|
||||||
import { HashRouterConfig } from '../hooks/useClientConfig';
|
import { HashRouterConfig } from '../hooks/useClientConfig';
|
||||||
|
@ -152,6 +153,8 @@ export const getExploreServerPath = (server: string): string => {
|
||||||
return generatePath(EXPLORE_SERVER_PATH, params);
|
return generatePath(EXPLORE_SERVER_PATH, params);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getCreatePath = (): string => CREATE_PATH;
|
||||||
|
|
||||||
export const getInboxPath = (): string => INBOX_PATH;
|
export const getInboxPath = (): string => INBOX_PATH;
|
||||||
export const getInboxNotificationsPath = (): string => INBOX_NOTIFICATIONS_PATH;
|
export const getInboxNotificationsPath = (): string => INBOX_NOTIFICATIONS_PATH;
|
||||||
export const getInboxInvitesPath = (): string => INBOX_INVITES_PATH;
|
export const getInboxInvitesPath = (): string => INBOX_INVITES_PATH;
|
||||||
|
|
|
@ -74,6 +74,8 @@ export type ExploreServerPathSearchParams = {
|
||||||
};
|
};
|
||||||
export const EXPLORE_SERVER_PATH = `/explore/${_SERVER_PATH}`;
|
export const EXPLORE_SERVER_PATH = `/explore/${_SERVER_PATH}`;
|
||||||
|
|
||||||
|
export const CREATE_PATH = '/create';
|
||||||
|
|
||||||
export const _NOTIFICATIONS_PATH = 'notifications/';
|
export const _NOTIFICATIONS_PATH = 'notifications/';
|
||||||
export const _INVITES_PATH = 'invites/';
|
export const _INVITES_PATH = 'invites/';
|
||||||
export const INBOX_PATH = '/inbox/';
|
export const INBOX_PATH = '/inbox/';
|
||||||
|
|
7
src/app/state/createRoomModal.ts
Normal file
7
src/app/state/createRoomModal.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { atom } from 'jotai';
|
||||||
|
|
||||||
|
export type CreateRoomModalState = {
|
||||||
|
spaceId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createRoomModalAtom = atom<CreateRoomModalState | undefined>(undefined);
|
7
src/app/state/createSpaceModal.ts
Normal file
7
src/app/state/createSpaceModal.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { atom } from 'jotai';
|
||||||
|
|
||||||
|
export type CreateSpaceModalState = {
|
||||||
|
spaceId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createSpaceModalAtom = atom<CreateSpaceModalState | undefined>(undefined);
|
34
src/app/state/hooks/createRoomModal.ts
Normal file
34
src/app/state/hooks/createRoomModal.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { useAtomValue, useSetAtom } from 'jotai';
|
||||||
|
import { createRoomModalAtom, CreateRoomModalState } from '../createRoomModal';
|
||||||
|
|
||||||
|
export const useCreateRoomModalState = (): CreateRoomModalState | undefined => {
|
||||||
|
const data = useAtomValue(createRoomModalAtom);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CloseCallback = () => void;
|
||||||
|
export const useCloseCreateRoomModal = (): CloseCallback => {
|
||||||
|
const setSettings = useSetAtom(createRoomModalAtom);
|
||||||
|
|
||||||
|
const close: CloseCallback = useCallback(() => {
|
||||||
|
setSettings(undefined);
|
||||||
|
}, [setSettings]);
|
||||||
|
|
||||||
|
return close;
|
||||||
|
};
|
||||||
|
|
||||||
|
type OpenCallback = (space?: string) => void;
|
||||||
|
export const useOpenCreateRoomModal = (): OpenCallback => {
|
||||||
|
const setSettings = useSetAtom(createRoomModalAtom);
|
||||||
|
|
||||||
|
const open: OpenCallback = useCallback(
|
||||||
|
(spaceId) => {
|
||||||
|
setSettings({ spaceId });
|
||||||
|
},
|
||||||
|
[setSettings]
|
||||||
|
);
|
||||||
|
|
||||||
|
return open;
|
||||||
|
};
|
34
src/app/state/hooks/createSpaceModal.ts
Normal file
34
src/app/state/hooks/createSpaceModal.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { useAtomValue, useSetAtom } from 'jotai';
|
||||||
|
import { createSpaceModalAtom, CreateSpaceModalState } from '../createSpaceModal';
|
||||||
|
|
||||||
|
export const useCreateSpaceModalState = (): CreateSpaceModalState | undefined => {
|
||||||
|
const data = useAtomValue(createSpaceModalAtom);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CloseCallback = () => void;
|
||||||
|
export const useCloseCreateSpaceModal = (): CloseCallback => {
|
||||||
|
const setSettings = useSetAtom(createSpaceModalAtom);
|
||||||
|
|
||||||
|
const close: CloseCallback = useCallback(() => {
|
||||||
|
setSettings(undefined);
|
||||||
|
}, [setSettings]);
|
||||||
|
|
||||||
|
return close;
|
||||||
|
};
|
||||||
|
|
||||||
|
type OpenCallback = (space?: string) => void;
|
||||||
|
export const useOpenCreateSpaceModal = (): OpenCallback => {
|
||||||
|
const setSettings = useSetAtom(createSpaceModalAtom);
|
||||||
|
|
||||||
|
const open: OpenCallback = useCallback(
|
||||||
|
(spaceId) => {
|
||||||
|
setSettings({ spaceId });
|
||||||
|
},
|
||||||
|
[setSettings]
|
||||||
|
);
|
||||||
|
|
||||||
|
return open;
|
||||||
|
};
|
|
@ -1,6 +1,6 @@
|
||||||
import { ComplexStyleRule } from '@vanilla-extract/css';
|
import { ComplexStyleRule } from '@vanilla-extract/css';
|
||||||
import { RecipeVariants, recipe } from '@vanilla-extract/recipes';
|
import { RecipeVariants, recipe } from '@vanilla-extract/recipes';
|
||||||
import { ContainerColor as TContainerColor, DefaultReset, color } from 'folds';
|
import { ContainerColor as TContainerColor, DefaultReset, color, config } from 'folds';
|
||||||
|
|
||||||
const getVariant = (variant: TContainerColor): ComplexStyleRule => ({
|
const getVariant = (variant: TContainerColor): ComplexStyleRule => ({
|
||||||
vars: {
|
vars: {
|
||||||
|
@ -9,6 +9,20 @@ const getVariant = (variant: TContainerColor): ComplexStyleRule => ({
|
||||||
outlineColor: color[variant].ContainerLine,
|
outlineColor: color[variant].ContainerLine,
|
||||||
color: color[variant].OnContainer,
|
color: color[variant].OnContainer,
|
||||||
},
|
},
|
||||||
|
selectors: {
|
||||||
|
'button&[aria-pressed=true]': {
|
||||||
|
backgroundColor: color[variant].ContainerActive,
|
||||||
|
},
|
||||||
|
'button&:hover, &:focus-visible': {
|
||||||
|
backgroundColor: color[variant].ContainerHover,
|
||||||
|
},
|
||||||
|
'button&:active': {
|
||||||
|
backgroundColor: color[variant].ContainerActive,
|
||||||
|
},
|
||||||
|
'button&[disabled]': {
|
||||||
|
opacity: config.opacity.Disabled,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ContainerColor = recipe({
|
export const ContainerColor = recipe({
|
||||||
|
|
|
@ -18,6 +18,13 @@ export const millisecondsToMinutesAndSeconds = (milliseconds: number): string =>
|
||||||
return `${mm}:${ss < 10 ? '0' : ''}${ss}`;
|
return `${mm}:${ss < 10 ? '0' : ''}${ss}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const millisecondsToMinutes = (milliseconds: number): string => {
|
||||||
|
const seconds = Math.floor(milliseconds / 1000);
|
||||||
|
const mm = Math.floor(seconds / 60);
|
||||||
|
|
||||||
|
return mm.toString();
|
||||||
|
};
|
||||||
|
|
||||||
export const secondsToMinutesAndSeconds = (seconds: number): string => {
|
export const secondsToMinutesAndSeconds = (seconds: number): string => {
|
||||||
const mm = Math.floor(seconds / 60);
|
const mm = Math.floor(seconds / 60);
|
||||||
const ss = Math.round(seconds % 60);
|
const ss = Math.round(seconds % 60);
|
||||||
|
|
|
@ -344,3 +344,16 @@ export const rateLimitedActions = async <T, R = void>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const knockSupported = (version: string): boolean => {
|
||||||
|
const unsupportedVersion = ['1', '2', '3', '4', '5', '6'];
|
||||||
|
return !unsupportedVersion.includes(version);
|
||||||
|
};
|
||||||
|
export const restrictedSupported = (version: string): boolean => {
|
||||||
|
const unsupportedVersion = ['1', '2', '3', '4', '5', '6', '7'];
|
||||||
|
return !unsupportedVersion.includes(version);
|
||||||
|
};
|
||||||
|
export const knockRestrictedSupported = (version: string): boolean => {
|
||||||
|
const unsupportedVersion = ['1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
||||||
|
return !unsupportedVersion.includes(version);
|
||||||
|
};
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue