diff options
Diffstat (limited to 'addons/mail/static/src/models/partner/partner.js')
| -rw-r--r-- | addons/mail/static/src/models/partner/partner.js | 527 |
1 files changed, 527 insertions, 0 deletions
diff --git a/addons/mail/static/src/models/partner/partner.js b/addons/mail/static/src/models/partner/partner.js new file mode 100644 index 00000000..4d007fb1 --- /dev/null +++ b/addons/mail/static/src/models/partner/partner.js @@ -0,0 +1,527 @@ +odoo.define('mail/static/src/models/partner/partner.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 { cleanSearchTerm } = require('mail/static/src/utils/utils.js'); + +function factory(dependencies) { + + class Partner extends dependencies['mail.model'] { + + //---------------------------------------------------------------------- + // Public + //---------------------------------------------------------------------- + + /** + * @static + * @private + * @param {Object} data + * @return {Object} + */ + static convertData(data) { + const data2 = {}; + if ('active' in data) { + data2.active = data.active; + } + if ('country' in data) { + if (!data.country) { + data2.country = [['unlink-all']]; + } else { + data2.country = [['insert', { + id: data.country[0], + name: data.country[1], + }]]; + } + } + if ('display_name' in data) { + data2.display_name = data.display_name; + } + if ('email' in data) { + data2.email = data.email; + } + if ('id' in data) { + data2.id = data.id; + } + if ('im_status' in data) { + data2.im_status = data.im_status; + } + if ('name' in data) { + data2.name = data.name; + } + + // relation + if ('user_id' in data) { + if (!data.user_id) { + data2.user = [['unlink-all']]; + } else { + let user = {}; + if (Array.isArray(data.user_id)) { + user = { + id: data.user_id[0], + display_name: data.user_id[1], + }; + } else { + user = { + id: data.user_id, + }; + } + user.isInternalUser = data.is_internal_user; + data2.user = [['insert', user]]; + } + } + + return data2; + } + + /** + * Fetches partners matching the given search term to extend the + * JS knowledge and to update the suggestion list accordingly. + * + * @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 kwargs = { search: searchTerm }; + const isNonPublicChannel = thread && thread.model === 'mail.channel' && thread.public !== 'public'; + if (isNonPublicChannel) { + kwargs.channel_id = thread.id; + } + const [ + mainSuggestedPartners, + extraSuggestedPartners, + ] = await this.env.services.rpc( + { + model: 'res.partner', + method: 'get_mention_suggestions', + kwargs, + }, + { shadow: true }, + ); + const partnersData = mainSuggestedPartners.concat(extraSuggestedPartners); + const partners = this.env.models['mail.partner'].insert(partnersData.map(data => + this.env.models['mail.partner'].convertData(data) + )); + if (isNonPublicChannel) { + thread.update({ members: [['link', partners]] }); + } + } + + /** + * Search for partners matching `keyword`. + * + * @static + * @param {Object} param0 + * @param {function} param0.callback + * @param {string} param0.keyword + * @param {integer} [param0.limit=10] + */ + static async imSearch({ callback, keyword, limit = 10 }) { + // prefetched partners + let partners = []; + const cleanedSearchTerm = cleanSearchTerm(keyword); + const currentPartner = this.env.messaging.currentPartner; + for (const partner of this.all(partner => partner.active)) { + if (partners.length < limit) { + if ( + partner !== currentPartner && + partner.name && + partner.user && + cleanSearchTerm(partner.name).includes(cleanedSearchTerm) + ) { + partners.push(partner); + } + } + } + if (!partners.length) { + const partnersData = await this.env.services.rpc( + { + model: 'res.partner', + method: 'im_search', + args: [keyword, limit] + }, + { shadow: true } + ); + const newPartners = this.insert(partnersData.map( + partnerData => this.convertData(partnerData) + )); + partners.push(...newPartners); + } + callback(partners); + } + + /** + * Returns partners that match the given 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.partner[], mail.partner[]]} + */ + static searchSuggestions(searchTerm, { thread } = {}) { + let partners; + const isNonPublicChannel = thread && thread.model === 'mail.channel' && thread.public !== 'public'; + if (isNonPublicChannel) { + // Only return the channel members when in the context of a + // non-public channel. Indeed, the message with the mention + // would be notified to the mentioned partner, so this prevents + // from inadvertently leaking the private message to the + // mentioned partner. + partners = thread.members; + } else { + partners = this.env.models['mail.partner'].all(); + } + const cleanedSearchTerm = cleanSearchTerm(searchTerm); + const mainSuggestionList = []; + const extraSuggestionList = []; + for (const partner of partners) { + if ( + (!partner.active && partner !== this.env.messaging.partnerRoot) || + partner.id <= 0 || + this.env.messaging.publicPartners.includes(partner) + ) { + // ignore archived partners (except OdooBot), temporary + // partners (livechat guests), public partners (technical) + continue; + } + if ( + (partner.nameOrDisplayName && cleanSearchTerm(partner.nameOrDisplayName).includes(cleanedSearchTerm)) || + (partner.email && cleanSearchTerm(partner.email).includes(cleanedSearchTerm)) + ) { + if (partner.user) { + mainSuggestionList.push(partner); + } else { + extraSuggestionList.push(partner); + } + } + } + return [mainSuggestionList, extraSuggestionList]; + } + + /** + * @static + */ + static async startLoopFetchImStatus() { + await this._fetchImStatus(); + this._loopFetchImStatus(); + } + + /** + * Checks whether this partner has a related user and links them if + * applicable. + */ + async checkIsUser() { + const userIds = await this.async(() => this.env.services.rpc({ + model: 'res.users', + method: 'search', + args: [[['partner_id', '=', this.id]]], + kwargs: { + context: { active_test: false }, + }, + }, { shadow: true })); + this.update({ hasCheckedUser: true }); + if (userIds.length > 0) { + this.update({ user: [['insert', { id: userIds[0] }]] }); + } + } + + /** + * Gets the chat between the user of this partner and the current user. + * + * If a chat is not appropriate, a notification is displayed instead. + * + * @returns {mail.thread|undefined} + */ + async getChat() { + if (!this.user && !this.hasCheckedUser) { + await this.async(() => this.checkIsUser()); + } + // prevent chatting with non-users + if (!this.user) { + this.env.services['notification'].notify({ + message: this.env._t("You can only chat with partners that have a dedicated user."), + type: 'info', + }); + return; + } + return this.user.getChat(); + } + + /** + * Returns the text that identifies this partner in a mention. + * + * @returns {string} + */ + getMentionText() { + return this.name; + } + + /** + * Returns a sort function to determine the order of display of partners + * 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 isAInternalUser = a.user && a.user.isInternalUser; + const isBInternalUser = b.user && b.user.isInternalUser; + if (isAInternalUser && !isBInternalUser) { + return -1; + } + if (!isAInternalUser && isBInternalUser) { + return 1; + } + if (thread && thread.model === 'mail.channel') { + const isAMember = thread.members.includes(a); + const isBMember = thread.members.includes(b); + if (isAMember && !isBMember) { + return -1; + } + if (!isAMember && isBMember) { + return 1; + } + } + if (thread) { + const isAFollower = thread.followersPartner.includes(a); + const isBFollower = thread.followersPartner.includes(b); + if (isAFollower && !isBFollower) { + return -1; + } + if (!isAFollower && isBFollower) { + return 1; + } + } + const cleanedAName = cleanSearchTerm(a.nameOrDisplayName || ''); + const cleanedBName = cleanSearchTerm(b.nameOrDisplayName || ''); + 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; + } + const cleanedAEmail = cleanSearchTerm(a.email || ''); + const cleanedBEmail = cleanSearchTerm(b.email || ''); + if (cleanedAEmail.startsWith(cleanedSearchTerm) && !cleanedAEmail.startsWith(cleanedSearchTerm)) { + return -1; + } + if (!cleanedBEmail.startsWith(cleanedSearchTerm) && cleanedBEmail.startsWith(cleanedSearchTerm)) { + return 1; + } + if (cleanedAEmail < cleanedBEmail) { + return -1; + } + if (cleanedAEmail > cleanedBEmail) { + return 1; + } + return a.id - b.id; + }; + } + + /** + * Opens a chat between the user of this partner and the current user + * and returns it. + * + * If a chat is not appropriate, a notification is displayed instead. + * + * @param {Object} [options] forwarded to @see `mail.thread:open()` + * @returns {mail.thread|undefined} + */ + async openChat(options) { + const chat = await this.async(() => this.getChat()); + if (!chat) { + return; + } + await this.async(() => chat.open(options)); + return chat; + } + + /** + * Opens the most appropriate view that is a profile for this partner. + */ + async openProfile() { + return this.env.messaging.openDocument({ + id: this.id, + model: 'res.partner', + }); + } + + //---------------------------------------------------------------------- + // Private + //---------------------------------------------------------------------- + + /** + * @private + * @returns {string} + */ + _computeAvatarUrl() { + return `/web/image/res.partner/${this.id}/image_128`; + } + + /** + * @override + */ + static _createRecordLocalId(data) { + return `${this.modelName}_${data.id}`; + } + + /** + * @static + * @private + */ + static async _fetchImStatus() { + const partnerIds = []; + for (const partner of this.all()) { + if (partner.im_status !== 'im_partner' && partner.id > 0) { + partnerIds.push(partner.id); + } + } + if (partnerIds.length === 0) { + return; + } + const dataList = await this.env.services.rpc({ + route: '/longpolling/im_status', + params: { + partner_ids: partnerIds, + }, + }, { shadow: true }); + this.insert(dataList); + } + + /** + * @static + * @private + */ + static _loopFetchImStatus() { + setTimeout(async () => { + await this._fetchImStatus(); + this._loopFetchImStatus(); + }, 50 * 1000); + } + + /** + * @private + * @returns {string|undefined} + */ + _computeDisplayName() { + return this.display_name || this.user && this.user.display_name; + } + + /** + * @private + * @returns {mail.messaging} + */ + _computeMessaging() { + return [['link', this.env.messaging]]; + } + + /** + * @private + * @returns {string|undefined} + */ + _computeNameOrDisplayName() { + return this.name || this.display_name; + } + + } + + Partner.fields = { + active: attr({ + default: true, + }), + avatarUrl: attr({ + compute: '_computeAvatarUrl', + dependencies: [ + 'id', + ], + }), + correspondentThreads: one2many('mail.thread', { + inverse: 'correspondent', + }), + country: many2one('mail.country'), + display_name: attr({ + compute: '_computeDisplayName', + default: "", + dependencies: [ + 'display_name', + 'userDisplayName', + ], + }), + email: attr(), + failureNotifications: one2many('mail.notification', { + related: 'messagesAsAuthor.failureNotifications', + }), + /** + * Whether an attempt was already made to fetch the user corresponding + * to this partner. This prevents doing the same RPC multiple times. + */ + hasCheckedUser: attr({ + default: false, + }), + id: attr(), + im_status: attr(), + memberThreads: many2many('mail.thread', { + inverse: 'members', + }), + messagesAsAuthor: one2many('mail.message', { + inverse: 'author', + }), + /** + * Serves as compute dependency. + */ + messaging: many2one('mail.messaging', { + compute: '_computeMessaging', + }), + model: attr({ + default: 'res.partner', + }), + /** + * Channels that are moderated by this partner. + */ + moderatedChannels: many2many('mail.thread', { + inverse: 'moderators', + }), + name: attr(), + nameOrDisplayName: attr({ + compute: '_computeNameOrDisplayName', + dependencies: [ + 'display_name', + 'name', + ], + }), + user: one2one('mail.user', { + inverse: 'partner', + }), + /** + * Serves as compute dependency. + */ + userDisplayName: attr({ + related: 'user.display_name', + }), + }; + + Partner.modelName = 'mail.partner'; + + return Partner; +} + +registerNewModel('mail.partner', factory); + +}); |
