mirror of
				https://github.com/cinnyapp/cinny.git
				synced 2025-11-04 06: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
				
			
		
							
								
								
									
										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