mirror of https://github.com/vector-im/riot-web
Ensure my votes from a different device show up (#7233)
Co-authored-by: Travis Ralston <travpc@gmail.com>pull/21833/head
parent
25c119dd5a
commit
141950d9e6
|
@ -36,14 +36,15 @@ import ErrorDialog from '../dialogs/ErrorDialog';
|
||||||
const TEXT_NODE_TYPE = "org.matrix.msc1767.text";
|
const TEXT_NODE_TYPE = "org.matrix.msc1767.text";
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
selected?: string;
|
selected?: string; // Which option was clicked by the local user
|
||||||
pollRelations: Relations;
|
pollRelations: Relations; // Allows us to access voting events
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.messages.MPollBody")
|
@replaceableComponent("views.messages.MPollBody")
|
||||||
export default class MPollBody extends React.Component<IBodyProps, IState> {
|
export default class MPollBody extends React.Component<IBodyProps, IState> {
|
||||||
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
|
||||||
|
|
||||||
constructor(props: IBodyProps) {
|
constructor(props: IBodyProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -98,7 +99,7 @@ export default class MPollBody extends React.Component<IBodyProps, IState> {
|
||||||
|
|
||||||
private onRelationsChange = () => {
|
private onRelationsChange = () => {
|
||||||
// We hold pollRelations in our state, and it has changed under us
|
// We hold pollRelations in our state, and it has changed under us
|
||||||
this.forceUpdate();
|
this.unselectIfNewEventFromMe();
|
||||||
};
|
};
|
||||||
|
|
||||||
private selectOption(answerId: string) {
|
private selectOption(answerId: string) {
|
||||||
|
@ -120,7 +121,7 @@ export default class MPollBody extends React.Component<IBodyProps, IState> {
|
||||||
this.props.mxEvent.getRoomId(),
|
this.props.mxEvent.getRoomId(),
|
||||||
POLL_RESPONSE_EVENT_TYPE.name,
|
POLL_RESPONSE_EVENT_TYPE.name,
|
||||||
responseContent,
|
responseContent,
|
||||||
).catch(e => {
|
).catch((e: any) => {
|
||||||
console.error("Failed to submit poll response event:", e);
|
console.error("Failed to submit poll response event:", e);
|
||||||
|
|
||||||
Modal.createTrackedDialog(
|
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 {
|
private totalVotes(collectedVotes: Map<string, number>): number {
|
||||||
let sum = 0;
|
let sum = 0;
|
||||||
for (const v of collectedVotes.values()) {
|
for (const v of collectedVotes.values()) {
|
||||||
|
@ -254,13 +282,6 @@ function userResponseFromPollResponseEvent(event: MatrixEvent): UserVote {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function allVotes(pollRelations: Relations): Array<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) {
|
if (pollRelations) {
|
||||||
return pollRelations.getRelations()
|
return pollRelations.getRelations()
|
||||||
.filter(isPollResponse)
|
.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.
|
* Figure out the correct vote for each user.
|
||||||
* @returns a Map of user ID to their vote info
|
* @returns a Map of user ID to their vote info
|
||||||
|
|
|
@ -23,9 +23,10 @@ import * as TestUtils from "../../../test-utils";
|
||||||
import { Callback, IContent, MatrixEvent } from "matrix-js-sdk";
|
import { Callback, IContent, MatrixEvent } from "matrix-js-sdk";
|
||||||
import { ISendEventResponse } from "matrix-js-sdk/src/@types/requests";
|
import { ISendEventResponse } from "matrix-js-sdk/src/@types/requests";
|
||||||
import { Relations } from "matrix-js-sdk/src/models/relations";
|
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 { UserVote, allVotes } from "../../../../src/components/views/messages/MPollBody";
|
||||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||||
|
import { IBodyProps } from "../../../../src/components/views/messages/IBodyProps";
|
||||||
|
|
||||||
const CHECKED = "mx_MPollBody_option_checked";
|
const CHECKED = "mx_MPollBody_option_checked";
|
||||||
|
|
||||||
|
@ -52,12 +53,12 @@ describe("MPollBody", () => {
|
||||||
new UserVote(
|
new UserVote(
|
||||||
ev1.getTs(),
|
ev1.getTs(),
|
||||||
ev1.getSender(),
|
ev1.getSender(),
|
||||||
ev1.getContent()["org.matrix.msc3381.poll.response"].answers,
|
ev1.getContent()[POLL_RESPONSE_EVENT_TYPE.name].answers,
|
||||||
),
|
),
|
||||||
new UserVote(
|
new UserVote(
|
||||||
ev2.getTs(),
|
ev2.getTs(),
|
||||||
ev2.getSender(),
|
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);
|
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", () => {
|
it("highlights my vote even if I did it on another device", () => {
|
||||||
// Given I voted italian
|
// Given I voted italian
|
||||||
const votes = [
|
const votes = [
|
||||||
|
@ -363,7 +413,7 @@ describe("MPollBody", () => {
|
||||||
|
|
||||||
function newPollRelations(relationEvents: Array<MatrixEvent>): Relations {
|
function newPollRelations(relationEvents: Array<MatrixEvent>): Relations {
|
||||||
const pollRelations = new 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) {
|
for (const ev of relationEvents) {
|
||||||
pollRelations.addEvent(ev);
|
pollRelations.addEvent(ev);
|
||||||
}
|
}
|
||||||
|
@ -375,7 +425,7 @@ function newMPollBody(
|
||||||
answers?: IPollAnswer[],
|
answers?: IPollAnswer[],
|
||||||
): ReactWrapper {
|
): ReactWrapper {
|
||||||
const pollRelations = new 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) {
|
for (const ev of relationEvents) {
|
||||||
pollRelations.addEvent(ev);
|
pollRelations.addEvent(ev);
|
||||||
}
|
}
|
||||||
|
@ -390,7 +440,7 @@ function newMPollBody(
|
||||||
(eventId: string, relationType: string, eventType: string) => {
|
(eventId: string, relationType: string, eventType: string) => {
|
||||||
expect(eventId).toBe("$mypoll");
|
expect(eventId).toBe("$mypoll");
|
||||||
expect(relationType).toBe("m.reference");
|
expect(relationType).toBe("m.reference");
|
||||||
expect(eventType).toBe("org.matrix.msc3381.poll.response");
|
expect(eventType).toBe(POLL_RESPONSE_EVENT_TYPE.name);
|
||||||
return pollRelations;
|
return pollRelations;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -440,7 +490,7 @@ function badResponseEvent(): MatrixEvent {
|
||||||
return new MatrixEvent(
|
return new MatrixEvent(
|
||||||
{
|
{
|
||||||
"event_id": nextId(),
|
"event_id": nextId(),
|
||||||
"type": "org.matrix.msc3381.poll.response",
|
"type": POLL_RESPONSE_EVENT_TYPE.name,
|
||||||
"content": {
|
"content": {
|
||||||
"m.relates_to": {
|
"m.relates_to": {
|
||||||
"rel_type": "m.reference",
|
"rel_type": "m.reference",
|
||||||
|
@ -463,14 +513,14 @@ function responseEvent(
|
||||||
"event_id": nextId(),
|
"event_id": nextId(),
|
||||||
"room_id": "#myroom:example.com",
|
"room_id": "#myroom:example.com",
|
||||||
"origin_server_ts": ts,
|
"origin_server_ts": ts,
|
||||||
"type": "org.matrix.msc3381.poll.response",
|
"type": POLL_RESPONSE_EVENT_TYPE.name,
|
||||||
"sender": sender,
|
"sender": sender,
|
||||||
"content": {
|
"content": {
|
||||||
"m.relates_to": {
|
"m.relates_to": {
|
||||||
"rel_type": "m.reference",
|
"rel_type": "m.reference",
|
||||||
"event_id": "$mypoll",
|
"event_id": "$mypoll",
|
||||||
},
|
},
|
||||||
"org.matrix.msc3381.poll.response": {
|
[POLL_RESPONSE_EVENT_TYPE.name]: {
|
||||||
"answers": ans,
|
"answers": ans,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -481,7 +531,7 @@ function responseEvent(
|
||||||
function expectedResponseEvent(answer: string) {
|
function expectedResponseEvent(answer: string) {
|
||||||
return {
|
return {
|
||||||
"content": {
|
"content": {
|
||||||
"org.matrix.msc3381.poll.response": {
|
[POLL_RESPONSE_EVENT_TYPE.name]: {
|
||||||
"answers": [answer],
|
"answers": [answer],
|
||||||
},
|
},
|
||||||
"m.relates_to": {
|
"m.relates_to": {
|
||||||
|
@ -489,7 +539,7 @@ function expectedResponseEvent(answer: string) {
|
||||||
"rel_type": "m.reference",
|
"rel_type": "m.reference",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"eventType": "org.matrix.msc3381.poll.response",
|
"eventType": POLL_RESPONSE_EVENT_TYPE.name,
|
||||||
"roomId": "#myroom:example.com",
|
"roomId": "#myroom:example.com",
|
||||||
"txnId": undefined,
|
"txnId": undefined,
|
||||||
"callback": undefined,
|
"callback": undefined,
|
||||||
|
|
Loading…
Reference in New Issue