From 2cf03901006dca55fd7914058a0c5aeee39ea0f1 Mon Sep 17 00:00:00 2001 From: Xavier Dubuc Date: Fri, 20 Nov 2020 18:18:11 +0000 Subject: [PATCH] [FIX] mail: prevent multiple send of message by spamming send button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit task-2388702 closes odoo/odoo#62123 X-original-commit: 909b66883c1085c3eba59452fd89f1328948acab Signed-off-by: Sébastien Theys (seb) --- .../src/components/composer/composer_tests.js | 70 ++++++++++++ .../static/src/models/composer/composer.js | 104 ++++++++++-------- 2 files changed, 127 insertions(+), 47 deletions(-) diff --git a/addons/mail/static/src/components/composer/composer_tests.js b/addons/mail/static/src/components/composer/composer_tests.js index a149a5b9770..0b21c908dda 100644 --- a/addons/mail/static/src/components/composer/composer_tests.js +++ b/addons/mail/static/src/components/composer/composer_tests.js @@ -2029,6 +2029,76 @@ QUnit.test("Show a thread name in the recipient status text.", async function (a ); }); +QUnit.test('send message only once when button send is clicked twice quickly', async function (assert) { + assert.expect(2); + + this.data['mail.channel'].records.push({ id: 20 }); + await this.start({ + async mockRPC(route, args) { + if (args.method === 'message_post') { + assert.step('message_post'); + } + return this._super(...arguments); + }, + }); + const thread = this.env.models['mail.thread'].find(thread => + thread.id === 20 && + thread.model === 'mail.channel' + ); + await this.createComposerComponent(thread.composer); + // Type message + await afterNextRender(() => { + document.querySelector(`.o_ComposerTextInput_textarea`).focus(); + document.execCommand('insertText', false, "test message"); + }); + + await afterNextRender(() => { + document.querySelector(`.o_Composer_buttonSend`).click(); + document.querySelector(`.o_Composer_buttonSend`).click(); + }); + assert.verifySteps( + ['message_post'], + "The message has been posted only once" + ); +}); + +QUnit.test('send message only once when enter is pressed twice quickly', async function (assert) { + assert.expect(2); + + this.data['mail.channel'].records.push({ id: 20 }); + await this.start({ + async mockRPC(route, args) { + if (args.method === 'message_post') { + assert.step('message_post'); + } + return this._super(...arguments); + }, + }); + const thread = this.env.models['mail.thread'].find(thread => + thread.id === 20 && + thread.model === 'mail.channel' + ); + await this.createComposerComponent(thread.composer, { + textInputSendShortcuts: ['enter'], + }); + // Type message + await afterNextRender(() => { + document.querySelector(`.o_ComposerTextInput_textarea`).focus(); + document.execCommand('insertText', false, "test message"); + }); + await afterNextRender(() => { + const enterEvent = new window.KeyboardEvent('keydown', { key: 'Enter' }); + document.querySelector(`.o_ComposerTextInput_textarea`) + .dispatchEvent(enterEvent); + document.querySelector(`.o_ComposerTextInput_textarea`) + .dispatchEvent(enterEvent); + }); + assert.verifySteps( + ['message_post'], + "The message has been posted only once" + ); +}); + }); }); }); diff --git a/addons/mail/static/src/models/composer/composer.js b/addons/mail/static/src/models/composer/composer.js index a5e37ec7da4..8bedfa4e3d7 100644 --- a/addons/mail/static/src/models/composer/composer.js +++ b/addons/mail/static/src/models/composer/composer.js @@ -265,19 +265,33 @@ function factory(dependencies) { if (this.subjectContent) { postData.subject = this.subjectContent; } - let messageId; - if (thread.model === 'mail.channel') { - const command = this._getCommandFromText(body); - Object.assign(postData, { - subtype_xmlid: 'mail.mt_comment', - }); - if (command) { - messageId = await this.async(() => this.env.models['mail.thread'].performRpcExecuteCommand({ - channelId: thread.id, - command: command.name, - postData, - })); + try { + let messageId; + this.update({ isPostingMessage: true }); + if (thread.model === 'mail.channel') { + const command = this._getCommandFromText(body); + Object.assign(postData, { + subtype_xmlid: 'mail.mt_comment', + }); + if (command) { + messageId = await this.async(() => this.env.models['mail.thread'].performRpcExecuteCommand({ + channelId: thread.id, + command: command.name, + postData, + })); + } else { + messageId = await this.async(() => + this.env.models['mail.thread'].performRpcMessagePost({ + postData, + threadId: thread.id, + threadModel: thread.model, + }) + ); + } } else { + Object.assign(postData, { + subtype_xmlid: this.isLog ? 'mail.mt_note' : 'mail.mt_comment', + }); messageId = await this.async(() => this.env.models['mail.thread'].performRpcMessagePost({ postData, @@ -285,42 +299,33 @@ function factory(dependencies) { threadModel: thread.model, }) ); + const [messageData] = await this.async(() => this.env.services.rpc({ + model: 'mail.message', + method: 'message_format', + args: [[messageId]], + }, { shadow: true })); + this.env.models['mail.message'].insert(Object.assign( + {}, + this.env.models['mail.message'].convertData(messageData), + { + originThread: [['insert', { + id: thread.id, + model: thread.model, + }]], + }) + ); + thread.loadNewMessages(); } - } else { - Object.assign(postData, { - subtype_xmlid: this.isLog ? 'mail.mt_note' : 'mail.mt_comment', - }); - messageId = await this.async(() => - this.env.models['mail.thread'].performRpcMessagePost({ - postData, - threadId: thread.id, - threadModel: thread.model, - }) - ); - const [messageData] = await this.async(() => this.env.services.rpc({ - model: 'mail.message', - method: 'message_format', - args: [[messageId]], - }, { shadow: true })); - this.env.models['mail.message'].insert(Object.assign( - {}, - this.env.models['mail.message'].convertData(messageData), - { - originThread: [['insert', { - id: thread.id, - model: thread.model, - }]], - }) - ); - thread.loadNewMessages(); + for (const threadView of this.thread.threadViews) { + // Reset auto scroll to be able to see the newly posted message. + threadView.update({ hasAutoScrollOnMessageReceived: true }); + } + thread.refreshFollowers(); + thread.fetchAndUpdateSuggestedRecipients(); + this._reset(); + } finally { + this.update({ isPostingMessage: false }); } - for (const threadView of this.thread.threadViews) { - // Reset auto scroll to be able to see the newly posted message. - threadView.update({ hasAutoScrollOnMessageReceived: true }); - } - thread.refreshFollowers(); - thread.fetchAndUpdateSuggestedRecipients(); - this._reset(); } /** @@ -449,7 +454,7 @@ function factory(dependencies) { if (!this.textInputContent && this.attachments.length === 0) { return false; } - return !this.hasUploadingAttachment; + return !this.hasUploadingAttachment && !this.isPostingMessage; } /** @@ -874,6 +879,7 @@ function factory(dependencies) { dependencies: [ 'attachments', 'hasUploadingAttachment', + 'isPostingMessage', 'textInputContent', ], default: false, @@ -947,6 +953,10 @@ function factory(dependencies) { isLog: attr({ default: false, }), + /** + * Determines whether a post_message request is currently pending. + */ + isPostingMessage: attr(), mainSuggestedRecordsList: attr({ compute: '_computeMainSuggestedRecordsList', dependencies: [