From 90bb7c1482c38a234099d023b4bb1a4ae0232f57 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 8 Jun 2021 16:33:47 +0100 Subject: [PATCH] Switch Space Settings for a tabbed view with a bunch more settings exposed --- res/css/views/dialogs/_SettingsDialog.scss | 2 +- .../views/dialogs/_SpaceSettingsDialog.scss | 51 ++++- res/img/element-icons/eye.svg | 3 + .../views/dialogs/SpaceSettingsDialog.tsx | 172 +++++------------ .../tabs/room/SecurityRoomSettingsTab.tsx | 6 +- .../views/spaces/SpaceSettingsGeneralTab.tsx | 143 ++++++++++++++ .../spaces/SpaceSettingsVisibilityTab.tsx | 181 ++++++++++++++++++ src/i18n/strings/en_EN.json | 40 ++-- 8 files changed, 456 insertions(+), 142 deletions(-) create mode 100644 res/img/element-icons/eye.svg create mode 100644 src/components/views/spaces/SpaceSettingsGeneralTab.tsx create mode 100644 src/components/views/spaces/SpaceSettingsVisibilityTab.tsx diff --git a/res/css/views/dialogs/_SettingsDialog.scss b/res/css/views/dialogs/_SettingsDialog.scss index 6c4ed35c5a..b3b6802c3d 100644 --- a/res/css/views/dialogs/_SettingsDialog.scss +++ b/res/css/views/dialogs/_SettingsDialog.scss @@ -15,7 +15,7 @@ limitations under the License. */ // Not actually a component but things shared by settings components -.mx_UserSettingsDialog, .mx_RoomSettingsDialog { +.mx_UserSettingsDialog, .mx_RoomSettingsDialog, .mx_SpaceSettingsDialog { width: 90vw; max-width: 1000px; // set the height too since tabbed view scrolls itself. diff --git a/res/css/views/dialogs/_SpaceSettingsDialog.scss b/res/css/views/dialogs/_SpaceSettingsDialog.scss index 6e5fd9c8c8..fa074fdbe8 100644 --- a/res/css/views/dialogs/_SpaceSettingsDialog.scss +++ b/res/css/views/dialogs/_SpaceSettingsDialog.scss @@ -15,7 +15,6 @@ limitations under the License. */ .mx_SpaceSettingsDialog { - width: 480px; color: $primary-fg-color; .mx_SpaceSettings_errorText { @@ -32,8 +31,44 @@ limitations under the License. margin-left: 16px; } - .mx_AccessibleButton_kind_danger { - margin-top: 28px; + .mx_SettingsTab_section { + .mx_SettingsTab_section_caption { + margin-top: 12px; + margin-bottom: 20px; + } + + & + .mx_SettingsTab_subheading { + border-top: 1px solid $message-body-panel-bg-color; + margin-top: 0; + padding-top: 24px; + } + + .mx_RadioButton { + margin-top: 8px; + margin-bottom: 4px; + + .mx_RadioButton_content { + font-weight: $font-semi-bold; + line-height: $font-18px; + color: $primary-fg-color; + } + + & + span { + font-size: $font-15px; + line-height: $font-18px; + color: $secondary-fg-color; + margin-left: 26px; + } + } + + .mx_SettingsTab_showAdvanced { + margin: 16px 0; + padding: 0; + } + + .mx_SettingsFlag { + margin-top: 24px; + } } .mx_SpaceSettingsDialog_buttons { @@ -52,4 +87,14 @@ limitations under the License. .mx_AccessibleButton_hasKind { padding: 8px 22px; } + + .mx_TabbedView_tabLabel { + .mx_SpaceSettingsDialog_generalIcon::before { + mask-image: url('$(res)/img/element-icons/settings.svg'); + } + + .mx_SpaceSettingsDialog_visibilityIcon::before { + mask-image: url('$(res)/img/element-icons/eye.svg'); + } + } } diff --git a/res/img/element-icons/eye.svg b/res/img/element-icons/eye.svg new file mode 100644 index 0000000000..0460a6201d --- /dev/null +++ b/res/img/element-icons/eye.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/views/dialogs/SpaceSettingsDialog.tsx b/src/components/views/dialogs/SpaceSettingsDialog.tsx index a135b6bc16..1273f06401 100644 --- a/src/components/views/dialogs/SpaceSettingsDialog.tsx +++ b/src/components/views/dialogs/SpaceSettingsDialog.tsx @@ -14,24 +14,27 @@ 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 {MatrixClient} from "matrix-js-sdk/src/client"; -import {EventType} from "matrix-js-sdk/src/@types/event"; +import React, { useMemo } from 'react'; +import { Room } from "matrix-js-sdk/src/models/room"; +import { MatrixClient } from "matrix-js-sdk/src/client"; -import {_t} from '../../../languageHandler'; -import {IDialogProps} from "./IDialogProps"; +import { _t, _td } from '../../../languageHandler'; +import { IDialogProps } from "./IDialogProps"; import BaseDialog from "./BaseDialog"; -import DevtoolsDialog from "./DevtoolsDialog"; -import SpaceBasicSettings from '../spaces/SpaceBasicSettings'; -import {getTopic} from "../elements/RoomTopic"; -import {avatarUrlForRoom} from "../../../Avatar"; -import ToggleSwitch from "../elements/ToggleSwitch"; -import AccessibleButton from "../elements/AccessibleButton"; -import Modal from "../../../Modal"; import defaultDispatcher from "../../../dispatcher/dispatcher"; -import {useDispatcher} from "../../../hooks/useDispatcher"; -import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView"; +import { useDispatcher } from "../../../hooks/useDispatcher"; +import TabbedView, { Tab } from "../../structures/TabbedView"; +import SpaceSettingsGeneralTab from '../spaces/SpaceSettingsGeneralTab'; +import SpaceSettingsVisibilityTab from "../spaces/SpaceSettingsVisibilityTab"; +import SettingsStore from "../../../settings/SettingsStore"; +import {UIFeature} from "../../../settings/UIFeature"; +import AdvancedRoomSettingsTab from "../settings/tabs/room/AdvancedRoomSettingsTab"; + +export enum SpaceSettingsTab { + General = "SPACE_GENERAL_TAB", + Visibility = "SPACE_VISIBILITY_TAB", + Advanced = "SPACE_ADVANCED_TAB", +} interface IProps extends IDialogProps { matrixClient: MatrixClient; @@ -45,63 +48,30 @@ const SpaceSettingsDialog: React.FC = ({ matrixClient: cli, space, onFin } }); - const [busy, setBusy] = useState(false); - const [error, setError] = useState(""); - - const userId = cli.getUserId(); - - const [newAvatar, setNewAvatar] = useState(null); // undefined means to remove avatar - const canSetAvatar = space.currentState.maySendStateEvent(EventType.RoomAvatar, userId); - const avatarChanged = newAvatar !== null; - - const [name, setName] = useState(space.name); - const canSetName = space.currentState.maySendStateEvent(EventType.RoomName, userId); - const nameChanged = name !== space.name; - - const currentTopic = getTopic(space); - const [topic, setTopic] = useState(currentTopic); - const canSetTopic = space.currentState.maySendStateEvent(EventType.RoomTopic, userId); - const topicChanged = topic !== currentTopic; - - const currentJoinRule = space.getJoinRule(); - const [joinRule, setJoinRule] = useState(currentJoinRule); - const canSetJoinRule = space.currentState.maySendStateEvent(EventType.RoomJoinRules, userId); - const joinRuleChanged = joinRule !== currentJoinRule; - - const onSave = async () => { - setBusy(true); - const promises = []; - - if (avatarChanged) { - if (newAvatar) { - promises.push(cli.sendStateEvent(space.roomId, EventType.RoomAvatar, { - url: await cli.uploadContent(newAvatar), - }, "")); - } else { - promises.push(cli.sendStateEvent(space.roomId, EventType.RoomAvatar, {}, "")); - } - } - - if (nameChanged) { - promises.push(cli.setRoomName(space.roomId, name)); - } - - if (topicChanged) { - promises.push(cli.setRoomTopic(space.roomId, topic)); - } - - if (joinRuleChanged) { - promises.push(cli.sendStateEvent(space.roomId, EventType.RoomJoinRules, { join_rule: joinRule }, "")); - } - - const results = await Promise.allSettled(promises); - setBusy(false); - const failures = results.filter(r => r.status === "rejected"); - if (failures.length > 0) { - console.error("Failed to save space settings: ", failures); - setError(_t("Failed to save space settings.")); - } - }; + const tabs = useMemo(() => { + return [ + new Tab( + SpaceSettingsTab.General, + _td("General"), + "mx_SpaceSettingsDialog_generalIcon", + , + ), + new Tab( + SpaceSettingsTab.Visibility, + _td("Visibility"), + "mx_SpaceSettingsDialog_visibilityIcon", + , + ), + SettingsStore.getValue(UIFeature.AdvancedSettings) + ? new Tab( + SpaceSettingsTab.Advanced, + _td("Advanced"), + "mx_RoomSettingsDialog_warningIcon", + , + ) + : null, + ].filter(Boolean); + }, [cli, space, onFinished]); return = ({ matrixClient: cli, space, onFin onFinished={onFinished} fixedWidth={false} > -
-
{ _t("Edit settings relating to your space.") }
- - { error &&
{ error }
} - - onFinished(false)} /> - - - -
- { _t("Make this space private") } - setJoinRule(checked ? "invite" : "public")} - disabled={!canSetJoinRule} - aria-label={_t("Make this space private")} - /> -
- - { - defaultDispatcher.dispatch({ - action: "leave_room", - room_id: space.roomId, - }); - }} - > - { _t("Leave Space") } - - -
- Modal.createDialog(DevtoolsDialog, {roomId: space.roomId})}> - { _t("View dev tools") } - - - { _t("Cancel") } - - - { busy ? _t("Saving...") : _t("Save Changes") } - -
+
+
; }; diff --git a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx index 02bbcfb751..99f525364e 100644 --- a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx @@ -29,19 +29,19 @@ import {UIFeature} from "../../../../../settings/UIFeature"; import { replaceableComponent } from "../../../../../utils/replaceableComponent"; // Knock and private are reserved keywords which are not yet implemented. -enum JoinRule { +export enum JoinRule { Public = "public", Knock = "knock", Invite = "invite", Private = "private", } -enum GuestAccess { +export enum GuestAccess { CanJoin = "can_join", Forbidden = "forbidden", } -enum HistoryVisibility { +export enum HistoryVisibility { Invited = "invited", Joined = "joined", Shared = "shared", diff --git a/src/components/views/spaces/SpaceSettingsGeneralTab.tsx b/src/components/views/spaces/SpaceSettingsGeneralTab.tsx new file mode 100644 index 0000000000..db0a180846 --- /dev/null +++ b/src/components/views/spaces/SpaceSettingsGeneralTab.tsx @@ -0,0 +1,143 @@ +/* +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 { MatrixClient } from "matrix-js-sdk/src/client"; +import { EventType } from "matrix-js-sdk/src/@types/event"; + +import { _t } from "../../../languageHandler"; +import AccessibleButton from "../elements/AccessibleButton"; +import { SpaceFeedbackPrompt } from "../../structures/SpaceRoomView"; +import SpaceBasicSettings from "./SpaceBasicSettings"; +import { avatarUrlForRoom } from "../../../Avatar"; +import { IDialogProps } from "../dialogs/IDialogProps"; +import { getTopic } from "../elements/RoomTopic"; +import { defaultDispatcher } from "../../../dispatcher/dispatcher"; + +interface IProps extends IDialogProps { + matrixClient: MatrixClient; + space: Room; +} + +const SpaceSettingsGeneralTab = ({ matrixClient: cli, space, onFinished }: IProps) => { + const [busy, setBusy] = useState(false); + const [error, setError] = useState(""); + + const userId = cli.getUserId(); + + const [newAvatar, setNewAvatar] = useState(null); // undefined means to remove avatar + const canSetAvatar = space.currentState.maySendStateEvent(EventType.RoomAvatar, userId); + const avatarChanged = newAvatar !== null; + + const [name, setName] = useState(space.name); + const canSetName = space.currentState.maySendStateEvent(EventType.RoomName, userId); + const nameChanged = name !== space.name; + + const currentTopic = getTopic(space); + const [topic, setTopic] = useState(currentTopic); + const canSetTopic = space.currentState.maySendStateEvent(EventType.RoomTopic, userId); + const topicChanged = topic !== currentTopic; + + const onCancel = () => { + setNewAvatar(null); + setName(space.name); + setTopic(currentTopic); + }; + + const onSave = async () => { + setBusy(true); + const promises = []; + + if (avatarChanged) { + if (newAvatar) { + promises.push(cli.sendStateEvent(space.roomId, EventType.RoomAvatar, { + url: await cli.uploadContent(newAvatar), + }, "")); + } else { + promises.push(cli.sendStateEvent(space.roomId, EventType.RoomAvatar, {}, "")); + } + } + + if (nameChanged) { + promises.push(cli.setRoomName(space.roomId, name)); + } + + if (topicChanged) { + promises.push(cli.setRoomTopic(space.roomId, topic)); + } + + const results = await Promise.allSettled(promises); + setBusy(false); + const failures = results.filter(r => r.status === "rejected"); + if (failures.length > 0) { + console.error("Failed to save space settings: ", failures); + setError(_t("Failed to save space settings.")); + } + }; + + return
+
{_t("General")}
+ +
{ _t("Edit settings relating to your space.") }
+ + { error &&
{ error }
} + + onFinished(false)} /> + +
+ + + + { _t("Cancel") } + + + { busy ? _t("Saving...") : _t("Save Changes") } + +
+ + {_t("Leave Space")} +
+ { + defaultDispatcher.dispatch({ + action: "leave_room", + room_id: space.roomId, + }); + }} + > + { _t("Leave Space") } + +
+
; +}; + +export default SpaceSettingsGeneralTab; diff --git a/src/components/views/spaces/SpaceSettingsVisibilityTab.tsx b/src/components/views/spaces/SpaceSettingsVisibilityTab.tsx new file mode 100644 index 0000000000..7fc3514b2d --- /dev/null +++ b/src/components/views/spaces/SpaceSettingsVisibilityTab.tsx @@ -0,0 +1,181 @@ +/* +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 { MatrixClient } from "matrix-js-sdk/src/client"; +import { EventType } from "matrix-js-sdk/src/@types/event"; + +import { _t } from "../../../languageHandler"; +import AccessibleButton from "../elements/AccessibleButton"; +import AliasSettings from "../room_settings/AliasSettings"; +import { useStateToggle } from "../../../hooks/useStateToggle"; +import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; +import { GuestAccess, HistoryVisibility, JoinRule } from "../settings/tabs/room/SecurityRoomSettingsTab"; +import StyledRadioGroup from "../elements/StyledRadioGroup"; + +interface IProps { + matrixClient: MatrixClient; + space: Room; +} + +enum SpaceVisibility { + Unlisted = "unlisted", + Private = "private", +} + +const useLocalEcho = ( + currentFactory: () => T, + setterFn: (value: T) => Promise, + errorFn: (error: Error) => void, +): [value: T, handler: (value: T) => void] => { + const [value, setValue] = useState(currentFactory); + const handler = async (value: T) => { + setValue(value); + try { + await setterFn(value); + } catch (e) { + setValue(currentFactory()); + errorFn(e); + } + }; + + return [value, handler]; +}; + +const SpaceSettingsVisibilityTab = ({ matrixClient: cli, space }: IProps) => { + const [busy, setBusy] = useState(false); + const [error, setError] = useState(""); + + const userId = cli.getUserId(); + + const [visibility, setVisibility] = useLocalEcho( + () => space.getJoinRule() === JoinRule.Private ? SpaceVisibility.Private : SpaceVisibility.Unlisted, + visibility => cli.sendStateEvent(space.roomId, EventType.RoomJoinRules, { + join_rule: visibility === SpaceVisibility.Unlisted ? JoinRule.Public : JoinRule.Private, + }, ""), + () => setError(_t("Failed to update the visibility of this space")), + ); + const [guestAccessEnabled, setGuestAccessEnabled] = useLocalEcho( + () => space.currentState.getStateEvents(EventType.RoomGuestAccess, "") + ?.getContent()?.guest_access === GuestAccess.CanJoin, + guestAccessEnabled => cli.sendStateEvent(space.roomId, EventType.RoomGuestAccess, { + guest_access: guestAccessEnabled ? GuestAccess.CanJoin : GuestAccess.Forbidden, + }, ""), + () => setError(_t("Failed to update the guest access of this space")), + ); + const [historyVisibility, setHistoryVisibility] = useLocalEcho( + () => space.currentState.getStateEvents(EventType.RoomHistoryVisibility, "") + ?.getContent()?.history_visibility || HistoryVisibility.Shared, + historyVisibility => cli.sendStateEvent(space.roomId, EventType.RoomHistoryVisibility, { + history_visibility: historyVisibility, + }, ""), + () => setError(_t("Failed to update the history visibility of this space")), + ); + + const [showAdvancedSection, toggleAdvancedSection] = useStateToggle(); + + const canSetJoinRule = space.currentState.maySendStateEvent(EventType.RoomJoinRules, userId); + const canSetGuestAccess = space.currentState.maySendStateEvent(EventType.RoomGuestAccess, userId); + const canSetHistoryVisibility = space.currentState.maySendStateEvent(EventType.RoomHistoryVisibility, userId); + const canSetCanonical = space.currentState.mayClientSendStateEvent(EventType.RoomCanonicalAlias, cli); + const canonicalAliasEv = space.currentState.getStateEvents(EventType.RoomCanonicalAlias, ""); + + let advancedSection; + if (showAdvancedSection) { + advancedSection = <> + + { _t("Hide advanced") } + + + +

+ { _t("Guests can join a space without having an account.") } +
+ { _t("This may be useful for public spaces.") } +

+ ; + } else { + advancedSection = <> + + { _t("Show advanced") } + + ; + } + + return
+
{_t("Visibility")}
+ + { error &&
{ error }
} + +
+
+ { _t("Decide who can view and join %(spaceName)s.", { spaceName: space.name }) } +
+ +
+ +
+ + { advancedSection } + + { + setHistoryVisibility(checked ? HistoryVisibility.WorldReadable : HistoryVisibility.Shared); + }} + disabled={!canSetHistoryVisibility} + label={_t("Preview Space")} + /> +
{ _t("Allow people to preview your space before they join.") }
+ { _t("Recommended for public spaces.") } +
+ + {_t("Address")} +
+ +
+
; +}; + +export default SpaceSettingsVisibilityTab; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a744d8e7be..8c04a81807 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1029,6 +1029,28 @@ "Share invite link": "Share invite link", "Invite people": "Invite people", "Invite with email or username": "Invite with email or username", + "Failed to save space settings.": "Failed to save space settings.", + "General": "General", + "Edit settings relating to your space.": "Edit settings relating to your space.", + "Saving...": "Saving...", + "Save Changes": "Save Changes", + "Leave Space": "Leave Space", + "Failed to update the visibility of this space": "Failed to update the visibility of this space", + "Failed to update the guest access of this space": "Failed to update the guest access of this space", + "Failed to update the history visibility of this space": "Failed to update the history visibility of this space", + "Hide advanced": "Hide advanced", + "Enable guest access": "Enable guest access", + "Guests can join a space without having an account.": "Guests can join a space without having an account.", + "This may be useful for public spaces.": "This may be useful for public spaces.", + "Show advanced": "Show advanced", + "Visibility": "Visibility", + "Decide who can view and join %(spaceName)s.": "Decide who can view and join %(spaceName)s.", + "anyone with the link can view and join": "anyone with the link can view and join", + "Invite only": "Invite only", + "only invited people can view and join": "only invited people can view and join", + "Preview Space": "Preview Space", + "Allow people to preview your space before they join.": "Allow people to preview your space before they join.", + "Recommended for public spaces.": "Recommended for public spaces.", "Settings": "Settings", "Leave space": "Leave space", "Create new room": "Create new room", @@ -1223,8 +1245,6 @@ "Custom theme URL": "Custom theme URL", "Add theme": "Add theme", "Theme": "Theme", - "Hide advanced": "Hide advanced", - "Show advanced": "Show advanced", "Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Set the name of a font installed on your system & %(brand)s will attempt to use it.", "Enable experimental, compact IRC style layout": "Enable experimental, compact IRC style layout", "Customise your appearance": "Customise your appearance", @@ -1245,7 +1265,6 @@ "Deactivate Account": "Deactivate Account", "Deactivate account": "Deactivate account", "Discovery": "Discovery", - "General": "General", "Legal": "Legal", "Credits": "Credits", "For help with using %(brand)s, click here.": "For help with using %(brand)s, click here.", @@ -1351,6 +1370,7 @@ "Upgrade this room to the recommended room version": "Upgrade this room to the recommended room version", "this room": "this room", "View older messages in %(roomName)s.": "View older messages in %(roomName)s.", + "Space information": "Space information", "Room information": "Room information", "Internal room ID:": "Internal room ID:", "Room version": "Room version", @@ -1675,14 +1695,18 @@ "Error removing address": "Error removing address", "Main address": "Main address", "not specified": "not specified", + "This space has no local addresses": "This space has no local addresses", "This room has no local addresses": "This room has no local addresses", "Local address": "Local address", "Published Addresses": "Published Addresses", - "Published addresses can be used by anyone on any server to join your room. To publish an address, it needs to be set as a local address first.": "Published addresses can be used by anyone on any server to join your room. To publish an address, it needs to be set as a local address first.", + "Published addresses can be used by anyone on any server to join your space.": "Published addresses can be used by anyone on any server to join your space.", + "Published addresses can be used by anyone on any server to join your room.": "Published addresses can be used by anyone on any server to join your room.", + "To publish an address, it needs to be set as a local address first.": "To publish an address, it needs to be set as a local address first.", "Other published addresses:": "Other published addresses:", "No other published addresses yet, add one below": "No other published addresses yet, add one below", "New published address (e.g. #alias:server)": "New published address (e.g. #alias:server)", "Local Addresses": "Local Addresses", + "Set addresses for this space so users can find this space through your homeserver (%(localDomain)s)": "Set addresses for this space so users can find this space through your homeserver (%(localDomain)s)", "Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)": "Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)", "Show more": "Show more", "Error updating flair": "Error updating flair", @@ -2370,14 +2394,8 @@ "Share Room Message": "Share Room Message", "Link to selected message": "Link to selected message", "Command Help": "Command Help", - "Failed to save space settings.": "Failed to save space settings.", "Space settings": "Space settings", - "Edit settings relating to your space.": "Edit settings relating to your space.", - "Make this space private": "Make this space private", - "Leave Space": "Leave Space", - "View dev tools": "View dev tools", - "Saving...": "Saving...", - "Save Changes": "Save Changes", + "Settings - %(spaceName)s": "Settings - %(spaceName)s", "To help us prevent this in future, please send us logs.": "To help us prevent this in future, please send us logs.", "Missing session data": "Missing session data", "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.",