The communications platform that puts data protection first.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
Rocket.Chat/app/ui-message/client/message.js

630 lines
17 KiB

import _ from 'underscore';
import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker';
import { Blaze } from 'meteor/blaze';
import { Template } from 'meteor/templating';
import { TAPi18n } from 'meteor/tap:i18n';
import { timeAgo, formatDateAndTime } from '../../lib/client/lib/formatDate';
import { DateFormat } from '../../lib/client';
import { renderMessageBody, MessageTypes, MessageAction, call, normalizeThreadMessage } from '../../ui-utils/client';
import { RoomRoles, UserRoles, Roles, Messages } from '../../models/client';
import { AutoTranslate } from '../../autotranslate/client';
import { callbacks } from '../../callbacks/client';
import { Markdown } from '../../markdown/client';
import { t, roomTypes, getURL } from '../../utils';
import { messageArgs } from '../../ui-utils/client/lib/messageArgs';
import './message.html';
import './messageThread.html';
async function renderPdfToCanvas(canvasId, pdfLink) {
const isSafari = /constructor/i.test(window.HTMLElement)
|| ((p) => p.toString() === '[object SafariRemoteNotification]')(!window.safari
|| (typeof window.safari !== 'undefined' && window.safari.pushNotification));
if (isSafari) {
const [, version] = /Version\/([0-9]+)/.exec(navigator.userAgent) || [null, 0];
if (version <= 12) {
return;
}
}
if (!pdfLink || !/\.pdf$/i.test(pdfLink)) {
return;
}
pdfLink = getURL(pdfLink);
const canvas = document.getElementById(canvasId);
if (!canvas) {
return;
}
const pdfjsLib = await import('pdfjs-dist');
pdfjsLib.GlobalWorkerOptions.workerSrc = `${ Meteor.absoluteUrl() }pdf.worker.min.js`;
const loader = document.getElementById(`js-loading-${ canvasId }`);
if (loader) {
loader.style.display = 'block';
}
const pdf = await pdfjsLib.getDocument(pdfLink);
const page = await pdf.getPage(1);
const scale = 0.5;
const viewport = page.getViewport(scale);
const context = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
await page.render({
canvasContext: context,
viewport,
}).promise;
if (loader) {
loader.style.display = 'none';
}
canvas.style.maxWidth = '-webkit-fill-available';
canvas.style.maxWidth = '-moz-available';
canvas.style.display = 'block';
}
const renderBody = (msg, settings) => {
const isSystemMessage = MessageTypes.isSystemMessage(msg);
const messageType = MessageTypes.getType(msg) || {};
if (messageType.render) {
msg = messageType.render(msg);
} else if (messageType.template) {
// render template
} else if (messageType.message) {
msg = TAPi18n.__(messageType.message, { ...typeof messageType.data === 'function' && messageType.data(msg) });
} else if (msg.u && msg.u.username === settings.Chatops_Username) {
msg.html = msg.msg;
msg = callbacks.run('renderMentions', msg);
msg = msg.html;
} else {
msg = renderMessageBody(msg);
}
if (isSystemMessage) {
msg.html = Markdown.parse(msg.html);
}
return msg;
};
Template.message.helpers({
body() {
const { msg, settings } = this;
return Tracker.nonreactive(() => renderBody(msg, settings));
},
and(a, b) {
return a && b;
},
i18nReplyCounter() {
const { msg } = this;
return `<span class='reply-counter'>${ msg.tcount }</span>`;
},
i18nDiscussionCounter() {
const { msg } = this;
return `<span class='reply-counter'>${ msg.dcount }</span>`;
},
formatDateAndTime,
encodeURI(text) {
return encodeURI(text);
},
broadcast() {
const { msg, room = {}, u } = this;
return !msg.private && !msg.t && msg.u._id !== u._id && room && room.broadcast;
},
isIgnored() {
const { ignored, msg } = this;
const isIgnored = typeof ignored !== 'undefined' ? ignored : msg.ignored;
return isIgnored;
},
ignoredClass() {
const { ignored, msg } = this;
const isIgnored = typeof ignored !== 'undefined' ? ignored : msg.ignored;
return isIgnored ? 'message--ignored' : '';
},
isDecrypting() {
const { msg } = this;
return msg.e2e === 'pending';
},
isBot() {
const { msg } = this;
return msg.bot && 'bot';
},
roleTags() {
const { msg, hideRoles, settings } = this;
if (settings.hideRoles || hideRoles) {
return [];
}
if (!msg.u || !msg.u._id) {
return [];
}
const userRoles = UserRoles.findOne(msg.u._id);
const roomRoles = RoomRoles.findOne({
'u._id': msg.u._id,
rid: msg.rid,
});
const roles = [...(userRoles && userRoles.roles) || [], ...(roomRoles && roomRoles.roles) || []];
return Roles.find({
_id: {
$in: roles,
},
description: {
$exists: 1,
$ne: '',
},
}, {
fields: {
description: 1,
},
});
},
isGroupable() {
const { msg, room = {}, settings, groupable } = this;
if (groupable === false || settings.allowGroup === false || room.broadcast || msg.groupable === false || (MessageTypes.isSystemMessage(msg) && !msg.tmid)) {
return 'false';
}
},
sequentialClass() {
const { msg, groupable } = this;
if (MessageTypes.isSystemMessage(msg) && !msg.tmid) {
return;
}
return groupable !== false && msg.groupable !== false && 'sequential';
},
avatarFromUsername() {
const { msg } = this;
if (msg.avatar != null && msg.avatar[0] === '@') {
return msg.avatar.replace(/^@/, '');
}
},
getName() {
const { msg, settings } = this;
if (msg.alias) {
return msg.alias;
}
if (!msg.u) {
return '';
}
return (settings.UI_Use_Real_Name && msg.u.name) || msg.u.username;
},
showUsername() {
const { msg, settings } = this;
return msg.alias || (settings.UI_Use_Real_Name && msg.u && msg.u.name);
},
own() {
const { msg, u } = this;
if (msg.u && msg.u._id === u._id) {
return 'own';
}
},
timestamp() {
const { msg } = this;
return +msg.ts;
},
chatops() {
const { msg, settings } = this;
if (msg.u && msg.u.username === settings.Chatops_Username) {
return 'chatops-message';
}
},
time() {
const { msg, timeAgo: useTimeAgo } = this;
return useTimeAgo ? timeAgo(msg.ts) : DateFormat.formatTime(msg.ts);
},
date() {
const { msg } = this;
return DateFormat.formatDate(msg.ts);
},
isTemp() {
const { msg } = this;
if (msg.temp === true) {
return 'temp';
}
},
threadMessage() {
const { msg } = this;
return normalizeThreadMessage(msg);
},
bodyClass() {
const { msg } = this;
return MessageTypes.isSystemMessage(msg) ? 'color-info-font-color' : 'color-primary-font-color';
},
system(returnClass) {
const { msg } = this;
if (MessageTypes.isSystemMessage(msg)) {
if (returnClass) {
return 'color-info-font-color';
}
return 'system';
}
},
showTranslated() {
const { msg, subscription, settings, u } = this;
if (settings.AutoTranslate_Enabled && msg.u && msg.u._id !== u._id && !MessageTypes.isSystemMessage(msg)) {
const language = AutoTranslate.getLanguage(msg.rid);
const autoTranslate = subscription && subscription.autoTranslate;
return msg.autoTranslateFetching || (!!autoTranslate !== !!msg.autoTranslateShowInverse && msg.translations && msg.translations[language]);
}
},
edited() {
return Template.instance().wasEdited;
},
editTime() {
const { msg } = this;
if (Template.instance().wasEdited) {
return DateFormat.formatDateAndTime(msg.editedAt);
}
},
editedBy() {
if (!Template.instance().wasEdited) {
return '';
}
const { msg } = this;
// try to return the username of the editor,
// otherwise a special "?" character that will be
// rendered as a special avatar
return (msg.editedBy && msg.editedBy.username) || '?';
},
label() {
const { msg } = this;
if (msg.i18nLabel) {
return t(msg.i18nLabel);
} if (msg.label) {
return msg.label;
}
},
hasOembed() {
const { msg, settings } = this;
// there is no URLs, there is no template to show the oembed (oembed package removed) or oembed is not enable
if (!(msg.urls && msg.urls.length > 0) || !Template.oembedBaseWidget || !settings.API_Embed) {
return false;
}
// check if oembed is disabled for message's sender
if ((settings.API_EmbedDisabledFor || '').split(',').map((username) => username.trim()).includes(msg.u && msg.u.username)) {
return false;
}
return true;
},
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,
count: displayNames.length,
usernames,
reaction: ` ${ t('Reacted_with').toLowerCase() } ${ emoji }`,
userReacted: displayNames.indexOf(myDisplayName) > -1,
};
});
},
markUserReaction(reaction) {
if (reaction.userReacted) {
return {
class: 'selected',
};
}
},
hideReactions() {
const { msg } = this;
if (_.isEmpty(msg.reactions)) {
return 'hidden';
}
},
hideMessageActions() {
const { msg } = this;
return msg.private || MessageTypes.isSystemMessage(msg);
},
actionLinks() {
const { msg } = this;
// remove 'method_id' and 'params' properties
return _.map(msg.actionLinks, function(actionLink, key) {
return _.extend({
id: key,
}, _.omit(actionLink, 'method_id', 'params'));
});
},
hideActionLinks() {
const { msg } = this;
if (_.isEmpty(msg.actionLinks)) {
return 'hidden';
}
},
injectIndex(data, index) {
data.index = index;
},
channelName() {
const { subscription } = this;
// const subscription = Subscriptions.findOne({ rid: this.rid });
return subscription && subscription.name;
},
roomIcon() {
const { room } = this;
if (room && room.t === 'd') {
return 'at';
}
return roomTypes.getIcon(room);
},
customClass() {
const { customClass, msg } = this;
return customClass || msg.customClass;
},
fromSearch() {
const { customClass, msg } = this;
return [msg.customClass, customClass].includes('search');
},
actionContext() {
const { msg } = this;
return msg.actionContext;
},
messageActions(group) {
const { msg, context: ctx } = this;
let messageGroup = group;
let context = ctx || msg.actionContext;
if (!group) {
messageGroup = 'message';
}
if (!context) {
context = 'message';
}
return MessageAction.getButtons(msg, context, messageGroup);
},
isSnippet() {
const { msg } = this;
return msg.actionContext === 'snippeted';
},
isThreadReply() {
const { msg: { tmid, t }, settings: { showreply } } = this;
return !!(tmid && showreply && !t);
},
collapsed() {
const { msg: { tmid, collapsed }, settings: { showreply }, shouldCollapseReplies } = this;
const isCollapsedThreadReply = shouldCollapseReplies && tmid && showreply && collapsed !== false;
if (isCollapsedThreadReply) {
return 'collapsed';
}
},
collapseSwitchClass() {
const { msg: { collapsed = true } } = this;
return collapsed ? 'icon-right-dir' : 'icon-down-dir';
},
parentMessage() {
const { msg: { threadMsg } } = this;
return threadMsg;
},
});
const findParentMessage = (() => {
const waiting = [];
const getMessages = _.debounce(async function() {
const uid = Tracker.nonreactive(() => Meteor.userId());
const _tmp = [...waiting];
waiting.length = 0;
const messages = await call('getMessages', _tmp);
messages.forEach((message) => {
if (!message) {
return;
}
const { _id, ...msg } = message;
Messages.update({ tmid: _id, repliesCount: { $exists: 0 } }, {
$set: {
following: message.replies && message.replies.indexOf(uid) > -1,
threadMsg: normalizeThreadMessage(msg),
repliesCount: msg.tcount,
},
}, { multi: true });
if (!Messages.findOne({ _id })) {
msg._hidden = true;
Messages.upsert({ _id }, msg);
}
});
}, 500);
return (tmid) => {
if (waiting.indexOf(tmid) > -1) {
return;
}
const message = Messages.findOne({ _id: tmid });
if (message) {
const uid = Tracker.nonreactive(() => Meteor.userId());
return Messages.update({ tmid, repliesCount: { $exists: 0 } }, {
$set: {
following: message.replies && message.replies.indexOf(uid) > -1,
threadMsg: normalizeThreadMessage(message),
repliesCount: message.tcount,
},
}, { multi: true });
}
waiting.push(tmid);
getMessages();
};
})();
Template.message.onCreated(function() {
const { msg, shouldCollapseReplies } = Template.currentData();
this.wasEdited = msg.editedAt && !MessageTypes.isSystemMessage(msg);
if (shouldCollapseReplies && msg.tmid && !msg.threadMsg) {
findParentMessage(msg.tmid);
}
});
const hasTempClass = (node) => node.classList.contains('temp');
const getPreviousSentMessage = (currentNode) => {
if (hasTempClass(currentNode)) {
return currentNode.previousElementSibling;
}
if (currentNode.previousElementSibling != null) {
let previousValid = currentNode.previousElementSibling;
while (previousValid != null && (hasTempClass(previousValid) || !previousValid.classList.contains('message'))) {
previousValid = previousValid.previousElementSibling;
}
return previousValid;
}
};
const isNewDay = (currentNode, previousNode, forceDate, showDateSeparator) => {
if (!showDateSeparator) {
return false;
}
if (forceDate || !previousNode) {
return true;
}
const { dataset: currentDataset } = currentNode;
const { dataset: previousDataset } = previousNode;
const previousMessageDate = new Date(parseInt(previousDataset.timestamp));
const currentMessageDate = new Date(parseInt(currentDataset.timestamp));
if (previousMessageDate.toDateString() !== currentMessageDate.toDateString()) {
return true;
}
return false;
};
const isSequential = (currentNode, previousNode, forceDate, period, showDateSeparator, shouldCollapseReplies) => {
if (!previousNode) {
return false;
}
if (showDateSeparator && forceDate) {
return false;
}
const { dataset: currentDataset } = currentNode;
const { dataset: previousDataset } = previousNode;
const previousMessageDate = new Date(parseInt(previousDataset.timestamp));
const currentMessageDate = new Date(parseInt(currentDataset.timestamp));
if (showDateSeparator && previousMessageDate.toDateString() !== currentMessageDate.toDateString()) {
return false;
}
if (shouldCollapseReplies && currentDataset.tmid) {
return previousDataset.id === currentDataset.tmid || previousDataset.tmid === currentDataset.tmid;
}
if (previousDataset.tmid && !currentDataset.tmid) {
return false;
}
if ([previousDataset.groupable, currentDataset.groupable].includes('false')) {
return false;
}
if (previousDataset.username !== currentDataset.username) {
return false;
}
if (parseInt(currentDataset.timestamp) - parseInt(previousDataset.timestamp) <= period) {
return true;
}
return false;
};
const processSequentials = ({ currentNode, settings, forceDate, showDateSeparator = true, groupable, msg, shouldCollapseReplies }) => {
if (!showDateSeparator && !groupable) {
return;
}
if (msg.file && msg.file.type === 'application/pdf') {
Meteor.defer(() => { renderPdfToCanvas(msg.file._id, msg.attachments[0].title_link); });
}
// const currentDataset = currentNode.dataset;
const previousNode = getPreviousSentMessage(currentNode);
const nextNode = currentNode.nextElementSibling;
if (isSequential(currentNode, previousNode, forceDate, settings.Message_GroupingPeriod, showDateSeparator, shouldCollapseReplies)) {
currentNode.classList.add('sequential');
} else {
currentNode.classList.remove('sequential');
}
if (isNewDay(currentNode, previousNode, forceDate, showDateSeparator)) {
currentNode.classList.add('new-day');
} else {
currentNode.classList.remove('new-day');
}
if (nextNode && nextNode.dataset) {
if (isSequential(nextNode, currentNode, forceDate, settings.Message_GroupingPeriod, showDateSeparator, shouldCollapseReplies)) {
nextNode.classList.add('sequential');
} else {
nextNode.classList.remove('sequential');
}
if (isNewDay(nextNode, currentNode, forceDate, showDateSeparator)) {
nextNode.classList.add('new-day');
} else {
nextNode.classList.remove('new-day');
}
} else if (shouldCollapseReplies) {
const [el] = $(`#chat-window-${ msg.rid }`);
const view = el && Blaze.getView(el);
const templateInstance = view && view.templateInstance();
if (!templateInstance) {
return;
}
if (currentNode.classList.contains('own') === true) {
templateInstance.atBottom = true;
}
templateInstance.sendToBottomIfNecessary();
}
};
Template.message.onRendered(function() { // duplicate of onViewRendered(NRR) the onRendered works only for non nrr templates
this.autorun(() => {
const currentNode = this.firstNode;
processSequentials({ currentNode, ...messageArgs(Template.currentData()) });
});
});
Template.message.onViewRendered = function() {
const args = messageArgs(Template.currentData());
// processSequentials({ currentNode, ...messageArgs(Template.currentData()) });
return this._domrange.onAttached((domRange) => {
const currentNode = domRange.lastNode();
processSequentials({ currentNode, ...args });
});
};