- { _t("You can also create a Space from a community.", {}, {
+ { _t("You can also make Spaces from communities.", {}, {
a: sub =>
- { _t("To join an existing space you'll need an invite.") }
+ { _t("To join a space you'll need an invite.") }
{ _t("If you leave, %(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"), - feedbackSubheading: _td("Your feedback will help make spaces better. " + - "The more detail you can go into, the better."), - feedbackLabel: "spaces-feedback", - }, - }, "feature_dnd": { isFeature: true, displayName: _td("Show options to enable 'Do not disturb' mode"), @@ -203,7 +164,7 @@ export const SETTINGS: {[setting: string]: ISetting} = { ), supportedLevels: LEVELS_FEATURE, default: false, - controller: new IncompatibleController("feature_spaces"), + controller: new IncompatibleController("showCommunitiesInsteadOfSpaces", false, false), }, "feature_pinning": { isFeature: true, @@ -232,7 +193,7 @@ export const SETTINGS: {[setting: string]: ISetting} = { displayName: _td("Group & filter rooms by custom tags (refresh to apply changes)"), supportedLevels: LEVELS_FEATURE, default: false, - controller: new IncompatibleController("feature_spaces"), + controller: new IncompatibleController("showCommunitiesInsteadOfSpaces", false, false), }, "feature_state_counters": { isFeature: true, @@ -780,6 +741,15 @@ export const SETTINGS: {[setting: string]: ISetting} = { description: _td("All rooms you're in will appear in Home."), supportedLevels: LEVELS_ACCOUNT_SETTINGS, default: false, + controller: new IncompatibleController("showCommunitiesInsteadOfSpaces", null), + }, + "showCommunitiesInsteadOfSpaces": { + displayName: _td("Display Communities instead of Spaces"), + description: _td("Temporarily show communities instead of Spaces for this session. " + + "Support for this will be removed in the near future. This will reload Element."), + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, + default: false, + controller: new ReloadOnChangeController(), }, [UIFeature.RoomHistorySettings]: { supportedLevels: LEVELS_UI_FEATURE, @@ -844,7 +814,7 @@ export const SETTINGS: {[setting: string]: ISetting} = { [UIFeature.Communities]: { supportedLevels: LEVELS_UI_FEATURE, default: true, - controller: new IncompatibleController("feature_spaces"), + controller: new IncompatibleController("showCommunitiesInsteadOfSpaces", false, false), }, [UIFeature.AdvancedSettings]: { supportedLevels: LEVELS_UI_FEATURE, diff --git a/src/settings/SettingsStore.ts b/src/settings/SettingsStore.ts index c5b83cbcd0..9487feff5e 100644 --- a/src/settings/SettingsStore.ts +++ b/src/settings/SettingsStore.ts @@ -467,6 +467,10 @@ export default class SettingsStore { throw new Error("Setting '" + settingName + "' does not appear to be a setting."); } + if (!SettingsStore.isEnabled(settingName)) { + return false; + } + // 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); diff --git a/src/settings/controllers/IncompatibleController.ts b/src/settings/controllers/IncompatibleController.ts index c48ce0a60b..aa064219f9 100644 --- a/src/settings/controllers/IncompatibleController.ts +++ b/src/settings/controllers/IncompatibleController.ts @@ -24,7 +24,11 @@ import SettingsStore from "../SettingsStore"; * labs flags. */ export default class IncompatibleController extends SettingController { - public constructor(private settingName: string, private forcedValue = false) { + public constructor( + private settingName: string, + private forcedValue: any = false, + private incompatibleValue: any = true, + ) { super(); } @@ -34,13 +38,17 @@ export default class IncompatibleController extends SettingController { calculatedValue: any, calculatedAtLevel: SettingLevel, ): any { - if (this.incompatibleSettingEnabled) { + if (this.incompatibleSetting) { return this.forcedValue; } return null; // no override } - public get incompatibleSettingEnabled(): boolean { - return SettingsStore.getValue(this.settingName); + public get settingDisabled(): boolean { + return this.incompatibleSetting; + } + + public get incompatibleSetting(): boolean { + return SettingsStore.getValue(this.settingName) === this.incompatibleValue; } } diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 8381d9e429..f28d279d00 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -71,7 +71,7 @@ export interface ISuggestedRoom extends IHierarchyRoom { const MAX_SUGGESTED_ROOMS = 20; // This setting causes the page to reload and can be costly if read frequently, so read it here only -const spacesEnabled = SettingsStore.getValue("feature_spaces"); +const spacesEnabled = !SettingsStore.getValue("showCommunitiesInsteadOfSpaces"); const getSpaceContextKey = (space?: Room) => `mx_space_context_${space?.roomId || "HOME_SPACE"}`; diff --git a/test/components/views/rooms/RoomList-test.js b/test/components/views/rooms/RoomList-test.js index cee0fe5a47..760aee69a8 100644 --- a/test/components/views/rooms/RoomList-test.js +++ b/test/components/views/rooms/RoomList-test.js @@ -9,23 +9,16 @@ import sdk from '../../../skinned-sdk'; import dis from '../../../../src/dispatcher/dispatcher'; import DMRoomMap from '../../../../src/utils/DMRoomMap'; -import GroupStore from '../../../../src/stores/GroupStore'; import { MatrixClient, Room, RoomMember } from 'matrix-js-sdk'; import { DefaultTagID } from "../../../../src/stores/room-list/models"; -import RoomListStore, { LISTS_UPDATE_EVENT, RoomListStoreClass } from "../../../../src/stores/room-list/RoomListStore"; +import RoomListStore, { RoomListStoreClass } from "../../../../src/stores/room-list/RoomListStore"; import RoomListLayoutStore from "../../../../src/stores/room-list/RoomListLayoutStore"; function generateRoomId() { return '!' + Math.random().toString().slice(2, 10) + ':domain'; } -function waitForRoomListStoreUpdate() { - return new Promise((resolve) => { - RoomListStore.instance.once(LISTS_UPDATE_EVENT, () => resolve()); - }); -} - describe('RoomList', () => { function createRoom(opts) { const room = new Room(generateRoomId(), MatrixClientPeg.get(), client.getUserId(), { @@ -239,73 +232,6 @@ describe('RoomList', () => { }); } - describe('when no tags are selected', () => { - itDoesCorrectOptimisticUpdatesForDraggedRoomTiles(); - }); - - describe('when tags are selected', () => { - function setupSelectedTag() { - // Simulate a complete sync BEFORE dispatching anything else - dis.dispatch({ - action: 'MatrixActions.sync', - prevState: null, - state: 'PREPARED', - matrixClient: client, - }, true); - - // Simulate joined groups being received - dis.dispatch({ - action: 'GroupActions.fetchJoinedGroups.success', - result: { - groups: ['+group:domain'], - }, - }, true); - - // Simulate receiving tag ordering account data - dis.dispatch({ - action: 'MatrixActions.accountData', - event_type: 'im.vector.web.tag_ordering', - event_content: { - tags: ['+group:domain'], - }, - }, true); - - // GroupStore is not flux, mock and notify - GroupStore.getGroupRooms = (groupId) => { - return [movingRoom]; - }; - GroupStore._notifyListeners(); - - // We also have to mock the client's getGroup function for the room list to filter it. - // It's not smart enough to tell the difference between a real group and a template though. - client.getGroup = (groupId) => { - return { groupId }; - }; - - // Select tag - dis.dispatch({ action: 'select_tag', tag: '+group:domain' }, true); - } - - beforeEach(() => { - setupSelectedTag(); - }); - - it('displays the correct rooms when the groups rooms are changed', async () => { - GroupStore.getGroupRooms = (groupId) => { - return [movingRoom, otherRoom]; - }; - GroupStore._notifyListeners(); - - await waitForRoomListStoreUpdate(); - - // XXX: Even though the store updated, it can take a bit before the update makes - // it to the components. This gives it plenty of time to figure out what to do. - await (new Promise(resolve => setTimeout(resolve, 500))); - - expectRoomInSubList(otherRoom, (s) => s.props.tagId === DefaultTagID.Untagged); - }); - - itDoesCorrectOptimisticUpdatesForDraggedRoomTiles(); - }); + itDoesCorrectOptimisticUpdatesForDraggedRoomTiles(); }); diff --git a/test/end-to-end-tests/src/scenario.js b/test/end-to-end-tests/src/scenario.js index c44f209bf3..b6581d4930 100644 --- a/test/end-to-end-tests/src/scenario.js +++ b/test/end-to-end-tests/src/scenario.js @@ -43,6 +43,9 @@ module.exports = async function scenario(createSession, restCreator) { console.log("create REST users:"); const charlies = await createRestUsers(restCreator); await lazyLoadingScenarios(alice, bob, charlies); + // do spaces scenarios last as the rest of the tests may get confused by spaces + // XXX: disabled for now as fails in CI but succeeds locally + // await spacesScenarios(alice, bob); }; async function createRestUsers(restCreator) { diff --git a/test/end-to-end-tests/src/scenarios/spaces.js b/test/end-to-end-tests/src/scenarios/spaces.js new file mode 100644 index 0000000000..303db65593 --- /dev/null +++ b/test/end-to-end-tests/src/scenarios/spaces.js @@ -0,0 +1,32 @@ +/* +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. +*/ + +const { createSpace, inviteSpace } = require("../usecases/create-space"); + +module.exports = async function spacesScenarios(alice, bob) { + console.log(" creating a space for spaces scenarios:"); + + await alice.delay(1000); // wait for dialogs to close + await setupSpaceUsingAliceAndInviteBob(alice, bob); +}; + +const space = "Test Space"; + +async function setupSpaceUsingAliceAndInviteBob(alice, bob) { + await createSpace(alice, space); + await inviteSpace(alice, space, "@bob:localhost"); + await bob.query(`.mx_SpaceButton[aria-label="${space}"]`); // assert invite received +} diff --git a/test/end-to-end-tests/src/usecases/create-space.js b/test/end-to-end-tests/src/usecases/create-space.js new file mode 100644 index 0000000000..f1dbfa6b5a --- /dev/null +++ b/test/end-to-end-tests/src/usecases/create-space.js @@ -0,0 +1,80 @@ +/* +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. +*/ + +async function openSpaceCreateMenu(session) { + const spaceCreateButton = await session.query('.mx_SpaceButton_new'); + await spaceCreateButton.click(); +} + +async function createSpace(session, name, isPublic = false) { + session.log.step(`creates space "${name}"`); + + await openSpaceCreateMenu(session); + const className = isPublic ? ".mx_SpaceCreateMenuType_public" : ".mx_SpaceCreateMenuType_private"; + const visibilityButton = await session.query(className); + await visibilityButton.click(); + + const nameInput = await session.query('input[name="spaceName"]'); + await session.replaceInputText(nameInput, name); + + await session.delay(100); + + const createButton = await session.query('.mx_SpaceCreateMenu_wrapper .mx_AccessibleButton_kind_primary'); + await createButton.click(); + + if (!isPublic) { + const justMeButton = await session.query('.mx_SpaceRoomView_privateScope_justMeButton'); + await justMeButton.click(); + const continueButton = await session.query('.mx_AddExistingToSpace_footer .mx_AccessibleButton_kind_primary'); + await continueButton.click(); + } else { + for (let i = 0; i < 2; i++) { + const continueButton = await session.query('.mx_SpaceRoomView_buttons .mx_AccessibleButton_kind_primary'); + await continueButton.click(); + } + } + + session.log.done(); +} + +async function inviteSpace(session, spaceName, userId) { + session.log.step(`invites "${userId}" to space "${spaceName}"`); + + const spaceButton = await session.query(`.mx_SpaceButton[aria-label="${spaceName}"]`); + await spaceButton.click({ + button: 'right', + }); + + const inviteButton = await session.query('[aria-label="Invite people"]'); + await inviteButton.click(); + + try { + const button = await session.query('.mx_SpacePublicShare_inviteButton'); + await button.click(); + } catch (e) { + // ignore + } + + const inviteTextArea = await session.query(".mx_InviteDialog_editor input"); + await inviteTextArea.type(userId); + const selectUserItem = await session.query(".mx_InviteDialog_roomTile"); + await selectUserItem.click(); + const confirmButton = await session.query(".mx_InviteDialog_goButton"); + await confirmButton.click(); + session.log.done(); +} + +module.exports = { openSpaceCreateMenu, createSpace, inviteSpace }; diff --git a/test/end-to-end-tests/src/usecases/room-settings.js b/test/end-to-end-tests/src/usecases/room-settings.js index 01431197a7..83d6fd79a8 100644 --- a/test/end-to-end-tests/src/usecases/room-settings.js +++ b/test/end-to-end-tests/src/usecases/room-settings.js @@ -161,8 +161,8 @@ async function changeRoomSettings(session, settings) { if (settings.visibility) { session.log.step(`sets visibility to ${settings.visibility}`); const radios = await session.queryAll(".mx_RoomSettingsDialog label"); - assert.equal(radios.length, 6); - const [inviteOnlyRoom, publicRoom] = radios; + assert.equal(radios.length, 7); + const [inviteOnlyRoom,, publicRoom] = radios; if (settings.visibility === "invite_only") { await inviteOnlyRoom.click(); diff --git a/test/stores/SpaceStore-setup.ts b/test/stores/SpaceStore-setup.ts deleted file mode 100644 index 78418d45cc..0000000000 --- a/test/stores/SpaceStore-setup.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* -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. -*/ - -// This needs to be executed before the SpaceStore gets imported but due to ES6 import hoisting we have to do this here. -// SpaceStore reads the SettingsStore which needs the localStorage values set at init time. - -localStorage.setItem("mx_labs_feature_feature_spaces", "true"); diff --git a/test/stores/SpaceStore-test.ts b/test/stores/SpaceStore-test.ts index 698bd01370..e7ca727e28 100644 --- a/test/stores/SpaceStore-test.ts +++ b/test/stores/SpaceStore-test.ts @@ -18,7 +18,6 @@ import { EventEmitter } from "events"; import { EventType } from "matrix-js-sdk/src/@types/event"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; -import "./SpaceStore-setup"; // enable space lab import "../skinned-sdk"; // Must be first for skinning to work import SpaceStore, { UPDATE_HOME_BEHAVIOUR, diff --git a/test/stores/room-list/SpaceWatcher-test.ts b/test/stores/room-list/SpaceWatcher-test.ts index 474c279fdd..cb2394349a 100644 --- a/test/stores/room-list/SpaceWatcher-test.ts +++ b/test/stores/room-list/SpaceWatcher-test.ts @@ -14,7 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import "../SpaceStore-setup"; // enable space lab import "../../skinned-sdk"; // Must be first for skinning to work import { SpaceWatcher } from "../../../src/stores/room-list/SpaceWatcher"; import type { RoomListStoreClass } from "../../../src/stores/room-list/RoomListStore";