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

507 lines
18 KiB
JavaScript

/** @odoo-module **/
import { attr, clear, many, one, Model } from "@mail/model";
Model({
name: "ThreadView",
template: "mail.ThreadView",
lifecycleHooks: {
_willDelete() {
this.messaging.browser.clearTimeout(this.loaderTimeout);
},
},
recordMethods: {
/**
* This function register a hint for the component related to this
* record. Hints are information on changes around this viewer that
* make require adjustment on the component. For instance, if this
* ThreadView initiated a thread cache load and it now has become
* loaded, then it may need to auto-scroll to last message.
*
* @param {string} hintType name of the hint. Used to determine what's
* the broad type of adjustement the component has to do.
* @param {any} [hintData] data of the hint. Used to fine-tune
* adjustments on the component.
*/
addComponentHint(hintType, hintData) {
const hint = { data: hintData, type: hintType };
this.update({
componentHintList: this.componentHintList.concat([hint]),
});
},
/**
* @param {Message} message
*/
handleVisibleMessage(message) {
if (!this.lastVisibleMessage || this.lastVisibleMessage.id < message.id) {
this.update({ lastVisibleMessage: message });
}
},
/**
* @param {Object} hint
*/
markComponentHintProcessed(hint) {
this.update({
componentHintList: this.componentHintList.filter((h) => h !== hint),
});
this.messaging.messagingBus.trigger("o-thread-view-hint-processed", {
hint,
threadViewer: this.threadViewer,
});
},
onClickRetryLoadMessages() {
if (!this.exists()) {
return;
}
if (!this.threadCache) {
return;
}
this.threadCache.update({ hasLoadingFailed: false });
},
/**
* Called when an element in the thread becomes focused.
*/
onFocusin() {
if (!this.exists()) {
// prevent crash on destroy
return;
}
if (this.threadViewer.chatWindow) {
this.threadViewer.chatWindow.update({ isFocused: true });
}
},
/**
* Starts editing the last message of this thread from the current user.
*/
startEditingLastMessageFromCurrentUser() {
const messageListViewItems = this.messageListView.messageListViewItems;
messageListViewItems.reverse();
const messageListViewItem = messageListViewItems.find(
(messageListViewItem) =>
messageListViewItem.message.isCurrentUserOrGuestAuthor &&
messageListViewItem.message.canBeDeleted
);
if (messageListViewItem) {
messageListViewItem.messageView.startEditing();
}
},
/**
* Not a real field, used to trigger `thread.markAsSeen` when one of
* the dependencies changes.
*
* @private
* @returns {boolean}
*/
_computeThreadShouldBeSetAsSeen() {
if (!this.thread) {
return;
}
if (!this.thread.lastNonTransientMessage) {
return;
}
if (!this.lastVisibleMessage) {
return;
}
if (this.lastVisibleMessage !== this.lastMessage) {
return;
}
if (!this.isComposerFocused) {
// FIXME condition should not be on "composer is focused" but "threadView is active"
// See task-2277543
return;
}
if (this.messaging.currentGuest) {
return;
}
this.thread.markAsSeen(this.thread.lastNonTransientMessage);
},
/**
* @private
*/
_onThreadCacheChanged() {
// clear obsolete hints
this.update({ componentHintList: clear() });
this.addComponentHint("change-of-thread-cache");
if (this.threadCache) {
this.threadCache.update({
isCacheRefreshRequested: true,
});
}
this.update({ lastVisibleMessage: clear() });
},
/**
* @private
*/
_onThreadCacheIsLoadingChanged() {
if (this.threadCache && this.threadCache.isLoading) {
if (!this.isLoading && !this.isPreparingLoading) {
this.update({ isPreparingLoading: true });
new Promise((resolve) => {
this.update({
loaderTimeout: this.messaging.browser.setTimeout(
resolve,
this.messaging.loadingBaseDelayDuration
),
});
}).then(() => {
if (!this.exists()) {
return;
}
const isLoading = this.threadCache ? this.threadCache.isLoading : false;
this.update({ isLoading, isPreparingLoading: false });
});
}
return;
}
this.messaging.browser.clearTimeout(this.loaderTimeout);
this.update({ isLoading: false, isPreparingLoading: false });
},
/**
* @param {Message} prevMessage
* @param {Message} message
* @returns {boolean}
*/
_shouldMessageBeSquashed(prevMessage, message) {
if (!this.hasSquashCloseMessages) {
return false;
}
if (message.parentMessage) {
return false;
}
if (!prevMessage) {
return false;
}
if (!prevMessage.date && message.date) {
return false;
}
if (
message.momentDate &&
prevMessage.momentDate &&
Math.abs(message.momentDate.diff(prevMessage.momentDate)) > 60000
) {
// more than 1 min. elasped
return false;
}
if (prevMessage.dateDay !== message.dateDay) {
return false;
}
if (prevMessage.message_type !== "comment" || message.message_type !== "comment") {
return false;
}
if (
prevMessage.author !== message.author ||
prevMessage.guestAuthor !== message.guestAuthor
) {
// from a different author
return false;
}
if (prevMessage.originThread !== message.originThread) {
return false;
}
if (prevMessage.notifications.length > 0 || message.notifications.length > 0) {
// visual about notifications is restricted to non-squashed messages
return false;
}
const prevOriginThread = prevMessage.originThread;
const originThread = message.originThread;
if (
prevOriginThread &&
originThread &&
prevOriginThread.model === originThread.model &&
originThread.model !== "mail.channel" &&
prevOriginThread.id !== originThread.id
) {
// messages linked to different document thread
return false;
}
return true;
},
},
fields: {
/**
* Model for the component with the controls for RTC related settings.
*/
callSettingsMenu: one("CallSettingsMenu", {
inverse: "threadViewOwner",
compute() {
if (this.isCallSettingsMenuOpen) {
return {};
}
return clear();
},
}),
channelMemberListView: one("ChannelMemberListView", {
inverse: "threadViewOwner",
compute() {
if (
this.thread &&
this.thread.hasMemberListFeature &&
this.hasMemberList &&
this.isMemberListOpened
) {
return {};
}
return clear();
},
}),
compact: attr({ related: "threadViewer.compact" }),
/**
* List of component hints. Hints contain information that help
* components make UI/UX decisions based on their UI state.
* For instance, on receiving new messages and the last message
* is visible, it should auto-scroll to this new last message.
*
* Format of a component hint:
*
* {
* type: {string} the name of the component hint. Useful
* for components to dispatch behaviour
* based on its type.
* data: {Object} data related to the component hint.
* For instance, if hint suggests to scroll
* to a certain message, data may contain
* message id.
* }
*/
componentHintList: attr({ default: [] }),
composerView: one("ComposerView", {
inverse: "threadView",
compute() {
if (!this.thread || this.thread.mailbox) {
return clear();
}
if (this.threadViewer && this.threadViewer.chatter) {
return clear();
}
return {};
},
}),
/**
* Determines which extra class this thread view component should have.
*/
extraClass: attr({ related: "threadViewer.extraClass" }),
/**
* Determines whether this thread viewer has a member list.
* Only makes sense if thread.hasMemberListFeature is true.
*/
hasMemberList: attr({ related: "threadViewer.hasMemberList" }),
/**
* Determines whether this thread view should squash close messages.
* See `_shouldMessageBeSquashed` for which conditions are considered
* to determine if messages are "close" to each other.
*/
hasSquashCloseMessages: attr({
compute() {
return Boolean(
this.threadViewer &&
!this.threadViewer.chatter &&
this.thread &&
!this.thread.mailbox
);
},
}),
/**
* Determines whether this thread view has a top bar.
*/
hasTopbar: attr({ related: "threadViewer.hasTopbar" }),
isCallSettingsMenuOpen: attr({ default: false }),
isComposerFocused: attr({ related: "composerView.isFocused" }),
/**
* 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 }),
/**
* Determines whether the member list of this thread is opened.
* Only makes sense if hasMemberListFeature and hasMemberList are true.
*/
isMemberListOpened: 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 }),
/**
* Determines whether `this` should automatically scroll on receiving
* a new message. Detection of new message is done through the component
* hint `message-received`.
*/
hasAutoScrollOnMessageReceived: attr({ default: true }),
hasComposerThreadName: attr({
default: false,
compute() {
if (this.threadViewer.discuss) {
return this.threadViewer.discuss.activeThread === this.messaging.inbox.thread;
}
return clear();
},
}),
/**
* If set, determines whether the composer should display status of
* members typing on related thread. When this prop is not provided,
* it defaults to composer component default value.
*/
hasComposerThreadTyping: attr({
default: false,
compute() {
if (this.threadViewer.threadView_hasComposerThreadTyping !== undefined) {
return this.threadViewer.threadView_hasComposerThreadTyping;
}
return clear();
},
}),
/**
* Last message in the context of the currently displayed thread cache.
*/
lastMessage: one("Message", { related: "thread.lastMessage" }),
lastMessageListViewItem: one("MessageListViewItem", {
inverse: "threadViewOwnerAsLastMessageListViewItem",
compute() {
if (!this.messageListView) {
return clear();
}
const {
length,
[length - 1]: messageListViewItem,
} = this.messageListView.messageListViewItems;
return messageListViewItem;
},
}),
/**
* Most recent message in this ThreadView that has been shown to the
* current partner in the currently displayed thread cache.
*/
lastVisibleMessage: one("Message"),
loaderTimeout: attr(),
messageListView: one("MessageListView", {
inverse: "threadViewOwner",
compute() {
return (this.thread && this.thread.isTemporary) ||
(this.threadCache && this.threadCache.isLoaded)
? {}
: clear();
},
}),
messages: many("Message", { related: "threadCache.messages" }),
/**
* States the order mode of the messages on this thread view.
* Either 'asc', or 'desc'.
*/
order: attr({ related: "threadViewer.order" }),
/**
* Determines the message that's currently being replied to.
*/
replyingToMessageView: one("MessageView"),
/**
* Determines the call view of this thread.
*/
callView: one("CallView", {
inverse: "threadView",
compute() {
return this.thread &&
this.thread.model === "mail.channel" &&
this.thread.rtcSessions.length > 0
? {}
: clear();
},
}),
/**
* Determines the `Thread` currently displayed by `this`.
*/
thread: one("Thread", { inverse: "threadViews", related: "threadViewer.thread" }),
/**
* States the `ThreadCache` currently displayed by `this`.
*/
threadCache: one("ThreadCache", {
inverse: "threadViews",
related: "threadViewer.threadCache",
}),
threadCacheInitialScrollHeight: attr({
compute() {
if (!this.threadCache) {
return clear();
}
const threadCacheInitialScrollHeight = this.threadCacheInitialScrollHeights[
this.threadCache.localId
];
if (threadCacheInitialScrollHeight !== undefined) {
return threadCacheInitialScrollHeight;
}
return clear();
},
}),
threadCacheInitialScrollPosition: attr({
compute() {
if (!this.threadCache) {
return clear();
}
const threadCacheInitialScrollPosition = this.threadCacheInitialScrollPositions[
this.threadCache.localId
];
if (threadCacheInitialScrollPosition !== undefined) {
return threadCacheInitialScrollPosition;
}
return clear();
},
}),
/**
* List of saved initial scroll heights of thread caches.
*/
threadCacheInitialScrollHeights: attr({
default: {},
related: "threadViewer.threadCacheInitialScrollHeights",
}),
/**
* List of saved initial scroll positions of thread caches.
*/
threadCacheInitialScrollPositions: attr({
default: {},
related: "threadViewer.threadCacheInitialScrollPositions",
}),
/**
* Determines the `ThreadViewer` currently managing `this`.
*/
threadViewer: one("ThreadViewer", { identifying: true, inverse: "threadView" }),
/**
* Determines the top bar of this thread view, if any.
*/
topbar: one("ThreadViewTopbar", {
inverse: "threadView",
compute() {
return this.hasTopbar ? {} : clear();
},
}),
},
onChanges: [
{
dependencies: ["threadCache"],
methodName: "_onThreadCacheChanged",
},
{
dependencies: ["threadCache.isLoading"],
methodName: "_onThreadCacheIsLoadingChanged",
},
{
dependencies: [
"isComposerFocused",
"lastMessage",
"thread.lastNonTransientMessage",
"lastVisibleMessage",
"threadCache",
],
methodName: "_computeThreadShouldBeSetAsSeen",
},
],
});