diff options
| author | Alexandre Kühn <aku@odoo.com> | 2018-06-12 15:46:46 +0200 |
|---|---|---|
| committer | Alexandre Kühn <aku@odoo.com> | 2018-07-02 15:06:54 +0200 |
| commit | cd34f6de727d5b3858420cff3c9d3c5995c4e75c (patch) | |
| tree | 8d25861f98a038b71728355375d634a5c178290e /addons/mail/static/src/js/thread.js | |
| parent | ee9c1f52c316c83775969a3c9038e7e9f560f7e7 (diff) | |
[REF] mail: JS mail refactoring
----------------------
Summary
----------------------
The purpose of this commit is to improve the JS code of the `mail` module.
It applies the new coding guidelines and makes some changes on the design
of some modules, such as the old ChatManager module.
Here is a short summary of the changes that have been made:
1. New coding guidelines
- snake_case to camelCase
- prefix private attributes and methods with '_'
- jsdoc on most methods
- one class per module
2. Rename/Merge some classes
- 'chat manager' becomes 'mail manager' (internal) and 'mail service' (external)
- 'chat window manager' is now included in mail manager
- 'thread' widget now named 'thread widget'
3. New model abstraction for mail objects:
- modules 'mail.model.*'
- modeling:
0..1 0..1 * *
ThreadWindow <------> Thread <-------> Message
/ \
/ \
Thread With Cache Document Thread
/ | \
/ | \
Mailbox Channel Support Channel
|
|
DM
- Thread: the superclass of threads.
- ThreadWindow: the window component of a thread.
- Message: mail objects representing messages.
- Thread With Cache: threads that can be used with search view (Discuss app compatible).
- Document Thread: represents the thread part of a chatter.
- Mailbox: represents what was previously called 'static' channel, e.g. 'Inbox'.
- Channel: mail objects representing channels, including livechat.
- DM: special kind of Channel for 1:1 communication in the backend.
- Support Channel: special channel for im_support module.
This new modeling approach let us easily add features on all threads, such as the
possibility to put any thread in a small window.
----------------------
Known issues
----------------------
[Already Present in Master]
1. When the Discuss app is in the background with 'Inbox' as the selected
Thread, when clicking on a document thread preview in the messaging menu
of the systray, the rainbow man appears.
2. When a document with the chatter is in the background, when receiving an
inbox notification from this document thread, the document thread is
automatically marked as read, which removes the notification right away.
3. Sometimes, opening a DM window from the "blank" thread window does not work.
4. Reply-to feature on Inbox is not working: no message is sent in the document
thread.
5. On the first login of admin user with demo data, the inbox counter is wrong
(it displays 6, instead of 3).
Explanation after investigation:
> On page load, it fetches the correct number of Inbox messages (3),
but the server notifies of 3 needaction messages right away,
so it wrongly assumes these are new needaction messages.
> Not possible for web client to detect that these messages should not
increment the Inbox counter while keeping same API.
6. Notifications for new document thread messages only work when the user sets
'handle with Odoo' for the Notification Management in the preferences.
> due to notifications on the longpoll bus for document thread
messages that come from needaction notifications.
> requires server-side changes to send notification on the longpoll
bus to mentionned user.
[New]
7. When receiving a message on a unjoined channel, thread window flickers
('open' > 'close' > 'open')
Explanation after investigation:
> JS logic:
a) On auto-join, ask server to join the channel and get channel infos.
b) The info tells the channel is not detached, but JS code makes decision
to detach it, and tells server the channel is now detached.
c) From (a), server notifies on longpoll bus the state of channel, which is
not detached. The web client thinks that the window state of the channel
has been changed somewhere else, and the channel is now closed.
d) From (b), server notifies on longpoll bus the state of channel, which is
detached. The web client opens the thread window of this channel.
> The flicker didn't occur before refactoring because the web client was only
updating the model of channel when it receives the longpoll notification.
> Server behaviour on 1st longpoll notification is necessary for cross-tab
synchronization for channel window state.
> New design implies that model and view should be synchronized, hence the
issue now.
> Solution: remove server-side thread window synchronization and replace with
client-side synchronization.
----------------------
Hacks
----------------------
The module `im_livechat` now uses mail objects that are compatible with Message
and Window objects:
Modeling for messages:
AbstractMessage
/ \
/ \
LivechatMessage Message
- AbstractMessage: message compatible with the thread widget.
- LivechatMessage: message used by im_livechat.
- Message: message used with the mail manager.
Modeling for thread windows:
AbstractThreadWindow
/ \
/ \
LivechatWindow ThreadWindow
- AbstractThreadWindow: behaviour share between all types of thread windows.
- LivechatWindow: window used by im_livechat.
- ThreadWindow: window used with mail_manager.
The reason for these hacks are twofold:
1. Use the thread widget in the frontend and livechat external lib bundles.
2. Do not have a dependency with the mail manager in the frontend and external
lib bundles.
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; }); |
