diff --git a/res/css/_components.scss b/res/css/_components.scss index 116189d64c..d4c383b1fe 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -182,6 +182,7 @@ @import "./views/messages/_MImageReplyBody.scss"; @import "./views/messages/_MJitsiWidgetEvent.scss"; @import "./views/messages/_MNoticeBody.scss"; +@import "./views/messages/_MPollBody.scss"; @import "./views/messages/_MStickerBody.scss"; @import "./views/messages/_MTextBody.scss"; @import "./views/messages/_MVideoBody.scss"; diff --git a/res/css/views/messages/_MPollBody.scss b/res/css/views/messages/_MPollBody.scss new file mode 100644 index 0000000000..46051821a0 --- /dev/null +++ b/res/css/views/messages/_MPollBody.scss @@ -0,0 +1,109 @@ +/* +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_MPollBody { + margin-top: 8px; + + h2 { + font-weight: 600; + font-size: $font-15px; + line-height: $font-24px; + margin-top: 0; + margin-bottom: 8px; + } + + h2::before { + content: ''; + position: relative; + display: inline-block; + margin-right: 12px; + top: 3px; + left: 3px; + height: 20px; + width: 20px; + background-color: $secondary-content; + mask-repeat: no-repeat; + mask-size: contain; + mask-position: center; + mask-image: url('$(res)/img/element-icons/room/composer/poll.svg'); + } + + .mx_MPollBody_option { + border: 1px solid $quinary-content; + border-radius: 8px; + margin-bottom: 16px; + padding: 6px; + max-width: 550px; + background-color: $background; + + .mx_StyledRadioButton { + margin-bottom: 8px; + } + + .mx_StyledRadioButton_content { + padding-top: 2px; + } + + .mx_MPollBody_optionVoteCount { + position: absolute; + color: $secondary-content; + right: 4px; + font-size: $font-12px; + } + + .mx_MPollBody_popularityBackground { + width: calc(100% - 4px); + height: 8px; + margin-right: 12px; + border-radius: 8px; + background-color: $system; + + .mx_MPollBody_popularityAmount { + width: 0%; + height: 8px; + border-radius: 8px; + background-color: $quaternary-content; + } + } + } + + .mx_MPollBody_option:last-child { + margin-bottom: 8px; + } + + .mx_MPollBody_option_checked { + border-color: $accent-color; + } + + .mx_StyledRadioButton_checked input[type="radio"] + div { + border-width: 2px; + border-color: $accent-color; + background-color: $accent-color; + background-image: url('$(res)/img/element-icons/check-white.svg'); + background-size: 12px; + background-repeat: no-repeat; + background-position: center; + + div { + visibility: hidden; + } + } + + .mx_MPollBody_totalVotes { + color: $secondary-content; + font-size: $font-12px; + } +} diff --git a/res/img/element-icons/check-white.svg b/res/img/element-icons/check-white.svg new file mode 100644 index 0000000000..018c8b33d9 --- /dev/null +++ b/res/img/element-icons/check-white.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/views/messages/MPollBody.tsx b/src/components/views/messages/MPollBody.tsx new file mode 100644 index 0000000000..93fbe974d1 --- /dev/null +++ b/src/components/views/messages/MPollBody.tsx @@ -0,0 +1,93 @@ +/* +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 { _t } from '../../../languageHandler'; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { IBodyProps } from "./IBodyProps"; +import { IPollAnswer, IPollContent, POLL_START_EVENT_TYPE } from '../../../polls/consts'; +import StyledRadioButton from '../elements/StyledRadioButton'; + +// TODO: [andyb] Use extensible events library when ready +const TEXT_NODE_TYPE = "org.matrix.msc1767.text"; + +interface IState { + selected?: string; +} + +@replaceableComponent("views.messages.MPollBody") +export default class MPollBody extends React.Component { + constructor(props: IBodyProps) { + super(props); + + this.state = { + selected: null, + }; + } + + private selectOption(answerId: string) { + this.setState({ selected: answerId }); + } + + private onOptionSelected = (e: React.FormEvent): void => { + this.selectOption(e.currentTarget.value); + }; + + render() { + const pollStart: IPollContent = + this.props.mxEvent.getContent()[POLL_START_EVENT_TYPE.name]; + const pollId = this.props.mxEvent.getId(); + + return
+

{ pollStart.question[TEXT_NODE_TYPE] }

+
+ { + pollStart.answers.map((answer: IPollAnswer) => { + const checked = this.state.selected === answer.id; + const classNames = `mx_MPollBody_option${ + checked ? " mx_MPollBody_option_checked": "" + }`; + return
this.selectOption(answer.id)} + > + +
+ { _t("%(number)s votes", { number: 0 }) } +
+
+ { answer[TEXT_NODE_TYPE] } +
+
+
+
+
+
; + }) + } +
+
+ { _t( "Based on %(total)s votes", { total: 0 } ) } +
+
; + } +} diff --git a/src/components/views/messages/MessageEvent.tsx b/src/components/views/messages/MessageEvent.tsx index b72e40d194..9590cd1ed7 100644 --- a/src/components/views/messages/MessageEvent.tsx +++ b/src/components/views/messages/MessageEvent.tsx @@ -27,6 +27,7 @@ import { MediaEventHelper } from "../../../utils/MediaEventHelper"; import { ReactAnyComponent } from "../../../@types/common"; import { EventType, MsgType } from "matrix-js-sdk/src/@types/event"; import { IBodyProps } from "./IBodyProps"; +import { POLL_START_EVENT_TYPE } from '../../../polls/consts'; // onMessageAllowed is handled internally interface IProps extends Omit { @@ -111,6 +112,15 @@ export default class MessageEvent extends React.Component implements IMe // Fallback to UnknownBody otherwise if not redacted BodyType = UnknownBody; } + + if (type && type === POLL_START_EVENT_TYPE.name) { + // TODO: this can all disappear when Polls comes out of labs - + // instead, add something like this into this.evTypes: + // [EventType.Poll]: "messages.MPollBody" + if (SettingsStore.getValue("feature_polls")) { + BodyType = sdk.getComponent('messages.MPollBody'); + } + } } if (SettingsStore.getValue("feature_mjolnir")) { diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 341870f92b..f6cfa4c352 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -65,12 +65,14 @@ import { logger } from "matrix-js-sdk/src/logger"; import { TimelineRenderingType } from "../../../contexts/RoomContext"; import { MediaEventHelper } from "../../../utils/MediaEventHelper"; import Toolbar from '../../../accessibility/Toolbar'; +import { POLL_START_EVENT_TYPE } from '../../../polls/consts'; import { RovingAccessibleTooltipButton } from '../../../accessibility/roving/RovingAccessibleTooltipButton'; import { ThreadListContextMenu } from '../context_menus/ThreadListContextMenu'; const eventTileTypes = { [EventType.RoomMessage]: 'messages.MessageEvent', [EventType.Sticker]: 'messages.MessageEvent', + [POLL_START_EVENT_TYPE.name]: 'messages.MessageEvent', [EventType.KeyVerificationCancel]: 'messages.MKeyVerificationConclusion', [EventType.KeyVerificationDone]: 'messages.MKeyVerificationConclusion', [EventType.CallInvite]: 'messages.CallEvent', diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 7b9e050e59..b0db4d44de 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2021,6 +2021,8 @@ "Declining …": "Declining …", "%(name)s wants to verify": "%(name)s wants to verify", "You sent a verification request": "You sent a verification request", + "%(number)s votes": "%(number)s votes", + "Based on %(total)s votes": "Based on %(total)s votes", "Error decrypting video": "Error decrypting video", "Error processing voice message": "Error processing voice message", "Add reaction": "Add reaction", diff --git a/src/polls/consts.ts b/src/polls/consts.ts index 6dc196f5ec..be9eb97b5e 100644 --- a/src/polls/consts.ts +++ b/src/polls/consts.ts @@ -24,16 +24,18 @@ export const POLL_KIND_UNDISCLOSED = new UnstableValue("m.poll.undisclosed", "or // TODO: [TravisR] Use extensible events library when ready const TEXT_NODE_TYPE = "org.matrix.msc1767.text"; +export interface IPollAnswer extends IContent { + id: string; + [TEXT_NODE_TYPE]: string; +} + 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; - }[]; + answers: IPollAnswer[]; }; [TEXT_NODE_TYPE]: string; } diff --git a/src/utils/EventUtils.ts b/src/utils/EventUtils.ts index 894dcb3955..1fdc3a5156 100644 --- a/src/utils/EventUtils.ts +++ b/src/utils/EventUtils.ts @@ -24,6 +24,7 @@ import { EventType } from "matrix-js-sdk/src/@types/event"; import { MatrixClient } from 'matrix-js-sdk/src/client'; import { Thread } from 'matrix-js-sdk/src/models/thread'; import { logger } from 'matrix-js-sdk/src/logger'; +import { POLL_START_EVENT_TYPE } from '../polls/consts'; /** * Returns whether an event should allow actions like reply, reactions, edit, etc. @@ -136,7 +137,8 @@ export function getEventDisplayInfo(mxEvent: MatrixEvent): { !isLeftAlignedBubbleMessage && eventType !== EventType.RoomMessage && eventType !== EventType.Sticker && - eventType !== EventType.RoomCreate + eventType !== EventType.RoomCreate && + eventType !== POLL_START_EVENT_TYPE.name ); // If we're showing hidden events in the timeline, we should use the