diff --git a/spec/integ/matrix-client-syncing.spec.js b/spec/integ/matrix-client-syncing.spec.js index 6adb35a50c0..7c33f0948ea 100644 --- a/spec/integ/matrix-client-syncing.spec.js +++ b/spec/integ/matrix-client-syncing.spec.js @@ -1,5 +1,20 @@ -import { MatrixEvent } from "../../src/models/event"; -import { EventTimeline } from "../../src/models/event-timeline"; +/* +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 { EventTimeline, MatrixEvent, RoomEvent } from "../../src"; import * as utils from "../test-utils/test-utils"; import { TestClient } from "../TestClient"; @@ -60,6 +75,112 @@ describe("MatrixClient syncing", function() { done(); }); }); + + it("should emit Room.myMembership for invite->leave->invite cycles", async () => { + const roomId = "!cycles:example.org"; + + // First sync: an invite + const inviteSyncRoomSection = { + invite: { + [roomId]: { + invite_state: { + events: [{ + type: "m.room.member", + state_key: selfUserId, + content: { + membership: "invite", + }, + }], + }, + }, + }, + }; + httpBackend.when("GET", "/sync").respond(200, { + ...syncData, + rooms: inviteSyncRoomSection, + }); + + // Second sync: a leave (reject of some kind) + httpBackend.when("POST", "/leave").respond(200, {}); + httpBackend.when("GET", "/sync").respond(200, { + ...syncData, + rooms: { + leave: { + [roomId]: { + account_data: { events: [] }, + ephemeral: { events: [] }, + state: { + events: [{ + type: "m.room.member", + state_key: selfUserId, + content: { + membership: "leave", + }, + prev_content: { + membership: "invite", + }, + // XXX: And other fields required on an event + }], + }, + timeline: { + limited: false, + events: [{ + type: "m.room.member", + state_key: selfUserId, + content: { + membership: "leave", + }, + prev_content: { + membership: "invite", + }, + // XXX: And other fields required on an event + }], + }, + }, + }, + }, + }); + + // Third sync: another invite + httpBackend.when("GET", "/sync").respond(200, { + ...syncData, + rooms: inviteSyncRoomSection, + }); + + // First fire: an initial invite + let fires = 0; + client.once(RoomEvent.MyMembership, (room, membership, oldMembership) => { // Room, string, string + fires++; + expect(room.roomId).toBe(roomId); + expect(membership).toBe("invite"); + expect(oldMembership).toBeFalsy(); + + // Second fire: a leave + client.once(RoomEvent.MyMembership, (room, membership, oldMembership) => { + fires++; + expect(room.roomId).toBe(roomId); + expect(membership).toBe("leave"); + expect(oldMembership).toBe("invite"); + + // Third/final fire: a second invite + client.once(RoomEvent.MyMembership, (room, membership, oldMembership) => { + fires++; + expect(room.roomId).toBe(roomId); + expect(membership).toBe("invite"); + expect(oldMembership).toBe("leave"); + }); + }); + + // For maximum safety, "leave" the room after we register the handler + client.leave(roomId); + }); + + // noinspection ES6MissingAwait + client.startClient(); + await httpBackend.flushAllExpected(); + + expect(fires).toBe(3); + }); }); describe("resolving invites to profile info", function() { diff --git a/src/sync.ts b/src/sync.ts index eee27af170f..b7f03400bfa 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -1166,6 +1166,9 @@ export class SyncApi { room.recalculate(); client.store.storeRoom(room); client.emit(ClientEvent.Room, room); + } else { + // Update room state for invite->reject->invite cycles + room.recalculate(); } stateEvents.forEach(function(e) { client.emit(ClientEvent.Event, e);