[FIX] mail: prevent multiple send of message by spamming send button

task-2388702

closes odoo/odoo#62123

X-original-commit: 909b66883c1085c3eba59452fd89f1328948acab
Signed-off-by: Sébastien Theys (seb) <seb@odoo.com>
This commit is contained in:
Xavier Dubuc
2020-11-20 18:18:11 +00:00
committed by Sébastien Theys
parent a37c11e113
commit 2cf0390100
2 changed files with 127 additions and 47 deletions
@@ -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"
);
});
});
});
});
@@ -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: [