diff --git a/addons/mail/data/ir_cron_data.xml b/addons/mail/data/ir_cron_data.xml index 27ae879580492cb2b4d0763e25d8362c13cfcda0..10073110a3aa6de2acebc0df01d20c65da2ea6e8 100644 --- a/addons/mail/data/ir_cron_data.xml +++ b/addons/mail/data/ir_cron_data.xml @@ -43,5 +43,17 @@ <field eval="False" name="doall" /> <field name="priority">1000</field> </record> + + <record id="ir_cron_delete_notification" model="ir.cron"> + <field name="name">Notification: Delete Notifications older than 6 Month</field> + <field name="interval_number">1</field> + <field name="interval_type">days</field> + <field name="numbercall">-1</field> + <field name="doall" eval="False"/> + <field name="model_id" ref="model_mail_notification"/> + <field name="code">model._gc_notifications(max_age_days=180)</field> + <field name="state">code</field> + </record> + </data> </odoo> diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py index 38426d629bdc064a78d000bfad9f82d505526502..a7061c77d3497dcebbb3a7e33eb94c955ee17c8f 100644 --- a/addons/mail/models/mail_message.py +++ b/addons/mail/models/mail_message.py @@ -190,53 +190,28 @@ class Message(models.Model): #------------------------------------------------------ @api.model - def mark_all_as_read(self, channel_ids=None, domain=None): - """ Remove all needactions of the current partner. If channel_ids is - given, restrict to messages written in one of those channels. """ + def mark_all_as_read(self, domain=None): + # not really efficient method: it does one db request for the + # search, and one for each message in the result set is_read to True in the + # current notifications from the relation. partner_id = self.env.user.partner_id.id - delete_mode = not self.env.user.share # delete employee notifs, keep customer ones - if not domain and delete_mode: - query = "DELETE FROM mail_message_res_partner_needaction_rel WHERE res_partner_id IN %s" - args = [(partner_id,)] - if channel_ids: - query += """ - AND mail_message_id in - (SELECT mail_message_id - FROM mail_message_mail_channel_rel - WHERE mail_channel_id in %s)""" - args += [tuple(channel_ids)] - query += " RETURNING mail_message_id as id" - self._cr.execute(query, args) - self.invalidate_cache() - - ids = [m['id'] for m in self._cr.dictfetchall()] - else: - # not really efficient method: it does one db request for the - # search, and one for each message in the result set to remove the - # current user from the relation. - msg_domain = [('needaction_partner_ids', 'in', partner_id)] - if channel_ids: - msg_domain += [('channel_ids', 'in', channel_ids)] - unread_messages = self.search(expression.AND([msg_domain, domain])) - notifications = self.env['mail.notification'].sudo().search([ - ('mail_message_id', 'in', unread_messages.ids), - ('res_partner_id', '=', self.env.user.partner_id.id), - ('is_read', '=', False)]) - if delete_mode: - notifications.unlink() - else: - notifications.write({'is_read': True}) - ids = unread_messages.mapped('id') + msg_domain = [('needaction_partner_ids', 'in', partner_id)] + unread_messages = self.search(expression.AND([msg_domain, domain])) + ids = unread_messages.ids + notifications = self.env['mail.notification'].sudo().search([ + ('mail_message_id', 'in', ids), + ('res_partner_id', '=', partner_id), + ('is_read', '=', False)]) + notifications.write({'is_read': True}) - notification = {'type': 'mark_as_read', 'message_ids': ids, 'channel_ids': channel_ids} - self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), notification) + notification = {'type': 'mark_as_read', 'message_ids': ids} + self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', partner_id), notification) return ids def set_message_done(self): """ Remove the needaction from messages for the current partner. """ partner_id = self.env.user.partner_id - delete_mode = not self.env.user.share # delete employee notifs, keep customer ones notifications = self.env['mail.notification'].sudo().search([ ('mail_message_id', 'in', self.ids), @@ -264,10 +239,7 @@ class Message(models.Model): current_group = [record.id] current_channel_ids = record.channel_ids - if delete_mode: - notifications.unlink() - else: - notifications.write({'is_read': True}) + notifications.write({'is_read': True}) for (msg_ids, channel_ids) in groups: notification = {'type': 'mark_as_read', 'message_ids': msg_ids, 'channel_ids': [c.id for c in channel_ids]} @@ -501,20 +473,27 @@ class Message(models.Model): note_id = self.env['ir.model.data'].xmlid_to_res_id('mail.mt_note') # fetch notification status - notif_dict = {} - notifs = self.env['mail.notification'].sudo().search([('mail_message_id', 'in', list(mid for mid in message_tree)), ('res_partner_id', '!=', False), ('is_read', '=', False)]) + + notif_dict = defaultdict(lambda: defaultdict(list)) + notifs = self.env['mail.notification'].sudo().search([('mail_message_id', 'in', list(mid for mid in message_tree)), ('res_partner_id', '!=', False)]) + for notif in notifs: mid = notif.mail_message_id.id - if not notif_dict.get(mid): - notif_dict[mid] = {'partner_id': list()} - notif_dict[mid]['partner_id'].append(notif.res_partner_id.id) + if notif.is_read: + notif_dict[mid]['history_partner_ids'].append(notif.res_partner_id.id) + else: + notif_dict[mid]['needaction_partner_ids'].append(notif.res_partner_id.id) for message in message_values: - message['needaction_partner_ids'] = notif_dict.get(message['id'], dict()).get('partner_id', []) - message['is_note'] = message['subtype_id'] and subtypes_dict[message['subtype_id'][0]]['id'] == note_id - message['is_discussion'] = message['subtype_id'] and subtypes_dict[message['subtype_id'][0]]['id'] == com_id + message.update({ + 'needaction_partner_ids': notif_dict[message['id']]['needaction_partner_ids'], + 'history_partner_ids': notif_dict[message['id']]['history_partner_ids'], + 'is_note': message['subtype_id'] and subtypes_dict[message['subtype_id'][0]]['id'] == note_id, + 'is_discussion': message['subtype_id'] and subtypes_dict[message['subtype_id'][0]]['id'] == com_id, + 'subtype_description': message['subtype_id'] and subtypes_dict[message['subtype_id'][0]]['description'] + }) message['is_notification'] = message['message_type'] == 'user_notification' - message['subtype_description'] = message['subtype_id'] and subtypes_dict[message['subtype_id'][0]]['description'] + if message['model'] and self.env[message['model']]._original_module: message['module_icon'] = modules.module.get_module_icon(self.env[message['model']]._original_module) return message_values diff --git a/addons/mail/models/mail_notification.py b/addons/mail/models/mail_notification.py index c89ce0fd57d5badf9f8cabf58eb83c792405e76a..e47f337e49153892392f8550ad9871cf368a4045 100644 --- a/addons/mail/models/mail_notification.py +++ b/addons/mail/models/mail_notification.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- +from dateutil.relativedelta import relativedelta + from odoo import api, fields, models from odoo.tools.translate import _ @@ -38,6 +40,7 @@ class Notification(models.Model): ("UNKNOWN", "Unknown error"), ], string='Failure type') failure_reason = fields.Text('Failure reason', copy=False) + read_date = fields.Datetime('Read Date', copy=False) _sql_constraints = [ # email notification;: partner is required @@ -57,3 +60,25 @@ class Notification(models.Model): return dict(type(self).failure_type.selection).get(self.failure_type, _('No Error')) else: return _("Unknown error") + ": %s" % (self.failure_reason or '') + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if vals.get('is_read'): + vals['read_date'] = fields.Datetime.now() + return super(Notification, self).create(vals_list) + + def write(self, vals): + if vals.get('is_read'): + vals['read_date'] = fields.Datetime.now() + return super(Notification, self).write(vals) + + @api.model + def _gc_notifications(self, max_age_days=180): + domain = [ + ('is_read', '=', True), + ('read_date', '<', fields.Datetime.now() - relativedelta(days=max_age_days)), + ('res_partner_id.partner_share', '=', False), + ('notification_type', '=', 'email') + ] + return self.search(domain).unlink() diff --git a/addons/mail/static/src/js/discuss_mobile.js b/addons/mail/static/src/js/discuss_mobile.js index 617700c20c146e2e46416e8dbdbe35bd5339ce91..f90491636a7bd7b705d42439d47d8daaf7cc7302 100644 --- a/addons/mail/static/src/js/discuss_mobile.js +++ b/addons/mail/static/src/js/discuss_mobile.js @@ -71,7 +71,7 @@ Discuss.include({ * @returns {Boolean} true iff we currently are in the Inbox tab */ _isInInboxTab: function () { - return _.contains(['mailbox_inbox', 'mailbox_starred'], this._currentState); + return _.contains(['mailbox_inbox', 'mailbox_starred', 'mailbox_history'], this._currentState); }, /** * @override @@ -160,7 +160,7 @@ Discuss.include({ */ _updateContent: function (type) { var self = this; - var inMailbox = type === 'mailbox_inbox' || type === 'mailbox_starred'; + var inMailbox = _.contains(['mailbox_inbox', 'mailbox_starred', 'mailbox_history'], type); if (!inMailbox && this._isInInboxTab()) { // we're leaving the inbox, so store the thread scrolltop this._storeThreadState(); @@ -230,8 +230,8 @@ Discuss.include({ // update bottom buttons self.$('.o_mail_mobile_tab').removeClass('active'); - // mailbox_inbox and mailbox_starred share the same tab - type = type === 'mailbox_starred' ? 'mailbox_inbox' : type; + // mailbox_inbox, mailbox_starred and mailbox_history share the same tab + type = _.contains(['mailbox_inbox', 'mailbox_starred', 'mailbox_history'], type) ? 'mailbox_inbox' : type; self.$('.o_mail_mobile_tab[data-type=' + type + ']').addClass('active'); }); }, diff --git a/addons/mail/static/src/js/models/messages/message.js b/addons/mail/static/src/js/models/messages/message.js index d22569461e58b46e3d3a217cec38dc5b4fa21ba1..e5470d219a2869b19863747e729fe5b0a946df7c 100644 --- a/addons/mail/static/src/js/models/messages/message.js +++ b/addons/mail/static/src/js/models/messages/message.js @@ -32,6 +32,7 @@ var Message = AbstractMessage.extend(Mixins.EventDispatcherMixin, ServicesMixin * @param {string} [data.moderation_status='accepted'] * @param {string} [data.module_icon] * @param {Array} [data.needaction_partner_ids = []] + * @param {Array} [data.history_partner_ids = []] * @param {string} [data.record_name] * @param {integer} [data.res_id] * @param {Array} [data.starred_partner_ids = []] @@ -648,6 +649,9 @@ var Message = AbstractMessage.extend(Mixins.EventDispatcherMixin, ServicesMixin if (_.contains(this._starredPartnerIDs, session.partner_id)) { this.setStarred(true); } + if (_.contains(this._historyPartnerIDs, session.partner_id)) { + this._setHistory(true); + } if ( this.originatesFromChannel() && _.contains( @@ -739,6 +743,7 @@ var Message = AbstractMessage.extend(Mixins.EventDispatcherMixin, ServicesMixin * @param {string} [data.moderation_status='accepted'] * @param {string} [data.module_icon] * @param {Array} [data.needaction_partner_ids = []] + * @param {Array} [data.history_partner_ids = []] * @param {string} [data.record_name] * @param {integer} [data.res_id] * @param {Array} [data.starred_partner_ids = []] @@ -757,6 +762,7 @@ var Message = AbstractMessage.extend(Mixins.EventDispatcherMixin, ServicesMixin this._moduleIcon = data.module_icon; this._needactionPartnerIDs = data.needaction_partner_ids || []; this._starredPartnerIDs = data.starred_partner_ids || []; + this._historyPartnerIDs = data.history_partner_ids || []; this._subject = data.subject; this._subtypeDescription = data.subtype_description; this._threadIDs = data.channel_ids || []; @@ -764,6 +770,21 @@ var Message = AbstractMessage.extend(Mixins.EventDispatcherMixin, ServicesMixin this._moderationStatus = data.moderation_status || 'accepted'; }, + /* + * Set whether the message is history or not. + * If it is history, the message is moved to the "History" mailbox. + * Note that this function only applies it locally, the server is not aware + * + * @private + * @param {boolean} history if set, the message is history + */ + _setHistory: function (history) { + if (history) { + this._addThread('mailbox_history'); + } else { + this.removeThread('mailbox_history'); + } + }, /** * Set whether the message is moderated by current user or not. * If it is moderated by the current user, the message is moved to the diff --git a/addons/mail/static/src/js/models/threads/mailbox.js b/addons/mail/static/src/js/models/threads/mailbox.js index bedd6af27e1652bb92c3cd656c7cae6e00223098..236d960c02dada2077218e65869c884c7a5657f4 100644 --- a/addons/mail/static/src/js/models/threads/mailbox.js +++ b/addons/mail/static/src/js/models/threads/mailbox.js @@ -128,7 +128,6 @@ var Mailbox = SearchableThread.extend({ model: 'mail.message', method: 'mark_all_as_read', kwargs: { - channel_ids: [], domain: domain, }, }); @@ -183,6 +182,8 @@ var Mailbox = SearchableThread.extend({ return [['needaction', '=', true]]; } else if (this._id === 'mailbox_starred') { return [['starred', '=', true]]; + } else if (this._id === 'mailbox_history') { + return [['needaction', '=', false]]; } else if (this._id === 'mailbox_moderation') { return [['need_moderation', '=', true]]; } else { diff --git a/addons/mail/static/src/js/models/threads/searchable_thread.js b/addons/mail/static/src/js/models/threads/searchable_thread.js index 511bc6348c82402ce741c2a2a0b4e00cdb94caf0..74018af1bc0fc29e9ac821193fbc4eb62730d848 100644 --- a/addons/mail/static/src/js/models/threads/searchable_thread.js +++ b/addons/mail/static/src/js/models/threads/searchable_thread.js @@ -135,11 +135,12 @@ var SearchableThread = Thread.extend({ * @override * @private * @param {mail.model.Message} message - * @param {Object} options + * @param {Object} [options={}] * @param {Array} [options.domain=[]] * @param {boolean} [options.incrementUnread=false] */ _addMessage: function (message, options) { + options = options || {}; this._super.apply(this, arguments); var cache = this._getCache(options.domain || []); var index = _.sortedIndex(cache.messages, message, function (msg) { diff --git a/addons/mail/static/src/js/services/mail_manager.js b/addons/mail/static/src/js/services/mail_manager.js index 6c7cefe42e3e6b871396697f0496052413c29620..2aa083a56d2ec9473e79233963fea3a99b5932f3 100644 --- a/addons/mail/static/src/js/services/mail_manager.js +++ b/addons/mail/static/src/js/services/mail_manager.js @@ -1257,7 +1257,7 @@ var MailManager = AbstractService.extend({ }, /** * Update the mailboxes with mail data fetched from server, namely 'Inbox', - * 'Starred', and 'Moderation Queue' if the user is a moderator of a channel + * 'Starred', 'History', and 'Moderation Queue' if the user is a moderator of a channel * * @private * @param {Object} data @@ -1281,6 +1281,10 @@ var MailManager = AbstractService.extend({ name: _t("Starred"), mailboxCounter: data.starred_counter || 0, }); + this._addMailbox({ + id: 'history', + name: _t("History"), + }); if (data.is_moderator) { this._addMailbox({ diff --git a/addons/mail/static/src/js/services/mail_notification_manager.js b/addons/mail/static/src/js/services/mail_notification_manager.js index c22276867b1e3adda41722bc1ebfaeaaf43e572d..704e114d8bd5c29be8307e1398032bb38b8c6496 100644 --- a/addons/mail/static/src/js/services/mail_notification_manager.js +++ b/addons/mail/static/src/js/services/mail_notification_manager.js @@ -319,12 +319,14 @@ MailManager.include({ */ _handlePartnerMarkAsReadNotification: function (data) { var self = this; + var history = this.getMailbox('history'); _.each(data.message_ids, function (messageID) { var message = _.find(self._messages, function (msg) { return msg.getID() === messageID; }); if (message) { self._removeMessageFromThread('mailbox_inbox', message); + history.addMessage(message); self._mailBus.trigger('update_message', message, data.type); } }); diff --git a/addons/mail/static/src/scss/thread.scss b/addons/mail/static/src/scss/thread.scss index 89e5f220ea22564dcb2f99d5f7048c807fd2e521..5654020c7836d8b89e4d81f1b98306195a7dc094 100644 --- a/addons/mail/static/src/scss/thread.scss +++ b/addons/mail/static/src/scss/thread.scss @@ -229,6 +229,12 @@ margin-bottom: 20px; font-weight: bold; font-size: 125%; + + &.o_neutral_face_icon:before { + @extend %o-nocontent-init-image; + @include size(120px, 140px); + background: transparent url(/web/static/src/img/neutral_face.svg) no-repeat center; + } } .o_mail_no_content { diff --git a/addons/mail/static/src/xml/discuss.xml b/addons/mail/static/src/xml/discuss.xml index afee3a84da20fa5bf36cb6bf476f2db3d77f3f04..71138ae1f24d489853cb185a06842da8560a1b57 100644 --- a/addons/mail/static/src/xml/discuss.xml +++ b/addons/mail/static/src/xml/discuss.xml @@ -60,6 +60,10 @@ <t t-set="counter" t-value="starred.getMailboxCounter()"/> <t t-call="mail.discuss.SidebarCounter"/> </div> + <div t-attf-class="o_mail_discuss_title_main o_mail_discuss_item #{(activeThreadID === 'mailbox_history') ? 'o_active': ''}" + data-thread-id="mailbox_history"> + <span class="o_thread_name"><i class="fa fa-history mr8"/>History</span> + </div> <div t-if="isMyselfModerator" t-attf-class="o_mail_discuss_title_main o_mail_discuss_item #{(activeThreadID == 'mailbox_moderation') ? 'o_active': ''}" data-thread-id="mailbox_moderation"> <span class="o_thread_name"> <i class="fa fa-envelope mr8"/>Moderation Queue</span> @@ -242,6 +246,9 @@ <button type="button" class="btn btn-secondary d-inline d-md-none o_mailbox_inbox_item" title="Starred" data-type="mailbox_starred"> Starred </button> + <button type="button" class="btn btn-secondary d-inline d-md-none o_mailbox_inbox_item" title="History" data-type="mailbox_history"> + History + </button> </div> <div class="o_mail_discuss_content"/> <div class="o_mail_mobile_tabs"> diff --git a/addons/mail/static/src/xml/thread.xml b/addons/mail/static/src/xml/thread.xml index dada890149106139bd8fc6c08f1b9c91b927a3bd..ef21b2c5ede7901f8b7f14811b723dc3c592a9ee 100644 --- a/addons/mail/static/src/xml/thread.xml +++ b/addons/mail/static/src/xml/thread.xml @@ -128,9 +128,13 @@ <div>New messages appear here.</div> </t> <t t-if="thread.getID() === 'mailbox_starred'"> - <div class="o_thread_title">No starred message</div> + <div class="o_thread_title">No starred messages</div> <div>You can mark any message as 'starred', and it shows up in this mailbox.</div> </t> + <t t-if="thread.getID() === 'mailbox_history'"> + <div class="o_thread_title o_neutral_face_icon">No history messages</div> + <div>Messages marked as read will appear in the history.</div> + </t> <t t-if="thread.getID() === 'mailbox_moderation'"> <div class="o_thread_title">You have no message to moderate</div> <div>Pending moderation messages appear here.</div> diff --git a/addons/mail/static/tests/discuss_tests.js b/addons/mail/static/tests/discuss_tests.js index 8c65266a392e56e8701fadebe90841f5c97376af..7473194d2b526f4222add6f49f6fc7e46c6a9042 100644 --- a/addons/mail/static/tests/discuss_tests.js +++ b/addons/mail/static/tests/discuss_tests.js @@ -50,6 +50,11 @@ QUnit.module('Discuss', { type: 'many2many', relation: 'res.partner', }, + history_partner_ids: { + string: "Partners with History", + type: 'many2many', + relation: 'res.partner', + }, model: { string: "Related Document model", type: 'char', @@ -72,6 +77,24 @@ QUnit.module('Discuss', { im_status: 'online', }] }, + 'mail.notification': { + fields: { + is_read: { + string: "Is Read", + type: 'boolean', + }, + mail_message_id: { + string: "Message", + type: 'many2one', + relation: 'mail.message', + }, + res_partner_id: { + string: "Needaction Recipient", + type: 'many2one', + relation: 'res.partner', + }, + }, + }, }; this.services = mailTestUtils.getMailServices(); }, @@ -83,7 +106,7 @@ QUnit.module('Discuss', { }); QUnit.test('basic rendering', async function (assert) { - assert.expect(5); + assert.expect(6); var discuss = await createDiscuss({ id: 1, @@ -105,11 +128,15 @@ QUnit.test('basic rendering', async function (assert) { var $inbox = $sidebar.find('.o_mail_discuss_item[data-thread-id=mailbox_inbox]'); assert.strictEqual($inbox.length, 1, - "should have the channel item 'mailbox_inbox' in the sidebar"); + "should have the mailbox item 'mailbox_inbox' in the sidebar"); var $starred = $sidebar.find('.o_mail_discuss_item[data-thread-id=mailbox_starred]'); assert.strictEqual($starred.length, 1, - "should have the channel item 'mailbox_starred' in the sidebar"); + "should have the mailbox item 'mailbox_starred' in the sidebar"); + + var $history = $sidebar.find('.o_mail_discuss_item[data-thread-id=mailbox_history]'); + assert.strictEqual($history.length, 1, + "should have the mailbox item 'mailbox_history' in the sidebar"); discuss.destroy(); }); @@ -1490,54 +1517,382 @@ QUnit.test('custom-named DM conversation', async function (assert) { discuss.destroy(); }); -QUnit.test('input not cleared on unresolved message_post rpc', async function (assert) { - assert.expect(2); - // Promise to simulate late server response on message post - var messagePostPromise = testUtils.makeTestPromise(); +QUnit.test('messages marked as read move to "History" mailbox', async function (assert) { + assert.expect(3); + + this.data['mail.message'].records = [{ + author_id: [5, 'Demo User'], + body: '<p>test 1</p>', + id: 1, + needaction: true, + needaction_partner_ids: [3], + }, { + author_id: [6, 'Test User'], + body: '<p>test 2</p>', + id: 2, + needaction: true, + needaction_partner_ids: [3], + }]; + this.data['mail.notification'].records = [{ + id: 50, + is_read: false, + mail_message_id: 1, + res_partner_id: 3, + }, { + id: 51, + is_read: false, + mail_message_id: 1, + res_partner_id: 3, + }]; this.data.initMessaging = { - channel_slots: { - channel_channel: [{ - id: 1, - channel_type: "channel", - name: "general", - }], + needaction_inbox_counter: 2, + }; + + var markAllReadDef = testUtils.makeTestPromise(); + var objectDiscuss; + + var discuss = await createDiscuss({ + id: 1, + context: {}, + params: {}, + data: this.data, + services: this.services, + session: { partner_id: 3 }, + mockRPC: function (route, args) { + if (args.method === 'mark_all_as_read') { + _.each(this.data['mail.message'].records, function (message) { + message.history_partner_ids = [3]; + message.needaction_partner_ids = []; + }); + var notificationData = { + type: 'mark_as_read', + message_ids: [1, 2], + }; + var notification = [[false, 'res.partner', 3], notificationData]; + objectDiscuss.call('bus_service', 'trigger', 'notification', [notification]); + markAllReadDef.resolve(); + return Promise.resolve(3); + } + return this._super.apply(this, arguments); }, + }) + objectDiscuss = discuss; + + var $inbox = discuss.$('.o_mail_discuss_item[data-thread-id="mailbox_inbox"]'); + var $history = discuss.$('.o_mail_discuss_item[data-thread-id="mailbox_history"]'); + + await testUtils.dom.click($history); + assert.containsOnce(discuss, '.o_mail_no_content', + "should display no content message"); + + await testUtils.dom.click($inbox); + + var $markAllReadButton = $('.o_mail_discuss_button_mark_all_read'); + testUtils.dom.click($markAllReadButton); + + await markAllReadDef; + // immediately jump to end of the fadeout animation on messages + discuss.$('.o_thread_message').stop(false, true); + assert.containsNone(discuss, '.o_thread_message', + "there should no message in inbox anymore"); + + $history = discuss.$('.o_mail_discuss_item[data-thread-id="mailbox_history"]'); + await testUtils.dom.click($history); + + assert.containsN(discuss, '.o_thread_message', 2, + "there should be two messages in History"); + + discuss.destroy(); +}); + +QUnit.test('all messages in "Inbox" in "History" after marked all as read', async function (assert) { + assert.expect(10); + + var messagesData = []; + + for (var i = 0; i < 40; i++) { + messagesData.push({ + author_id: [i, 'User ' + i], + body: '<p>test ' + i + '</p>', + id: i, + needaction: true, + needaction_partner_ids: [3], + }); + } + + this.data['mail.message'].records = messagesData; + this.data.initMessaging = { + needaction_inbox_counter: 2, }; + var messageFetchCount = 0; + var loadMoreDef = testUtils.makeTestPromise(); + var markAllReadDef = testUtils.makeTestPromise(); + var objectDiscuss; + var discuss = await createDiscuss({ id: 1, context: {}, params: {}, data: this.data, services: this.services, + session: { partner_id: 3 }, mockRPC: function (route, args) { - if (args.method === 'message_post') { - return messagePostPromise; + if (args.method === 'mark_all_as_read') { + var messageIDs = []; + for (var i = 0; i < messagesData.length; i++) { + this.data['mail.message'].records[i].history_partner_ids = [3]; + this.data['mail.message'].records[i].needaction_partner_ids = []; + this.data['mail.message'].records[i].needaction = false; + messageIDs.push(i); + } + var notificationData = { + type: 'mark_as_read', + message_ids: messageIDs, + }; + var notification = [[false, 'res.partner', 3], notificationData]; + objectDiscuss.call('bus_service', 'trigger', 'notification', [notification]); + markAllReadDef.resolve(); + return Promise.resolve(3); + } + if (args.method === 'message_fetch') { + // 1st message_fetch: 'Inbox' initially + // 2nd message_fetch: 'History' initially + // 3rd message_fetch: 'History' load more + assert.step(args.method); + + messageFetchCount++; + if (messageFetchCount === 3) { + loadMoreDef.resolve(); + } } return this._super.apply(this, arguments); }, }); + objectDiscuss = discuss; - // Click on channel 'general' - var $general = discuss.$('.o_mail_discuss_sidebar').find('.o_mail_discuss_item[data-thread-id=1]'); - await testUtils.dom.click($general); + assert.verifySteps(['message_fetch'], + "should fetch messages once for needaction messages (Inbox)"); + assert.containsN(discuss, '.o_thread_message', 30, + "there should be 30 messages that are loaded in Inbox"); - // Type message - var $input = discuss.$('textarea.o_composer_text_field').first(); - $input.focus(); - $input.val('test message'); + var $markAllReadButton = $('.o_mail_discuss_button_mark_all_read'); - // Send message - await testUtils.fields.triggerKeydown($input, 'enter'); - assert.strictEqual($input.val(), 'test message', "composer should not be cleared on send without server response"); + await testUtils.dom.click($markAllReadButton); + await markAllReadDef; + + // immediately jump to end of the fadeout animation on messages + discuss.$('.o_thread_message').stop(false, true); + assert.containsNone(discuss, '.o_thread_message', + "there should no message in inbox anymore"); + + var $history = discuss.$('.o_mail_discuss_item[data-thread-id="mailbox_history"]'); + + await testUtils.dom.click($history); + + assert.verifySteps(['message_fetch'], + "should fetch messages once for history"); + + assert.containsN(discuss, '.o_thread_message', 30, + "there should be 30 messages in History"); + + // simulate a scroll to top to load more messages + discuss.$('.o_mail_thread').scrollTop(0); + + await loadMoreDef; + await testUtils.nextTick(); + + assert.verifySteps(['message_fetch'], + "should fetch more messages in history for loadMore"); + assert.containsN(discuss, '.o_thread_message', 40, + "there should be 40 messages in History"); + + discuss.destroy(); +}); + +QUnit.test('messages marked as read move to "History" mailbox', async function (assert) { + assert.expect(3); + + this.data['mail.message'].records = [{ + author_id: [5, 'Demo User'], + body: '<p>test 1</p>', + id: 1, + needaction: true, + needaction_partner_ids: [3], + }, { + author_id: [6, 'Test User'], + body: '<p>test 2</p>', + id: 2, + needaction: true, + needaction_partner_ids: [3], + }]; + this.data['mail.notification'].records = [{ + id: 50, + is_read: false, + mail_message_id: 1, + res_partner_id: 3, + }, { + id: 51, + is_read: false, + mail_message_id: 1, + res_partner_id: 3, + }]; + + this.data.initMessaging = { + needaction_inbox_counter: 2, + }; + + const markAllReadDef = testUtils.makeTestPromise(); + let objectDiscuss; + + const discuss = await createDiscuss({ + id: 1, + context: {}, + params: {}, + data: this.data, + services: this.services, + session: { partner_id: 3 }, + mockRPC(route, args) { + if (args.method === 'mark_all_as_read') { + for (const message of (this.data['mail.message'].records)) { + message.history_partner_ids = [3]; + message.needaction_partner_ids = []; + } + const notificationData = { + type: 'mark_as_read', + message_ids: [1, 2], + }; + const notification = [[false, 'res.partner', 3], notificationData]; + objectDiscuss.call('bus_service', 'trigger', 'notification', [notification]); + markAllReadDef.resolve(); + return Promise.resolve(3); + } + return this._super.apply(this, arguments); + }, + }) + objectDiscuss = discuss; + + const $inbox = discuss.$('.o_mail_discuss_item[data-thread-id="mailbox_inbox"]'); + let $history = discuss.$('.o_mail_discuss_item[data-thread-id="mailbox_history"]'); + await testUtils.dom.click($history); + assert.containsOnce(discuss, '.o_mail_no_content', + "should display no content message"); + + await testUtils.dom.click($inbox); + var $markAllReadButton = $('.o_mail_discuss_button_mark_all_read'); + testUtils.dom.click($markAllReadButton); + await markAllReadDef; + // immediately jump to end of the fadeout animation on messages + discuss.$('.o_thread_message').stop(false, true); + assert.containsNone(discuss, '.o_thread_message', + "there should no message in inbox anymore"); + + $history = discuss.$('.o_mail_discuss_item[data-thread-id="mailbox_history"]'); + await testUtils.dom.click($history); + assert.containsN(discuss, '.o_thread_message', 2, + "there should be two messages in History"); + + discuss.destroy(); +}); + +QUnit.test('all messages in "Inbox" in "History" after marked all as read', async function (assert) { + assert.expect(10); + + const messagesData = []; + for (let i = 0; i < 40; i++) { + messagesData.push({ + author_id: [i, 'User ' + i], + body: '<p>test ' + i + '</p>', + id: i, + needaction: true, + needaction_partner_ids: [3], + }); + } + + this.data['mail.message'].records = messagesData; + this.data.initMessaging = { + needaction_inbox_counter: 2, + }; + + let messageFetchCount = 0; + const loadMoreDef = testUtils.makeTestPromise(); + const markAllReadDef = testUtils.makeTestPromise(); + let objectDiscuss; + + const discuss = await createDiscuss({ + id: 1, + context: {}, + params: {}, + data: this.data, + services: this.services, + session: { partner_id: 3 }, + mockRPC(route, args) { + if (args.method === 'mark_all_as_read') { + const messageIDs = []; + for (let i = 0; i < messagesData.length; i++) { + this.data['mail.message'].records[i].history_partner_ids = [3]; + this.data['mail.message'].records[i].needaction_partner_ids = []; + this.data['mail.message'].records[i].needaction = false; + messageIDs.push(i); + } + const notificationData = { + type: 'mark_as_read', + message_ids: messageIDs, + }; + const notification = [[false, 'res.partner', 3], notificationData]; + objectDiscuss.call('bus_service', 'trigger', 'notification', [notification]); + markAllReadDef.resolve(); + return Promise.resolve(3); + } + if (args.method === 'message_fetch') { + // 1st message_fetch: 'Inbox' initially + // 2nd message_fetch: 'History' initially + // 3rd message_fetch: 'History' load more + assert.step(args.method); + + messageFetchCount++; + if (messageFetchCount === 3) { + loadMoreDef.resolve(); + } + } + return this._super.apply(this, arguments); + }, + }); + objectDiscuss = discuss; + + assert.verifySteps(['message_fetch'], + "should fetch messages once for needaction messages (Inbox)"); + assert.containsN(discuss, '.o_thread_message', 30, + "there should be 30 messages that are loaded in Inbox"); - // Simulate server response - messagePostPromise.resolve(); + const $markAllReadButton = $('.o_mail_discuss_button_mark_all_read'); + await testUtils.dom.click($markAllReadButton); + await markAllReadDef; + // immediately jump to end of the fadeout animation on messages + discuss.$('.o_thread_message').stop(false, true); + assert.containsNone(discuss, '.o_thread_message', + "there should no message in inbox anymore"); + + const $history = discuss.$('.o_mail_discuss_item[data-thread-id="mailbox_history"]'); + await testUtils.dom.click($history); + assert.verifySteps(['message_fetch'], + "should fetch messages once for history"); + assert.containsN(discuss, '.o_thread_message', 30, + "there should be 30 messages in History"); + + // simulate a scroll to top to load more messages + discuss.$('.o_mail_thread').scrollTop(0); + await loadMoreDef; await testUtils.nextTick(); - assert.strictEqual($input.val(), '', "composer should be cleared on send after server response"); + assert.verifySteps(['message_fetch'], + "should fetch more messages in history for loadMore"); + assert.containsN(discuss, '.o_thread_message', 40, + "there should be 40 messages in History"); + discuss.destroy(); }); + }); }); diff --git a/addons/test_mail/tests/common.py b/addons/test_mail/tests/common.py index 752c489cadb2a9f0ce6adaa6bfbfa51e7628c665..d3c41a16ab1677220c26f66656121cfcd3fe628e 100644 --- a/addons/test_mail/tests/common.py +++ b/addons/test_mail/tests/common.py @@ -73,7 +73,7 @@ class BaseFunctionalTest(common.SavepointCase): for partner_attribute in counters.keys(): counter, notif_type, notif_read = counters[partner_attribute] partner = getattr(self, partner_attribute) - partner_notif = new_notifications.filtered(lambda n: n.res_partner_id == partner) + partner_notif = new_notifications.filtered(lambda n: n.res_partner_id == partner and (n.is_read == (notif_read not in ['unread', '']))) self.assertEqual(len(partner_notif), counter) diff --git a/addons/test_mail/tests/test_mail_message.py b/addons/test_mail/tests/test_mail_message.py index 07116885592f93f0cf5ce67fc72f2763e9e36e15..d4181f5b3349233af025399f6121e79eac644a10 100644 --- a/addons/test_mail/tests/test_mail_message.py +++ b/addons/test_mail/tests/test_mail_message.py @@ -336,7 +336,7 @@ class TestMessageAccess(common.BaseFunctionalTest, common.MockEmails): # mark all as read clear needactions group_private.message_post(body='Test', message_type='comment', subtype='mail.mt_comment', partner_ids=[emp_partner.id]) - emp_partner.env['mail.message'].mark_all_as_read(channel_ids=[], domain=[]) + emp_partner.env['mail.message'].mark_all_as_read(domain=[]) na_count = emp_partner.get_needaction_count() self.assertEqual(na_count, 0, "mark all as read should conclude all needactions") @@ -353,7 +353,7 @@ class TestMessageAccess(common.BaseFunctionalTest, common.MockEmails): na_count = emp_partner.get_needaction_count() self.assertEqual(na_count, 1, "message not accessible is currently still counted") - emp_partner.env['mail.message'].mark_all_as_read(channel_ids=[], domain=[]) + emp_partner.env['mail.message'].mark_all_as_read(domain=[]) na_count = emp_partner.get_needaction_count() self.assertEqual(na_count, 0, "mark all read should conclude all needactions even inacessible ones") @@ -364,7 +364,7 @@ class TestMessageAccess(common.BaseFunctionalTest, common.MockEmails): # mark all as read clear needactions self.group_pigs.message_post(body='Test', message_type='comment', subtype='mail.mt_comment', partner_ids=[portal_partner.id]) - portal_partner.env['mail.message'].mark_all_as_read(channel_ids=[], domain=[]) + portal_partner.env['mail.message'].mark_all_as_read(domain=[]) na_count = portal_partner.get_needaction_count() self.assertEqual(na_count, 0, "mark all as read should conclude all needactions") @@ -380,7 +380,7 @@ class TestMessageAccess(common.BaseFunctionalTest, common.MockEmails): na_count = portal_partner.get_needaction_count() self.assertEqual(na_count, 1, "message not accessible is currently still counted") - portal_partner.env['mail.message'].mark_all_as_read(channel_ids=[], domain=[]) + portal_partner.env['mail.message'].mark_all_as_read(domain=[]) na_count = portal_partner.get_needaction_count() self.assertEqual(na_count, 0, "mark all read should conclude all needactions even inacessible ones")