Allow editing polls (#7806)
This commit is contained in:
parent
fa9af44523
commit
7387f3c80a
8 changed files with 310 additions and 38 deletions
|
@ -16,7 +16,8 @@ limitations under the License.
|
|||
|
||||
import React, { ChangeEvent, createRef } from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { M_POLL_KIND_DISCLOSED, PollStartEvent } from "matrix-events-sdk";
|
||||
import { IPartialEvent, M_POLL_KIND_DISCLOSED, M_POLL_START, PollStartEvent } from "matrix-events-sdk";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
import ScrollableBaseModal, { IScrollableBaseState } from "../dialogs/ScrollableBaseModal";
|
||||
import { IDialogProps } from "../dialogs/IDialogProps";
|
||||
|
@ -31,6 +32,7 @@ import Spinner from "./Spinner";
|
|||
interface IProps extends IDialogProps {
|
||||
room: Room;
|
||||
threadId?: string;
|
||||
editingMxEvent?: MatrixEvent; // Truthy if we are editing an existing poll
|
||||
}
|
||||
|
||||
interface IState extends IScrollableBaseState {
|
||||
|
@ -45,21 +47,42 @@ const DEFAULT_NUM_OPTIONS = 2;
|
|||
const MAX_QUESTION_LENGTH = 340;
|
||||
const MAX_OPTION_LENGTH = 340;
|
||||
|
||||
function creatingInitialState(): IState {
|
||||
return {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
function editingInitialState(editingMxEvent: MatrixEvent): IState {
|
||||
const poll = editingMxEvent.unstableExtensibleEvent as PollStartEvent;
|
||||
if (!poll?.isEquivalentTo(M_POLL_START)) return creatingInitialState();
|
||||
|
||||
return {
|
||||
title: _t("Edit poll"),
|
||||
actionLabel: _t("Done"),
|
||||
canSubmit: true,
|
||||
question: poll.question.text,
|
||||
options: poll.answers.map(ans => ans.text),
|
||||
busy: false,
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
this.state = (
|
||||
props.editingMxEvent
|
||||
? editingInitialState(props.editingMxEvent)
|
||||
: creatingInitialState()
|
||||
);
|
||||
}
|
||||
|
||||
private checkCanSubmit() {
|
||||
|
@ -97,13 +120,32 @@ export default class PollCreateDialog extends ScrollableBaseModal<IProps, IState
|
|||
});
|
||||
};
|
||||
|
||||
protected submit(): void {
|
||||
this.setState({ busy: true, canSubmit: false });
|
||||
const pollEvent = PollStartEvent.from(
|
||||
private createEvent(): IPartialEvent<object> {
|
||||
const pollStart = PollStartEvent.from(
|
||||
this.state.question.trim(),
|
||||
this.state.options.map(a => a.trim()).filter(a => !!a),
|
||||
M_POLL_KIND_DISCLOSED,
|
||||
).serialize();
|
||||
|
||||
if (!this.props.editingMxEvent) {
|
||||
return pollStart;
|
||||
} else {
|
||||
return {
|
||||
"content": {
|
||||
"m.new_content": pollStart.content,
|
||||
"m.relates_to": {
|
||||
"rel_type": "m.replace",
|
||||
"event_id": this.props.editingMxEvent.getId(),
|
||||
},
|
||||
},
|
||||
"type": pollStart.type,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
protected submit(): void {
|
||||
this.setState({ busy: true, canSubmit: false });
|
||||
const pollEvent = this.createEvent();
|
||||
this.matrixClient.sendEvent(
|
||||
this.props.room.roomId,
|
||||
this.props.threadId,
|
||||
|
@ -159,7 +201,10 @@ export default class PollCreateDialog extends ScrollableBaseModal<IProps, IState
|
|||
maxLength={MAX_OPTION_LENGTH}
|
||||
label={_t("Option %(number)s", { number: i + 1 })}
|
||||
placeholder={_t("Write an option")}
|
||||
onChange={e => this.onOptionChange(i, e)}
|
||||
onChange={
|
||||
(e: ChangeEvent<HTMLInputElement>) =>
|
||||
this.onOptionChange(i, e)
|
||||
}
|
||||
usePlaceholderAsHint={true}
|
||||
disabled={this.state.busy}
|
||||
/>
|
||||
|
|
|
@ -45,6 +45,28 @@ interface IState {
|
|||
endRelations: RelatedRelations; // Poll end events
|
||||
}
|
||||
|
||||
export function createVoteRelations(
|
||||
getRelationsForEvent: (
|
||||
eventId: string,
|
||||
relationType: string,
|
||||
eventType: string
|
||||
) => Relations,
|
||||
eventId: string,
|
||||
) {
|
||||
return new RelatedRelations([
|
||||
getRelationsForEvent(
|
||||
eventId,
|
||||
"m.reference",
|
||||
M_POLL_RESPONSE.name,
|
||||
),
|
||||
getRelationsForEvent(
|
||||
eventId,
|
||||
"m.reference",
|
||||
M_POLL_RESPONSE.altName,
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
export function findTopAnswer(
|
||||
pollEvent: MatrixEvent,
|
||||
matrixClient: MatrixClient,
|
||||
|
@ -68,18 +90,7 @@ export function findTopAnswer(
|
|||
return poll.answers.find(a => a.id === answerId)?.text ?? "";
|
||||
};
|
||||
|
||||
const voteRelations = new RelatedRelations([
|
||||
getRelationsForEvent(
|
||||
pollEvent.getId(),
|
||||
"m.reference",
|
||||
M_POLL_RESPONSE.name,
|
||||
),
|
||||
getRelationsForEvent(
|
||||
pollEvent.getId(),
|
||||
"m.reference",
|
||||
M_POLL_RESPONSE.altName,
|
||||
),
|
||||
]);
|
||||
const voteRelations = createVoteRelations(getRelationsForEvent, pollEvent.getId());
|
||||
|
||||
const endRelations = new RelatedRelations([
|
||||
getRelationsForEvent(
|
||||
|
|
|
@ -20,6 +20,7 @@ import React, { ReactElement, useEffect } from 'react';
|
|||
import { EventStatus, MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import classNames from 'classnames';
|
||||
import { MsgType } from 'matrix-js-sdk/src/@types/event';
|
||||
import { M_POLL_START } from 'matrix-events-sdk';
|
||||
|
||||
import type { Relations } from 'matrix-js-sdk/src/models/relations';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
@ -42,6 +43,10 @@ import ReplyChain from '../elements/ReplyChain';
|
|||
import { showThread } from '../../../dispatcher/dispatch-actions/threads';
|
||||
import ReactionPicker from "../emojipicker/ReactionPicker";
|
||||
import { CardContext } from '../right_panel/BaseCard';
|
||||
import Modal from '../../../Modal';
|
||||
import PollCreateDialog from '../elements/PollCreateDialog';
|
||||
import ErrorDialog from '../dialogs/ErrorDialog';
|
||||
import { createVoteRelations } from './MPollBody';
|
||||
|
||||
interface IOptionsButtonProps {
|
||||
mxEvent: MatrixEvent;
|
||||
|
@ -228,12 +233,59 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
|
|||
});
|
||||
};
|
||||
|
||||
private onEditClick = (ev: React.MouseEvent): void => {
|
||||
dis.dispatch({
|
||||
action: Action.EditEvent,
|
||||
event: this.props.mxEvent,
|
||||
timelineRenderingType: this.context.timelineRenderingType,
|
||||
});
|
||||
private pollAlreadyHasVotes = (): boolean => {
|
||||
if (!this.props.getRelationsForEvent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const voteRelations = createVoteRelations(
|
||||
this.props.getRelationsForEvent,
|
||||
this.props.mxEvent.getId(),
|
||||
);
|
||||
|
||||
return voteRelations.getRelations().length > 0;
|
||||
};
|
||||
|
||||
private launchPollEditor = (): void => {
|
||||
if (this.pollAlreadyHasVotes()) {
|
||||
Modal.createTrackedDialog(
|
||||
'Not allowed to edit poll',
|
||||
'',
|
||||
ErrorDialog,
|
||||
{
|
||||
title: _t("Can't edit poll"),
|
||||
description: _t(
|
||||
"Sorry, you can't edit a poll after votes have been cast.",
|
||||
),
|
||||
},
|
||||
);
|
||||
} else {
|
||||
Modal.createTrackedDialog(
|
||||
'Polls',
|
||||
'create',
|
||||
PollCreateDialog,
|
||||
{
|
||||
room: this.context.room,
|
||||
threadId: this.context.threadId ?? null,
|
||||
editingMxEvent: this.props.mxEvent,
|
||||
},
|
||||
'mx_CompoundDialog',
|
||||
false, // isPriorityModal
|
||||
true, // isStaticModal
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
private onEditClick = (): void => {
|
||||
if (M_POLL_START.matches(this.props.mxEvent.getType())) {
|
||||
this.launchPollEditor();
|
||||
} else {
|
||||
dis.dispatch({
|
||||
action: Action.EditEvent,
|
||||
event: this.props.mxEvent,
|
||||
timelineRenderingType: this.context.timelineRenderingType,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private readonly forbiddenThreadHeadMsgType = [
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue