Stick connected video rooms to the top of the room list (#8353)
This commit is contained in:
parent
988d300258
commit
2a396a406d
3 changed files with 149 additions and 9 deletions
|
@ -70,6 +70,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
constructor() {
|
||||
super(defaultDispatcher);
|
||||
this.setMaxListeners(20); // RoomList + LeftPanel + 8xRoomSubList + spares
|
||||
this.algorithm.start();
|
||||
}
|
||||
|
||||
private setupWatchers() {
|
||||
|
@ -96,6 +97,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
|
||||
this.algorithm.off(LIST_UPDATED_EVENT, this.onAlgorithmListUpdated);
|
||||
this.algorithm.off(FILTER_CHANGED, this.onAlgorithmListUpdated);
|
||||
this.algorithm.stop();
|
||||
this.algorithm = new Algorithm();
|
||||
this.algorithm.on(LIST_UPDATED_EVENT, this.onAlgorithmListUpdated);
|
||||
this.algorithm.on(FILTER_CHANGED, this.onAlgorithmListUpdated);
|
||||
|
@ -479,8 +481,9 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
}
|
||||
}
|
||||
|
||||
private onAlgorithmListUpdated = () => {
|
||||
private onAlgorithmListUpdated = (forceUpdate: boolean) => {
|
||||
this.updateFn.mark();
|
||||
if (forceUpdate) this.updateFn.trigger();
|
||||
};
|
||||
|
||||
private onAlgorithmFilterUpdated = () => {
|
||||
|
|
|
@ -35,6 +35,7 @@ import { EffectiveMembership, getEffectiveMembership, splitRoomsByMembership } f
|
|||
import { OrderingAlgorithm } from "./list-ordering/OrderingAlgorithm";
|
||||
import { getListAlgorithmInstance } from "./list-ordering";
|
||||
import { VisibilityProvider } from "../filters/VisibilityProvider";
|
||||
import VideoChannelStore, { VideoChannelEvent } from "../../VideoChannelStore";
|
||||
|
||||
/**
|
||||
* Fired when the Algorithm has determined a list has been updated.
|
||||
|
@ -84,8 +85,14 @@ export class Algorithm extends EventEmitter {
|
|||
*/
|
||||
public updatesInhibited = false;
|
||||
|
||||
public constructor() {
|
||||
super();
|
||||
public start() {
|
||||
VideoChannelStore.instance.on(VideoChannelEvent.Connect, this.updateVideoRoom);
|
||||
VideoChannelStore.instance.on(VideoChannelEvent.Disconnect, this.updateVideoRoom);
|
||||
}
|
||||
|
||||
public stop() {
|
||||
VideoChannelStore.instance.off(VideoChannelEvent.Connect, this.updateVideoRoom);
|
||||
VideoChannelStore.instance.off(VideoChannelEvent.Disconnect, this.updateVideoRoom);
|
||||
}
|
||||
|
||||
public get stickyRoom(): Room {
|
||||
|
@ -108,6 +115,7 @@ export class Algorithm extends EventEmitter {
|
|||
this._cachedRooms = val;
|
||||
this.recalculateFilteredRooms();
|
||||
this.recalculateStickyRoom();
|
||||
this.recalculateVideoRoom();
|
||||
}
|
||||
|
||||
protected get cachedRooms(): ITagMap {
|
||||
|
@ -145,6 +153,7 @@ export class Algorithm extends EventEmitter {
|
|||
this._cachedRooms[tagId] = algorithm.orderedRooms;
|
||||
this.recalculateFilteredRoomsForTag(tagId); // update filter to re-sort the list
|
||||
this.recalculateStickyRoom(tagId); // update sticky room to make sure it appears if needed
|
||||
this.recalculateVideoRoom(tagId);
|
||||
}
|
||||
|
||||
public getListOrdering(tagId: TagID): ListAlgorithm {
|
||||
|
@ -164,6 +173,7 @@ export class Algorithm extends EventEmitter {
|
|||
this._cachedRooms[tagId] = algorithm.orderedRooms;
|
||||
this.recalculateFilteredRoomsForTag(tagId); // update filter to re-sort the list
|
||||
this.recalculateStickyRoom(tagId); // update sticky room to make sure it appears if needed
|
||||
this.recalculateVideoRoom(tagId);
|
||||
}
|
||||
|
||||
public addFilterCondition(filterCondition: IFilterCondition): void {
|
||||
|
@ -311,12 +321,30 @@ export class Algorithm extends EventEmitter {
|
|||
this.recalculateFilteredRoomsForTag(tag);
|
||||
if (lastStickyRoom && lastStickyRoom.tag !== tag) this.recalculateFilteredRoomsForTag(lastStickyRoom.tag);
|
||||
this.recalculateStickyRoom();
|
||||
this.recalculateVideoRoom(tag);
|
||||
if (lastStickyRoom && lastStickyRoom.tag !== tag) this.recalculateVideoRoom(lastStickyRoom.tag);
|
||||
|
||||
// Finally, trigger an update
|
||||
if (this.updatesInhibited) return;
|
||||
this.emit(LIST_UPDATED_EVENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the stickiness of video rooms.
|
||||
*/
|
||||
public updateVideoRoom = () => {
|
||||
// In case we're unsticking a video room, sort it back into natural order
|
||||
this.recalculateFilteredRooms();
|
||||
this.recalculateStickyRoom();
|
||||
|
||||
this.recalculateVideoRoom();
|
||||
|
||||
if (this.updatesInhibited) return;
|
||||
// This isn't in response to any particular RoomListStore update,
|
||||
// so notify the store that it needs to force-update
|
||||
this.emit(LIST_UPDATED_EVENT, true);
|
||||
};
|
||||
|
||||
protected recalculateFilteredRooms() {
|
||||
if (!this.hasFilters) {
|
||||
return;
|
||||
|
@ -374,6 +402,13 @@ export class Algorithm extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
private initCachedStickyRooms() {
|
||||
this._cachedStickyRooms = {};
|
||||
for (const tagId of Object.keys(this.cachedRooms)) {
|
||||
this._cachedStickyRooms[tagId] = [...this.cachedRooms[tagId]]; // shallow clone
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recalculate the sticky room position. If this is being called in relation to
|
||||
* a specific tag being updated, it should be given to this function to optimize
|
||||
|
@ -400,17 +435,13 @@ export class Algorithm extends EventEmitter {
|
|||
}
|
||||
|
||||
if (!this._cachedStickyRooms || !updatedTag) {
|
||||
const stickiedTagMap: ITagMap = {};
|
||||
for (const tagId of Object.keys(this.cachedRooms)) {
|
||||
stickiedTagMap[tagId] = this.cachedRooms[tagId].map(r => r); // shallow clone
|
||||
}
|
||||
this._cachedStickyRooms = stickiedTagMap;
|
||||
this.initCachedStickyRooms();
|
||||
}
|
||||
|
||||
if (updatedTag) {
|
||||
// Update the tag indicated by the caller, if possible. This is mostly to ensure
|
||||
// our cache is up to date.
|
||||
this._cachedStickyRooms[updatedTag] = this.cachedRooms[updatedTag].map(r => r); // shallow clone
|
||||
this._cachedStickyRooms[updatedTag] = [...this.cachedRooms[updatedTag]]; // shallow clone
|
||||
}
|
||||
|
||||
// Now try to insert the sticky room, if we need to.
|
||||
|
@ -426,6 +457,46 @@ export class Algorithm extends EventEmitter {
|
|||
this.emit(LIST_UPDATED_EVENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recalculate the position of any video rooms. If this is being called in relation to
|
||||
* a specific tag being updated, it should be given to this function to optimize
|
||||
* the call.
|
||||
*
|
||||
* This expects to be called *after* the sticky rooms are updated, and sticks the
|
||||
* currently connected video room to the top of its tag.
|
||||
*
|
||||
* @param updatedTag The tag that was updated, if possible.
|
||||
*/
|
||||
protected recalculateVideoRoom(updatedTag: TagID = null): void {
|
||||
if (!updatedTag) {
|
||||
// Assume all tags need updating
|
||||
// We're not modifying the map here, so can safely rely on the cached values
|
||||
// rather than the explicitly sticky map.
|
||||
for (const tagId of Object.keys(this.cachedRooms)) {
|
||||
if (!tagId) {
|
||||
throw new Error("Unexpected recursion: falsy tag");
|
||||
}
|
||||
this.recalculateVideoRoom(tagId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const videoRoomId = VideoChannelStore.instance.connected ? VideoChannelStore.instance.roomId : null;
|
||||
|
||||
if (videoRoomId) {
|
||||
// We operate directly on the sticky rooms map
|
||||
if (!this._cachedStickyRooms) this.initCachedStickyRooms();
|
||||
const rooms = this._cachedStickyRooms[updatedTag];
|
||||
const videoRoomIdxInTag = rooms.findIndex(r => r.roomId === videoRoomId);
|
||||
if (videoRoomIdxInTag < 0) return; // no-op
|
||||
|
||||
const videoRoom = rooms[videoRoomIdxInTag];
|
||||
rooms.splice(videoRoomIdxInTag, 1);
|
||||
rooms.unshift(videoRoom);
|
||||
this._cachedStickyRooms[updatedTag] = rooms; // re-set because references aren't always safe
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks the Algorithm to regenerate all lists, using the tags given
|
||||
* as reference for which lists to generate and which way to generate
|
||||
|
@ -706,6 +777,7 @@ export class Algorithm extends EventEmitter {
|
|||
this._cachedRooms[rmTag] = algorithm.orderedRooms;
|
||||
this.recalculateFilteredRoomsForTag(rmTag); // update filter to re-sort the list
|
||||
this.recalculateStickyRoom(rmTag); // update sticky room to make sure it moves if needed
|
||||
this.recalculateVideoRoom(rmTag);
|
||||
}
|
||||
for (const addTag of diff.added) {
|
||||
const algorithm: OrderingAlgorithm = this.algorithms[addTag];
|
||||
|
@ -782,6 +854,7 @@ export class Algorithm extends EventEmitter {
|
|||
// Flag that we've done something
|
||||
this.recalculateFilteredRoomsForTag(tag); // update filter to re-sort the list
|
||||
this.recalculateStickyRoom(tag); // update sticky room to make sure it appears if needed
|
||||
this.recalculateVideoRoom(tag);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
|
|
64
test/stores/room-list/algorithms/Algorithm-test.ts
Normal file
64
test/stores/room-list/algorithms/Algorithm-test.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
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 { stubClient, stubVideoChannelStore, mkRoom } from "../../../test-utils";
|
||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
||||
import { DefaultTagID } from "../../../../src/stores/room-list/models";
|
||||
import { SortAlgorithm, ListAlgorithm } from "../../../../src/stores/room-list/algorithms/models";
|
||||
import "../../../../src/stores/room-list/RoomListStore"; // must be imported before Algorithm to avoid cycles
|
||||
import { Algorithm } from "../../../../src/stores/room-list/algorithms/Algorithm";
|
||||
|
||||
describe("Algorithm", () => {
|
||||
let videoChannelStore;
|
||||
let algorithm;
|
||||
let textRoom;
|
||||
let videoRoom;
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
const cli = MatrixClientPeg.get();
|
||||
DMRoomMap.makeShared();
|
||||
videoChannelStore = stubVideoChannelStore();
|
||||
algorithm = new Algorithm();
|
||||
algorithm.start();
|
||||
|
||||
textRoom = mkRoom(cli, "!text:example.org");
|
||||
videoRoom = mkRoom(cli, "!video:example.org");
|
||||
videoRoom.isElementVideoRoom.mockReturnValue(true);
|
||||
algorithm.populateTags(
|
||||
{ [DefaultTagID.Untagged]: SortAlgorithm.Alphabetic },
|
||||
{ [DefaultTagID.Untagged]: ListAlgorithm.Natural },
|
||||
);
|
||||
algorithm.setKnownRooms([textRoom, videoRoom]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
algorithm.stop();
|
||||
});
|
||||
|
||||
it("sticks video rooms to the top when they connect", () => {
|
||||
expect(algorithm.getOrderedRooms()[DefaultTagID.Untagged]).toEqual([textRoom, videoRoom]);
|
||||
videoChannelStore.connect("!video:example.org");
|
||||
expect(algorithm.getOrderedRooms()[DefaultTagID.Untagged]).toEqual([videoRoom, textRoom]);
|
||||
});
|
||||
|
||||
it("unsticks video rooms from the top when they disconnect", () => {
|
||||
videoChannelStore.connect("!video:example.org");
|
||||
expect(algorithm.getOrderedRooms()[DefaultTagID.Untagged]).toEqual([videoRoom, textRoom]);
|
||||
videoChannelStore.disconnect();
|
||||
expect(algorithm.getOrderedRooms()[DefaultTagID.Untagged]).toEqual([textRoom, videoRoom]);
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue