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;
}
/*