From 846417a3a864667736864dbcc9f9e7f9c45589e0 Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Wed, 26 Oct 2022 13:45:39 +0200 Subject: [PATCH 1/5] Unify voice broadcast css (#9504) --- res/css/_components.pcss | 4 +-- ...rdingPip.pcss => _VoiceBroadcastBody.pcss} | 14 +++++---- .../_VoiceBroadcastPlaybackBody.pcss | 27 ----------------- .../_VoiceBroadcastRecordingBody.pcss | 29 ------------------- .../molecules/VoiceBroadcastPlaybackBody.tsx | 4 +-- .../molecules/VoiceBroadcastRecordingBody.tsx | 2 +- .../molecules/VoiceBroadcastRecordingPip.tsx | 6 ++-- .../VoiceBroadcastPlaybackBody-test.tsx.snap | 12 ++++---- .../VoiceBroadcastRecordingBody-test.tsx.snap | 2 +- .../VoiceBroadcastRecordingPip-test.tsx.snap | 12 ++++---- 10 files changed, 29 insertions(+), 83 deletions(-) rename res/css/voice-broadcast/molecules/{_VoiceBroadcastRecordingPip.pcss => _VoiceBroadcastBody.pcss} (84%) delete mode 100644 res/css/voice-broadcast/molecules/_VoiceBroadcastPlaybackBody.pcss delete mode 100644 res/css/voice-broadcast/molecules/_VoiceBroadcastRecordingBody.pcss diff --git a/res/css/_components.pcss b/res/css/_components.pcss index 9179085cab..cc7c6a2e2a 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -374,6 +374,4 @@ @import "./voice-broadcast/atoms/_PlaybackControlButton.pcss"; @import "./voice-broadcast/atoms/_VoiceBroadcastControl.pcss"; @import "./voice-broadcast/atoms/_VoiceBroadcastHeader.pcss"; -@import "./voice-broadcast/molecules/_VoiceBroadcastPlaybackBody.pcss"; -@import "./voice-broadcast/molecules/_VoiceBroadcastRecordingBody.pcss"; -@import "./voice-broadcast/molecules/_VoiceBroadcastRecordingPip.pcss"; +@import "./voice-broadcast/molecules/_VoiceBroadcastBody.pcss"; diff --git a/res/css/voice-broadcast/molecules/_VoiceBroadcastRecordingPip.pcss b/res/css/voice-broadcast/molecules/_VoiceBroadcastBody.pcss similarity index 84% rename from res/css/voice-broadcast/molecules/_VoiceBroadcastRecordingPip.pcss rename to res/css/voice-broadcast/molecules/_VoiceBroadcastBody.pcss index 11534a4797..37606f993c 100644 --- a/res/css/voice-broadcast/molecules/_VoiceBroadcastRecordingPip.pcss +++ b/res/css/voice-broadcast/molecules/_VoiceBroadcastBody.pcss @@ -14,22 +14,26 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_VoiceBroadcastRecordingPip { - background-color: $system; +.mx_VoiceBroadcastBody { + background-color: $quinary-content; border-radius: 8px; - box-shadow: 0 2px 8px 0 #0000004a; display: inline-block; padding: $spacing-12; } -.mx_VoiceBroadcastRecordingPip_divider { +.mx_VoiceBroadcastBody--pip { + background-color: $system; + box-shadow: 0 2px 8px 0 #0000004a; +} + +.mx_VoiceBroadcastBody_divider { background-color: $quinary-content; border: 0; height: 1px; margin: $spacing-12 0; } -.mx_VoiceBroadcastRecordingPip_controls { +.mx_VoiceBroadcastBody_controls { display: flex; justify-content: space-around; } diff --git a/res/css/voice-broadcast/molecules/_VoiceBroadcastPlaybackBody.pcss b/res/css/voice-broadcast/molecules/_VoiceBroadcastPlaybackBody.pcss deleted file mode 100644 index 11921e1f95..0000000000 --- a/res/css/voice-broadcast/molecules/_VoiceBroadcastPlaybackBody.pcss +++ /dev/null @@ -1,27 +0,0 @@ -/* -Copyright 2022 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -.mx_VoiceBroadcastPlaybackBody { - background-color: $quinary-content; - border-radius: 8px; - display: inline-block; - padding: 12px; -} - -.mx_VoiceBroadcastPlaybackBody_controls { - display: flex; - justify-content: center; -} diff --git a/res/css/voice-broadcast/molecules/_VoiceBroadcastRecordingBody.pcss b/res/css/voice-broadcast/molecules/_VoiceBroadcastRecordingBody.pcss deleted file mode 100644 index 13e3104c9a..0000000000 --- a/res/css/voice-broadcast/molecules/_VoiceBroadcastRecordingBody.pcss +++ /dev/null @@ -1,29 +0,0 @@ -/* -Copyright 2022 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -.mx_VoiceBroadcastRecordingBody { - align-items: flex-start; - background-color: $quinary-content; - border-radius: 8px; - display: inline-flex; - gap: $spacing-8; - padding: 12px; -} - -.mx_VoiceBroadcastRecordingBody_title { - font-size: $font-12px; - font-weight: $font-semi-bold; -} diff --git a/src/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody.tsx b/src/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody.tsx index e0634636a7..e6f2e343cb 100644 --- a/src/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody.tsx +++ b/src/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody.tsx @@ -74,14 +74,14 @@ export const VoiceBroadcastPlaybackBody: React.FC +
-
+
{ control }
diff --git a/src/voice-broadcast/components/molecules/VoiceBroadcastRecordingBody.tsx b/src/voice-broadcast/components/molecules/VoiceBroadcastRecordingBody.tsx index b9721170eb..1b13377da9 100644 --- a/src/voice-broadcast/components/molecules/VoiceBroadcastRecordingBody.tsx +++ b/src/voice-broadcast/components/molecules/VoiceBroadcastRecordingBody.tsx @@ -27,7 +27,7 @@ export const VoiceBroadcastRecordingBody: React.FC +
; return
-
-
+
+
{ toggleControl }


Date: Wed, 26 Oct 2022 12:50:24 +0100 Subject: [PATCH 2/5] Update cypress.yaml (#9506) --- .github/workflows/cypress.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cypress.yaml b/.github/workflows/cypress.yaml index 57e6a7837e..cbb5347173 100644 --- a/.github/workflows/cypress.yaml +++ b/.github/workflows/cypress.yaml @@ -79,8 +79,8 @@ jobs: strategy: fail-fast: false matrix: - # Run 3 instances in Parallel - runner: [1, 2, 3] + # Run 4 instances in Parallel + runner: [1, 2, 3, 4] steps: - uses: actions/checkout@v2 with: From 625971acb5472779f62f2a6175bccc624722e024 Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Wed, 26 Oct 2022 14:54:44 +0200 Subject: [PATCH 3/5] Replace voice broadcast running with resumed (#9502) --- src/voice-broadcast/hooks/useVoiceBroadcastRecording.tsx | 2 +- src/voice-broadcast/index.ts | 2 +- src/voice-broadcast/models/VoiceBroadcastRecording.ts | 6 +++--- .../molecules/VoiceBroadcastRecordingBody-test.tsx | 2 +- .../molecules/VoiceBroadcastRecordingPip-test.tsx | 2 +- .../voice-broadcast/models/VoiceBroadcastPlayback-test.ts | 8 ++++---- .../models/VoiceBroadcastRecording-test.ts | 8 ++++---- .../utils/hasRoomLiveVoiceBroadcast-test.ts | 6 +++--- .../shouldDisplayAsVoiceBroadcastRecordingTile-test.ts | 2 +- .../utils/shouldDisplayAsVoiceBroadcastTile-test.ts | 2 +- .../utils/startNewVoiceBroadcastRecording-test.ts | 4 ++-- 11 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/voice-broadcast/hooks/useVoiceBroadcastRecording.tsx b/src/voice-broadcast/hooks/useVoiceBroadcastRecording.tsx index ed27119de1..209b539bf6 100644 --- a/src/voice-broadcast/hooks/useVoiceBroadcastRecording.tsx +++ b/src/voice-broadcast/hooks/useVoiceBroadcastRecording.tsx @@ -68,7 +68,7 @@ export const useVoiceBroadcastRecording = (recording: VoiceBroadcastRecording) = const live = [ VoiceBroadcastInfoState.Started, VoiceBroadcastInfoState.Paused, - VoiceBroadcastInfoState.Running, + VoiceBroadcastInfoState.Resumed, ].includes(recordingState); return { diff --git a/src/voice-broadcast/index.ts b/src/voice-broadcast/index.ts index 39149c0a78..1ed5ff5377 100644 --- a/src/voice-broadcast/index.ts +++ b/src/voice-broadcast/index.ts @@ -49,7 +49,7 @@ export const VoiceBroadcastChunkEventType = "io.element.voice_broadcast_chunk"; export enum VoiceBroadcastInfoState { Started = "started", Paused = "paused", - Running = "running", + Resumed = "resumed", Stopped = "stopped", } diff --git a/src/voice-broadcast/models/VoiceBroadcastRecording.ts b/src/voice-broadcast/models/VoiceBroadcastRecording.ts index 28cdd72301..c025dc1a2a 100644 --- a/src/voice-broadcast/models/VoiceBroadcastRecording.ts +++ b/src/voice-broadcast/models/VoiceBroadcastRecording.ts @@ -105,15 +105,15 @@ export class VoiceBroadcastRecording public async resume(): Promise { if (this.state !== VoiceBroadcastInfoState.Paused) return; - this.setState(VoiceBroadcastInfoState.Running); + this.setState(VoiceBroadcastInfoState.Resumed); await this.getRecorder().start(); - await this.sendInfoStateEvent(VoiceBroadcastInfoState.Running); + await this.sendInfoStateEvent(VoiceBroadcastInfoState.Resumed); } public toggle = async (): Promise => { if (this.getState() === VoiceBroadcastInfoState.Paused) return this.resume(); - if ([VoiceBroadcastInfoState.Started, VoiceBroadcastInfoState.Running].includes(this.getState())) { + if ([VoiceBroadcastInfoState.Started, VoiceBroadcastInfoState.Resumed].includes(this.getState())) { return this.pause(); } }; diff --git a/test/voice-broadcast/components/molecules/VoiceBroadcastRecordingBody-test.tsx b/test/voice-broadcast/components/molecules/VoiceBroadcastRecordingBody-test.tsx index 25e1f7c215..36b2b4c5a7 100644 --- a/test/voice-broadcast/components/molecules/VoiceBroadcastRecordingBody-test.tsx +++ b/test/voice-broadcast/components/molecules/VoiceBroadcastRecordingBody-test.tsx @@ -50,7 +50,7 @@ describe("VoiceBroadcastRecordingBody", () => { room: roomId, user: userId, }); - recording = new VoiceBroadcastRecording(infoEvent, client, VoiceBroadcastInfoState.Running); + recording = new VoiceBroadcastRecording(infoEvent, client, VoiceBroadcastInfoState.Resumed); }); describe("when rendering a live broadcast", () => { diff --git a/test/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip-test.tsx b/test/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip-test.tsx index 4cd85d37ef..f07b7dd0bd 100644 --- a/test/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip-test.tsx +++ b/test/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip-test.tsx @@ -118,7 +118,7 @@ describe("VoiceBroadcastRecordingPip", () => { }); it("should resume the recording", () => { - expect(recording.getState()).toBe(VoiceBroadcastInfoState.Running); + expect(recording.getState()).toBe(VoiceBroadcastInfoState.Resumed); }); }); }); diff --git a/test/voice-broadcast/models/VoiceBroadcastPlayback-test.ts b/test/voice-broadcast/models/VoiceBroadcastPlayback-test.ts index ec28a95fd6..8471727026 100644 --- a/test/voice-broadcast/models/VoiceBroadcastPlayback-test.ts +++ b/test/voice-broadcast/models/VoiceBroadcastPlayback-test.ts @@ -190,9 +190,9 @@ describe("VoiceBroadcastPlayback", () => { onStateChanged = jest.fn(); }); - describe("when there is a running broadcast without chunks yet", () => { + describe(`when there is a ${VoiceBroadcastInfoState.Resumed} broadcast without chunks yet`, () => { beforeEach(() => { - infoEvent = mkInfoEvent(VoiceBroadcastInfoState.Running); + infoEvent = mkInfoEvent(VoiceBroadcastInfoState.Resumed); playback = mkPlayback(); setUpChunkEvents([]); }); @@ -236,9 +236,9 @@ describe("VoiceBroadcastPlayback", () => { }); }); - describe("when there is a running voice broadcast with some chunks", () => { + describe(`when there is a ${VoiceBroadcastInfoState.Resumed} voice broadcast with some chunks`, () => { beforeEach(() => { - infoEvent = mkInfoEvent(VoiceBroadcastInfoState.Running); + infoEvent = mkInfoEvent(VoiceBroadcastInfoState.Resumed); playback = mkPlayback(); setUpChunkEvents([chunk2Event, chunk0Event, chunk1Event]); }); diff --git a/test/voice-broadcast/models/VoiceBroadcastRecording-test.ts b/test/voice-broadcast/models/VoiceBroadcastRecording-test.ts index b3076d72c0..b8b8008c13 100644 --- a/test/voice-broadcast/models/VoiceBroadcastRecording-test.ts +++ b/test/voice-broadcast/models/VoiceBroadcastRecording-test.ts @@ -423,15 +423,15 @@ describe("VoiceBroadcastRecording", () => { await action(); }); - itShouldBeInState(VoiceBroadcastInfoState.Running); - itShouldSendAnInfoEvent(VoiceBroadcastInfoState.Running); + itShouldBeInState(VoiceBroadcastInfoState.Resumed); + itShouldSendAnInfoEvent(VoiceBroadcastInfoState.Resumed); it("should start the recorder", () => { expect(mocked(voiceBroadcastRecorder.start)).toHaveBeenCalled(); }); - it("should emit a running state changed event", () => { - expect(onStateChanged).toHaveBeenCalledWith(VoiceBroadcastInfoState.Running); + it(`should emit a ${VoiceBroadcastInfoState.Resumed} state changed event`, () => { + expect(onStateChanged).toHaveBeenCalledWith(VoiceBroadcastInfoState.Resumed); }); }); }); diff --git a/test/voice-broadcast/utils/hasRoomLiveVoiceBroadcast-test.ts b/test/voice-broadcast/utils/hasRoomLiveVoiceBroadcast-test.ts index 40b50ec883..a984ed5fd6 100644 --- a/test/voice-broadcast/utils/hasRoomLiveVoiceBroadcast-test.ts +++ b/test/voice-broadcast/utils/hasRoomLiveVoiceBroadcast-test.ts @@ -121,7 +121,7 @@ describe("hasRoomLiveVoiceBroadcast", () => { // all there are kind of live states VoiceBroadcastInfoState.Started, VoiceBroadcastInfoState.Paused, - VoiceBroadcastInfoState.Running, + VoiceBroadcastInfoState.Resumed, ])("when there is a live broadcast (%s) from the current user", (state: VoiceBroadcastInfoState) => { beforeEach(() => { addVoiceBroadcastInfoEvent(state, client.getUserId()); @@ -132,7 +132,7 @@ describe("hasRoomLiveVoiceBroadcast", () => { describe("when there was a live broadcast, that has been stopped", () => { beforeEach(() => { - addVoiceBroadcastInfoEvent(VoiceBroadcastInfoState.Running, client.getUserId()); + addVoiceBroadcastInfoEvent(VoiceBroadcastInfoState.Resumed, client.getUserId()); addVoiceBroadcastInfoEvent(VoiceBroadcastInfoState.Stopped, client.getUserId()); }); @@ -141,7 +141,7 @@ describe("hasRoomLiveVoiceBroadcast", () => { describe("when there is a live broadcast from another user", () => { beforeEach(() => { - addVoiceBroadcastInfoEvent(VoiceBroadcastInfoState.Running, otherUserId); + addVoiceBroadcastInfoEvent(VoiceBroadcastInfoState.Resumed, otherUserId); }); itShouldReturnTrueFalse(); diff --git a/test/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastRecordingTile-test.ts b/test/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastRecordingTile-test.ts index 7673dc4d7b..fc4ec2c04b 100644 --- a/test/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastRecordingTile-test.ts +++ b/test/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastRecordingTile-test.ts @@ -40,7 +40,7 @@ const testCases = [ [ "@user1:example.com", "@user1:example.com", - VoiceBroadcastInfoState.Running, + VoiceBroadcastInfoState.Resumed, true, ], [ diff --git a/test/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastTile-test.ts b/test/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastTile-test.ts index 3d9e87ee3b..394b8c4c11 100644 --- a/test/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastTile-test.ts +++ b/test/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastTile-test.ts @@ -128,7 +128,7 @@ describe("shouldDisplayAsVoiceBroadcastTile", () => { describe.each( [ VoiceBroadcastInfoState.Paused, - VoiceBroadcastInfoState.Running, + VoiceBroadcastInfoState.Resumed, VoiceBroadcastInfoState.Stopped, ], )("when a voice broadcast info event in state %s occurs", (state: VoiceBroadcastInfoState) => { diff --git a/test/voice-broadcast/utils/startNewVoiceBroadcastRecording-test.ts b/test/voice-broadcast/utils/startNewVoiceBroadcastRecording-test.ts index b19ea3c691..dc72868c83 100644 --- a/test/voice-broadcast/utils/startNewVoiceBroadcastRecording-test.ts +++ b/test/voice-broadcast/utils/startNewVoiceBroadcastRecording-test.ts @@ -161,7 +161,7 @@ describe("startNewVoiceBroadcastRecording", () => { room.currentState.setStateEvents([ mkVoiceBroadcastInfoStateEvent( roomId, - VoiceBroadcastInfoState.Running, + VoiceBroadcastInfoState.Resumed, client.getUserId(), client.getDeviceId(), ), @@ -184,7 +184,7 @@ describe("startNewVoiceBroadcastRecording", () => { room.currentState.setStateEvents([ mkVoiceBroadcastInfoStateEvent( roomId, - VoiceBroadcastInfoState.Running, + VoiceBroadcastInfoState.Resumed, otherUserId, "ASD123", ), From 097ca43420b892cff1c89b481c9e963ee395f7d3 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 26 Oct 2022 13:58:40 +0100 Subject: [PATCH 4/5] Add cypress test for the composer emoji picker (#9505) --- cypress/e2e/composer/composer.spec.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/cypress/e2e/composer/composer.spec.ts b/cypress/e2e/composer/composer.spec.ts index f3fc374cf0..6fe562e12a 100644 --- a/cypress/e2e/composer/composer.spec.ts +++ b/cypress/e2e/composer/composer.spec.ts @@ -64,6 +64,21 @@ describe("Composer", () => { cy.contains('.mx_EventTile_body strong', 'bold message'); }); + it("should allow user to input emoji via graphical picker", () => { + cy.getComposer(false).within(() => { + cy.get('[aria-label="Emoji"]').click(); + }); + + cy.get('[data-testid="mx_EmojiPicker"]').within(() => { + cy.contains(".mx_EmojiPicker_item", "😇").click(); + }); + + cy.get(".mx_ContextualMenu_background").click(); // Close emoji picker + cy.get('div[contenteditable=true]').type("{enter}"); // Send message + + cy.contains(".mx_EventTile_body", "😇"); + }); + describe("when Ctrl+Enter is required to send", () => { beforeEach(() => { cy.setSettingValue("MessageComposerInput.ctrlEnterToSend", null, SettingLevel.ACCOUNT, true); From 0453b264e3b447e6a137502f899748fc47254144 Mon Sep 17 00:00:00 2001 From: kegsay Date: Wed, 26 Oct 2022 14:04:03 +0100 Subject: [PATCH 5/5] Sliding Sync: improve sort order, show subspace rooms, better tombstoned room handling (#9484) * Add support for include_old_rooms and by_notification_level * Include subspaces when apply spaces filter * Remove stray is_tombstoned * tests: add SlidingRoomListStore jest tests; update proxy version in cypress * Add additional tests * Additional tests * Linting * Update test/stores/room-list/SlidingRoomListStore-test.ts Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> --- cypress/plugins/sliding-sync/index.ts | 2 +- src/SlidingSyncManager.ts | 31 +- src/components/views/rooms/RoomSublist.tsx | 3 +- src/hooks/useSlidingSyncRoomSearch.ts | 1 - src/stores/room-list/RoomListStore.ts | 10 +- src/stores/room-list/SlidingRoomListStore.ts | 75 ++-- .../room-list/SlidingRoomListStore-test.ts | 319 ++++++++++++++++++ test/test-utils/client.ts | 20 ++ 8 files changed, 418 insertions(+), 43 deletions(-) create mode 100644 test/stores/room-list/SlidingRoomListStore-test.ts diff --git a/cypress/plugins/sliding-sync/index.ts b/cypress/plugins/sliding-sync/index.ts index 61a62aad13..608ada8dbf 100644 --- a/cypress/plugins/sliding-sync/index.ts +++ b/cypress/plugins/sliding-sync/index.ts @@ -77,7 +77,7 @@ async function proxyStart(synapse: SynapseInstance): Promise { const port = await getFreePort(); console.log(new Date(), "starting proxy container..."); const containerId = await dockerRun({ - image: "ghcr.io/matrix-org/sliding-sync-proxy:v0.4.0", + image: "ghcr.io/matrix-org/sliding-sync-proxy:v0.6.0", containerName: "react-sdk-cypress-sliding-sync-proxy", params: [ "--rm", diff --git a/src/SlidingSyncManager.ts b/src/SlidingSyncManager.ts index 0e5736465e..c41e6a78e3 100644 --- a/src/SlidingSyncManager.ts +++ b/src/SlidingSyncManager.ts @@ -63,6 +63,15 @@ const DEFAULT_ROOM_SUBSCRIPTION_INFO = { required_state: [ ["*", "*"], // all events ], + include_old_rooms: { + timeline_limit: 0, + required_state: [ // state needed to handle space navigation and tombstone chains + [EventType.RoomCreate, ""], + [EventType.RoomTombstone, ""], + [EventType.SpaceChild, "*"], + [EventType.SpaceParent, "*"], + ], + }, }; export type PartialSlidingSyncRequest = { @@ -121,6 +130,16 @@ export class SlidingSyncManager { [EventType.SpaceParent, "*"], // all space parents [EventType.RoomMember, this.client.getUserId()!], // lets the client calculate that we are in fact in the room ], + include_old_rooms: { + timeline_limit: 0, + required_state: [ + [EventType.RoomCreate, ""], + [EventType.RoomTombstone, ""], // lets JS SDK hide rooms which are dead + [EventType.SpaceChild, "*"], // all space children + [EventType.SpaceParent, "*"], // all space parents + [EventType.RoomMember, this.client.getUserId()!], // lets the client calculate that we are in fact in the room + ], + }, filters: { room_types: ["m.space"], }, @@ -176,7 +195,7 @@ export class SlidingSyncManager { list = { ranges: [[0, 20]], sort: [ - "by_highlight_count", "by_notification_count", "by_recency", + "by_notification_level", "by_recency", ], timeline_limit: 1, // most recent message display: though this seems to only be needed for favourites? required_state: [ @@ -187,6 +206,16 @@ export class SlidingSyncManager { [EventType.RoomCreate, ""], // for isSpaceRoom checks [EventType.RoomMember, this.client.getUserId()], // lets the client calculate that we are in fact in the room ], + include_old_rooms: { + timeline_limit: 0, + required_state: [ + [EventType.RoomCreate, ""], + [EventType.RoomTombstone, ""], // lets JS SDK hide rooms which are dead + [EventType.SpaceChild, "*"], // all space children + [EventType.SpaceParent, "*"], // all space parents + [EventType.RoomMember, this.client.getUserId()!], // lets the client calculate that we are in fact in the room + ], + }, }; list = Object.assign(list, updateArgs); } else { diff --git a/src/components/views/rooms/RoomSublist.tsx b/src/components/views/rooms/RoomSublist.tsx index 9e890e9c21..9f8133d55c 100644 --- a/src/components/views/rooms/RoomSublist.tsx +++ b/src/components/views/rooms/RoomSublist.tsx @@ -570,8 +570,7 @@ export default class RoomSublist extends React.Component { const slidingList = SlidingSyncManager.instance.slidingSync.getList(slidingSyncIndex); isAlphabetical = slidingList.sort[0] === "by_name"; isUnreadFirst = ( - slidingList.sort[0] === "by_highlight_count" || - slidingList.sort[0] === "by_notification_count" + slidingList.sort[0] === "by_notification_level" ); } diff --git a/src/hooks/useSlidingSyncRoomSearch.ts b/src/hooks/useSlidingSyncRoomSearch.ts index 6ba08dc1a7..97e43d8b28 100644 --- a/src/hooks/useSlidingSyncRoomSearch.ts +++ b/src/hooks/useSlidingSyncRoomSearch.ts @@ -52,7 +52,6 @@ export const useSlidingSyncRoomSearch = () => { ranges: [[0, limit]], filters: { room_name_like: term, - is_tombstoned: false, }, }); const rooms = []; diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index d6f9de79c3..73d6bdbd51 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -24,7 +24,7 @@ import SettingsStore from "../../settings/SettingsStore"; import { DefaultTagID, OrderedDefaultTagIDs, RoomUpdateCause, TagID } from "./models"; import { IListOrderingMap, ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/models"; import { ActionPayload } from "../../dispatcher/payloads"; -import defaultDispatcher from "../../dispatcher/dispatcher"; +import defaultDispatcher, { MatrixDispatcher } from "../../dispatcher/dispatcher"; import { readReceiptChangeIsFor } from "../../utils/read-receipts"; import { FILTER_CHANGED, IFilterCondition } from "./filters/IFilterCondition"; import { Algorithm, LIST_UPDATED_EVENT } from "./algorithms/Algorithm"; @@ -65,8 +65,8 @@ export class RoomListStoreClass extends AsyncStoreWithClient implements this.emit(LISTS_UPDATE_EVENT); }); - constructor() { - super(defaultDispatcher); + constructor(dis: MatrixDispatcher) { + super(dis); this.setMaxListeners(20); // RoomList + LeftPanel + 8xRoomSubList + spares this.algorithm.start(); } @@ -613,11 +613,11 @@ export default class RoomListStore { if (!RoomListStore.internalInstance) { if (SettingsStore.getValue("feature_sliding_sync")) { logger.info("using SlidingRoomListStoreClass"); - const instance = new SlidingRoomListStoreClass(); + const instance = new SlidingRoomListStoreClass(defaultDispatcher, SdkContextClass.instance); instance.start(); RoomListStore.internalInstance = instance; } else { - const instance = new RoomListStoreClass(); + const instance = new RoomListStoreClass(defaultDispatcher); instance.start(); RoomListStore.internalInstance = instance; } diff --git a/src/stores/room-list/SlidingRoomListStore.ts b/src/stores/room-list/SlidingRoomListStore.ts index 35550d04f1..1c5fd1adea 100644 --- a/src/stores/room-list/SlidingRoomListStore.ts +++ b/src/stores/room-list/SlidingRoomListStore.ts @@ -21,12 +21,10 @@ import { MSC3575Filter, SlidingSyncEvent } from "matrix-js-sdk/src/sliding-sync" import { RoomUpdateCause, TagID, OrderedDefaultTagIDs, DefaultTagID } from "./models"; import { ITagMap, ListAlgorithm, SortAlgorithm } from "./algorithms/models"; import { ActionPayload } from "../../dispatcher/payloads"; -import defaultDispatcher from "../../dispatcher/dispatcher"; +import { MatrixDispatcher } from "../../dispatcher/dispatcher"; import { IFilterCondition } from "./filters/IFilterCondition"; import { AsyncStoreWithClient } from "../AsyncStoreWithClient"; import { RoomListStore as Interface, RoomListStoreEvent } from "./Interface"; -import { SlidingSyncManager } from "../../SlidingSyncManager"; -import SpaceStore from "../spaces/SpaceStore"; import { MetaSpace, SpaceKey, UPDATE_SELECTED_SPACE } from "../spaces"; import { LISTS_LOADING_EVENT } from "./RoomListStore"; import { UPDATE_EVENT } from "../AsyncStore"; @@ -38,7 +36,7 @@ interface IState { export const SlidingSyncSortToFilter: Record = { [SortAlgorithm.Alphabetic]: ["by_name", "by_recency"], - [SortAlgorithm.Recent]: ["by_highlight_count", "by_notification_count", "by_recency"], + [SortAlgorithm.Recent]: ["by_notification_level", "by_recency"], [SortAlgorithm.Manual]: ["by_recency"], }; @@ -48,21 +46,18 @@ const filterConditions: Record = { }, [DefaultTagID.Favourite]: { tags: ["m.favourite"], - is_tombstoned: false, }, // TODO https://github.com/vector-im/element-web/issues/23207 // DefaultTagID.SavedItems, [DefaultTagID.DM]: { is_dm: true, is_invite: false, - is_tombstoned: false, // If a DM has a Favourite & Low Prio tag then it'll be shown in those lists instead not_tags: ["m.favourite", "m.lowpriority"], }, [DefaultTagID.Untagged]: { is_dm: false, is_invite: false, - is_tombstoned: false, not_room_types: ["m.space"], not_tags: ["m.favourite", "m.lowpriority"], // spaces filter added dynamically @@ -71,7 +66,6 @@ const filterConditions: Record = { tags: ["m.lowpriority"], // If a room has both Favourite & Low Prio tags then it'll be shown under Favourites not_tags: ["m.favourite"], - is_tombstoned: false, }, // TODO https://github.com/vector-im/element-web/issues/23207 // DefaultTagID.ServerNotice, @@ -87,25 +81,25 @@ export class SlidingRoomListStoreClass extends AsyncStoreWithClient impl private counts: Record = {}; private stickyRoomId: string | null; - public constructor() { - super(defaultDispatcher); + public constructor(dis: MatrixDispatcher, private readonly context: SdkContextClass) { + super(dis); this.setMaxListeners(20); // RoomList + LeftPanel + 8xRoomSubList + spares } public async setTagSorting(tagId: TagID, sort: SortAlgorithm) { logger.info("SlidingRoomListStore.setTagSorting ", tagId, sort); this.tagIdToSortAlgo[tagId] = sort; - const slidingSyncIndex = SlidingSyncManager.instance.getOrAllocateListIndex(tagId); + const slidingSyncIndex = this.context.slidingSyncManager.getOrAllocateListIndex(tagId); switch (sort) { case SortAlgorithm.Alphabetic: - await SlidingSyncManager.instance.ensureListRegistered( + await this.context.slidingSyncManager.ensureListRegistered( slidingSyncIndex, { sort: SlidingSyncSortToFilter[SortAlgorithm.Alphabetic], }, ); break; case SortAlgorithm.Recent: - await SlidingSyncManager.instance.ensureListRegistered( + await this.context.slidingSyncManager.ensureListRegistered( slidingSyncIndex, { sort: SlidingSyncSortToFilter[SortAlgorithm.Recent], }, @@ -174,10 +168,13 @@ export class SlidingRoomListStoreClass extends AsyncStoreWithClient impl // check all lists for each tag we know about and see if the room is there const tags: TagID[] = []; for (const tagId in this.tagIdToSortAlgo) { - const index = SlidingSyncManager.instance.getOrAllocateListIndex(tagId); - const { roomIndexToRoomId } = SlidingSyncManager.instance.slidingSync.getListData(index); - for (const roomIndex in roomIndexToRoomId) { - const roomId = roomIndexToRoomId[roomIndex]; + const index = this.context.slidingSyncManager.getOrAllocateListIndex(tagId); + const listData = this.context.slidingSyncManager.slidingSync.getListData(index); + if (!listData) { + continue; + } + for (const roomIndex in listData.roomIndexToRoomId) { + const roomId = listData.roomIndexToRoomId[roomIndex]; if (roomId === room.roomId) { tags.push(tagId); break; @@ -207,7 +204,7 @@ export class SlidingRoomListStoreClass extends AsyncStoreWithClient impl // this room will not move due to it being viewed: it is sticky. This can be null to indicate // no sticky room if you aren't viewing a room. - this.stickyRoomId = SdkContextClass.instance.roomViewStore.getRoomId(); + this.stickyRoomId = this.context.roomViewStore.getRoomId(); let stickyRoomNewIndex = -1; const stickyRoomOldIndex = (tagMap[tagId] || []).findIndex((room) => { return room.roomId === this.stickyRoomId; @@ -264,7 +261,7 @@ export class SlidingRoomListStoreClass extends AsyncStoreWithClient impl } private onSlidingSyncListUpdate(listIndex: number, joinCount: number, roomIndexToRoomId: Record) { - const tagId = SlidingSyncManager.instance.listIdForIndex(listIndex); + const tagId = this.context.slidingSyncManager.listIdForIndex(listIndex); this.counts[tagId]= joinCount; this.refreshOrderedLists(tagId, roomIndexToRoomId); // let the UI update @@ -273,7 +270,7 @@ export class SlidingRoomListStoreClass extends AsyncStoreWithClient impl private onRoomViewStoreUpdated() { // we only care about this to know when the user has clicked on a room to set the stickiness value - if (SdkContextClass.instance.roomViewStore.getRoomId() === this.stickyRoomId) { + if (this.context.roomViewStore.getRoomId() === this.stickyRoomId) { return; } @@ -296,14 +293,17 @@ export class SlidingRoomListStoreClass extends AsyncStoreWithClient impl if (room) { // resort it based on the slidingSync view of the list. This may cause this old sticky // room to cease to exist. - const index = SlidingSyncManager.instance.getOrAllocateListIndex(tagId); - const { roomIndexToRoomId } = SlidingSyncManager.instance.slidingSync.getListData(index); - this.refreshOrderedLists(tagId, roomIndexToRoomId); + const index = this.context.slidingSyncManager.getOrAllocateListIndex(tagId); + const listData = this.context.slidingSyncManager.slidingSync.getListData(index); + if (!listData) { + continue; + } + this.refreshOrderedLists(tagId, listData.roomIndexToRoomId); hasUpdatedAnyList = true; } } // in the event we didn't call refreshOrderedLists, it helps to still remember the sticky room ID. - this.stickyRoomId = SdkContextClass.instance.roomViewStore.getRoomId(); + this.stickyRoomId = this.context.roomViewStore.getRoomId(); if (hasUpdatedAnyList) { this.emit(LISTS_UPDATE_EVENT); @@ -313,11 +313,11 @@ export class SlidingRoomListStoreClass extends AsyncStoreWithClient impl protected async onReady(): Promise { logger.info("SlidingRoomListStore.onReady"); // permanent listeners: never get destroyed. Could be an issue if we want to test this in isolation. - SlidingSyncManager.instance.slidingSync.on(SlidingSyncEvent.List, this.onSlidingSyncListUpdate.bind(this)); - SdkContextClass.instance.roomViewStore.addListener(UPDATE_EVENT, this.onRoomViewStoreUpdated.bind(this)); - SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdated.bind(this)); - if (SpaceStore.instance.activeSpace) { - this.onSelectedSpaceUpdated(SpaceStore.instance.activeSpace, false); + this.context.slidingSyncManager.slidingSync.on(SlidingSyncEvent.List, this.onSlidingSyncListUpdate.bind(this)); + this.context.roomViewStore.addListener(UPDATE_EVENT, this.onRoomViewStoreUpdated.bind(this)); + this.context.spaceStore.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdated.bind(this)); + if (this.context.spaceStore.activeSpace) { + this.onSelectedSpaceUpdated(this.context.spaceStore.activeSpace, false); } // sliding sync has an initial response for spaces. Now request all the lists. @@ -332,8 +332,8 @@ export class SlidingRoomListStoreClass extends AsyncStoreWithClient impl const sort = SortAlgorithm.Recent; // default to recency sort, TODO: read from config this.tagIdToSortAlgo[tagId] = sort; this.emit(LISTS_LOADING_EVENT, tagId, true); - const index = SlidingSyncManager.instance.getOrAllocateListIndex(tagId); - SlidingSyncManager.instance.ensureListRegistered(index, { + const index = this.context.slidingSyncManager.getOrAllocateListIndex(tagId); + this.context.slidingSyncManager.ensureListRegistered(index, { filters: filter, sort: SlidingSyncSortToFilter[sort], }).then(() => { @@ -350,9 +350,18 @@ export class SlidingRoomListStoreClass extends AsyncStoreWithClient impl const oldSpace = filters.spaces?.[0]; filters.spaces = (activeSpace && activeSpace != MetaSpace.Home) ? [activeSpace] : undefined; if (oldSpace !== activeSpace) { + // include subspaces in this list + this.context.spaceStore.traverseSpace(activeSpace, (roomId: string) => { + if (roomId === activeSpace) { + return; + } + filters.spaces.push(roomId); // add subspace + }, false); + this.emit(LISTS_LOADING_EVENT, tagId, true); - SlidingSyncManager.instance.ensureListRegistered( - SlidingSyncManager.instance.getOrAllocateListIndex(tagId), + const index = this.context.slidingSyncManager.getOrAllocateListIndex(tagId); + this.context.slidingSyncManager.ensureListRegistered( + index, { filters: filters, }, diff --git a/test/stores/room-list/SlidingRoomListStore-test.ts b/test/stores/room-list/SlidingRoomListStore-test.ts new file mode 100644 index 0000000000..488c92396a --- /dev/null +++ b/test/stores/room-list/SlidingRoomListStore-test.ts @@ -0,0 +1,319 @@ +/* +Copyright 2022 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 { mocked } from 'jest-mock'; +import { SlidingSync, SlidingSyncEvent } from 'matrix-js-sdk/src/sliding-sync'; +import { Room } from 'matrix-js-sdk/src/matrix'; + +import { + LISTS_UPDATE_EVENT, + SlidingRoomListStoreClass, + SlidingSyncSortToFilter, +} from "../../../src/stores/room-list/SlidingRoomListStore"; +import { SpaceStoreClass } from "../../../src/stores/spaces/SpaceStore"; +import { MockEventEmitter, stubClient, untilEmission } from "../../test-utils"; +import { TestSdkContext } from '../../TestSdkContext'; +import { SlidingSyncManager } from '../../../src/SlidingSyncManager'; +import { RoomViewStore } from '../../../src/stores/RoomViewStore'; +import { MatrixDispatcher } from '../../../src/dispatcher/dispatcher'; +import { SortAlgorithm } from '../../../src/stores/room-list/algorithms/models'; +import { DefaultTagID, TagID } from '../../../src/stores/room-list/models'; +import { UPDATE_SELECTED_SPACE } from '../../../src/stores/spaces'; +import { LISTS_LOADING_EVENT } from '../../../src/stores/room-list/RoomListStore'; +import { UPDATE_EVENT } from '../../../src/stores/AsyncStore'; + +jest.mock('../../../src/SlidingSyncManager'); +const MockSlidingSyncManager = >SlidingSyncManager; + +describe("SlidingRoomListStore", () => { + let store: SlidingRoomListStoreClass; + let context: TestSdkContext; + let dis: MatrixDispatcher; + let activeSpace: string; + let tagIdToIndex = {}; + + beforeEach(async () => { + context = new TestSdkContext(); + context.client = stubClient(); + context._SpaceStore = new MockEventEmitter({ + traverseSpace: jest.fn(), + get activeSpace() { + return activeSpace; + }, + }) as SpaceStoreClass; + context._SlidingSyncManager = new MockSlidingSyncManager(); + context._SlidingSyncManager.slidingSync = mocked(new MockEventEmitter({ + getListData: jest.fn(), + }) as unknown as SlidingSync); + context._RoomViewStore = mocked(new MockEventEmitter({ + getRoomId: jest.fn(), + }) as unknown as RoomViewStore); + + // mock implementations to allow the store to map tag IDs to sliding sync list indexes and vice versa + let index = 0; + tagIdToIndex = {}; + mocked(context._SlidingSyncManager.getOrAllocateListIndex).mockImplementation((listId: string): number => { + if (tagIdToIndex[listId] != null) { + return tagIdToIndex[listId]; + } + tagIdToIndex[listId] = index; + index++; + return index; + }); + mocked(context.slidingSyncManager.listIdForIndex).mockImplementation((i) => { + for (const tagId in tagIdToIndex) { + const j = tagIdToIndex[tagId]; + if (i === j) { + return tagId; + } + } + return null; + }); + mocked(context._SlidingSyncManager.ensureListRegistered).mockResolvedValue({ + ranges: [[0, 10]], + }); + + dis = new MatrixDispatcher(); + store = new SlidingRoomListStoreClass(dis, context); + }); + + describe("spaces", () => { + it("alters 'filters.spaces' on the DefaultTagID.Untagged list when the selected space changes", async () => { + await store.start(); // call onReady + const spaceRoomId = "!foo:bar"; + + const p = untilEmission(store, LISTS_LOADING_EVENT, (listName, isLoading) => { + return listName === DefaultTagID.Untagged && !isLoading; + }); + + // change the active space + activeSpace = spaceRoomId; + context._SpaceStore.emit(UPDATE_SELECTED_SPACE, spaceRoomId, false); + await p; + + expect(context._SlidingSyncManager.ensureListRegistered).toHaveBeenCalledWith( + tagIdToIndex[DefaultTagID.Untagged], + { + filters: expect.objectContaining({ + spaces: [spaceRoomId], + }), + }, + ); + }); + + it("alters 'filters.spaces' on the DefaultTagID.Untagged list if it loads with an active space", async () => { + // change the active space before we are ready + const spaceRoomId = "!foo2:bar"; + activeSpace = spaceRoomId; + const p = untilEmission(store, LISTS_LOADING_EVENT, (listName, isLoading) => { + return listName === DefaultTagID.Untagged && !isLoading; + }); + await store.start(); // call onReady + await p; + expect(context._SlidingSyncManager.ensureListRegistered).toHaveBeenCalledWith( + tagIdToIndex[DefaultTagID.Untagged], + expect.objectContaining({ + filters: expect.objectContaining({ + spaces: [spaceRoomId], + }), + }), + ); + }); + + it("includes subspaces in 'filters.spaces' when the selected space has subspaces", async () => { + await store.start(); // call onReady + const spaceRoomId = "!foo:bar"; + const subSpace1 = "!ss1:bar"; + const subSpace2 = "!ss2:bar"; + + const p = untilEmission(store, LISTS_LOADING_EVENT, (listName, isLoading) => { + return listName === DefaultTagID.Untagged && !isLoading; + }); + + mocked(context._SpaceStore.traverseSpace).mockImplementation( + (spaceId: string, fn: (roomId: string) => void) => { + if (spaceId === spaceRoomId) { + fn(subSpace1); + fn(subSpace2); + } + }, + ); + + // change the active space + activeSpace = spaceRoomId; + context._SpaceStore.emit(UPDATE_SELECTED_SPACE, spaceRoomId, false); + await p; + + expect(context._SlidingSyncManager.ensureListRegistered).toHaveBeenCalledWith( + tagIdToIndex[DefaultTagID.Untagged], + { + filters: expect.objectContaining({ + spaces: [spaceRoomId, subSpace1, subSpace2], + }), + }, + ); + }); + }); + + it("setTagSorting alters the 'sort' option in the list", async () => { + mocked(context._SlidingSyncManager.getOrAllocateListIndex).mockReturnValue(0); + const tagId: TagID = "foo"; + await store.setTagSorting(tagId, SortAlgorithm.Alphabetic); + expect(context._SlidingSyncManager.ensureListRegistered).toBeCalledWith(0, { + sort: SlidingSyncSortToFilter[SortAlgorithm.Alphabetic], + }); + expect(store.getTagSorting(tagId)).toEqual(SortAlgorithm.Alphabetic); + + await store.setTagSorting(tagId, SortAlgorithm.Recent); + expect(context._SlidingSyncManager.ensureListRegistered).toBeCalledWith(0, { + sort: SlidingSyncSortToFilter[SortAlgorithm.Recent], + }); + expect(store.getTagSorting(tagId)).toEqual(SortAlgorithm.Recent); + }); + + it("getTagsForRoom gets the tags for the room", async () => { + await store.start(); + const untaggedIndex = context._SlidingSyncManager.getOrAllocateListIndex(DefaultTagID.Untagged); + const favIndex = context._SlidingSyncManager.getOrAllocateListIndex(DefaultTagID.Favourite); + const roomA = "!a:localhost"; + const roomB = "!b:localhost"; + const indexToListData = { + [untaggedIndex]: { + joinedCount: 10, + roomIndexToRoomId: { + 0: roomA, + 1: roomB, + }, + }, + [favIndex]: { + joinedCount: 2, + roomIndexToRoomId: { + 0: roomB, + }, + }, + }; + mocked(context._SlidingSyncManager.slidingSync.getListData).mockImplementation((i: number) => { + return indexToListData[i] || null; + }); + + expect(store.getTagsForRoom(new Room(roomA, context.client, context.client.getUserId()))).toEqual( + [DefaultTagID.Untagged], + ); + expect(store.getTagsForRoom(new Room(roomB, context.client, context.client.getUserId()))).toEqual( + [DefaultTagID.Favourite, DefaultTagID.Untagged], + ); + }); + + it("emits LISTS_UPDATE_EVENT when slidingSync lists update", async () => { + await store.start(); + const roomA = "!a:localhost"; + const roomB = "!b:localhost"; + const roomC = "!c:localhost"; + const tagId = DefaultTagID.Favourite; + const listIndex = context.slidingSyncManager.getOrAllocateListIndex(tagId); + const joinCount = 10; + const roomIndexToRoomId = { // mixed to ensure we sort + 1: roomB, + 2: roomC, + 0: roomA, + }; + const rooms = [ + new Room(roomA, context.client, context.client.getUserId()), + new Room(roomB, context.client, context.client.getUserId()), + new Room(roomC, context.client, context.client.getUserId()), + ]; + mocked(context.client.getRoom).mockImplementation((roomId: string) => { + switch (roomId) { + case roomA: + return rooms[0]; + case roomB: + return rooms[1]; + case roomC: + return rooms[2]; + } + return null; + }); + const p = untilEmission(store, LISTS_UPDATE_EVENT); + context.slidingSyncManager.slidingSync.emit(SlidingSyncEvent.List, listIndex, joinCount, roomIndexToRoomId); + await p; + expect(store.getCount(tagId)).toEqual(joinCount); + expect(store.orderedLists[tagId]).toEqual(rooms); + }); + + it("sets the sticky room on the basis of the viewed room in RoomViewStore", async () => { + await store.start(); + // seed the store with 3 rooms + const roomIdA = "!a:localhost"; + const roomIdB = "!b:localhost"; + const roomIdC = "!c:localhost"; + const tagId = DefaultTagID.Favourite; + const listIndex = context.slidingSyncManager.getOrAllocateListIndex(tagId); + const joinCount = 10; + const roomIndexToRoomId = { // mixed to ensure we sort + 1: roomIdB, + 2: roomIdC, + 0: roomIdA, + }; + const roomA = new Room(roomIdA, context.client, context.client.getUserId()); + const roomB = new Room(roomIdB, context.client, context.client.getUserId()); + const roomC = new Room(roomIdC, context.client, context.client.getUserId()); + mocked(context.client.getRoom).mockImplementation((roomId: string) => { + switch (roomId) { + case roomIdA: + return roomA; + case roomIdB: + return roomB; + case roomIdC: + return roomC; + } + return null; + }); + mocked(context._SlidingSyncManager.slidingSync.getListData).mockImplementation((i: number) => { + if (i !== listIndex) { + return null; + } + return { + roomIndexToRoomId: roomIndexToRoomId, + joinedCount: joinCount, + }; + }); + let p = untilEmission(store, LISTS_UPDATE_EVENT); + context.slidingSyncManager.slidingSync.emit(SlidingSyncEvent.List, listIndex, joinCount, roomIndexToRoomId); + await p; + expect(store.orderedLists[tagId]).toEqual([roomA, roomB, roomC]); + + // make roomB sticky and inform the store + mocked(context.roomViewStore.getRoomId).mockReturnValue(roomIdB); + context.roomViewStore.emit(UPDATE_EVENT); + + // bump room C to the top, room B should not move from i=1 despite the list update saying to + roomIndexToRoomId[0] = roomIdC; + roomIndexToRoomId[1] = roomIdA; + roomIndexToRoomId[2] = roomIdB; + p = untilEmission(store, LISTS_UPDATE_EVENT); + context.slidingSyncManager.slidingSync.emit(SlidingSyncEvent.List, listIndex, joinCount, roomIndexToRoomId); + await p; + + // check that B didn't move and that A was put below B + expect(store.orderedLists[tagId]).toEqual([roomC, roomB, roomA]); + + // make room C sticky: rooms should move as a result, without needing an additional list update + mocked(context.roomViewStore.getRoomId).mockReturnValue(roomIdC); + p = untilEmission(store, LISTS_UPDATE_EVENT); + context.roomViewStore.emit(UPDATE_EVENT); + await p; + expect(store.orderedLists[tagId].map((r) => r.roomId)).toEqual([roomC, roomA, roomB].map((r) => r.roomId)); + }); +}); diff --git a/test/test-utils/client.ts b/test/test-utils/client.ts index e0c532c021..e155dd17c4 100644 --- a/test/test-utils/client.ts +++ b/test/test-utils/client.ts @@ -21,6 +21,26 @@ import { MatrixClient, User } from "matrix-js-sdk/src/matrix"; import { MatrixClientPeg } from "../../src/MatrixClientPeg"; +/** + * Mocked generic class with a real EventEmitter. + * Useful for mocks which need event emitters. + */ +export class MockEventEmitter extends EventEmitter { + /** + * Construct a new event emitter with additional properties/functions. The event emitter functions + * like .emit and .on will be real. + * @param mockProperties An object with the mock property or function implementations. 'getters' + * are correctly cloned to this event emitter. + */ + constructor(mockProperties: Partial|PropertyKeysOf, unknown>> = {}) { + super(); + // We must use defineProperties and not assign as the former clones getters correctly, + // whereas the latter invokes the getter and sets the return value permanently on the + // destination object. + Object.defineProperties(this, Object.getOwnPropertyDescriptors(mockProperties)); + } +} + /** * Mock client with real event emitter * useful for testing code that listens