flectra/addons/mail/static/src/js/thread.js

345 lines
12 KiB
JavaScript

flectra.define('mail.ChatThread', function (require) {
"use strict";
var core = require('web.core');
var time = require('web.time');
var DocumentViewer = require('mail.DocumentViewer');
var Widget = require('web.Widget');
var QWeb = core.qweb;
var _t = core._t;
var ORDER = {
ASC: 1,
DESC: -1,
};
var read_more = _t('read more');
var read_less = _t('read less');
function time_from_now(date) {
if (moment().diff(date, 'seconds') < 45) {
return _t("now");
}
return date.fromNow();
}
var Thread = Widget.extend({
className: 'o_mail_thread',
events: {
"click a": "on_click_redirect",
"click img": "on_click_redirect",
"click strong": "on_click_redirect",
"click .o_thread_show_more": "on_click_show_more",
"click .o_attachment_download": "_onAttachmentDownload",
"click .o_attachment_view": "_onAttachmentView",
"click .o_thread_message_needaction": function (event) {
var message_id = $(event.currentTarget).data('message-id');
this.trigger("mark_as_read", message_id);
},
"click .o_thread_message_star": function (event) {
var message_id = $(event.currentTarget).data('message-id');
this.trigger("toggle_star_status", message_id);
},
"click .o_thread_message_reply": function (event) {
this.selected_id = $(event.currentTarget).data('message-id');
this.$('.o_thread_message').removeClass('o_thread_selected_message');
this.$('.o_thread_message[data-message-id="' + this.selected_id + '"]')
.addClass('o_thread_selected_message');
this.trigger('select_message', this.selected_id);
event.stopPropagation();
},
"click .oe_mail_expand": function (event) {
event.preventDefault();
var $message = $(event.currentTarget).parents('.o_thread_message');
$message.addClass('o_message_expanded');
this.expanded_msg_ids.push($message.data('message-id'));
},
"click .o_thread_message": function (event) {
$(event.currentTarget).toggleClass('o_thread_selected_message');
},
"click": function () {
if (this.selected_id) {
this.unselect();
this.trigger('unselect_message');
}
},
},
init: function (parent, options) {
this._super.apply(this, arguments);
this.options = _.defaults(options || {}, {
display_order: ORDER.ASC,
display_needactions: true,
display_stars: true,
display_document_link: true,
display_avatar: true,
squash_close_messages: true,
display_email_icon: true,
display_reply_icon: false,
});
this.expanded_msg_ids = [];
this.selected_id = null;
},
render: function (messages, options) {
var self = this;
var msgs = _.map(messages, this._preprocess_message.bind(this));
if (this.options.display_order === ORDER.DESC) {
msgs.reverse();
}
options = _.extend({}, this.options, options);
// Hide avatar and info of a message if that message and the previous
// one are both comments wrote by the same author at the same minute
// and in the same document (users can now post message in documents
// directly from a channel that follows it)
var prev_msg;
_.each(msgs, function (msg) {
if (!prev_msg || (Math.abs(msg.date.diff(prev_msg.date)) > 60000) ||
prev_msg.message_type !== 'comment' || msg.message_type !== 'comment' ||
(prev_msg.author_id[0] !== msg.author_id[0]) || prev_msg.model !== msg.model ||
prev_msg.res_id !== msg.res_id) {
msg.display_author = true;
} else {
msg.display_author = !options.squash_close_messages;
}
prev_msg = msg;
});
this.$el.html(QWeb.render('mail.ChatThread', {
messages: msgs,
options: options,
ORDER: ORDER,
date_format: time.getLangDatetimeFormat(),
}));
this.attachments = _.uniq(_.flatten(_.map(messages, 'attachment_ids')));
_.each(msgs, function(msg) {
var $msg = self.$('.o_thread_message[data-message-id="'+ msg.id +'"]');
$msg.find('.o_mail_timestamp').data('date', msg.date);
self.insert_read_more($msg);
});
if (!this.update_timestamps_interval) {
this.update_timestamps_interval = setInterval(function() {
self.update_timestamps();
}, 1000*60);
}
},
/**
* Modifies $element to add the 'read more/read less' functionality
* All element nodes with "data-o-mail-quote" attribute are concerned.
* All text nodes after a ``#stopSpelling`` element are concerned.
* Those text nodes need to be wrapped in a span (toggle functionality).
* All consecutive elements are joined in one 'read more/read less'.
*/
insert_read_more: function ($element) {
var self = this;
var groups = [];
var read_more_nodes;
// nodeType 1: element_node
// nodeType 3: text_node
var $children = $element.contents()
.filter(function() {
return this.nodeType === 1 || this.nodeType === 3 && this.nodeValue.trim();
});
_.each($children, function(child) {
var $child = $(child);
// Hide Text nodes if "stopSpelling"
if (child.nodeType === 3 && $child.prevAll("[id*='stopSpelling']").length > 0) {
// Convert Text nodes to Element nodes
var $child = $('<span>', {
text: child.textContent,
"data-o-mail-quote": "1",
});
child.parentNode.replaceChild($child[0], child);
}
// Create array for each "read more" with nodes to toggle
if ($child.attr('data-o-mail-quote') || ($child.get(0).nodeName === 'BR' && $child.prev("[data-o-mail-quote='1']").length > 0)) {
if (!read_more_nodes) {
read_more_nodes = [];
groups.push(read_more_nodes);
}
$child.hide();
read_more_nodes.push($child);
} else {
read_more_nodes = undefined;
self.insert_read_more($child);
}
});
_.each(groups, function(group) {
// Insert link just before the first node
var $read_more = $('<a>', {
class: "o_mail_read_more",
href: "#",
text: read_more,
}).insertBefore(group[0]);
// Toggle All next nodes
var is_read_more = true;
$read_more.click(function(e) {
e.preventDefault();
is_read_more = !is_read_more;
_.each(group, function ($child) {
$child.hide();
$child.toggle(!is_read_more);
});
$read_more.text(is_read_more ? read_more : read_less);
});
});
},
update_timestamps: function () {
var isAtBottom = this.is_at_bottom();
this.$('.o_mail_timestamp').each(function() {
var date = $(this).data('date');
$(this).html(time_from_now(date));
});
if (isAtBottom && !this.is_at_bottom()) {
this.scroll_to();
}
},
on_click_redirect: function (event) {
// ignore inherited branding
if ($(event.target).data('oe-field') !== undefined) {
return;
}
var id = $(event.target).data('oe-id');
if (id) {
event.preventDefault();
var model = $(event.target).data('oe-model');
var options = model && (model !== 'mail.channel') ? {model: model, id: id} : {channel_id: id};
this._redirect(options);
}
},
_redirect: _.debounce(function (options) {
if ('channel_id' in options) {
this.trigger('redirect_to_channel', options.channel_id);
} else {
this.trigger('redirect', options.model, options.id);
}
}, 500, true),
on_click_show_more: function () {
this.trigger('load_more_messages');
},
_preprocess_message: function (message) {
var msg = _.extend({}, message);
msg.date = moment.min(msg.date, moment());
msg.hour = time_from_now(msg.date);
var date = msg.date.format('YYYY-MM-DD');
if (date === moment().format('YYYY-MM-DD')) {
msg.day = _t("Today");
} else if (date === moment().subtract(1, 'days').format('YYYY-MM-DD')) {
msg.day = _t("Yesterday");
} else {
msg.day = msg.date.format('LL');
}
if (_.contains(this.expanded_msg_ids, message.id)) {
msg.expanded = true;
}
msg.display_subject = message.subject && message.message_type !== 'notification' && !(message.model && (message.model !== 'mail.channel'));
msg.is_selected = msg.id === this.selected_id;
return msg;
},
/**
* Removes a message and re-renders the thread
* @param {int} [message_id] the id of the removed message
* @param {array} [messages] the list of messages to display, without the removed one
* @param {object} [options] options for the thread rendering
*/
remove_message_and_render: function (message_id, messages, options) {
var self = this;
var done = $.Deferred();
this.$('.o_thread_message[data-message-id="' + message_id + '"]').fadeOut({
done: function () { self.render(messages, options); done.resolve();},
duration: 200,
});
return done;
},
/**
* Scrolls the thread to a given message or offset if any, to bottom otherwise
* @param {int} [options.id] optional: the id of the message to scroll to
* @param {int} [options.offset] optional: the number of pixels to scroll
*/
scroll_to: function (options) {
options = options || {};
if (options.id !== undefined) {
var $target = this.$('.o_thread_message[data-message-id="' + options.id + '"]');
if (options.only_if_necessary) {
var delta = $target.parent().height() - $target.height();
var offset = delta < 0 ? 0 : delta - ($target.offset().top - $target.offsetParent().offset().top);
offset = - Math.min(offset, 0);
this.$el.scrollTo("+=" + offset + "px", options);
} else if ($target.length) {
this.$el.scrollTo($target);
}
} else if (options.offset !== undefined) {
this.$el.scrollTop(options.offset);
} else {
this.$el.scrollTop(this.el.scrollHeight);
}
},
get_scrolltop: function () {
return this.$el.scrollTop();
},
is_at_bottom: function () {
return this.el.scrollHeight - this.$el.scrollTop() - this.$el.outerHeight() < 5;
},
unselect: function () {
this.$('.o_thread_message').removeClass('o_thread_selected_message');
this.selected_id = null;
},
destroy: function () {
clearInterval(this.update_timestamps_interval);
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
* @param {MouseEvent} event
*/
_onAttachmentDownload: function (event) {
event.stopPropagation();
},
/**
* @private
* @param {MouseEvent} event
*/
_onAttachmentView: function (event) {
event.stopPropagation();
var activeAttachmentID = $(event.currentTarget).data('id');
if (activeAttachmentID) {
var attachmentViewer = new DocumentViewer(this, this.attachments, activeAttachmentID);
attachmentViewer.appendTo($('body'));
}
},
});
Thread.ORDER = ORDER;
return Thread;
});