diff --git a/src/app/components/editor/Toolbar.tsx b/src/app/components/editor/Toolbar.tsx
index 17ba6bc..7d701c4 100644
--- a/src/app/components/editor/Toolbar.tsx
+++ b/src/app/components/editor/Toolbar.tsx
@@ -339,7 +339,7 @@ export function Toolbar() {
}
+ tooltip={}
delay={500}
>
{(triggerRef) => (
diff --git a/src/app/components/sequence-card/SequenceCard.tsx b/src/app/components/sequence-card/SequenceCard.tsx
index 4036b96..d0e77ae 100644
--- a/src/app/components/sequence-card/SequenceCard.tsx
+++ b/src/app/components/sequence-card/SequenceCard.tsx
@@ -7,12 +7,31 @@ import * as css from './style.css';
export const SequenceCard = as<
'div',
ComponentProps & ContainerColorVariants & css.SequenceCardVariants
->(({ className, variant, firstChild, lastChild, outlined, ...props }, ref) => (
-
-));
+>(
+ (
+ {
+ as: AsSequenceCard = 'div',
+ className,
+ variant,
+ radii,
+ firstChild,
+ lastChild,
+ outlined,
+ ...props
+ },
+ ref
+ ) => (
+
+ )
+);
diff --git a/src/app/components/sequence-card/style.css.ts b/src/app/components/sequence-card/style.css.ts
index c8ed48b..9d50326 100644
--- a/src/app/components/sequence-card/style.css.ts
+++ b/src/app/components/sequence-card/style.css.ts
@@ -3,6 +3,7 @@ import { RecipeVariants, recipe } from '@vanilla-extract/recipes';
import { config } from 'folds';
const outlinedWidth = createVar('0');
+const radii = createVar(config.radii.R400);
export const SequenceCard = recipe({
base: {
vars: {
@@ -13,33 +14,59 @@ export const SequenceCard = recipe({
borderBottomWidth: 0,
selectors: {
'&:first-child, :not(&) + &': {
- borderTopLeftRadius: config.radii.R400,
- borderTopRightRadius: config.radii.R400,
+ borderTopLeftRadius: [radii],
+ borderTopRightRadius: [radii],
},
'&:last-child, &:not(:has(+&))': {
- borderBottomLeftRadius: config.radii.R400,
- borderBottomRightRadius: config.radii.R400,
+ borderBottomLeftRadius: [radii],
+ borderBottomRightRadius: [radii],
borderBottomWidth: outlinedWidth,
},
[`&[data-first-child="true"]`]: {
- borderTopLeftRadius: config.radii.R400,
- borderTopRightRadius: config.radii.R400,
+ borderTopLeftRadius: [radii],
+ borderTopRightRadius: [radii],
},
[`&[data-first-child="false"]`]: {
borderTopLeftRadius: 0,
borderTopRightRadius: 0,
},
[`&[data-last-child="true"]`]: {
- borderBottomLeftRadius: config.radii.R400,
- borderBottomRightRadius: config.radii.R400,
+ borderBottomLeftRadius: [radii],
+ borderBottomRightRadius: [radii],
},
[`&[data-last-child="false"]`]: {
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
},
+
+ 'button&': {
+ cursor: 'pointer',
+ },
},
},
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: {
true: {
vars: {
@@ -48,5 +75,8 @@ export const SequenceCard = recipe({
},
},
},
+ defaultVariants: {
+ radii: '400',
+ },
});
export type SequenceCardVariants = RecipeVariants;
diff --git a/src/app/components/splash-screen/SplashScreen.tsx b/src/app/components/splash-screen/SplashScreen.tsx
index 27adadb..7027069 100644
--- a/src/app/components/splash-screen/SplashScreen.tsx
+++ b/src/app/components/splash-screen/SplashScreen.tsx
@@ -21,7 +21,7 @@ export function SplashScreen({ children }: SplashScreenProps) {
justifyContent="Center"
>
- Cinny
+ Gaboule Chat
diff --git a/src/app/components/upload-card/CompactUploadCardRenderer.tsx b/src/app/components/upload-card/CompactUploadCardRenderer.tsx
index 998b517..b9bada7 100644
--- a/src/app/components/upload-card/CompactUploadCardRenderer.tsx
+++ b/src/app/components/upload-card/CompactUploadCardRenderer.tsx
@@ -4,7 +4,8 @@ import { UploadCard, UploadCardError, CompactUploadCardProgress } from './Upload
import { TUploadAtom, UploadStatus, UploadSuccess, useBindUploadAtom } from '../../state/upload';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { TUploadContent } from '../../utils/matrix';
-import { getFileTypeIcon } from '../../utils/common';
+import { bytesToSize, getFileTypeIcon } from '../../utils/common';
+import { useMediaConfig } from '../../hooks/useMediaConfig';
type CompactUploadCardRendererProps = {
isEncrypted?: boolean;
@@ -19,10 +20,16 @@ export function CompactUploadCardRenderer({
onComplete,
}: CompactUploadCardRendererProps) {
const mx = useMatrixClient();
+ const mediaConfig = useMediaConfig();
+ const allowSize = mediaConfig['m.upload.size'] || Infinity;
+
const { upload, startUpload, cancelUpload } = useBindUploadAtom(mx, uploadAtom, isEncrypted);
const { file } = upload;
+ const fileSizeExceeded = file.size >= allowSize;
- if (upload.status === UploadStatus.Idle) startUpload();
+ if (upload.status === UploadStatus.Idle && !fileSizeExceeded) {
+ startUpload();
+ }
const removeUpload = () => {
cancelUpload();
@@ -76,7 +83,7 @@ export function CompactUploadCardRenderer({
>
) : (
<>
- {upload.status === UploadStatus.Idle && (
+ {upload.status === UploadStatus.Idle && !fileSizeExceeded && (
)}
{upload.status === UploadStatus.Loading && (
@@ -87,6 +94,15 @@ export function CompactUploadCardRenderer({
{upload.error.message}
)}
+ {upload.status === UploadStatus.Idle && fileSizeExceeded && (
+
+
+ The file size exceeds the limit. Maximum allowed size is{' '}
+ {bytesToSize(allowSize)}, but the uploaded file is{' '}
+ {bytesToSize(file.size)}.
+
+
+ )}
>
)}
diff --git a/src/app/components/upload-card/UploadCardRenderer.tsx b/src/app/components/upload-card/UploadCardRenderer.tsx
index 4383e20..0a127a0 100644
--- a/src/app/components/upload-card/UploadCardRenderer.tsx
+++ b/src/app/components/upload-card/UploadCardRenderer.tsx
@@ -4,13 +4,14 @@ import { UploadCard, UploadCardError, UploadCardProgress } from './UploadCard';
import { UploadStatus, UploadSuccess, useBindUploadAtom } from '../../state/upload';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { TUploadContent } from '../../utils/matrix';
-import { getFileTypeIcon } from '../../utils/common';
+import { bytesToSize, getFileTypeIcon } from '../../utils/common';
import {
roomUploadAtomFamily,
TUploadItem,
TUploadMetadata,
} from '../../state/room/roomInputDrafts';
import { useObjectURL } from '../../hooks/useObjectURL';
+import { useMediaConfig } from '../../hooks/useMediaConfig';
type ImagePreviewProps = { fileItem: TUploadItem; onSpoiler: (marked: boolean) => void };
function ImagePreview({ fileItem, onSpoiler }: ImagePreviewProps) {
@@ -75,12 +76,18 @@ export function UploadCardRenderer({
onComplete,
}: UploadCardRendererProps) {
const mx = useMatrixClient();
+ const mediaConfig = useMediaConfig();
+ const allowSize = mediaConfig['m.upload.size'] || Infinity;
+
const uploadAtom = roomUploadAtomFamily(fileItem.file);
const { metadata } = fileItem;
const { upload, startUpload, cancelUpload } = useBindUploadAtom(mx, uploadAtom, isEncrypted);
const { file } = upload;
+ const fileSizeExceeded = file.size >= allowSize;
- if (upload.status === UploadStatus.Idle) startUpload();
+ if (upload.status === UploadStatus.Idle && !fileSizeExceeded) {
+ startUpload();
+ }
const handleSpoiler = (marked: boolean) => {
setMetadata(fileItem, { ...metadata, markedAsSpoiler: marked });
@@ -131,7 +138,7 @@ export function UploadCardRenderer({
{fileItem.originalFile.type.startsWith('image') && (
)}
- {upload.status === UploadStatus.Idle && (
+ {upload.status === UploadStatus.Idle && !fileSizeExceeded && (
)}
{upload.status === UploadStatus.Loading && (
@@ -142,6 +149,15 @@ export function UploadCardRenderer({
{upload.error.message}
)}
+ {upload.status === UploadStatus.Idle && fileSizeExceeded && (
+
+
+ The file size exceeds the limit. Maximum allowed size is{' '}
+ {bytesToSize(allowSize)}, but the uploaded file is{' '}
+ {bytesToSize(file.size)}.
+
+
+ )}
>
}
>
diff --git a/src/app/features/common-settings/general/RoomJoinRules.tsx b/src/app/features/common-settings/general/RoomJoinRules.tsx
index c0d62a6..f47ff75 100644
--- a/src/app/features/common-settings/general/RoomJoinRules.tsx
+++ b/src/app/features/common-settings/general/RoomJoinRules.tsx
@@ -27,6 +27,11 @@ import {
} from '../../../state/hooks/roomList';
import { allRoomsAtom } from '../../../state/room-list/roomList';
import { roomToParentsAtom } from '../../../state/room/roomToParents';
+import {
+ knockRestrictedSupported,
+ knockSupported,
+ restrictedSupported,
+} from '../../../utils/matrix';
type RestrictedRoomAllowContent = {
room_id: string;
@@ -39,10 +44,9 @@ type RoomJoinRulesProps = {
export function RoomJoinRules({ powerLevels }: RoomJoinRulesProps) {
const mx = useMatrixClient();
const room = useRoom();
- const roomVersion = parseInt(room.getVersion(), 10);
- const allowKnockRestricted = roomVersion >= 10;
- const allowRestricted = roomVersion >= 8;
- const allowKnock = roomVersion >= 7;
+ const allowKnockRestricted = knockRestrictedSupported(room.getVersion());
+ const allowRestricted = restrictedSupported(room.getVersion());
+ const allowKnock = knockSupported(room.getVersion());
const roomIdToParents = useAtomValue(roomToParentsAtom);
const space = useSpaceOptionally();
diff --git a/src/app/features/common-settings/general/RoomPublish.tsx b/src/app/features/common-settings/general/RoomPublish.tsx
index e27c687..9edfe89 100644
--- a/src/app/features/common-settings/general/RoomPublish.tsx
+++ b/src/app/features/common-settings/general/RoomPublish.tsx
@@ -1,6 +1,7 @@
import React from 'react';
import { Box, color, Spinner, Switch, Text } from 'folds';
-import { MatrixError } from 'matrix-js-sdk';
+import { JoinRule, MatrixError } from 'matrix-js-sdk';
+import { RoomJoinRulesEventContent } from 'matrix-js-sdk/lib/types';
import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../../room-settings/styles.css';
import { SettingTile } from '../../../components/setting-tile';
@@ -10,6 +11,8 @@ import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { IPowerLevels, powerLevelAPI } from '../../../hooks/usePowerLevels';
import { StateEvent } from '../../../../types/matrix/room';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
+import { useStateEvent } from '../../../hooks/useStateEvent';
+import { ExtendedJoinRules } from '../../../components/JoinRulesSwitcher';
type RoomPublishProps = {
powerLevels: IPowerLevels;
@@ -23,6 +26,9 @@ export function RoomPublish({ powerLevels }: RoomPublishProps) {
StateEvent.RoomCanonicalAlias,
userPowerLevel
);
+ const joinRuleEvent = useStateEvent(room, StateEvent.RoomJoinRules);
+ const content = joinRuleEvent?.getContent
();
+ const rule: ExtendedJoinRules = (content?.join_rule as ExtendedJoinRules) ?? JoinRule.Invite;
const { visibilityState, setVisibility } = useRoomDirectoryVisibility(room.roomId);
@@ -30,6 +36,8 @@ export function RoomPublish({ powerLevels }: RoomPublishProps) {
const loading =
visibilityState.status === AsyncStatus.Loading || toggleState.status === AsyncStatus.Loading;
+ const validRule =
+ rule === JoinRule.Public || rule === JoinRule.Knock || rule === 'knock_restricted';
return (
{loading && }
@@ -47,7 +60,7 @@ export function RoomPublish({ powerLevels }: RoomPublishProps) {
)}
diff --git a/src/app/features/create-room/CreateRoom.tsx b/src/app/features/create-room/CreateRoom.tsx
new file mode 100644
index 0000000..c88bf68
--- /dev/null
+++ b/src/app/features/create-room/CreateRoom.tsx
@@ -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(
+ 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 = (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 (
+
+
+ Access
+
+
+
+ Name
+ }
+ name="nameInput"
+ autoFocus
+ size="500"
+ variant="SurfaceVariant"
+ radii="400"
+ autoComplete="off"
+ disabled={disabled}
+ />
+
+
+ Topic (Optional)
+
+
+
+ {kind === CreateRoomKind.Public && }
+
+
+
+ Options
+
+ }
+ onClick={() => setAdvance(!advance)}
+ type="button"
+ >
+ Advance Options
+
+
+
+ {kind !== CreateRoomKind.Public && (
+ <>
+
+
+ }
+ />
+
+ {advance && (allowKnock || allowKnockRestricted) && (
+
+
+ }
+ />
+
+ )}
+ >
+ )}
+
+
+
+ }
+ />
+
+ {advance && (
+
+ )}
+
+
+ {error && (
+
+
+
+
+ {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}
+
+
+
+ )}
+
+ }
+ >
+ Create
+
+
+
+ );
+}
diff --git a/src/app/features/create-room/CreateRoomModal.tsx b/src/app/features/create-room/CreateRoomModal.tsx
new file mode 100644
index 0000000..c1c9ba3
--- /dev/null
+++ b/src/app/features/create-room/CreateRoomModal.tsx
@@ -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 (
+
+ }>
+
+
+
+
+
+
+ New Room
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export function CreateRoomModalRenderer() {
+ const state = useCreateRoomModalState();
+
+ if (!state) return null;
+ return ;
+}
diff --git a/src/app/features/create-room/index.ts b/src/app/features/create-room/index.ts
new file mode 100644
index 0000000..f60c94b
--- /dev/null
+++ b/src/app/features/create-room/index.ts
@@ -0,0 +1,2 @@
+export * from './CreateRoom';
+export * from './CreateRoomModal';
diff --git a/src/app/features/create-space/CreateSpace.tsx b/src/app/features/create-space/CreateSpace.tsx
new file mode 100644
index 0000000..d964152
--- /dev/null
+++ b/src/app/features/create-space/CreateSpace.tsx
@@ -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(
+ 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 = (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 (
+
+
+ Access
+
+
+
+ Name
+ }
+ name="nameInput"
+ autoFocus
+ size="500"
+ variant="SurfaceVariant"
+ radii="400"
+ autoComplete="off"
+ disabled={disabled}
+ />
+
+
+ Topic (Optional)
+
+
+
+ {kind === CreateRoomKind.Public && }
+
+
+
+ Options
+
+ }
+ onClick={() => setAdvance(!advance)}
+ type="button"
+ >
+ Advance Options
+
+
+
+ {kind !== CreateRoomKind.Public && advance && (allowKnock || allowKnockRestricted) && (
+
+
+ }
+ />
+
+ )}
+
+
+
+ }
+ />
+
+ {advance && (
+
+ )}
+
+
+ {error && (
+
+
+
+
+ {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}
+
+
+
+ )}
+
+ }
+ >
+ Create
+
+
+
+ );
+}
diff --git a/src/app/features/create-space/CreateSpaceModal.tsx b/src/app/features/create-space/CreateSpaceModal.tsx
new file mode 100644
index 0000000..c1bc689
--- /dev/null
+++ b/src/app/features/create-space/CreateSpaceModal.tsx
@@ -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 (
+
+ }>
+
+
+
+
+
+
+ New Space
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export function CreateSpaceModalRenderer() {
+ const state = useCreateSpaceModalState();
+
+ if (!state) return null;
+ return ;
+}
diff --git a/src/app/features/create-space/index.ts b/src/app/features/create-space/index.ts
new file mode 100644
index 0000000..d203993
--- /dev/null
+++ b/src/app/features/create-space/index.ts
@@ -0,0 +1,2 @@
+export * from './CreateSpace';
+export * from './CreateSpaceModal';
diff --git a/src/app/features/lobby/Lobby.tsx b/src/app/features/lobby/Lobby.tsx
index 069e925..45610ff 100644
--- a/src/app/features/lobby/Lobby.tsx
+++ b/src/app/features/lobby/Lobby.tsx
@@ -220,14 +220,12 @@ export function Lobby() {
() =>
hierarchy
.flatMap((i) => {
- const childRooms = Array.isArray(i.rooms)
- ? i.rooms.map((r) => mx.getRoom(r.roomId))
- : [];
+ const childRooms = Array.isArray(i.rooms) ? i.rooms.map((r) => getRoom(r.roomId)) : [];
- return [mx.getRoom(i.space.roomId), ...childRooms];
+ return [getRoom(i.space.roomId), ...childRooms];
})
.filter((r) => !!r) as Room[],
- [mx, hierarchy]
+ [hierarchy, getRoom]
)
);
diff --git a/src/app/features/lobby/SpaceItem.tsx b/src/app/features/lobby/SpaceItem.tsx
index 0a4d9de..e881a97 100644
--- a/src/app/features/lobby/SpaceItem.tsx
+++ b/src/app/features/lobby/SpaceItem.tsx
@@ -30,10 +30,12 @@ import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import * as css from './SpaceItem.css';
import * as styleCss from './style.css';
import { useDraggableItem } from './DnD';
-import { openCreateRoom, openSpaceAddExisting } from '../../../client/action/navigation';
+import { openSpaceAddExisting } from '../../../client/action/navigation';
import { stopPropagation } from '../../utils/keyboard';
import { mxcUrlToHttp } from '../../utils/matrix';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
+import { useOpenCreateRoomModal } from '../../state/hooks/createRoomModal';
+import { useOpenCreateSpaceModal } from '../../state/hooks/createSpaceModal';
function SpaceProfileLoading() {
return (
@@ -240,13 +242,14 @@ function RootSpaceProfile({ closed, categoryId, handleClose }: RootSpaceProfileP
function AddRoomButton({ item }: { item: HierarchyItem }) {
const [cords, setCords] = useState();
+ const openCreateRoomModal = useOpenCreateRoomModal();
const handleAddRoom: MouseEventHandler = (evt) => {
setCords(evt.currentTarget.getBoundingClientRect());
};
const handleCreateRoom = () => {
- openCreateRoom(false, item.roomId as any);
+ openCreateRoomModal(item.roomId);
setCords(undefined);
};
@@ -303,13 +306,14 @@ function AddRoomButton({ item }: { item: HierarchyItem }) {
function AddSpaceButton({ item }: { item: HierarchyItem }) {
const [cords, setCords] = useState();
+ const openCreateSpaceModal = useOpenCreateSpaceModal();
const handleAddSpace: MouseEventHandler = (evt) => {
setCords(evt.currentTarget.getBoundingClientRect());
};
const handleCreateSpace = () => {
- openCreateRoom(true, item.roomId as any);
+ openCreateSpaceModal(item.roomId as any);
setCords(undefined);
};
@@ -470,7 +474,7 @@ export const SpaceItemCard = as<'div', SpaceItemCardProps>(
>
)}
- {canEditChild && (
+ {space && canEditChild && (
{item.parentId === undefined && }
diff --git a/src/app/features/room/RoomViewHeader.tsx b/src/app/features/room/RoomViewHeader.tsx
index 63e9d55..d8e2e8b 100644
--- a/src/app/features/room/RoomViewHeader.tsx
+++ b/src/app/features/room/RoomViewHeader.tsx
@@ -34,7 +34,7 @@ import { RoomTopicViewer } from '../../components/room-topic-viewer';
import { StateEvent } from '../../../types/matrix/room';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useRoom } from '../../hooks/useRoom';
-import { useSetSetting, useSetting } from '../../state/hooks/settings';
+import { useSetting } from '../../state/hooks/settings';
import { settingsAtom } from '../../state/settings';
import { useSpaceOptionally } from '../../hooks/useSpace';
import { getHomeSearchPath, getSpaceSearchPath, withSearchParam } from '../../pages/pathUtils';
@@ -260,7 +260,7 @@ export function RoomViewHeader() {
? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96, 'crop') ?? undefined
: undefined;
- const setPeopleDrawer = useSetSetting(settingsAtom, 'isPeopleDrawer');
+ const [peopleDrawer, setPeopleDrawer] = useSetting(settingsAtom, 'isPeopleDrawer');
const handleSearchClick = () => {
const searchParams: _SearchPathSearchParams = {
@@ -434,7 +434,7 @@ export function RoomViewHeader() {
offset={4}
tooltip={
- Members
+ {peopleDrawer ? 'Hide Members' : 'Show Members'}
}
>
diff --git a/src/app/features/settings/about/About.tsx b/src/app/features/settings/about/About.tsx
index 5a80e06..a26705c 100644
--- a/src/app/features/settings/about/About.tsx
+++ b/src/app/features/settings/about/About.tsx
@@ -46,16 +46,18 @@ export function About({ requestClose }: AboutProps) {
- Cinny
- v{cons.version}
+ Gaboule Chat
+ {cons.version}
- Yet another matrix client.
+ Yet another matrix client.
+ This is a fork of Cinny.
+
+
+
+ }>
+ Join with Address
+
+ Become a part of existing community.
+
+
+
+
+
+
+ }
+ >
+
+
+
+
+ )}
+
+
+ );
+}
diff --git a/src/app/pages/client/sidebar/SettingsTab.tsx b/src/app/pages/client/sidebar/SettingsTab.tsx
index 83cd118..bb21218 100644
--- a/src/app/pages/client/sidebar/SettingsTab.tsx
+++ b/src/app/pages/client/sidebar/SettingsTab.tsx
@@ -28,7 +28,7 @@ export function SettingsTab() {
return (
-
+
{(triggerRef) => (
{
return generatePath(EXPLORE_SERVER_PATH, params);
};
+export const getCreatePath = (): string => CREATE_PATH;
+
export const getInboxPath = (): string => INBOX_PATH;
export const getInboxNotificationsPath = (): string => INBOX_NOTIFICATIONS_PATH;
export const getInboxInvitesPath = (): string => INBOX_INVITES_PATH;
diff --git a/src/app/pages/paths.ts b/src/app/pages/paths.ts
index 54da892..9cf4bfb 100644
--- a/src/app/pages/paths.ts
+++ b/src/app/pages/paths.ts
@@ -74,6 +74,8 @@ export type ExploreServerPathSearchParams = {
};
export const EXPLORE_SERVER_PATH = `/explore/${_SERVER_PATH}`;
+export const CREATE_PATH = '/create';
+
export const _NOTIFICATIONS_PATH = 'notifications/';
export const _INVITES_PATH = 'invites/';
export const INBOX_PATH = '/inbox/';
diff --git a/src/app/state/createRoomModal.ts b/src/app/state/createRoomModal.ts
new file mode 100644
index 0000000..81af5d5
--- /dev/null
+++ b/src/app/state/createRoomModal.ts
@@ -0,0 +1,7 @@
+import { atom } from 'jotai';
+
+export type CreateRoomModalState = {
+ spaceId?: string;
+};
+
+export const createRoomModalAtom = atom(undefined);
diff --git a/src/app/state/createSpaceModal.ts b/src/app/state/createSpaceModal.ts
new file mode 100644
index 0000000..fe4db75
--- /dev/null
+++ b/src/app/state/createSpaceModal.ts
@@ -0,0 +1,7 @@
+import { atom } from 'jotai';
+
+export type CreateSpaceModalState = {
+ spaceId?: string;
+};
+
+export const createSpaceModalAtom = atom(undefined);
diff --git a/src/app/state/hooks/createRoomModal.ts b/src/app/state/hooks/createRoomModal.ts
new file mode 100644
index 0000000..15db728
--- /dev/null
+++ b/src/app/state/hooks/createRoomModal.ts
@@ -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;
+};
diff --git a/src/app/state/hooks/createSpaceModal.ts b/src/app/state/hooks/createSpaceModal.ts
new file mode 100644
index 0000000..ea7cb47
--- /dev/null
+++ b/src/app/state/hooks/createSpaceModal.ts
@@ -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;
+};
diff --git a/src/app/styles/ContainerColor.css.ts b/src/app/styles/ContainerColor.css.ts
index cb1f933..cefc525 100644
--- a/src/app/styles/ContainerColor.css.ts
+++ b/src/app/styles/ContainerColor.css.ts
@@ -1,6 +1,6 @@
import { ComplexStyleRule } from '@vanilla-extract/css';
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 => ({
vars: {
@@ -9,6 +9,20 @@ const getVariant = (variant: TContainerColor): ComplexStyleRule => ({
outlineColor: color[variant].ContainerLine,
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({
diff --git a/src/app/utils/common.ts b/src/app/utils/common.ts
index 34e1ecb..678f1b6 100644
--- a/src/app/utils/common.ts
+++ b/src/app/utils/common.ts
@@ -18,6 +18,13 @@ export const millisecondsToMinutesAndSeconds = (milliseconds: number): string =>
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 => {
const mm = Math.floor(seconds / 60);
const ss = Math.round(seconds % 60);
diff --git a/src/app/utils/matrix.ts b/src/app/utils/matrix.ts
index 610ef0a..b31677a 100644
--- a/src/app/utils/matrix.ts
+++ b/src/app/utils/matrix.ts
@@ -344,3 +344,16 @@ export const rateLimitedActions = async (
}
}
};
+
+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);
+};
diff --git a/src/client/state/cons.js b/src/client/state/cons.js
index 1cb8b10..5ef1a62 100644
--- a/src/client/state/cons.js
+++ b/src/client/state/cons.js
@@ -1,5 +1,5 @@
const cons = {
- version: '4.8.1',
+ version: 'latest',
secretKey: {
ACCESS_TOKEN: 'cinny_access_token',
DEVICE_ID: 'cinny_device_id',