diff --git a/sent_mails/__init__.py b/sent_mails/__init__.py new file mode 100644 index 000000000..b8b509efd --- /dev/null +++ b/sent_mails/__init__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Cybrosys Technologies Pvt. Ltd. +# Copyright (C) 2009-TODAY Cybrosys Technologies(). +# Author: Nilmar Shereef() +# you can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# It is forbidden to publish, distribute, sublicense, or sell copies +# of the Software or modified copies of the Software. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# GENERAL PUBLIC LICENSE (LGPL v3) along with this program. +# If not, see . +# +############################################################################## diff --git a/sent_mails/__openerp__.py b/sent_mails/__openerp__.py new file mode 100644 index 000000000..3313a0633 --- /dev/null +++ b/sent_mails/__openerp__.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Cybrosys Technologies Pvt. Ltd. +# Copyright (C) 2009-TODAY Cybrosys Technologies(). +# Author: Nilmar Shereef() +# you can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# It is forbidden to publish, distribute, sublicense, or sell copies +# of the Software or modified copies of the Software. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# GENERAL PUBLIC LICENSE (LGPL v3) along with this program. +# If not, see . +# +############################################################################## +{ + 'name': 'Sent Mails', + 'version': '1.0', + 'author': 'Cybrosys Techno Solutions', + 'company': 'Cybrosys Techno Solutions', + 'website': 'http://www.cybrosys.com', + "category": "Discuss", + 'depends': ['base', 'mail'], + 'license': 'AGPL-3', + 'data': [ + 'views/get_sent_mails.xml', + ], + 'qweb': [ + 'static/src/xml/client_action_sent_mails.xml', + + ], + 'installable': True, + 'auto_install': False +} diff --git a/sent_mails/static/description/cybro_logo.png b/sent_mails/static/description/cybro_logo.png new file mode 100644 index 000000000..bb309114c Binary files /dev/null and b/sent_mails/static/description/cybro_logo.png differ diff --git a/sent_mails/static/description/icon.png b/sent_mails/static/description/icon.png new file mode 100644 index 000000000..b34677ee0 Binary files /dev/null and b/sent_mails/static/description/icon.png differ diff --git a/sent_mails/static/description/index.html b/sent_mails/static/description/index.html new file mode 100644 index 000000000..01f9793be --- /dev/null +++ b/sent_mails/static/description/index.html @@ -0,0 +1,44 @@ +
+
+

Show sent mails

+

Shows the mails or discussions sent by current user

+

Author : Cybrosys Techno Solutions, www.cybrosys.com

+
+ +
+
+

+ ☛This module enables the feature to display the mails and discussions done by current user +

+
+
+
+
+
+ +
+
+

Sent Mail Menu Under Discuss

+
+
+ +
+
+
+
+ +
+

Need Any Help?

+ + +
diff --git a/sent_mails/static/description/sent_mails_demo.png b/sent_mails/static/description/sent_mails_demo.png new file mode 100644 index 000000000..2a534f872 Binary files /dev/null and b/sent_mails/static/description/sent_mails_demo.png differ diff --git a/sent_mails/static/scripts/openerp_mailgate.py b/sent_mails/static/scripts/openerp_mailgate.py new file mode 100755 index 000000000..dc570a7be --- /dev/null +++ b/sent_mails/static/scripts/openerp_mailgate.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +""" + openerp_mailgate.py +""" + +import cgitb +import time +import optparse +import sys +import xmlrpclib +import smtplib +from email.mime.multipart import MIMEMultipart +from email.mime.base import MIMEBase +from email.mime.text import MIMEText +from email.utils import COMMASPACE, formatdate +from email import Encoders + +class DefaultConfig(object): + """ + Default configuration + """ + OPENERP_DEFAULT_USER_ID = 1 + OPENERP_DEFAULT_PASSWORD = 'admin' + OPENERP_HOSTNAME = 'localhost' + OPENERP_PORT = 8069 + OPENERP_DEFAULT_DATABASE = 'openerp' + MAIL_ERROR = 'error@example.com' + MAIL_SERVER = 'smtp.example.com' + MAIL_SERVER_PORT = 25 + MAIL_ADMINS = ('info@example.com',) + +config = DefaultConfig() + + +def send_mail(_from_, to_, subject, text, files=None, server=config.MAIL_SERVER, port=config.MAIL_SERVER_PORT): + assert isinstance(to_, (list, tuple)) + + if files is None: + files = [] + + msg = MIMEMultipart() + msg['From'] = _from_ + msg['To'] = COMMASPACE.join(to_) + msg['Date'] = formatdate(localtime=True) + msg['Subject'] = subject + + msg.attach( MIMEText(text) ) + + for file_name, file_content in files: + part = MIMEBase('application', "octet-stream") + part.set_payload( file_content ) + Encoders.encode_base64(part) + part.add_header('Content-Disposition', 'attachment; filename="%s"' + % file_name) + msg.attach(part) + + smtp = smtplib.SMTP(server, port=port) + smtp.sendmail(_from_, to_, msg.as_string() ) + smtp.close() + +class RPCProxy(object): + def __init__(self, uid, passwd, + host=config.OPENERP_HOSTNAME, + port=config.OPENERP_PORT, + path='object', + dbname=config.OPENERP_DEFAULT_DATABASE): + self.rpc = xmlrpclib.ServerProxy('http://%s:%s/xmlrpc/%s' % (host, port, path), allow_none=True) + self.user_id = uid + self.passwd = passwd + self.dbname = dbname + + def __call__(self, *request, **kwargs): + return self.rpc.execute(self.dbname, self.user_id, self.passwd, *request, **kwargs) + +class EmailParser(object): + def __init__(self, uid, password, dbname, host, port, model=False, email_default=False): + self.rpc = RPCProxy(uid, password, host=host, port=port, dbname=dbname) + if model: + try: + self.model_id = int(model) + self.model = str(model) + except: + self.model_id = self.rpc('ir.model', 'search', [('model', '=', model)])[0] + self.model = str(model) + self.email_default = email_default + + + def parse(self, message, custom_values=None, save_original=None): + # pass message as bytes because we don't know its encoding until we parse its headers + # and hence can't convert it to utf-8 for transport + return self.rpc('mail.thread', + 'message_process', + self.model, + xmlrpclib.Binary(message), + custom_values or {}, + save_original or False) + +def configure_parser(): + parser = optparse.OptionParser(usage='usage: %prog [options]', version='%prog v1.1') + group = optparse.OptionGroup(parser, "Note", + "This program parse a mail from standard input and communicate " + "with the Odoo server for case management in the CRM module.") + parser.add_option_group(group) + parser.add_option("-u", "--user", dest="userid", + help="Odoo user id to connect with", + default=config.OPENERP_DEFAULT_USER_ID, type='int') + parser.add_option("-p", "--password", dest="password", + help="Odoo user password", + default=config.OPENERP_DEFAULT_PASSWORD) + parser.add_option("-o", "--model", dest="model", + help="Name or ID of destination model", + default="crm.lead") + parser.add_option("-m", "--default", dest="default", + help="Admin email for error notifications.", + default=None) + parser.add_option("-d", "--dbname", dest="dbname", + help="Odoo database name (default: %default)", + default=config.OPENERP_DEFAULT_DATABASE) + parser.add_option("--host", dest="host", + help="Odoo Server hostname", + default=config.OPENERP_HOSTNAME) + parser.add_option("--port", dest="port", + help="Odoo Server XML-RPC port number", + default=config.OPENERP_PORT) + parser.add_option("--custom-values", dest="custom_values", + help="Dictionary of extra values to pass when creating records", + default=None) + parser.add_option("-s", dest="save_original", + action="store_true", + help="Keep a full copy of the email source attached to each message", + default=False) + + return parser + +def main(): + """ + Receive the email via the stdin and send it to the OpenERP Server + """ + + parser = configure_parser() + (options, args) = parser.parse_args() + email_parser = EmailParser(options.userid, + options.password, + options.dbname, + options.host, + options.port, + model=options.model, + email_default= options.default) + msg_txt = sys.stdin.read() + custom_values = {} + try: + custom_values = dict(eval(options.custom_values or "{}" )) + except: + import traceback + traceback.print_exc() + + try: + email_parser.parse(msg_txt, custom_values, options.save_original or False) + except Exception: + msg = '\n'.join([ + 'parameters', + '==========', + '%r' % (options,), + 'traceback', + '=========', + '%s' % (cgitb.text(sys.exc_info())), + ]) + + subject = '[Odoo]:ERROR: Mailgateway - %s' % time.strftime('%Y-%m-%d %H:%M:%S') + send_mail( + config.MAIL_ERROR, + config.MAIL_ADMINS, + subject, msg, files=[('message.txt', msg_txt)] + ) + sys.stderr.write("Failed to deliver email to Odoo Server, sending error notification to %s\n" % config.MAIL_ADMINS) + +if __name__ == '__main__': + main() diff --git a/sent_mails/static/src/js/chat_manager_sent_mail.js b/sent_mails/static/src/js/chat_manager_sent_mail.js new file mode 100644 index 000000000..6c2cb8875 --- /dev/null +++ b/sent_mails/static/src/js/chat_manager_sent_mail.js @@ -0,0 +1,1100 @@ +odoo.define('mail.chat_manager', function (require) { +"use strict"; + +var bus = require('bus.bus').bus; +var config = require('web.config'); +var core = require('web.core'); +var data = require('web.data'); +var Model = require('web.Model'); +var session = require('web.session'); +var time = require('web.time'); +var web_client = require('web.web_client'); + +var _t = core._t; +var _lt = core._lt; +var LIMIT = 25; +var preview_msg_max_size = 350; // optimal for native english speakers + +var MessageModel = new Model('mail.message', session.user_context); +var ChannelModel = new Model('mail.channel', session.user_context); +var UserModel = new Model('res.users', session.user_context); +var PartnerModel = new Model('res.partner', session.user_context); + +// Private model +//---------------------------------------------------------------------------------- +var messages = []; +var channels = []; +var channels_preview_def; +var channel_defs = {}; +var chat_unread_counter = 0; +var unread_conversation_counter = 0; +var emojis = []; +var emoji_substitutions = {}; +var needaction_counter = 0; +var mention_partner_suggestions = []; +var discuss_ids = {}; +var global_unread_counter = 0; +var pinned_dm_partners = []; // partner_ids we have a pinned DM with +var client_action_open = false; + +// Utils: Window focus/unfocus, beep, tab title, parsing html strings +//---------------------------------------------------------------------------------- +var beep = (function () { + if (typeof(Audio) === "undefined") { + return function () {}; + } + var audio = new Audio(); + var ext = audio.canPlayType("audio/ogg; codecs=vorbis") ? ".ogg" : ".mp3"; + audio.src = session.url("/mail/static/src/audio/ting" + ext); + return function () { audio.play(); }; +})(); + +bus.on("window_focus", null, function() { + global_unread_counter = 0; + web_client.set_title_part("_chat"); +}); + +// to do: move this to mail.utils +function send_native_notification(title, content) { + var notification = new Notification(title, {body: content, icon: "/mail/static/src/img/odoo_o.png"}); + notification.onclick = function (e) { + window.focus(); + if (this.cancel) { + this.cancel(); + } else if (this.close) { + this.close(); + } + }; +} + +function notify_incoming_message (msg, options) { + if (bus.is_odoo_focused() && options.is_displayed) { + // no need to notify + return; + } + var title = _t('New message'); + if (msg.author_id[1]) { + title = _.escape(msg.author_id[1]); + } + var content = parse_and_transform(msg.body, strip_html).substr(0, preview_msg_max_size); + + if (!bus.is_odoo_focused()) { + global_unread_counter++; + var tab_title = _.str.sprintf(_t("%d Messages"), global_unread_counter); + web_client.set_title_part("_chat", tab_title); + } + + if (window.Notification && Notification.permission === "granted") { + if (bus.is_master) { + send_native_notification(title, content); + } + } else { + web_client.do_notify(title, content); + if (bus.is_master) { + beep(); + } + } +} + +function parse_and_transform(html_string, transform_function) { + var open_token = "OPEN" + Date.now(); + var string = html_string.replace(/</g, open_token); + var children = $('
').html(string).contents(); + return _parse_and_transform(children, transform_function) + .replace(new RegExp(open_token, "g"), "<"); +} + +function _parse_and_transform(nodes, transform_function) { + return _.map(nodes, function (node) { + return transform_function(node, function () { + return _parse_and_transform(node.childNodes, transform_function); + }); + }).join(""); +} + +// suggested regexp (gruber url matching regexp, adapted to js, see https://gist.github.com/gruber/8891611) +var url_regexp = /\b((?:https?:\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))/gi; +function add_link (node, transform_children) { + if (node.nodeType === 3) { // text node + return node.data.replace(url_regexp, function (url) { + var href = (!/^(f|ht)tps?:\/\//i.test(url)) ? "http://" + url : url; + return '' + url + ''; + }); + } + if (node.tagName === "A") return node.outerHTML; + node.innerHTML = transform_children(); + return node.outerHTML; +} + +function strip_html (node, transform_children) { + if (node.nodeType === 3) return node.data; // text node + if (node.tagName === "BR") return "\n"; + return transform_children(); +} + +function inline (node, transform_children) { + if (node.nodeType === 3) return node.data; + if (node.tagName === "BR") return " "; + if (node.tagName.match(/^(A|P|DIV|PRE|BLOCKQUOTE)$/)) return transform_children(); + node.innerHTML = transform_children(); + return node.outerHTML; +} + +// Message and channel manipulation helpers +//---------------------------------------------------------------------------------- + +// options: channel_id, silent +function add_message (data, options) { + options = options || {}; + var msg = _.findWhere(messages, { id: data.id }); + if (!msg) { + msg = chat_manager.make_message(data); + // Keep the array ordered by id when inserting the new message + messages.splice(_.sortedIndex(messages, msg, 'id'), 0, msg); + _.each(msg.channel_ids, function (channel_id) { + var channel = chat_manager.get_channel(channel_id); + if (channel) { + add_to_cache(msg, []); + if (options.domain && options.domain !== []) { + add_to_cache(msg, options.domain); + } + if (channel.hidden) { + channel.hidden = false; + chat_manager.bus.trigger('new_channel', channel); + } + if (channel.type !== 'static' && !msg.is_author && !msg.is_system_notification) { + if (options.increment_unread) { + update_channel_unread_counter(channel, channel.unread_counter+1); + } + if (channel.is_chat && options.show_notification) { + if (!client_action_open && config.device.size_class !== config.device.SIZES.XS) { + // automatically open chat window + chat_manager.bus.trigger('open_chat', channel, { passively: true }); + } + var query = {is_displayed: false}; + chat_manager.bus.trigger('anyone_listening', channel, query); + notify_incoming_message(msg, query); + } + } + } + }); + if (!options.silent) { + chat_manager.bus.trigger('new_message', msg); + } + } else if (options.domain && options.domain !== []) { + add_to_cache(msg, options.domain); + } + return msg; +} + +function make_message (data) { + var msg = { + id: data.id, + author_id: data.author_id, + body_short: data.body_short || "", + body: data.body || "", + date: moment(time.str_to_datetime(data.date)), + message_type: data.message_type, + subtype_description: data.subtype_description, + is_author: data.author_id && data.author_id[0] === session.partner_id, + is_note: data.is_note, + is_system_notification: data.message_type === 'notification' && data.model === 'mail.channel', + attachment_ids: data.attachment_ids, + subject: data.subject, + email_from: data.email_from, + record_name: data.record_name, + tracking_value_ids: data.tracking_value_ids, + channel_ids: data.channel_ids, + model: data.model, + res_id: data.res_id, + url: session.url("/mail/view?message_id=" + data.id), + }; + + _.each(_.keys(emoji_substitutions), function (key) { + var escaped_key = String(key).replace(/([.*+?=^!:${}()|[\]\/\\])/g, '\\$1'); + var regexp = new RegExp("(?:^|\\s|<[a-z]*>)(" + escaped_key + ")(?=\\s|$|)", "g"); + msg.body = msg.body.replace(regexp, ' '+emoji_substitutions[key]+' '); + }); + + function property_descr(channel) { + return { + enumerable: true, + get: function () { + return _.contains(msg.channel_ids, channel); + }, + set: function (bool) { + if (bool) { + add_channel_to_message(msg, channel); + } else { + msg.channel_ids = _.without(msg.channel_ids, channel); + } + } + }; + } + Object.defineProperties(msg, { + is_sent: property_descr("channel_sent"), + is_starred: property_descr("channel_starred"), + is_needaction: property_descr("channel_inbox"), + }); + + if (_.contains(data.needaction_partner_ids, session.partner_id)) { + msg.is_needaction = true; + } + if (_.contains(data.starred_partner_ids, session.partner_id)) { + msg.is_starred = true; + } + if (_.contains(data.author_id, session.partner_id)) { + msg.is_sent = true; + } + if (msg.model === 'mail.channel') { + var real_channels = _.without(msg.channel_ids, 'channel_inbox', 'channel_starred', 'channel_sent'); + var origin = real_channels.length === 1 ? real_channels[0] : undefined; + var channel = origin && chat_manager.get_channel(origin); + if (channel) { + msg.origin_id = origin; + msg.origin_name = channel.name; + } + } + + // Compute displayed author name or email + if ((!msg.author_id || !msg.author_id[0]) && msg.email_from) { + msg.mailto = msg.email_from; + } else { + msg.displayed_author = msg.author_id && msg.author_id[1] || + msg.email_from || _t('Anonymous'); + } + + // Don't redirect on author clicked of self-posted messages + msg.author_redirect = !msg.is_author; + + // Compute the avatar_url + if (msg.author_id && msg.author_id[0]) { + msg.avatar_src = "/web/image/res.partner/" + msg.author_id[0] + "/image_small"; + } else if (msg.message_type === 'email') { + msg.avatar_src = "/mail/static/src/img/email_icon.png"; + } else { + msg.avatar_src = "/mail/static/src/img/smiley/avatar.jpg"; + } + + // add anchor tags to urls + msg.body = parse_and_transform(msg.body, add_link); + + // Compute url of attachments + _.each(msg.attachment_ids, function(a) { + a.url = '/web/content/' + a.id + '?download=true'; + }); + + // format date to the local only once by message + // can not be done in preprocess, since it alter the original value + if (msg.tracking_value_ids && msg.tracking_value_ids.length) { + _.each(msg.tracking_value_ids, function(f) { + if (_.contains(['date', 'datetime'], f.field_type)) { + var format = (f.field_type === 'date') ? 'LL' : 'LLL'; + if (f.old_value) { + f.old_value = moment.utc(f.old_value).local().format(format); + } + if (f.new_value) { + f.new_value = moment.utc(f.new_value).local().format(format); + } + } + }); + } + + return msg; +} + +function add_channel_to_message (message, channel_id) { + message.channel_ids.push(channel_id); + message.channel_ids = _.uniq(message.channel_ids); +} + +function add_channel (data, options) { + options = typeof options === "object" ? options : {}; + var channel = chat_manager.get_channel(data.id); + if (channel) { + if (channel.is_folded !== (data.state === "folded")) { + channel.is_folded = (data.state === "folded"); + chat_manager.bus.trigger("channel_toggle_fold", channel); + } + } else { + channel = chat_manager.make_channel(data, options); + channels.push(channel); + // In case of a static channel (Inbox, Starred), the name is translated thanks to _lt + // (lazy translate). In this case, channel.name is an object, not a string. + channels = _.sortBy(channels, function (channel) { return _.isString(channel.name) ? channel.name.toLowerCase() : '' }); + if (!options.silent) { + chat_manager.bus.trigger("new_channel", channel); + } + if (channel.is_detached) { + chat_manager.bus.trigger("open_chat", channel); + } + } + return channel; +} + +function make_channel (data, options) { + var channel = { + id: data.id, + name: data.name, + type: data.type || data.channel_type, + all_history_loaded: false, + uuid: data.uuid, + is_detached: data.is_minimized, + is_folded: data.state === "folded", + autoswitch: 'autoswitch' in options ? options.autoswitch : true, + hidden: options.hidden, + display_needactions: options.display_needactions, + mass_mailing: data.mass_mailing, + needaction_counter: data.message_needaction_counter || 0, + unread_counter: 0, + last_seen_message_id: data.seen_message_id, + cache: {'[]': { + all_history_loaded: false, + loaded: false, + messages: [], + }}, + }; + if (channel.type === "channel") { + channel.type = data.public !== "private" ? "public" : "private"; + } + if (_.size(data.direct_partner) > 0) { + channel.type = "dm"; + channel.name = data.direct_partner[0].name; + channel.direct_partner_id = data.direct_partner[0].id; + channel.status = data.direct_partner[0].im_status; + pinned_dm_partners.push(channel.direct_partner_id); + bus.update_option('bus_presence_partner_ids', pinned_dm_partners); + } else if ('anonymous_name' in data) { + channel.name = data.anonymous_name; + } + channel.is_chat = !channel.type.match(/^(public|private|static)$/); + if (data.message_unread_counter) { + update_channel_unread_counter(channel, data.message_unread_counter); + } + return channel; +} + +function remove_channel (channel) { + if (!channel) { return; } + if (channel.type === 'dm') { + var index = pinned_dm_partners.indexOf(channel.direct_partner_id); + if (index > -1) { + pinned_dm_partners.splice(index, 1); + bus.update_option('bus_presence_partner_ids', pinned_dm_partners); + } + } + channels = _.without(channels, channel); + delete channel_defs[channel.id]; +} + +function get_channel_cache (channel, domain) { + var stringified_domain = JSON.stringify(domain || []); + if (!channel.cache[stringified_domain]) { + channel.cache[stringified_domain] = { + all_history_loaded: false, + loaded: false, + messages: [], + }; + } + return channel.cache[stringified_domain]; +} + +function invalidate_caches(channel_ids) { + _.each(channel_ids, function (channel_id) { + var channel = chat_manager.get_channel(channel_id); + if (channel) { + channel.cache = { '[]': channel.cache['[]']}; + } + }); +} + +function add_to_cache(message, domain) { + _.each(message.channel_ids, function (channel_id) { + var channel = chat_manager.get_channel(channel_id); + if (channel) { + var channel_cache = get_channel_cache(channel, domain); + var index = _.sortedIndex(channel_cache.messages, message, 'id'); + if (channel_cache.messages[index] !== message) { + channel_cache.messages.splice(index, 0, message); + } + } + }); +} + +function remove_message_from_channel (channel_id, message) { + message.channel_ids = _.without(message.channel_ids, channel_id); + var channel = _.findWhere(channels, { id: channel_id }); + _.each(channel.cache, function (cache) { + cache.messages = _.without(cache.messages, message); + }); +} +// options: domain, load_more +function fetch_from_channel (channel, options) { + options = options || {}; + var domain = + (channel.id === "channel_inbox") ? [['needaction', '=', true]] : + (channel.id === "channel_sent") ? [['subtype_id', '=', 'Discussions']] : + (channel.id === "channel_starred") ? [['starred', '=', true]] : + [['channel_ids', 'in', channel.id]]; + var cache = get_channel_cache(channel, options.domain); + + if (options.domain) { + domain = new data.CompoundDomain(domain, options.domain || []); + } + if (options.load_more) { + var min_message_id = cache.messages[0].id; + domain = new data.CompoundDomain([['id', '<', min_message_id]], domain); + } + + return MessageModel.call('message_fetch', [domain], {limit: LIMIT, context: session.user_context}).then(function (msgs) { + if (!cache.all_history_loaded) { + cache.all_history_loaded = msgs.length < LIMIT; + } + cache.loaded = true; + + _.each(msgs, function (msg) { + add_message(msg, {channel_id: channel.id, silent: true, domain: options.domain}); + }); + var channel_cache = get_channel_cache(channel, options.domain || []); + return channel_cache.messages; + }); +} + +// options: force_fetch +function fetch_document_messages (ids, options) { + var loaded_msgs = _.filter(messages, function (message) { + return _.contains(ids, message.id); + }); + var loaded_msg_ids = _.pluck(loaded_msgs, 'id'); + + options = options || {}; + if (options.force_fetch || _.difference(ids.slice(0, LIMIT), loaded_msg_ids).length) { + var ids_to_load = _.difference(ids, loaded_msg_ids).slice(0, LIMIT); + + return MessageModel.call('message_format', [ids_to_load], {context: session.user_context}).then(function (msgs) { + var processed_msgs = []; + _.each(msgs, function (msg) { + processed_msgs.push(add_message(msg, {silent: true})); + }); + return _.sortBy(loaded_msgs.concat(processed_msgs), function (msg) { + return msg.id; + }); + }); + } else { + return $.when(loaded_msgs); + } +} + +function update_channel_unread_counter (channel, counter) { + if (channel.unread_counter > 0 && counter === 0) { + unread_conversation_counter = Math.max(0, unread_conversation_counter-1); + } else if (channel.unread_counter === 0 && counter > 0) { + unread_conversation_counter++; + } + if (channel.is_chat) { + chat_unread_counter = Math.max(0, chat_unread_counter - channel.unread_counter + counter); + } + channel.unread_counter = counter; + chat_manager.bus.trigger("update_channel_unread_counter", channel); +} + +var channel_seen = _.throttle(function (channel) { + return ChannelModel.call('channel_seen', [[channel.id]], {}, {shadow: true}); +}, 3000); + +// Notification handlers +// --------------------------------------------------------------------------------- +function on_notification (notifications) { + // sometimes, the web client receives unsubscribe notification and an extra + // notification on that channel. This is then followed by an attempt to + // rejoin the channel that we just left. The next few lines remove the + // extra notification to prevent that situation to occur. + var unsubscribed_notif = _.find(notifications, function (notif) { + return notif[1].info === "unsubscribe"; + }); + if (unsubscribed_notif) { + notifications = _.reject(notifications, function (notif) { + return notif[0][1] === "mail.channel" && notif[0][2] === unsubscribed_notif[1].id; + }); + } + _.each(notifications, function (notification) { + var model = notification[0][1]; + if (model === 'ir.needaction') { + // new message in the inbox + on_needaction_notification(notification[1]); + } else if (model === 'mail.channel') { + // new message in a channel + on_channel_notification(notification[1]); + } else if (model === 'res.partner') { + // channel joined/left, message marked as read/(un)starred, chat open/closed + on_partner_notification(notification[1]); + } else if (model === 'bus.presence') { + // update presence of users + on_presence_notification(notification[1]); + } + }); +} + +function on_needaction_notification (message) { + message = add_message(message, { + channel_id: 'channel_inbox', + show_notification: true, + increment_unread: true, + }); + invalidate_caches(message.channel_ids); + needaction_counter++; + _.each(message.channel_ids, function (channel_id) { + var channel = chat_manager.get_channel(channel_id); + if (channel) { + channel.needaction_counter++; + } + }); + chat_manager.bus.trigger('update_needaction', needaction_counter); +} + +function on_channel_notification (message) { + var def; + var channel_already_in_cache = true; + if (message.channel_ids.length === 1) { + channel_already_in_cache = !!chat_manager.get_channel(message.channel_ids[0]); + def = chat_manager.join_channel(message.channel_ids[0], {autoswitch: false}); + } else { + def = $.when(); + } + def.then(function () { + // don't increment unread if channel wasn't in cache yet as its unread counter has just been fetched + add_message(message, { show_notification: true, increment_unread: channel_already_in_cache }); + invalidate_caches(message.channel_ids); + }); +} + +function on_partner_notification (data) { + if (data.info === "unsubscribe") { + remove_channel(chat_manager.get_channel(data.id)); + chat_manager.bus.trigger("unsubscribe_from_channel", data.id); + } else if (data.type === 'toggle_star') { + on_toggle_star_notification(data); + } else if (data.type === 'mark_as_read') { + on_mark_as_read_notification(data); + } else if (data.type === 'mark_as_unread') { + on_mark_as_unread_notification(data); + } else if (data.info === 'channel_seen') { + on_channel_seen_notification(data); + } else { + on_chat_session_notification(data); + } +} + +function on_toggle_star_notification (data) { + _.each(data.message_ids, function (msg_id) { + var message = _.findWhere(messages, { id: msg_id }); + if (message) { + invalidate_caches(message.channel_ids); + message.is_starred = data.starred; + if (!message.is_starred) { + remove_message_from_channel("channel_starred", message); + } else { + add_to_cache(message, []); + var channel_starred = chat_manager.get_channel('channel_starred'); + channel_starred.cache = _.pick(channel_starred.cache, "[]"); + } + chat_manager.bus.trigger('update_message', message); + } + }); +} + +function on_mark_as_read_notification (data) { + _.each(data.message_ids, function (msg_id) { + var message = _.findWhere(messages, { id: msg_id }); + if (message) { + invalidate_caches(message.channel_ids); + remove_message_from_channel("channel_inbox", message); + chat_manager.bus.trigger('update_message', message); + } + }); + if (data.channel_ids) { + _.each(data.channel_ids, function (channel_id) { + var channel = chat_manager.get_channel(channel_id); + if (channel) { + channel.needaction_counter = Math.max(channel.needaction_counter - data.message_ids.length, 0); + } + }); + } else { // if no channel_ids specified, this is a 'mark all read' in the inbox + _.each(channels, function (channel) { + channel.needaction_counter = 0; + }); + } + needaction_counter = Math.max(needaction_counter - data.message_ids.length, 0); + chat_manager.bus.trigger('update_needaction', needaction_counter); +} + +function on_mark_as_unread_notification (data) { + _.each(data.message_ids, function (message_id) { + var message = _.findWhere(messages, { id: message_id }); + if (message) { + invalidate_caches(message.channel_ids); + add_channel_to_message(message, 'channel_inbox'); + add_to_cache(message, []); + } + }); + var channel_inbox = chat_manager.get_channel('channel_inbox'); + channel_inbox.cache = _.pick(channel_inbox.cache, "[]"); + + _.each(data.channel_ids, function (channel_id) { + var channel = chat_manager.get_channel(channel_id); + if (channel) { + channel.needaction_counter += data.message_ids.length; + } + }); + needaction_counter += data.message_ids.length; + chat_manager.bus.trigger('update_needaction', needaction_counter); +} + +function on_channel_seen_notification (data) { + var channel = chat_manager.get_channel(data.id); + if (channel) { + channel.last_seen_message_id = data.last_message_id; + if (channel.unread_counter) { + update_channel_unread_counter(channel, 0); + } + } +} + +function on_chat_session_notification (chat_session) { + var channel; + if ((chat_session.channel_type === "channel") && (chat_session.state === "open")) { + add_channel(chat_session, {autoswitch: false}); + if (!chat_session.is_minimized && chat_session.info !== 'creation') { + web_client.do_notify(_t("Invitation"), _t("You have been invited to: ") + chat_session.name); + } + } + // partner specific change (open a detached window for example) + if ((chat_session.state === "open") || (chat_session.state === "folded")) { + channel = chat_session.is_minimized && chat_manager.get_channel(chat_session.id); + if (channel) { + channel.is_detached = true; + channel.is_folded = (chat_session.state === "folded"); + chat_manager.bus.trigger("open_chat", channel); + } + } else if (chat_session.state === "closed") { + channel = chat_manager.get_channel(chat_session.id); + if (channel) { + channel.is_detached = false; + chat_manager.bus.trigger("close_chat", channel, {keep_open_if_unread: true}); + } + } +} + +function on_presence_notification (data) { + var dm = chat_manager.get_dm_from_partner_id(data.id); + if (dm) { + dm.status = data.im_status; + chat_manager.bus.trigger('update_dm_presence', dm); + } +} + +// Public interface +//---------------------------------------------------------------------------------- +var chat_manager = { + // these two functions are exposed for extensibility purposes and shouldn't be called by other modules + make_message: make_message, + make_channel: make_channel, + + post_message: function (data, options) { + options = options || {}; + var msg = { + partner_ids: data.partner_ids, + body: _.str.trim(data.content), + attachment_ids: data.attachment_ids, + }; + if ('subject' in data) { + msg.subject = data.subject; + } + if ('channel_id' in options) { + // post a message in a channel + return ChannelModel.call('message_post', [options.channel_id], _.extend(msg, { + message_type: 'comment', + content_subtype: 'html', + subtype: 'mail.mt_comment', + })); + } + if ('model' in options && 'res_id' in options) { + // post a message in a chatter + _.extend(msg, { + content_subtype: data.content_subtype, + context: data.context, + message_type: data.message_type, + subtype: data.subtype, + subtype_id: data.subtype_id, + }); + + var model = new Model(options.model); + return model.call('message_post', [options.res_id], msg).then(function (msg_id) { + return MessageModel.call('message_format', [msg_id]).then(function (msgs) { + msgs[0].model = options.model; + msgs[0].res_id = options.res_id; + add_message(msgs[0]); + }); + }); + } + }, + + get_message: function (id) { + return _.findWhere(messages, {id: id}); + }, + get_messages: function (options) { + var channel; + + if ('channel_id' in options && options.load_more) { + // get channel messages, force load_more + channel = this.get_channel(options.channel_id); + return fetch_from_channel(channel, {domain: options.domain || {}, load_more: true}); + } + if ('channel_id' in options) { + // channel message, check in cache first + channel = this.get_channel(options.channel_id); + var channel_cache = get_channel_cache(channel, options.domain); + if (channel_cache.loaded) { + return $.when(channel_cache.messages); + } else { + return fetch_from_channel(channel, {domain: options.domain}); + } + } + if ('ids' in options) { + // get messages from their ids (chatter is the main use case) + return fetch_document_messages(options.ids, options).then(function(result) { + chat_manager.mark_as_read(options.ids); + return result; + }); + } + if ('model' in options && 'res_id' in options) { + // get messages for a chatter, when it doesn't know the ids (use + // case is when using the full composer) + var domain = [['model', '=', options.model], ['res_id', '=', options.res_id]]; + MessageModel.call('message_fetch', [domain], {limit: 30}).then(function (msgs) { + return _.map(msgs, add_message); + }); + } + }, + toggle_star_status: function (message_id) { + var msg = _.findWhere(messages, { id: message_id }); + + return MessageModel.call('set_message_starred', [[message_id], !msg.is_starred]); + }, + unstar_all: function () { + return MessageModel.call('unstar_all', [[]], {}); + }, + mark_as_read: function (message_ids) { + var ids = _.filter(message_ids, function (id) { + var message = _.findWhere(messages, {id: id}); + // If too many messages, not all are fetched, and some might not be found + return !message || message.is_needaction; + }); + if (ids.length) { + return MessageModel.call('set_message_done', [ids]); + } else { + return $.when(); + } + }, + mark_all_as_read: function (channel, domain) { + if ((channel.id === "channel_inbox" && needaction_counter) || (channel && channel.needaction_counter)) { + return MessageModel.call('mark_all_as_read', [], {channel_ids: channel.id !== "channel_inbox" ? [channel.id] : [], domain: domain}); + } + return $.when(); + }, + undo_mark_as_read: function (message_ids, channel) { + return MessageModel.call('mark_as_unread', [message_ids, [channel.id]]); + }, + mark_channel_as_seen: function (channel) { + if (channel.unread_counter > 0 && channel.type !== 'static') { + update_channel_unread_counter(channel, 0); + channel_seen(channel); + } + }, + + get_channels: function () { + return _.clone(channels); + }, + + get_channel: function (id) { + return _.findWhere(channels, {id: id}); + }, + + get_dm_from_partner_id: function (partner_id) { + return _.findWhere(channels, {direct_partner_id: partner_id}); + }, + + all_history_loaded: function (channel, domain) { + return get_channel_cache(channel, domain).all_history_loaded; + }, + + get_mention_partner_suggestions: function (channel) { + if (!channel) { + return mention_partner_suggestions; + } + if (!channel.members_deferred) { + channel.members_deferred = ChannelModel + .call("channel_fetch_listeners", [channel.uuid], {}, {shadow: true}) + .then(function (members) { + var suggestions = []; + _.each(mention_partner_suggestions, function (partners) { + suggestions.push(_.filter(partners, function (partner) { + return !_.findWhere(members, { id: partner.id }); + })); + }); + + return [members]; + }); + } + return channel.members_deferred; + }, + + get_emojis: function() { + return emojis; + }, + + get_needaction_counter: function () { + return needaction_counter; + }, + get_chat_unread_counter: function () { + return chat_unread_counter; + }, + get_unread_conversation_counter: function () { + return unread_conversation_counter; + }, + + get_last_seen_message: function (channel) { + if (channel.last_seen_message_id) { + var messages = channel.cache['[]'].messages; + var msg = _.findWhere(messages, {id: channel.last_seen_message_id}); + if (msg) { + var i = _.sortedIndex(messages, msg, 'id') + 1; + while (i < messages.length && (messages[i].is_author || messages[i].is_system_notification)) { + msg = messages[i]; + i++; + } + return msg; + } + } + }, + + get_discuss_ids: function () { + return discuss_ids; + }, + + detach_channel: function (channel) { + return ChannelModel.call("channel_minimize", [channel.uuid, true], {}, {shadow: true}); + }, + remove_chatter_messages: function (model) { + messages = _.reject(messages, function (message) { + return message.channel_ids.length === 0 && message.model === model; + }); + }, + bus: new core.Bus(), + + create_channel: function (name, type) { + var method = type === "dm" ? "channel_get" : "channel_create"; + var args = type === "dm" ? [[name]] : [name, type]; + + return ChannelModel + .call(method, args) + .then(add_channel); + }, + join_channel: function (channel_id, options) { + if (channel_id in channel_defs) { + // prevents concurrent calls to channel_join_and_get_info + return channel_defs[channel_id]; + } + var channel = this.get_channel(channel_id); + if (channel) { + // channel already joined + channel_defs[channel_id] = $.when(channel); + } else { + channel_defs[channel_id] = ChannelModel + .call('channel_join_and_get_info', [[channel_id]]) + .then(function (result) { + return add_channel(result, options); + }); + } + return channel_defs[channel_id]; + }, + open_and_detach_dm: function (partner_id) { + return ChannelModel.call('channel_get_and_minimize', [[partner_id]]).then(add_channel); + }, + open_channel: function (channel) { + chat_manager.bus.trigger(client_action_open ? 'open_channel' : 'detach_channel', channel); + }, + + unsubscribe: function (channel) { + var def; + if (_.contains(['public', 'private'], channel.type)) { + def = ChannelModel.call('action_unfollow', [[channel.id]]); + } else { + def = ChannelModel.call('channel_pin', [channel.uuid, false]); + } + return def.then(function () { + remove_channel(channel); + }); + }, + close_chat_session: function (channel_id) { + var channel = this.get_channel(channel_id); + ChannelModel.call("channel_fold", [], {uuid : channel.uuid, state : "closed"}, {shadow: true}); + }, + fold_channel: function (channel_id, folded) { + var args = { + uuid: this.get_channel(channel_id).uuid, + }; + if (_.isBoolean(folded)) { + args.state = folded ? 'folded' : 'open'; + } + return ChannelModel.call("channel_fold", [], args, {shadow: true}); + }, + /** + * Special redirection handling for given model and id + * + * If the model is res.partner, and there is a user associated with this + * partner which isn't the current user, open the DM with this user. + * Otherwhise, open the record's form view, if this is not the current user's. + */ + redirect: function (res_model, res_id, dm_redirection_callback) { + var self = this; + var redirect_to_document = function (res_model, res_id, view_id) { + web_client.do_action({ + type:'ir.actions.act_window', + view_type: 'form', + view_mode: 'form', + res_model: res_model, + views: [[view_id || false, 'form']], + res_id: res_id, + }); + }; + if (res_model === "res.partner") { + var domain = [["partner_id", "=", res_id]]; + UserModel.call("search", [domain]).then(function (user_ids) { + if (user_ids.length && user_ids[0] !== session.uid) { + self.create_channel(res_id, 'dm').then(dm_redirection_callback || function () {}); + } else if (!user_ids.length) { + redirect_to_document(res_model, res_id); + } + }); + } else { + new Model(res_model).call('get_formview_id', [res_id, session.user_context]).then(function (view_id) { + redirect_to_document(res_model, res_id, view_id); + }); + } + }, + + get_channels_preview: function (channels) { + var channels_preview = _.map(channels, function (channel) { + var info = _.pick(channel, 'id', 'is_chat', 'name', 'status', 'unread_counter'); + info.last_message = _.last(channel.cache['[]'].messages); + if (!info.is_chat) { + info.image_src = '/web/image/mail.channel/'+channel.id+'/image_small'; + } else if (channel.direct_partner_id) { + info.image_src = '/web/image/res.partner/'+channel.direct_partner_id+'/image_small'; + } else { + info.image_src = '/mail/static/src/img/smiley/avatar.jpg'; + } + return info; + }); + var missing_channels = _.where(channels_preview, {last_message: undefined}); + if (!channels_preview_def) { + if (missing_channels.length) { + var missing_channel_ids = _.pluck(missing_channels, 'id'); + channels_preview_def = ChannelModel.call('channel_fetch_preview', [missing_channel_ids], {}, {shadow: true}); + } else { + channels_preview_def = $.when(); + } + } + return channels_preview_def.then(function (channels) { + _.each(missing_channels, function (channel_preview) { + var channel = _.findWhere(channels, {id: channel_preview.id}); + if (channel) { + channel_preview.last_message = add_message(channel.last_message); + } + }); + return _.filter(channels_preview, function (channel) { + return channel.last_message; // remove empty channels + }); + }); + }, + get_message_body_preview: function (message_body) { + return parse_and_transform(message_body, inline); + }, + + search_partner: function (search_val, limit) { + return PartnerModel.call('im_search', [search_val, limit || 20], {}, {shadow: true}).then(function(result) { + var values = []; + _.each(result, function(user) { + var escaped_name = _.escape(user.name); + values.push(_.extend(user, { + 'value': escaped_name, + 'label': escaped_name, + })); + }); + return values; + }); + }, + + send_native_notification: send_native_notification, +}; + +chat_manager.bus.on('client_action_open', null, function (open) { + client_action_open = open; +}); + +// Initialization +// --------------------------------------------------------------------------------- +function init () { + add_channel({ + id: "channel_inbox", + name: _lt("Inbox"), + type: "static", + }, { display_needactions: true }); + + add_channel({ + id: "channel_starred", + name: _lt("Starred"), + type: "static" + }); + add_channel({ + id: "channel_sent", + name: _lt("Sent"), + type: "static" + }); //*** + + var load_channels = session.rpc('/mail/client_action').then(function (result) { + _.each(result.channel_slots, function (channels) { + _.each(channels, add_channel); + }); + needaction_counter = result.needaction_inbox_counter; + mention_partner_suggestions = result.mention_partner_suggestions; + }); + + var load_emojis = session.rpc("/mail/chat_init").then(function (result) { + emojis = result.emoji; + _.each(emojis, function(emoji) { + emoji_substitutions[_.escape(emoji.source)] = emoji.substitution; + }); + }); + + var ir_model = new Model("ir.model.data"); + var load_menu_id = ir_model.call("xmlid_to_res_id", ["mail.mail_channel_menu_root_chat"], {}, {shadow: true}); + var load_action_id = ir_model.call("xmlid_to_res_id", ["mail.mail_channel_action_client_chat"], {}, {shadow: true}); + + bus.on('notification', null, on_notification); + + return $.when(load_menu_id, load_action_id, load_channels, load_emojis).then(function (menu_id, action_id) { + discuss_ids = { + menu_id: menu_id, + action_id: action_id, + }; + bus.start_polling(); + }); +} + +chat_manager.is_ready = init(); + +return chat_manager; + +}); diff --git a/sent_mails/static/src/js/client_action_sent_mail.js b/sent_mails/static/src/js/client_action_sent_mail.js new file mode 100644 index 000000000..5b00646ed --- /dev/null +++ b/sent_mails/static/src/js/client_action_sent_mail.js @@ -0,0 +1,666 @@ +odoo.define('mail.chat_client_action', function (require) { +"use strict"; + +var chat_manager = require('mail.chat_manager'); +var composer = require('mail.composer'); +var ChatThread = require('mail.ChatThread'); + +var config = require('web.config'); +var ControlPanelMixin = require('web.ControlPanelMixin'); +var core = require('web.core'); +var data = require('web.data'); +var Dialog = require('web.Dialog'); +var framework = require('web.framework'); +var Model = require('web.Model'); + +var pyeval = require('web.pyeval'); +var SearchView = require('web.SearchView'); +var Widget = require('web.Widget'); + +var QWeb = core.qweb; +var _t = core._t; + +/** + * Widget : Invite People to Channel Dialog + * + * Popup containing a 'many2many_tags' custom input to select multiple partners. + * Search user according to the input, and trigger event when selection is validated. + **/ +var PartnerInviteDialog = Dialog.extend({ + dialog_title: _t('Invite people'), + template: "mail.PartnerInviteDialog", + init: function(parent, title, channel_id){ + this.channel_id = channel_id; + + this._super(parent, { + title: title, + size: "medium", + buttons: [{ + text: _t("Invite"), + close: true, + classes: "btn-primary", + click: _.bind(this.on_click_add, this), + }], + }); + this.PartnersModel = new Model('res.partner'); + }, + start: function(){ + var self = this; + this.$input = this.$('.o_mail_chat_partner_invite_input'); + this.$input.select2({ + width: '100%', + allowClear: true, + multiple: true, + formatResult: function(item) { + var status = QWeb.render('mail.chat.UserStatus', {status: item.im_status}); + return $('').text(item.text).prepend(status); + }, + query: function (query) { + self.PartnersModel.call('im_search', [query.term, 20]).then(function(result){ + var data = []; + _.each(result, function(partner){ + partner.text = partner.name; + data.push(partner); + }); + query.callback({results: data}); + }); + } + }); + return this._super.apply(this, arguments); + }, + on_click_add: function(){ + var self = this; + var data = this.$input.select2('data'); + if(data.length >= 1){ + var ChannelModel = new Model('mail.channel'); + return ChannelModel.call('channel_invite', [], {ids : [this.channel_id], partner_ids: _.pluck(data, 'id')}) + .then(function(){ + var names = _.escape(_.pluck(data, 'text').join(', ')); + var notification = _.str.sprintf(_t('You added %s to the conversation.'), names); + self.do_notify(_t('New people'), notification); + }); + } + }, +}); + +var ChatAction = Widget.extend(ControlPanelMixin, { + template: 'mail.client_action', + + events: { + "click .o_mail_chat_channel_item": function (event) { + event.preventDefault(); + var channel_id = this.$(event.currentTarget).data('channel-id'); + this.set_channel(chat_manager.get_channel(channel_id)); + }, + "click .o_mail_sidebar_title .o_add": function (event) { + event.preventDefault(); + var type = $(event.target).data("type"); + this.$('.o_mail_add_channel[data-type=' + type + ']') + .show() + .find("input").focus(); + }, + "blur .o_mail_add_channel input": function () { + this.$('.o_mail_add_channel') + .hide(); + }, + "click .o_mail_partner_unpin": function (event) { + event.stopPropagation(); + var channel_id = $(event.target).data("channel-id"); + this.unsubscribe_from_channel(chat_manager.get_channel(channel_id)); + }, + "click .o_snackbar_undo": function (event) { + event.preventDefault(); + var channel = this.channel; + this.$snackbar.remove(); + this.clear_needactions_def.then(function (msgs_ids) { + chat_manager.undo_mark_as_read(msgs_ids, channel); + }); + }, + "click .o_mail_annoying_notification_bar .fa-close": function () { + this.$(".o_mail_annoying_notification_bar").slideUp(); + }, + "click .o_mail_request_permission": function (event) { + event.preventDefault(); + this.$(".o_mail_annoying_notification_bar").slideUp(); + var def = window.Notification.requestPermission(); + if (def) { + def.then(function () { + chat_manager.send_native_notification('Permission granted', 'Odoo has now the permission to send you native notifications on this device.'); + }); + } + }, + "keydown": function (event) { + if (event.which === $.ui.keyCode.ESCAPE && this.selected_message) { + this.unselect_message(); + } + }, + }, + + on_attach_callback: function () { + chat_manager.bus.trigger('client_action_open', true); + if (this.channel) { + this.thread.scroll_to({offset: this.channels_scrolltop[this.channel.id]}); + } + }, + on_detach_callback: function () { + chat_manager.bus.trigger('client_action_open', false); + this.channels_scrolltop[this.channel.id] = this.thread.get_scrolltop(); + }, + + init: function(parent, action, options) { + this._super.apply(this, arguments); + this.action_manager = parent; + this.domain = []; + this.action = action; + this.options = options || {}; + this.channels_scrolltop = {}; + this.throttled_render_sidebar = _.throttle(this.render_sidebar.bind(this), 100, { leading: false }); + this.notification_bar = (window.Notification && window.Notification.permission === "default"); + this.selected_message = null; + }, + + willStart: function () { + return chat_manager.is_ready; + }, + + start: function() { + var self = this; + + // create searchview + var options = { + $buttons: $("
"), + action: this.action, + disable_groupby: true, + }; + var dataset = new data.DataSetSearch(this, 'mail.message'); + var view_id = (this.action && this.action.search_view_id && this.action.search_view_id[0]) || false; + var default_channel_id = this.options.active_id || + this.action.context.active_id || + this.action.params.default_active_id || + 'channel_inbox'; + var default_channel = chat_manager.get_channel(default_channel_id) || + chat_manager.get_channel('channel_inbox'); + + this.searchview = new SearchView(this, dataset, view_id, {}, options); + this.searchview.on('search_data', this, this.on_search); + + this.basic_composer = new composer.BasicComposer(this, {mention_partners_restricted: true}); + this.extended_composer = new composer.ExtendedComposer(this, {mention_partners_restricted: true}); + this.thread = new ChatThread(this, { + display_help: true, + shorten_messages: false, + }); + + this.$buttons = $(QWeb.render("mail.chat.ControlButtons", {})); + this.$buttons.find('button').css({display:"inline-block"}); + this.$buttons.on('click', '.o_mail_chat_button_invite', this.on_click_button_invite); + this.$buttons.on('click', '.o_mail_chat_button_unsubscribe', this.on_click_button_unsubscribe); + this.$buttons.on('click', '.o_mail_chat_button_settings', this.on_click_button_settings); + this.$buttons.on('click', '.o_mail_toggle_channels', function () { + self.$('.o_mail_chat_sidebar').slideToggle(200); + }); + this.$buttons.on('click', '.o_mail_chat_button_mark_read', function () { + chat_manager.mark_all_as_read(self.channel, self.domain); + }); + this.$buttons.on('click', '.o_mail_chat_button_unstar_all', chat_manager.unstar_all); + this.$buttons.on('click', '.o_mail_chat_button_new_message', this.on_click_new_message); + this.$buttons.on('click', '.o_mail_chat_button_new_message_sent', this.on_click_new_message); + + this.thread.on('redirect', this, function (res_model, res_id) { + chat_manager.redirect(res_model, res_id, this.set_channel.bind(this)); + }); + this.thread.on('redirect_to_channel', this, function (channel_id) { + chat_manager.join_channel(channel_id).then(this.set_channel.bind(this)); + }); + this.thread.on('load_more_messages', this, this.load_more_messages); + this.thread.on('mark_as_read', this, function (message_id) { + chat_manager.mark_as_read([message_id]); + }); + this.thread.on('toggle_star_status', this, function (message_id) { + chat_manager.toggle_star_status(message_id); + }); + this.thread.on('select_message', this, this.select_message); + this.thread.on('unselect_message', this, this.unselect_message); + + this.basic_composer.on('post_message', this, this.on_post_message); + this.basic_composer.on('input_focused', this, this.on_composer_input_focused); + this.extended_composer.on('post_message', this, this.on_post_message); + this.extended_composer.on('input_focused', this, this.on_composer_input_focused); + + var def1 = this.thread.appendTo(this.$('.o_mail_chat_content')); + var def2 = this.basic_composer.appendTo(this.$('.o_mail_chat_content')); + var def3 = this.extended_composer.appendTo(this.$('.o_mail_chat_content')); + var def4 = this.searchview.appendTo($("
")).then(function () { + self.$searchview_buttons = self.searchview.$buttons.contents(); + }); + + this.render_sidebar(); + + return $.when(def1, def2, def3, def4) + .then(this.set_channel.bind(this, default_channel)) + .then(function () { + chat_manager.bus.on('open_channel', self, self.set_channel); + chat_manager.bus.on('new_message', self, self.on_new_message); + chat_manager.bus.on('update_message', self, self.on_update_message); + chat_manager.bus.on('new_channel', self, self.on_new_channel); + chat_manager.bus.on('anyone_listening', self, function (channel, query) { + query.is_displayed = query.is_displayed || (channel.id === self.channel.id && self.thread.is_at_bottom()); + }); + chat_manager.bus.on('unsubscribe_from_channel', self, self.render_sidebar); + chat_manager.bus.on('update_needaction', self, self.throttled_render_sidebar); + chat_manager.bus.on('update_channel_unread_counter', self, self.throttled_render_sidebar); + chat_manager.bus.on('update_dm_presence', self, self.throttled_render_sidebar); + self.thread.$el.on("scroll", null, _.debounce(function () { + if (self.thread.is_at_bottom()) { + chat_manager.mark_channel_as_seen(self.channel); + } + }, 100)); + }); + }, + + select_message: function(message_id) { + this.$el.addClass('o_mail_selection_mode'); + var message = chat_manager.get_message(message_id); + this.selected_message = message; + var subject = "Re: " + message.record_name; + this.extended_composer.set_subject(subject); + if (this.channel.type !== 'static') { + this.basic_composer.toggle(false); + } + this.extended_composer.toggle(true); + this.thread.scroll_to({id: message_id, duration: 200, only_if_necessary: true}); + this.extended_composer.focus('body'); + }, + + unselect_message: function() { + this.basic_composer.toggle(this.channel.type !== 'static' && !this.channel.mass_mailing); + this.extended_composer.toggle(this.channel.type !== 'static' && this.channel.mass_mailing); + if (!config.device.touch) { + var composer = this.channel.mass_mailing ? this.extended_composer : this.basic_composer; + composer.focus(); + } + this.$el.removeClass('o_mail_selection_mode'); + this.thread.unselect(); + this.selected_message = null; + }, + + render_sidebar: function () { + var self = this; + var $sidebar = $(QWeb.render("mail.chat.Sidebar", { + active_channel_id: this.channel ? this.channel.id: undefined, + channels: chat_manager.get_channels(), + needaction_counter: chat_manager.get_needaction_counter(), + })); + this.$(".o_mail_chat_sidebar").html($sidebar.contents()); + + this.$('.o_mail_add_channel[data-type=public]').find("input").autocomplete({ + source: function(request, response) { + self.last_search_val = _.escape(request.term); + self.do_search_channel(self.last_search_val).done(function(result){ + result.push({ + 'label': _.str.sprintf(''+_t("Create %s")+'', '"#'+self.last_search_val+'"'), + 'value': '_create', + }); + response(result); + }); + }, + select: function(event, ui) { + if (self.last_search_val) { + if (ui.item.value === '_create') { + chat_manager.create_channel(self.last_search_val, "public"); + } else { + chat_manager.join_channel(ui.item.id); + } + } + }, + focus: function(event) { + event.preventDefault(); + }, + html: true, + }); + + this.$('.o_mail_add_channel[data-type=dm]').find("input").autocomplete({ + source: function(request, response) { + self.last_search_val = _.escape(request.term); + chat_manager.search_partner(self.last_search_val).done(response); + }, + select: function(event, ui) { + var partner_id = ui.item.id; + chat_manager.create_channel(partner_id, "dm"); + }, + focus: function(event) { + event.preventDefault(); + }, + html: true, + }); + + this.$('.o_mail_add_channel[data-type=private]').find("input").on('keyup', this, function (event) { + var name = _.escape($(event.target).val()); + if(event.which === $.ui.keyCode.ENTER && name) { + chat_manager.create_channel(name, "private"); + } + }); + }, + + render_snackbar: function (template, context, timeout) { + if (this.$snackbar) { + this.$snackbar.remove(); + } + timeout = timeout || 20000; + this.$snackbar = $(QWeb.render(template, context)); + this.$('.o_mail_chat_content').append(this.$snackbar); + // Hide snackbar after [timeout] milliseconds (by default, 20s) + var $snackbar = this.$snackbar; + setTimeout(function() { $snackbar.fadeOut(); }, timeout); + }, + + do_search_channel: function(search_val){ + var Channel = new Model("mail.channel"); + return Channel.call('channel_search_to_join', [search_val]).then(function(result){ + var values = []; + _.each(result, function(channel){ + var escaped_name = _.escape(channel.name); + values.push(_.extend(channel, { + 'value': escaped_name, + 'label': escaped_name, + })); + }); + return values; + }); + }, + + set_channel: function (channel) { + var self = this; + // Store scroll position of previous channel + if (this.channel) { + this.channels_scrolltop[this.channel.id] = this.thread.get_scrolltop(); + } + var new_channel_scrolltop = this.channels_scrolltop[channel.id]; + + this.channel = channel; + this.messages_separator_position = undefined; // reset value on channel change + this.unread_counter = this.channel.unread_counter; + this.last_seen_message_id = this.channel.last_seen_message_id; + this.clear_needactions_def = $.Deferred(); + if (this.$snackbar) { + this.$snackbar.remove(); + } + + this.action.context.active_id = channel.id; + this.action.context.active_ids = [channel.id]; + + return this.fetch_and_render_thread().then(function () { + // Mark channel's messages as read and clear needactions + if (channel.type !== 'static') { + // Display snackbar if needactions have been cleared + if (channel.needaction_counter > 0) { + self.render_snackbar('mail.chat.UndoSnackbar', { + nb_needactions: channel.needaction_counter, + }); + } + chat_manager.mark_channel_as_seen(channel); + self.clear_needactions_def = chat_manager.mark_all_as_read(channel); + } + + // Update control panel + self.set("title", '#' + channel.name); + // Hide 'invite', 'unsubscribe' and 'settings' buttons in static channels and DM + self.$buttons + .find('.o_mail_chat_button_invite, .o_mail_chat_button_unsubscribe, .o_mail_chat_button_settings') + .toggle(channel.type !== "dm" && channel.type !== 'static'); + self.$buttons + .find('.o_mail_chat_button_mark_read, .o_mail_chat_button_new_message') + .toggle(channel.id === "channel_inbox"); + self.$buttons + .find('.o_mail_chat_button_new_message_sent') + .toggle(channel.id === "channel_sent"); + self.$buttons + .find('.o_mail_chat_button_unstar_all') + .toggle(channel.id === "channel_starred"); + + self.$('.o_mail_chat_channel_item') + .removeClass('o_active') + .filter('[data-channel-id=' + channel.id + ']') + .removeClass('o_unread_message') + .addClass('o_active'); + + var $new_messages_separator = self.$('.o_thread_new_messages_separator'); + if ($new_messages_separator.length) { + self.thread.$el.scrollTo($new_messages_separator); + } else { + self.thread.scroll_to({offset: new_channel_scrolltop}); + } + + // Update control panel before focusing the composer, otherwise focus is on the searchview + self.update_cp(); + if (config.device.size_class === config.device.SIZES.XS) { + self.$('.o_mail_chat_sidebar').hide(); + } + + // Display and focus the adequate composer, and unselect possibly selected message + // to prevent sending messages as reply to that message + self.unselect_message(); + + self.action_manager.do_push_state({ + action: self.action.id, + active_id: self.channel.id, + }); + }); + }, + unsubscribe_from_channel: function (channel) { + var self = this; + chat_manager + .unsubscribe(channel) + .then(this.render_sidebar.bind(this)) + .then(this.set_channel.bind(this, chat_manager.get_channel("channel_inbox"))) + .then(function () { + if (_.contains(['public', 'private'], channel.type)) { + var msg = _.str.sprintf(_t('You unsubscribed from %s.'), channel.name); + self.do_notify(_t("Unsubscribed"), msg); + } + delete self.channels_scrolltop[channel.id]; + }); + }, + + get_thread_rendering_options: function (messages) { + // Compute position of the 'New messages' separator, only once when joining + // a channel to keep it in the thread when new messages arrive + if (_.isUndefined(this.messages_separator_position)) { + if (!this.unread_counter) { + this.messages_separator_position = false; // no unread message -> don't display separator + } else { + var msg = chat_manager.get_last_seen_message(this.channel); + this.messages_separator_position = msg ? msg.id : 'top'; + } + } + return { + channel_id: this.channel.id, + display_load_more: !chat_manager.all_history_loaded(this.channel, this.domain), + display_needactions: this.channel.display_needactions, + messages_separator_position: this.messages_separator_position, + squash_close_messages: this.channel.type !== 'static' && !this.channel.mass_mailing, + display_empty_channel: !messages.length && !this.domain.length, + display_no_match: !messages.length && this.domain.length, + display_subject: this.channel.mass_mailing || this.channel.id === "channel_inbox", + display_reply_icon: true, + }; + }, + + fetch_and_render_thread: function () { + var self = this; + return chat_manager.get_messages({channel_id: this.channel.id, domain: this.domain}).then(function(result) { + self.thread.render(result, self.get_thread_rendering_options(result)); + self.update_button_status(result.length === 0); + }); + }, + + update_button_status: function (disabled) { + if (this.channel.id === "channel_inbox") { + this.$buttons + .find('.o_mail_chat_button_mark_read') + .toggleClass('disabled', disabled); + } + if (this.channel.id === "channel_starred") { + this.$buttons + .find('.o_mail_chat_button_unstar_all') + .toggleClass('disabled', disabled); + } + }, + + load_more_messages: function () { + var self = this; + var oldest_msg_id = this.$('.o_thread_message').first().data('messageId'); + var oldest_msg_selector = '.o_thread_message[data-message-id="' + oldest_msg_id + '"]'; + var offset = -framework.getPosition(document.querySelector(oldest_msg_selector)).top; + return chat_manager + .get_messages({channel_id: this.channel.id, domain: this.domain, load_more: true}) + .then(function(result) { + if (self.messages_separator_position === 'top') { + self.messages_separator_position = undefined; // reset value to re-compute separator position + } + self.thread.render(result, self.get_thread_rendering_options(result)); + offset += framework.getPosition(document.querySelector(oldest_msg_selector)).top; + self.thread.scroll_to({offset: offset}); + }); + }, + + update_cp: function () { + this.update_control_panel({ + breadcrumbs: this.action_manager.get_breadcrumbs(), + cp_content: { + $buttons: this.$buttons, + $searchview: this.searchview.$el, + $searchview_buttons: this.$searchview_buttons, + }, + searchview: this.searchview, + }); + }, + + do_show: function () { + this._super.apply(this, arguments); + this.update_cp(); + this.action_manager.do_push_state({ + action: this.action.id, + active_id: this.channel.id, + }); + }, + + on_search: function (domains) { + var result = pyeval.sync_eval_domains_and_contexts({ + domains: domains + }); + + this.domain = result.domain; + this.fetch_and_render_thread(); + }, + + on_post_message: function (message) { + var self = this; + var options = this.selected_message ? {} : {channel_id: this.channel.id}; + if (this.selected_message) { + message.subtype = 'mail.mt_comment'; + message.subtype_id = false; + message.message_type = 'comment'; + message.content_subtype = 'html'; + + options.model = this.selected_message.model; + options.res_id = this.selected_message.res_id; + } + chat_manager + .post_message(message, options) + .then(function() { + if (self.selected_message) { + self.render_snackbar('mail.chat.MessageSentSnackbar', {record_name: self.selected_message.record_name}, 5000); + self.unselect_message(); + } else { + self.thread.scroll_to(); + } + }) + .fail(function () { + // todo: display notification + }); + }, + on_new_message: function (message) { + var self = this; + if (_.contains(message.channel_ids, this.channel.id)) { + if (this.channel.type !== 'static' && this.thread.is_at_bottom()) { + chat_manager.mark_channel_as_seen(this.channel); + } + + var should_scroll = this.thread.is_at_bottom(); + this.fetch_and_render_thread().then(function () { + if (should_scroll) { + self.thread.scroll_to({id: message.id}); + } + }); + } + // Re-render sidebar to indicate that there is a new message in the corresponding channels + this.render_sidebar(); + // Dump scroll position of channels in which the new message arrived + this.channels_scrolltop = _.omit(this.channels_scrolltop, message.channel_ids); + }, + on_update_message: function (message) { + var self = this; + var current_channel_id = this.channel.id; + if ((current_channel_id === "channel_starred" && !message.is_starred) || + (current_channel_id === "channel_inbox" && !message.is_needaction)) { + chat_manager.get_messages({channel_id: this.channel.id, domain: this.domain}).then(function (messages) { + var options = self.get_thread_rendering_options(messages); + self.thread.remove_message_and_render(message.id, messages, options).then(function () { + self.update_button_status(messages.length === 0); + }); + }); + } else if (_.contains(message.channel_ids, current_channel_id)) { + this.fetch_and_render_thread(); + } + }, + on_new_channel: function (channel) { + this.render_sidebar(); + if (channel.autoswitch) { + this.set_channel(channel); + } + }, + on_composer_input_focused: function () { + var suggestions = chat_manager.get_mention_partner_suggestions(this.channel); + var composer = this.channel.mass_mailing ? this.extended_composer : this.basic_composer; + composer.mention_set_prefetched_partners(suggestions); + }, + + on_click_button_invite: function () { + var title = _.str.sprintf(_t('Invite people to #%s'), this.channel.name); + new PartnerInviteDialog(this, title, this.channel.id).open(); + }, + + on_click_button_unsubscribe: function () { + this.unsubscribe_from_channel(this.channel); + }, + on_click_button_settings: function() { + this.do_action({ + type: 'ir.actions.act_window', + res_model: "mail.channel", + res_id: this.channel.id, + views: [[false, 'form']], + target: 'current' + }); + }, + on_click_new_message: function () { + this.do_action({ + type: 'ir.actions.act_window', + res_model: 'mail.compose.message', + view_mode: 'form', + view_type: 'form', + views: [[false, 'form']], + target: 'new', + context: "{'default_no_auto_thread': False, 'active_model': 'mail.message'}", + }); + }, + destroy: function() { + this.$buttons.off().destroy(); + this._super.apply(this, arguments); + }, +}); + + +core.action_registry.add('mail.chat.instant_messaging', ChatAction); + +}); diff --git a/sent_mails/static/src/xml/client_action_sent_mails.xml b/sent_mails/static/src/xml/client_action_sent_mails.xml new file mode 100644 index 000000000..52e0f414c --- /dev/null +++ b/sent_mails/static/src/xml/client_action_sent_mails.xml @@ -0,0 +1,19 @@ + + + + + +
+ Sent +
+
+
+ + + + + + + +
\ No newline at end of file diff --git a/sent_mails/views/get_sent_mails.xml b/sent_mails/views/get_sent_mails.xml new file mode 100644 index 000000000..2fc3967f4 --- /dev/null +++ b/sent_mails/views/get_sent_mails.xml @@ -0,0 +1,27 @@ + + + + + +