Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit fa2403d

Browse files
committed
Stick connected video rooms to the top of the room list
1 parent 605fbd3 commit fa2403d

File tree

3 files changed

+149
-9
lines changed

3 files changed

+149
-9
lines changed

src/stores/room-list/RoomListStore.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
7070
constructor() {
7171
super(defaultDispatcher);
7272
this.setMaxListeners(20); // RoomList + LeftPanel + 8xRoomSubList + spares
73+
this.algorithm.start();
7374
}
7475

7576
private setupWatchers() {
@@ -96,6 +97,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
9697

9798
this.algorithm.off(LIST_UPDATED_EVENT, this.onAlgorithmListUpdated);
9899
this.algorithm.off(FILTER_CHANGED, this.onAlgorithmListUpdated);
100+
this.algorithm.stop();
99101
this.algorithm = new Algorithm();
100102
this.algorithm.on(LIST_UPDATED_EVENT, this.onAlgorithmListUpdated);
101103
this.algorithm.on(FILTER_CHANGED, this.onAlgorithmListUpdated);
@@ -479,8 +481,9 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
479481
}
480482
}
481483

482-
private onAlgorithmListUpdated = () => {
484+
private onAlgorithmListUpdated = (forceUpdate: boolean) => {
483485
this.updateFn.mark();
486+
if (forceUpdate) this.updateFn.trigger();
484487
};
485488

486489
private onAlgorithmFilterUpdated = () => {

src/stores/room-list/algorithms/Algorithm.ts

Lines changed: 81 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { EffectiveMembership, getEffectiveMembership, splitRoomsByMembership } f
3535
import { OrderingAlgorithm } from "./list-ordering/OrderingAlgorithm";
3636
import { getListAlgorithmInstance } from "./list-ordering";
3737
import { VisibilityProvider } from "../filters/VisibilityProvider";
38+
import VideoChannelStore, { VideoChannelEvent } from "../../VideoChannelStore";
3839

3940
/**
4041
* Fired when the Algorithm has determined a list has been updated.
@@ -84,8 +85,14 @@ export class Algorithm extends EventEmitter {
8485
*/
8586
public updatesInhibited = false;
8687

87-
public constructor() {
88-
super();
88+
public start() {
89+
VideoChannelStore.instance.on(VideoChannelEvent.Connect, this.updateVideoRoom);
90+
VideoChannelStore.instance.on(VideoChannelEvent.Disconnect, this.updateVideoRoom);
91+
}
92+
93+
public stop() {
94+
VideoChannelStore.instance.off(VideoChannelEvent.Connect, this.updateVideoRoom);
95+
VideoChannelStore.instance.off(VideoChannelEvent.Disconnect, this.updateVideoRoom);
8996
}
9097

9198
public get stickyRoom(): Room {
@@ -108,6 +115,7 @@ export class Algorithm extends EventEmitter {
108115
this._cachedRooms = val;
109116
this.recalculateFilteredRooms();
110117
this.recalculateStickyRoom();
118+
this.recalculateVideoRoom();
111119
}
112120

113121
protected get cachedRooms(): ITagMap {
@@ -145,6 +153,7 @@ export class Algorithm extends EventEmitter {
145153
this._cachedRooms[tagId] = algorithm.orderedRooms;
146154
this.recalculateFilteredRoomsForTag(tagId); // update filter to re-sort the list
147155
this.recalculateStickyRoom(tagId); // update sticky room to make sure it appears if needed
156+
this.recalculateVideoRoom(tagId);
148157
}
149158

150159
public getListOrdering(tagId: TagID): ListAlgorithm {
@@ -164,6 +173,7 @@ export class Algorithm extends EventEmitter {
164173
this._cachedRooms[tagId] = algorithm.orderedRooms;
165174
this.recalculateFilteredRoomsForTag(tagId); // update filter to re-sort the list
166175
this.recalculateStickyRoom(tagId); // update sticky room to make sure it appears if needed
176+
this.recalculateVideoRoom(tagId);
167177
}
168178

169179
public addFilterCondition(filterCondition: IFilterCondition): void {
@@ -311,12 +321,30 @@ export class Algorithm extends EventEmitter {
311321
this.recalculateFilteredRoomsForTag(tag);
312322
if (lastStickyRoom && lastStickyRoom.tag !== tag) this.recalculateFilteredRoomsForTag(lastStickyRoom.tag);
313323
this.recalculateStickyRoom();
324+
this.recalculateVideoRoom(tag);
325+
if (lastStickyRoom && lastStickyRoom.tag !== tag) this.recalculateVideoRoom(lastStickyRoom.tag);
314326

315327
// Finally, trigger an update
316328
if (this.updatesInhibited) return;
317329
this.emit(LIST_UPDATED_EVENT);
318330
}
319331

332+
/**
333+
* Update the stickiness of video rooms.
334+
*/
335+
public updateVideoRoom = () => {
336+
// In case we're unsticking a video room, sort it back into natural order
337+
this.recalculateFilteredRooms();
338+
this.recalculateStickyRoom();
339+
340+
this.recalculateVideoRoom();
341+
342+
if (this.updatesInhibited) return;
343+
// This isn't in response to any particular RoomListStore update,
344+
// so notify the store that it needs to force-update
345+
this.emit(LIST_UPDATED_EVENT, true);
346+
};
347+
320348
protected recalculateFilteredRooms() {
321349
if (!this.hasFilters) {
322350
return;
@@ -374,6 +402,13 @@ export class Algorithm extends EventEmitter {
374402
}
375403
}
376404

405+
private initCachedStickyRooms() {
406+
this._cachedStickyRooms = {};
407+
for (const tagId of Object.keys(this.cachedRooms)) {
408+
this._cachedStickyRooms[tagId] = [...this.cachedRooms[tagId]]; // shallow clone
409+
}
410+
}
411+
377412
/**
378413
* Recalculate the sticky room position. If this is being called in relation to
379414
* a specific tag being updated, it should be given to this function to optimize
@@ -400,17 +435,13 @@ export class Algorithm extends EventEmitter {
400435
}
401436

402437
if (!this._cachedStickyRooms || !updatedTag) {
403-
const stickiedTagMap: ITagMap = {};
404-
for (const tagId of Object.keys(this.cachedRooms)) {
405-
stickiedTagMap[tagId] = this.cachedRooms[tagId].map(r => r); // shallow clone
406-
}
407-
this._cachedStickyRooms = stickiedTagMap;
438+
this.initCachedStickyRooms();
408439
}
409440

410441
if (updatedTag) {
411442
// Update the tag indicated by the caller, if possible. This is mostly to ensure
412443
// our cache is up to date.
413-
this._cachedStickyRooms[updatedTag] = this.cachedRooms[updatedTag].map(r => r); // shallow clone
444+
this._cachedStickyRooms[updatedTag] = [...this.cachedRooms[updatedTag]]; // shallow clone
414445
}
415446

416447
// Now try to insert the sticky room, if we need to.
@@ -426,6 +457,46 @@ export class Algorithm extends EventEmitter {
426457
this.emit(LIST_UPDATED_EVENT);
427458
}
428459

460+
/**
461+
* Recalculate the position of any video rooms. If this is being called in relation to
462+
* a specific tag being updated, it should be given to this function to optimize
463+
* the call.
464+
*
465+
* This expects to be called *after* the sticky rooms are updated, and sticks the
466+
* currently connected video room to the top of its tag.
467+
*
468+
* @param updatedTag The tag that was updated, if possible.
469+
*/
470+
protected recalculateVideoRoom(updatedTag: TagID = null): void {
471+
if (!updatedTag) {
472+
// Assume all tags need updating
473+
// We're not modifying the map here, so can safely rely on the cached values
474+
// rather than the explicitly sticky map.
475+
for (const tagId of Object.keys(this.cachedRooms)) {
476+
if (!tagId) {
477+
throw new Error("Unexpected recursion: falsy tag");
478+
}
479+
this.recalculateVideoRoom(tagId);
480+
}
481+
return;
482+
}
483+
484+
const videoRoomId = VideoChannelStore.instance.connected ? VideoChannelStore.instance.roomId : null;
485+
486+
if (videoRoomId) {
487+
// We operate directly on the sticky rooms map
488+
if (!this._cachedStickyRooms) this.initCachedStickyRooms();
489+
const rooms = this._cachedStickyRooms[updatedTag];
490+
const videoRoomIdxInTag = rooms.findIndex(r => r.roomId === videoRoomId);
491+
if (videoRoomIdxInTag < 0) return; // no-op
492+
493+
const videoRoom = rooms[videoRoomIdxInTag];
494+
rooms.splice(videoRoomIdxInTag, 1);
495+
rooms.unshift(videoRoom);
496+
this._cachedStickyRooms[updatedTag] = rooms; // re-set because references aren't always safe
497+
}
498+
}
499+
429500
/**
430501
* Asks the Algorithm to regenerate all lists, using the tags given
431502
* as reference for which lists to generate and which way to generate
@@ -706,6 +777,7 @@ export class Algorithm extends EventEmitter {
706777
this._cachedRooms[rmTag] = algorithm.orderedRooms;
707778
this.recalculateFilteredRoomsForTag(rmTag); // update filter to re-sort the list
708779
this.recalculateStickyRoom(rmTag); // update sticky room to make sure it moves if needed
780+
this.recalculateVideoRoom(rmTag);
709781
}
710782
for (const addTag of diff.added) {
711783
const algorithm: OrderingAlgorithm = this.algorithms[addTag];
@@ -782,6 +854,7 @@ export class Algorithm extends EventEmitter {
782854
// Flag that we've done something
783855
this.recalculateFilteredRoomsForTag(tag); // update filter to re-sort the list
784856
this.recalculateStickyRoom(tag); // update sticky room to make sure it appears if needed
857+
this.recalculateVideoRoom(tag);
785858
changed = true;
786859
}
787860

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { stubClient, stubVideoChannelStore, mkRoom } from "../../../test-utils";
18+
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
19+
import DMRoomMap from "../../../../src/utils/DMRoomMap";
20+
import { DefaultTagID } from "../../../../src/stores/room-list/models";
21+
import { SortAlgorithm, ListAlgorithm } from "../../../../src/stores/room-list/algorithms/models";
22+
import "../../../../src/stores/room-list/RoomListStore"; // must be imported before Algorithm to avoid cycles
23+
import { Algorithm } from "../../../../src/stores/room-list/algorithms/Algorithm";
24+
25+
describe("Algorithm", () => {
26+
let videoChannelStore;
27+
let algorithm;
28+
let textRoom;
29+
let videoRoom;
30+
beforeEach(() => {
31+
stubClient();
32+
const cli = MatrixClientPeg.get();
33+
DMRoomMap.makeShared();
34+
videoChannelStore = stubVideoChannelStore();
35+
algorithm = new Algorithm();
36+
algorithm.start();
37+
38+
textRoom = mkRoom(cli, "!text:example.org");
39+
videoRoom = mkRoom(cli, "!video:example.org");
40+
videoRoom.isElementVideoRoom.mockReturnValue(true);
41+
algorithm.populateTags(
42+
{ [DefaultTagID.Untagged]: SortAlgorithm.Alphabetic },
43+
{ [DefaultTagID.Untagged]: ListAlgorithm.Natural },
44+
);
45+
algorithm.setKnownRooms([textRoom, videoRoom]);
46+
});
47+
48+
afterEach(() => {
49+
algorithm.stop();
50+
});
51+
52+
it("sticks video rooms to the top when they connect", () => {
53+
expect(algorithm.getOrderedRooms()[DefaultTagID.Untagged]).toEqual([textRoom, videoRoom]);
54+
videoChannelStore.connect("!video:example.org");
55+
expect(algorithm.getOrderedRooms()[DefaultTagID.Untagged]).toEqual([videoRoom, textRoom]);
56+
});
57+
58+
it("unsticks video rooms from the top when they disconnect", () => {
59+
videoChannelStore.connect("!video:example.org");
60+
expect(algorithm.getOrderedRooms()[DefaultTagID.Untagged]).toEqual([videoRoom, textRoom]);
61+
videoChannelStore.disconnect();
62+
expect(algorithm.getOrderedRooms()[DefaultTagID.Untagged]).toEqual([textRoom, videoRoom]);
63+
});
64+
});

0 commit comments

Comments
 (0)