mirror of
https://github.com/element-hq/element-web
synced 2024-11-23 17:56:01 +03:00
Ensure my votes from a different device show up (#7233)
Co-authored-by: Travis Ralston <travpc@gmail.com>
This commit is contained in:
parent
25c119dd5a
commit
141950d9e6
2 changed files with 101 additions and 23 deletions
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue