/** @odoo-module **/ import { registerModel } from '@mail/model/model_core'; import { attr, one } from '@mail/model/model_field'; import { clear, insert, link } from '@mail/model/model_field_command'; import { makeDeferred } from '@mail/utils/deferred'; const getThreadNextTemporaryId = (function () { let tmpId = 0; return () => { tmpId -= 1; return tmpId; }; })(); const getMessageNextTemporaryId = (function () { let tmpId = 0; return () => { tmpId -= 1; return tmpId; }; })(); registerModel({ name: 'Chatter', recordMethods: { focus() { if (this.composerView) { this.composerView.update({ doFocus: true }); } }, async doSaveRecord() { const saved = await this.saveRecord(); if (!saved) { return saved; } let composerData = null; if (this.composerView) { const { attachments, isLog, rawMentionedChannels, rawMentionedPartners, textInputContent, textInputCursorEnd, textInputCursorStart, textInputSelectionDirection, } = this.composerView.composer; composerData = { attachments, isLog, rawMentionedChannels, rawMentionedPartners, textInputContent, textInputCursorEnd, textInputCursorStart, textInputSelectionDirection, }; } // Wait for next render from chatter_container, // So that it changes to composer of new thread this.update({ createNewRecordComposerData: composerData, createNewRecordDeferred: composerData ? makeDeferred() : null, }); await this.createNewRecordDeferred; // Give some time to chatter model being updated by save. await new Promise((resolve) => setTimeout(() => requestAnimationFrame(resolve))); return saved; }, onAttachmentsLoadingTimeout() { this.update({ attachmentsLoaderTimer: clear(), isShowingAttachmentsLoading: true, }); }, /** * Handles click on the attach button. */ async onClickButtonAddAttachments() { if (this.isTemporary) { const saved = await this.doSaveRecord(); if (!saved) { return; } } this.fileUploader.openBrowserFileUploader(); }, /** * Handles click on the attachments button. */ async onClickButtonToggleAttachments() { if (this.isTemporary) { const saved = await this.doSaveRecord(); if (!saved) { return; } } this.update({ attachmentBoxView: this.attachmentBoxView ? clear() : {} }); if (this.attachmentBoxView) { this.scrollPanelRef.el.scrollTop = 0; } }, /** * Handles click on top bar close button. * * @param {MouseEvent} ev */ onClickChatterTopbarClose(ev) { this.component.trigger('o-close-chatter'); }, /** * Handles click on "log note" button. * * @param {MouseEvent} ev */ onClickLogNote() { if (this.composerView && this.composerView.composer.isLog) { this.update({ composerView: clear() }); } else { this.showLogNote(); } }, /** * Handles click on "schedule activity" button. * * @param {MouseEvent} ev */ async onClickScheduleActivity(ev) { if (this.isTemporary) { const saved = await this.doSaveRecord(); if (!saved) { return; } } await this.messaging.openActivityForm({ thread: this.thread }); if (this.exists()) { this.reloadParentView(); } }, /** * Handles click on "send message" button. * * @param {MouseEvent} ev */ onClickSendMessage(ev) { if (this.composerView && !this.composerView.composer.isLog) { this.update({ composerView: clear() }); } else { this.showSendMessage(); } }, /** * Handles scroll on this scroll panel. * * @param {Event} ev */ onScrollScrollPanel(ev) { if (!this.threadView || !this.threadView.messageListView || !this.threadView.messageListView.component) { return; } this.threadView.messageListView.component.onScroll(ev); }, openAttachmentBoxView() { this.update({ attachmentBoxView: {} }); }, /** * Open a dialog to add partners as followers. */ promptAddPartnerFollower() { const action = { type: 'ir.actions.act_window', res_model: 'mail.wizard.invite', view_mode: 'form', views: [[false, 'form']], name: this.env._t("Invite Follower"), target: 'new', context: { default_res_model: this.thread.model, default_res_id: this.thread.id, }, }; this.env.services.action.doAction( action, { onClose: async () => { if (!this.exists() && !this.thread) { return; } await this.thread.fetchData(['followers']); if (this.exists() && this.hasParentReloadOnFollowersUpdate) { this.reloadParentView(); } }, } ); }, async refresh() { const requestData = ['activities', 'followers', 'suggestedRecipients']; if (this.hasMessageList) { requestData.push('attachments', 'messages'); } this.thread.fetchData(requestData); }, /** * @param {Object} [param0={}] * @param {string[]} [fieldNames] */ async reloadParentView({ fieldNames } = {}) { if (this.saveRecord) { await this.saveRecord(); } if (this.webRecord) { await this.webRecord.model.root.load({ resId: this.threadId }, { keepChanges: true }); this.webRecord.model.notify(); return; } if (this.component) { const options = { keepChanges: true }; if (fieldNames) { options.fieldNames = fieldNames; } this.component.trigger('reload', options); } }, showLogNote() { this.update({ composerView: {} }); this.composerView.composer.update({ isLog: true }); this.focus(); }, showSendMessage() { this.update({ composerView: {} }); this.composerView.composer.update({ isLog: false }); this.focus(); }, /** * @private */ _onThreadIdOrThreadModelChanged() { if (!this.threadModel) { return; } if (this.threadId) { if (this.thread && this.thread.isTemporary) { this.thread.delete(); } this.update({ attachmentBoxView: this.isAttachmentBoxVisibleInitially ? {} : clear(), thread: insert({ // If the thread was considered to have the activity // mixin once, it will have it forever. hasActivities: this.hasActivities ? true : undefined, id: this.threadId, model: this.threadModel, }), }); } else if (!this.thread || !this.thread.isTemporary) { const currentPartner = this.messaging.currentPartner; const message = this.messaging.models['Message'].insert({ author: currentPartner, body: this.env._t("Creating a new record..."), id: getMessageNextTemporaryId(), isTemporary: true, }); const nextId = getThreadNextTemporaryId(); this.update({ attachmentBoxView: clear(), thread: insert({ areAttachmentsLoaded: true, id: nextId, isTemporary: true, model: this.threadModel, }), }); this.thread.cache.update({ temporaryMessages: link(message) }); } // continuation of saving new record: restore composer state if (this.createNewRecordComposerData) { this.update({ composerView: { composer: { ...this.createNewRecordComposerData, thread: this.thread, }, }, }); this.createNewRecordDeferred.resolve(); } this.update({ createNewRecordComposerData: clear(), createNewRecordDeferred: clear(), }); }, /** * @private */ _onThreadIsLoadingAttachmentsChanged() { if (!this.thread || !this.thread.isLoadingAttachments) { this.update({ attachmentsLoaderTimer: clear(), isShowingAttachmentsLoading: false, }); return; } if (this.isPreparingAttachmentsLoading || this.isShowingAttachmentsLoading) { return; } this._prepareAttachmentsLoading(); }, /** * @private */ _prepareAttachmentsLoading() { this.update({ attachmentsLoaderTimer: {} }); }, }, fields: { activityBoxView: one('ActivityBoxView', { compute() { if (this.thread && this.thread.hasActivities && this.thread.activities.length > 0) { return {}; } return clear(); }, inverse: 'chatter', }), attachmentBoxView: one('AttachmentBoxView', { inverse: 'chatter', }), attachmentsLoaderTimer: one('Timer', { inverse: 'chatterOwnerAsAttachmentsLoader', }), canPostMessage: attr({ compute() { return Boolean(this.isTemporary || this.hasWriteAccess || (this.hasReadAccess && this.thread && this.thread.canPostOnReadonly)); }, }), /** * States the OWL Chatter component of this chatter. */ component: attr(), /** * Determines the composer view used to post in this chatter (if any). */ composerView: one('ComposerView', { inverse: 'chatter', }), context: attr({ default: {}, }), dropZoneView: one('DropZoneView', { compute() { if (!this.thread) { return clear(); } if (this.useDragVisibleDropZone.isVisible) { return {}; } return clear(); }, inverse: 'chatterOwner', }), fileUploader: one('FileUploader', { compute() { return this.thread ? {} : clear(); }, inverse: 'chatterOwner', }), followButtonView: one('FollowButtonView', { compute() { if (this.hasFollowers && this.thread && (!this.thread.channel || this.thread.channel.channel_type !== 'chat')) { return {}; } return clear(); }, inverse: 'chatterOwner', }), followerListMenuView: one('FollowerListMenuView', { compute() { if (this.hasFollowers && this.thread) { return {}; } return clear(); }, inverse: 'chatterOwner', }), /** * Determines whether `this` should display an activity box. */ hasActivities: attr({ default: true, }), hasExternalBorder: attr({ default: true, }), /** * Determines whether `this` should display followers menu. */ 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, * then the scroll is managed by the message list. * Also, the message list shoud not manage the scroll if it shares it * with the rest of the page. */ hasMessageListScrollAdjust: attr({ default: false, }), hasParentReloadOnAttachmentsChanged: attr({ default: false, }), hasParentReloadOnFollowersUpdate: attr({ default: false, }), hasParentReloadOnMessagePosted: attr({ default: false, }), hasReadAccess: attr({ compute() { return Boolean(this.thread && !this.thread.isTemporary && this.thread.hasReadAccess); }, }), /** * Determines whether `this.thread` should be displayed. */ hasThreadView: attr({ compute() { return Boolean(this.thread && this.hasMessageList); }, }), hasWriteAccess: attr({ compute() { return Boolean(this.thread && !this.thread.isTemporary && this.thread.hasWriteAccess); }, }), hasTopbarCloseButton: attr({ default: false, }), /** * States the id of this chatter. This id does not correspond to any * specific value, it is just a unique identifier given by the creator * of this record. */ id: attr({ identifying: true, }), /** * Determiners whether the attachment box is visible initially. */ isAttachmentBoxVisibleInitially: attr({ default: false, }), isInFormSheetBg: attr({ default: false, }), isPreparingAttachmentsLoading: attr({ compute() { return Boolean(this.attachmentsLoaderTimer); }, default: false, }), isShowingAttachmentsLoading: attr({ default: false, }), isTemporary: attr({ compute() { return Boolean(!this.thread || this.thread.isTemporary); }, }), saveRecord: attr(), scrollPanelRef: attr(), /** * Determines whether the view should reload after file changed in this chatter, * such as from a file upload. */ shouldReloadParentFromFileChanged: attr({ compute() { return this.hasParentReloadOnAttachmentsChanged; }, }), /** * Determines the `Thread` that should be displayed by `this`. */ thread: one('Thread'), /** * Determines the id of the thread that will be displayed by `this`. */ threadId: attr(), /** * Determines the model of the thread that will be displayed by `this`. */ threadModel: attr(), /** * States the `ThreadView` displaying `this.thread`. */ threadView: one('ThreadView', { related: 'threadViewer.threadView', }), /** * Determines the `ThreadViewer` managing the display of `this.thread`. */ threadViewer: one('ThreadViewer', { compute() { if (!this.thread) { return clear(); } return { hasThreadView: this.hasThreadView, order: 'desc', thread: this.thread ? this.thread : clear(), }; }, inverse: 'chatter', }), topbar: one('ChatterTopbar', { compute() { return this.thread ? {} : clear(); }, inverse: 'chatter', }), useDragVisibleDropZone: one('UseDragVisibleDropZone', { default: {}, inverse: 'chatterOwner', readonly: true, required: true, }), webRecord: attr(), createNewRecordComposerData: attr(), createNewRecordDeferred: attr(), }, onChanges: [ { dependencies: ['threadId', 'threadModel'], methodName: '_onThreadIdOrThreadModelChanged', }, { dependencies: ['thread.isLoadingAttachments'], methodName: '_onThreadIsLoadingAttachmentsChanged', }, ], });