@@ -22,6 +22,21 @@
{{> messageLocation location=msg.location}}
{{/if}}
+
+
+ {{#each reaction in reactions}}
+
+ {{> icon icon=reaction.icon }}
+ {{#if $gt reaction.count 1}}
+ {{reaction.count}}
+ {{/if}}
+
+ {{/each}}
+ {{#if $gt reactionsCount 1}}
+
+ {{/if}}
+
+
{{#with readReceipt}}
diff --git a/app/ui-message/client/messageBubble.js b/app/ui-message/client/messageBubble.js
index fad80700b0993..30a28d332a1d4 100644
--- a/app/ui-message/client/messageBubble.js
+++ b/app/ui-message/client/messageBubble.js
@@ -1,9 +1,11 @@
+import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker';
import { Template } from 'meteor/templating';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import { MessageTypes } from '../../ui-utils/client';
import { Markdown } from '../../markdown/client';
+import { t } from '../../utils';
import './messageThread';
import { escapeHTML } from '../../../lib/escapeHTML';
import { renderMentions } from '../../mentions/client/client';
@@ -11,6 +13,7 @@ import { renderMessageBody } from '../../../client/lib/renderMessageBody';
import { settings } from '../../settings/client';
import './messageBubble.html';
+let activeReactionMessage;
const renderBody = (msg, settings) => {
const searchedText = msg.searchedText ? msg.searchedText : '';
const isSystemMessage = MessageTypes.isSystemMessage(msg);
@@ -41,7 +44,7 @@ const renderBody = (msg, settings) => {
return msg;
};
-
+const validReactions = ['heart', 'thumbsup', 'thumbsdown', 'question', 'exclamation'];
Template.messageBubble.helpers({
unread() {
const { msg, subscription } = this;
@@ -129,6 +132,47 @@ Template.messageBubble.helpers({
return '';
},
+ reactions() {
+ const { msg: { reactions = {} }, u: { username: myUsername, name: myName } } = this;
+
+ return Object.entries(reactions)
+ .map(([emoji, reaction]) => {
+ const myDisplayName = reaction.names ? myName : `@${ myUsername }`;
+ const displayNames = reaction.names || reaction.usernames.map((username) => `@${ username }`);
+ const selectedDisplayNames = displayNames.slice(0, 15).filter((displayName) => displayName !== myDisplayName);
+
+ if (displayNames.some((displayName) => displayName === myDisplayName)) {
+ selectedDisplayNames.unshift(t('You'));
+ }
+
+ let usernames;
+
+ if (displayNames.length > 15) {
+ usernames = `${ selectedDisplayNames.join(', ') } ${ t('And_more', { length: displayNames.length - 15 }).toLowerCase() }`;
+ } else if (displayNames.length > 1) {
+ usernames = `${ selectedDisplayNames.slice(0, -1).join(', ') } ${ t('and') } ${ selectedDisplayNames[selectedDisplayNames.length - 1] }`;
+ } else {
+ usernames = selectedDisplayNames?.[0];
+ }
+
+ return {
+ emoji,
+ icon: emoji.replaceAll(':', ''),
+ count: displayNames.length,
+ title: `${ usernames } ${ t('Reacted_with').toLowerCase() } ${ emoji }`,
+ };
+ });
+ },
+ hasReactions() {
+ const { msg: { reactions = {} } } = this;
+
+ return Object.keys(reactions).length ? 'hasReactions' : '';
+ },
+ reactionsCount() {
+ const { msg: { reactions = {} } } = this;
+
+ return Object.keys(reactions).length;
+ },
timestamp() {
const { msg } = this;
return +msg.ts;
@@ -278,21 +322,56 @@ const setCornerClasses = (previousNode, currentNode, nextNode, iterateCount) =>
setCornerClasses(getPreviousSentMessage(previousNode), previousNode, currentNode, iterateCount);
}
};
+const showReactions = (target) => {
+ activeReactionMessage = target;
+ const wrapper = document.querySelector('.messages-box .wrapper');
+ const reactions = document.querySelector('.messages-box .wrapper .reactions');
+
+ activeReactionMessage.style.opacity = '1';
+ reactions.style.top = `${ activeReactionMessage.offsetTop - reactions.offsetHeight - 5 }px`;
+
+ if (activeReactionMessage?.classList.contains('messageSent')) {
+ reactions.style.left = '';
+ reactions.style.right = '15px';
+ } else if (activeReactionMessage?.classList.contains('messageReceived')) {
+ reactions.style.left = '15px';
+ reactions.style.right = '';
+ }
+ wrapper.classList.add('show-reactions');
+};
const processSequentials = ({ index, currentNode, settings, forceDate, showDateSeparator = true, groupable, shouldCollapseReplies }) => {
if (!showDateSeparator && !groupable) {
return;
}
- // const currentDataset = currentNode.dataset;
const previousNode = (index === undefined || index > 0) && getPreviousSentMessage(currentNode);
- const nextNode = currentNode.nextElementSibling;
+ let nextNode = currentNode.nextElementSibling;
if (nextNode) {
nextNode.previousElementSibling = currentNode;
+ if (nextNode?.tagName === 'DIV') {
+ nextNode = null;
+ }
}
setCornerClasses(previousNode, currentNode, nextNode, 2);
+ if (currentNode.getAttribute('longPressListener') !== 'true') {
+ currentNode.setAttribute('longPressListener', 'true');
+
+ let longPressTimeout;
+ const mouseDown = () => {
+ longPressTimeout = setTimeout(() => showReactions(currentNode), 500);
+ };
+ const mouseUp = () => clearTimeout(longPressTimeout);
+
+ currentNode.addEventListener('mousedown', mouseDown);
+ currentNode.addEventListener('mouseup', mouseUp);
+ currentNode.addEventListener('touchstart', mouseDown);
+ currentNode.addEventListener('touchend', mouseUp);
+ currentNode.addEventListener('contextmenu', (event) => event.preventDefault());
+ }
+
if (!previousNode) {
setTimeout(() => {
currentNode.dispatchEvent(new CustomEvent('MessageGroup', { bubbles: true }));
@@ -324,8 +403,30 @@ const processSequentials = ({ index, currentNode, settings, forceDate, showDateS
}
}
};
-
Template.messageBubble.onRendered(function() {
const currentNode = this.firstNode;
+
+ const wrapper = document.querySelector('.messages-box .wrapper');
+ const reactionBackdrop = document.querySelector('.reactions-backdrop');
+ reactionBackdrop.addEventListener('contextmenu', (event) => event.preventDefault());
+ reactionBackdrop.addEventListener('click', () => {
+ if (activeReactionMessage) {
+ activeReactionMessage.style.opacity = '';
+ activeReactionMessage = null;
+ }
+ wrapper.classList.remove('show-reactions');
+ });
+
+ $('.reaction-icon').on('click', function(event) {
+ event.stopPropagation();
+ event.stopImmediatePropagation();
+
+ if (validReactions.includes(event.target.getAttribute('data'))) {
+ Meteor.call('setReaction', `:${ event.target.getAttribute('data') }:`, activeReactionMessage.id);
+ }
+
+ reactionBackdrop.click();
+ });
+
this.autorun(() => processSequentials({ currentNode, ...Template.currentData() }));
});
diff --git a/app/ui/client/views/app/room.html b/app/ui/client/views/app/room.html
index fdf655d23a277..c0c99a1e39714 100644
--- a/app/ui/client/views/app/room.html
+++ b/app/ui/client/views/app/room.html
@@ -131,6 +131,17 @@
{{/if}}
{{/if}}
+
+ {{#if isFriendlyUIEnabled}}
+
+
+ {{> icon block="reactions__heart-icon" icon="heart" }}
+ {{> icon block="reactions__thumbsup-icon" icon="thumbsup" }}
+ {{> icon block="reactions__thumbsdown-icon" icon="thumbsdown" }}
+ {{> icon block="reactions__question-icon" icon="question" }}
+ {{> icon block="reactions__exclamation-icon" icon="exclamation" }}
+
+ {{/if}}
diff --git a/private/public/icons.svg b/private/public/icons.svg
index eef102df4ea8f..87567bba282c8 100644
--- a/private/public/icons.svg
+++ b/private/public/icons.svg
@@ -112,6 +112,10 @@