[FIX] mail, sms, snailmail: prevent message_fetch for hidden threads
Automatically load messages from a thread cache only if the thread cache is used in at least one thread view. Ensure thread viewer don't keep thread view records alive when they are not displayed. This change is particularly important for performance, to avoid spamming RPC at init if many threads are opened in chat windows. task-2310623 closes odoo/odoo#57121 X-original-commit: 8d3413ff647de8d98c6858062dd07dde4160af95 Related: odoo/enterprise#12979 Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com> Signed-off-by: Sébastien Theys (seb) <seb@odoo.com>
This commit is contained in:
committed by
Sébastien Theys
parent
58fb79aa78
commit
8d96bfd080
@@ -115,12 +115,10 @@ class ChatWindow extends Component {
|
||||
isDoFocus: false,
|
||||
isFocused: true,
|
||||
});
|
||||
if (this.chatWindow.isFolded) {
|
||||
return;
|
||||
}
|
||||
if (!this.chatWindow.thread) {
|
||||
if (this._inputRef.comp) {
|
||||
this._inputRef.comp.focus();
|
||||
} else {
|
||||
}
|
||||
if (this._threadRef.comp) {
|
||||
this._threadRef.comp.focus();
|
||||
}
|
||||
}
|
||||
@@ -134,7 +132,10 @@ class ChatWindow extends Component {
|
||||
* @private
|
||||
*/
|
||||
_saveThreadScrollTop() {
|
||||
this.chatWindow.threadView.saveThreadCacheScrollPositionsAsInitial(
|
||||
if (!this._threadRef.comp || !this.chatWindow.threadViewer) {
|
||||
return;
|
||||
}
|
||||
this.chatWindow.threadViewer.saveThreadCacheScrollPositionsAsInitial(
|
||||
this._threadRef.comp.getScrollTop()
|
||||
);
|
||||
}
|
||||
@@ -248,9 +249,7 @@ class ChatWindow extends Component {
|
||||
this.chatWindow.unfold();
|
||||
this.chatWindow.focus();
|
||||
} else {
|
||||
if (this.chatWindow.thread) {
|
||||
this._saveThreadScrollTop();
|
||||
}
|
||||
this._saveThreadScrollTop();
|
||||
this.chatWindow.fold();
|
||||
}
|
||||
}
|
||||
@@ -336,12 +335,7 @@ class ChatWindow extends Component {
|
||||
* @private
|
||||
*/
|
||||
async _onWillHideHomeMenu() {
|
||||
if (!this.chatWindow.thread) {
|
||||
return;
|
||||
}
|
||||
if (!this.chatWindow.isFolded) {
|
||||
this._saveThreadScrollTop();
|
||||
}
|
||||
this._saveThreadScrollTop();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -353,12 +347,7 @@ class ChatWindow extends Component {
|
||||
* @private
|
||||
*/
|
||||
async _onWillShowHomeMenu() {
|
||||
if (!this.chatWindow.thread) {
|
||||
return;
|
||||
}
|
||||
if (!this.chatWindow.isFolded) {
|
||||
this._saveThreadScrollTop();
|
||||
}
|
||||
this._saveThreadScrollTop();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -20,35 +20,33 @@
|
||||
t-on-o-clicked="_onClickedHeader"
|
||||
t-ref="header"
|
||||
/>
|
||||
<t t-if="!chatWindow.isFolded">
|
||||
<t t-if="chatWindow.thread">
|
||||
<ThreadView
|
||||
class="o_ChatWindow_thread"
|
||||
composerAttachmentsDetailsMode="'card'"
|
||||
hasComposer="chatWindow.thread.model !== 'mail.box' and (!chatWindow.thread.mass_mailing or env.messaging.device.isMobile)"
|
||||
hasComposerCurrentPartnerAvatar="false"
|
||||
hasComposerSendButton="env.messaging.device.isMobile"
|
||||
hasSquashCloseMessages="chatWindow.thread.model !== 'mail.box'"
|
||||
threadViewLocalId="chatWindow.threadView.localId"
|
||||
t-on-focusin="_onFocusinThread"
|
||||
t-ref="thread"
|
||||
<t t-if="chatWindow.threadView">
|
||||
<ThreadView
|
||||
class="o_ChatWindow_thread"
|
||||
composerAttachmentsDetailsMode="'card'"
|
||||
hasComposer="chatWindow.thread.model !== 'mail.box' and (!chatWindow.thread.mass_mailing or env.messaging.device.isMobile)"
|
||||
hasComposerCurrentPartnerAvatar="false"
|
||||
hasComposerSendButton="env.messaging.device.isMobile"
|
||||
hasSquashCloseMessages="chatWindow.thread.model !== 'mail.box'"
|
||||
threadViewLocalId="chatWindow.threadView.localId"
|
||||
t-on-focusin="_onFocusinThread"
|
||||
t-ref="thread"
|
||||
/>
|
||||
</t>
|
||||
<t t-if="chatWindow.hasNewMessageForm">
|
||||
<div class="o_ChatWindow_newMessageForm">
|
||||
<span class="o_ChatWindow_newMessageFormLabel">
|
||||
To:
|
||||
</span>
|
||||
<AutocompleteInput
|
||||
class="o_ChatWindow_newMessageFormInput"
|
||||
isHtml="true"
|
||||
placeholder="newMessageFormInputPlaceholder"
|
||||
select="_onAutocompleteSelect"
|
||||
source="_onAutocompleteSource"
|
||||
t-ref="input"
|
||||
/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="o_ChatWindow_newMessageForm">
|
||||
<span class="o_ChatWindow_newMessageFormLabel">
|
||||
To:
|
||||
</span>
|
||||
<AutocompleteInput
|
||||
class="o_ChatWindow_newMessageFormInput"
|
||||
isHtml="true"
|
||||
placeholder="newMessageFormInputPlaceholder"
|
||||
select="_onAutocompleteSelect"
|
||||
source="_onAutocompleteSource"
|
||||
t-ref="input"
|
||||
/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
@@ -1753,6 +1753,122 @@ QUnit.test('[technical] chat window with a thread: keep scroll position in messa
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('chat window does not fetch messages if hidden', async function (assert) {
|
||||
/**
|
||||
* computation uses following info:
|
||||
* ([mocked] global window width: 900px)
|
||||
* (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: 1080px
|
||||
*
|
||||
* Enough space for 2 visible chat windows, and one hidden chat window:
|
||||
* 3 visible chat windows:
|
||||
* 10 + 325 + 5 + 325 + 5 + 325 + 10 = 1000 > 900
|
||||
* 2 visible chat windows + hidden menu:
|
||||
* 10 + 325 + 5 + 325 + 10 + 200 + 5 = 875 < 900
|
||||
*/
|
||||
assert.expect(14);
|
||||
|
||||
// 3 channels are expected to be found in the messaging menu, each with a
|
||||
// random unique id that will be referenced in the test
|
||||
this.data['mail.channel'].records = [
|
||||
{
|
||||
id: 10,
|
||||
is_minimized: true,
|
||||
name: "Channel #10",
|
||||
state: 'open',
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
is_minimized: true,
|
||||
name: "Channel #11",
|
||||
state: 'open',
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
is_minimized: true,
|
||||
name: "Channel #12",
|
||||
state: 'open',
|
||||
},
|
||||
];
|
||||
await this.start({
|
||||
env: {
|
||||
browser: {
|
||||
innerWidth: 900,
|
||||
},
|
||||
},
|
||||
mockRPC(route, args) {
|
||||
if (args.method === 'message_fetch') {
|
||||
// domain should be like [['channel_id', 'in', [X]]] with X the channel id
|
||||
const channel_ids = args.kwargs.domain[0][2];
|
||||
assert.strictEqual(channel_ids.length, 1, "messages should be fetched channel per channel");
|
||||
assert.step(`rpc:message_fetch:${channel_ids[0]}`);
|
||||
}
|
||||
return this._super(...arguments);
|
||||
},
|
||||
});
|
||||
|
||||
assert.containsN(
|
||||
document.body,
|
||||
'.o_ChatWindow',
|
||||
2,
|
||||
"2 chat windows should be visible"
|
||||
);
|
||||
assert.containsNone(
|
||||
document.body,
|
||||
`.o_ChatWindow[data-thread-local-id="${
|
||||
this.env.models['mail.thread'].find(t =>
|
||||
t.model === 'mail.channel' && t.id === 12
|
||||
).localId
|
||||
}"]`,
|
||||
"chat window for Channel #12 should be hidden"
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_ChatWindowHiddenMenu',
|
||||
"chat window hidden menu should be displayed"
|
||||
);
|
||||
assert.verifySteps(
|
||||
['rpc:message_fetch:10', 'rpc:message_fetch:11'],
|
||||
"messages should be fetched for the two visible chat windows"
|
||||
);
|
||||
|
||||
await afterNextRender(() =>
|
||||
document.querySelector('.o_ChatWindowHiddenMenu_dropdownToggle').click()
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_ChatWindowHiddenMenu_chatWindowHeader',
|
||||
"1 hidden chat window should be listed in hidden menu"
|
||||
);
|
||||
|
||||
await afterNextRender(() =>
|
||||
document.querySelector('.o_ChatWindowHiddenMenu_chatWindowHeader').click()
|
||||
);
|
||||
assert.containsN(
|
||||
document.body,
|
||||
'.o_ChatWindow',
|
||||
2,
|
||||
"2 chat windows should still be visible"
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
`.o_ChatWindow[data-thread-local-id="${
|
||||
this.env.models['mail.thread'].find(t =>
|
||||
t.model === 'mail.channel' && t.id === 12
|
||||
).localId
|
||||
}"]`,
|
||||
"chat window for Channel #12 should now be visible"
|
||||
);
|
||||
assert.verifySteps(
|
||||
['rpc:message_fetch:12'],
|
||||
"messages should now be fetched for Channel #12"
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
class="o_Chatter_topbar"
|
||||
chatterLocalId="chatter.localId"
|
||||
/>
|
||||
<t t-if="chatter.hasThread and chatter.thread and chatter.isComposerVisible">
|
||||
<t t-if="chatter.threadView and chatter.isComposerVisible">
|
||||
<Composer
|
||||
class="o_Chatter_composer"
|
||||
t-att-class="{ 'o-bordered': chatter.hasExternalBorder }"
|
||||
@@ -27,22 +27,21 @@
|
||||
threadLocalId="chatter.thread.localId"
|
||||
/>
|
||||
</t>
|
||||
<t t-if="chatter.thread">
|
||||
<t t-if="chatter.hasActivities and chatter.activities.length > 0">
|
||||
<ActivityBox
|
||||
class="o_Chatter_activityBox"
|
||||
chatterLocalId="chatter.localId"
|
||||
/>
|
||||
</t>
|
||||
<t t-if="chatter.hasThread">
|
||||
<ThreadView
|
||||
class="o_Chatter_thread"
|
||||
hasComposer="false"
|
||||
hasScrollAdjust="chatter.hasMessageListScrollAdjust"
|
||||
order="'desc'"
|
||||
threadViewLocalId="chatter.threadView.localId"
|
||||
/>
|
||||
</t>
|
||||
<t t-if="chatter.thread and chatter.hasActivities and chatter.activities.length > 0">
|
||||
<ActivityBox
|
||||
class="o_Chatter_activityBox"
|
||||
chatterLocalId="chatter.localId"
|
||||
/>
|
||||
</t>
|
||||
<t t-if="chatter.threadView">
|
||||
<ThreadView
|
||||
class="o_Chatter_thread"
|
||||
hasComposer="false"
|
||||
hasScrollAdjust="chatter.hasMessageListScrollAdjust"
|
||||
order="'desc'"
|
||||
threadViewLocalId="chatter.threadView.localId"
|
||||
t-ref="thread"
|
||||
/>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<div class="o_ChatterTopbar">
|
||||
<t t-if="chatter">
|
||||
<div class="o_ChatterTopbar_actions" t-att-class="{'o-has-active-button': chatter.isComposerVisible }">
|
||||
<t t-if="chatter.hasThread">
|
||||
<t t-if="chatter.threadView">
|
||||
<button class="btn btn-link o_ChatterTopbar_button o_ChatterTopbar_buttonSendMessage"
|
||||
type="button"
|
||||
t-att-class="{
|
||||
|
||||
@@ -26,18 +26,17 @@ class Discuss extends Component {
|
||||
super(...args);
|
||||
useStore(props => {
|
||||
const discuss = this.env.messaging && this.env.messaging.discuss;
|
||||
const threadView = discuss && discuss.threadView;
|
||||
return {
|
||||
checkedMessages: discuss ? discuss.threadView.checkedMessages.map(message => message.__state) : [],
|
||||
checkedMessages: threadView ? threadView.checkedMessages.map(message => message.__state) : [],
|
||||
discuss: discuss ? discuss.__state : undefined,
|
||||
isDeviceMobile: this.env.messaging && this.env.messaging.device.isMobile,
|
||||
isMessagingInitialized: this.env.isMessagingInitialized(),
|
||||
thread: discuss && discuss.thread ? discuss.thread.__state : undefined,
|
||||
threadCache: (discuss && discuss.threadView && discuss.threadView.threadCache)
|
||||
? discuss.threadView.threadCache.__state
|
||||
threadCache: (threadView && threadView.threadCache)
|
||||
? threadView.threadCache.__state
|
||||
: undefined,
|
||||
uncheckedMessages: discuss && discuss.threadView
|
||||
? discuss.threadView.uncheckedMessages.map(message => message.__state)
|
||||
: [],
|
||||
uncheckedMessages: threadView ? threadView.uncheckedMessages.map(message => message.__state) : [],
|
||||
};
|
||||
}, {
|
||||
compareDepth: {
|
||||
@@ -78,12 +77,13 @@ class Discuss extends Component {
|
||||
if (
|
||||
this.discuss.thread &&
|
||||
this.discuss.thread === this.env.messaging.inbox &&
|
||||
this.discuss.threadView &&
|
||||
this._lastThreadCache === this.discuss.threadView.threadCache.localId &&
|
||||
this._lastThreadCounter > 0 && this.discuss.thread.counter === 0
|
||||
) {
|
||||
this.trigger('o-show-rainbow-man');
|
||||
}
|
||||
this._activeThreadCache = this.discuss.threadView.threadCache;
|
||||
this._activeThreadCache = this.discuss.threadView && this.discuss.threadView.threadCache;
|
||||
this._updateLocalStoreProps();
|
||||
this._update();
|
||||
}
|
||||
@@ -170,6 +170,7 @@ class Discuss extends Component {
|
||||
* rainbox man on inbox.
|
||||
*/
|
||||
this._lastThreadCache = (
|
||||
this.discuss.threadView &&
|
||||
this.discuss.threadView.threadCache &&
|
||||
this.discuss.threadView.threadCache.localId
|
||||
);
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
/>
|
||||
</div>
|
||||
</t>
|
||||
<t t-if="(env.messaging.device.isMobile and discuss.activeMobileNavbarTabId === 'mailbox' and discuss.thread and discuss.thread.model === 'mail.box') or (!env.messaging.device.isMobile and discuss.thread)">
|
||||
<t t-if="discuss.threadView">
|
||||
<ThreadView
|
||||
class="o_Discuss_thread"
|
||||
t-att-class="{ 'o-mobile': env.messaging.device.isMobile }"
|
||||
|
||||
@@ -126,8 +126,8 @@ QUnit.test('sidebar: pinned channel 3: open pinned channel and unpin it', async
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
`.o_Discuss_thread[data-thread-local-id="${this.env.messaging.inbox.localId}"]`,
|
||||
"The Inbox is opened in discuss"
|
||||
'.o_Discuss_noThread',
|
||||
"should have no thread opened in discuss"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -182,8 +182,8 @@ QUnit.test('sidebar: unpin channel from bus', async function (assert) {
|
||||
});
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
`.o_Discuss_thread[data-thread-local-id="${this.env.messaging.inbox.localId}"]`,
|
||||
"The Inbox is opened in discuss"
|
||||
'.o_Discuss_noThread',
|
||||
"should have no thread opened in discuss"
|
||||
);
|
||||
assert.containsNone(
|
||||
document.body,
|
||||
|
||||
@@ -3,6 +3,7 @@ odoo.define('mail/static/src/components/discuss/tests/discuss_tests.js', functio
|
||||
|
||||
const BusService = require('bus.BusService');
|
||||
|
||||
const { makeDeferred } = require('mail/static/src/utils/deferred/deferred.js');
|
||||
const {
|
||||
afterEach,
|
||||
afterNextRender,
|
||||
@@ -1115,7 +1116,7 @@ QUnit.test('default thread rendering', async function (assert) {
|
||||
});
|
||||
|
||||
QUnit.test('initially load messages from inbox', async function (assert) {
|
||||
assert.expect(5);
|
||||
assert.expect(4);
|
||||
|
||||
await this.start({
|
||||
async mockRPC(route, args) {
|
||||
@@ -1126,13 +1127,8 @@ QUnit.test('initially load messages from inbox', async function (assert) {
|
||||
30,
|
||||
"should fetch up to 30 messages"
|
||||
);
|
||||
assert.strictEqual(
|
||||
args.args.length,
|
||||
1,
|
||||
"should have a single item in args"
|
||||
);
|
||||
assert.deepEqual(
|
||||
args.args[0],
|
||||
args.kwargs.domain,
|
||||
[["needaction", "=", true]],
|
||||
"should fetch needaction messages"
|
||||
);
|
||||
@@ -1186,7 +1182,7 @@ QUnit.test('auto-select thread in discuss context', async function (assert) {
|
||||
});
|
||||
|
||||
QUnit.test('load single message from channel initially', async function (assert) {
|
||||
assert.expect(8);
|
||||
assert.expect(7);
|
||||
|
||||
// channel expected to be rendered, with a random unique id that will be referenced in the test
|
||||
this.data['mail.channel'].records.push({ id: 20 });
|
||||
@@ -1210,13 +1206,8 @@ QUnit.test('load single message from channel initially', async function (assert)
|
||||
30,
|
||||
"should fetch up to 30 messages"
|
||||
);
|
||||
assert.strictEqual(
|
||||
args.args.length,
|
||||
1,
|
||||
"should have a single item in args"
|
||||
);
|
||||
assert.deepEqual(
|
||||
args.args[0],
|
||||
args.kwargs.domain,
|
||||
[["channel_ids", "in", [20]]],
|
||||
"should fetch messages from channel"
|
||||
);
|
||||
@@ -1829,7 +1820,7 @@ QUnit.test('new messages separator [REQUIRE FOCUS]', async function (assert) {
|
||||
});
|
||||
|
||||
QUnit.test('restore thread scroll position', async function (assert) {
|
||||
assert.expect(4);
|
||||
assert.expect(5);
|
||||
// channels expected to be rendered, with random unique id that will be referenced in the test
|
||||
this.data['mail.channel'].records.push({ id: 11 }, { id: 12 });
|
||||
for (let i = 1; i <= 25; i++) {
|
||||
@@ -1839,26 +1830,43 @@ QUnit.test('restore thread scroll position', async function (assert) {
|
||||
res_id: 11,
|
||||
});
|
||||
}
|
||||
for (let i = 1; i <= 25; i++) {
|
||||
for (let i = 1; i <= 24; i++) {
|
||||
this.data['mail.message'].records.push({
|
||||
channel_ids: [12],
|
||||
model: 'mail.channel',
|
||||
res_id: 12,
|
||||
});
|
||||
}
|
||||
await this.start({
|
||||
discuss: {
|
||||
params: {
|
||||
default_active_id: 'mail.channel_11',
|
||||
const messageFetchChannel11Def = makeDeferred();
|
||||
const messageFetchChannel12Def = makeDeferred();
|
||||
await afterNextRender(async () => {
|
||||
await this.start({
|
||||
discuss: {
|
||||
params: {
|
||||
default_active_id: 'mail.channel_11',
|
||||
},
|
||||
},
|
||||
},
|
||||
async mockRPC(route, args) {
|
||||
const res = await this._super(...arguments);
|
||||
// domain should be like [['channel_id', 'in', [X]]] with X the channel id
|
||||
if (route.includes('message_fetch') && args.kwargs.domain[0][2].includes(11)) {
|
||||
messageFetchChannel11Def.resolve();
|
||||
}
|
||||
if (route.includes('message_fetch') && args.kwargs.domain[0][2].includes(12)) {
|
||||
messageFetchChannel12Def.resolve();
|
||||
}
|
||||
return res;
|
||||
},
|
||||
});
|
||||
// wait until messages are fetched, ignore other renders that are too early
|
||||
await messageFetchChannel11Def;
|
||||
});
|
||||
assert.strictEqual(
|
||||
document.querySelectorAll(`
|
||||
.o_Discuss_thread .o_ThreadView_messageList .o_MessageList_message
|
||||
`).length,
|
||||
25,
|
||||
"should have 25 messages"
|
||||
"should have 25 messages in channel 11"
|
||||
);
|
||||
|
||||
// scroll to top of channel11
|
||||
@@ -1871,8 +1879,13 @@ QUnit.test('restore thread scroll position', async function (assert) {
|
||||
"should have scrolled to top of thread"
|
||||
);
|
||||
|
||||
// select channel12
|
||||
await afterNextRender(() =>
|
||||
const scrollChannel12Def = makeDeferred();
|
||||
const onScroll = () => {
|
||||
scrollChannel12Def.resolve();
|
||||
};
|
||||
document.addEventListener('o-message-list-scrolled', onScroll);
|
||||
// select channel 12
|
||||
await afterNextRender(async () => {
|
||||
document.querySelector(`
|
||||
.o_DiscussSidebar_groupChannel
|
||||
.o_DiscussSidebar_item[data-thread-local-id="${
|
||||
@@ -1881,9 +1894,26 @@ QUnit.test('restore thread scroll position', async function (assert) {
|
||||
thread.model === 'mail.channel'
|
||||
).localId
|
||||
}"]
|
||||
`).click()
|
||||
`).click();
|
||||
// wait until messages are fetched, ignore other renders that are too early
|
||||
await messageFetchChannel12Def;
|
||||
});
|
||||
assert.strictEqual(
|
||||
document.querySelectorAll(`
|
||||
.o_Discuss_thread .o_ThreadView_messageList .o_MessageList_message
|
||||
`).length,
|
||||
24,
|
||||
"should have 24 messages in channel 12"
|
||||
);
|
||||
// select channel11
|
||||
|
||||
// Ensure scrollIntoView of channel 12 has enough time to complete before
|
||||
// going back to channel 11. Await is needed to prevent the scrollIntoView
|
||||
// initially planned for channel 12 to actually apply on channel 11.
|
||||
// task-2333535
|
||||
await scrollChannel12Def;
|
||||
document.removeEventListener('o-message-list-scrolled', onScroll);
|
||||
|
||||
// select channel 11
|
||||
await afterNextRender(() =>
|
||||
document.querySelector(`
|
||||
.o_DiscussSidebar_groupChannel
|
||||
@@ -1898,7 +1928,7 @@ QUnit.test('restore thread scroll position', async function (assert) {
|
||||
assert.strictEqual(
|
||||
document.querySelector(`.o_Discuss_thread .o_ThreadView_messageList`).scrollTop,
|
||||
0,
|
||||
"should have recovered scroll position of channel11 (scroll to top)"
|
||||
"should have recovered scroll position of channel 11 (scroll to top)"
|
||||
);
|
||||
|
||||
// select channel12
|
||||
@@ -1920,7 +1950,7 @@ QUnit.test('restore thread scroll position', async function (assert) {
|
||||
assert.strictEqual(
|
||||
messageList.scrollTop + messageList.clientHeight,
|
||||
messageList.scrollHeight,
|
||||
"should have recovered scroll position of channel12 (scroll to bottom)"
|
||||
"should have recovered scroll position of channel 12 (scroll to bottom)"
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -190,7 +190,8 @@ QUnit.test('Notification Sent', async function (assert) {
|
||||
assert.expect(9);
|
||||
|
||||
await this.start();
|
||||
const threadView = this.env.models['mail.thread_view'].create({
|
||||
const threadViewer = this.env.models['mail.thread_viewer'].create({
|
||||
hasThreadView: true,
|
||||
thread: [['create', {
|
||||
id: 11,
|
||||
model: 'mail.channel',
|
||||
@@ -205,10 +206,10 @@ QUnit.test('Notification Sent', async function (assert) {
|
||||
notification_type: 'email',
|
||||
partner: [['insert', { id: 12, name: "Someone" }]],
|
||||
}]],
|
||||
originThread: [['link', threadView.thread]],
|
||||
originThread: [['link', threadViewer.thread]],
|
||||
});
|
||||
await this.createMessageComponent(message, {
|
||||
threadViewLocalId: threadView.localId
|
||||
threadViewLocalId: threadViewer.threadView.localId
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
@@ -281,7 +282,8 @@ QUnit.test('Notification Error', async function (assert) {
|
||||
});
|
||||
|
||||
await this.start({ env: { bus } });
|
||||
const threadView = this.env.models['mail.thread_view'].create({
|
||||
const threadViewer = this.env.models['mail.thread_viewer'].create({
|
||||
hasThreadView: true,
|
||||
thread: [['create', {
|
||||
id: 11,
|
||||
model: 'mail.channel',
|
||||
@@ -295,10 +297,10 @@ QUnit.test('Notification Error', async function (assert) {
|
||||
notification_status: 'exception',
|
||||
notification_type: 'email',
|
||||
}]],
|
||||
originThread: [['link', threadView.thread]],
|
||||
originThread: [['link', threadViewer.thread]],
|
||||
});
|
||||
await this.createMessageComponent(message, {
|
||||
threadViewLocalId: threadView.localId
|
||||
threadViewLocalId: threadViewer.threadView.localId
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
@@ -347,7 +349,10 @@ QUnit.test("'channel_fetch' notification received is correctly handled", async f
|
||||
],
|
||||
model: 'mail.channel',
|
||||
});
|
||||
const threadView = this.env.models['mail.thread_view'].create({ thread: [['link', thread]] });
|
||||
const threadViewer = this.env.models['mail.thread_viewer'].create({
|
||||
hasThreadView: true,
|
||||
thread: [['link', thread]],
|
||||
});
|
||||
const message = this.env.models['mail.message'].create({
|
||||
author: [['link', currentPartner]],
|
||||
body: "<p>Test</p>",
|
||||
@@ -355,7 +360,9 @@ QUnit.test("'channel_fetch' notification received is correctly handled", async f
|
||||
originThread: [['link', thread]],
|
||||
});
|
||||
|
||||
await this.createMessageComponent(message, { threadViewLocalId: threadView.localId });
|
||||
await this.createMessageComponent(message, {
|
||||
threadViewLocalId: threadViewer.threadView.localId,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
@@ -403,14 +410,19 @@ QUnit.test("'channel_seen' notification received is correctly handled", async fu
|
||||
],
|
||||
model: 'mail.channel',
|
||||
});
|
||||
const threadView = this.env.models['mail.thread_view'].create({ thread: [['link', thread]] });
|
||||
const threadViewer = this.env.models['mail.thread_viewer'].create({
|
||||
hasThreadView: true,
|
||||
thread: [['link', thread]],
|
||||
});
|
||||
const message = this.env.models['mail.message'].create({
|
||||
author: [['link', currentPartner]],
|
||||
body: "<p>Test</p>",
|
||||
id: 100,
|
||||
originThread: [['link', thread]],
|
||||
});
|
||||
await this.createMessageComponent(message, { threadViewLocalId: threadView.localId });
|
||||
await this.createMessageComponent(message, {
|
||||
threadViewLocalId: threadViewer.threadView.localId,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
@@ -458,14 +470,19 @@ QUnit.test("'channel_fetch' notification then 'channel_seen' received are corre
|
||||
],
|
||||
model: 'mail.channel',
|
||||
});
|
||||
const threadView = this.env.models['mail.thread_view'].create({ thread: [['link', thread]] });
|
||||
const threadViewer = this.env.models['mail.thread_viewer'].create({
|
||||
hasThreadView: true,
|
||||
thread: [['link', thread]],
|
||||
});
|
||||
const message = this.env.models['mail.message'].create({
|
||||
author: [['link', currentPartner]],
|
||||
body: "<p>Test</p>",
|
||||
id: 100,
|
||||
originThread: [['link', thread]],
|
||||
});
|
||||
await this.createMessageComponent(message, { threadViewLocalId: threadView.localId });
|
||||
await this.createMessageComponent(message, {
|
||||
threadViewLocalId: threadViewer.threadView.localId,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
@@ -527,25 +544,28 @@ QUnit.test('do not show messaging seen indicator if not authored by me', async f
|
||||
partnerSeenInfos: [['create', [
|
||||
{
|
||||
id: this.env.session.partner_id,
|
||||
lastFetchedMessage: [['insert', {id: 100}]],
|
||||
partner: [['insert', {id: this.env.messaging.currentPartner.id}]],
|
||||
lastFetchedMessage: [['insert', { id: 100 }]],
|
||||
partner: [['insert', { id: this.env.messaging.currentPartner.id }]],
|
||||
},
|
||||
{
|
||||
id: 100,
|
||||
lastFetchedMessage: [['insert', {id: 100}]],
|
||||
lastFetchedMessage: [['insert', { id: 100 }]],
|
||||
partner: [['link', author]],
|
||||
},
|
||||
]]],
|
||||
model: 'mail.channel',
|
||||
});
|
||||
const threadView = this.env.models['mail.thread_view'].create({ thread: [['link', thread]] });
|
||||
const threadViewer = this.env.models['mail.thread_viewer'].create({
|
||||
hasThreadView: true,
|
||||
thread: [['link', thread]],
|
||||
});
|
||||
const message = this.env.models['mail.message'].insert({
|
||||
author: [['link', author]],
|
||||
body: "<p>Test</p>",
|
||||
id: 100,
|
||||
originThread: [['link', thread]],
|
||||
});
|
||||
await this.createMessageComponent(message, { threadViewLocalId: threadView.localId });
|
||||
await this.createMessageComponent(message, { threadViewLocalId: threadViewer.threadView.localId });
|
||||
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
@@ -571,11 +591,14 @@ QUnit.test('do not show messaging seen indicator if before last seen by all mess
|
||||
id: 11,
|
||||
messageSeenIndicators: [['insert', {
|
||||
id: this.env.models['mail.message_seen_indicator'].computeId(99, 11),
|
||||
message: [['insert', {id: 99}]],
|
||||
message: [['insert', { id: 99 }]],
|
||||
}]],
|
||||
model: 'mail.channel',
|
||||
});
|
||||
const threadView = this.env.models['mail.thread_view'].create({ thread: [['link', thread]] });
|
||||
const threadViewer = this.env.models['mail.thread_viewer'].create({
|
||||
hasThreadView: true,
|
||||
thread: [['link', thread]],
|
||||
});
|
||||
const lastSeenMessage = this.env.models['mail.message'].create({
|
||||
author: [['link', currentPartner]],
|
||||
body: "<p>You already saw me</p>",
|
||||
@@ -598,11 +621,13 @@ QUnit.test('do not show messaging seen indicator if before last seen by all mess
|
||||
{
|
||||
id: 100,
|
||||
lastSeenMessage: [['link', lastSeenMessage]],
|
||||
partner: [['insert', {id: 100}]],
|
||||
partner: [['insert', { id: 100 }]],
|
||||
},
|
||||
]]],
|
||||
});
|
||||
await this.createMessageComponent(message, { threadViewLocalId: threadView.localId });
|
||||
await this.createMessageComponent(message, {
|
||||
threadViewLocalId: threadViewer.threadView.localId,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
@@ -634,30 +659,35 @@ QUnit.test('only show messaging seen indicator if authored by me, after last see
|
||||
partnerSeenInfos: [['create', [
|
||||
{
|
||||
id: currentPartner.id,
|
||||
lastSeenMessage: [['insert', {id: 100}]],
|
||||
lastSeenMessage: [['insert', { id: 100 }]],
|
||||
partner: [['link', currentPartner]],
|
||||
},
|
||||
{
|
||||
id: 100,
|
||||
partner: [['insert', {id: 100}]],
|
||||
lastFetchedMessage: [['insert', {id: 100}]],
|
||||
lastSeenMessage: [['insert', {id: 99}]],
|
||||
partner: [['insert', { id: 100 }]],
|
||||
lastFetchedMessage: [['insert', { id: 100 }]],
|
||||
lastSeenMessage: [['insert', { id: 99 }]],
|
||||
},
|
||||
]]],
|
||||
messageSeenIndicators: [['insert', {
|
||||
id: this.env.models['mail.message_seen_indicator'].computeId(100, 11),
|
||||
message: [['insert', {id: 100}]],
|
||||
message: [['insert', { id: 100 }]],
|
||||
}]],
|
||||
model: 'mail.channel',
|
||||
});
|
||||
const threadView = this.env.models['mail.thread_view'].create({ thread: [['link', thread]] });
|
||||
const threadViewer = this.env.models['mail.thread_viewer'].create({
|
||||
hasThreadView: true,
|
||||
thread: [['link', thread]],
|
||||
});
|
||||
const message = this.env.models['mail.message'].insert({
|
||||
author: [['link', currentPartner]],
|
||||
body: "<p>Test</p>",
|
||||
id: 100,
|
||||
originThread: [['link', thread]],
|
||||
});
|
||||
await this.createMessageComponent(message, { threadViewLocalId: threadView.localId });
|
||||
await this.createMessageComponent(message, {
|
||||
threadViewLocalId: threadViewer.threadView.localId,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
|
||||
@@ -51,17 +51,6 @@ class MessageList extends Component {
|
||||
* on scroll when it becomes visible.
|
||||
*/
|
||||
this._loadMoreRef = useRef('loadMore');
|
||||
/**
|
||||
* Tracked last thread cache rendered. Useful to determine scroll
|
||||
* position on patch if it is on the same thread cache or not.
|
||||
*/
|
||||
this._renderedThreadCache = null;
|
||||
/**
|
||||
* Tracked last selected message. Useful to determine when patch comes
|
||||
* from a message selection on a given thread cache, so that it
|
||||
* auto-scroll to that message.
|
||||
*/
|
||||
this._selectedMessage = null;
|
||||
/**
|
||||
* Snapshot computed during willPatch, which is used by patched.
|
||||
*/
|
||||
@@ -121,14 +110,14 @@ class MessageList extends Component {
|
||||
* fixed height, which is the case for the moment.
|
||||
*/
|
||||
async adjustFromComponentHints() {
|
||||
if (!this.threadView) {
|
||||
return;
|
||||
}
|
||||
for (const hint of this.threadView.componentHintList) {
|
||||
switch (hint.type) {
|
||||
case 'change-of-thread-cache':
|
||||
this._adjustFromChangeOfThreadCache(hint);
|
||||
break;
|
||||
case 'chat-window-unfolded':
|
||||
this._adjustFromChatWindowUnfolded(hint);
|
||||
break;
|
||||
case 'current-partner-just-posted-message':
|
||||
this._adjustFromCurrentPartnerJustPostedMessage(hint);
|
||||
break;
|
||||
@@ -427,6 +416,9 @@ class MessageList extends Component {
|
||||
* @private
|
||||
*/
|
||||
_checkMostRecentMessageIsVisible() {
|
||||
if (!this.threadView) {
|
||||
return;
|
||||
}
|
||||
const thread = this.threadView.thread;
|
||||
const threadCache = this.threadView.threadCache;
|
||||
const lastMessageIsVisible =
|
||||
@@ -531,7 +523,11 @@ class MessageList extends Component {
|
||||
// could be unmounted in the meantime (due to throttled behavior)
|
||||
return;
|
||||
}
|
||||
this.threadView.saveThreadCacheScrollPositionsAsInitial(this.el.scrollTop);
|
||||
if (!this.threadView || !this.threadView.threadViewer) {
|
||||
return;
|
||||
}
|
||||
this.trigger('o-message-list-scrolled');
|
||||
this.threadView.threadViewer.saveThreadCacheScrollPositionsAsInitial(this.el.scrollTop);
|
||||
if (!this._isAutoLoadOnScrollActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
<t t-name="mail.ThreadView" owl="1">
|
||||
<div class="o_ThreadView" t-att-data-correspondent-id="threadView and threadView.thread and threadView.thread.correspondent and threadView.thread.correspondent.id" t-att-data-thread-local-id="threadView and threadView.thread and threadView.thread.localId">
|
||||
<t t-if="threadView">
|
||||
<t t-if="threadView.isShowingLoading" name="loadingCondition">
|
||||
<t t-if="threadView.isLoading" name="loadingCondition">
|
||||
<div class="o_ThreadView_loading">
|
||||
<span><i class="o_ThreadView_loadingIcon fa fa-spinner fa-spin" title="Loading..." role="img"/>Loading...</span>
|
||||
</div>
|
||||
</t>
|
||||
<t t-elif="threadView.threadCache.isLoaded">
|
||||
<t t-elif="threadView.threadCache.isLoaded or threadView.thread.isTemporary">
|
||||
<MessageList
|
||||
class="o_ThreadView_messageList"
|
||||
hasMessageCheckbox="props.hasMessageCheckbox"
|
||||
|
||||
@@ -81,8 +81,11 @@ QUnit.test('dragover files on thread with composer', async function (assert) {
|
||||
name: "General",
|
||||
public: 'public',
|
||||
});
|
||||
const threadView = this.env.models['mail.thread_view'].create({ thread: [['link', thread]] });
|
||||
await this.createThreadViewComponent(threadView, { hasComposer: true });
|
||||
const threadViewer = this.env.models['mail.thread_viewer'].create({
|
||||
hasThreadView: true,
|
||||
thread: [['link', thread]],
|
||||
});
|
||||
await this.createThreadViewComponent(threadViewer.threadView, { hasComposer: true });
|
||||
await afterNextRender(() =>
|
||||
dragenterFiles(document.querySelector('.o_ThreadView'))
|
||||
);
|
||||
@@ -122,8 +125,11 @@ QUnit.test('message list desc order', async function (assert) {
|
||||
name: "General",
|
||||
public: 'public',
|
||||
});
|
||||
const threadView = this.env.models['mail.thread_view'].create({ thread: [['link', thread]] });
|
||||
await this.createThreadViewComponent(threadView, { order: 'desc' }, { isFixedSize: true });
|
||||
const threadViewer = this.env.models['mail.thread_viewer'].create({
|
||||
hasThreadView: true,
|
||||
thread: [['link', thread]],
|
||||
});
|
||||
await this.createThreadViewComponent(threadViewer.threadView, { order: 'desc' }, { isFixedSize: true });
|
||||
const messageItems = document.querySelectorAll(`.o_MessageList_item`);
|
||||
assert.notOk(
|
||||
messageItems[0].classList.contains("o_MessageList_loadMore"),
|
||||
@@ -190,8 +196,11 @@ QUnit.test('message list asc order', async function (assert) {
|
||||
name: "General",
|
||||
public: 'public',
|
||||
});
|
||||
const threadView = this.env.models['mail.thread_view'].create({ thread: [['link', thread]] });
|
||||
await this.createThreadViewComponent(threadView, { order: 'asc' }, { isFixedSize: true });
|
||||
const threadViewer = this.env.models['mail.thread_viewer'].create({
|
||||
hasThreadView: true,
|
||||
thread: [['link', thread]],
|
||||
});
|
||||
await this.createThreadViewComponent(threadViewer.threadView, { order: 'asc' }, { isFixedSize: true });
|
||||
const messageItems = document.querySelectorAll(`.o_MessageList_item`);
|
||||
assert.notOk(
|
||||
messageItems[messageItems.length - 1].classList.contains("o_MessageList_loadMore"),
|
||||
@@ -283,8 +292,11 @@ QUnit.test('mark channel as fetched when a new message is loaded and as seen whe
|
||||
model: 'mail.channel',
|
||||
});
|
||||
|
||||
const threadView = this.env.models['mail.thread_view'].create({ thread: [['link', thread]] });
|
||||
await this.createThreadViewComponent(threadView, { hasComposer: true });
|
||||
const threadViewer = this.env.models['mail.thread_viewer'].create({
|
||||
hasThreadView: true,
|
||||
thread: [['link', thread]],
|
||||
});
|
||||
await this.createThreadViewComponent(threadViewer.threadView, { hasComposer: true });
|
||||
const notifications = [
|
||||
[['myDB', 'mail.channel', 100], {
|
||||
channelId: 100,
|
||||
@@ -353,9 +365,11 @@ QUnit.test('mark channel as fetched and seen when a new message is loaded if com
|
||||
model: 'mail.channel',
|
||||
});
|
||||
|
||||
const threadView = this.env.models['mail.thread_view'].create({ thread: [['link', thread]] });
|
||||
await this.createThreadViewComponent(threadView, { hasComposer: true });
|
||||
|
||||
const threadViewer = this.env.models['mail.thread_viewer'].create({
|
||||
hasThreadView: true,
|
||||
thread: [['link', thread]],
|
||||
});
|
||||
await this.createThreadViewComponent(threadViewer.threadView, { hasComposer: true });
|
||||
document.querySelector('.o_ComposerTextInput_textarea').focus();
|
||||
const notifications = [
|
||||
[['myDB', 'mail.channel', 100], {
|
||||
@@ -394,8 +408,11 @@ QUnit.test('show message subject if thread is mailing channel', async function (
|
||||
name: "General",
|
||||
public: 'public',
|
||||
});
|
||||
const threadView = this.env.models['mail.thread_view'].create({ thread: [['link', thread]] });
|
||||
await this.createThreadViewComponent(threadView);
|
||||
const threadViewer = this.env.models['mail.thread_viewer'].create({
|
||||
hasThreadView: true,
|
||||
thread: [['link', thread]],
|
||||
});
|
||||
await this.createThreadViewComponent(threadViewer.threadView);
|
||||
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
|
||||
@@ -77,7 +77,7 @@ function factory(dependencies) {
|
||||
if (this.thread) {
|
||||
this.thread.update({ pendingFoldState: 'folded' });
|
||||
} else {
|
||||
this.update({ _isFolded: true });
|
||||
this.update({ isFolded: true });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,9 +120,8 @@ function factory(dependencies) {
|
||||
unfold() {
|
||||
if (this.thread) {
|
||||
this.thread.update({ pendingFoldState: 'open' });
|
||||
this.threadView.addComponentHint('chat-window-unfolded');
|
||||
} else {
|
||||
this.update({ _isFolded: false });
|
||||
this.update({ isFolded: false });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,6 +129,14 @@ function factory(dependencies) {
|
||||
// Private
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_computeHasNewMessageForm() {
|
||||
return this.isVisible && !this.isFolded && !this.thread;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
@@ -161,6 +168,14 @@ function factory(dependencies) {
|
||||
return index > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_computeHasThreadView() {
|
||||
return this.isVisible && !this.isFolded && this.thread;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
@@ -170,7 +185,15 @@ function factory(dependencies) {
|
||||
if (thread) {
|
||||
return thread.foldState === 'folded';
|
||||
}
|
||||
return this._isFolded;
|
||||
return this.isFolded;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_computeIsVisible() {
|
||||
return this.manager.allOrderedVisible.includes(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -195,6 +218,22 @@ function factory(dependencies) {
|
||||
return this.env._t("New message");
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @returns {integer|undefined}
|
||||
*/
|
||||
_computeVisibleIndex() {
|
||||
if (!this.manager) {
|
||||
return undefined;
|
||||
}
|
||||
const visible = this.manager.visual.visible;
|
||||
const index = visible.findIndex(visible => visible.chatWindowLocalId === this.localId);
|
||||
if (index === -1) {
|
||||
return undefined;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @returns {integer}
|
||||
@@ -278,33 +317,19 @@ function factory(dependencies) {
|
||||
this.threadView.addComponentHint('home-menu-shown');
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @returns {integer|undefined}
|
||||
*/
|
||||
_computeVisibleIndex() {
|
||||
if (!this.manager) {
|
||||
return undefined;
|
||||
}
|
||||
const visible = this.manager.visual.visible;
|
||||
const index = visible.findIndex(visible => visible.chatWindowLocalId === this.localId);
|
||||
if (index === -1) {
|
||||
return undefined;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
ChatWindow.fields = {
|
||||
/**
|
||||
* Determine whether the chat window is folded or not, when not
|
||||
* linked to a thread.
|
||||
* Note: this value only make sense for chat window not linked
|
||||
* to a thread. State of chat window of a thread is entirely
|
||||
* based on thread.foldState. @see isFolded .
|
||||
* Determines whether "new message form" should be displayed.
|
||||
*/
|
||||
_isFolded: attr({
|
||||
default: false,
|
||||
hasNewMessageForm: attr({
|
||||
compute: '_computeHasNewMessageForm',
|
||||
dependencies: [
|
||||
'isFolded',
|
||||
'isVisible',
|
||||
'thread',
|
||||
],
|
||||
}),
|
||||
hasShiftLeft: attr({
|
||||
compute: '_computeHasShiftLeft',
|
||||
@@ -316,6 +341,17 @@ function factory(dependencies) {
|
||||
dependencies: ['managerAllOrderedVisible'],
|
||||
default: false,
|
||||
}),
|
||||
/**
|
||||
* Determines whether `this.thread` should be displayed.
|
||||
*/
|
||||
hasThreadView: attr({
|
||||
compute: '_computeHasThreadView',
|
||||
dependencies: [
|
||||
'isFolded',
|
||||
'isVisible',
|
||||
'thread',
|
||||
],
|
||||
}),
|
||||
/**
|
||||
* Determine whether the chat window should be programmatically
|
||||
* focused by observed component of chat window. Those components
|
||||
@@ -326,18 +362,33 @@ function factory(dependencies) {
|
||||
default: false,
|
||||
}),
|
||||
/**
|
||||
* Determine whether the chat window is focused or not. Useful for
|
||||
* visual clue.
|
||||
* States whether `this` is focused. Useful for visual clue.
|
||||
*/
|
||||
isFocused: attr({
|
||||
default: false,
|
||||
}),
|
||||
/**
|
||||
* Determines whether `this` is folded.
|
||||
*
|
||||
* Note: writing this value directly only makes sense when `this.thread`
|
||||
* is empty. State of chat window of a thread is entirely based on
|
||||
* `thread.foldState`.
|
||||
*/
|
||||
isFolded: attr({
|
||||
compute: '_computeIsFolded',
|
||||
dependencies: [
|
||||
'thread',
|
||||
'threadFoldState',
|
||||
'_isFolded',
|
||||
],
|
||||
default: false,
|
||||
}),
|
||||
/**
|
||||
* States whether `this` is visible or not.
|
||||
*/
|
||||
isVisible: attr({
|
||||
compute: '_computeIsVisible',
|
||||
dependencies: [
|
||||
'managerAllOrderedVisible',
|
||||
],
|
||||
default: false,
|
||||
}),
|
||||
@@ -368,17 +419,30 @@ function factory(dependencies) {
|
||||
'threadDisplayName',
|
||||
],
|
||||
}),
|
||||
thread: many2one('mail.thread', {
|
||||
related: 'threadView.thread',
|
||||
}),
|
||||
/**
|
||||
* Determines the `mail.thread` that should be displayed by `this`.
|
||||
* If no `mail.thread` is linked, `this` is considered "new message".
|
||||
*/
|
||||
thread: many2one('mail.thread'),
|
||||
threadDisplayName: attr({
|
||||
related: 'thread.displayName',
|
||||
}),
|
||||
threadFoldState: attr({
|
||||
related: 'thread.foldState',
|
||||
}),
|
||||
/**
|
||||
* States the `mail.thread_view` displaying `this.thread`.
|
||||
*/
|
||||
threadView: one2one('mail.thread_view', {
|
||||
related: 'threadViewer.threadView',
|
||||
}),
|
||||
/**
|
||||
* Determines the `mail.thread_viewer` managing the display of `this.thread`.
|
||||
*/
|
||||
threadViewer: one2one('mail.thread_viewer', {
|
||||
default: [['create']],
|
||||
inverse: 'chatWindow',
|
||||
isCausal: true,
|
||||
}),
|
||||
/**
|
||||
* This field handle the "order" (index) of the visible chatWindow inside the UI.
|
||||
|
||||
@@ -110,9 +110,7 @@ function factory(dependencies) {
|
||||
if (!chatWindow) {
|
||||
chatWindow = this.env.models['mail.chat_window'].create({
|
||||
manager: [['link', this]],
|
||||
threadView: [['create', {
|
||||
thread: [['link', thread]],
|
||||
}]],
|
||||
thread: [['link', thread]],
|
||||
});
|
||||
}
|
||||
if (thread.foldState === 'closed') {
|
||||
|
||||
@@ -106,6 +106,14 @@ function factory(dependencies) {
|
||||
return [['replace', this.activities.filter(activity => activity.state === 'planned')]];
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_computeHasThreadView() {
|
||||
return this.thread && this.hasMessageList;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
@@ -136,7 +144,7 @@ function factory(dependencies) {
|
||||
_prepareAttachmentsLoading() {
|
||||
this._isPreparingAttachmentsLoading = true;
|
||||
this._attachmentsLoaderTimeout = this.env.browser.setTimeout(() => {
|
||||
this.update({isShowingAttachmentsLoading: true});
|
||||
this.update({ isShowingAttachmentsLoading: true });
|
||||
this._isPreparingAttachmentsLoading = false;
|
||||
}, this.env.loadingBaseDelayDuration);
|
||||
}
|
||||
@@ -147,7 +155,7 @@ function factory(dependencies) {
|
||||
_stopAttachmentsLoading() {
|
||||
this.env.browser.clearTimeout(this._attachmentsLoaderTimeout);
|
||||
this._attachmentsLoaderTimeout = null;
|
||||
this.update({isShowingAttachmentsLoading: false});
|
||||
this.update({ isShowingAttachmentsLoading: false });
|
||||
this._isPreparingAttachmentsLoading = false;
|
||||
}
|
||||
|
||||
@@ -232,7 +240,7 @@ function factory(dependencies) {
|
||||
id: getMessageNextTemporaryId(),
|
||||
isTemporary: true,
|
||||
});
|
||||
this.threadView.update({ thread: [['link', thread]] });
|
||||
this.update({ thread: [['link', thread]] });
|
||||
for (const cache of thread.caches) {
|
||||
cache.update({ messages: [['link', message]] });
|
||||
}
|
||||
@@ -242,7 +250,7 @@ function factory(dependencies) {
|
||||
id: this.threadId,
|
||||
model: this.threadModel,
|
||||
});
|
||||
this.threadView.update({ thread: [['link', thread]] });
|
||||
this.update({ thread: [['link', thread]] });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,6 +288,12 @@ function factory(dependencies) {
|
||||
hasFollowers: attr({
|
||||
default: true,
|
||||
}),
|
||||
/**
|
||||
* Determines whether `this` should display a message list.
|
||||
*/
|
||||
hasMessageList: attr({
|
||||
default: true,
|
||||
}),
|
||||
/**
|
||||
* Whether the message list should manage its scroll.
|
||||
* In particular, when the chatter is on the form view's side,
|
||||
@@ -290,8 +304,15 @@ function factory(dependencies) {
|
||||
hasMessageListScrollAdjust: attr({
|
||||
default: false,
|
||||
}),
|
||||
hasThread: attr({
|
||||
default: true,
|
||||
/**
|
||||
* Determines whether `this.thread` should be displayed.
|
||||
*/
|
||||
hasThreadView: attr({
|
||||
compute: '_computeHasThreadView',
|
||||
dependencies: [
|
||||
'hasMessageList',
|
||||
'thread',
|
||||
],
|
||||
}),
|
||||
hasTopbarCloseButton: attr({
|
||||
default: false,
|
||||
@@ -326,16 +347,28 @@ function factory(dependencies) {
|
||||
compute: '_computeOverdueActivities',
|
||||
dependencies: ['activitiesState'],
|
||||
}),
|
||||
thread: many2one('mail.thread', {
|
||||
related: 'threadView.thread',
|
||||
}),
|
||||
/**
|
||||
* Determines the `mail.thread` that should be displayed by `this`.
|
||||
*/
|
||||
thread: many2one('mail.thread'),
|
||||
threadAttachmentCount: attr({
|
||||
default: 0,
|
||||
}),
|
||||
threadId: attr(),
|
||||
threadModel: attr(),
|
||||
/**
|
||||
* States the `mail.thread_view` displaying `this.thread`.
|
||||
*/
|
||||
threadView: one2one('mail.thread_view', {
|
||||
related: 'threadViewer.threadView',
|
||||
}),
|
||||
/**
|
||||
* Determines the `mail.thread_viewer` managing the display of `this.thread`.
|
||||
*/
|
||||
threadViewer: one2one('mail.thread_viewer', {
|
||||
default: [['create']],
|
||||
inverse: 'chatter',
|
||||
isCausal: true,
|
||||
}),
|
||||
todayActivities: one2many('mail.activity', {
|
||||
compute: '_computeTodayActivities',
|
||||
|
||||
@@ -151,10 +151,10 @@ function factory(dependencies) {
|
||||
*/
|
||||
openInitThread() {
|
||||
const [model, id] = this.initActiveId.split('_');
|
||||
const thread = this.env.models['mail.thread'].find(thread =>
|
||||
thread.id === (model !== 'mail.box' ? Number(id) : id) &&
|
||||
thread.model === model
|
||||
);
|
||||
const thread = this.env.models['mail.thread'].findFromIdentifyingData({
|
||||
id: model !== 'mail.box' ? Number(id) : id,
|
||||
model,
|
||||
});
|
||||
if (!thread) {
|
||||
return;
|
||||
}
|
||||
@@ -168,7 +168,7 @@ function factory(dependencies) {
|
||||
* @param {mail.thread} thread
|
||||
*/
|
||||
async openThread(thread) {
|
||||
this.threadView.update({
|
||||
this.update({
|
||||
stringifiedDomain: '[]',
|
||||
thread: [['link', thread]],
|
||||
});
|
||||
@@ -253,16 +253,22 @@ function factory(dependencies) {
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @returns {string}
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_computeInitActiveId() {
|
||||
if (!this.isOpen) {
|
||||
return this.defaultInitActiveId;
|
||||
_computeHasThreadView() {
|
||||
if (!this.thread || !this.isOpen) {
|
||||
return false;
|
||||
}
|
||||
if (this.thread) {
|
||||
return this.threadToActiveId(this.thread);
|
||||
if (
|
||||
this.env.messaging.device.isMobile &&
|
||||
(
|
||||
this.activeMobileNavbarTabId !== 'mailbox' ||
|
||||
this.thread.model !== 'mail.box'
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return this.initActiveId;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -308,23 +314,21 @@ function factory(dependencies) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Only pinned threads are allowed in discuss.
|
||||
*
|
||||
* @private
|
||||
* @returns {mail.thread|undefined}
|
||||
*/
|
||||
_onChangeThreadIsPinned() {
|
||||
_computeThread() {
|
||||
let thread = this.thread;
|
||||
// No thread, or thread is being removed
|
||||
// so we display discuss the messaging's Inbox.
|
||||
if (
|
||||
(!thread || !thread.isPinned) &&
|
||||
this.messaging
|
||||
) {
|
||||
thread = this.messaging.inbox;
|
||||
}
|
||||
if (thread && this.threadView && thread !== this.thread) {
|
||||
this.threadView.update({ thread: [['link', thread]] });
|
||||
if (!thread || !thread.isPinned) {
|
||||
return [['unlink']];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Discuss.fields = {
|
||||
@@ -350,8 +354,17 @@ function factory(dependencies) {
|
||||
default: "",
|
||||
dependencies: ['isOpen'],
|
||||
}),
|
||||
defaultInitActiveId: attr({
|
||||
default: 'mail.box_inbox',
|
||||
/**
|
||||
* Serves as compute dependency.
|
||||
*/
|
||||
device: one2one('mail.device', {
|
||||
related: 'messaging.device',
|
||||
}),
|
||||
/**
|
||||
* Serves as compute dependency.
|
||||
*/
|
||||
deviceIsMobile: attr({
|
||||
related: 'device.isMobile',
|
||||
}),
|
||||
/**
|
||||
* Determine if the moderation discard dialog is displayed.
|
||||
@@ -365,6 +378,19 @@ function factory(dependencies) {
|
||||
hasModerationRejectDialog: attr({
|
||||
default: false,
|
||||
}),
|
||||
/**
|
||||
* Determines whether `this.thread` should be displayed.
|
||||
*/
|
||||
hasThreadView: attr({
|
||||
compute: '_computeHasThreadView',
|
||||
dependencies: [
|
||||
'activeMobileNavbarTabId',
|
||||
'deviceIsMobile',
|
||||
'isOpen',
|
||||
'thread',
|
||||
'threadModel',
|
||||
],
|
||||
}),
|
||||
/**
|
||||
* Formatted init thread on opening discuss for the first time,
|
||||
* when no active thread is defined. Useful to set a thread to
|
||||
@@ -372,14 +398,7 @@ function factory(dependencies) {
|
||||
* format: <threadModel>_<threadId>
|
||||
*/
|
||||
initActiveId: attr({
|
||||
compute: '_computeInitActiveId',
|
||||
default: 'mail.box_inbox',
|
||||
dependencies: [
|
||||
'isOpen',
|
||||
'thread',
|
||||
'threadId',
|
||||
'threadModel',
|
||||
],
|
||||
}),
|
||||
/**
|
||||
* Determine whether current user is currently adding a channel from
|
||||
@@ -430,17 +449,6 @@ function factory(dependencies) {
|
||||
messaging: one2one('mail.messaging', {
|
||||
inverse: 'discuss',
|
||||
}),
|
||||
/**
|
||||
* When a thread changes, or some properties of it change
|
||||
* Computes whether we should display it or change it
|
||||
*/
|
||||
onChangeThreadIsPinned: attr({
|
||||
compute: '_onChangeThreadIsPinned',
|
||||
dependencies: [
|
||||
'isThreadPinned',
|
||||
'thread',
|
||||
],
|
||||
}),
|
||||
renamingThreads: one2many('mail.thread'),
|
||||
/**
|
||||
* The message that is currently selected as being replied to in Inbox.
|
||||
@@ -476,8 +484,21 @@ function factory(dependencies) {
|
||||
sidebarQuickSearchValue: attr({
|
||||
default: "",
|
||||
}),
|
||||
/**
|
||||
* Determines the domain to apply when fetching messages for `this.thread`.
|
||||
*/
|
||||
stringifiedDomain: attr({
|
||||
default: '[]',
|
||||
}),
|
||||
/**
|
||||
* Determines the `mail.thread` that should be displayed by `this`.
|
||||
*/
|
||||
thread: many2one('mail.thread', {
|
||||
related: 'threadView.thread',
|
||||
compute: '_computeThread',
|
||||
dependencies: [
|
||||
'isThreadPinned',
|
||||
'thread',
|
||||
],
|
||||
}),
|
||||
threadId: attr({
|
||||
related: 'thread.id',
|
||||
@@ -485,8 +506,18 @@ function factory(dependencies) {
|
||||
threadModel: attr({
|
||||
related: 'thread.model',
|
||||
}),
|
||||
/**
|
||||
* States the `mail.thread_view` displaying `this.thread`.
|
||||
*/
|
||||
threadView: one2one('mail.thread_view', {
|
||||
related: 'threadViewer.threadView',
|
||||
}),
|
||||
/**
|
||||
* Determines the `mail.thread_viewer` managing the display of `this.thread`.
|
||||
*/
|
||||
threadViewer: one2one('mail.thread_viewer', {
|
||||
default: [['create']],
|
||||
inverse: 'discuss',
|
||||
isCausal: true,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -197,6 +197,51 @@ function factory(dependencies) {
|
||||
kwargs: kwargs,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Performs the `message_fetch` RPC on `mail.message`.
|
||||
*
|
||||
* @static
|
||||
* @param {Array[]} domain
|
||||
* @param {integer} [limit]
|
||||
* @param {integer[]} [moderated_channel_ids]
|
||||
* @param {Object} [context]
|
||||
* @returns {mail.message[]}
|
||||
*/
|
||||
static async performRpcMessageFetch(domain, limit, moderated_channel_ids, context) {
|
||||
const messagesData = await this.env.services.rpc({
|
||||
model: 'mail.message',
|
||||
method: 'message_fetch',
|
||||
kwargs: {
|
||||
context,
|
||||
domain,
|
||||
limit,
|
||||
moderated_channel_ids,
|
||||
},
|
||||
}, { shadow: true });
|
||||
const messages = this.env.models['mail.message'].insert(messagesData.map(
|
||||
messageData => this.env.models['mail.message'].convertData(messageData)
|
||||
));
|
||||
// compute seen indicators (if applicable)
|
||||
for (const message of messages) {
|
||||
for (const thread of message.threads) {
|
||||
if (thread.model !== 'mail.channel' || thread.channel_type === 'channel') {
|
||||
// disabled on non-channel threads and
|
||||
// on `channel` channels for performance reasons
|
||||
continue;
|
||||
}
|
||||
thread.update({
|
||||
messageSeenIndicators: [[
|
||||
'insert',
|
||||
{
|
||||
id: this.env.models['mail.message_seen_indicator'].computeId(message.id, thread.id),
|
||||
message: [['link', message]],
|
||||
},
|
||||
]],
|
||||
});
|
||||
}
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* @static
|
||||
|
||||
@@ -30,6 +30,11 @@ function factory(dependencies) {
|
||||
model: 'mail.box',
|
||||
name: this.env._t("Inbox"),
|
||||
}]],
|
||||
moderation: [['create', {
|
||||
id: 'moderation',
|
||||
model: 'mail.box',
|
||||
name: this.env._t("Moderation"),
|
||||
}]],
|
||||
starred: [['create', {
|
||||
id: 'starred',
|
||||
isServerPinned: true,
|
||||
@@ -37,7 +42,6 @@ function factory(dependencies) {
|
||||
name: this.env._t("Starred"),
|
||||
}]],
|
||||
});
|
||||
|
||||
const device = this.messaging.device;
|
||||
device.start();
|
||||
const context = Object.assign({
|
||||
@@ -199,14 +203,9 @@ function factory(dependencies) {
|
||||
this.env.messaging.inbox.update({ counter: needaction_inbox_counter });
|
||||
this.env.messaging.starred.update({ counter: starred_counter });
|
||||
if (moderation_channel_ids.length > 0) {
|
||||
this.messaging.update({
|
||||
moderation: [['create', {
|
||||
counter: moderation_counter,
|
||||
id: 'moderation',
|
||||
isServerPinned: true,
|
||||
model: 'mail.box',
|
||||
name: this.env._t("Moderation"),
|
||||
}]],
|
||||
this.messaging.moderation.update({
|
||||
counter: moderation_counter,
|
||||
isServerPinned: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ function factory(dependencies) {
|
||||
return;
|
||||
}
|
||||
// populate some needaction messages on threads.
|
||||
inbox.mainCache.loadMessages();
|
||||
inbox.mainCache.update({ hasToLoadMessages: true });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+13
-1
@@ -155,6 +155,10 @@ function factory(dependencies) {
|
||||
// knowledge of the channel
|
||||
return;
|
||||
}
|
||||
if (channel.channel_type === 'channel') {
|
||||
// disabled on `channel` channels for performance reasons
|
||||
return;
|
||||
}
|
||||
channel.update({
|
||||
partnerSeenInfos: [['insert',
|
||||
{
|
||||
@@ -259,7 +263,11 @@ function factory(dependencies) {
|
||||
message,
|
||||
});
|
||||
}
|
||||
channel.markAsFetched();
|
||||
if (channel.model === 'mail.channel' && channel.channel_type !== 'channel') {
|
||||
// disabled on non-channel threads and
|
||||
// on `channel` channels for performance reasons
|
||||
channel.markAsFetched();
|
||||
}
|
||||
// (re)open chat on receiving new message
|
||||
if (channel.channel_type !== 'channel') {
|
||||
this.env.messaging.chatWindowManager.openThread(channel);
|
||||
@@ -303,6 +311,10 @@ function factory(dependencies) {
|
||||
// knowledge of the channel
|
||||
return;
|
||||
}
|
||||
if (channel.channel_type === 'channel') {
|
||||
// disabled on `channel` channels for performance reasons
|
||||
return;
|
||||
}
|
||||
const lastMessage = this.env.models['mail.message'].insert({id: last_message_id});
|
||||
const updateData = {
|
||||
partnerSeenInfos: [['insert',
|
||||
|
||||
@@ -2,9 +2,7 @@ odoo.define('mail/static/src/models/thread_cache/thread_cache.js', function (req
|
||||
'use strict';
|
||||
|
||||
const { registerNewModel } = require('mail/static/src/model/model_core.js');
|
||||
const { attr, many2many, many2one } = require('mail/static/src/model/model_field.js');
|
||||
|
||||
const MESSAGE_FETCH_LIMIT = 30;
|
||||
const { attr, many2many, many2one, one2many } = require('mail/static/src/model/model_field.js');
|
||||
|
||||
function factory(dependencies) {
|
||||
|
||||
@@ -15,79 +13,49 @@ function factory(dependencies) {
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Load this thread cache, by fetching the most recent messages in this
|
||||
* conversation.
|
||||
* @returns {mail.message[]|undefined}
|
||||
*/
|
||||
async loadMessages() {
|
||||
if (this.isLoaded && this.isLoading) {
|
||||
async loadMoreMessages() {
|
||||
if (this.isAllHistoryLoaded || this.isLoading) {
|
||||
return;
|
||||
}
|
||||
const searchDomain = JSON.parse(this.stringifiedDomain);
|
||||
let domain = searchDomain.length ? searchDomain : [];
|
||||
domain = this._extendMessageDomain(domain);
|
||||
this.update({ isLoading: true });
|
||||
let messagesData = [];
|
||||
if (!this.thread.isTemporary) {
|
||||
messagesData = await this.async(() => this.env.services.rpc({
|
||||
model: 'mail.message',
|
||||
method: 'message_fetch',
|
||||
args: [domain],
|
||||
kwargs: this._getFetchMessagesKwargs(),
|
||||
}, { shadow: true }));
|
||||
}
|
||||
this._handleMessagesLoaded(messagesData);
|
||||
}
|
||||
|
||||
async loadMoreMessages() {
|
||||
const searchDomain = JSON.parse(this.stringifiedDomain);
|
||||
let domain = searchDomain.length ? searchDomain : [];
|
||||
domain = this._extendMessageDomain(domain);
|
||||
if (this.isAllHistoryLoaded && this.isLoadingMore) {
|
||||
if (!this.isLoaded) {
|
||||
this.update({ hasToLoadMessages: true });
|
||||
return;
|
||||
}
|
||||
this.update({ isLoadingMore: true });
|
||||
const minMessageId = Math.min(
|
||||
...this.fetchedMessages.map(message => message.id)
|
||||
);
|
||||
domain = [['id', '<', minMessageId]].concat(domain);
|
||||
const messagesData = await this.async(() => this.env.services.rpc({
|
||||
model: 'mail.message',
|
||||
method: 'message_fetch',
|
||||
args: [domain],
|
||||
kwargs: this._getFetchMessagesKwargs(),
|
||||
}, { shadow: true }));
|
||||
for (const threadView of this.thread.threadViews) {
|
||||
const messageIds = this.fetchedMessages.map(message => message.id);
|
||||
const limit = 30;
|
||||
const fetchedMessages = await this.async(() => this._loadMessages({
|
||||
extraDomain: [['id', '<', Math.min(...messageIds)]],
|
||||
limit,
|
||||
}));
|
||||
for (const threadView of this.threadViews) {
|
||||
threadView.addComponentHint('more-messages-loaded');
|
||||
}
|
||||
this._handleMessagesLoaded(messagesData);
|
||||
this.update({ isLoadingMore: false });
|
||||
if (fetchedMessages.length < limit) {
|
||||
this.update({ isAllHistoryLoaded: true });
|
||||
}
|
||||
return fetchedMessages;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {mail.message[]|undefined}
|
||||
*/
|
||||
async loadNewMessages() {
|
||||
if (this.isLoading) {
|
||||
return;
|
||||
}
|
||||
if (!this.isLoaded) {
|
||||
await this.async(() => this.loadMessages());
|
||||
this.update({ hasToLoadMessages: true });
|
||||
return;
|
||||
}
|
||||
const messageIds = this.fetchedMessages.map(message => message.id);
|
||||
const searchDomain = JSON.parse(this.stringifiedDomain);
|
||||
let domain = searchDomain.length ? searchDomain : [];
|
||||
domain = this._extendMessageDomain(domain);
|
||||
if (messageIds.length > 0) {
|
||||
const lastMessageId = Math.max(...messageIds);
|
||||
domain = [['id', '>', lastMessageId]].concat(domain);
|
||||
}
|
||||
this.update({ isLoading: true });
|
||||
const messageFetchKwargs = this._getFetchMessagesKwargs();
|
||||
messageFetchKwargs.limit = false;
|
||||
const messagesData = await this.async(() => this.env.services.rpc({
|
||||
model: 'mail.message',
|
||||
method: 'message_fetch',
|
||||
args: [domain],
|
||||
kwargs: messageFetchKwargs,
|
||||
}, { shadow: true }));
|
||||
this._handleMessagesLoaded(messagesData);
|
||||
return this._loadMessages({
|
||||
extraDomain: [['id', '>', Math.max(...messageIds)]],
|
||||
limit: false,
|
||||
});
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
@@ -205,6 +173,20 @@ function factory(dependencies) {
|
||||
return [['replace', this.messages.sort((m1, m2) => m1.id < m2.id ? -1 : 1)]];
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_computeHasToLoadMessages() {
|
||||
return (
|
||||
this.thread &&
|
||||
!this.thread.isTemporary &&
|
||||
this.threadViews.length > 0 &&
|
||||
!this.isLoaded &&
|
||||
!this.isLoading
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @returns {mail.message[]}
|
||||
@@ -247,54 +229,53 @@ function factory(dependencies) {
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @returns {Object}
|
||||
* @param {Object} [param0={}]
|
||||
* @param {Array[]} [param0.extraDomain]
|
||||
* @param {integer} [param0.limit=30]
|
||||
* @returns {mail.message[]}
|
||||
*/
|
||||
_getFetchMessagesKwargs() {
|
||||
const thread = this.thread;
|
||||
let kwargs = {
|
||||
limit: MESSAGE_FETCH_LIMIT,
|
||||
context: this.env.session.user_context,
|
||||
};
|
||||
if (thread.moderation) {
|
||||
// thread is a channel
|
||||
kwargs.moderated_channel_ids = [thread.id];
|
||||
async _loadMessages({ extraDomain, limit = 30 } = {}) {
|
||||
this.update({ isLoading: true });
|
||||
const searchDomain = JSON.parse(this.stringifiedDomain);
|
||||
let domain = searchDomain.length ? searchDomain : [];
|
||||
domain = this._extendMessageDomain(domain);
|
||||
if (extraDomain) {
|
||||
domain = extraDomain.concat(domain);
|
||||
}
|
||||
return kwargs;
|
||||
const context = this.env.session.user_context;
|
||||
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,
|
||||
)
|
||||
);
|
||||
this.update({
|
||||
fetchedMessages: [['link', messages]],
|
||||
isLoaded: true,
|
||||
isLoading: false,
|
||||
});
|
||||
if (!extraDomain && messages.length < limit) {
|
||||
this.update({ isAllHistoryLoaded: true });
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads this thread cache, by fetching the most recent messages in this
|
||||
* conversation.
|
||||
*
|
||||
* @private
|
||||
* @param {Object[]} messageData
|
||||
*/
|
||||
_handleMessagesLoaded(messagesData) {
|
||||
const messages = this.env.models['mail.message'].insert(messagesData.map(
|
||||
messageData => this.env.models['mail.message'].convertData(messageData)
|
||||
));
|
||||
|
||||
if (!this.thread) {
|
||||
_onHasToLoadMessagesChanged() {
|
||||
if (!this.hasToLoadMessages) {
|
||||
return;
|
||||
}
|
||||
this.thread.update({
|
||||
messageSeenIndicators: [[
|
||||
'insert',
|
||||
messagesData.map(messageData => {
|
||||
return {
|
||||
id: this.env.models['mail.message_seen_indicator'].computeId(messageData.id, this.thread.id),
|
||||
message: [['insert', { id: messageData.id }]],
|
||||
};
|
||||
})
|
||||
]],
|
||||
});
|
||||
this.update({
|
||||
fetchedMessages: [['link', messages]],
|
||||
isAllHistoryLoaded: messagesData.length < MESSAGE_FETCH_LIMIT,
|
||||
isLoaded: true,
|
||||
isLoading: false,
|
||||
isLoadingMore: false,
|
||||
});
|
||||
for (const threadView of this.thread.threadViews) {
|
||||
threadView.handleThreadCacheLoaded(this);
|
||||
}
|
||||
this._loadMessages();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -329,6 +310,19 @@ function factory(dependencies) {
|
||||
compute: '_computeFetchedMessages',
|
||||
dependencies: ['threadMessages'],
|
||||
}),
|
||||
/**
|
||||
* Determines whether `this` should load initial messages.
|
||||
*/
|
||||
hasToLoadMessages: attr({
|
||||
compute: '_computeHasToLoadMessages',
|
||||
dependencies: [
|
||||
'isLoaded',
|
||||
'isLoading',
|
||||
'thread',
|
||||
'threadIsTemporary',
|
||||
'threadViews',
|
||||
],
|
||||
}),
|
||||
isAllHistoryLoaded: attr({
|
||||
default: false,
|
||||
}),
|
||||
@@ -369,6 +363,17 @@ function factory(dependencies) {
|
||||
'threadMessages',
|
||||
],
|
||||
}),
|
||||
/**
|
||||
* Loads initial messages from `this`.
|
||||
* This is not a "real" field, its compute function is used to trigger
|
||||
* the load of messages at the right time.
|
||||
*/
|
||||
onHasToLoadMessagesChanged: attr({
|
||||
compute: '_onHasToLoadMessagesChanged',
|
||||
dependencies: [
|
||||
'hasToLoadMessages',
|
||||
],
|
||||
}),
|
||||
/**
|
||||
* Ordered list of messages that have been fetched by this cache.
|
||||
*
|
||||
@@ -393,9 +398,21 @@ function factory(dependencies) {
|
||||
thread: many2one('mail.thread', {
|
||||
inverse: 'caches',
|
||||
}),
|
||||
/**
|
||||
* Serves as compute dependency.
|
||||
*/
|
||||
threadIsTemporary: attr({
|
||||
related: 'thread.isTemporary',
|
||||
}),
|
||||
threadMessages: many2many('mail.message', {
|
||||
related: 'thread.messages',
|
||||
}),
|
||||
/**
|
||||
* States the 'mail.thread_view' that are currently displaying `this`.
|
||||
*/
|
||||
threadViews: one2many('mail.thread_view', {
|
||||
inverse: 'threadCache',
|
||||
}),
|
||||
uncheckedMessages: many2many('mail.message', {
|
||||
compute: '_computeUncheckedMessages',
|
||||
dependencies: [
|
||||
|
||||
@@ -13,7 +13,7 @@ function factory(dependencies) {
|
||||
* @override
|
||||
*/
|
||||
_willDelete() {
|
||||
this._stopLoading();
|
||||
this.env.browser.clearTimeout(this._loaderTimeout);
|
||||
return super._willDelete(...arguments);
|
||||
}
|
||||
|
||||
@@ -40,16 +40,6 @@ function factory(dependencies) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {mail.thread_cache} threadCache
|
||||
*/
|
||||
handleThreadCacheLoaded(threadCache) {
|
||||
if (threadCache !== this.threadCache) {
|
||||
return;
|
||||
}
|
||||
this._stopLoading();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} hint
|
||||
*/
|
||||
@@ -77,33 +67,19 @@ function factory(dependencies) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} scrollTop
|
||||
*/
|
||||
saveThreadCacheScrollPositionsAsInitial(scrollTop) {
|
||||
if (!this.threadCache) {
|
||||
return;
|
||||
}
|
||||
this.update({
|
||||
threadCacheInitialScrollPositions: Object.assign({}, this.threadCacheInitialScrollPositions, {
|
||||
[this.threadCache.localId]: scrollTop,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Private
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @returns {mail.thread_cache|undefined}
|
||||
* @returns {integer|undefined}
|
||||
*/
|
||||
_computeThreadCache() {
|
||||
if (!this.thread) {
|
||||
return [];
|
||||
_computeThreadCacheInitialScrollPosition() {
|
||||
if (!this.threadCache) {
|
||||
return;
|
||||
}
|
||||
return [['link', this.thread.cache(this.stringifiedDomain)]];
|
||||
return this.threadCacheInitialScrollPositions[this.threadCache.localId];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -131,75 +107,37 @@ function factory(dependencies) {
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @returns {integer|undefined}
|
||||
*/
|
||||
_computeThreadCacheInitialScrollPosition() {
|
||||
if (!this.threadCache) {
|
||||
return undefined;
|
||||
}
|
||||
return this.threadCacheInitialScrollPositions[this.threadCache.localId];
|
||||
_onThreadCacheChanged() {
|
||||
this.addComponentHint('change-of-thread-cache');
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_prepareLoading() {
|
||||
this._isPreparingLoading = true;
|
||||
this._loaderTimeout = setTimeout(() => {
|
||||
this.isShowingLoading = true;
|
||||
this._isPreparingLoading = false;
|
||||
}, 400);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_stopLoading() {
|
||||
clearTimeout(this._loaderTimeout);
|
||||
this._loaderTimeout = null;
|
||||
this.isShowingLoading = false;
|
||||
this._isPreparingLoading = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_updateAfter(previous) {
|
||||
if (this.thread && this.thread !== previous.thread) {
|
||||
this._stopLoading();
|
||||
if (!this.threadCache.isLoaded && !this.threadCache.isLoading) {
|
||||
this.threadCache.loadMessages();
|
||||
_onThreadCacheIsLoadingChanged() {
|
||||
if (this.threadCache && this.threadCache.isLoading) {
|
||||
if (!this.isLoading && !this.isPreparingLoading) {
|
||||
this.update({ isPreparingLoading: true });
|
||||
this.async(() =>
|
||||
new Promise(resolve => {
|
||||
this._loaderTimeout = this.env.browser.setTimeout(resolve, 400);
|
||||
}
|
||||
)).then(() => {
|
||||
const isLoading = this.threadCache
|
||||
? this.threadCache.isLoading
|
||||
: false;
|
||||
this.update({ isLoading, isPreparingLoading: false });
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (this.threadCache !== previous.threadCache) {
|
||||
this._stopLoading();
|
||||
this.addComponentHint('change-of-thread-cache');
|
||||
}
|
||||
|
||||
if (
|
||||
this.thread && this.threadCache && this.threadCache.isLoading &&
|
||||
!this.isShowingLoading && !this._isPreparingLoading
|
||||
) {
|
||||
this._prepareLoading();
|
||||
}
|
||||
this.env.browser.clearTimeout(this._loaderTimeout);
|
||||
this.update({ isLoading: false, isPreparingLoading: false });
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_updateBefore() {
|
||||
return {
|
||||
thread: this.thread,
|
||||
threadCache: this.threadCache,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ThreadView.fields = {
|
||||
chatWindow: one2one('mail.chat_window', {
|
||||
inverse: 'threadView',
|
||||
}),
|
||||
checkedMessages: many2many('mail.message', {
|
||||
related: 'threadCache.checkedMessages',
|
||||
}),
|
||||
@@ -230,6 +168,31 @@ function factory(dependencies) {
|
||||
hasComposerFocus: attr({
|
||||
related: 'composer.hasFocus',
|
||||
}),
|
||||
/**
|
||||
* States whether `this.threadCache` is currently loading messages.
|
||||
*
|
||||
* This field is related to `this.threadCache.isLoading` but with a
|
||||
* delay on its update to avoid flickering on the UI.
|
||||
*
|
||||
* It is computed through `_onThreadCacheIsLoadingChanged` and it should
|
||||
* otherwise be considered read-only.
|
||||
*/
|
||||
isLoading: attr({
|
||||
default: false,
|
||||
}),
|
||||
/**
|
||||
* States whether `this` is aware of `this.threadCache` currently
|
||||
* loading messages, but `this` is not yet ready to display that loading
|
||||
* on the UI.
|
||||
*
|
||||
* This field is computed through `_onThreadCacheIsLoadingChanged` and
|
||||
* it should otherwise be considered read-only.
|
||||
*
|
||||
* @see `this.isLoading`
|
||||
*/
|
||||
isPreparingLoading: attr({
|
||||
default: false,
|
||||
}),
|
||||
lastMessage: many2one('mail.message', {
|
||||
related: 'thread.lastMessage',
|
||||
}),
|
||||
@@ -241,19 +204,48 @@ function factory(dependencies) {
|
||||
messages: many2many('mail.message', {
|
||||
related: 'threadCache.messages',
|
||||
}),
|
||||
stringifiedDomain: attr({
|
||||
default: '[]',
|
||||
/**
|
||||
* Not a real field, used to trigger `_onThreadCacheChanged` when one of
|
||||
* the dependencies changes.
|
||||
*/
|
||||
onThreadCacheChanged: attr({
|
||||
compute: '_onThreadCacheChanged',
|
||||
dependencies: [
|
||||
'threadCache'
|
||||
],
|
||||
}),
|
||||
/**
|
||||
* Not a real field, used to trigger `_onThreadCacheIsLoadingChanged`
|
||||
* when one of the dependencies changes.
|
||||
*
|
||||
* @see `this.isLoading`
|
||||
*/
|
||||
onThreadCacheIsLoadingChanged: attr({
|
||||
compute: '_onThreadCacheIsLoadingChanged',
|
||||
dependencies: [
|
||||
'threadCache',
|
||||
'threadCacheIsLoading',
|
||||
],
|
||||
}),
|
||||
/**
|
||||
* Determines the domain to apply when fetching messages for `this.thread`.
|
||||
*/
|
||||
stringifiedDomain: attr({
|
||||
related: 'threadViewer.stringifiedDomain',
|
||||
}),
|
||||
/**
|
||||
* Determines the `mail.thread` currently displayed by `this`.
|
||||
*/
|
||||
thread: many2one('mail.thread', {
|
||||
inverse: 'threadViews',
|
||||
related: 'threadViewer.thread',
|
||||
}),
|
||||
/**
|
||||
* States the `mail.thread_cache` currently displayed by `this`.
|
||||
*/
|
||||
threadCache: many2one('mail.thread_cache', {
|
||||
compute: '_computeThreadCache',
|
||||
dependencies: [
|
||||
'stringifiedDomain',
|
||||
'thread',
|
||||
'threadCaches',
|
||||
],
|
||||
inverse: 'threadViews',
|
||||
related: 'threadViewer.threadCache',
|
||||
}),
|
||||
threadCacheInitialScrollPosition: attr({
|
||||
compute: '_computeThreadCacheInitialScrollPosition',
|
||||
@@ -262,18 +254,18 @@ function factory(dependencies) {
|
||||
'threadCacheInitialScrollPositions',
|
||||
],
|
||||
}),
|
||||
/**
|
||||
* Serves as compute dependency.
|
||||
*/
|
||||
threadCacheIsLoading: attr({
|
||||
related: 'threadCache.isLoading',
|
||||
}),
|
||||
/**
|
||||
* List of saved initial scroll positions of thread caches.
|
||||
* Useful to restore scroll position on changing back to this
|
||||
* thread cache. Note that this is only applied when opening
|
||||
* the thread cache, because scroll position may change fast so
|
||||
* save is already throttled
|
||||
*/
|
||||
threadCacheInitialScrollPositions: attr({
|
||||
default: {},
|
||||
}),
|
||||
threadCaches: many2many('mail.thread_cache', {
|
||||
related: 'thread.caches',
|
||||
related: 'threadViewer.threadCacheInitialScrollPositions',
|
||||
}),
|
||||
/**
|
||||
* Not a real field, used to trigger `thread.markAsSeen` when one of
|
||||
@@ -288,6 +280,12 @@ function factory(dependencies) {
|
||||
'threadCache',
|
||||
],
|
||||
}),
|
||||
/**
|
||||
* Determines the `mail.thread_viewer` currently managing `this`.
|
||||
*/
|
||||
threadViewer: one2one('mail.thread_viewer', {
|
||||
inverse: 'threadView',
|
||||
}),
|
||||
uncheckedMessages: many2many('mail.message', {
|
||||
related: 'threadCache.uncheckedMessages',
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
odoo.define('mail/static/src/models/thread_viewer/thread_viewer.js', function (require) {
|
||||
'use strict';
|
||||
|
||||
const { registerNewModel } = require('mail/static/src/model/model_core.js');
|
||||
const { attr, many2one, one2one } = require('mail/static/src/model/model_field.js');
|
||||
|
||||
function factory(dependencies) {
|
||||
|
||||
class ThreadViewer extends dependencies['mail.model'] {
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Public
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @param {string} scrollTop
|
||||
*/
|
||||
saveThreadCacheScrollPositionsAsInitial(scrollTop) {
|
||||
if (!this.threadCache) {
|
||||
return;
|
||||
}
|
||||
this.update({
|
||||
threadCacheInitialScrollPositions: Object.assign({}, this.threadCacheInitialScrollPositions, {
|
||||
[this.threadCache.localId]: scrollTop,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Private
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_computeHasThreadView() {
|
||||
if (this.chatter) {
|
||||
return this.chatter.hasThreadView;
|
||||
}
|
||||
if (this.chatWindow) {
|
||||
return this.chatWindow.hasThreadView;
|
||||
}
|
||||
if (this.discuss) {
|
||||
return this.discuss.hasThreadView;
|
||||
}
|
||||
return this.hasThreadView;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @returns {string}
|
||||
*/
|
||||
_computeStringifiedDomain() {
|
||||
if (this.chatter) {
|
||||
return '[]';
|
||||
}
|
||||
if (this.chatWindow) {
|
||||
return '[]';
|
||||
}
|
||||
if (this.discuss) {
|
||||
return this.discuss.stringifiedDomain;
|
||||
}
|
||||
return this.stringifiedDomain;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @returns {mail.thread|undefined}
|
||||
*/
|
||||
_computeThread() {
|
||||
if (this.chatter) {
|
||||
if (!this.chatter.thread) {
|
||||
return [['unlink']];
|
||||
}
|
||||
return [['link', this.chatter.thread]];
|
||||
}
|
||||
if (this.chatWindow) {
|
||||
if (!this.chatWindow.thread) {
|
||||
return [['unlink']];
|
||||
}
|
||||
return [['link', this.chatWindow.thread]];
|
||||
}
|
||||
if (this.discuss) {
|
||||
if (!this.discuss.thread) {
|
||||
return [['unlink']];
|
||||
}
|
||||
return [['link', this.discuss.thread]];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @returns {mail.thread_cache|undefined}
|
||||
*/
|
||||
_computeThreadCache() {
|
||||
if (!this.thread) {
|
||||
return [['unlink']];
|
||||
}
|
||||
return [['link', this.thread.cache(this.stringifiedDomain)]];
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @returns {mail.thread_viewer|undefined}
|
||||
*/
|
||||
_computeThreadView() {
|
||||
if (!this.hasThreadView) {
|
||||
return [['unlink']];
|
||||
}
|
||||
if (this.threadView) {
|
||||
return [];
|
||||
}
|
||||
return [['create']];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ThreadViewer.fields = {
|
||||
/**
|
||||
* States the `mail.chatter` managing `this`. This field is computed
|
||||
* through the inverse relation and should be considered read-only.
|
||||
*/
|
||||
chatter: one2one('mail.chatter', {
|
||||
inverse: 'threadViewer',
|
||||
}),
|
||||
/**
|
||||
* Serves as compute dependency.
|
||||
*/
|
||||
chatterHasThreadView: attr({
|
||||
related: 'chatter.hasThreadView',
|
||||
}),
|
||||
/**
|
||||
* Serves as compute dependency.
|
||||
*/
|
||||
chatterThread: many2one('mail.thread', {
|
||||
related: 'chatter.thread',
|
||||
}),
|
||||
/**
|
||||
* States the `mail.chat_window` managing `this`. This field is computed
|
||||
* through the inverse relation and should be considered read-only.
|
||||
*/
|
||||
chatWindow: one2one('mail.chat_window', {
|
||||
inverse: 'threadViewer',
|
||||
}),
|
||||
/**
|
||||
* Serves as compute dependency.
|
||||
*/
|
||||
chatWindowHasThreadView: attr({
|
||||
related: 'chatWindow.hasThreadView',
|
||||
}),
|
||||
/**
|
||||
* Serves as compute dependency.
|
||||
*/
|
||||
chatWindowThread: many2one('mail.thread', {
|
||||
related: 'chatWindow.thread',
|
||||
}),
|
||||
/**
|
||||
* States the `mail.discuss` managing `this`. This field is computed
|
||||
* through the inverse relation and should be considered read-only.
|
||||
*/
|
||||
discuss: one2one('mail.discuss', {
|
||||
inverse: 'threadViewer',
|
||||
}),
|
||||
/**
|
||||
* Serves as compute dependency.
|
||||
*/
|
||||
discussHasThreadView: attr({
|
||||
related: 'discuss.hasThreadView',
|
||||
}),
|
||||
/**
|
||||
* Serves as compute dependency.
|
||||
*/
|
||||
discussStringifiedDomain: attr({
|
||||
related: 'discuss.stringifiedDomain',
|
||||
}),
|
||||
/**
|
||||
* Serves as compute dependency.
|
||||
*/
|
||||
discussThread: many2one('mail.thread', {
|
||||
related: 'discuss.thread',
|
||||
}),
|
||||
/**
|
||||
* Determines whether `this.thread` should be displayed.
|
||||
*/
|
||||
hasThreadView: attr({
|
||||
compute: '_computeHasThreadView',
|
||||
default: false,
|
||||
dependencies: [
|
||||
'chatterHasThreadView',
|
||||
'chatWindowHasThreadView',
|
||||
'discussHasThreadView',
|
||||
],
|
||||
}),
|
||||
/**
|
||||
* Determines the domain to apply when fetching messages for `this.thread`.
|
||||
*/
|
||||
stringifiedDomain: attr({
|
||||
compute: '_computeStringifiedDomain',
|
||||
default: '[]',
|
||||
dependencies: [
|
||||
'discussStringifiedDomain',
|
||||
],
|
||||
}),
|
||||
/**
|
||||
* Determines the `mail.thread` that should be displayed by `this`.
|
||||
*/
|
||||
thread: many2one('mail.thread', {
|
||||
compute: '_computeThread',
|
||||
dependencies: [
|
||||
'chatterThread',
|
||||
'chatWindowThread',
|
||||
'discussThread',
|
||||
],
|
||||
}),
|
||||
/**
|
||||
* States the `mail.thread_cache` that should be displayed by `this`.
|
||||
*/
|
||||
threadCache: many2one('mail.thread_cache', {
|
||||
compute: '_computeThreadCache',
|
||||
dependencies: [
|
||||
'stringifiedDomain',
|
||||
'thread',
|
||||
],
|
||||
}),
|
||||
/**
|
||||
* Determines the initial scroll positions of thread caches.
|
||||
* Useful to restore scroll position on changing back to this
|
||||
* thread cache. Note that this is only applied when opening
|
||||
* the thread cache, because scroll position may change fast so
|
||||
* save is already throttled.
|
||||
*/
|
||||
threadCacheInitialScrollPositions: attr({
|
||||
default: {},
|
||||
}),
|
||||
/**
|
||||
* States the `mail.thread_view` currently displayed and managed by `this`.
|
||||
*/
|
||||
threadView: one2one('mail.thread_view', {
|
||||
compute: '_computeThreadView',
|
||||
dependencies: [
|
||||
'hasThreadView',
|
||||
],
|
||||
inverse: 'threadViewer',
|
||||
isCausal: true,
|
||||
}),
|
||||
};
|
||||
|
||||
ThreadViewer.modelName = 'mail.thread_viewer';
|
||||
|
||||
return ThreadViewer;
|
||||
}
|
||||
|
||||
registerNewModel('mail.thread_viewer', factory);
|
||||
|
||||
});
|
||||
@@ -186,6 +186,7 @@ const DiscussWidget = AbstractAction.extend({
|
||||
}
|
||||
// Mark All Read
|
||||
if (
|
||||
this.discuss.threadView &&
|
||||
this.discuss.thread &&
|
||||
this.discuss.thread === this.env.messaging.inbox
|
||||
) {
|
||||
@@ -200,6 +201,7 @@ const DiscussWidget = AbstractAction.extend({
|
||||
}
|
||||
// Unstar All
|
||||
if (
|
||||
this.discuss.threadView &&
|
||||
this.discuss.thread &&
|
||||
this.discuss.thread === this.env.messaging.starred
|
||||
) {
|
||||
@@ -243,8 +245,11 @@ const DiscussWidget = AbstractAction.extend({
|
||||
const $unselectAll = this.$buttons.find('.o_widget_Discuss_controlPanelButtonUnselectAll');
|
||||
|
||||
if (
|
||||
this.discuss.threadView.checkedMessages.length > 0 ||
|
||||
this.discuss.threadView.uncheckedMessages.length > 0
|
||||
this.discuss.threadView &&
|
||||
(
|
||||
this.discuss.threadView.checkedMessages.length > 0 ||
|
||||
this.discuss.threadView.uncheckedMessages.length > 0
|
||||
)
|
||||
) {
|
||||
$selectAll.removeClass('o_hidden');
|
||||
$selectAll.toggleClass('disabled', this.discuss.threadView.uncheckedMessages.length === 0);
|
||||
@@ -260,6 +265,7 @@ const DiscussWidget = AbstractAction.extend({
|
||||
// Moderation Actions
|
||||
const $moderationButtons = this.$buttons.find('.o_widget_Discuss_controlPanelButtonModeration');
|
||||
if (
|
||||
this.discuss.threadView &&
|
||||
this.discuss.threadView.checkedMessages.length > 0 &&
|
||||
this.discuss.threadView.checkedMessages.filter(
|
||||
message => !message.isModeratedByCurrentPartner
|
||||
@@ -371,7 +377,7 @@ const DiscussWidget = AbstractAction.extend({
|
||||
* @param {Object} searchQuery
|
||||
*/
|
||||
_onSearch: function (searchQuery) {
|
||||
this.discuss.threadView.update({
|
||||
this.discuss.update({
|
||||
stringifiedDomain: JSON.stringify(searchQuery.domain),
|
||||
});
|
||||
},
|
||||
|
||||
@@ -102,7 +102,7 @@ FormRenderer.include({
|
||||
followerIds,
|
||||
hasActivities: this.chatterFields.hasActivityIds,
|
||||
hasFollowers: this.chatterFields.hasMessageFollowerIds,
|
||||
hasThread: this.chatterFields.hasMessageIds,
|
||||
hasMessageList: this.chatterFields.hasMessageIds,
|
||||
isAttachmentBoxVisible: this.chatterFields.isAttachmentBoxVisible,
|
||||
messageIds,
|
||||
threadAttachmentCount,
|
||||
|
||||
@@ -468,9 +468,17 @@ QUnit.test('chatter updating', async function (assert) {
|
||||
{ display_name: "first partner", id: 11 },
|
||||
{ display_name: "second partner", id: 12 }
|
||||
);
|
||||
const messageFetchChannelDef = makeDeferred();
|
||||
await this.createView({
|
||||
data: this.data,
|
||||
hasView: true,
|
||||
async mockRPC(route, args) {
|
||||
const res = await this._super(...arguments);
|
||||
if (route.includes('message_fetch')) {
|
||||
messageFetchChannelDef.resolve();
|
||||
}
|
||||
return res;
|
||||
},
|
||||
// View params
|
||||
View: FormView,
|
||||
model: 'res.partner',
|
||||
@@ -501,8 +509,10 @@ QUnit.test('chatter updating', async function (assert) {
|
||||
"there should be no message"
|
||||
);
|
||||
|
||||
await afterNextRender(() => {
|
||||
await afterNextRender(async () => {
|
||||
document.querySelector('.o_pager_next').click();
|
||||
// wait until messages are fetched, ignore other renders that are too early
|
||||
await messageFetchChannelDef;
|
||||
});
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
odoo.define('mail.MockServer', function (require) {
|
||||
"use strict";
|
||||
|
||||
const { nextAnimationFrame } = require('mail/static/src/utils/test_utils.js');
|
||||
|
||||
const MockServer = require('web.MockServer');
|
||||
|
||||
MockServer.include({
|
||||
@@ -197,6 +199,9 @@ MockServer.include({
|
||||
return this._mockMailMessageMarkAllAsRead(domain);
|
||||
}
|
||||
if (args.model === 'mail.message' && args.method === 'message_fetch') {
|
||||
// TODO FIXME delay RPC until next potential render as a workaround
|
||||
// to issue https://github.com/odoo/owl/pull/724
|
||||
await nextAnimationFrame();
|
||||
const domain = args.args[0] || args.kwargs.domain;
|
||||
const limit = args.args[1] || args.kwargs.limit;
|
||||
const moderated_channel_ids = args.args[2] || args.kwargs.moderated_channel_ids;
|
||||
|
||||
@@ -151,6 +151,7 @@
|
||||
<script type="text/javascript" src="/mail/static/src/models/thread_cache/thread_cache.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/models/thread_partner_seen_info/thread_partner_seen_info.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/models/thread_view/thread_view.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/models/thread_view/thread_viewer.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/models/user/user.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/services/chat_window_service/chat_window_service.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/services/dialog_service/dialog_service.js"></script>
|
||||
|
||||
@@ -46,7 +46,8 @@ QUnit.test('Notification Sent', async function (assert) {
|
||||
assert.expect(9);
|
||||
|
||||
await this.start();
|
||||
const threadView = this.env.models['mail.thread_view'].create({
|
||||
const threadViewer = this.env.models['mail.thread_viewer'].create({
|
||||
hasThreadView: true,
|
||||
thread: [['create', {
|
||||
id: 11,
|
||||
model: 'mail.channel',
|
||||
@@ -61,10 +62,10 @@ QUnit.test('Notification Sent', async function (assert) {
|
||||
notification_type: 'sms',
|
||||
partner: [['insert', { id: 12, name: "Someone" }]],
|
||||
}]],
|
||||
originThread: [['link', threadView.thread]]
|
||||
originThread: [['link', threadViewer.thread]]
|
||||
});
|
||||
await this.createMessageComponent(message, {
|
||||
threadViewLocalId: threadView.localId
|
||||
threadViewLocalId: threadViewer.threadView.localId
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
@@ -137,7 +138,8 @@ QUnit.test('Notification Error', async function (assert) {
|
||||
});
|
||||
|
||||
await this.start({ env: { bus } });
|
||||
const threadView = this.env.models['mail.thread_view'].create({
|
||||
const threadViewer = this.env.models['mail.thread_viewer'].create({
|
||||
hasThreadView: true,
|
||||
thread: [['create', {
|
||||
id: 11,
|
||||
model: 'mail.channel',
|
||||
@@ -151,10 +153,10 @@ QUnit.test('Notification Error', async function (assert) {
|
||||
notification_status: 'exception',
|
||||
notification_type: 'sms',
|
||||
}]],
|
||||
originThread: [['link', threadView.thread]]
|
||||
originThread: [['link', threadViewer.thread]]
|
||||
});
|
||||
await this.createMessageComponent(message, {
|
||||
threadViewLocalId: threadView.localId
|
||||
threadViewLocalId: threadViewer.threadView.localId
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
|
||||
@@ -46,7 +46,8 @@ QUnit.test('Sent', async function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
await this.start();
|
||||
const threadView = this.env.models['mail.thread_view'].create({
|
||||
const threadViewer = this.env.models['mail.thread_viewer'].create({
|
||||
hasThreadView: true,
|
||||
thread: [['create', {
|
||||
id: 11,
|
||||
model: 'mail.channel',
|
||||
@@ -60,10 +61,10 @@ QUnit.test('Sent', async function (assert) {
|
||||
notification_status: 'sent',
|
||||
notification_type: 'snail',
|
||||
}]],
|
||||
originThread: [['link', threadView.thread]],
|
||||
originThread: [['link', threadViewer.thread]],
|
||||
});
|
||||
await this.createMessageComponent(message, {
|
||||
threadViewLocalId: threadView.localId
|
||||
threadViewLocalId: threadViewer.threadView.localId
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
@@ -116,7 +117,8 @@ QUnit.test('Canceled', async function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
await this.start();
|
||||
const threadView = this.env.models['mail.thread_view'].create({
|
||||
const threadViewer = this.env.models['mail.thread_viewer'].create({
|
||||
hasThreadView: true,
|
||||
thread: [['create', {
|
||||
id: 11,
|
||||
model: 'mail.channel',
|
||||
@@ -130,10 +132,10 @@ QUnit.test('Canceled', async function (assert) {
|
||||
notification_status: 'canceled',
|
||||
notification_type: 'snail',
|
||||
}]],
|
||||
originThread: [['link', threadView.thread]],
|
||||
originThread: [['link', threadViewer.thread]],
|
||||
});
|
||||
await this.createMessageComponent(message, {
|
||||
threadViewLocalId: threadView.localId
|
||||
threadViewLocalId: threadViewer.threadView.localId
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
@@ -186,7 +188,8 @@ QUnit.test('Pending', async function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
await this.start();
|
||||
const threadView = this.env.models['mail.thread_view'].create({
|
||||
const threadViewer = this.env.models['mail.thread_viewer'].create({
|
||||
hasThreadView: true,
|
||||
thread: [['create', {
|
||||
id: 11,
|
||||
model: 'mail.channel',
|
||||
@@ -200,10 +203,10 @@ QUnit.test('Pending', async function (assert) {
|
||||
notification_status: 'ready',
|
||||
notification_type: 'snail',
|
||||
}]],
|
||||
originThread: [['link', threadView.thread]],
|
||||
originThread: [['link', threadViewer.thread]],
|
||||
});
|
||||
await this.createMessageComponent(message, {
|
||||
threadViewLocalId: threadView.localId
|
||||
threadViewLocalId: threadViewer.threadView.localId
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
@@ -263,7 +266,8 @@ QUnit.test('No Price Available', async function (assert) {
|
||||
return this._super(...arguments);
|
||||
},
|
||||
});
|
||||
const threadView = this.env.models['mail.thread_view'].create({
|
||||
const threadViewer = this.env.models['mail.thread_viewer'].create({
|
||||
hasThreadView: true,
|
||||
thread: [['create', {
|
||||
id: 11,
|
||||
model: 'mail.channel',
|
||||
@@ -278,10 +282,10 @@ QUnit.test('No Price Available', async function (assert) {
|
||||
notification_status: 'exception',
|
||||
notification_type: 'snail',
|
||||
}]],
|
||||
originThread: [['link', threadView.thread]],
|
||||
originThread: [['link', threadViewer.thread]],
|
||||
});
|
||||
await this.createMessageComponent(message, {
|
||||
threadViewLocalId: threadView.localId
|
||||
threadViewLocalId: threadViewer.threadView.localId
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
@@ -349,7 +353,8 @@ QUnit.test('Credit Error', async function (assert) {
|
||||
return this._super(...arguments);
|
||||
},
|
||||
});
|
||||
const threadView = this.env.models['mail.thread_view'].create({
|
||||
const threadViewer = this.env.models['mail.thread_viewer'].create({
|
||||
hasThreadView: true,
|
||||
thread: [['create', {
|
||||
id: 11,
|
||||
model: 'mail.channel',
|
||||
@@ -364,10 +369,10 @@ QUnit.test('Credit Error', async function (assert) {
|
||||
notification_status: 'exception',
|
||||
notification_type: 'snail',
|
||||
}]],
|
||||
originThread: [['link', threadView.thread]],
|
||||
originThread: [['link', threadViewer.thread]],
|
||||
});
|
||||
await this.createMessageComponent(message, {
|
||||
threadViewLocalId: threadView.localId
|
||||
threadViewLocalId: threadViewer.threadView.localId
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
@@ -440,7 +445,8 @@ QUnit.test('Trial Error', async function (assert) {
|
||||
return this._super(...arguments);
|
||||
},
|
||||
});
|
||||
const threadView = this.env.models['mail.thread_view'].create({
|
||||
const threadViewer = this.env.models['mail.thread_viewer'].create({
|
||||
hasThreadView: true,
|
||||
thread: [['create', {
|
||||
id: 11,
|
||||
model: 'mail.channel',
|
||||
@@ -455,10 +461,10 @@ QUnit.test('Trial Error', async function (assert) {
|
||||
notification_status: 'exception',
|
||||
notification_type: 'snail',
|
||||
}]],
|
||||
originThread: [['link', threadView.thread]],
|
||||
originThread: [['link', threadViewer.thread]],
|
||||
});
|
||||
await this.createMessageComponent(message, {
|
||||
threadViewLocalId: threadView.localId
|
||||
threadViewLocalId: threadViewer.threadView.localId
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
@@ -539,7 +545,8 @@ QUnit.test('Format Error', async function (assert) {
|
||||
});
|
||||
|
||||
await this.start({ env: { bus } });
|
||||
const threadView = this.env.models['mail.thread_view'].create({
|
||||
const threadViewer = this.env.models['mail.thread_viewer'].create({
|
||||
hasThreadView: true,
|
||||
thread: [['create', {
|
||||
id: 11,
|
||||
model: 'mail.channel',
|
||||
@@ -554,10 +561,10 @@ QUnit.test('Format Error', async function (assert) {
|
||||
notification_status: 'exception',
|
||||
notification_type: 'snail',
|
||||
}]],
|
||||
originThread: [['link', threadView.thread]],
|
||||
originThread: [['link', threadViewer.thread]],
|
||||
});
|
||||
await this.createMessageComponent(message, {
|
||||
threadViewLocalId: threadView.localId
|
||||
threadViewLocalId: threadViewer.threadView.localId
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
@@ -627,11 +634,14 @@ QUnit.test('Missing Required Fields', async function (assert) {
|
||||
await this.start({
|
||||
env: { bus },
|
||||
});
|
||||
const threadView = this.env.models['mail.thread_view'].create({
|
||||
const threadViewer = this.env.models['mail.thread_viewer'].create({
|
||||
hasThreadView: true,
|
||||
thread: [['insert', { id: 20, model: 'res.partner' }]],
|
||||
});
|
||||
const message = this.env.models['mail.message'].find(message => message.id === 10);
|
||||
await this.createMessageComponent(message, { threadViewLocalId: threadView.localId });
|
||||
await this.createMessageComponent(message, {
|
||||
threadViewLocalId: threadViewer.threadView.localId,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
|
||||
Reference in New Issue
Block a user