From d313deb92aee7f5c46acc11ab226ac2c788e040a Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Tue, 2 Nov 2021 17:28:21 +0000 Subject: [PATCH 1/4] Display started polls in timeline (without votes) --- res/css/_components.scss | 1 + res/css/views/messages/_MPollBody.scss | 106 ++++++++++++++++++ res/img/element-icons/check-white.svg | 3 + src/components/views/messages/MPollBody.tsx | 93 +++++++++++++++ .../views/messages/MessageEvent.tsx | 10 ++ src/components/views/rooms/EventTile.tsx | 2 + src/i18n/strings/en_EN.json | 14 ++- src/polls/consts.ts | 10 +- 8 files changed, 229 insertions(+), 10 deletions(-) create mode 100644 res/css/views/messages/_MPollBody.scss create mode 100644 res/img/element-icons/check-white.svg create mode 100644 src/components/views/messages/MPollBody.tsx diff --git a/res/css/_components.scss b/res/css/_components.scss index 116189d64cc..d4c383b1fe6 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 00000000000..7abcea27133 --- /dev/null +++ b/res/css/views/messages/_MPollBody.scss @@ -0,0 +1,106 @@ +/* +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 { + 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; + + .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 00000000000..018c8b33d96 --- /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 00000000000..93fbe974d1f --- /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 b72e40d194b..9590cd1ed76 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 341870f92b6..f6cfa4c3525 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 f57ce2cc716..ce65948baa5 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2021,6 +2021,14 @@ "Declining …": "Declining …", "%(name)s wants to verify": "%(name)s wants to verify", "You sent a verification request": "You sent a verification request", + "Add an Integration": "Add an Integration", + "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?", + "Edited at %(date)s": "Edited at %(date)s", + "Click to view edits": "Click to view edits", + "Edited at %(date)s. Click to view edits.": "Edited at %(date)s. Click to view edits.", + "edited": "edited", + "%(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", @@ -2034,12 +2042,6 @@ "%(senderDisplayName)s changed the room avatar to ": "%(senderDisplayName)s changed the room avatar to ", "Click here to see older messages.": "Click here to see older messages.", "This room is a continuation of another conversation.": "This room is a continuation of another conversation.", - "Add an Integration": "Add an Integration", - "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?", - "Edited at %(date)s": "Edited at %(date)s", - "Click to view edits": "Click to view edits", - "Edited at %(date)s. Click to view edits.": "Edited at %(date)s. Click to view edits.", - "edited": "edited", "Submit logs": "Submit logs", "Can't load this message": "Can't load this message", "Failed to load group members": "Failed to load group members", diff --git a/src/polls/consts.ts b/src/polls/consts.ts index 6dc196f5ecb..be9eb97b5e6 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; } From c8b7c40e4daf6353f64ba2c071e17ae6e184d3e7 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 5 Nov 2021 11:37:49 +0000 Subject: [PATCH 2/4] Update i18n info --- src/i18n/strings/en_EN.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index ce65948baa5..4b1334a1ef7 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2021,12 +2021,6 @@ "Declining …": "Declining …", "%(name)s wants to verify": "%(name)s wants to verify", "You sent a verification request": "You sent a verification request", - "Add an Integration": "Add an Integration", - "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?", - "Edited at %(date)s": "Edited at %(date)s", - "Click to view edits": "Click to view edits", - "Edited at %(date)s. Click to view edits.": "Edited at %(date)s. Click to view edits.", - "edited": "edited", "%(number)s votes": "%(number)s votes", "Based on %(total)s votes": "Based on %(total)s votes", "Error decrypting video": "Error decrypting video", @@ -2042,6 +2036,12 @@ "%(senderDisplayName)s changed the room avatar to ": "%(senderDisplayName)s changed the room avatar to ", "Click here to see older messages.": "Click here to see older messages.", "This room is a continuation of another conversation.": "This room is a continuation of another conversation.", + "Add an Integration": "Add an Integration", + "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?", + "Edited at %(date)s": "Edited at %(date)s", + "Click to view edits": "Click to view edits", + "Edited at %(date)s. Click to view edits.": "Edited at %(date)s. Click to view edits.", + "edited": "edited", "Submit logs": "Submit logs", "Can't load this message": "Can't load this message", "Failed to load group members": "Failed to load group members", From d182ba67f78df294a59fa7e19d0030ee2605c46a Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Wed, 10 Nov 2021 10:45:01 +0000 Subject: [PATCH 3/4] Keep original background colour of poll options, even on hover --- res/css/views/messages/_MPollBody.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/css/views/messages/_MPollBody.scss b/res/css/views/messages/_MPollBody.scss index 7abcea27133..64d32d585e9 100644 --- a/res/css/views/messages/_MPollBody.scss +++ b/res/css/views/messages/_MPollBody.scss @@ -45,6 +45,7 @@ limitations under the License. margin-bottom: 16px; padding: 6px; max-width: 550px; + background-color: $background; .mx_StyledRadioButton { margin-bottom: 8px; From 06a26baec61847d8cee1cb591e14aa191d4bd876 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Wed, 10 Nov 2021 11:20:45 +0000 Subject: [PATCH 4/4] Show full avatar above a poll message --- res/css/views/messages/_MPollBody.scss | 2 ++ src/utils/EventUtils.ts | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/res/css/views/messages/_MPollBody.scss b/res/css/views/messages/_MPollBody.scss index 64d32d585e9..46051821a0a 100644 --- a/res/css/views/messages/_MPollBody.scss +++ b/res/css/views/messages/_MPollBody.scss @@ -15,6 +15,8 @@ limitations under the License. */ .mx_MPollBody { + margin-top: 8px; + h2 { font-weight: 600; font-size: $font-15px; diff --git a/src/utils/EventUtils.ts b/src/utils/EventUtils.ts index 894dcb39559..1fdc3a51569 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