diff options
| author | Sébastien Theys <seb@odoo.com> | 2020-08-28 12:53:38 +0000 |
|---|---|---|
| committer | Sébastien Theys <seb@odoo.com> | 2020-08-28 16:15:15 +0000 |
| commit | 17611f4adc5e950b0a75a2a6615b1da3c956387f (patch) | |
| tree | ede6cbc0bc67c1dceed3b5ca6c1bb3e08389675c /addons/mail/static/src/components | |
| parent | 5ba64925fd846e9a9a4884913e3d1004d05ecdf6 (diff) | |
[FIX] mail, *: fix opens and redirects, adapt messaging menu close
task-2244279
task-2262862
task-2274993
task-2277537
task-2282238
closes odoo/odoo#56745
X-original-commit: 109f202
Related: odoo/enterprise#12761
Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
Signed-off-by: Sébastien Theys (seb) <seb@odoo.com>
Diffstat (limited to 'addons/mail/static/src/components')
29 files changed, 281 insertions, 175 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 4652da4975b..7f9b53d2670 100644 --- a/addons/mail/static/src/components/chat_window/chat_window.js +++ b/addons/mail/static/src/components/chat_window/chat_window.js @@ -167,20 +167,15 @@ class ChatWindow extends Component { * @param {Object} ui.item * @param {integer} ui.item.id */ - _onAutocompleteSelect(ev, ui) { - const partnerId = ui.item.id; - const partner = this.env.models['mail.partner'].find(partner => partner.id === partnerId); - const chat = partner.correspondentThreads.find(thread => thread.channel_type === 'chat'); - if (chat) { - chat.open({ chatWindowMode: 'from_new_message' }); - } else { - this.env.models['mail.thread'].createChannel({ - autoselect: true, - autoselectChatWindowMode: 'from_new_message', - partnerId, - type: 'chat', - }); + async _onAutocompleteSelect(ev, ui) { + const chat = await this.env.messaging.getChat({ partnerId: ui.item.id }); + if (!chat) { + return; } + this.env.messaging.chatWindowManager.openThread(chat, { + makeActive: true, + replaceNewMessage: true, + }); } /** @@ -225,6 +220,12 @@ class ChatWindow extends Component { if (this.chatWindow.isFocused) { return; } + if (isEventHandled(ev, 'Message.authorOpenChat')) { + return; + } + if (isEventHandled(ev, 'Message.authorOpenProfile')) { + return; + } this.chatWindow.focus(); } diff --git a/addons/mail/static/src/components/chat_window/chat_window.xml b/addons/mail/static/src/components/chat_window/chat_window.xml index d711f267ac7..f92a32cf2fe 100644 --- a/addons/mail/static/src/components/chat_window/chat_window.xml +++ b/addons/mail/static/src/components/chat_window/chat_window.xml @@ -29,7 +29,6 @@ hasComposerCurrentPartnerAvatar="false" hasComposerSendButton="env.messaging.device.isMobile" hasSquashCloseMessages="chatWindow.thread.model !== 'mail.box'" - haveMessagesAuthorRedirect="chatWindow.thread.channel_type !== 'chat'" threadViewLocalId="chatWindow.threadView.localId" t-on-focusin="_onFocusinThread" t-ref="thread" diff --git a/addons/mail/static/src/components/chat_window_hidden_menu/chat_window_hidden_menu.js b/addons/mail/static/src/components/chat_window_hidden_menu/chat_window_hidden_menu.js index b598eeee4b4..c6ec0b74e9f 100644 --- a/addons/mail/static/src/components/chat_window_hidden_menu/chat_window_hidden_menu.js +++ b/addons/mail/static/src/components/chat_window_hidden_menu/chat_window_hidden_menu.js @@ -123,9 +123,7 @@ class ChatWindowHiddenMenu extends Component { */ _onClickedChatWindow(ev) { const chatWindow = ev.detail.chatWindow; - chatWindow.makeVisible(); - chatWindow.unfold(); - chatWindow.focus(); + chatWindow.makeActive(); this.env.messaging.chatWindowManager.closeHiddenMenu(); } 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 3306dbed905..efba64cecde 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 @@ -255,6 +255,128 @@ QUnit.test('chat window new message: fold', async function (assert) { ); }); +QUnit.test('open chat from "new message" chat window should open chat in place of this "new message" chat window', async function (assert) { + /** + * InnerWith computation uses following info: + * ([mocked] global window width: @see `mail/static/src/utils/test_utils.js:start()` method) + * (others: @see mail/static/src/models/chat_window_manager/chat_window_manager.js:visual) + * + * - chat window width: 325px + * - start/end/between gap width: 10px/10px/5px + * - hidden menu width: 200px + * - global width: 1920px + * + * Enough space for 3 visible chat windows: + * 10 + 325 + 5 + 325 + 5 + 325 + 10 = 1000 < 1920 + */ + assert.expect(11); + + this.data['res.partner'].records.push({ id: 131, name: "Partner 131" }); + this.data['res.users'].records.push({ partner_id: 131 }); + this.data['mail.channel'].records.push( + { is_minimized: true }, + { is_minimized: true }, + ); + const imSearchDef = makeDeferred(); + await this.start({ + env: { + browser: { + innerWidth: 1920, + }, + }, + async mockRPC(route, args) { + const res = await this._super(...arguments); + if (args.method === 'im_search') { + imSearchDef.resolve(); + } + return res; + } + }); + assert.containsN( + document.body, + '.o_ChatWindow', + 2, + "should have 2 chat windows initially" + ); + assert.containsNone( + document.body, + '.o_ChatWindow.o-new-message', + "should not have any 'new message' chat window initially" + ); + + // open "new message" chat window + await afterNextRender(() => + document.querySelector(`.o_MessagingMenu_toggler`).click() + ); + await afterNextRender(() => + document.querySelector(`.o_MessagingMenu_newMessageButton`).click() + ); + assert.containsOnce( + document.body, + '.o_ChatWindow.o-new-message', + "should have 'new message' chat window after clicking 'new message' in messaging menu" + ); + assert.containsN( + document.body, + '.o_ChatWindow', + 3, + "should have 3 chat window after opening 'new message' chat window", + ); + assert.containsOnce( + document.body, + '.o_ChatWindow_newMessageFormInput', + "'new message' chat window should have new message form input" + ); + assert.hasClass( + document.querySelector('.o_ChatWindow[data-visible-index="2"]'), + 'o-new-message', + "'new message' chat window should be the last chat window initially", + ); + + await afterNextRender(() => + document.querySelector('.o_ChatWindow[data-visible-index="2"] .o_ChatWindowHeader_commandShiftRight').click() + ); + assert.hasClass( + document.querySelector('.o_ChatWindow[data-visible-index="1"]'), + 'o-new-message', + "'new message' chat window should have moved to the middle after clicking shift previous", + ); + + // search for a user in "new message" autocomplete + document.execCommand('insertText', false, "131"); + document.querySelector(`.o_ChatWindow_newMessageFormInput`) + .dispatchEvent(new window.KeyboardEvent('keydown')); + document.querySelector(`.o_ChatWindow_newMessageFormInput`) + .dispatchEvent(new window.KeyboardEvent('keyup')); + // Wait for search RPC to be resolved. The following await lines are + // necessary because autocomplete is an external lib therefore it is not + // possible to use `afterNextRender`. + await imSearchDef; + await nextAnimationFrame(); + const link = document.querySelector('.ui-autocomplete .ui-menu-item a'); + assert.ok( + link, + "should have autocomplete suggestion after typing on 'new message' input" + ); + assert.strictEqual( + link.textContent, + "Partner 131", + "autocomplete suggestion should target the partner matching search term" + ); + + await afterNextRender(() => link.click()); + assert.containsNone( + document.body, + '.o_ChatWindow.o-new-message', + "should have removed the 'new message' chat window after selecting a partner" + ); + assert.strictEqual( + document.querySelector('.o_ChatWindow[data-visible-index="1"] .o_ChatWindowHeader_name').textContent, + "Partner 131", + "chat window with selected partner should be opened in position where 'new message' chat window was, which is in the middle" + ); +}); + QUnit.test('chat window: basic rendering', async function (assert) { assert.expect(11); @@ -1351,7 +1473,7 @@ QUnit.test('chat window: switch on TAB', async function (assert) { ); }); -QUnit.test('chat window: TAB cycle with 3 open chat windows', async function (assert) { +QUnit.test('chat window: TAB cycle with 3 open chat windows [REQUIRE FOCUS]', async function (assert) { /** * InnerWith computation uses following info: * ([mocked] global window width: @see `mail/static/src/utils/test_utils.js:start()` method) diff --git a/addons/mail/static/src/components/chatter/chatter.js b/addons/mail/static/src/components/chatter/chatter.js index 61fbb78d619..cd1f8f8720b 100644 --- a/addons/mail/static/src/components/chatter/chatter.js +++ b/addons/mail/static/src/components/chatter/chatter.js @@ -37,19 +37,18 @@ class Chatter extends Component { attachments: 1, }, }); - this._threadRef = useRef('thread'); + /** + * Reference of the composer. Useful to focus it. + */ + this._composerRef = useRef('composer'); } mounted() { - if (this.chatter.thread) { - this._notifyRendered(); - } + this._update(); } patched() { - if (this.chatter.thread) { - this._notifyRendered(); - } + this._update(); } //-------------------------------------------------------------------------- @@ -77,6 +76,22 @@ class Chatter extends Component { }); } + /** + * @private + */ + _update() { + if (this.chatter.thread) { + this._notifyRendered(); + } + if (this.chatter.isDoFocus) { + this.chatter.update({ isDoFocus: false }); + const composer = this._composerRef.comp; + if (composer) { + composer.focus(); + } + } + } + //-------------------------------------------------------------------------- // Handlers //-------------------------------------------------------------------------- diff --git a/addons/mail/static/src/components/chatter/chatter.xml b/addons/mail/static/src/components/chatter/chatter.xml index b83c9ad733f..a48396f813c 100644 --- a/addons/mail/static/src/components/chatter/chatter.xml +++ b/addons/mail/static/src/components/chatter/chatter.xml @@ -39,10 +39,8 @@ class="o_Chatter_thread" hasComposer="false" hasScrollAdjust="chatter.hasMessageListScrollAdjust" - haveMessagesAuthorRedirect="true" order="'desc'" threadViewLocalId="chatter.threadView.localId" - t-ref="thread" /> </t> </t> diff --git a/addons/mail/static/src/components/composer/composer.js b/addons/mail/static/src/components/composer/composer.js index afb7d811eae..0b23ee4ec22 100644 --- a/addons/mail/static/src/components/composer/composer.js +++ b/addons/mail/static/src/components/composer/composer.js @@ -121,7 +121,7 @@ class Composer extends Component { if (this.env.messaging.device.isMobile) { this.el.scrollIntoView(); } - this.composer.focus(); + this._textInputRef.comp.focus(); } /** @@ -288,7 +288,7 @@ class Composer extends Component { ev.stopPropagation(); this._textInputRef.comp.saveStateInStore(); this.composer.insertIntoTextInput(ev.detail.unicode); - this.composer.focus(); + this.focus(); } /** @@ -323,7 +323,7 @@ class Composer extends Component { if (ev.key === 'Escape') { if (this._emojisPopoverRef.comp) { this._emojisPopoverRef.comp.close(); - this.composer.focus(); + this.focus(); markEventHandled(ev, 'Composer.closeEmojisPopover'); } } 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 53616e112e9..6a0943f9ca8 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 @@ -82,6 +82,10 @@ class ComposerTextInput extends Component { return this.env._t("Write something..."); } + focus() { + this._textareaRef.el.focus(); + } + focusout() { this.saveStateInStore(); this._textareaRef.el.blur(); @@ -155,10 +159,6 @@ class ComposerTextInput extends Component { this.composer.textInputCursorEnd ); this._updateHeight(); - if (this.composer.isDoFocus) { - this._textareaRef.el.focus(); - this.composer.update({ isDoFocus: false }); - } } /** @@ -273,7 +273,7 @@ class ComposerTextInput extends Component { if (this.composer.activeSuggestedPartner) { this.composer.insertMentionedPartner(this.composer.activeSuggestedPartner); this.composer.closeMentionSuggestions(); - this.composer.focus(); + this.focus(); } } break; @@ -322,7 +322,7 @@ class ComposerTextInput extends Component { _onPartnerMentionSuggestionClicked(ev) { this.composer.insertMentionedPartner(ev.detail.partner); this.composer.closeMentionSuggestions(); - this.composer.focus(); + this.focus(); } /** diff --git a/addons/mail/static/src/components/discuss/discuss.js b/addons/mail/static/src/components/discuss/discuss.js index 83aee1e201e..4c8c4a58ab7 100644 --- a/addons/mail/static/src/components/discuss/discuss.js +++ b/addons/mail/static/src/components/discuss/discuss.js @@ -47,12 +47,13 @@ class Discuss extends Component { }); this._updateLocalStoreProps(); /** - * Reference of the thread. Useful to update scroll position correctly - * on patch. AKU TODO: this made sense when composer was outside of - * thread, but this may no longer be necessary?? + * Reference of the composer. Useful to focus it. */ - this._threadRef = useRef('thread'); - + this._composerRef = useRef('composer'); + /** + * Reference of the ThreadView. Useful to focus it. + */ + this._threadViewRef = useRef('threadView'); // bind since passed as props this._onMobileAddItemHeaderInputSelect = this._onMobileAddItemHeaderInputSelect.bind(this); this._onMobileAddItemHeaderInputSource = this._onMobileAddItemHeaderInputSource.bind(this); @@ -66,6 +67,7 @@ class Discuss extends Component { this.discuss.openInitThread(); } this._updateLocalStoreProps(); + this._update(); } patched() { @@ -83,6 +85,7 @@ class Discuss extends Component { } this._activeThreadCache = this.discuss.threadView.threadCache; this._updateLocalStoreProps(); + this._update(); } willUnmount() { @@ -142,6 +145,24 @@ class Discuss extends Component { /** * @private */ + _update() { + if (this.discuss.isDoFocus) { + this.discuss.update({ isDoFocus: false }); + const composer = this._composerRef.comp; + if (composer) { + composer.focus(); + } else { + const threadView = this._threadViewRef.comp; + if (threadView) { + threadView.focus(); + } + } + } + } + + /** + * @private + */ _updateLocalStoreProps() { /** * Locally tracked store props `activeThreadCache`. @@ -251,16 +272,6 @@ class Discuss extends Component { /** * @private * @param {CustomEvent} ev - * @param {Object} ev.detail - * @param {mail.thread} ev.detail.thread - */ - _onSelectThread(ev) { - this.discuss.threadView.update({ thread: [['link', ev.detail.thread]] }); - } - - /** - * @private - * @param {CustomEvent} ev */ _onThreadRendered(ev) { this.trigger('o-update-control-panel'); diff --git a/addons/mail/static/src/components/discuss/discuss.xml b/addons/mail/static/src/components/discuss/discuss.xml index 94234d0fc9b..d88b4796d9c 100644 --- a/addons/mail/static/src/components/discuss/discuss.xml +++ b/addons/mail/static/src/components/discuss/discuss.xml @@ -58,7 +58,6 @@ hasComposerThreadTyping="true" hasMessageCheckbox="true" hasSquashCloseMessages="discuss.thread.model !== 'mail.box'" - haveMessagesAuthorRedirect="discuss.thread.channel_type !== 'chat'" haveMessagesMarkAsReadIcon="discuss.thread === env.messaging.inbox" haveMessagesReplyIcon="discuss.thread === env.messaging.inbox" selectedMessageLocalId=" @@ -66,6 +65,7 @@ discuss.replyingToMessage.localId" threadViewLocalId="discuss.threadView.localId" t-on-o-rendered="_onThreadRendered" + t-ref="threadView" /> </t> <t t-if="!discuss.thread and (!env.messaging.device.isMobile or discuss.activeMobileNavbarTabId === 'mailbox')"> @@ -77,7 +77,6 @@ <NotificationList class="o_Discuss_notificationList" filter="discuss.activeMobileNavbarTabId" - t-on-o-select-thread="_onSelectThread" /> </t> <t t-if="env.messaging.device.isMobile and !discuss.isReplyingToMessage"> @@ -96,6 +95,7 @@ hasDiscardButton="true" hasThreadName="true" t-on-o-message-posted="_onReplyingToMessageMessagePosted" + t-ref="composer" /> </t> </t> diff --git a/addons/mail/static/src/components/discuss/tests/discuss_tests.js b/addons/mail/static/src/components/discuss/tests/discuss_tests.js index 8c437ce5b18..3f9e93d1de8 100644 --- a/addons/mail/static/src/components/discuss/tests/discuss_tests.js +++ b/addons/mail/static/src/components/discuss/tests/discuss_tests.js @@ -2116,7 +2116,7 @@ QUnit.test('message origin redirect to channel', async function (assert) { }); QUnit.test('redirect to author (open chat)', async function (assert) { - assert.expect(9); + assert.expect(7); // expected correspondent, with a random unique id that will be used to link // partner to chat and a random name that will be asserted in the test @@ -2143,13 +2143,6 @@ QUnit.test('redirect to author (open chat)', async function (assert) { id: 100, model: 'mail.channel', res_id: 1, - }, - { - author_id: this.data.currentPartnerId, - channel_ids: [1], - id: 101, - model: 'mail.channel', - res_id: 1, } ); await this.start({ @@ -2187,8 +2180,8 @@ QUnit.test('redirect to author (open chat)', async function (assert) { ); assert.strictEqual( document.querySelectorAll(`.o_Discuss_thread .o_Message`).length, - 2, - "should have 2 messages" + 1, + "should have 1 message" ); const msg1 = document.querySelector(` .o_Discuss_thread @@ -2196,12 +2189,6 @@ QUnit.test('redirect to author (open chat)', async function (assert) { this.env.models['mail.message'].find(message => message.id === 100).localId }"] `); - const msg2 = document.querySelector(` - .o_Discuss_thread - .o_Message[data-message-local-id="${ - this.env.models['mail.message'].find(message => message.id === 101).localId - }"] - `); assert.strictEqual( msg1.querySelectorAll(`:scope .o_Message_authorAvatar`).length, 1, @@ -2211,15 +2198,6 @@ QUnit.test('redirect to author (open chat)', async function (assert) { msg1.querySelector(`:scope .o_Message_authorAvatar`).classList.contains('o_redirect'), "message1 should have redirect to author" ); - assert.strictEqual( - msg2.querySelectorAll(`:scope .o_Message_authorAvatar`).length, - 1, - "message2 should have author image" - ); - assert.notOk( - msg2.querySelector(`:scope .o_Message_authorAvatar`).classList.contains('o_redirect'), - "message2 should not have redirect to author (self-author)" - ); await afterNextRender(() => msg1.querySelector(`:scope .o_Message_authorAvatar`).click() diff --git a/addons/mail/static/src/components/discuss_mobile_mailbox_selection/discuss_mobile_mailbox_selection.js b/addons/mail/static/src/components/discuss_mobile_mailbox_selection/discuss_mobile_mailbox_selection.js index cea920a45f9..c2185a60dc0 100644 --- a/addons/mail/static/src/components/discuss_mobile_mailbox_selection/discuss_mobile_mailbox_selection.js +++ b/addons/mail/static/src/components/discuss_mobile_mailbox_selection/discuss_mobile_mailbox_selection.js @@ -74,7 +74,7 @@ class DiscussMobileMailboxSelection extends Component { */ _onClick(ev) { const { mailbox } = ev.currentTarget.dataset; - this.discuss.threadView.update({ thread: [['link', mailbox]] }); + mailbox.open(); } } diff --git a/addons/mail/static/src/components/discuss_sidebar_item/discuss_sidebar_item.js b/addons/mail/static/src/components/discuss_sidebar_item/discuss_sidebar_item.js index daaa8b44b5d..2b84762c9dd 100644 --- a/addons/mail/static/src/components/discuss_sidebar_item/discuss_sidebar_item.js +++ b/addons/mail/static/src/components/discuss_sidebar_item/discuss_sidebar_item.js @@ -117,7 +117,7 @@ class DiscussSidebarItem extends Component { * @param {MouseEvent} ev */ _onClick(ev) { - this.discuss.threadView.update({ thread: [['link', this.thread]] }); + this.thread.open(); } /** diff --git a/addons/mail/static/src/components/follower/follower.js b/addons/mail/static/src/components/follower/follower.js index f446c098914..dc3e395f03c 100644 --- a/addons/mail/static/src/components/follower/follower.js +++ b/addons/mail/static/src/components/follower/follower.js @@ -43,10 +43,7 @@ class Follower extends Component { _onClickDetails(ev) { ev.preventDefault(); ev.stopPropagation(); - this.env.messaging.openDocument({ - id: this.follower.resId, - model: this.follower.resModel, - }); + this.follower.openProfile(); } /** diff --git a/addons/mail/static/src/components/follower/follower_tests.js b/addons/mail/static/src/components/follower/follower_tests.js index d93cb24b6c2..a742e899e6b 100644 --- a/addons/mail/static/src/components/follower/follower_tests.js +++ b/addons/mail/static/src/components/follower/follower_tests.js @@ -4,6 +4,7 @@ odoo.define('mail/static/src/components/follower/follower_tests.js', function (r const components = { Follower: require('mail/static/src/components/follower/follower.js'), }; +const { makeDeferred } = require('mail/static/src/utils/deferred/deferred.js'); const { afterEach, afterNextRender, @@ -141,8 +142,8 @@ QUnit.test('click on channel follower details', async function (assert) { assert.step('do_action'); assert.strictEqual( payload.action.res_id, - 1, - "The redirect action should redirect to the right res id (1)" + 10, + "The redirect action should redirect to the right res id (10)" ); assert.strictEqual( payload.action.res_model, @@ -155,7 +156,8 @@ QUnit.test('click on channel follower details', async function (assert) { "The redirect action should be of type 'ir.actions.act_window'" ); }); - + this.data['res.partner'].records.push({ id: 100 }); + this.data['mail.channel'].records.push({ id: 10 }); await this.start({ env: { bus }, }); @@ -164,7 +166,7 @@ QUnit.test('click on channel follower details', async function (assert) { model: 'res.partner', }); const follower = await this.env.models['mail.follower'].create({ - channel: [['insert', { id: 1, model: 'mail.channel', name: "channel" }]], + channel: [['insert', { id: 10, model: 'mail.channel', name: "channel" }]], followedThread: [['link', thread]], id: 2, isActive: true, @@ -192,6 +194,7 @@ QUnit.test('click on channel follower details', async function (assert) { QUnit.test('click on partner follower details', async function (assert) { assert.expect(7); + const openFormDef = makeDeferred(); const bus = new Bus(); bus.on('do-action', null, payload => { assert.step('do_action'); @@ -210,8 +213,9 @@ QUnit.test('click on partner follower details', async function (assert) { "ir.actions.act_window", "The redirect action should be of type 'ir.actions.act_window'" ); + openFormDef.resolve(); }); - + this.data['res.partner'].records.push({ id: 100 }); await this.start({ env: { bus }, }); @@ -243,6 +247,7 @@ QUnit.test('click on partner follower details', async function (assert) { ); document.querySelector('.o_Follower_details').click(); + await openFormDef; assert.verifySteps( ['do_action'], "clicking on follower should redirect to partner form view" @@ -302,6 +307,7 @@ QUnit.test('click on edit follower', async function (assert) { QUnit.test('edit follower and close subtype dialog', async function (assert) { assert.expect(6); + this.data['res.partner'].records.push({ id: 100 }); await this.start({ hasDialog: true, async mockRPC(route, args) { diff --git a/addons/mail/static/src/components/message/message.js b/addons/mail/static/src/components/message/message.js index 157f3dc2acd..7d314647bb2 100644 --- a/addons/mail/static/src/components/message/message.js +++ b/addons/mail/static/src/components/message/message.js @@ -11,6 +11,7 @@ const components = { PartnerImStatusIcon: require('mail/static/src/components/partner_im_status_icon/partner_im_status_icon.js'), }; const useStore = require('mail/static/src/component_hooks/use_store/use_store.js'); +const { markEventHandled } = require('mail/static/src/utils/utils.js'); const { timeFromNow } = require('mail.utils'); const { _lt } = require('web.core'); @@ -156,19 +157,18 @@ class Message extends Component { } /** - * Determine whether author redirect feature is enabled on message. - * Click on message author should redirect to author. + * Determines whether author open chat feature is enabled on message. * * @returns {boolean} */ - get hasAuthorRedirect() { - if (!this.props.hasAuthorRedirect) { - return false; - } + get hasAuthorOpenChat() { if (!this.message.author) { return false; } - if (this.message.author === this.env.messaging.currentPartner) { + if ( + this.message.originThread && + this.message.originThread.correspondent === this.message.author + ) { return false; } return true; @@ -237,6 +237,13 @@ class Message extends Component { } /** + * @returns {string} + */ + get OPEN_CHAT() { + return this.env._t("Open chat"); + } + + /** * Make this message viewable in its enclosing scroll environment (usually * message list). * @@ -245,7 +252,7 @@ class Message extends Component { * @param {string} [param0.block='end'] * @returns {Promise} */ - async scrollIntoView({ behavior='auto', block='end' }={}) { + async scrollIntoView({ behavior = 'auto', block = 'end' } = {}) { this.el.scrollIntoView({ behavior, block, @@ -419,23 +426,33 @@ class Message extends Component { * @param {MouseEvent} ev */ _onClick(ev) { + if (ev.target.closest('.o_channel_redirect')) { + this.env.messaging.openProfile({ + id: Number(ev.target.dataset.oeId), + model: 'mail.channel', + }); + // avoid following dummy href + ev.preventDefault(); + return; + } if (ev.target.closest('.o_mention')) { - this.env.messaging.redirect({ + this.env.messaging.openProfile({ id: Number(ev.target.dataset.oeId), model: ev.target.dataset.oeModel, }); + // avoid following dummy href ev.preventDefault(); return; } if (ev.target.closest('.o_mail_redirect')) { - this.env.messaging.redirect({ + this.env.messaging.openProfile({ id: Number(ev.target.dataset.oeId), model: ev.target.dataset.oeModel, }); + // avoid following dummy href ev.preventDefault(); return; } - ev.stopPropagation(); this.state.isClicked = !this.state.isClicked; } @@ -443,17 +460,24 @@ class Message extends Component { * @private * @param {MouseEvent} ev */ - _onClickAuthor(ev) { - if (!this.hasAuthorRedirect) { + _onClickAuthorAvatar(ev) { + if (!this.hasAuthorOpenChat) { return; } + markEventHandled(ev, 'Message.authorOpenChat'); + this.message.author.openChat(); + } + + /** + * @private + * @param {MouseEvent} ev + */ + _onClickAuthorName(ev) { if (!this.message.author) { return; } - this.env.messaging.redirect({ - id: this.message.author.id, - model: this.message.author.model, - }); + markEventHandled(ev, 'Message.authorOpenProfile'); + this.message.author.openProfile(); } /** @@ -514,11 +538,9 @@ class Message extends Component { * @param {MouseEvent} ev */ _onClickOriginThread(ev) { + // avoid following dummy href ev.preventDefault(); - this.env.messaging.redirect({ - id: this.message.originThread.id, - model: this.message.originThread.model, - }); + this.message.originThread.open(); } /** @@ -581,7 +603,6 @@ class Message extends Component { Object.assign(Message, { components, defaultProps: { - hasAuthorRedirect: false, hasCheckbox: false, hasMarkAsReadIcon: false, hasReplyIcon: false, @@ -594,7 +615,6 @@ Object.assign(Message, { optional: true, validate: prop => ['auto', 'card', 'hover', 'none'].includes(prop), }, - hasAuthorRedirect: Boolean, hasCheckbox: Boolean, hasMarkAsReadIcon: Boolean, hasReplyIcon: Boolean, diff --git a/addons/mail/static/src/components/message/message.xml b/addons/mail/static/src/components/message/message.xml index 7739c79337a..b0a41947c88 100644 --- a/addons/mail/static/src/components/message/message.xml +++ b/addons/mail/static/src/components/message/message.xml @@ -18,7 +18,7 @@ <div class="o_Message_sidebar" t-att-class="{ 'o-message-squashed': props.isSquashed }"> <t t-if="!props.isSquashed"> <div class="o_Message_authorAvatarContainer o_Message_sidebarItem"> - <img class="o_Message_authorAvatar rounded-circle" t-att-class="{ o_Message_authorRedirect: hasAuthorRedirect, o_redirect: hasAuthorRedirect }" t-att-src="avatar" t-on-click="_onClickAuthor" t-att-title="hasAuthorRedirect ? 'Open chat' : ''" alt=""/> + <img class="o_Message_authorAvatar rounded-circle" t-att-class="{ o_Message_authorRedirect: hasAuthorOpenChat, o_redirect: hasAuthorOpenChat }" t-att-src="avatar" t-on-click="_onClickAuthorAvatar" t-att-title="hasAuthorOpenChat ? OPEN_CHAT : ''" alt="Avatar"/> <t t-if="message.author and message.author.im_status"> <PartnerImStatusIcon class="o_Message_partnerImStatusIcon" @@ -58,7 +58,7 @@ <t t-if="!props.isSquashed"> <div class="o_Message_header"> <t t-if="message.author"> - <div class="o_Message_authorName" t-att-class="hasAuthorRedirect ? 'o_Message_authorRedirect o_redirect' : ''" t-on-click="_onClickAuthor" t-att-title="hasAuthorRedirect ? 'Open chat' : ''"> + <div class="o_Message_authorName o_Message_authorRedirect o_redirect" t-on-click="_onClickAuthorName" title="Open profile"> <t t-esc="message.author.nameOrDisplayName"/> </div> </t> diff --git a/addons/mail/static/src/components/message_list/message_list.js b/addons/mail/static/src/components/message_list/message_list.js index 0cdf4893305..966bff9a5ba 100644 --- a/addons/mail/static/src/components/message_list/message_list.js +++ b/addons/mail/static/src/components/message_list/message_list.js @@ -141,9 +141,6 @@ class MessageList extends Component { case 'more-messages-loaded': this._adjustFromMoreMessagesLoaded(hint); break; - default: - this.threadView.markComponentHintProcessed(hint); - break; } } this._willPatchSnapshot = undefined; @@ -552,7 +549,6 @@ Object.assign(MessageList, { hasMessageCheckbox: false, hasScrollAdjust: true, hasSquashCloseMessages: false, - haveMessagesAuthorRedirect: false, haveMessagesMarkAsReadIcon: false, haveMessagesReplyIcon: false, order: 'asc', @@ -560,7 +556,6 @@ Object.assign(MessageList, { props: { hasMessageCheckbox: Boolean, hasSquashCloseMessages: Boolean, - haveMessagesAuthorRedirect: Boolean, haveMessagesMarkAsReadIcon: Boolean, haveMessagesReplyIcon: Boolean, hasScrollAdjust: Boolean, diff --git a/addons/mail/static/src/components/message_list/message_list.xml b/addons/mail/static/src/components/message_list/message_list.xml index a678f7a7199..0a87558e839 100644 --- a/addons/mail/static/src/components/message_list/message_list.xml +++ b/addons/mail/static/src/components/message_list/message_list.xml @@ -65,7 +65,6 @@ t-att-class="{ 'o-has-message-selection': props.selectedMessageLocalId, }" - hasAuthorRedirect="props.haveMessagesAuthorRedirect" hasMarkAsReadIcon="props.haveMessagesMarkAsReadIcon" hasCheckbox="props.hasMessageCheckbox" hasReplyIcon="props.haveMessagesReplyIcon" diff --git a/addons/mail/static/src/components/messaging_menu/messaging_menu.js b/addons/mail/static/src/components/messaging_menu/messaging_menu.js index ac4472d8209..4e5ede8f635 100644 --- a/addons/mail/static/src/components/messaging_menu/messaging_menu.js +++ b/addons/mail/static/src/components/messaging_menu/messaging_menu.js @@ -33,13 +33,6 @@ class MessagingMenu extends Component { }; }); - /** - * Reference of the new message input in mobile. Useful to include it - * and autocomplete menu as "inside" the messaging menu, to prevent - * closing the messaging menu otherwise. - */ - this._mobileNewMessageInputRef = useRef('mobileNewMessageInput'); - // bind since passed as props this._onMobileNewMessageInputSelect = this._onMobileNewMessageInputSelect.bind(this); this._onMobileNewMessageInputSource = this._onMobileNewMessageInputSource.bind(this); @@ -121,20 +114,14 @@ class MessagingMenu extends Component { // in mobile: keeps the messaging menu open in background // TODO: maybe need to move this to a mobile component? // task-2089887 - if ( - this.env.messaging.device.isMobile && - this.env.messaging.chatWindowManager.hasVisibleChatWindows - ) { + if (this.env.messaging.device.isMobile) { return; } // ignore click inside the menu if (this.el.contains(ev.target)) { return; } - const input = this._mobileNewMessageInputRef.comp; - if (input && input.contains(ev.target)) { - return; - } + // in all other cases: close the messaging menu when clicking outside this.messagingMenu.close(); } @@ -194,21 +181,7 @@ class MessagingMenu extends Component { * @param {integer} ui.item.id */ _onMobileNewMessageInputSelect(ev, ui) { - const partnerId = ui.item.id; - const partner = this.env.models['mail.partner'].find(partner => partner.id === partnerId); - const chat = partner.correspondentThreads.find(thread => thread.channel_type === 'chat'); - if (chat) { - chat.open(); - } else { - this.env.models['mail.thread'].createChannel({ - autoselect: true, - partnerId, - type: 'chat', - }); - } - if (!this.env.messaging.device.isMobile) { - this.messagingMenu.close(); - } + this.env.messaging.openChat({ partnerId: ui.item.id }); } /** @@ -246,20 +219,6 @@ class MessagingMenu extends Component { this.messagingMenu.update({ activeTabId: ev.detail.tabId }); } - /** - * @private - * @param {CustomEvent} ev - * @param {Object} ev.detail - * @param {mail.thread} ev.detail.thread - */ - _onSelectThread(ev) { - ev.stopPropagation(); - ev.detail.thread.open(); - if (!this.env.messaging.device.isMobile) { - this.messagingMenu.close(); - } - } - } Object.assign(MessagingMenu, { diff --git a/addons/mail/static/src/components/messaging_menu/messaging_menu.xml b/addons/mail/static/src/components/messaging_menu/messaging_menu.xml index b743c7ca92d..fc779231dd8 100644 --- a/addons/mail/static/src/components/messaging_menu/messaging_menu.xml +++ b/addons/mail/static/src/components/messaging_menu/messaging_menu.xml @@ -46,7 +46,6 @@ select="_onMobileNewMessageInputSelect" source="_onMobileNewMessageInputSource" t-on-o-hide="_onHideMobileNewMessage" - t-ref="mobileNewMessageInput" /> </t> </div> @@ -54,7 +53,6 @@ class="o_MessagingMenu_notificationList" t-att-class="{ 'o-mobile': env.messaging.device.isMobile }" filter="messagingMenu.activeTabId" - t-on-o-select-thread="_onSelectThread" /> <t t-if="env.messaging.device.isMobile"> <MobileMessagingNavbar diff --git a/addons/mail/static/src/components/notification_group/notification_group.js b/addons/mail/static/src/components/notification_group/notification_group.js index 4020a610dc6..65cfcea3076 100644 --- a/addons/mail/static/src/components/notification_group/notification_group.js +++ b/addons/mail/static/src/components/notification_group/notification_group.js @@ -61,6 +61,9 @@ class NotificationGroup extends Component { return; } this.group.openDocuments(); + if (!this.env.messaging.device.isMobile) { + this.env.messaging.messagingMenu.close(); + } } /** @@ -69,6 +72,9 @@ class NotificationGroup extends Component { */ _onClickMarkAsRead(ev) { this.group.openCancelAction(); + if (!this.env.messaging.device.isMobile) { + this.env.messaging.messagingMenu.close(); + } } } diff --git a/addons/mail/static/src/components/notification_list/notification_list_notification_group_tests.js b/addons/mail/static/src/components/notification_list/notification_list_notification_group_tests.js index fcab0251524..aa1d36593ad 100644 --- a/addons/mail/static/src/components/notification_list/notification_list_notification_group_tests.js +++ b/addons/mail/static/src/components/notification_list/notification_list_notification_group_tests.js @@ -364,6 +364,7 @@ QUnit.test('different mail.channel are not grouped', async function (assert) { // they are linked to different channels, even though the model is the same. assert.expect(6); + this.data['mail.channel'].records.push({ id: 31 }, { id: 32 }); this.data['mail.message'].records.push( // first message that is expected to have a failure { diff --git a/addons/mail/static/src/components/notification_request/notification_request.js b/addons/mail/static/src/components/notification_request/notification_request.js index cb1b59ccbaa..69fb5beab2f 100644 --- a/addons/mail/static/src/components/notification_request/notification_request.js +++ b/addons/mail/static/src/components/notification_request/notification_request.js @@ -74,9 +74,7 @@ class NotificationRequest extends Component { if (def) { def.then(this._handleResponseNotificationPermission.bind(this)); } - - const device = this.env.messaging.device; - if (!device.isMobile) { + if (!this.env.messaging.device.isMobile) { this.env.messaging.messagingMenu.close(); } } diff --git a/addons/mail/static/src/components/thread_needaction_preview/thread_needaction_preview.js b/addons/mail/static/src/components/thread_needaction_preview/thread_needaction_preview.js index efcd8f5d86e..24486cfc93c 100644 --- a/addons/mail/static/src/components/thread_needaction_preview/thread_needaction_preview.js +++ b/addons/mail/static/src/components/thread_needaction_preview/thread_needaction_preview.js @@ -105,7 +105,10 @@ class ThreadNeedactionPreview extends Component { // handled in `_onClickMarkAsRead` return; } - this.trigger('o-select-thread', { thread: this.thread }); + this.thread.open(); + if (!this.env.messaging.device.isMobile) { + this.env.messaging.messagingMenu.close(); + } } /** diff --git a/addons/mail/static/src/components/thread_preview/thread_preview.js b/addons/mail/static/src/components/thread_preview/thread_preview.js index 614201e821d..6e30005225c 100644 --- a/addons/mail/static/src/components/thread_preview/thread_preview.js +++ b/addons/mail/static/src/components/thread_preview/thread_preview.js @@ -97,7 +97,10 @@ class ThreadPreview extends Component { // handled in `_onClickMarkAsRead` return; } - this.trigger('o-select-thread', { thread: this.thread }); + this.thread.open(); + if (!this.env.messaging.device.isMobile) { + this.env.messaging.messagingMenu.close(); + } } /** diff --git a/addons/mail/static/src/components/thread_view/thread_view.js b/addons/mail/static/src/components/thread_view/thread_view.js index 7b1680f31a8..395fac68474 100644 --- a/addons/mail/static/src/components/thread_view/thread_view.js +++ b/addons/mail/static/src/components/thread_view/thread_view.js @@ -132,7 +132,6 @@ Object.assign(ThreadView, { hasComposer: false, hasMessageCheckbox: false, hasSquashCloseMessages: false, - haveMessagesAuthorRedirect: false, haveMessagesMarkAsReadIcon: false, haveMessagesReplyIcon: false, order: 'asc', @@ -168,7 +167,6 @@ Object.assign(ThreadView, { optional: true, }, hasSquashCloseMessages: Boolean, - haveMessagesAuthorRedirect: Boolean, haveMessagesMarkAsReadIcon: Boolean, haveMessagesReplyIcon: Boolean, order: { diff --git a/addons/mail/static/src/components/thread_view/thread_view.xml b/addons/mail/static/src/components/thread_view/thread_view.xml index 30844135e65..eead567fa1e 100644 --- a/addons/mail/static/src/components/thread_view/thread_view.xml +++ b/addons/mail/static/src/components/thread_view/thread_view.xml @@ -15,7 +15,6 @@ hasMessageCheckbox="props.hasMessageCheckbox" hasScrollAdjust="props.hasScrollAdjust" hasSquashCloseMessages="props.hasSquashCloseMessages" - haveMessagesAuthorRedirect="props.haveMessagesAuthorRedirect" haveMessagesMarkAsReadIcon="props.haveMessagesMarkAsReadIcon" haveMessagesReplyIcon="props.haveMessagesReplyIcon" order="props.order" diff --git a/addons/mail/static/src/components/thread_view/thread_view_tests.js b/addons/mail/static/src/components/thread_view/thread_view_tests.js index 06832473b99..c8f41632ae3 100644 --- a/addons/mail/static/src/components/thread_view/thread_view_tests.js +++ b/addons/mail/static/src/components/thread_view/thread_view_tests.js @@ -304,7 +304,7 @@ QUnit.test('mark channel as fetched when a new message is loaded and as seen whe "Channel should have been fetched but not seen yet" ); - await afterNextRender(() => thread.composer.focus()); + await afterNextRender(() => document.querySelector('.o_ComposerTextInput_textarea').focus()); assert.verifySteps( ['rpc:channel_seen'], "Channel should have been marked as seen after threadView got the focus" @@ -355,6 +355,8 @@ QUnit.test('mark channel as fetched and seen when a new message is loaded if com const threadView = this.env.models['mail.thread_view'].create({ thread: [['link', thread]] }); await this.createThreadViewComponent(threadView, { hasComposer: true }); + + document.querySelector('.o_ComposerTextInput_textarea').focus(); const notifications = [ [['myDB', 'mail.channel', 100], { channelId: 100, |
