odoo.define('mail/static/src/models/discuss.discuss.js', function (require) {
'use strict';
const { registerNewModel } = require('mail/static/src/model/model_core.js');
const { attr, many2one, one2many, one2one } = require('mail/static/src/model/model_field.js');
const { clear } = require('mail/static/src/model/model_field_command.js');
function factory(dependencies) {
class Discuss extends dependencies['mail.model'] {
//----------------------------------------------------------------------
// Public
//----------------------------------------------------------------------
/**
* @param {mail.thread} thread
*/
cancelThreadRenaming(thread) {
this.update({ renamingThreads: [['unlink', thread]] });
}
clearIsAddingItem() {
this.update({
addingChannelValue: "",
isAddingChannel: false,
isAddingChat: false,
});
}
clearReplyingToMessage() {
this.update({ replyingToMessage: [['unlink-all']] });
}
/**
* Close the discuss app. Should reset its internal state.
*/
close() {
this.update({ isOpen: false });
}
focus() {
this.update({ isDoFocus: true });
}
/**
* @param {Event} ev
* @param {Object} ui
* @param {Object} ui.item
* @param {integer} ui.item.id
*/
async handleAddChannelAutocompleteSelect(ev, ui) {
const name = this.addingChannelValue;
this.clearIsAddingItem();
if (ui.item.special) {
const channel = await this.async(() =>
this.env.models['mail.thread'].performRpcCreateChannel({
name,
privacy: ui.item.special,
})
);
channel.open();
} else {
const channel = await this.async(() =>
this.env.models['mail.thread'].performRpcJoinChannel({
channelId: ui.item.id,
})
);
channel.open();
}
}
/**
* @param {Object} req
* @param {string} req.term
* @param {function} res
*/
async handleAddChannelAutocompleteSource(req, res) {
const value = req.term;
const escapedValue = owl.utils.escape(value);
this.update({ addingChannelValue: value });
const domain = [
['channel_type', '=', 'channel'],
['name', 'ilike', value],
];
const fields = ['channel_type', 'name', 'public', 'uuid'];
const result = await this.async(() => this.env.services.rpc({
model: "mail.channel",
method: "search_read",
kwargs: {
domain,
fields,
},
}));
const items = result.map(data => {
let escapedName = owl.utils.escape(data.name);
return Object.assign(data, {
label: escapedName,
value: escapedName
});
});
// XDU FIXME could use a component but be careful with owl's
// renderToString https://github.com/odoo/owl/issues/708
items.push({
label: _.str.sprintf(
`${this.env._t('Create %s')}`,
`${escapedValue}`,
),
escapedValue,
special: 'public'
}, {
label: _.str.sprintf(
`${this.env._t('Create %s')}`,
`${escapedValue}`,
),
escapedValue,
special: 'private'
});
res(items);
}
/**
* @param {Event} ev
* @param {Object} ui
* @param {Object} ui.item
* @param {integer} ui.item.id
*/
handleAddChatAutocompleteSelect(ev, ui) {
this.env.messaging.openChat({ partnerId: ui.item.id });
this.clearIsAddingItem();
}
/**
* @param {Object} req
* @param {string} req.term
* @param {function} res
*/
handleAddChatAutocompleteSource(req, res) {
const value = owl.utils.escape(req.term);
this.env.models['mail.partner'].imSearch({
callback: partners => {
const suggestions = partners.map(partner => {
return {
id: partner.id,
value: partner.nameOrDisplayName,
label: partner.nameOrDisplayName,
};
});
res(_.sortBy(suggestions, 'label'));
},
keyword: value,
limit: 10,
});
}
/**
* Open thread from init active id. `initActiveId` is used to refer to
* a thread that we may not have full data yet, such as when messaging
* is not yet initialized.
*/
openInitThread() {
const [model, id] = typeof this.initActiveId === 'number'
? ['mail.channel', this.initActiveId]
: this.initActiveId.split('_');
const thread = this.env.models['mail.thread'].findFromIdentifyingData({
id: model !== 'mail.box' ? Number(id) : id,
model,
});
if (!thread) {
return;
}
thread.open();
if (this.env.messaging.device.isMobile && thread.channel_type) {
this.update({ activeMobileNavbarTabId: thread.channel_type });
}
}
/**
* Opens the given thread in Discuss, and opens Discuss if necessary.
*
* @param {mail.thread} thread
*/
async openThread(thread) {
this.update({
thread: [['link', thread]],
});
this.focus();
if (!this.isOpen) {
this.env.bus.trigger('do-action', {
action: 'mail.action_discuss',
options: {
active_id: this.threadToActiveId(this),
clear_breadcrumbs: false,
on_reverse_breadcrumb: () => this.close(),
},
});
}
}
/**
* @param {mail.thread} thread
* @param {string} newName
*/
async renameThread(thread, newName) {
await this.async(() => thread.rename(newName));
this.update({ renamingThreads: [['unlink', thread]] });
}
/**
* Action to initiate reply to given message in Inbox. Assumes that
* Discuss and Inbox are already opened.
*
* @param {mail.message} message
*/
replyToMessage(message) {
this.update({ replyingToMessage: [['link', message]] });
// avoid to reply to a note by a message and vice-versa.
// subject to change later by allowing subtype choice.
this.replyingToMessageOriginThreadComposer.update({
isLog: !message.is_discussion && !message.is_notification
});
this.focus();
}
/**
* @param {mail.thread} thread
*/
setThreadRenaming(thread) {
this.update({ renamingThreads: [['link', thread]] });
}
/**
* @param {mail.thread} thread
* @returns {string}
*/
threadToActiveId(thread) {
return `${thread.model}_${thread.id}`;
}
//----------------------------------------------------------------------
// Private
//----------------------------------------------------------------------
/**
* @private
* @returns {string|undefined}
*/
_computeActiveId() {
if (!this.thread) {
return clear();
}
return this.threadToActiveId(this.thread);
}
/**
* @private
* @returns {string}
*/
_computeAddingChannelValue() {
if (!this.isOpen) {
return "";
}
return this.addingChannelValue;
}
/**
* @private
* @returns {boolean}
*/
_computeHasThreadView() {
if (!this.thread || !this.isOpen) {
return false;
}
if (
this.env.messaging.device.isMobile &&
(
this.activeMobileNavbarTabId !== 'mailbox' ||
this.thread.model !== 'mail.box'
)
) {
return false;
}
return true;
}
/**
* @private
* @returns {boolean}
*/
_computeIsAddingChannel() {
if (!this.isOpen) {
return false;
}
return this.isAddingChannel;
}
/**
* @private
* @returns {boolean}
*/
_computeIsAddingChat() {
if (!this.isOpen) {
return false;
}
return this.isAddingChat;
}
/**
* @private
* @returns {boolean}
*/
_computeIsReplyingToMessage() {
return !!this.replyingToMessage;
}
/**
* Ensures the reply feature is disabled if discuss is not open.
*
* @private
* @returns {mail.message|undefined}
*/
_computeReplyingToMessage() {
if (!this.isOpen) {
return [['unlink-all']];
}
return [];
}
/**
* Only pinned threads are allowed in discuss.
*
* @private
* @returns {mail.thread|undefined}
*/
_computeThread() {
let thread = this.thread;
if (this.env.messaging &&
this.env.messaging.inbox &&
this.env.messaging.device.isMobile &&
this.activeMobileNavbarTabId === 'mailbox' &&
this.initActiveId !== 'mail.box_inbox' &&
!thread
) {
// After loading Discuss from an arbitrary tab other then 'mailbox',
// switching to 'mailbox' requires to also set its inner-tab ;
// by default the 'inbox'.
return [['replace', this.env.messaging.inbox]];
}
if (!thread || !thread.isPinned) {
return [['unlink']];
}
return [];
}
}
Discuss.fields = {
activeId: attr({
compute: '_computeActiveId',
dependencies: [
'thread',
'threadId',
'threadModel',
],
}),
/**
* Active mobile navbar tab, either 'mailbox', 'chat', or 'channel'.
*/
activeMobileNavbarTabId: attr({
default: 'mailbox',
}),
/**
* Value that is used to create a channel from the sidebar.
*/
addingChannelValue: attr({
compute: '_computeAddingChannelValue',
default: "",
dependencies: ['isOpen'],
}),
/**
* Serves as compute dependency.
*/
device: one2one('mail.device', {
related: 'messaging.device',
}),
/**
* Serves as compute dependency.
*/
deviceIsMobile: attr({
related: 'device.isMobile',
}),
/**
* Determine if the moderation discard dialog is displayed.
*/
hasModerationDiscardDialog: attr({
default: false,
}),
/**
* Determine if the moderation reject dialog is displayed.
*/
hasModerationRejectDialog: attr({
default: false,
}),
/**
* Determines whether `this.thread` should be displayed.
*/
hasThreadView: attr({
compute: '_computeHasThreadView',
dependencies: [
'activeMobileNavbarTabId',
'deviceIsMobile',
'isOpen',
'thread',
'threadModel',
],
}),
/**
* Formatted init thread on opening discuss for the first time,
* when no active thread is defined. Useful to set a thread to
* open without knowing its local id in advance.
* Support two formats:
* {string} _
* {int} with default model of 'mail.channel'
*/
initActiveId: attr({
default: 'mail.box_inbox',
}),
/**
* Determine whether current user is currently adding a channel from
* the sidebar.
*/
isAddingChannel: attr({
compute: '_computeIsAddingChannel',
default: false,
dependencies: ['isOpen'],
}),
/**
* Determine whether current user is currently adding a chat from
* the sidebar.
*/
isAddingChat: attr({
compute: '_computeIsAddingChat',
default: false,
dependencies: ['isOpen'],
}),
/**
* Determine whether this discuss should be focused at next render.
*/
isDoFocus: attr({
default: false,
}),
/**
* Whether the discuss app is open or not. Useful to determine
* whether the discuss or chat window logic should be applied.
*/
isOpen: attr({
default: false,
}),
isReplyingToMessage: attr({
compute: '_computeIsReplyingToMessage',
default: false,
dependencies: ['replyingToMessage'],
}),
isThreadPinned: attr({
related: 'thread.isPinned',
}),
/**
* The menu_id of discuss app, received on mail/init_messaging and
* used to open discuss from elsewhere.
*/
menu_id: attr({
default: null,
}),
messaging: one2one('mail.messaging', {
inverse: 'discuss',
}),
messagingInbox: many2one('mail.thread', {
related: 'messaging.inbox',
}),
renamingThreads: one2many('mail.thread'),
/**
* The message that is currently selected as being replied to in Inbox.
* There is only one reply composer shown at a time, which depends on
* this selected message.
*/
replyingToMessage: many2one('mail.message', {
compute: '_computeReplyingToMessage',
dependencies: [
'isOpen',
'replyingToMessage',
],
}),
/**
* The thread concerned by the reply feature in Inbox. It depends on the
* message set to be replied, and should be considered read-only.
*/
replyingToMessageOriginThread: many2one('mail.thread', {
related: 'replyingToMessage.originThread',
}),
/**
* The composer to display for the reply feature in Inbox. It depends
* on the message set to be replied, and should be considered read-only.
*/
replyingToMessageOriginThreadComposer: one2one('mail.composer', {
inverse: 'discussAsReplying',
related: 'replyingToMessageOriginThread.composer',
}),
/**
* Quick search input value in the discuss sidebar (desktop). Useful
* to filter channels and chats based on this input content.
*/
sidebarQuickSearchValue: attr({
default: "",
}),
/**
* Determines the domain to apply when fetching messages for `this.thread`.
* This value should only be written by the control panel.
*/
stringifiedDomain: attr({
default: '[]',
}),
/**
* Determines the `mail.thread` that should be displayed by `this`.
*/
thread: many2one('mail.thread', {
compute: '_computeThread',
dependencies: [
'activeMobileNavbarTabId',
'deviceIsMobile',
'isThreadPinned',
'messaging',
'messagingInbox',
'thread',
'threadModel',
],
}),
threadId: attr({
related: 'thread.id',
}),
threadModel: attr({
related: 'thread.model',
}),
/**
* States the `mail.thread_view` displaying `this.thread`.
*/
threadView: one2one('mail.thread_view', {
related: 'threadViewer.threadView',
}),
/**
* Determines the `mail.thread_viewer` managing the display of `this.thread`.
*/
threadViewer: one2one('mail.thread_viewer', {
default: [['create']],
inverse: 'discuss',
isCausal: true,
}),
};
Discuss.modelName = 'mail.discuss';
return Discuss;
}
registerNewModel('mail.discuss', factory);
});