Skip to content

Commit

Permalink
Stick connected video rooms to the top of the room list (#8353)
Browse files Browse the repository at this point in the history
  • Loading branch information
robintown committed Apr 22, 2022
1 parent 988d300 commit 2a396a4
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 9 deletions.
5 changes: 4 additions & 1 deletion src/stores/room-list/RoomListStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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);
Expand Down Expand Up @@ -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 = () => {
Expand Down
89 changes: 81 additions & 8 deletions src/stores/room-list/algorithms/Algorithm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand All @@ -108,6 +115,7 @@ export class Algorithm extends EventEmitter {
this._cachedRooms = val;
this.recalculateFilteredRooms();
this.recalculateStickyRoom();
this.recalculateVideoRoom();
}

protected get cachedRooms(): ITagMap {
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -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;
}

Expand Down
64 changes: 64 additions & 0 deletions test/stores/room-list/algorithms/Algorithm-test.ts
Original file line number Diff line number Diff line change
@@ -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]);
});
});

0 comments on commit 2a396a4

Please sign in to comment.