Skip to content

Commit

Permalink
first cut poll model
Browse files Browse the repository at this point in the history
  • Loading branch information
Kerry Archibald committed Dec 30, 2022
1 parent 61e2606 commit 5ca533c
Show file tree
Hide file tree
Showing 3 changed files with 171 additions and 1 deletion.
3 changes: 3 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9372,6 +9372,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
if (!room) return;

room.currentState.processBeaconEvents(events, this);

// @TODO(kerrya) obviously
room.processPollEvents(events, this);
}

/**
Expand Down
142 changes: 142 additions & 0 deletions src/models/poll.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import { M_POLL_END, M_POLL_RESPONSE, M_POLL_START, PollStartEvent } from "matrix-events-sdk";
import { MatrixClient } from "..";
import { MPollEventContent } from "../@types/poll";
import { MatrixEvent } from "./event";
import { RelatedRelations } from "./related-relations";
import { Relations } from "./relations";
import { TypedEventEmitter } from "./typed-event-emitter";

export enum PollEvent {
New = "Poll.new",
Update = "Poll.update",
Responses = "Poll.Responses",
Destroy = "Poll.Destroy",
}

export type PollEventHandlerMap = {
[PollEvent.Update]: (event: MatrixEvent, poll: Poll) => void;
[PollEvent.Destroy]: (pollIdentifier: string) => void;
[PollEvent.Destroy]: (pollIdentifier: string) => void;
[PollEvent.Responses]: (responses: Relations) => void;
};

export const isTimestampInDuration = (startTimestamp: number, durationMs: number, timestamp: number): boolean =>
timestamp >= startTimestamp && startTimestamp + durationMs >= timestamp;

// poll info events are uniquely identified by
// `<roomId>_<state_key>`
export type PollIdentifier = string;
export const getPollInfoIdentifier = (event: MatrixEvent): PollIdentifier =>
`${event.getRoomId()}_${event.getStateKey()}`;

// /~https://github.com/matrix-org/matrix-spec-proposals/pull/3672
export class Poll extends TypedEventEmitter<Exclude<PollEvent, PollEvent.New>, PollEventHandlerMap> {
public readonly roomId: string;
private pollEvent: PollStartEvent | undefined;
private fetchingResponsesPromise: null | Promise<void> = null;
private responses: null | Relations = null;
private closeEvent: MatrixEvent | undefined;

public constructor(private rootEvent: MatrixEvent, private matrixClient: MatrixClient) {
super();
this.roomId = this.rootEvent.getRoomId()!;
this.setPollInstance(this.rootEvent);
}

public get pollId(): string {
return this.rootEvent.getId()!;
}

public get isEnded(): boolean {
// @TODO(kerrya) should be false while responses are loading?
return !!this.closeEvent;
}

public setPollInstance(event: MatrixEvent): void {
this.pollEvent = event.unstableExtensibleEvent as PollStartEvent;
}

public getPollInstance(): PollStartEvent {
return this.pollEvent!;
}

public update(pollInfoEvent: MatrixEvent): void {

}

public destroy(): void {
}

public async getResponses(): Promise<Relations> {
// if we have already fetched the responses
// just return them
if (this.responses) {
return this.responses;
}
if (!this.fetchingResponsesPromise) {
this.fetchingResponsesPromise = this.fetchResponses();
}
await this.fetchingResponsesPromise;
return this.responses!;
}

private async fetchResponses(): Promise<void> {
this.fetchingResponsesPromise = new Promise<void>(() => {});

// we want:
// - stable and unstable M_POLL_RESPONSE
// - stable and unstable M_POLL_END
// 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',
)

console.log('hhh', { allRelations });

// @TODO(kerrya) paging results

const responses = new Relations('m.reference', M_POLL_RESPONSE.name, this.matrixClient);
let undecryptableRelationsCount = 0;

const pollCloseEvent = allRelations.events.find(event => M_POLL_END.matches(event.getType()));
const pollCloseTimestamp = pollCloseEvent?.getTs() || Number.MAX_SAFE_INTEGER;

allRelations.events.forEach(event => {
if (event.isDecryptionFailure()) {
undecryptableRelationsCount++
return;
}
if (
M_POLL_RESPONSE.matches(event.getType()) &&
// response made before poll closed
event.getTs() <= pollCloseTimestamp
) {
responses.addEvent(event);
}
})

console.log('hhh', 'relations!!', responses);

this.responses = responses;
this.closeEvent = pollCloseEvent;
this.emit(PollEvent.Responses, this.responses);
}
}
27 changes: 26 additions & 1 deletion src/models/room.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import { Optional } from "matrix-events-sdk";
import { M_POLL_START, Optional } from "matrix-events-sdk";

import {
EventTimelineSet,
Expand Down Expand Up @@ -59,6 +59,7 @@ import { IStateEventWithRoomId } from "../@types/search";
import { RelationsContainer } from "./relations-container";
import { ReadReceipt, synthesizeReceipt } from "./read-receipt";
import { Feature, ServerSupport } from "../feature";
import { Poll, PollEvent } from "./poll";

// These constants are used as sane defaults when the homeserver doesn't support
// the m.room_versions capability. In practice, KNOWN_SAFE_ROOM_VERSION should be
Expand Down Expand Up @@ -304,6 +305,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
private readonly threadNotifications = new Map<string, NotificationCount>();
public readonly cachedThreadReadReceipts = new Map<string, { event: MatrixEvent; synthetic: boolean }[]>();
private readonly timelineSets: EventTimelineSet[];
public readonly polls: Map<string, Poll> = new Map<string, Poll>();
public readonly threadsTimelineSets: EventTimelineSet[] = [];
// any filtered timeline sets we're maintaining for this room
private readonly filteredTimelineSets: Record<string, EventTimelineSet> = {}; // filter_id: timelineSet
Expand Down Expand Up @@ -1867,6 +1869,29 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
this.threadsReady = true;
}

public processPollEvents(events: MatrixEvent[], matrixClient: MatrixClient): void {
const processPollStartEvent = (event: MatrixEvent) => {
if (!M_POLL_START.matches(event.getType())) return;
const poll = new Poll(event, matrixClient);
this.polls.set(event.getId()!, poll);
console.log('hhh processPollEvents', this.roomId, { poll });
this.emit(PollEvent.New, poll);
}

events.forEach((event: MatrixEvent) => {
matrixClient.decryptEventIfNeeded(event);

if (event.isBeingDecrypted() || event.isDecryptionFailure()) {
// add an event listener for once the event is decrypted.
event.once(MatrixEventEvent.Decrypted, async () => {
processPollStartEvent(event);
});
} else {
processPollStartEvent(event);
}
});
}

/**
* Fetch a single page of threadlist messages for the specific thread filter
* @internal
Expand Down

0 comments on commit 5ca533c

Please sign in to comment.