From 32c68feae2c5c66af26415e9dd7611b29e12fad5 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 17 May 2019 11:53:44 +0100 Subject: [PATCH 1/4] Run translation substitution in 2 passes By first substituting variables and then tags after, the translation handling can now support strings with variables inside tags, such as: "people reacted with %(foo)s" --- src/languageHandler.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/languageHandler.js b/src/languageHandler.js index 854ac079bc5..bdef829933d 100644 --- a/src/languageHandler.js +++ b/src/languageHandler.js @@ -125,20 +125,25 @@ export function _t(text, variables, tags) { * @return a React component if any non-strings were used in substitutions, otherwise a string */ export function substitute(text, variables, tags) { - const regexpMapping = {}; + let result = text; if (variables !== undefined) { + const regexpMapping = {}; for (const variable in variables) { regexpMapping[`%\\(${variable}\\)s`] = variables[variable]; } + result = replaceByRegexes(result, regexpMapping); } if (tags !== undefined) { + const regexpMapping = {}; for (const tag in tags) { regexpMapping[`(<${tag}>(.*?)<\\/${tag}>|<${tag}>|<${tag}\\s*\\/>)`] = tags[tag]; } + result = replaceByRegexes(result, regexpMapping); } - return replaceByRegexes(text, regexpMapping); + + return result; } /* From 3da1f73ea40b6b76cdbc449778a793b3272f87f6 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 17 May 2019 11:52:03 +0100 Subject: [PATCH 2/4] 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 2e0c91bd8c5..9823b4ac3d1 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 00000000000..086271e5562 --- /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 592b1a1887b..251f038f756 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 fc15170b878..375b4a44b3f 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 d9d8bac93b6..1032c52e326 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 d55ecd65788..d3bf6a2035d 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 721147cdb8d..19cae27b87d 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 00000000000..fc4aed04101 --- /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 f5355492324..3a740ea5151 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 ", From 059988ff5c1968fd0780f0d000c671d13f285910 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 17 May 2019 12:09:47 +0100 Subject: [PATCH 3/4] Extract tooltip styling to a shared class We want to use the same styling with edited tooltip as well, so this extracts the shared bits. --- res/css/_common.scss | 1 - res/css/views/elements/_Tooltip.scss | 13 +++++++++++++ .../views/messages/_ReactionsRowButtonTooltip.scss | 10 ---------- res/themes/dark/css/_dark.scss | 5 +++-- res/themes/light/css/_light.scss | 5 +++-- .../views/messages/ReactionsRowButtonTooltip.js | 2 +- 6 files changed, 20 insertions(+), 16 deletions(-) diff --git a/res/css/_common.scss b/res/css/_common.scss index d3cf1921e00..d46f38bddb0 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -557,4 +557,3 @@ textarea { .mx_Username_color8 { color: $username-variant8-color; } - diff --git a/res/css/views/elements/_Tooltip.scss b/res/css/views/elements/_Tooltip.scss index 43ddf6dde5d..66e8b5943f4 100644 --- a/res/css/views/elements/_Tooltip.scss +++ b/res/css/views/elements/_Tooltip.scss @@ -74,3 +74,16 @@ limitations under the License. animation: mx_fadeout 0.1s forwards; } } + +.mx_Tooltip_timeline { + box-shadow: none; + background-color: $tooltip-timeline-bg-color; + color: $tooltip-timeline-fg-color; + text-align: center; + border: none; + border-radius: 3px; + + .mx_Tooltip_chevron::after { + border-right-color: $tooltip-timeline-bg-color; + } +} diff --git a/res/css/views/messages/_ReactionsRowButtonTooltip.scss b/res/css/views/messages/_ReactionsRowButtonTooltip.scss index 086271e5562..95e339144fe 100644 --- a/res/css/views/messages/_ReactionsRowButtonTooltip.scss +++ b/res/css/views/messages/_ReactionsRowButtonTooltip.scss @@ -15,18 +15,8 @@ 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 251f038f756..bdccf715409 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -156,8 +156,9 @@ $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; + +$tooltip-timeline-bg-color: $tagpanel-bg-color; +$tooltip-timeline-fg-color: #ffffff; // ***** Mixins! ***** diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 375b4a44b3f..d11dfebda30 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -264,8 +264,9 @@ $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; + +$tooltip-timeline-bg-color: $tagpanel-bg-color; +$tooltip-timeline-fg-color: #ffffff; // ***** Mixins! ***** diff --git a/src/components/views/messages/ReactionsRowButtonTooltip.js b/src/components/views/messages/ReactionsRowButtonTooltip.js index fc4aed04101..709c20c1138 100644 --- a/src/components/views/messages/ReactionsRowButtonTooltip.js +++ b/src/components/views/messages/ReactionsRowButtonTooltip.js @@ -69,7 +69,7 @@ export default class ReactionsRowButtonTooltip extends React.PureComponent { let tooltip; if (tooltipLabel) { tooltip = ; From 603e6b7055a8cbfa12665c403bcf156085731426 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 17 May 2019 12:19:02 +0100 Subject: [PATCH 4/4] Adjust edited tooltip to use shared styles --- res/css/views/rooms/_EventTile.scss | 5 +++++ src/components/views/messages/TextualBody.js | 7 +++++-- src/i18n/strings/en_EN.json | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 8f67069c824..aa473ec3177 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -490,6 +490,11 @@ limitations under the License. } */ +.mx_EventTile_editedTooltip { + font-size: 10px; + padding: 5px 6px; +} + /* end of overrides */ .mx_MatrixChat_useCompactLayout { diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index 0ca4711b07f..44c807e4e48 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -448,14 +448,17 @@ module.exports = React.createClass({ const Tooltip = sdk.getComponent('elements.Tooltip'); const editEvent = this.props.mxEvent.replacingEvent(); const date = editEvent && formatDate(editEvent.getDate()); - editedTooltip = ; + editedTooltip = ; } return (
{editedTooltip}{`(${_t("Edited")})`}
+ >{editedTooltip}{`(${_t("edited")})`} ); }, diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 3a740ea5151..9d54a65edad 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -918,7 +918,7 @@ "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.", - "Edited": "Edited", + "edited": "edited", "Removed or unknown message type": "Removed or unknown message type", "Message removed by %(userId)s": "Message removed by %(userId)s", "Message removed": "Message removed",