Display and send votes in polls (#7158)
Co-authored-by: Travis Ralston <travpc@gmail.com>
This commit is contained in:
parent
a156ba8be9
commit
d705fdd6e4
9 changed files with 1740 additions and 15 deletions
|
@ -86,6 +86,12 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_MPollBody_option_checked {
|
.mx_MPollBody_option_checked {
|
||||||
border-color: $accent;
|
border-color: $accent;
|
||||||
|
|
||||||
|
.mx_MPollBody_popularityBackground {
|
||||||
|
.mx_MPollBody_popularityAmount {
|
||||||
|
background-color: $accent;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_StyledRadioButton_checked input[type="radio"] + div {
|
.mx_StyledRadioButton_checked input[type="radio"] + div {
|
||||||
|
|
|
@ -14,11 +14,12 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src";
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
import { TileShape } from "../rooms/EventTile";
|
import { TileShape } from "../rooms/EventTile";
|
||||||
import { MediaEventHelper } from "../../../utils/MediaEventHelper";
|
import { MediaEventHelper } from "../../../utils/MediaEventHelper";
|
||||||
import EditorStateTransfer from "../../../utils/EditorStateTransfer";
|
import EditorStateTransfer from "../../../utils/EditorStateTransfer";
|
||||||
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
||||||
|
import { Relations } from "matrix-js-sdk/src/models/relations";
|
||||||
|
|
||||||
export interface IBodyProps {
|
export interface IBodyProps {
|
||||||
mxEvent: MatrixEvent;
|
mxEvent: MatrixEvent;
|
||||||
|
@ -41,4 +42,7 @@ export interface IBodyProps {
|
||||||
onMessageAllowed: () => void; // TODO: Docs
|
onMessageAllowed: () => void; // TODO: Docs
|
||||||
permalinkCreator: RoomPermalinkCreator;
|
permalinkCreator: RoomPermalinkCreator;
|
||||||
mediaEventHelper: MediaEventHelper;
|
mediaEventHelper: MediaEventHelper;
|
||||||
|
|
||||||
|
// helper function to access relations for this event
|
||||||
|
getRelationsForEvent?: (eventId: string, relationType: string, eventType: string) => Relations;
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,14 +18,24 @@ import React from 'react';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import { IBodyProps } from "./IBodyProps";
|
import { IBodyProps } from "./IBodyProps";
|
||||||
import { IPollAnswer, IPollContent, POLL_START_EVENT_TYPE } from '../../../polls/consts';
|
import {
|
||||||
|
IPollAnswer,
|
||||||
|
IPollContent,
|
||||||
|
IPollResponse,
|
||||||
|
POLL_RESPONSE_EVENT_TYPE,
|
||||||
|
POLL_START_EVENT_TYPE,
|
||||||
|
} from '../../../polls/consts';
|
||||||
import StyledRadioButton from '../elements/StyledRadioButton';
|
import StyledRadioButton from '../elements/StyledRadioButton';
|
||||||
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
import { Relations } from 'matrix-js-sdk/src/models/relations';
|
||||||
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
|
|
||||||
// TODO: [andyb] Use extensible events library when ready
|
// TODO: [andyb] Use extensible events library when ready
|
||||||
const TEXT_NODE_TYPE = "org.matrix.msc1767.text";
|
const TEXT_NODE_TYPE = "org.matrix.msc1767.text";
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
selected?: string;
|
selected?: string;
|
||||||
|
pollRelations: Relations;
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.messages.MPollBody")
|
@replaceableComponent("views.messages.MPollBody")
|
||||||
|
@ -33,12 +43,88 @@ export default class MPollBody extends React.Component<IBodyProps, IState> {
|
||||||
constructor(props: IBodyProps) {
|
constructor(props: IBodyProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
const pollRelations = this.fetchPollRelations();
|
||||||
selected: null,
|
let selected = null;
|
||||||
};
|
|
||||||
|
const userVotes = collectUserVotes(allVotes(pollRelations), null);
|
||||||
|
const userId = MatrixClientPeg.get().getUserId();
|
||||||
|
const currentVote = userVotes.get(userId);
|
||||||
|
if (currentVote) {
|
||||||
|
selected = currentVote.answers[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.state = { selected, pollRelations };
|
||||||
|
|
||||||
|
this.addListeners(this.state.pollRelations);
|
||||||
|
this.props.mxEvent.on("Event.relationsCreated", this.onPollRelationsCreated);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.props.mxEvent.off("Event.relationsCreated", this.onPollRelationsCreated);
|
||||||
|
this.removeListeners(this.state.pollRelations);
|
||||||
|
}
|
||||||
|
|
||||||
|
private addListeners(pollRelations?: Relations) {
|
||||||
|
if (pollRelations) {
|
||||||
|
pollRelations.on("Relations.add", this.onRelationsChange);
|
||||||
|
pollRelations.on("Relations.remove", this.onRelationsChange);
|
||||||
|
pollRelations.on("Relations.redaction", this.onRelationsChange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeListeners(pollRelations?: Relations) {
|
||||||
|
if (pollRelations) {
|
||||||
|
pollRelations.off("Relations.add", this.onRelationsChange);
|
||||||
|
pollRelations.off("Relations.remove", this.onRelationsChange);
|
||||||
|
pollRelations.off("Relations.redaction", this.onRelationsChange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onPollRelationsCreated = (relationType: string, eventType: string) => {
|
||||||
|
if (
|
||||||
|
relationType === "m.reference" &&
|
||||||
|
POLL_RESPONSE_EVENT_TYPE.matches(eventType)
|
||||||
|
) {
|
||||||
|
this.props.mxEvent.removeListener(
|
||||||
|
"Event.relationsCreated", this.onPollRelationsCreated);
|
||||||
|
|
||||||
|
const newPollRelations = this.fetchPollRelations();
|
||||||
|
this.addListeners(newPollRelations);
|
||||||
|
this.removeListeners(this.state.pollRelations);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
pollRelations: newPollRelations,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private onRelationsChange = () => {
|
||||||
|
// We hold pollRelations in our state, and it has changed under us
|
||||||
|
this.forceUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
private selectOption(answerId: string) {
|
private selectOption(answerId: string) {
|
||||||
|
if (answerId === this.state.selected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseContent: IPollResponse = {
|
||||||
|
[POLL_RESPONSE_EVENT_TYPE.name]: {
|
||||||
|
"answers": [answerId],
|
||||||
|
},
|
||||||
|
"m.relates_to": {
|
||||||
|
"event_id": this.props.mxEvent.getId(),
|
||||||
|
"rel_type": "m.reference",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
MatrixClientPeg.get().sendEvent(
|
||||||
|
this.props.mxEvent.getRoomId(),
|
||||||
|
POLL_RESPONSE_EVENT_TYPE.name,
|
||||||
|
responseContent,
|
||||||
|
).catch(e => {
|
||||||
|
console.error("Failed to submit poll response event:", e);
|
||||||
|
});
|
||||||
|
|
||||||
this.setState({ selected: answerId });
|
this.setState({ selected: answerId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,20 +132,60 @@ export default class MPollBody extends React.Component<IBodyProps, IState> {
|
||||||
this.selectOption(e.currentTarget.value);
|
this.selectOption(e.currentTarget.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private fetchPollRelations(): Relations | null {
|
||||||
|
if (this.props.getRelationsForEvent) {
|
||||||
|
return this.props.getRelationsForEvent(
|
||||||
|
this.props.mxEvent.getId(),
|
||||||
|
"m.reference",
|
||||||
|
POLL_RESPONSE_EVENT_TYPE.name,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns answer-id -> number-of-votes
|
||||||
|
*/
|
||||||
|
private collectVotes(): Map<string, number> {
|
||||||
|
return countVotes(
|
||||||
|
collectUserVotes(allVotes(this.state.pollRelations), this.state.selected),
|
||||||
|
this.props.mxEvent.getContent(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private totalVotes(collectedVotes: Map<string, number>): number {
|
||||||
|
let sum = 0;
|
||||||
|
for (const v of collectedVotes.values()) {
|
||||||
|
sum += v;
|
||||||
|
}
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const pollStart: IPollContent =
|
const pollStart: IPollContent = this.props.mxEvent.getContent();
|
||||||
this.props.mxEvent.getContent()[POLL_START_EVENT_TYPE.name];
|
const pollInfo = pollStart[POLL_START_EVENT_TYPE.name];
|
||||||
|
|
||||||
|
if (pollInfo.answers.length < 1 || pollInfo.answers.length > 20) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const pollId = this.props.mxEvent.getId();
|
const pollId = this.props.mxEvent.getId();
|
||||||
|
const votes = this.collectVotes();
|
||||||
|
const totalVotes = this.totalVotes(votes);
|
||||||
|
|
||||||
return <div className="mx_MPollBody">
|
return <div className="mx_MPollBody">
|
||||||
<h2>{ pollStart.question[TEXT_NODE_TYPE] }</h2>
|
<h2>{ pollInfo.question[TEXT_NODE_TYPE] }</h2>
|
||||||
<div className="mx_MPollBody_allOptions">
|
<div className="mx_MPollBody_allOptions">
|
||||||
{
|
{
|
||||||
pollStart.answers.map((answer: IPollAnswer) => {
|
pollInfo.answers.map((answer: IPollAnswer) => {
|
||||||
const checked = this.state.selected === answer.id;
|
const checked = this.state.selected === answer.id;
|
||||||
const classNames = `mx_MPollBody_option${
|
const classNames = `mx_MPollBody_option${
|
||||||
checked ? " mx_MPollBody_option_checked": ""
|
checked ? " mx_MPollBody_option_checked": ""
|
||||||
}`;
|
}`;
|
||||||
|
const answerVotes = votes.get(answer.id) ?? 0;
|
||||||
|
const answerPercent = Math.round(
|
||||||
|
100.0 * answerVotes / totalVotes);
|
||||||
return <div
|
return <div
|
||||||
key={answer.id}
|
key={answer.id}
|
||||||
className={classNames}
|
className={classNames}
|
||||||
|
@ -72,22 +198,116 @@ export default class MPollBody extends React.Component<IBodyProps, IState> {
|
||||||
onChange={this.onOptionSelected}
|
onChange={this.onOptionSelected}
|
||||||
>
|
>
|
||||||
<div className="mx_MPollBody_optionVoteCount">
|
<div className="mx_MPollBody_optionVoteCount">
|
||||||
{ _t("%(number)s votes", { number: 0 }) }
|
{ _t("%(count)s votes", { count: answerVotes }) }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_MPollBody_optionText">
|
<div className="mx_MPollBody_optionText">
|
||||||
{ answer[TEXT_NODE_TYPE] }
|
{ answer[TEXT_NODE_TYPE] }
|
||||||
</div>
|
</div>
|
||||||
</StyledRadioButton>
|
</StyledRadioButton>
|
||||||
<div className="mx_MPollBody_popularityBackground">
|
<div className="mx_MPollBody_popularityBackground">
|
||||||
<div className="mx_MPollBody_popularityAmount" />
|
<div
|
||||||
|
className="mx_MPollBody_popularityAmount"
|
||||||
|
style={{ "width": `${answerPercent}%` }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_MPollBody_totalVotes">
|
<div className="mx_MPollBody_totalVotes">
|
||||||
{ _t( "Based on %(total)s votes", { total: 0 } ) }
|
{ _t( "Based on %(count)s votes", { count: totalVotes } ) }
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class UserVote {
|
||||||
|
constructor(public readonly ts: number, public readonly sender: string, public readonly answers: string[]) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function userResponseFromPollResponseEvent(event: MatrixEvent): UserVote {
|
||||||
|
const pr = event.getContent() as IPollResponse;
|
||||||
|
const answers = pr[POLL_RESPONSE_EVENT_TYPE.name].answers;
|
||||||
|
|
||||||
|
return new UserVote(
|
||||||
|
event.getTs(),
|
||||||
|
event.getSender(),
|
||||||
|
answers,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
.map(userResponseFromPollResponseEvent);
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Figure out the correct vote for each user.
|
||||||
|
* @returns a Map of user ID to their vote info
|
||||||
|
*/
|
||||||
|
function collectUserVotes(
|
||||||
|
userResponses: Array<UserVote>,
|
||||||
|
selected?: string,
|
||||||
|
): Map<string, UserVote> {
|
||||||
|
const userVotes: Map<string, UserVote> = new Map();
|
||||||
|
|
||||||
|
for (const response of userResponses) {
|
||||||
|
const otherResponse = userVotes.get(response.sender);
|
||||||
|
if (!otherResponse || otherResponse.ts < response.ts) {
|
||||||
|
userVotes.set(response.sender, response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selected) {
|
||||||
|
const client = MatrixClientPeg.get();
|
||||||
|
const userId = client.getUserId();
|
||||||
|
userVotes.set(userId, new UserVote(0, userId, [selected]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return userVotes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function countVotes(
|
||||||
|
userVotes: Map<string, UserVote>,
|
||||||
|
pollStart: IPollContent,
|
||||||
|
): Map<string, number> {
|
||||||
|
const collected = new Map<string, number>();
|
||||||
|
|
||||||
|
const pollInfo = pollStart[POLL_START_EVENT_TYPE.name];
|
||||||
|
const maxSelections = 1; // See MSC3381 - later this will be in pollInfo
|
||||||
|
|
||||||
|
const allowedAnswerIds = pollInfo.answers.map((ans: IPollAnswer) => ans.id);
|
||||||
|
function isValidAnswer(answerId: string) {
|
||||||
|
return allowedAnswerIds.includes(answerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const response of userVotes.values()) {
|
||||||
|
if (response.answers.every(isValidAnswer)) {
|
||||||
|
for (const [index, answerId] of response.answers.entries()) {
|
||||||
|
if (index >= maxSelections) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (collected.has(answerId)) {
|
||||||
|
collected.set(answerId, collected.get(answerId) + 1);
|
||||||
|
} else {
|
||||||
|
collected.set(answerId, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return collected;
|
||||||
|
}
|
||||||
|
|
|
@ -28,12 +28,16 @@ import { ReactAnyComponent } from "../../../@types/common";
|
||||||
import { EventType, MsgType } from "matrix-js-sdk/src/@types/event";
|
import { EventType, MsgType } from "matrix-js-sdk/src/@types/event";
|
||||||
import { IBodyProps } from "./IBodyProps";
|
import { IBodyProps } from "./IBodyProps";
|
||||||
import { POLL_START_EVENT_TYPE } from '../../../polls/consts';
|
import { POLL_START_EVENT_TYPE } from '../../../polls/consts';
|
||||||
|
import { Relations } from 'matrix-js-sdk/src/models/relations';
|
||||||
|
|
||||||
// onMessageAllowed is handled internally
|
// onMessageAllowed is handled internally
|
||||||
interface IProps extends Omit<IBodyProps, "onMessageAllowed"> {
|
interface IProps extends Omit<IBodyProps, "onMessageAllowed"> {
|
||||||
/* overrides for the msgtype-specific components, used by ReplyTile to override file rendering */
|
/* overrides for the msgtype-specific components, used by ReplyTile to override file rendering */
|
||||||
overrideBodyTypes?: Record<string, React.Component>;
|
overrideBodyTypes?: Record<string, React.Component>;
|
||||||
overrideEventTypes?: Record<string, React.Component>;
|
overrideEventTypes?: Record<string, React.Component>;
|
||||||
|
|
||||||
|
// helper function to access relations for this event
|
||||||
|
getRelationsForEvent?: (eventId: string, relationType: string, eventType: string) => Relations;
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.messages.MessageEvent")
|
@replaceableComponent("views.messages.MessageEvent")
|
||||||
|
@ -154,6 +158,7 @@ export default class MessageEvent extends React.Component<IProps> implements IMe
|
||||||
onMessageAllowed={this.onTileUpdate}
|
onMessageAllowed={this.onTileUpdate}
|
||||||
permalinkCreator={this.props.permalinkCreator}
|
permalinkCreator={this.props.permalinkCreator}
|
||||||
mediaEventHelper={this.mediaHelper}
|
mediaEventHelper={this.mediaHelper}
|
||||||
|
getRelationsForEvent={this.props.getRelationsForEvent}
|
||||||
/> : null;
|
/> : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -990,7 +990,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
return this.props.getRelationsForEvent(eventId, "m.annotation", "m.reaction");
|
return this.props.getRelationsForEvent(eventId, "m.annotation", "m.reaction");
|
||||||
};
|
};
|
||||||
|
|
||||||
private onReactionsCreated = (relationType, eventType) => {
|
private onReactionsCreated = (relationType: string, eventType: string) => {
|
||||||
if (relationType !== "m.annotation" || eventType !== "m.reaction") {
|
if (relationType !== "m.annotation" || eventType !== "m.reaction") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1286,6 +1286,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
onHeightChanged={this.props.onHeightChanged}
|
onHeightChanged={this.props.onHeightChanged}
|
||||||
tileShape={this.props.tileShape}
|
tileShape={this.props.tileShape}
|
||||||
editState={this.props.editState}
|
editState={this.props.editState}
|
||||||
|
getRelationsForEvent={this.props.getRelationsForEvent}
|
||||||
/>
|
/>
|
||||||
</div>,
|
</div>,
|
||||||
]);
|
]);
|
||||||
|
@ -1324,6 +1325,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
tileShape={this.props.tileShape}
|
tileShape={this.props.tileShape}
|
||||||
editState={this.props.editState}
|
editState={this.props.editState}
|
||||||
replacingEventId={this.props.replacingEventId}
|
replacingEventId={this.props.replacingEventId}
|
||||||
|
getRelationsForEvent={this.props.getRelationsForEvent}
|
||||||
/>
|
/>
|
||||||
{ actionBar }
|
{ actionBar }
|
||||||
{ timestamp }
|
{ timestamp }
|
||||||
|
@ -1373,6 +1375,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
onHeightChanged={this.props.onHeightChanged}
|
onHeightChanged={this.props.onHeightChanged}
|
||||||
callEventGrouper={this.props.callEventGrouper}
|
callEventGrouper={this.props.callEventGrouper}
|
||||||
tileShape={this.props.tileShape}
|
tileShape={this.props.tileShape}
|
||||||
|
getRelationsForEvent={this.props.getRelationsForEvent}
|
||||||
/>
|
/>
|
||||||
{ keyRequestInfo }
|
{ keyRequestInfo }
|
||||||
{ this.renderThreadPanelSummary() }
|
{ this.renderThreadPanelSummary() }
|
||||||
|
@ -1410,6 +1413,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
tileShape={this.props.tileShape}
|
tileShape={this.props.tileShape}
|
||||||
onHeightChanged={this.props.onHeightChanged}
|
onHeightChanged={this.props.onHeightChanged}
|
||||||
editState={this.props.editState}
|
editState={this.props.editState}
|
||||||
|
getRelationsForEvent={this.props.getRelationsForEvent}
|
||||||
/>
|
/>
|
||||||
</div>,
|
</div>,
|
||||||
<a
|
<a
|
||||||
|
@ -1463,6 +1467,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
permalinkCreator={this.props.permalinkCreator}
|
permalinkCreator={this.props.permalinkCreator}
|
||||||
onHeightChanged={this.props.onHeightChanged}
|
onHeightChanged={this.props.onHeightChanged}
|
||||||
callEventGrouper={this.props.callEventGrouper}
|
callEventGrouper={this.props.callEventGrouper}
|
||||||
|
getRelationsForEvent={this.props.getRelationsForEvent}
|
||||||
/>
|
/>
|
||||||
{ keyRequestInfo }
|
{ keyRequestInfo }
|
||||||
{ actionBar }
|
{ actionBar }
|
||||||
|
|
|
@ -2045,8 +2045,10 @@
|
||||||
"Declining …": "Declining …",
|
"Declining …": "Declining …",
|
||||||
"%(name)s wants to verify": "%(name)s wants to verify",
|
"%(name)s wants to verify": "%(name)s wants to verify",
|
||||||
"You sent a verification request": "You sent a verification request",
|
"You sent a verification request": "You sent a verification request",
|
||||||
"%(number)s votes": "%(number)s votes",
|
"%(count)s votes|other": "%(count)s votes",
|
||||||
"Based on %(total)s votes": "Based on %(total)s votes",
|
"%(count)s votes|one": "%(count)s vote",
|
||||||
|
"Based on %(count)s votes|other": "Based on %(count)s votes",
|
||||||
|
"Based on %(count)s votes|one": "Based on %(count)s vote",
|
||||||
"Error decrypting video": "Error decrypting video",
|
"Error decrypting video": "Error decrypting video",
|
||||||
"Error processing voice message": "Error processing voice message",
|
"Error processing voice message": "Error processing voice message",
|
||||||
"Add reaction": "Add reaction",
|
"Add reaction": "Add reaction",
|
||||||
|
|
|
@ -18,6 +18,7 @@ import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue";
|
||||||
import { IContent } from "matrix-js-sdk/src/models/event";
|
import { IContent } from "matrix-js-sdk/src/models/event";
|
||||||
|
|
||||||
export const POLL_START_EVENT_TYPE = new UnstableValue("m.poll.start", "org.matrix.msc3381.poll.start");
|
export const POLL_START_EVENT_TYPE = new UnstableValue("m.poll.start", "org.matrix.msc3381.poll.start");
|
||||||
|
export const POLL_RESPONSE_EVENT_TYPE = new UnstableValue("m.poll.response", "org.matrix.msc3381.poll.response");
|
||||||
export const POLL_KIND_DISCLOSED = new UnstableValue("m.poll.disclosed", "org.matrix.msc3381.poll.disclosed");
|
export const POLL_KIND_DISCLOSED = new UnstableValue("m.poll.disclosed", "org.matrix.msc3381.poll.disclosed");
|
||||||
export const POLL_KIND_UNDISCLOSED = new UnstableValue("m.poll.undisclosed", "org.matrix.msc3381.poll.undisclosed");
|
export const POLL_KIND_UNDISCLOSED = new UnstableValue("m.poll.undisclosed", "org.matrix.msc3381.poll.undisclosed");
|
||||||
|
|
||||||
|
@ -40,6 +41,12 @@ export interface IPollContent extends IContent {
|
||||||
[TEXT_NODE_TYPE]: string;
|
[TEXT_NODE_TYPE]: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IPollResponse extends IContent {
|
||||||
|
[POLL_RESPONSE_EVENT_TYPE.name]: {
|
||||||
|
answers: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function makePollContent(question: string, answers: string[], kind: string): IPollContent {
|
export function makePollContent(question: string, answers: string[], kind: string): IPollContent {
|
||||||
question = question.trim();
|
question = question.trim();
|
||||||
answers = answers.map(a => a.trim()).filter(a => !!a);
|
answers = answers.map(a => a.trim()).filter(a => !!a);
|
||||||
|
|
476
test/components/views/messages/MPollBody-test.tsx
Normal file
476
test/components/views/messages/MPollBody-test.tsx
Normal file
|
@ -0,0 +1,476 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 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 React from "react";
|
||||||
|
import { mount, ReactWrapper } from "enzyme";
|
||||||
|
|
||||||
|
import sdk from "../../../skinned-sdk";
|
||||||
|
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 { UserVote, allVotes } from "../../../../src/components/views/messages/MPollBody";
|
||||||
|
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||||
|
|
||||||
|
const _MPollBody = sdk.getComponent("views.messages.MPollBody");
|
||||||
|
const MPollBody = TestUtils.wrapInMatrixClientContext(_MPollBody);
|
||||||
|
|
||||||
|
MatrixClientPeg.matrixClient = {
|
||||||
|
getUserId: () => "@me:example.com",
|
||||||
|
sendEvent: () => Promise.resolve({ "event_id": "fake_send_id" }),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("MPollBody", () => {
|
||||||
|
it("finds no votes if there are none", () => {
|
||||||
|
expect(allVotes(newPollRelations([]))).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can find all the valid responses to a poll", () => {
|
||||||
|
const ev1 = responseEvent();
|
||||||
|
const ev2 = responseEvent();
|
||||||
|
const badEvent = badResponseEvent();
|
||||||
|
|
||||||
|
const pollRelations = newPollRelations([ev1, badEvent, ev2]);
|
||||||
|
expect(allVotes(pollRelations)).toEqual([
|
||||||
|
new UserVote(
|
||||||
|
ev1.getTs(),
|
||||||
|
ev1.getSender(),
|
||||||
|
ev1.getContent()["org.matrix.msc3381.poll.response"].answers,
|
||||||
|
),
|
||||||
|
new UserVote(
|
||||||
|
ev2.getTs(),
|
||||||
|
ev2.getSender(),
|
||||||
|
ev2.getContent()["org.matrix.msc3381.poll.response"].answers,
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("finds no votes if none were made", () => {
|
||||||
|
const votes = [];
|
||||||
|
const body = newMPollBody(votes);
|
||||||
|
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("0 votes");
|
||||||
|
expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("Based on 0 votes");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("finds votes from multiple people", () => {
|
||||||
|
const votes = [
|
||||||
|
responseEvent("@andyb:example.com", "pizza"),
|
||||||
|
responseEvent("@bellc:example.com", "pizza"),
|
||||||
|
responseEvent("@catrd:example.com", "poutine"),
|
||||||
|
responseEvent("@dune2:example.com", "wings"),
|
||||||
|
];
|
||||||
|
const body = newMPollBody(votes);
|
||||||
|
expect(votesCount(body, "pizza")).toBe("2 votes");
|
||||||
|
expect(votesCount(body, "poutine")).toBe("1 vote");
|
||||||
|
expect(votesCount(body, "italian")).toBe("0 votes");
|
||||||
|
expect(votesCount(body, "wings")).toBe("1 vote");
|
||||||
|
expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("Based on 4 votes");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("takes someone's most recent vote if they voted several times", () => {
|
||||||
|
const votes = [
|
||||||
|
responseEvent("@fiona:example.com", "pizza", 12),
|
||||||
|
responseEvent("@fiona:example.com", "wings", 20), // latest fiona
|
||||||
|
responseEvent("@qbert:example.com", "pizza", 14),
|
||||||
|
responseEvent("@qbert:example.com", "poutine", 16), // latest qbert
|
||||||
|
responseEvent("@qbert:example.com", "wings", 15),
|
||||||
|
];
|
||||||
|
const body = newMPollBody(votes);
|
||||||
|
expect(votesCount(body, "pizza")).toBe("0 votes");
|
||||||
|
expect(votesCount(body, "poutine")).toBe("1 vote");
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses my local vote", () => {
|
||||||
|
// Given I haven't voted
|
||||||
|
const votes = [
|
||||||
|
responseEvent("@nf:example.com", "pizza", 15),
|
||||||
|
responseEvent("@fg:example.com", "pizza", 15),
|
||||||
|
responseEvent("@hi:example.com", "pizza", 15),
|
||||||
|
];
|
||||||
|
const body = newMPollBody(votes);
|
||||||
|
|
||||||
|
// When I vote for Italian
|
||||||
|
clickRadio(body, "italian");
|
||||||
|
|
||||||
|
// My vote is counted
|
||||||
|
expect(votesCount(body, "pizza")).toBe("3 votes");
|
||||||
|
expect(votesCount(body, "poutine")).toBe("0 votes");
|
||||||
|
expect(votesCount(body, "italian")).toBe("1 vote");
|
||||||
|
expect(votesCount(body, "wings")).toBe("0 votes");
|
||||||
|
|
||||||
|
expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("Based on 4 votes");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("overrides my other votes with my local vote", () => {
|
||||||
|
// Given two of us have voted for Italian
|
||||||
|
const votes = [
|
||||||
|
responseEvent("@me:example.com", "pizza", 12),
|
||||||
|
responseEvent("@me:example.com", "poutine", 13),
|
||||||
|
responseEvent("@me:example.com", "italian", 14),
|
||||||
|
responseEvent("@nf:example.com", "italian", 15),
|
||||||
|
];
|
||||||
|
const body = newMPollBody(votes);
|
||||||
|
|
||||||
|
// When I click Wings
|
||||||
|
clickRadio(body, "wings");
|
||||||
|
|
||||||
|
// Then my vote is counted for Wings, and not for Italian
|
||||||
|
expect(votesCount(body, "pizza")).toBe("0 votes");
|
||||||
|
expect(votesCount(body, "poutine")).toBe("0 votes");
|
||||||
|
expect(votesCount(body, "italian")).toBe("1 vote");
|
||||||
|
expect(votesCount(body, "wings")).toBe("1 vote");
|
||||||
|
|
||||||
|
expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("Based on 2 votes");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores extra answers", () => {
|
||||||
|
// When cb votes for 2 things, we consider the first only
|
||||||
|
const votes = [
|
||||||
|
responseEvent("@cb:example.com", ["pizza", "wings"]),
|
||||||
|
responseEvent("@da:example.com", "wings"),
|
||||||
|
];
|
||||||
|
const body = newMPollBody(votes);
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows un-voting by passing an empty vote", () => {
|
||||||
|
const votes = [
|
||||||
|
responseEvent("@nc:example.com", "pizza", 12),
|
||||||
|
responseEvent("@nc:example.com", [], 13),
|
||||||
|
responseEvent("@md:example.com", "italian"),
|
||||||
|
];
|
||||||
|
const body = newMPollBody(votes);
|
||||||
|
expect(votesCount(body, "pizza")).toBe("0 votes");
|
||||||
|
expect(votesCount(body, "poutine")).toBe("0 votes");
|
||||||
|
expect(votesCount(body, "italian")).toBe("1 vote");
|
||||||
|
expect(votesCount(body, "wings")).toBe("0 votes");
|
||||||
|
expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("Based on 1 vote");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows re-voting after un-voting", () => {
|
||||||
|
const votes = [
|
||||||
|
responseEvent("@op:example.com", "pizza", 12),
|
||||||
|
responseEvent("@op:example.com", [], 13),
|
||||||
|
responseEvent("@op:example.com", "italian", 14),
|
||||||
|
responseEvent("@qr:example.com", "italian"),
|
||||||
|
];
|
||||||
|
const body = newMPollBody(votes);
|
||||||
|
expect(votesCount(body, "pizza")).toBe("0 votes");
|
||||||
|
expect(votesCount(body, "poutine")).toBe("0 votes");
|
||||||
|
expect(votesCount(body, "italian")).toBe("2 votes");
|
||||||
|
expect(votesCount(body, "wings")).toBe("0 votes");
|
||||||
|
expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("Based on 2 votes");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("treats any invalid answer as a spoiled ballot", () => {
|
||||||
|
// Note that tr's second vote has a valid first answer, but
|
||||||
|
// the ballot is still spoiled because the second answer is
|
||||||
|
// invalid, even though we would ignore it if we continued.
|
||||||
|
const votes = [
|
||||||
|
responseEvent("@tr:example.com", "pizza", 12),
|
||||||
|
responseEvent("@tr:example.com", ["pizza", "doesntexist"], 13),
|
||||||
|
responseEvent("@uy:example.com", "italian", 14),
|
||||||
|
responseEvent("@uy:example.com", "doesntexist", 15),
|
||||||
|
];
|
||||||
|
const body = newMPollBody(votes);
|
||||||
|
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("0 votes");
|
||||||
|
expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("Based on 0 votes");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows re-voting after a spoiled ballot", () => {
|
||||||
|
const votes = [
|
||||||
|
responseEvent("@tr:example.com", "pizza", 12),
|
||||||
|
responseEvent("@tr:example.com", ["pizza", "doesntexist"], 13),
|
||||||
|
responseEvent("@uy:example.com", "italian", 14),
|
||||||
|
responseEvent("@uy:example.com", "doesntexist", 15),
|
||||||
|
responseEvent("@uy:example.com", "poutine", 16),
|
||||||
|
];
|
||||||
|
const body = newMPollBody(votes);
|
||||||
|
expect(votesCount(body, "pizza")).toBe("0 votes");
|
||||||
|
expect(votesCount(body, "poutine")).toBe("1 vote");
|
||||||
|
expect(votesCount(body, "italian")).toBe("0 votes");
|
||||||
|
expect(votesCount(body, "wings")).toBe("0 votes");
|
||||||
|
expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("Based on 1 vote");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders nothing if poll has no answers", () => {
|
||||||
|
const answers = [];
|
||||||
|
const votes = [];
|
||||||
|
const body = newMPollBody(votes, answers);
|
||||||
|
expect(body.html()).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders nothing if poll has more than 20 answers", () => {
|
||||||
|
const answers = [...Array(21).keys()].map((i) => {
|
||||||
|
return { "id": `id${i}`, "org.matrix.msc1767.text": `Name ${i}` };
|
||||||
|
});
|
||||||
|
const votes = [];
|
||||||
|
const body = newMPollBody(votes, answers);
|
||||||
|
expect(body.html()).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sends a vote event when I choose an option", () => {
|
||||||
|
const receivedEvents = [];
|
||||||
|
MatrixClientPeg.matrixClient.sendEvent = (
|
||||||
|
roomId: string,
|
||||||
|
eventType: string,
|
||||||
|
content: IContent,
|
||||||
|
txnId?: string,
|
||||||
|
callback?: Callback,
|
||||||
|
): Promise<ISendEventResponse> => {
|
||||||
|
receivedEvents.push( { roomId, eventType, content, txnId, callback } );
|
||||||
|
return Promise.resolve({ "event_id": "fake_tracked_send_id" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const votes = [];
|
||||||
|
const body = newMPollBody(votes);
|
||||||
|
clickRadio(body, "wings");
|
||||||
|
expect(receivedEvents).toEqual([
|
||||||
|
expectedResponseEvent("wings"),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sends only one vote event when I click several times", () => {
|
||||||
|
const receivedEvents = [];
|
||||||
|
MatrixClientPeg.matrixClient.sendEvent = (
|
||||||
|
roomId: string,
|
||||||
|
eventType: string,
|
||||||
|
content: IContent,
|
||||||
|
txnId?: string,
|
||||||
|
callback?: Callback,
|
||||||
|
): Promise<ISendEventResponse> => {
|
||||||
|
receivedEvents.push( { roomId, eventType, content, txnId, callback } );
|
||||||
|
return Promise.resolve({ "event_id": "fake_tracked_send_id" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const votes = [];
|
||||||
|
const body = newMPollBody(votes);
|
||||||
|
clickRadio(body, "wings");
|
||||||
|
clickRadio(body, "wings");
|
||||||
|
clickRadio(body, "wings");
|
||||||
|
clickRadio(body, "wings");
|
||||||
|
expect(receivedEvents).toEqual([
|
||||||
|
expectedResponseEvent("wings"),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sends several events when I click different options", () => {
|
||||||
|
const receivedEvents = [];
|
||||||
|
MatrixClientPeg.matrixClient.sendEvent = (
|
||||||
|
roomId: string,
|
||||||
|
eventType: string,
|
||||||
|
content: IContent,
|
||||||
|
txnId?: string,
|
||||||
|
callback?: Callback,
|
||||||
|
): Promise<ISendEventResponse> => {
|
||||||
|
receivedEvents.push( { roomId, eventType, content, txnId, callback } );
|
||||||
|
return Promise.resolve({ "event_id": "fake_tracked_send_id" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const votes = [];
|
||||||
|
const body = newMPollBody(votes);
|
||||||
|
clickRadio(body, "wings");
|
||||||
|
clickRadio(body, "italian");
|
||||||
|
clickRadio(body, "poutine");
|
||||||
|
expect(receivedEvents).toEqual([
|
||||||
|
expectedResponseEvent("wings"),
|
||||||
|
expectedResponseEvent("italian"),
|
||||||
|
expectedResponseEvent("poutine"),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders a poll with no votes", () => {
|
||||||
|
const votes = [];
|
||||||
|
const body = newMPollBody(votes);
|
||||||
|
expect(body).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders a poll with only non-local votes", () => {
|
||||||
|
const votes = [
|
||||||
|
responseEvent("@op:example.com", "pizza", 12),
|
||||||
|
responseEvent("@op:example.com", [], 13),
|
||||||
|
responseEvent("@op:example.com", "italian", 14),
|
||||||
|
responseEvent("@st:example.com", "wings", 15),
|
||||||
|
responseEvent("@qr:example.com", "italian", 16),
|
||||||
|
];
|
||||||
|
const body = newMPollBody(votes);
|
||||||
|
expect(body).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders a poll with local, non-local and invalid votes", () => {
|
||||||
|
const votes = [
|
||||||
|
responseEvent("@a:example.com", "pizza", 12),
|
||||||
|
responseEvent("@b:example.com", [], 13),
|
||||||
|
responseEvent("@c:example.com", "italian", 14),
|
||||||
|
responseEvent("@d:example.com", "italian", 14),
|
||||||
|
responseEvent("@e:example.com", "wings", 15),
|
||||||
|
responseEvent("@me:example.com", "italian", 16),
|
||||||
|
];
|
||||||
|
const body = newMPollBody(votes);
|
||||||
|
clickRadio(body, "italian");
|
||||||
|
expect(body).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function newPollRelations(relationEvents: Array<MatrixEvent>): Relations {
|
||||||
|
const pollRelations = new Relations(
|
||||||
|
"m.reference", "org.matrix.msc3381.poll.response", null);
|
||||||
|
for (const ev of relationEvents) {
|
||||||
|
pollRelations.addEvent(ev);
|
||||||
|
}
|
||||||
|
return pollRelations;
|
||||||
|
}
|
||||||
|
|
||||||
|
function newMPollBody(
|
||||||
|
relationEvents: Array<MatrixEvent>,
|
||||||
|
answers?: IPollAnswer[],
|
||||||
|
): ReactWrapper {
|
||||||
|
const pollRelations = new Relations(
|
||||||
|
"m.reference", "org.matrix.msc3381.poll.response", null);
|
||||||
|
for (const ev of relationEvents) {
|
||||||
|
pollRelations.addEvent(ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mount(<MPollBody
|
||||||
|
mxEvent={new MatrixEvent({
|
||||||
|
"event_id": "$mypoll",
|
||||||
|
"room_id": "#myroom:example.com",
|
||||||
|
"content": newPollStart(answers),
|
||||||
|
})}
|
||||||
|
getRelationsForEvent={
|
||||||
|
(eventId: string, relationType: string, eventType: string) => {
|
||||||
|
expect(eventId).toBe("$mypoll");
|
||||||
|
expect(relationType).toBe("m.reference");
|
||||||
|
expect(eventType).toBe("org.matrix.msc3381.poll.response");
|
||||||
|
return pollRelations;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clickRadio(wrapper: ReactWrapper, value: string) {
|
||||||
|
wrapper.find(`StyledRadioButton[value="${value}"]`).simulate("click");
|
||||||
|
}
|
||||||
|
|
||||||
|
function votesCount(wrapper: ReactWrapper, value: string): string {
|
||||||
|
return wrapper.find(
|
||||||
|
`StyledRadioButton[value="${value}"] .mx_MPollBody_optionVoteCount`,
|
||||||
|
).text();
|
||||||
|
}
|
||||||
|
|
||||||
|
function newPollStart(answers?: IPollAnswer[]): IPollContent {
|
||||||
|
if (!answers) {
|
||||||
|
answers = [
|
||||||
|
{ "id": "pizza", "org.matrix.msc1767.text": "Pizza" },
|
||||||
|
{ "id": "poutine", "org.matrix.msc1767.text": "Poutine" },
|
||||||
|
{ "id": "italian", "org.matrix.msc1767.text": "Italian" },
|
||||||
|
{ "id": "wings", "org.matrix.msc1767.text": "Wings" },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"org.matrix.msc3381.poll.start": {
|
||||||
|
"question": {
|
||||||
|
"org.matrix.msc1767.text": "What should we order for the party?",
|
||||||
|
},
|
||||||
|
"kind": "org.matrix.msc3381.poll.disclosed",
|
||||||
|
"answers": answers,
|
||||||
|
},
|
||||||
|
"org.matrix.msc1767.text": "What should we order for the party?\n" +
|
||||||
|
"1. Pizza\n2. Poutine\n3. Italian\n4. Wings",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function badResponseEvent(): MatrixEvent {
|
||||||
|
return new MatrixEvent(
|
||||||
|
{
|
||||||
|
"event_id": nextId(),
|
||||||
|
"type": "org.matrix.msc3381.poll.response",
|
||||||
|
"content": {
|
||||||
|
"m.relates_to": {
|
||||||
|
"rel_type": "m.reference",
|
||||||
|
"event_id": "$mypoll",
|
||||||
|
},
|
||||||
|
// Does not actually contain a response
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function responseEvent(
|
||||||
|
sender = "@alice:example.com",
|
||||||
|
answers: string | Array<string> = "italian",
|
||||||
|
ts = 0,
|
||||||
|
): MatrixEvent {
|
||||||
|
const ans = typeof answers === "string" ? [answers] : answers;
|
||||||
|
return new MatrixEvent(
|
||||||
|
{
|
||||||
|
"event_id": nextId(),
|
||||||
|
"room_id": "#myroom:example.com",
|
||||||
|
"origin_server_ts": ts,
|
||||||
|
"type": "org.matrix.msc3381.poll.response",
|
||||||
|
"sender": sender,
|
||||||
|
"content": {
|
||||||
|
"m.relates_to": {
|
||||||
|
"rel_type": "m.reference",
|
||||||
|
"event_id": "$mypoll",
|
||||||
|
},
|
||||||
|
"org.matrix.msc3381.poll.response": {
|
||||||
|
"answers": ans,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectedResponseEvent(answer: string) {
|
||||||
|
return {
|
||||||
|
"content": {
|
||||||
|
"org.matrix.msc3381.poll.response": {
|
||||||
|
"answers": [answer],
|
||||||
|
},
|
||||||
|
"m.relates_to": {
|
||||||
|
"event_id": "$mypoll",
|
||||||
|
"rel_type": "m.reference",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"eventType": "org.matrix.msc3381.poll.response",
|
||||||
|
"roomId": "#myroom:example.com",
|
||||||
|
"txnId": undefined,
|
||||||
|
"callback": undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let EVENT_ID = 0;
|
||||||
|
function nextId(): string {
|
||||||
|
EVENT_ID++;
|
||||||
|
return EVENT_ID.toString();
|
||||||
|
}
|
1000
test/components/views/messages/__snapshots__/MPollBody-test.tsx.snap
Normal file
1000
test/components/views/messages/__snapshots__/MPollBody-test.tsx.snap
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue