Ensure my votes from a different device show up (#7233)

Co-authored-by: Travis Ralston <travpc@gmail.com>
This commit is contained in:
Andy Balaam 2021-12-02 17:12:18 +00:00 committed by GitHub
parent 25c119dd5a
commit 141950d9e6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 101 additions and 23 deletions

View file

@ -36,14 +36,15 @@ import ErrorDialog from '../dialogs/ErrorDialog';
const TEXT_NODE_TYPE = "org.matrix.msc1767.text";
interface IState {
selected?: string;
pollRelations: Relations;
selected?: string; // Which option was clicked by the local user
pollRelations: Relations; // Allows us to access voting events
}
@replaceableComponent("views.messages.MPollBody")
export default class MPollBody extends React.Component<IBodyProps, IState> {
static contextType = MatrixClientContext;
public static contextType = MatrixClientContext;
public context!: React.ContextType<typeof MatrixClientContext>;
private seenEventIds: string[] = []; // Events we have already seen
constructor(props: IBodyProps) {
super(props);
@ -98,7 +99,7 @@ export default class MPollBody extends React.Component<IBodyProps, IState> {
private onRelationsChange = () => {
// We hold pollRelations in our state, and it has changed under us
this.forceUpdate();
this.unselectIfNewEventFromMe();
};
private selectOption(answerId: string) {
@ -120,7 +121,7 @@ export default class MPollBody extends React.Component<IBodyProps, IState> {
this.props.mxEvent.getRoomId(),
POLL_RESPONSE_EVENT_TYPE.name,
responseContent,
).catch(e => {
).catch((e: any) => {
console.error("Failed to submit poll response event:", e);
Modal.createTrackedDialog(
@ -165,6 +166,33 @@ export default class MPollBody extends React.Component<IBodyProps, IState> {
);
}
/**
* If we've just received a new event that we hadn't seen
* before, and that event is me voting (e.g. from a different
* device) then forget when the local user selected.
*
* Either way, calls setState to update our list of events we
* have already seen.
*/
private unselectIfNewEventFromMe() {
const newEvents: MatrixEvent[] = this.state.pollRelations.getRelations()
.filter(isPollResponse)
.filter((mxEvent: MatrixEvent) =>
!this.seenEventIds.includes(mxEvent.getId()));
let newSelected = this.state.selected;
if (newEvents.length > 0) {
for (const mxEvent of newEvents) {
if (mxEvent.getSender() === this.context.getUserId()) {
newSelected = null;
}
}
}
const newEventIds = newEvents.map((mxEvent: MatrixEvent) => mxEvent.getId());
this.seenEventIds = this.seenEventIds.concat(newEventIds);
this.setState( { selected: newSelected } );
}
private totalVotes(collectedVotes: Map<string, number>): number {
let sum = 0;
for (const v of collectedVotes.values()) {
@ -254,13 +282,6 @@ function userResponseFromPollResponseEvent(event: MatrixEvent): UserVote {
}
export function allVotes(pollRelations: Relations): Array<UserVote> {
function isPollResponse(responseEvent: MatrixEvent): boolean {
return (
responseEvent.getType() === POLL_RESPONSE_EVENT_TYPE.name &&
responseEvent.getContent().hasOwnProperty(POLL_RESPONSE_EVENT_TYPE.name)
);
}
if (pollRelations) {
return pollRelations.getRelations()
.filter(isPollResponse)
@ -270,6 +291,13 @@ export function allVotes(pollRelations: Relations): Array<UserVote> {
}
}
function isPollResponse(responseEvent: MatrixEvent): boolean {
return (
POLL_RESPONSE_EVENT_TYPE.matches(responseEvent.getType()) &&
POLL_RESPONSE_EVENT_TYPE.findIn(responseEvent.getContent())
);
}
/**
* Figure out the correct vote for each user.
* @returns a Map of user ID to their vote info

View file

@ -23,9 +23,10 @@ import * as TestUtils from "../../../test-utils";
import { Callback, IContent, MatrixEvent } from "matrix-js-sdk";
import { ISendEventResponse } from "matrix-js-sdk/src/@types/requests";
import { Relations } from "matrix-js-sdk/src/models/relations";
import { IPollAnswer, IPollContent } from "../../../../src/polls/consts";
import { IPollAnswer, IPollContent, POLL_RESPONSE_EVENT_TYPE } from "../../../../src/polls/consts";
import { UserVote, allVotes } from "../../../../src/components/views/messages/MPollBody";
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import { IBodyProps } from "../../../../src/components/views/messages/IBodyProps";
const CHECKED = "mx_MPollBody_option_checked";
@ -52,12 +53,12 @@ describe("MPollBody", () => {
new UserVote(
ev1.getTs(),
ev1.getSender(),
ev1.getContent()["org.matrix.msc3381.poll.response"].answers,
ev1.getContent()[POLL_RESPONSE_EVENT_TYPE.name].answers,
),
new UserVote(
ev2.getTs(),
ev2.getSender(),
ev2.getContent()["org.matrix.msc3381.poll.response"].answers,
ev2.getContent()[POLL_RESPONSE_EVENT_TYPE.name].answers,
),
]);
});
@ -150,6 +151,55 @@ describe("MPollBody", () => {
expect(voteButton(body, "italian").hasClass(CHECKED)).toBe(false);
});
it("cancels my local vote if another comes in", () => {
// Given I voted locally
const votes = [responseEvent("@me:example.com", "pizza", 100)];
const body = newMPollBody(votes);
const props: IBodyProps = body.instance().props as IBodyProps;
const pollRelations: Relations = props.getRelationsForEvent(
"$mypoll", "m.reference", POLL_RESPONSE_EVENT_TYPE.name);
clickRadio(body, "pizza");
// When a new vote from me comes in
pollRelations.addEvent(responseEvent("@me:example.com", "wings", 101));
// Then the new vote is counted, not the old one
expect(votesCount(body, "pizza")).toBe("0 votes");
expect(votesCount(body, "poutine")).toBe("0 votes");
expect(votesCount(body, "italian")).toBe("0 votes");
expect(votesCount(body, "wings")).toBe("1 vote");
expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("Based on 1 vote");
});
it("doesn't cancel my local vote if someone else votes", () => {
// Given I voted locally
const votes = [responseEvent("@me:example.com", "pizza")];
const body = newMPollBody(votes);
const props: IBodyProps = body.instance().props as IBodyProps;
const pollRelations: Relations = props.getRelationsForEvent(
"$mypoll", "m.reference", POLL_RESPONSE_EVENT_TYPE.name);
clickRadio(body, "pizza");
// When a new vote from someone else comes in
pollRelations.addEvent(responseEvent("@xx:example.com", "wings", 101));
// Then my vote is still for pizza
// NOTE: the new event does not affect the counts for other people -
// that is handled through the Relations, not by listening to
// these timeline events.
expect(votesCount(body, "pizza")).toBe("1 vote");
expect(votesCount(body, "poutine")).toBe("0 votes");
expect(votesCount(body, "italian")).toBe("0 votes");
expect(votesCount(body, "wings")).toBe("1 vote");
expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("Based on 2 votes");
// And my vote is highlighted
expect(voteButton(body, "pizza").hasClass(CHECKED)).toBe(true);
expect(voteButton(body, "wings").hasClass(CHECKED)).toBe(false);
});
it("highlights my vote even if I did it on another device", () => {
// Given I voted italian
const votes = [
@ -363,7 +413,7 @@ describe("MPollBody", () => {
function newPollRelations(relationEvents: Array<MatrixEvent>): Relations {
const pollRelations = new Relations(
"m.reference", "org.matrix.msc3381.poll.response", null);
"m.reference", POLL_RESPONSE_EVENT_TYPE.name, null);
for (const ev of relationEvents) {
pollRelations.addEvent(ev);
}
@ -375,7 +425,7 @@ function newMPollBody(
answers?: IPollAnswer[],
): ReactWrapper {
const pollRelations = new Relations(
"m.reference", "org.matrix.msc3381.poll.response", null);
"m.reference", POLL_RESPONSE_EVENT_TYPE.name, null);
for (const ev of relationEvents) {
pollRelations.addEvent(ev);
}
@ -390,7 +440,7 @@ function newMPollBody(
(eventId: string, relationType: string, eventType: string) => {
expect(eventId).toBe("$mypoll");
expect(relationType).toBe("m.reference");
expect(eventType).toBe("org.matrix.msc3381.poll.response");
expect(eventType).toBe(POLL_RESPONSE_EVENT_TYPE.name);
return pollRelations;
}
}
@ -440,7 +490,7 @@ function badResponseEvent(): MatrixEvent {
return new MatrixEvent(
{
"event_id": nextId(),
"type": "org.matrix.msc3381.poll.response",
"type": POLL_RESPONSE_EVENT_TYPE.name,
"content": {
"m.relates_to": {
"rel_type": "m.reference",
@ -463,14 +513,14 @@ function responseEvent(
"event_id": nextId(),
"room_id": "#myroom:example.com",
"origin_server_ts": ts,
"type": "org.matrix.msc3381.poll.response",
"type": POLL_RESPONSE_EVENT_TYPE.name,
"sender": sender,
"content": {
"m.relates_to": {
"rel_type": "m.reference",
"event_id": "$mypoll",
},
"org.matrix.msc3381.poll.response": {
[POLL_RESPONSE_EVENT_TYPE.name]: {
"answers": ans,
},
},
@ -481,7 +531,7 @@ function responseEvent(
function expectedResponseEvent(answer: string) {
return {
"content": {
"org.matrix.msc3381.poll.response": {
[POLL_RESPONSE_EVENT_TYPE.name]: {
"answers": [answer],
},
"m.relates_to": {
@ -489,7 +539,7 @@ function expectedResponseEvent(answer: string) {
"rel_type": "m.reference",
},
},
"eventType": "org.matrix.msc3381.poll.response",
"eventType": POLL_RESPONSE_EVENT_TYPE.name,
"roomId": "#myroom:example.com",
"txnId": undefined,
"callback": undefined,