Files
odoo_source/addons/mail/static/src/models/thread_cache.js
T
2022-11-14 16:21:39 +01:00

331 lines
12 KiB
JavaScript

/** @odoo-module **/
import { attr, clear, link, many, one, Model } from "@mail/model";
Model({
name: "ThreadCache",
recordMethods: {
async loadMoreMessages() {
if (this.isAllHistoryLoaded || this.isLoading) {
return;
}
if (!this.isLoaded) {
this.update({ isCacheRefreshRequested: true });
return;
}
this.update({ isLoadingMore: true });
const messageIds = this.fetchedMessages.map((message) => message.id);
const limit = 30;
let fetchedMessages;
let success;
try {
fetchedMessages = await this._loadMessages({
limit,
maxId: Math.min(...messageIds),
});
success = true;
} catch {
success = false;
}
if (!this.exists()) {
return;
}
if (success) {
if (fetchedMessages.length < limit) {
this.update({ isAllHistoryLoaded: true });
}
for (const threadView of this.threadViews) {
threadView.addComponentHint("more-messages-loaded", { fetchedMessages });
}
}
this.update({ isLoadingMore: false });
},
/**
* @returns {Message[]|undefined}
*/
async loadNewMessages() {
if (this.isLoading) {
return;
}
if (!this.isLoaded) {
this.update({ isCacheRefreshRequested: true });
return;
}
const messageIds = this.fetchedMessages.map((message) => message.id);
const fetchedMessages = this._loadMessages({ minId: Math.max(...messageIds, 0) });
if (!fetchedMessages || fetchedMessages.length === 0) {
return;
}
for (const threadView of this.threadViews) {
threadView.addComponentHint("new-messages-loaded", { fetchedMessages });
}
return fetchedMessages;
},
/**
* @private
* @returns {boolean}
*/
_computeHasToLoadMessages() {
const res = { hasToLoadMessages: false };
if (!this.thread) {
// happens during destroy or compute executed in wrong order
return res;
}
if (this.hasLoadingFailed) {
return res;
}
const wasCacheRefreshRequested = this.isCacheRefreshRequested;
// mark hint as processed
if (this.isCacheRefreshRequested) {
res.isCacheRefreshRequested = false;
}
if (this.thread.isTemporary) {
// temporary threads don't exist on the server
return res;
}
if (!wasCacheRefreshRequested && this.threadViews.length === 0) {
// don't load message that won't be used
return res;
}
if (this.isLoading) {
// avoid duplicate RPC
return res;
}
if (!wasCacheRefreshRequested && this.isLoaded) {
// avoid duplicate RPC
return res;
}
if (this.isLoaded) {
// Ignore request if it is already loaded or loading. Indeed
// messages are automatically sync with server updates already,
// so there is never a need to refresh it past the first time.
return res;
}
res.hasToLoadMessages = true;
return res;
},
/**
* @private
* @param {Object} [param0={}]
* @param {integer} [param0.limit=30]
* @param {integer} [param0.maxId]
* @param {integer} [param0.minId]
* @returns {Message[]}
* @throws {Error} when failed to load messages
*/
async _loadMessages({ limit = 30, maxId, minId } = {}) {
this.update({ isLoading: true });
let messages;
try {
messages = await this.messaging.models["Message"].performRpcMessageFetch(
this.thread.fetchMessagesUrl,
{
...this.thread.fetchMessagesParams,
limit,
max_id: maxId,
min_id: minId,
}
);
} catch (e) {
if (this.exists()) {
this.update({
hasLoadingFailed: true,
isLoading: false,
});
}
throw e;
}
if (!this.exists()) {
return;
}
this.update({
rawFetchedMessages: link(messages),
hasLoadingFailed: false,
isLoaded: true,
isLoading: false,
});
if (!minId && messages.length < limit) {
this.update({ isAllHistoryLoaded: true });
}
this.messaging.messagingBus.trigger("o-thread-cache-loaded-messages", {
fetchedMessages: messages,
threadCache: this,
});
return messages;
},
/**
* Split method for `_computeHasToLoadMessages` because it has to write
* on 2 fields at once which is not supported by standard compute.
*
* @private
*/
_onChangeForHasToLoadMessages() {
this.update(this._computeHasToLoadMessages());
},
/**
* Loads this thread cache, by fetching the most recent messages in this
* conversation.
*
* @private
*/
async _onHasToLoadMessagesChanged() {
if (!this.hasToLoadMessages) {
return;
}
const fetchedMessages = await this._loadMessages();
if (!this.exists()) {
return;
}
for (const threadView of this.threadViews) {
threadView.addComponentHint("messages-loaded", { fetchedMessages });
}
this.messaging.messagingBus.trigger("o-thread-loaded-messages", {
thread: this.thread,
});
},
},
fields: {
/**
* List of messages that have been fetched by this cache.
*
* This DOES NOT necessarily includes all messages linked to this thread
* cache (@see messages field for that): it just contains list
* of successive messages that have been explicitly fetched by this
* cache. For all non-main caches, this corresponds to all messages.
* For the main cache, however, messages received from the bus
* should be displayed on main cache but they have not been explicitly
* fetched by cache, so they ARE NOT in this list (at least, not until a
* fetch on this thread cache contains this message).
*
* The distinction between messages and fetched messages is important
* to manage "holes" in message list, while still allowing to display
* new messages on main cache of thread in real-time.
*/
fetchedMessages: many("Message", {
compute() {
if (!this.thread) {
return clear();
}
return this.rawFetchedMessages.filter((m) => this.thread.messages.includes(m));
},
}),
/**
* Determines whether the last message fetch failed.
*/
hasLoadingFailed: attr({ default: false }),
/**
* Determines whether `this` should load initial messages.
* @see `onChangeForHasToLoadMessages` value of this field is mainly set
* from this "on change".
* @see `isCacheRefreshRequested` to request manual refresh of messages.
*/
hasToLoadMessages: attr(),
isAllHistoryLoaded: attr({ default: false }),
isLoaded: attr({ default: false }),
isLoading: attr({ default: false }),
isLoadingMore: attr({ default: false }),
/**
* Determines whether `this` should consider refreshing its messages.
* This field is a hint that may or may not lead to an actual refresh.
* @see `hasToLoadMessages`
*/
isCacheRefreshRequested: attr({ default: false }),
/**
* Last message that has been fetched by this thread cache.
*
* This DOES NOT necessarily mean the last message linked to this thread
* cache (@see lastMessage field for that). @see fetchedMessages field
* for a deeper explanation about "fetched" messages.
*/
lastFetchedMessage: one("Message", {
compute() {
const { length: l, [l - 1]: lastFetchedMessage } = this.orderedFetchedMessages;
if (!lastFetchedMessage) {
return clear();
}
return lastFetchedMessage;
},
}),
lastMessage: one("Message", {
compute() {
const { length: l, [l - 1]: lastMessage } = this.orderedMessages;
if (!lastMessage) {
return clear();
}
return lastMessage;
},
}),
/**
* List of messages linked to this cache.
*/
messages: many("Message", {
compute() {
if (!this.thread) {
return clear();
}
let newerMessages;
if (!this.lastFetchedMessage) {
newerMessages = this.thread.messages;
} else {
newerMessages = this.thread.messages.filter(
(message) => message.id > this.lastFetchedMessage.id
);
}
return [...this.fetchedMessages, ...this.temporaryMessages, ...newerMessages];
},
}),
/**
* Ordered list of messages that have been fetched by this cache.
*
* This DOES NOT necessarily includes all messages linked to this thread
* cache (@see orderedMessages field for that). @see fetchedMessages
* field for deeper explanation about "fetched" messages.
*/
orderedFetchedMessages: many("Message", {
compute() {
return this.fetchedMessages.sort((m1, m2) => (m1.id < m2.id ? -1 : 1));
},
}),
/**
* Ordered list of messages linked to this cache.
*/
orderedMessages: many("Message", {
compute() {
return this.messages.sort((m1, m2) => (m1.id < m2.id ? -1 : 1));
},
}),
/**
* List of ordered non empty messages linked to this cache.
*/
orderedNonEmptyMessages: many("Message", {
compute() {
return this.orderedMessages.filter((message) => !message.isEmpty);
},
}),
rawFetchedMessages: many("Message"),
temporaryMessages: many("Message"),
thread: one("Thread", { identifying: true, inverse: "cache" }),
/**
* States the 'ThreadView' that are currently displaying `this`.
*/
threadViews: many("ThreadView", { inverse: "threadCache" }),
},
onChanges: [
{
dependencies: [
"hasLoadingFailed",
"isCacheRefreshRequested",
"isLoaded",
"isLoading",
"thread.isTemporary",
"threadViews",
],
methodName: "_onChangeForHasToLoadMessages",
},
{
dependencies: ["hasToLoadMessages"],
methodName: "_onHasToLoadMessagesChanged",
},
],
});