You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-07-31 15:24:23 +03:00
Poll model - page /relations results (#3073)
* first cut poll model * process incoming poll relations * allow alt event types in relations model * allow alt event types in relations model * remove unneccesary checks on remove relation * comment * Revert "allow alt event types in relations model" This reverts commite578d84464
. * Revert "Revert "allow alt event types in relations model"" This reverts commit515db7a8bc
. * basic handling for new poll relations * tests * test room.processPollEvents * join processBeaconEvents and poll events in client * tidy and set 23 copyrights * use rooms instance of matrixClient * tidy * more copyright * simplify processPollEvent code * throw when poll start event has no roomId * updates for events-sdk move * more type changes for events-sdk changes * page poll relation results * validate poll end event senders * reformatted copyright * undo more comment reformatting * test paging * use correct pollstartevent type * emit after updating _isFetchingResponses state * make rootEvent public readonly * fix poll end validation logic to allow poll creator to end poll regardless of redaction
This commit is contained in:
@ -46,7 +46,7 @@ describe("Poll", () => {
|
|||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
jest.setSystemTime(now);
|
jest.setSystemTime(now);
|
||||||
|
|
||||||
mockClient.relations.mockResolvedValue({ events: [] });
|
mockClient.relations.mockReset().mockResolvedValue({ events: [] });
|
||||||
|
|
||||||
maySendRedactionForEventSpy.mockClear().mockReturnValue(true);
|
maySendRedactionForEventSpy.mockClear().mockReturnValue(true);
|
||||||
});
|
});
|
||||||
@ -95,8 +95,17 @@ describe("Poll", () => {
|
|||||||
it("calls relations api and emits", async () => {
|
it("calls relations api and emits", async () => {
|
||||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||||
const emitSpy = jest.spyOn(poll, "emit");
|
const emitSpy = jest.spyOn(poll, "emit");
|
||||||
const responses = await poll.getResponses();
|
const fetchResponsePromise = poll.getResponses();
|
||||||
expect(mockClient.relations).toHaveBeenCalledWith(roomId, basePollStartEvent.getId(), "m.reference");
|
expect(poll.isFetchingResponses).toBe(true);
|
||||||
|
const responses = await fetchResponsePromise;
|
||||||
|
expect(poll.isFetchingResponses).toBe(false);
|
||||||
|
expect(mockClient.relations).toHaveBeenCalledWith(
|
||||||
|
roomId,
|
||||||
|
basePollStartEvent.getId(),
|
||||||
|
"m.reference",
|
||||||
|
undefined,
|
||||||
|
{ from: undefined },
|
||||||
|
);
|
||||||
expect(emitSpy).toHaveBeenCalledWith(PollEvent.Responses, responses);
|
expect(emitSpy).toHaveBeenCalledWith(PollEvent.Responses, responses);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -133,6 +142,48 @@ describe("Poll", () => {
|
|||||||
expect(responses.getRelations()).toEqual([stableResponseEvent, unstableResponseEvent]);
|
expect(responses.getRelations()).toEqual([stableResponseEvent, unstableResponseEvent]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("with multiple pages of relations", () => {
|
||||||
|
const makeResponses = (count = 1, timestamp = now): MatrixEvent[] =>
|
||||||
|
new Array(count)
|
||||||
|
.fill("x")
|
||||||
|
.map((_x, index) =>
|
||||||
|
makeRelatedEvent(
|
||||||
|
{ type: M_POLL_RESPONSE.stable!, sender: "@bob@server.org" },
|
||||||
|
timestamp + index,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
it("page relations responses", async () => {
|
||||||
|
const responseEvents = makeResponses(6);
|
||||||
|
mockClient.relations
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
events: responseEvents.slice(0, 2),
|
||||||
|
nextBatch: "test-next-1",
|
||||||
|
})
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
events: responseEvents.slice(2, 4),
|
||||||
|
nextBatch: "test-next-2",
|
||||||
|
})
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
events: responseEvents.slice(4),
|
||||||
|
});
|
||||||
|
|
||||||
|
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||||
|
jest.spyOn(poll, "emit");
|
||||||
|
const responses = await poll.getResponses();
|
||||||
|
|
||||||
|
expect(mockClient.relations.mock.calls).toEqual([
|
||||||
|
[roomId, basePollStartEvent.getId(), "m.reference", undefined, { from: undefined }],
|
||||||
|
[roomId, basePollStartEvent.getId(), "m.reference", undefined, { from: "test-next-1" }],
|
||||||
|
[roomId, basePollStartEvent.getId(), "m.reference", undefined, { from: "test-next-2" }],
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(poll.emit).toHaveBeenCalledTimes(3);
|
||||||
|
expect(poll.isFetchingResponses).toBeFalsy();
|
||||||
|
expect(responses.getRelations().length).toEqual(6);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("with poll end event", () => {
|
describe("with poll end event", () => {
|
||||||
const stablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.stable!, sender: "@bob@server.org" });
|
const stablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.stable!, sender: "@bob@server.org" });
|
||||||
const unstablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.unstable!, sender: "@bob@server.org" });
|
const unstablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.unstable!, sender: "@bob@server.org" });
|
||||||
@ -156,17 +207,6 @@ describe("Poll", () => {
|
|||||||
expect(poll.emit).toHaveBeenCalledWith(PollEvent.End);
|
expect(poll.emit).toHaveBeenCalledWith(PollEvent.End);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not set poll end event when sent by a user without redaction rights", async () => {
|
|
||||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
|
||||||
maySendRedactionForEventSpy.mockReturnValue(false);
|
|
||||||
jest.spyOn(poll, "emit");
|
|
||||||
await poll.getResponses();
|
|
||||||
|
|
||||||
expect(maySendRedactionForEventSpy).toHaveBeenCalledWith(basePollStartEvent, "@bob@server.org");
|
|
||||||
expect(poll.isEnded).toBe(false);
|
|
||||||
expect(poll.emit).not.toHaveBeenCalledWith(PollEvent.End);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("sets poll end event when endevent sender also created the poll, but does not have redaction rights", async () => {
|
it("sets poll end event when endevent sender also created the poll, but does not have redaction rights", async () => {
|
||||||
const pollStartEvent = new MatrixEvent({
|
const pollStartEvent = new MatrixEvent({
|
||||||
...PollStartEvent.from("What?", ["a", "b"], M_POLL_KIND_DISCLOSED.name).serialize(),
|
...PollStartEvent.from("What?", ["a", "b"], M_POLL_KIND_DISCLOSED.name).serialize(),
|
||||||
@ -316,6 +356,89 @@ describe("Poll", () => {
|
|||||||
expect(responses.getRelations()).toEqual([responseEventAtEnd, responseEventBeforeEnd]);
|
expect(responses.getRelations()).toEqual([responseEventAtEnd, responseEventBeforeEnd]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not set poll end event when sent by invalid user", async () => {
|
||||||
|
maySendRedactionForEventSpy.mockReturnValue(false);
|
||||||
|
const stablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.stable!, sender: "@charlie:server.org" });
|
||||||
|
const responseEventAfterEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now + 1000);
|
||||||
|
mockClient.relations.mockResolvedValue({
|
||||||
|
events: [responseEventAfterEnd],
|
||||||
|
});
|
||||||
|
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||||
|
await poll.getResponses();
|
||||||
|
jest.spyOn(poll, "emit");
|
||||||
|
|
||||||
|
poll.onNewRelation(stablePollEndEvent);
|
||||||
|
|
||||||
|
// didn't end, didn't refilter responses
|
||||||
|
expect(poll.emit).not.toHaveBeenCalled();
|
||||||
|
expect(poll.isEnded).toBeFalsy();
|
||||||
|
expect(maySendRedactionForEventSpy).toHaveBeenCalledWith(basePollStartEvent, "@charlie:server.org");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not set poll end event when an earlier end event already exists", async () => {
|
||||||
|
const earlierPollEndEvent = makeRelatedEvent(
|
||||||
|
{ type: M_POLL_END.stable!, sender: "@valid:server.org" },
|
||||||
|
now,
|
||||||
|
);
|
||||||
|
const laterPollEndEvent = makeRelatedEvent(
|
||||||
|
{ type: M_POLL_END.stable!, sender: "@valid:server.org" },
|
||||||
|
now + 2000,
|
||||||
|
);
|
||||||
|
|
||||||
|
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||||
|
await poll.getResponses();
|
||||||
|
|
||||||
|
poll.onNewRelation(earlierPollEndEvent);
|
||||||
|
|
||||||
|
// first end event set correctly
|
||||||
|
expect(poll.isEnded).toBeTruthy();
|
||||||
|
|
||||||
|
// reset spy count
|
||||||
|
jest.spyOn(poll, "emit").mockClear();
|
||||||
|
|
||||||
|
poll.onNewRelation(laterPollEndEvent);
|
||||||
|
// didn't set new end event, didn't refilter responses
|
||||||
|
expect(poll.emit).not.toHaveBeenCalled();
|
||||||
|
expect(poll.isEnded).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("replaces poll end event and refilters when an older end event already exists", async () => {
|
||||||
|
const earlierPollEndEvent = makeRelatedEvent(
|
||||||
|
{ type: M_POLL_END.stable!, sender: "@valid:server.org" },
|
||||||
|
now,
|
||||||
|
);
|
||||||
|
const laterPollEndEvent = makeRelatedEvent(
|
||||||
|
{ type: M_POLL_END.stable!, sender: "@valid:server.org" },
|
||||||
|
now + 2000,
|
||||||
|
);
|
||||||
|
const responseEventBeforeEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now - 1000);
|
||||||
|
const responseEventAtEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now);
|
||||||
|
const responseEventAfterEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now + 1000);
|
||||||
|
mockClient.relations.mockResolvedValue({
|
||||||
|
events: [responseEventAfterEnd, responseEventAtEnd, responseEventBeforeEnd, laterPollEndEvent],
|
||||||
|
});
|
||||||
|
|
||||||
|
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||||
|
const responses = await poll.getResponses();
|
||||||
|
|
||||||
|
// all responses have a timestamp < laterPollEndEvent
|
||||||
|
expect(responses.getRelations().length).toEqual(3);
|
||||||
|
// first end event set correctly
|
||||||
|
expect(poll.isEnded).toBeTruthy();
|
||||||
|
|
||||||
|
// reset spy count
|
||||||
|
jest.spyOn(poll, "emit").mockClear();
|
||||||
|
|
||||||
|
// add a valid end event with earlier timestamp
|
||||||
|
poll.onNewRelation(earlierPollEndEvent);
|
||||||
|
|
||||||
|
// emitted new end event
|
||||||
|
expect(poll.emit).toHaveBeenCalledWith(PollEvent.End);
|
||||||
|
// filtered responses and emitted
|
||||||
|
expect(poll.emit).toHaveBeenCalledWith(PollEvent.Responses, responses);
|
||||||
|
expect(responses.getRelations()).toEqual([responseEventAtEnd, responseEventBeforeEnd]);
|
||||||
|
});
|
||||||
|
|
||||||
it("sets poll end event and refilters responses based on timestamp", async () => {
|
it("sets poll end event and refilters responses based on timestamp", async () => {
|
||||||
const stablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.stable!, sender: userId });
|
const stablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.stable!, sender: userId });
|
||||||
const responseEventBeforeEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now - 1000);
|
const responseEventBeforeEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now - 1000);
|
||||||
|
@ -14,8 +14,9 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { M_POLL_END, M_POLL_RESPONSE, PollStartEvent } from "../@types/polls";
|
import { M_POLL_END, M_POLL_RESPONSE } from "../@types/polls";
|
||||||
import { MatrixClient } from "../client";
|
import { MatrixClient } from "../client";
|
||||||
|
import { PollStartEvent } from "../extensible_events_v1/PollStartEvent";
|
||||||
import { MatrixEvent } from "./event";
|
import { MatrixEvent } from "./event";
|
||||||
import { Relations } from "./relations";
|
import { Relations } from "./relations";
|
||||||
import { Room } from "./room";
|
import { Room } from "./room";
|
||||||
@ -61,11 +62,12 @@ const filterResponseRelations = (
|
|||||||
export class Poll extends TypedEventEmitter<Exclude<PollEvent, PollEvent.New>, PollEventHandlerMap> {
|
export class Poll extends TypedEventEmitter<Exclude<PollEvent, PollEvent.New>, PollEventHandlerMap> {
|
||||||
public readonly roomId: string;
|
public readonly roomId: string;
|
||||||
public readonly pollEvent: PollStartEvent;
|
public readonly pollEvent: PollStartEvent;
|
||||||
private fetchingResponsesPromise: null | Promise<void> = null;
|
private _isFetchingResponses = false;
|
||||||
|
private relationsNextBatch: string | undefined;
|
||||||
private responses: null | Relations = null;
|
private responses: null | Relations = null;
|
||||||
private endEvent: MatrixEvent | undefined;
|
private endEvent: MatrixEvent | undefined;
|
||||||
|
|
||||||
public constructor(private rootEvent: MatrixEvent, private matrixClient: MatrixClient, private room: Room) {
|
public constructor(public readonly rootEvent: MatrixEvent, private matrixClient: MatrixClient, private room: Room) {
|
||||||
super();
|
super();
|
||||||
if (!this.rootEvent.getRoomId() || !this.rootEvent.getId()) {
|
if (!this.rootEvent.getRoomId() || !this.rootEvent.getId()) {
|
||||||
throw new Error("Invalid poll start event.");
|
throw new Error("Invalid poll start event.");
|
||||||
@ -82,16 +84,23 @@ export class Poll extends TypedEventEmitter<Exclude<PollEvent, PollEvent.New>, P
|
|||||||
return !!this.endEvent;
|
return !!this.endEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get isFetchingResponses(): boolean {
|
||||||
|
return this._isFetchingResponses;
|
||||||
|
}
|
||||||
|
|
||||||
public async getResponses(): Promise<Relations> {
|
public async getResponses(): Promise<Relations> {
|
||||||
// if we have already fetched the responses
|
// if we have already fetched some responses
|
||||||
// just return them
|
// just return them
|
||||||
if (this.responses) {
|
if (this.responses) {
|
||||||
return this.responses;
|
return this.responses;
|
||||||
}
|
}
|
||||||
if (!this.fetchingResponsesPromise) {
|
|
||||||
this.fetchingResponsesPromise = this.fetchResponses();
|
// if there is no fetching in progress
|
||||||
|
// start fetching
|
||||||
|
if (!this.isFetchingResponses) {
|
||||||
|
await this.fetchResponses();
|
||||||
}
|
}
|
||||||
await this.fetchingResponsesPromise;
|
// return whatever responses we got from the first page
|
||||||
return this.responses!;
|
return this.responses!;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,21 +133,34 @@ export class Poll extends TypedEventEmitter<Exclude<PollEvent, PollEvent.New>, P
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async fetchResponses(): Promise<void> {
|
private async fetchResponses(): Promise<void> {
|
||||||
|
this._isFetchingResponses = true;
|
||||||
|
|
||||||
// we want:
|
// we want:
|
||||||
// - stable and unstable M_POLL_RESPONSE
|
// - stable and unstable M_POLL_RESPONSE
|
||||||
// - stable and unstable M_POLL_END
|
// - stable and unstable M_POLL_END
|
||||||
// so make one api call and filter by event type client side
|
// so make one api call and filter by event type client side
|
||||||
const allRelations = await this.matrixClient.relations(this.roomId, this.rootEvent.getId()!, "m.reference");
|
const allRelations = await this.matrixClient.relations(
|
||||||
|
this.roomId,
|
||||||
|
this.rootEvent.getId()!,
|
||||||
|
"m.reference",
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
from: this.relationsNextBatch || undefined,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// @TODO(kerrya) paging results
|
const responses =
|
||||||
|
this.responses ||
|
||||||
|
new Relations("m.reference", M_POLL_RESPONSE.name, this.matrixClient, [M_POLL_RESPONSE.altName!]);
|
||||||
|
|
||||||
const responses = new Relations("m.reference", M_POLL_RESPONSE.name, this.matrixClient, [
|
const pollEndEvent = allRelations.events.find((event) => M_POLL_END.matches(event.getType()));
|
||||||
M_POLL_RESPONSE.altName!,
|
if (this.validateEndEvent(pollEndEvent)) {
|
||||||
]);
|
this.endEvent = pollEndEvent;
|
||||||
|
this.refilterResponsesOnEnd();
|
||||||
|
this.emit(PollEvent.End);
|
||||||
|
}
|
||||||
|
|
||||||
const potentialEndEvent = allRelations.events.find((event) => M_POLL_END.matches(event.getType()));
|
const pollCloseTimestamp = this.endEvent?.getTs() || Number.MAX_SAFE_INTEGER;
|
||||||
const pollEndEvent = this.validateEndEvent(potentialEndEvent) ? potentialEndEvent : undefined;
|
|
||||||
const pollCloseTimestamp = pollEndEvent?.getTs() || Number.MAX_SAFE_INTEGER;
|
|
||||||
|
|
||||||
const { responseEvents } = filterResponseRelations(allRelations.events, pollCloseTimestamp);
|
const { responseEvents } = filterResponseRelations(allRelations.events, pollCloseTimestamp);
|
||||||
|
|
||||||
@ -146,11 +168,21 @@ export class Poll extends TypedEventEmitter<Exclude<PollEvent, PollEvent.New>, P
|
|||||||
responses.addEvent(event);
|
responses.addEvent(event);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.relationsNextBatch = allRelations.nextBatch ?? undefined;
|
||||||
this.responses = responses;
|
this.responses = responses;
|
||||||
this.endEvent = pollEndEvent;
|
|
||||||
if (this.endEvent) {
|
// while there are more pages of relations
|
||||||
this.emit(PollEvent.End);
|
// fetch them
|
||||||
|
if (this.relationsNextBatch) {
|
||||||
|
// don't await
|
||||||
|
// we want to return the first page as soon as possible
|
||||||
|
this.fetchResponses();
|
||||||
|
} else {
|
||||||
|
// no more pages
|
||||||
|
this._isFetchingResponses = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// emit after updating _isFetchingResponses state
|
||||||
this.emit(PollEvent.Responses, this.responses);
|
this.emit(PollEvent.Responses, this.responses);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user