Polls: Creation form & start event (#7001)

* PSFD-423: Permission check for polls dialog

* PSFD-423: Implement compound scrollable dialog and skeleton create poll

* PSFD-325: Ask the question

* PSFD-328: Ask for options

* PSFD-423: Ensure form submission semantics work for dialogs

* PSFD-328: Option semantics

* Can delete all option to end up with zero
* Minimum 2 to submit the form

* PSFD-316: Send poll start event

* Appease the linter

* PSFD-328: Reduce padding between options to account for field size

* Iterate per design

* Fix submission
This commit is contained in:
Travis Ralston 2021-11-01 23:44:42 -06:00 committed by GitHub
parent 5202c0a237
commit 73731cc478
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 546 additions and 23 deletions

View file

@ -404,7 +404,10 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
* We should go through and have one consistent set of styles for buttons throughout the app.
* For now, I am duplicating the selectors here for mx_Dialog and mx_DialogButtons.
*/
.mx_Dialog button, .mx_Dialog input[type="submit"], .mx_Dialog_buttons button, .mx_Dialog_buttons input[type="submit"] {
.mx_Dialog button:not(.mx_Dialog_nonDialogButton),
.mx_Dialog input[type="submit"],
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton),
.mx_Dialog_buttons input[type="submit"] {
@mixin mx_DialogButton;
margin-left: 0px;
margin-right: 8px;
@ -417,36 +420,52 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
font-family: inherit;
}
.mx_Dialog button:last-child {
.mx_Dialog button:not(.mx_Dialog_nonDialogButton):last-child {
margin-right: 0px;
}
.mx_Dialog button:hover, .mx_Dialog input[type="submit"]:hover, .mx_Dialog_buttons button:hover, .mx_Dialog_buttons input[type="submit"]:hover {
.mx_Dialog button:not(.mx_Dialog_nonDialogButton):hover,
.mx_Dialog input[type="submit"]:hover,
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):hover,
.mx_Dialog_buttons input[type="submit"]:hover {
@mixin mx_DialogButton_hover;
}
.mx_Dialog button:focus, .mx_Dialog input[type="submit"]:focus, .mx_Dialog_buttons button:focus, .mx_Dialog_buttons input[type="submit"]:focus {
.mx_Dialog button:not(.mx_Dialog_nonDialogButton):focus,
.mx_Dialog input[type="submit"]:focus,
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):focus,
.mx_Dialog_buttons input[type="submit"]:focus {
filter: brightness($focus-brightness);
}
.mx_Dialog button.mx_Dialog_primary, .mx_Dialog input[type="submit"].mx_Dialog_primary, .mx_Dialog_buttons button.mx_Dialog_primary, .mx_Dialog_buttons input[type="submit"].mx_Dialog_primary {
.mx_Dialog button.mx_Dialog_primary,
.mx_Dialog input[type="submit"].mx_Dialog_primary,
.mx_Dialog_buttons button.mx_Dialog_primary,
.mx_Dialog_buttons input[type="submit"].mx_Dialog_primary {
color: $accent-fg-color;
background-color: $accent-color;
min-width: 156px;
}
.mx_Dialog button.danger, .mx_Dialog input[type="submit"].danger, .mx_Dialog_buttons button.danger, .mx_Dialog_buttons input[type="submit"].danger {
.mx_Dialog button.danger,
.mx_Dialog input[type="submit"].danger,
.mx_Dialog_buttons button.danger,
.mx_Dialog_buttons input[type="submit"].danger {
background-color: $warning-color;
border: solid 1px $warning-color;
color: $accent-fg-color;
}
.mx_Dialog button.warning, .mx_Dialog input[type="submit"].warning {
.mx_Dialog button.warning,
.mx_Dialog input[type="submit"].warning {
border: solid 1px $warning-color;
color: $warning-color;
}
.mx_Dialog button:disabled, .mx_Dialog input[type="submit"]:disabled, .mx_Dialog_buttons button:disabled, .mx_Dialog_buttons input[type="submit"]:disabled {
.mx_Dialog button:not(.mx_Dialog_nonDialogButton):disabled,
.mx_Dialog input[type="submit"]:disabled,
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):disabled,
.mx_Dialog_buttons input[type="submit"]:disabled {
background-color: $light-fg-color;
border: solid 1px $light-fg-color;
opacity: 0.7;

View file

@ -74,6 +74,7 @@
@import "./views/dialogs/_ChangelogDialog.scss";
@import "./views/dialogs/_ChatCreateOrReuseChatDialog.scss";
@import "./views/dialogs/_CommunityPrototypeInviteDialog.scss";
@import "./views/dialogs/_CompoundDialog.scss";
@import "./views/dialogs/_ConfirmSpaceUserActionDialog.scss";
@import "./views/dialogs/_ConfirmUserActionDialog.scss";
@import "./views/dialogs/_CreateCommunityPrototypeDialog.scss";
@ -99,6 +100,7 @@
@import "./views/dialogs/_MessageEditHistoryDialog.scss";
@import "./views/dialogs/_ModalWidgetDialog.scss";
@import "./views/dialogs/_NewSessionReviewDialog.scss";
@import "./views/dialogs/_PollCreateDialog.scss";
@import "./views/dialogs/_RegistrationEmailPromptDialog.scss";
@import "./views/dialogs/_RoomSettingsDialog.scss";
@import "./views/dialogs/_RoomSettingsDialogBridges.scss";

View file

@ -0,0 +1,87 @@
/*
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.
*/
// --------------------------------------------------------------------------------
// DEV NOTE: This stylesheet covers dialogs listed by the compound, including
// over multiple React components. The actual inner contents of the dialog should
// be in their respective stylesheets.
// --------------------------------------------------------------------------------
// Override legacy/default styles for dialogs
.mx_Dialog_wrapper.mx_CompoundDialog > .mx_Dialog {
padding: 0; // we'll manage it ourselves
color: $primary-content;
}
.mx_CompoundDialog {
.mx_CompoundDialog_header {
padding: 32px 32px 16px 32px;
h1 {
display: inline-block;
font-weight: 600;
font-size: $font-24px;
margin: 0; // managed by header class
}
.mx_CompoundDialog_cancelButton {
mask: url('$(res)/img/feather-customised/cancel.svg');
mask-repeat: no-repeat;
mask-position: center;
mask-size: cover;
width: 20px;
height: 20px;
background-color: $dialog-close-fg-color;
cursor: pointer;
// Align with middle of title, 34px from right edge
position: absolute;
top: 34px;
right: 34px;
}
}
.mx_CompoundDialog_content {
overflow: auto;
padding: 8px 32px;
}
.mx_CompoundDialog_footer {
padding: 20px 32px;
text-align: right;
position: absolute;
bottom: 0;
left: 0;
right: 0;
.mx_AccessibleButton {
margin-left: 24px;
}
}
}
.mx_ScrollableBaseDialog {
width: 544px; // fixed
height: 516px; // fixed
.mx_CompoundDialog_content {
height: 349px; // dialogHeight - header - footer
}
.mx_CompoundDialog_footer {
box-shadow: 0px -4px 4px rgba(0, 0, 0, 0.05); // hardcoded colour for both themes
}
}

View file

@ -0,0 +1,70 @@
/*
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.
*/
.mx_PollCreateDialog {
h2 {
font-weight: 600;
font-size: $font-15px;
line-height: $font-24px;
margin-top: 0;
margin-bottom: 8px;
&:nth-child(n + 2) {
margin-top: 20px;
}
}
.mx_PollCreateDialog_option {
display: flex;
align-items: center;
margin-top: 11px;
margin-bottom: 16px; // 11px from the top will collapse, so this creates a 16px gap between options
.mx_Field {
flex: 1;
margin: 0;
}
.mx_PollCreateDialog_removeOption {
margin-left: 12px;
width: 20px;
height: 20px;
border-radius: 50%;
background-color: $quinary-content;
cursor: pointer;
position: relative;
&::before {
content: "";
mask: url('$(res)/img/element-icons/x-8px.svg');
mask-repeat: no-repeat;
mask-position: center;
mask-size: cover;
width: 8px;
height: 8px;
position: absolute;
top: 6px;
left: 6px;
background-color: $secondary-content;
}
}
}
.mx_PollCreateDialog_addOption {
padding: 0;
margin-bottom: 40px; // arbitrary to create scrollable area under the poll
}
}

View file

@ -30,11 +30,13 @@ limitations under the License.
align-items: center;
justify-content: center;
font-size: $font-14px;
border: none; // override default <button /> styles
}
.mx_AccessibleButton_kind_primary {
color: $button-primary-fg-color;
background-color: $button-primary-bg-color;
border: 1px solid $button-primary-bg-color; // account for size loss of no border
font-weight: 600;
}

View file

@ -89,7 +89,8 @@ limitations under the License.
}
.mx_Field input:placeholder-shown:focus::placeholder,
.mx_Field textarea:placeholder-shown:focus::placeholder {
.mx_Field textarea:placeholder-shown:focus::placeholder,
.mx_Field.mx_Field_placeholderIsHint input::placeholder {
transition: color 0.25s ease-in 0.1s;
color: $greyed-fg-color;
}

View file

@ -0,0 +1,4 @@
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.999756 0.999756L8.99975 8.99975" stroke="#737D8C" stroke-width="2" stroke-linecap="round"/>
<path d="M9.00049 0.999756L1.00049 8.99975" stroke="#737D8C" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 310 B

View file

@ -0,0 +1,116 @@
/*
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, { FormEvent } from "react";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { Key } from "../../../Keyboard";
import { IDialogProps } from "./IDialogProps";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import FocusLock from "react-focus-lock";
import { _t } from "../../../languageHandler";
import AccessibleButton from "../elements/AccessibleButton";
export interface IScrollableBaseState {
canSubmit: boolean;
title: string;
actionLabel: string;
}
/**
* Scrollable dialog base from Compound (Web Components).
*/
export default abstract class ScrollableBaseModal<TProps extends IDialogProps, TState extends IScrollableBaseState>
extends React.PureComponent<TProps, TState> {
protected constructor(props: TProps) {
super(props);
}
protected get matrixClient(): MatrixClient {
return MatrixClientPeg.get();
}
private onKeyDown = (e: KeyboardEvent | React.KeyboardEvent): void => {
if (e.key === Key.ESCAPE) {
e.stopPropagation();
e.preventDefault();
this.cancel();
}
};
private onCancel = () => {
this.cancel();
};
private onSubmit = (e: MouseEvent | FormEvent) => {
e.stopPropagation();
e.preventDefault();
if (!this.state.canSubmit) return; // pretend the submit button was disabled
this.submit();
};
protected abstract cancel(): void;
protected abstract submit(): void;
protected abstract renderContent(): React.ReactNode;
public render(): JSX.Element {
return (
<MatrixClientContext.Provider value={this.matrixClient}>
<FocusLock
returnFocus={true}
lockProps={{
onKeyDown: this.onKeyDown,
role: "dialog",
["aria-labelledby"]: "mx_CompoundDialog_title",
// Like BaseDialog, we'll just point this at the whole content
["aria-describedby"]: "mx_CompoundDialog_content",
}}
className="mx_CompoundDialog mx_ScrollableBaseDialog"
>
<div className="mx_CompoundDialog_header">
<h1>{ this.state.title }</h1>
<AccessibleButton
onClick={this.onCancel}
className="mx_CompoundDialog_cancelButton"
aria-label={_t("Close dialog")}
/>
</div>
<form onSubmit={this.onSubmit}>
<div className="mx_CompoundDialog_content">
{ this.renderContent() }
</div>
<div className="mx_CompoundDialog_footer">
<AccessibleButton onClick={this.onCancel} kind="primary_outline">
{ _t("Cancel") }
</AccessibleButton>
<AccessibleButton
onClick={this.onSubmit}
kind="primary"
disabled={!this.state.canSubmit}
type="submit"
element="button"
className="mx_Dialog_nonDialogButton"
>
{ this.state.actionLabel }
</AccessibleButton>
</div>
</form>
</FocusLock>
</MatrixClientContext.Provider>
);
}
}

View file

@ -45,6 +45,9 @@ interface IProps {
label?: string;
// The field's placeholder string. Defaults to the label.
placeholder?: string;
// When true (default false), the placeholder will be shown instead of the label when
// the component is unfocused & empty.
usePlaceholderAsHint?: boolean;
// Optional component to include inside the field before the input.
prefixComponent?: React.ReactNode;
// Optional component to include inside the field after the input.
@ -226,6 +229,7 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
/* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */
const { element, prefixComponent, postfixComponent, className, onValidate, children,
tooltipContent, forceValidity, tooltipClassName, list, validateOnBlur, validateOnChange, validateOnFocus,
usePlaceholderAsHint,
...inputProps } = this.props;
// Set some defaults for the <input> element
@ -256,7 +260,8 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
// If we have a prefix element, leave the label always at the top left and
// don't animate it, as it looks a bit clunky and would add complexity to do
// properly.
mx_Field_labelAlwaysTopLeft: prefixComponent,
mx_Field_labelAlwaysTopLeft: prefixComponent || usePlaceholderAsHint,
mx_Field_placeholderIsHint: usePlaceholderAsHint,
mx_Field_valid: hasValidationFlag ? forceValidity : onValidate && this.state.valid === true,
mx_Field_invalid: hasValidationFlag
? !forceValidity

View file

@ -0,0 +1,144 @@
/*
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 ScrollableBaseModal, { IScrollableBaseState } from "../dialogs/ScrollableBaseModal";
import { IDialogProps } from "../dialogs/IDialogProps";
import React, { ChangeEvent, createRef } from "react";
import { _t } from "../../../languageHandler";
import { Room } from "matrix-js-sdk/src/models/room";
import { arrayFastClone, arraySeed } from "../../../utils/arrays";
import Field from "./Field";
import AccessibleButton from "./AccessibleButton";
import { makePollContent, POLL_KIND_DISCLOSED, POLL_START_EVENT_TYPE } from "../../../polls/consts";
interface IProps extends IDialogProps {
room: Room;
}
interface IState extends IScrollableBaseState {
question: string;
options: string[];
busy: boolean;
}
const MIN_OPTIONS = 2;
const MAX_OPTIONS = 20;
const DEFAULT_NUM_OPTIONS = 2;
export default class PollCreateDialog extends ScrollableBaseModal<IProps, IState> {
private addOptionRef = createRef<HTMLDivElement>();
public constructor(props: IProps) {
super(props);
this.state = {
title: _t("Create poll"),
actionLabel: _t("Create Poll"),
canSubmit: false, // need to add a question and at least one option first
question: "",
options: arraySeed("", DEFAULT_NUM_OPTIONS),
busy: false,
};
}
private checkCanSubmit() {
this.setState({
canSubmit:
!this.state.busy &&
this.state.question.trim().length > 0 &&
this.state.options.filter(op => op.trim().length > 0).length >= MIN_OPTIONS,
});
}
private onQuestionChange = (e: ChangeEvent<HTMLInputElement>) => {
this.setState({ question: e.target.value }, () => this.checkCanSubmit());
};
private onOptionChange = (i: number, e: ChangeEvent<HTMLInputElement>) => {
const newOptions = arrayFastClone(this.state.options);
newOptions[i] = e.target.value;
this.setState({ options: newOptions }, () => this.checkCanSubmit());
};
private onOptionRemove = (i: number) => {
const newOptions = arrayFastClone(this.state.options);
newOptions.splice(i, 1);
this.setState({ options: newOptions }, () => this.checkCanSubmit());
};
private onOptionAdd = () => {
const newOptions = arrayFastClone(this.state.options);
newOptions.push("");
this.setState({ options: newOptions }, () => {
// Scroll the button into view after the state update to ensure we don't experience
// a pop-in effect, and to avoid the button getting cut off due to a mid-scroll render.
this.addOptionRef.current?.scrollIntoView();
});
};
protected submit(): void {
this.setState({ busy: true, canSubmit: false });
this.matrixClient.sendEvent(
this.props.room.roomId,
POLL_START_EVENT_TYPE.name,
makePollContent(this.state.question, this.state.options, POLL_KIND_DISCLOSED.name),
).then(() => this.props.onFinished(true)).catch(e => {
console.error("Failed to submit poll event:", e);
this.setState({ busy: false, canSubmit: true });
});
}
protected cancel(): void {
this.props.onFinished(false);
}
protected renderContent(): React.ReactNode {
return <div className="mx_PollCreateDialog">
<h2>{ _t("What is your poll question or topic?") }</h2>
<Field
value={this.state.question}
label={_t("Question or topic")}
placeholder={_t("Write something...")}
onChange={this.onQuestionChange}
usePlaceholderAsHint={true}
/>
<h2>{ _t("Create options") }</h2>
{
this.state.options.map((op, i) => <div key={`option_${i}`} className="mx_PollCreateDialog_option">
<Field
value={op}
label={_t("Option %(number)s", { number: i + 1 })}
placeholder={_t("Write an option")}
onChange={e => this.onOptionChange(i, e)}
usePlaceholderAsHint={true}
/>
<AccessibleButton
onClick={() => this.onOptionRemove(i)}
className="mx_PollCreateDialog_removeOption"
/>
</div>)
}
<AccessibleButton
onClick={this.onOptionAdd}
disabled={this.state.options.length >= MAX_OPTIONS}
kind="secondary"
className="mx_PollCreateDialog_addOption"
inputRef={this.addOptionRef}
>{ _t("Add option") }</AccessibleButton>
</div>;
}
}

View file

@ -53,9 +53,11 @@ import EmojiPicker from '../emojipicker/EmojiPicker';
import MemberStatusMessageAvatar from "../avatars/MemberStatusMessageAvatar";
import UIStore, { UI_EVENTS } from '../../../stores/UIStore';
import Modal from "../../../Modal";
import InfoDialog from "../dialogs/InfoDialog";
import { RelationType } from 'matrix-js-sdk/src/@types/event';
import RoomContext from '../../../contexts/RoomContext';
import { POLL_START_EVENT_TYPE } from "../../../polls/consts";
import ErrorDialog from "../dialogs/ErrorDialog";
import PollCreateDialog from "../elements/PollCreateDialog";
let instanceCount = 0;
const NARROW_MODE_BREAKPOINT = 500;
@ -197,18 +199,26 @@ class UploadButton extends React.Component<IUploadButtonProps> {
}
}
// TODO: [polls] Make this component actually do something
class PollButton extends React.PureComponent {
interface IPollButtonProps {
room: Room;
}
class PollButton extends React.PureComponent<IPollButtonProps> {
private onCreateClick = () => {
Modal.createTrackedDialog('Polls', 'Not Yet Implemented', InfoDialog, {
// XXX: Deliberately not translated given this dialog is meant to be replaced and we don't
// want to clutter the language files with short-lived strings.
title: "Polls are currently in development",
description: "" +
"Thanks for testing polls! We haven't quite gotten a chance to write the feature yet " +
"though. Check back later for updates.",
hasCloseButton: true,
});
const canSend = this.props.room.currentState.maySendEvent(
POLL_START_EVENT_TYPE.name,
MatrixClientPeg.get().getUserId(),
);
if (!canSend) {
Modal.createTrackedDialog('Polls', 'permissions error: cannot start', ErrorDialog, {
title: _t("Permission Required"),
description: _t("You do not have permission to start polls in this room."),
});
} else {
Modal.createTrackedDialog('Polls', 'create', PollCreateDialog, {
room: this.props.room,
}, 'mx_CompoundDialog');
}
};
render() {
@ -465,7 +475,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
if (!this.state.haveRecording) {
if (SettingsStore.getValue("feature_polls")) {
buttons.push(
<PollButton key="polls" />,
<PollButton key="polls" room={this.props.room} />,
);
}
buttons.push(

View file

@ -1596,6 +1596,7 @@
"Emoji picker": "Emoji picker",
"Add emoji": "Add emoji",
"Upload file": "Upload file",
"You do not have permission to start polls in this room.": "You do not have permission to start polls in this room.",
"Create poll": "Create poll",
"Reply to encrypted thread…": "Reply to encrypted thread…",
"Reply to thread…": "Reply to thread…",
@ -2170,6 +2171,14 @@
"%(oneUser)schanged the server ACLs %(count)s times|one": "%(oneUser)schanged the server ACLs",
"%(severalUsers)schanged the <a>pinned messages</a> for the room %(count)s times.|other": "%(severalUsers)schanged the <a>pinned messages</a> for the room %(count)s times.",
"%(oneUser)schanged the <a>pinned messages</a> for the room %(count)s times.|other": "%(oneUser)schanged the <a>pinned messages</a> for the room %(count)s times.",
"Create Poll": "Create Poll",
"What is your poll question or topic?": "What is your poll question or topic?",
"Question or topic": "Question or topic",
"Write something...": "Write something...",
"Create options": "Create options",
"Option %(number)s": "Option %(number)s",
"Write an option": "Write an option",
"Add option": "Add option",
"Power level": "Power level",
"Custom level": "Custom level",
"QR Code": "QR Code",

54
src/polls/consts.ts Normal file
View file

@ -0,0 +1,54 @@
/*
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 { UnstableValue } from "matrix-js-sdk/src/NamespacedValue";
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_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");
// TODO: [TravisR] Use extensible events library when ready
const TEXT_NODE_TYPE = "org.matrix.msc1767.text";
export interface IPollContent extends IContent {
[POLL_START_EVENT_TYPE.name]: {
kind: string; // disclosed or undisclosed (untypeable for now)
question: {
[TEXT_NODE_TYPE]: string;
};
answers: {
id: string;
[TEXT_NODE_TYPE]: string;
}[];
};
[TEXT_NODE_TYPE]: string;
}
export function makePollContent(question: string, answers: string[], kind: string): IPollContent {
question = question.trim();
answers = answers.map(a => a.trim()).filter(a => !!a);
return {
[TEXT_NODE_TYPE]: `${question}\n${answers.map((a, i) => `${i + 1}. ${a}`).join('\n')}`,
[POLL_START_EVENT_TYPE.name]: {
kind: kind,
question: {
[TEXT_NODE_TYPE]: question,
},
answers: answers.map((a, i) => ({ id: `${i}-${a}`, [TEXT_NODE_TYPE]: a })),
},
};
}