diff --git a/res/css/_common.scss b/res/css/_common.scss index d3cf1921e0..d46f38bddb 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/_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/elements/_Tooltip.scss b/res/css/views/elements/_Tooltip.scss index 43ddf6dde5..66e8b5943f 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 new file mode 100644 index 0000000000..95e339144f --- /dev/null +++ b/res/css/views/messages/_ReactionsRowButtonTooltip.scss @@ -0,0 +1,24 @@ +/* +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 { + font-size: 8px; + padding: 6px; + + .mx_ReactionsRowButtonTooltip_reactedWith { + opacity: 0.7; + } +} diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 8f67069c82..aa473ec317 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/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 592b1a1887..bdccf71540 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -157,6 +157,9 @@ $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; +$tooltip-timeline-bg-color: $tagpanel-bg-color; +$tooltip-timeline-fg-color: #ffffff; + // ***** Mixins! ***** @define-mixin mx_DialogButton { diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index fc15170b87..d11dfebda3 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -265,6 +265,9 @@ $reaction-row-button-hover-border-color: $focus-bg-color; $reaction-row-button-selected-bg-color: #e9fff9; $reaction-row-button-selected-border-color: $accent-color; +$tooltip-timeline-bg-color: $tagpanel-bg-color; +$tooltip-timeline-fg-color: #ffffff; + // ***** Mixins! ***** @define-mixin mx_DialogButton { 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..709c20c113 --- /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/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index 0ca4711b07..44c807e4e4 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 f535549232..9d54a65eda 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 ", @@ -917,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", diff --git a/src/languageHandler.js b/src/languageHandler.js index 854ac079bc..bdef829933 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; } /*