From 3da1f73ea40b6b76cdbc449778a793b3272f87f6 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 17 May 2019 11:52:03 +0100 Subject: [PATCH] Add a basic tooltip showing who reacted This adds a first attempt at tooltip showing who reacted to a message. It doesn't limit senders or position the tooltip nicely, but the info is there at least. Part of https://github.com/vector-im/riot-web/issues/9722 --- res/css/_components.scss | 1 + .../messages/_ReactionsRowButtonTooltip.scss | 34 ++++++++ res/themes/dark/css/_dark.scss | 2 + res/themes/light/css/_light.scss | 2 + src/HtmlUtils.js | 11 +++ src/components/views/messages/ReactionsRow.js | 2 +- .../views/messages/ReactionsRowButton.js | 50 +++++++++++- .../messages/ReactionsRowButtonTooltip.js | 80 +++++++++++++++++++ src/i18n/strings/en_EN.json | 1 + 9 files changed, 180 insertions(+), 3 deletions(-) create mode 100644 res/css/views/messages/_ReactionsRowButtonTooltip.scss create mode 100644 src/components/views/messages/ReactionsRowButtonTooltip.js diff --git a/res/css/_components.scss b/res/css/_components.scss index 2e0c91bd8c..9823b4ac3d 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -119,6 +119,7 @@ @import "./views/messages/_ReactionDimension.scss"; @import "./views/messages/_ReactionsRow.scss"; @import "./views/messages/_ReactionsRowButton.scss"; +@import "./views/messages/_ReactionsRowButtonTooltip.scss"; @import "./views/messages/_RoomAvatarEvent.scss"; @import "./views/messages/_SenderProfile.scss"; @import "./views/messages/_TextualEvent.scss"; diff --git a/res/css/views/messages/_ReactionsRowButtonTooltip.scss b/res/css/views/messages/_ReactionsRowButtonTooltip.scss new file mode 100644 index 0000000000..086271e556 --- /dev/null +++ b/res/css/views/messages/_ReactionsRowButtonTooltip.scss @@ -0,0 +1,34 @@ +/* +Copyright 2019 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_ReactionsRowButtonTooltip { + box-shadow: none; + background-color: $reaction-row-button-tooltip-bg-color; + color: $reaction-row-button-tooltip-fg-color; + text-align: center; + font-size: 8px; + padding: 6px; + border: none; + border-radius: 3px; + + .mx_Tooltip_chevron::after { + border-right-color: $reaction-row-button-tooltip-bg-color; + } + + .mx_ReactionsRowButtonTooltip_reactedWith { + opacity: 0.7; + } +} diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 592b1a1887..251f038f75 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -156,6 +156,8 @@ $reaction-row-button-border-color: #616b7f; $reaction-row-button-hover-border-color: $header-panel-text-primary-color; $reaction-row-button-selected-bg-color: #1f6954; $reaction-row-button-selected-border-color: $accent-color; +$reaction-row-button-tooltip-bg-color: $tagpanel-bg-color; +$reaction-row-button-tooltip-fg-color: #ffffff; // ***** Mixins! ***** diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index fc15170b87..375b4a44b3 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -264,6 +264,8 @@ $reaction-row-button-border-color: #e9edf1; $reaction-row-button-hover-border-color: $focus-bg-color; $reaction-row-button-selected-bg-color: #e9fff9; $reaction-row-button-selected-border-color: $accent-color; +$reaction-row-button-tooltip-bg-color: $tagpanel-bg-color; +$reaction-row-button-tooltip-fg-color: #ffffff; // ***** Mixins! ***** diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index d9d8bac93b..1032c52e32 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -107,6 +107,17 @@ function unicodeToImage(str, addAlt) { return str; } +/** + * Returns the shortcode for an emoji character. + * + * @param {String} char The emoji character + * @return {String} The shortcode (such as :thumbup:) + */ +export function unicodeToShort(char) { + const unicode = emojione.jsEscapeMap[char]; + return emojione.mapUnicodeToShort()[unicode]; +} + /** * Given one or more unicode characters (represented by unicode * character number), return an image node with the corresponding diff --git a/src/components/views/messages/ReactionsRow.js b/src/components/views/messages/ReactionsRow.js index d55ecd6578..d3bf6a2035 100644 --- a/src/components/views/messages/ReactionsRow.js +++ b/src/components/views/messages/ReactionsRow.js @@ -116,8 +116,8 @@ export default class ReactionsRow extends React.PureComponent { return ; }); diff --git a/src/components/views/messages/ReactionsRowButton.js b/src/components/views/messages/ReactionsRowButton.js index 721147cdb8..19cae27b87 100644 --- a/src/components/views/messages/ReactionsRowButton.js +++ b/src/components/views/messages/ReactionsRowButton.js @@ -19,17 +19,28 @@ import PropTypes from 'prop-types'; import classNames from 'classnames'; import MatrixClientPeg from '../../../MatrixClientPeg'; +import sdk from '../../../index'; export default class ReactionsRowButton extends React.PureComponent { static propTypes = { // The event we're displaying reactions for mxEvent: PropTypes.object.isRequired, + // The reaction content / key / emoji content: PropTypes.string.isRequired, - count: PropTypes.number.isRequired, + // A Set of Martix reaction events for this key + reactionEvents: PropTypes.object.isRequired, // A possible Matrix event if the current user has voted for this type myReactionEvent: PropTypes.object, } + constructor(props) { + super(props); + + this.state = { + tooltipVisible: false, + }; + } + onClick = (ev) => { const { mxEvent, myReactionEvent, content } = this.props; if (myReactionEvent) { @@ -48,18 +59,53 @@ export default class ReactionsRowButton extends React.PureComponent { } }; + onMouseOver = () => { + this.setState({ + // To avoid littering the DOM with a tooltip for every reaction, + // only render it on first use. + tooltipRendered: true, + tooltipVisible: true, + }); + } + + onMouseOut = () => { + this.setState({ + tooltipVisible: false, + }); + } + render() { - const { content, count, myReactionEvent } = this.props; + const ReactionsRowButtonTooltip = + sdk.getComponent('messages.ReactionsRowButtonTooltip'); + const { content, reactionEvents, myReactionEvent } = this.props; + + const count = reactionEvents.size; + if (!count) { + return null; + } const classes = classNames({ mx_ReactionsRowButton: true, mx_ReactionsRowButton_selected: !!myReactionEvent, }); + let tooltip; + if (this.state.tooltipRendered) { + tooltip = ; + } + return {content} {count} + {tooltip} ; } } diff --git a/src/components/views/messages/ReactionsRowButtonTooltip.js b/src/components/views/messages/ReactionsRowButtonTooltip.js new file mode 100644 index 0000000000..fc4aed0410 --- /dev/null +++ b/src/components/views/messages/ReactionsRowButtonTooltip.js @@ -0,0 +1,80 @@ +/* +Copyright 2019 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 PropTypes from 'prop-types'; + +import MatrixClientPeg from '../../../MatrixClientPeg'; +import sdk from '../../../index'; +import { unicodeToShort } from '../../../HtmlUtils'; +import { _t } from '../../../languageHandler'; + +export default class ReactionsRowButtonTooltip extends React.PureComponent { + static propTypes = { + // The event we're displaying reactions for + mxEvent: PropTypes.object.isRequired, + // The reaction content / key / emoji + content: PropTypes.string.isRequired, + // A Set of Martix reaction events for this key + reactionEvents: PropTypes.object.isRequired, + visible: PropTypes.bool.isRequired, + } + + render() { + const Tooltip = sdk.getComponent('elements.Tooltip'); + const { content, reactionEvents, mxEvent, visible } = this.props; + + const room = MatrixClientPeg.get().getRoom(mxEvent.getRoomId()); + let tooltipLabel; + if (room) { + const senders = []; + for (const reactionEvent of reactionEvents) { + const { name } = room.getMember(reactionEvent.getSender()); + senders.push(name); + } + const shortName = unicodeToShort(content) || content; + tooltipLabel =
{_t( + "reacted with %(shortName)s", + { + shortName, + }, + { + reactors: () => { + return
+ {senders.join(", ")} +
; + }, + reactedWith: (sub) => { + return
+ {sub} +
; + }, + }, + )}
; + } + + let tooltip; + if (tooltipLabel) { + tooltip = ; + } + + return tooltip; + } +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f535549232..3a740ea515 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -907,6 +907,7 @@ "Invalid file%(extra)s": "Invalid file%(extra)s", "Error decrypting image": "Error decrypting image", "Error decrypting video": "Error decrypting video", + "reacted with %(shortName)s": "reacted with %(shortName)s", "%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s changed the avatar for %(roomName)s", "%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s removed the room avatar.", "%(senderDisplayName)s changed the room avatar to ": "%(senderDisplayName)s changed the room avatar to ",