diff --git a/res/css/_components.scss b/res/css/_components.scss index ec9592f3a1..e6c55c0250 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -54,6 +54,7 @@ @import "./views/avatars/_MemberStatusMessageAvatar.scss"; @import "./views/avatars/_PulsedAvatar.scss"; @import "./views/avatars/_WidgetAvatar.scss"; +@import "./views/beta/_BetaCard.scss"; @import "./views/context_menus/_CallContextMenu.scss"; @import "./views/context_menus/_IconizedContextMenu.scss"; @import "./views/context_menus/_MessageContextMenu.scss"; @@ -237,6 +238,7 @@ @import "./views/settings/tabs/user/_AppearanceUserSettingsTab.scss"; @import "./views/settings/tabs/user/_GeneralUserSettingsTab.scss"; @import "./views/settings/tabs/user/_HelpUserSettingsTab.scss"; +@import "./views/settings/tabs/user/_LabsUserSettingsTab.scss"; @import "./views/settings/tabs/user/_MjolnirUserSettingsTab.scss"; @import "./views/settings/tabs/user/_NotificationUserSettingsTab.scss"; @import "./views/settings/tabs/user/_PreferencesUserSettingsTab.scss"; diff --git a/res/css/structures/_GroupFilterPanel.scss b/res/css/structures/_GroupFilterPanel.scss index e5a8ef6df2..444435dd57 100644 --- a/res/css/structures/_GroupFilterPanel.scss +++ b/res/css/structures/_GroupFilterPanel.scss @@ -56,6 +56,12 @@ limitations under the License. .mx_GroupFilterPanel .mx_TagTile { // opacity: 0.5; position: relative; + + .mx_BetaDot { + position: absolute; + right: -13px; + top: -11px; + } } .mx_GroupFilterPanel .mx_TagTile.mx_TagTile_prototype { diff --git a/res/css/structures/_MyGroups.scss b/res/css/structures/_MyGroups.scss index 73f1332cd0..9c0062b72d 100644 --- a/res/css/structures/_MyGroups.scss +++ b/res/css/structures/_MyGroups.scss @@ -17,6 +17,11 @@ limitations under the License. .mx_MyGroups { display: flex; flex-direction: column; + + .mx_BetaCard { + margin: 0 72px; + max-width: 760px; + } } .mx_MyGroups .mx_RoomHeader_simpleHeader { @@ -30,7 +35,7 @@ limitations under the License. flex-wrap: wrap; } -.mx_MyGroups > :not(.mx_RoomHeader) { +.mx_MyGroups > :not(.mx_RoomHeader):not(.mx_BetaCard) { max-width: 960px; margin: 40px; } diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss index 6b71f42671..c4b3f9a8d3 100644 --- a/res/css/structures/_SpaceRoomView.scss +++ b/res/css/structures/_SpaceRoomView.scss @@ -137,6 +137,44 @@ $SpaceRoomViewInnerWidth: 428px; box-sizing: border-box; box-shadow: 2px 15px 30px $dialog-shadow-color; border-radius: 8px; + position: relative; + + // XXX remove this when spaces leaves Beta + .mx_BetaCard_betaPill { + position: absolute; + right: 24px; + top: 32px; + } + // XXX remove this when spaces leaves Beta + .mx_SpaceRoomView_preview_spaceBetaPrompt { + font-weight: $font-semi-bold; + font-size: $font-14px; + line-height: $font-24px; + color: $primary-fg-color; + margin-top: 24px; + position: relative; + padding-left: 24px; + + .mx_AccessibleButton_kind_link { + display: inline; + padding: 0; + font-size: inherit; + line-height: inherit; + } + + &::before { + content: ""; + position: absolute; + height: $font-24px; + width: 20px; + left: 0; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + mask-image: url('$(res)/img/element-icons/room/room-summary.svg'); + background-color: $secondary-fg-color; + } + } .mx_SpaceRoomView_preview_inviter { display: flex; @@ -314,6 +352,23 @@ $SpaceRoomViewInnerWidth: 428px; } .mx_SpaceRoomView_inviteTeammates { + // XXX remove this when spaces leaves Beta + .mx_SpaceRoomView_inviteTeammates_betaDisclaimer { + padding: 58px 16px 16px; + position: relative; + border-radius: 8px; + background-color: $header-panel-bg-color; + max-width: $SpaceRoomViewInnerWidth; + margin: 20px 0 30px; + box-sizing: border-box; + + .mx_BetaCard_betaPill { + position: absolute; + left: 16px; + top: 16px; + } + } + .mx_SpaceRoomView_inviteTeammates_buttons { color: $secondary-fg-color; margin-top: 28px; diff --git a/res/css/views/beta/_BetaCard.scss b/res/css/views/beta/_BetaCard.scss new file mode 100644 index 0000000000..05149b7f6c --- /dev/null +++ b/res/css/views/beta/_BetaCard.scss @@ -0,0 +1,112 @@ +/* +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. +*/ + +.mx_BetaCard { + margin-bottom: 20px; + padding: 24px; + background-color: $settings-profile-placeholder-bg-color; + border-radius: 8px; + display: flex; + box-sizing: border-box; + + > div { + .mx_BetaCard_title { + font-weight: $font-semi-bold; + font-size: $font-18px; + line-height: $font-22px; + color: $primary-fg-color; + margin: 4px 0 14px; + + .mx_BetaCard_betaPill { + margin-left: 12px; + } + } + + .mx_BetaCard_caption { + font-size: $font-15px; + line-height: $font-20px; + color: $secondary-fg-color; + } + + .mx_AccessibleButton { + display: block; + margin: 20px 0; + padding: 12px 40px; + width: max-content; + } + + .mx_BetaCard_disclaimer { + font-size: $font-12px; + line-height: $font-15px; + color: $secondary-fg-color; + } + } + + > img { + margin: auto 0 auto 20px; + width: 300px; + object-fit: contain; + height: 100%; + } +} + +.mx_BetaCard_betaPill { + background-color: $accent-color-alt; + padding: 4px 10px; + border-radius: 8px; + text-transform: uppercase; + font-size: 12px; + line-height: 15px; + color: #FFFFFF; + display: inline-block; + vertical-align: text-bottom; + + &.mx_BetaCard_betaPill_clickable { + cursor: pointer; + } +} + +$pulse-color: $accent-color-alt; +$dot-size: 12px; + +.mx_BetaDot { + border-radius: 50%; + margin: 10px; + height: $dot-size; + width: $dot-size; + transform: scale(1); + background: rgba($pulse-color, 1); + box-shadow: 0 0 0 0 rgba($pulse-color, 1); + animation: mx_Beta_bluePulse 2s infinite; + animation-iteration-count: 20; +} + +@keyframes mx_Beta_bluePulse { + 0% { + transform: scale(0.95); + box-shadow: 0 0 0 0 rgba($pulse-color, 0.7); + } + + 70% { + transform: scale(1); + box-shadow: 0 0 0 10px rgba($pulse-color, 0); + } + + 100% { + transform: scale(0.95); + box-shadow: 0 0 0 0 rgba($pulse-color, 0); + } +} diff --git a/res/css/views/settings/tabs/user/_LabsUserSettingsTab.scss b/res/css/views/settings/tabs/user/_LabsUserSettingsTab.scss new file mode 100644 index 0000000000..540db48d65 --- /dev/null +++ b/res/css/views/settings/tabs/user/_LabsUserSettingsTab.scss @@ -0,0 +1,25 @@ +/* +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. +*/ + +.mx_LabsUserSettingsTab { + .mx_SettingsTab_section { + margin-top: 32px; + + .mx_SettingsFlag { + margin-right: 0; // remove right margin to align with beta cards + } + } +} diff --git a/res/css/views/spaces/_SpaceCreateMenu.scss b/res/css/views/spaces/_SpaceCreateMenu.scss index ef3fea351b..ef15206d22 100644 --- a/res/css/views/spaces/_SpaceCreateMenu.scss +++ b/res/css/views/spaces/_SpaceCreateMenu.scss @@ -29,6 +29,7 @@ $spacePanelWidth: 71px; width: 480px; box-sizing: border-box; background-color: $primary-bg-color; + position: relative; > div { > h2 { @@ -44,6 +45,13 @@ $spacePanelWidth: 71px; } } + // XXX remove this when spaces leaves Beta + .mx_BetaCard_betaPill { + position: absolute; + top: 24px; + right: 24px; + } + .mx_SpaceCreateMenuType { @mixin SpacePillButton; } diff --git a/res/img/betas/spaces.png b/res/img/betas/spaces.png new file mode 100644 index 0000000000..87394e0a5b Binary files /dev/null and b/res/img/betas/spaces.png differ diff --git a/src/components/structures/GroupFilterPanel.js b/src/components/structures/GroupFilterPanel.js index 7c050e7433..2ff91e4976 100644 --- a/src/components/structures/GroupFilterPanel.js +++ b/src/components/structures/GroupFilterPanel.js @@ -123,12 +123,19 @@ class GroupFilterPanel extends React.Component { mx_GroupFilterPanel_items_selected: itemsSelected, }); + let betaDot; + if (SettingsStore.getBetaInfo("feature_spaces") && !localStorage.getItem("mx_seenSpacesBeta")) { + betaDot =
; + } + let createButton = ( + className="mx_TagTile mx_TagTile_plus"> + { betaDot } + ); if (SettingsStore.getValue("feature_communities_v2_prototypes")) { diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index e330dc7d38..f01c1ea960 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -740,6 +740,8 @@ export default class MatrixChat extends React.PureComponent { this.showScreenAfterLogin(); break; case 'toggle_my_groups': + // persist that the user has interacted with this, use it to dismiss the beta dot + localStorage.setItem("mx_seenSpacesBeta", "1"); // We just dispatch the page change rather than have to worry about // what the logic is for each of these branches. if (this.state.page_type === PageTypes.MyGroups) { @@ -1684,6 +1686,10 @@ export default class MatrixChat extends React.PureComponent { const type = screen === "start_sso" ? "sso" : "cas"; PlatformPeg.get().startSingleSignOn(cli, type, this.getFragmentAfterLogin()); } else if (screen === 'groups') { + if (SettingsStore.getValue("feature_spaces")) { + dis.dispatch({ action: "view_home_page" }); + return; + } dis.dispatch({ action: 'view_my_groups', }); @@ -1767,6 +1773,11 @@ export default class MatrixChat extends React.PureComponent { subAction: params.action, }); } else if (screen.indexOf('group/') === 0) { + if (SettingsStore.getValue("feature_spaces")) { + dis.dispatch({ action: "view_home_page" }); + return; + } + const groupId = screen.substring(6); // TODO: Check valid group ID diff --git a/src/components/structures/MyGroups.js b/src/components/structures/MyGroups.js index 2ab11dad25..1fab6c4348 100644 --- a/src/components/structures/MyGroups.js +++ b/src/components/structures/MyGroups.js @@ -25,6 +25,7 @@ import AccessibleButton from '../views/elements/AccessibleButton'; import MatrixClientContext from "../../contexts/MatrixClientContext"; import AutoHideScrollbar from "./AutoHideScrollbar"; import {replaceableComponent} from "../../utils/replaceableComponent"; +import BetaCard from "../views/beta/BetaCard"; @replaceableComponent("structures.MyGroups") export default class MyGroups extends React.Component { @@ -139,6 +140,7 @@ export default class MyGroups extends React.Component {
*/} +
{ contentHeader } { content } diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 58a87e6641..c0f3c59457 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -1917,7 +1917,7 @@ export default class RoomView extends React.Component { ); } - if (SettingsStore.getValue("feature_spaces") && this.state.room?.isSpaceRoom()) { + if (this.state.room?.isSpaceRoom()) { return {
}; +const onBetaClick = () => { + defaultDispatcher.dispatch({ + action: Action.ViewUserSettings, + initialTabId: USER_LABS_TAB, + }); +}; + const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => { const cli = useContext(MatrixClientContext); const myMembership = useMyRoomMembership(space); const [busy, setBusy] = useState(false); + const spacesEnabled = SettingsStore.getValue("feature_spaces"); + let inviterSection; let joinButtons; - if (myMembership === "invite") { + if (myMembership === "join") { + // XXX remove this when spaces leaves Beta + joinButtons = ( + { + dis.dispatch({ + action: "leave_room", + room_id: space.roomId, + }); + }} + > + { _t("Leave") } + + ); + } else if (myMembership === "invite") { const inviteSender = space.getMember(cli.getUserId())?.events.member?.getSender(); const inviter = inviteSender && space.getMember(inviteSender); @@ -180,6 +208,7 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => setBusy(true); onJoinButtonClicked(); }} + disabled={!spacesEnabled} > { _t("Accept") } @@ -192,10 +221,11 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => setBusy(true); onJoinButtonClicked(); }} + disabled={!spacesEnabled} > { _t("Join") } - ) + ); } if (busy) { @@ -203,6 +233,7 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => } return
+ { inviterSection }

@@ -220,6 +251,20 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
{ joinButtons }
+ { !spacesEnabled &&
+ { myMembership === "join" + ? _t("To view %(spaceName)s, turn on the Spaces beta", { + spaceName: space.name, + }, { + a: sub => { sub }, + }) + : _t("To join %(spaceName)s, turn on the Spaces beta", { + spaceName: space.name, + }, { + a: sub => { sub }, + }) + } +
}

; }; @@ -513,9 +558,11 @@ const SpaceAddExistingRooms = ({ space, onFinished }) => { ; }; -const SpaceSetupPublicShare = ({ space, onFinished }) => { +const SpaceSetupPublicShare = ({ justCreatedOpts, space, onFinished }) => { return
-

{ _t("Share %(name)s", { name: space.name }) }

+

{ _t("Share %(name)s", { + name: justCreatedOpts?.createOpts?.name || space.name, + }) }

{ _t("It's just you at the moment, it will be even better with others.") }
@@ -530,11 +577,13 @@ const SpaceSetupPublicShare = ({ space, onFinished }) => {
; }; -const SpaceSetupPrivateScope = ({ space, onFinished }) => { +const SpaceSetupPrivateScope = ({ space, justCreatedOpts, onFinished }) => { return

{ _t("Who are you working with?") }

- { _t("Make sure the right people have access to %(name)s", { name: space.name }) } + { _t("Make sure the right people have access to %(name)s", { + name: justCreatedOpts?.createOpts?.name || space.name, + }) }
{ { _t("Make sure the right people have access. You can invite more later.") }
+
+ + { _t("This is an experimental feature. For now, " + + "new users receiving an invite will have to open the invite on to actually join.", {}, { + b: sub => { sub }, + link: () => + app.element.io + , + }) } +
+ { error &&
{ error }
}
{ fields } @@ -781,7 +841,7 @@ export default class SpaceRoomView extends React.PureComponent { private renderBody() { switch (this.state.phase) { case Phase.Landing: - if (this.state.myMembership === "join") { + if (this.state.myMembership === "join" && SettingsStore.getValue("feature_spaces")) { return ; } else { return { return { onFinished={() => this.setState({ phase: Phase.PublicShare })} />; case Phase.PublicShare: - return ; + return ; case Phase.PrivateScope: return { this.setState({ phase: invite ? Phase.PrivateInvite : Phase.PrivateExistingRooms }); }} diff --git a/src/components/views/beta/BetaCard.tsx b/src/components/views/beta/BetaCard.tsx new file mode 100644 index 0000000000..96b1fbabe5 --- /dev/null +++ b/src/components/views/beta/BetaCard.tsx @@ -0,0 +1,90 @@ +/* +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 from "react"; +import classNames from "classnames"; + +import {_t} from "../../../languageHandler"; +import AccessibleButton from "../elements/AccessibleButton"; +import SettingsStore from "../../../settings/SettingsStore"; +import {SettingLevel} from "../../../settings/SettingLevel"; +import TextWithTooltip from "../elements/TextWithTooltip"; + +interface IProps { + title?: string; + featureId: string; +} + +export const BetaPill = ({ onClick }: { onClick?: () => void }) => { + if (onClick) { + return +
+ { _t("Spaces is a beta feature") } +
+
+ { _t("Tap for more info") } +
+ } + onClick={onClick} + tooltipProps={{ yOffset: -10 }} + > + { _t("Beta") } +
; + } + + return + { _t("Beta") } + ; +}; + +const BetaCard = ({ title: titleOverride, featureId }: IProps) => { + const info = SettingsStore.getBetaInfo(featureId); + if (!info) return null; // Beta is invalid/disabled + + const { title, caption, disclaimer, image } = info; + const value = SettingsStore.getValue(featureId); + + return
+
+

+ { titleOverride || _t(title) } + +

+ { _t(caption) } + SettingsStore.setValue(featureId, null, SettingLevel.DEVICE, !value)} + kind="primary" + > + { value ? _t("Leave the beta") : _t("Join the beta") } + + { disclaimer &&
+ { disclaimer(value) } +
} +
+ +
; +}; + +export default BetaCard; diff --git a/src/components/views/dialogs/UserSettingsDialog.js b/src/components/views/dialogs/UserSettingsDialog.js index e7f6953589..fe29b85aea 100644 --- a/src/components/views/dialogs/UserSettingsDialog.js +++ b/src/components/views/dialogs/UserSettingsDialog.js @@ -125,7 +125,10 @@ export default class UserSettingsDialog extends React.Component { "mx_UserSettingsDialog_securityIcon", , )); - if (SdkConfig.get()['showLabsSettings']) { + // Show the Labs tab if enabled or if there are any active betas + if (SdkConfig.get()['showLabsSettings'] + || SettingsStore.getFeatureSettingNames().some(k => SettingsStore.getBetaInfo(k)) + ) { tabs.push(new Tab( USER_LABS_TAB, _td("Labs"), diff --git a/src/components/views/elements/ActionButton.js b/src/components/views/elements/ActionButton.js index 9903c631b2..5013bcec0d 100644 --- a/src/components/views/elements/ActionButton.js +++ b/src/components/views/elements/ActionButton.js @@ -32,6 +32,7 @@ export default class ActionButton extends React.Component { label: PropTypes.string.isRequired, iconPath: PropTypes.string, className: PropTypes.string, + children: PropTypes.node, }; static defaultProps = { @@ -79,7 +80,8 @@ export default class ActionButton extends React.Component { } return ( - { icon } { tooltip } + { this.props.children } ); } diff --git a/src/components/views/settings/tabs/user/LabsUserSettingsTab.js b/src/components/views/settings/tabs/user/LabsUserSettingsTab.js index f515f1862b..98148b19e0 100644 --- a/src/components/views/settings/tabs/user/LabsUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/LabsUserSettingsTab.js @@ -22,6 +22,8 @@ import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch"; import * as sdk from "../../../../../index"; import {SettingLevel} from "../../../../../settings/SettingLevel"; import {replaceableComponent} from "../../../../../utils/replaceableComponent"; +import SdkConfig from "../../../../../SdkConfig"; +import BetaCard from "../../../beta/BetaCard"; export class LabsSettingToggle extends React.Component { static propTypes = { @@ -48,14 +50,40 @@ export default class LabsUserSettingsTab extends React.Component { } render() { - const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag"); - const flags = SettingsStore.getFeatureSettingNames().map(f => ); + const features = SettingsStore.getFeatureSettingNames(); + const [labs, betas] = features.reduce((arr, f) => { + arr[SettingsStore.getBetaInfo(f) ? 1 : 0].push(f); + return arr; + }, [[], []]); + + let betaSection; + if (betas.length) { + betaSection =
+ { betas.map(f => ) } +
; + } + + let labsSection; + if (SdkConfig.get()['showLabsSettings']) { + const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag"); + const flags = labs.map(f => ); + + labsSection =
+ {flags} + + + + +
; + } + return ( -
+
{_t("Labs")}
{ - _t('Customise your experience with experimental labs features. ' + + _t('Feeling experimental? Labs are the best way to get things early, ' + + 'test out new features and help shape them before they actually launch. ' + 'Learn more.', {}, { 'a': (sub) => { return -
- {flags} - - - - -
+ { betaSection } + { labsSection }
); } diff --git a/src/components/views/spaces/SpaceCreateMenu.tsx b/src/components/views/spaces/SpaceCreateMenu.tsx index 491a9bffee..2551b63646 100644 --- a/src/components/views/spaces/SpaceCreateMenu.tsx +++ b/src/components/views/spaces/SpaceCreateMenu.tsx @@ -17,6 +17,7 @@ limitations under the License. import React, {useContext, useRef, useState} from "react"; import classNames from "classnames"; import {EventType, RoomType, RoomCreateTypeField} from "matrix-js-sdk/src/@types/event"; +import FocusLock from "react-focus-lock"; import {_t} from "../../../languageHandler"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; @@ -25,7 +26,10 @@ import createRoom, {IStateEvent, Preset} from "../../../createRoom"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {SpaceAvatar} from "./SpaceBasicSettings"; import AccessibleButton from "../elements/AccessibleButton"; -import FocusLock from "react-focus-lock"; +import {BetaPill} from "../beta/BetaCard"; +import defaultDispatcher from "../../../dispatcher/dispatcher"; +import {Action} from "../../../dispatcher/actions"; +import {USER_LABS_TAB} from "../dialogs/UserSettingsDialog"; import Field from "../elements/Field"; import withValidation from "../elements/Validation"; @@ -131,7 +135,7 @@ const SpaceCreateMenu = ({ onFinished }) => { if (visibility === null) { body =

{ _t("Create a space") }

-

{ _t("Spaces are new ways to group rooms and people. " + +

{ _t("Spaces are a new way to group rooms and people. " + "To join an existing space you'll need an invite.") }

{ managed={false} > + { + onFinished(); + defaultDispatcher.dispatch({ + action: Action.ViewUserSettings, + initialTabId: USER_LABS_TAB, + }); + }} /> { body } ; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index ba63674093..9b58b7713e 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -785,6 +785,13 @@ "%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s", "Change notification settings": "Change notification settings", "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.": "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.", + "Spaces": "Spaces", + "Spaces are a new way to group rooms and people.": "Spaces are a new way to group rooms and people.", + "%(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "%(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.", + "Beta available for web, desktop and Android. Thank you for trying the beta.": "Beta available for web, desktop and Android. Thank you for trying the beta.", + "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.", + "You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "You can leave the beta any time from settings or tapping on a beta badge, like the one above.", + "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.", "Show options to enable 'Do not disturb' mode": "Show options to enable 'Do not disturb' mode", "Send and receive voice messages (in development)": "Send and receive voice messages (in development)", "Render LaTeX maths in messages": "Render LaTeX maths in messages", @@ -998,7 +1005,7 @@ "Description": "Description", "Please enter a name for the space": "Please enter a name for the space", "Create a space": "Create a space", - "Spaces are new ways to group rooms and people. To join an existing space you'll need an invite.": "Spaces are new ways to group rooms and people. To join an existing space you'll need an invite.", + "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.", "Public": "Public", "Open space for anyone, best for communities": "Open space for anyone, best for communities", "Private": "Private", @@ -1259,7 +1266,7 @@ "Copy": "Copy", "Clear cache and reload": "Clear cache and reload", "Labs": "Labs", - "Customise your experience with experimental labs features.
Learn more.": "Customise your experience with experimental labs features. Learn more.", + "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. Learn more.": "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. Learn more.", "Ignored/Blocked": "Ignored/Blocked", "Error adding ignored user/server": "Error adding ignored user/server", "Something went wrong. Please try again or view your console for hints.": "Something went wrong. Please try again or view your console for hints.", @@ -2033,7 +2040,6 @@ "%(networkName)s rooms": "%(networkName)s rooms", "Matrix rooms": "Matrix rooms", "Filter your rooms and spaces": "Filter your rooms and spaces", - "Spaces": "Spaces", "Direct Messages": "Direct Messages", "Space selection": "Space selection", "Add existing rooms": "Add existing rooms", @@ -2466,6 +2472,11 @@ "Revoke permissions": "Revoke permissions", "Move left": "Move left", "Move right": "Move right", + "Spaces is a beta feature": "Spaces is a beta feature", + "Tap for more info": "Tap for more info", + "Beta": "Beta", + "Leave the beta": "Leave the beta", + "Join the beta": "Join the beta", "Avatar": "Avatar", "This room is public": "This room is public", "Away": "Away", @@ -2599,6 +2610,7 @@ "Error whilst fetching joined communities": "Error whilst fetching joined communities", "Create a new community": "Create a new community", "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.", + "Communities are changing to Spaces": "Communities are changing to Spaces", "You’re all caught up": "You’re all caught up", "You have no visible notifications.": "You have no visible notifications.", "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.": "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.", @@ -2672,6 +2684,8 @@ "Public space": "Public space", "Private space": "Private space", " invites you": " invites you", + "To view %(spaceName)s, turn on the Spaces beta": "To view %(spaceName)s, turn on the Spaces beta", + "To join %(spaceName)s, turn on the Spaces beta": "To join %(spaceName)s, turn on the Spaces beta", "Welcome to ": "Welcome to ", "Random": "Random", "Support": "Support", @@ -2696,6 +2710,7 @@ "Inviting...": "Inviting...", "Invite your teammates": "Invite your teammates", "Make sure the right people have access. You can invite more later.": "Make sure the right people have access. You can invite more later.", + "This is an experimental feature. For now, new users receiving an invite will have to open the invite on to actually join.": "This is an experimental feature. For now, new users receiving an invite will have to open the invite on to actually join.", "Invite by username": "Invite by username", "What are some things you want to discuss in %(spaceName)s?": "What are some things you want to discuss in %(spaceName)s?", "Let's create a room for each of them.": "Let's create a room for each of them.", diff --git a/src/settings/Settings.ts b/src/settings/Settings.tsx similarity index 94% rename from src/settings/Settings.ts rename to src/settings/Settings.tsx index 1497a2208d..47e63376bb 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.tsx @@ -16,8 +16,9 @@ limitations under the License. */ import { MatrixClient } from 'matrix-js-sdk/src/client'; +import React, { ReactNode } from "react"; -import { _td } from '../languageHandler'; +import { _t, _td } from '../languageHandler'; import { NotificationBodyEnabledController, NotificationsEnabledController, @@ -39,6 +40,7 @@ import { OrderedMultiController } from "./controllers/OrderedMultiController"; import { Layout } from "./Layout"; import ReducedMotionController from './controllers/ReducedMotionController'; import IncompatibleController from "./controllers/IncompatibleController"; +import SdkConfig from "../SdkConfig"; // These are just a bunch of helper arrays to avoid copy/pasting a bunch of times const LEVELS_ROOM_SETTINGS = [ @@ -117,6 +119,13 @@ export interface ISetting { // historical settings which we don't want existing user's values be wiped. Do // not use this for new settings. invertedSettingName?: string; + + betaInfo?: { + title: string; // _td + caption: string; // _td + disclaimer?: (enabled: boolean) => ReactNode; + image: string; // require(...) + }; } export const SETTINGS: {[setting: string]: ISetting} = { @@ -127,6 +136,33 @@ export const SETTINGS: {[setting: string]: ISetting} = { supportedLevels: LEVELS_FEATURE, default: false, controller: new ReloadOnChangeController(), + betaInfo: { + title: _td("Spaces"), + caption: _td("Spaces are a new way to group rooms and people."), + disclaimer: (enabled) => { + if (enabled) { + return <> +

{ _t("%(brand)s will reload with Spaces disabled. " + + "Communities and custom tags will be visible again.", { + brand: SdkConfig.get().brand, + }) }

+

{ _t("Beta available for web, desktop and Android. Thank you for trying the beta.") }

+ ; + } + + return <> +

{ _t("%(brand)s will reload with Spaces enabled. " + + "Communities and custom tags will be hidden.", { + brand: SdkConfig.get().brand, + }) }

+ { _t("You can leave the beta any time from settings or tapping on a beta badge, " + + "like the one above.") } +

{ _t("Beta available for web, desktop and Android. " + + "Some features may be unavailable on your homeserver.") }

+ ; + }, + image: require("../../res/img/betas/spaces.png"), + }, }, "feature_dnd": { isFeature: true, diff --git a/src/settings/SettingsStore.ts b/src/settings/SettingsStore.ts index c2675bd8f8..c32bbe731d 100644 --- a/src/settings/SettingsStore.ts +++ b/src/settings/SettingsStore.ts @@ -257,6 +257,15 @@ export default class SettingsStore { return SETTINGS[settingName].isFeature; } + public static getBetaInfo(settingName: string) { + // consider a beta disabled if the config is explicitly set to false, in which case treat as normal Labs flag + if (SettingsStore.isFeature(settingName) + && SettingsStore.getValueAt(SettingLevel.CONFIG, settingName, null, true, true) !== false + ) { + return SETTINGS[settingName]?.betaInfo; + } + } + /** * Determines if a setting is enabled. * If a setting is disabled then it should be hidden from the user. @@ -445,8 +454,8 @@ export default class SettingsStore { throw new Error("Setting '" + settingName + "' does not appear to be a setting."); } - // When features are specified in the config.json, we force them as enabled or disabled. - if (SettingsStore.isFeature(settingName)) { + // When non-beta features are specified in the config.json, we force them as enabled or disabled. + if (SettingsStore.isFeature(settingName) && !SETTINGS[settingName]?.betaInfo) { const configVal = SettingsStore.getValueAt(SettingLevel.CONFIG, settingName, roomId, true, true); if (configVal === true || configVal === false) return false; }