Fix changing space sometimes bouncing to the wrong space (#7910)

This commit is contained in:
Michael Telatynski 2022-03-01 08:33:29 +00:00 committed by GitHub
parent 482d756bd0
commit 08c47ac473
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 77 additions and 11 deletions

View file

@ -69,7 +69,7 @@ export class SpaceWatcher {
if (!isMetaSpace(this.activeSpace)) { if (!isMetaSpace(this.activeSpace)) {
SpaceStore.instance.traverseSpace(this.activeSpace, roomId => { SpaceStore.instance.traverseSpace(this.activeSpace, roomId => {
this.store.matrixClient?.getRoom(roomId)?.loadMembersIfNeeded(); this.store.matrixClient?.getRoom(roomId)?.loadMembersIfNeeded();
}); }, false);
} }
this.filter.updateSpace(this.activeSpace); this.filter.updateSpace(this.activeSpace);
}; };

View file

@ -235,13 +235,9 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
return; return;
} }
this._activeSpace = space;
this.emit(UPDATE_SELECTED_SPACE, this.activeSpace);
this.emit(UPDATE_SUGGESTED_ROOMS, this._suggestedRooms = []);
if (contextSwitch) { if (contextSwitch) {
// view last selected room from space // view last selected room from space
const roomId = window.localStorage.getItem(getSpaceContextKey(this.activeSpace)); const roomId = window.localStorage.getItem(getSpaceContextKey(space));
// if the space being selected is an invite then always view that invite // if the space being selected is an invite then always view that invite
// else if the last viewed room in this space is joined then view that // else if the last viewed room in this space is joined then view that
@ -255,22 +251,31 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
room_id: roomId, room_id: roomId,
context_switch: true, context_switch: true,
metricsTrigger: "WebSpaceContextSwitch", metricsTrigger: "WebSpaceContextSwitch",
}); }, true);
} else if (cliSpace) { } else if (cliSpace) {
defaultDispatcher.dispatch<ViewRoomPayload>({ defaultDispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom, action: Action.ViewRoom,
room_id: space, room_id: space,
context_switch: true, context_switch: true,
metricsTrigger: "WebSpaceContextSwitch", metricsTrigger: "WebSpaceContextSwitch",
}); }, true);
} else { } else {
defaultDispatcher.dispatch<ViewHomePagePayload>({ defaultDispatcher.dispatch<ViewHomePagePayload>({
action: Action.ViewHomePage, action: Action.ViewHomePage,
context_switch: true, context_switch: true,
}); }, true);
} }
} }
// We can set the space after context switching as the dispatch handler which stores the last viewed room
// specifically no-ops on context_switch=true.
this._activeSpace = space;
// Emit after a synchronous dispatch for context switching to prevent racing with SpaceWatcher calling
// Room::loadMembersIfNeeded which could (via onMemberUpdate) call upon switchSpaceIfNeeded causing the
// space to wrongly bounce.
this.emit(UPDATE_SELECTED_SPACE, this.activeSpace);
this.emit(UPDATE_SUGGESTED_ROOMS, this._suggestedRooms = []);
// persist space selected // persist space selected
window.localStorage.setItem(ACTIVE_SPACE_LS_KEY, space); window.localStorage.setItem(ACTIVE_SPACE_LS_KEY, space);

View file

@ -16,9 +16,10 @@ limitations under the License.
import { EventType } from "matrix-js-sdk/src/@types/event"; import { EventType } from "matrix-js-sdk/src/@types/event";
import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
import { defer } from "matrix-js-sdk/src/utils";
import "../skinned-sdk"; // Must be first for skinning to work import "../skinned-sdk"; // Must be first for skinning to work
import SpaceStore from "../../src/stores/spaces/SpaceStore"; import SpaceStore from "../../src/stores/spaces/SpaceStore";
import { import {
MetaSpace, MetaSpace,
@ -30,11 +31,11 @@ import {
import * as testUtils from "../test-utils"; import * as testUtils from "../test-utils";
import { mkEvent, stubClient } from "../test-utils"; import { mkEvent, stubClient } from "../test-utils";
import DMRoomMap from "../../src/utils/DMRoomMap"; import DMRoomMap from "../../src/utils/DMRoomMap";
import { MatrixClientPeg } from "../../src/MatrixClientPeg";
import defaultDispatcher from "../../src/dispatcher/dispatcher"; import defaultDispatcher from "../../src/dispatcher/dispatcher";
import SettingsStore from "../../src/settings/SettingsStore"; import SettingsStore from "../../src/settings/SettingsStore";
import { SettingLevel } from "../../src/settings/SettingLevel"; import { SettingLevel } from "../../src/settings/SettingLevel";
import { Action } from "../../src/dispatcher/actions"; import { Action } from "../../src/dispatcher/actions";
import { MatrixClientPeg } from "../../src/MatrixClientPeg";
jest.useFakeTimers(); jest.useFakeTimers();
@ -92,6 +93,8 @@ describe("SpaceStore", () => {
const store = SpaceStore.instance; const store = SpaceStore.instance;
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
const spyDispatcher = jest.spyOn(defaultDispatcher, "dispatch");
let rooms = []; let rooms = [];
const mkRoom = (roomId: string) => testUtils.mkRoom(client, roomId, rooms); const mkRoom = (roomId: string) => testUtils.mkRoom(client, roomId, rooms);
const mkSpace = (spaceId: string, children: string[] = []) => testUtils.mkSpace(client, spaceId, rooms, children); const mkSpace = (spaceId: string, children: string[] = []) => testUtils.mkSpace(client, spaceId, rooms, children);
@ -122,6 +125,8 @@ describe("SpaceStore", () => {
[MetaSpace.People]: true, [MetaSpace.People]: true,
[MetaSpace.Orphans]: true, [MetaSpace.Orphans]: true,
}); });
spyDispatcher.mockClear();
}); });
afterEach(async () => { afterEach(async () => {
@ -842,6 +847,61 @@ describe("SpaceStore", () => {
}); });
}); });
it("does not race with lazy loading", async () => {
store.setActiveSpace(MetaSpace.Home);
mkRoom(room1);
const space = mkSpace(space1, [room1]);
// seed the context for space1 to be room1
window.localStorage.setItem(`mx_space_context_${space1}`, room1);
await run();
const deferred = defer<void>();
(space.loadMembersIfNeeded as jest.Mock).mockImplementation(() => {
const event = mkEvent({
event: true,
type: EventType.RoomMember,
content: { membership: "join" },
skey: dm1Partner.userId,
user: dm1Partner.userId,
room: space1,
});
client.emit(RoomStateEvent.Members, event, null, null);
deferred.resolve();
});
spyDispatcher.mockClear();
const getCurrentRoom = () => {
for (let i = spyDispatcher.mock.calls.length - 1; i >= 0; i--) {
if (spyDispatcher.mock.calls[i][0].action === Action.ViewRoom) {
return spyDispatcher.mock.calls[i][0]["room_id"];
}
}
};
// set up space with LL where loadMembersIfNeeded emits membership events which trip switchSpaceIfNeeded
expect(space.loadMembersIfNeeded).not.toHaveBeenCalled();
store.setActiveSpace(space1, true);
// traverse the space and call loadMembersIfNeeded, similarly to SpaceWatcher's behaviour
store.traverseSpace(space1, roomId => {
client.getRoom(roomId)?.loadMembersIfNeeded();
}, false);
expect(store.activeSpace).toBe(space1);
expect(getCurrentRoom()).toBe(room1);
await deferred.promise;
expect(store.activeSpace).toBe(space1);
expect(getCurrentRoom()).toBe(room1);
jest.runAllTimers();
expect(store.activeSpace).toBe(space1);
expect(getCurrentRoom()).toBe(room1);
});
describe("context switching tests", () => { describe("context switching tests", () => {
let dispatcherRef; let dispatcherRef;
let currentRoom = null; let currentRoom = null;

View file

@ -349,6 +349,7 @@ export function mkStubRoom(roomId = null, name: string, client: MatrixClient): R
getAltAliases: jest.fn().mockReturnValue([]), getAltAliases: jest.fn().mockReturnValue([]),
timeline: [], timeline: [],
getJoinRule: jest.fn().mockReturnValue("invite"), getJoinRule: jest.fn().mockReturnValue("invite"),
loadMembersIfNeeded: jest.fn(),
client, client,
} as unknown as Room; } as unknown as Room;
} }