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

Commit 7173ea7

Browse files
authored
Merge pull request #4905 from matrix-org/travis/room-list/room-safety
Improve room safety in the new room list
2 parents b3b404f + 8739e2f commit 7173ea7

File tree

1 file changed

+131
-14
lines changed

1 file changed

+131
-14
lines changed

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

Lines changed: 131 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { Room } from "matrix-js-sdk/src/models/room";
1818
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
1919
import DMRoomMap from "../../../utils/DMRoomMap";
2020
import { EventEmitter } from "events";
21-
import { arrayHasDiff, ArrayUtil } from "../../../utils/arrays";
21+
import { arrayDiff, arrayHasDiff, ArrayUtil } from "../../../utils/arrays";
2222
import { getEnumValues } from "../../../utils/enums";
2323
import { DefaultTagID, RoomUpdateCause, TagID } from "../models";
2424
import {
@@ -57,6 +57,7 @@ export class Algorithm extends EventEmitter {
5757
private _cachedStickyRooms: ITagMap = {}; // a clone of the _cachedRooms, with the sticky room
5858
private filteredRooms: ITagMap = {};
5959
private _stickyRoom: IStickyRoom = null;
60+
private _lastStickyRoom: IStickyRoom = null; // only not-null when changing the sticky room
6061
private sortAlgorithms: ITagSortingMap;
6162
private listAlgorithms: IListOrderingMap;
6263
private algorithms: IOrderingAlgorithmMap;
@@ -162,9 +163,21 @@ export class Algorithm extends EventEmitter {
162163
}
163164

164165
private async updateStickyRoom(val: Room) {
166+
try {
167+
return await this.doUpdateStickyRoom(val);
168+
} finally {
169+
this._lastStickyRoom = null; // clear to indicate we're done changing
170+
}
171+
}
172+
173+
private async doUpdateStickyRoom(val: Room) {
165174
// Note throughout: We need async so we can wait for handleRoomUpdate() to do its thing,
166175
// otherwise we risk duplicating rooms.
167176

177+
// Set the last sticky room to indicate that we're in a change. The code throughout the
178+
// class can safely handle a null room, so this should be safe to do as a backup.
179+
this._lastStickyRoom = this._stickyRoom || <IStickyRoom>{};
180+
168181
// It's possible to have no selected room. In that case, clear the sticky room
169182
if (!val) {
170183
if (this._stickyRoom) {
@@ -179,7 +192,7 @@ export class Algorithm extends EventEmitter {
179192
}
180193

181194
// When we do have a room though, we expect to be able to find it
182-
const tag = this.roomIdsToTags[val.roomId][0];
195+
let tag = this.roomIdsToTags[val.roomId][0];
183196
if (!tag) throw new Error(`${val.roomId} does not belong to a tag and cannot be sticky`);
184197

185198
// We specifically do NOT use the ordered rooms set as it contains the sticky room, which
@@ -196,19 +209,41 @@ export class Algorithm extends EventEmitter {
196209
// the same thing it no-ops. After we're done calling the algorithm, we'll issue
197210
// a new update for ourselves.
198211
const lastStickyRoom = this._stickyRoom;
199-
this._stickyRoom = null;
212+
this._stickyRoom = null; // clear before we update the algorithm
200213
this.recalculateStickyRoom();
201214

202215
// When we do have the room, re-add the old room (if needed) to the algorithm
203216
// and remove the sticky room from the algorithm. This is so the underlying
204217
// algorithm doesn't try and confuse itself with the sticky room concept.
205-
if (lastStickyRoom) {
218+
// We don't add the new room if the sticky room isn't changing because that's
219+
// an easy way to cause duplication. We have to do room ID checks instead of
220+
// referential checks as the references can differ through the lifecycle.
221+
if (lastStickyRoom && lastStickyRoom.room && lastStickyRoom.room.roomId !== val.roomId) {
206222
// Lie to the algorithm and re-add the room to the algorithm
207223
await this.handleRoomUpdate(lastStickyRoom.room, RoomUpdateCause.NewRoom);
208224
}
209225
// Lie to the algorithm and remove the room from it's field of view
210226
await this.handleRoomUpdate(val, RoomUpdateCause.RoomRemoved);
211227

228+
// Check for tag & position changes while we're here. We also check the room to ensure
229+
// it is still the same room.
230+
if (this._stickyRoom) {
231+
if (this._stickyRoom.room !== val) {
232+
// Check the room IDs just in case
233+
if (this._stickyRoom.room.roomId === val.roomId) {
234+
console.warn("Sticky room changed references");
235+
} else {
236+
throw new Error("Sticky room changed while the sticky room was changing");
237+
}
238+
}
239+
240+
console.warn(`Sticky room changed tag & position from ${tag} / ${position} `
241+
+ `to ${this._stickyRoom.tag} / ${this._stickyRoom.position}`);
242+
243+
tag = this._stickyRoom.tag;
244+
position = this._stickyRoom.position;
245+
}
246+
212247
// Now that we're done lying to the algorithm, we need to update our position
213248
// marker only if the user is moving further down the same list. If they're switching
214249
// lists, or moving upwards, the position marker will splice in just fine but if
@@ -560,7 +595,7 @@ export class Algorithm extends EventEmitter {
560595
/**
561596
* Updates the roomsToTags map
562597
*/
563-
protected updateTagsFromCache() {
598+
private updateTagsFromCache() {
564599
const newMap = {};
565600

566601
const tags = Object.keys(this.cachedRooms);
@@ -607,21 +642,94 @@ export class Algorithm extends EventEmitter {
607642
* processing.
608643
*/
609644
public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<boolean> {
645+
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
646+
console.log(`Handle room update for ${room.roomId} called with cause ${cause}`);
610647
if (!this.algorithms) throw new Error("Not ready: no algorithms to determine tags from");
611648

649+
// Note: check the isSticky against the room ID just in case the reference is wrong
650+
const isSticky = this._stickyRoom && this._stickyRoom.room && this._stickyRoom.room.roomId === room.roomId;
612651
if (cause === RoomUpdateCause.NewRoom) {
652+
const isForLastSticky = this._lastStickyRoom && this._lastStickyRoom.room === room;
613653
const roomTags = this.roomIdsToTags[room.roomId];
614-
if (roomTags && roomTags.length > 0) {
654+
const hasTags = roomTags && roomTags.length > 0;
655+
656+
// Don't change the cause if the last sticky room is being re-added. If we fail to
657+
// pass the cause through as NewRoom, we'll fail to lie to the algorithm and thus
658+
// lose the room.
659+
if (hasTags && !isForLastSticky) {
615660
console.warn(`${room.roomId} is reportedly new but is already known - assuming TagChange instead`);
616661
cause = RoomUpdateCause.PossibleTagChange;
617662
}
663+
664+
// If we have tags for a room and don't have the room referenced, the room reference
665+
// probably changed. We need to swap out the problematic reference.
666+
if (hasTags && !this.rooms.includes(room) && !isSticky) {
667+
console.warn(`${room.roomId} is missing from room array but is known - trying to find duplicate`);
668+
this.rooms = this.rooms.map(r => r.roomId === room.roomId ? room : r);
669+
670+
// Sanity check
671+
if (!this.rooms.includes(room)) {
672+
throw new Error(`Failed to replace ${room.roomId} with an updated reference`);
673+
}
674+
}
675+
676+
// Like above, update the reference to the sticky room if we need to
677+
if (hasTags && isSticky) {
678+
// Go directly in and set the sticky room's new reference, being careful not
679+
// to trigger a sticky room update ourselves.
680+
this._stickyRoom.room = room;
681+
}
618682
}
619683

620684
if (cause === RoomUpdateCause.PossibleTagChange) {
621-
// TODO: Be smarter and splice rather than regen the planet. https://github.com/vector-im/riot-web/issues/14035
622-
// TODO: No-op if no change. https://github.com/vector-im/riot-web/issues/14035
623-
await this.setKnownRooms(this.rooms);
624-
return true;
685+
let didTagChange = false;
686+
const oldTags = this.roomIdsToTags[room.roomId] || [];
687+
const newTags = this.getTagsForRoom(room);
688+
const diff = arrayDiff(oldTags, newTags);
689+
if (diff.removed.length > 0 || diff.added.length > 0) {
690+
for (const rmTag of diff.removed) {
691+
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
692+
console.log(`Removing ${room.roomId} from ${rmTag}`);
693+
const algorithm: OrderingAlgorithm = this.algorithms[rmTag];
694+
if (!algorithm) throw new Error(`No algorithm for ${rmTag}`);
695+
await algorithm.handleRoomUpdate(room, RoomUpdateCause.RoomRemoved);
696+
}
697+
for (const addTag of diff.added) {
698+
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
699+
console.log(`Adding ${room.roomId} to ${addTag}`);
700+
const algorithm: OrderingAlgorithm = this.algorithms[addTag];
701+
if (!algorithm) throw new Error(`No algorithm for ${addTag}`);
702+
await algorithm.handleRoomUpdate(room, RoomUpdateCause.NewRoom);
703+
}
704+
705+
// Update the tag map so we don't regen it in a moment
706+
this.roomIdsToTags[room.roomId] = newTags;
707+
708+
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
709+
console.log(`Changing update cause for ${room.roomId} to Timeline to sort rooms`);
710+
cause = RoomUpdateCause.Timeline;
711+
didTagChange = true;
712+
} else {
713+
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
714+
console.warn(`Received no-op update for ${room.roomId} - changing to Timeline update`);
715+
cause = RoomUpdateCause.Timeline;
716+
}
717+
718+
if (didTagChange && isSticky) {
719+
// Manually update the tag for the sticky room without triggering a sticky room
720+
// update. The update will be handled implicitly by the sticky room handling and
721+
// requires no changes on our part, if we're in the middle of a sticky room change.
722+
if (this._lastStickyRoom) {
723+
this._stickyRoom = {
724+
room,
725+
tag: this.roomIdsToTags[room.roomId][0],
726+
position: 0, // right at the top as it changed tags
727+
};
728+
} else {
729+
// We have to clear the lock as the sticky room change will trigger updates.
730+
await this.setStickyRoomAsync(room);
731+
}
732+
}
625733
}
626734

627735
// If the update is for a room change which might be the sticky room, prevent it. We
@@ -635,8 +743,9 @@ export class Algorithm extends EventEmitter {
635743
}
636744
}
637745

638-
if (cause === RoomUpdateCause.NewRoom && !this.roomIdsToTags[room.roomId]) {
639-
console.log(`[RoomListDebug] Updating tags for new room ${room.roomId} (${room.name})`);
746+
if (!this.roomIdsToTags[room.roomId]) {
747+
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
748+
console.log(`[RoomListDebug] Updating tags for room ${room.roomId} (${room.name})`);
640749

641750
// Get the tags for the room and populate the cache
642751
const roomTags = this.getTagsForRoom(room).filter(t => !isNullOrUndefined(this.cachedRooms[t]));
@@ -646,9 +755,15 @@ export class Algorithm extends EventEmitter {
646755
if (!roomTags.length) throw new Error(`Tags cannot be determined for ${room.roomId}`);
647756

648757
this.roomIdsToTags[room.roomId] = roomTags;
758+
759+
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
760+
console.log(`[RoomListDebug] Updated tags for ${room.roomId}:`, roomTags);
649761
}
650762

651-
let tags = this.roomIdsToTags[room.roomId];
763+
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
764+
console.log(`[RoomListDebug] Reached algorithmic handling for ${room.roomId} and cause ${cause}`);
765+
766+
const tags = this.roomIdsToTags[room.roomId];
652767
if (!tags) {
653768
console.warn(`No tags known for "${room.name}" (${room.roomId})`);
654769
return false;
@@ -668,6 +783,8 @@ export class Algorithm extends EventEmitter {
668783
changed = true;
669784
}
670785

671-
return true;
786+
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
787+
console.log(`[RoomListDebug] Finished handling ${room.roomId} with cause ${cause} (changed=${changed})`);
788+
return changed;
672789
}
673790
}

0 commit comments

Comments
 (0)