diff options
Diffstat (limited to 'addons/mail/static/src/js/thread.js')
| -rw-r--r-- | addons/mail/static/src/js/thread.js | 636 |
1 files changed, 369 insertions, 267 deletions
diff --git a/addons/mail/static/src/js/thread.js b/addons/mail/static/src/js/thread.js index 54a36e169e4..7d4833e81cd 100644 --- a/addons/mail/static/src/js/thread.js +++ b/addons/mail/static/src/js/thread.js @@ -1,9 +1,11 @@ -odoo.define('mail.ChatThread', function (require) { +odoo.define('mail.widget.Thread', function (require) { "use strict"; +var DocumentViewer = require('mail.DocumentViewer'); +var mailUtils = require('mail.utils'); + var core = require('web.core'); var time = require('web.time'); -var DocumentViewer = require('mail.DocumentViewer'); var Widget = require('web.Widget'); var QWeb = core.qweb; @@ -14,60 +16,31 @@ var ORDER = { 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 READ_MORE = _t("read more"); +var READ_LESS = _t("read less"); -var Thread = Widget.extend({ +/** + * This is a generic widget to render a chat thread + */ +var ThreadWidget = 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 messageId = $(event.currentTarget).data('message-id'); - this.trigger("mark_as_read", messageId); - }, - "click .o_thread_message_moderation": "_onClickMessageModeration", - "change .moderation_checkbox": "_onChangeModerationCheckbox", - "click .o_thread_message_star": function (event) { - var messageId = $(event.currentTarget).data('message-id'); - this.trigger("toggle_star_status", messageId); - }, - "click .o_thread_message_email_exception": "_onClickMailException", - "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'); - } - }, + 'click a': '_onClickRedirect', + 'click img': '_onClickRedirect', + 'click strong': '_onClickRedirect', + 'click .o_thread_show_more': '_onClickShowMore', + 'click .o_attachment_download': '_onAttachmentDownload', + 'click .o_attachment_view': '_onAttachmentView', + 'click .o_thread_message_needaction': '_onClickMessageNeedaction', + 'click .o_thread_message_star': '_onClickMessageStar', + 'click .o_thread_message_reply': '_onClickMessageReply', + 'click .oe_mail_expand': '_onClickMailExpand', + 'click .o_thread_message': '_onClickMessage', + 'click': '_onClick', + 'click .o_thread_message_email_exception': '_onClickEmailException', + 'click .o_thread_message_moderation': '_onClickMessageModeration', + 'change .moderation_checkbox': '_onChangeModerationCheckbox', }, /** @@ -77,253 +50,315 @@ var Thread = Widget.extend({ */ init: function (parent, options) { this._super.apply(this, arguments); - this.enabledOptions = _.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, + // options when the thread is enabled (e.g. can send message, + // interact on messages, etc.) + this._enabledOptions = _.defaults(options || {}, { + displayOrder: ORDER.ASC, + displayMarkAsRead: true, + displayStars: true, + displayDocumentLinks: true, + displayAvatars: true, + squashCloseMessages: true, + displayEmailIcons: true, + displayReplyIcons: false, loadMoreOnScroll: false, }); - this.disabledOptions = { - display_order: this.enabledOptions.display_order, - display_needactions: false, - display_stars: false, - display_document_link: false, - display_avatar: this.enabledOptions.display_avatar, - squash_close_messages: false, - display_email_icon: false, - display_reply_icon: false, - loadMoreOnScroll: this.enabledOptions.loadMoreOnScroll, + // options when the thread is disabled + this._disabledOptions = { + displayOrder: this._enabledOptions.displayOrder, + displayMarkAsRead: false, + displayStars: false, + displayDocumentLinks: false, + displayAvatars: this._enabledOptions.displayAvatars, + squashCloseMessages: false, + displayEmailIcons: false, + displayReplyIcons: false, + loadMoreOnScroll: this._enabledOptions.loadMoreOnScroll, }; - this.expanded_msg_ids = []; - this.selected_id = null; + this._selectedMessageID = null; }, /** - * @param {Object[]} messages + * @override + */ + destroy: function () { + clearInterval(this._updateTimestampsInterval); + }, + + /** + * @param {mail.model.AbstractMessage[]} messages list of messages of the + * thread, ordered by increasing IDs (a higher ID means a more recent + * message). * @param {Object} [options] - * @param {integer} [options.display_order=ORDER.ASC] - * @param {boolean} [options.display_load_more] + * @param {integer} [options.displayOrder=ORDER.ASC] order of displaying + * messages in the thread: + * - ORDER.ASC: last message is at the bottom of the thread + * - ORDER.DESC: last message is at the top of the thread + * @param {boolean} [options.displayLoadMore] * @param {boolean} [options.isCreateMode] - * @param {boolean} [options.squash_close_messages] + * @param {boolean} [options.squashCloseMessages] */ render: function (messages, options) { var self = this; - var msgs = _.map(messages, this._preprocessMessage.bind(this)); - var modeOptions = options.isCreateMode ? this.disabledOptions : this.enabledOptions; - if (modeOptions.display_order === ORDER.DESC) { - msgs.reverse(); + // copy so that reverse do not alter order in the thread object + messages = _.clone(messages); + + var modeOptions = options.isCreateMode ? this._disabledOptions : + this._enabledOptions; + + // attachments ordered by messages order (increasing ID) + this.attachments = _.uniq(_.flatten(_.map(messages, function (message) { + return message.getAttachments(); + }))); + + if (modeOptions.displayOrder === ORDER.DESC) { + messages.reverse(); } - options = _.extend({}, modeOptions, options); + options = _.extend({}, modeOptions, options, { + selectedMessageID: this._selectedMessageID, + }); // 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; + var prevMessage; + _.each(messages, function (message) { + if ( + // is first message of thread + !prevMessage || + // more than 1 min. elasped + (Math.abs(message.getDate().diff(prevMessage.getDate())) > 60000) || + prevMessage.getType() !== 'comment' || + message.getType() !== 'comment' || + // from a different author + (prevMessage.getAuthorID() !== message.getAuthorID()) || + ( + // messages are linked to a document thread + ( + prevMessage.isLinkedToDocumentThread() && + message.isLinkedToDocumentThread() + ) && + ( + // are from different documents + prevMessage.getDocumentModel() !== message.getDocumentModel() || + prevMessage.getDocumentID() !== message.getDocumentID() + ) + ) + ) { + message.displayAuthor(true); } else { - msg.display_author = !options.squash_close_messages; - } - if (msg.attachment_ids) { - var sortedAttachments = _.partition(msg.attachment_ids, function (att) { - return att.mimetype && att.mimetype.split('/')[0] === 'image'; - }); - msg.images = sortedAttachments[0]; - msg.others = sortedAttachments[1]; + message.displayAuthor(!options.squashCloseMessages); } - prev_msg = msg; + prevMessage = message; }); - this.$el.html(QWeb.render('mail.ChatThread', { - messages: msgs, + this.$el.html(QWeb.render('mail.widget.Thread', { + messages: messages, options: options, ORDER: ORDER, - date_format: time.getLangDatetimeFormat(), + dateFormat: 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); + _.each(messages, function (message) { + var $message = self.$('.o_thread_message[data-message-id="'+ message.getID() +'"]'); + $message.find('.o_mail_timestamp').data('date', message.getDate()); - self.insert_read_more($msg); + self._insertReadMore($message); }); - if (!this.update_timestamps_interval) { - this.update_timestamps_interval = setInterval(function () { - self.update_timestamps(); + if (!this._updateTimestampsInterval) { + this.updateTimestampsInterval = setInterval(function () { + self._updateTimestamps(); }, 1000*60); } }, + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + getScrolltop: function () { + return this.$el.scrollTop(); + }, + /** + * State whether the bottom of the thread is visible or not, + * with a tolerance of 5 pixels + * + * @return {boolean} + */ + isAtBottom: function () { + var fullHeight = this.el.scrollHeight; + var topHiddenHeight = this.$el.scrollTop(); + var visibleHeight = this.$el.outerHeight(); + var bottomHiddenHeight = fullHeight - topHiddenHeight - visibleHeight; + return bottomHiddenHeight < 5; + }, + /** + * Removes a message and re-renders the thread + * + * @param {integer} [messageID] the id of the removed message + * @param {mail.model.AbstractMessage[]} [messages] the list of messages to + * display, without the removed one + * @param {Object} [options] options for the thread rendering + */ + removeMessageAndRender: function (messageID, messages, options) { + var self = this; + var done = $.Deferred(); + this.$('.o_thread_message[data-message-id="' + messageID + '"]') + .fadeOut({ + done: function () { + self.render(messages, options); + done.resolve(); + }, + duration: 200, + }); + return done; + }, + /** + * Scroll to the bottom of the thread + */ + scrollToBottom: function () { + this.$el.scrollTop(this.el.scrollHeight); + }, + /** + * Scrolls the thread to a given message + * + * @param {integer} options.messageID the ID of the message to scroll to + * @param {integer} [options.duration] + * @param {boolean} [options.onlyIfNecessary] + */ + scrollToMessage: function (options) { + var $target = this.$('.o_thread_message[data-message-id="' + options.messageID + '"]'); + if (options.onlyIfNecessary) { + 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.duration); + } else if ($target.length) { + this.$el.scrollTo($target); + } + }, + /** + * Scroll to the specific position in pixel + * + * If no position is provided, scroll to the bottom of the thread + * + * @param {integer} [position] distance from top to position in pixels. + * If not provided, scroll to the bottom. + */ + scrollToPosition: function (position) { + if (position) { + this.$el.scrollTop(position); + } else { + this.scrollToBottom(); + } + }, + /** + * Toggle all the moderation checkboxes in the thread + * + * @param {boolean} checked if true, check the boxes, + * otherwise uncheck them. + */ + toggleModerationCheckboxes: function (checked) { + this.$('.moderation_checkbox').prop('checked', checked); + }, + /** + * Unselect the selected message + */ + unselectMessage: function () { + this.$('.o_thread_message').removeClass('o_thread_selected_message'); + this._selectedMessageID = null; + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + /** * Modifies $element to add the 'read more/read less' functionality - * All element nodes with "data-o-mail-quote" attribute are concerned. + * 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'. + * + * @private + * @param {jQuery} $element */ - insert_read_more: function ($element) { + _insertReadMore: function ($element) { var self = this; var groups = []; - var read_more_nodes; + var readMoreNodes; // nodeType 1: element_node // nodeType 3: text_node var $children = $element.contents() .filter(function () { - return this.nodeType === 1 || this.nodeType === 3 && this.nodeValue.trim(); + 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) { + if ( + child.nodeType === 3 && + $child.prevAll('[id*="stopSpelling"]').length > 0 + ) { // Convert Text nodes to Element nodes $child = $('<span>', { text: child.textContent, - "data-o-mail-quote": "1", + '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); + // 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 (!readMoreNodes) { + readMoreNodes = []; + groups.push(readMoreNodes); } $child.hide(); - read_more_nodes.push($child); + readMoreNodes.push($child); } else { - read_more_nodes = undefined; - self.insert_read_more($child); + readMoreNodes = undefined; + self._insertReadMore($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, + var $readMore = $('<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) { + var isReadMore = true; + $readMore.click(function (e) { e.preventDefault(); - is_read_more = !is_read_more; + isReadMore = !isReadMore; _.each(group, function ($child) { $child.hide(); - $child.toggle(!is_read_more); + $child.toggle(!isReadMore); }); - $read_more.text(is_read_more ? read_more : read_less); + $readMore.text(isReadMore ? 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); - } - }, - - on_click_show_more: function () { - this.trigger('load_more_messages'); - }, - - /** - * 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); - }, - /** - * Toggle all the moderation checkboxes in the thread - * - * @param {boolean} checked if true, check the boxes, - * otherwise uncheck them. - */ - toggleModerationCheckboxes: function (checked) { - this.$('.moderation_checkbox').prop('checked', checked); - }, //-------------------------------------------------------------------------- // Private @@ -331,49 +366,31 @@ var Thread = Widget.extend({ /** * @private - * @param {Object} message - * @param {integer} [message.id] - * @param {string} [message.model] - * @param {string} [message.subject] - * @param {string} [message.message_type] - */ - _preprocessMessage: 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; - }, - /** - * @private * @param {Object} options - * @param {integer} [options.channel_id] + * @param {integer} [options.channelID] * @param {string} options.model * @param {integer} options.id */ _redirect: _.debounce(function (options) { - if ('channel_id' in options) { - this.trigger('redirect_to_channel', options.channel_id); + if ('channelID' in options) { + this.trigger('redirect_to_channel', options.channelID); } else { this.trigger('redirect', options.model, options.id); } }, 500, true), + /** + * @private + */ + _updateTimestamps: function () { + var isAtBottom = this.isAtBottom(); + this.$('.o_mail_timestamp').each(function () { + var date = $(this).data('date'); + $(this).html(mailUtils.timeFromNow(date)); + }); + if (isAtBottom && !this.isAtBottom()) { + this.scrollToBottom(); + } + }, //-------------------------------------------------------------------------- // Handlers @@ -407,30 +424,115 @@ var Thread = Widget.extend({ }, /** * @private + */ + _onClick: function () { + if (this._selectedMessageID) { + this.unselectMessage(); + this.trigger('unselect_message'); + } + }, + /** + * @private + * @param {MouseEvent} ev + */ + _onClickEmailException: function (ev) { + var messageID = $(ev.currentTarget).data('message-id'); + this.do_action('mail.mail_resend_message_action', { + additional_context: { + mail_message_to_resend: messageID + } + }); + }, + /** + * @private + * @param {MouseEvent} ev + */ + _onClickMailExpand: function (ev) { + ev.preventDefault(); + }, + /** + * @private + * @param {MouseEvent} ev + */ + _onClickMessage: function (ev) { + $(ev.currentTarget).toggleClass('o_thread_selected_message'); + }, + /** + * @private + * @param {MouseEvent} ev + */ + _onClickMessageNeedaction: function (ev) { + var messageID = $(ev.currentTarget).data('message-id'); + this.trigger('mark_as_read', messageID); + }, + /** + * @private + * @param {MouseEvent} ev + */ + _onClickMessageReply: function (ev) { + this._selectedMessageID = $(ev.currentTarget).data('message-id'); + this.$('.o_thread_message').removeClass('o_thread_selected_message'); + this.$('.o_thread_message[data-message-id="' + this._selectedMessageID + '"]') + .addClass('o_thread_selected_message'); + this.trigger('select_message', this._selectedMessageID); + ev.stopPropagation(); + }, + /** + * @private + * @param {MouseEvent} ev + */ + _onClickMessageStar: function (ev) { + var messageID = $(ev.currentTarget).data('message-id'); + this.trigger('toggle_star_status', messageID); + }, + /** + * @private * @param {MouseEvent} ev */ - _onClickMessageModeration: function (ev) { + _onClickMessageModeration: function (ev) { var $button = $(ev.currentTarget); var messageID = $button.data('message-id'); var decision = $button.data('decision'); - this.trigger_up('message_moderation', { messageID: messageID, decision: decision }); + this.trigger_up('message_moderation', { + messageID: messageID, + decision: decision, + }); }, /** * @private - * @param {MouseEvent} event + * @param {MouseEvent} ev */ - _onClickMailException: function (event) { - var messageId = $(event.currentTarget).data('message-id'); - this.do_action('mail.mail_resend_message_action', { - additional_context: { - mail_message_to_resend: messageId + _onClickRedirect: function (ev) { + // ignore inherited branding + if ($(ev.target).data('oe-field') !== undefined) { + return; + } + var id = $(ev.target).data('oe-id'); + if (id) { + ev.preventDefault(); + var model = $(ev.target).data('oe-model'); + var options; + if (model && (model !== 'mail.channel')) { + options = { + model: model, + id: id + }; + } else { + options = { channelID: id }; } - }); + this._redirect(options); + } + }, + /** + * @private + */ + _onClickShowMore: function () { + this.trigger('load_more_messages'); }, }); -Thread.ORDER = ORDER; +ThreadWidget.ORDER = ORDER; -return Thread; +return ThreadWidget; }); |
