;
+};
+
+export default SpaceBasicSettings;
diff --git a/src/components/views/spaces/SpaceCreateMenu.tsx b/src/components/views/spaces/SpaceCreateMenu.tsx
new file mode 100644
index 0000000000..9d0543a6c5
--- /dev/null
+++ b/src/components/views/spaces/SpaceCreateMenu.tsx
@@ -0,0 +1,175 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React, {useContext, useState} from "react";
+import classNames from "classnames";
+import {EventType, RoomType, RoomCreateTypeField} from "matrix-js-sdk/src/@types/event";
+
+import {_t} from "../../../languageHandler";
+import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
+import {ChevronFace, ContextMenu} from "../../structures/ContextMenu";
+import FormButton from "../elements/FormButton";
+import createRoom, {IStateEvent, Preset} from "../../../createRoom";
+import MatrixClientContext from "../../../contexts/MatrixClientContext";
+import SpaceBasicSettings from "./SpaceBasicSettings";
+import AccessibleButton from "../elements/AccessibleButton";
+import FocusLock from "react-focus-lock";
+
+const SpaceCreateMenuType = ({ title, description, className, onClick }) => {
+ return (
+
+
+ {
+ visibility === Visibility.Public
+ ? _t("Personalise your public space")
+ : _t("Personalise your private space")
+ }
+
+
+ {
+ _t("Give it a photo, name and description to help you identify it.")
+ } {
+ _t("You can change these at any point.")
+ }
+
+
+
+
+
+ ;
+ }
+
+ return
+
+ { body }
+
+ ;
+}
+
+export default SpaceCreateMenu;
diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx
index 760181e0e0..48e2c86b2c 100644
--- a/src/components/views/spaces/SpacePanel.tsx
+++ b/src/components/views/spaces/SpacePanel.tsx
@@ -20,6 +20,8 @@ import {Room} from "matrix-js-sdk/src/models/room";
import {_t} from "../../../languageHandler";
import RoomAvatar from "../avatars/RoomAvatar";
+import {useContextMenu} from "../../structures/ContextMenu";
+import SpaceCreateMenu from "./SpaceCreateMenu";
import {SpaceItem} from "./SpaceTreeLevel";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import {useEventEmitter} from "../../../hooks/useEventEmitter";
@@ -112,9 +114,21 @@ const useSpaces = (): [Room[], Room | null] => {
};
const SpacePanel = () => {
+ // We don't need the handle as we position the menu in a constant location
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
const [spaces, activeSpace] = useSpaces();
const [isPanelCollapsed, setPanelCollapsed] = useState(true);
+ const newClasses = classNames("mx_SpaceButton_new", {
+ mx_SpaceButton_newCancel: menuDisplayed,
+ });
+
+ let contextMenu = null;
+ if (menuDisplayed) {
+ contextMenu = ;
+ }
+
const onKeyDown = (ev: React.KeyboardEvent) => {
let handled = true;
@@ -203,12 +217,19 @@ const SpacePanel = () => {
onExpand={() => setPanelCollapsed(false)}
/>) }
+ setPanelCollapsed(!isPanelCollapsed)}
title={expandCollapseButtonTitle}
/>
+ { contextMenu }
)}
diff --git a/src/components/views/spaces/SpacePublicShare.tsx b/src/components/views/spaces/SpacePublicShare.tsx
new file mode 100644
index 0000000000..064d1640a2
--- /dev/null
+++ b/src/components/views/spaces/SpacePublicShare.tsx
@@ -0,0 +1,65 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React, {useState} from "react";
+import {Room} from "matrix-js-sdk/src/models/room";
+
+import {_t} from "../../../languageHandler";
+import AccessibleButton from "../elements/AccessibleButton";
+import {copyPlaintext} from "../../../utils/strings";
+import {sleep} from "../../../utils/promise";
+import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
+import {showSpaceInviteDialog} from "../../../RoomInvite";
+
+interface IProps {
+ space: Room;
+ onFinished(): void;
+}
+
+const SpacePublicShare = ({ space, onFinished }: IProps) => {
+ const [copiedText, setCopiedText] = useState(_t("Click to copy"));
+
+ return
+ {
+ const permalinkCreator = new RoomPermalinkCreator(space);
+ permalinkCreator.load();
+ const success = await copyPlaintext(permalinkCreator.forRoom());
+ const text = success ? _t("Copied!") : _t("Failed to copy");
+ setCopiedText(text);
+ await sleep(10);
+ if (copiedText === text) { // if the text hasn't changed by another click then clear it after some time
+ setCopiedText(_t("Click to copy"));
+ }
+ }}
+ >
+ { _t("Share invite link") }
+ { copiedText }
+
+ {
+ showSpaceInviteDialog(space.roomId);
+ onFinished();
+ }}
+ >
+ { _t("Invite by email or username") }
+
+
;
+};
+
+export default SpacePublicShare;
diff --git a/src/createRoom.ts b/src/createRoom.ts
index 9e3960cdb7..a5343076ac 100644
--- a/src/createRoom.ts
+++ b/src/createRoom.ts
@@ -17,6 +17,7 @@ limitations under the License.
import { MatrixClient } from "matrix-js-sdk/src/client";
import { Room } from "matrix-js-sdk/src/models/room";
+import { EventType } from "matrix-js-sdk/src/@types/event";
import { MatrixClientPeg } from './MatrixClientPeg';
import Modal from './Modal';
@@ -31,6 +32,8 @@ import GroupStore from "./stores/GroupStore";
import CountlyAnalytics from "./CountlyAnalytics";
import { isJoinedOrNearlyJoined } from "./utils/membership";
import { VIRTUAL_ROOM_EVENT_TYPE } from "./CallHandler";
+import SpaceStore from "./stores/SpaceStore";
+import { makeSpaceParentEvent } from "./utils/space";
// we define a number of interfaces which take their names from the js-sdk
/* eslint-disable camelcase */
@@ -41,7 +44,7 @@ enum Visibility {
Private = "private",
}
-enum Preset {
+export enum Preset {
PrivateChat = "private_chat",
TrustedPrivateChat = "trusted_private_chat",
PublicChat = "public_chat",
@@ -54,7 +57,7 @@ interface Invite3PID {
address: string;
}
-interface IStateEvent {
+export interface IStateEvent {
type: string;
state_key?: string; // defaults to an empty string
content: object;
@@ -75,7 +78,7 @@ interface ICreateOpts {
power_level_content_override?: object;
}
-interface IOpts {
+export interface IOpts {
dmUserId?: string;
createOpts?: ICreateOpts;
spinner?: boolean;
@@ -84,6 +87,7 @@ interface IOpts {
inlineErrors?: boolean;
andView?: boolean;
associatedWithCommunity?: string;
+ parentSpace?: Room;
}
/**
@@ -175,6 +179,16 @@ export default function createRoom(opts: IOpts): Promise {
});
}
+ if (opts.parentSpace) {
+ opts.createOpts.initial_state.push(makeSpaceParentEvent(opts.parentSpace, true));
+ opts.createOpts.initial_state.push({
+ type: EventType.RoomHistoryVisibility,
+ content: {
+ "history_visibility": opts.createOpts.preset === Preset.PublicChat ? "world_readable" : "invited",
+ },
+ });
+ }
+
let modal;
if (opts.spinner) modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
@@ -189,6 +203,9 @@ export default function createRoom(opts: IOpts): Promise {
return Promise.resolve();
}
}).then(() => {
+ if (opts.parentSpace) {
+ return SpaceStore.instance.addRoomToSpace(opts.parentSpace, roomId, [client.getDomain()], true);
+ }
if (opts.associatedWithCommunity) {
return GroupStore.addRoomToGroup(opts.associatedWithCommunity, roomId, false);
}
@@ -197,6 +214,9 @@ export default function createRoom(opts: IOpts): Promise {
// room has been created, so we race here with the client knowing that
// the room exists, causing things like
// https://github.com/vector-im/vector-web/issues/1813
+ // Even if we were to block on the echo, servers tend to split the room
+ // state over multiple syncs so we can't atomically know when we have the
+ // entire thing.
if (opts.andView) {
dis.dispatch({
action: 'view_room',
@@ -206,6 +226,7 @@ export default function createRoom(opts: IOpts): Promise {
// so we are expecting the room to come down the sync
// stream, if it hasn't already.
joining: true,
+ justCreatedOpts: opts,
});
}
CountlyAnalytics.instance.trackRoomCreate(startTime, roomId);
diff --git a/src/dispatcher/payloads/SetRightPanelPhasePayload.ts b/src/dispatcher/payloads/SetRightPanelPhasePayload.ts
index 4126e8a669..430fad6145 100644
--- a/src/dispatcher/payloads/SetRightPanelPhasePayload.ts
+++ b/src/dispatcher/payloads/SetRightPanelPhasePayload.ts
@@ -15,6 +15,7 @@ limitations under the License.
*/
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
+import { Room } from "matrix-js-sdk/src/models/room";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
import { ActionPayload } from "../payloads";
@@ -35,4 +36,5 @@ export interface SetRightPanelPhaseRefireParams {
// XXX: The type for event should 'view_3pid_invite' action's payload
event?: any;
widgetId?: string;
+ space?: Room;
}
diff --git a/src/hooks/useStateArray.ts b/src/hooks/useStateArray.ts
new file mode 100644
index 0000000000..e8ff6efff0
--- /dev/null
+++ b/src/hooks/useStateArray.ts
@@ -0,0 +1,29 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import {useState} from "react";
+
+// Hook to simplify managing state of arrays of a common type
+export const useStateArray = (initialSize: number, initialState: T | T[]): [T[], (i: number, v: T) => void] => {
+ const [data, setData] = useState(() => {
+ return Array.isArray(initialState) ? initialState : new Array(initialSize).fill(initialState);
+ });
+ return [data, (index: number, value: T) => setData(data => {
+ const copy = [...data];
+ copy[index] = value;
+ return copy;
+ })]
+};
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 4fd60f9466..8abe12c528 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -978,11 +978,32 @@
"From %(deviceName)s (%(deviceId)s)": "From %(deviceName)s (%(deviceId)s)",
"Decline (%(counter)s)": "Decline (%(counter)s)",
"Accept to continue:": "Accept to continue:",
+ "Delete": "Delete",
+ "Upload": "Upload",
+ "Name": "Name",
+ "Description": "Description",
+ "Create a space": "Create a space",
+ "Organise rooms into spaces, for just you or anyone": "Organise rooms into spaces, for just you or anyone",
+ "Public": "Public",
+ "Open space for anyone, best for communities": "Open space for anyone, best for communities",
+ "Private": "Private",
+ "Invite only space, best for yourself or teams": "Invite only space, best for yourself or teams",
+ "Go back": "Go back",
+ "Personalise your public space": "Personalise your public space",
+ "Personalise your private space": "Personalise your private space",
+ "Give it a photo, name and description to help you identify it.": "Give it a photo, name and description to help you identify it.",
+ "You can change these at any point.": "You can change these at any point.",
+ "Creating...": "Creating...",
+ "Create": "Create",
"Expand space panel": "Expand space panel",
"Collapse space panel": "Collapse space panel",
"Home": "Home",
+ "Click to copy": "Click to copy",
+ "Copied!": "Copied!",
+ "Failed to copy": "Failed to copy",
+ "Share invite link": "Share invite link",
+ "Invite by email or username": "Invite by email or username",
"Remove": "Remove",
- "Upload": "Upload",
"This bridge was provisioned by .": "This bridge was provisioned by .",
"This bridge is managed by .": "This bridge is managed by .",
"Workspace: ": "Workspace: ",
@@ -1136,7 +1157,6 @@
"Disconnect anyway": "Disconnect anyway",
"You are still sharing your personal data on the identity server .": "You are still sharing your personal data on the identity server .",
"We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.",
- "Go back": "Go back",
"Identity Server (%(server)s)": "Identity Server (%(server)s)",
"You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.",
"If you don't want to use to discover and be discoverable by existing contacts you know, enter another identity server below.": "If you don't want to use to discover and be discoverable by existing contacts you know, enter another identity server below.",
@@ -1800,8 +1820,6 @@
"%(senderDisplayName)s changed the room avatar to ": "%(senderDisplayName)s changed the room avatar to ",
"Click here to see older messages.": "Click here to see older messages.",
"This room is a continuation of another conversation.": "This room is a continuation of another conversation.",
- "Copied!": "Copied!",
- "Failed to copy": "Failed to copy",
"Add an Integration": "Add an Integration",
"You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?",
"Edited at %(date)s": "Edited at %(date)s",
@@ -2012,7 +2030,6 @@
"You can change this later if needed.": "You can change this later if needed.",
"What's the name of your community or team?": "What's the name of your community or team?",
"Enter name": "Enter name",
- "Create": "Create",
"Add image (optional)": "Add image (optional)",
"An image will help people identify your community.": "An image will help people identify your community.",
"Community IDs cannot be empty.": "Community IDs cannot be empty.",
@@ -2034,7 +2051,6 @@
"Create a public room": "Create a public room",
"Create a private room": "Create a private room",
"Create a room in %(communityName)s": "Create a room in %(communityName)s",
- "Name": "Name",
"Topic (optional)": "Topic (optional)",
"Make this room public": "Make this room public",
"Block anyone not part of %(serverName)s from ever joining this room.": "Block anyone not part of %(serverName)s from ever joining this room.",
@@ -2152,8 +2168,11 @@
"Start a conversation with someone using their name or username (like ).": "Start a conversation with someone using their name or username (like ).",
"This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click here": "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click here",
"Go": "Go",
+ "Invite to this space": "Invite to this space",
"Invite someone using their name, email address, username (like ) or share this room.": "Invite someone using their name, email address, username (like ) or share this room.",
"Invite someone using their name, username (like ) or share this room.": "Invite someone using their name, username (like ) or share this room.",
+ "Invite someone using their name, email address, username (like ) or share this space.": "Invite someone using their name, email address, username (like ) or share this space.",
+ "Invite someone using their name, username (like ) or share this space.": "Invite someone using their name, username (like ) or share this space.",
"Transfer": "Transfer",
"a new master key signature": "a new master key signature",
"a new cross-signing key signature": "a new cross-signing key signature",
@@ -2457,7 +2476,6 @@
"Your community hasn't got a Long Description, a HTML page to show to community members. Click here to open settings and give it one!": "Your community hasn't got a Long Description, a HTML page to show to community members. Click here to open settings and give it one!",
"Long Description (HTML)": "Long Description (HTML)",
"Upload avatar": "Upload avatar",
- "Description": "Description",
"Community %(groupId)s not found": "Community %(groupId)s not found",
"This homeserver does not support communities": "This homeserver does not support communities",
"Failed to load %(groupId)s": "Failed to load %(groupId)s",
@@ -2539,6 +2557,37 @@
"Failed to reject invite": "Failed to reject invite",
"You have %(count)s unread notifications in a prior version of this room.|other": "You have %(count)s unread notifications in a prior version of this room.",
"You have %(count)s unread notifications in a prior version of this room.|one": "You have %(count)s unread notification in a prior version of this room.",
+ "Accept Invite": "Accept Invite",
+ "%(count)s members|other": "%(count)s members",
+ "%(count)s members|one": "%(count)s member",
+ " invited you to ": " invited you to ",
+ "You have been invited to ": "You have been invited to ",
+ "Your public space ": "Your public space ",
+ "Your private space ": "Your private space ",
+ "Welcome to ": "Welcome to ",
+ "Random": "Random",
+ "Support": "Support",
+ "Room name": "Room name",
+ "Failed to create initial space rooms": "Failed to create initial space rooms",
+ "Skip for now": "Skip for now",
+ "Creating rooms...": "Creating rooms...",
+ "Share your public space": "Share your public space",
+ "At the moment only you can see it.": "At the moment only you can see it.",
+ "Finish": "Finish",
+ "Who are you working with?": "Who are you working with?",
+ "Ensure the right people have access to the space.": "Ensure the right people have access to the space.",
+ "Just Me": "Just Me",
+ "A private space just for you": "A private space just for you",
+ "Me and my teammates": "Me and my teammates",
+ "A private space for you and your teammates": "A private space for you and your teammates",
+ "Failed to invite the following users to your space: %(csvUsers)s": "Failed to invite the following users to your space: %(csvUsers)s",
+ "Invite your teammates": "Invite your teammates",
+ "Invite by username": "Invite by username",
+ "Inviting...": "Inviting...",
+ "What discussions do you want to have?": "What discussions do you want to have?",
+ "We'll create rooms for each topic.": "We'll create rooms for each topic.",
+ "What projects are you working on?": "What projects are you working on?",
+ "We'll create rooms for each of them. You can add existing rooms after setup.": "We'll create rooms for each of them. You can add existing rooms after setup.",
"Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.",
"Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.",
"Failed to load timeline position": "Failed to load timeline position",
diff --git a/src/stores/RightPanelStorePhases.ts b/src/stores/RightPanelStorePhases.ts
index 11b51dfc2d..aea78c7460 100644
--- a/src/stores/RightPanelStorePhases.ts
+++ b/src/stores/RightPanelStorePhases.ts
@@ -31,6 +31,11 @@ export enum RightPanelPhases {
GroupRoomList = 'GroupRoomList',
GroupRoomInfo = 'GroupRoomInfo',
GroupMemberInfo = 'GroupMemberInfo',
+
+ // Space stuff
+ SpaceMemberList = "SpaceMemberList",
+ SpaceMemberInfo = "SpaceMemberInfo",
+ Space3pidMemberInfo = "Space3pidMemberInfo",
}
// These are the phases that are safe to persist (the ones that don't require additional
@@ -43,3 +48,10 @@ export const RIGHT_PANEL_PHASES_NO_ARGS = [
RightPanelPhases.GroupMemberList,
RightPanelPhases.GroupRoomList,
];
+
+// Subset of phases visible in the Space View
+export const RIGHT_PANEL_SPACE_PHASES = [
+ RightPanelPhases.SpaceMemberList,
+ RightPanelPhases.Space3pidMemberInfo,
+ RightPanelPhases.SpaceMemberInfo,
+];
diff --git a/src/utils/permalinks/Permalinks.js b/src/utils/permalinks/Permalinks.js
index 086abc669d..bcf4d87136 100644
--- a/src/utils/permalinks/Permalinks.js
+++ b/src/utils/permalinks/Permalinks.js
@@ -14,9 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import {MatrixClientPeg} from "../../MatrixClientPeg";
import isIp from "is-ip";
-import * as utils from 'matrix-js-sdk/src/utils';
+import * as utils from "matrix-js-sdk/src/utils";
+import {Room} from "matrix-js-sdk/src/models/room";
+
+import {MatrixClientPeg} from "../../MatrixClientPeg";
import SpecPermalinkConstructor, {baseUrl as matrixtoBaseUrl} from "./SpecPermalinkConstructor";
import PermalinkConstructor, {PermalinkParts} from "./PermalinkConstructor";
import ElementPermalinkConstructor from "./ElementPermalinkConstructor";
@@ -121,6 +123,10 @@ export class RoomPermalinkCreator {
this._started = false;
}
+ get serverCandidates() {
+ return this._serverCandidates;
+ }
+
isStarted() {
return this._started;
}
@@ -451,3 +457,9 @@ function isHostnameIpAddress(hostname) {
return isIp(hostname);
}
+
+export const calculateRoomVia = (room: Room) => {
+ const permalinkCreator = new RoomPermalinkCreator(room);
+ permalinkCreator.load();
+ return permalinkCreator.serverCandidates;
+};
diff --git a/src/utils/space.ts b/src/utils/space.ts
new file mode 100644
index 0000000000..98801cabd0
--- /dev/null
+++ b/src/utils/space.ts
@@ -0,0 +1,39 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import {Room} from "matrix-js-sdk/src/models/room";
+import {MatrixClient} from "matrix-js-sdk/src/client";
+import {EventType} from "matrix-js-sdk/src/@types/event";
+
+import {calculateRoomVia} from "../utils/permalinks/Permalinks";
+
+export const shouldShowSpaceSettings = (cli: MatrixClient, space: Room) => {
+ const userId = cli.getUserId();
+ return space.getMyMembership() === "join"
+ && (space.currentState.maySendStateEvent(EventType.RoomAvatar, userId)
+ || space.currentState.maySendStateEvent(EventType.RoomName, userId)
+ || space.currentState.maySendStateEvent(EventType.RoomTopic, userId)
+ || space.currentState.maySendStateEvent(EventType.RoomJoinRules, userId));
+};
+
+export const makeSpaceParentEvent = (room: Room, canonical = false) => ({
+ type: EventType.SpaceParent,
+ content: {
+ "via": calculateRoomVia(room),
+ "canonical": canonical,
+ },
+ state_key: room.roomId,
+});