diff --git a/spec/integ/matrix-client-event-emitter.spec.ts b/spec/integ/matrix-client-event-emitter.spec.ts index 1ad244b54..d0050e77e 100644 --- a/spec/integ/matrix-client-event-emitter.spec.ts +++ b/spec/integ/matrix-client-event-emitter.spec.ts @@ -201,7 +201,7 @@ describe("MatrixClient events", function() { }); client!.on(RoomEvent.Timeline, function(event, room) { timelineFireCount++; - expect(room.roomId).toEqual("!erufh:bar"); + expect(room?.roomId).toEqual("!erufh:bar"); }); client!.on(RoomEvent.Name, function(room) { roomNameInvokeCount++; diff --git a/spec/integ/matrix-client-event-timeline.spec.ts b/spec/integ/matrix-client-event-timeline.spec.ts index f2bfa5f6a..da089d768 100644 --- a/spec/integ/matrix-client-event-timeline.spec.ts +++ b/spec/integ/matrix-client-event-timeline.spec.ts @@ -368,7 +368,7 @@ describe("MatrixClient event timelines", function() { expect(tl!.getEvents().length).toEqual(4); for (let i = 0; i < 4; i++) { expect(tl!.getEvents()[i].event).toEqual(EVENTS[i]); - expect(tl!.getEvents()[i]?.sender.name).toEqual(userName); + expect(tl!.getEvents()[i]?.sender?.name).toEqual(userName); } expect(tl!.getPaginationToken(EventTimeline.BACKWARDS)) .toEqual("start_token"); @@ -406,7 +406,7 @@ describe("MatrixClient event timelines", function() { }).then(function(tl) { expect(tl!.getEvents().length).toEqual(2); expect(tl!.getEvents()[1].event).toEqual(EVENTS[0]); - expect(tl!.getEvents()[1]?.sender.name).toEqual(userName); + expect(tl!.getEvents()[1]?.sender?.name).toEqual(userName); expect(tl!.getPaginationToken(EventTimeline.BACKWARDS)) .toEqual("f_1_1"); // expect(tl.getPaginationToken(EventTimeline.FORWARDS)) @@ -767,8 +767,8 @@ describe("MatrixClient event timelines", function() { httpBackend = testClient.httpBackend; await startClient(httpBackend, client); - const room = client.getRoom(roomId); - const timelineSet = room.getTimelineSets()[0]; + const room = client.getRoom(roomId)!; + const timelineSet = room.getTimelineSets()[0]!; await expect(client.getLatestTimeline(timelineSet)).rejects.toBeTruthy(); }); @@ -786,7 +786,7 @@ describe("MatrixClient event timelines", function() { httpBackend = testClient.httpBackend; return startClient(httpBackend, client).then(() => { - const room = client.getRoom(roomId); + const room = client.getRoom(roomId)!; const timelineSet = room.getTimelineSets()[0]; expect(client.getLatestTimeline(timelineSet)).rejects.toBeFalsy(); }); @@ -849,7 +849,7 @@ describe("MatrixClient event timelines", function() { expect(tl!.getEvents().length).toEqual(4); for (let i = 0; i < 4; i++) { expect(tl!.getEvents()[i].event).toEqual(EVENTS[i]); - expect(tl!.getEvents()[i]?.sender.name).toEqual(userName); + expect(tl!.getEvents()[i]?.sender?.name).toEqual(userName); } expect(tl!.getPaginationToken(EventTimeline.BACKWARDS)) .toEqual("start_token"); @@ -1129,8 +1129,8 @@ describe("MatrixClient event timelines", function() { }); it("should allow fetching all threads", async function() { - const room = client.getRoom(roomId); - const timelineSets = await room?.createThreadsTimelineSets(); + const room = client.getRoom(roomId)!; + const timelineSets = await room.createThreadsTimelineSets(); expect(timelineSets).not.toBeNull(); respondToThreads(); respondToThreads(); @@ -1185,14 +1185,14 @@ describe("MatrixClient event timelines", function() { }); it("should allow fetching all threads", async function() { - const room = client.getRoom(roomId); + const room = client.getRoom(roomId)!; respondToFilter(); respondToSync(); respondToFilter(); respondToSync(); - const timelineSetsPromise = room?.createThreadsTimelineSets(); + const timelineSetsPromise = room.createThreadsTimelineSets(); expect(timelineSetsPromise).not.toBeNull(); await flushHttp(timelineSetsPromise!); respondToFilter(); @@ -1218,7 +1218,7 @@ describe("MatrixClient event timelines", function() { const [allThreads] = timelineSets!; respondToThreads().check((request) => { - expect(request.queryParams.filter).toEqual(JSON.stringify({ + expect(request.queryParams?.filter).toEqual(JSON.stringify({ "lazy_load_members": true, })); }); @@ -1244,7 +1244,7 @@ describe("MatrixClient event timelines", function() { state: [], next_batch: null, }).check((request) => { - expect(request.queryParams.from).toEqual(RANDOM_TOKEN); + expect(request.queryParams?.from).toEqual(RANDOM_TOKEN); }); allThreads.getLiveTimeline().setPaginationToken(RANDOM_TOKEN, Direction.Backward); diff --git a/spec/integ/matrix-client-room-timeline.spec.ts b/spec/integ/matrix-client-room-timeline.spec.ts index 42d90d91c..f89ab04e2 100644 --- a/spec/integ/matrix-client-room-timeline.spec.ts +++ b/spec/integ/matrix-client-room-timeline.spec.ts @@ -160,8 +160,8 @@ describe("MatrixClient room timelines", function() { expect(room.timeline[1].status).toEqual(EventStatus.SENDING); // check member const member = room.timeline[1].sender; - expect(member.userId).toEqual(userId); - expect(member.name).toEqual(userName); + expect(member?.userId).toEqual(userId); + expect(member?.name).toEqual(userName); httpBackend!.flush("/sync", 1).then(function() { done(); @@ -327,11 +327,11 @@ describe("MatrixClient room timelines", function() { client!.scrollback(room).then(function() { expect(room.timeline.length).toEqual(5); const joinMsg = room.timeline[0]; - expect(joinMsg.sender.name).toEqual("Old Alice"); + expect(joinMsg.sender?.name).toEqual("Old Alice"); const oldMsg = room.timeline[1]; - expect(oldMsg.sender.name).toEqual("Old Alice"); + expect(oldMsg.sender?.name).toEqual("Old Alice"); const newMsg = room.timeline[3]; - expect(newMsg.sender.name).toEqual(userName); + expect(newMsg.sender?.name).toEqual(userName); // still have a sync to flush httpBackend!.flush("/sync", 1).then(() => { @@ -468,8 +468,8 @@ describe("MatrixClient room timelines", function() { ]).then(function() { const preNameEvent = room.timeline[room.timeline.length - 3]; const postNameEvent = room.timeline[room.timeline.length - 1]; - expect(preNameEvent.sender.name).toEqual(userName); - expect(postNameEvent.sender.name).toEqual("New Name"); + expect(preNameEvent.sender?.name).toEqual(userName); + expect(postNameEvent.sender?.name).toEqual("New Name"); }); }); }); diff --git a/spec/integ/matrix-client-syncing.spec.ts b/spec/integ/matrix-client-syncing.spec.ts index fd8fd7b7d..9eab95077 100644 --- a/spec/integ/matrix-client-syncing.spec.ts +++ b/spec/integ/matrix-client-syncing.spec.ts @@ -1276,9 +1276,9 @@ describe("MatrixClient syncing", () => { client!.on(RoomEvent.TimelineReset, (room) => { resetCallCount++; - const tl = room.getLiveTimeline(); - expect(tl.getEvents().length).toEqual(0); - const tok = tl.getPaginationToken(EventTimeline.BACKWARDS); + const tl = room?.getLiveTimeline(); + expect(tl?.getEvents().length).toEqual(0); + const tok = tl?.getPaginationToken(EventTimeline.BACKWARDS); expect(tok).toEqual("newerTok"); }); diff --git a/spec/unit/filter.spec.ts b/spec/unit/filter.spec.ts index e246ec1a2..dbeb932e6 100644 --- a/spec/unit/filter.spec.ts +++ b/spec/unit/filter.spec.ts @@ -1,5 +1,7 @@ import { UNREAD_THREAD_NOTIFICATIONS } from "../../src/@types/sync"; import { Filter, IFilterDefinition } from "../../src/filter"; +import { mkEvent } from "../test-utils/test-utils"; +import { EventType } from "../../src"; describe("Filter", function() { const filterId = "f1lt3ring15g00d4ursoul"; @@ -57,4 +59,27 @@ describe("Filter", function() { }); }); }); + + describe("filterRoomTimeline", () => { + it("should return input if no roomTimelineFilter and roomFilter", () => { + const events = [mkEvent({ type: EventType.Sticker, content: {}, event: true })]; + expect(new Filter(undefined).filterRoomTimeline(events)).toStrictEqual(events); + }); + + it("should filter using components when present", () => { + const definition: IFilterDefinition = { + room: { + timeline: { + types: [EventType.Sticker], + }, + }, + }; + const filter = Filter.fromJson(userId, filterId, definition); + const events = [ + mkEvent({ type: EventType.Sticker, content: {}, event: true }), + mkEvent({ type: EventType.RoomMessage, content: {}, event: true }), + ]; + expect(filter.filterRoomTimeline(events)).toStrictEqual([events[0]]); + }); + }); }); diff --git a/spec/unit/interactive-auth.spec.ts b/spec/unit/interactive-auth.spec.ts index ba3eef89b..098cadad5 100644 --- a/spec/unit/interactive-auth.spec.ts +++ b/spec/unit/interactive-auth.spec.ts @@ -259,7 +259,6 @@ describe("InteractiveAuth", () => { const requestEmailToken = jest.fn(); const ia = new InteractiveAuth({ - authData: null, matrixClient: getFakeClient(), stateUpdated, doRequest, diff --git a/spec/unit/models/event.spec.ts b/spec/unit/models/event.spec.ts index 7f2b26313..674fd3258 100644 --- a/spec/unit/models/event.spec.ts +++ b/spec/unit/models/event.spec.ts @@ -14,7 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { MatrixEvent } from "../../../src/models/event"; +import { MatrixEvent, MatrixEventEvent } from "../../../src/models/event"; +import { emitPromise } from "../../test-utils/test-utils"; +import { EventType } from "../../../src"; +import { Crypto } from "../../../src/crypto"; describe('MatrixEvent', () => { it('should create copies of itself', () => { @@ -84,4 +87,36 @@ describe('MatrixEvent', () => { expect(ev.getWireContent().body).toBeUndefined(); expect(ev.getWireContent().ciphertext).toBeUndefined(); }); + + it("should abort decryption if fails with an error other than a DecryptionError", async () => { + const ev = new MatrixEvent({ + type: EventType.RoomMessageEncrypted, + content: { + body: "Test", + }, + event_id: "$event1:server", + }); + await ev.attemptDecryption({ + decryptEvent: jest.fn().mockRejectedValue(new Error("Not a DecryptionError")), + } as unknown as Crypto); + expect(ev.isEncrypted()).toBeTruthy(); + expect(ev.isBeingDecrypted()).toBeFalsy(); + expect(ev.isDecryptionFailure()).toBeFalsy(); + }); + + describe("applyVisibilityEvent", () => { + it("should emit VisibilityChange if a change was made", async () => { + const ev = new MatrixEvent({ + type: "m.room.message", + content: { + body: "Test", + }, + event_id: "$event1:server", + }); + + const prom = emitPromise(ev, MatrixEventEvent.VisibilityChange); + ev.applyVisibilityEvent({ visible: false, eventId: ev.getId(), reason: null }); + await prom; + }); + }); }); diff --git a/spec/unit/room-member.spec.ts b/spec/unit/room-member.spec.ts index e435cca22..8eb3096b6 100644 --- a/spec/unit/room-member.spec.ts +++ b/spec/unit/room-member.spec.ts @@ -16,7 +16,7 @@ limitations under the License. import * as utils from "../test-utils/test-utils"; import { RoomMember, RoomMemberEvent } from "../../src/models/room-member"; -import { RoomState } from "../../src"; +import { EventType, RoomState } from "../../src"; describe("RoomMember", function() { const roomId = "!foo:bar"; @@ -142,33 +142,72 @@ describe("RoomMember", function() { expect(emitCount).toEqual(1); }); - it("should not honor string power levels.", - function() { - const event = utils.mkEvent({ - type: "m.room.power_levels", - room: roomId, - user: userA, - content: { - users_default: 20, - users: { - "@alice:bar": "5", - }, + it("should not honor string power levels.", function() { + const event = utils.mkEvent({ + type: "m.room.power_levels", + room: roomId, + user: userA, + content: { + users_default: 20, + users: { + "@alice:bar": "5", }, - event: true, - }); - let emitCount = 0; - - member.on(RoomMemberEvent.PowerLevel, function(emitEvent, emitMember) { - emitCount += 1; - expect(emitMember.userId).toEqual('@alice:bar'); - expect(emitMember.powerLevel).toEqual(20); - expect(emitEvent).toEqual(event); - }); - - member.setPowerLevelEvent(event); - expect(member.powerLevel).toEqual(20); - expect(emitCount).toEqual(1); + }, + event: true, }); + let emitCount = 0; + + member.on(RoomMemberEvent.PowerLevel, function(emitEvent, emitMember) { + emitCount += 1; + expect(emitMember.userId).toEqual('@alice:bar'); + expect(emitMember.powerLevel).toEqual(20); + expect(emitEvent).toEqual(event); + }); + + member.setPowerLevelEvent(event); + expect(member.powerLevel).toEqual(20); + expect(emitCount).toEqual(1); + }); + + it("should no-op if given a non-state or unrelated event", () => { + const fn = jest.spyOn(member, "emit"); + expect(fn).not.toHaveBeenCalledWith(RoomMemberEvent.PowerLevel); + member.setPowerLevelEvent(utils.mkEvent({ + type: EventType.RoomPowerLevels, + room: roomId, + user: userA, + content: { + users_default: 20, + users: { + "@alice:bar": "5", + }, + }, + skey: "invalid", + event: true, + })); + const nonStateEv = utils.mkEvent({ + type: EventType.RoomPowerLevels, + room: roomId, + user: userA, + content: { + users_default: 20, + users: { + "@alice:bar": "5", + }, + }, + event: true, + }); + delete nonStateEv.event.state_key; + member.setPowerLevelEvent(nonStateEv); + member.setPowerLevelEvent(utils.mkEvent({ + type: EventType.Sticker, + room: roomId, + user: userA, + content: {}, + event: true, + })); + expect(fn).not.toHaveBeenCalledWith(RoomMemberEvent.PowerLevel); + }); }); describe("setTypingEvent", function() { @@ -234,6 +273,79 @@ describe("RoomMember", function() { }); }); + describe("isKicked", () => { + it("should return false if membership is not `leave`", () => { + const member1 = new RoomMember(roomId, userA); + member1.membership = "join"; + expect(member1.isKicked()).toBeFalsy(); + + const member2 = new RoomMember(roomId, userA); + member2.membership = "invite"; + expect(member2.isKicked()).toBeFalsy(); + + const member3 = new RoomMember(roomId, userA); + expect(member3.isKicked()).toBeFalsy(); + }); + + it("should return false if the membership event is unknown", () => { + const member = new RoomMember(roomId, userA); + member.membership = "leave"; + expect(member.isKicked()).toBeFalsy(); + }); + + it("should return false if the member left of their own accord", () => { + const member = new RoomMember(roomId, userA); + member.membership = "leave"; + member.events.member = utils.mkMembership({ + event: true, + sender: userA, + mship: "leave", + skey: userA, + }); + expect(member.isKicked()).toBeFalsy(); + }); + + it("should return true if the member's leave was sent by another user", () => { + const member = new RoomMember(roomId, userA); + member.membership = "leave"; + member.events.member = utils.mkMembership({ + event: true, + sender: userB, + mship: "leave", + skey: userA, + }); + expect(member.isKicked()).toBeTruthy(); + }); + }); + + describe("getDMInviter", () => { + it("should return userId of the sender of the invite if is_direct=true", () => { + const member = new RoomMember(roomId, userA); + member.membership = "invite"; + member.events.member = utils.mkMembership({ + event: true, + sender: userB, + mship: "invite", + skey: userA, + }); + member.events.member.event.content!.is_direct = true; + expect(member.getDMInviter()).toBe(userB); + }); + + it("should not return userId of the sender of the invite if is_direct=false", () => { + const member = new RoomMember(roomId, userA); + member.membership = "invite"; + member.events.member = utils.mkMembership({ + event: true, + sender: userB, + mship: "invite", + skey: userA, + }); + member.events.member.event.content!.is_direct = false; + expect(member.getDMInviter()).toBeUndefined(); + }); + }); + describe("setMembershipEvent", function() { const joinEvent = utils.mkMembership({ event: true, diff --git a/spec/unit/room-state.spec.ts b/spec/unit/room-state.spec.ts index 858bed80c..a990d6294 100644 --- a/spec/unit/room-state.spec.ts +++ b/spec/unit/room-state.spec.ts @@ -303,92 +303,92 @@ describe("RoomState", function() { state.setStateEvents(events, { timelineWasEmpty: true }); expect(emitCount).toEqual(1); }); + }); - describe('beacon events', () => { - it('adds new beacon info events to state and emits', () => { - const beaconEvent = makeBeaconInfoEvent(userA, roomId); - const emitSpy = jest.spyOn(state, 'emit'); + describe('beacon events', () => { + it('adds new beacon info events to state and emits', () => { + const beaconEvent = makeBeaconInfoEvent(userA, roomId); + const emitSpy = jest.spyOn(state, 'emit'); - state.setStateEvents([beaconEvent]); + state.setStateEvents([beaconEvent]); - expect(state.beacons.size).toEqual(1); - const beaconInstance = state.beacons.get(`${roomId}_${userA}`); - expect(beaconInstance).toBeTruthy(); - expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.New, beaconEvent, beaconInstance); - }); + expect(state.beacons.size).toEqual(1); + const beaconInstance = state.beacons.get(`${roomId}_${userA}`); + expect(beaconInstance).toBeTruthy(); + expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.New, beaconEvent, beaconInstance); + }); - it('does not add redacted beacon info events to state', () => { - const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId); - const redactionEvent = new MatrixEvent({ type: 'm.room.redaction' }); - redactedBeaconEvent.makeRedacted(redactionEvent); - const emitSpy = jest.spyOn(state, 'emit'); + it('does not add redacted beacon info events to state', () => { + const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId); + const redactionEvent = new MatrixEvent({ type: 'm.room.redaction' }); + redactedBeaconEvent.makeRedacted(redactionEvent); + const emitSpy = jest.spyOn(state, 'emit'); - state.setStateEvents([redactedBeaconEvent]); + state.setStateEvents([redactedBeaconEvent]); - // no beacon added - expect(state.beacons.size).toEqual(0); - expect(state.beacons.get(getBeaconInfoIdentifier(redactedBeaconEvent))).toBeFalsy(); - // no new beacon emit - expect(filterEmitCallsByEventType(BeaconEvent.New, emitSpy).length).toBeFalsy(); - }); + // no beacon added + expect(state.beacons.size).toEqual(0); + expect(state.beacons.get(getBeaconInfoIdentifier(redactedBeaconEvent))).toBeFalsy(); + // no new beacon emit + expect(filterEmitCallsByEventType(BeaconEvent.New, emitSpy).length).toBeFalsy(); + }); - it('updates existing beacon info events in state', () => { - const beaconId = '$beacon1'; - const beaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId); - const updatedBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: false }, beaconId); + it('updates existing beacon info events in state', () => { + const beaconId = '$beacon1'; + const beaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId); + const updatedBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: false }, beaconId); - state.setStateEvents([beaconEvent]); - const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beaconEvent)); - expect(beaconInstance?.isLive).toEqual(true); + state.setStateEvents([beaconEvent]); + const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beaconEvent)); + expect(beaconInstance?.isLive).toEqual(true); - state.setStateEvents([updatedBeaconEvent]); + state.setStateEvents([updatedBeaconEvent]); - // same Beacon - expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent))).toBe(beaconInstance); - // updated liveness - expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent))?.isLive).toEqual(false); - }); + // same Beacon + expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent))).toBe(beaconInstance); + // updated liveness + expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent))?.isLive).toEqual(false); + }); - it('destroys and removes redacted beacon events', () => { - const beaconId = '$beacon1'; - const beaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId); - const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId); - const redactionEvent = new MatrixEvent({ type: 'm.room.redaction', redacts: beaconEvent.getId() }); - redactedBeaconEvent.makeRedacted(redactionEvent); + it('destroys and removes redacted beacon events', () => { + const beaconId = '$beacon1'; + const beaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId); + const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId); + const redactionEvent = new MatrixEvent({ type: 'm.room.redaction', redacts: beaconEvent.getId() }); + redactedBeaconEvent.makeRedacted(redactionEvent); - state.setStateEvents([beaconEvent]); - const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beaconEvent)); - const destroySpy = jest.spyOn(beaconInstance as Beacon, 'destroy'); - expect(beaconInstance?.isLive).toEqual(true); + state.setStateEvents([beaconEvent]); + const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beaconEvent)); + const destroySpy = jest.spyOn(beaconInstance as Beacon, 'destroy'); + expect(beaconInstance?.isLive).toEqual(true); - state.setStateEvents([redactedBeaconEvent]); + state.setStateEvents([redactedBeaconEvent]); - expect(destroySpy).toHaveBeenCalled(); - expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent))).toBe(undefined); - }); + expect(destroySpy).toHaveBeenCalled(); + expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent))).toBe(undefined); + }); - it('updates live beacon ids once after setting state events', () => { - const liveBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, '$beacon1'); - const deadBeaconEvent = makeBeaconInfoEvent(userB, roomId, { isLive: false }, '$beacon2'); + it('updates live beacon ids once after setting state events', () => { + const liveBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, '$beacon1'); + const deadBeaconEvent = makeBeaconInfoEvent(userB, roomId, { isLive: false }, '$beacon2'); - const emitSpy = jest.spyOn(state, 'emit'); + const emitSpy = jest.spyOn(state, 'emit'); - state.setStateEvents([liveBeaconEvent, deadBeaconEvent]); + state.setStateEvents([liveBeaconEvent, deadBeaconEvent]); - // called once - expect(filterEmitCallsByEventType(RoomStateEvent.BeaconLiveness, emitSpy).length).toBe(1); + // called once + expect(filterEmitCallsByEventType(RoomStateEvent.BeaconLiveness, emitSpy).length).toBe(1); - // live beacon is now not live - const updatedLiveBeaconEvent = makeBeaconInfoEvent( - userA, roomId, { isLive: false }, liveBeaconEvent.getId(), - ); + // live beacon is now not live + const updatedLiveBeaconEvent = makeBeaconInfoEvent( + userA, roomId, { isLive: false }, liveBeaconEvent.getId(), + ); - state.setStateEvents([updatedLiveBeaconEvent]); + state.setStateEvents([updatedLiveBeaconEvent]); - expect(state.hasLiveBeacons).toBe(false); - expect(filterEmitCallsByEventType(RoomStateEvent.BeaconLiveness, emitSpy).length).toBe(3); - expect(emitSpy).toHaveBeenCalledWith(RoomStateEvent.BeaconLiveness, state, false); - }); + expect(state.hasLiveBeacons).toBe(false); + expect(filterEmitCallsByEventType(RoomStateEvent.BeaconLiveness, emitSpy).length).toBe(3); + expect(emitSpy).toHaveBeenCalledWith(RoomStateEvent.BeaconLiveness, state, false); }); }); @@ -1007,4 +1007,20 @@ describe("RoomState", function() { }); }); }); + + describe("mayClientSendStateEvent", () => { + it("should return false if the user isn't authenticated", () => { + expect(state.mayClientSendStateEvent("m.room.message", { + isGuest: jest.fn().mockReturnValue(false), + credentials: {}, + } as unknown as MatrixClient)).toBeFalsy(); + }); + + it("should return false if the user is a guest", () => { + expect(state.mayClientSendStateEvent("m.room.message", { + isGuest: jest.fn().mockReturnValue(true), + credentials: { userId: userA }, + } as unknown as MatrixClient)).toBeFalsy(); + }); + }); }); diff --git a/spec/unit/room.spec.ts b/spec/unit/room.spec.ts index 902437e0b..f2c7aee2b 100644 --- a/spec/unit/room.spec.ts +++ b/spec/unit/room.spec.ts @@ -27,6 +27,7 @@ import { EventType, JoinRule, MatrixEvent, + MatrixEventEvent, PendingEventOrdering, RelationType, RoomEvent, @@ -40,6 +41,7 @@ import { emitPromise } from "../test-utils/test-utils"; import { ReceiptType } from "../../src/@types/read_receipts"; import { FeatureSupport, Thread, ThreadEvent } from "../../src/models/thread"; import { WrappedReceipt } from "../../src/models/read-receipt"; +import { Crypto } from "../../src/crypto"; describe("Room", function() { const roomId = "!foo:bar"; @@ -337,12 +339,12 @@ describe("Room", function() { expect(event.getId()).toEqual(localEventId); expect(event.status).toEqual(EventStatus.SENDING); expect(emitRoom).toEqual(room); - expect(oldEventId).toBe(null); - expect(oldStatus).toBe(null); + expect(oldEventId).toBeUndefined(); + expect(oldStatus).toBeUndefined(); break; case 1: expect(event.getId()).toEqual(remoteEventId); - expect(event.status).toBe(null); + expect(event.status).toBeNull(); expect(emitRoom).toEqual(room); expect(oldEventId).toEqual(localEventId); expect(oldStatus).toBe(EventStatus.SENDING); @@ -371,7 +373,7 @@ describe("Room", function() { delete eventJson["event_id"]; const localEvent = new MatrixEvent(Object.assign({ event_id: "$temp" }, eventJson)); localEvent.status = EventStatus.SENDING; - expect(localEvent.getTxnId()).toBeNull(); + expect(localEvent.getTxnId()).toBeUndefined(); expect(room.timeline.length).toEqual(0); // first add the local echo. This is done before the /send request is even sent. @@ -386,7 +388,7 @@ describe("Room", function() { // then /sync returns the remoteEvent, it should de-dupe based on the event ID. const remoteEvent = new MatrixEvent(Object.assign({ event_id: realEventId }, eventJson)); - expect(remoteEvent.getTxnId()).toBeNull(); + expect(remoteEvent.getTxnId()).toBeUndefined(); room.addLiveEvents([remoteEvent]); // the duplicate strategy code should ensure we don't add a 2nd event to the live timeline expect(room.timeline.length).toEqual(1); @@ -1535,6 +1537,36 @@ describe("Room", function() { [eventA, eventB, eventC], ); }); + + it("should apply redactions eagerly in the pending event list", () => { + const client = (new TestClient("@alice:example.com", "alicedevice")).client; + const room = new Room(roomId, client, userA, { + pendingEventOrdering: PendingEventOrdering.Detached, + }); + + const eventA = utils.mkMessage({ + room: roomId, + user: userA, + msg: "remote 1", + event: true, + }); + eventA.status = EventStatus.SENDING; + const redactA = utils.mkEvent({ + room: roomId, + user: userA, + type: EventType.RoomRedaction, + content: {}, + redacts: eventA.getId(), + event: true, + }); + redactA.status = EventStatus.SENDING; + + room.addPendingEvent(eventA, "TXN1"); + expect(room.getPendingEvents()).toEqual([eventA]); + room.addPendingEvent(redactA, "TXN2"); + expect(room.getPendingEvents()).toEqual([eventA, redactA]); + expect(eventA.isRedacted()).toBeTruthy(); + }); }); describe("updatePendingEvent", function() { @@ -1721,7 +1753,7 @@ describe("Room", function() { }); room.updateMyMembership(JoinRule.Invite); expect(room.getMyMembership()).toEqual(JoinRule.Invite); - expect(events[0]).toEqual({ membership: "invite", oldMembership: null }); + expect(events[0]).toEqual({ membership: "invite", oldMembership: undefined }); events.splice(0); //clear room.updateMyMembership(JoinRule.Invite); expect(events.length).toEqual(0); @@ -2636,4 +2668,42 @@ describe("Room", function() { expect(room.hasThreadUnreadNotification()).toBe(false); }); }); + + it("should load pending events from from the store and decrypt if needed", async () => { + const client = new TestClient(userA).client; + client.crypto = { + decryptEvent: jest.fn().mockResolvedValue({ clearEvent: { body: "enc" } }), + } as unknown as Crypto; + client.store.getPendingEvents = jest.fn(async roomId => [{ + event_id: "$1:server", + type: "m.room.message", + content: { body: "1" }, + sender: "@1:server", + room_id: roomId, + origin_server_ts: 1, + txn_id: "txn1", + }, { + event_id: "$2:server", + type: "m.room.encrypted", + content: { body: "2" }, + sender: "@2:server", + room_id: roomId, + origin_server_ts: 2, + txn_id: "txn2", + }]); + const room = new Room(roomId, client, userA, { + pendingEventOrdering: PendingEventOrdering.Detached, + }); + await emitPromise(room, RoomEvent.LocalEchoUpdated); + await emitPromise(client, MatrixEventEvent.Decrypted); + await emitPromise(room, RoomEvent.LocalEchoUpdated); + const pendingEvents = room.getPendingEvents(); + expect(pendingEvents).toHaveLength(2); + expect(pendingEvents[1].isDecryptionFailure()).toBeFalsy(); + expect(pendingEvents[1].isBeingDecrypted()).toBeFalsy(); + expect(pendingEvents[1].isEncrypted()).toBeTruthy(); + for (const ev of pendingEvents) { + expect(room.getPendingEvent(ev.getId())).toBe(ev); + } + }); }); diff --git a/spec/unit/stores/indexeddb.spec.ts b/spec/unit/stores/indexeddb.spec.ts index d0dd87243..abd71274b 100644 --- a/spec/unit/stores/indexeddb.spec.ts +++ b/spec/unit/stores/indexeddb.spec.ts @@ -60,7 +60,7 @@ describe("IndexedDBStore", () => { expect(await store.getOutOfBandMembers(roomId)).toHaveLength(1); // Simulate a broken IDB - (store.backend as LocalIndexedDBStoreBackend)["db"].transaction = (): IDBTransaction => { + (store.backend as LocalIndexedDBStoreBackend)["db"]!.transaction = (): IDBTransaction => { const err = new Error("Failed to execute 'transaction' on 'IDBDatabase': " + "The database connection is closing."); err.name = "InvalidStateError"; diff --git a/src/client.ts b/src/client.ts index 572ae1d37..fc049a420 100644 --- a/src/client.ts +++ b/src/client.ts @@ -3572,7 +3572,7 @@ export class MatrixClient extends TypedEventEmitter { - if (event.shouldAttemptDecryption()) { - event.attemptDecryption(this.crypto, options); + if (event.shouldAttemptDecryption() && this.isCryptoEnabled()) { + event.attemptDecryption(this.crypto!, options); } if (event.isBeingDecrypted()) { - return event.getDecryptionPromise(); + return event.getDecryptionPromise()!; } else { return Promise.resolve(); } @@ -9036,8 +9036,8 @@ export function fixNotificationCountOnDecryption(cli: MatrixClient, event: Matri // TODO: Handle mentions received while the client is offline // See also https://github.com/vector-im/element-web/issues/9069 const hasReadEvent = isThreadEvent - ? room.getThread(event.threadRootId).hasUserReadEvent(cli.getUserId(), event.getId()) - : room.hasUserReadEvent(cli.getUserId(), event.getId()); + ? room.getThread(event.threadRootId)?.hasUserReadEvent(cli.getUserId()!, event.getId()) + : room.hasUserReadEvent(cli.getUserId()!, event.getId()); if (!hasReadEvent) { let newCount = currentCount; diff --git a/src/content-repo.ts b/src/content-repo.ts index 8ff8a9270..d6cf81f2e 100644 --- a/src/content-repo.ts +++ b/src/content-repo.ts @@ -35,7 +35,7 @@ import * as utils from "./utils"; */ export function getHttpUriForMxc( baseUrl: string, - mxc: string, + mxc?: string, width?: number, height?: number, resizeMethod?: string, diff --git a/src/filter.ts b/src/filter.ts index 14565c26f..fb499c4da 100644 --- a/src/filter.ts +++ b/src/filter.ts @@ -107,8 +107,8 @@ export class Filter { } private definition: IFilterDefinition = {}; - private roomFilter: FilterComponent; - private roomTimelineFilter: FilterComponent; + private roomFilter?: FilterComponent; + private roomTimelineFilter?: FilterComponent; constructor(public readonly userId: string | undefined | null, public filterId?: string) {} @@ -116,7 +116,7 @@ export class Filter { * Get the ID of this filter on your homeserver (if known) * @return {?string} The filter ID */ - getFilterId(): string | null { + public getFilterId(): string | undefined { return this.filterId; } @@ -124,7 +124,7 @@ export class Filter { * Get the JSON body of the filter. * @return {Object} The filter definition */ - getDefinition(): IFilterDefinition { + public getDefinition(): IFilterDefinition { return this.definition; } @@ -132,7 +132,7 @@ export class Filter { * Set the JSON body of the filter * @param {Object} definition The filter definition */ - setDefinition(definition: IFilterDefinition) { + public setDefinition(definition: IFilterDefinition) { this.definition = definition; // This is all ported from synapse's FilterCollection() @@ -201,7 +201,7 @@ export class Filter { * Get the room.timeline filter component of the filter * @return {FilterComponent} room timeline filter component */ - getRoomTimelineFilterComponent(): FilterComponent { + public getRoomTimelineFilterComponent(): FilterComponent | undefined { return this.roomTimelineFilter; } @@ -211,15 +211,21 @@ export class Filter { * @param {MatrixEvent[]} events the list of events being filtered * @return {MatrixEvent[]} the list of events which match the filter */ - filterRoomTimeline(events: MatrixEvent[]): MatrixEvent[] { - return this.roomTimelineFilter.filter(this.roomFilter.filter(events)); + public filterRoomTimeline(events: MatrixEvent[]): MatrixEvent[] { + if (this.roomFilter) { + events = this.roomFilter.filter(events); + } + if (this.roomTimelineFilter) { + events = this.roomTimelineFilter.filter(events); + } + return events; } /** * Set the max number of events to return for each room's timeline. * @param {Number} limit The max number of events to return for each room. */ - setTimelineLimit(limit: number) { + public setTimelineLimit(limit: number) { setProp(this.definition, "room.timeline.limit", limit); } @@ -234,14 +240,14 @@ export class Filter { ...this.definition?.room, timeline: { ...this.definition?.room?.timeline, - [UNREAD_THREAD_NOTIFICATIONS.name]: !!enabled, + [UNREAD_THREAD_NOTIFICATIONS.name]: enabled, }, }, }; } - setLazyLoadMembers(enabled: boolean): void { - setProp(this.definition, "room.state.lazy_load_members", !!enabled); + public setLazyLoadMembers(enabled: boolean): void { + setProp(this.definition, "room.state.lazy_load_members", enabled); } /** @@ -249,7 +255,7 @@ export class Filter { * @param {boolean} includeLeave True to make rooms the user has left appear * in responses. */ - setIncludeLeaveRooms(includeLeave: boolean) { + public setIncludeLeaveRooms(includeLeave: boolean) { setProp(this.definition, "room.include_leave", includeLeave); } } diff --git a/src/http-api/index.ts b/src/http-api/index.ts index 6beb15050..1656c04ae 100644 --- a/src/http-api/index.ts +++ b/src/http-api/index.ts @@ -112,11 +112,11 @@ export class MatrixHttpApi extends FetchHttpApi { defer.resolve(JSON.parse(xhr.responseText)); } } catch (err) { - if (err.name === "AbortError") { + if ((err).name === "AbortError") { defer.reject(err); return; } - defer.reject(new ConnectionError("request failed", err)); + defer.reject(new ConnectionError("request failed", err)); } break; } @@ -207,7 +207,7 @@ export class MatrixHttpApi extends FetchHttpApi { base: this.opts.baseUrl, path: MediaPrefix.R0 + "/upload", params: { - access_token: this.opts.accessToken, + access_token: this.opts.accessToken!, }, }; } diff --git a/src/models/event-context.ts b/src/models/event-context.ts index bffecd317..de241fa02 100644 --- a/src/models/event-context.ts +++ b/src/models/event-context.ts @@ -82,7 +82,7 @@ export class EventContext { * backwards in time * @return {string} */ - public getPaginateToken(backwards = false): string { + public getPaginateToken(backwards = false): string | null { return this.paginateTokens[backwards ? Direction.Backward : Direction.Forward]; } diff --git a/src/models/event-timeline-set.ts b/src/models/event-timeline-set.ts index e7d687425..83a96b5b4 100644 --- a/src/models/event-timeline-set.ts +++ b/src/models/event-timeline-set.ts @@ -75,13 +75,22 @@ export interface IAddLiveEventOptions type EmittedEvents = RoomEvent.Timeline | RoomEvent.TimelineReset; export type EventTimelineSetHandlerMap = { - [RoomEvent.Timeline]: - (event: MatrixEvent, room: Room, toStartOfTimeline: boolean, removed: boolean, data: IRoomTimelineData) => void; - [RoomEvent.TimelineReset]: (room: Room, eventTimelineSet: EventTimelineSet, resetAllTimelines: boolean) => void; + [RoomEvent.Timeline]: ( + event: MatrixEvent, + room: Room | undefined, + toStartOfTimeline: boolean | undefined, + removed: boolean, + data: IRoomTimelineData, + ) => void; + [RoomEvent.TimelineReset]: ( + room: Room | undefined, + eventTimelineSet: EventTimelineSet, + resetAllTimelines: boolean, + ) => void; }; export class EventTimelineSet extends TypedEventEmitter { - public readonly relations?: RelationsContainer; + public readonly relations: RelationsContainer; private readonly timelineSupport: boolean; private readonly displayPendingEvents: boolean; private liveTimeline: EventTimeline; @@ -145,7 +154,7 @@ export class EventTimelineSet extends TypedEventEmitter void; [MatrixEventEvent.VisibilityChange]: (event: MatrixEvent, visible: boolean) => void; [MatrixEventEvent.LocalEventIdReplaced]: (event: MatrixEvent) => void; - [MatrixEventEvent.Status]: (event: MatrixEvent, status: EventStatus) => void; + [MatrixEventEvent.Status]: (event: MatrixEvent, status: EventStatus | null) => void; [MatrixEventEvent.Replaced]: (event: MatrixEvent) => void; [MatrixEventEvent.RelationsCreated]: (relationType: string, eventType: string) => void; } & ThreadEventHandlerMap; export class MatrixEvent extends TypedEventEmitter { - private pushActions: IActionsObject = null; - private _replacingEvent: MatrixEvent = null; - private _localRedactionEvent: MatrixEvent = null; + private pushActions: IActionsObject | null = null; + private _replacingEvent: MatrixEvent | null = null; + private _localRedactionEvent: MatrixEvent | null = null; private _isCancelled = false; private clearEvent?: IClearEvent; @@ -222,12 +222,12 @@ export class MatrixEvent extends TypedEventEmitter = null; + private decryptionPromise: Promise | null = null; /* flag to indicate if we should retry decrypting this event after the * first attempt (eg, we have received new data which means that a second @@ -253,14 +253,14 @@ export class MatrixEvent extends TypedEventEmitter; @@ -332,7 +332,7 @@ export class MatrixEvent extends TypedEventEmitter { - return this._decryptionPromise; + public getDecryptionPromise(): Promise | null { + return this.decryptionPromise; } /** @@ -713,16 +713,16 @@ export class MatrixEvent extends TypedEventEmitter { // make sure that this method never runs completely synchronously. - // (doing so would mean that we would clear _decryptionPromise *before* + // (doing so would mean that we would clear decryptionPromise *before* // it is set in attemptDecryption - and hence end up with a stuck - // `_decryptionPromise`). + // `decryptionPromise`). await Promise.resolve(); // eslint-disable-next-line no-constant-condition @@ -778,7 +778,7 @@ export class MatrixEvent extends TypedEventEmittere).name !== "DecryptionError") { // not a decryption error: log the whole exception as an error // (and don't bother with a retry) const re = options.isRetry ? 're' : ''; @@ -799,25 +799,25 @@ export class MatrixEvent extends TypedEventEmitter} */ - public getKeysClaimed(): Record<"ed25519", string> { + public getKeysClaimed(): Partial> { + if (!this.claimedEd25519Key) return {}; + return { ed25519: this.claimedEd25519Key, }; @@ -992,8 +992,8 @@ export class MatrixEvent extends TypedEventEmitter { + private onEventStatus = (event: MatrixEvent, status: EventStatus | null) => { if (!event.isSending()) { // Sending is done, so we don't need to listen anymore event.removeListener(MatrixEventEvent.Status, this.onEventStatus); diff --git a/src/models/room-member.ts b/src/models/room-member.ts index 2ea13b536..122a55770 100644 --- a/src/models/room-member.ts +++ b/src/models/room-member.ts @@ -35,7 +35,7 @@ export enum RoomMemberEvent { } export type RoomMemberEventHandlerMap = { - [RoomMemberEvent.Membership]: (event: MatrixEvent, member: RoomMember, oldMembership: string | null) => void; + [RoomMemberEvent.Membership]: (event: MatrixEvent, member: RoomMember, oldMembership?: string) => void; [RoomMemberEvent.Name]: (event: MatrixEvent, member: RoomMember, oldName: string | null) => void; [RoomMemberEvent.PowerLevel]: (event: MatrixEvent, member: RoomMember) => void; [RoomMemberEvent.Typing]: (event: MatrixEvent, member: RoomMember) => void; @@ -43,8 +43,8 @@ export type RoomMemberEventHandlerMap = { export class RoomMember extends TypedEventEmitter { private _isOutOfBand = false; - private _modified: number; - public _requestedProfileInfo: boolean; // used by sync.ts + private modified = -1; + public requestedProfileInfo = false; // used by sync.ts // XXX these should be read-only public typing = false; @@ -52,14 +52,12 @@ export class RoomMember extends TypedEventEmitter { maxLevel = Math.max(maxLevel, lvl); }); const oldPowerLevel = this.powerLevel; @@ -244,7 +233,7 @@ export class RoomMember extends TypedEventEmitter void; [RoomStateEvent.Update]: (state: RoomState) => void; [RoomStateEvent.BeaconLiveness]: (state: RoomState, hasLiveBeacons: boolean) => void; - [RoomStateEvent.Marker]: (event: MatrixEvent, setStateOptions: IMarkerFoundOptions) => void; + [RoomStateEvent.Marker]: (event: MatrixEvent, setStateOptions?: IMarkerFoundOptions) => void; [BeaconEvent.New]: (event: MatrixEvent, beacon: Beacon) => void; }; @@ -82,17 +82,17 @@ export class RoomState extends TypedEventEmitter private displayNameToUserIds = new Map(); private userIdsToDisplayNames: Record = {}; private tokenToInvite: Record = {}; // 3pid invite state_key to m.room.member invite - private joinedMemberCount: number = null; // cache of the number of joined members + private joinedMemberCount: number | null = null; // cache of the number of joined members // joined members count from summary api // once set, we know the server supports the summary api // and we should only trust that // we could also only trust that before OOB members // are loaded but doesn't seem worth the hassle atm - private summaryJoinedMemberCount: number = null; + private summaryJoinedMemberCount: number |null = null; // same for invited member count - private invitedMemberCount: number = null; - private summaryInvitedMemberCount: number = null; - private modified: number; + private invitedMemberCount: number | null = null; + private summaryInvitedMemberCount: number | null = null; + private modified = -1; // XXX: Should be read-only public members: Record = {}; // userId: RoomMember @@ -232,7 +232,7 @@ export class RoomState extends TypedEventEmitter if (sentinel === undefined) { sentinel = new RoomMember(this.roomId, userId); const member = this.members[userId]; - if (member) { + if (member?.events.member) { sentinel.setMembershipEvent(member.events.member, this); } this.sentinels[userId] = sentinel; @@ -257,9 +257,9 @@ export class RoomState extends TypedEventEmitter return stateKey === undefined ? [] : null; } if (stateKey === undefined) { // return all values - return Array.from(this.events.get(eventType).values()); + return Array.from(this.events.get(eventType)!.values()); } - const event = this.events.get(eventType).get(stateKey); + const event = this.events.get(eventType)!.get(stateKey); return event ? event : null; } @@ -306,8 +306,7 @@ export class RoomState extends TypedEventEmitter // copy markOutOfBand flags this.getMembers().forEach((member) => { if (member.isOutOfBand()) { - const copyMember = copy.getMember(member.userId); - copyMember.markOutOfBand(); + copy.getMember(member.userId)?.markOutOfBand(); } }); } @@ -324,8 +323,7 @@ export class RoomState extends TypedEventEmitter */ public setUnknownStateEvents(events: MatrixEvent[]): void { const unknownStateEvents = events.filter((event) => { - return !this.events.has(event.getType()) || - !this.events.get(event.getType()).has(event.getStateKey()); + return !this.events.has(event.getType()) || !this.events.get(event.getType())!.has(event.getStateKey()!); }); this.setStateEvents(unknownStateEvents); @@ -349,12 +347,7 @@ export class RoomState extends TypedEventEmitter // update the core event dict stateEvents.forEach((event) => { - if (event.getRoomId() !== this.roomId) { - return; - } - if (!event.isState()) { - return; - } + if (event.getRoomId() !== this.roomId || !event.isState()) return; if (M_BEACON_INFO.matches(event.getType())) { this.setBeacon(event); @@ -363,7 +356,7 @@ export class RoomState extends TypedEventEmitter const lastStateEvent = this.getStateEventMatching(event); this.setStateEvent(event); if (event.getType() === EventType.RoomMember) { - this.updateDisplayNameCache(event.getStateKey(), event.getContent().displayname); + this.updateDisplayNameCache(event.getStateKey()!, event.getContent().displayname ?? ""); this.updateThirdPartyTokenCache(event); } this.emit(RoomStateEvent.Events, event, this, lastStateEvent); @@ -375,15 +368,10 @@ export class RoomState extends TypedEventEmitter // the given array (e.g. disambiguating display names in one go to do both // clashing names rather than progressively which only catches 1 of them). stateEvents.forEach((event) => { - if (event.getRoomId() !== this.roomId) { - return; - } - if (!event.isState()) { - return; - } + if (event.getRoomId() !== this.roomId || !event.isState()) return; if (event.getType() === EventType.RoomMember) { - const userId = event.getStateKey(); + const userId = event.getStateKey()!; // leave events apparently elide the displayname or avatar_url, // so let's fake one up so that we don't leak user ids @@ -456,11 +444,8 @@ export class RoomState extends TypedEventEmitter events.forEach((event: MatrixEvent) => { const relatedToEventId = event.getRelation()?.event_id; - // not related to a beacon we know about - // discard - if (!beaconByEventIdDict[relatedToEventId]) { - return; - } + // not related to a beacon we know about; discard + if (!relatedToEventId || !beaconByEventIdDict[relatedToEventId]) return; matrixClient.decryptEventIfNeeded(event); @@ -501,7 +486,7 @@ export class RoomState extends TypedEventEmitter if (!this.events.has(event.getType())) { this.events.set(event.getType(), new Map()); } - this.events.get(event.getType()).set(event.getStateKey(), event); + this.events.get(event.getType())!.set(event.getStateKey()!, event); } /** @@ -511,7 +496,7 @@ export class RoomState extends TypedEventEmitter const beaconIdentifier = getBeaconInfoIdentifier(event); if (this.beacons.has(beaconIdentifier)) { - const beacon = this.beacons.get(beaconIdentifier); + const beacon = this.beacons.get(beaconIdentifier)!; if (event.isRedacted()) { if (beacon.beaconInfoId === event.getRedactionEvent()?.['redacts']) { @@ -558,7 +543,7 @@ export class RoomState extends TypedEventEmitter } private getStateEventMatching(event: MatrixEvent): MatrixEvent | null { - return this.events.get(event.getType())?.get(event.getStateKey()) ?? null; + return this.events.get(event.getType())?.get(event.getStateKey()!) ?? null; } private updateMember(member: RoomMember): void { @@ -646,7 +631,7 @@ export class RoomState extends TypedEventEmitter if (stateEvent.getType() !== EventType.RoomMember) { return; } - const userId = stateEvent.getStateKey(); + const userId = stateEvent.getStateKey()!; const existingMember = this.getMember(userId); // never replace members received as part of the sync if (existingMember && !existingMember.isOutOfBand()) { @@ -788,7 +773,7 @@ export class RoomState extends TypedEventEmitter * according to the room's state. */ public mayClientSendStateEvent(stateEventType: EventType | string, cli: MatrixClient): boolean { - if (cli.isGuest()) { + if (cli.isGuest() || !cli.credentials.userId) { return false; } return this.maySendStateEvent(stateEventType, cli.credentials.userId); diff --git a/src/models/room.ts b/src/models/room.ts index b1019dc4e..5ce322afb 100644 --- a/src/models/room.ts +++ b/src/models/room.ts @@ -69,7 +69,6 @@ export const KNOWN_SAFE_ROOM_VERSION = '9'; const SAFE_ROOM_VERSIONS = ['1', '2', '3', '4', '5', '6', '7', '8', '9']; interface IOpts { - storageToken?: string; pendingEventOrdering?: PendingEventOrdering; timelineSupport?: boolean; lazyLoadMembers?: boolean; @@ -158,7 +157,7 @@ export type RoomEventHandlerMap = { event: MatrixEvent, room: Room, oldEventId?: string, - oldStatus?: EventStatus, + oldStatus?: EventStatus | null, ) => void; [RoomEvent.OldStateUpdated]: (room: Room, previousRoomState: RoomState, roomState: RoomState) => void; [RoomEvent.CurrentStateUpdated]: (room: Room, previousRoomState: RoomState, roomState: RoomState) => void; @@ -201,8 +200,8 @@ export class Room extends ReadReceipt { private timelineNeedsRefresh = false; private readonly pendingEventList?: MatrixEvent[]; // read by megolm via getter; boolean value - null indicates "use global value" - private blacklistUnverifiedDevices: boolean = null; - private selfMembership: string = null; + private blacklistUnverifiedDevices?: boolean; + private selfMembership?: string; private summaryHeroes: string[] = null; // flags to stop logspam about missing m.room.create events private getTypeWarning = false; @@ -232,10 +231,6 @@ export class Room extends ReadReceipt { * The room summary. */ public summary: RoomSummary = null; - /** - * A token which a data store can use to remember the state of the room. - */ - public readonly storageToken?: string; // legacy fields /** * The live event timeline for this room, with the oldest event at index 0. @@ -260,7 +255,7 @@ export class Room extends ReadReceipt { * @experimental */ private threads = new Map(); - public lastThread: Thread; + public lastThread?: Thread; /** * A mapping of eventId to all visibility changes to apply @@ -304,9 +299,6 @@ export class Room extends ReadReceipt { * @param {MatrixClient} client Required. The client, used to lazy load members. * @param {string} myUserId Required. The ID of the syncing user. * @param {Object=} opts Configuration options - * @param {*} opts.storageToken Optional. The token which a data store can use - * to remember the state of the room. What this means is dependent on the store - * implementation. * * @param {String=} opts.pendingEventOrdering Controls where pending messages * appear in a room's timeline. If "chronological", messages will appear @@ -332,6 +324,7 @@ export class Room extends ReadReceipt { opts.pendingEventOrdering = opts.pendingEventOrdering || PendingEventOrdering.Chronological; this.name = roomId; + this.normalizedName = roomId; // all our per-room timeline sets. the first one is the unfiltered ones; // the subsequent ones are the filtered ones in no particular order. @@ -346,13 +339,17 @@ export class Room extends ReadReceipt { if (this.opts.pendingEventOrdering === PendingEventOrdering.Detached) { this.pendingEventList = []; this.client.store.getPendingEvents(this.roomId).then(events => { + const mapper = this.client.getEventMapper({ + toDevice: false, + decrypt: false, + }); events.forEach(async (serializedEvent: Partial) => { - const event = new MatrixEvent(serializedEvent); - if (event.getType() === EventType.RoomMessageEncrypted) { - await event.attemptDecryption(this.client.crypto); + const event = mapper(serializedEvent); + if (event.getType() === EventType.RoomMessageEncrypted && this.client.isCryptoEnabled()) { + await event.attemptDecryption(this.client.crypto!); } event.setStatus(EventStatus.NOT_SENT); - this.addPendingEvent(event, event.getTxnId()); + this.addPendingEvent(event, event.getTxnId()!); }); }); } @@ -361,7 +358,7 @@ export class Room extends ReadReceipt { if (!this.opts.lazyLoadMembers) { this.membersPromise = Promise.resolve(false); } else { - this.membersPromise = null; + this.membersPromise = undefined; } } @@ -399,8 +396,10 @@ export class Room extends ReadReceipt { * * @returns {Promise} Signals when all events have been decrypted */ - public decryptCriticalEvents(): Promise { - const readReceiptEventId = this.getEventReadUpTo(this.client.getUserId(), true); + public async decryptCriticalEvents(): Promise { + if (!this.client.isCryptoEnabled()) return; + + const readReceiptEventId = this.getEventReadUpTo(this.client.getUserId()!, true); const events = this.getLiveTimeline().getEvents(); const readReceiptTimelineIndex = events.findIndex(matrixEvent => { return matrixEvent.event.event_id === readReceiptEventId; @@ -410,9 +409,9 @@ export class Room extends ReadReceipt { .slice(readReceiptTimelineIndex) .filter(event => event.shouldAttemptDecryption()) .reverse() - .map(event => event.attemptDecryption(this.client.crypto, { isRetry: true })); + .map(event => event.attemptDecryption(this.client.crypto!, { isRetry: true })); - return Promise.allSettled(decryptionPromises) as unknown as Promise; + await Promise.allSettled(decryptionPromises); } /** @@ -420,16 +419,18 @@ export class Room extends ReadReceipt { * * @returns {Promise} Signals when all events have been decrypted */ - public decryptAllEvents(): Promise { + public async decryptAllEvents(): Promise { + if (!this.client.isCryptoEnabled()) return; + const decryptionPromises = this .getUnfilteredTimelineSet() .getLiveTimeline() .getEvents() .filter(event => event.shouldAttemptDecryption()) .reverse() - .map(event => event.attemptDecryption(this.client.crypto, { isRetry: true })); + .map(event => event.attemptDecryption(this.client.crypto!, { isRetry: true })); - return Promise.allSettled(decryptionPromises) as unknown as Promise; + await Promise.allSettled(decryptionPromises); } /** @@ -445,7 +446,7 @@ export class Room extends ReadReceipt { * Gets the version of the room * @returns {string} The version of the room, or null if it could not be determined */ - public getVersion(): string | null { + public getVersion(): string { const createEvent = this.currentState.getStateEvents(EventType.RoomCreate, ""); if (!createEvent) { if (!this.getVersionWarning) { @@ -454,9 +455,7 @@ export class Room extends ReadReceipt { } return '1'; } - const ver = createEvent.getContent()['room_version']; - if (ver === undefined) return '1'; - return ver; + return createEvent.getContent()['room_version'] ?? '1'; } /** @@ -535,7 +534,7 @@ export class Room extends ReadReceipt { logger.log(`[${this.roomId}] Current version: ${currentVersion}`); logger.log(`[${this.roomId}] Version capability: `, versionCap); - const result = { + const result: IRecommendedVersion = { version: currentVersion, needsUpgrade: false, urgent: false, @@ -645,7 +644,7 @@ export class Room extends ReadReceipt { return null; } - return this.pendingEventList.find(event => event.getId() === eventId); + return this.pendingEventList.find(event => event.getId() === eventId) ?? null; } /** @@ -677,7 +676,7 @@ export class Room extends ReadReceipt { * @return {string} the membership type (join | leave | invite) for the logged in user */ public getMyMembership(): string { - return this.selfMembership; + return this.selfMembership ?? "leave"; } /** @@ -685,7 +684,7 @@ export class Room extends ReadReceipt { * try to find out who invited us * @return {string} user id of the inviter */ - public getDMInviter(): string { + public getDMInviter(): string | undefined { if (this.myUserId) { const me = this.getMember(this.myUserId); if (me) { @@ -731,7 +730,7 @@ export class Room extends ReadReceipt { return this.myUserId; } - public getAvatarFallbackMember(): RoomMember { + public getAvatarFallbackMember(): RoomMember | undefined { const memberCount = this.getInvitedAndJoinedMemberCount(); if (memberCount > 2) { return; @@ -789,7 +788,7 @@ export class Room extends ReadReceipt { private async loadMembersFromServer(): Promise { const lastSyncToken = this.client.store.getSyncToken(); - const response = await this.client.members(this.roomId, undefined, "leave", lastSyncToken); + const response = await this.client.members(this.roomId, undefined, "leave", lastSyncToken ?? undefined); return response.chunk; } @@ -836,12 +835,12 @@ export class Room extends ReadReceipt { this.currentState.setOutOfBandMembers(result.memberEvents); // now the members are loaded, start to track the e2e devices if needed if (this.client.isCryptoEnabled() && this.client.isRoomEncrypted(this.roomId)) { - this.client.crypto.trackRoomDevices(this.roomId); + this.client.crypto!.trackRoomDevices(this.roomId); } return result.fromServer; }).catch((err) => { // allow retries on fail - this.membersPromise = null; + this.membersPromise = undefined; this.currentState.markOutOfBandMembersFailed(); throw err; }); @@ -850,7 +849,7 @@ export class Room extends ReadReceipt { if (fromServer) { const oobMembers = this.currentState.getMembers() .filter((m) => m.isOutOfBand()) - .map((m) => m.events.member.event as IStateEventWithRoomId); + .map((m) => m.events.member?.event as IStateEventWithRoomId); logger.log(`LL: telling store to write ${oobMembers.length}` + ` members for room ${this.roomId}`); const store = this.client.store; @@ -881,7 +880,7 @@ export class Room extends ReadReceipt { await this.loadMembersIfNeeded(); await this.client.store.clearOutOfBandMembers(this.roomId); this.currentState.clearOutOfBandMembers(); - this.membersPromise = null; + this.membersPromise = undefined; } } @@ -1328,7 +1327,7 @@ export class Room extends ReadReceipt { * if the global value should be used for this room. */ public getBlacklistUnverifiedDevices(): boolean { - return this.blacklistUnverifiedDevices; + return !!this.blacklistUnverifiedDevices; } /** @@ -1457,8 +1456,8 @@ export class Room extends ReadReceipt { /** * @experimental */ - public getThread(eventId: string): Thread { - return this.threads.get(eventId); + public getThread(eventId: string): Thread | null { + return this.threads.get(eventId) ?? null; } /** @@ -1584,7 +1583,6 @@ export class Room extends ReadReceipt { * Add a timelineSet for this room with the given filter * @param {Filter} filter The filter to be applied to this timelineSet * @param {Object=} opts Configuration options - * @param {*} opts.storageToken Optional. * @return {EventTimelineSet} The timelineSet */ public getOrCreateFilteredTimelineSet( @@ -1799,7 +1797,7 @@ export class Room extends ReadReceipt { const threadRelationship = rootEvent .getServerAggregatedRelation(THREAD_RELATION_TYPE.name); - if (threadRelationship.current_user_participated) { + if (threadRelationship?.current_user_participated) { this.threadsTimelineSets[1].addLiveEvent(rootEvent, { duplicateStrategy: DuplicateStrategy.Ignore, fromCache: false, @@ -2035,7 +2033,7 @@ export class Room extends ReadReceipt { const redactId = event.event.redacts; // if we know about this event, redact its contents now. - const redactedEvent = this.findEventById(redactId); + const redactedEvent = redactId ? this.findEventById(redactId) : undefined; if (redactedEvent) { redactedEvent.makeRedacted(event); @@ -2043,7 +2041,7 @@ export class Room extends ReadReceipt { if (redactedEvent.isState()) { const currentStateEvent = this.currentState.getStateEvents( redactedEvent.getType(), - redactedEvent.getStateKey(), + redactedEvent.getStateKey()!, ); if (currentStateEvent.getId() === redactedEvent.getId()) { this.currentState.setStateEvents([redactedEvent]); @@ -2059,7 +2057,7 @@ export class Room extends ReadReceipt { // they are based on are changed. // Remove any visibility change on this event. - this.visibilityEvents.delete(redactId); + this.visibilityEvents.delete(redactId!); // If this event is a visibility change event, remove it from the // list of visibility changes and update any event affected by it. @@ -2183,7 +2181,7 @@ export class Room extends ReadReceipt { EventTimeline.setEventMetadata(event, this.getLiveTimeline().getState(EventTimeline.FORWARDS), false); this.txnToEvent[txnId] = event; - if (this.opts.pendingEventOrdering === PendingEventOrdering.Detached) { + if (this.pendingEventList) { if (this.pendingEventList.some((e) => e.status === EventStatus.NOT_SENT)) { logger.warn("Setting event as NOT_SENT due to messages in the same state"); event.setStatus(EventStatus.NOT_SENT); @@ -2199,8 +2197,8 @@ export class Room extends ReadReceipt { if (event.isRedaction()) { const redactId = event.event.redacts; - let redactedEvent = this.pendingEventList?.find(e => e.getId() === redactId); - if (!redactedEvent) { + let redactedEvent = this.pendingEventList.find(e => e.getId() === redactId); + if (!redactedEvent && redactId) { redactedEvent = this.findEventById(redactId); } if (redactedEvent) { @@ -2212,7 +2210,7 @@ export class Room extends ReadReceipt { for (let i = 0; i < this.timelineSets.length; i++) { const timelineSet = this.timelineSets[i]; if (timelineSet.getFilter()) { - if (timelineSet.getFilter().filterRoomTimeline([event]).length) { + if (timelineSet.getFilter()!.filterRoomTimeline([event]).length) { timelineSet.addEventToTimeline(event, timelineSet.getLiveTimeline(), { toStartOfTimeline: false, @@ -2227,7 +2225,7 @@ export class Room extends ReadReceipt { } } - this.emit(RoomEvent.LocalEchoUpdated, event, this, null, null); + this.emit(RoomEvent.LocalEchoUpdated, event, this); } /** @@ -2348,20 +2346,19 @@ export class Room extends ReadReceipt { // if the message was sent, we expect an event id if (newStatus == EventStatus.SENT && !newEventId) { - throw new Error("updatePendingEvent called with status=SENT, " + - "but no new event id"); + throw new Error("updatePendingEvent called with status=SENT, but no new event id"); } // SENT races against /sync, so we have to special-case it. if (newStatus == EventStatus.SENT) { - const timeline = this.getTimelineForEvent(newEventId); + const timeline = this.getTimelineForEvent(newEventId!); if (timeline) { // we've already received the event via the event stream. // nothing more to do here, assuming the transaction ID was correctly matched. // Let's check that. - const remoteEvent = this.findEventById(newEventId); - const remoteTxnId = remoteEvent.getUnsigned().transaction_id; - if (!remoteTxnId) { + const remoteEvent = this.findEventById(newEventId!); + const remoteTxnId = remoteEvent?.getUnsigned().transaction_id; + if (!remoteTxnId && remoteEvent) { // This code path is mostly relevant for the Sliding Sync proxy. // The remote event did not contain a transaction ID, so we did not handle // the remote echo yet. Handle it now. @@ -2395,18 +2392,18 @@ export class Room extends ReadReceipt { if (newStatus == EventStatus.SENT) { // update the event id - event.replaceLocalEventId(newEventId); + event.replaceLocalEventId(newEventId!); const { shouldLiveInRoom, threadId } = this.eventShouldLiveIn(event); - const thread = this.getThread(threadId); - thread?.timelineSet.replaceEventId(oldEventId, newEventId); + const thread = threadId ? this.getThread(threadId) : undefined; + thread?.timelineSet.replaceEventId(oldEventId, newEventId!); if (shouldLiveInRoom) { // if the event was already in the timeline (which will be the case if // opts.pendingEventOrdering==chronological), we need to update the // timeline map. for (let i = 0; i < this.timelineSets.length; i++) { - this.timelineSets[i].replaceEventId(oldEventId, newEventId); + this.timelineSets[i].replaceEventId(oldEventId, newEventId!); } } } else if (newStatus == EventStatus.CANCELLED) { @@ -2414,7 +2411,7 @@ export class Room extends ReadReceipt { if (this.pendingEventList) { const removedEvent = this.getPendingEvent(oldEventId); this.removePendingEvent(oldEventId); - if (removedEvent.isRedaction()) { + if (removedEvent?.isRedaction()) { this.revertRedactionLocalEcho(removedEvent); } } @@ -2449,7 +2446,7 @@ export class Room extends ReadReceipt { * they will go to the end of the timeline. * * @param {MatrixEvent[]} events A list of events to add. - * @param {IAddLiveEventOptions} options addLiveEvent options + * @param {IAddLiveEventOptions} addLiveEventOptions addLiveEvent options * @throws If duplicateStrategy is not falsey, 'replace' or 'ignore'. */ public addLiveEvents(events: MatrixEvent[], addLiveEventOptions?: IAddLiveEventOptions): void; @@ -2462,8 +2459,8 @@ export class Room extends ReadReceipt { duplicateStrategyOrOpts?: DuplicateStrategy | IAddLiveEventOptions, fromCache = false, ): void { - let duplicateStrategy = duplicateStrategyOrOpts as DuplicateStrategy; - let timelineWasEmpty = false; + let duplicateStrategy: DuplicateStrategy | undefined = duplicateStrategyOrOpts as DuplicateStrategy; + let timelineWasEmpty: boolean | undefined = false; if (typeof (duplicateStrategyOrOpts) === 'object') { ({ duplicateStrategy, @@ -2679,7 +2676,7 @@ export class Room extends ReadReceipt { const membershipEvent = this.currentState.getStateEvents(EventType.RoomMember, this.myUserId); if (membershipEvent) { const membership = membershipEvent.getContent().membership; - this.updateMyMembership(membership); + this.updateMyMembership(membership!); if (membership === "invite") { const strippedStateEvents = membershipEvent.getUnsigned().invite_room_state || []; @@ -2919,11 +2916,9 @@ export class Room extends ReadReceipt { } // get members that are NOT ourselves and are actually in the room. - let otherNames: string[] = null; + let otherNames: string[] = []; if (this.summaryHeroes) { - // if we have a summary, the member state events - // should be in the room state - otherNames = []; + // if we have a summary, the member state events should be in the room state this.summaryHeroes.forEach((userId) => { // filter service members if (excludedUserIds.includes(userId)) { diff --git a/src/models/user.ts b/src/models/user.ts index df580b0f1..ed1b0f01d 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -38,13 +38,13 @@ export type UserEventHandlerMap = { }; export class User extends TypedEventEmitter { - private modified: number; + private modified = -1; // XXX these should be read-only - public displayName: string; - public rawDisplayName: string; - public avatarUrl: string; - public presenceStatusMsg: string = null; + public displayName?: string; + public rawDisplayName?: string; + public avatarUrl?: string; + public presenceStatusMsg?: string; public presence = "offline"; public lastActiveAgo = 0; public lastPresenceTs = 0; @@ -52,10 +52,7 @@ export class User extends TypedEventEmitter { public events: { presence?: MatrixEvent; profile?: MatrixEvent; - } = { - presence: null, - profile: null, - }; + } = {}; /** * Construct a new User. A User must have an ID and can optionally have extra @@ -83,7 +80,6 @@ export class User extends TypedEventEmitter { super(); this.displayName = userId; this.rawDisplayName = userId; - this.avatarUrl = null; this.updateModifiedTime(); } @@ -150,11 +146,7 @@ export class User extends TypedEventEmitter { */ public setDisplayName(name: string): void { const oldName = this.displayName; - if (typeof name === "string") { - this.displayName = name; - } else { - this.displayName = undefined; - } + this.displayName = name; if (name !== oldName) { this.updateModifiedTime(); } @@ -165,12 +157,8 @@ export class User extends TypedEventEmitter { * in response to this as there is no underlying MatrixEvent to emit with. * @param {string} name The new display name. */ - public setRawDisplayName(name: string): void { - if (typeof name === "string") { - this.rawDisplayName = name; - } else { - this.rawDisplayName = undefined; - } + public setRawDisplayName(name?: string): void { + this.rawDisplayName = name; } /** @@ -178,7 +166,7 @@ export class User extends TypedEventEmitter { * as there is no underlying MatrixEvent to emit with. * @param {string} url The new avatar URL. */ - public setAvatarUrl(url: string): void { + public setAvatarUrl(url?: string): void { const oldUrl = this.avatarUrl; this.avatarUrl = url; if (url !== oldUrl) { diff --git a/src/sliding-sync-sdk.ts b/src/sliding-sync-sdk.ts index e1072f8da..834b15775 100644 --- a/src/sliding-sync-sdk.ts +++ b/src/sliding-sync-sdk.ts @@ -670,8 +670,8 @@ export class SlidingSyncSdk { // For each invited room member we want to give them a displayname/avatar url // if they have one (the m.room.member invites don't contain this). room.getMembersWithMembership("invite").forEach(function(member) { - if (member._requestedProfileInfo) return; - member._requestedProfileInfo = true; + if (member.requestedProfileInfo) return; + member.requestedProfileInfo = true; // try to get a cached copy first. const user = client.getUser(member.userId); let promise: ReturnType; diff --git a/src/store/index.ts b/src/store/index.ts index 4e99304fd..d58b58193 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -195,7 +195,7 @@ export interface IStore { * client state to where it was at the last save, or null if there * is no saved sync data. */ - getSavedSync(): Promise; + getSavedSync(): Promise; /** * @return {Promise} If there is a saved sync, the nextBatch token diff --git a/src/store/indexeddb-backend.ts b/src/store/indexeddb-backend.ts index f7547d6e5..1e08c2e73 100644 --- a/src/store/indexeddb-backend.ts +++ b/src/store/indexeddb-backend.ts @@ -23,7 +23,7 @@ export interface IIndexedDBBackend { syncToDatabase(userTuples: UserTuple[]): Promise; isNewlyCreated(): Promise; setSyncData(syncData: ISyncResponse): Promise; - getSavedSync(): Promise; + getSavedSync(): Promise; getNextBatchToken(): Promise; clearDatabase(): Promise; getOutOfBandMembers(roomId: string): Promise; diff --git a/src/store/indexeddb-local-backend.ts b/src/store/indexeddb-local-backend.ts index 908ecec9e..007895ea2 100644 --- a/src/store/indexeddb-local-backend.ts +++ b/src/store/indexeddb-local-backend.ts @@ -124,7 +124,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { private readonly dbName: string; private readonly syncAccumulator: SyncAccumulator; - private db: IDBDatabase = null; + private db?: IDBDatabase; private disconnected = true; private _isNewlyCreated = false; private isPersisting = false; @@ -141,8 +141,8 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { * @param {string=} dbName Optional database name. The same name must be used * to open the same database. */ - constructor(private readonly indexedDB: IDBFactory, dbName: string) { - this.dbName = "matrix-js-sdk:" + (dbName || "default"); + constructor(private readonly indexedDB: IDBFactory, dbName = "default") { + this.dbName = "matrix-js-sdk:" + dbName; this.syncAccumulator = new SyncAccumulator(); } @@ -188,7 +188,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { // add a poorly-named listener for when deleteDatabase is called // so we can close our db connections. this.db.onversionchange = () => { - this.db.close(); + this.db?.close(); }; return this.init(); @@ -229,7 +229,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { */ public getOutOfBandMembers(roomId: string): Promise { return new Promise((resolve, reject) => { - const tx = this.db.transaction(["oob_membership_events"], "readonly"); + const tx = this.db!.transaction(["oob_membership_events"], "readonly"); const store = tx.objectStore("oob_membership_events"); const roomIndex = store.index("room"); const range = IDBKeyRange.only(roomId); @@ -279,7 +279,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { public async setOutOfBandMembers(roomId: string, membershipEvents: IStateEventWithRoomId[]): Promise { logger.log(`LL: backend about to store ${membershipEvents.length}` + ` members for ${roomId}`); - const tx = this.db.transaction(["oob_membership_events"], "readwrite"); + const tx = this.db!.transaction(["oob_membership_events"], "readwrite"); const store = tx.objectStore("oob_membership_events"); membershipEvents.forEach((e) => { store.put(e); @@ -306,7 +306,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { // keys in the store. // this should be way faster than deleting every member // individually for a large room. - const readTx = this.db.transaction( + const readTx = this.db!.transaction( ["oob_membership_events"], "readonly"); const store = readTx.objectStore("oob_membership_events"); @@ -322,7 +322,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { const [minStateKey, maxStateKey] = await Promise.all( [minStateKeyProm, maxStateKeyProm]); - const writeTx = this.db.transaction( + const writeTx = this.db!.transaction( ["oob_membership_events"], "readwrite"); const writeStore = writeTx.objectStore("oob_membership_events"); @@ -374,7 +374,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { * client state to where it was at the last save, or null if there * is no saved sync data. */ - public getSavedSync(copy = true): Promise { + public getSavedSync(copy = true): Promise { const data = this.syncAccumulator.getJSON(); if (!data.nextBatch) return Promise.resolve(null); if (copy) { @@ -431,7 +431,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { ): Promise { logger.log("Persisting sync data up to", nextBatch); return utils.promiseTry(() => { - const txn = this.db.transaction(["sync"], "readwrite"); + const txn = this.db!.transaction(["sync"], "readwrite"); const store = txn.objectStore("sync"); store.put({ clobber: "-", // constant key so will always clobber @@ -452,7 +452,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { */ private persistAccountData(accountData: IMinimalEvent[]): Promise { return utils.promiseTry(() => { - const txn = this.db.transaction(["accountData"], "readwrite"); + const txn = this.db!.transaction(["accountData"], "readwrite"); const store = txn.objectStore("accountData"); for (let i = 0; i < accountData.length; i++) { store.put(accountData[i]); // put == UPSERT @@ -471,7 +471,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { */ private persistUserPresenceEvents(tuples: UserTuple[]): Promise { return utils.promiseTry(() => { - const txn = this.db.transaction(["users"], "readwrite"); + const txn = this.db!.transaction(["users"], "readwrite"); const store = txn.objectStore("users"); for (const tuple of tuples) { store.put({ @@ -491,7 +491,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { */ public getUserPresenceEvents(): Promise { return utils.promiseTry(() => { - const txn = this.db.transaction(["users"], "readonly"); + const txn = this.db!.transaction(["users"], "readonly"); const store = txn.objectStore("users"); return selectQuery(store, undefined, (cursor) => { return [cursor.value.userId, cursor.value.event]; @@ -506,7 +506,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { private loadAccountData(): Promise { logger.log(`LocalIndexedDBStoreBackend: loading account data...`); return utils.promiseTry(() => { - const txn = this.db.transaction(["accountData"], "readonly"); + const txn = this.db!.transaction(["accountData"], "readonly"); const store = txn.objectStore("accountData"); return selectQuery(store, undefined, (cursor) => { return cursor.value; @@ -524,7 +524,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { private loadSyncData(): Promise { logger.log(`LocalIndexedDBStoreBackend: loading sync data...`); return utils.promiseTry(() => { - const txn = this.db.transaction(["sync"], "readonly"); + const txn = this.db!.transaction(["sync"], "readonly"); const store = txn.objectStore("sync"); return selectQuery(store, undefined, (cursor) => { return cursor.value; @@ -540,7 +540,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { public getClientOptions(): Promise { return Promise.resolve().then(() => { - const txn = this.db.transaction(["client_options"], "readonly"); + const txn = this.db!.transaction(["client_options"], "readonly"); const store = txn.objectStore("client_options"); return selectQuery(store, undefined, (cursor) => { return cursor.value?.options; @@ -549,7 +549,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { } public async storeClientOptions(options: IStartClientOpts): Promise { - const txn = this.db.transaction(["client_options"], "readwrite"); + const txn = this.db!.transaction(["client_options"], "readwrite"); const store = txn.objectStore("client_options"); store.put({ clobber: "-", // constant key so will always clobber @@ -559,7 +559,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { } public async saveToDeviceBatches(batches: ToDeviceBatchWithTxnId[]): Promise { - const txn = this.db.transaction(["to_device_queue"], "readwrite"); + const txn = this.db!.transaction(["to_device_queue"], "readwrite"); const store = txn.objectStore("to_device_queue"); for (const batch of batches) { store.add(batch); @@ -568,7 +568,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { } public async getOldestToDeviceBatch(): Promise { - const txn = this.db.transaction(["to_device_queue"], "readonly"); + const txn = this.db!.transaction(["to_device_queue"], "readonly"); const store = txn.objectStore("to_device_queue"); const cursor = await reqAsCursorPromise(store.openCursor()); if (!cursor) return null; @@ -584,7 +584,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { } public async removeToDeviceBatch(id: number): Promise { - const txn = this.db.transaction(["to_device_queue"], "readwrite"); + const txn = this.db!.transaction(["to_device_queue"], "readwrite"); const store = txn.objectStore("to_device_queue"); store.delete(id); await txnAsPromise(txn); diff --git a/src/store/indexeddb-remote-backend.ts b/src/store/indexeddb-remote-backend.ts index d36404b90..025b3755f 100644 --- a/src/store/indexeddb-remote-backend.ts +++ b/src/store/indexeddb-remote-backend.ts @@ -23,13 +23,13 @@ import { IIndexedDBBackend, UserTuple } from "./indexeddb-backend"; import { IndexedToDeviceBatch, ToDeviceBatchWithTxnId } from "../models/ToDeviceMessage"; export class RemoteIndexedDBStoreBackend implements IIndexedDBBackend { - private worker: Worker; + private worker?: Worker; private nextSeq = 0; // The currently in-flight requests to the actual backend private inFlight: Record> = {}; // seq: promise // Once we start connecting, we keep the promise and re-use it // if we try to connect again - private startPromise: Promise = null; + private startPromise?: Promise; /** * An IndexedDB store backend where the actual backend sits in a web @@ -44,7 +44,7 @@ export class RemoteIndexedDBStoreBackend implements IIndexedDBBackend { */ constructor( private readonly workerFactory: () => Worker, - private readonly dbName: string, + private readonly dbName?: string, ) {} /** @@ -147,12 +147,12 @@ export class RemoteIndexedDBStoreBackend implements IIndexedDBBackend { } private ensureStarted(): Promise { - if (this.startPromise === null) { + if (!this.startPromise) { this.worker = this.workerFactory(); this.worker.onmessage = this.onWorkerMessage; // tell the worker the db name. - this.startPromise = this.doCmd('_setupWorker', [this.dbName]).then(() => { + this.startPromise = this.doCmd('setupWorker', [this.dbName]).then(() => { logger.log("IndexedDB worker is ready"); }); } @@ -168,7 +168,7 @@ export class RemoteIndexedDBStoreBackend implements IIndexedDBBackend { this.inFlight[seq] = def; - this.worker.postMessage({ command, seq, args }); + this.worker?.postMessage({ command, seq, args }); return def.promise; }); diff --git a/src/store/indexeddb-store-worker.ts b/src/store/indexeddb-store-worker.ts index ced776961..691a21f86 100644 --- a/src/store/indexeddb-store-worker.ts +++ b/src/store/indexeddb-store-worker.ts @@ -20,7 +20,7 @@ import { logger } from '../logger'; interface ICmd { command: string; seq: number; - args?: any[]; + args: any[]; } /** @@ -39,7 +39,7 @@ interface ICmd { * */ export class IndexedDBStoreWorker { - private backend: LocalIndexedDBStoreBackend = null; + private backend?: LocalIndexedDBStoreBackend; /** * @param {function} postMessage The web worker postMessage function that @@ -58,59 +58,59 @@ export class IndexedDBStoreWorker { let prom; switch (msg.command) { - case '_setupWorker': + case 'setupWorker': // this is the 'indexedDB' global (where global != window // because it's a web worker and there is no window). this.backend = new LocalIndexedDBStoreBackend(indexedDB, msg.args[0]); prom = Promise.resolve(); break; case 'connect': - prom = this.backend.connect(); + prom = this.backend?.connect(); break; case 'isNewlyCreated': - prom = this.backend.isNewlyCreated(); + prom = this.backend?.isNewlyCreated(); break; case 'clearDatabase': - prom = this.backend.clearDatabase(); + prom = this.backend?.clearDatabase(); break; case 'getSavedSync': - prom = this.backend.getSavedSync(false); + prom = this.backend?.getSavedSync(false); break; case 'setSyncData': - prom = this.backend.setSyncData(msg.args[0]); + prom = this.backend?.setSyncData(msg.args[0]); break; case 'syncToDatabase': - prom = this.backend.syncToDatabase(msg.args[0]); + prom = this.backend?.syncToDatabase(msg.args[0]); break; case 'getUserPresenceEvents': - prom = this.backend.getUserPresenceEvents(); + prom = this.backend?.getUserPresenceEvents(); break; case 'getNextBatchToken': - prom = this.backend.getNextBatchToken(); + prom = this.backend?.getNextBatchToken(); break; case 'getOutOfBandMembers': - prom = this.backend.getOutOfBandMembers(msg.args[0]); + prom = this.backend?.getOutOfBandMembers(msg.args[0]); break; case 'clearOutOfBandMembers': - prom = this.backend.clearOutOfBandMembers(msg.args[0]); + prom = this.backend?.clearOutOfBandMembers(msg.args[0]); break; case 'setOutOfBandMembers': - prom = this.backend.setOutOfBandMembers(msg.args[0], msg.args[1]); + prom = this.backend?.setOutOfBandMembers(msg.args[0], msg.args[1]); break; case 'getClientOptions': - prom = this.backend.getClientOptions(); + prom = this.backend?.getClientOptions(); break; case 'storeClientOptions': - prom = this.backend.storeClientOptions(msg.args[0]); + prom = this.backend?.storeClientOptions(msg.args[0]); break; case 'saveToDeviceBatches': - prom = this.backend.saveToDeviceBatches(msg.args[0]); + prom = this.backend?.saveToDeviceBatches(msg.args[0]); break; case 'getOldestToDeviceBatch': - prom = this.backend.getOldestToDeviceBatch(); + prom = this.backend?.getOldestToDeviceBatch(); break; case 'removeToDeviceBatch': - prom = this.backend.removeToDeviceBatch(msg.args[0]); + prom = this.backend?.removeToDeviceBatch(msg.args[0]); break; } diff --git a/src/store/indexeddb.ts b/src/store/indexeddb.ts index 2bc4f28f6..bb5dc1f16 100644 --- a/src/store/indexeddb.ts +++ b/src/store/indexeddb.ts @@ -155,7 +155,7 @@ export class IndexedDBStore extends MemoryStore { * client state to where it was at the last save, or null if there * is no saved sync data. */ - public getSavedSync = this.degradable((): Promise => { + public getSavedSync = this.degradable((): Promise => { return this.backend.getSavedSync(); }, "getSavedSync"); @@ -292,16 +292,16 @@ export class IndexedDBStore extends MemoryStore { */ private degradable, R = void>( func: DegradableFn, - fallback?: string, + fallback?: keyof MemoryStore, ): DegradableFn { - const fallbackFn = super[fallback]; + const fallbackFn = fallback ? super[fallback] as Function : null; return async (...args) => { try { return await func.call(this, ...args); } catch (e) { logger.error("IndexedDBStore failure, degrading to MemoryStore", e); - this.emitter.emit("degraded", e); + this.emitter.emit("degraded", e as Error); try { // We try to delete IndexedDB after degrading since this store is only a // cache (the app will still function correctly without the data). diff --git a/src/store/memory.ts b/src/store/memory.ts index b44f24ca4..061b1336a 100644 --- a/src/store/memory.ts +++ b/src/store/memory.ts @@ -32,7 +32,7 @@ import { ISyncResponse } from "../sync-accumulator"; import { IStateEventWithRoomId } from "../@types/search"; import { IndexedToDeviceBatch, ToDeviceBatchWithTxnId } from "../models/ToDeviceMessage"; -function isValidFilterId(filterId: string): boolean { +function isValidFilterId(filterId?: string | number | null): boolean { const isValidStr = typeof filterId === "string" && !!filterId && filterId !== "undefined" && // exclude these as we've serialized undefined in localStorage before @@ -55,13 +55,13 @@ export interface IOpts { export class MemoryStore implements IStore { private rooms: Record = {}; // roomId: Room private users: Record = {}; // userId: User - private syncToken: string = null; + private syncToken: string | null = null; // userId: { // filterId: Filter // } private filters: Record> = {}; public accountData: Record = {}; // type : content - protected readonly localStorage: Storage; + protected readonly localStorage?: Storage; private oobMembers: Record = {}; // roomId: [member events] private pendingEvents: { [roomId: string]: Partial[] } = {}; private clientOptions = {}; @@ -115,7 +115,7 @@ export class MemoryStore implements IStore { * @param {RoomState} state * @param {RoomMember} member */ - private onRoomMember = (event: MatrixEvent, state: RoomState, member: RoomMember) => { + private onRoomMember = (event: MatrixEvent | null, state: RoomState, member: RoomMember) => { if (member.membership === "invite") { // We do NOT add invited members because people love to typo user IDs // which would then show up in these lists (!) @@ -126,9 +126,7 @@ export class MemoryStore implements IStore { if (member.name) { user.setDisplayName(member.name); if (member.events.member) { - user.setRawDisplayName( - member.events.member.getDirectionalContent().displayname, - ); + user.setRawDisplayName(member.events.member.getDirectionalContent().displayname); } } if (member.events.member && member.events.member.getContent().avatar_url) { @@ -227,9 +225,7 @@ export class MemoryStore implements IStore { * @param {Filter} filter */ public storeFilter(filter: Filter): void { - if (!filter?.userId) { - return; - } + if (!filter?.userId || !filter?.filterId) return; if (!this.filters[filter.userId]) { this.filters[filter.userId] = {}; } @@ -352,7 +348,7 @@ export class MemoryStore implements IStore { * client state to where it was at the last save, or null if there * is no saved sync data. */ - public getSavedSync(): Promise { + public getSavedSync(): Promise { return Promise.resolve(null); } diff --git a/src/sync.ts b/src/sync.ts index 60fb34aed..c8f2545fd 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -1615,8 +1615,8 @@ export class SyncApi { // For each invited room member we want to give them a displayname/avatar url // if they have one (the m.room.member invites don't contain this). room.getMembersWithMembership("invite").forEach(function(member) { - if (member._requestedProfileInfo) return; - member._requestedProfileInfo = true; + if (member.requestedProfileInfo) return; + member.requestedProfileInfo = true; // try to get a cached copy first. const user = client.getUser(member.userId); let promise; diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index ad98e2079..7ced5744c 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -2210,7 +2210,7 @@ export class MatrixCall extends TypedEventEmitter {