use Poll model with relations API in poll rendering (#9877)
* wip * remove dupe * use poll model relations in all cases * update mpollbody tests to use poll instance * update poll fetching login in pinned messages card * add pinned polls to room polls state * add spinner while relations are still loading * handle no poll in end poll dialog * strict errors * strict fix * more strict fix
This commit is contained in:
parent
b45b933a65
commit
544baa30ed
9 changed files with 350 additions and 670 deletions
|
@ -150,8 +150,16 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MPollBody_totalVotes {
|
.mx_MPollBody_totalVotes {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: inline;
|
||||||
|
justify-content: start;
|
||||||
color: $secondary-content;
|
color: $secondary-content;
|
||||||
font-size: $font-12px;
|
font-size: $font-12px;
|
||||||
|
|
||||||
|
.mx_Spinner {
|
||||||
|
flex: 0;
|
||||||
|
margin-left: $spacing-8;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -195,7 +195,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
|
||||||
return (
|
return (
|
||||||
M_POLL_START.matches(mxEvent.getType()) &&
|
M_POLL_START.matches(mxEvent.getType()) &&
|
||||||
this.state.canRedact &&
|
this.state.canRedact &&
|
||||||
!isPollEnded(mxEvent, MatrixClientPeg.get(), this.props.getRelationsForEvent)
|
!isPollEnded(mxEvent, MatrixClientPeg.get())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -35,26 +35,34 @@ interface IProps extends IDialogProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class EndPollDialog extends React.Component<IProps> {
|
export default class EndPollDialog extends React.Component<IProps> {
|
||||||
private onFinished = (endPoll: boolean): void => {
|
private onFinished = async (endPoll: boolean): Promise<void> => {
|
||||||
const topAnswer = findTopAnswer(this.props.event, this.props.matrixClient, this.props.getRelationsForEvent);
|
if (endPoll) {
|
||||||
|
const room = this.props.matrixClient.getRoom(this.props.event.getRoomId());
|
||||||
|
const poll = room?.polls.get(this.props.event.getId()!);
|
||||||
|
|
||||||
|
if (!poll) {
|
||||||
|
throw new Error("No poll instance found in room.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const responses = await poll.getResponses();
|
||||||
|
const topAnswer = findTopAnswer(this.props.event, responses);
|
||||||
|
|
||||||
const message =
|
const message =
|
||||||
topAnswer === ""
|
topAnswer === ""
|
||||||
? _t("The poll has ended. No votes were cast.")
|
? _t("The poll has ended. No votes were cast.")
|
||||||
: _t("The poll has ended. Top answer: %(topAnswer)s", { topAnswer });
|
: _t("The poll has ended. Top answer: %(topAnswer)s", { topAnswer });
|
||||||
|
|
||||||
if (endPoll) {
|
const endEvent = PollEndEvent.from(this.props.event.getId()!, message).serialize();
|
||||||
const endEvent = PollEndEvent.from(this.props.event.getId(), message).serialize();
|
|
||||||
|
|
||||||
this.props.matrixClient
|
await this.props.matrixClient.sendEvent(this.props.event.getRoomId()!, endEvent.type, endEvent.content);
|
||||||
.sendEvent(this.props.event.getRoomId(), endEvent.type, endEvent.content)
|
} catch (e) {
|
||||||
.catch((e: any) => {
|
|
||||||
console.error("Failed to submit poll response event:", e);
|
console.error("Failed to submit poll response event:", e);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: _t("Failed to end poll"),
|
title: _t("Failed to end poll"),
|
||||||
description: _t("Sorry, the poll did not end. Please try again."),
|
description: _t("Sorry, the poll did not end. Please try again."),
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
this.props.onFinished(endPoll);
|
this.props.onFinished(endPoll);
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,17 +14,17 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React, { ReactNode } from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/models/event";
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
import { Relations, RelationsEvent } from "matrix-js-sdk/src/models/relations";
|
import { Relations } from "matrix-js-sdk/src/models/relations";
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
import { M_POLL_END, M_POLL_KIND_DISCLOSED, M_POLL_RESPONSE, M_POLL_START } from "matrix-js-sdk/src/@types/polls";
|
import { M_POLL_KIND_DISCLOSED, M_POLL_RESPONSE, M_POLL_START } from "matrix-js-sdk/src/@types/polls";
|
||||||
import { RelatedRelations } from "matrix-js-sdk/src/models/related-relations";
|
import { RelatedRelations } from "matrix-js-sdk/src/models/related-relations";
|
||||||
import { NamespacedValue } from "matrix-events-sdk";
|
|
||||||
import { PollStartEvent, PollAnswerSubevent } from "matrix-js-sdk/src/extensible_events_v1/PollStartEvent";
|
import { PollStartEvent, PollAnswerSubevent } from "matrix-js-sdk/src/extensible_events_v1/PollStartEvent";
|
||||||
import { PollResponseEvent } from "matrix-js-sdk/src/extensible_events_v1/PollResponseEvent";
|
import { PollResponseEvent } from "matrix-js-sdk/src/extensible_events_v1/PollResponseEvent";
|
||||||
|
import { Poll, PollEvent } from "matrix-js-sdk/src/models/poll";
|
||||||
|
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
|
@ -36,11 +36,14 @@ import ErrorDialog from "../dialogs/ErrorDialog";
|
||||||
import { GetRelationsForEvent } from "../rooms/EventTile";
|
import { GetRelationsForEvent } from "../rooms/EventTile";
|
||||||
import PollCreateDialog from "../elements/PollCreateDialog";
|
import PollCreateDialog from "../elements/PollCreateDialog";
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
|
import Spinner from "../elements/Spinner";
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
|
poll?: Poll;
|
||||||
|
// poll instance has fetched at least one page of responses
|
||||||
|
pollInitialised: boolean;
|
||||||
selected?: string | null | undefined; // Which option was clicked by the local user
|
selected?: string | null | undefined; // Which option was clicked by the local user
|
||||||
voteRelations: RelatedRelations; // Voting (response) events
|
voteRelations?: Relations; // Voting (response) events
|
||||||
endRelations: RelatedRelations; // Poll end events
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createVoteRelations(getRelationsForEvent: GetRelationsForEvent, eventId: string): RelatedRelations {
|
export function createVoteRelations(getRelationsForEvent: GetRelationsForEvent, eventId: string): RelatedRelations {
|
||||||
|
@ -59,15 +62,7 @@ export function createVoteRelations(getRelationsForEvent: GetRelationsForEvent,
|
||||||
return new RelatedRelations(relationsList);
|
return new RelatedRelations(relationsList);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function findTopAnswer(
|
export function findTopAnswer(pollEvent: MatrixEvent, voteRelations: Relations): string {
|
||||||
pollEvent: MatrixEvent,
|
|
||||||
matrixClient: MatrixClient,
|
|
||||||
getRelationsForEvent?: GetRelationsForEvent,
|
|
||||||
): string {
|
|
||||||
if (!getRelationsForEvent) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const pollEventId = pollEvent.getId();
|
const pollEventId = pollEvent.getId();
|
||||||
if (!pollEventId) {
|
if (!pollEventId) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
|
@ -87,25 +82,7 @@ export function findTopAnswer(
|
||||||
return poll.answers.find((a) => a.id === answerId)?.text ?? "";
|
return poll.answers.find((a) => a.id === answerId)?.text ?? "";
|
||||||
};
|
};
|
||||||
|
|
||||||
const voteRelations = createVoteRelations(getRelationsForEvent, pollEventId);
|
const userVotes: Map<string, UserVote> = collectUserVotes(allVotes(voteRelations));
|
||||||
|
|
||||||
const relationsList: Relations[] = [];
|
|
||||||
|
|
||||||
const pollEndRelations = getRelationsForEvent(pollEventId, "m.reference", M_POLL_END.name);
|
|
||||||
if (pollEndRelations) {
|
|
||||||
relationsList.push(pollEndRelations);
|
|
||||||
}
|
|
||||||
|
|
||||||
const pollEndAltRelations = getRelationsForEvent(pollEventId, "m.reference", M_POLL_END.altName);
|
|
||||||
if (pollEndAltRelations) {
|
|
||||||
relationsList.push(pollEndAltRelations);
|
|
||||||
}
|
|
||||||
|
|
||||||
const endRelations = new RelatedRelations(relationsList);
|
|
||||||
|
|
||||||
const userVotes: Map<string, UserVote> = collectUserVotes(
|
|
||||||
allVotes(pollEvent, matrixClient, voteRelations, endRelations),
|
|
||||||
);
|
|
||||||
|
|
||||||
const votes: Map<string, number> = countVotes(userVotes, poll);
|
const votes: Map<string, number> = countVotes(userVotes, poll);
|
||||||
const highestScore: number = Math.max(...votes.values());
|
const highestScore: number = Math.max(...votes.values());
|
||||||
|
@ -122,62 +99,13 @@ export function findTopAnswer(
|
||||||
return formatCommaSeparatedList(bestAnswerTexts, 3);
|
return formatCommaSeparatedList(bestAnswerTexts, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isPollEnded(
|
export function isPollEnded(pollEvent: MatrixEvent, matrixClient: MatrixClient): boolean {
|
||||||
pollEvent: MatrixEvent,
|
const room = matrixClient.getRoom(pollEvent.getRoomId());
|
||||||
matrixClient: MatrixClient,
|
const poll = room?.polls.get(pollEvent.getId()!);
|
||||||
getRelationsForEvent?: GetRelationsForEvent,
|
if (!poll || poll.isFetchingResponses) {
|
||||||
): boolean {
|
|
||||||
if (!getRelationsForEvent) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
return poll.isEnded;
|
||||||
const pollEventId = pollEvent.getId();
|
|
||||||
if (!pollEventId) {
|
|
||||||
logger.warn(
|
|
||||||
"isPollEnded: Poll event must have event ID in order to determine whether it has ended " +
|
|
||||||
"- assuming poll has not ended",
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const roomId = pollEvent.getRoomId();
|
|
||||||
if (!roomId) {
|
|
||||||
logger.warn(
|
|
||||||
"isPollEnded: Poll event must have room ID in order to determine whether it has ended " +
|
|
||||||
"- assuming poll has not ended",
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const roomCurrentState = matrixClient.getRoom(roomId)?.currentState;
|
|
||||||
function userCanRedact(endEvent: MatrixEvent): boolean {
|
|
||||||
const endEventSender = endEvent.getSender();
|
|
||||||
return (
|
|
||||||
endEventSender && roomCurrentState && roomCurrentState.maySendRedactionForEvent(pollEvent, endEventSender)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const relationsList: Relations[] = [];
|
|
||||||
|
|
||||||
const pollEndRelations = getRelationsForEvent(pollEventId, "m.reference", M_POLL_END.name);
|
|
||||||
if (pollEndRelations) {
|
|
||||||
relationsList.push(pollEndRelations);
|
|
||||||
}
|
|
||||||
|
|
||||||
const pollEndAltRelations = getRelationsForEvent(pollEventId, "m.reference", M_POLL_END.altName);
|
|
||||||
if (pollEndAltRelations) {
|
|
||||||
relationsList.push(pollEndAltRelations);
|
|
||||||
}
|
|
||||||
|
|
||||||
const endRelations = new RelatedRelations(relationsList);
|
|
||||||
|
|
||||||
if (!endRelations) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const authorisedRelations = endRelations.getRelations().filter(userCanRedact);
|
|
||||||
|
|
||||||
return authorisedRelations.length > 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function pollAlreadyHasVotes(mxEvent: MatrixEvent, getRelationsForEvent?: GetRelationsForEvent): boolean {
|
export function pollAlreadyHasVotes(mxEvent: MatrixEvent, getRelationsForEvent?: GetRelationsForEvent): boolean {
|
||||||
|
@ -215,75 +143,58 @@ export default class MPollBody extends React.Component<IBodyProps, IState> {
|
||||||
public static contextType = MatrixClientContext;
|
public static contextType = MatrixClientContext;
|
||||||
public context!: React.ContextType<typeof MatrixClientContext>;
|
public context!: React.ContextType<typeof MatrixClientContext>;
|
||||||
private seenEventIds: string[] = []; // Events we have already seen
|
private seenEventIds: string[] = []; // Events we have already seen
|
||||||
private voteRelationsReceived = false;
|
|
||||||
private endRelationsReceived = false;
|
|
||||||
|
|
||||||
public constructor(props: IBodyProps) {
|
public constructor(props: IBodyProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
selected: null,
|
selected: null,
|
||||||
voteRelations: this.fetchVoteRelations(),
|
pollInitialised: false,
|
||||||
endRelations: this.fetchEndRelations(),
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
this.addListeners(this.state.voteRelations, this.state.endRelations);
|
public componentDidMount(): void {
|
||||||
this.props.mxEvent.on(MatrixEventEvent.RelationsCreated, this.onRelationsCreated);
|
const room = this.context.getRoom(this.props.mxEvent.getRoomId());
|
||||||
|
const poll = room?.polls.get(this.props.mxEvent.getId()!);
|
||||||
|
if (poll) {
|
||||||
|
this.setPollInstance(poll);
|
||||||
|
} else {
|
||||||
|
room?.on(PollEvent.New, this.setPollInstance.bind(this));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount(): void {
|
public componentWillUnmount(): void {
|
||||||
this.props.mxEvent.off(MatrixEventEvent.RelationsCreated, this.onRelationsCreated);
|
this.removeListeners();
|
||||||
this.removeListeners(this.state.voteRelations, this.state.endRelations);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private addListeners(voteRelations?: RelatedRelations, endRelations?: RelatedRelations): void {
|
private async setPollInstance(poll: Poll): Promise<void> {
|
||||||
if (voteRelations) {
|
if (poll.pollId !== this.props.mxEvent.getId()) {
|
||||||
voteRelations.on(RelationsEvent.Add, this.onRelationsChange);
|
|
||||||
voteRelations.on(RelationsEvent.Remove, this.onRelationsChange);
|
|
||||||
voteRelations.on(RelationsEvent.Redaction, this.onRelationsChange);
|
|
||||||
}
|
|
||||||
if (endRelations) {
|
|
||||||
endRelations.on(RelationsEvent.Add, this.onRelationsChange);
|
|
||||||
endRelations.on(RelationsEvent.Remove, this.onRelationsChange);
|
|
||||||
endRelations.on(RelationsEvent.Redaction, this.onRelationsChange);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private removeListeners(voteRelations?: RelatedRelations, endRelations?: RelatedRelations): void {
|
|
||||||
if (voteRelations) {
|
|
||||||
voteRelations.off(RelationsEvent.Add, this.onRelationsChange);
|
|
||||||
voteRelations.off(RelationsEvent.Remove, this.onRelationsChange);
|
|
||||||
voteRelations.off(RelationsEvent.Redaction, this.onRelationsChange);
|
|
||||||
}
|
|
||||||
if (endRelations) {
|
|
||||||
endRelations.off(RelationsEvent.Add, this.onRelationsChange);
|
|
||||||
endRelations.off(RelationsEvent.Remove, this.onRelationsChange);
|
|
||||||
endRelations.off(RelationsEvent.Redaction, this.onRelationsChange);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private onRelationsCreated = (relationType: string, eventType: string): void => {
|
|
||||||
if (relationType !== "m.reference") {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this.setState({ poll }, () => {
|
||||||
|
this.addListeners();
|
||||||
|
});
|
||||||
|
const responses = await poll.getResponses();
|
||||||
|
const voteRelations = responses;
|
||||||
|
|
||||||
if (M_POLL_RESPONSE.matches(eventType)) {
|
this.setState({ pollInitialised: true, voteRelations });
|
||||||
this.voteRelationsReceived = true;
|
|
||||||
const newVoteRelations = this.fetchVoteRelations();
|
|
||||||
this.addListeners(newVoteRelations);
|
|
||||||
this.removeListeners(this.state.voteRelations);
|
|
||||||
this.setState({ voteRelations: newVoteRelations });
|
|
||||||
} else if (M_POLL_END.matches(eventType)) {
|
|
||||||
this.endRelationsReceived = true;
|
|
||||||
const newEndRelations = this.fetchEndRelations();
|
|
||||||
this.addListeners(newEndRelations);
|
|
||||||
this.removeListeners(this.state.endRelations);
|
|
||||||
this.setState({ endRelations: newEndRelations });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.voteRelationsReceived && this.endRelationsReceived) {
|
private addListeners(): void {
|
||||||
this.props.mxEvent.removeListener(MatrixEventEvent.RelationsCreated, this.onRelationsCreated);
|
this.state.poll?.on(PollEvent.Responses, this.onResponsesChange);
|
||||||
|
this.state.poll?.on(PollEvent.End, this.onRelationsChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private removeListeners(): void {
|
||||||
|
if (this.state.poll) {
|
||||||
|
this.state.poll.off(PollEvent.Responses, this.onResponsesChange);
|
||||||
|
this.state.poll.off(PollEvent.End, this.onRelationsChange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onResponsesChange = (responses: Relations): void => {
|
||||||
|
this.setState({ voteRelations: responses });
|
||||||
|
this.onRelationsChange();
|
||||||
};
|
};
|
||||||
|
|
||||||
private onRelationsChange = (): void => {
|
private onRelationsChange = (): void => {
|
||||||
|
@ -295,19 +206,19 @@ export default class MPollBody extends React.Component<IBodyProps, IState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
private selectOption(answerId: string): void {
|
private selectOption(answerId: string): void {
|
||||||
if (this.isEnded()) {
|
if (this.state.poll?.isEnded) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const userVotes = this.collectUserVotes();
|
const userVotes = this.collectUserVotes();
|
||||||
const userId = this.context.getUserId();
|
const userId = this.context.getSafeUserId();
|
||||||
const myVote = userVotes.get(userId)?.answers[0];
|
const myVote = userVotes.get(userId)?.answers[0];
|
||||||
if (answerId === myVote) {
|
if (answerId === myVote) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = PollResponseEvent.from([answerId], this.props.mxEvent.getId()).serialize();
|
const response = PollResponseEvent.from([answerId], this.props.mxEvent.getId()!).serialize();
|
||||||
|
|
||||||
this.context.sendEvent(this.props.mxEvent.getRoomId(), response.type, response.content).catch((e: any) => {
|
this.context.sendEvent(this.props.mxEvent.getRoomId()!, response.type, response.content).catch((e: any) => {
|
||||||
console.error("Failed to submit poll response event:", e);
|
console.error("Failed to submit poll response event:", e);
|
||||||
|
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
|
@ -323,51 +234,14 @@ export default class MPollBody extends React.Component<IBodyProps, IState> {
|
||||||
this.selectOption(e.currentTarget.value);
|
this.selectOption(e.currentTarget.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
private fetchVoteRelations(): RelatedRelations | null {
|
|
||||||
return this.fetchRelations(M_POLL_RESPONSE);
|
|
||||||
}
|
|
||||||
|
|
||||||
private fetchEndRelations(): RelatedRelations | null {
|
|
||||||
return this.fetchRelations(M_POLL_END);
|
|
||||||
}
|
|
||||||
|
|
||||||
private fetchRelations(eventType: NamespacedValue<string, string>): RelatedRelations | null {
|
|
||||||
if (this.props.getRelationsForEvent) {
|
|
||||||
const relationsList: Relations[] = [];
|
|
||||||
|
|
||||||
const eventId = this.props.mxEvent.getId();
|
|
||||||
if (!eventId) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const relations = this.props.getRelationsForEvent(eventId, "m.reference", eventType.name);
|
|
||||||
if (relations) {
|
|
||||||
relationsList.push(relations);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there is an alternatve experimental event type, also look for that
|
|
||||||
if (eventType.altName) {
|
|
||||||
const altRelations = this.props.getRelationsForEvent(eventId, "m.reference", eventType.altName);
|
|
||||||
if (altRelations) {
|
|
||||||
relationsList.push(altRelations);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new RelatedRelations(relationsList);
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns userId -> UserVote
|
* @returns userId -> UserVote
|
||||||
*/
|
*/
|
||||||
private collectUserVotes(): Map<string, UserVote> {
|
private collectUserVotes(): Map<string, UserVote> {
|
||||||
return collectUserVotes(
|
if (!this.state.voteRelations) {
|
||||||
allVotes(this.props.mxEvent, this.context, this.state.voteRelations, this.state.endRelations),
|
return new Map<string, UserVote>();
|
||||||
this.context.getUserId(),
|
}
|
||||||
this.state.selected,
|
return collectUserVotes(allVotes(this.state.voteRelations), this.context.getUserId(), this.state.selected);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -379,10 +253,10 @@ export default class MPollBody extends React.Component<IBodyProps, IState> {
|
||||||
* have already seen.
|
* have already seen.
|
||||||
*/
|
*/
|
||||||
private unselectIfNewEventFromMe(): void {
|
private unselectIfNewEventFromMe(): void {
|
||||||
const newEvents: MatrixEvent[] = this.state.voteRelations
|
const relations = this.state.voteRelations?.getRelations() || [];
|
||||||
.getRelations()
|
const newEvents: MatrixEvent[] = relations.filter(
|
||||||
.filter(isPollResponse)
|
(mxEvent: MatrixEvent) => !this.seenEventIds.includes(mxEvent.getId()!),
|
||||||
.filter((mxEvent: MatrixEvent) => !this.seenEventIds.includes(mxEvent.getId()!));
|
);
|
||||||
let newSelected = this.state.selected;
|
let newSelected = this.state.selected;
|
||||||
|
|
||||||
if (newEvents.length > 0) {
|
if (newEvents.length > 0) {
|
||||||
|
@ -392,7 +266,7 @@ export default class MPollBody extends React.Component<IBodyProps, IState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const newEventIds = newEvents.map((mxEvent: MatrixEvent) => mxEvent.getId());
|
const newEventIds = newEvents.map((mxEvent: MatrixEvent) => mxEvent.getId()!);
|
||||||
this.seenEventIds = this.seenEventIds.concat(newEventIds);
|
this.seenEventIds = this.seenEventIds.concat(newEventIds);
|
||||||
this.setState({ selected: newSelected });
|
this.setState({ selected: newSelected });
|
||||||
}
|
}
|
||||||
|
@ -405,30 +279,30 @@ export default class MPollBody extends React.Component<IBodyProps, IState> {
|
||||||
return sum;
|
return sum;
|
||||||
}
|
}
|
||||||
|
|
||||||
private isEnded(): boolean {
|
public render(): ReactNode {
|
||||||
return isPollEnded(this.props.mxEvent, this.context, this.props.getRelationsForEvent);
|
const { poll, pollInitialised } = this.state;
|
||||||
|
if (!poll?.pollEvent) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public render(): JSX.Element {
|
const pollEvent = poll.pollEvent;
|
||||||
const poll = this.props.mxEvent.unstableExtensibleEvent as PollStartEvent;
|
|
||||||
if (!poll?.isEquivalentTo(M_POLL_START)) return null; // invalid
|
|
||||||
|
|
||||||
const ended = this.isEnded();
|
const pollId = this.props.mxEvent.getId()!;
|
||||||
const pollId = this.props.mxEvent.getId();
|
const isFetchingResponses = !pollInitialised || poll.isFetchingResponses;
|
||||||
const userVotes = this.collectUserVotes();
|
const userVotes = this.collectUserVotes();
|
||||||
const votes = countVotes(userVotes, poll);
|
const votes = countVotes(userVotes, pollEvent);
|
||||||
const totalVotes = this.totalVotes(votes);
|
const totalVotes = this.totalVotes(votes);
|
||||||
const winCount = Math.max(...votes.values());
|
const winCount = Math.max(...votes.values());
|
||||||
const userId = this.context.getUserId();
|
const userId = this.context.getUserId();
|
||||||
const myVote = userVotes?.get(userId!)?.answers[0];
|
const myVote = userVotes?.get(userId!)?.answers[0];
|
||||||
const disclosed = M_POLL_KIND_DISCLOSED.matches(poll.kind.name);
|
const disclosed = M_POLL_KIND_DISCLOSED.matches(pollEvent.kind.name);
|
||||||
|
|
||||||
// Disclosed: votes are hidden until I vote or the poll ends
|
// Disclosed: votes are hidden until I vote or the poll ends
|
||||||
// Undisclosed: votes are hidden until poll ends
|
// Undisclosed: votes are hidden until poll ends
|
||||||
const showResults = ended || (disclosed && myVote !== undefined);
|
const showResults = poll.isEnded || (disclosed && myVote !== undefined);
|
||||||
|
|
||||||
let totalText: string;
|
let totalText: string;
|
||||||
if (ended) {
|
if (poll.isEnded) {
|
||||||
totalText = _t("Final result based on %(count)s votes", { count: totalVotes });
|
totalText = _t("Final result based on %(count)s votes", { count: totalVotes });
|
||||||
} else if (!disclosed) {
|
} else if (!disclosed) {
|
||||||
totalText = _t("Results will be visible when the poll is ended");
|
totalText = _t("Results will be visible when the poll is ended");
|
||||||
|
@ -449,11 +323,11 @@ export default class MPollBody extends React.Component<IBodyProps, IState> {
|
||||||
return (
|
return (
|
||||||
<div className="mx_MPollBody">
|
<div className="mx_MPollBody">
|
||||||
<h2 data-testid="pollQuestion">
|
<h2 data-testid="pollQuestion">
|
||||||
{poll.question.text}
|
{pollEvent.question.text}
|
||||||
{editedSpan}
|
{editedSpan}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="mx_MPollBody_allOptions">
|
<div className="mx_MPollBody_allOptions">
|
||||||
{poll.answers.map((answer: PollAnswerSubevent) => {
|
{pollEvent.answers.map((answer: PollAnswerSubevent) => {
|
||||||
let answerVotes = 0;
|
let answerVotes = 0;
|
||||||
let votesText = "";
|
let votesText = "";
|
||||||
|
|
||||||
|
@ -462,11 +336,12 @@ export default class MPollBody extends React.Component<IBodyProps, IState> {
|
||||||
votesText = _t("%(count)s votes", { count: answerVotes });
|
votesText = _t("%(count)s votes", { count: answerVotes });
|
||||||
}
|
}
|
||||||
|
|
||||||
const checked = (!ended && myVote === answer.id) || (ended && answerVotes === winCount);
|
const checked =
|
||||||
|
(!poll.isEnded && myVote === answer.id) || (poll.isEnded && answerVotes === winCount);
|
||||||
const cls = classNames({
|
const cls = classNames({
|
||||||
mx_MPollBody_option: true,
|
mx_MPollBody_option: true,
|
||||||
mx_MPollBody_option_checked: checked,
|
mx_MPollBody_option_checked: checked,
|
||||||
mx_MPollBody_option_ended: ended,
|
mx_MPollBody_option_ended: poll.isEnded,
|
||||||
});
|
});
|
||||||
|
|
||||||
const answerPercent = totalVotes === 0 ? 0 : Math.round((100.0 * answerVotes) / totalVotes);
|
const answerPercent = totalVotes === 0 ? 0 : Math.round((100.0 * answerVotes) / totalVotes);
|
||||||
|
@ -477,7 +352,7 @@ export default class MPollBody extends React.Component<IBodyProps, IState> {
|
||||||
className={cls}
|
className={cls}
|
||||||
onClick={() => this.selectOption(answer.id)}
|
onClick={() => this.selectOption(answer.id)}
|
||||||
>
|
>
|
||||||
{ended ? (
|
{poll.isEnded ? (
|
||||||
<EndedPollOption answer={answer} checked={checked} votesText={votesText} />
|
<EndedPollOption answer={answer} checked={checked} votesText={votesText} />
|
||||||
) : (
|
) : (
|
||||||
<LivePollOption
|
<LivePollOption
|
||||||
|
@ -500,6 +375,7 @@ export default class MPollBody extends React.Component<IBodyProps, IState> {
|
||||||
</div>
|
</div>
|
||||||
<div data-testid="totalVotes" className="mx_MPollBody_totalVotes">
|
<div data-testid="totalVotes" className="mx_MPollBody_totalVotes">
|
||||||
{totalText}
|
{totalText}
|
||||||
|
{isFetchingResponses && <Spinner w={16} h={16} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -562,68 +438,17 @@ function userResponseFromPollResponseEvent(event: MatrixEvent): UserVote {
|
||||||
throw new Error("Failed to parse Poll Response Event to determine user response");
|
throw new Error("Failed to parse Poll Response Event to determine user response");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new UserVote(event.getTs(), event.getSender(), response.answerIds);
|
return new UserVote(event.getTs(), event.getSender()!, response.answerIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function allVotes(
|
export function allVotes(voteRelations: Relations): Array<UserVote> {
|
||||||
pollEvent: MatrixEvent,
|
|
||||||
matrixClient: MatrixClient,
|
|
||||||
voteRelations: RelatedRelations,
|
|
||||||
endRelations: RelatedRelations,
|
|
||||||
): Array<UserVote> {
|
|
||||||
const endTs = pollEndTs(pollEvent, matrixClient, endRelations);
|
|
||||||
|
|
||||||
function isOnOrBeforeEnd(responseEvent: MatrixEvent): boolean {
|
|
||||||
// From MSC3381:
|
|
||||||
// "Votes sent on or before the end event's timestamp are valid votes"
|
|
||||||
return endTs === null || responseEvent.getTs() <= endTs;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (voteRelations) {
|
if (voteRelations) {
|
||||||
return voteRelations
|
return voteRelations.getRelations().map(userResponseFromPollResponseEvent);
|
||||||
.getRelations()
|
|
||||||
.filter(isPollResponse)
|
|
||||||
.filter(isOnOrBeforeEnd)
|
|
||||||
.map(userResponseFromPollResponseEvent);
|
|
||||||
} else {
|
} else {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the earliest timestamp from the supplied list of end_poll events
|
|
||||||
* or null if there are no authorised events.
|
|
||||||
*/
|
|
||||||
export function pollEndTs(
|
|
||||||
pollEvent: MatrixEvent,
|
|
||||||
matrixClient: MatrixClient,
|
|
||||||
endRelations: RelatedRelations,
|
|
||||||
): number | null {
|
|
||||||
if (!endRelations) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const roomCurrentState = matrixClient.getRoom(pollEvent.getRoomId()).currentState;
|
|
||||||
function userCanRedact(endEvent: MatrixEvent): boolean {
|
|
||||||
return roomCurrentState.maySendRedactionForEvent(pollEvent, endEvent.getSender());
|
|
||||||
}
|
|
||||||
|
|
||||||
const tss: number[] = endRelations
|
|
||||||
.getRelations()
|
|
||||||
.filter(userCanRedact)
|
|
||||||
.map((evt: MatrixEvent) => evt.getTs());
|
|
||||||
|
|
||||||
if (tss.length === 0) {
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
return Math.min(...tss);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isPollResponse(responseEvent: MatrixEvent): boolean {
|
|
||||||
return responseEvent.unstableExtensibleEvent?.isEquivalentTo(M_POLL_RESPONSE);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Figure out the correct vote for each user.
|
* Figure out the correct vote for each user.
|
||||||
* @param userResponses current vote responses in the poll
|
* @param userResponses current vote responses in the poll
|
||||||
|
@ -662,7 +487,7 @@ function countVotes(userVotes: Map<string, UserVote>, pollStart: PollStartEvent)
|
||||||
if (!tempResponse.spoiled) {
|
if (!tempResponse.spoiled) {
|
||||||
for (const answerId of tempResponse.answerIds) {
|
for (const answerId of tempResponse.answerIds) {
|
||||||
if (collected.has(answerId)) {
|
if (collected.has(answerId)) {
|
||||||
collected.set(answerId, collected.get(answerId) + 1);
|
collected.set(answerId, collected.get(answerId)! + 1);
|
||||||
} else {
|
} else {
|
||||||
collected.set(answerId, 1);
|
collected.set(answerId, 1);
|
||||||
}
|
}
|
||||||
|
|
|
@ -133,6 +133,7 @@ const PinnedMessagesCard: React.FC<IProps> = ({ room, onClose, permalinkCreator
|
||||||
if (event.isEncrypted()) {
|
if (event.isEncrypted()) {
|
||||||
await cli.decryptEventIfNeeded(event); // TODO await?
|
await cli.decryptEventIfNeeded(event); // TODO await?
|
||||||
}
|
}
|
||||||
|
await room.processPollEvents([event]);
|
||||||
|
|
||||||
if (event && PinningUtils.isPinnable(event)) {
|
if (event && PinningUtils.isPinnable(event)) {
|
||||||
// Inject sender information
|
// Inject sender information
|
||||||
|
|
|
@ -19,8 +19,6 @@ import React from "react";
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
import { Relations } from "matrix-js-sdk/src/models/relations";
|
import { Relations } from "matrix-js-sdk/src/models/relations";
|
||||||
import { EventType, RelationType } from "matrix-js-sdk/src/@types/event";
|
import { EventType, RelationType } from "matrix-js-sdk/src/@types/event";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
|
||||||
import { M_POLL_START, M_POLL_RESPONSE, M_POLL_END } from "matrix-js-sdk/src/@types/polls";
|
|
||||||
|
|
||||||
import dis from "../../../dispatcher/dispatcher";
|
import dis from "../../../dispatcher/dispatcher";
|
||||||
import { Action } from "../../../dispatcher/actions";
|
import { Action } from "../../../dispatcher/actions";
|
||||||
|
@ -69,47 +67,6 @@ export default class PinnedEventTile extends React.Component<IProps> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public async componentDidMount(): Promise<void> {
|
|
||||||
// Fetch poll responses
|
|
||||||
if (M_POLL_START.matches(this.props.event.getType())) {
|
|
||||||
const eventId = this.props.event.getId();
|
|
||||||
const roomId = this.props.event.getRoomId();
|
|
||||||
const room = this.context.getRoom(roomId);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await Promise.all(
|
|
||||||
[M_POLL_RESPONSE.name, M_POLL_RESPONSE.altName, M_POLL_END.name, M_POLL_END.altName].map(
|
|
||||||
async (eventType): Promise<void> => {
|
|
||||||
const relations = new Relations(RelationType.Reference, eventType, room);
|
|
||||||
relations.setTargetEvent(this.props.event);
|
|
||||||
|
|
||||||
if (!this.relations.has(RelationType.Reference)) {
|
|
||||||
this.relations.set(RelationType.Reference, new Map<string, Relations>());
|
|
||||||
}
|
|
||||||
this.relations.get(RelationType.Reference).set(eventType, relations);
|
|
||||||
|
|
||||||
let nextBatch: string | undefined;
|
|
||||||
do {
|
|
||||||
const page = await this.context.relations(
|
|
||||||
roomId,
|
|
||||||
eventId,
|
|
||||||
RelationType.Reference,
|
|
||||||
eventType,
|
|
||||||
{ from: nextBatch },
|
|
||||||
);
|
|
||||||
nextBatch = page.nextBatch;
|
|
||||||
page.events.forEach((event) => relations.addEvent(event));
|
|
||||||
} while (nextBatch);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(`Error fetching responses to pinned poll ${eventId} in room ${roomId}`);
|
|
||||||
logger.error(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
const sender = this.props.event.getSender();
|
const sender = this.props.event.getSender();
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -456,6 +456,8 @@ exports[`MPollBody renders a finished poll with no votes 1`] = `
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`MPollBody renders a loader while responses are still loading 1`] = `"Based on 4 votes<div class="mx_Spinner"><div class="mx_Spinner_icon" style="width: 16px; height: 16px;" aria-label="Loading..." role="progressbar" data-testid="spinner"></div></div>"`;
|
||||||
|
|
||||||
exports[`MPollBody renders a poll that I have not voted in 1`] = `
|
exports[`MPollBody renders a poll that I have not voted in 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
|
@ -769,7 +771,6 @@ exports[`MPollBody renders a poll with local, non-local and invalid votes 1`] =
|
||||||
class="mx_StyledRadioButton mx_MPollBody_live-option mx_StyledRadioButton_enabled mx_StyledRadioButton_checked"
|
class="mx_StyledRadioButton mx_MPollBody_live-option mx_StyledRadioButton_enabled mx_StyledRadioButton_checked"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
checked=""
|
|
||||||
name="poll_answer_select-$mypoll"
|
name="poll_answer_select-$mypoll"
|
||||||
type="radio"
|
type="radio"
|
||||||
value="italian"
|
value="italian"
|
||||||
|
@ -1224,7 +1225,6 @@ exports[`MPollBody renders a poll with only non-local votes 1`] = `
|
||||||
class="mx_StyledRadioButton mx_MPollBody_live-option mx_StyledRadioButton_enabled mx_StyledRadioButton_checked"
|
class="mx_StyledRadioButton mx_MPollBody_live-option mx_StyledRadioButton_enabled mx_StyledRadioButton_checked"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
checked=""
|
|
||||||
name="poll_answer_select-$mypoll"
|
name="poll_answer_select-$mypoll"
|
||||||
type="radio"
|
type="radio"
|
||||||
value="wings"
|
value="wings"
|
||||||
|
|
|
@ -23,12 +23,12 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
import { EventType, RelationType, MsgType } from "matrix-js-sdk/src/@types/event";
|
import { EventType, RelationType, MsgType } from "matrix-js-sdk/src/@types/event";
|
||||||
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
|
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
|
||||||
import { IEvent, Room, EventTimelineSet, IMinimalEvent } from "matrix-js-sdk/src/matrix";
|
import { IEvent, Room, EventTimelineSet, IMinimalEvent } from "matrix-js-sdk/src/matrix";
|
||||||
import { M_POLL_RESPONSE, M_POLL_END, M_POLL_KIND_DISCLOSED } from "matrix-js-sdk/src/@types/polls";
|
import { M_POLL_KIND_DISCLOSED } from "matrix-js-sdk/src/@types/polls";
|
||||||
import { PollStartEvent } from "matrix-js-sdk/src/extensible_events_v1/PollStartEvent";
|
import { PollStartEvent } from "matrix-js-sdk/src/extensible_events_v1/PollStartEvent";
|
||||||
import { PollResponseEvent } from "matrix-js-sdk/src/extensible_events_v1/PollResponseEvent";
|
import { PollResponseEvent } from "matrix-js-sdk/src/extensible_events_v1/PollResponseEvent";
|
||||||
import { PollEndEvent } from "matrix-js-sdk/src/extensible_events_v1/PollEndEvent";
|
import { PollEndEvent } from "matrix-js-sdk/src/extensible_events_v1/PollEndEvent";
|
||||||
|
|
||||||
import { stubClient, mkStubRoom, mkEvent, mkMessage } from "../../../test-utils";
|
import { stubClient, mkEvent, mkMessage, flushPromises } from "../../../test-utils";
|
||||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||||
import PinnedMessagesCard from "../../../../src/components/views/right_panel/PinnedMessagesCard";
|
import PinnedMessagesCard from "../../../../src/components/views/right_panel/PinnedMessagesCard";
|
||||||
import PinnedEventTile from "../../../../src/components/views/rooms/PinnedEventTile";
|
import PinnedEventTile from "../../../../src/components/views/rooms/PinnedEventTile";
|
||||||
|
@ -40,16 +40,16 @@ describe("<PinnedMessagesCard />", () => {
|
||||||
stubClient();
|
stubClient();
|
||||||
const cli = mocked(MatrixClientPeg.get());
|
const cli = mocked(MatrixClientPeg.get());
|
||||||
cli.getUserId.mockReturnValue("@alice:example.org");
|
cli.getUserId.mockReturnValue("@alice:example.org");
|
||||||
cli.setRoomAccountData.mockReturnValue(undefined);
|
cli.setRoomAccountData.mockResolvedValue({});
|
||||||
cli.relations.mockResolvedValue({ originalEvent: {} as unknown as MatrixEvent, events: [] });
|
cli.relations.mockResolvedValue({ originalEvent: {} as unknown as MatrixEvent, events: [] });
|
||||||
|
|
||||||
const mkRoom = (localPins: MatrixEvent[], nonLocalPins: MatrixEvent[]): Room => {
|
const mkRoom = (localPins: MatrixEvent[], nonLocalPins: MatrixEvent[]): Room => {
|
||||||
const room = mkStubRoom("!room:example.org", "room", cli);
|
const room = new Room("!room:example.org", cli, "@me:example.org");
|
||||||
// Deferred since we may be adding or removing pins later
|
// Deferred since we may be adding or removing pins later
|
||||||
const pins = () => [...localPins, ...nonLocalPins];
|
const pins = () => [...localPins, ...nonLocalPins];
|
||||||
|
|
||||||
// Insert pin IDs into room state
|
// Insert pin IDs into room state
|
||||||
mocked(room.currentState).getStateEvents.mockImplementation((): any =>
|
jest.spyOn(room.currentState, "getStateEvents").mockImplementation((): any =>
|
||||||
mkEvent({
|
mkEvent({
|
||||||
event: true,
|
event: true,
|
||||||
type: EventType.RoomPinnedEvents,
|
type: EventType.RoomPinnedEvents,
|
||||||
|
@ -61,6 +61,8 @@ describe("<PinnedMessagesCard />", () => {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
jest.spyOn(room.currentState, "on");
|
||||||
|
|
||||||
// Insert local pins into local timeline set
|
// Insert local pins into local timeline set
|
||||||
room.getUnfilteredTimelineSet = () =>
|
room.getUnfilteredTimelineSet = () =>
|
||||||
({
|
({
|
||||||
|
@ -75,6 +77,8 @@ describe("<PinnedMessagesCard />", () => {
|
||||||
return Promise.resolve(event as IMinimalEvent);
|
return Promise.resolve(event as IMinimalEvent);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
cli.getRoom.mockReturnValue(room);
|
||||||
|
|
||||||
return room;
|
return room;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -131,8 +135,8 @@ describe("<PinnedMessagesCard />", () => {
|
||||||
|
|
||||||
it("updates when messages are pinned", async () => {
|
it("updates when messages are pinned", async () => {
|
||||||
// Start with nothing pinned
|
// Start with nothing pinned
|
||||||
const localPins = [];
|
const localPins: MatrixEvent[] = [];
|
||||||
const nonLocalPins = [];
|
const nonLocalPins: MatrixEvent[] = [];
|
||||||
const pins = await mountPins(mkRoom(localPins, nonLocalPins));
|
const pins = await mountPins(mkRoom(localPins, nonLocalPins));
|
||||||
expect(pins.find(PinnedEventTile).length).toBe(0);
|
expect(pins.find(PinnedEventTile).length).toBe(0);
|
||||||
|
|
||||||
|
@ -240,31 +244,27 @@ describe("<PinnedMessagesCard />", () => {
|
||||||
["@eve:example.org", 1],
|
["@eve:example.org", 1],
|
||||||
].map(([user, option], i) =>
|
].map(([user, option], i) =>
|
||||||
mkEvent({
|
mkEvent({
|
||||||
...PollResponseEvent.from([answers[option].id], poll.getId()).serialize(),
|
...PollResponseEvent.from([answers[option as number].id], poll.getId()!).serialize(),
|
||||||
event: true,
|
event: true,
|
||||||
room: "!room:example.org",
|
room: "!room:example.org",
|
||||||
user: user as string,
|
user: user as string,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const end = mkEvent({
|
const end = mkEvent({
|
||||||
...PollEndEvent.from(poll.getId(), "Closing the poll").serialize(),
|
...PollEndEvent.from(poll.getId()!, "Closing the poll").serialize(),
|
||||||
event: true,
|
event: true,
|
||||||
room: "!room:example.org",
|
room: "!room:example.org",
|
||||||
user: "@alice:example.org",
|
user: "@alice:example.org",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Make the responses available
|
// Make the responses available
|
||||||
cli.relations.mockImplementation(async (roomId, eventId, relationType, eventType, { from }) => {
|
cli.relations.mockImplementation(async (roomId, eventId, relationType, eventType, opts) => {
|
||||||
if (eventId === poll.getId() && relationType === RelationType.Reference) {
|
if (eventId === poll.getId() && relationType === RelationType.Reference) {
|
||||||
switch (eventType) {
|
|
||||||
case M_POLL_RESPONSE.name:
|
|
||||||
// Paginate the results, for added challenge
|
// Paginate the results, for added challenge
|
||||||
return from === "page2"
|
return opts?.from === "page2"
|
||||||
? { originalEvent: poll, events: responses.slice(2) }
|
? { originalEvent: poll, events: responses.slice(2) }
|
||||||
: { originalEvent: poll, events: responses.slice(0, 2), nextBatch: "page2" };
|
: { originalEvent: poll, events: [...responses.slice(0, 2), end], nextBatch: "page2" };
|
||||||
case M_POLL_END.name:
|
|
||||||
return { originalEvent: null, events: [end] };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// type does not allow originalEvent to be falsy
|
// type does not allow originalEvent to be falsy
|
||||||
// but code seems to
|
// but code seems to
|
||||||
|
@ -272,8 +272,20 @@ describe("<PinnedMessagesCard />", () => {
|
||||||
return { originalEvent: undefined as unknown as MatrixEvent, events: [] };
|
return { originalEvent: undefined as unknown as MatrixEvent, events: [] };
|
||||||
});
|
});
|
||||||
|
|
||||||
const pins = await mountPins(mkRoom([], [poll]));
|
const room = mkRoom([], [poll]);
|
||||||
|
// poll end event validates against this
|
||||||
|
jest.spyOn(room.currentState, "maySendRedactionForEvent").mockReturnValue(true);
|
||||||
|
|
||||||
|
const pins = await mountPins(room);
|
||||||
|
// two pages of results
|
||||||
|
await flushPromises();
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
const pollInstance = room.polls.get(poll.getId()!);
|
||||||
|
expect(pollInstance).toBeTruthy();
|
||||||
|
|
||||||
const pinTile = pins.find(MPollBody);
|
const pinTile = pins.find(MPollBody);
|
||||||
|
|
||||||
expect(pinTile.length).toEqual(1);
|
expect(pinTile.length).toEqual(1);
|
||||||
expect(pinTile.find(".mx_MPollBody_option_ended").length).toEqual(2);
|
expect(pinTile.find(".mx_MPollBody_option_ended").length).toEqual(2);
|
||||||
expect(pinTile.find(".mx_MPollBody_optionVoteCount").first().text()).toEqual("2 votes");
|
expect(pinTile.find(".mx_MPollBody_optionVoteCount").first().text()).toEqual("2 votes");
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue