Skip to content

Commit 2eaaf6a

Browse files
authored
Merge pull request matrix-org#4828 from matrix-org/travis/room-list/proliferation
Fix a number of proliferation issues in the new room list
2 parents b456529 + 9de4251 commit 2eaaf6a

File tree

5 files changed

+202
-69
lines changed

5 files changed

+202
-69
lines changed

src/stores/room-list/RoomListStore2.ts

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { IFilterCondition } from "./filters/IFilterCondition";
2929
import { TagWatcher } from "./TagWatcher";
3030
import RoomViewStore from "../RoomViewStore";
3131
import { Algorithm, LIST_UPDATED_EVENT } from "./algorithms/Algorithm";
32+
import { EffectiveMembership, getEffectiveMembership } from "./membership";
3233

3334
interface IState {
3435
tagsEnabled?: boolean;
@@ -97,8 +98,10 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
9798
this.algorithm.stickyRoom = null;
9899
} else if (activeRoomId) {
99100
const activeRoom = this.matrixClient.getRoom(activeRoomId);
100-
if (!activeRoom) throw new Error(`${activeRoomId} is current in RVS but missing from client`);
101-
if (activeRoom !== this.algorithm.stickyRoom) {
101+
if (!activeRoom) {
102+
console.warn(`${activeRoomId} is current in RVS but missing from client - clearing sticky room`);
103+
this.algorithm.stickyRoom = null;
104+
} else if (activeRoom !== this.algorithm.stickyRoom) {
102105
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
103106
console.log(`Changing sticky room to ${activeRoomId}`);
104107
this.algorithm.stickyRoom = activeRoom;
@@ -187,10 +190,13 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
187190
console.log(`[RoomListDebug] Live timeline event ${eventPayload.event.getId()} in ${updatedRoom.roomId}`);
188191
if (eventPayload.event.getType() === 'm.room.tombstone' && eventPayload.event.getStateKey() === '') {
189192
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
190-
console.log(`[RoomListDebug] Got tombstone event - regenerating room list`);
191-
// TODO: We could probably be smarter about this: https://github.com/vector-im/riot-web/issues/14035
192-
await this.regenerateAllLists();
193-
return; // don't pass the update down - we will have already handled it in the regen
193+
console.log(`[RoomListDebug] Got tombstone event - trying to remove now-dead room`);
194+
const newRoom = this.matrixClient.getRoom(eventPayload.event.getContent()['replacement_room']);
195+
if (newRoom) {
196+
// If we have the new room, then the new room check will have seen the predecessor
197+
// and did the required updates, so do nothing here.
198+
return;
199+
}
194200
}
195201
await this.handleRoomUpdate(updatedRoom, RoomUpdateCause.Timeline);
196202
};
@@ -242,18 +248,49 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
242248
}
243249
} else if (payload.action === 'MatrixActions.Room.myMembership') {
244250
const membershipPayload = (<any>payload); // TODO: Type out the dispatcher types
245-
if (membershipPayload.oldMembership !== "join" && membershipPayload.membership === "join") {
251+
const oldMembership = getEffectiveMembership(membershipPayload.oldMembership);
252+
const newMembership = getEffectiveMembership(membershipPayload.membership);
253+
if (oldMembership !== EffectiveMembership.Join && newMembership === EffectiveMembership.Join) {
246254
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
247255
console.log(`[RoomListDebug] Handling new room ${membershipPayload.room.roomId}`);
248-
await this.algorithm.handleRoomUpdate(membershipPayload.room, RoomUpdateCause.NewRoom);
256+
257+
// If we're joining an upgraded room, we'll want to make sure we don't proliferate
258+
// the dead room in the list.
259+
const createEvent = membershipPayload.room.currentState.getStateEvents("m.room.create", "");
260+
if (createEvent && createEvent.getContent()['predecessor']) {
261+
console.log(`[RoomListDebug] Room has a predecessor`);
262+
const prevRoom = this.matrixClient.getRoom(createEvent.getContent()['predecessor']['room_id']);
263+
if (prevRoom) {
264+
const isSticky = this.algorithm.stickyRoom === prevRoom;
265+
if (isSticky) {
266+
console.log(`[RoomListDebug] Clearing sticky room due to room upgrade`);
267+
await this.algorithm.setStickyRoomAsync(null);
268+
}
269+
270+
// Note: we hit the algorithm instead of our handleRoomUpdate() function to
271+
// avoid redundant updates.
272+
console.log(`[RoomListDebug] Removing previous room from room list`);
273+
await this.algorithm.handleRoomUpdate(prevRoom, RoomUpdateCause.RoomRemoved);
274+
}
275+
}
276+
277+
console.log(`[RoomListDebug] Adding new room to room list`);
278+
await this.handleRoomUpdate(membershipPayload.room, RoomUpdateCause.NewRoom);
279+
return;
280+
}
281+
282+
if (oldMembership !== EffectiveMembership.Invite && newMembership === EffectiveMembership.Invite) {
283+
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
284+
console.log(`[RoomListDebug] Handling invite to ${membershipPayload.room.roomId}`);
285+
await this.handleRoomUpdate(membershipPayload.room, RoomUpdateCause.NewRoom);
249286
return;
250287
}
251288

252289
// If it's not a join, it's transitioning into a different list (possibly historical)
253-
if (membershipPayload.oldMembership !== membershipPayload.membership) {
290+
if (oldMembership !== newMembership) {
254291
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
255292
console.log(`[RoomListDebug] Handling membership change in ${membershipPayload.room.roomId}`);
256-
await this.algorithm.handleRoomUpdate(membershipPayload.room, RoomUpdateCause.PossibleTagChange);
293+
await this.handleRoomUpdate(membershipPayload.room, RoomUpdateCause.PossibleTagChange);
257294
return;
258295
}
259296
}

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

Lines changed: 93 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import {
3030
SortAlgorithm
3131
} from "./models";
3232
import { FILTER_CHANGED, FilterPriority, IFilterCondition } from "../filters/IFilterCondition";
33-
import { EffectiveMembership, splitRoomsByMembership } from "../membership";
33+
import { EffectiveMembership, getEffectiveMembership, splitRoomsByMembership } from "../membership";
3434
import { OrderingAlgorithm } from "./list-ordering/OrderingAlgorithm";
3535
import { getListAlgorithmInstance } from "./list-ordering";
3636

@@ -99,6 +99,14 @@ export class Algorithm extends EventEmitter {
9999
return this._cachedRooms;
100100
}
101101

102+
/**
103+
* Awaitable version of the sticky room setter.
104+
* @param val The new room to sticky.
105+
*/
106+
public async setStickyRoomAsync(val: Room) {
107+
await this.updateStickyRoom(val);
108+
}
109+
102110
public getTagSorting(tagId: TagID): SortAlgorithm {
103111
return this.sortAlgorithms[tagId];
104112
}
@@ -160,10 +168,13 @@ export class Algorithm extends EventEmitter {
160168
// It's possible to have no selected room. In that case, clear the sticky room
161169
if (!val) {
162170
if (this._stickyRoom) {
171+
const stickyRoom = this._stickyRoom.room;
172+
this._stickyRoom = null; // clear before we go to update the algorithm
173+
163174
// Lie to the algorithm and re-add the room to the algorithm
164-
await this.handleRoomUpdate(this._stickyRoom.room, RoomUpdateCause.NewRoom);
175+
await this.handleRoomUpdate(stickyRoom, RoomUpdateCause.NewRoom);
176+
return;
165177
}
166-
this._stickyRoom = null;
167178
return;
168179
}
169180

@@ -289,6 +300,8 @@ export class Algorithm extends EventEmitter {
289300
}
290301

291302
protected recalculateFilteredRoomsForTag(tagId: TagID): void {
303+
if (!this.hasFilters) return; // don't bother doing work if there's nothing to do
304+
292305
console.log(`Recalculating filtered rooms for ${tagId}`);
293306
delete this.filteredRooms[tagId];
294307
const rooms = this.cachedRooms[tagId].map(r => r); // cheap clone
@@ -428,6 +441,13 @@ export class Algorithm extends EventEmitter {
428441
if (isNullOrUndefined(rooms)) throw new Error(`Array of rooms cannot be null`);
429442
if (!this.sortAlgorithms) throw new Error(`Cannot set known rooms without a tag sorting map`);
430443

444+
console.warn("Resetting known rooms, initiating regeneration");
445+
446+
// Before we go any further we need to clear (but remember) the sticky room to
447+
// avoid accidentally duplicating it in the list.
448+
const oldStickyRoom = this._stickyRoom;
449+
await this.updateStickyRoom(null);
450+
431451
this.rooms = rooms;
432452

433453
const newTags: ITagMap = {};
@@ -458,14 +478,7 @@ export class Algorithm extends EventEmitter {
458478

459479
// Now process all the joined rooms. This is a bit more complicated
460480
for (const room of memberships[EffectiveMembership.Join]) {
461-
let tags = Object.keys(room.tags || {});
462-
463-
if (tags.length === 0) {
464-
// Check to see if it's a DM if it isn't anything else
465-
if (DMRoomMap.shared().getUserIdForRoomId(room.roomId)) {
466-
tags = [DefaultTagID.DM];
467-
}
468-
}
481+
const tags = this.getTagsOfJoinedRoom(room);
469482

470483
let inTag = false;
471484
if (tags.length > 0) {
@@ -494,6 +507,54 @@ export class Algorithm extends EventEmitter {
494507

495508
this.cachedRooms = newTags;
496509
this.updateTagsFromCache();
510+
this.recalculateFilteredRooms();
511+
512+
// Now that we've finished generation, we need to update the sticky room to what
513+
// it was. It's entirely possible that it changed lists though, so if it did then
514+
// we also have to update the position of it.
515+
if (oldStickyRoom && oldStickyRoom.room) {
516+
await this.updateStickyRoom(oldStickyRoom.room);
517+
if (this._stickyRoom && this._stickyRoom.room) { // just in case the update doesn't go according to plan
518+
if (this._stickyRoom.tag !== oldStickyRoom.tag) {
519+
// We put the sticky room at the top of the list to treat it as an obvious tag change.
520+
this._stickyRoom.position = 0;
521+
this.recalculateStickyRoom(this._stickyRoom.tag);
522+
}
523+
}
524+
}
525+
}
526+
527+
private getTagsForRoom(room: Room): TagID[] {
528+
// XXX: This duplicates a lot of logic from setKnownRooms above, but has a slightly
529+
// different use case and therefore different performance curve
530+
531+
const tags: TagID[] = [];
532+
533+
const membership = getEffectiveMembership(room.getMyMembership());
534+
if (membership === EffectiveMembership.Invite) {
535+
tags.push(DefaultTagID.Invite);
536+
} else if (membership === EffectiveMembership.Leave) {
537+
tags.push(DefaultTagID.Archived);
538+
} else {
539+
tags.push(...this.getTagsOfJoinedRoom(room));
540+
}
541+
542+
if (!tags.length) tags.push(DefaultTagID.Untagged);
543+
544+
return tags;
545+
}
546+
547+
private getTagsOfJoinedRoom(room: Room): TagID[] {
548+
let tags = Object.keys(room.tags || {});
549+
550+
if (tags.length === 0) {
551+
// Check to see if it's a DM if it isn't anything else
552+
if (DMRoomMap.shared().getUserIdForRoomId(room.roomId)) {
553+
tags = [DefaultTagID.DM];
554+
}
555+
}
556+
557+
return tags;
497558
}
498559

499560
/**
@@ -548,6 +609,14 @@ export class Algorithm extends EventEmitter {
548609
public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<boolean> {
549610
if (!this.algorithms) throw new Error("Not ready: no algorithms to determine tags from");
550611

612+
if (cause === RoomUpdateCause.NewRoom) {
613+
const roomTags = this.roomIdsToTags[room.roomId];
614+
if (roomTags && roomTags.length > 0) {
615+
console.warn(`${room.roomId} is reportedly new but is already known - assuming TagChange instead`);
616+
cause = RoomUpdateCause.PossibleTagChange;
617+
}
618+
}
619+
551620
if (cause === RoomUpdateCause.PossibleTagChange) {
552621
// TODO: Be smarter and splice rather than regen the planet. https://github.com/vector-im/riot-web/issues/14035
553622
// TODO: No-op if no change. https://github.com/vector-im/riot-web/issues/14035
@@ -566,6 +635,19 @@ export class Algorithm extends EventEmitter {
566635
}
567636
}
568637

638+
if (cause === RoomUpdateCause.NewRoom && !this.roomIdsToTags[room.roomId]) {
639+
console.log(`[RoomListDebug] Updating tags for new room ${room.roomId} (${room.name})`);
640+
641+
// Get the tags for the room and populate the cache
642+
const roomTags = this.getTagsForRoom(room).filter(t => !isNullOrUndefined(this.cachedRooms[t]));
643+
644+
// "This should never happen" condition - we specify DefaultTagID.Untagged in getTagsForRoom(),
645+
// which means we should *always* have a tag to go off of.
646+
if (!roomTags.length) throw new Error(`Tags cannot be determined for ${room.roomId}`);
647+
648+
this.roomIdsToTags[room.roomId] = roomTags;
649+
}
650+
569651
let tags = this.roomIdsToTags[room.roomId];
570652
if (!tags) {
571653
console.warn(`No tags known for "${room.name}" (${room.roomId})`);

src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts

Lines changed: 39 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -179,45 +179,51 @@ export class ImportanceAlgorithm extends OrderingAlgorithm {
179179
}
180180

181181
public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<boolean> {
182-
if (cause === RoomUpdateCause.NewRoom || cause === RoomUpdateCause.RoomRemoved) {
183-
return this.handleSplice(room, cause);
184-
}
182+
try {
183+
await this.updateLock.acquireAsync();
185184

186-
if (cause !== RoomUpdateCause.Timeline && cause !== RoomUpdateCause.ReadReceipt) {
187-
throw new Error(`Unsupported update cause: ${cause}`);
188-
}
185+
if (cause === RoomUpdateCause.NewRoom || cause === RoomUpdateCause.RoomRemoved) {
186+
return this.handleSplice(room, cause);
187+
}
189188

190-
const category = this.getRoomCategory(room);
191-
if (this.sortingAlgorithm === SortAlgorithm.Manual) {
192-
return; // Nothing to do here.
193-
}
189+
if (cause !== RoomUpdateCause.Timeline && cause !== RoomUpdateCause.ReadReceipt) {
190+
throw new Error(`Unsupported update cause: ${cause}`);
191+
}
194192

195-
const roomIdx = this.getRoomIndex(room);
196-
if (roomIdx === -1) {
197-
throw new Error(`Room ${room.roomId} has no index in ${this.tagId}`);
198-
}
193+
const category = this.getRoomCategory(room);
194+
if (this.sortingAlgorithm === SortAlgorithm.Manual) {
195+
return; // Nothing to do here.
196+
}
199197

200-
// Try to avoid doing array operations if we don't have to: only move rooms within
201-
// the categories if we're jumping categories
202-
const oldCategory = this.getCategoryFromIndices(roomIdx, this.indices);
203-
if (oldCategory !== category) {
204-
// Move the room and update the indices
205-
this.moveRoomIndexes(1, oldCategory, category, this.indices);
206-
this.cachedOrderedRooms.splice(roomIdx, 1); // splice out the old index (fixed position)
207-
this.cachedOrderedRooms.splice(this.indices[category], 0, room); // splice in the new room (pre-adjusted)
208-
// Note: if moveRoomIndexes() is called after the splice then the insert operation
209-
// will happen in the wrong place. Because we would have already adjusted the index
210-
// for the category, we don't need to determine how the room is moving in the list.
211-
// If we instead tried to insert before updating the indices, we'd have to determine
212-
// whether the room was moving later (towards IDLE) or earlier (towards RED) from its
213-
// current position, as it'll affect the category's start index after we remove the
214-
// room from the array.
215-
}
198+
const roomIdx = this.getRoomIndex(room);
199+
if (roomIdx === -1) {
200+
throw new Error(`Room ${room.roomId} has no index in ${this.tagId}`);
201+
}
216202

217-
// Sort the category now that we've dumped the room in
218-
await this.sortCategory(category);
203+
// Try to avoid doing array operations if we don't have to: only move rooms within
204+
// the categories if we're jumping categories
205+
const oldCategory = this.getCategoryFromIndices(roomIdx, this.indices);
206+
if (oldCategory !== category) {
207+
// Move the room and update the indices
208+
this.moveRoomIndexes(1, oldCategory, category, this.indices);
209+
this.cachedOrderedRooms.splice(roomIdx, 1); // splice out the old index (fixed position)
210+
this.cachedOrderedRooms.splice(this.indices[category], 0, room); // splice in the new room (pre-adjusted)
211+
// Note: if moveRoomIndexes() is called after the splice then the insert operation
212+
// will happen in the wrong place. Because we would have already adjusted the index
213+
// for the category, we don't need to determine how the room is moving in the list.
214+
// If we instead tried to insert before updating the indices, we'd have to determine
215+
// whether the room was moving later (towards IDLE) or earlier (towards RED) from its
216+
// current position, as it'll affect the category's start index after we remove the
217+
// room from the array.
218+
}
219219

220-
return true; // change made
220+
// Sort the category now that we've dumped the room in
221+
await this.sortCategory(category);
222+
223+
return true; // change made
224+
} finally {
225+
await this.updateLock.release();
226+
}
221227
}
222228

223229
private async sortCategory(category: Category) {

0 commit comments

Comments
 (0)