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 75c71cc43f1..5418a20273c 100644 --- a/addons/mail/static/src/components/message_list/message_list.js +++ b/addons/mail/static/src/components/message_list/message_list.js @@ -490,6 +490,20 @@ class MessageList extends Component { this._loadMore(); } + /** + * @private + */ + _onClickRetryLoadMoreMessages() { + if (!this.threadView) { + return; + } + if (!this.threadView.threadCache) { + return; + } + this.threadView.threadCache.update({ hasLoadingFailed: false }); + this._loadMore(); + } + /** * @private * @param {ScrollEvent} ev 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 6d3730fdcea..b7675a938ba 100644 --- a/addons/mail/static/src/components/message_list/message_list.xml +++ b/addons/mail/static/src/components/message_list/message_list.xml @@ -37,7 +37,17 @@ - + +
+
+ An error occured while fetching messages. +
+ +
+
+ @@ -75,7 +85,7 @@
- + 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 e4795bc0d45..f6b55a86ec5 100644 --- a/addons/mail/static/src/components/thread_view/thread_view.js +++ b/addons/mail/static/src/components/thread_view/thread_view.js @@ -130,6 +130,7 @@ class ThreadView extends Component { isDeviceMobile: this.env.messaging.device.isMobile, thread, threadCacheIsLoaded: threadCache && threadCache.isLoaded, + threadCacheHasLoadingFailed: threadCache && threadCache.hasLoadingFailed, threadIsTemporary: thread && thread.isTemporary, threadMassMailing: thread && thread.mass_mailing, threadModel: thread && thread.model, @@ -138,6 +139,23 @@ class ThreadView extends Component { }; } + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + */ + _onClickRetryLoadMessages() { + if (!this.threadView) { + return; + } + if (!this.threadView.threadCache) { + return; + } + this.threadView.threadCache.update({ hasLoadingFailed: false }); + } + } Object.assign(ThreadView, { diff --git a/addons/mail/static/src/components/thread_view/thread_view.scss b/addons/mail/static/src/components/thread_view/thread_view.scss index 0db54501341..7a9df9bbd54 100644 --- a/addons/mail/static/src/components/thread_view/thread_view.scss +++ b/addons/mail/static/src/components/thread_view/thread_view.scss @@ -13,6 +13,11 @@ flex: 0 0 auto; } +.o_ThreadView_alertLoadingFailed { + display: flex; + align-items: center; +} + .o_ThreadView_loading { display: flex; align-self: center; @@ -20,6 +25,14 @@ align-items: center; } +.o_ThreadView_loadingFailed { + display: flex; + flex: 1 1 auto; + align-items: center; + justify-content: center; + flex-direction: column; +} + .o_ThreadView_messageList { flex: 1 1 auto; } 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 67234d77eb9..ffbb9154d26 100644 --- a/addons/mail/static/src/components/thread_view/thread_view.xml +++ b/addons/mail/static/src/components/thread_view/thread_view.xml @@ -22,6 +22,16 @@ t-ref="messageList" /> + +
+
+ An error occured while fetching messages. +
+ +
+
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 efab5e6221f..87d33c89548 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 @@ -1816,6 +1816,240 @@ QUnit.test('composer should be focused automatically after clicking on the send ); }); +QUnit.test('failure on loading messages should display error', async function (assert) { + assert.expect(1); + + this.data['mail.channel'].records.push({ + channel_type: 'channel', + id: 20, + is_pinned: true, + name: "General", + }); + await this.start({ + async mockRPC(route, args) { + if (args.method === 'message_fetch') { + throw new Error(); + } + return this._super(...arguments); + }, + }); + const thread = this.env.models['mail.thread'].findFromIdentifyingData({ + id: 20, + model: 'mail.channel' + }); + const threadViewer = this.env.models['mail.thread_viewer'].create({ + hasThreadView: true, + thread: [['link', thread]], + }); + await this.createThreadViewComponent(threadViewer.threadView); + + assert.containsOnce( + document.body, + '.o_ThreadView_loadingFailed', + "should show loading error message" + ); +}); + +QUnit.test('failure on loading messages should prompt retry button', async function (assert) { + assert.expect(1); + + this.data['mail.channel'].records.push({ + channel_type: 'channel', + id: 20, + is_pinned: true, + name: "General", + }); + await this.start({ + async mockRPC(route, args) { + if (args.method === 'message_fetch') { + throw new Error(); + } + return this._super(...arguments); + }, + }); + const thread = this.env.models['mail.thread'].findFromIdentifyingData({ + id: 20, + model: 'mail.channel' + }); + const threadViewer = this.env.models['mail.thread_viewer'].create({ + hasThreadView: true, + thread: [['link', thread]], + }); + await this.createThreadViewComponent(threadViewer.threadView); + + assert.containsOnce( + document.body, + '.o_ThreadView_loadingFailedRetryButton', + "should show a button to allow user to retry loading" + ); +}); + +QUnit.test('failure on loading more messages should not alter message list display', async function (assert) { + assert.expect(1); + + // first call needs to be successful as it is the initial loading of messages + // second call comes from load more and needs to fail in order to show the error alert + // any later call should work so that retry button and load more clicks would now work + let messageFetchShouldFail = false; + this.data['mail.channel'].records.push({ + channel_type: 'channel', + id: 20, + is_pinned: true, + name: "General", + }); + this.data['mail.message'].records.push(...[...Array(60).keys()].map(id => { + return { + body: 'coucou', + channel_ids: [20], + id, + }; + })); + await this.start({ + async mockRPC(route, args) { + if (args.method === 'message_fetch') { + if (messageFetchShouldFail) { + throw new Error(); + } + } + return this._super(...arguments); + }, + }); + const thread = this.env.models['mail.thread'].findFromIdentifyingData({ + id: 20, + model: 'mail.channel' + }); + const threadViewer = this.env.models['mail.thread_viewer'].create({ + hasThreadView: true, + thread: [['link', thread]], + }); + await this.createThreadViewComponent(threadViewer.threadView, { hasComposer: true }); + + messageFetchShouldFail = true; + await afterNextRender(() => document.querySelector('.o_MessageList_loadMore').click()); + assert.containsN( + document.body, + '.o_Message', + 30, + "should still show 30 messages as load more has failed" + ); +}); + +QUnit.test('failure on loading more messages should display error and prompt retry button', async function (assert) { + assert.expect(3); + + // first call needs to be successful as it is the initial loading of messages + // second call comes from load more and needs to fail in order to show the error alert + // any later call should work so that retry button and load more clicks would now work + let messageFetchShouldFail = false; + this.data['mail.channel'].records.push({ + channel_type: 'channel', + id: 20, + is_pinned: true, + name: "General", + }); + this.data['mail.message'].records.push(...[...Array(60).keys()].map(id => { + return { + body: 'coucou', + channel_ids: [20], + id, + }; + })); + await this.start({ + async mockRPC(route, args) { + if (args.method === 'message_fetch') { + if (messageFetchShouldFail) { + throw new Error(); + } + } + return this._super(...arguments); + }, + }); + const thread = this.env.models['mail.thread'].findFromIdentifyingData({ + id: 20, + model: 'mail.channel' + }); + const threadViewer = this.env.models['mail.thread_viewer'].create({ + hasThreadView: true, + thread: [['link', thread]], + }); + await this.createThreadViewComponent(threadViewer.threadView, { hasComposer: true }); + + messageFetchShouldFail = true; + await afterNextRender(() => document.querySelector('.o_MessageList_loadMore').click()); + assert.containsOnce( + document.body, + '.o_ThreadView_alertLoadingFailed', + "should show loading error message" + ); + assert.containsOnce( + document.body, + '.o_ThreadView_alertLoadingFailedRetryButton', + "should show loading error message button" + ); + assert.containsNone( + document.body, + '.o_MessageList_loadMore', + "should not show load more buttton" + ); +}); + +QUnit.test('Retry loading more messages on failed load more messages should load more messages', async function (assert) { + assert.expect(0); + + // first call needs to be successful as it is the initial loading of messages + // second call comes from load more and needs to fail in order to show the error alert + // any later call should work so that retry button and load more clicks would now work + let messageFetchShouldFail = false; + this.data['mail.channel'].records.push({ + channel_type: 'channel', + id: 20, + is_pinned: true, + name: "General", + }); + this.data['mail.message'].records = [...Array(90).keys()].map(id => { + return { + body: 'coucou', + channel_ids: [20], + id, + }; + }); + await this.start({ + async mockRPC(route, args) { + if (args.method === 'message_fetch') { + if (messageFetchShouldFail) { + throw new Error(); + } + } + return this._super(...arguments); + }, + }); + const thread = this.env.models['mail.thread'].findFromIdentifyingData({ + id: 20, + model: 'mail.channel' + }); + const threadViewer = this.env.models['mail.thread_viewer'].create({ + hasThreadView: true, + thread: [['link', thread]], + }); + await this.createThreadViewComponent(threadViewer.threadView, { hasComposer: true }); + messageFetchShouldFail = true; + await afterNextRender(() => document.querySelector('.o_MessageList_loadMore').click()); + + messageFetchShouldFail = false; + await this.afterEvent({ + eventName: 'o-thread-view-hint-processed', + func: () => document.querySelector('.o_ThreadView_alertLoadingFailedRetryButton').click(), + message: "should wait until channel 20 loaded more messages after clicked on load more", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === 'more-messages-loaded' && + threadViewer.thread.model === 'mail.channel' && + threadViewer.thread.id === 20 + ); + }, + }); +}); + }); }); }); diff --git a/addons/mail/static/src/models/thread_cache/thread_cache.js b/addons/mail/static/src/models/thread_cache/thread_cache.js index 1760a509fba..f97222307da 100644 --- a/addons/mail/static/src/models/thread_cache/thread_cache.js +++ b/addons/mail/static/src/models/thread_cache/thread_cache.js @@ -12,9 +12,6 @@ function factory(dependencies) { // Public //---------------------------------------------------------------------- - /** - * @returns {mail.message[]|undefined} - */ async loadMoreMessages() { if (this.isAllHistoryLoaded || this.isLoading) { return; @@ -26,18 +23,26 @@ function factory(dependencies) { this.update({ isLoadingMore: true }); const messageIds = this.fetchedMessages.map(message => message.id); const limit = 30; - const fetchedMessages = await this.async(() => this._loadMessages({ - extraDomain: [['id', '<', Math.min(...messageIds)]], - limit, - })); + let fetchedMessages; + let success; + try { + fetchedMessages = await this.async(() => this._loadMessages({ + extraDomain: [['id', '<', Math.min(...messageIds)]], + limit, + })); + success = true; + } catch (e) { + success = false; + } + if (success) { + if (fetchedMessages.length < limit) { + this.update({ isAllHistoryLoaded: true }); + } + for (const threadView of this.threadViews) { + threadView.addComponentHint('more-messages-loaded', { fetchedMessages }); + } + } this.update({ isLoadingMore: false }); - if (fetchedMessages.length < limit) { - this.update({ isAllHistoryLoaded: true }); - } - for (const threadView of this.threadViews) { - threadView.addComponentHint('more-messages-loaded', { fetchedMessages }); - } - return fetchedMessages; } /** @@ -56,6 +61,9 @@ function factory(dependencies) { extraDomain: [['id', '>', Math.max(...messageIds)]], limit: false, }); + if (!fetchedMessages) { + return; + } for (const threadView of this.threadViews) { threadView.addComponentHint('new-messages-loaded', { fetchedMessages }); } @@ -193,6 +201,9 @@ function factory(dependencies) { // happens during destroy or compute executed in wrong order return false; } + if (this.hasLoadingFailed) { + return false; + } const wasCacheRefreshRequested = this.isCacheRefreshRequested; // mark hint as processed if (this.isCacheRefreshRequested) { @@ -271,6 +282,7 @@ function factory(dependencies) { * @param {Array[]} [param0.extraDomain] * @param {integer} [param0.limit=30] * @returns {mail.message[]} + * @throws {Error} when failed to load messages */ async _loadMessages({ extraDomain, limit = 30 } = {}) { this.update({ isLoading: true }); @@ -284,16 +296,26 @@ function factory(dependencies) { const moderated_channel_ids = this.thread.moderation ? [this.thread.id] : undefined; - const messages = await this.async(() => - this.env.models['mail.message'].performRpcMessageFetch( - domain, - limit, - moderated_channel_ids, - context, - ) - ); + let messages; + try { + messages = await this.async(() => + this.env.models['mail.message'].performRpcMessageFetch( + domain, + limit, + moderated_channel_ids, + context, + ) + ); + } catch(e) { + this.update({ + hasLoadingFailed: true, + isLoading: false, + }); + throw e; + } this.update({ fetchedMessages: [['link', messages]], + hasLoadingFailed: false, isLoaded: true, isLoading: false, }); @@ -351,15 +373,14 @@ function factory(dependencies) { * * @private */ - _onHasToLoadMessagesChanged() { + async _onHasToLoadMessagesChanged() { if (!this.hasToLoadMessages) { return; } - this._loadMessages().then(fetchedMessages => { - for (const threadView of this.threadViews) { - threadView.addComponentHint('messages-loaded', { fetchedMessages }); - } - }); + const fetchedMessages = await this.async(() => this._loadMessages()); + for (const threadView of this.threadViews) { + threadView.addComponentHint('messages-loaded', { fetchedMessages }); + } } /** @@ -414,6 +435,12 @@ function factory(dependencies) { compute: '_computeFetchedMessages', dependencies: ['threadMessages'], }), + /** + * Determines whether the last message fetch failed. + */ + hasLoadingFailed: attr({ + default: false, + }), /** * Determines whether `this` should load initial messages. This field is * computed and should be considered read-only. @@ -422,6 +449,7 @@ function factory(dependencies) { hasToLoadMessages: attr({ compute: '_computeHasToLoadMessages', dependencies: [ + 'hasLoadingFailed', 'isCacheRefreshRequested', 'isLoaded', 'isLoading',