diff --git a/spec/unit/models/poll.spec.ts b/spec/unit/models/poll.spec.ts index 245d5646501..2acd405fbee 100644 --- a/spec/unit/models/poll.spec.ts +++ b/spec/unit/models/poll.spec.ts @@ -132,7 +132,7 @@ describe("Poll", () => { }); it("filters relations for relevent response events", async () => { - const replyEvent = new MatrixEvent({ type: "m.room.message" }); + const replyEvent = makeRelatedEvent({ type: "m.room.message" }); const stableResponseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.stable! }); const unstableResponseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.unstable }); @@ -188,6 +188,47 @@ describe("Poll", () => { }); }); + describe("undecryptable relations", () => { + it("counts undecryptable relation events when getting responses", async () => { + const replyEvent = makeRelatedEvent({ type: "m.room.message" }); + const stableResponseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.stable! }); + const undecryptableEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.unstable }); + jest.spyOn(undecryptableEvent, "isDecryptionFailure").mockReturnValue(true); + + mockClient.relations.mockResolvedValue({ + events: [replyEvent, stableResponseEvent, undecryptableEvent], + }); + const poll = new Poll(basePollStartEvent, mockClient, room); + jest.spyOn(poll, "emit"); + await poll.getResponses(); + expect(poll.undecryptableRelationsCount).toBe(1); + expect(poll.emit).toHaveBeenCalledWith(PollEvent.UndecryptableRelations, 1); + }); + + it("adds to undercryptable event count when new relation is undecryptable", async () => { + const replyEvent = makeRelatedEvent({ type: "m.room.message" }); + const stableResponseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.stable! }); + const undecryptableEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.unstable }); + const undecryptableEvent2 = makeRelatedEvent({ type: M_POLL_RESPONSE.unstable }); + jest.spyOn(undecryptableEvent, "isDecryptionFailure").mockReturnValue(true); + jest.spyOn(undecryptableEvent2, "isDecryptionFailure").mockReturnValue(true); + + mockClient.relations.mockResolvedValue({ + events: [replyEvent, stableResponseEvent, undecryptableEvent], + }); + const poll = new Poll(basePollStartEvent, mockClient, room); + jest.spyOn(poll, "emit"); + await poll.getResponses(); + expect(poll.undecryptableRelationsCount).toBe(1); + + await poll.onNewRelation(undecryptableEvent2); + + expect(poll.undecryptableRelationsCount).toBe(2); + + expect(poll.emit).toHaveBeenCalledWith(PollEvent.UndecryptableRelations, 2); + }); + }); + describe("with poll end event", () => { const stablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.stable!, sender: "@bob@server.org" }); const unstablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.unstable!, sender: "@bob@server.org" }); diff --git a/src/models/poll.ts b/src/models/poll.ts index 8a080ef9015..1d4344a901c 100644 --- a/src/models/poll.ts +++ b/src/models/poll.ts @@ -28,6 +28,7 @@ export enum PollEvent { Update = "Poll.update", Responses = "Poll.Responses", Destroy = "Poll.Destroy", + UndecryptableRelations = "Poll.UndecryptableRelations", } export type PollEventHandlerMap = { @@ -35,6 +36,7 @@ export type PollEventHandlerMap = { [PollEvent.Destroy]: (pollIdentifier: string) => void; [PollEvent.End]: () => void; [PollEvent.Responses]: (responses: Relations) => void; + [PollEvent.UndecryptableRelations]: (count: number) => void; }; const filterResponseRelations = ( @@ -45,7 +47,6 @@ const filterResponseRelations = ( } => { const responseEvents = relationEvents.filter((event) => { if (event.isDecryptionFailure()) { - // @TODO(kerrya) PSG-1023 track and return these return; } return ( @@ -66,6 +67,11 @@ export class Poll extends TypedEventEmitter, P private relationsNextBatch: string | undefined; private responses: null | Relations = null; private endEvent: MatrixEvent | undefined; + /** + * Keep track of undecryptable relations + * As incomplete result sets affect poll results + */ + private undecryptableRelationEventIds = new Set(); public constructor(public readonly rootEvent: MatrixEvent, private matrixClient: MatrixClient, private room: Room) { super(); @@ -80,6 +86,10 @@ export class Poll extends TypedEventEmitter, P return this.rootEvent.getId()!; } + public get endEventId(): string | undefined { + return this.endEvent?.getId(); + } + public get isEnded(): boolean { return !!this.endEvent; } @@ -88,6 +98,10 @@ export class Poll extends TypedEventEmitter, P return this._isFetchingResponses; } + public get undecryptableRelationsCount(): number { + return this.undecryptableRelationEventIds.size; + } + public async getResponses(): Promise { // if we have already fetched some responses // just return them @@ -124,10 +138,13 @@ export class Poll extends TypedEventEmitter, P const pollEndTimestamp = this.endEvent?.getTs() || Number.MAX_SAFE_INTEGER; const { responseEvents } = filterResponseRelations([event], pollEndTimestamp); + this.countUndecryptableEvents([event]); + if (responseEvents.length) { responseEvents.forEach((event) => { this.responses!.addEvent(event); }); + this.emit(PollEvent.Responses, this.responses); } } @@ -173,6 +190,7 @@ export class Poll extends TypedEventEmitter, P this.relationsNextBatch = allRelations.nextBatch ?? undefined; this.responses = responses; + this.countUndecryptableEvents(allRelations.events); // while there are more pages of relations // fetch them @@ -209,6 +227,19 @@ export class Poll extends TypedEventEmitter, P this.emit(PollEvent.Responses, this.responses); } + private countUndecryptableEvents = (events: MatrixEvent[]): void => { + const undecryptableEventIds = events + .filter((event) => event.isDecryptionFailure()) + .map((event) => event.getId()!); + + const previousCount = this.undecryptableRelationsCount; + this.undecryptableRelationEventIds = new Set([...this.undecryptableRelationEventIds, ...undecryptableEventIds]); + + if (this.undecryptableRelationsCount !== previousCount) { + this.emit(PollEvent.UndecryptableRelations, this.undecryptableRelationsCount); + } + }; + private validateEndEvent(endEvent?: MatrixEvent): boolean { if (!endEvent) { return false;