From 12461a79e1ddaa45355b231bc18accd867cfa7a2 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 27 Jul 2021 17:19:45 +0100 Subject: [PATCH 01/11] Move SettingsStore `setting_updated` dispatch to action enum --- src/dispatcher/actions.ts | 7 +++++ .../payloads/SettingUpdatedPayload.ts | 29 +++++++++++++++++++ src/settings/SettingsStore.ts | 10 ++++--- src/stores/BreadcrumbsStore.ts | 9 ++++-- src/stores/room-list/RoomListStore.ts | 9 ++++-- 5 files changed, 54 insertions(+), 10 deletions(-) create mode 100644 src/dispatcher/payloads/SettingUpdatedPayload.ts diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts index 57324282016..043c69df36e 100644 --- a/src/dispatcher/actions.ts +++ b/src/dispatcher/actions.ts @@ -193,4 +193,11 @@ export enum Action { * Switches space. Should be used with SwitchSpacePayload. */ SwitchSpace = "switch_space", + + /** + * Fires when a monitored setting is updated, + * see SettingsStore::monitorSetting for more details. + * Should be used with SettingUpdatedPayload. + */ + SettingUpdated = "setting_updated", } diff --git a/src/dispatcher/payloads/SettingUpdatedPayload.ts b/src/dispatcher/payloads/SettingUpdatedPayload.ts new file mode 100644 index 00000000000..8d457facfbb --- /dev/null +++ b/src/dispatcher/payloads/SettingUpdatedPayload.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 { ActionPayload } from "../payloads"; +import { Action } from "../actions"; +import { SettingLevel } from "../../settings/SettingLevel"; + +export interface SettingUpdatedPayload extends ActionPayload { + action: Action.SettingUpdated; + + settingName: string; + roomId: string; + level: SettingLevel; + newValueAtLevel: SettingLevel; + newValue: any; +} diff --git a/src/settings/SettingsStore.ts b/src/settings/SettingsStore.ts index 44f3d5d838b..c5b83cbcd00 100644 --- a/src/settings/SettingsStore.ts +++ b/src/settings/SettingsStore.ts @@ -29,6 +29,8 @@ import LocalEchoWrapper from "./handlers/LocalEchoWrapper"; import { WatchManager, CallbackFn as WatchCallbackFn } from "./WatchManager"; import { SettingLevel } from "./SettingLevel"; import SettingsHandler from "./handlers/SettingsHandler"; +import { SettingUpdatedPayload } from "../dispatcher/payloads/SettingUpdatedPayload"; +import { Action } from "../dispatcher/actions"; const defaultWatchManager = new WatchManager(); @@ -147,7 +149,7 @@ export default class SettingsStore { * if the change in value is worthwhile enough to react upon. * @returns {string} A reference to the watcher that was employed. */ - public static watchSetting(settingName: string, roomId: string, callbackFn: CallbackFn): string { + public static watchSetting(settingName: string, roomId: string | null, callbackFn: CallbackFn): string { const setting = SETTINGS[settingName]; const originalSettingName = settingName; if (!setting) throw new Error(`${settingName} is not a setting`); @@ -193,7 +195,7 @@ export default class SettingsStore { * @param {string} settingName The setting name to monitor. * @param {String} roomId The room ID to monitor for changes in. Use null for all rooms. */ - public static monitorSetting(settingName: string, roomId: string) { + public static monitorSetting(settingName: string, roomId: string | null) { roomId = roomId || null; // the thing wants null specifically to work, so appease it. if (!this.monitors.has(settingName)) this.monitors.set(settingName, new Map()); @@ -201,8 +203,8 @@ export default class SettingsStore { const registerWatcher = () => { this.monitors.get(settingName).set(roomId, SettingsStore.watchSetting( settingName, roomId, (settingName, inRoomId, level, newValueAtLevel, newValue) => { - dis.dispatch({ - action: 'setting_updated', + dis.dispatch({ + action: Action.SettingUpdated, settingName, roomId: inRoomId, level, diff --git a/src/stores/BreadcrumbsStore.ts b/src/stores/BreadcrumbsStore.ts index aceaf8b8988..8a85ca354fd 100644 --- a/src/stores/BreadcrumbsStore.ts +++ b/src/stores/BreadcrumbsStore.ts @@ -23,6 +23,8 @@ import { arrayHasDiff } from "../utils/arrays"; import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; import { SettingLevel } from "../settings/SettingLevel"; import SpaceStore from "./SpaceStore"; +import { Action } from "../dispatcher/actions"; +import { SettingUpdatedPayload } from "../dispatcher/payloads/SettingUpdatedPayload"; const MAX_ROOMS = 20; // arbitrary const AUTOJOIN_WAIT_THRESHOLD_MS = 90000; // 90s, the time we wait for an autojoined room to show up @@ -63,10 +65,11 @@ export class BreadcrumbsStore extends AsyncStoreWithClient { protected async onAction(payload: ActionPayload) { if (!this.matrixClient) return; - if (payload.action === 'setting_updated') { - if (payload.settingName === 'breadcrumb_rooms') { + if (payload.action === Action.SettingUpdated) { + const settingUpdatedPayload = payload as SettingUpdatedPayload; + if (settingUpdatedPayload.settingName === 'breadcrumb_rooms') { await this.updateRooms(); - } else if (payload.settingName === 'breadcrumbs') { + } else if (settingUpdatedPayload.settingName === 'breadcrumbs') { await this.updateState({ enabled: SettingsStore.getValue("breadcrumbs", null) }); } } else if (payload.action === 'view_room') { diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index 3913a2220f2..b7af70ad998 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -36,6 +36,8 @@ import { RoomNotificationStateStore } from "../notifications/RoomNotificationSta import { VisibilityProvider } from "./filters/VisibilityProvider"; import { SpaceWatcher } from "./SpaceWatcher"; import SpaceStore from "../SpaceStore"; +import { Action } from "../../dispatcher/actions"; +import { SettingUpdatedPayload } from "../../dispatcher/payloads/SettingUpdatedPayload"; interface IState { tagsEnabled?: boolean; @@ -213,10 +215,11 @@ export class RoomListStoreClass extends AsyncStoreWithClient { const logicallyReady = this.matrixClient && this.initialListsGenerated; if (!logicallyReady) return; - if (payload.action === 'setting_updated') { - if (this.watchedSettings.includes(payload.settingName)) { + if (payload.action === Action.SettingUpdated) { + const settingUpdatedPayload = payload as SettingUpdatedPayload; + if (this.watchedSettings.includes(settingUpdatedPayload.settingName)) { // TODO: Remove with /~https://github.com/vector-im/element-web/issues/14602 - if (payload.settingName === "advancedRoomListLogging") { + if (settingUpdatedPayload.settingName === "advancedRoomListLogging") { // Log when the setting changes so we know when it was turned on in the rageshake const enabled = SettingsStore.getValue("advancedRoomListLogging"); console.warn("Advanced room list logging is enabled? " + enabled); From 8c073a643904bc70d71a7b864aa2a149cceefff7 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 27 Jul 2021 17:53:03 +0100 Subject: [PATCH 02/11] RoomListStore removeFilter skip triggering update if nothing changed --- src/stores/room-list/RoomListStore.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index b7af70ad998..1a5ef0484e7 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -711,6 +711,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient { } let promise = Promise.resolve(); let idx = this.filterConditions.indexOf(filter); + let removed = false; if (idx >= 0) { this.filterConditions.splice(idx, 1); @@ -721,14 +722,20 @@ export class RoomListStoreClass extends AsyncStoreWithClient { if (SpaceStore.spacesEnabled) { promise = this.recalculatePrefiltering(); } + removed = true; } + idx = this.prefilterConditions.indexOf(filter); if (idx >= 0) { filter.off(FILTER_CHANGED, this.onPrefilterUpdated); this.prefilterConditions.splice(idx, 1); promise = this.recalculatePrefiltering(); + removed = true; + } + + if (removed) { + promise.then(() => this.updateFn.trigger()); } - promise.then(() => this.updateFn.trigger()); } /** From ec173e74e60246d2e4bf096d6345c1eb41c569a1 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 27 Jul 2021 20:15:40 +0100 Subject: [PATCH 03/11] Test & Refactor SpaceWatcher to allow all rooms/home change without needing reload --- src/stores/room-list/SpaceWatcher.ts | 36 ++-- test/stores/SpaceStore-test.ts | 68 ++------ test/stores/room-list/SpaceWatcher-test.ts | 186 +++++++++++++++++++++ test/utils/test-utils.ts | 51 ++++++ 4 files changed, 273 insertions(+), 68 deletions(-) create mode 100644 test/stores/room-list/SpaceWatcher-test.ts diff --git a/src/stores/room-list/SpaceWatcher.ts b/src/stores/room-list/SpaceWatcher.ts index 1cec612e6f2..fe2eb1e881b 100644 --- a/src/stores/room-list/SpaceWatcher.ts +++ b/src/stores/room-list/SpaceWatcher.ts @@ -18,41 +18,49 @@ import { Room } from "matrix-js-sdk/src/models/room"; import { RoomListStoreClass } from "./RoomListStore"; import { SpaceFilterCondition } from "./filters/SpaceFilterCondition"; -import SpaceStore, { UPDATE_SELECTED_SPACE } from "../SpaceStore"; +import SpaceStore, { UPDATE_HOME_BEHAVIOUR, UPDATE_SELECTED_SPACE } from "../SpaceStore"; /** * Watches for changes in spaces to manage the filter on the provided RoomListStore */ export class SpaceWatcher { - private filter: SpaceFilterCondition; + private readonly filter = new SpaceFilterCondition(); + // we track these separately to the SpaceStore as we need to observe transitions private activeSpace: Room = SpaceStore.instance.activeSpace; + private allRoomsInHome: boolean = SpaceStore.instance.allRoomsInHome; constructor(private store: RoomListStoreClass) { - if (!SpaceStore.spacesTweakAllRoomsEnabled) { - this.filter = new SpaceFilterCondition(); + if (!this.allRoomsInHome || this.activeSpace) { this.updateFilter(); store.addFilter(this.filter); } SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdated); + SpaceStore.instance.on(UPDATE_HOME_BEHAVIOUR, this.onHomeBehaviourUpdated); } - private onSelectedSpaceUpdated = (activeSpace?: Room) => { + private onSelectedSpaceUpdated = (activeSpace?: Room, allRoomsInHome = this.allRoomsInHome) => { + if (activeSpace === this.activeSpace && allRoomsInHome === this.allRoomsInHome) return; // nop + + const oldActiveSpace = this.activeSpace; + const oldAllRoomsInHome = this.allRoomsInHome; this.activeSpace = activeSpace; + this.allRoomsInHome = allRoomsInHome; - if (this.filter) { - if (activeSpace || !SpaceStore.spacesTweakAllRoomsEnabled) { - this.updateFilter(); - } else { - this.store.removeFilter(this.filter); - this.filter = null; - } - } else if (activeSpace) { - this.filter = new SpaceFilterCondition(); + if (activeSpace || !allRoomsInHome) { this.updateFilter(); + } + + if (oldAllRoomsInHome && !oldActiveSpace) { this.store.addFilter(this.filter); + } else if (allRoomsInHome && !activeSpace) { + this.store.removeFilter(this.filter); } }; + private onHomeBehaviourUpdated = (allRoomsInHome: boolean) => { + this.onSelectedSpaceUpdated(this.activeSpace, allRoomsInHome); + }; + private updateFilter = () => { if (this.activeSpace) { SpaceStore.instance.traverseSpace(this.activeSpace.roomId, roomId => { diff --git a/test/stores/SpaceStore-test.ts b/test/stores/SpaceStore-test.ts index d772a7a6588..8b809be95d1 100644 --- a/test/stores/SpaceStore-test.ts +++ b/test/stores/SpaceStore-test.ts @@ -16,7 +16,6 @@ limitations under the License. import { EventEmitter } from "events"; import { EventType } from "matrix-js-sdk/src/@types/event"; -import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import "./SpaceStore-setup"; // enable space lab @@ -26,31 +25,14 @@ import SpaceStore, { UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES, } from "../../src/stores/SpaceStore"; -import { resetAsyncStoreWithClient, setupAsyncStoreWithClient } from "../utils/test-utils"; -import { mkEvent, mkStubRoom, stubClient } from "../test-utils"; -import { EnhancedMap } from "../../src/utils/maps"; +import * as testUtils from "../utils/test-utils"; +import { mkEvent, stubClient } from "../test-utils"; import DMRoomMap from "../../src/utils/DMRoomMap"; import { MatrixClientPeg } from "../../src/MatrixClientPeg"; import defaultDispatcher from "../../src/dispatcher/dispatcher"; jest.useFakeTimers(); -const mockStateEventImplementation = (events: MatrixEvent[]) => { - const stateMap = new EnhancedMap>(); - events.forEach(event => { - stateMap.getOrCreate(event.getType(), new Map()).set(event.getStateKey(), event); - }); - - return (eventType: string, stateKey?: string) => { - if (stateKey || stateKey === "") { - return stateMap.get(eventType)?.get(stateKey) || null; - } - return Array.from(stateMap.get(eventType)?.values() || []); - }; -}; - -const emitPromise = (e: EventEmitter, k: string | symbol) => new Promise(r => e.once(k, r)); - const testUserId = "@test:user"; const getUserIdForRoomId = jest.fn(); @@ -87,36 +69,13 @@ describe("SpaceStore", () => { const client = MatrixClientPeg.get(); let rooms = []; - - const mkRoom = (roomId: string) => { - const room = mkStubRoom(roomId, roomId, client); - room.currentState.getStateEvents.mockImplementation(mockStateEventImplementation([])); - rooms.push(room); - return room; - }; - - const mkSpace = (spaceId: string, children: string[] = []) => { - const space = mkRoom(spaceId); - space.isSpaceRoom.mockReturnValue(true); - space.currentState.getStateEvents.mockImplementation(mockStateEventImplementation(children.map(roomId => - mkEvent({ - event: true, - type: EventType.SpaceChild, - room: spaceId, - user: testUserId, - skey: roomId, - content: { via: [] }, - ts: Date.now(), - }), - ))); - return space; - }; - + const mkRoom = (roomId: string) => testUtils.mkRoom(client, roomId, rooms); + const mkSpace = (spaceId: string, children: string[] = []) => testUtils.mkSpace(client, spaceId, rooms, children); const viewRoom = roomId => defaultDispatcher.dispatch({ action: "view_room", room_id: roomId }, true); const run = async () => { client.getRoom.mockImplementation(roomId => rooms.find(room => room.roomId === roomId)); - await setupAsyncStoreWithClient(store, client); + await testUtils.setupAsyncStoreWithClient(store, client); jest.runAllTimers(); }; @@ -125,7 +84,7 @@ describe("SpaceStore", () => { client.getVisibleRooms.mockReturnValue(rooms = []); }); afterEach(async () => { - await resetAsyncStoreWithClient(store); + await testUtils.resetAsyncStoreWithClient(store); }); describe("static hierarchy resolution tests", () => { @@ -488,7 +447,7 @@ describe("SpaceStore", () => { await run(); expect(store.spacePanelSpaces).toStrictEqual([]); const space = mkSpace(space1); - const prom = emitPromise(store, UPDATE_TOP_LEVEL_SPACES); + const prom = testUtils.emitPromise(store, UPDATE_TOP_LEVEL_SPACES); emitter.emit("Room", space); await prom; expect(store.spacePanelSpaces).toStrictEqual([space]); @@ -501,7 +460,7 @@ describe("SpaceStore", () => { expect(store.spacePanelSpaces).toStrictEqual([space]); space.getMyMembership.mockReturnValue("leave"); - const prom = emitPromise(store, UPDATE_TOP_LEVEL_SPACES); + const prom = testUtils.emitPromise(store, UPDATE_TOP_LEVEL_SPACES); emitter.emit("Room.myMembership", space, "leave", "join"); await prom; expect(store.spacePanelSpaces).toStrictEqual([]); @@ -513,7 +472,7 @@ describe("SpaceStore", () => { expect(store.invitedSpaces).toStrictEqual([]); const space = mkSpace(space1); space.getMyMembership.mockReturnValue("invite"); - const prom = emitPromise(store, UPDATE_INVITED_SPACES); + const prom = testUtils.emitPromise(store, UPDATE_INVITED_SPACES); emitter.emit("Room", space); await prom; expect(store.spacePanelSpaces).toStrictEqual([]); @@ -528,7 +487,7 @@ describe("SpaceStore", () => { expect(store.spacePanelSpaces).toStrictEqual([]); expect(store.invitedSpaces).toStrictEqual([space]); space.getMyMembership.mockReturnValue("join"); - const prom = emitPromise(store, UPDATE_TOP_LEVEL_SPACES); + const prom = testUtils.emitPromise(store, UPDATE_TOP_LEVEL_SPACES); emitter.emit("Room.myMembership", space, "join", "invite"); await prom; expect(store.spacePanelSpaces).toStrictEqual([space]); @@ -543,7 +502,7 @@ describe("SpaceStore", () => { expect(store.spacePanelSpaces).toStrictEqual([]); expect(store.invitedSpaces).toStrictEqual([space]); space.getMyMembership.mockReturnValue("leave"); - const prom = emitPromise(store, UPDATE_INVITED_SPACES); + const prom = testUtils.emitPromise(store, UPDATE_INVITED_SPACES); emitter.emit("Room.myMembership", space, "leave", "invite"); await prom; expect(store.spacePanelSpaces).toStrictEqual([]); @@ -563,7 +522,7 @@ describe("SpaceStore", () => { const invite = mkRoom(invite1); invite.getMyMembership.mockReturnValue("invite"); - const prom = emitPromise(store, space1); + const prom = testUtils.emitPromise(store, space1); emitter.emit("Room", space); await prom; @@ -704,7 +663,8 @@ describe("SpaceStore", () => { mkSpace(space1, [room1, room2, room3]); mkSpace(space2, [room1, room2]); - client.getRoom(room2).currentState.getStateEvents.mockImplementation(mockStateEventImplementation([ + const cliRoom2 = client.getRoom(room2); + cliRoom2.currentState.getStateEvents.mockImplementation(testUtils.mockStateEventImplementation([ mkEvent({ event: true, type: EventType.SpaceParent, diff --git a/test/stores/room-list/SpaceWatcher-test.ts b/test/stores/room-list/SpaceWatcher-test.ts new file mode 100644 index 00000000000..c27088b6439 --- /dev/null +++ b/test/stores/room-list/SpaceWatcher-test.ts @@ -0,0 +1,186 @@ +/* +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 "../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"; +import SettingsStore from "../../../src/settings/SettingsStore"; +import SpaceStore, { UPDATE_HOME_BEHAVIOUR } from "../../../src/stores/SpaceStore"; +import { stubClient } from "../../test-utils"; +import { SettingLevel } from "../../../src/settings/SettingLevel"; +import { setupAsyncStoreWithClient } from "../../utils/test-utils"; +import { MatrixClientPeg } from "../../../src/MatrixClientPeg"; +import * as testUtils from "../../utils/test-utils"; +import { SpaceFilterCondition } from "../../../src/stores/room-list/filters/SpaceFilterCondition"; + +let filter: SpaceFilterCondition = null; + +const mockRoomListStore = { + addFilter: f => filter = f, + removeFilter: () => filter = null, +} as unknown as RoomListStoreClass; + +const space1Id = "!space1:server"; +const space2Id = "!space2:server"; + +describe("SpaceWatcher", () => { + stubClient(); + const store = SpaceStore.instance; + const client = MatrixClientPeg.get(); + + let rooms = []; + const mkSpace = (spaceId: string, children: string[] = []) => testUtils.mkSpace(client, spaceId, rooms, children); + + const setShowAllRooms = async (value: boolean) => { + if (store.allRoomsInHome === value) return; + await SettingsStore.setValue("feature_spaces.all_rooms", null, SettingLevel.DEVICE, value); + await testUtils.emitPromise(store, UPDATE_HOME_BEHAVIOUR); + }; + + let space1; + let space2; + + beforeEach(async () => { + filter = null; + store.removeAllListeners(); + await store.setActiveSpace(null); + client.getVisibleRooms.mockReturnValue(rooms = []); + + space1 = mkSpace(space1Id); + space2 = mkSpace(space2Id); + + client.getRoom.mockImplementation(roomId => rooms.find(room => room.roomId === roomId)); + await setupAsyncStoreWithClient(store, client); + }); + + it("initialises sanely with home behaviour", async () => { + await setShowAllRooms(false); + new SpaceWatcher(mockRoomListStore); + + expect(filter).toBeInstanceOf(SpaceFilterCondition); + }); + + it("initialises sanely with all behaviour", async () => { + await setShowAllRooms(true); + new SpaceWatcher(mockRoomListStore); + + expect(filter).toBeNull(); + }); + + it("sets space=null filter for all -> home transition", async () => { + await setShowAllRooms(true); + new SpaceWatcher(mockRoomListStore); + + await setShowAllRooms(false); + + expect(filter).toBeInstanceOf(SpaceFilterCondition); + expect(filter["space"]).toBeNull(); + }); + + it("sets filter correctly for all -> space transition", async () => { + await setShowAllRooms(true); + new SpaceWatcher(mockRoomListStore); + + await SpaceStore.instance.setActiveSpace(space1); + + expect(filter).toBeInstanceOf(SpaceFilterCondition); + expect(filter["space"]).toBe(space1); + }); + + it("removes filter for home -> all transition", async () => { + await setShowAllRooms(false); + new SpaceWatcher(mockRoomListStore); + + await setShowAllRooms(true); + + expect(filter).toBeNull(); + }); + + it("sets filter correctly for home -> space transition", async () => { + await setShowAllRooms(false); + new SpaceWatcher(mockRoomListStore); + + await SpaceStore.instance.setActiveSpace(space1); + + expect(filter).toBeInstanceOf(SpaceFilterCondition); + expect(filter["space"]).toBe(space1); + }); + + it("removes filter for space -> all transition", async () => { + await setShowAllRooms(true); + new SpaceWatcher(mockRoomListStore); + + await SpaceStore.instance.setActiveSpace(space1); + expect(filter).toBeInstanceOf(SpaceFilterCondition); + expect(filter["space"]).toBe(space1); + await SpaceStore.instance.setActiveSpace(null); + + expect(filter).toBeNull(); + }); + + it("updates filter correctly for space -> home transition", async () => { + await setShowAllRooms(false); + await SpaceStore.instance.setActiveSpace(space1); + + new SpaceWatcher(mockRoomListStore); + expect(filter).toBeInstanceOf(SpaceFilterCondition); + expect(filter["space"]).toBe(space1); + await SpaceStore.instance.setActiveSpace(null); + + expect(filter).toBeInstanceOf(SpaceFilterCondition); + expect(filter["space"]).toBe(null); + }); + + it("updates filter correctly for space -> space transition", async () => { + await setShowAllRooms(false); + await SpaceStore.instance.setActiveSpace(space1); + + new SpaceWatcher(mockRoomListStore); + expect(filter).toBeInstanceOf(SpaceFilterCondition); + expect(filter["space"]).toBe(space1); + await SpaceStore.instance.setActiveSpace(space2); + + expect(filter).toBeInstanceOf(SpaceFilterCondition); + expect(filter["space"]).toBe(space2); + }); + + it("doesn't change filter when changing showAllRooms mode to true", async () => { + await setShowAllRooms(false); + await SpaceStore.instance.setActiveSpace(space1); + + new SpaceWatcher(mockRoomListStore); + expect(filter).toBeInstanceOf(SpaceFilterCondition); + expect(filter["space"]).toBe(space1); + await setShowAllRooms(true); + + expect(filter).toBeInstanceOf(SpaceFilterCondition); + expect(filter["space"]).toBe(space1); + }); + + it("doesn't change filter when changing showAllRooms mode to false", async () => { + await setShowAllRooms(true); + await SpaceStore.instance.setActiveSpace(space1); + + new SpaceWatcher(mockRoomListStore); + expect(filter).toBeInstanceOf(SpaceFilterCondition); + expect(filter["space"]).toBe(space1); + await setShowAllRooms(false); + + expect(filter).toBeInstanceOf(SpaceFilterCondition); + expect(filter["space"]).toBe(space1); + }); +}); diff --git a/test/utils/test-utils.ts b/test/utils/test-utils.ts index af92987a3d0..8bc602fe35e 100644 --- a/test/utils/test-utils.ts +++ b/test/utils/test-utils.ts @@ -15,7 +15,13 @@ limitations under the License. */ import { MatrixClient } from "matrix-js-sdk/src/client"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { EventType } from "matrix-js-sdk/src/@types/event"; + import { AsyncStoreWithClient } from "../../src/stores/AsyncStoreWithClient"; +import { mkEvent, mkStubRoom } from "../test-utils"; +import { EnhancedMap } from "../../src/utils/maps"; +import { EventEmitter } from "events"; // These methods make some use of some private methods on the AsyncStoreWithClient to simplify getting into a consistent // ready state without needing to wire up a dispatcher and pretend to be a js-sdk client. @@ -31,3 +37,48 @@ export const resetAsyncStoreWithClient = async (store: AsyncStoreWithClient // @ts-ignore await store.onNotReady(); }; + +export const mockStateEventImplementation = (events: MatrixEvent[]) => { + const stateMap = new EnhancedMap>(); + events.forEach(event => { + stateMap.getOrCreate(event.getType(), new Map()).set(event.getStateKey(), event); + }); + + return (eventType: string, stateKey?: string) => { + if (stateKey || stateKey === "") { + return stateMap.get(eventType)?.get(stateKey) || null; + } + return Array.from(stateMap.get(eventType)?.values() || []); + }; +}; + +export const mkRoom = (client: MatrixClient, roomId: string, rooms?: ReturnType[]) => { + const room = mkStubRoom(roomId, roomId, client); + room.currentState.getStateEvents.mockImplementation(mockStateEventImplementation([])); + rooms?.push(room); + return room; +}; + +export const mkSpace = ( + client: MatrixClient, + spaceId: string, + rooms?: ReturnType[], + children: string[] = [], +) => { + const space = mkRoom(client, spaceId, rooms); + space.isSpaceRoom.mockReturnValue(true); + space.currentState.getStateEvents.mockImplementation(mockStateEventImplementation(children.map(roomId => + mkEvent({ + event: true, + type: EventType.SpaceChild, + room: spaceId, + user: "@user:server", + skey: roomId, + content: { via: [] }, + ts: Date.now(), + }), + ))); + return space; +}; + +export const emitPromise = (e: EventEmitter, k: string | symbol) => new Promise(r => e.once(k, r)); From 0a9d3302baf96ed0c3b2d8497fcd44a65475c895 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 27 Jul 2021 21:11:47 +0100 Subject: [PATCH 04/11] Fix home vs all rooms requiring app reload and change default to `home` Consolidate ALL_ROOMS and HOME_SPACE storage Fix behaviour when recalled room is no longer part of the target space Improve tests --- src/components/views/spaces/SpacePanel.tsx | 13 +++- src/settings/Settings.tsx | 3 +- src/stores/SpaceStore.tsx | 75 ++++++++++++++-------- test/stores/SpaceStore-test.ts | 48 ++++++++++++-- 4 files changed, 100 insertions(+), 39 deletions(-) diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx index 1c4043f1504..8223d84dbb3 100644 --- a/src/components/views/spaces/SpacePanel.tsx +++ b/src/components/views/spaces/SpacePanel.tsx @@ -137,15 +137,22 @@ const InnerSpacePanel = React.memo(({ children, isPanelCo const [invites, spaces, activeSpace] = useSpaces(); const activeSpaces = activeSpace ? [activeSpace] : []; - const homeNotificationState = SpaceStore.spacesTweakAllRoomsEnabled - ? RoomNotificationStateStore.instance.globalState : SpaceStore.instance.getNotificationState(HOME_SPACE); + let homeTooltip: string; + let homeNotificationState: NotificationState; + if (SpaceStore.instance.allRoomsInHome) { + homeTooltip = _t("All rooms"); + homeNotificationState = RoomNotificationStateStore.instance.globalState; + } else { + homeTooltip = _t("Home"); + homeNotificationState = SpaceStore.instance.getNotificationState(HOME_SPACE); + } return
SpaceStore.instance.setActiveSpace(null)} selected={!activeSpace} - tooltip={SpaceStore.spacesTweakAllRoomsEnabled ? _t("All rooms") : _t("Home")} + tooltip={homeTooltip} notificationState={homeNotificationState} isNarrow={isPanelCollapsed} /> diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 5aa49df8a13..54153b3d754 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -187,8 +187,7 @@ export const SETTINGS: {[setting: string]: ISetting} = { "feature_spaces.all_rooms": { displayName: _td("Show all rooms in Home"), supportedLevels: LEVELS_FEATURE, - default: true, - controller: new ReloadOnChangeController(), + default: false, }, "feature_dnd": { isFeature: true, diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index d064b01257a..42ecc25651b 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -37,9 +37,8 @@ import { EnhancedMap, mapDiff } from "../utils/maps"; import { setHasDiff } from "../utils/sets"; import RoomViewStore from "./RoomViewStore"; import { Action } from "../dispatcher/actions"; -import { arrayHasDiff } from "../utils/arrays"; +import { arrayHasDiff, arrayHasOrderChange } from "../utils/arrays"; import { objectDiff } from "../utils/objects"; -import { arrayHasOrderChange } from "../utils/arrays"; import { reorderLexicographically } from "../utils/stringOrderField"; import { TAG_ORDER } from "../components/views/rooms/RoomList"; import { shouldShowSpaceSettings } from "../utils/space"; @@ -48,6 +47,7 @@ import { _t } from "../languageHandler"; import GenericToast from "../components/views/toasts/GenericToast"; import Modal from "../Modal"; import InfoDialog from "../components/views/dialogs/InfoDialog"; +import { SettingUpdatedPayload } from "../dispatcher/payloads/SettingUpdatedPayload"; type SpaceKey = string | symbol; @@ -61,6 +61,7 @@ export const SUGGESTED_ROOMS = Symbol("suggested-rooms"); export const UPDATE_TOP_LEVEL_SPACES = Symbol("top-level-spaces"); export const UPDATE_INVITED_SPACES = Symbol("invited-spaces"); export const UPDATE_SELECTED_SPACE = Symbol("selected-space"); +export const UPDATE_HOME_BEHAVIOUR = Symbol("home-behaviour"); // Space Room ID/HOME_SPACE will be emitted when a Space's children change export interface ISuggestedRoom extends ISpaceSummaryRoom { @@ -69,12 +70,10 @@ export interface ISuggestedRoom extends ISpaceSummaryRoom { const MAX_SUGGESTED_ROOMS = 20; -// All of these settings cause the page to reload and can be costly if read frequently, so read them here only +// 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 spacesTweakAllRoomsEnabled = SettingsStore.getValue("feature_spaces.all_rooms"); -const homeSpaceKey = spacesTweakAllRoomsEnabled ? "ALL_ROOMS" : "HOME_SPACE"; -const getSpaceContextKey = (space?: Room) => `mx_space_context_${space?.roomId || homeSpaceKey}`; +const getSpaceContextKey = (space?: Room) => `mx_space_context_${space?.roomId || "HOME_SPACE"}`; const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces, rooms] return arr.reduce((result, room: Room) => { @@ -102,10 +101,6 @@ const getRoomFn: FetchRoomFn = (room: Room) => { }; export class SpaceStoreClass extends AsyncStoreWithClient { - constructor() { - super(defaultDispatcher, {}); - } - // The spaces representing the roots of the various tree-like hierarchies private rootSpaces: Room[] = []; // The list of rooms not present in any currently joined spaces @@ -122,6 +117,13 @@ export class SpaceStoreClass extends AsyncStoreWithClient { private _invitedSpaces = new Set(); private spaceOrderLocalEchoMap = new Map(); private _restrictedJoinRuleSupport?: IRoomCapability; + private _allRoomsInHome: boolean = SettingsStore.getValue("feature_spaces.all_rooms"); + + constructor() { + super(defaultDispatcher, {}); + + SettingsStore.monitorSetting("feature_spaces.all_rooms", null); + } public get invitedSpaces(): Room[] { return Array.from(this._invitedSpaces); @@ -139,13 +141,16 @@ export class SpaceStoreClass extends AsyncStoreWithClient { return this._suggestedRooms; } + public get allRoomsInHome(): boolean { + return this._allRoomsInHome; + } + public async setActiveRoomInSpace(space: Room | null): Promise { if (space && !space.isSpaceRoom()) return; if (space !== this.activeSpace) await this.setActiveSpace(space); if (space) { - const notificationState = this.getNotificationState(space.roomId); - const roomId = notificationState.getFirstRoomWithNotifications(); + const roomId = this.getNotificationState(space.roomId).getFirstRoomWithNotifications(); defaultDispatcher.dispatch({ action: "view_room", room_id: roomId, @@ -200,7 +205,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // else if the last viewed room in this space is joined then view that // else view space home or home depending on what is being clicked on if (space?.getMyMembership() !== "invite" && - this.matrixClient?.getRoom(roomId)?.getMyMembership() === "join" + this.matrixClient?.getRoom(roomId)?.getMyMembership() === "join" && + this.getSpaceFilteredRoomIds(space).has(roomId) ) { defaultDispatcher.dispatch({ action: "view_room", @@ -377,7 +383,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } public getSpaceFilteredRoomIds = (space: Room | null): Set => { - if (!space && spacesTweakAllRoomsEnabled) { + if (!space && this.allRoomsInHome) { return new Set(this.matrixClient.getVisibleRooms().map(r => r.roomId)); } return this.spaceFilteredRooms.get(space?.roomId || HOME_SPACE) || new Set(); @@ -474,7 +480,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { }; private showInHomeSpace = (room: Room) => { - if (spacesTweakAllRoomsEnabled) return true; + if (this.allRoomsInHome) return true; if (room.isSpaceRoom()) return false; return !this.parentMap.get(room.roomId)?.size // put all orphaned rooms in the Home Space || DMRoomMap.shared().getUserIdForRoomId(room.roomId) // put all DMs in the Home Space @@ -506,7 +512,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { const oldFilteredRooms = this.spaceFilteredRooms; this.spaceFilteredRooms = new Map(); - if (!spacesTweakAllRoomsEnabled) { + if (!this.allRoomsInHome) { // put all room invites in the Home Space const invites = visibleRooms.filter(r => !r.isSpaceRoom() && r.getMyMembership() === "invite"); this.spaceFilteredRooms.set(HOME_SPACE, new Set(invites.map(room => room.roomId))); @@ -562,8 +568,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient { }); this.spaceFilteredRooms.forEach((roomIds, s) => { + if (this.allRoomsInHome && s === HOME_SPACE) return; // we'll be using the global notification state, skip + // Update NotificationStates - this.getNotificationState(s)?.setRooms(visibleRooms.filter(room => { + this.getNotificationState(s).setRooms(visibleRooms.filter(room => { if (!roomIds.has(room.roomId)) return false; if (DMRoomMap.shared().getUserIdForRoomId(room.roomId)) { @@ -663,7 +671,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // TODO confirm this after implementing parenting behaviour if (room.isSpaceRoom()) { this.onSpaceUpdate(); - } else if (!spacesTweakAllRoomsEnabled) { + } else if (!this.allRoomsInHome) { this.onRoomUpdate(room); } this.emit(room.roomId); @@ -687,7 +695,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { if (order !== lastOrder) { this.notifyIfOrderChanged(); } - } else if (ev.getType() === EventType.Tag && !spacesTweakAllRoomsEnabled) { + } else if (ev.getType() === EventType.Tag && !this.allRoomsInHome) { // If the room was in favourites and now isn't or the opposite then update its position in the trees const oldTags = lastEv?.getContent()?.tags || {}; const newTags = ev.getContent()?.tags || {}; @@ -698,7 +706,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { }; private onAccountData = (ev: MatrixEvent, lastEvent: MatrixEvent) => { - if (ev.getType() === EventType.Direct) { + if (!this.allRoomsInHome && ev.getType() === EventType.Direct) { const lastContent = lastEvent.getContent(); const content = ev.getContent(); @@ -733,9 +741,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.matrixClient.removeListener("Room.myMembership", this.onRoom); this.matrixClient.removeListener("Room.accountData", this.onRoomAccountData); this.matrixClient.removeListener("RoomState.events", this.onRoomState); - if (!spacesTweakAllRoomsEnabled) { - this.matrixClient.removeListener("accountData", this.onAccountData); - } + this.matrixClient.removeListener("accountData", this.onAccountData); } await this.reset(); } @@ -746,9 +752,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.matrixClient.on("Room.myMembership", this.onRoom); this.matrixClient.on("Room.accountData", this.onRoomAccountData); this.matrixClient.on("RoomState.events", this.onRoomState); - if (!spacesTweakAllRoomsEnabled) { - this.matrixClient.on("accountData", this.onAccountData); - } + this.matrixClient.on("accountData", this.onAccountData); this.matrixClient.getCapabilities().then(capabilities => { this._restrictedJoinRuleSupport = capabilities @@ -779,7 +783,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // as it will cause you to end up in the wrong room this.setActiveSpace(room, false); } else if ( - (!spacesTweakAllRoomsEnabled || this.activeSpace) && + (!this.allRoomsInHome || this.activeSpace) && !this.getSpaceFilteredRoomIds(this.activeSpace).has(roomId) ) { this.switchToRelatedSpace(roomId); @@ -791,17 +795,33 @@ export class SpaceStoreClass extends AsyncStoreWithClient { window.localStorage.setItem(getSpaceContextKey(this.activeSpace), payload.room_id); break; } + case "after_leave_room": if (this._activeSpace && payload.room_id === this._activeSpace.roomId) { this.setActiveSpace(null, false); } break; + case Action.SwitchSpace: if (payload.num === 0) { this.setActiveSpace(null); } else if (this.spacePanelSpaces.length >= payload.num) { this.setActiveSpace(this.spacePanelSpaces[payload.num - 1]); } + break; + + case Action.SettingUpdated: { + const settingUpdatedPayload = payload as SettingUpdatedPayload; + if (settingUpdatedPayload.settingName === "feature_spaces.all_rooms") { + const newValue = SettingsStore.getValue("feature_spaces.all_rooms"); + if (this.allRoomsInHome !== newValue) { + this._allRoomsInHome = newValue; + this.emit(UPDATE_HOME_BEHAVIOUR, this.allRoomsInHome); + this.rebuild(); // rebuild everything + } + } + break; + } } } @@ -872,7 +892,6 @@ export class SpaceStoreClass extends AsyncStoreWithClient { export default class SpaceStore { public static spacesEnabled = spacesEnabled; - public static spacesTweakAllRoomsEnabled = spacesTweakAllRoomsEnabled; private static internalInstance = new SpaceStoreClass(); diff --git a/test/stores/SpaceStore-test.ts b/test/stores/SpaceStore-test.ts index 8b809be95d1..09005e3d842 100644 --- a/test/stores/SpaceStore-test.ts +++ b/test/stores/SpaceStore-test.ts @@ -21,6 +21,7 @@ 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, UPDATE_INVITED_SPACES, UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES, @@ -30,6 +31,8 @@ import { mkEvent, stubClient } from "../test-utils"; import DMRoomMap from "../../src/utils/DMRoomMap"; import { MatrixClientPeg } from "../../src/MatrixClientPeg"; import defaultDispatcher from "../../src/dispatcher/dispatcher"; +import SettingsStore from "../../src/settings/SettingsStore"; +import { SettingLevel } from "../../src/settings/SettingLevel"; jest.useFakeTimers(); @@ -79,8 +82,16 @@ describe("SpaceStore", () => { jest.runAllTimers(); }; + const setShowAllRooms = async (value: boolean) => { + if (store.allRoomsInHome === value) return; + const emitProm = testUtils.emitPromise(store, UPDATE_HOME_BEHAVIOUR); + await SettingsStore.setValue("feature_spaces.all_rooms", null, SettingLevel.DEVICE, value); + jest.runAllTimers(); // run async dispatch + await emitProm; + }; + beforeEach(() => { - jest.runAllTimers(); + jest.runAllTimers(); // run async dispatch client.getVisibleRooms.mockReturnValue(rooms = []); }); afterEach(async () => { @@ -346,10 +357,16 @@ describe("SpaceStore", () => { expect(store.getSpaceFilteredRoomIds(null).has(invite2)).toBeTruthy(); }); - it("home space does contain rooms/low priority even if they are also shown in a space", () => { + it("all rooms space does contain rooms/low priority even if they are also shown in a space", async () => { + await setShowAllRooms(true); expect(store.getSpaceFilteredRoomIds(null).has(room1)).toBeTruthy(); }); + it("home space doesn't contain rooms/low priority if they are also shown in a space", async () => { + await setShowAllRooms(false); + expect(store.getSpaceFilteredRoomIds(null).has(room1)).toBeFalsy(); + }); + it("space contains child rooms", () => { const space = client.getRoom(space1); expect(store.getSpaceFilteredRoomIds(space).has(fav1)).toBeTruthy(); @@ -592,20 +609,30 @@ describe("SpaceStore", () => { }); describe("context switching tests", () => { - const fn = jest.spyOn(defaultDispatcher, "dispatch"); + let dispatcherRef; + let currentRoom = null; beforeEach(async () => { [room1, room2, orphan1].forEach(mkRoom); mkSpace(space1, [room1, room2]); mkSpace(space2, [room2]); await run(); + + dispatcherRef = defaultDispatcher.register(payload => { + if (payload.action === "view_room" || payload.action === "view_home_page") { + currentRoom = payload.room_id || null; + } + }); }); afterEach(() => { - fn.mockClear(); localStorage.clear(); + defaultDispatcher.unregister(dispatcherRef); }); - const getCurrentRoom = () => fn.mock.calls.reverse().find(([p]) => p.action === "view_room")?.[0].room_id; + const getCurrentRoom = () => { + jest.runAllTimers(); + return currentRoom; + }; it("last viewed room in target space is the current viewed and in both spaces", async () => { await store.setActiveSpace(client.getRoom(space1)); @@ -642,6 +669,14 @@ describe("SpaceStore", () => { expect(getCurrentRoom()).toBe(space2); }); + it("last viewed room is target space is no longer in that space", async () => { + await store.setActiveSpace(client.getRoom(space1)); + viewRoom(room1); + localStorage.setItem(`mx_space_context_${space2}`, room1); + await store.setActiveSpace(client.getRoom(space2)); + expect(getCurrentRoom()).toBe(space2); // Space home instead of room1 + }); + it("no last viewed room in target space", async () => { await store.setActiveSpace(client.getRoom(space1)); viewRoom(room1); @@ -653,7 +688,7 @@ describe("SpaceStore", () => { await store.setActiveSpace(client.getRoom(space1)); viewRoom(room1); await store.setActiveSpace(null); - expect(fn.mock.calls[fn.mock.calls.length - 1][0]).toStrictEqual({ action: "view_home_page" }); + expect(getCurrentRoom()).toBeNull(); // Home }); }); @@ -707,6 +742,7 @@ describe("SpaceStore", () => { }); it("when switching rooms in the all rooms home space don't switch to related space", async () => { + await setShowAllRooms(true); viewRoom(room2); await store.setActiveSpace(null, false); viewRoom(room1); From 776435f6208e48d9845cbdcbaafc2b2d8bdf1d64 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 27 Jul 2021 21:17:24 +0100 Subject: [PATCH 05/11] Switch all-rooms toggle for spaces to non-feature settings key --- src/i18n/strings/en_EN.json | 2 +- src/settings/Settings.tsx | 12 ++++++------ src/stores/SpaceStore.tsx | 8 ++++---- test/stores/SpaceStore-setup.ts | 1 - test/stores/SpaceStore-test.ts | 2 +- test/stores/room-list/SpaceWatcher-test.ts | 2 +- 6 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index de432d61778..2cd2a096ad0 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -797,7 +797,6 @@ "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.", "Your feedback will help make spaces better. The more detail you can go into, the better.": "Your feedback will help make spaces better. The more detail you can go into, the better.", - "Show all rooms in Home": "Show all rooms in Home", "Show options to enable 'Do not disturb' mode": "Show options to enable 'Do not disturb' mode", "Send and receive voice messages": "Send and receive voice messages", "Render LaTeX maths in messages": "Render LaTeX maths in messages", @@ -868,6 +867,7 @@ "Manually verify all remote sessions": "Manually verify all remote sessions", "IRC display name width": "IRC display name width", "Show chat effects (animations when receiving e.g. confetti)": "Show chat effects (animations when receiving e.g. confetti)", + "Show all rooms in Home": "Show all rooms in Home", "Collecting app version information": "Collecting app version information", "Collecting logs": "Collecting logs", "Uploading logs": "Uploading logs", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 54153b3d754..dfd6f1eec95 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -180,15 +180,10 @@ export const SETTINGS: {[setting: string]: ISetting} = { "The more detail you can go into, the better."), feedbackLabel: "spaces-feedback", extraSettings: [ - "feature_spaces.all_rooms", + "Spaces.all_rooms_in_home", ], }, }, - "feature_spaces.all_rooms": { - displayName: _td("Show all rooms in Home"), - supportedLevels: LEVELS_FEATURE, - default: false, - }, "feature_dnd": { isFeature: true, displayName: _td("Show options to enable 'Do not disturb' mode"), @@ -756,6 +751,11 @@ export const SETTINGS: {[setting: string]: ISetting} = { supportedLevels: LEVELS_ACCOUNT_SETTINGS, default: null, }, + "Spaces.all_rooms_in_home": { + displayName: _td("Show all rooms in Home"), + supportedLevels: LEVELS_FEATURE, + default: false, + }, [UIFeature.RoomHistorySettings]: { supportedLevels: LEVELS_UI_FEATURE, default: true, diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 42ecc25651b..3fc4a1bc6d5 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -117,12 +117,12 @@ export class SpaceStoreClass extends AsyncStoreWithClient { private _invitedSpaces = new Set(); private spaceOrderLocalEchoMap = new Map(); private _restrictedJoinRuleSupport?: IRoomCapability; - private _allRoomsInHome: boolean = SettingsStore.getValue("feature_spaces.all_rooms"); + private _allRoomsInHome: boolean = SettingsStore.getValue("Spaces.all_rooms_in_home"); constructor() { super(defaultDispatcher, {}); - SettingsStore.monitorSetting("feature_spaces.all_rooms", null); + SettingsStore.monitorSetting("Spaces.all_rooms_in_home", null); } public get invitedSpaces(): Room[] { @@ -812,8 +812,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient { case Action.SettingUpdated: { const settingUpdatedPayload = payload as SettingUpdatedPayload; - if (settingUpdatedPayload.settingName === "feature_spaces.all_rooms") { - const newValue = SettingsStore.getValue("feature_spaces.all_rooms"); + if (settingUpdatedPayload.settingName === "Spaces.all_rooms_in_home") { + const newValue = SettingsStore.getValue("Spaces.all_rooms_in_home"); if (this.allRoomsInHome !== newValue) { this._allRoomsInHome = newValue; this.emit(UPDATE_HOME_BEHAVIOUR, this.allRoomsInHome); diff --git a/test/stores/SpaceStore-setup.ts b/test/stores/SpaceStore-setup.ts index b9b865e89a3..78418d45cc2 100644 --- a/test/stores/SpaceStore-setup.ts +++ b/test/stores/SpaceStore-setup.ts @@ -18,4 +18,3 @@ limitations under the License. // SpaceStore reads the SettingsStore which needs the localStorage values set at init time. localStorage.setItem("mx_labs_feature_feature_spaces", "true"); -localStorage.setItem("mx_labs_feature_feature_spaces.all_rooms", "true"); diff --git a/test/stores/SpaceStore-test.ts b/test/stores/SpaceStore-test.ts index 09005e3d842..eb3d5f0b97a 100644 --- a/test/stores/SpaceStore-test.ts +++ b/test/stores/SpaceStore-test.ts @@ -85,7 +85,7 @@ describe("SpaceStore", () => { const setShowAllRooms = async (value: boolean) => { if (store.allRoomsInHome === value) return; const emitProm = testUtils.emitPromise(store, UPDATE_HOME_BEHAVIOUR); - await SettingsStore.setValue("feature_spaces.all_rooms", null, SettingLevel.DEVICE, value); + await SettingsStore.setValue("Spaces.all_rooms_in_home", null, SettingLevel.DEVICE, value); jest.runAllTimers(); // run async dispatch await emitProm; }; diff --git a/test/stores/room-list/SpaceWatcher-test.ts b/test/stores/room-list/SpaceWatcher-test.ts index c27088b6439..c6254349b5d 100644 --- a/test/stores/room-list/SpaceWatcher-test.ts +++ b/test/stores/room-list/SpaceWatcher-test.ts @@ -47,7 +47,7 @@ describe("SpaceWatcher", () => { const setShowAllRooms = async (value: boolean) => { if (store.allRoomsInHome === value) return; - await SettingsStore.setValue("feature_spaces.all_rooms", null, SettingLevel.DEVICE, value); + await SettingsStore.setValue("Spaces.all_rooms_in_home", null, SettingLevel.DEVICE, value); await testUtils.emitPromise(store, UPDATE_HOME_BEHAVIOUR); }; From 07b9d6b30b7468c6030cf5be71e89ec1854d9494 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 28 Jul 2021 14:39:19 +0100 Subject: [PATCH 06/11] Fix styling of setting flag descriptions in preferences and add description to spaces all/home setting and make it an account setting rather than device one and hide it from the Beta card --- res/css/views/settings/tabs/_SettingsTab.scss | 7 +++++++ .../settings/tabs/user/PreferencesUserSettingsTab.tsx | 10 ++++++++++ src/i18n/strings/en_EN.json | 1 + src/settings/Settings.tsx | 6 ++---- 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/res/css/views/settings/tabs/_SettingsTab.scss b/res/css/views/settings/tabs/_SettingsTab.scss index 0d679af4e51..804a06186d0 100644 --- a/res/css/views/settings/tabs/_SettingsTab.scss +++ b/res/css/views/settings/tabs/_SettingsTab.scss @@ -73,6 +73,13 @@ limitations under the License. padding-right: 10px; } +.mx_SettingsTab_section .mx_SettingsFlag .mx_SettingsFlag_microcopy { + margin-top: 4px; + font-size: $font-12px; + line-height: $font-15px; + color: $secondary-fg-color; +} + .mx_SettingsTab_section .mx_SettingsFlag .mx_ToggleSwitch { float: right; } diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx index 2e5db59d9b3..53d8d41f690 100644 --- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx @@ -26,6 +26,7 @@ import { replaceableComponent } from "../../../../../utils/replaceableComponent" import SettingsFlag from '../../../elements/SettingsFlag'; import * as KeyboardShortcuts from "../../../../../accessibility/KeyboardShortcuts"; import AccessibleButton from "../../../elements/AccessibleButton"; +import SpaceStore from "../../../../../stores/SpaceStore"; interface IState { autoLaunch: boolean; @@ -47,6 +48,10 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta 'breadcrumbs', ]; + static SPACES_SETTINGS = [ + "Spaces.all_rooms_in_home", + ]; + static KEYBINDINGS_SETTINGS = [ 'ctrlFForSearch', ]; @@ -231,6 +236,11 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta { this.renderGroup(PreferencesUserSettingsTab.ROOM_LIST_SETTINGS) }
+ { SpaceStore.spacesEnabled &&
+ { _t("Spaces") } + { this.renderGroup(PreferencesUserSettingsTab.SPACES_SETTINGS) } +
} +
{ _t("Keyboard shortcuts") } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 2cd2a096ad0..4a728b72b63 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -868,6 +868,7 @@ "IRC display name width": "IRC display name width", "Show chat effects (animations when receiving e.g. confetti)": "Show chat effects (animations when receiving e.g. confetti)", "Show all rooms in Home": "Show all rooms in Home", + "All rooms you're in will appear in Home.": "All rooms you're in will appear in Home.", "Collecting app version information": "Collecting app version information", "Collecting logs": "Collecting logs", "Uploading logs": "Uploading logs", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index dfd6f1eec95..290f5de7895 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -179,9 +179,6 @@ export const SETTINGS: {[setting: string]: ISetting} = { feedbackSubheading: _td("Your feedback will help make spaces better. " + "The more detail you can go into, the better."), feedbackLabel: "spaces-feedback", - extraSettings: [ - "Spaces.all_rooms_in_home", - ], }, }, "feature_dnd": { @@ -753,7 +750,8 @@ export const SETTINGS: {[setting: string]: ISetting} = { }, "Spaces.all_rooms_in_home": { displayName: _td("Show all rooms in Home"), - supportedLevels: LEVELS_FEATURE, + description: _td("All rooms you're in will appear in Home."), + supportedLevels: LEVELS_ACCOUNT_SETTINGS, default: false, }, [UIFeature.RoomHistorySettings]: { From cdf0d98c3fca269a0be32fb6caaf12846dc6bd70 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 28 Jul 2021 17:39:02 +0100 Subject: [PATCH 07/11] Fix IconizedContextMenuCheckbox layout --- .../views/context_menus/_IconizedContextMenu.scss | 13 +++++++++---- .../views/context_menus/IconizedContextMenu.tsx | 5 ++++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/res/css/views/context_menus/_IconizedContextMenu.scss b/res/css/views/context_menus/_IconizedContextMenu.scss index 204435995f1..f83699b505b 100644 --- a/res/css/views/context_menus/_IconizedContextMenu.scss +++ b/res/css/views/context_menus/_IconizedContextMenu.scss @@ -145,12 +145,17 @@ limitations under the License. } } - .mx_IconizedContextMenu_checked { + .mx_IconizedContextMenu_checked, + .mx_IconizedContextMenu_unchecked { margin-left: 16px; margin-right: -5px; + } - &::before { - mask-image: url('$(res)/img/element-icons/roomlist/checkmark.svg'); - } + .mx_IconizedContextMenu_checked::before { + mask-image: url('$(res)/img/element-icons/roomlist/checkmark.svg'); + } + + .mx_IconizedContextMenu_unchecked::before { + content: unset; } } diff --git a/src/components/views/context_menus/IconizedContextMenu.tsx b/src/components/views/context_menus/IconizedContextMenu.tsx index 1d822fd246c..7ad07f04662 100644 --- a/src/components/views/context_menus/IconizedContextMenu.tsx +++ b/src/components/views/context_menus/IconizedContextMenu.tsx @@ -86,7 +86,10 @@ export const IconizedContextMenuCheckbox: React.FC = ({ > { label } - { active && } + ; }; From b3a28bde8966e3b07106445de3d89312c5186f6d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 28 Jul 2021 17:39:56 +0100 Subject: [PATCH 08/11] Factor out useEventEmitterState hook --- src/components/views/spaces/SpacePanel.tsx | 15 +++++++++------ src/hooks/useEventEmitter.ts | 13 ++++++++++++- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx index 8223d84dbb3..3bb8d8e3d26 100644 --- a/src/components/views/spaces/SpacePanel.tsx +++ b/src/components/views/spaces/SpacePanel.tsx @@ -117,12 +117,15 @@ const SpaceButton: React.FC = ({ }; const useSpaces = (): [Room[], Room[], Room | null] => { - const [invites, setInvites] = useState(SpaceStore.instance.invitedSpaces); - useEventEmitter(SpaceStore.instance, UPDATE_INVITED_SPACES, setInvites); - const [spaces, setSpaces] = useState(SpaceStore.instance.spacePanelSpaces); - useEventEmitter(SpaceStore.instance, UPDATE_TOP_LEVEL_SPACES, setSpaces); - const [activeSpace, setActiveSpace] = useState(SpaceStore.instance.activeSpace); - useEventEmitter(SpaceStore.instance, UPDATE_SELECTED_SPACE, setActiveSpace); + const invites = useEventEmitterState(SpaceStore.instance, UPDATE_INVITED_SPACES, () => { + return SpaceStore.instance.invitedSpaces; + }); + const spaces = useEventEmitterState(SpaceStore.instance, UPDATE_TOP_LEVEL_SPACES, () => { + return SpaceStore.instance.spacePanelSpaces; + }); + const activeSpace = useEventEmitterState(SpaceStore.instance, UPDATE_SELECTED_SPACE, () => { + return SpaceStore.instance.activeSpace; + }); return [invites, spaces, activeSpace]; }; diff --git a/src/hooks/useEventEmitter.ts b/src/hooks/useEventEmitter.ts index a81bba56995..74b23f0198f 100644 --- a/src/hooks/useEventEmitter.ts +++ b/src/hooks/useEventEmitter.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { useRef, useEffect } from "react"; +import { useRef, useEffect, useState, useCallback } from "react"; import type { EventEmitter } from "events"; type Handler = (...args: any[]) => void; @@ -48,3 +48,14 @@ export const useEventEmitter = (emitter: EventEmitter, eventName: string | symbo [eventName, emitter], // Re-run if eventName or emitter changes ); }; + +type Mapper = (...args: any[]) => T; + +export const useEventEmitterState = (emitter: EventEmitter, eventName: string | symbol, fn: Mapper): T => { + const [value, setValue] = useState(fn()); + const handler = useCallback((...args: any[]) => { + setValue(fn(...args)); + }, [fn]); + useEventEmitter(emitter, eventName, handler); + return value; +}; From 67ef263940b990959d435deec37b60415d9596e4 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 28 Jul 2021 17:40:33 +0100 Subject: [PATCH 09/11] Refactor SpaceButton to be more reusable and add context menu to Home button --- res/css/structures/_SpacePanel.scss | 2 +- src/components/structures/ContextMenu.tsx | 10 +- .../views/context_menus/SpaceContextMenu.tsx | 201 ++++++++++ src/components/views/spaces/SpacePanel.tsx | 240 ++++++------ .../views/spaces/SpaceTreeLevel.tsx | 366 ++++++------------ src/i18n/strings/en_EN.json | 21 +- 6 files changed, 443 insertions(+), 397 deletions(-) create mode 100644 src/components/views/context_menus/SpaceContextMenu.tsx diff --git a/res/css/structures/_SpacePanel.scss b/res/css/structures/_SpacePanel.scss index e64057d16c9..9d9c3ff8ab7 100644 --- a/res/css/structures/_SpacePanel.scss +++ b/res/css/structures/_SpacePanel.scss @@ -297,7 +297,7 @@ $activeBorderColor: $secondary-fg-color; .mx_SpaceButton:hover, .mx_SpaceButton:focus-within, .mx_SpaceButton_hasMenuOpen { - &:not(.mx_SpaceButton_home):not(.mx_SpaceButton_invite) { + &:not(.mx_SpaceButton_invite) { // Hide the badge container on hover because it'll be a menu button .mx_SpacePanel_badgeContainer { width: 0; diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx index 407dc6f04c3..0822d3768b5 100644 --- a/src/components/structures/ContextMenu.tsx +++ b/src/components/structures/ContextMenu.tsx @@ -16,7 +16,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { CSSProperties, RefObject, useRef, useState } from "react"; +import React, { CSSProperties, RefObject, SyntheticEvent, useRef, useState } from "react"; import ReactDOM from "react-dom"; import classNames from "classnames"; @@ -461,10 +461,14 @@ type ContextMenuTuple = [boolean, RefObject, () => void, () => void, (val: export const useContextMenu = (): ContextMenuTuple => { const button = useRef(null); const [isOpen, setIsOpen] = useState(false); - const open = () => { + const open = (ev?: SyntheticEvent) => { + ev?.preventDefault(); + ev?.stopPropagation(); setIsOpen(true); }; - const close = () => { + const close = (ev?: SyntheticEvent) => { + ev?.preventDefault(); + ev?.stopPropagation(); setIsOpen(false); }; diff --git a/src/components/views/context_menus/SpaceContextMenu.tsx b/src/components/views/context_menus/SpaceContextMenu.tsx new file mode 100644 index 00000000000..1555870f265 --- /dev/null +++ b/src/components/views/context_menus/SpaceContextMenu.tsx @@ -0,0 +1,201 @@ +/* +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 } from "react"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { EventType } from "matrix-js-sdk/src/@types/event"; + +import { + IProps as IContextMenuProps, +} from "../../structures/ContextMenu"; +import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from "./IconizedContextMenu"; +import { _t } from "../../../languageHandler"; +import { + shouldShowSpaceSettings, + showAddExistingRooms, + showCreateNewRoom, + showSpaceInvite, + showSpaceSettings, +} from "../../../utils/space"; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import { ButtonEvent } from "../elements/AccessibleButton"; +import defaultDispatcher from "../../../dispatcher/dispatcher"; +import RoomViewStore from "../../../stores/RoomViewStore"; +import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload"; +import { Action } from "../../../dispatcher/actions"; +import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; + +interface IProps extends IContextMenuProps { + space: Room; +} + +const SpaceContextMenu = ({ space, onFinished, ...props }: IProps) => { + const cli = useContext(MatrixClientContext); + const userId = cli.getUserId(); + + let inviteOption; + if (space.getJoinRule() === "public" || space.canInvite(userId)) { + const onInviteClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + showSpaceInvite(space); + onFinished(); + }; + + inviteOption = ( + + ); + } + + let settingsOption; + let leaveSection; + if (shouldShowSpaceSettings(space)) { + const onSettingsClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + showSpaceSettings(space); + onFinished(); + }; + + settingsOption = ( + + ); + } else { + const onLeaveClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + defaultDispatcher.dispatch({ + action: "leave_room", + room_id: space.roomId, + }); + onFinished(); + }; + + leaveSection = + + ; + } + + const canAddRooms = space.currentState.maySendStateEvent(EventType.SpaceChild, userId); + + let newRoomSection; + if (space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) { + const onNewRoomClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + showCreateNewRoom(space); + onFinished(); + }; + + const onAddExistingRoomClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + showAddExistingRooms(space); + onFinished(); + }; + + newRoomSection = + + + ; + } + + const onMembersClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + if (!RoomViewStore.getRoomId()) { + defaultDispatcher.dispatch({ + action: "view_room", + room_id: space.roomId, + }, true); + } + + defaultDispatcher.dispatch({ + action: Action.SetRightPanelPhase, + phase: RightPanelPhases.SpaceMemberList, + refireParams: { space: space }, + }); + onFinished(); + }; + + const onExploreRoomsClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + defaultDispatcher.dispatch({ + action: "view_room", + room_id: space.roomId, + }); + onFinished(); + }; + + return +
+ { space.name } +
+ + { inviteOption } + + { settingsOption } + + + { newRoomSection } + { leaveSection } +
; +}; + +export default SpaceContextMenu; + diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx index 3bb8d8e3d26..a339cb81322 100644 --- a/src/components/views/spaces/SpacePanel.tsx +++ b/src/components/views/spaces/SpacePanel.tsx @@ -14,107 +14,35 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { Dispatch, ReactNode, SetStateAction, useEffect, useState } from "react"; -import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd"; +import React, { ComponentProps, Dispatch, ReactNode, SetStateAction, useEffect, useState } from "react"; +import { DragDropContext, Draggable, Droppable } from "react-beautiful-dnd"; import classNames from "classnames"; 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 { SpaceButton, SpaceItem } from "./SpaceTreeLevel"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; -import { useEventEmitter } from "../../../hooks/useEventEmitter"; +import { useEventEmitterState } from "../../../hooks/useEventEmitter"; import SpaceStore, { HOME_SPACE, + UPDATE_HOME_BEHAVIOUR, UPDATE_INVITED_SPACES, UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES, } from "../../../stores/SpaceStore"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; -import NotificationBadge from "../rooms/NotificationBadge"; -import { - RovingAccessibleButton, - RovingAccessibleTooltipButton, - RovingTabIndexProvider, -} from "../../../accessibility/RovingTabIndex"; +import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex"; import { Key } from "../../../Keyboard"; import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; -import { NotificationState } from "../../../stores/notifications/NotificationState"; - -interface IButtonProps { - space?: Room; - className?: string; - selected?: boolean; - tooltip?: string; - notificationState?: NotificationState; - isNarrow?: boolean; - onClick(): void; -} - -const SpaceButton: React.FC = ({ - space, - className, - selected, - onClick, - tooltip, - notificationState, - isNarrow, - children, -}) => { - const classes = classNames("mx_SpaceButton", className, { - mx_SpaceButton_active: selected, - mx_SpaceButton_narrow: isNarrow, - }); - - let avatar =
; - if (space) { - avatar = ; - } - - let notifBadge; - if (notificationState) { - notifBadge =
- SpaceStore.instance.setActiveRoomInSpace(space)} - forceCount={false} - notification={notificationState} - /> -
; - } - - let button; - if (isNarrow) { - button = ( - -
- { avatar } - { notifBadge } - { children } -
-
- ); - } else { - button = ( - -
- { avatar } - { tooltip } - { notifBadge } - { children } -
-
- ); - } - - return
  • - { button } -
  • ; -}; +import SpaceContextMenu from "../context_menus/SpaceContextMenu"; +import IconizedContextMenu, { + IconizedContextMenuCheckbox, + IconizedContextMenuOptionList, +} from "../context_menus/IconizedContextMenu"; +import SettingsStore from "../../../settings/SettingsStore"; +import { SettingLevel } from "../../../settings/SettingLevel"; const useSpaces = (): [Room[], Room[], Room | null] => { const invites = useEventEmitterState(SpaceStore.instance, UPDATE_INVITED_SPACES, () => { @@ -135,30 +63,108 @@ interface IInnerSpacePanelProps { setPanelCollapsed: Dispatch>; } -// Optimisation based on /~https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/api/droppable.md#recommended-droppable--performance-optimisation -const InnerSpacePanel = React.memo(({ children, isPanelCollapsed, setPanelCollapsed }) => { - const [invites, spaces, activeSpace] = useSpaces(); - const activeSpaces = activeSpace ? [activeSpace] : []; +const HomeButtonContextMenu = ({ onFinished, ...props }: ComponentProps) => { + const allRoomsInHome = useEventEmitterState(SpaceStore.instance, UPDATE_HOME_BEHAVIOUR, () => { + return SpaceStore.instance.allRoomsInHome; + }); - let homeTooltip: string; - let homeNotificationState: NotificationState; - if (SpaceStore.instance.allRoomsInHome) { - homeTooltip = _t("All rooms"); - homeNotificationState = RoomNotificationStateStore.instance.globalState; - } else { - homeTooltip = _t("Home"); - homeNotificationState = SpaceStore.instance.getNotificationState(HOME_SPACE); - } + return +
    + { _t("Home") } +
    + + { + SettingsStore.setValue("Spaces.all_rooms_in_home", null, SettingLevel.ACCOUNT, !allRoomsInHome); + }} + /> + +
    ; +}; - return
    +interface IHomeButtonProps { + selected: boolean; + isPanelCollapsed: boolean; +} + +const HomeButton = ({ selected, isPanelCollapsed }: IHomeButtonProps) => { + const allRoomsInHome = useEventEmitterState(SpaceStore.instance, UPDATE_HOME_BEHAVIOUR, () => { + return SpaceStore.instance.allRoomsInHome; + }); + + return
  • SpaceStore.instance.setActiveSpace(null)} - selected={!activeSpace} - tooltip={homeTooltip} - notificationState={homeNotificationState} + selected={selected} + label={allRoomsInHome ? _t("All rooms") : _t("Home")} + notificationState={allRoomsInHome + ? RoomNotificationStateStore.instance.globalState + : SpaceStore.instance.getNotificationState(HOME_SPACE)} isNarrow={isPanelCollapsed} + ContextMenuComponent={HomeButtonContextMenu} + contextMenuTooltip={_t("Options")} /> +
  • ; +}; + +const CreateSpaceButton = ({ + isPanelCollapsed, + setPanelCollapsed, +}: Pick) => { + // 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(); + + useEffect(() => { + if (!isPanelCollapsed && menuDisplayed) { + closeMenu(); + } + }, [isPanelCollapsed]); // eslint-disable-line react-hooks/exhaustive-deps + + let contextMenu = null; + if (menuDisplayed) { + contextMenu = ; + } + + const onNewClick = menuDisplayed ? closeMenu : () => { + if (!isPanelCollapsed) setPanelCollapsed(true); + openMenu(); + }; + + return
  • + + + { contextMenu } +
  • ; +}; + +// Optimisation based on /~https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/api/droppable.md#recommended-droppable--performance-optimisation +const InnerSpacePanel = React.memo(({ children, isPanelCollapsed, setPanelCollapsed }) => { + const [invites, spaces, activeSpace] = useSpaces(); + const activeSpaces = activeSpace ? [activeSpace] : []; + + return
    + { invites.map(s => ( (({ children, isPanelCo )) } { children } +
    ; }); 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 [isPanelCollapsed, setPanelCollapsed] = useState(true); - useEffect(() => { - if (!isPanelCollapsed && menuDisplayed) { - closeMenu(); - } - }, [isPanelCollapsed]); // eslint-disable-line react-hooks/exhaustive-deps - - let contextMenu = null; - if (menuDisplayed) { - contextMenu = ; - } - const onKeyDown = (ev: React.KeyboardEvent) => { let handled = true; @@ -269,11 +262,6 @@ const SpacePanel = () => { } }; - const onNewClick = menuDisplayed ? closeMenu : () => { - if (!isPanelCollapsed) setPanelCollapsed(true); - openMenu(); - }; - return ( { if (!result.destination) return; // dropped outside the list @@ -301,15 +289,6 @@ const SpacePanel = () => { > { provided.placeholder } - - ) } @@ -318,7 +297,6 @@ const SpacePanel = () => { onClick={() => setPanelCollapsed(!isPanelCollapsed)} title={isPanelCollapsed ? _t("Expand space panel") : _t("Collapse space panel")} /> - { contextMenu } ) } diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index 90584a5361f..bb2184853e3 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -14,7 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { createRef, InputHTMLAttributes, LegacyRef } from "react"; +import React, { + createRef, + MouseEvent, + InputHTMLAttributes, + LegacyRef, + ComponentProps, + ComponentType, +} from "react"; import classNames from "classnames"; import { Room } from "matrix-js-sdk/src/models/room"; @@ -23,31 +30,104 @@ import SpaceStore from "../../../stores/SpaceStore"; import SpaceTreeLevelLayoutStore from "../../../stores/SpaceTreeLevelLayoutStore"; import NotificationBadge from "../rooms/NotificationBadge"; import { RovingAccessibleTooltipButton } from "../../../accessibility/roving/RovingAccessibleTooltipButton"; -import IconizedContextMenu, { - IconizedContextMenuOption, - IconizedContextMenuOptionList, -} from "../context_menus/IconizedContextMenu"; import { _t } from "../../../languageHandler"; import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton"; -import { toRightOf } from "../../structures/ContextMenu"; -import { - shouldShowSpaceSettings, - showAddExistingRooms, - showCreateNewRoom, - showSpaceInvite, - showSpaceSettings, -} from "../../../utils/space"; +import { toRightOf, useContextMenu } from "../../structures/ContextMenu"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; -import defaultDispatcher from "../../../dispatcher/dispatcher"; -import { Action } from "../../../dispatcher/actions"; -import RoomViewStore from "../../../stores/RoomViewStore"; -import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload"; -import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; -import { EventType } from "matrix-js-sdk/src/@types/event"; +import AccessibleButton from "../elements/AccessibleButton"; import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState"; import { NotificationColor } from "../../../stores/notifications/NotificationColor"; import { getKeyBindingsManager, RoomListAction } from "../../../KeyBindingsManager"; +import { NotificationState } from "../../../stores/notifications/NotificationState"; +import SpaceContextMenu from "../context_menus/SpaceContextMenu"; + +interface IButtonProps extends Omit, "title"> { + space?: Room; + className?: string; + selected?: boolean; + label: string; + contextMenuTooltip?: string; + notificationState?: NotificationState; + isNarrow?: boolean; + avatarSize?: number; + ContextMenuComponent?: ComponentType>; + onClick(ev: MouseEvent): void; +} + +export const SpaceButton: React.FC = ({ + space, + className, + selected, + onClick, + label, + contextMenuTooltip, + notificationState, + avatarSize, + isNarrow, + children, + ContextMenuComponent, + ...props +}) => { + const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu(); + + let avatar =
    ; + if (space) { + avatar = ; + } + + let notifBadge; + if (notificationState) { + notifBadge =
    + SpaceStore.instance.setActiveRoomInSpace(space || null)} + forceCount={false} + notification={notificationState} + /> +
    ; + } + + let contextMenu: JSX.Element; + if (menuDisplayed && ContextMenuComponent) { + contextMenu = ; + } + + return ( + + { children } +
    + { avatar } + { !isNarrow && { label } } + { notifBadge } + + { ContextMenuComponent && } + + { contextMenu } +
    +
    + ); +}; interface IItemProps extends InputHTMLAttributes { space?: Room; @@ -61,7 +141,6 @@ interface IItemProps extends InputHTMLAttributes { interface IItemState { collapsed: boolean; - contextMenuPosition: Pick; childSpaces: Room[]; } @@ -81,7 +160,6 @@ export class SpaceItem extends React.PureComponent { this.state = { collapsed: collapsed, - contextMenuPosition: null, childSpaces: this.childSpaces, }; @@ -124,19 +202,6 @@ export class SpaceItem extends React.PureComponent { evt.stopPropagation(); }; - private onContextMenu = (ev: React.MouseEvent) => { - if (this.props.space.getMyMembership() !== "join") return; - ev.preventDefault(); - ev.stopPropagation(); - this.setState({ - contextMenuPosition: { - right: ev.clientX, - top: ev.clientY, - height: 0, - }, - }); - }; - private onKeyDown = (ev: React.KeyboardEvent) => { let handled = true; const action = getKeyBindingsManager().getRoomListAction(ev); @@ -180,188 +245,6 @@ export class SpaceItem extends React.PureComponent { SpaceStore.instance.setActiveSpace(this.props.space); }; - private onMenuOpenClick = (ev: React.MouseEvent) => { - ev.preventDefault(); - ev.stopPropagation(); - const target = ev.target as HTMLButtonElement; - this.setState({ contextMenuPosition: target.getBoundingClientRect() }); - }; - - private onMenuClose = () => { - this.setState({ contextMenuPosition: null }); - }; - - private onInviteClick = (ev: ButtonEvent) => { - ev.preventDefault(); - ev.stopPropagation(); - - showSpaceInvite(this.props.space); - this.setState({ contextMenuPosition: null }); // also close the menu - }; - - private onSettingsClick = (ev: ButtonEvent) => { - ev.preventDefault(); - ev.stopPropagation(); - - showSpaceSettings(this.props.space); - this.setState({ contextMenuPosition: null }); // also close the menu - }; - - private onLeaveClick = (ev: ButtonEvent) => { - ev.preventDefault(); - ev.stopPropagation(); - - defaultDispatcher.dispatch({ - action: "leave_room", - room_id: this.props.space.roomId, - }); - this.setState({ contextMenuPosition: null }); // also close the menu - }; - - private onNewRoomClick = (ev: ButtonEvent) => { - ev.preventDefault(); - ev.stopPropagation(); - - showCreateNewRoom(this.props.space); - this.setState({ contextMenuPosition: null }); // also close the menu - }; - - private onAddExistingRoomClick = (ev: ButtonEvent) => { - ev.preventDefault(); - ev.stopPropagation(); - - showAddExistingRooms(this.props.space); - this.setState({ contextMenuPosition: null }); // also close the menu - }; - - private onMembersClick = (ev: ButtonEvent) => { - ev.preventDefault(); - ev.stopPropagation(); - - if (!RoomViewStore.getRoomId()) { - defaultDispatcher.dispatch({ - action: "view_room", - room_id: this.props.space.roomId, - }, true); - } - - defaultDispatcher.dispatch({ - action: Action.SetRightPanelPhase, - phase: RightPanelPhases.SpaceMemberList, - refireParams: { space: this.props.space }, - }); - this.setState({ contextMenuPosition: null }); // also close the menu - }; - - private onExploreRoomsClick = (ev: ButtonEvent) => { - ev.preventDefault(); - ev.stopPropagation(); - - defaultDispatcher.dispatch({ - action: "view_room", - room_id: this.props.space.roomId, - }); - this.setState({ contextMenuPosition: null }); // also close the menu - }; - - private renderContextMenu(): React.ReactElement { - if (this.props.space.getMyMembership() !== "join") return null; - - let contextMenu = null; - if (this.state.contextMenuPosition) { - const userId = this.context.getUserId(); - - let inviteOption; - if (this.props.space.getJoinRule() === "public" || this.props.space.canInvite(userId)) { - inviteOption = ( - - ); - } - - let settingsOption; - let leaveSection; - if (shouldShowSpaceSettings(this.props.space)) { - settingsOption = ( - - ); - } else { - leaveSection = - - ; - } - - const canAddRooms = this.props.space.currentState.maySendStateEvent(EventType.SpaceChild, userId); - - let newRoomSection; - if (this.props.space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) { - newRoomSection = - - - ; - } - - contextMenu = -
    - { this.props.space.name } -
    - - { inviteOption } - - { settingsOption } - - - { newRoomSection } - { leaveSection } -
    ; - } - - return ( - - - { contextMenu } - - ); - } - render() { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { space, activeSpaces, isNested, isPanelCollapsed, onExpand, parents, innerRef, @@ -369,7 +252,6 @@ export class SpaceItem extends React.PureComponent { const collapsed = this.isCollapsed; - const isActive = activeSpaces.includes(space); const itemClasses = classNames(this.props.className, { "mx_SpaceItem": true, "mx_SpaceItem_narrow": isPanelCollapsed, @@ -378,12 +260,7 @@ export class SpaceItem extends React.PureComponent { }); const isInvite = space.getMyMembership() === "invite"; - const classes = classNames("mx_SpaceButton", { - mx_SpaceButton_active: isActive, - mx_SpaceButton_hasMenuOpen: !!this.state.contextMenuPosition, - mx_SpaceButton_narrow: isPanelCollapsed, - mx_SpaceButton_invite: isInvite, - }); + const notificationState = isInvite ? StaticNotificationState.forSymbol("!", NotificationColor.Red) : SpaceStore.instance.getNotificationState(space.roomId); @@ -398,19 +275,6 @@ export class SpaceItem extends React.PureComponent { />; } - let notifBadge; - if (notificationState) { - notifBadge =
    - SpaceStore.instance.setActiveRoomInSpace(space)} - forceCount={false} - notification={notificationState} - /> -
    ; - } - - const avatarSize = isNested ? 24 : 32; - const toggleCollapseButton = this.state.childSpaces?.length ? { return (
  • - { toggleCollapseButton } -
    - - { !isPanelCollapsed && { space.name } } - { notifBadge } - { this.renderContextMenu() } -
    -
    + { childItems }
  • diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 4a728b72b63..fc6a58708dc 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1019,8 +1019,10 @@ "Address": "Address", "Creating...": "Creating...", "Create": "Create", - "All rooms": "All rooms", "Home": "Home", + "Show all rooms in home": "Show all rooms in home", + "All rooms": "All rooms", + "Options": "Options", "Expand space panel": "Expand space panel", "Collapse space panel": "Collapse space panel", "Click to copy": "Click to copy", @@ -1050,16 +1052,9 @@ "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", - "Add existing room": "Add existing room", - "Members": "Members", - "Manage & explore rooms": "Manage & explore rooms", - "Explore rooms": "Explore rooms", - "Space options": "Space options", "Expand": "Expand", "Collapse": "Collapse", + "Space options": "Space options", "Remove": "Remove", "This bridge was provisioned by .": "This bridge was provisioned by .", "This bridge is managed by .": "This bridge is managed by .", @@ -1583,8 +1578,11 @@ "Start chat": "Start chat", "Rooms": "Rooms", "Add room": "Add room", + "Create new room": "Create new room", "You do not have permissions to create new rooms in this space": "You do not have permissions to create new rooms in this space", + "Add existing room": "Add existing room", "You do not have permissions to add rooms to this space": "You do not have permissions to add rooms to this space", + "Explore rooms": "Explore rooms", "Explore community rooms": "Explore community rooms", "Explore public rooms": "Explore public rooms", "Low priority": "Low priority", @@ -1662,6 +1660,7 @@ "Low Priority": "Low Priority", "Invite People": "Invite People", "Copy Room Link": "Copy Room Link", + "Settings": "Settings", "Leave Room": "Leave Room", "Room options": "Room options", "%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.", @@ -1755,13 +1754,13 @@ "The homeserver the user you’re verifying is connected to": "The homeserver the user you’re verifying is connected to", "Yours, or the other users’ internet connection": "Yours, or the other users’ internet connection", "Yours, or the other users’ session": "Yours, or the other users’ session", + "Members": "Members", "Nothing pinned, yet": "Nothing pinned, yet", "If you have permissions, open the menu on any message and select Pin to stick them here.": "If you have permissions, open the menu on any message and select Pin to stick them here.", "Pinned messages": "Pinned messages", "Room Info": "Room Info", "You can only pin up to %(count)s widgets|other": "You can only pin up to %(count)s widgets", "Unpin a widget to view it in this panel": "Unpin a widget to view it in this panel", - "Options": "Options", "Set my room layout for everyone": "Set my room layout for everyone", "Widgets": "Widgets", "Edit widgets, bridges & bots": "Edit widgets, bridges & bots", @@ -2563,6 +2562,8 @@ "Source URL": "Source URL", "Collapse reply thread": "Collapse reply thread", "Report": "Report", + "Leave space": "Leave space", + "Manage & explore rooms": "Manage & explore rooms", "Clear status": "Clear status", "Update status": "Update status", "Set status": "Set status", From 9fb1c8e4cd74c618594bf6e84906961cd4e27b74 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 28 Jul 2021 19:33:07 +0100 Subject: [PATCH 10/11] Iterate PR --- res/css/structures/_SpacePanel.scss | 8 ++++++++ src/components/views/spaces/SpacePanel.tsx | 4 ++-- src/i18n/strings/en_EN.json | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/res/css/structures/_SpacePanel.scss b/res/css/structures/_SpacePanel.scss index 9d9c3ff8ab7..1dea6332f58 100644 --- a/res/css/structures/_SpacePanel.scss +++ b/res/css/structures/_SpacePanel.scss @@ -368,6 +368,14 @@ $activeBorderColor: $secondary-fg-color; .mx_SpacePanel_iconExplore::before { mask-image: url('$(res)/img/element-icons/roomlist/browse.svg'); } + + .mx_SpacePanel_noIcon { + display: none; + + & + .mx_IconizedContextMenu_label { + padding-left: 5px !important; // override default iconized label style to align with header + } + } } diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx index a339cb81322..bbe27ced754 100644 --- a/src/components/views/spaces/SpacePanel.tsx +++ b/src/components/views/spaces/SpacePanel.tsx @@ -79,8 +79,8 @@ const HomeButtonContextMenu = ({ onFinished, ...props }: ComponentProps { SettingsStore.setValue("Spaces.all_rooms_in_home", null, SettingLevel.ACCOUNT, !allRoomsInHome); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index ae14c6ed9d4..b0752ab0bd9 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1023,7 +1023,7 @@ "Creating...": "Creating...", "Create": "Create", "Home": "Home", - "Show all rooms in home": "Show all rooms in home", + "Show all rooms": "Show all rooms", "All rooms": "All rooms", "Options": "Options", "Expand space panel": "Expand space panel", From 3c436c96171a677a683ed9c5fe831138294f4e7b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 30 Jul 2021 12:20:02 +0100 Subject: [PATCH 11/11] update setting naming to match style --- .../settings/tabs/user/PreferencesUserSettingsTab.tsx | 2 +- src/components/views/spaces/SpacePanel.tsx | 2 +- src/settings/Settings.tsx | 2 +- src/stores/SpaceStore.tsx | 8 ++++---- test/stores/SpaceStore-test.ts | 2 +- test/stores/room-list/SpaceWatcher-test.ts | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx index 53d8d41f690..d3da8a77844 100644 --- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx @@ -49,7 +49,7 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta ]; static SPACES_SETTINGS = [ - "Spaces.all_rooms_in_home", + "Spaces.allRoomsInHome", ]; static KEYBINDINGS_SETTINGS = [ diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx index bbe27ced754..58e1db4b1dd 100644 --- a/src/components/views/spaces/SpacePanel.tsx +++ b/src/components/views/spaces/SpacePanel.tsx @@ -83,7 +83,7 @@ const HomeButtonContextMenu = ({ onFinished, ...props }: ComponentProps { - SettingsStore.setValue("Spaces.all_rooms_in_home", null, SettingLevel.ACCOUNT, !allRoomsInHome); + SettingsStore.setValue("Spaces.allRoomsInHome", null, SettingLevel.ACCOUNT, !allRoomsInHome); }} /> diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 8ee9c8c4270..7ca57adb083 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -749,7 +749,7 @@ export const SETTINGS: {[setting: string]: ISetting} = { supportedLevels: LEVELS_ACCOUNT_SETTINGS, default: null, }, - "Spaces.all_rooms_in_home": { + "Spaces.allRoomsInHome": { displayName: _td("Show all rooms in Home"), description: _td("All rooms you're in will appear in Home."), supportedLevels: LEVELS_ACCOUNT_SETTINGS, diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 3b2f402173f..da18646d0f5 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -117,12 +117,12 @@ export class SpaceStoreClass extends AsyncStoreWithClient { private _invitedSpaces = new Set(); private spaceOrderLocalEchoMap = new Map(); private _restrictedJoinRuleSupport?: IRoomCapability; - private _allRoomsInHome: boolean = SettingsStore.getValue("Spaces.all_rooms_in_home"); + private _allRoomsInHome: boolean = SettingsStore.getValue("Spaces.allRoomsInHome"); constructor() { super(defaultDispatcher, {}); - SettingsStore.monitorSetting("Spaces.all_rooms_in_home", null); + SettingsStore.monitorSetting("Spaces.allRoomsInHome", null); } public get invitedSpaces(): Room[] { @@ -812,8 +812,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient { case Action.SettingUpdated: { const settingUpdatedPayload = payload as SettingUpdatedPayload; - if (settingUpdatedPayload.settingName === "Spaces.all_rooms_in_home") { - const newValue = SettingsStore.getValue("Spaces.all_rooms_in_home"); + if (settingUpdatedPayload.settingName === "Spaces.allRoomsInHome") { + const newValue = SettingsStore.getValue("Spaces.allRoomsInHome"); if (this.allRoomsInHome !== newValue) { this._allRoomsInHome = newValue; this.emit(UPDATE_HOME_BEHAVIOUR, this.allRoomsInHome); diff --git a/test/stores/SpaceStore-test.ts b/test/stores/SpaceStore-test.ts index eb3d5f0b97a..2e823aa72bb 100644 --- a/test/stores/SpaceStore-test.ts +++ b/test/stores/SpaceStore-test.ts @@ -85,7 +85,7 @@ describe("SpaceStore", () => { const setShowAllRooms = async (value: boolean) => { if (store.allRoomsInHome === value) return; const emitProm = testUtils.emitPromise(store, UPDATE_HOME_BEHAVIOUR); - await SettingsStore.setValue("Spaces.all_rooms_in_home", null, SettingLevel.DEVICE, value); + await SettingsStore.setValue("Spaces.allRoomsInHome", null, SettingLevel.DEVICE, value); jest.runAllTimers(); // run async dispatch await emitProm; }; diff --git a/test/stores/room-list/SpaceWatcher-test.ts b/test/stores/room-list/SpaceWatcher-test.ts index c6254349b5d..85f79c75b6a 100644 --- a/test/stores/room-list/SpaceWatcher-test.ts +++ b/test/stores/room-list/SpaceWatcher-test.ts @@ -47,7 +47,7 @@ describe("SpaceWatcher", () => { const setShowAllRooms = async (value: boolean) => { if (store.allRoomsInHome === value) return; - await SettingsStore.setValue("Spaces.all_rooms_in_home", null, SettingLevel.DEVICE, value); + await SettingsStore.setValue("Spaces.allRoomsInHome", null, SettingLevel.DEVICE, value); await testUtils.emitPromise(store, UPDATE_HOME_BEHAVIOUR); };