odoo.define('mail/static/src/models/thread/thread.js', function (require) {
'use strict';

const { registerNewModel } = require('mail/static/src/model/model_core.js');
const { attr, many2many, many2one, one2many, one2one } = require('mail/static/src/model/model_field.js');
const { clear } = require('mail/static/src/model/model_field_command.js');
const throttle = require('mail/static/src/utils/throttle/throttle.js');
const Timer = require('mail/static/src/utils/timer/timer.js');
const { cleanSearchTerm } = require('mail/static/src/utils/utils.js');
const mailUtils = require('mail.utils');

function factory(dependencies) {

    class Thread extends dependencies['mail.model'] {

        /**
         * @override
         */
        _willCreate() {
            const res = super._willCreate(...arguments);
            /**
             * Timer of current partner that was currently typing something, but
             * there is no change on the input for 5 seconds. This is used
             * in order to automatically notify other members that current
             * partner has stopped typing something, due to making no changes
             * on the composer for some time.
             */
            this._currentPartnerInactiveTypingTimer = new Timer(
                this.env,
                () => this.async(() => this._onCurrentPartnerInactiveTypingTimeout()),
                5 * 1000
            );
            /**
             * Last 'is_typing' status of current partner that has been notified
             * to other members. Useful to prevent spamming typing notifications
             * to other members if it hasn't changed. An exception is the
             * current partner long typing scenario where current partner has
             * to re-send the same typing notification from time to time, so
             * that other members do not assume he/she is no longer typing
             * something from not receiving any typing notifications for a
             * very long time.
             *
             * Supported values: true/false/undefined.
             * undefined makes only sense initially and during current partner
             * long typing timeout flow.
             */
            this._currentPartnerLastNotifiedIsTyping = undefined;
            /**
             * Timer of current partner that is typing a very long text. When
             * the other members do not receive any typing notification for a
             * long time, they must assume that the related partner is no longer
             * typing something (e.g. they have closed the browser tab).
             * This is a timer to let other members know that current partner
             * is still typing something, so that they should not assume he/she
             * has stopped typing something.
             */
            this._currentPartnerLongTypingTimer = new Timer(
                this.env,
                () => this.async(() => this._onCurrentPartnerLongTypingTimeout()),
                50 * 1000
            );
            /**
             * Determines whether the next request to notify current partner
             * typing status should always result to making RPC, regardless of
             * whether last notified current partner typing status is the same.
             * Most of the time we do not want to notify if value hasn't
             * changed, exception being the long typing scenario of current
             * partner.
             */
            this._forceNotifyNextCurrentPartnerTypingStatus = false;
            /**
             * Registry of timers of partners currently typing in the thread,
             * excluding current partner. This is useful in order to
             * automatically unregister typing members when not receive any
             * typing notification after a long time. Timers are internally
             * indexed by partner records as key. The current partner is
             * ignored in this registry of timers.
             *
             * @see registerOtherMemberTypingMember
             * @see unregisterOtherMemberTypingMember
             */
            this._otherMembersLongTypingTimers = new Map();

            /**
             * Clearable and cancellable throttled version of the
             * `_notifyCurrentPartnerTypingStatus` method.
             * This is useful when the current partner posts a message and
             * types something else afterwards: it must notify immediately that
             * he/she is typing something, instead of waiting for the throttle
             * internal timer.
             *
             * @see _notifyCurrentPartnerTypingStatus
             */
            this._throttleNotifyCurrentPartnerTypingStatus = throttle(
                this.env,
                ({ isTyping }) => this.async(() => this._notifyCurrentPartnerTypingStatus({ isTyping })),
                2.5 * 1000
            );
            return res;
        }

        /**
         * @override
         */
        _willDelete() {
            this._currentPartnerInactiveTypingTimer.clear();
            this._currentPartnerLongTypingTimer.clear();
            this._throttleNotifyCurrentPartnerTypingStatus.clear();
            for (const timer of this._otherMembersLongTypingTimers.values()) {
                timer.clear();
            }
            if (this.isTemporary) {
                for (const message of this.messages) {
                    message.delete();
                }
            }
            return super._willDelete(...arguments);
        }

        //----------------------------------------------------------------------
        // Public
        //----------------------------------------------------------------------

        /**
         * @static
         * @param {mail.thread} [thread] the concerned thread
         */
        static computeLastCurrentPartnerMessageSeenByEveryone(thread = undefined) {
            const threads = thread ? [thread] : this.env.models['mail.thread'].all();
            threads.map(localThread => {
                localThread.update({
                    lastCurrentPartnerMessageSeenByEveryone: localThread._computeLastCurrentPartnerMessageSeenByEveryone(),
                });
            });
        }

        /**
         * @static
         * @param {Object} data
         * @return {Object}
         */
        static convertData(data) {
            const data2 = {
                messagesAsServerChannel: [],
            };
            if ('model' in data) {
                data2.model = data.model;
            }
            if ('channel_type' in data) {
                data2.channel_type = data.channel_type;
                data2.model = 'mail.channel';
            }
            if ('create_uid' in data) {
                data2.creator = [['insert', { id: data.create_uid }]];
            }
            if ('custom_channel_name' in data) {
                data2.custom_channel_name = data.custom_channel_name;
            }
            if ('group_based_subscription' in data) {
                data2.group_based_subscription = data.group_based_subscription;
            }
            if ('id' in data) {
                data2.id = data.id;
            }
            if ('is_minimized' in data && 'state' in data) {
                data2.serverFoldState = data.is_minimized ? data.state : 'closed';
            }
            if ('is_moderator' in data) {
                data2.is_moderator = data.is_moderator;
            }
            if ('is_pinned' in data) {
                data2.isServerPinned = data.is_pinned;
            }
            if ('last_message' in data && data.last_message) {
                data2.messagesAsServerChannel.push(['insert', { id: data.last_message.id }]);
                data2.serverLastMessageId = data.last_message.id;
            }
            if ('last_message_id' in data && data.last_message_id) {
                data2.messagesAsServerChannel.push(['insert', { id: data.last_message_id }]);
                data2.serverLastMessageId = data.last_message_id;
            }
            if ('mass_mailing' in data) {
                data2.mass_mailing = data.mass_mailing;
            }
            if ('moderation' in data) {
                data2.moderation = data.moderation;
            }
            if ('message_needaction_counter' in data) {
                data2.message_needaction_counter = data.message_needaction_counter;
            }
            if ('message_unread_counter' in data) {
                data2.serverMessageUnreadCounter = data.message_unread_counter;
            }
            if ('name' in data) {
                data2.name = data.name;
            }
            if ('public' in data) {
                data2.public = data.public;
            }
            if ('seen_message_id' in data) {
                data2.lastSeenByCurrentPartnerMessageId = data.seen_message_id || 0;
            }
            if ('uuid' in data) {
                data2.uuid = data.uuid;
            }

            // relations
            if ('members' in data) {
                if (!data.members) {
                    data2.members = [['unlink-all']];
                } else {
                    data2.members = [
                        ['insert-and-replace', data.members.map(memberData =>
                            this.env.models['mail.partner'].convertData(memberData)
                        )],
                    ];
                }
            }
            if ('seen_partners_info' in data) {
                if (!data.seen_partners_info) {
                    data2.partnerSeenInfos = [['unlink-all']];
                } else {
                    /*
                     * FIXME: not optimal to write on relation given the fact that the relation
                     * will be (re)computed based on given fields.
                     * (here channelId will compute partnerSeenInfo.thread))
                     * task-2336946
                     */
                    data2.partnerSeenInfos = [
                        ['insert-and-replace',
                            data.seen_partners_info.map(
                                ({ fetched_message_id, partner_id, seen_message_id }) => {
                                    return {
                                        channelId: data2.id,
                                        lastFetchedMessage: [fetched_message_id ? ['insert', { id: fetched_message_id }] : ['unlink-all']],
                                        lastSeenMessage: [seen_message_id ? ['insert', { id: seen_message_id }] : ['unlink-all']],
                                        partnerId: partner_id,
                                    };
                                })
                        ]
                    ];
                    if (data.id || this.id) {
                        const messageIds = data.seen_partners_info.reduce((currentSet, { fetched_message_id, seen_message_id }) => {
                            if (fetched_message_id) {
                                currentSet.add(fetched_message_id);
                            }
                            if (seen_message_id) {
                                currentSet.add(seen_message_id);
                            }
                            return currentSet;
                        }, new Set());
                        if (messageIds.size > 0) {
                            /*
                             * FIXME: not optimal to write on relation given the fact that the relation
                             * will be (re)computed based on given fields.
                             * (here channelId will compute messageSeenIndicator.thread))
                             * task-2336946
                             */
                            data2.messageSeenIndicators = [
                                ['insert',
                                    [...messageIds].map(messageId => {
                                       return {
                                           channelId: data.id || this.id,
                                           messageId,
                                       };
                                    })
                                ]
                            ];
                        }
                    }
                }
            }

            return data2;
        }

        /**
         * Fetches threads matching the given composer search state to extend
         * the JS knowledge and to update the suggestion list accordingly.
         * More specifically only thread of model 'mail.channel' are fetched.
         *
         * @static
         * @param {string} searchTerm
         * @param {Object} [options={}]
         * @param {mail.thread} [options.thread] prioritize and/or restrict
         *  result in the context of given thread
         */
        static async fetchSuggestions(searchTerm, { thread } = {}) {
            const channelsData = await this.env.services.rpc(
                {
                    model: 'mail.channel',
                    method: 'get_mention_suggestions',
                    kwargs: { search: searchTerm },
                },
                { shadow: true },
            );
            this.env.models['mail.thread'].insert(channelsData.map(channelData =>
                Object.assign(
                    { model: 'mail.channel' },
                    this.env.models['mail.thread'].convertData(channelData),
                )
            ));
        }

        /**
         * Returns a sort function to determine the order of display of threads
         * in the suggestion list.
         *
         * @static
         * @param {string} searchTerm
         * @param {Object} [options={}]
         * @param {mail.thread} [options.thread] prioritize result in the
         *  context of given thread
         * @returns {function}
         */
        static getSuggestionSortFunction(searchTerm, { thread } = {}) {
            const cleanedSearchTerm = cleanSearchTerm(searchTerm);
            return (a, b) => {
                const isAPublic = a.model === 'mail.channel' && a.public === 'public';
                const isBPublic = b.model === 'mail.channel' && b.public === 'public';
                if (isAPublic && !isBPublic) {
                    return -1;
                }
                if (!isAPublic && isBPublic) {
                    return 1;
                }
                const isMemberOfA = a.model === 'mail.channel' && a.members.includes(this.env.messaging.currentPartner);
                const isMemberOfB = b.model === 'mail.channel' && b.members.includes(this.env.messaging.currentPartner);
                if (isMemberOfA && !isMemberOfB) {
                    return -1;
                }
                if (!isMemberOfA && isMemberOfB) {
                    return 1;
                }
                const cleanedAName = cleanSearchTerm(a.name || '');
                const cleanedBName = cleanSearchTerm(b.name || '');
                if (cleanedAName.startsWith(cleanedSearchTerm) && !cleanedBName.startsWith(cleanedSearchTerm)) {
                    return -1;
                }
                if (!cleanedAName.startsWith(cleanedSearchTerm) && cleanedBName.startsWith(cleanedSearchTerm)) {
                    return 1;
                }
                if (cleanedAName < cleanedBName) {
                    return -1;
                }
                if (cleanedAName > cleanedBName) {
                    return 1;
                }
                return a.id - b.id;
            };
        }

        /**
         * Load the previews of the specified threads. Basically, it fetches the
         * last messages, since they are used to display inline content of them.
         *
         * @static
         * @param {mail.thread[]} threads
         */
        static async loadPreviews(threads) {
            const channelIds = threads.reduce((list, thread) => {
                if (thread.model === 'mail.channel') {
                    return list.concat(thread.id);
                }
                return list;
            }, []);
            if (channelIds.length === 0) {
                return;
            }
            const channelPreviews = await this.env.services.rpc({
                model: 'mail.channel',
                method: 'channel_fetch_preview',
                args: [channelIds],
            }, { shadow: true });
            this.env.models['mail.message'].insert(channelPreviews.filter(p => p.last_message).map(
                channelPreview => this.env.models['mail.message'].convertData(channelPreview.last_message)
            ));
        }


        /**
         * Performs the `channel_fold` RPC on `mail.channel`.
         *
         * @static
         * @param {string} uuid
         * @param {string} state
         */
        static async performRpcChannelFold(uuid, state) {
            return this.env.services.rpc({
                model: 'mail.channel',
                method: 'channel_fold',
                kwargs: {
                    state,
                    uuid,
                }
            }, { shadow: true });
        }

        /**
         * Performs the `channel_info` RPC on `mail.channel`.
         *
         * @static
         * @param {Object} param0
         * @param {integer[]} param0.ids list of id of channels
         * @returns {mail.thread[]}
         */
        static async performRpcChannelInfo({ ids }) {
            const channelInfos = await this.env.services.rpc({
                model: 'mail.channel',
                method: 'channel_info',
                args: [ids],
            }, { shadow: true });
            const channels = this.env.models['mail.thread'].insert(
                channelInfos.map(channelInfo => this.env.models['mail.thread'].convertData(channelInfo))
            );
            // manually force recompute of counter
            this.env.messaging.messagingMenu.update();
            return channels;
        }

        /**
         * Performs the `channel_seen` RPC on `mail.channel`.
         *
         * @static
         * @param {Object} param0
         * @param {integer[]} param0.ids list of id of channels
         * @param {integer[]} param0.lastMessageId
         */
        static async performRpcChannelSeen({ ids, lastMessageId }) {
            return this.env.services.rpc({
                model: 'mail.channel',
                method: 'channel_seen',
                args: [ids],
                kwargs: {
                    last_message_id: lastMessageId,
                },
            }, { shadow: true });
        }

        /**
         * Performs the `channel_pin` RPC on `mail.channel`.
         *
         * @static
         * @param {Object} param0
         * @param {boolean} [param0.pinned=false]
         * @param {string} param0.uuid
         */
        static async performRpcChannelPin({ pinned = false, uuid }) {
            return this.env.services.rpc({
                model: 'mail.channel',
                method: 'channel_pin',
                kwargs: {
                    uuid,
                    pinned,
                },
            }, { shadow: true });
        }

        /**
         * Performs the `channel_create` RPC on `mail.channel`.
         *
         * @static
         * @param {Object} param0
         * @param {string} param0.name
         * @param {string} [param0.privacy]
         * @returns {mail.thread} the created channel
         */
        static async performRpcCreateChannel({ name, privacy }) {
            const device = this.env.messaging.device;
            const data = await this.env.services.rpc({
                model: 'mail.channel',
                method: 'channel_create',
                args: [name, privacy],
                kwargs: {
                    context: Object.assign({}, this.env.session.user_content, {
                        // optimize the return value by avoiding useless queries
                        // in non-mobile devices
                        isMobile: device.isMobile,
                    }),
                },
            });
            return this.env.models['mail.thread'].insert(
                this.env.models['mail.thread'].convertData(data)
            );
        }

        /**
         * Performs the `channel_get` RPC on `mail.channel`.
         *
         * `openChat` is preferable in business code because it will avoid the
         * RPC if the chat already exists.
         *
         * @static
         * @param {Object} param0
         * @param {integer[]} param0.partnerIds
         * @param {boolean} [param0.pinForCurrentPartner]
         * @returns {mail.thread|undefined} the created or existing chat
         */
        static async performRpcCreateChat({ partnerIds, pinForCurrentPartner }) {
            const device = this.env.messaging.device;
            // TODO FIX: potential duplicate chat task-2276490
            const data = await this.env.services.rpc({
                model: 'mail.channel',
                method: 'channel_get',
                kwargs: {
                    context: Object.assign({}, this.env.session.user_content, {
                        // optimize the return value by avoiding useless queries
                        // in non-mobile devices
                        isMobile: device.isMobile,
                    }),
                    partners_to: partnerIds,
                    pin: pinForCurrentPartner,
                },
            });
            if (!data) {
                return;
            }
            return this.env.models['mail.thread'].insert(
                this.env.models['mail.thread'].convertData(data)
            );
        }

        /**
         * Performs the `channel_join_and_get_info` RPC on `mail.channel`.
         *
         * @static
         * @param {Object} param0
         * @param {integer} param0.channelId
         * @returns {mail.thread} the channel that was joined
         */
        static async performRpcJoinChannel({ channelId }) {
            const device = this.env.messaging.device;
            const data = await this.env.services.rpc({
                model: 'mail.channel',
                method: 'channel_join_and_get_info',
                args: [[channelId]],
                kwargs: {
                    context: Object.assign({}, this.env.session.user_content, {
                        // optimize the return value by avoiding useless queries
                        // in non-mobile devices
                        isMobile: device.isMobile,
                    }),
                },
            });
            return this.env.models['mail.thread'].insert(
                this.env.models['mail.thread'].convertData(data)
            );
        }

        /**
         * Performs the `execute_command` RPC on `mail.channel`.
         *
         * @static
         * @param {Object} param0
         * @param {integer} param0.channelId
         * @param {string} param0.command
         * @param {Object} [param0.postData={}]
         */
        static async performRpcExecuteCommand({ channelId, command, postData = {} }) {
            return this.env.services.rpc({
                model: 'mail.channel',
                method: 'execute_command',
                args: [[channelId]],
                kwargs: Object.assign({ command }, postData),
            });
        }

        /**
         * Performs the `message_post` RPC on given threadModel.
         *
         * @static
         * @param {Object} param0
         * @param {Object} param0.postData
         * @param {integer} param0.threadId
         * @param {string} param0.threadModel
         * @return {integer} the posted message id
         */
        static async performRpcMessagePost({ postData, threadId, threadModel }) {
            return this.env.services.rpc({
                model: threadModel,
                method: 'message_post',
                args: [threadId],
                kwargs: postData,
            });
        }

        /**
         * Performs RPC on the route `/mail/get_suggested_recipients`.
         *
         * @static
         * @param {Object} param0
         * @param {string} param0.model
         * @param {integer[]} param0.res_id
         */
        static async performRpcMailGetSuggestedRecipients({ model, res_ids }) {
            const data = await this.env.services.rpc({
                route: '/mail/get_suggested_recipients',
                params: {
                    model,
                    res_ids,
                },
            }, { shadow: true });
            for (const id in data) {
                const recipientInfoList = data[id].map(recipientInfoData => {
                    const [partner_id, emailInfo, reason] = recipientInfoData;
                    const [name, email] = emailInfo && mailUtils.parseEmail(emailInfo);
                    return {
                        email,
                        name,
                        partner: [partner_id ? ['insert', { id: partner_id }] : ['unlink']],
                        reason,
                    };
                });
                this.insert({
                    id: parseInt(id),
                    model,
                    suggestedRecipientInfoList: [['insert-and-replace', recipientInfoList]],
                });
            }
        }

        /*
         * Returns threads that match the given search term. More specially only
         * threads of model 'mail.channel' are suggested, and if the context
         * thread is a private channel, only itself is returned if it matches
         * the search term.
         *
         * @static
         * @param {string} searchTerm
         * @param {Object} [options={}]
         * @param {mail.thread} [options.thread] prioritize and/or restrict
         *  result in the context of given thread
         * @returns {[mail.threads[], mail.threads[]]}
         */
        static searchSuggestions(searchTerm, { thread } = {}) {
            let threads;
            if (thread && thread.model === 'mail.channel' && thread.public !== 'public') {
                // Only return the current channel when in the context of a
                // non-public channel. Indeed, the message with the mention
                // would appear in the target channel, so this prevents from
                // inadvertently leaking the private message into the mentioned
                // channel.
                threads = [thread];
            } else {
                threads = this.env.models['mail.thread'].all();
            }
            const cleanedSearchTerm = cleanSearchTerm(searchTerm);
            return [threads.filter(thread =>
                !thread.isTemporary &&
                thread.model === 'mail.channel' &&
                thread.channel_type === 'channel' &&
                thread.name &&
                cleanSearchTerm(thread.name).includes(cleanedSearchTerm)
            )];
        }

        /**
         * @param {string} [stringifiedDomain='[]']
         * @returns {mail.thread_cache}
         */
        cache(stringifiedDomain = '[]') {
            return this.env.models['mail.thread_cache'].insert({
                stringifiedDomain,
                thread: [['link', this]],
            });
        }

        /**
         * Fetch attachments linked to a record. Useful for populating the store
         * with these attachments, which are used by attachment box in the chatter.
         */
        async fetchAttachments() {
            const attachmentsData = await this.async(() => this.env.services.rpc({
                model: 'ir.attachment',
                method: 'search_read',
                domain: [
                    ['res_id', '=', this.id],
                    ['res_model', '=', this.model],
                ],
                fields: ['id', 'name', 'mimetype'],
                orderBy: [{ name: 'id', asc: false }],
            }, { shadow: true }));
            this.update({
                originThreadAttachments: [['insert-and-replace',
                    attachmentsData.map(data =>
                        this.env.models['mail.attachment'].convertData(data)
                    )
                ]],
            });
            this.update({ areAttachmentsLoaded: true });
        }

        /**
         * Fetches suggested recipients.
         */
        async fetchAndUpdateSuggestedRecipients() {
            if (this.isTemporary) {
                return;
            }
            return this.env.models['mail.thread'].performRpcMailGetSuggestedRecipients({
                model: this.model,
                res_ids: [this.id],
            });
        }

        /**
         * Add current user to provided thread's followers.
         */
        async follow() {
            await this.async(() => this.env.services.rpc({
                model: this.model,
                method: 'message_subscribe',
                args: [[this.id]],
                kwargs: {
                    partner_ids: [this.env.messaging.currentPartner.id],
                    context: {}, // FIXME empty context to be overridden in session.js with 'allowed_company_ids' task-2243187
                },
            }));
            this.refreshFollowers();
            this.fetchAndUpdateSuggestedRecipients();
        }

        /**
         * Returns the text that identifies this thread in a mention.
         *
         * @returns {string}
         */
        getMentionText() {
            return this.name;
        }

        /**
         * Load new messages on the main cache of this thread.
         */
        loadNewMessages() {
            this.mainCache.loadNewMessages();
        }

        /**
         * Mark the specified conversation as fetched.
         */
        async markAsFetched() {
            await this.async(() => this.env.services.rpc({
                model: 'mail.channel',
                method: 'channel_fetched',
                args: [[this.id]],
            }, { shadow: true }));
        }

        /**
         * Mark the specified conversation as read/seen.
         *
         * @param {mail.message} message the message to be considered as last seen.
         */
        async markAsSeen(message) {
            if (this.model !== 'mail.channel') {
                return;
            }
            if (this.pendingSeenMessageId && message.id <= this.pendingSeenMessageId) {
                return;
            }
            if (
                this.lastSeenByCurrentPartnerMessageId &&
                message.id <= this.lastSeenByCurrentPartnerMessageId
            ) {
                return;
            }
            this.update({ pendingSeenMessageId: message.id });
            return this.env.models['mail.thread'].performRpcChannelSeen({
                ids: [this.id],
                lastMessageId: message.id,
            });
        }

        /**
         * Marks as read all needaction messages with this thread as origin.
         */
        async markNeedactionMessagesAsOriginThreadAsRead() {
            await this.async(() =>
                this.env.models['mail.message'].markAsRead(this.needactionMessagesAsOriginThread)
            );
        }

        /**
         * Mark as read all needaction messages of this thread.
         */
        async markNeedactionMessagesAsRead() {
            await this.async(() =>
                this.env.models['mail.message'].markAsRead(this.needactionMessages)
            );
        }

        /**
         * Notifies the server of new fold state. Useful for initial,
         * cross-tab, and cross-device chat window state synchronization.
         *
         * @param {string} state
         */
        async notifyFoldStateToServer(state) {
            if (this.model !== 'mail.channel') {
                // Server sync of fold state is only supported for channels.
                return;
            }
            if (!this.uuid) {
                return;
            }
            return this.env.models['mail.thread'].performRpcChannelFold(this.uuid, state);
        }

        /**
         * Notify server to leave the current channel. Useful for cross-tab
         * and cross-device chat window state synchronization.
         *
         * Only makes sense if isPendingPinned is set to the desired value.
         */
        async notifyPinStateToServer() {
            if (this.isPendingPinned) {
                await this.env.models['mail.thread'].performRpcChannelPin({
                    pinned: true,
                    uuid: this.uuid,
                });
            } else {
                this.env.models['mail.thread'].performRpcExecuteCommand({
                    channelId: this.id,
                    command: 'leave',
                });
            }
        }

        /**
         * Opens this thread either as form view, in discuss app, or as a chat
         * window. The thread will be opened in an "active" matter, which will
         * interrupt current user flow.
         *
         * @param {Object} [param0]
         * @param {boolean} [param0.expanded=false]
         */
        async open({ expanded = false } = {}) {
            const discuss = this.env.messaging.discuss;
            // check if thread must be opened in form view
            if (!['mail.box', 'mail.channel'].includes(this.model)) {
                if (expanded || discuss.isOpen) {
                    // Close chat window because having the same thread opened
                    // both in chat window and as main document does not look
                    // good.
                    this.env.messaging.chatWindowManager.closeThread(this);
                    return this.env.messaging.openDocument({
                        id: this.id,
                        model: this.model,
                    });
                }
            }
            // check if thread must be opened in discuss
            const device = this.env.messaging.device;
            if (
                (!device.isMobile && (discuss.isOpen || expanded)) ||
                this.model === 'mail.box'
            ) {
                return discuss.openThread(this);
            }
            // thread must be opened in chat window
            return this.env.messaging.chatWindowManager.openThread(this, {
                makeActive: true,
            });
        }

        /**
         * Opens the most appropriate view that is a profile for this thread.
         */
        async openProfile() {
            return this.env.messaging.openDocument({
                id: this.id,
                model: this.model,
            });
        }

        /**
         * Pin this thread and notify server of the change.
         */
        async pin() {
            this.update({ isPendingPinned: true });
            await this.notifyPinStateToServer();
        }

        /**
         * Open a dialog to add channels as followers.
         */
        promptAddChannelFollower() {
            this._promptAddFollower({ mail_invite_follower_channel_only: true });
        }

        /**
         * Open a dialog to add partners as followers.
         */
        promptAddPartnerFollower() {
            this._promptAddFollower({ mail_invite_follower_channel_only: false });
        }

        async refresh() {
            if (this.isTemporary) {
                return;
            }
            this.loadNewMessages();
            this.update({ isLoadingAttachments: true });
            await this.async(() => this.fetchAttachments());
            this.update({ isLoadingAttachments: false });
        }

        async refreshActivities() {
            if (!this.hasActivities) {
                return;
            }
            if (this.isTemporary) {
                return;
            }
            // A bit "extreme", may be improved
            const [{ activity_ids: newActivityIds }] = await this.async(() => this.env.services.rpc({
                model: this.model,
                method: 'read',
                args: [this.id, ['activity_ids']]
            }, { shadow: true }));
            const activitiesData = await this.async(() => this.env.services.rpc({
                model: 'mail.activity',
                method: 'activity_format',
                args: [newActivityIds]
            }, { shadow: true }));
            const activities = this.env.models['mail.activity'].insert(activitiesData.map(
                activityData => this.env.models['mail.activity'].convertData(activityData)
            ));
            this.update({ activities: [['replace', activities]] });
        }

        /**
         * Refresh followers information from server.
         */
        async refreshFollowers() {
            if (this.isTemporary) {
                this.update({ followers: [['unlink-all']] });
                return;
            }
            const { followers } = await this.async(() => this.env.services.rpc({
                route: '/mail/read_followers',
                params: {
                    res_id: this.id,
                    res_model: this.model,
                },
            }, { shadow: true }));
            this.update({ areFollowersLoaded: true });
            if (followers.length > 0) {
                this.update({
                    followers: [['insert-and-replace', followers.map(data =>
                        this.env.models['mail.follower'].convertData(data))
                    ]],
                });
            } else {
                this.update({
                    followers: [['unlink-all']],
                });
            }
        }

        /**
         * Refresh the typing status of the current partner.
         */
        refreshCurrentPartnerIsTyping() {
            this._currentPartnerInactiveTypingTimer.reset();
        }

        /**
         * Called to refresh a registered other member partner that is typing
         * something.
         *
         * @param {mail.partner} partner
         */
        refreshOtherMemberTypingMember(partner) {
            this._otherMembersLongTypingTimers.get(partner).reset();
        }

        /**
         * Called when current partner is inserting some input in composer.
         * Useful to notify current partner is currently typing something in the
         * composer of this thread to all other members.
         */
        async registerCurrentPartnerIsTyping() {
            // Handling of typing timers.
            this._currentPartnerInactiveTypingTimer.start();
            this._currentPartnerLongTypingTimer.start();
            // Manage typing member relation.
            const currentPartner = this.env.messaging.currentPartner;
            const newOrderedTypingMemberLocalIds = this.orderedTypingMemberLocalIds
                .filter(localId => localId !== currentPartner.localId);
            newOrderedTypingMemberLocalIds.push(currentPartner.localId);
            this.update({
                orderedTypingMemberLocalIds: newOrderedTypingMemberLocalIds,
                typingMembers: [['link', currentPartner]],
            });
            // Notify typing status to other members.
            await this._throttleNotifyCurrentPartnerTypingStatus({ isTyping: true });
        }

        /**
         * Called to register a new other member partner that is typing
         * something.
         *
         * @param {mail.partner} partner
         */
        registerOtherMemberTypingMember(partner) {
            const timer = new Timer(
                this.env,
                () => this.async(() => this._onOtherMemberLongTypingTimeout(partner)),
                60 * 1000
            );
            this._otherMembersLongTypingTimers.set(partner, timer);
            timer.start();
            const newOrderedTypingMemberLocalIds = this.orderedTypingMemberLocalIds
                .filter(localId => localId !== partner.localId);
            newOrderedTypingMemberLocalIds.push(partner.localId);
            this.update({
                orderedTypingMemberLocalIds: newOrderedTypingMemberLocalIds,
                typingMembers: [['link', partner]],
            });
        }

        /**
         * Rename the given thread with provided new name.
         *
         * @param {string} newName
         */
        async rename(newName) {
            if (this.channel_type === 'chat') {
                await this.async(() => this.env.services.rpc({
                    model: 'mail.channel',
                    method: 'channel_set_custom_name',
                    args: [this.id],
                    kwargs: {
                        name: newName,
                    },
                }));
            }
            this.update({ custom_channel_name: newName });
        }

        /**
         * Unfollow current partner from this thread.
         */
        async unfollow() {
            const currentPartnerFollower = this.followers.find(
                follower => follower.partner === this.env.messaging.currentPartner
            );
            await this.async(() => currentPartnerFollower.remove());
        }

        /**
         * Unpin this thread and notify server of the change.
         */
        async unpin() {
            this.update({ isPendingPinned: false });
            await this.notifyPinStateToServer();
        }

        /**
         * Called when current partner has explicitly stopped inserting some
         * input in composer. Useful to notify current partner has currently
         * stopped typing something in the composer of this thread to all other
         * members.
         *
         * @param {Object} [param0={}]
         * @param {boolean} [param0.immediateNotify=false] if set, is typing
         *   status of current partner is immediately notified and doesn't
         *   consume throttling at all.
         */
        async unregisterCurrentPartnerIsTyping({ immediateNotify = false } = {}) {
            // Handling of typing timers.
            this._currentPartnerInactiveTypingTimer.clear();
            this._currentPartnerLongTypingTimer.clear();
            // Manage typing member relation.
            const currentPartner = this.env.messaging.currentPartner;
            const newOrderedTypingMemberLocalIds = this.orderedTypingMemberLocalIds
                .filter(localId => localId !== currentPartner.localId);
            this.update({
                orderedTypingMemberLocalIds: newOrderedTypingMemberLocalIds,
                typingMembers: [['unlink', currentPartner]],
            });
            // Notify typing status to other members.
            if (immediateNotify) {
                this._throttleNotifyCurrentPartnerTypingStatus.clear();
            }
            await this.async(
                () => this._throttleNotifyCurrentPartnerTypingStatus({ isTyping: false })
            );
        }

        /**
         * Called to unregister an other member partner that is no longer typing
         * something.
         *
         * @param {mail.partner} partner
         */
        unregisterOtherMemberTypingMember(partner) {
            this._otherMembersLongTypingTimers.get(partner).clear();
            this._otherMembersLongTypingTimers.delete(partner);
            const newOrderedTypingMemberLocalIds = this.orderedTypingMemberLocalIds
                .filter(localId => localId !== partner.localId);
            this.update({
                orderedTypingMemberLocalIds: newOrderedTypingMemberLocalIds,
                typingMembers: [['unlink', partner]],
            });
        }

        /**
         * Unsubscribe current user from provided channel.
         */
        unsubscribe() {
            this.env.messaging.chatWindowManager.closeThread(this);
            this.unpin();
        }

        //----------------------------------------------------------------------
        // Private
        //----------------------------------------------------------------------

        /**
         * @override
         */
        static _createRecordLocalId(data) {
            const { channel_type, id, model } = data;
            let threadModel = model;
            if (!threadModel && channel_type) {
                threadModel = 'mail.channel';
            }
            return `${this.modelName}_${threadModel}_${id}`;
        }

        /**
         * @private
         * @returns {mail.attachment[]}
         */
        _computeAllAttachments() {
            const allAttachments = [...new Set(this.originThreadAttachments.concat(this.attachments))]
                .sort((a1, a2) => {
                    // "uploading" before "uploaded" attachments.
                    if (!a1.isTemporary && a2.isTemporary) {
                        return 1;
                    }
                    if (a1.isTemporary && !a2.isTemporary) {
                        return -1;
                    }
                    // "most-recent" before "oldest" attachments.
                    return Math.abs(a2.id) - Math.abs(a1.id);
                });
            return [['replace', allAttachments]];
        }

        /**
         * @private
         * @returns {mail.partner}
         */
        _computeCorrespondent() {
            if (this.channel_type === 'channel') {
                return [['unlink']];
            }
            const correspondents = this.members.filter(partner =>
                partner !== this.env.messaging.currentPartner
            );
            if (correspondents.length === 1) {
                // 2 members chat
                return [['link', correspondents[0]]];
            }
            if (this.members.length === 1) {
                // chat with oneself
                return [['link', this.members[0]]];
            }
            return [['unlink']];
        }

        /**
         * @private
         * @returns {string}
         */
        _computeDisplayName() {
            if (this.channel_type === 'chat' && this.correspondent) {
                return this.custom_channel_name || this.correspondent.nameOrDisplayName;
            }
            return this.name;
        }

        /**
         * @private
         * @returns {mail.activity[]}
         */
        _computeFutureActivities() {
            return [['replace', this.activities.filter(activity => activity.state === 'planned')]];
        }

        /**
         * @private
         * @returns {boolean}
         */
        _computeHasSeenIndicators() {
            if (this.model !== 'mail.channel') {
                return false;
            }
            if (this.mass_mailing) {
                return false;
            }
            return ['chat', 'livechat'].includes(this.channel_type);
        }

        /**
         * @private
         * @returns {boolean}
         */
        _computeIsChatChannel() {
            return this.channel_type === 'chat';
        }

        /**
         * @private
         * @returns {boolean}
         */
        _computeIsCurrentPartnerFollowing() {
            return this.followers.some(follower =>
                follower.partner && follower.partner === this.env.messaging.currentPartner
            );
        }

        /**
         * @private
         * @returns {boolean}
         */
        _computeIsModeratedByCurrentPartner() {
            if (!this.messaging) {
                return false;
            }
            if (!this.messaging.currentPartner) {
                return false;
            }
            return this.moderators.includes(this.env.messaging.currentPartner);
        }

        /**
         * @private
         * @returns {boolean}
         */
        _computeIsPinned() {
            return this.isPendingPinned !== undefined ? this.isPendingPinned : this.isServerPinned;
        }

        /**
         * @private
         * @returns {mail.message}
         */
        _computeLastCurrentPartnerMessageSeenByEveryone() {
            const otherPartnerSeenInfos =
                this.partnerSeenInfos.filter(partnerSeenInfo =>
                    partnerSeenInfo.partner !== this.messagingCurrentPartner);
            if (otherPartnerSeenInfos.length === 0) {
                return [['unlink-all']];
            }

            const otherPartnersLastSeenMessageIds =
                otherPartnerSeenInfos.map(partnerSeenInfo =>
                    partnerSeenInfo.lastSeenMessage ? partnerSeenInfo.lastSeenMessage.id : 0
                );
            if (otherPartnersLastSeenMessageIds.length === 0) {
                return [['unlink-all']];
            }
            const lastMessageSeenByAllId = Math.min(
                ...otherPartnersLastSeenMessageIds
            );
            const currentPartnerOrderedSeenMessages =
                this.orderedNonTransientMessages.filter(message =>
                    message.author === this.messagingCurrentPartner &&
                    message.id <= lastMessageSeenByAllId);

            if (
                !currentPartnerOrderedSeenMessages ||
                currentPartnerOrderedSeenMessages.length === 0
            ) {
                return [['unlink-all']];
            }
            return [['link', currentPartnerOrderedSeenMessages.slice().pop()]];
        }

        /**
         * @private
         * @returns {mail.message|undefined}
         */
        _computeLastMessage() {
            const {
                length: l,
                [l - 1]: lastMessage,
            } = this.orderedMessages;
            if (lastMessage) {
                return [['link', lastMessage]];
            }
            return [['unlink']];
        }

        /**
         * @private
         * @returns {mail.message|undefined}
         */
        _computeLastNonTransientMessage() {
            const {
                length: l,
                [l - 1]: lastMessage,
            } = this.orderedNonTransientMessages;
            if (lastMessage) {
                return [['link', lastMessage]];
            }
            return [['unlink']];
        }

        /**
         * Adjusts the last seen message received from the server to consider
         * the following messages also as read if they are either transient
         * messages or messages from the current partner.
         *
         * @private
         * @returns {integer}
         */
        _computeLastSeenByCurrentPartnerMessageId() {
            const firstMessage = this.orderedMessages[0];
            if (
                firstMessage &&
                this.lastSeenByCurrentPartnerMessageId &&
                this.lastSeenByCurrentPartnerMessageId < firstMessage.id
            ) {
                // no deduction can be made if there is a gap
                return this.lastSeenByCurrentPartnerMessageId;
            }
            let lastSeenByCurrentPartnerMessageId = this.lastSeenByCurrentPartnerMessageId;
            for (const message of this.orderedMessages) {
                if (message.id <= this.lastSeenByCurrentPartnerMessageId) {
                    continue;
                }
                if (
                    message.author === this.env.messaging.currentPartner ||
                    message.isTransient
                ) {
                    lastSeenByCurrentPartnerMessageId = message.id;
                    continue;
                }
                return lastSeenByCurrentPartnerMessageId;
            }
            return lastSeenByCurrentPartnerMessageId;
        }

        /**
         * @private
         * @returns {mail.message|undefined}
         */
        _computeLastNeedactionMessage() {
            const orderedNeedactionMessages = this.needactionMessages.sort(
                (m1, m2) => m1.id < m2.id ? -1 : 1
            );
            const {
                length: l,
                [l - 1]: lastNeedactionMessage,
            } = orderedNeedactionMessages;
            if (lastNeedactionMessage) {
                return [['link', lastNeedactionMessage]];
            }
            return [['unlink']];
        }

        /**
         * @private
         * @returns {mail.message|undefined}
         */
        _computeLastNeedactionMessageAsOriginThread() {
            const orderedNeedactionMessagesAsOriginThread = this.needactionMessagesAsOriginThread.sort(
                (m1, m2) => m1.id < m2.id ? -1 : 1
            );
            const {
                length: l,
                [l - 1]: lastNeedactionMessageAsOriginThread,
            } = orderedNeedactionMessagesAsOriginThread;
            if (lastNeedactionMessageAsOriginThread) {
                return [['link', lastNeedactionMessageAsOriginThread]];
            }
            return [['unlink']];
        }

        /**
         * @private
         * @returns {mail.thread_cache}
         */
        _computeMainCache() {
            return [['link', this.cache()]];
        }

        /**
         * @private
         * @returns {integer}
         */
        _computeLocalMessageUnreadCounter() {
            if (this.model !== 'mail.channel') {
                // unread counter only makes sense on channels
                return clear();
            }
            // By default trust the server up to the last message it used
            // because it's not possible to do better.
            let baseCounter = this.serverMessageUnreadCounter;
            let countFromId = this.serverLastMessageId;
            // But if the client knows the last seen message that the server
            // returned (and by assumption all the messages that come after),
            // the counter can be computed fully locally, ignoring potentially
            // obsolete values from the server.
            const firstMessage = this.orderedMessages[0];
            if (
                firstMessage &&
                this.lastSeenByCurrentPartnerMessageId &&
                this.lastSeenByCurrentPartnerMessageId >= firstMessage.id
            ) {
                baseCounter = 0;
                countFromId = this.lastSeenByCurrentPartnerMessageId;
            }
            // Include all the messages that are known locally but the server
            // didn't take into account.
            return this.orderedMessages.reduce((total, message) => {
                if (message.id <= countFromId) {
                    return total;
                }
                return total + 1;
            }, baseCounter);
        }

        /**
         * @private
         * @returns {mail.messaging}
         */
        _computeMessaging() {
            return [['link', this.env.messaging]];
        }

        /**
         * @private
         * @returns {mail.message[]}
         */
        _computeNeedactionMessages() {
            return [['replace', this.messages.filter(message => message.isNeedaction)]];
        }

        /**
         * @private
         * @returns {mail.message[]}
         */
        _computeNeedactionMessagesAsOriginThread() {
            return [['replace', this.messagesAsOriginThread.filter(message => message.isNeedaction)]];
        }

        /**
         * @private
         * @returns {mail.message|undefined}
         */
        _computeMessageAfterNewMessageSeparator() {
            if (this.model !== 'mail.channel') {
                return [['unlink']];
            }
            if (this.localMessageUnreadCounter === 0) {
                return [['unlink']];
            }
            const index = this.orderedMessages.findIndex(message =>
                message.id === this.lastSeenByCurrentPartnerMessageId
            );
            if (index === -1) {
                return [['unlink']];
            }
            const message = this.orderedMessages[index + 1];
            if (!message) {
                return [['unlink']];
            }
            return [['link', message]];
        }

        /**
         * @private
         * @returns {mail.message[]}
         */
        _computeOrderedMessages() {
            return [['replace', this.messages.sort((m1, m2) => m1.id < m2.id ? -1 : 1)]];
        }

        /**
         * @private
         * @returns {mail.message[]}
         */
        _computeOrderedNonTransientMessages() {
            return [['replace', this.orderedMessages.filter(m => !m.isTransient)]];
        }

        /**
         * @private
         * @returns {mail.partner[]}
         */
        _computeOrderedOtherTypingMembers() {
            return [[
                'replace',
                this.orderedTypingMembers.filter(
                    member => member !== this.env.messaging.currentPartner
                ),
            ]];
        }

        /**
         * @private
         * @returns {mail.partner[]}
         */
        _computeOrderedTypingMembers() {
            return [[
                'replace',
                this.orderedTypingMemberLocalIds
                    .map(localId => this.env.models['mail.partner'].get(localId))
                    .filter(member => !!member),
            ]];
        }

        /**
         * @private
         * @returns {mail.activity[]}
         */
        _computeOverdueActivities() {
            return [['replace', this.activities.filter(activity => activity.state === 'overdue')]];
        }

        /**
         * @private
         * @returns {mail.activity[]}
         */
        _computeTodayActivities() {
            return [['replace', this.activities.filter(activity => activity.state === 'today')]];
        }

        /**
         * @private
         * @returns {string}
         */
        _computeTypingStatusText() {
            if (this.orderedOtherTypingMembers.length === 0) {
                return this.constructor.fields.typingStatusText.default;
            }
            if (this.orderedOtherTypingMembers.length === 1) {
                return _.str.sprintf(
                    this.env._t("%s is typing..."),
                    this.orderedOtherTypingMembers[0].nameOrDisplayName
                );
            }
            if (this.orderedOtherTypingMembers.length === 2) {
                return _.str.sprintf(
                    this.env._t("%s and %s are typing..."),
                    this.orderedOtherTypingMembers[0].nameOrDisplayName,
                    this.orderedOtherTypingMembers[1].nameOrDisplayName
                );
            }
            return _.str.sprintf(
                this.env._t("%s, %s and more are typing..."),
                this.orderedOtherTypingMembers[0].nameOrDisplayName,
                this.orderedOtherTypingMembers[1].nameOrDisplayName
            );
        }

        /**
         * Compute an url string that can be used inside a href attribute
         *
         * @private
         * @returns {string}
         */
        _computeUrl() {
            const baseHref = this.env.session.url('/web');
            if (this.model === 'mail.channel') {
                return `${baseHref}#action=mail.action_discuss&active_id=${this.model}_${this.id}`;
            }
            return `${baseHref}#model=${this.model}&id=${this.id}`;
        }

        /**
         * @private
         * @param {Object} param0
         * @param {boolean} param0.isTyping
         */
        async _notifyCurrentPartnerTypingStatus({ isTyping }) {
            if (
                this._forceNotifyNextCurrentPartnerTypingStatus ||
                isTyping !== this._currentPartnerLastNotifiedIsTyping
            ) {
                if (this.model === 'mail.channel') {
                    await this.async(() => this.env.services.rpc({
                        model: 'mail.channel',
                        method: 'notify_typing',
                        args: [this.id],
                        kwargs: { is_typing: isTyping },
                    }, { shadow: true }));
                }
                if (isTyping && this._currentPartnerLongTypingTimer.isRunning) {
                    this._currentPartnerLongTypingTimer.reset();
                }
            }
            this._forceNotifyNextCurrentPartnerTypingStatus = false;
            this._currentPartnerLastNotifiedIsTyping = isTyping;
        }

        /**
         * Cleans followers of current thread. In particular, chats are supposed
         * to work with "members", not with "followers". This clean up is only
         * necessary to remove illegitimate followers in stable version, it can
         * be removed in master after proper migration to clean the database.
         *
         * @private
         */
        _onChangeFollowersPartner() {
            if (this.channel_type !== 'chat') {
                return;
            }
            for (const follower of this.followers) {
                if (follower.partner) {
                    follower.remove();
                }
            }
        }

        /**
         * @private
         */
        _onChangeLastSeenByCurrentPartnerMessageId() {
            this.env.messagingBus.trigger('o-thread-last-seen-by-current-partner-message-id-changed', {
                thread: this,
            });
        }

        /**
         * @private
         */
        _onChangeThreadViews() {
            if (this.threadViews.length === 0) {
                return;
            }
            /**
             * Fetches followers of chats when they are displayed for the first
             * time. This is necessary to clean the followers.
             * @see `_onChangeFollowersPartner` for more information.
             */
            if (this.channel_type === 'chat' && !this.areFollowersLoaded) {
                this.refreshFollowers();
            }
        }

        /**
         * Handles change of pinned state coming from the server. Useful to
         * clear pending state once server acknowledged the change.
         *
         * @private
         * @see isPendingPinned
         */
        _onIsServerPinnedChanged() {
            if (this.isServerPinned === this.isPendingPinned) {
                this.update({ isPendingPinned: clear() });
            }
        }

        /**
         * Handles change of fold state coming from the server. Useful to
         * synchronize corresponding chat window.
         *
         * @private
         */
        _onServerFoldStateChanged() {
            if (!this.env.messaging.chatWindowManager) {
                // avoid crash during destroy
                return;
            }
            if (this.env.messaging.device.isMobile) {
                return;
            }
            if (this.serverFoldState === 'closed') {
                this.env.messaging.chatWindowManager.closeThread(this, {
                    notifyServer: false,
                });
            } else {
                this.env.messaging.chatWindowManager.openThread(this, {
                    isFolded: this.serverFoldState === 'folded',
                    notifyServer: false,
                });
            }
        }

        /**
         * @private
         * @param {Object} [param0={}]
         * @param {boolean} [param0.mail_invite_follower_channel_only=false]
         */
        _promptAddFollower({ mail_invite_follower_channel_only = false } = {}) {
            const self = this;
            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.model,
                    default_res_id: this.id,
                    mail_invite_follower_channel_only,
                },
            };
            this.env.bus.trigger('do-action', {
                action,
                options: {
                    on_close: async () => {
                       await this.async(() => this.refreshFollowers());
                       this.env.bus.trigger('mail.thread:promptAddFollower-closed');
                    },
                },
            });
        }

        //----------------------------------------------------------------------
        // Handlers
        //----------------------------------------------------------------------

        /**
         * @private
         */
        async _onCurrentPartnerInactiveTypingTimeout() {
            await this.async(() => this.unregisterCurrentPartnerIsTyping());
        }

        /**
         * Called when current partner has been typing for a very long time.
         * Immediately notify other members that he/she is still typing.
         *
         * @private
         */
        async _onCurrentPartnerLongTypingTimeout() {
            this._forceNotifyNextCurrentPartnerTypingStatus = true;
            this._throttleNotifyCurrentPartnerTypingStatus.clear();
            await this.async(
                () => this._throttleNotifyCurrentPartnerTypingStatus({ isTyping: true })
            );
        }

        /**
         * @private
         * @param {mail.partner} partner
         */
        async _onOtherMemberLongTypingTimeout(partner) {
            if (!this.typingMembers.includes(partner)) {
                this._otherMembersLongTypingTimers.delete(partner);
                return;
            }
            this.unregisterOtherMemberTypingMember(partner);
        }

    }

    Thread.fields = {
        /**
         * Determines the `mail.activity` that belong to `this`, assuming `this`
         * has activities (@see hasActivities).
         */
        activities: one2many('mail.activity', {
            inverse: 'thread',
        }),
        /**
         * Serves as compute dependency.
         */
        activitiesState: attr({
            related: 'activities.state',
        }),
        allAttachments: many2many('mail.attachment', {
            compute: '_computeAllAttachments',
            dependencies: [
                'attachments',
                'originThreadAttachments',
            ],
        }),
        areAttachmentsLoaded: attr({
            default: false,
        }),
        /**
         * States whether followers have been loaded at least once for this
         * thread.
         */
        areFollowersLoaded: attr({
            default: false,
        }),
        attachments: many2many('mail.attachment', {
            inverse: 'threads',
        }),
        caches: one2many('mail.thread_cache', {
            inverse: 'thread',
            isCausal: true,
        }),
        channel_type: attr(),
        /**
         * States the `mail.chat_window` related to `this`. Serves as compute
         * dependency. It is computed from the inverse relation and it should
         * otherwise be considered read-only.
         */
        chatWindow: one2one('mail.chat_window', {
            inverse: 'thread',
        }),
        /**
         * Serves as compute dependency.
         */
        chatWindowIsFolded: attr({
            related: 'chatWindow.isFolded',
        }),
        composer: one2one('mail.composer', {
            default: [['create']],
            inverse: 'thread',
            isCausal: true,
        }),
        correspondent: many2one('mail.partner', {
            compute: '_computeCorrespondent',
            dependencies: [
                'channel_type',
                'members',
                'messagingCurrentPartner',
            ],
            inverse: 'correspondentThreads',
        }),
        correspondentNameOrDisplayName: attr({
            related: 'correspondent.nameOrDisplayName',
        }),
        counter: attr({
            default: 0,
        }),
        creator: many2one('mail.user'),
        custom_channel_name: attr(),
        displayName: attr({
            compute: '_computeDisplayName',
            dependencies: [
                'channel_type',
                'correspondent',
                'correspondentNameOrDisplayName',
                'custom_channel_name',
                'name',
            ],
        }),
        followersPartner: many2many('mail.partner', {
            related: 'followers.partner',
        }),
        followers: one2many('mail.follower', {
            inverse: 'followedThread',
        }),
        /**
         * States the `mail.activity` that belongs to `this` and that are
         * planned in the future (due later than today).
         */
        futureActivities: one2many('mail.activity', {
            compute: '_computeFutureActivities',
            dependencies: ['activitiesState'],
        }),
        group_based_subscription: attr({
            default: false,
        }),
        /**
         * States whether `this` has activities (`mail.activity.mixin` server side).
         */
        hasActivities: attr({
            default: false,
        }),
        /**
         * Determine whether this thread has the seen indicators (V and VV)
         * enabled or not.
         */
        hasSeenIndicators: attr({
            compute: '_computeHasSeenIndicators',
            default: false,
            dependencies: [
                'channel_type',
                'mass_mailing',
                'model',
            ],
        }),
        id: attr(),
        /**
         * States whether this thread is a `mail.channel` qualified as chat.
         *
         * Useful to list chat channels, like in messaging menu with the filter
         * 'chat'.
         */
        isChatChannel: attr({
            compute: '_computeIsChatChannel',
            dependencies: [
                'channel_type',
            ],
            default: false,
        }),
        isCurrentPartnerFollowing: attr({
            compute: '_computeIsCurrentPartnerFollowing',
            default: false,
            dependencies: [
                'followersPartner',
                'messagingCurrentPartner',
            ],
        }),
        /**
         * States whether `this` is currently loading attachments.
         */
        isLoadingAttachments: attr({
            default: false,
        }),
        isModeratedByCurrentPartner: attr({
            compute: '_computeIsModeratedByCurrentPartner',
            dependencies: [
                'messagingCurrentPartner',
                'moderators',
            ],
        }),
        /**
         * Determine if there is a pending pin state change, which is a change
         * of pin state requested by the client but not yet confirmed by the
         * server.
         *
         * This field can be updated to immediately change the pin state on the
         * interface and to notify the server of the new state.
         */
        isPendingPinned: attr(),
        /**
         * Boolean that determines whether this thread is pinned
         * in discuss and present in the messaging menu.
         */
        isPinned: attr({
            compute: '_computeIsPinned',
            dependencies: [
                'isPendingPinned',
                'isServerPinned',
            ],
        }),
        /**
         * Determine the last pin state known by the server, which is the pin
         * state displayed after initialization or when the last pending
         * pin state change was confirmed by the server.
         *
         * This field should be considered read only in most situations. Only
         * the code handling pin state change from the server should typically
         * update it.
         */
        isServerPinned: attr({
            default: false,
        }),
        isTemporary: attr({
            default: false,
        }),
        is_moderator: attr({
            default: false,
        }),
        lastCurrentPartnerMessageSeenByEveryone: many2one('mail.message', {
            compute: '_computeLastCurrentPartnerMessageSeenByEveryone',
            dependencies: [
                'messagingCurrentPartner',
                'orderedNonTransientMessages',
                'partnerSeenInfos',
            ],
        }),
        /**
         * Last message of the thread, could be a transient one.
         */
        lastMessage: many2one('mail.message', {
            compute: '_computeLastMessage',
            dependencies: ['orderedMessages'],
        }),
        lastNeedactionMessage: many2one('mail.message', {
            compute: '_computeLastNeedactionMessage',
            dependencies: ['needactionMessages'],
        }),
        /**
         * States the last known needaction message having this thread as origin.
         */
        lastNeedactionMessageAsOriginThread: many2one('mail.message', {
            compute: '_computeLastNeedactionMessageAsOriginThread',
            dependencies: [
                'needactionMessagesAsOriginThread',
            ],
        }),
        /**
         * Last non-transient message.
         */
        lastNonTransientMessage: many2one('mail.message', {
            compute: '_computeLastNonTransientMessage',
            dependencies: ['orderedNonTransientMessages'],
        }),
        /**
         * Last seen message id of the channel by current partner.
         *
         * Also, it needs to be kept as an id because it's considered like a "date" and could stay
         * even if corresponding message is deleted. It is basically used to know which
         * messages are before or after it.
         */
        lastSeenByCurrentPartnerMessageId: attr({
            compute: '_computeLastSeenByCurrentPartnerMessageId',
            default: 0,
            dependencies: [
                'lastSeenByCurrentPartnerMessageId',
                'messagingCurrentPartner',
                'orderedMessages',
                'orderedMessagesIsTransient',
                // FIXME missing dependency 'orderedMessages.author', (task-2261221)
            ],
        }),
        /**
         * Local value of message unread counter, that means it is based on initial server value and
         * updated with interface updates.
         */
        localMessageUnreadCounter: attr({
            compute: '_computeLocalMessageUnreadCounter',
            dependencies: [
                'lastSeenByCurrentPartnerMessageId',
                'messagingCurrentPartner',
                'orderedMessages',
                'serverLastMessageId',
                'serverMessageUnreadCounter',
            ],
        }),
        mainCache: one2one('mail.thread_cache', {
            compute: '_computeMainCache',
        }),
        mass_mailing: attr({
            default: false,
        }),
        members: many2many('mail.partner', {
            inverse: 'memberThreads',
        }),
        /**
         * Determines the message before which the "new message" separator must
         * be positioned, if any.
         */
        messageAfterNewMessageSeparator: many2one('mail.message', {
            compute: '_computeMessageAfterNewMessageSeparator',
            dependencies: [
                'lastSeenByCurrentPartnerMessageId',
                'localMessageUnreadCounter',
                'model',
                'orderedMessages',
            ],
        }),
        message_needaction_counter: attr({
            default: 0,
        }),
        /**
         * All messages that this thread is linked to.
         * Note that this field is automatically computed by inverse
         * computed field. This field is readonly.
         */
        messages: many2many('mail.message', {
            inverse: 'threads',
        }),
        /**
         * All messages that have been originally posted in this thread.
         */
        messagesAsOriginThread: one2many('mail.message', {
            inverse: 'originThread',
        }),
        /**
         * Serves as compute dependency.
         */
        messagesAsOriginThreadIsNeedaction: attr({
            related: 'messagesAsOriginThread.isNeedaction',
        }),
        /**
         * All messages that are contained on this channel on the server.
         * Equivalent to the inverse of python field `channel_ids`.
         */
        messagesAsServerChannel: many2many('mail.message', {
            inverse: 'serverChannels',
        }),
        /**
         * Serves as compute dependency.
         */
        messagesIsNeedaction: attr({
            related: 'messages.isNeedaction',
        }),
        messageSeenIndicators: one2many('mail.message_seen_indicator', {
            inverse: 'thread',
            isCausal: true,
        }),
        messaging: many2one('mail.messaging', {
            compute: '_computeMessaging',
        }),
        messagingCurrentPartner: many2one('mail.partner', {
            related: 'messaging.currentPartner',
        }),
        model: attr(),
        model_name: attr(),
        moderation: attr({
            default: false,
        }),
        /**
         * Partners that are moderating this thread (only applies to channels).
         */
        moderators: many2many('mail.partner', {
            inverse: 'moderatedChannels',
        }),
        moduleIcon: attr(),
        name: attr(),
        needactionMessages: many2many('mail.message', {
            compute: '_computeNeedactionMessages',
            dependencies: [
                'messages',
                'messagesIsNeedaction',
            ],
        }),
        /**
         * States all known needaction messages having this thread as origin.
         */
        needactionMessagesAsOriginThread: many2many('mail.message', {
            compute: '_computeNeedactionMessagesAsOriginThread',
            dependencies: [
                'messagesAsOriginThread',
                'messagesAsOriginThreadIsNeedaction',
            ],
        }),
        /**
         * Not a real field, used to trigger `_onChangeFollowersPartner` when one of
         * the dependencies changes.
         */
        onChangeFollowersPartner: attr({
            compute: '_onChangeFollowersPartner',
            dependencies: [
                'followersPartner',
            ],
        }),
        /**
         * Not a real field, used to trigger `_onChangeLastSeenByCurrentPartnerMessageId` when one of
         * the dependencies changes.
         */
        onChangeLastSeenByCurrentPartnerMessageId: attr({
            compute: '_onChangeLastSeenByCurrentPartnerMessageId',
            dependencies: [
                'lastSeenByCurrentPartnerMessageId',
            ],
        }),
        /**
         * Not a real field, used to trigger `_onChangeThreadViews` when one of
         * the dependencies changes.
         */
        onChangeThreadView: attr({
            compute: '_onChangeThreadViews',
            dependencies: [
                'threadViews',
            ],
        }),
        /**
         * Not a real field, used to trigger `_onIsServerPinnedChanged` when one of
         * the dependencies changes.
         */
        onIsServerPinnedChanged: attr({
            compute: '_onIsServerPinnedChanged',
            dependencies: [
                'isServerPinned',
            ],
        }),
        /**
         * Not a real field, used to trigger `_onServerFoldStateChanged` when one of
         * the dependencies changes.
         */
        onServerFoldStateChanged: attr({
            compute: '_onServerFoldStateChanged',
            dependencies: [
                'serverFoldState',
            ],
        }),
        /**
         * All messages ordered like they are displayed.
         */
        orderedMessages: many2many('mail.message', {
            compute: '_computeOrderedMessages',
            dependencies: ['messages'],
        }),
        /**
         * Serves as compute dependency. (task-2261221)
         */
        orderedMessagesIsTransient: attr({
            related: 'orderedMessages.isTransient',
        }),
        /**
         * All messages ordered like they are displayed. This field does not
         * contain transient messages which are not "real" records.
         */
        orderedNonTransientMessages: many2many('mail.message', {
            compute: '_computeOrderedNonTransientMessages',
            dependencies: [
                'orderedMessages',
                'orderedMessagesIsTransient',
            ],
        }),
        /**
         * Ordered typing members on this thread, excluding the current partner.
         */
        orderedOtherTypingMembers: many2many('mail.partner', {
            compute: '_computeOrderedOtherTypingMembers',
            dependencies: ['orderedTypingMembers'],
        }),
        /**
         * Ordered typing members on this thread. Lower index means this member
         * is currently typing for the longest time. This list includes current
         * partner as typer.
         */
        orderedTypingMembers: many2many('mail.partner', {
            compute: '_computeOrderedTypingMembers',
            dependencies: [
                'orderedTypingMemberLocalIds',
                'typingMembers',
            ],
        }),
        /**
         * Technical attribute to manage ordered list of typing members.
         */
        orderedTypingMemberLocalIds: attr({
            default: [],
        }),
        originThreadAttachments: one2many('mail.attachment', {
            inverse: 'originThread',
        }),
        /**
         * States the `mail.activity` that belongs to `this` and that are
         * overdue (due earlier than today).
         */
        overdueActivities: one2many('mail.activity', {
            compute: '_computeOverdueActivities',
            dependencies: ['activitiesState'],
        }),
        partnerSeenInfos: one2many('mail.thread_partner_seen_info', {
            inverse: 'thread',
            isCausal: true,
        }),
        /**
         * Determine if there is a pending seen message change, which is a change
         * of seen message requested by the client but not yet confirmed by the
         * server.
         */
        pendingSeenMessageId: attr(),
        public: attr(),
        /**
         * Determine the last fold state known by the server, which is the fold
         * state displayed after initialization or when the last pending
         * fold state change was confirmed by the server.
         *
         * This field should be considered read only in most situations. Only
         * the code handling fold state change from the server should typically
         * update it.
         */
        serverFoldState: attr({
            default: 'closed',
        }),
        /**
         * Last message id considered by the server.
         *
         * Useful to compute localMessageUnreadCounter field.
         *
         * @see localMessageUnreadCounter
         */
        serverLastMessageId: attr({
            default: 0,
        }),
        /**
         * Message unread counter coming from server.
         *
         * Value of this field is unreliable, due to dynamic nature of
         * messaging. So likely outdated/unsync with server. Should use
         * localMessageUnreadCounter instead, which smartly guess the actual
         * message unread counter at all time.
         *
         * @see localMessageUnreadCounter
         */
        serverMessageUnreadCounter: attr({
            default: 0,
        }),
        /**
         * Determines the `mail.suggested_recipient_info` concerning `this`.
         */
        suggestedRecipientInfoList: one2many('mail.suggested_recipient_info', {
            inverse: 'thread',
        }),
        threadViews: one2many('mail.thread_view', {
            inverse: 'thread',
        }),
        /**
         * States the `mail.activity` that belongs to `this` and that are due
         * specifically today.
         */
        todayActivities: one2many('mail.activity', {
            compute: '_computeTodayActivities',
            dependencies: ['activitiesState'],
        }),
        /**
         * Members that are currently typing something in the composer of this
         * thread, including current partner.
         */
        typingMembers: many2many('mail.partner'),
        /**
         * Text that represents the status on this thread about typing members.
         */
        typingStatusText: attr({
            compute: '_computeTypingStatusText',
            default: '',
            dependencies: ['orderedOtherTypingMembers'],
        }),
        /**
         * URL to access to the conversation.
         */
        url: attr({
            compute: '_computeUrl',
            default: '',
            dependencies: [
                'id',
                'model',
            ]
        }),
        uuid: attr(),
    };

    Thread.modelName = 'mail.thread';

    return Thread;
}

registerNewModel('mail.thread', factory);

});
