diff options
| author | Sébastien Theys <seb@odoo.com> | 2020-06-26 09:32:32 +0000 |
|---|---|---|
| committer | Sébastien Theys <seb@odoo.com> | 2020-06-26 11:21:35 +0000 |
| commit | 1c058b72d83da3be1068961305577b0ad2ca1a2a (patch) | |
| tree | c6bae7618cbe91424c4174fca2e9da39610f4124 /addons/mail/static/src | |
| parent | dc011d12463df07ac75931a5ad332aac12c020a4 (diff) | |
[FIX] mail: fix (inbox reply) composer event handlers (and discard)
task-2092950
closes odoo/odoo#53731
X-original-commit: c29f5776594c4d2278ce163a78d8eb917fb672c0
Signed-off-by: Sébastien Theys (seb) <seb@odoo.com>
Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
Diffstat (limited to 'addons/mail/static/src')
16 files changed, 577 insertions, 68 deletions
diff --git a/addons/mail/static/src/components/chat_window/chat_window.js b/addons/mail/static/src/components/chat_window/chat_window.js index 8d1851c78fc..24801e5f301 100644 --- a/addons/mail/static/src/components/chat_window/chat_window.js +++ b/addons/mail/static/src/components/chat_window/chat_window.js @@ -7,6 +7,7 @@ const components = { ThreadViewer: require('mail/static/src/components/thread_viewer/thread_viewer.js'), }; const useStore = require('mail/static/src/component_hooks/use_store/use_store.js'); +const { isEventHandled } = require('mail/static/src/utils/utils.js'); const { Component } = owl; const { useRef } = owl.hooks; @@ -250,6 +251,10 @@ class ChatWindow extends Component { */ _onFocusinThread(ev) { ev.stopPropagation(); + if (!this.chatWindow) { + // prevent crash on destroy + return; + } this.chatWindow.update({ isFocused: true }); } @@ -293,6 +298,12 @@ class ChatWindow extends Component { } break; case 'Escape': + if (isEventHandled(ev, 'ComposerTextInput.closeMentionSuggestions')) { + break; + } + if (isEventHandled(ev, 'Composer.closeEmojisPopover')) { + break; + } ev.preventDefault(); this.chatWindow.close(); break; diff --git a/addons/mail/static/src/components/chat_window_manager/chat_window_manager_tests.js b/addons/mail/static/src/components/chat_window_manager/chat_window_manager_tests.js index 1c51a669312..5bd516c5214 100644 --- a/addons/mail/static/src/components/chat_window_manager/chat_window_manager_tests.js +++ b/addons/mail/static/src/components/chat_window_manager/chat_window_manager_tests.js @@ -527,8 +527,13 @@ QUnit.test('chat window: open / close', async function (assert) { }); QUnit.test('chat window: close on ESCAPE', async function (assert) { - assert.expect(4); + assert.expect(12); + this.data['res.partner'].records = [{ + email: "testpartnert@odoo.com", + id: 11, + name: "TestPartner", + }]; Object.assign(this.data.initMessaging, { channel_slots: { channel_channel: [{ @@ -569,12 +574,73 @@ QUnit.test('chat window: close on ESCAPE', async function (assert) { document.querySelector(`.o_MessagingMenu_dropdownMenu .o_NotificationList_preview`).click() ); assert.verifySteps(['rpc:channel_fold/open']); + assert.containsOnce( + document.body, + '.o_ChatWindow', + "chat window should be opened after click on preview" + ); + + await afterNextRender(() => + document.querySelector(`.o_Composer_buttonEmojis`).click() + ); + assert.containsOnce( + document.body, + '.o_EmojisPopover', + "emojis popover should be opened after click on emojis button" + ); + + await afterNextRender(() => { + const ev = new window.KeyboardEvent('keydown', { bubbles: true, key: "Escape" }); + document.querySelector(`.o_Composer_buttonEmojis`).dispatchEvent(ev); + }); + assert.containsNone( + document.body, + '.o_EmojisPopover', + "emojis popover should be closed after pressing escape on emojis button" + ); + assert.containsOnce( + document.body, + '.o_ChatWindow', + "chat window should still be opened after pressing escape on emojis button" + ); await afterNextRender(() => { - document.querySelector(`.o_ChatWindow`).focus(); - const kevt = new window.KeyboardEvent('keydown', { bubbles: true, key: "Escape" }); - document.querySelector(`.o_ChatWindow`).dispatchEvent(kevt); + document.execCommand('insertText', false, "@"); + document.querySelector(`.o_ComposerTextInput_textarea`) + .dispatchEvent(new window.KeyboardEvent('keydown')); + document.querySelector(`.o_ComposerTextInput_textarea`) + .dispatchEvent(new window.KeyboardEvent('keyup')); }); + assert.containsOnce( + document.body, + '.o_ComposerTextInput_mentionDropdownPropositionList', + "mention suggestion should be opened after typing @" + ); + + await afterNextRender(() => { + const ev = new window.KeyboardEvent('keydown', { bubbles: true, key: "Escape" }); + document.querySelector(`.o_ComposerTextInput_textarea`).dispatchEvent(ev); + }); + assert.containsNone( + document.body, + '.o_ComposerTextInput_mentionDropdownPropositionList', + "mention suggestion should be closed after pressing escape on mention suggestion" + ); + assert.containsOnce( + document.body, + '.o_ChatWindow', + "chat window should still be opened after pressing escape on mention suggestion" + ); + + await afterNextRender(() => { + const ev = new window.KeyboardEvent('keydown', { bubbles: true, key: "Escape" }); + document.querySelector(`.o_ComposerTextInput_textarea`).dispatchEvent(ev); + }); + assert.containsNone( + document.body, + '.o_ChatWindow', + "chat window should be closed after pressing escape if there was no other priority escape handler" + ); assert.verifySteps(['rpc:channel_fold/closed']); }); diff --git a/addons/mail/static/src/components/composer/composer.js b/addons/mail/static/src/components/composer/composer.js index 3fd82216b05..a0fe1490bc7 100644 --- a/addons/mail/static/src/components/composer/composer.js +++ b/addons/mail/static/src/components/composer/composer.js @@ -11,6 +11,10 @@ const components = { }; const useDragVisibleDropZone = require('mail/static/src/component_hooks/use_drag_visible_dropzone/use_drag_visible_dropzone.js'); const useStore = require('mail/static/src/component_hooks/use_store/use_store.js'); +const { + isEventHandled, + markEventHandled, +} = require('mail/static/src/utils/utils.js'); const mailUtils = require('mail.utils'); const { Component } = owl; @@ -35,8 +39,8 @@ class Composer extends Component { }; }); /** - * Reference of the emoji button. Useful to include emoji popover as - * click "inside" the composer for the prop `isDiscardOnClickAway`. + * Reference of the emoji popover. Useful to include emoji popover as + * contained "inside" the composer. */ this._emojisPopoverRef = useRef('emojisPopover'); /** @@ -48,6 +52,7 @@ class Composer extends Component { * Reference of the text input component. */ this._textInputRef = useRef('textInput'); + this._onClickCaptureGlobal = this._onClickCaptureGlobal.bind(this); } @@ -71,6 +76,22 @@ class Composer extends Component { } /** + * Returns whether the given node is self or a children of self, including + * the emoji popover. + * + * @param {Node} node + * @returns {boolean} + */ + contains(node) { + // emoji popover is outside but should be considered inside + const emojisPopover = this._emojisPopoverRef.comp; + if (emojisPopover && emojisPopover.contains(node)) { + return true; + } + return this.el.contains(node); + } + + /** * Get the current partner image URL. * * @returns {string} @@ -172,23 +193,13 @@ class Composer extends Component { } /** - * Discards the composer when clicking away from the Inbox reply in discuss. - * TODO maybe move this in discuss. task-2092950 + * Discards the composer when clicking away. * * @private * @param {MouseEvent} ev */ _onClickCaptureGlobal(ev) { - // in discuss app: prevents discarding the composer when clicking away - if (!this.props.isDiscardOnClickAway) { - return; - } - // prevents discarding when clicking on self (on discuss and not on discuss) - if (this.el.contains(ev.target)) { - return; - } - // emoji popover is outside but should be considered inside - if (this._emojisPopoverRef.comp.isInsideEventTarget(ev.target)) { + if (this.contains(ev.target)) { return; } this.composer.discard(); @@ -282,6 +293,37 @@ class Composer extends Component { /** * @private + * @param {KeyboardEvent} ev + */ + _onKeydown(ev) { + if (ev.key === 'Escape') { + if (isEventHandled(ev, 'ComposerTextInput.closeMentionSuggestions')) { + return; + } + if (isEventHandled(ev, 'Composer.closeEmojisPopover')) { + return; + } + ev.preventDefault(); + this.composer.discard(); + } + } + + /** + * @private + * @param {KeyboardEvent} ev + */ + _onKeydownEmojiButton(ev) { + if (ev.key === 'Escape') { + if (this._emojisPopoverRef.comp) { + this._emojisPopoverRef.comp.close(); + this.composer.focus(); + markEventHandled(ev, 'Composer.closeEmojisPopover'); + } + } + } + + /** + * @private * @param {CustomEvent} ev */ async _onPasteTextInput(ev) { @@ -315,7 +357,6 @@ Object.assign(Composer, { hasThreadName: false, hasThreadTyping: false, isCompact: true, - isDiscardOnClickAway: false, isExpandable: false, }, props: { @@ -357,7 +398,6 @@ Object.assign(Composer, { optional: true, }, isCompact: Boolean, - isDiscardOnClickAway: Boolean, isExpandable: Boolean, }, template: 'mail.Composer', diff --git a/addons/mail/static/src/components/composer/composer.xml b/addons/mail/static/src/components/composer/composer.xml index bd14aed503a..4a1c20ca9f2 100644 --- a/addons/mail/static/src/components/composer/composer.xml +++ b/addons/mail/static/src/components/composer/composer.xml @@ -10,6 +10,7 @@ 'o-has-header': hasHeader, 'o-is-compact': props.isCompact, }" + t-on-keydown="_onKeydown" > <t t-if="composer"> <t t-if="isDropZoneVisible.value"> @@ -77,18 +78,19 @@ <div class="o_Composer_toolButtonSeparator"/> </t> <div class="o_Composer_primaryToolButtons" t-att-class="{ 'o-composer-is-compact': props.isCompact }"> - <Popover position="'top'" t-on-o-emoji-selection="_onEmojiSelection" t-ref="emojisPopover"> + <Popover position="'top'" t-on-o-emoji-selection="_onEmojiSelection"> <!-- TODO FIXME o-open not possible to code due to https://github.com/odoo/owl/issues/693 --> <button class="o_Composer_button o_Composer_buttonEmojis o_Composer_toolButton btn btn-light" t-att-class="{ 'o-open': false and state.displayed, 'o-mobile': env.messaging.device.isMobile, }" + t-on-keydown="_onKeydownEmojiButton" > <i class="fa fa-smile-o"/> </button> <t t-set="opened"> - <EmojisPopover/> + <EmojisPopover t-ref="emojisPopover"/> </t> </Popover> <button class="o_Composer_button o_Composer_buttonAttachment o_Composer_toolButton btn btn-light fa fa-paperclip" t-att-class="{ 'o-mobile': env.messaging.device.isMobile }" title="Add attachment" type="button" t-on-click="_onClickAddAttachment"/> diff --git a/addons/mail/static/src/components/composer/composer_tests.js b/addons/mail/static/src/components/composer/composer_tests.js index 0cd79c1a9fa..0b9f67fdc51 100644 --- a/addons/mail/static/src/components/composer/composer_tests.js +++ b/addons/mail/static/src/components/composer/composer_tests.js @@ -266,7 +266,7 @@ QUnit.test('display partner mention suggestions on typing "@"', async function ( this.data['res.users'].records = [{ partner_id: 11, }]; - await this.start({}); + await this.start(); const composer = this.env.models['mail.composer'].create(); await this.createComposerComponent(composer); diff --git a/addons/mail/static/src/components/composer_text_input/composer_text_input.js b/addons/mail/static/src/components/composer_text_input/composer_text_input.js index 279d5a6ae93..2ff9f9f6a89 100644 --- a/addons/mail/static/src/components/composer_text_input/composer_text_input.js +++ b/addons/mail/static/src/components/composer_text_input/composer_text_input.js @@ -6,6 +6,7 @@ const useStore = require('mail/static/src/component_hooks/use_store/use_store.js const components = { PartnerMentionSuggestion: require('mail/static/src/components/partner_mention_suggestion/partner_mention_suggestion.js'), }; +const { markEventHandled } = require('mail/static/src/utils/utils.js'); const { Component } = owl; const { useRef } = owl.hooks; @@ -206,6 +207,13 @@ class ComposerTextInput extends Component { */ _onKeydownTextarea(ev) { switch (ev.key) { + case 'Escape': + if (this.composer.hasSuggestedPartners) { + ev.preventDefault(); + this.composer.closeMentionSuggestions(); + markEventHandled(ev, 'ComposerTextInput.closeMentionSuggestions'); + } + break; // UP, DOWN, TAB: prevent moving cursor if navigation in mention suggestions case 'ArrowUp': case 'PageUp': @@ -256,9 +264,8 @@ class ComposerTextInput extends Component { */ _onKeyupTextarea(ev) { switch (ev.key) { - // ESCAPE: close mention suggestions case 'Escape': - this._onKeyupTextareaEscape(ev); + // already handled in _onKeydownTextarea, break to avoid default break; // ENTER, HOME, END, UP, DOWN, PAGE UP, PAGE DOWN, TAB: check if navigation in mention suggestions case 'Enter': @@ -310,21 +317,6 @@ class ComposerTextInput extends Component { /** * @private - * @param {KeyboardEvent} ev - */ - _onKeyupTextareaEscape(ev) { - if (this.composer.hasSuggestedPartners) { - this.composer.closeMentionSuggestions(); - } else { - if (!this._isEmpty()) { - return; - } - this.composer.discard(); - } - } - - /** - * @private * @param {Event} ev */ _onPartnerMentionSuggestionClicked(ev) { diff --git a/addons/mail/static/src/components/discuss/discuss.xml b/addons/mail/static/src/components/discuss/discuss.xml index f20f08b409b..f682278199c 100644 --- a/addons/mail/static/src/components/discuss/discuss.xml +++ b/addons/mail/static/src/components/discuss/discuss.xml @@ -96,7 +96,6 @@ hasCurrentPartnerAvatar="!env.messaging.device.isMobile" hasDiscardButton="true" hasThreadName="true" - isDiscardOnClickAway="true" t-on-o-message-posted="_onReplyingToMessageMessagePosted" /> </t> diff --git a/addons/mail/static/src/components/discuss/tests/discuss_inbox_tests.js b/addons/mail/static/src/components/discuss/tests/discuss_inbox_tests.js new file mode 100644 index 00000000000..49203d7fb8b --- /dev/null +++ b/addons/mail/static/src/components/discuss/tests/discuss_inbox_tests.js @@ -0,0 +1,300 @@ +odoo.define('mail/static/src/components/discuss/tests/discuss_inbox_tests.js', function (require) { +'use strict'; + +const { + afterEach: utilsAfterEach, + afterNextRender, + beforeEach: utilsBeforeEach, + nextAnimationFrame, + start: utilsStart, +} = require('mail/static/src/utils/test_utils.js'); + + +QUnit.module('mail', {}, function () { +QUnit.module('components', {}, function () { +QUnit.module('discuss', {}, function () { +QUnit.module('discuss_inbox_tests.js', { + beforeEach() { + utilsBeforeEach(this); + + this.start = async params => { + let { env, widget } = await utilsStart(Object.assign({}, params, { + autoOpenDiscuss: true, + data: this.data, + hasDiscuss: true, + })); + this.env = env; + this.widget = widget; + }; + }, + afterEach() { + if (this.widget) { + this.widget.destroy(); + } + utilsAfterEach(this); + }, +}); + +QUnit.test('reply: discard on pressing escape', async function (assert) { + assert.expect(9); + + this.data['res.partner'].records = [{ + email: "testpartnert@odoo.com", + id: 11, + name: "TestPartner", + }]; + this.data['mail.message'].records = [{ + author_id: [7, "Demo"], + body: "<p>Test</p>", + date: "2019-04-20 11:00:00", + id: 100, + message_type: 'comment', + needaction: true, + needaction_partner_ids: [3], + model: 'project.task', + record_name: 'Refactoring', + res_id: 20, + }]; + await this.start(); + assert.containsOnce( + document.body, + '.o_Message', + "should display a single message" + ); + + await afterNextRender(() => + document.querySelector('.o_Message_commandReply').click() + ); + assert.containsOnce( + document.body, + '.o_Composer', + "should have composer after clicking on reply to message" + ); + + await afterNextRender(() => + document.querySelector(`.o_Composer_buttonEmojis`).click() + ); + assert.containsOnce( + document.body, + '.o_EmojisPopover', + "emojis popover should be opened after click on emojis button" + ); + + await afterNextRender(() => { + const ev = new window.KeyboardEvent('keydown', { bubbles: true, key: "Escape" }); + document.querySelector(`.o_Composer_buttonEmojis`).dispatchEvent(ev); + }); + assert.containsNone( + document.body, + '.o_EmojisPopover', + "emojis popover should be closed after pressing escape on emojis button" + ); + assert.containsOnce( + document.body, + '.o_Composer', + "reply composer should still be opened after pressing escape on emojis button" + ); + + await afterNextRender(() => { + document.execCommand('insertText', false, "@"); + document.querySelector(`.o_ComposerTextInput_textarea`) + .dispatchEvent(new window.KeyboardEvent('keydown')); + document.querySelector(`.o_ComposerTextInput_textarea`) + .dispatchEvent(new window.KeyboardEvent('keyup')); + }); + assert.containsOnce( + document.body, + '.o_ComposerTextInput_mentionDropdownPropositionList', + "mention suggestion should be opened after typing @" + ); + + await afterNextRender(() => { + const ev = new window.KeyboardEvent('keydown', { bubbles: true, key: "Escape" }); + document.querySelector(`.o_ComposerTextInput_textarea`).dispatchEvent(ev); + }); + assert.containsNone( + document.body, + '.o_ComposerTextInput_mentionDropdownPropositionList', + "mention suggestion should be closed after pressing escape on mention suggestion" + ); + assert.containsOnce( + document.body, + '.o_Composer', + "reply composer should still be opened after pressing escape on mention suggestion" + ); + + await afterNextRender(() => { + const ev = new window.KeyboardEvent('keydown', { bubbles: true, key: "Escape" }); + document.querySelector(`.o_ComposerTextInput_textarea`).dispatchEvent(ev); + }); + assert.containsNone( + document.body, + '.o_Composer', + "reply composer should be closed after pressing escape if there was no other priority escape handler" + ); +}); + +QUnit.test('reply: discard on discard button click', async function (assert) { + assert.expect(4); + + this.data['mail.message'].records = [{ + author_id: [7, "Demo"], + body: "<p>Test</p>", + date: "2019-04-20 11:00:00", + id: 100, + message_type: 'comment', + needaction: true, + needaction_partner_ids: [3], + model: 'project.task', + record_name: 'Refactoring', + res_id: 20, + }]; + await this.start(); + assert.containsOnce( + document.body, + '.o_Message', + "should display a single message" + ); + + await afterNextRender(() => + document.querySelector('.o_Message_commandReply').click() + ); + assert.containsOnce( + document.body, + '.o_Composer', + "should have composer after clicking on reply to message" + ); + assert.containsOnce( + document.body, + '.o_Composer_buttonDiscard', + "composer should have a discard button" + ); + + await afterNextRender(() => + document.querySelector(`.o_Composer_buttonDiscard`).click() + ); + assert.containsNone( + document.body, + '.o_Composer', + "reply composer should be closed after clicking on discard" + ); +}); + +QUnit.test('reply: discard on reply button toggle', async function (assert) { + assert.expect(3); + + this.data['mail.message'].records = [{ + author_id: [7, "Demo"], + body: "<p>Test</p>", + date: "2019-04-20 11:00:00", + id: 100, + message_type: 'comment', + needaction: true, + needaction_partner_ids: [3], + model: 'project.task', + record_name: 'Refactoring', + res_id: 20, + }]; + await this.start(); + assert.containsOnce( + document.body, + '.o_Message', + "should display a single message" + ); + + await afterNextRender(() => + document.querySelector('.o_Message_commandReply').click() + ); + assert.containsOnce( + document.body, + '.o_Composer', + "should have composer after clicking on reply to message" + ); + + await afterNextRender(() => + document.querySelector(`.o_Message_commandReply`).click() + ); + assert.containsNone( + document.body, + '.o_Composer', + "reply composer should be closed after clicking on reply button again" + ); +}); + +QUnit.test('reply: discard on click away', async function (assert) { + assert.expect(7); + + this.data['mail.message'].records = [{ + author_id: [7, "Demo"], + body: "<p>Test</p>", + date: "2019-04-20 11:00:00", + id: 100, + message_type: 'comment', + needaction: true, + needaction_partner_ids: [3], + model: 'project.task', + record_name: 'Refactoring', + res_id: 20, + }]; + await this.start(); + assert.containsOnce( + document.body, + '.o_Message', + "should display a single message" + ); + + await afterNextRender(() => + document.querySelector('.o_Message_commandReply').click() + ); + assert.containsOnce( + document.body, + '.o_Composer', + "should have composer after clicking on reply to message" + ); + + document.querySelector(`.o_ComposerTextInput_textarea`).click(); + await nextAnimationFrame(); // wait just in case, but nothing is supposed to happen + assert.containsOnce( + document.body, + '.o_Composer', + "reply composer should still be there after clicking inside itself" + ); + + await afterNextRender(() => + document.querySelector(`.o_Composer_buttonEmojis`).click() + ); + assert.containsOnce( + document.body, + '.o_EmojisPopover', + "emojis popover should be opened after clicking on emojis button" + ); + + await afterNextRender(() => { + document.querySelector(`.o_EmojisPopover_emoji`).click(); + }); + assert.containsNone( + document.body, + '.o_EmojisPopover', + "emojis popover should be closed after selecting an emoji" + ); + assert.containsOnce( + document.body, + '.o_Composer', + "reply composer should still be there after selecting an emoji (even though it is technically a click away, it should be considered inside)" + ); + + await afterNextRender(() => + document.querySelector(`.o_Message`).click() + ); + assert.containsNone( + document.body, + '.o_Composer', + "reply composer should be closed after clicking away" + ); +}); + +}); +}); +}); + +}); diff --git a/addons/mail/static/src/components/emojis_popover/emojis_popover.js b/addons/mail/static/src/components/emojis_popover/emojis_popover.js index 2e8ee6c6ae8..6d3798d0774 100644 --- a/addons/mail/static/src/components/emojis_popover/emojis_popover.js +++ b/addons/mail/static/src/components/emojis_popover/emojis_popover.js @@ -16,6 +16,27 @@ class EmojisPopover extends Component { } //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + close() { + this.trigger('o-popover-close'); + } + + /** + * Returns whether the given node is self or a children of self. + * + * @param {Node} node + * @returns {boolean} + */ + contains(node) { + if (!this.el) { + return false; + } + return this.el.contains(node); + } + + //-------------------------------------------------------------------------- // Handlers //-------------------------------------------------------------------------- @@ -24,7 +45,7 @@ class EmojisPopover extends Component { * @param {MouseEvent} ev */ _onClickEmoji(ev) { - this.trigger('o-popover-close'); + this.close(); this.trigger('o-emoji-selection', { unicode: ev.currentTarget.dataset.unicode, }); diff --git a/addons/mail/static/src/components/message/message.js b/addons/mail/static/src/components/message/message.js index 091c47f0090..1881b614d2c 100644 --- a/addons/mail/static/src/components/message/message.js +++ b/addons/mail/static/src/components/message/message.js @@ -79,6 +79,10 @@ class Message extends Component { }, }); /** + * The intent of the reply button depends on the last rendered state. + */ + this._wasSelected; + /** * Reference to the content of the message. */ this._contentRef = useRef('content'); @@ -399,6 +403,7 @@ class Message extends Component { * @private */ _update() { + this._wasSelected = this.props.isSelected; if (!this.state.timeElapsed) { this.state.timeElapsed = timeFromNow(this.message.date); } @@ -549,8 +554,15 @@ class Message extends Component { * @param {MouseEvent} ev */ _onClickReply(ev) { - ev.stopPropagation(); - this.message.replyTo(); + // Use this._wasSelected because this.props.isSelected might be changed + // by a global capture click handler (for example the one from Composer) + // before the current handler is executed. Indeed because it does a + // toggle it needs to take into account the value before the click. + if (this._wasSelected) { + this.env.messaging.discuss.clearReplyingToMessage(); + } else { + this.message.replyTo(); + } } /** diff --git a/addons/mail/static/src/components/thread_viewer/thread_viewer.js b/addons/mail/static/src/components/thread_viewer/thread_viewer.js index 26fc0d88f08..a21aa54efb9 100644 --- a/addons/mail/static/src/components/thread_viewer/thread_viewer.js +++ b/addons/mail/static/src/components/thread_viewer/thread_viewer.js @@ -89,7 +89,7 @@ class ThreadViewer extends Component { * * @private */ - async _update() { + _update() { const messageList = this._messageListRef.comp; this.trigger('o-rendered'); /** diff --git a/addons/mail/static/src/models/chat_window/chat_window.js b/addons/mail/static/src/models/chat_window/chat_window.js index e08fc9029b2..1a60a368405 100644 --- a/addons/mail/static/src/models/chat_window/chat_window.js +++ b/addons/mail/static/src/models/chat_window/chat_window.js @@ -294,7 +294,7 @@ function factory(dependencies) { * chat window to put initially on mount. */ threadInitialScrollTop: attr(), - thread: one2one('mail.thread', { + thread: many2one('mail.thread', { related: 'threadViewer.thread', }), threadDisplayName: attr({ diff --git a/addons/mail/static/src/models/composer/composer.js b/addons/mail/static/src/models/composer/composer.js index 86dfe78aa5e..da1c6e7ad10 100644 --- a/addons/mail/static/src/models/composer/composer.js +++ b/addons/mail/static/src/models/composer/composer.js @@ -27,17 +27,14 @@ function factory(dependencies) { }); } + /** + * Hides the composer, which only makes sense if the composer is + * currently used as a Discuss Inbox reply composer. + */ discard() { - const discuss = this.env.messaging.discuss; - const thread = this.thread; - if ( - !discuss.isOpen || - discuss.thread !== thread || - !discuss.isReplyingToMessage - ) { - return; + if (this.discussAsReplying) { + this.discussAsReplying.clearReplyingToMessage(); } - discuss.clearReplyingToMessage(); } focus() { @@ -509,6 +506,14 @@ function factory(dependencies) { ], default: false, }), + /** + * Instance of discuss if this composer is used as the reply composer + * from Inbox. This field is computed from the inverse relation and + * should be considered read-only. + */ + discussAsReplying: one2one('mail.discuss', { + inverse: 'replyingToMessageOriginThreadComposer', + }), extraSuggestedPartners: many2many('mail.partner', { compute: '_computeExtraSuggestedPartners', dependencies: [ diff --git a/addons/mail/static/src/models/discuss/discuss.js b/addons/mail/static/src/models/discuss/discuss.js index 77a7f0fca9a..8ed864eb82e 100644 --- a/addons/mail/static/src/models/discuss/discuss.js +++ b/addons/mail/static/src/models/discuss/discuss.js @@ -177,10 +177,21 @@ function factory(dependencies) { } /** + * Action to initiate reply to given message in Inbox. Assumes that + * Discuss and Inbox are already opened. + * + * @param {mail.message} message + */ + replyToMessage(message) { + this.update({ replyingToMessage: [['link', message]] }); + this.replyingToMessageOriginThreadComposer.focus(); + } + + /** * @param {mail.thread} thread */ setThreadRenaming(thread) { - this.update({ renamingThreads: [['link', thread ]] }); + this.update({ renamingThreads: [['link', thread]] }); } /** @@ -262,6 +273,8 @@ function factory(dependencies) { } /** + * Ensures the reply feature is disabled if discuss is not open. + * * @private * @returns {mail.message|undefined} */ @@ -400,9 +413,32 @@ function factory(dependencies) { ], }), renamingThreads: one2many('mail.thread'), - replyingToMessage: one2one('mail.message', { + /** + * The message that is currently selected as being replied to in Inbox. + * There is only one reply composer shown at a time, which depends on + * this selected message. + */ + replyingToMessage: many2one('mail.message', { compute: '_computeReplyingToMessage', - dependencies: ['isOpen'], + dependencies: [ + 'isOpen', + 'replyingToMessage', + ], + }), + /** + * The thread concerned by the reply feature in Inbox. It depends on the + * message set to be replied, and should be considered read-only. + */ + replyingToMessageOriginThread: many2one('mail.thread', { + related: 'replyingToMessage.originThread', + }), + /** + * The composer to display for the reply feature in Inbox. It depends + * on the message set to be replied, and should be considered read-only. + */ + replyingToMessageOriginThreadComposer: one2one('mail.composer', { + inverse: 'discussAsReplying', + related: 'replyingToMessageOriginThread.composer', }), /** * Quick search input value in the discuss sidebar (desktop). Useful diff --git a/addons/mail/static/src/models/message/message.js b/addons/mail/static/src/models/message/message.js index dda712dbf71..889ddef8306 100644 --- a/addons/mail/static/src/models/message/message.js +++ b/addons/mail/static/src/models/message/message.js @@ -277,18 +277,11 @@ function factory(dependencies) { } /** - * Action to initiate reply to given message. + * Action to initiate reply to current message in Discuss Inbox. Assumes + * that Discuss and Inbox are already opened. */ replyTo() { - const discuss = this.env.messaging.discuss; - if (!discuss.isOpen) { - return; - } - if (discuss.replyingToMessage === this) { - discuss.clearReplyingToMessage(); - } else { - discuss.update({ replyingToMessage: [['link', this]] }); - } + this.env.messaging.discuss.replyToMessage(this); } /** diff --git a/addons/mail/static/src/utils/utils.js b/addons/mail/static/src/utils/utils.js index da0855437f7..8112eeca980 100644 --- a/addons/mail/static/src/utils/utils.js +++ b/addons/mail/static/src/utils/utils.js @@ -12,6 +12,36 @@ const { //------------------------------------------------------------------------------ const classPatchMap = new WeakMap(); +const eventHandledWeakMap = new WeakMap(); + +/** + * Returns whether the given event has been handled with the given markName. + * + * @param {Event} ev + * @param {string} markName + * @returns {boolean} + */ +function isEventHandled(ev, markName) { + if (!eventHandledWeakMap.get(ev)) { + return false; + } + return eventHandledWeakMap.get(ev).includes(markName); +} + +/** + * Marks the given event as handled by the given markName. Useful to allow + * handlers in the propagation chain to make a decision based on what has + * already been done. + * + * @param {Event} ev + * @param {string} markName + */ +function markEventHandled(ev, markName) { + if (!eventHandledWeakMap.get(ev)) { + eventHandledWeakMap.set(ev, []); + } + eventHandledWeakMap.get(ev).push(markName); +} /** * Wait a task tick, so that anything in micro-task queue that can be processed @@ -117,6 +147,8 @@ function unpatchInstanceMethods(Class, patchName) { //------------------------------------------------------------------------------ return { + isEventHandled, + markEventHandled, nextTick, patchClassMethods, patchInstanceMethods, |
