Stick connected video rooms to the top of the room list (#8353)

This commit is contained in:
Robin 2022-04-22 08:17:31 -04:00 committed by GitHub
parent 988d300258
commit 2a396a406d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 149 additions and 9 deletions

View file

@ -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 = () => {

View file

@ -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;
}

View 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]);
});
});