From 4af96310721237d3640bbd162ff9ea84e627028d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9rome=20Maes?= Date: Mon, 1 Jun 2015 16:37:07 +0200 Subject: [PATCH 01/38] [MOV] mail, im_chat : move files and code from im_chat to mail and bus - im_chat.session will be replaced by mail.channel - im_chat.shortode is renamed into mail.shortcode - im_chat.presence is moved to bus module - js and controller code is moved from im_chat to mail module This commit only move files, and modify manifests, bundles, ... The code will be adapt in the next commits. --- addons/bus/controllers/main.py | 3 + addons/bus/models/__init__.py | 2 + addons/bus/models/bus_presence.py | 87 +++++++++++ addons/{im_chat => bus}/models/res_users.py | 0 addons/bus/security/ir.model.access.csv | 4 +- addons/bus/static/src/js/bus.js | 16 ++ addons/mail/__openerp__.py | 3 + addons/mail/controllers/__init__.py | 2 + .../main.py => mail/controllers/bus.py} | 0 .../data/mail_shortcode_data.xml} | 0 addons/mail/models/__init__.py | 3 +- addons/mail/models/mail_channel.py | 138 ++++++++++++++++++ addons/mail/models/mail_shortcode.py | 48 ++++++ .../static/src/audio/purr.mp3 | Bin .../static/src/audio/purr.ogg | Bin .../static/src/audio/ting.mp3 | Bin .../static/src/audio/ting.ogg | Bin .../static/src/css/mail_chat_backend.css} | 0 .../static/src/css/mail_chat_common.css} | 0 .../static/src/img/smiley}/avatar.jpg | Bin .../static/src/img/smiley}/beer.png | Bin .../static/src/img/smiley}/boar.png | Bin .../static/src/img/smiley}/cat.png | Bin .../static/src/img/smiley}/confused.png | Bin .../static/src/img/smiley}/cookie.png | Bin .../static/src/img/smiley}/ghost.png | Bin .../static/src/img/smiley}/green.png | Bin .../static/src/img/smiley}/heart.png | Bin .../static/src/img/smiley}/laugh.png | Bin .../static/src/img/smiley}/man.png | Bin .../src/img/smiley}/man_with_turban.png | Bin .../static/src/img/smiley}/musti.png | Bin .../static/src/img/smiley}/neutral.png | Bin .../static/src/img/smiley}/open_mouth.png | Bin .../static/src/img/smiley}/pinky.png | Bin .../static/src/img/smiley}/poop.png | Bin .../static/src/img/smiley}/postal_horn.png | Bin .../static/src/img/smiley}/sad.png | Bin .../static/src/img/smiley}/scream.png | Bin .../static/src/img/smiley}/smile.png | Bin .../src/img/smiley}/stuck_out_tongue.png | Bin .../static/src/img/smiley}/thumbsdown.png | Bin .../static/src/img/smiley}/thumbsup.png | Bin .../static/src/img/smiley}/trollface.png | Bin .../static/src/img/smiley}/wink.png | Bin .../static/src/img/smiley}/yellow.png | Bin .../static/src/img/smiley}/yum.png | Bin .../static/src/js/mail_chat_backend.js} | 0 .../static/src/js/mail_chat_common.js} | 0 .../static/src/xml/mail_chat_common.xml} | 0 .../views/mail_shortcode_views.xml} | 0 addons/mail/views/mail_templates.xml | 5 + openerp/tools/mail.py | 20 ++- 53 files changed, 328 insertions(+), 3 deletions(-) create mode 100644 addons/bus/models/bus_presence.py rename addons/{im_chat => bus}/models/res_users.py (100%) rename addons/{im_chat/controllers/main.py => mail/controllers/bus.py} (100%) rename addons/{im_chat/data/im_chat_session_data.xml => mail/data/mail_shortcode_data.xml} (100%) create mode 100644 addons/mail/models/mail_shortcode.py rename addons/{im_chat => mail}/static/src/audio/purr.mp3 (100%) rename addons/{im_chat => mail}/static/src/audio/purr.ogg (100%) rename addons/{im_chat => mail}/static/src/audio/ting.mp3 (100%) rename addons/{im_chat => mail}/static/src/audio/ting.ogg (100%) rename addons/{im_chat/static/src/css/im_chat_backend.css => mail/static/src/css/mail_chat_backend.css} (100%) rename addons/{im_chat/static/src/css/im_chat_common.css => mail/static/src/css/mail_chat_common.css} (100%) rename addons/{im_chat/static/src/img => mail/static/src/img/smiley}/avatar.jpg (100%) rename addons/{im_chat/static/src/img => mail/static/src/img/smiley}/beer.png (100%) rename addons/{im_chat/static/src/img => mail/static/src/img/smiley}/boar.png (100%) rename addons/{im_chat/static/src/img => mail/static/src/img/smiley}/cat.png (100%) rename addons/{im_chat/static/src/img => mail/static/src/img/smiley}/confused.png (100%) rename addons/{im_chat/static/src/img => mail/static/src/img/smiley}/cookie.png (100%) rename addons/{im_chat/static/src/img => mail/static/src/img/smiley}/ghost.png (100%) rename addons/{im_chat/static/src/img => mail/static/src/img/smiley}/green.png (100%) rename addons/{im_chat/static/src/img => mail/static/src/img/smiley}/heart.png (100%) rename addons/{im_chat/static/src/img => mail/static/src/img/smiley}/laugh.png (100%) rename addons/{im_chat/static/src/img => mail/static/src/img/smiley}/man.png (100%) rename addons/{im_chat/static/src/img => mail/static/src/img/smiley}/man_with_turban.png (100%) rename addons/{im_chat/static/src/img => mail/static/src/img/smiley}/musti.png (100%) rename addons/{im_chat/static/src/img => mail/static/src/img/smiley}/neutral.png (100%) rename addons/{im_chat/static/src/img => mail/static/src/img/smiley}/open_mouth.png (100%) rename addons/{im_chat/static/src/img => mail/static/src/img/smiley}/pinky.png (100%) rename addons/{im_chat/static/src/img => mail/static/src/img/smiley}/poop.png (100%) rename addons/{im_chat/static/src/img => mail/static/src/img/smiley}/postal_horn.png (100%) rename addons/{im_chat/static/src/img => mail/static/src/img/smiley}/sad.png (100%) rename addons/{im_chat/static/src/img => mail/static/src/img/smiley}/scream.png (100%) rename addons/{im_chat/static/src/img => mail/static/src/img/smiley}/smile.png (100%) rename addons/{im_chat/static/src/img => mail/static/src/img/smiley}/stuck_out_tongue.png (100%) rename addons/{im_chat/static/src/img => mail/static/src/img/smiley}/thumbsdown.png (100%) rename addons/{im_chat/static/src/img => mail/static/src/img/smiley}/thumbsup.png (100%) rename addons/{im_chat/static/src/img => mail/static/src/img/smiley}/trollface.png (100%) rename addons/{im_chat/static/src/img => mail/static/src/img/smiley}/wink.png (100%) rename addons/{im_chat/static/src/img => mail/static/src/img/smiley}/yellow.png (100%) rename addons/{im_chat/static/src/img => mail/static/src/img/smiley}/yum.png (100%) rename addons/{im_chat/static/src/js/im_chat_backend.js => mail/static/src/js/mail_chat_backend.js} (100%) rename addons/{im_chat/static/src/js/im_chat_common.js => mail/static/src/js/mail_chat_common.js} (100%) rename addons/{im_chat/static/src/xml/im_chat.xml => mail/static/src/xml/mail_chat_common.xml} (100%) rename addons/{im_chat/views/im_chat_session_views.xml => mail/views/mail_shortcode_views.xml} (100%) diff --git a/addons/bus/controllers/main.py b/addons/bus/controllers/main.py index ee9902ec983..3da386e80b2 100644 --- a/addons/bus/controllers/main.py +++ b/addons/bus/controllers/main.py @@ -20,6 +20,9 @@ class BusController(openerp.http.Controller): # override to add channels def _poll(self, dbname, channels, last, options): + # update the user presence + if request.session.uid and 'im_presence' in options: + request.env['bus.presence'].update(options.get('im_presence')) request.cr.close() request._cr = None return dispatch.poll(dbname, channels, last) diff --git a/addons/bus/models/__init__.py b/addons/bus/models/__init__.py index c1c365d98c0..208eb0c6c81 100644 --- a/addons/bus/models/__init__.py +++ b/addons/bus/models/__init__.py @@ -1,2 +1,4 @@ # -*- coding: utf-8 -*- import bus +import bus_presence +import res_users diff --git a/addons/bus/models/bus_presence.py b/addons/bus/models/bus_presence.py new file mode 100644 index 00000000000..d89cfc9a104 --- /dev/null +++ b/addons/bus/models/bus_presence.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +import datetime +import random +import time + +from openerp import api, fields, models +from openerp import tools +from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT + +from openerp.addons.bus.models.bus import TIMEOUT + +DISCONNECTION_TIMER = TIMEOUT + 5 +AWAY_TIMER = 600 # 10 minutes + + +class ImChatPresence(models.Model): + """ User Chat Presence + Its status is 'online', 'away' or 'offline'. This model should be a one2one, but is not + attached to res_users to avoid database concurrence errors. Since the 'update' method is executed + at each poll, if the user have multiple opened tabs, concurrence errors can happend, but are 'muted-logged'. + """ + + _name = 'im_chat.presence' + _description = 'User Chat Presence' + + _sql_constraints = [('im_chat_user_status_unique', 'unique(user_id)', 'A user can only have one IM status.')] + + user_id = fields.Many2one('res.users', 'Users', required=True, index=True, ondelete='cascade') + last_poll = fields.Datetime('Last Poll', default=lambda self: fields.Datetime.now()) + last_presence = fields.Datetime('Last Presence', default=lambda self: fields.Datetime.now()) + status = fields.Selection([('online', 'Online'), ('away', 'Away'), ('offline', 'Offline')], 'IM Status', default='offline') + + + @api.model + def update(self, user_presence=True): + """ Register the given presence of the current user, and trigger a im_status change if necessary. + The status will not be written or sent if not necessary. + :param user_presence : True, if the user (self._uid) is still detected using its browser. + :type user_presence : boolean + """ + presence = self.search([('user_id', '=', self._uid)], limit=1) + # set the default values + send_notification = True + values = { + 'last_poll': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT), + 'status' : presence and presence.status or 'offline' + } + # update the user or a create a new one + if not presence: # create a new presence for the user + values['status'] = 'online' + values['user_id'] = self._uid + self.create(values) + else: # write the user presence if necessary + if user_presence: + values['last_presence'] = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT) + values['status'] = 'online' + else: + threshold = datetime.datetime.now() - datetime.timedelta(seconds=AWAY_TIMER) + if datetime.datetime.strptime(presence.last_presence, DEFAULT_SERVER_DATETIME_FORMAT) < threshold: + values['status'] = 'away' + send_notification = presence.status != values['status'] + # write only if the last_poll is passed TIMEOUT, or if the status has changed + delta = datetime.datetime.utcnow() - datetime.datetime.strptime(presence.last_poll, DEFAULT_SERVER_DATETIME_FORMAT) + if delta > datetime.timedelta(seconds=TIMEOUT) or send_notification: + # Hide transaction serialization errors, which can be ignored, the presence update is not essential + with tools.mute_logger('openerp.sql_db'): + presence.write(values) + # avoid TransactionRollbackError + self.env.cr.commit() # TODO : check if still necessary + # notify if the status has changed + if send_notification: # TODO : add user_id to the channel tuple to allow using user_watch in controller presence + self.env['bus.bus'].sendone((self._cr.dbname, 'im_chat.presence'), {'id': self._uid, 'im_status': values['status']}) + # gc : disconnect the users having a too old last_poll. 1 on 100 chance to do it. + if random.random() < 0.01: + self.check_users_disconnection() + return True + + @api.model + def check_users_disconnection(self): + """ Disconnect the users having a too old last_poll """ + limit_date = (datetime.datetime.utcnow() - datetime.timedelta(0, DISCONNECTION_TIMER)).strftime(DEFAULT_SERVER_DATETIME_FORMAT) + presences = self.search([('last_poll', '<', limit_date), ('status', '!=', 'offline')]) + presences.write({'status': 'offline'}) + notifications = [] + for presence in presences: + notifications.append([(self._cr.dbname, 'im_chat.presence'), {'id': presence.user_id.id, 'im_status': presence.status}]) + self.env['bus.bus'].sendmany(notifications) diff --git a/addons/im_chat/models/res_users.py b/addons/bus/models/res_users.py similarity index 100% rename from addons/im_chat/models/res_users.py rename to addons/bus/models/res_users.py diff --git a/addons/bus/security/ir.model.access.csv b/addons/bus/security/ir.model.access.csv index 8a679b74f81..4a626a0eb5a 100644 --- a/addons/bus/security/ir.model.access.csv +++ b/addons/bus/security/ir.model.access.csv @@ -1,2 +1,4 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_bus_bus,bus.bus public,model_bus_bus,,0,0,0,0 \ No newline at end of file +access_bus_bus,bus.bus public,model_bus_bus,,0,0,0,0 +access_bus_presence,bus.presence,model_bus_presence,base.group_user,1,1,1,1 +access_bus_presence_portal,bus.presence,model_bus_presence,base.group_portal,1,1,1,1 diff --git a/addons/bus/static/src/js/bus.js b/addons/bus/static/src/js/bus.js index b0756132012..3c828668308 100644 --- a/addons/bus/static/src/js/bus.js +++ b/addons/bus/static/src/js/bus.js @@ -9,12 +9,21 @@ bus.ERROR_DELAY = 10000; bus.Bus = Widget.extend({ init: function(){ + var self = this; this._super(); this.options = {}; this.activated = false; this.channels = []; this.last = 0; this.stop = false; + + // bus presence + this.set("window_focus", true); + this.on("change:window_focus", self, function(e) { + self.options.im_presence = self.get("window_focus"); + }); + $(window).on("focus", _.bind(this.window_focus, this)); + $(window).on("blur", _.bind(this.window_blur, this)); }, start_polling: function(){ if(!this.activated){ @@ -57,6 +66,13 @@ bus.Bus = Widget.extend({ delete_channel: function(channel){ this.channels = _.without(this.channels, channel); }, + // bus presence : window focus/unfocus + window_focus: function() { + this.set("window_focus", true); + }, + window_blur: function() { + this.set("window_focus", false); + }, }); // singleton diff --git a/addons/mail/__openerp__.py b/addons/mail/__openerp__.py index e1f124c77c5..1e9149dc4fb 100644 --- a/addons/mail/__openerp__.py +++ b/addons/mail/__openerp__.py @@ -36,9 +36,11 @@ Main Features 'views/mail_followers_views.xml', 'views/mail_thread_views.xml', 'views/mail_channel_views.xml', + 'views/mail_shortcode_views.xml', 'views/res_config_views.xml', 'data/mail_data.xml', 'data/mail_channel_data.xml', + 'data/mail_shortcode.xml', 'security/mail_security.xml', 'security/ir.model.access.csv', 'views/mail_alias_views.xml', @@ -58,6 +60,7 @@ Main Features 'application': True, 'qweb': [ 'static/src/xml/mail.xml', + 'static/src/xml/mail_chat_common.xml', 'static/src/xml/mail_followers.xml', 'static/src/xml/announcement.xml', ], diff --git a/addons/mail/controllers/__init__.py b/addons/mail/controllers/__init__.py index 8ee9bae18d9..c59e37b30c8 100644 --- a/addons/mail/controllers/__init__.py +++ b/addons/mail/controllers/__init__.py @@ -1 +1,3 @@ +# -*- coding: utf-8 -* import main +import bus diff --git a/addons/im_chat/controllers/main.py b/addons/mail/controllers/bus.py similarity index 100% rename from addons/im_chat/controllers/main.py rename to addons/mail/controllers/bus.py diff --git a/addons/im_chat/data/im_chat_session_data.xml b/addons/mail/data/mail_shortcode_data.xml similarity index 100% rename from addons/im_chat/data/im_chat_session_data.xml rename to addons/mail/data/mail_shortcode_data.xml diff --git a/addons/mail/models/__init__.py b/addons/mail/models/__init__.py index ca72296a6fb..d24e5295cf0 100644 --- a/addons/mail/models/__init__.py +++ b/addons/mail/models/__init__.py @@ -9,9 +9,10 @@ import mail_mail import mail_thread import mail_channel import mail_template +import mail_shortcode import res_partner import res_users import res_config import update import ir_actions -import html2text \ No newline at end of file +import html2text diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py index f118133ddf9..635a4999cce 100644 --- a/addons/mail/models/mail_channel.py +++ b/addons/mail/models/mail_channel.py @@ -14,6 +14,8 @@ class ChannelPartner(models.Model): partner_id = fields.Many2one('res.partner', string='Recipient') channel_id = fields.Many2one('mail.channel', string='Channel') seen_message_id = fields.Many2one('mail.message', string='Last Seen') + state = fields.Selection([('open', 'Open'), ('folded', 'Folded'), ('closed', 'Closed')], string='Status', default='open') + seen_datetime = fields.Datetime('Last Seen Datetime') class Channel(models.Model): @@ -161,3 +163,139 @@ class Channel(models.Model): headers['X-Forge-To'] = list_to res['headers'] = repr(headers) return res + + + # -------------------------- + # Session Methods + # -------------------------- + @api.multi + def session_info(self): + """ Get the header of the current session + :returns the values of the session + :rtype : dict + """ + self.ensure_one() + info = { + 'uuid': self.uuid, + 'users': self.session_user_info(), + 'state': 'open', + } + # add user session state, if available and if user is logged + if self._uid: + domain = [('user_id', '=', self._uid), ('session_id', 'in', self.ids)] + session_state = self.env['im_chat.conversation_state'].search(domain, limit=1) + if session_state: + info['state'] = session_state.state + return info + + @api.multi + def session_user_info(self): + """ Get the user infos for all the identified user in the session + :returns a list of user infos + :rtype : list(dict) + """ + self.ensure_one() + return self.env['res.users'].browse(self.user_ids.ids).read(['id', 'name', 'im_status']) + + @api.model + def session_user_image(self, uuid, user_id): + """ Get the avatar of a user in the given session + :param uuid : the uuid of the session + :param user_id : the user identifier + """ + # get the session + image_b64 = False + if user_id: + session = self.env["im_chat.session"].search([('uuid', '=', uuid)], limit=1) + if session and session.sudo(user_id).is_in_session(): + image_b64 = self.env["res.users"].sudo().browse(user_id).image_small + # set default image if not found + if not image_b64: + image_b64 = 'R0lGODlhAQABAIABAP///wAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==' + return image_b64 + + @api.model + def session_get(self, user_to): + """ Get the canonical session between 2 users, create it if needed. + To reuse an old session, this one must be private, and contains only 2 users. + :param user_to : the identifier of the user + :returns a session header, or False if the user_to was False + :rtype : dict + """ + if user_to: + session = self.search([('user_ids', 'in', user_to), ('user_ids', 'in', [self._uid])], limit=1) + if not (session and len(session.user_ids) == 2 and session.is_private()): + session = self.create({'user_ids': [(6, 0, (user_to, self._uid))]}) + return session.session_info() + + # -------------------------- + # Utils Methods + # -------------------------- + @api.multi + def is_private(self): + """ Return true if the session is private between identified users (no external messages). + The only way to do that is to check if there is a message without author (from_id is False). + """ + self.ensure_one() + message_ids = self.env["im_chat.message"].search([('to_id', 'in', self.ids), ('from_id', '=', None)]) + return len(message_ids) == 0 + + @api.multi + def is_in_session(self): + """ Return True if the current user is in the user_ids of the session. False otherwise. + Note : the user need to be logged + """ + self.ensure_one() + user_id = self._uid + return user_id and user_id in [u.id for u in self.user_ids] + + @api.model + def update_state(self, uuid, state=None): + """ Update the fold_state of the given session. In order to syncronize web browser + tabs, the change will be broadcast to himself (the current user channel). + Note : the user need to be logged + :param status : the new status of the session for the current user. + """ + domain = [('user_id', '=', self._uid), ('session_id.uuid', '=', uuid)] + for session_state in self.env['im_chat.conversation_state'].search(domain): + if not state: + state = session_state.state + if session_state.state == 'open': + state = 'folded' + else: + state = 'open' + session_state.write({'state': state}) + self.env['bus.bus'].sendone((self._cr.dbname, 'im_chat.session', self._uid), session_state.session_id.session_info()) + + @api.model + def get_init_notifications(self): + """ Get unread messages and old messages received less than AWAY_TIMER + ago and the session_info for open or folded window + Note : the user need to be logged + """ + # get the message since the AWAY_TIMER + threshold = datetime.datetime.now() - datetime.timedelta(seconds=AWAY_TIMER) + threshold = threshold.strftime(DEFAULT_SERVER_DATETIME_FORMAT) + domain = [('to_id.user_ids', 'in', [self._uid]), ('create_date', '>', threshold)] + + # get the message since the last poll of the user + presence = self.env['im_chat.presence'].search([('user_id', '=', self._uid)], limit=1) + if presence: + domain.append(('create_date', '>', presence.last_poll)) + messages = self.search_read(domain, ['from_id', 'to_id', 'create_date', 'type', 'message'], order='id asc') + + # get the session of the messages and the not-closed ones + session_ids = [m['to_id'][0] for m in messages] + domain = [('user_id', '=', self._uid), '|', ('state', '!=', 'closed'), ('session_id', 'in', session_ids)] + session_states = self.env['im_chat.conversation_state'].search(domain) + # re-open the session where a message have been recieved recently + session_states.filtered(lambda r: r.state == 'closed').write({'state': 'folded'}) + + # create the notifications (session infos first, then messages) + notifications = [] + for state in session_states: + session_infos = state.session_id.session_info() + notifications.append([(self._cr.dbname, 'im_chat.session', self._uid), session_infos]) + for message in messages: + notifications.append([(self._cr.dbname, 'im_chat.session', self._uid), message]) + return notifications diff --git a/addons/mail/models/mail_shortcode.py b/addons/mail/models/mail_shortcode.py new file mode 100644 index 00000000000..42627e5abe5 --- /dev/null +++ b/addons/mail/models/mail_shortcode.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +import cgi +import re + +from openerp import api, fields, models + + +class ImChatShortcode(models.Model): + """ Shortcode + Canned Responses, allowing the user to defined shortcuts in its chat message. + These shortcode are globals and are available for every user. Smiley use this mecanism. + """ + + _name = 'im_chat.shortcode' + _description = 'Canned Response / Shortcode' + + source = fields.Char('Shortcut', required=True, index=True, help="The shortcut which must be replace in the Chat Messages") + substitution = fields.Char('Substitution', required=True, index=True, help="The excaped html code replacing the shortcut") + description = fields.Char('Description') + + @api.model + def create(self, values): + if values.get('substitution'): + values['substitution'] = self._sanitize_shorcode(values['substitution']) + return super(ImChatShortcode, self).create(values) + + @api.multi + def write(self, values): + if values.get('substitution'): + values['substitution'] = self._sanitize_shorcode(values['substitution']) + return super(ImChatShortcode, self).write(values) + + def _sanitize_shorcode(self, substitution): + """ Sanitize the shortcode substitution : + - HTML substitution : only allow the img tag (smiley) + - escape other substitutions to avoid XSS + """ + is_img_tag = re.match(r'''^$''', substitution, re.M|re.I) + if is_img_tag: + return substitution + return cgi.escape(substitution) + + @api.model + def replace_shortcode(self, message): + for shortcode in self.search([]): + regex = '(?:^|\s)(%s)(?:\s|$)' % re.escape(shortcode.source) + message = re.sub(regex, " " + shortcode.substitution + " ", message) + return message diff --git a/addons/im_chat/static/src/audio/purr.mp3 b/addons/mail/static/src/audio/purr.mp3 similarity index 100% rename from addons/im_chat/static/src/audio/purr.mp3 rename to addons/mail/static/src/audio/purr.mp3 diff --git a/addons/im_chat/static/src/audio/purr.ogg b/addons/mail/static/src/audio/purr.ogg similarity index 100% rename from addons/im_chat/static/src/audio/purr.ogg rename to addons/mail/static/src/audio/purr.ogg diff --git a/addons/im_chat/static/src/audio/ting.mp3 b/addons/mail/static/src/audio/ting.mp3 similarity index 100% rename from addons/im_chat/static/src/audio/ting.mp3 rename to addons/mail/static/src/audio/ting.mp3 diff --git a/addons/im_chat/static/src/audio/ting.ogg b/addons/mail/static/src/audio/ting.ogg similarity index 100% rename from addons/im_chat/static/src/audio/ting.ogg rename to addons/mail/static/src/audio/ting.ogg diff --git a/addons/im_chat/static/src/css/im_chat_backend.css b/addons/mail/static/src/css/mail_chat_backend.css similarity index 100% rename from addons/im_chat/static/src/css/im_chat_backend.css rename to addons/mail/static/src/css/mail_chat_backend.css diff --git a/addons/im_chat/static/src/css/im_chat_common.css b/addons/mail/static/src/css/mail_chat_common.css similarity index 100% rename from addons/im_chat/static/src/css/im_chat_common.css rename to addons/mail/static/src/css/mail_chat_common.css diff --git a/addons/im_chat/static/src/img/avatar.jpg b/addons/mail/static/src/img/smiley/avatar.jpg similarity index 100% rename from addons/im_chat/static/src/img/avatar.jpg rename to addons/mail/static/src/img/smiley/avatar.jpg diff --git a/addons/im_chat/static/src/img/beer.png b/addons/mail/static/src/img/smiley/beer.png similarity index 100% rename from addons/im_chat/static/src/img/beer.png rename to addons/mail/static/src/img/smiley/beer.png diff --git a/addons/im_chat/static/src/img/boar.png b/addons/mail/static/src/img/smiley/boar.png similarity index 100% rename from addons/im_chat/static/src/img/boar.png rename to addons/mail/static/src/img/smiley/boar.png diff --git a/addons/im_chat/static/src/img/cat.png b/addons/mail/static/src/img/smiley/cat.png similarity index 100% rename from addons/im_chat/static/src/img/cat.png rename to addons/mail/static/src/img/smiley/cat.png diff --git a/addons/im_chat/static/src/img/confused.png b/addons/mail/static/src/img/smiley/confused.png similarity index 100% rename from addons/im_chat/static/src/img/confused.png rename to addons/mail/static/src/img/smiley/confused.png diff --git a/addons/im_chat/static/src/img/cookie.png b/addons/mail/static/src/img/smiley/cookie.png similarity index 100% rename from addons/im_chat/static/src/img/cookie.png rename to addons/mail/static/src/img/smiley/cookie.png diff --git a/addons/im_chat/static/src/img/ghost.png b/addons/mail/static/src/img/smiley/ghost.png similarity index 100% rename from addons/im_chat/static/src/img/ghost.png rename to addons/mail/static/src/img/smiley/ghost.png diff --git a/addons/im_chat/static/src/img/green.png b/addons/mail/static/src/img/smiley/green.png similarity index 100% rename from addons/im_chat/static/src/img/green.png rename to addons/mail/static/src/img/smiley/green.png diff --git a/addons/im_chat/static/src/img/heart.png b/addons/mail/static/src/img/smiley/heart.png similarity index 100% rename from addons/im_chat/static/src/img/heart.png rename to addons/mail/static/src/img/smiley/heart.png diff --git a/addons/im_chat/static/src/img/laugh.png b/addons/mail/static/src/img/smiley/laugh.png similarity index 100% rename from addons/im_chat/static/src/img/laugh.png rename to addons/mail/static/src/img/smiley/laugh.png diff --git a/addons/im_chat/static/src/img/man.png b/addons/mail/static/src/img/smiley/man.png similarity index 100% rename from addons/im_chat/static/src/img/man.png rename to addons/mail/static/src/img/smiley/man.png diff --git a/addons/im_chat/static/src/img/man_with_turban.png b/addons/mail/static/src/img/smiley/man_with_turban.png similarity index 100% rename from addons/im_chat/static/src/img/man_with_turban.png rename to addons/mail/static/src/img/smiley/man_with_turban.png diff --git a/addons/im_chat/static/src/img/musti.png b/addons/mail/static/src/img/smiley/musti.png similarity index 100% rename from addons/im_chat/static/src/img/musti.png rename to addons/mail/static/src/img/smiley/musti.png diff --git a/addons/im_chat/static/src/img/neutral.png b/addons/mail/static/src/img/smiley/neutral.png similarity index 100% rename from addons/im_chat/static/src/img/neutral.png rename to addons/mail/static/src/img/smiley/neutral.png diff --git a/addons/im_chat/static/src/img/open_mouth.png b/addons/mail/static/src/img/smiley/open_mouth.png similarity index 100% rename from addons/im_chat/static/src/img/open_mouth.png rename to addons/mail/static/src/img/smiley/open_mouth.png diff --git a/addons/im_chat/static/src/img/pinky.png b/addons/mail/static/src/img/smiley/pinky.png similarity index 100% rename from addons/im_chat/static/src/img/pinky.png rename to addons/mail/static/src/img/smiley/pinky.png diff --git a/addons/im_chat/static/src/img/poop.png b/addons/mail/static/src/img/smiley/poop.png similarity index 100% rename from addons/im_chat/static/src/img/poop.png rename to addons/mail/static/src/img/smiley/poop.png diff --git a/addons/im_chat/static/src/img/postal_horn.png b/addons/mail/static/src/img/smiley/postal_horn.png similarity index 100% rename from addons/im_chat/static/src/img/postal_horn.png rename to addons/mail/static/src/img/smiley/postal_horn.png diff --git a/addons/im_chat/static/src/img/sad.png b/addons/mail/static/src/img/smiley/sad.png similarity index 100% rename from addons/im_chat/static/src/img/sad.png rename to addons/mail/static/src/img/smiley/sad.png diff --git a/addons/im_chat/static/src/img/scream.png b/addons/mail/static/src/img/smiley/scream.png similarity index 100% rename from addons/im_chat/static/src/img/scream.png rename to addons/mail/static/src/img/smiley/scream.png diff --git a/addons/im_chat/static/src/img/smile.png b/addons/mail/static/src/img/smiley/smile.png similarity index 100% rename from addons/im_chat/static/src/img/smile.png rename to addons/mail/static/src/img/smiley/smile.png diff --git a/addons/im_chat/static/src/img/stuck_out_tongue.png b/addons/mail/static/src/img/smiley/stuck_out_tongue.png similarity index 100% rename from addons/im_chat/static/src/img/stuck_out_tongue.png rename to addons/mail/static/src/img/smiley/stuck_out_tongue.png diff --git a/addons/im_chat/static/src/img/thumbsdown.png b/addons/mail/static/src/img/smiley/thumbsdown.png similarity index 100% rename from addons/im_chat/static/src/img/thumbsdown.png rename to addons/mail/static/src/img/smiley/thumbsdown.png diff --git a/addons/im_chat/static/src/img/thumbsup.png b/addons/mail/static/src/img/smiley/thumbsup.png similarity index 100% rename from addons/im_chat/static/src/img/thumbsup.png rename to addons/mail/static/src/img/smiley/thumbsup.png diff --git a/addons/im_chat/static/src/img/trollface.png b/addons/mail/static/src/img/smiley/trollface.png similarity index 100% rename from addons/im_chat/static/src/img/trollface.png rename to addons/mail/static/src/img/smiley/trollface.png diff --git a/addons/im_chat/static/src/img/wink.png b/addons/mail/static/src/img/smiley/wink.png similarity index 100% rename from addons/im_chat/static/src/img/wink.png rename to addons/mail/static/src/img/smiley/wink.png diff --git a/addons/im_chat/static/src/img/yellow.png b/addons/mail/static/src/img/smiley/yellow.png similarity index 100% rename from addons/im_chat/static/src/img/yellow.png rename to addons/mail/static/src/img/smiley/yellow.png diff --git a/addons/im_chat/static/src/img/yum.png b/addons/mail/static/src/img/smiley/yum.png similarity index 100% rename from addons/im_chat/static/src/img/yum.png rename to addons/mail/static/src/img/smiley/yum.png diff --git a/addons/im_chat/static/src/js/im_chat_backend.js b/addons/mail/static/src/js/mail_chat_backend.js similarity index 100% rename from addons/im_chat/static/src/js/im_chat_backend.js rename to addons/mail/static/src/js/mail_chat_backend.js diff --git a/addons/im_chat/static/src/js/im_chat_common.js b/addons/mail/static/src/js/mail_chat_common.js similarity index 100% rename from addons/im_chat/static/src/js/im_chat_common.js rename to addons/mail/static/src/js/mail_chat_common.js diff --git a/addons/im_chat/static/src/xml/im_chat.xml b/addons/mail/static/src/xml/mail_chat_common.xml similarity index 100% rename from addons/im_chat/static/src/xml/im_chat.xml rename to addons/mail/static/src/xml/mail_chat_common.xml diff --git a/addons/im_chat/views/im_chat_session_views.xml b/addons/mail/views/mail_shortcode_views.xml similarity index 100% rename from addons/im_chat/views/im_chat_session_views.xml rename to addons/mail/views/mail_shortcode_views.xml diff --git a/addons/mail/views/mail_templates.xml b/addons/mail/views/mail_templates.xml index 7472cdd39ff..8fa17f867d7 100644 --- a/addons/mail/views/mail_templates.xml +++ b/addons/mail/views/mail_templates.xml @@ -11,6 +11,11 @@ + + + + + diff --git a/openerp/tools/mail.py b/openerp/tools/mail.py index 3c22262e14c..f358044bd08 100644 --- a/openerp/tools/mail.py +++ b/openerp/tools/mail.py @@ -514,6 +514,24 @@ def html2plaintext(html, body_id=None, encoding='utf-8'): return html +def html_escape_keep_url(self, message): + """ Escape the message and transform the url into clickable link + :param string message: the message to escape url and transform them into clickable link + :returns the escaped message + :rtype : string + """ + safe_message = "" + first = 0 + last = 0 + for m in re.finditer('(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?', message): + last = m.start() + safe_message += cgi.escape(message[first:last]) + safe_message += '%s' % (cgi.escape(m.group(0)), m.group(0)) + first = m.end() + last = m.end() + safe_message += cgi.escape(message[last:]) + return safe_message + def plaintext2html(text, container_tag=False): """ Convert plaintext into html. Content of the text is escaped to manage html entities, using cgi.escape(). @@ -587,7 +605,7 @@ def append_content_to_html(html, content, plaintext=True, preserve=False, contai #---------------------------------------------------------- # matches any email in a body of text -email_re = re.compile(r"""([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6})""", re.VERBOSE) +email_re = re.compile(r"""([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6})""", re.VERBOSE) # matches a string containing only one email single_email_re = re.compile(r"""^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$""", re.VERBOSE) From a9a78ee9b6a499c29b284e1612951cc20249502f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9rome=20Maes?= Date: Mon, 1 Jun 2015 16:48:41 +0200 Subject: [PATCH 02/38] [REM] im_chat : im_chat module is deleted since its code is moved to mail and bus modules --- addons/im_chat/__init__.py | 3 - addons/im_chat/__openerp__.py | 28 -- addons/im_chat/controllers/__init__.py | 2 - addons/im_chat/i18n/bg.po | 197 -------- addons/im_chat/i18n/de.po | 198 -------- addons/im_chat/i18n/es.po | 197 -------- addons/im_chat/i18n/hr.po | 197 -------- addons/im_chat/i18n/hu.po | 197 -------- addons/im_chat/i18n/im_chat.pot | 339 ------------- addons/im_chat/i18n/mk.po | 197 -------- addons/im_chat/i18n/nl.po | 197 -------- addons/im_chat/i18n/pl.po | 198 -------- addons/im_chat/i18n/pt.po | 197 -------- addons/im_chat/i18n/pt_BR.po | 198 -------- addons/im_chat/i18n/ro.po | 197 -------- addons/im_chat/i18n/tr.po | 197 -------- addons/im_chat/models/__init__.py | 3 - addons/im_chat/models/im_chat_session.py | 462 ------------------ .../security/im_chat_session_security.xml | 37 -- addons/im_chat/security/ir.model.access.csv | 11 - addons/im_chat/static/description/icon.png | Bin 20425 -> 0 bytes .../views/im_chat_session_templates.xml | 13 - 22 files changed, 3265 deletions(-) delete mode 100644 addons/im_chat/__init__.py delete mode 100644 addons/im_chat/__openerp__.py delete mode 100644 addons/im_chat/controllers/__init__.py delete mode 100644 addons/im_chat/i18n/bg.po delete mode 100644 addons/im_chat/i18n/de.po delete mode 100644 addons/im_chat/i18n/es.po delete mode 100644 addons/im_chat/i18n/hr.po delete mode 100644 addons/im_chat/i18n/hu.po delete mode 100644 addons/im_chat/i18n/im_chat.pot delete mode 100644 addons/im_chat/i18n/mk.po delete mode 100644 addons/im_chat/i18n/nl.po delete mode 100644 addons/im_chat/i18n/pl.po delete mode 100644 addons/im_chat/i18n/pt.po delete mode 100644 addons/im_chat/i18n/pt_BR.po delete mode 100644 addons/im_chat/i18n/ro.po delete mode 100644 addons/im_chat/i18n/tr.po delete mode 100644 addons/im_chat/models/__init__.py delete mode 100644 addons/im_chat/models/im_chat_session.py delete mode 100644 addons/im_chat/security/im_chat_session_security.xml delete mode 100644 addons/im_chat/security/ir.model.access.csv delete mode 100644 addons/im_chat/static/description/icon.png delete mode 100644 addons/im_chat/views/im_chat_session_templates.xml diff --git a/addons/im_chat/__init__.py b/addons/im_chat/__init__.py deleted file mode 100644 index 14ce547a094..00000000000 --- a/addons/im_chat/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# -*- coding: utf-8 -*- -import models -import controllers diff --git a/addons/im_chat/__openerp__.py b/addons/im_chat/__openerp__.py deleted file mode 100644 index c7d615e447e..00000000000 --- a/addons/im_chat/__openerp__.py +++ /dev/null @@ -1,28 +0,0 @@ -# -*- coding: utf-8 -*- -{ - 'name' : 'Instant Messaging', - 'version': '1.0', - 'summary': 'Chat between users', - 'sequence': 115, - 'category': 'Tools', - 'complexity': 'easy', - 'website': 'https://www.odoo.com/page/live-chat', - 'description': - """ -Instant Messaging -================= -Allows users to chat with each other in real time. Find other users easily and -chat in real time. It support several chats in parallel. - """, - 'data': [ - 'security/ir.model.access.csv', - 'security/im_chat_session_security.xml', - 'views/im_chat_session_templates.xml', - 'views/im_chat_session_views.xml', - 'data/im_chat_session_data.xml', - ], - 'depends': ['base', 'web', 'bus'], - 'qweb': ['static/src/xml/*.xml'], - 'application': True, - 'installable': True, -} diff --git a/addons/im_chat/controllers/__init__.py b/addons/im_chat/controllers/__init__.py deleted file mode 100644 index 12d2d759f34..00000000000 --- a/addons/im_chat/controllers/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# -*- coding: utf-8 -* -import main diff --git a/addons/im_chat/i18n/bg.po b/addons/im_chat/i18n/bg.po deleted file mode 100644 index 41e4d66d85a..00000000000 --- a/addons/im_chat/i18n/bg.po +++ /dev/null @@ -1,197 +0,0 @@ -# Bulgarian translation for openobject-addons -# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014 -# This file is distributed under the same license as the openobject-addons package. -# FIRST AUTHOR , 2014. -# -msgid "" -msgstr "" -"Project-Id-Version: openobject-addons\n" -"Report-Msgid-Bugs-To: FULL NAME \n" -"POT-Creation-Date: 2014-09-23 16:27+0000\n" -"PO-Revision-Date: 2014-11-18 12:00+0000\n" -"Last-Translator: Dimitar Dimitrov \n" -"Language-Team: Bulgarian \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-11-19 06:43+0000\n" -"X-Generator: Launchpad (build 17252)\n" - -#. module: im_chat -#. openerp-web -#: code:addons/im_chat/static/src/js/im_chat.js:88 -#, python-format -msgid "%d Messages" -msgstr "%d Съобщения" - -#. module: im_chat -#: sql_constraint:im_chat.presence:0 -msgid "A user can only have one IM status." -msgstr "Един потребител може да има само един IM статус." - -#. module: im_chat -#: field:im_chat.message,from_id:0 -msgid "Author" -msgstr "Автор" - -#. module: im_chat -#: selection:im_chat.presence,status:0 -msgid "Away" -msgstr "Отсъстващ(а)" - -#. module: im_chat -#: selection:im_chat.conversation_state,state:0 -msgid "Closed" -msgstr "Приключен" - -#. module: im_chat -#: field:im_chat.message,create_date:0 -msgid "Create Date" -msgstr "Дата на създаване" - -#. module: im_chat -#: field:im_chat.conversation_state,create_uid:0 -#: field:im_chat.message,create_uid:0 -#: field:im_chat.presence,create_uid:0 -#: field:im_chat.session,create_uid:0 -msgid "Created by" -msgstr "Създадено от" - -#. module: im_chat -#: field:im_chat.conversation_state,create_date:0 -#: field:im_chat.presence,create_date:0 -#: field:im_chat.session,create_date:0 -msgid "Created on" -msgstr "Създадено на" - -#. module: im_chat -#: selection:im_chat.conversation_state,state:0 -msgid "Folded" -msgstr "" - -#. module: im_chat -#: field:im_chat.conversation_state,id:0 -#: field:im_chat.message,id:0 -#: field:im_chat.presence,id:0 -#: field:im_chat.session,id:0 -msgid "ID" -msgstr "" - -#. module: im_chat -#: field:im_chat.presence,status:0 -#: field:res.users,im_status:0 -msgid "IM Status" -msgstr "IM състояние" - -#. module: im_chat -#: field:im_chat.presence,last_poll:0 -msgid "Last Poll" -msgstr "" - -#. module: im_chat -#: field:im_chat.presence,last_presence:0 -msgid "Last Presence" -msgstr "Последно Присъствие" - -#. module: im_chat -#: field:im_chat.conversation_state,write_uid:0 -#: field:im_chat.message,write_uid:0 -#: field:im_chat.presence,write_uid:0 -#: field:im_chat.session,write_uid:0 -msgid "Last Updated by" -msgstr "Последно обновено от" - -#. module: im_chat -#: field:im_chat.conversation_state,write_date:0 -#: field:im_chat.message,write_date:0 -#: field:im_chat.presence,write_date:0 -#: field:im_chat.session,write_date:0 -msgid "Last Updated on" -msgstr "Последно обновено на" - -#. module: im_chat -#: field:im_chat.message,message:0 -#: selection:im_chat.message,type:0 -msgid "Message" -msgstr "Съобщение" - -#. module: im_chat -#: field:im_chat.session,message_ids:0 -msgid "Messages" -msgstr "Съобщения" - -#. module: im_chat -#: selection:im_chat.message,type:0 -msgid "Meta" -msgstr "" - -#. module: im_chat -#: selection:im_chat.presence,status:0 -msgid "Offline" -msgstr "Извън линия" - -#. module: im_chat -#: selection:im_chat.presence,status:0 -msgid "Online" -msgstr "На линия" - -#. module: im_chat -#: selection:im_chat.conversation_state,state:0 -msgid "Open" -msgstr "Отвoри" - -#. module: im_chat -#: field:im_chat.session,session_res_users_rel:0 -msgid "Relation Session Users" -msgstr "" - -#. module: im_chat -#. openerp-web -#: code:addons/im_chat/static/src/js/im_chat.js:18 -#, python-format -msgid "Say something..." -msgstr "Кажи нещо..." - -#. module: im_chat -#: field:im_chat.conversation_state,session_id:0 -msgid "Session" -msgstr "Сесия" - -#. module: im_chat -#: field:im_chat.message,to_id:0 -msgid "Session To" -msgstr "" - -#. module: im_chat -#: field:im_chat.session,user_ids:0 -msgid "Session Users" -msgstr "" - -#. module: im_chat -#: field:im_chat.message,type:0 -msgid "Type" -msgstr "" - -#. module: im_chat -#: field:im_chat.session,uuid:0 -msgid "UUID" -msgstr "" - -#. module: im_chat -#: field:im_chat.conversation_state,user_id:0 -#: field:im_chat.presence,user_id:0 -#: model:ir.model,name:im_chat.model_res_users -msgid "Users" -msgstr "Потребители" - -#. module: im_chat -#. openerp-web -#: code:addons/im_chat/static/src/js/im_chat.js:20 -#, python-format -msgid "Visitor" -msgstr "Посетител" - -#. module: im_chat -#: field:im_chat.conversation_state,state:0 -msgid "unknown" -msgstr "Неизвестен" diff --git a/addons/im_chat/i18n/de.po b/addons/im_chat/i18n/de.po deleted file mode 100644 index dde7b9c1578..00000000000 --- a/addons/im_chat/i18n/de.po +++ /dev/null @@ -1,198 +0,0 @@ -# German translation for openobject-addons -# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014 -# This file is distributed under the same license as the openobject-addons package. -# FIRST AUTHOR , 2014. -# -msgid "" -msgstr "" -"Project-Id-Version: openobject-addons\n" -"Report-Msgid-Bugs-To: FULL NAME \n" -"POT-Creation-Date: 2014-09-23 16:27+0000\n" -"PO-Revision-Date: 2014-09-18 13:08+0000\n" -"Last-Translator: Thorsten Vocks (OpenBig.org) \n" -"Language-Team: German \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-09-24 09:14+0000\n" -"X-Generator: Launchpad (build 17196)\n" - -#. module: im_chat -#. openerp-web -#: code:addons/im_chat/static/src/js/im_chat.js:88 -#, python-format -msgid "%d Messages" -msgstr "%d Mitteilungen" - -#. module: im_chat -#: sql_constraint:im_chat.presence:0 -msgid "A user can only have one IM status." -msgstr "Ein Benutzer kann nur einen eindeutigen Status haben." - -#. module: im_chat -#: field:im_chat.message,from_id:0 -msgid "Author" -msgstr "Verfasser" - -#. module: im_chat -#: selection:im_chat.presence,status:0 -msgid "Away" -msgstr "Abwesend" - -#. module: im_chat -#: selection:im_chat.conversation_state,state:0 -msgid "Closed" -msgstr "Beendet" - -#. module: im_chat -#: field:im_chat.message,create_date:0 -msgid "Create Date" -msgstr "Erstellt am" - -#. module: im_chat -#: field:im_chat.conversation_state,create_uid:0 -#: field:im_chat.message,create_uid:0 -#: field:im_chat.presence,create_uid:0 -#: field:im_chat.session,create_uid:0 -msgid "Created by" -msgstr "Erstellt durch" - -#. module: im_chat -#: field:im_chat.conversation_state,create_date:0 -#: field:im_chat.presence,create_date:0 -#: field:im_chat.session,create_date:0 -msgid "Created on" -msgstr "Erstellt am" - -#. module: im_chat -#: selection:im_chat.conversation_state,state:0 -msgid "Folded" -msgstr "Zusammen gefaltet" - -#. module: im_chat -#: field:im_chat.conversation_state,id:0 -#: field:im_chat.message,id:0 -#: field:im_chat.presence,id:0 -#: field:im_chat.session,id:0 -msgid "ID" -msgstr "ID" - -#. module: im_chat -#: field:im_chat.presence,status:0 -#: field:res.users,im_status:0 -msgid "IM Status" -msgstr "Sofortnachrichten-Status" - -#. module: im_chat -#: field:im_chat.presence,last_poll:0 -msgid "Last Poll" -msgstr "Letzte Abfrage" - -#. module: im_chat -#: field:im_chat.presence,last_presence:0 -msgid "Last Presence" -msgstr "Letzte Anwesenheit" - -#. module: im_chat -#: field:im_chat.conversation_state,write_uid:0 -#: field:im_chat.message,write_uid:0 -#: field:im_chat.presence,write_uid:0 -#: field:im_chat.session,write_uid:0 -msgid "Last Updated by" -msgstr "Zuletzt aktualisiert von" - -#. module: im_chat -#: field:im_chat.conversation_state,write_date:0 -#: field:im_chat.message,write_date:0 -#: field:im_chat.presence,write_date:0 -#: field:im_chat.session,write_date:0 -msgid "Last Updated on" -msgstr "Zuletzt aktualisiert am" - -#. module: im_chat -#: field:im_chat.message,message:0 -#: selection:im_chat.message,type:0 -msgid "Message" -msgstr "Nachricht" - -#. module: im_chat -#: field:im_chat.session,message_ids:0 -msgid "Messages" -msgstr "Mitteilungen" - -#. module: im_chat -#: selection:im_chat.message,type:0 -msgid "Meta" -msgstr "Meta" - -#. module: im_chat -#: selection:im_chat.presence,status:0 -msgid "Offline" -msgstr "Offline" - -#. module: im_chat -#: selection:im_chat.presence,status:0 -msgid "Online" -msgstr "Online" - -#. module: im_chat -#: selection:im_chat.conversation_state,state:0 -msgid "Open" -msgstr "Offen" - -#. module: im_chat -#: field:im_chat.session,session_res_users_rel:0 -msgid "Relation Session Users" -msgstr "Verbundene Teilnehmer" - -#. module: im_chat -#. openerp-web -#: code:addons/im_chat/static/src/js/im_chat.js:18 -#, python-format -msgid "Say something..." -msgstr "Sag etwas ..." - -#. module: im_chat -#: field:im_chat.conversation_state,session_id:0 -msgid "Session" -msgstr "Sitzung" - -#. module: im_chat -#: field:im_chat.message,to_id:0 -msgid "Session To" -msgstr "Sitzung mit" - -#. module: im_chat -#: field:im_chat.session,user_ids:0 -msgid "Session Users" -msgstr "Teilnehmer" - -#. module: im_chat -#: field:im_chat.message,type:0 -msgid "Type" -msgstr "Typ" - -#. module: im_chat -#: field:im_chat.session,uuid:0 -msgid "UUID" -msgstr "UUID" - -#. module: im_chat -#: field:im_chat.conversation_state,user_id:0 -#: field:im_chat.presence,user_id:0 -#: model:ir.model,name:im_chat.model_res_users -msgid "Users" -msgstr "Benutzer" - -#. module: im_chat -#. openerp-web -#: code:addons/im_chat/static/src/js/im_chat.js:20 -#, python-format -msgid "Visitor" -msgstr "Besucher" - -#. module: im_chat -#: field:im_chat.conversation_state,state:0 -msgid "unknown" -msgstr "unbekannt" diff --git a/addons/im_chat/i18n/es.po b/addons/im_chat/i18n/es.po deleted file mode 100644 index aebf79e2dcf..00000000000 --- a/addons/im_chat/i18n/es.po +++ /dev/null @@ -1,197 +0,0 @@ -# Spanish translation for openobject-addons -# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014 -# This file is distributed under the same license as the openobject-addons package. -# FIRST AUTHOR , 2014. -# -msgid "" -msgstr "" -"Project-Id-Version: openobject-addons\n" -"Report-Msgid-Bugs-To: FULL NAME \n" -"POT-Creation-Date: 2014-09-23 16:27+0000\n" -"PO-Revision-Date: 2014-08-18 14:41+0000\n" -"Last-Translator: Pedro Manuel Baeza \n" -"Language-Team: Spanish \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-09-24 09:14+0000\n" -"X-Generator: Launchpad (build 17196)\n" - -#. module: im_chat -#. openerp-web -#: code:addons/im_chat/static/src/js/im_chat.js:88 -#, python-format -msgid "%d Messages" -msgstr "%d mensajes" - -#. module: im_chat -#: sql_constraint:im_chat.presence:0 -msgid "A user can only have one IM status." -msgstr "Un usuario sólo puede tener un estado de charla." - -#. module: im_chat -#: field:im_chat.message,from_id:0 -msgid "Author" -msgstr "Autor" - -#. module: im_chat -#: selection:im_chat.presence,status:0 -msgid "Away" -msgstr "Ausente" - -#. module: im_chat -#: selection:im_chat.conversation_state,state:0 -msgid "Closed" -msgstr "Cerrado" - -#. module: im_chat -#: field:im_chat.message,create_date:0 -msgid "Create Date" -msgstr "Fecha de creación" - -#. module: im_chat -#: field:im_chat.conversation_state,create_uid:0 -#: field:im_chat.message,create_uid:0 -#: field:im_chat.presence,create_uid:0 -#: field:im_chat.session,create_uid:0 -msgid "Created by" -msgstr "Creado por" - -#. module: im_chat -#: field:im_chat.conversation_state,create_date:0 -#: field:im_chat.presence,create_date:0 -#: field:im_chat.session,create_date:0 -msgid "Created on" -msgstr "Creado en" - -#. module: im_chat -#: selection:im_chat.conversation_state,state:0 -msgid "Folded" -msgstr "Replegado" - -#. module: im_chat -#: field:im_chat.conversation_state,id:0 -#: field:im_chat.message,id:0 -#: field:im_chat.presence,id:0 -#: field:im_chat.session,id:0 -msgid "ID" -msgstr "ID" - -#. module: im_chat -#: field:im_chat.presence,status:0 -#: field:res.users,im_status:0 -msgid "IM Status" -msgstr "Estado del chat" - -#. module: im_chat -#: field:im_chat.presence,last_poll:0 -msgid "Last Poll" -msgstr "Última encuesta" - -#. module: im_chat -#: field:im_chat.presence,last_presence:0 -msgid "Last Presence" -msgstr "Última presencia" - -#. module: im_chat -#: field:im_chat.conversation_state,write_uid:0 -#: field:im_chat.message,write_uid:0 -#: field:im_chat.presence,write_uid:0 -#: field:im_chat.session,write_uid:0 -msgid "Last Updated by" -msgstr "Última actualización de" - -#. module: im_chat -#: field:im_chat.conversation_state,write_date:0 -#: field:im_chat.message,write_date:0 -#: field:im_chat.presence,write_date:0 -#: field:im_chat.session,write_date:0 -msgid "Last Updated on" -msgstr "Última actualización en" - -#. module: im_chat -#: field:im_chat.message,message:0 -#: selection:im_chat.message,type:0 -msgid "Message" -msgstr "Mensaje" - -#. module: im_chat -#: field:im_chat.session,message_ids:0 -msgid "Messages" -msgstr "Mensajes" - -#. module: im_chat -#: selection:im_chat.message,type:0 -msgid "Meta" -msgstr "Meta" - -#. module: im_chat -#: selection:im_chat.presence,status:0 -msgid "Offline" -msgstr "Desconectado" - -#. module: im_chat -#: selection:im_chat.presence,status:0 -msgid "Online" -msgstr "En línea" - -#. module: im_chat -#: selection:im_chat.conversation_state,state:0 -msgid "Open" -msgstr "Abrir" - -#. module: im_chat -#: field:im_chat.session,session_res_users_rel:0 -msgid "Relation Session Users" -msgstr "Usuarios de sesión relacionados" - -#. module: im_chat -#. openerp-web -#: code:addons/im_chat/static/src/js/im_chat.js:18 -#, python-format -msgid "Say something..." -msgstr "Diga algo..." - -#. module: im_chat -#: field:im_chat.conversation_state,session_id:0 -msgid "Session" -msgstr "Sesión" - -#. module: im_chat -#: field:im_chat.message,to_id:0 -msgid "Session To" -msgstr "Sesión a" - -#. module: im_chat -#: field:im_chat.session,user_ids:0 -msgid "Session Users" -msgstr "Usuarios de sesión" - -#. module: im_chat -#: field:im_chat.message,type:0 -msgid "Type" -msgstr "Tipo" - -#. module: im_chat -#: field:im_chat.session,uuid:0 -msgid "UUID" -msgstr "UUID" - -#. module: im_chat -#: field:im_chat.conversation_state,user_id:0 -#: field:im_chat.presence,user_id:0 -#: model:ir.model,name:im_chat.model_res_users -msgid "Users" -msgstr "Usuarios" - -#. module: im_chat -#. openerp-web -#: code:addons/im_chat/static/src/js/im_chat.js:20 -#, python-format -msgid "Visitor" -msgstr "Visitante" - -#. module: im_chat -#: field:im_chat.conversation_state,state:0 -msgid "unknown" -msgstr "desconocido" diff --git a/addons/im_chat/i18n/hr.po b/addons/im_chat/i18n/hr.po deleted file mode 100644 index c433b9480b6..00000000000 --- a/addons/im_chat/i18n/hr.po +++ /dev/null @@ -1,197 +0,0 @@ -# Croatian translation for openobject-addons -# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014 -# This file is distributed under the same license as the openobject-addons package. -# FIRST AUTHOR , 2014. -# -msgid "" -msgstr "" -"Project-Id-Version: openobject-addons\n" -"Report-Msgid-Bugs-To: FULL NAME \n" -"POT-Creation-Date: 2014-09-23 16:27+0000\n" -"PO-Revision-Date: 2014-11-15 12:35+0000\n" -"Last-Translator: Davor Bojkić \n" -"Language-Team: Croatian \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-11-16 06:19+0000\n" -"X-Generator: Launchpad (build 17241)\n" - -#. module: im_chat -#. openerp-web -#: code:addons/im_chat/static/src/js/im_chat.js:88 -#, python-format -msgid "%d Messages" -msgstr "%d poruka" - -#. module: im_chat -#: sql_constraint:im_chat.presence:0 -msgid "A user can only have one IM status." -msgstr "Korisnik može imati samo jedan IM status" - -#. module: im_chat -#: field:im_chat.message,from_id:0 -msgid "Author" -msgstr "Autor" - -#. module: im_chat -#: selection:im_chat.presence,status:0 -msgid "Away" -msgstr "Odsutan" - -#. module: im_chat -#: selection:im_chat.conversation_state,state:0 -msgid "Closed" -msgstr "Zatvoren" - -#. module: im_chat -#: field:im_chat.message,create_date:0 -msgid "Create Date" -msgstr "Datum kreiranja" - -#. module: im_chat -#: field:im_chat.conversation_state,create_uid:0 -#: field:im_chat.message,create_uid:0 -#: field:im_chat.presence,create_uid:0 -#: field:im_chat.session,create_uid:0 -msgid "Created by" -msgstr "Kreirao" - -#. module: im_chat -#: field:im_chat.conversation_state,create_date:0 -#: field:im_chat.presence,create_date:0 -#: field:im_chat.session,create_date:0 -msgid "Created on" -msgstr "Vrijeme kreiranja" - -#. module: im_chat -#: selection:im_chat.conversation_state,state:0 -msgid "Folded" -msgstr "Omotano" - -#. module: im_chat -#: field:im_chat.conversation_state,id:0 -#: field:im_chat.message,id:0 -#: field:im_chat.presence,id:0 -#: field:im_chat.session,id:0 -msgid "ID" -msgstr "ID" - -#. module: im_chat -#: field:im_chat.presence,status:0 -#: field:res.users,im_status:0 -msgid "IM Status" -msgstr "IM Status" - -#. module: im_chat -#: field:im_chat.presence,last_poll:0 -msgid "Last Poll" -msgstr "" - -#. module: im_chat -#: field:im_chat.presence,last_presence:0 -msgid "Last Presence" -msgstr "Zadnja prijava" - -#. module: im_chat -#: field:im_chat.conversation_state,write_uid:0 -#: field:im_chat.message,write_uid:0 -#: field:im_chat.presence,write_uid:0 -#: field:im_chat.session,write_uid:0 -msgid "Last Updated by" -msgstr "Promijenio" - -#. module: im_chat -#: field:im_chat.conversation_state,write_date:0 -#: field:im_chat.message,write_date:0 -#: field:im_chat.presence,write_date:0 -#: field:im_chat.session,write_date:0 -msgid "Last Updated on" -msgstr "Vrijeme promjene" - -#. module: im_chat -#: field:im_chat.message,message:0 -#: selection:im_chat.message,type:0 -msgid "Message" -msgstr "Poruka" - -#. module: im_chat -#: field:im_chat.session,message_ids:0 -msgid "Messages" -msgstr "Poruke" - -#. module: im_chat -#: selection:im_chat.message,type:0 -msgid "Meta" -msgstr "" - -#. module: im_chat -#: selection:im_chat.presence,status:0 -msgid "Offline" -msgstr "Odspojen" - -#. module: im_chat -#: selection:im_chat.presence,status:0 -msgid "Online" -msgstr "Online" - -#. module: im_chat -#: selection:im_chat.conversation_state,state:0 -msgid "Open" -msgstr "" - -#. module: im_chat -#: field:im_chat.session,session_res_users_rel:0 -msgid "Relation Session Users" -msgstr "" - -#. module: im_chat -#. openerp-web -#: code:addons/im_chat/static/src/js/im_chat.js:18 -#, python-format -msgid "Say something..." -msgstr "Reci nešto..." - -#. module: im_chat -#: field:im_chat.conversation_state,session_id:0 -msgid "Session" -msgstr "" - -#. module: im_chat -#: field:im_chat.message,to_id:0 -msgid "Session To" -msgstr "" - -#. module: im_chat -#: field:im_chat.session,user_ids:0 -msgid "Session Users" -msgstr "" - -#. module: im_chat -#: field:im_chat.message,type:0 -msgid "Type" -msgstr "" - -#. module: im_chat -#: field:im_chat.session,uuid:0 -msgid "UUID" -msgstr "" - -#. module: im_chat -#: field:im_chat.conversation_state,user_id:0 -#: field:im_chat.presence,user_id:0 -#: model:ir.model,name:im_chat.model_res_users -msgid "Users" -msgstr "" - -#. module: im_chat -#. openerp-web -#: code:addons/im_chat/static/src/js/im_chat.js:20 -#, python-format -msgid "Visitor" -msgstr "" - -#. module: im_chat -#: field:im_chat.conversation_state,state:0 -msgid "unknown" -msgstr "" diff --git a/addons/im_chat/i18n/hu.po b/addons/im_chat/i18n/hu.po deleted file mode 100644 index 86ea7355a4a..00000000000 --- a/addons/im_chat/i18n/hu.po +++ /dev/null @@ -1,197 +0,0 @@ -# Hungarian translation for openobject-addons -# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014 -# This file is distributed under the same license as the openobject-addons package. -# FIRST AUTHOR , 2014. -# -msgid "" -msgstr "" -"Project-Id-Version: openobject-addons\n" -"Report-Msgid-Bugs-To: FULL NAME \n" -"POT-Creation-Date: 2014-09-23 16:27+0000\n" -"PO-Revision-Date: 2014-10-28 07:51+0000\n" -"Last-Translator: krnkris \n" -"Language-Team: Hungarian \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-10-29 07:49+0000\n" -"X-Generator: Launchpad (build 17203)\n" - -#. module: im_chat -#. openerp-web -#: code:addons/im_chat/static/src/js/im_chat.js:88 -#, python-format -msgid "%d Messages" -msgstr "%d Üzenetek" - -#. module: im_chat -#: sql_constraint:im_chat.presence:0 -msgid "A user can only have one IM status." -msgstr "Egy felhasználónak csak egy üzenőküldési állapota lehet." - -#. module: im_chat -#: field:im_chat.message,from_id:0 -msgid "Author" -msgstr "Szerző" - -#. module: im_chat -#: selection:im_chat.presence,status:0 -msgid "Away" -msgstr "Távollét" - -#. module: im_chat -#: selection:im_chat.conversation_state,state:0 -msgid "Closed" -msgstr "Lezárt" - -#. module: im_chat -#: field:im_chat.message,create_date:0 -msgid "Create Date" -msgstr "Létrehozás dátuma" - -#. module: im_chat -#: field:im_chat.conversation_state,create_uid:0 -#: field:im_chat.message,create_uid:0 -#: field:im_chat.presence,create_uid:0 -#: field:im_chat.session,create_uid:0 -msgid "Created by" -msgstr "Készítette" - -#. module: im_chat -#: field:im_chat.conversation_state,create_date:0 -#: field:im_chat.presence,create_date:0 -#: field:im_chat.session,create_date:0 -msgid "Created on" -msgstr "Létrehozás dátuma" - -#. module: im_chat -#: selection:im_chat.conversation_state,state:0 -msgid "Folded" -msgstr "Behajtott" - -#. module: im_chat -#: field:im_chat.conversation_state,id:0 -#: field:im_chat.message,id:0 -#: field:im_chat.presence,id:0 -#: field:im_chat.session,id:0 -msgid "ID" -msgstr "Azonosító ID" - -#. module: im_chat -#: field:im_chat.presence,status:0 -#: field:res.users,im_status:0 -msgid "IM Status" -msgstr "Üzenetküldő-állapot" - -#. module: im_chat -#: field:im_chat.presence,last_poll:0 -msgid "Last Poll" -msgstr "Utolsó szavazás" - -#. module: im_chat -#: field:im_chat.presence,last_presence:0 -msgid "Last Presence" -msgstr "Utolsó jelenlét" - -#. module: im_chat -#: field:im_chat.conversation_state,write_uid:0 -#: field:im_chat.message,write_uid:0 -#: field:im_chat.presence,write_uid:0 -#: field:im_chat.session,write_uid:0 -msgid "Last Updated by" -msgstr "Utoljára frissítve, által" - -#. module: im_chat -#: field:im_chat.conversation_state,write_date:0 -#: field:im_chat.message,write_date:0 -#: field:im_chat.presence,write_date:0 -#: field:im_chat.session,write_date:0 -msgid "Last Updated on" -msgstr "Utoljára frissítve ekkor" - -#. module: im_chat -#: field:im_chat.message,message:0 -#: selection:im_chat.message,type:0 -msgid "Message" -msgstr "Üzenet" - -#. module: im_chat -#: field:im_chat.session,message_ids:0 -msgid "Messages" -msgstr "Üzenetek" - -#. module: im_chat -#: selection:im_chat.message,type:0 -msgid "Meta" -msgstr "" - -#. module: im_chat -#: selection:im_chat.presence,status:0 -msgid "Offline" -msgstr "Nem elérhető" - -#. module: im_chat -#: selection:im_chat.presence,status:0 -msgid "Online" -msgstr "Elérhető" - -#. module: im_chat -#: selection:im_chat.conversation_state,state:0 -msgid "Open" -msgstr "Nyitott" - -#. module: im_chat -#: field:im_chat.session,session_res_users_rel:0 -msgid "Relation Session Users" -msgstr "Szakasz felhasználók környezete" - -#. module: im_chat -#. openerp-web -#: code:addons/im_chat/static/src/js/im_chat.js:18 -#, python-format -msgid "Say something..." -msgstr "Mondj valamit..." - -#. module: im_chat -#: field:im_chat.conversation_state,session_id:0 -msgid "Session" -msgstr "Munkafázis" - -#. module: im_chat -#: field:im_chat.message,to_id:0 -msgid "Session To" -msgstr "Munkafázis ehhez" - -#. module: im_chat -#: field:im_chat.session,user_ids:0 -msgid "Session Users" -msgstr "Munkafázis felhasználói" - -#. module: im_chat -#: field:im_chat.message,type:0 -msgid "Type" -msgstr "Típus" - -#. module: im_chat -#: field:im_chat.session,uuid:0 -msgid "UUID" -msgstr "UUID" - -#. module: im_chat -#: field:im_chat.conversation_state,user_id:0 -#: field:im_chat.presence,user_id:0 -#: model:ir.model,name:im_chat.model_res_users -msgid "Users" -msgstr "Felhasználók" - -#. module: im_chat -#. openerp-web -#: code:addons/im_chat/static/src/js/im_chat.js:20 -#, python-format -msgid "Visitor" -msgstr "Látogató" - -#. module: im_chat -#: field:im_chat.conversation_state,state:0 -msgid "unknown" -msgstr "ismeretlen" diff --git a/addons/im_chat/i18n/im_chat.pot b/addons/im_chat/i18n/im_chat.pot deleted file mode 100644 index f19f26fa466..00000000000 --- a/addons/im_chat/i18n/im_chat.pot +++ /dev/null @@ -1,339 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * im_chat -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server 9.0\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-08-25 09:00+0000\n" -"PO-Revision-Date: 2015-08-25 09:00+0000\n" -"Last-Translator: <>\n" -"Language-Team: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Plural-Forms: \n" - -#. module: im_chat -#. openerp-web -#: code:addons/im_chat/static/src/js/im_chat_backend.js:89 -#: code:addons/im_chat/static/src/js/im_chat_common.js:92 -#, python-format -msgid "%d Messages" -msgstr "" - -#. module: im_chat -#: code:addons/im_chat/models/im_chat_session.py:180 -#, python-format -msgid "%s joined the conversation." -msgstr "" - -#. module: im_chat -#: code:addons/im_chat/models/im_chat_session.py:202 -#, python-format -msgid "%s left the conversation." -msgstr "" - -#. module: im_chat -#: model:ir.actions.act_window,help:im_chat.im_chat_shortcode_action -msgid "A shortcode is a keyboard shortcut. For instance, you type #gm and it will be transformed into \"Good Morning\"." -msgstr "" - -#. module: im_chat -#: sql_constraint:im_chat.presence:0 -msgid "A user can only have one IM status." -msgstr "" - -#. module: im_chat -#: model:ir.model.fields,field_description:im_chat.field_im_chat_message_from_id -msgid "Author" -msgstr "" - -#. module: im_chat -#: selection:im_chat.presence,status:0 -msgid "Away" -msgstr "" - -#. module: im_chat -#: model:ir.model,name:im_chat.model_im_chat_shortcode -msgid "Canned Response / Shortcode" -msgstr "" - -#. module: im_chat -#: model:ir.model,name:im_chat.model_im_chat_session -msgid "Chat Conversation" -msgstr "" - -#. module: im_chat -#: model:ir.model,name:im_chat.model_im_chat_conversation_state -msgid "Chat Conversation State" -msgstr "" - -#. module: im_chat -#: model:ir.model,name:im_chat.model_im_chat_message -msgid "Chat Message" -msgstr "" - -#. module: im_chat -#: model:ir.actions.act_window,name:im_chat.im_chat_shortcode_action -msgid "Chat Shortcode" -msgstr "" - -#. module: im_chat -#: model:ir.actions.act_window,help:im_chat.im_chat_shortcode_action -msgid "Click to define a new chat shortcode." -msgstr "" - -#. module: im_chat -#: selection:im_chat.conversation_state,state:0 -msgid "Closed" -msgstr "" - -#. module: im_chat -#: model:ir.model.fields,field_description:im_chat.field_im_chat_session_create_date -msgid "Create Date" -msgstr "" - -#. module: im_chat -#: model:ir.model.fields,field_description:im_chat.field_im_chat_message_create_date -msgid "Create date" -msgstr "" - -#. module: im_chat -#: model:ir.model.fields,field_description:im_chat.field_im_chat_message_create_uid -#: model:ir.model.fields,field_description:im_chat.field_im_chat_presence_create_uid -#: model:ir.model.fields,field_description:im_chat.field_im_chat_session_create_uid -#: model:ir.model.fields,field_description:im_chat.field_im_chat_session_res_users_rel_create_uid -#: model:ir.model.fields,field_description:im_chat.field_im_chat_shortcode_create_uid -msgid "Created by" -msgstr "" - -#. module: im_chat -#: model:ir.model.fields,field_description:im_chat.field_im_chat_presence_create_date -#: model:ir.model.fields,field_description:im_chat.field_im_chat_session_res_users_rel_create_date -#: model:ir.model.fields,field_description:im_chat.field_im_chat_shortcode_create_date -msgid "Created on" -msgstr "" - -#. module: im_chat -#: model:ir.model.fields,field_description:im_chat.field_im_chat_shortcode_description -msgid "Description" -msgstr "" - -#. module: im_chat -#: model:ir.model.fields,field_description:im_chat.field_im_chat_message_display_name -#: model:ir.model.fields,field_description:im_chat.field_im_chat_presence_display_name -#: model:ir.model.fields,field_description:im_chat.field_im_chat_session_display_name -#: model:ir.model.fields,field_description:im_chat.field_im_chat_session_res_users_rel_display_name -#: model:ir.model.fields,field_description:im_chat.field_im_chat_shortcode_display_name -msgid "Display Name" -msgstr "" - -#. module: im_chat -#: selection:im_chat.conversation_state,state:0 -msgid "Folded" -msgstr "" - -#. module: im_chat -#: model:ir.model.fields,field_description:im_chat.field_im_chat_message_id -#: model:ir.model.fields,field_description:im_chat.field_im_chat_presence_id -#: model:ir.model.fields,field_description:im_chat.field_im_chat_session_id -#: model:ir.model.fields,field_description:im_chat.field_im_chat_session_res_users_rel_id -#: model:ir.model.fields,field_description:im_chat.field_im_chat_shortcode_id -msgid "ID" -msgstr "" - -#. module: im_chat -#: model:ir.model.fields,field_description:im_chat.field_im_chat_presence_status -#: model:ir.model.fields,field_description:im_chat.field_res_users_im_status -msgid "IM Status" -msgstr "" - -#. module: im_chat -#: model:ir.model.fields,field_description:im_chat.field_im_chat_message___last_update -#: model:ir.model.fields,field_description:im_chat.field_im_chat_presence___last_update -#: model:ir.model.fields,field_description:im_chat.field_im_chat_session___last_update -#: model:ir.model.fields,field_description:im_chat.field_im_chat_session_res_users_rel___last_update -#: model:ir.model.fields,field_description:im_chat.field_im_chat_shortcode___last_update -msgid "Last Modified on" -msgstr "" - -#. module: im_chat -#: model:ir.model.fields,field_description:im_chat.field_im_chat_presence_last_poll -msgid "Last Poll" -msgstr "" - -#. module: im_chat -#: model:ir.model.fields,field_description:im_chat.field_im_chat_presence_last_presence -msgid "Last Presence" -msgstr "" - -#. module: im_chat -#: model:ir.model.fields,field_description:im_chat.field_im_chat_message_write_uid -#: model:ir.model.fields,field_description:im_chat.field_im_chat_presence_write_uid -#: model:ir.model.fields,field_description:im_chat.field_im_chat_session_res_users_rel_write_uid -#: model:ir.model.fields,field_description:im_chat.field_im_chat_session_write_uid -#: model:ir.model.fields,field_description:im_chat.field_im_chat_shortcode_write_uid -msgid "Last Updated by" -msgstr "" - -#. module: im_chat -#: model:ir.model.fields,field_description:im_chat.field_im_chat_message_write_date -#: model:ir.model.fields,field_description:im_chat.field_im_chat_presence_write_date -#: model:ir.model.fields,field_description:im_chat.field_im_chat_session_res_users_rel_write_date -#: model:ir.model.fields,field_description:im_chat.field_im_chat_session_write_date -#: model:ir.model.fields,field_description:im_chat.field_im_chat_shortcode_write_date -msgid "Last Updated on" -msgstr "" - -#. module: im_chat -#: model:ir.model.fields,field_description:im_chat.field_im_chat_message_message -msgid "Message" -msgstr "" - -#. module: im_chat -#: model:ir.model.fields,field_description:im_chat.field_im_chat_message_type -msgid "Message type" -msgstr "" - -#. module: im_chat -#: model:ir.model.fields,field_description:im_chat.field_im_chat_session_message_ids -msgid "Messages" -msgstr "" - -#. module: im_chat -#: selection:im_chat.presence,status:0 -msgid "Offline" -msgstr "" - -#. module: im_chat -#: selection:im_chat.presence,status:0 -msgid "Online" -msgstr "" - -#. module: im_chat -#: selection:im_chat.conversation_state,state:0 -msgid "Open" -msgstr "" - -#. module: im_chat -#. openerp-web -#: code:addons/im_chat/static/src/js/im_chat_backend.js:47 -#, python-format -msgid "Quit discussion" -msgstr "" - -#. module: im_chat -#: model:ir.model.fields,field_description:im_chat.field_im_chat_session_session_res_users_rel -msgid "Relation Session Users" -msgstr "" - -#. module: im_chat -#. openerp-web -#: code:addons/im_chat/static/src/js/im_chat_common.js:24 -#, python-format -msgid "Say something..." -msgstr "" - -#. module: im_chat -#. openerp-web -#: code:addons/im_chat/static/src/xml/im_chat.xml:90 -#, python-format -msgid "Search users..." -msgstr "" - -#. module: im_chat -#: model:ir.model.fields,field_description:im_chat.field_im_chat_message_to_id -#: model:ir.model.fields,field_description:im_chat.field_im_chat_session_res_users_rel_session_id -msgid "Session" -msgstr "" - -#. module: im_chat -#: model:ir.model.fields,field_description:im_chat.field_im_chat_session_user_ids -msgid "Session Users" -msgstr "" - -#. module: im_chat -#. openerp-web -#: code:addons/im_chat/static/src/js/im_chat_backend.js:52 -#, python-format -msgid "Shortcode" -msgstr "" - -#. module: im_chat -#: model:ir.ui.view,arch_db:im_chat.im_chat_shortcode_view_form -#: model:ir.ui.view,arch_db:im_chat.im_chat_shortcode_view_tree -msgid "Shortcodes" -msgstr "" - -#. module: im_chat -#: model:ir.model.fields,field_description:im_chat.field_im_chat_shortcode_source -msgid "Shortcut" -msgstr "" - -#. module: im_chat -#. openerp-web -#: code:addons/im_chat/static/src/js/im_chat_backend.js:46 -#, python-format -msgid "Shortcuts" -msgstr "" - -#. module: im_chat -#: model:ir.model.fields,field_description:im_chat.field_im_chat_session_res_users_rel_state -msgid "Status" -msgstr "" - -#. module: im_chat -#: model:ir.model.fields,field_description:im_chat.field_im_chat_shortcode_substitution -msgid "Substitution" -msgstr "" - -#. module: im_chat -#: model:ir.model.fields,help:im_chat.field_im_chat_shortcode_substitution -msgid "The excaped html code replacing the shortcut" -msgstr "" - -#. module: im_chat -#: model:ir.model.fields,help:im_chat.field_im_chat_shortcode_source -msgid "The shortcut which must be replace in the Chat Messages" -msgstr "" - -#. module: im_chat -#: model:ir.model.fields,field_description:im_chat.field_im_chat_session_uuid -msgid "UUID" -msgstr "" - -#. module: im_chat -#: model:ir.model,name:im_chat.model_im_chat_presence -msgid "User Chat Presence" -msgstr "" - -#. module: im_chat -#: model:ir.model,name:im_chat.model_res_users -#: model:ir.model.fields,field_description:im_chat.field_im_chat_presence_user_id -#: model:ir.model.fields,field_description:im_chat.field_im_chat_session_res_users_rel_user_id -msgid "Users" -msgstr "" - -#. module: im_chat -#. openerp-web -#: code:addons/im_chat/static/src/js/im_chat_common.js:26 -#, python-format -msgid "Visitor" -msgstr "" - -#. module: im_chat -#. openerp-web -#: code:addons/im_chat/static/src/js/im_chat_backend.js:70 -#, python-format -msgid "Warning" -msgstr "" - -#. module: im_chat -#. openerp-web -#: code:addons/im_chat/static/src/js/im_chat_backend.js:70 -#, python-format -msgid "You are only 2 identified users. Just close the conversation to leave." -msgstr "" - diff --git a/addons/im_chat/i18n/mk.po b/addons/im_chat/i18n/mk.po deleted file mode 100644 index 9df8670027e..00000000000 --- a/addons/im_chat/i18n/mk.po +++ /dev/null @@ -1,197 +0,0 @@ -# Macedonian translation for openobject-addons -# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014 -# This file is distributed under the same license as the openobject-addons package. -# FIRST AUTHOR , 2014. -# -msgid "" -msgstr "" -"Project-Id-Version: openobject-addons\n" -"Report-Msgid-Bugs-To: FULL NAME \n" -"POT-Creation-Date: 2014-09-23 16:27+0000\n" -"PO-Revision-Date: 2014-10-18 10:21+0000\n" -"Last-Translator: Tome Barbov \n" -"Language-Team: Macedonian \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-10-19 07:12+0000\n" -"X-Generator: Launchpad (build 17196)\n" - -#. module: im_chat -#. openerp-web -#: code:addons/im_chat/static/src/js/im_chat.js:88 -#, python-format -msgid "%d Messages" -msgstr "%d Пораки" - -#. module: im_chat -#: sql_constraint:im_chat.presence:0 -msgid "A user can only have one IM status." -msgstr "Еден корисник може да има само еден IM статус." - -#. module: im_chat -#: field:im_chat.message,from_id:0 -msgid "Author" -msgstr "Автор" - -#. module: im_chat -#: selection:im_chat.presence,status:0 -msgid "Away" -msgstr "Отсутен" - -#. module: im_chat -#: selection:im_chat.conversation_state,state:0 -msgid "Closed" -msgstr "Затворено" - -#. module: im_chat -#: field:im_chat.message,create_date:0 -msgid "Create Date" -msgstr "Креирај датум" - -#. module: im_chat -#: field:im_chat.conversation_state,create_uid:0 -#: field:im_chat.message,create_uid:0 -#: field:im_chat.presence,create_uid:0 -#: field:im_chat.session,create_uid:0 -msgid "Created by" -msgstr "Креирано од" - -#. module: im_chat -#: field:im_chat.conversation_state,create_date:0 -#: field:im_chat.presence,create_date:0 -#: field:im_chat.session,create_date:0 -msgid "Created on" -msgstr "Креирано на" - -#. module: im_chat -#: selection:im_chat.conversation_state,state:0 -msgid "Folded" -msgstr "" - -#. module: im_chat -#: field:im_chat.conversation_state,id:0 -#: field:im_chat.message,id:0 -#: field:im_chat.presence,id:0 -#: field:im_chat.session,id:0 -msgid "ID" -msgstr "ID" - -#. module: im_chat -#: field:im_chat.presence,status:0 -#: field:res.users,im_status:0 -msgid "IM Status" -msgstr "IM Статус" - -#. module: im_chat -#: field:im_chat.presence,last_poll:0 -msgid "Last Poll" -msgstr "" - -#. module: im_chat -#: field:im_chat.presence,last_presence:0 -msgid "Last Presence" -msgstr "Последно присуство" - -#. module: im_chat -#: field:im_chat.conversation_state,write_uid:0 -#: field:im_chat.message,write_uid:0 -#: field:im_chat.presence,write_uid:0 -#: field:im_chat.session,write_uid:0 -msgid "Last Updated by" -msgstr "Последно ажурирање од" - -#. module: im_chat -#: field:im_chat.conversation_state,write_date:0 -#: field:im_chat.message,write_date:0 -#: field:im_chat.presence,write_date:0 -#: field:im_chat.session,write_date:0 -msgid "Last Updated on" -msgstr "Последно ажурирање на" - -#. module: im_chat -#: field:im_chat.message,message:0 -#: selection:im_chat.message,type:0 -msgid "Message" -msgstr "Порака" - -#. module: im_chat -#: field:im_chat.session,message_ids:0 -msgid "Messages" -msgstr "Пораки" - -#. module: im_chat -#: selection:im_chat.message,type:0 -msgid "Meta" -msgstr "Мета" - -#. module: im_chat -#: selection:im_chat.presence,status:0 -msgid "Offline" -msgstr "Исклучен" - -#. module: im_chat -#: selection:im_chat.presence,status:0 -msgid "Online" -msgstr "Вклучен" - -#. module: im_chat -#: selection:im_chat.conversation_state,state:0 -msgid "Open" -msgstr "Отвори" - -#. module: im_chat -#: field:im_chat.session,session_res_users_rel:0 -msgid "Relation Session Users" -msgstr "" - -#. module: im_chat -#. openerp-web -#: code:addons/im_chat/static/src/js/im_chat.js:18 -#, python-format -msgid "Say something..." -msgstr "Речете нешто..." - -#. module: im_chat -#: field:im_chat.conversation_state,session_id:0 -msgid "Session" -msgstr "Сесија" - -#. module: im_chat -#: field:im_chat.message,to_id:0 -msgid "Session To" -msgstr "Сесија до" - -#. module: im_chat -#: field:im_chat.session,user_ids:0 -msgid "Session Users" -msgstr "Корисници на сесија" - -#. module: im_chat -#: field:im_chat.message,type:0 -msgid "Type" -msgstr "Тип" - -#. module: im_chat -#: field:im_chat.session,uuid:0 -msgid "UUID" -msgstr "UUID" - -#. module: im_chat -#: field:im_chat.conversation_state,user_id:0 -#: field:im_chat.presence,user_id:0 -#: model:ir.model,name:im_chat.model_res_users -msgid "Users" -msgstr "Корисници" - -#. module: im_chat -#. openerp-web -#: code:addons/im_chat/static/src/js/im_chat.js:20 -#, python-format -msgid "Visitor" -msgstr "Посетител" - -#. module: im_chat -#: field:im_chat.conversation_state,state:0 -msgid "unknown" -msgstr "непознато" diff --git a/addons/im_chat/i18n/nl.po b/addons/im_chat/i18n/nl.po deleted file mode 100644 index fc48acfc54e..00000000000 --- a/addons/im_chat/i18n/nl.po +++ /dev/null @@ -1,197 +0,0 @@ -# Dutch translation for openobject-addons -# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014 -# This file is distributed under the same license as the openobject-addons package. -# FIRST AUTHOR , 2014. -# -msgid "" -msgstr "" -"Project-Id-Version: openobject-addons\n" -"Report-Msgid-Bugs-To: FULL NAME \n" -"POT-Creation-Date: 2014-09-23 16:27+0000\n" -"PO-Revision-Date: 2014-08-15 16:07+0000\n" -"Last-Translator: Mario Gielissen (Openworx) \n" -"Language-Team: Dutch \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-09-24 09:14+0000\n" -"X-Generator: Launchpad (build 17196)\n" - -#. module: im_chat -#. openerp-web -#: code:addons/im_chat/static/src/js/im_chat.js:88 -#, python-format -msgid "%d Messages" -msgstr "%d Berichten" - -#. module: im_chat -#: sql_constraint:im_chat.presence:0 -msgid "A user can only have one IM status." -msgstr "Een gebruiker kan maar een IM status hebben." - -#. module: im_chat -#: field:im_chat.message,from_id:0 -msgid "Author" -msgstr "Auteur" - -#. module: im_chat -#: selection:im_chat.presence,status:0 -msgid "Away" -msgstr "Afwezig" - -#. module: im_chat -#: selection:im_chat.conversation_state,state:0 -msgid "Closed" -msgstr "Gesloten" - -#. module: im_chat -#: field:im_chat.message,create_date:0 -msgid "Create Date" -msgstr "Aanmaakdatum" - -#. module: im_chat -#: field:im_chat.conversation_state,create_uid:0 -#: field:im_chat.message,create_uid:0 -#: field:im_chat.presence,create_uid:0 -#: field:im_chat.session,create_uid:0 -msgid "Created by" -msgstr "Aangemaakt door" - -#. module: im_chat -#: field:im_chat.conversation_state,create_date:0 -#: field:im_chat.presence,create_date:0 -#: field:im_chat.session,create_date:0 -msgid "Created on" -msgstr "Aangemaakt op" - -#. module: im_chat -#: selection:im_chat.conversation_state,state:0 -msgid "Folded" -msgstr "Gevouwen" - -#. module: im_chat -#: field:im_chat.conversation_state,id:0 -#: field:im_chat.message,id:0 -#: field:im_chat.presence,id:0 -#: field:im_chat.session,id:0 -msgid "ID" -msgstr "ID" - -#. module: im_chat -#: field:im_chat.presence,status:0 -#: field:res.users,im_status:0 -msgid "IM Status" -msgstr "IM status" - -#. module: im_chat -#: field:im_chat.presence,last_poll:0 -msgid "Last Poll" -msgstr "Laatste pol" - -#. module: im_chat -#: field:im_chat.presence,last_presence:0 -msgid "Last Presence" -msgstr "Laatst aanwezig" - -#. module: im_chat -#: field:im_chat.conversation_state,write_uid:0 -#: field:im_chat.message,write_uid:0 -#: field:im_chat.presence,write_uid:0 -#: field:im_chat.session,write_uid:0 -msgid "Last Updated by" -msgstr "Laatste bijgewerkt door" - -#. module: im_chat -#: field:im_chat.conversation_state,write_date:0 -#: field:im_chat.message,write_date:0 -#: field:im_chat.presence,write_date:0 -#: field:im_chat.session,write_date:0 -msgid "Last Updated on" -msgstr "Laatst bijgewerkt op" - -#. module: im_chat -#: field:im_chat.message,message:0 -#: selection:im_chat.message,type:0 -msgid "Message" -msgstr "Bericht" - -#. module: im_chat -#: field:im_chat.session,message_ids:0 -msgid "Messages" -msgstr "Berichten" - -#. module: im_chat -#: selection:im_chat.message,type:0 -msgid "Meta" -msgstr "Meta" - -#. module: im_chat -#: selection:im_chat.presence,status:0 -msgid "Offline" -msgstr "Offline" - -#. module: im_chat -#: selection:im_chat.presence,status:0 -msgid "Online" -msgstr "Online" - -#. module: im_chat -#: selection:im_chat.conversation_state,state:0 -msgid "Open" -msgstr "Open" - -#. module: im_chat -#: field:im_chat.session,session_res_users_rel:0 -msgid "Relation Session Users" -msgstr "Relatie gebruikers sessie" - -#. module: im_chat -#. openerp-web -#: code:addons/im_chat/static/src/js/im_chat.js:18 -#, python-format -msgid "Say something..." -msgstr "Zeg iets ..." - -#. module: im_chat -#: field:im_chat.conversation_state,session_id:0 -msgid "Session" -msgstr "Sessie" - -#. module: im_chat -#: field:im_chat.message,to_id:0 -msgid "Session To" -msgstr "Sessie naar" - -#. module: im_chat -#: field:im_chat.session,user_ids:0 -msgid "Session Users" -msgstr "Gebruikers sessie" - -#. module: im_chat -#: field:im_chat.message,type:0 -msgid "Type" -msgstr "Type" - -#. module: im_chat -#: field:im_chat.session,uuid:0 -msgid "UUID" -msgstr "UUID" - -#. module: im_chat -#: field:im_chat.conversation_state,user_id:0 -#: field:im_chat.presence,user_id:0 -#: model:ir.model,name:im_chat.model_res_users -msgid "Users" -msgstr "Gebruikers" - -#. module: im_chat -#. openerp-web -#: code:addons/im_chat/static/src/js/im_chat.js:20 -#, python-format -msgid "Visitor" -msgstr "Bezoeker" - -#. module: im_chat -#: field:im_chat.conversation_state,state:0 -msgid "unknown" -msgstr "onbekend" diff --git a/addons/im_chat/i18n/pl.po b/addons/im_chat/i18n/pl.po deleted file mode 100644 index 96c7c3e098a..00000000000 --- a/addons/im_chat/i18n/pl.po +++ /dev/null @@ -1,198 +0,0 @@ -# Polish translation for openobject-addons -# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014 -# This file is distributed under the same license as the openobject-addons package. -# FIRST AUTHOR , 2014. -# -msgid "" -msgstr "" -"Project-Id-Version: openobject-addons\n" -"Report-Msgid-Bugs-To: FULL NAME \n" -"POT-Creation-Date: 2014-09-23 16:27+0000\n" -"PO-Revision-Date: 2014-08-27 21:10+0000\n" -"Last-Translator: Monika Grzelak (OpenGLOBE.pl) " -"\n" -"Language-Team: Polish \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-09-24 09:14+0000\n" -"X-Generator: Launchpad (build 17196)\n" - -#. module: im_chat -#. openerp-web -#: code:addons/im_chat/static/src/js/im_chat.js:88 -#, python-format -msgid "%d Messages" -msgstr "%d Wiadomości" - -#. module: im_chat -#: sql_constraint:im_chat.presence:0 -msgid "A user can only have one IM status." -msgstr "Użytkownik może mieć tylko jeden status komunikatora." - -#. module: im_chat -#: field:im_chat.message,from_id:0 -msgid "Author" -msgstr "Autor" - -#. module: im_chat -#: selection:im_chat.presence,status:0 -msgid "Away" -msgstr "Zaraz wracam" - -#. module: im_chat -#: selection:im_chat.conversation_state,state:0 -msgid "Closed" -msgstr "Zamknięte" - -#. module: im_chat -#: field:im_chat.message,create_date:0 -msgid "Create Date" -msgstr "Data utworzenia" - -#. module: im_chat -#: field:im_chat.conversation_state,create_uid:0 -#: field:im_chat.message,create_uid:0 -#: field:im_chat.presence,create_uid:0 -#: field:im_chat.session,create_uid:0 -msgid "Created by" -msgstr "Utworzone przez" - -#. module: im_chat -#: field:im_chat.conversation_state,create_date:0 -#: field:im_chat.presence,create_date:0 -#: field:im_chat.session,create_date:0 -msgid "Created on" -msgstr "Data utworzenia" - -#. module: im_chat -#: selection:im_chat.conversation_state,state:0 -msgid "Folded" -msgstr "Zwinięte" - -#. module: im_chat -#: field:im_chat.conversation_state,id:0 -#: field:im_chat.message,id:0 -#: field:im_chat.presence,id:0 -#: field:im_chat.session,id:0 -msgid "ID" -msgstr "ID" - -#. module: im_chat -#: field:im_chat.presence,status:0 -#: field:res.users,im_status:0 -msgid "IM Status" -msgstr "Status komunikatora" - -#. module: im_chat -#: field:im_chat.presence,last_poll:0 -msgid "Last Poll" -msgstr "Ostatnia ankieta" - -#. module: im_chat -#: field:im_chat.presence,last_presence:0 -msgid "Last Presence" -msgstr "Ostatnia obecność" - -#. module: im_chat -#: field:im_chat.conversation_state,write_uid:0 -#: field:im_chat.message,write_uid:0 -#: field:im_chat.presence,write_uid:0 -#: field:im_chat.session,write_uid:0 -msgid "Last Updated by" -msgstr "Ostatnio modyfikowane przez" - -#. module: im_chat -#: field:im_chat.conversation_state,write_date:0 -#: field:im_chat.message,write_date:0 -#: field:im_chat.presence,write_date:0 -#: field:im_chat.session,write_date:0 -msgid "Last Updated on" -msgstr "Data ostatniej modyfikacji" - -#. module: im_chat -#: field:im_chat.message,message:0 -#: selection:im_chat.message,type:0 -msgid "Message" -msgstr "Wiadomość" - -#. module: im_chat -#: field:im_chat.session,message_ids:0 -msgid "Messages" -msgstr "Wiadomości" - -#. module: im_chat -#: selection:im_chat.message,type:0 -msgid "Meta" -msgstr "" - -#. module: im_chat -#: selection:im_chat.presence,status:0 -msgid "Offline" -msgstr "Niedostępny" - -#. module: im_chat -#: selection:im_chat.presence,status:0 -msgid "Online" -msgstr "Dostępny" - -#. module: im_chat -#: selection:im_chat.conversation_state,state:0 -msgid "Open" -msgstr "Otwórz" - -#. module: im_chat -#: field:im_chat.session,session_res_users_rel:0 -msgid "Relation Session Users" -msgstr "Uczestnicy sesji relacyjnej" - -#. module: im_chat -#. openerp-web -#: code:addons/im_chat/static/src/js/im_chat.js:18 -#, python-format -msgid "Say something..." -msgstr "Powiedz coś..." - -#. module: im_chat -#: field:im_chat.conversation_state,session_id:0 -msgid "Session" -msgstr "Sesja" - -#. module: im_chat -#: field:im_chat.message,to_id:0 -msgid "Session To" -msgstr "Sesja do" - -#. module: im_chat -#: field:im_chat.session,user_ids:0 -msgid "Session Users" -msgstr "Użytkownicy sesji" - -#. module: im_chat -#: field:im_chat.message,type:0 -msgid "Type" -msgstr "Typ" - -#. module: im_chat -#: field:im_chat.session,uuid:0 -msgid "UUID" -msgstr "" - -#. module: im_chat -#: field:im_chat.conversation_state,user_id:0 -#: field:im_chat.presence,user_id:0 -#: model:ir.model,name:im_chat.model_res_users -msgid "Users" -msgstr "Użytkownicy" - -#. module: im_chat -#. openerp-web -#: code:addons/im_chat/static/src/js/im_chat.js:20 -#, python-format -msgid "Visitor" -msgstr "Gość" - -#. module: im_chat -#: field:im_chat.conversation_state,state:0 -msgid "unknown" -msgstr "nieznane" diff --git a/addons/im_chat/i18n/pt.po b/addons/im_chat/i18n/pt.po deleted file mode 100644 index 614a9d49cbb..00000000000 --- a/addons/im_chat/i18n/pt.po +++ /dev/null @@ -1,197 +0,0 @@ -# Portuguese translation for openobject-addons -# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014 -# This file is distributed under the same license as the openobject-addons package. -# FIRST AUTHOR , 2014. -# -msgid "" -msgstr "" -"Project-Id-Version: openobject-addons\n" -"Report-Msgid-Bugs-To: FULL NAME \n" -"POT-Creation-Date: 2014-09-23 16:27+0000\n" -"PO-Revision-Date: 2014-09-17 17:42+0000\n" -"Last-Translator: Opencloud - PT \n" -"Language-Team: Portuguese \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-09-24 09:14+0000\n" -"X-Generator: Launchpad (build 17196)\n" - -#. module: im_chat -#. openerp-web -#: code:addons/im_chat/static/src/js/im_chat.js:88 -#, python-format -msgid "%d Messages" -msgstr "%d Mensagens" - -#. module: im_chat -#: sql_constraint:im_chat.presence:0 -msgid "A user can only have one IM status." -msgstr "Um utilizador pode apenas ter um estado IM." - -#. module: im_chat -#: field:im_chat.message,from_id:0 -msgid "Author" -msgstr "Autor" - -#. module: im_chat -#: selection:im_chat.presence,status:0 -msgid "Away" -msgstr "Ausente" - -#. module: im_chat -#: selection:im_chat.conversation_state,state:0 -msgid "Closed" -msgstr "Fechado" - -#. module: im_chat -#: field:im_chat.message,create_date:0 -msgid "Create Date" -msgstr "Data de Criação" - -#. module: im_chat -#: field:im_chat.conversation_state,create_uid:0 -#: field:im_chat.message,create_uid:0 -#: field:im_chat.presence,create_uid:0 -#: field:im_chat.session,create_uid:0 -msgid "Created by" -msgstr "Criado por" - -#. module: im_chat -#: field:im_chat.conversation_state,create_date:0 -#: field:im_chat.presence,create_date:0 -#: field:im_chat.session,create_date:0 -msgid "Created on" -msgstr "Criado em" - -#. module: im_chat -#: selection:im_chat.conversation_state,state:0 -msgid "Folded" -msgstr "Dobrado" - -#. module: im_chat -#: field:im_chat.conversation_state,id:0 -#: field:im_chat.message,id:0 -#: field:im_chat.presence,id:0 -#: field:im_chat.session,id:0 -msgid "ID" -msgstr "ID" - -#. module: im_chat -#: field:im_chat.presence,status:0 -#: field:res.users,im_status:0 -msgid "IM Status" -msgstr "Estado IM" - -#. module: im_chat -#: field:im_chat.presence,last_poll:0 -msgid "Last Poll" -msgstr "Última Votação" - -#. module: im_chat -#: field:im_chat.presence,last_presence:0 -msgid "Last Presence" -msgstr "Última Presença" - -#. module: im_chat -#: field:im_chat.conversation_state,write_uid:0 -#: field:im_chat.message,write_uid:0 -#: field:im_chat.presence,write_uid:0 -#: field:im_chat.session,write_uid:0 -msgid "Last Updated by" -msgstr "Última Actualização por" - -#. module: im_chat -#: field:im_chat.conversation_state,write_date:0 -#: field:im_chat.message,write_date:0 -#: field:im_chat.presence,write_date:0 -#: field:im_chat.session,write_date:0 -msgid "Last Updated on" -msgstr "Última Actualização em" - -#. module: im_chat -#: field:im_chat.message,message:0 -#: selection:im_chat.message,type:0 -msgid "Message" -msgstr "Mensagem" - -#. module: im_chat -#: field:im_chat.session,message_ids:0 -msgid "Messages" -msgstr "Mensagens" - -#. module: im_chat -#: selection:im_chat.message,type:0 -msgid "Meta" -msgstr "Meta" - -#. module: im_chat -#: selection:im_chat.presence,status:0 -msgid "Offline" -msgstr "Desligado" - -#. module: im_chat -#: selection:im_chat.presence,status:0 -msgid "Online" -msgstr "Ligado" - -#. module: im_chat -#: selection:im_chat.conversation_state,state:0 -msgid "Open" -msgstr "Abrir" - -#. module: im_chat -#: field:im_chat.session,session_res_users_rel:0 -msgid "Relation Session Users" -msgstr "Utilizadores da Sessão Relacionados" - -#. module: im_chat -#. openerp-web -#: code:addons/im_chat/static/src/js/im_chat.js:18 -#, python-format -msgid "Say something..." -msgstr "Diga algo..." - -#. module: im_chat -#: field:im_chat.conversation_state,session_id:0 -msgid "Session" -msgstr "Sessão" - -#. module: im_chat -#: field:im_chat.message,to_id:0 -msgid "Session To" -msgstr "Sessão a" - -#. module: im_chat -#: field:im_chat.session,user_ids:0 -msgid "Session Users" -msgstr "Utilizadores da Sessão" - -#. module: im_chat -#: field:im_chat.message,type:0 -msgid "Type" -msgstr "Tipo" - -#. module: im_chat -#: field:im_chat.session,uuid:0 -msgid "UUID" -msgstr "UUID" - -#. module: im_chat -#: field:im_chat.conversation_state,user_id:0 -#: field:im_chat.presence,user_id:0 -#: model:ir.model,name:im_chat.model_res_users -msgid "Users" -msgstr "Utilizadores" - -#. module: im_chat -#. openerp-web -#: code:addons/im_chat/static/src/js/im_chat.js:20 -#, python-format -msgid "Visitor" -msgstr "Visitante" - -#. module: im_chat -#: field:im_chat.conversation_state,state:0 -msgid "unknown" -msgstr "desconhecido" diff --git a/addons/im_chat/i18n/pt_BR.po b/addons/im_chat/i18n/pt_BR.po deleted file mode 100644 index cd0a1ff72b5..00000000000 --- a/addons/im_chat/i18n/pt_BR.po +++ /dev/null @@ -1,198 +0,0 @@ -# Brazilian Portuguese translation for openobject-addons -# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014 -# This file is distributed under the same license as the openobject-addons package. -# FIRST AUTHOR , 2014. -# -msgid "" -msgstr "" -"Project-Id-Version: openobject-addons\n" -"Report-Msgid-Bugs-To: FULL NAME \n" -"POT-Creation-Date: 2014-09-23 16:27+0000\n" -"PO-Revision-Date: 2014-09-06 19:08+0000\n" -"Last-Translator: Fábio Martinelli - http://zupy.com.br " -"\n" -"Language-Team: Brazilian Portuguese \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-09-24 09:14+0000\n" -"X-Generator: Launchpad (build 17196)\n" - -#. module: im_chat -#. openerp-web -#: code:addons/im_chat/static/src/js/im_chat.js:88 -#, python-format -msgid "%d Messages" -msgstr "%d Mensagens" - -#. module: im_chat -#: sql_constraint:im_chat.presence:0 -msgid "A user can only have one IM status." -msgstr "Um usuário pode ter apenas um status" - -#. module: im_chat -#: field:im_chat.message,from_id:0 -msgid "Author" -msgstr "Autor" - -#. module: im_chat -#: selection:im_chat.presence,status:0 -msgid "Away" -msgstr "Ausente" - -#. module: im_chat -#: selection:im_chat.conversation_state,state:0 -msgid "Closed" -msgstr "Fechado" - -#. module: im_chat -#: field:im_chat.message,create_date:0 -msgid "Create Date" -msgstr "Data de criação" - -#. module: im_chat -#: field:im_chat.conversation_state,create_uid:0 -#: field:im_chat.message,create_uid:0 -#: field:im_chat.presence,create_uid:0 -#: field:im_chat.session,create_uid:0 -msgid "Created by" -msgstr "Criado por" - -#. module: im_chat -#: field:im_chat.conversation_state,create_date:0 -#: field:im_chat.presence,create_date:0 -#: field:im_chat.session,create_date:0 -msgid "Created on" -msgstr "Criado em" - -#. module: im_chat -#: selection:im_chat.conversation_state,state:0 -msgid "Folded" -msgstr "Dobrado" - -#. module: im_chat -#: field:im_chat.conversation_state,id:0 -#: field:im_chat.message,id:0 -#: field:im_chat.presence,id:0 -#: field:im_chat.session,id:0 -msgid "ID" -msgstr "ID" - -#. module: im_chat -#: field:im_chat.presence,status:0 -#: field:res.users,im_status:0 -msgid "IM Status" -msgstr "Status do mensageiro" - -#. module: im_chat -#: field:im_chat.presence,last_poll:0 -msgid "Last Poll" -msgstr "Última votação" - -#. module: im_chat -#: field:im_chat.presence,last_presence:0 -msgid "Last Presence" -msgstr "Última presença" - -#. module: im_chat -#: field:im_chat.conversation_state,write_uid:0 -#: field:im_chat.message,write_uid:0 -#: field:im_chat.presence,write_uid:0 -#: field:im_chat.session,write_uid:0 -msgid "Last Updated by" -msgstr "Última atualização por" - -#. module: im_chat -#: field:im_chat.conversation_state,write_date:0 -#: field:im_chat.message,write_date:0 -#: field:im_chat.presence,write_date:0 -#: field:im_chat.session,write_date:0 -msgid "Last Updated on" -msgstr "Última atualização em" - -#. module: im_chat -#: field:im_chat.message,message:0 -#: selection:im_chat.message,type:0 -msgid "Message" -msgstr "Mensagem" - -#. module: im_chat -#: field:im_chat.session,message_ids:0 -msgid "Messages" -msgstr "Mensagens" - -#. module: im_chat -#: selection:im_chat.message,type:0 -msgid "Meta" -msgstr "Meta" - -#. module: im_chat -#: selection:im_chat.presence,status:0 -msgid "Offline" -msgstr "Desconectado" - -#. module: im_chat -#: selection:im_chat.presence,status:0 -msgid "Online" -msgstr "Conectado" - -#. module: im_chat -#: selection:im_chat.conversation_state,state:0 -msgid "Open" -msgstr "Aberto" - -#. module: im_chat -#: field:im_chat.session,session_res_users_rel:0 -msgid "Relation Session Users" -msgstr "Relação Sessões Usuários" - -#. module: im_chat -#. openerp-web -#: code:addons/im_chat/static/src/js/im_chat.js:18 -#, python-format -msgid "Say something..." -msgstr "Diga alguma coisa..." - -#. module: im_chat -#: field:im_chat.conversation_state,session_id:0 -msgid "Session" -msgstr "Sessão" - -#. module: im_chat -#: field:im_chat.message,to_id:0 -msgid "Session To" -msgstr "Sessão para" - -#. module: im_chat -#: field:im_chat.session,user_ids:0 -msgid "Session Users" -msgstr "Seção de usuários" - -#. module: im_chat -#: field:im_chat.message,type:0 -msgid "Type" -msgstr "Digite" - -#. module: im_chat -#: field:im_chat.session,uuid:0 -msgid "UUID" -msgstr "UUID" - -#. module: im_chat -#: field:im_chat.conversation_state,user_id:0 -#: field:im_chat.presence,user_id:0 -#: model:ir.model,name:im_chat.model_res_users -msgid "Users" -msgstr "Usuários" - -#. module: im_chat -#. openerp-web -#: code:addons/im_chat/static/src/js/im_chat.js:20 -#, python-format -msgid "Visitor" -msgstr "Visitante" - -#. module: im_chat -#: field:im_chat.conversation_state,state:0 -msgid "unknown" -msgstr "desconhecido" diff --git a/addons/im_chat/i18n/ro.po b/addons/im_chat/i18n/ro.po deleted file mode 100644 index ffb78e7b545..00000000000 --- a/addons/im_chat/i18n/ro.po +++ /dev/null @@ -1,197 +0,0 @@ -# Romanian translation for openobject-addons -# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014 -# This file is distributed under the same license as the openobject-addons package. -# FIRST AUTHOR , 2014. -# -msgid "" -msgstr "" -"Project-Id-Version: openobject-addons\n" -"Report-Msgid-Bugs-To: FULL NAME \n" -"POT-Creation-Date: 2014-09-23 16:27+0000\n" -"PO-Revision-Date: 2014-10-16 04:31+0000\n" -"Last-Translator: Dorin \n" -"Language-Team: Romanian \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-10-17 06:27+0000\n" -"X-Generator: Launchpad (build 17196)\n" - -#. module: im_chat -#. openerp-web -#: code:addons/im_chat/static/src/js/im_chat.js:88 -#, python-format -msgid "%d Messages" -msgstr "%d Mesaje" - -#. module: im_chat -#: sql_constraint:im_chat.presence:0 -msgid "A user can only have one IM status." -msgstr "" - -#. module: im_chat -#: field:im_chat.message,from_id:0 -msgid "Author" -msgstr "Autor" - -#. module: im_chat -#: selection:im_chat.presence,status:0 -msgid "Away" -msgstr "Absent" - -#. module: im_chat -#: selection:im_chat.conversation_state,state:0 -msgid "Closed" -msgstr "Închis" - -#. module: im_chat -#: field:im_chat.message,create_date:0 -msgid "Create Date" -msgstr "Data creării" - -#. module: im_chat -#: field:im_chat.conversation_state,create_uid:0 -#: field:im_chat.message,create_uid:0 -#: field:im_chat.presence,create_uid:0 -#: field:im_chat.session,create_uid:0 -msgid "Created by" -msgstr "Creat de" - -#. module: im_chat -#: field:im_chat.conversation_state,create_date:0 -#: field:im_chat.presence,create_date:0 -#: field:im_chat.session,create_date:0 -msgid "Created on" -msgstr "Creat la" - -#. module: im_chat -#: selection:im_chat.conversation_state,state:0 -msgid "Folded" -msgstr "Îndoită" - -#. module: im_chat -#: field:im_chat.conversation_state,id:0 -#: field:im_chat.message,id:0 -#: field:im_chat.presence,id:0 -#: field:im_chat.session,id:0 -msgid "ID" -msgstr "ID" - -#. module: im_chat -#: field:im_chat.presence,status:0 -#: field:res.users,im_status:0 -msgid "IM Status" -msgstr "Stare mesagerie instant" - -#. module: im_chat -#: field:im_chat.presence,last_poll:0 -msgid "Last Poll" -msgstr "" - -#. module: im_chat -#: field:im_chat.presence,last_presence:0 -msgid "Last Presence" -msgstr "" - -#. module: im_chat -#: field:im_chat.conversation_state,write_uid:0 -#: field:im_chat.message,write_uid:0 -#: field:im_chat.presence,write_uid:0 -#: field:im_chat.session,write_uid:0 -msgid "Last Updated by" -msgstr "Ultima actualizare făcută de" - -#. module: im_chat -#: field:im_chat.conversation_state,write_date:0 -#: field:im_chat.message,write_date:0 -#: field:im_chat.presence,write_date:0 -#: field:im_chat.session,write_date:0 -msgid "Last Updated on" -msgstr "" - -#. module: im_chat -#: field:im_chat.message,message:0 -#: selection:im_chat.message,type:0 -msgid "Message" -msgstr "Mesaj" - -#. module: im_chat -#: field:im_chat.session,message_ids:0 -msgid "Messages" -msgstr "Mesaje" - -#. module: im_chat -#: selection:im_chat.message,type:0 -msgid "Meta" -msgstr "Meta" - -#. module: im_chat -#: selection:im_chat.presence,status:0 -msgid "Offline" -msgstr "Deconectat" - -#. module: im_chat -#: selection:im_chat.presence,status:0 -msgid "Online" -msgstr "Activ" - -#. module: im_chat -#: selection:im_chat.conversation_state,state:0 -msgid "Open" -msgstr "Deschide" - -#. module: im_chat -#: field:im_chat.session,session_res_users_rel:0 -msgid "Relation Session Users" -msgstr "" - -#. module: im_chat -#. openerp-web -#: code:addons/im_chat/static/src/js/im_chat.js:18 -#, python-format -msgid "Say something..." -msgstr "" - -#. module: im_chat -#: field:im_chat.conversation_state,session_id:0 -msgid "Session" -msgstr "Sesiune" - -#. module: im_chat -#: field:im_chat.message,to_id:0 -msgid "Session To" -msgstr "" - -#. module: im_chat -#: field:im_chat.session,user_ids:0 -msgid "Session Users" -msgstr "" - -#. module: im_chat -#: field:im_chat.message,type:0 -msgid "Type" -msgstr "Tip" - -#. module: im_chat -#: field:im_chat.session,uuid:0 -msgid "UUID" -msgstr "UUID" - -#. module: im_chat -#: field:im_chat.conversation_state,user_id:0 -#: field:im_chat.presence,user_id:0 -#: model:ir.model,name:im_chat.model_res_users -msgid "Users" -msgstr "Utilizatori" - -#. module: im_chat -#. openerp-web -#: code:addons/im_chat/static/src/js/im_chat.js:20 -#, python-format -msgid "Visitor" -msgstr "Vizitator" - -#. module: im_chat -#: field:im_chat.conversation_state,state:0 -msgid "unknown" -msgstr "necunoscut" diff --git a/addons/im_chat/i18n/tr.po b/addons/im_chat/i18n/tr.po deleted file mode 100644 index 9c3f92ccf02..00000000000 --- a/addons/im_chat/i18n/tr.po +++ /dev/null @@ -1,197 +0,0 @@ -# Turkish translation for openobject-addons -# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014 -# This file is distributed under the same license as the openobject-addons package. -# FIRST AUTHOR , 2014. -# -msgid "" -msgstr "" -"Project-Id-Version: openobject-addons\n" -"Report-Msgid-Bugs-To: FULL NAME \n" -"POT-Creation-Date: 2014-09-23 16:27+0000\n" -"PO-Revision-Date: 2014-10-11 07:08+0000\n" -"Last-Translator: Ayhan KIZILTAN \n" -"Language-Team: Turkish \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-10-12 07:19+0000\n" -"X-Generator: Launchpad (build 17196)\n" - -#. module: im_chat -#. openerp-web -#: code:addons/im_chat/static/src/js/im_chat.js:88 -#, python-format -msgid "%d Messages" -msgstr "%d Mesaj" - -#. module: im_chat -#: sql_constraint:im_chat.presence:0 -msgid "A user can only have one IM status." -msgstr "Bir kullanıcı yalnozca bir IM durumunda olabilir." - -#. module: im_chat -#: field:im_chat.message,from_id:0 -msgid "Author" -msgstr "Yazan" - -#. module: im_chat -#: selection:im_chat.presence,status:0 -msgid "Away" -msgstr "Dışarıda" - -#. module: im_chat -#: selection:im_chat.conversation_state,state:0 -msgid "Closed" -msgstr "Kapalı" - -#. module: im_chat -#: field:im_chat.message,create_date:0 -msgid "Create Date" -msgstr "Oluşturma Tarihi" - -#. module: im_chat -#: field:im_chat.conversation_state,create_uid:0 -#: field:im_chat.message,create_uid:0 -#: field:im_chat.presence,create_uid:0 -#: field:im_chat.session,create_uid:0 -msgid "Created by" -msgstr "Oluşturan" - -#. module: im_chat -#: field:im_chat.conversation_state,create_date:0 -#: field:im_chat.presence,create_date:0 -#: field:im_chat.session,create_date:0 -msgid "Created on" -msgstr "Oluşturulma" - -#. module: im_chat -#: selection:im_chat.conversation_state,state:0 -msgid "Folded" -msgstr "Katlanmış" - -#. module: im_chat -#: field:im_chat.conversation_state,id:0 -#: field:im_chat.message,id:0 -#: field:im_chat.presence,id:0 -#: field:im_chat.session,id:0 -msgid "ID" -msgstr "ID" - -#. module: im_chat -#: field:im_chat.presence,status:0 -#: field:res.users,im_status:0 -msgid "IM Status" -msgstr "Anlık İleti Durumu" - -#. module: im_chat -#: field:im_chat.presence,last_poll:0 -msgid "Last Poll" -msgstr "Son Anket" - -#. module: im_chat -#: field:im_chat.presence,last_presence:0 -msgid "Last Presence" -msgstr "Son Durum" - -#. module: im_chat -#: field:im_chat.conversation_state,write_uid:0 -#: field:im_chat.message,write_uid:0 -#: field:im_chat.presence,write_uid:0 -#: field:im_chat.session,write_uid:0 -msgid "Last Updated by" -msgstr "Son Güncelleyen" - -#. module: im_chat -#: field:im_chat.conversation_state,write_date:0 -#: field:im_chat.message,write_date:0 -#: field:im_chat.presence,write_date:0 -#: field:im_chat.session,write_date:0 -msgid "Last Updated on" -msgstr "Son Güncelleme" - -#. module: im_chat -#: field:im_chat.message,message:0 -#: selection:im_chat.message,type:0 -msgid "Message" -msgstr "Mesaj" - -#. module: im_chat -#: field:im_chat.session,message_ids:0 -msgid "Messages" -msgstr "Mesajlar" - -#. module: im_chat -#: selection:im_chat.message,type:0 -msgid "Meta" -msgstr "Meta" - -#. module: im_chat -#: selection:im_chat.presence,status:0 -msgid "Offline" -msgstr "Çevrimdışı" - -#. module: im_chat -#: selection:im_chat.presence,status:0 -msgid "Online" -msgstr "Çevrimiçi" - -#. module: im_chat -#: selection:im_chat.conversation_state,state:0 -msgid "Open" -msgstr "Aç" - -#. module: im_chat -#: field:im_chat.session,session_res_users_rel:0 -msgid "Relation Session Users" -msgstr "İlgili Oturum Kullanıcısı" - -#. module: im_chat -#. openerp-web -#: code:addons/im_chat/static/src/js/im_chat.js:18 -#, python-format -msgid "Say something..." -msgstr "Bir şey söyle..." - -#. module: im_chat -#: field:im_chat.conversation_state,session_id:0 -msgid "Session" -msgstr "Oturum" - -#. module: im_chat -#: field:im_chat.message,to_id:0 -msgid "Session To" -msgstr "Oturum Açılan" - -#. module: im_chat -#: field:im_chat.session,user_ids:0 -msgid "Session Users" -msgstr "Oturum Kullanıcısı" - -#. module: im_chat -#: field:im_chat.message,type:0 -msgid "Type" -msgstr "Türü" - -#. module: im_chat -#: field:im_chat.session,uuid:0 -msgid "UUID" -msgstr "UUID" - -#. module: im_chat -#: field:im_chat.conversation_state,user_id:0 -#: field:im_chat.presence,user_id:0 -#: model:ir.model,name:im_chat.model_res_users -msgid "Users" -msgstr "Kullanıcılar" - -#. module: im_chat -#. openerp-web -#: code:addons/im_chat/static/src/js/im_chat.js:20 -#, python-format -msgid "Visitor" -msgstr "Ziyaretçi" - -#. module: im_chat -#: field:im_chat.conversation_state,state:0 -msgid "unknown" -msgstr "bilinmeyen" diff --git a/addons/im_chat/models/__init__.py b/addons/im_chat/models/__init__.py deleted file mode 100644 index 1ee6a543eb9..00000000000 --- a/addons/im_chat/models/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# -*- coding: utf-8 -*- -import im_chat_session -import res_users diff --git a/addons/im_chat/models/im_chat_session.py b/addons/im_chat/models/im_chat_session.py deleted file mode 100644 index 0d3f11c5d14..00000000000 --- a/addons/im_chat/models/im_chat_session.py +++ /dev/null @@ -1,462 +0,0 @@ -# -*- coding: utf-8 -*- -import cgi -import datetime -import random -import re -import time -import uuid - -from openerp import tools, _ -from openerp import api, fields, models -from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT - -from openerp.addons.bus.models.bus import TIMEOUT - - -DISCONNECTION_TIMER = TIMEOUT + 5 -AWAY_TIMER = 600 # 10 minutes - - - -class ImChatConversationState(models.Model): - """ User Session State - Adds a state on the m2m between user and session. - """ - - _name = 'im_chat.conversation_state' - _description = 'Chat Conversation State' - _table = 'im_chat_session_res_users_rel' - - state = fields.Selection([('open', 'Open'), ('folded', 'Folded'), ('closed', 'Closed')], string='Status', default='open') - session_id = fields.Many2one('im_chat.session', string='Session', required=True, ondelete='cascade') - user_id = fields.Many2one('res.users', string='Users', required=True, ondelete='cascade') - - - -class ImChatSession(models.Model): - """ Session - Reprensenting a conversation between users. It manages anonymous user for method starting - with 'session_'. - """ - - _name = 'im_chat.session' - _rec_name = 'uuid' - _description = 'Chat Conversation' - _order = 'id desc' - - def _default_uuid(self): - return uuid.uuid4() - - uuid = fields.Char(string='UUID', index=True, default=_default_uuid) - message_ids = fields.One2many('im_chat.message', 'to_id', string='Messages') - session_res_users_rel = fields.One2many('im_chat.conversation_state', 'session_id', string='Relation Session Users') - user_ids = fields.Many2many('res.users', 'im_chat_session_res_users_rel', 'session_id', 'user_id', string='Session Users') - - # -------------------------- - # Session Methods - # -------------------------- - @api.multi - def session_info(self): - """ Get the header of the current session - :returns the values of the session - :rtype : dict - """ - self.ensure_one() - info = { - 'uuid': self.uuid, - 'users': self.session_user_info(), - 'state': 'open', - } - # add user session state, if available and if user is logged - if self._uid: - domain = [('user_id', '=', self._uid), ('session_id', 'in', self.ids)] - session_state = self.env['im_chat.conversation_state'].search(domain, limit=1) - if session_state: - info['state'] = session_state.state - return info - - @api.multi - def session_user_info(self): - """ Get the user infos for all the identified user in the session - :returns a list of user infos - :rtype : list(dict) - """ - self.ensure_one() - return self.env['res.users'].browse(self.user_ids.ids).read(['id', 'name', 'im_status']) - - @api.model - def session_user_image(self, uuid, user_id): - """ Get the avatar of a user in the given session - :param uuid : the uuid of the session - :param user_id : the user identifier - """ - # get the session - image_b64 = False - if user_id: - session = self.env["im_chat.session"].search([('uuid', '=', uuid)], limit=1) - if session and session.sudo(user_id).is_in_session(): - image_b64 = self.env["res.users"].sudo().browse(user_id).image_small - # set default image if not found - if not image_b64: - image_b64 = 'R0lGODlhAQABAIABAP///wAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==' - return image_b64 - - @api.model - def session_get(self, user_to): - """ Get the canonical session between 2 users, create it if needed. - To reuse an old session, this one must be private, and contains only 2 users. - :param user_to : the identifier of the user - :returns a session header, or False if the user_to was False - :rtype : dict - """ - if user_to: - session = self.search([('user_ids', 'in', user_to), ('user_ids', 'in', [self._uid])], limit=1) - if not (session and len(session.user_ids) == 2 and session.is_private()): - session = self.create({'user_ids': [(6, 0, (user_to, self._uid))]}) - return session.session_info() - - # -------------------------- - # Utils Methods - # -------------------------- - @api.multi - def is_private(self): - """ Return true if the session is private between identified users (no external messages). - The only way to do that is to check if there is a message without author (from_id is False). - """ - self.ensure_one() - message_ids = self.env["im_chat.message"].search([('to_id', 'in', self.ids), ('from_id', '=', None)]) - return len(message_ids) == 0 - - @api.multi - def is_in_session(self): - """ Return True if the current user is in the user_ids of the session. False otherwise. - Note : the user need to be logged - """ - self.ensure_one() - user_id = self._uid - return user_id and user_id in [u.id for u in self.user_ids] - - @api.model - def update_state(self, uuid, state=None): - """ Update the fold_state of the given session. In order to syncronize web browser - tabs, the change will be broadcast to himself (the current user channel). - Note : the user need to be logged - :param status : the new status of the session for the current user. - """ - domain = [('user_id', '=', self._uid), ('session_id.uuid', '=', uuid)] - for session_state in self.env['im_chat.conversation_state'].search(domain): - if not state: - state = session_state.state - if session_state.state == 'open': - state = 'folded' - else: - state = 'open' - session_state.write({'state': state}) - self.env['bus.bus'].sendone((self._cr.dbname, 'im_chat.session', self._uid), session_state.session_id.session_info()) - - # -------------------------- - # User Actions - # -------------------------- - @api.model - def add_user(self, uuid): - """ Add the current user to the session, and broadcast the meta message. - Note : the user need to be logged - :param uuid : the uuid of the session - """ - session = self.search([('uuid', '=', uuid)], limit=1) - if user_id not in [u.id for u in session.user_ids]: - session.write({'user_ids': [(4, self._uid)]}) - # notify the all the channel users and anonymous channel - notifications = [] - for user in session.user_ids: - info = session.sudo(user.id).session_info() - notifications.append([(self._cr.dbname, 'im_chat.session', user.id), info]) - # anonymous are not notified when a new user is added : cannot execute session_info as uid = None - info = session.sudo().session_info() - notifications.append([session.uuid, info]) - self.env['bus.bus'].sendmany(notifications) - # send a message to the conversation - user = self.env['res.users'].browse(self._uid) - message = _("%s joined the conversation.") % (user.name,) - self.env["im_chat.message"].send_message(session.uuid, "meta", message) - - @api.model - def quit_user(self, uuid): - """ Remove the current user from the given session. - Note : the user need to be logged - """ - session = self.search([('uuid', '=', uuid)], limit=1) - if self._uid in [u.id for u in session.user_ids] and len(session.user_ids) > 2: - return session._remove_user() - return False - - @api.multi - def _remove_user(self): - """ Private implementation of removing the current user from the current session, - and broadcast the meta message. - Note : the user need to be logged - """ - self.ensure_one() - # send a message to the conversation - user = self.env['res.users'].browse(self._uid) - message = _("%s left the conversation.") % (user.name,) - self.env["im_chat.message"].send_message(self.uuid, "meta", message) - # close his session state - self.update_state(self.uuid, 'closed') - # remove the user from session - self.write({"user_ids": [(3, self._uid)]}) - # notify the all the channel users and anonymous channel - notifications = [] - for user in self.user_ids: - info = self.sudo(user.id).session_info() - notifications.append([(self._cr.dbname, 'im_chat.session', user.id), info]) - # anonymous are not notified when a new user left : cannot execute session_info as uid = None - info = self.sudo().session_info() - notifications.append([self.uuid, info]) - self.env['bus.bus'].sendmany(notifications) - return True - - - -class ImChatMessage(models.Model): - """ Session Messsages - The chat provide 2 message type : 'message' (normal user message) - or 'meta' (message relative to changes about sessions) - For anonymous message, the from_id is False. - Messages are sent to a session not to users. - """ - - _name = 'im_chat.message' - _description = 'Chat Message' - _order = 'create_date asc' - - @api.model - def _default_type(self): - return [('message', 'Message'), ('meta', 'Meta')] - - create_date = fields.Datetime(string="Create date", required=True, index=True) - from_id = fields.Many2one('res.users', 'Author') - to_id = fields.Many2one('im_chat.session', 'Session', required=True, index=True) - type = fields.Selection('_default_type', 'Message type', default='message') - message = fields.Char('Message') - - @api.model - def _escape_keep_url(self, message): - """ Escape the message and transform the url into clickable link - :param message : the message to escape url and transform them into clickable link - :returns the escaped message - """ - safe_message = "" - first = 0 - last = 0 - for m in re.finditer('(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?', message): - last = m.start() - safe_message += cgi.escape(message[first:last]) - safe_message += '%s' % (cgi.escape(m.group(0)), m.group(0)) - first = m.end() - last = m.end() - safe_message += cgi.escape(message[last:]) - return safe_message - - @api.model - def get_init_notifications(self): - """ Get unread messages and old messages received less than AWAY_TIMER - ago and the session_info for open or folded window - Note : the user need to be logged - """ - # get the message since the AWAY_TIMER - threshold = datetime.datetime.now() - datetime.timedelta(seconds=AWAY_TIMER) - threshold = threshold.strftime(DEFAULT_SERVER_DATETIME_FORMAT) - domain = [('to_id.user_ids', 'in', [self._uid]), ('create_date', '>', threshold)] - - # get the message since the last poll of the user - presence = self.env['im_chat.presence'].search([('user_id', '=', self._uid)], limit=1) - if presence: - domain.append(('create_date', '>', presence.last_poll)) - messages = self.search_read(domain, ['from_id', 'to_id', 'create_date', 'type', 'message'], order='id asc') - - # get the session of the messages and the not-closed ones - session_ids = [m['to_id'][0] for m in messages] - domain = [('user_id', '=', self._uid), '|', ('state', '!=', 'closed'), ('session_id', 'in', session_ids)] - session_states = self.env['im_chat.conversation_state'].search(domain) - # re-open the session where a message have been recieved recently - session_states.filtered(lambda r: r.state == 'closed').write({'state': 'folded'}) - - # create the notifications (session infos first, then messages) - notifications = [] - for state in session_states: - session_infos = state.session_id.session_info() - notifications.append([(self._cr.dbname, 'im_chat.session', self._uid), session_infos]) - for message in messages: - notifications.append([(self._cr.dbname, 'im_chat.session', self._uid), message]) - return notifications - - - @api.model - def send_message(self, from_uid, uuid, message_content, message_type='message'): - """ Post and broadcast a message for the given session, return the message id - :param from_uid : user id of the author of the message - :param uuid : the uuid of the session - :param message_content : the content of the message - :param message_type : the type of the message - :returns the identifier of the new created message - :rtype : integer - """ - session = self.env['im_chat.session'].search([('uuid', '=', uuid)], limit=1) - # escape the new message - message_content = self._escape_keep_url(message_content) - message_content = self.env['im_chat.shortcode'].replace_shortcode(message_content) - # create the chat message - values = { - "from_id": from_uid, - "to_id": session.id, - "type": message_type, - "message": message_content, - } - message = self.create(values) - # broadcast the message - data = message.read(['from_id', 'to_id', 'create_date', 'type', 'message'])[0] - notifications = [] - notifications.append([uuid, data]) - for user in session.sudo().user_ids: # sudo required for portal user - notifications.append([(self._cr.dbname, 'im_chat.session', user.id), data]) - self.env['bus.bus'].sendmany(notifications) - return message.id - - @api.model - def fetch_message(self, uuid, last_id=False, limit=20): - """ Return messages for the given session uuid. If the session is public, fetch_message - as 'sudo', else (private session) fetch them as the current user. The security rule - 'message_rule_1' will check the access right. - :param uuid : uuid of the session to fetch messages - :param last_id : last message id to start the research - :param limit : maximum number of messages to fetch - :returns list of messages values - :rtype : list(dict) - """ - session = self.env['im_chat.session'].search([('uuid', '=', uuid)], limit=1) - if session and session.is_in_session(): - domain = [("to_id.uuid", "=", uuid)] - if last_id: - domain.append(("id", "<", last_id)) - return self.search_read(domain, ['id', 'create_date', 'to_id', 'from_id', 'type', 'message'], limit=limit, order="id desc") - return False - - - -class ImChatShortcode(models.Model): - """ Shortcode - Canned Responses, allowing the user to defined shortcuts in its chat message. - These shortcode are globals and are available for every user. Smiley use this mecanism. - """ - - _name = 'im_chat.shortcode' - _description = 'Canned Response / Shortcode' - - source = fields.Char('Shortcut', required=True, index=True, help="The shortcut which must be replace in the Chat Messages") - substitution = fields.Char('Substitution', required=True, index=True, help="The excaped html code replacing the shortcut") - description = fields.Char('Description') - - @api.model - def create(self, values): - if values.get('substitution'): - values['substitution'] = self._sanitize_shorcode(values['substitution']) - return super(ImChatShortcode, self).create(values) - - @api.multi - def write(self, values): - if values.get('substitution'): - values['substitution'] = self._sanitize_shorcode(values['substitution']) - return super(ImChatShortcode, self).write(values) - - def _sanitize_shorcode(self, substitution): - """ Sanitize the shortcode substitution : - - HTML substitution : only allow the img tag (smiley) - - escape other substitutions to avoid XSS - """ - is_img_tag = re.match(r'''^$''', substitution, re.M|re.I) - if is_img_tag: - return substitution - return cgi.escape(substitution) - - @api.model - def replace_shortcode(self, message): - for shortcode in self.search([]): - regex = '(?:^|\s)(%s)(?:\s|$)' % re.escape(shortcode.source) - message = re.sub(regex, " " + shortcode.substitution + " ", message) - return message - - - -class ImChatPresence(models.Model): - """ User Chat Presence - Its status is 'online', 'away' or 'offline'. This model should be a one2one, but is not - attached to res_users to avoid database concurrence errors. Since the 'update' method is executed - at each poll, if the user have multiple opened tabs, concurrence errors can happend, but are 'muted-logged'. - """ - - _name = 'im_chat.presence' - _description = 'User Chat Presence' - - _sql_constraints = [('im_chat_user_status_unique', 'unique(user_id)', 'A user can only have one IM status.')] - - user_id = fields.Many2one('res.users', 'Users', required=True, index=True, ondelete='cascade') - last_poll = fields.Datetime('Last Poll', default=lambda self: fields.Datetime.now()) - last_presence = fields.Datetime('Last Presence', default=lambda self: fields.Datetime.now()) - status = fields.Selection([('online', 'Online'), ('away', 'Away'), ('offline', 'Offline')], 'IM Status', default='offline') - - - @api.model - def update(self, user_presence=True): - """ Register the given presence of the current user, and trigger a im_status change if necessary. - The status will not be written or sent if not necessary. - :param user_presence : True, if the user (self._uid) is still detected using its browser. - :type user_presence : boolean - """ - presence = self.search([('user_id', '=', self._uid)], limit=1) - # set the default values - send_notification = True - values = { - 'last_poll': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT), - 'status' : presence and presence.status or 'offline' - } - # update the user or a create a new one - if not presence: # create a new presence for the user - values['status'] = 'online' - values['user_id'] = self._uid - self.create(values) - else: # write the user presence if necessary - if user_presence: - values['last_presence'] = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT) - values['status'] = 'online' - else: - threshold = datetime.datetime.now() - datetime.timedelta(seconds=AWAY_TIMER) - if datetime.datetime.strptime(presence.last_presence, DEFAULT_SERVER_DATETIME_FORMAT) < threshold: - values['status'] = 'away' - send_notification = presence.status != values['status'] - # write only if the last_poll is passed TIMEOUT, or if the status has changed - delta = datetime.datetime.utcnow() - datetime.datetime.strptime(presence.last_poll, DEFAULT_SERVER_DATETIME_FORMAT) - if delta > datetime.timedelta(seconds=TIMEOUT) or send_notification: - # Hide transaction serialization errors, which can be ignored, the presence update is not essential - with tools.mute_logger('openerp.sql_db'): - presence.write(values) - # avoid TransactionRollbackError - self.env.cr.commit() # TODO : check if still necessary - # notify if the status has changed - if send_notification: # TODO : add user_id to the channel tuple to allow using user_watch in controller presence - self.env['bus.bus'].sendone((self._cr.dbname, 'im_chat.presence'), {'id': self._uid, 'im_status': values['status']}) - # gc : disconnect the users having a too old last_poll. 1 on 100 chance to do it. - if random.random() < 0.01: - self.check_users_disconnection() - return True - - @api.model - def check_users_disconnection(self): - """ Disconnect the users having a too old last_poll """ - limit_date = (datetime.datetime.utcnow() - datetime.timedelta(0, DISCONNECTION_TIMER)).strftime(DEFAULT_SERVER_DATETIME_FORMAT) - presences = self.search([('last_poll', '<', limit_date), ('status', '!=', 'offline')]) - presences.write({'status': 'offline'}) - notifications = [] - for presence in presences: - notifications.append([(self._cr.dbname, 'im_chat.presence'), {'id': presence.user_id.id, 'im_status': presence.status}]) - self.env['bus.bus'].sendmany(notifications) diff --git a/addons/im_chat/security/im_chat_session_security.xml b/addons/im_chat/security/im_chat_session_security.xml deleted file mode 100644 index a70fe1835b6..00000000000 --- a/addons/im_chat/security/im_chat_session_security.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - Can only read messages that you sent or messages sent to you - - - [('to_id.user_ids', 'in', [user.id])] - - - - - - - - Can only modify your session - - - [('user_ids', 'in', [user.id])] - - - - - - - - Can only modify your own session relations - - - [('user_id', '=', user.id)] - - - - - - - diff --git a/addons/im_chat/security/ir.model.access.csv b/addons/im_chat/security/ir.model.access.csv deleted file mode 100644 index 7d09c73050e..00000000000 --- a/addons/im_chat/security/ir.model.access.csv +++ /dev/null @@ -1,11 +0,0 @@ -id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_im_chat_message,im_chat.message,model_im_chat_message,base.group_user,1,0,1,0 -access_im_chat_session,im_chat.session,model_im_chat_session,base.group_user,1,1,1,0 -access_im_chat_conversation_state,im_chat.conversation_state,model_im_chat_conversation_state,base.group_user,1,1,1,0 -access_im_chat_presence,im_chat.presence,model_im_chat_presence,base.group_user,1,1,1,1 -access_im_chat_shortcode,im_chat.shortcode,model_im_chat_shortcode,base.group_user,1,1,1,1 -access_im_chat_message_portal,im_chat.message,model_im_chat_message,base.group_portal,1,0,1,0 -access_im_chat_session_portal,im_chat.session,model_im_chat_session,base.group_portal,1,1,1,0 -access_im_chat_conversation_state_portal,im_chat.conversation_state,model_im_chat_conversation_state,base.group_portal,1,1,1,0 -access_im_chat_presence_portal,im_chat.presence,model_im_chat_presence,base.group_portal,1,1,1,1 -access_im_chat_shortcode_portal,im_chat.shortcode,model_im_chat_shortcode,base.group_portal,1,0,0,0 \ No newline at end of file diff --git a/addons/im_chat/static/description/icon.png b/addons/im_chat/static/description/icon.png deleted file mode 100644 index 4fe0a5ddfbc0169b1a5b83ab8b6c0e0dbc4f5af8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20425 zcmeI4c|4Te+ra6mJeE?(7Lh14%oxPTl6@V!LUsldW@H9qFI!~|8D&i(Nf9dhnspj$ z_DYnHEmYQcZ`D)t^z{7Z{k@;}&za9>=DwVBUDrA1dtK+c&;7^kioUM;9y(4sDk`cy zni?vGz!ks!{c|_)D@%QT47kv`YnWlEsMwgcztm0{-GG)fAJW(qYYNwqu}8a!+B%@^ z5TZCYcR(93Q^2{~+PfgIymkmDBub8NzM_VY7wI6!cSaHpfx9asoRJ#79ta~}U1NJ+ z7kiikpMpG{EKUY+;D*53^5WcFQ5YGV9N$;JGJw3@4Cdqgs)BWq<5S!okk=Hh&#R2~ zK=4Y6N{ZM+#3Xp7V4`A@ViM9~AYLd$OdJdm2aAb|h(To_Fc~pP-tQkic{)In^>A>M zF;r3eJ{<6u9G^25>n;NZdwY9}dW(yqJ)FQ|Fc=IBfr6n>5kNx(vD7dF%Sy#ogF|CjI(tIi-|(OzcX^M z|6$|q>EZfyY7X{bge$@gfx=<{JF(x|xjUn=XpA%ZUxxhA{jUQ9F@eK>jQ!jBy1D&! zXbe`>3qbH4>2EDD#y;){upt72_VlnvsCofQIr6*JW08(OgY!2}+dcm}cLWakFJ{|4 z-^{+I!nX*?0&A5~_CVNT(H_QVw5$BLEYtrBA+NGBuOQsk9*NpUDkKa3RsUzOf4V}b z*kTd#Kr%{-K%_;)AjV=6GU6v>#HEfyfUJa&IQgdeiw`*30qN-TdmoYzQXeFmzxn_+ zkAp4N_J8GtRF}k$gT0I++QZEjE01)ubwYsMQBJbppN%A%KT=9Y8SRSp0H%eI7ncS9 zu9?K@YnsVuqA*xnls!UIMILAnMIs$!q#dOo2$;PzKo0>EkwC!2MeHQRB}E(^5Dt=d zP$@B6sKi%@a5$;?KU}Mz?LD`*%6Hce|D|gZ^MAP3^FRV+&eoMgXM6Vl?GV3l^N*o4 zkQg92eMpiPNZqf6%m{J*@2!7zxFWw6BzId63}Sn`$?^TQ-+olE9|daL=IgeVv9;eW zW%Bmhy8+=K3;tW{&uRUz{BDN)zXtG|e-iV5AH&-jf%;Dy>YJ;tTL10@gLcGv+j<}r zoq(JmyV1Y7{JYJszGcDNb>9{F_b7a8{d;i!O4ok|Q+B(Y$pDD}Ov?6a0K{a$|7iO; z@;80c?SQ&^e$$tPY=6LF+rQs!N&A1bH6vq7+W)KVPg4^V5-Sh=KHM+OBv#v1_ghsk z0uBfW`4f_o;*zr9Uz>lm&`07BuBIwTVEbaWlTu6)SlL&jUweLQY4)#{Bt73PzlDqB zX8tvr-&%pA0&q$J|2U@nJ1 zEFw|`3NBI_WMv92vWQ3(D7Z*zkd-O8$RZ+Dpx`2VB8!Mrfr5*a23eVci!35i z1qv=w8f0Y(F0zP76)3n!X^@pExX2ap~ zWeP5`h)5MExJYS`l_|K$A|h3w;3B0#R;J)0i-=T#f{T;}S($>1EFw|`3NBI_WMv92 zvWQ3(D7Z*zkd^-xgo^6D4HcElb>P)Pbi8$=8gM`ilkinp#;h;BL~(^nS*mMiC%*vjmiH>EDU!z&luW$m>PeTN?YTC0DmnL6O#=sZDHt6 zQzfMxpzj^udw`B_JwV4#lWm)?CfhdOOaNzs>$fA$esD6&xq>jeo^u7s&K#7!QIQ~^ z$rKMd(h4mC9Xa0ibVQR$Bfd0l=>Uiz(IvS@B<_?pzs6A}8?)RK2P0mF6X$B@u(t+W zbMqqdQ+-o(dTKjM6q+xqbsgJ^LfB%YbvNz|&9)SQYLg?O@PwxW?Dlbg<{<67Wm>_) z^=GYODoU@%w`pk|zt^&iYXO?#k}ipZ8GBLco&yHGM(BjhgU+q|zI=*&tVi}AbaIte zl7Q;u-b<uJ}UGQJ2xla{)6gDd^LspoD`{xZv9#4VGz%sDZmu9cMtF@y}0+?b@if$tbjnSm!DR~sBg*^dQv8?PBGlKoMUq4R!vo!JxFHz3caIq7p?c;&4QuRnc}}Ei&n}MdbU9bFwnkG?E?pkPHHgzgLk>oZ=On`Jy^a zxx%Zf+#4K`r*FZj@@N=>L9-A<3i-{NpbScKavJd}FUyXbdQD~LSB}xKc+TC9^xOOZ ziXMK_Q|TwwisQ2Of#r$Zp(5-l%z3Ze5D>u6O#8kacBz+Vot<#nTK3MF(;@{`O-r#` z!3}%%P~iw|9*BsU*0qANe8Hz`>Kt?PcPgvXC8y=zctU|Ls~c5<~L zOCh^zifUK79bH7E*dAs6`5w(@G%_b2p0un)H|&1pH~VPm*6xpvzCHva*DnjPR~Ysh z8k$|n7aS~)c-FpO`!vf~mtcscW$GheNuKgme7^N4FQC6QPfAdm8YO-#g7+MCa3qEW`GkoORo{|;4Qw_E&Rqgww=8%o>df7C+h{w z%5#Y(-Fzpp*fpMH6@h;~P~mpa2PKt9#81VmNMzZ-6?-uy(UApZ;nQ=gw|E6qMao|9 zFiuGe=$NgIn^1ZrEo48^;@UpvJpx>%l())$b|~zpvnbw+-!(AnwHCwO5Y%+TVk@zL zj?>YUr+ms$v~*F}H>%>Xrs;Vdr_a3PlfmiP|5>EI}S+z~S=a`v! z)`AL?P0=58J5hp*`a?wI#fQWK0}Bh#pBh>6t6=NK9Zl+Lj&dXMPRzop2DnUHw)v42 zAyLyuTC%GW_e%2OV_*0l1QHB`ewuuJ(^Vt>X}g@C;Dh|JVrv#e*kHFR&nD#4L|U`W zng0BDXWzS?LYT6;$;n~8JQt$Z=Tc5T%iIiX%Ibn4X8a-#UPf}VBwdR?qkq-o`f>R$ z%dXY4r?Hk9lBLhFO5-JioeUv;n}OyrT%%PyD^&Ls+*P#}%7bz zPYsfKUIb#WY@Wj|rV~V+-dK9u6{l^URi#e?J2fJb9y(2*4u15gcuWO;&{Ve~?RHLA z_pmnTh6v^kQRfGieE*XbS_);8?tpuYq|e`>T?sa{U=0x9>e zzT@6%!C|9RoifrAUu9tla~@+0tIh+}Y3kK$CaBf6GrA|$dT(f(c`Y<@;YN2=BF4*- zDo@UOH8Ig9`}u0TmjR}$fxAo(t^sA^I@|`82p5mCf-I!LnKE%MW~_;xo-_Vl$4Bex zuGNDAI)vjFp5|XWs^~d-_ zEgo$6s~y=N!!4g`c)4*RL+?C91A3@~yU@bxWP`nZ$;Z!j*N=Ls*6j$Vp)q~zA83R3mBvWJFSTj zXb%o9N#c5LoN35UTR~Kt{{U%LK5VS2m^f~f8X2BrFuAbLR3COfvhvO5`fB%{evEs^ zp@=VmT)Ix8%aYQwal)KeAgtWuSs9yt4=SeKAsd%=2ft-IVC&3T)DR~JZ1M5J!O25( zJsg}*bMlz=t42qTKH^8k(pV#>N<@opSEgQi>^j;NwB?;_6>tE!0~@$8hozUL8s1<| zT=svGqWz#e`4P|hK#4u;>KB$(?a6uPX1s`v*#%+3083cA{^dD$oE^q2esjbwn^o`i z-A_jP#uBsiCM=11uL5bP%5x4A+FsBQifW4-vFlp#hz)vS>A*?PMW@$gItM|7qCz3M zyu5BP$SGjqdsFtT!f)=l+-$cJQpeE|X`BMj-*=}t7%ZrnAh9-f70Cxs;tP&#vY+ zj%h{Sc~lVVE)g*;;p>%`r{(JE-vl;)Yub~LIgQ+^zlZ*$4V(4C_T0YzBq`2xrsU=h z_fiXnk63$lwYJ_dq3FpKe>+F6Ws=!`5cU{2~bm7SQ=Y{oV z9;cuN(GzZgINz)QXseCWu^0)dHIosBJRxu|4yu{n5Z4`IJnID{zEVIy&yML7?^lZI zF2|kHf;KC$(m-Oi<>lelCbr<8#soHICloR~@<%rV)f$wM*2Z5N@i7F{u}mb_)YM_T z&RDV<50#0*6*WHWlP7olF9xkO@HlVuWSHWbIF|ZO*uzosXL@^!KP_1Ai)OgPb^Q6u z2Al)C0MTev$Oh(mqA6Z8CiQK0a$o3?i?-|~QaqZlr>~N%l!ATA)K}C>7hj_IMj>tm zJX$c_AoY`1aI&+@M)E$lIU}2qb8~7vpJ*@DGrSd3mFMxZ!tIx9>}o0sMZAk1KiH!c z@W*P)+?n&^Y`4Ea$AxnF-n@}m{%mK?8k4|9*JF~|th{vb3m4K=XKErT-Fvh0$upg( zO{Rwn=U*IRl#EQeEp6`E**9=`cp9!?Dtaj-)A`!F^mOLN&Knl!)*>C~A*YT=@a>OF zNfw?5wA2q(o z1Ow92Ec_c2U$`&J2{jLIZ7czslGqkt6wA1mkHB<}^QKIklTQ`bV1G+-jeqbx`G=b& zLDgQZOw~&%Ba2H!#`7OL4>vX6($K)jXG+1DHSvN458k0sr58Uvd4*qeF5U{8K)$L< zSu8I%qt-6pEa;1BiGTaJdxJ>NxOa?xCvNhtNKf7xXL!3aj=cvOC*ac_l09G?5pgGj zIba!|GIb7>BfanXo`3{FO5M~vMw}xpADUP1%bESyXP}T6AO`LXVUY--xf%4{Zg=Xt7SgJEt9r1xnXA5O=>S4%lJPk!ermexduhywCfa(xh#^x(aTul z&L-z}e??dr`j{L4Q0Z#sw6Tb}%F+3AqEp3U&&G@IG^mXkk!uKf zGNQZEdHCEkTc&i{;AA0DzA)#sbUf&dh|pm!PKRcrn(@iehMjE2V~o2e1oYF~++<6U z%r&c<%ejJ}+vU}Vh>)U3VTuxdRn_<~TlSSdsGr3%ZBC5NkJ9fn^=OAJxeP_B;5KMt zKcuMhs2=1Pt-5#G7O0)X`jitr27M_lcg#6Y_lmrgOR|(lU*MdX9UruouX}90m7LR5 z1(L%E5!w}a8X!f7@=~ejZKPT3x{45>vt=IbfAkJLe;Qe{#XTJK#YM*aZ0oYGPlxKg z+{M*SPi}ZWranIHlZ|GP4(?{gG^!lge!?P>A#AW_8sX(O($s$cEC=nSg157opjCUf#@DoHUgC97q3s27%A56!W??$}-xcdge3nH-Kt_TLbfdiNY2-E0@S z<~!x;_NHYs0Lv|>rKe64U}+gjv%7F`wp8wrR$JTEW7ZWZ!CK-BdD>ckvK)Y)LYTn@ zcY|E&3|Ycr@T{+bh?1bxCQXKU9D5y2pW23mTg z0}9vac?w~OYBfm)5n3XGiKymLtQ|)BLsDX9<$~&KDHaI{-m_%DrPgZ>!gV13{|KT-o&mqv3q`r&CHj`iL9lMbHE0dr95sW$ z?J@3V@%`wE>MvAD7Cz%qO(P?nwR$}MSF3`CU{llP!M5zhGN00N(MV38y)>Wdc=Ph& zJ4;~0dGc8{n(?8T(_C`c%N-j%M<1Bi1UA_{k&;?cNRe9V?iWNoC80~?H?C?vS?WByzB-fRcZa*ccKw9c#%CeU z$7p+LmRtFG5y=ADkUdAT`d*qsuROVVVdxaq(K8`SnU8QywOUW@-j1`muS-?Oc|^Ur ze}BejLDeb%MN?4qk;xpYSy;qm%?pWBN9+@%zkDoVg`WPdh)`pRPCRTZyA4@5_U4VT zsKsUUV#8Fm(jQKRzehkh{->6 zvvU-@BvY1ZqoZVU+CnFLBm=X%ilc%}s&c+SjmrC|N4`E9Ea{Y5m$n>~0Mra%Zoo|k z+S#M5r+#vKV)mC9dq!S1PP*^xb<8Tgy|e#qtI6Q=n6q}bxN{uYulZCN>y*jE6MS$F z)byHagRh9BUYtYGRaxyAKM)nGO3S#HDO%-nY;4w$D+>$jSX)QO`-VUGPi2JWwM`r| zooV$MCSdE@CVQY*?jT4DPC{#Xn#RERz@>(ZPfA$K+M`bo8|hEwz>Wb+&#;HLO2AWn ze58B)A@=4seM1yG6x1t?F+1{8;kw86_?oiP|3O{3YuD39{Pqo+N znf4f4^t;1-@6G^c{dCu(^o8BDi8%(Zv4(66x7wf!FxO6I8zP@^m?)d)McyJ1@!)VI z)$!;Efdm#k&X1sd`m}|PyM;4`;ciGFJc4)}oJe(iPZ21xG{?ZuR0EEjs{CjOsF=Qc z+rsh@2UPi8#yj$-kZszD&pq2Jtyow5m->KtB`fA)dsL(tfWHCU{GM{hQqpJ(ix`B7 zklfEn`vgB<)i^p)m85k+5}qs3P+I~zr==|<0^(2Cp#w&A;oN5Mnjf5aqRUbba=12` zHdhzrb(FtWivxH*FiHEcJqNnHQoKsy0oKGs1Kvtt7B@7WziE9dWR-V*G@Ho=e&#&< zWY^3y(pAv=!?-mK1NpbbsZiZbm9Z7b54l-aCTI;W)t37ZuNu`$E@Jveos z-*d+y#@@iKr|*g`2pb8T2&f`y=%~e$f|wg5gpP~$0Tl2kcIW!f@vR^8mbZ3O&6TjZ V%2;pS-~MNNnyR`g50q?z{|7@9UTOdU diff --git a/addons/im_chat/views/im_chat_session_templates.xml b/addons/im_chat/views/im_chat_session_templates.xml deleted file mode 100644 index 7007e148699..00000000000 --- a/addons/im_chat/views/im_chat_session_templates.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - From 449f25463843767ba41a9a8c2adfdd4ceb2f2e5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9rome=20Maes?= Date: Tue, 11 Aug 2015 10:17:10 +0200 Subject: [PATCH 03/38] [REF] mail : include real time, and replace Timeline features This commit add instant messaging features in mail module to make it a real team collaboration tool. Lots of these new features will replace Timeleine view (which is not removed in this commit). Many files have been moved, split or created for a better structure. - My Channels : A user can be member of channel (mail.channel). These are discussion group, but also an aggregate of notification. Setting a channel as document follower, it will receive all the message (of the chatter) in real time, as information stream. - A new Inbox : this aims to replace the timeline view. The user will see its channels grouped by 'slot', and receive the message in real time. Jump to the form view, invite people, create private discussion group, talk directly to another employee, ... are the main features. Your message can still be starred. The 'Inbox' will only contain your needaction. - Needaction concept : a needaction is a message calling you to do something. If you follow a document as a person, the posted message will be 'needaction' for you. It can also be a message where you are mention with a '@my_name' (this feature is not added in this commit). - The chatter doesn't change so much. It still allow you to see the discussion about the document, to star some message, and treat your needaction. - Chat : the only way to have a minified conversation is from the Inbox Client Action. It allow you to keep an eye on a channel when working on document. - Compose Message : it is now common to the Inbox, and the chatter. It offers shortcode substitution, attachments management, and optional subtype. - UI : the chat window has been clean, they have a new look now. To do so, - Chatter code have been completely rewrite, independently of timeline view - Add 'bus' as mail dependency, to insure real time - A mail.message is broadcasted on the bus channel (db, mail.channel, channel_id), and a channel header on (db, res.partner, partner_id) - Extracting mail_thread mixin used for Chatter and ChatMailThread (Inbox client action). This uniformize the programming structure with the server side - Clean CSS style and use LESS instead of CSS - Define a new messafe format (message_format method in mail_message.py), compatible with fetching and broadcasting - Apply guideline coding conventions This commit also break the module im_livechat, since im_chat models don't exists anymore and will be fixed in the next commit. Thanks to Thibault Delavallee (tde) for the long debates and precious advices, Richard Mathot (rim) for the support, Simon Lejeune (sle) for the client action layout, and the Usability Team (apr, lle, bst yti) for the long testing. Conflicts: addons/mail/models/mail_thread.py --- addons/bus/models/__init__.py | 1 + addons/bus/models/bus_presence.py | 20 +- addons/bus/models/res_partner.py | 43 + addons/bus/models/res_users.py | 8 +- addons/im_livechat/__openerp__.py | 2 +- addons/im_odoo_support/__openerp__.py | 2 +- addons/mail/__openerp__.py | 7 +- addons/mail/controllers/bus.py | 68 +- addons/mail/controllers/main.py | 13 + addons/mail/data/mail_shortcode_data.xml | 125 +- addons/mail/models/mail_channel.py | 383 +++-- addons/mail/models/mail_message.py | 91 +- addons/mail/models/mail_shortcode.py | 33 +- addons/mail/models/mail_thread.py | 9 +- addons/mail/models/res_partner.py | 9 + addons/mail/security/ir.model.access.csv | 4 +- addons/mail/static/src/css/mail.css | 102 +- addons/mail/static/src/css/mail_channel.css | 29 - .../mail/static/src/css/mail_chat_backend.css | 136 +- .../mail/static/src/css/mail_chat_common.css | 282 +--- addons/mail/static/src/js/mail.js | 8 +- .../mail/static/src/js/mail_chat_backend.js | 1245 +++++++++++++---- addons/mail/static/src/js/mail_chat_common.js | 427 +++--- addons/mail/static/src/js/mail_chatter.js | 471 +++++++ addons/mail/static/src/js/mail_followers.js | 30 +- addons/mail/static/src/js/mail_thread.js | 430 ++++++ addons/mail/static/src/js/mail_utils.js | 105 +- .../static/src/less/mail_chat_backend.less | 212 +++ addons/mail/static/src/less/mail_chatter.less | 54 + addons/mail/static/src/less/mail_thread.less | 184 +++ .../mail/static/src/xml/mail_chat_backend.xml | 155 ++ .../mail/static/src/xml/mail_chat_common.xml | 187 ++- addons/mail/static/src/xml/mail_chatter.xml | 123 ++ addons/mail/static/src/xml/mail_followers.xml | 98 +- addons/mail/static/src/xml/mail_thread.xml | 186 +++ addons/mail/views/mail_channel_views.xml | 112 +- addons/mail/views/mail_shortcode_views.xml | 18 +- addons/mail/views/mail_templates.xml | 8 + addons/mail/wizard/invite.py | 2 +- 39 files changed, 4097 insertions(+), 1325 deletions(-) create mode 100644 addons/bus/models/res_partner.py create mode 100644 addons/mail/static/src/js/mail_chatter.js create mode 100644 addons/mail/static/src/js/mail_thread.js create mode 100644 addons/mail/static/src/less/mail_chat_backend.less create mode 100644 addons/mail/static/src/less/mail_chatter.less create mode 100644 addons/mail/static/src/less/mail_thread.less create mode 100644 addons/mail/static/src/xml/mail_chat_backend.xml create mode 100644 addons/mail/static/src/xml/mail_chatter.xml create mode 100644 addons/mail/static/src/xml/mail_thread.xml diff --git a/addons/bus/models/__init__.py b/addons/bus/models/__init__.py index 208eb0c6c81..3e2808e6e2e 100644 --- a/addons/bus/models/__init__.py +++ b/addons/bus/models/__init__.py @@ -2,3 +2,4 @@ import bus import bus_presence import res_users +import res_partner diff --git a/addons/bus/models/bus_presence.py b/addons/bus/models/bus_presence.py index d89cfc9a104..445e9491a75 100644 --- a/addons/bus/models/bus_presence.py +++ b/addons/bus/models/bus_presence.py @@ -13,17 +13,17 @@ DISCONNECTION_TIMER = TIMEOUT + 5 AWAY_TIMER = 600 # 10 minutes -class ImChatPresence(models.Model): - """ User Chat Presence +class BusPresence(models.Model): + """ User Presence Its status is 'online', 'away' or 'offline'. This model should be a one2one, but is not attached to res_users to avoid database concurrence errors. Since the 'update' method is executed at each poll, if the user have multiple opened tabs, concurrence errors can happend, but are 'muted-logged'. """ - _name = 'im_chat.presence' - _description = 'User Chat Presence' + _name = 'bus.presence' + _description = 'User Presence' - _sql_constraints = [('im_chat_user_status_unique', 'unique(user_id)', 'A user can only have one IM status.')] + _sql_constraints = [('bus_user_presence_unique', 'unique(user_id)', 'A user can only have one IM status.')] user_id = fields.Many2one('res.users', 'Users', required=True, index=True, ondelete='cascade') last_poll = fields.Datetime('Last Poll', default=lambda self: fields.Datetime.now()) @@ -43,14 +43,14 @@ class ImChatPresence(models.Model): send_notification = True values = { 'last_poll': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT), - 'status' : presence and presence.status or 'offline' + 'status': presence and presence.status or 'offline' } # update the user or a create a new one - if not presence: # create a new presence for the user + if not presence: # create a new presence for the user values['status'] = 'online' values['user_id'] = self._uid self.create(values) - else: # write the user presence if necessary + else: # write the user presence if necessary if user_presence: values['last_presence'] = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT) values['status'] = 'online' @@ -69,7 +69,7 @@ class ImChatPresence(models.Model): self.env.cr.commit() # TODO : check if still necessary # notify if the status has changed if send_notification: # TODO : add user_id to the channel tuple to allow using user_watch in controller presence - self.env['bus.bus'].sendone((self._cr.dbname, 'im_chat.presence'), {'id': self._uid, 'im_status': values['status']}) + self.env['bus.bus'].sendone((self._cr.dbname, 'bus.presence'), {'id': self._uid, 'im_status': values['status']}) # gc : disconnect the users having a too old last_poll. 1 on 100 chance to do it. if random.random() < 0.01: self.check_users_disconnection() @@ -83,5 +83,5 @@ class ImChatPresence(models.Model): presences.write({'status': 'offline'}) notifications = [] for presence in presences: - notifications.append([(self._cr.dbname, 'im_chat.presence'), {'id': presence.user_id.id, 'im_status': presence.status}]) + notifications.append([(self._cr.dbname, 'bus.presence'), {'id': presence.user_id.id, 'im_status': presence.status}]) self.env['bus.bus'].sendmany(notifications) diff --git a/addons/bus/models/res_partner.py b/addons/bus/models/res_partner.py new file mode 100644 index 00000000000..358f2a41298 --- /dev/null +++ b/addons/bus/models/res_partner.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +from openerp import api, fields, models + +class ResPartner(models.Model): + _inherit = 'res.partner' + + im_status = fields.Char('IM Status', compute='_compute_im_status') + + @api.multi + def _compute_im_status(self): + self.env.cr.execute(""" + SELECT P.id as id, B.status as im_status + FROM bus_presence B + JOIN res_users U ON B.user_id = U.id + JOIN res_partner P ON P.id = U.partner_id + WHERE P.id IN %s AND U.active = 't' + """, (tuple(self.ids),)) + fetch_result = self.env.cr.dictfetchall() + result = dict(((user_presence['id'], user_presence['im_status']) for user_presence in fetch_result)) + for partner in self: + partner.im_status = result.get(partner.id, 'offline') + + @api.model + def im_search(self, name, limit=20): + """ Search partner with a name and return its id, name and im_status. + Note : the user must be logged + :param name : the partner name to search + :param limit : the limit of result to return + """ + name = '%' + name + '%' + excluded_partner_ids = [self.env.user.partner_id.id] + self.env.cr.execute(""" + SELECT U.id as user_id, P.id as id, P.name as name, COALESCE(B.status, 'offline') as im_status + FROM res_users U + JOIN res_partner P ON P.id = U.partner_id + LEFT JOIN bus_presence B ON B.user_id = U.id + WHERE P.name ILIKE %s + AND P.id NOT IN %s + AND U.active = 't' + LIMIT %s + """, (name, tuple(excluded_partner_ids), limit)) + result = self.env.cr.dictfetchall() + return result diff --git a/addons/bus/models/res_users.py b/addons/bus/models/res_users.py index 5e9d664b6a8..2b7d7ccfe49 100644 --- a/addons/bus/models/res_users.py +++ b/addons/bus/models/res_users.py @@ -12,7 +12,7 @@ class ResUsers(models.Model): def _compute_im_status(self): """ Compute the im_status of the users """ res = dict((uid, 'offline') for uid in self.ids) - presences = self.env['im_chat.presence'].search([('user_id', 'in', self.ids)]) + presences = self.env['bus.presence'].search([('user_id', 'in', self.ids)]) for presence in presences: res[presence.user_id.id] = presence.status for user in self: @@ -37,7 +37,7 @@ class ResUsers(models.Model): # first query to find online employee self._cr.execute('''SELECT U.id as id, P.name as name, COALESCE(S.status, 'offline') as im_status - FROM im_chat_presence S + FROM bus_presence S JOIN res_users U ON S.user_id = U.id JOIN res_partner P ON P.id = U.partner_id WHERE '''+where_clause_base+''' @@ -52,7 +52,7 @@ class ResUsers(models.Model): # second query to find other online people if len(result) < limit: self._cr.execute('''SELECT U.id as id, P.name as name, COALESCE(S.status, 'offline') as im_status - FROM im_chat_presence S + FROM bus_presence S JOIN res_users U ON S.user_id = U.id JOIN res_partner P ON P.id = U.partner_id WHERE '''+where_clause_base+''' @@ -67,7 +67,7 @@ class ResUsers(models.Model): if len(result) < limit: self._cr.execute('''SELECT U.id as id, P.name as name, COALESCE(S.status, 'offline') as im_status FROM res_users U - LEFT JOIN im_chat_presence S ON S.user_id = U.id + LEFT JOIN bus_presence S ON S.user_id = U.id LEFT JOIN res_partner P ON P.id = U.partner_id WHERE '''+where_clause_base+''' AND U.id NOT IN %s diff --git a/addons/im_livechat/__openerp__.py b/addons/im_livechat/__openerp__.py index 5d460e6f58e..128d80489d4 100644 --- a/addons/im_livechat/__openerp__.py +++ b/addons/im_livechat/__openerp__.py @@ -30,7 +30,7 @@ Help your customers with this chat, and analyse their feedback. 'demo': [ "data/im_livechat_channel_demo.xml", ], - 'depends' : ["mail", "rating", "im_chat"], + 'depends' : ["mail", "rating"], 'installable': True, 'auto_install': False, 'application': True, diff --git a/addons/im_odoo_support/__openerp__.py b/addons/im_odoo_support/__openerp__.py index b68e1c872fd..1399441b50a 100644 --- a/addons/im_odoo_support/__openerp__.py +++ b/addons/im_odoo_support/__openerp__.py @@ -16,7 +16,7 @@ Ask your functionnal question directly to the Odoo Operators with the livechat s 'data': [ "views/im_odoo_support.xml" ], - 'depends' : ["web", "im_chat"], + 'depends' : ["web", "mail"], 'qweb': [ 'static/src/xml/im_odoo_support.xml' ], diff --git a/addons/mail/__openerp__.py b/addons/mail/__openerp__.py index 1e9149dc4fb..ca88214169e 100644 --- a/addons/mail/__openerp__.py +++ b/addons/mail/__openerp__.py @@ -25,7 +25,7 @@ Main Features * Includes an extensible generic email composition assistant, that can turn into a mass-mailing assistant and is capable of interpreting simple *placeholder expressions* that will be replaced with dynamic data when each email is actually sent. """, 'website': 'https://www.odoo.com/page/enterprise-social-network', - 'depends': ['base', 'base_setup'], + 'depends': ['base', 'base_setup', 'bus'], 'data': [ 'wizard/invite_view.xml', 'wizard/mail_compose_message_view.xml', @@ -40,7 +40,7 @@ Main Features 'views/res_config_views.xml', 'data/mail_data.xml', 'data/mail_channel_data.xml', - 'data/mail_shortcode.xml', + 'data/mail_shortcode_data.xml', 'security/mail_security.xml', 'security/ir.model.access.csv', 'views/mail_alias_views.xml', @@ -60,7 +60,10 @@ Main Features 'application': True, 'qweb': [ 'static/src/xml/mail.xml', + 'static/src/xml/mail_thread.xml', + 'static/src/xml/mail_chatter.xml', 'static/src/xml/mail_chat_common.xml', + 'static/src/xml/mail_chat_backend.xml', 'static/src/xml/mail_followers.xml', 'static/src/xml/announcement.xml', ], diff --git a/addons/mail/controllers/bus.py b/addons/mail/controllers/bus.py index 35ede172690..0a6e52a2514 100644 --- a/addons/mail/controllers/bus.py +++ b/addons/mail/controllers/bus.py @@ -6,12 +6,12 @@ from openerp import SUPERUSER_ID from openerp.http import request -class ChatController(openerp.addons.bus.controllers.main.BusController): +class MailChatController(openerp.addons.bus.controllers.main.BusController): def _default_request_uid(self): """ For Anonymous people, they receive the access right of SUPERUSER_ID since they have NO access (auth=none) !!! Each time a method from this controller is call, there is a check if the user (who can be anonymous and Sudo access) - can access to the ressource, thanks to 'is_in_session', (for instance). + can access to the ressource. """ return request.session.uid and request.session.uid or SUPERUSER_ID @@ -20,44 +20,42 @@ class ChatController(openerp.addons.bus.controllers.main.BusController): # -------------------------- def _poll(self, dbname, channels, last, options): if request.session.uid: - request.env['im_chat.presence'].update(options.get('im_presence')) - # channel to receive message - channels.append((request.db, 'im_chat.session', request.session.uid)) - ## For performance issue, the real time status notification is disabled. This means a change of status are still braoadcasted - ## but not received by anyone. Otherwise, all listening user restart their longpolling at the same time and cause a 'ConnectionPool Full Error' - ## since there is not enought cursors for everyone. Now, when a user open his list of users, an RPC call is made to update his user status list. - ##channels.append((request.db,'im_chat.presence')) - return super(ChatController, self)._poll(dbname, channels, last, options) + partner_id = request.env.user.partner_id.id + # mail channels to listen + for mail_channel in request.env['mail.channel'].search([('channel_partner_ids', 'in', [partner_id])]): + channels.append((request.db, 'mail.channel', mail_channel.id)) + # personal channel + channels.append((request.db, 'res.partner', partner_id)) + return super(MailChatController, self)._poll(dbname, channels, last, options) # -------------------------- # Anonymous routes (Common Methods) # -------------------------- - @openerp.http.route('/im_chat/post', type="json", auth="none") - def post(self, uuid, message_content, message_type): + @openerp.http.route('/mail/chat_post', type="json", auth="none") + def mail_chat_post(self, uuid, message_content, **kwargs): request_uid = self._default_request_uid() - return request.env["im_chat.message"].sudo(request_uid).send_message(request.session.uid, uuid, message_content, message_type) + # find the author from the user session, which can be None + author_id = False # message_post accept 'False' author_id, but not 'None' + if request.session.uid: + author_id = request.env['res.users'].sudo().browse(request.session.uid).partner_id.id + # post a message without adding followers to the channel. email_from=False avoid to get author from email data + mail_channel = request.env["mail.channel"].sudo(request_uid).search([('uuid', '=', uuid)], limit=1) + message = mail_channel.sudo(request_uid).with_context(mail_create_nosubscribe=True).message_post(author_id=author_id, email_from=False, body=message_content, message_type='comment', subtype='mail.mt_comment', **kwargs) + return message and message.id or False - @openerp.http.route(['/im_chat/image//', '/im_chat/image//'], type='http', auth="none") - def image(self, uuid, user_id): + @openerp.http.route(['/mail/chat_history'], type="json", auth="none") + def mail_chat_history(self, uuid, last_id=False, limit=20): request_uid = self._default_request_uid() - # anonymous will have a user_id = False, interpreted by string - if isinstance(user_id, (basestring, unicode)): - user_id = False - # get image for the people in the channel - image_b64 = request.env['im_chat.session'].sudo(request_uid).session_user_image(uuid, user_id) - image_data = base64.b64decode(image_b64) - headers = [('Content-Type', 'image/png')] - headers.append(('Content-Length', len(image_data))) - return request.make_response(image_data, headers) + channel = request.env["mail.channel"].sudo(request_uid).search([('uuid', '=', uuid)], limit=1) + return channel.sudo(request_uid).channel_fetch_message(last_id, limit) - @openerp.http.route(['/im_chat/history'], type="json", auth="none") - def history(self, uuid, last_id=False, limit=20): - request_uid = self._default_request_uid() - return request.env["im_chat.message"].sudo(request_uid).fetch_message(uuid, last_id, limit) - - # -------------------------- - # User routes (Only Logged User Methods) - # -------------------------- - @openerp.http.route('/im_chat/init', type="json", auth="user") - def init(self): - return request.env['im_chat.message'].get_init_notifications() + @openerp.http.route('/mail/chat_init', type="json", auth="none") + def mail_chat_init(self): + result = { + 'emoji': request.env['mail.shortcode'].sudo().search_read([('shortcode_type', '=', 'image')], ['source', 'substitution', 'description']) + } + # include the previous notifications, only for identified user + if request.session.uid: + request_uid = self._default_request_uid() + result['notifications'] = request.env['mail.channel'].sudo(request_uid).get_init_notifications() + return result diff --git a/addons/mail/controllers/main.py b/addons/mail/controllers/main.py index 30fc9c77d4f..edde14ffb6e 100644 --- a/addons/mail/controllers/main.py +++ b/addons/mail/controllers/main.py @@ -205,3 +205,16 @@ class MailController(http.Controller): except: return self._redirect_to_messaging() return werkzeug.utils.redirect('/mail/view?%s' % url_encode({'model': model, 'res_id': res_id})) + + @http.route('/mail/needaction', type='json', auth='user') + def needaction(self): + return request.env['res.partner'].get_needaction_count() + + @http.route('/mail/client_action', type='json', auth='user') + def mail_client_action(self): + values = { + 'needaction_inbox_counter': request.env['res.partner'].get_needaction_count(), + 'chatter_needaction_auto': request.env.user.chatter_needaction_auto, + 'channel_slots': request.env['mail.channel'].channel_fetch_slot() + } + return values diff --git a/addons/mail/data/mail_shortcode_data.xml b/addons/mail/data/mail_shortcode_data.xml index bd77ecbf8da..b85479ee30a 100644 --- a/addons/mail/data/mail_shortcode_data.xml +++ b/addons/mail/data/mail_shortcode_data.xml @@ -3,155 +3,180 @@ - + :) - <img src='/im_chat/static/src/img/smile.png'/> + <img src='/mail/static/src/img/smiley/smile.png'/> Emoji Smile + image - + :( - <img src='/im_chat/static/src/img/sad.png'/> + <img src='/mail/static/src/img/smiley/sad.png'/> Emoji Sad + image - + :D - <img src='/im_chat/static/src/img/laugh.png'/> + <img src='/mail/static/src/img/smiley/laugh.png'/> Emoji Laugh + image - + ;) - <img src='/im_chat/static/src/img/wink.png'/> + <img src='/mail/static/src/img/smiley/wink.png'/> Emoji Wink + image - + O_0 - <img src='/im_chat/static/src/img/stuck_out_tongue.png'/> + <img src='/mail/static/src/img/smiley/stuck_out_tongue.png'/> Emoji Stuck Out Tongue Winking Eye + image - + :@ - <img src='/im_chat/static/src/img/scream.png'/> + <img src='/mail/static/src/img/smiley/scream.png'/> Emoji Scream + image - + :| - <img src='/im_chat/static/src/img/neutral.png'/> + <img src='/mail/static/src/img/smiley/neutral.png'/> Emoji Speechless/Neutral face + image - + :p - <img src='/im_chat/static/src/img/yum.png'/> + <img src='/mail/static/src/img/smiley/yum.png'/> Emoji Cheeky + image - + :/ - <img src='/im_chat/static/src/img/confused.png'/> + <img src='/mail/static/src/img/smiley/confused.png'/> Emoji Confused + image - + :O - <img src='/im_chat/static/src/img/open_mouth.png'/> + <img src='/mail/static/src/img/smiley/open_mouth.png'/> Emoji Open Mouth + image - + &lt;3 - <img src='/im_chat/static/src/img/heart.png'/> + <img src='/mail/static/src/img/smiley/heart.png'/> Emoji Heart + image - + :+1 - <img src='/im_chat/static/src/img/thumbsup.png'/> + <img src='/mail/static/src/img/smiley/thumbsup.png'/> Emoji Thumbs Up + image - + :-1 - <img src='/im_chat/static/src/img/thumbsdown.png'/> + <img src='/mail/static/src/img/smiley/thumbsdown.png'/> Emoji Thumbs Down + image - + :kitten - <img src='/im_chat/static/src/img/cat.png'/> + <img src='/mail/static/src/img/smiley/cat.png'/> Emoji Kitten + image - + :pinky - <img src='/im_chat/static/src/img/pinky.png'/> + <img src='/mail/static/src/img/smiley/pinky.png'/> Emoji Pinky + image - + :musti - <img src='/im_chat/static/src/img/musti.png'/> + <img src='/mail/static/src/img/smiley/musti.png'/> Emoji Musti + image - + :turban - <img src='/im_chat/static/src/img/man_with_turban.png'/> + <img src='/mail/static/src/img/smiley/man_with_turban.png'/> Emoji Man With Turban + image - + :troll - <img src='/im_chat/static/src/img/trollface.png'/> + <img src='/mail/static/src/img/smiley/trollface.png'/> Emoji Troll Face + image - + :boar - <img src='/im_chat/static/src/img/boar.png'/> + <img src='/mail/static/src/img/smiley/boar.png'/> Emoji Boar + image - + :poop - <img src='/im_chat/static/src/img/poop.png'/> + <img src='/mail/static/src/img/smiley/poop.png'/> Emoji Poop + image - + :cookie - <img src='/im_chat/static/src/img/cookie.png'/> + <img src='/mail/static/src/img/smiley/cookie.png'/> Emoji Cookie + image - + :postal_horn - <img src='/im_chat/static/src/img/postal_horn.png'/> + <img src='/mail/static/src/img/smiley/postal_horn.png'/> Emoji Postal Horn + image - + :ghost - <img src='/im_chat/static/src/img/ghost.png'/> + <img src='/mail/static/src/img/smiley/ghost.png'/> Emoji Ghost + image - + :beer - <img src='/im_chat/static/src/img/beer.png'/> + <img src='/mail/static/src/img/smiley/beer.png'/> Emoji Beer + image - + :man - <img src='/im_chat/static/src/img/man.png'/> + <img src='/mail/static/src/img/smiley/man.png'/> Emoji Man + image diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py index 635a4999cce..6eeebcef21d 100644 --- a/addons/mail/models/mail_channel.py +++ b/addons/mail/models/mail_channel.py @@ -1,21 +1,28 @@ # -*- coding: utf-8 -*- +import datetime +import time import uuid from openerp import _, api, fields, models, modules, tools +from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT from openerp.exceptions import UserError +from openerp.addons.bus.models.bus_presence import AWAY_TIMER + class ChannelPartner(models.Model): _name = 'mail.channel.partner' _description = 'Last Seen Many2many' + _table = 'mail_channel_partner' _rec_name = 'partner_id' partner_id = fields.Many2one('res.partner', string='Recipient') channel_id = fields.Many2one('mail.channel', string='Channel') seen_message_id = fields.Many2one('mail.message', string='Last Seen') - state = fields.Selection([('open', 'Open'), ('folded', 'Folded'), ('closed', 'Closed')], string='Status', default='open') - seen_datetime = fields.Datetime('Last Seen Datetime') + fold_state = fields.Selection([('open', 'Open'), ('folded', 'Folded'), ('closed', 'Closed')], string='Conversation Fold State', default='open') + is_minimized = fields.Boolean("Conversation is minimied") + is_pinned = fields.Boolean("Is pinned on the interface", default=True) class Channel(models.Model): @@ -124,10 +131,6 @@ class Channel(models.Model): self._subscribe_users() return result - def _notify(self, message): - # DO SOMETHING USEFULL - return True - def _subscribe_users(self): for mail_channel in self: mail_channel.write({'channel_partner_ids': [(4, pid) for pid in mail_channel.mapped('group_ids').mapped('users').mapped('partner_id').ids]}) @@ -164,138 +167,310 @@ class Channel(models.Model): res['headers'] = repr(headers) return res - - # -------------------------- - # Session Methods - # -------------------------- @api.multi - def session_info(self): - """ Get the header of the current session - :returns the values of the session - :rtype : dict + @api.returns('self', lambda value: value.id) + def message_post(self, body='', subject=None, message_type='notification', subtype=None, parent_id=False, attachments=None, content_subtype='html', **kwargs): + # auto pin 'direct_message' channel partner + self.filtered(lambda channel: channel.channel_type == 'direct_message').mapped('channel_last_seen_partner_ids').write({'is_pinned': True}) + # apply shortcode (text only) subsitution + body = self.env['mail.shortcode'].apply_shortcode(body, shortcode_type='text') + message = super(Channel, self).message_post(body=body, message_type=message_type, subtype=subtype, content_subtype=content_subtype, **kwargs) + return message + + #------------------------------------------------------ + # Instant Messaging API + #------------------------------------------------------ + # A channel header should be broadcasted: + # - when adding user to channel (only to the new added partners) + # - when folding/minimizing a channel (only to the user making the action) + # A message should be broadcasted: + # - when a message is posted on a channel (to the channel, using _notify() method) + + # Anonymous method + @api.multi + def _broadcast(self, partner_ids): + """ Broadcast the current channel header to the given partner ids + :param partner_ids : the partner to notify """ - self.ensure_one() - info = { - 'uuid': self.uuid, - 'users': self.session_user_info(), - 'state': 'open', - } - # add user session state, if available and if user is logged - if self._uid: - domain = [('user_id', '=', self._uid), ('session_id', 'in', self.ids)] - session_state = self.env['im_chat.conversation_state'].search(domain, limit=1) - if session_state: - info['state'] = session_state.state - return info + notifications = [] + for partner in self.env['res.partner'].browse(partner_ids): + user_id = partner.user_ids and partner.user_ids[0] or False + if user_id: + for channel_info in self.sudo(user_id).channel_info(): + notifications.append([(self._cr.dbname, 'res.partner', partner.id), channel_info]) + self.env['bus.bus'].sendmany(notifications) @api.multi - def session_user_info(self): - """ Get the user infos for all the identified user in the session - :returns a list of user infos + def _notify(self, message): + """ Broadcast the given message on the current channels. + Send the message on the Bus Channel (uuid for public mail.channel, and partner private bus channel (the tuple)). + A partner will receive only on message on its bus channel, even if this message belongs to multiple mail channel. Then 'channel_ids' field + of the received message indicates on wich mail channel the message should be displayed. + :param : mail.message to broadcast + """ + message.ensure_one() + message_values = message.message_format()[0] + + notifications = [] + for channel in self: + message_values['channel_ids'] = [channel.id] + notifications.append([(self._cr.dbname, 'mail.channel', channel.id), dict(message_values)]) + # add uuid to allow anonymous to listen + if channel.public == 'public': + notifications.append([channel.uuid, dict(message_values)]) + self.env['bus.bus'].sendmany(notifications) + + @api.multi + def channel_info(self): + """ Get the informations header for the current channels + :returns a list of channels values + :rtype : list(dict) + """ + channel_infos = [] + partner_channels = self.env['mail.channel.partner'] + # find the channel partner state, if logged user + if self.env.user and self.env.user.partner_id: + partner_channels = self.env['mail.channel.partner'].search([('partner_id', '=', self.env.user.partner_id.id), ('channel_id', 'in', self.ids)]) + # for each channel, build the information header and include the logged partner information + for channel in self: + info = { + 'id': channel.id, + 'name': channel.name, + 'uuid': channel.uuid, + 'state': 'open', + 'is_minimized': False, + 'channel_type': channel.channel_type, + 'public': channel.public, + } + # add the partner for 'direct mesage' channel + if channel.channel_type == 'chat': + info['direct_partner'] = channel.sudo().channel_partner_ids.filtered(lambda p: p.id != self.env.user.partner_id.id).read(['id', 'name', 'im_status']) + # add user session state, if available and if user is logged + if partner_channels.ids: + partner_channel = partner_channels.filtered(lambda c: channel.id == c.channel_id.id) + if len(partner_channel) >= 1: + partner_channel = partner_channel[0] + info['state'] = partner_channel.fold_state or 'open' + info['is_minimized'] = partner_channel.is_minimized + info['seen_message_id'] = partner_channel.seen_message_id.id + # add needaction and unread counter, since the user is logged + info['message_needaction_counter'] = channel.message_needaction_counter + info['message_unread_counter'] = channel.message_unread_counter + channel_infos.append(info) + return channel_infos + + @api.multi + def channel_fetch_message(self, last_id=False, limit=20): + """ Return message values of the current channel. + :param last_id : last message id to start the research + :param limit : maximum number of messages to fetch + :returns list of messages values :rtype : list(dict) """ self.ensure_one() - return self.env['res.users'].browse(self.user_ids.ids).read(['id', 'name', 'im_status']) + domain = [("channel_ids", "in", self.ids)] + if last_id: + domain.append(("id", "<", last_id)) + return self.env['mail.message'].message_fetch(domain=domain, limit=limit) + # User methods @api.model - def session_user_image(self, uuid, user_id): - """ Get the avatar of a user in the given session - :param uuid : the uuid of the session - :param user_id : the user identifier - """ - # get the session - image_b64 = False - if user_id: - session = self.env["im_chat.session"].search([('uuid', '=', uuid)], limit=1) - if session and session.sudo(user_id).is_in_session(): - image_b64 = self.env["res.users"].sudo().browse(user_id).image_small - # set default image if not found - if not image_b64: - image_b64 = 'R0lGODlhAQABAIABAP///wAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==' - return image_b64 - - @api.model - def session_get(self, user_to): - """ Get the canonical session between 2 users, create it if needed. - To reuse an old session, this one must be private, and contains only 2 users. - :param user_to : the identifier of the user - :returns a session header, or False if the user_to was False + def channel_get(self, partners_to, pin=True): + """ Get the canonical private channel between some partners, create it if needed. + To reuse an old channel (conversation), this one must be private, and contains + only the given partners. + :param partners_to : list of res.partner ids to add to the conversation + :param pin : True if getting the channel should pin it for the current user + :returns a channel header, or False if the users_to was False :rtype : dict """ - if user_to: - session = self.search([('user_ids', 'in', user_to), ('user_ids', 'in', [self._uid])], limit=1) - if not (session and len(session.user_ids) == 2 and session.is_private()): - session = self.create({'user_ids': [(6, 0, (user_to, self._uid))]}) - return session.session_info() - - # -------------------------- - # Utils Methods - # -------------------------- - @api.multi - def is_private(self): - """ Return true if the session is private between identified users (no external messages). - The only way to do that is to check if there is a message without author (from_id is False). - """ - self.ensure_one() - message_ids = self.env["im_chat.message"].search([('to_id', 'in', self.ids), ('from_id', '=', None)]) - return len(message_ids) == 0 - - @api.multi - def is_in_session(self): - """ Return True if the current user is in the user_ids of the session. False otherwise. - Note : the user need to be logged - """ - self.ensure_one() - user_id = self._uid - return user_id and user_id in [u.id for u in self.user_ids] + if partners_to: + partners_to.append(self.env.user.partner_id.id) + # determine type according to the number of partner in the channel + self.env.cr.execute(""" + SELECT P.channel_id as channel_id + FROM mail_channel C, mail_channel_partner P + WHERE P.channel_id = C.id + AND C.public LIKE 'private' + AND P.partner_id IN %s + AND channel_type LIKE 'chat' + GROUP BY P.channel_id + HAVING COUNT(P.partner_id) = %s + """, (tuple(partners_to), len(partners_to),)) + result = self.env.cr.dictfetchall() + if result: + # get the existing channel between the given partners + channel = self.browse(result[0].get('channel_id')) + # pin up the channel for the current partner + if pin: + self.env['mail.channel.partner'].search([('partner_id', '=', self.env.user.partner_id.id), ('channel_id', '=', channel.id)]).write({'is_pinned': True}) + else: + # create a new one + channel = self.create({ + 'channel_partner_ids': [(6, 0, partners_to)], + 'public': 'private', + 'channel_type': 'chat', + 'name': ', '.join(self.env['res.partner'].sudo().browse(partners_to).mapped('name')), + }) + # broadcast the channel header to the other partner (not me) + channel._broadcast(partners_to) + return channel.channel_info()[0] + return False @api.model - def update_state(self, uuid, state=None): + def channel_fold(self, uuid, state=None): """ Update the fold_state of the given session. In order to syncronize web browser tabs, the change will be broadcast to himself (the current user channel). - Note : the user need to be logged - :param status : the new status of the session for the current user. + Note: the user need to be logged + :param state : the new status of the session for the current user. """ - domain = [('user_id', '=', self._uid), ('session_id.uuid', '=', uuid)] - for session_state in self.env['im_chat.conversation_state'].search(domain): + domain = [('partner_id', '=', self.env.user.partner_id.id), ('channel_id.uuid', '=', uuid)] + for session_state in self.env['mail.channel.partner'].search(domain): if not state: - state = session_state.state - if session_state.state == 'open': + state = session_state.fold_state + if session_state.fold_state == 'open': state = 'folded' else: state = 'open' - session_state.write({'state': state}) - self.env['bus.bus'].sendone((self._cr.dbname, 'im_chat.session', self._uid), session_state.session_id.session_info()) + session_state.write({ + 'fold_state': state, + 'is_minimized': bool(state != 'closed'), + 'seen_datetime': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT), # TODO JEM (to check) set seen_datetime to read needaction + }) + self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), session_state.channel_id.channel_info()[0]) + @api.model + def channel_minimize(self, uuid, minimized=True): + values = { + 'fold_state': minimized and 'open' or 'closed', + 'is_minimized': minimized + } + domain = [('partner_id', '=', self.env.user.partner_id.id), ('channel_id.uuid', '=', uuid)] + channel_partners = self.env['mail.channel.partner'].search(domain) + channel_partners.write(values) + self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), channel_partners.channel_id.channel_info()[0]) + + @api.model + def channel_pin(self, uuid, pinned=False): + # add the person in the channel, and pin it (or unpin it) + channel = self.search([('uuid', '=', uuid)]) + channel_partners = self.env['mail.channel.partner'].search([('partner_id', '=', self.env.user.partner_id.id), ('channel_id', '=', channel.id)]) + if channel_partners: + channel_partners.write({'is_pinned': pinned}) + + @api.multi + def channel_seen(self): + self.ensure_one() + if self.channel_message_ids.ids: + last_message_id = self.channel_message_ids.ids[0] # zero is the index of the last message + self.env['mail.channel.partner'].search([('channel_id', 'in', self.ids), ('partner_id', '=', self.env.user.partner_id.id)]).write({'seen_message_id': last_message_id}) + + @api.multi + def channel_invite(self, partner_ids): + """ Add the given partner_ids to the current channels and broadcast the channel header to them. + :param partner_ids : list of partner id to add + """ + partners = self.env['res.partner'].browse(partner_ids) + # add the partner + for channel in self: + partners_to_add = partners - channel.channel_partner_ids + channel.write({'channel_partner_ids': [(4, partner_id) for partner_id in partners_to_add.ids]}) + # broadcast the channel header to the added partner + self._broadcast(partner_ids) + + #------------------------------------------------------ + # Instant Messaging View Specific (Slack Client Action) + #------------------------------------------------------ @api.model def get_init_notifications(self): """ Get unread messages and old messages received less than AWAY_TIMER - ago and the session_info for open or folded window + ago of minimized channel ONLY. This aims to set the minimized channel + when refreshing the page. Note : the user need to be logged """ + # get current user's minimzed channel + minimized_channels = self.env['mail.channel.partner'].search([('is_minimized', '=', True), ('partner_id', '=', self.env.user.partner_id.id)]).mapped('channel_id') + # get the message since the AWAY_TIMER threshold = datetime.datetime.now() - datetime.timedelta(seconds=AWAY_TIMER) threshold = threshold.strftime(DEFAULT_SERVER_DATETIME_FORMAT) - domain = [('to_id.user_ids', 'in', [self._uid]), ('create_date', '>', threshold)] + domain = [('channel_ids', 'in', minimized_channels.ids), ('create_date', '>', threshold)] # get the message since the last poll of the user - presence = self.env['im_chat.presence'].search([('user_id', '=', self._uid)], limit=1) + presence = self.env['bus.presence'].search([('user_id', '=', self._uid)], limit=1) if presence: domain.append(('create_date', '>', presence.last_poll)) - messages = self.search_read(domain, ['from_id', 'to_id', 'create_date', 'type', 'message'], order='id asc') - # get the session of the messages and the not-closed ones - session_ids = [m['to_id'][0] for m in messages] - domain = [('user_id', '=', self._uid), '|', ('state', '!=', 'closed'), ('session_id', 'in', session_ids)] - session_states = self.env['im_chat.conversation_state'].search(domain) - # re-open the session where a message have been recieved recently - session_states.filtered(lambda r: r.state == 'closed').write({'state': 'folded'}) + # do the message search + message_values = self.env['mail.message'].message_fetch(domain=domain) - # create the notifications (session infos first, then messages) + # create the notifications (channel infos first, then messages) notifications = [] - for state in session_states: - session_infos = state.session_id.session_info() - notifications.append([(self._cr.dbname, 'im_chat.session', self._uid), session_infos]) - for message in messages: - notifications.append([(self._cr.dbname, 'im_chat.session', self._uid), message]) + for channel_info in minimized_channels.channel_info(): + notifications.append([(self._cr.dbname, 'res.partner', self.env.user.partner_id.id), channel_info]) + for message_value in message_values: + for channel_id in message_value['channel_ids']: + if channel_id in minimized_channels.ids: + message_value['channel_ids'] = [channel_id] + notifications.append([(self._cr.dbname, 'mail.channel', channel_id), dict(message_value)]) return notifications + + @api.model + def channel_fetch_slot(self): + """ Return the channels of the user grouped by 'slot' (channel, direct_message or private_group), and + the mapping between partner_id/channel_id for direct_message channels. + :returns dict : the grouped channels and the mapping + """ + values = {} + my_partner_id = self.env.user.partner_id.id + pinned_channels = self.env['mail.channel.partner'].search([('partner_id', '=', my_partner_id), ('is_pinned', '=', True)]).mapped('channel_id') + + # get the group/public channels + values['channel_channel'] = self.search([('channel_type', '=', 'channel'), ('public', 'in', ['public', 'groups']), ('channel_partner_ids', 'in', [my_partner_id])]).channel_info() + + # get the pinned 'direct message' channel + direct_message_channels = self.search([('channel_type', '=', 'chat'), ('id', 'in', pinned_channels.ids)]) + values['channel_direct_message'] = direct_message_channels.channel_info() + + # get the partner of the pinned 'direct message' channels with mapping partner/channel + direct_channel_partner = direct_message_channels.mapped('channel_last_seen_partner_ids').filtered(lambda cp: cp.partner_id.id != my_partner_id) + values['mapping'] = dict((item.partner_id.id, item.channel_id.id) for item in direct_channel_partner) + # partners of the 'direct message' channels + values['partners'] = direct_message_channels.mapped('channel_partner_ids').filtered(lambda partner: partner.id != my_partner_id).read(['id', 'name', 'im_status']) + + # get the private group + values['channel_private_group'] = self.search([('channel_type', '=', 'channel'), ('public', '=', 'private'), ('channel_partner_ids', 'in', [my_partner_id])]).channel_info() + return values + + @api.model + def channel_search_to_join(self, name=None, domain=None): + """ Return the channel info of the channel the current partner can join + :param name : the name of the researched channels + :param domain : the base domain of the research + :returns dict : channel dict + """ + if not domain: + domain = [] + domain += [('channel_type', '=', 'channel'), ('channel_partner_ids', 'not in', [self.env.user.partner_id.id])] + if name: + domain.append(('name', 'ilike', '%'+name+'%')) + return self.search(domain).read(['name', 'public', 'uuid', 'channel_type']) + + @api.multi + def channel_join_and_get_info(self): + self.ensure_one() + self.action_follow() + return self.channel_info()[0] + + @api.model + def channel_create(self, name, privacy='public'): + """ Create a channel and add the current partner, broadcast it (to make the user directly + listen to it when polling) + :param name : the name of the channel to create + :param privacy : privacy of the channel. Should be 'public' or 'private'. + :return dict : channel header + """ + # create the channel + new_channel = self.create({'name': name, 'public': privacy}) + new_channel.action_follow() + return new_channel.channel_info()[0] diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py index 8f180609475..08d5478a660 100644 --- a/addons/mail/models/mail_message.py +++ b/addons/mail/models/mail_message.py @@ -248,12 +248,12 @@ class Message(models.Model): tracking_value_ids.append(tracking_tree[tracking_value.id]) message_dict.update({ - 'is_author': pid == author[0], + #'is_author': pid == author[0], 'author_id': author, 'partner_ids': partner_ids, 'attachment_ids': attachment_ids, 'tracking_value_ids': tracking_value_ids, - 'user_pid': pid + #'user_pid': pid }) return True @@ -281,7 +281,8 @@ class Message(models.Model): 'record_name': self.record_name, 'subject': self.subject, 'date': self.date, - 'needaction': self.needaction, + #'needaction': self.needaction, JEM : not used. + 'needaction_partner_ids': self.needaction_partner_ids.ids, 'parent_id': parent_id, 'is_private': is_private, 'author_id': False, @@ -291,12 +292,13 @@ class Message(models.Model): 'is_favorite': self.starred, 'attachment_ids': [], 'tracking_value_ids': [], + 'channel_ids': self.channel_ids.ids, } @api.cr_uid_context def message_read_wrapper(self, cr, uid, ids=None, domain=None, context=None, thread_level=0, parent_id=False, limit=None, child_limit=None): - return self.message_read(cr, uid, ids, domain=domain, thread_level=thread_level, context=context, + return self.message_read(cr, uid, ids, domain=domain, thread_level=thread_level, context=context, parent_id=parent_id, limit=limit, child_limit=child_limit) @api.multi @@ -308,7 +310,7 @@ class Message(models.Model): well formed threads, if uid has access to them. After reading the messages, expandable messages are added in the - message list. It consists in messages holding the 'read more' data: + message list. It consists in messages holding the 'read more' data: number of messages to read, domain to apply. :param list ids: optional IDs to fetch @@ -320,7 +322,7 @@ class Message(models.Model): :param int limit: number of messages to fetch, before adding the ancestors and expandables :param int child_limit: number of child messages to fetch - :return dict: + :return dict: - int: number of messages read (status 'unread' to 'read') - list: list of threads [[messages_of_thread1], [messages_of_thread2]] """ @@ -397,7 +399,7 @@ class Message(models.Model): 'nb_messages': more_count, 'parent_id': parent} - if parent and thread_level: + if parent and thread_level: #insert expandable before parent message parent_tree[parent].insert(len(parent_tree[parent])-1, exp) else: @@ -408,7 +410,7 @@ class Message(models.Model): parent_list = parent_tree.values() parent_list = sorted(parent_list, key=lambda item: max([msg.get('id') for msg in item]), reverse=True) - #add 'thread' expandable + #add 'thread' expandable if thread_level: exp_domain = domain + [('id', '<', min(self.ids)), ('id', 'not in', parent_ids), ('parent_id', 'not in', parent_ids)] more_count = self.search_count(exp_domain) @@ -419,15 +421,80 @@ class Message(models.Model): 'parent_id': parent_id}]) nb_read = 0 - if context and 'mail_read_set_read' in context and context['mail_read_set_read']: + if context and 'mail_read_set_read' in context and context['mail_read_set_read']: nb_read = self.set_message_read(True, create_missing=False) else: nb_read = 0 - parent_list = [] + parent_list = [] return {'nb_read': nb_read, 'threads': parent_list} + + @api.model + def message_fetch(self, domain, limit=20): + return self.search(domain, limit=limit).message_format() + + @api.multi + def message_format(self): + """ Get the message values in the format for web client. Since message values can be broadcasted, + computed fields MUST NOT BE READ and broadcasted. + :returns list(dict). + Example : + { + 'body': HTML content of the message + 'model': u'res.partner', + 'record_name': u'Agrolait', + 'attachment_ids': [ + { + 'file_type_icon': u'webimage', + 'id': 45, + 'name': u'sample.png', + 'filename': u'sample.png' + } + ], + 'needaction_partner_ids': [], # list of partner ids + 'res_id': 7, + 'tracking_value_ids': [ + { + 'old_value': "", + 'changed_field': "Customer", + 'id': 2965, + 'new_value': "Axelor" + } + ], + 'author_id': (3, u'Administrator'), + 'author_avatar': .... # base64 of the author avatar + 'email_from': 'sacha@pokemon.com' # email address or False + 'subtype_id': (1, u'Discussions'), + 'channel_ids': [], # list of channel ids + 'date': '2015-06-30 08:22:33', + 'partner_ids': [[7, "Sacha Du Bourg-Palette"]], # list of partner name_get + 'message_type': u'comment', + 'id': 59, + 'subject': False + 'is_note': True # only if the subtype is internal + } + """ + message_values = self.read([ + 'id', 'body', 'date', 'author_id', 'author_avatar', 'email_from', # base message fields + 'message_type', 'subtype_id', 'subject', # message specific + 'model', 'res_id', 'record_name', # document related + 'channel_ids', 'partner_ids', # recipients + 'needaction_partner_ids', # list of partner ids for whom the message is a needaction + 'starred_partner_ids', # list of partner ids for whom the message is starred + ]) + message_tree = dict((m.id, m) for m in self) + self._message_read_dict_postprocess(message_values, message_tree) + + # add is_note flag + internal_subtype_ids = self.env['mail.message.subtype'].search([('internal', '=', True)]).ids + for message in message_values: + if message['subtype_id'] and message['subtype_id'][0]: + message['is_note'] = bool(message['subtype_id'][0] in internal_subtype_ids) + return message_values + + #------------------------------------------------------ # mail_message internals #------------------------------------------------------ @@ -750,7 +817,9 @@ class Message(models.Model): self_sudo = self.sudo() # TDE CHECK: add partners / channels as arguments to be able to notify a message with / without computation ?? - self.ensure_one() # tde: not sure, just for testing, will see + self.ensure_one() # tde: not sure, just for testinh, will see + partners = self.env['res.partner'] | self.partner_ids + channels = self.env['mail.channel'] | self.channel_ids # all followers of the mail.message document have to be added as partners and notified # and filter to employees only if the subtype is internal diff --git a/addons/mail/models/mail_shortcode.py b/addons/mail/models/mail_shortcode.py index 42627e5abe5..a28bd0df552 100644 --- a/addons/mail/models/mail_shortcode.py +++ b/addons/mail/models/mail_shortcode.py @@ -5,44 +5,55 @@ import re from openerp import api, fields, models -class ImChatShortcode(models.Model): +class MailShortcode(models.Model): """ Shortcode - Canned Responses, allowing the user to defined shortcuts in its chat message. - These shortcode are globals and are available for every user. Smiley use this mecanism. + Canned Responses, allowing the user to defined shortcuts in its message. Should be applied before storing message in database. + Emoji allowing replacing text with image for visual effect. Should be applied when the message is displayed (only for final rendering). + These shortcodes are global and are available for every user. """ - _name = 'im_chat.shortcode' + _name = 'mail.shortcode' _description = 'Canned Response / Shortcode' source = fields.Char('Shortcut', required=True, index=True, help="The shortcut which must be replace in the Chat Messages") substitution = fields.Char('Substitution', required=True, index=True, help="The excaped html code replacing the shortcut") description = fields.Char('Description') + shortcode_type = fields.Selection([('image', 'Smiley'), ('text', 'Canned Response')], required=True, default='text', + help="* Smiley are only used for HTML code to display an image "\ + "* Text (default value) is use to substitute text with another text") @api.model def create(self, values): if values.get('substitution'): values['substitution'] = self._sanitize_shorcode(values['substitution']) - return super(ImChatShortcode, self).create(values) + return super(MailShortcode, self).create(values) @api.multi def write(self, values): if values.get('substitution'): values['substitution'] = self._sanitize_shorcode(values['substitution']) - return super(ImChatShortcode, self).write(values) + return super(MailShortcode, self).write(values) def _sanitize_shorcode(self, substitution): """ Sanitize the shortcode substitution : - - HTML substitution : only allow the img tag (smiley) - - escape other substitutions to avoid XSS + - HTML substitution : only allow the img tag (emoji) + - escape other substitutions to avoid XSS """ - is_img_tag = re.match(r'''^$''', substitution, re.M|re.I) + is_img_tag = re.match(r'''^$''', substitution, re.M | re.I) if is_img_tag: return substitution return cgi.escape(substitution) @api.model - def replace_shortcode(self, message): - for shortcode in self.search([]): + def apply_shortcode(self, message, shortcode_type=None): + """ Apply the substitution on the given text + :param message : the text where the shortcode will be applied + :param shortcode_type : shortcode type that should be applied during the substitution + """ + domain = [] + if shortcode_type: + domain.append(('shortcode_type', '=', shortcode_type)) + for shortcode in self.search(domain): regex = '(?:^|\s)(%s)(?:\s|$)' % re.escape(shortcode.source) message = re.sub(regex, " " + shortcode.substitution + " ", message) return message diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index f8966608bf4..f295d2f8347 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -346,6 +346,9 @@ class MailThread(models.AbstractModel): ('res_model', 'in', [False, self._name]), ('internal', '=', True)], ['name', 'description', 'sequence']) options['internal_subtypes'] = internal_subtypes + # emoji list + options['emoji_list'] = self.env['mail.shortcode'].search([('shortcode_type', '=', 'image')]).read(['source', 'description', 'substitution']) + # save options on the node node.set('options', json.dumps(options)) res['arch'] = etree.tostring(doc) return res @@ -1855,9 +1858,9 @@ class MailThread(models.AbstractModel): @api.multi def message_set_read(self): - # Nothing done here currently. Will be implemented with the final - # slack modeling. - return True + messages = self.env['mail.message'].search([('model', '=', self._name), ('res_id', 'in', self.ids), ('needaction', '=', True)]) + messages.write({'needaction_partner_ids': [(3, self.env.user.partner_id.id)]}) + return messages.ids @api.multi def message_change_thread(self, new_thread): diff --git a/addons/mail/models/res_partner.py b/addons/mail/models/res_partner.py index 9034dbe4f19..3f12b8846bf 100644 --- a/addons/mail/models/res_partner.py +++ b/addons/mail/models/res_partner.py @@ -173,3 +173,12 @@ class Partner(models.Model): emails.send() return True + + @api.model + def get_needaction_count(self): + """ compute the number of needaction of the current user """ + self.env.cr.execute(""" + SELECT count(*) as needaction_count + FROM mail_message_res_partner_needaction_rel R + WHERE R.res_partner_id = %s """, (self.env.user.partner_id.id,)) + return self.env.cr.dictfetchall()[0].get('needaction_count') diff --git a/addons/mail/security/ir.model.access.csv b/addons/mail/security/ir.model.access.csv index 13410b21185..6471273b1db 100644 --- a/addons/mail/security/ir.model.access.csv +++ b/addons/mail/security/ir.model.access.csv @@ -27,4 +27,6 @@ access_mail_tracking_value_system,mail.tracking.value.system,model_mail_tracking access_mail_thread_all,mail.thread.all,model_mail_thread,,1,1,1,1 access_publisher_warranty_contract_all,publisher.warranty.contract.all,model_publisher_warranty_contract,,1,1,1,1 access_mail_template,mail.template,model_mail_template,base.group_user,1,1,1,0 -access_mail_template_system,mail.template_system,model_mail_template,base.group_system,1,1,1,1 \ No newline at end of file +access_mail_template_system,mail.template_system,model_mail_template,base.group_system,1,1,1,1 +access_mail_shortcode,mail.shortcode,model_mail_shortcode,base.group_user,1,1,1,1 +access_mail_shortcode_portal,mail.shortcode.portal,model_mail_shortcode,base.group_portal,1,0,0,0 diff --git a/addons/mail/static/src/css/mail.css b/addons/mail/static/src/css/mail.css index 2d14d8f98b0..522495a8fac 100644 --- a/addons/mail/static/src/css/mail.css +++ b/addons/mail/static/src/css/mail.css @@ -4,7 +4,7 @@ position: relative; color: white; } - + /* ----------------------- TIMELINE ----------------------- */ @@ -23,7 +23,7 @@ .o_web_client .o_timeline.o_timeline_inbox { padding-top: 15px; - padding-bottom: 15px; + padding-bottom: 15px; } .o_web_client .o_timeline.o_timeline_inbox .oe_thread { @@ -117,7 +117,7 @@ } .o_web_client .o_timeline .o_timeline_thread_parent .o_timeline_parent_message .o_timeline_parent_subject .o_timeline_nb_unread_msg { - font-weight: bold; + font-weight: bold; } .o_web_client .o_timeline .o_timeline_thread_parent .o_timeline_parent_message .o_timeline_parent_author { @@ -140,10 +140,10 @@ /* avatar */ - .o_web_client .o_timeline .o_timeline_thread_parent .o_timeline_parent_message .o_timeline_parent_avatar { - left:0; - top: 0; - bottom: 0; + .o_web_client .o_timeline .o_timeline_thread_parent .o_timeline_parent_message .o_timeline_parent_avatar { + left:0; + top: 0; + bottom: 0; width: 30px; overflow: hidden; padding-right: 3px; @@ -188,7 +188,7 @@ .o_web_client .o_timeline .o_timeline_thread_parent .o_timeline_parent_followers { display: inline-block; min-width: 45px; - margin-right: 10px; + margin-right: 10px; color: #337ab7; } @@ -337,7 +337,7 @@ .o_web_client .o_timeline button.oe_full, .o_web_client .o_timeline button.oe_attach { - background: transparent; + background: transparent; color: #337ab7; box-shadow: none; border: none; @@ -456,22 +456,22 @@ @-webkit-keyframes oe_mail_attach_loading_anim { 0% { background: #4BBD00 } - 50% { background: #009123 } + 50% { background: #009123 } 100% { background: #4BBD00 } } @-moz-keyframes oe_mail_attach_loading_anim { 0% { background: #4BBD00 } - 50% { background: #009123 } + 50% { background: #009123 } 100% { background: #4BBD00 } } @-o-keyframes oe_mail_attach_loading_anim { 0% { background: #4BBD00 } - 50% { background: #009123 } + 50% { background: #009123 } 100% { background: #4BBD00 } } @keyframes oe_mail_attach_loading_anim { 0% { background: #4BBD00 } - 50% { background: #009123 } + 50% { background: #009123 } 100% { background: #4BBD00 } } @@ -482,29 +482,29 @@ left: 0px; right: 0px; max-height: 64px; - background: rgba(0,0,0,0.8); + background: rgba(0,0,0,0.8); color: white; border-radius: 1px; border-top-left-radius: 0px; border-top-right-radius: 0px; opacity: 0; - -webkit-transition: opacity 0.2s linear; - -moz-transition: opacity 0.2s linear; - -o-transition: opacity 0.2s linear; - transition: opacity 0.2s linear; + -webkit-transition: opacity 0.2s linear; + -moz-transition: opacity 0.2s linear; + -o-transition: opacity 0.2s linear; + transition: opacity 0.2s linear; } .oe_attachment.oe_preview:hover .oe_name { opacity: 1; - -webkit-transition: opacity 0.2s linear; - -moz-transition: opacity 0.2s linear; - -o-transition: opacity 0.2s linear; - transition: opacity 0.2s linear; + -webkit-transition: opacity 0.2s linear; + -moz-transition: opacity 0.2s linear; + -o-transition: opacity 0.2s linear; + transition: opacity 0.2s linear; } .o_web_client .o_timeline .o_timeline_msg_footer button.oe_attach { width: 24px; - margin-bottom: 5px; + margin-bottom: 5px; filter: none; } @@ -521,7 +521,7 @@ cursor: pointer; } - .o_web_client .o_timeline .o_timeline_msg_footer .oe_attachment_file .oe_form_binary_file { + .o_web_client .o_timeline .o_timeline_msg_footer .oe_attachment_file .oe_form_binary_file { display: inline; position: relative; margin-left: -100px; @@ -538,7 +538,7 @@ vertical-align: middle; } - .o_web_client .o_timeline .oe_hidden_input_file, + .o_web_client .o_timeline .oe_hidden_input_file, .o_web_client .o_timeline .oe_hidden_input_file form { display: inline; cursor: pointer; @@ -557,26 +557,26 @@ text-shadow: 1px 0px white, -1px 0px white, 0px 1px white, 0px -1px white; cursor: pointer; opacity: 0; - -webkit-transition: opacity 0.2s linear; - -moz-transition: opacity 0.2s linear; - -o-transition: opacity 0.2s linear; - transition: opacity 0.2s linear; + -webkit-transition: opacity 0.2s linear; + -moz-transition: opacity 0.2s linear; + -o-transition: opacity 0.2s linear; + transition: opacity 0.2s linear; } .o_web_client .o_timeline .o_timeline_msg_composer .oe_attachment:hover .oe_delete .fa { opacity: 1; - -webkit-transition: opacity 0.2s linear; - -moz-transition: opacity 0.2s linear; - -o-transition: opacity 0.2s linear; - transition: opacity 0.2s linear; + -webkit-transition: opacity 0.2s linear; + -moz-transition: opacity 0.2s linear; + -o-transition: opacity 0.2s linear; + transition: opacity 0.2s linear; } - /* ------------ expandable message ------------ */ + /* ------------ expandable message ------------ */ - .o_web_client .o_timeline .o_timeline_msg.o_timeline_msg_expandable, - .o_web_client .o_timeline .o_timeline_parent_expandable { + .o_web_client .o_timeline .o_timeline_msg.o_timeline_msg_expandable, + .o_web_client .o_timeline .o_timeline_parent_expandable { color: #A4A4A4; - cursor: pointer; + cursor: pointer; margin-bottom: 0px; min-height: 30px; height: 30px; @@ -637,10 +637,10 @@ /* 2. avatar (message_left) */ .o_web_client .o_timeline .o_timeline_msg .o_timeline_msg_left { - float: left; - left:0; - top: 0; - bottom: 0; + float: left; + left:0; + top: 0; + bottom: 0; width: 45px; overflow: hidden; } @@ -677,11 +677,11 @@ -ms-border-bottom-right-radius: 4px; -o-border-bottom-right-radius: 4px; border-bottom-right-radius: 4px; - position: relative; - background: white; - margin-left: 53px; - padding: 5px 5px 11px 7px; - box-shadow: 0px 1px #CACACA; + position: relative; + background: white; + margin-left: 53px; + padding: 5px 5px 11px 7px; + box-shadow: 0px 1px #CACACA; } .o_web_client .o_timeline .o_timeline_msg.o_timeline_msg_comment .o_timeline_msg_center:after, /*********/ @@ -704,7 +704,7 @@ background-color: #DADADA; } .o_web_client .o_timeline .o_timeline_msg.o_timeline_msg_comment.o_timeline_msg_nobody .o_timeline_msg_center:after, - .o_web_client .o_timeline .o_timeline_msg.o_timeline_msg_email.o_timeline_msg_nobody .o_timeline_msg_center:after { + .o_web_client .o_timeline .o_timeline_msg.o_timeline_msg_email.o_timeline_msg_nobody .o_timeline_msg_center:after { border-right-color: #DADADA; } @@ -717,7 +717,7 @@ .o_web_client .o_timeline .o_timeline_msg .o_timeline_msg_content .o_timeline_header { font-weight: normal; - margin-bottom: 11px; + margin-bottom: 11px; } .o_web_client .o_timeline .o_timeline_msg .o_timeline_msg_content .o_timeline_header a { @@ -733,7 +733,7 @@ margin-right: 5px; } - /* message header - votes */ + /* message header - votes */ .o_web_client .o_timeline .o_timeline_vote { display: inline-block; @@ -927,8 +927,8 @@ text-overflow: ellipsis; } - .o_web_client .o_timeline_followers button.o_timeline_follower .o_timeline_follow, - .o_web_client .o_timeline_followers button.o_timeline_follower .o_timeline_unfollow, + .o_web_client .o_timeline_followers button.o_timeline_follower .o_timeline_follow, + .o_web_client .o_timeline_followers button.o_timeline_follower .o_timeline_unfollow, .o_web_client .o_timeline_followers button.o_timeline_follower .o_timeline_following { display: none; } diff --git a/addons/mail/static/src/css/mail_channel.css b/addons/mail/static/src/css/mail_channel.css index 51b04a4a9fc..923cdffe726 100644 --- a/addons/mail/static/src/css/mail_channel.css +++ b/addons/mail/static/src/css/mail_channel.css @@ -1,32 +1,3 @@ -/* ------------------------------ */ -/* Group Form */ -/* ------------------------------ */ - -/* Resize sheet width */ -.o_web_client .oe_form_sheetbg.o_web_client_mail_channel_sheet { - min-height: 0px; - max-height: none; -} - -.o_web_client .oe_form_sheetbg.o_web_client_mail_channel_sheet .oe_form_sheet { - min-height: 0px; - max-height: none; - padding: 0px 18px; -} - -.o_web_client .oe_form_sheetbg.o_web_client_mail_channel_sheet .oe_avatar { - padding-top: 3px; -} - -/* Resize footer width */ -.o_web_client .oe_form div.oe_mail_channel_footer { - max-width: 80%; -} - -/* Resize group description */ -.o_web_client .oe_form_sheetbg.o_web_client_mail_channel_sheet .oe_form_field_text > textarea { - height: 60px; -} /* ------------------------------ */ /* Group Kanban */ diff --git a/addons/mail/static/src/css/mail_chat_backend.css b/addons/mail/static/src/css/mail_chat_backend.css index d01577eb56a..d896befbf5e 100644 --- a/addons/mail/static/src/css/mail_chat_backend.css +++ b/addons/mail/static/src/css/mail_chat_backend.css @@ -1,12 +1,10 @@ + .oe_im { position: fixed; - background-color: #eaeaea; - width: 250px; + background-color: #E8EBEF; + width: 220px; border-left: 1px solid #AEB9BD; z-index: 500; - -webkit-box-shadow: -2px 0px 5px 0px rgba(50, 50, 50, 0.25); - -moz-box-shadow: -2px 0px 5px 0px rgba(50, 50, 50, 0.25); - box-shadow: -2px 0px 5px 0px rgba(50, 50, 50, 0.25); } @media print { @@ -23,92 +21,66 @@ top: -8px; } -/* search stuff */ -.oe_im_frame_header { - position: relative; - padding: 16px; -} -.oe_im_frame_header .oe_im_searchbox { - width: 200px; - padding: 1px 21px 1px 19px; - font-size: 13px; - border: none; - border-bottom: 1px solid #c6c6c6; - background: none; - margin-left: 20px; - height: 38px; -} -.oe_im_frame_header .oe_im_searchbox:focus { - outline: none; -} -.oe_im_frame_header .oe_im_search_icon { - position: absolute; - color: #888; - top: 28px; - left: 8px; - font-size: 16px; - font-weight: 300 !important; -} -.oe_im_frame_header .oe_im_search_clear { - display: none; - position: absolute; - right: 11px; - top: 4px; - font-size: 26px; - color: #b6b6b6; - cursor: pointer; -} -.oe_im_frame_header .oe_im_search_clear:hover { - color: #888; -} -/* users */ -.oe_im_users { - padding-bottom: 38px; + +/* +INSTANT MESSAGING VIEW +*/ +.o_mail_chat_im_container{ + margin: 0; height: 100%; +} +.o_mail_chat_im_row{ + height: inherit; +} +.o_mail_chat_im_container .o_mail_chat_im_sidebar{ + background-color: #f5f5f5; + height: inherit; overflow-y: scroll; } -.oe_im_user { - position: relative; - padding: 2px 8px; - cursor: pointer; - font-size: 13px; - margin-bottom: 10px; + +.o_mail_chat_im_container .o_mail_chat_im_content{ + height: inherit; } -.oe_im_user:hover { - background: lightGrey; +/* content of conversation */ +.o_mail_chat_im_container .o_mail_chat_im_content_wrap_conversation{ + height: inherit; } -.oe_im_user_clip { - display: inline-block; - width: 26px; - height: 26px; - margin-right: 4px; - -moz-box-shadow: 0 0 2px 1px rgba(0,0,0,0.25); - -webkit-box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.25); - box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.25); +.o_mail_chat_im_container .o_mail_chat_im_content_wrap_conversation .o_mail_chat_im_messages{ + height: 80%; + overflow-y: scroll; } -.oe_im_user_avatar { - width: 26px; - height: auto; +.o_mail_chat_im_container .o_mail_chat_im_content_wrap_conversation .o_mail_compose_message{ + height: 20%; + padding:10px; + background-color: #f5f5f5; } -.oe_im_user_name { - width: 162px; - line-height: 26px; - padding-right: 15px; - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - position: relative; +.o_mail_chat_im_container .o_mail_chat_im_messages .o_mail_chat_im_message{ + margin: 0; +} +/* content of personal inbox and starred */ +.o_mail_chat_im_container .o_mail_chat_im_content_wrap_personal{ + height: inherit; + overflow-y: scroll; } -.oe_im_user_online { - display: none; - position: absolute; - top: 9.5px; - right: 11px; - width: 11px; - height: 11px; - vertical-align: middle; - border: 0; +/* message row */ +.o_mail_chat_im_container .o_mail_chat_im_message .o_mail_chat_im_star{ + opacity: 0.5; + color: black; } +.o_mail_chat_im_container .o_mail_chat_im_message .o_mail_chat_im_star:hover{ + opacity: 1; + cursor: pointer; +} +.o_mail_chat_im_container .o_mail_chat_im_message .o_mail_chat_im_star.o_mail_starred{ + color: orange; +} + + + + +.o_mail_chat_im_search_partner{ + height: 50px; +} \ No newline at end of file diff --git a/addons/mail/static/src/css/mail_chat_common.css b/addons/mail/static/src/css/mail_chat_common.css index 1ad2981eb41..97119ed506c 100644 --- a/addons/mail/static/src/css/mail_chat_common.css +++ b/addons/mail/static/src/css/mail_chat_common.css @@ -1,92 +1,51 @@ - - -.openerp_style { /* base style of openerp */ - font-family: "Lucida Grande", Helvetica, Verdana, Arial, sans-serif; - color: #4c4c4c; - font-size: 13px; - background: white; +/* emoji size */ +.o_mail_emoji img{ + height: 20px; } -/* button */ +// ------------------------------------------------------------------ +// Chat Window +// ------------------------------------------------------------------ -.oe_chat_button { - position: fixed; - bottom: 0px; - right: 6px; - display: inline-block; - min-width: 100px; - background-color: rgba(60, 60, 60, 0.6); - font-family: 'Lucida Grande', 'Lucida Sans Unicode', Arial, Verdana, sans-serif; - font-size: 14px; - font-weight: bold; - padding: 10px; - color: white; - text-shadow: rgb(59, 76, 88) 1px 1px 0px; - border: 1px solid rgb(80, 80, 80); - border-bottom: 0px; - border-top-left-radius: 5px; - border-top-right-radius: 5px; - cursor: pointer; - z-index:500; +@media screen and (max-width:360px) { + .o_mail_chat_im_window { + width: 100%; + margin-right: 0; + } + .o_mail_chat_im_window .o_mail_chat_im_window_content { + width: 100% !important; + } + .o_mail_chat_im_window .o_mail_chat_im_window_input { + width: 100% !important; + } } -/* conversations */ -.oe_im_chatview { +.o_mail_chat_im_window { position: fixed; overflow: hidden; margin-bottom: 5px; margin-right: 6px; - background-color: #fff; + background: rgba(250, 250, 250, 0.95); border: 1px solid #DEDEDE; width: 240px; z-index:500; } -@media screen and (max-width:360px) { - .oe_im_chatview { - width: 100%; - margin-right: 0; - } - .oe_im_chatview .oe_im_chatview_content { - width: 100% !important; - } - .oe_im_chatview .oe_im_chatview_input { - width: 100% !important; - } -} -.oe_im_chatview .oe_im_chatview_disconnected { - display:none; - z-index: 100; - width: 100%; - background: #E8EBEF; - padding: 5px; - font-size: 11px; - color: #999; - line-height: 14px; - height: 28px; - overflow: hidden; -} -.oe_im_chatview.oe_im_chatview_disconnected_status .oe_im_chatview_disconnected { - display: block; -} - - -/* ####### Chat View : Header ####### */ -.oe_im_chatview_header { +/* HEADER */ +.o_mail_chat_im_window .o_mail_chat_im_window_header { padding: 3px 6px 2px; + cursor: pointer; background: #DEDEDE; background: -moz-linear-gradient(#FCFCFC, #DEDEDE); background: -webkit-gradient(linear, left top, left bottom, from(#FCFCFC), to(#DEDEDE)); border-radius: 3px 3px 0 0; -moz-border-radius: 3px 3px 0 0; -webkit-border-radius: 3px 3px 0 0; - border-bottom: 1px solid #AEB9BD; - cursor: pointer; } -.oe_im_chatview_header .header_name{ +.o_mail_chat_im_window .o_mail_chat_im_window_header_name { max-width: 75%; word-wrap: break-word; display: inline-block; @@ -96,164 +55,61 @@ text-overflow: ellipsis; } -.oe_im_chatview_header .right_buttons { +.o_mail_chat_im_window .o_mail_chat_im_window_header_buttons{ float: right; } -.oe_im_chatview_header .right_buttons > * { - display: inline-block; - border: 0; - cursor: pointer; - color: gray ; - background: transparent; - font-size: 16px; - line-height: 16px; - margin-left: 5px; - -webkit-appearance: none; -} - -.oe_im_chatview_header .right_buttons .dropdown-menu{ - right: 0; - left: auto; -} -.oe_im_chatview_header .right_buttons .dropdown-menu > li{ - display: block; - padding: 3px 20px; - clear: both; - font-weight: normal; - line-height: 1.428571429; - color: #4c4c4c; - white-space: nowrap; -} - -.oe_im_chatview .oe_im_chatview_content { - overflow: auto; - height: 287px; - width: 240px; -} -.oe_im_chatview.oe_im_chatview_disconnected_status .oe_im_chatview_content { - height: 249px; -} -.oe_im_chatview .oe_im_chatview_footer { - position: relative; - padding: 3px; - border-top: 1px solid #AEB9BD; - background: #DEDEDE; - background: -moz-linear-gradient(#FCFCFC, #DEDEDE); - background: -webkit-gradient(linear, left top, left bottom, from(#FCFCFC), to(#DEDEDE)); - -moz-border-radius: 0 0 3px 3px; - -webkit-border-radius: 0 0 3px 3px; - border-radius: 0 0 3px 3px; -} -.oe_im_chatview .oe_im_chatview_input { - width: 222px; - font-family: Lato, Helvetica, sans-serif; - font-size: 13px; - color: #333; - padding: 1px 5px; - border: 1px solid #AEB9BD; - -moz-border-radius: 3px; - -webkit-border-radius: 3px; - border-radius: 3px; - -moz-box-shadow: inset 0 1px 4px rgba(0,0,0,0.2); - -webkit-box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.2); - box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.2); -} -.oe_im_chatview .oe_im_chatview_message_bubble { - background: white; - position: relative; - min-height: 32px; - padding: 3px; - margin: 3px; - -moz-border-radius: 3px; - -webkit-border-radius: 3px; - border-radius: 3px; +/* CONTENT (messages, ...) */ +.o_mail_chat_im_window .o_mail_chat_im_window_content { + height: 280px; + overflow-y: scroll; } -/* Smiley size */ -.oe_im_chatview .oe_im_chatview_bubble_item img{ - width: 18px; -} - -.oe_im_chatview .oe_im_chatview_technical_bubble { - background: #D8D8D8; - position: relative; - min-height: 32px; - padding: 3px; - margin: 3px; - -moz-border-radius: 3px; - -webkit-border-radius: 3px; - border-radius: 3px; - - font-style:italic; -} - -.oe_im_chatview .oe_im_chatview_clip { - position: relative; - float: left; - width: 26px; - height: 26px; - margin-right: 4px; - -moz-box-shadow: 0 0 2px 1px rgba(0,0,0,0.25); - -webkit-box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.25); - box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.25); -} -.oe_im_chatview .oe_im_chatview_avatar { - float: left; - width: 26px; - height: auto; - clip: rect(0, 26px, 26px, 0); - max-width: 100%; - width: auto 9; - height: auto; - vertical-align: middle; - border: 0; - -ms-interpolation-mode: bicubic; -} -.oe_im_chatview .oe_im_chatview_time { - position: absolute; - right: 0px; - top: 0px; - margin: 3px; +.o_mail_chat_im_window .o_mail_chat_im_messages .o_mail_chat_im_window_date_sperator{ + padding: 5px; + padding-right: 10px; text-align: right; - line-height: 13px; - font-size: 11px; - color: #999; - width: 60px; - overflow: hidden; -} -.oe_im_chatview .oe_im_chatview_from { - margin: 0 0 2px 0; - line-height: 14px; font-weight: bold; - font-size: 12px; - width: 140px; - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - color: #3A87AD; -} -.oe_im_chatview .oe_im_chatview_bubble_list { -} -.oe_im_chatview .oe_im_chatview_bubble_item { - margin: 0 0 2px 30px; - line-height: 14px; - word-wrap: break-word; } -.oe_im_chatview_online { - margin-top: -4px; - width: 11px; - height: 11px; +.o_mail_chat_im_window .o_mail_chat_im_message{ + margin: auto; + margin-bottom: 5px; } -.oe_im_chatview .oe_im_chatview_date_separator { - background: transparent; - position: relative; - padding: 3px; - color: #aaa; - margin: 3px; - text-align: right; - font-size: 12px; -} \ No newline at end of file +.o_mail_chat_im_window .o_mail_chat_im_message .o_mail_chat_im_message_avatar{ + width: 20%; + display: inline-block; + vertical-align: top; +} + +.o_mail_chat_im_window .o_mail_chat_im_message .o_mail_chat_im_message_avatar img{ + width: 80%; + float: right; +} + +.o_mail_chat_im_window .o_mail_chat_im_message .o_mail_chat_im_message_core{ + width: 75%; + display: inline-block; + padding: 5px; + + background: white; + border-radius: 2px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); + max-width:100%; +} + +.o_mail_chat_im_window .o_mail_chat_im_message .o_mail_chat_im_message_core p{ + margin: 0; +} + +/* FOOTER */ +.o_mail_chat_im_window .o_mail_chat_im_window_footer { + padding:3px; + background: #DEDEDE; +} + +.o_mail_chat_im_window .o_mail_chat_im_window_footer .o_mail_chat_im_window_input { + width: 100%; +} diff --git a/addons/mail/static/src/js/mail.js b/addons/mail/static/src/js/mail.js index 9dd4b2c508d..8064fca4ee1 100644 --- a/addons/mail/static/src/js/mail.js +++ b/addons/mail/static/src/js/mail.js @@ -1167,7 +1167,7 @@ var ComposeMessage = Attachment.extend ({ var names_to_find = _.pluck(recipients_to_find, 'full_name'); var recipients_to_check = _.filter(recipients, function (recipient) { return (recipient.partner_id && ! recipient.email_address); }); var recipient_ids = _.pluck(_.filter(recipients, function (recipient) { return recipient.partner_id && recipient.email_address; }), 'partner_id'); - + var names_to_remove = []; var recipient_ids_to_remove = []; @@ -1236,7 +1236,7 @@ var ComposeMessage = Attachment.extend ({ }); }); }); - + return check_done; }, @@ -1445,10 +1445,10 @@ var ComposeMessageTopButton = Widget.extend({ }); // Put the ComposeMessageTopButton widget in the systray menu -SystrayMenu.Items.push(ComposeMessageTopButton); +//SystrayMenu.Items.push(ComposeMessageTopButton); core.view_registry.add('timeline', TimelineView); -core.form_widget_registry.add('mail_thread', TimelineRecordThread); +//core.form_widget_registry.add('mail_thread', TimelineRecordThread); return { TimelineView: TimelineView, diff --git a/addons/mail/static/src/js/mail_chat_backend.js b/addons/mail/static/src/js/mail_chat_backend.js index cd1cc00fdf2..9353afb68ab 100644 --- a/addons/mail/static/src/js/mail_chat_backend.js +++ b/addons/mail/static/src/js/mail_chat_backend.js @@ -1,301 +1,1032 @@ -odoo.define('im_chat.im_chat', function (require) { +odoo.define('mail.chat_backend', function (require) { "use strict"; -// to do: make this work in website - var bus = require('bus.bus'); var core = require('web.core'); var data = require('web.data'); -var Model = require('web.DataModel'); -var session = require('web.session'); +var Model = require('web.Model'); +var pyeval = require('web.pyeval'); var SystrayMenu = require('web.SystrayMenu'); +var Widget = require('web.Widget'); +var Dialog = require('web.Dialog'); +var ControlPanelMixin = require('web.ControlPanelMixin'); +var SearchView = require('web.SearchView'); +var WebClient = require('web.WebClient'); + +var session = require('web.session'); var utils = require('web.utils'); var web_client = require('web.web_client'); -var Widget = require('web.Widget'); -var im_chat_common = require('im_chat.im_chat_common'); + +var mail_utils = require('mail.utils'); +var mail_chat_common = require('mail.chat_common'); +var mail_thread = require('mail.thread'); var _t = core._t; var QWeb = core.qweb; - -var USERS_LIMIT = 20; +var internal_bus = core.bus; -// ########## CONVERSATION extentions ############### - -im_chat_common.ConversationManager.include({ - _message_receive: function(message){ +/** + * Widget handeling the channels, in the backend + * + * Responsible to listen the bus and apply action with the received message. Add layer to coordinate the + * folded conversation and trigger event for the InstantMessagingView client action (using internal + * comminication bus). It is a component of the WebClient. + **/ +var ConversationManagerBackend = mail_chat_common.ConversationManager.extend({ + _setup: function(init_data){ var self = this; - var session_id = message.to_id[0]; - var conv = this.sessions[message.to_id[1]]; - if(!conv){ - // fetch the session, and init it with the message - var def_session = new Model("im_chat.session").call("session_info", [], {"ids" : [session_id]}).then(function(session){ - conv = self.session_apply(session, {'force_open': true}); - conv.message_receive(message); - }); - }else{ - this._super(message); - } - }, -}); - -im_chat_common.Conversation.include({ - // user actions - prepare_action_menu: function(){ - this._super(); - this._add_action(_t('Shortcuts'), 'im_chat_option_shortcut', 'fa fa-info-circle', this.action_shorcode); - this._add_action(_t('Quit discussion'), 'im_chat_option_quit', 'fa fa-minus-square', this.action_quit_session); - }, - action_shorcode: function(e){ - return web_client.action_manager.do_action({ - type: 'ir.actions.act_window', - name : _t('Shortcode'), - res_model: 'im_chat.shortcode', - view_mode: 'tree,form', - view_type: 'tree', - views: [[false, 'list'], [false, 'form']], - target: "new", - limit: 80, - flags: { - action_buttons: true, - pager: true, - } - }); - }, - action_quit_session: function(e){ - var self = this; - var Session = new Model("im_chat.session"); - return Session.call("quit_user", [this.get("session").uuid]).then(function(res) { - if(! res){ - self.do_warn(_t("Warning"), _t("You are only 2 identified users. Just close the conversation to leave.")); - } - }); - }, - // session - session_update_state: function(state){ - var self = this; - var args = arguments; - var super_call = this._super; - // broadcast the state changing - return new Model("im_chat.session").call("update_state", [], {"uuid" : this.get("session").uuid, "state" : state}).then(function(){ - super_call.apply(self, args); + this._super.apply(this, arguments); + _.each(init_data['notifications'], function(n){ + self.on_notification(n); }); }, // window title window_title_change: function() { - this.super(); + this._super.apply(this, arguments); var title = undefined; if (this.get("waiting_messages") !== 0) { - title = _.str.sprintf(_t("%d Messages"), this.get("waiting_messages")) + title = _.str.sprintf(_t("%d Messages"), this.get("waiting_messages")); } web_client.set_title_part("im_messages", title); }, - // TODO : change this way - add_user: function(user){ - return new Model("im_chat.session").call("add_user", [this.get("session").uuid , user.id]); - }, -}); - - - -// ###### BACKEND : contact panel, top menu button ######### - -var UserWidget = Widget.extend({ - template: "im_chat.UserWidget", - events: { - "click": "activate_user", - }, - init: function(parent, user) { - this._super(parent); - this.set("id", user.id); - this.set("name", user.name); - this.set("im_status", user.im_status); - this.set("image_url", user.image_url); - }, - start: function() { - this.$el.data("user", {id:this.get("id"), name:this.get("name")}); - this.$el.draggable({helper: "clone"}); - this.on("change:im_status", this, this.update_status); - this.update_status(); - }, - update_status: function(){ - this.$(".oe_im_user_online").toggle(this.get('im_status') !== 'offline'); - var img_src = (this.get('im_status') == 'away' ? '/im_chat/static/src/img/yellow.png' : '/im_chat/static/src/img/green.png'); - this.$(".oe_im_user_online").attr('src', img_src); - }, - activate_user: function() { - this.trigger("user_clicked", this.get("id")); - }, -}); - -var InstantMessaging = Widget.extend({ - template: "im_chat.InstantMessaging", - events: { - "keydown .oe_im_searchbox": "input_change", - "keyup .oe_im_searchbox": "input_change", - "change .oe_im_searchbox": "input_change", - }, - init: function(parent) { - this._super(parent); - this.shown = false; - this.set("right_offset", 0); - this.set("current_search", ""); - this.users = []; - this.widgets = {}; - - // listen bus - this.bus = bus.bus; - this.bus.on("notification", this, this.on_notification); - }, - start: function() { - var self = this; - // ui - this.conv_manager = new im_chat_common.ConversationManager(this); - this.on("change:right_offset", this.conv_manager, _.bind(function() { - this.conv_manager.set("right_offset", this.get("right_offset")); - }, this)); - $(window).scroll(_.bind(this.position_compute, this)); - $(window).resize(_.bind(this.position_compute, this)); - this.$el.css("right", -this.$el.outerWidth()); - this.position_compute(); - - // business - this.on("change:current_search", this, this.user_search); - - // add a drag & drop listener - self.conv_manager.on("im_session_activated", self, function(conv) { - conv.$el.droppable({ - drop: function(event, ui) { - conv.add_user(ui.draggable.data("user")); - } - }); - }); - - // fetch the unread message and the recent activity (e.i. to re-init in case of refreshing page) - return session.rpc("/im_chat/init", {}).then(function(notifications) { - _.each(notifications, function(notif){ - self.conv_manager.on_notification(notif, {'load_history': true}); - }); - // start polling - self.bus.start_polling(); - }); - }, - on_notification: function(notification){ - var channel = notification[0]; - var message = notification[1]; - // user status notification - if(channel[1] === 'im_chat.presence'){ - if(message.im_status){ - this.user_update_status([message]); - } + // sessions and messages + session_apply: function(active_session, options){ + // for chat windows + if(active_session.is_minimized || (!active_session.is_minimized && active_session.state === 'closed')){ + this._super.apply(this, arguments); + } + // for client action + if(!active_session.is_minimized){ // if not minimized, + internal_bus.trigger('mail_session_receive', active_session); } }, - // ui - position_compute: function() { - var top = 48; - this.$el.css("top", top); - this.$el.css("bottom", 0); - }, - input_change: function() { - this.set("current_search", this.$(".oe_im_searchbox").val()); - }, - switch_display: function() { - this.position_compute(); - var fct = _.bind(function(place) { - this.set("right_offset", place + this.$el.outerWidth()); - this.$(".oe_im_searchbox").focus(); - }, this); - var opt = { - step: fct, - }; - if (this.shown) { - this.$el.animate({ - right: -this.$el.outerWidth(), - }, opt); - } else { - if (! bus.bus.activated) { - this.do_warn("Instant Messaging is not activated on this server. Try later.", ""); - return; - } - // update the list of user status when show the IM - this.user_search(); - this.$el.animate({ - right: 0, - }, opt); + message_receive: function(message) { + var actual_channel_ids = _.map(_.keys(this.sessions), function(item){ + return parseInt(item); + }); + var message_channel_ids = message.channel_ids; + if(_.intersection(actual_channel_ids, message_channel_ids).length){ + this._super.apply(this, arguments); + } + // broadcast the message to the NotificationButton and the InstantMessagingView + internal_bus.trigger('mail_message_receive', message); + // increment the needaction top counter + if(message.needaction_partner_ids && _.contains(message.needaction_partner_ids, session.partner_id)){ + internal_bus.trigger('mail_needaction_new', 1); } - this.shown = ! this.shown; }, - // user methods - user_search: function(e) { - var self = this; - var user_model = new Model("res.users"); - return user_model.call('im_search', [this.get("current_search"), USERS_LIMIT]).then(function(result){ - self.$(".oe_im_input").val(""); - var old_widgets = self.widgets; - self.widgets = {}; - self.users = []; - _.each(result, function(user) { - user.image_url = session.url('/web/image', {model:'res.users', field: 'image_small', id: user.id}); - var widget = new UserWidget(self, user); - widget.appendTo(self.$(".oe_im_users")); - widget.on("user_clicked", self, self.user_clicked); - self.widgets[user.id] = widget; - self.users.push(user); - }); - _.each(old_widgets, function(w) { - w.destroy(); - }); - }); - }, - user_clicked: function(user_id) { - var self = this; - var sessions = new Model("im_chat.session"); - return sessions.call("session_get", [user_id]).then(function(session) { - self.conv_manager.session_apply(session, {'focus': true, 'force_open': true}); - }); - }, - user_update_status: function(users_list){ - var self = this; - _.each(users_list, function(el) { - if (self.widgets[el.id]) { - self.widgets[el.id].set("im_status", el.im_status); - } - }); - } }); -var ImTopButton = Widget.extend({ - template:'im_chat.ImTopButton', +/** + * Widget Minimized Conversation + * + * Add layer of WebClient integration, and user fold state handling (comminication with server) + **/ +mail_chat_common.Conversation.include({ + session_fold: function(state){ + var self = this; + var args = arguments; + var super_call = this._super; + // broadcast the state changing + return new Model("mail.channel").call("channel_fold", [], {"uuid" : this.get("session").uuid, "state" : state}).then(function(){ + super_call.apply(self, args); + }); + }, +}); + +/** + * Widget : Patch for WebClient + * + * Create the conversation manager, and attach it to the web_client. + **/ +WebClient.include({ + show_application: function(){ + var self = this; + var args = arguments; + var super_call = this._super; + this.mail_conversation_manager = new ConversationManagerBackend(this); + this.mail_conversation_manager.start().then(function(){ + super_call.apply(self, args); + self.mail_conversation_manager.bus.start_polling(); + }); + }, +}); + + +/** + * Widget Top Menu Notification Counter + * + * Counter of notification in the Systray Menu. Need to know if InstantMessagingView is displayed to + * increment (or not) the counter. On click, should redirect to the client action. + **/ +var NotificationTopButton = Widget.extend({ + template:'mail.chat.NotificationTopButton', events: { - "click": "clicked", + "click": "on_click", + }, + init: function(parent){ + this._super.apply(this, arguments); + this.set('counter', 0); + }, + willStart: function(){ + var self = this; + return $.when(session.rpc('/mail/needaction'), this._super()).then(function(needaction_count){ + self.set('counter', needaction_count); + }); }, start: function() { - // Create the InstantMessaging widget and put it in the DOM - var im = new InstantMessaging(this); - web_client.im_messaging = im; - im.appendTo(web_client.$el); - // Bind the click action to the ImTopButton - this.on("clicked", im, im.switch_display); + var self = this; + this.on("change:counter", this, this.on_change_counter); + // events + internal_bus.on('mail_needaction_new', this, this.counter_increment); + internal_bus.on('mail_needaction_done', this, this.counter_decrement); return this._super(); }, - clicked: function(ev) { - ev.preventDefault(); - this.trigger("clicked"); + counter_increment: function(inc){ + this.set('counter', this.get('counter')+inc); + }, + counter_decrement: function(dec){ + this.set('counter', this.get('counter')-dec); + }, + on_change_counter: function() { + this.$('.fa-comment').html(this.get('counter') || ''); + }, + on_click: function(e){ + e.preventDefault(); + this.do_action({ + type: 'ir.actions.client', + tag: 'mail.chat.instant_messaging', + params: { + 'default_active_id': 'channel_inbox', + }, + }, { + clear_breadcrumbs: true, + }); }, }); -// Put the ImTopButton widget in the systray menu if the user is an employee -var Users = new Model('res.users'); -Users.call('has_group', ['base.group_user']).done(function(is_employee) { - if (is_employee) { - SystrayMenu.Items.push(ImTopButton); - } +SystrayMenu.Items.push(NotificationTopButton); + + +/** + * Abstract Class to 'Add More/Search' Widget + * + * Inputbox using jQueryUI autocomplete to fetch selection, like a Many2One field (on form view) + * Used to create or pin a mail.channel or a res.partner on the InstantMessagingView + **/ +var AbstractAddMoreSearch = Widget.extend({ + template: 'mail.chat.AbstractAddMoreSearch', + events: { + "click .o_mail_chat_add_more_text": "on_click_text", + "blur .o_mail_chat_search_input": "_toggle_elements", + }, + init: function(parent, options){ + this._super.apply(this, arguments); + options = _.defaults(options || {}, { + 'can_create': false, + 'label': _t('+ Add More'), + }); + this.limit = 10; + this.can_create = options.can_create; + this.label = options.label; + }, + start: function(){ + var self = this; + this.last_search_val = false; + this.$input = this.$('.o_mail_chat_search_input'); + this._bind_events(); + return this._super(); + }, + _bind_events: function(){ + // autocomplete + var self = this; + this.$input.autocomplete({ + source: function(request, response) { + self.last_search_val = request.term; + self.do_search(request.term).done(function(result){ + if(self.can_create){ + result.push({ + 'label': _.str.sprintf(''+_t("Create %s")+'', '"'+self.last_search_val+'"'), + 'value': '_create', + }); + } + response(result); + }); + }, + select: function(event, ui) { + self.on_click_item(ui.item); + }, + focus: function(event, ui) { + event.preventDefault(); + }, + html: true, + }); + }, + // ui + _toggle_elements: function(){ + this.$('.o_mail_chat_add_more_text').toggle(); + this.$('.o_mail_chat_add_more_search_bar').toggle(); + this.$input.val(''); + this.$input.focus(); + }, + on_click_text: function(event){ + event.preventDefault(); + this._toggle_elements(); + }, + // to be redefined + do_search: function(search_val){ + return $.when(); + }, + on_click_item: function(item){ + if(item.value === '_create'){ + if(this.last_search_val){ + this.trigger('item_create', this.last_search_val); + } + }else{ + this.trigger('item_clicked', item); + } + }, }); +var PartnerAddMoreSeach = AbstractAddMoreSearch.extend({ + /** + * Do the search call + * @override + */ + do_search: function(search_val){ + var self = this; + var Partner = new Model("res.partner"); + return Partner.call('im_search', [search_val, this.limit]).then(function(result){ + var values = []; + _.each(result, function(user){ + values.push(_.extend(user, { + 'value': user.name, + 'label': user.name, + })); + }); + return values; + }); + }, +}); + +var ChannelAddMoreSearch = AbstractAddMoreSearch.extend({ + /** + * Do the search call + * @override + */ + do_search: function(search_val){ + var self = this; + var Channel = new Model("mail.channel"); + return Channel.call('channel_search_to_join', [search_val]).then(function(result){ + var values = []; + _.each(result, function(channel){ + values.push(_.extend(channel, { + 'value': channel.name, + 'label': channel.name, + })); + }); + return values; + }); + }, +}); + +var PrivateGroupAddMoreSearch = AbstractAddMoreSearch.extend({ + _bind_events: function(){ + // don't call the super to avoid autocomplete + this.$input.on('keyup', this, this.on_keydown); + }, + on_keydown: function(event){ + if(event.which === $.ui.keyCode.ENTER && this.$input.val()){ + this.trigger('item_create', this.$input.val()); + } + }, +}); + + +/** + * 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.chat.PartnerInviteDialog", + init: function(parent, options){ + options = _.defaults(options || {}, { + buttons: [{ + text: _t("Invite"), + close: true, + classes: "btn-primary", + click: _.bind(this.on_click_add, this), + }], + channel: undefined, + }); + this._super.apply(this, arguments); + this.set("partners", []); + this.channel = options.channel; + this.PartnersModel = new Model('res.partner'); + this.ChannelModel = new Model('mail.channel'); + this.limit = 20; + }, + start: function(){ + var self = this; + this.$('.o_mail_chat_partner_invite_input').select2({ + width: '100%', + allowClear: true, + multiple: true, + formatResult: function(item){ + if(item.im_status === 'online'){ + return ' ' + item.text + ''; + } + return ' ' + item.text + ''; + }, + query: function (query) { + self.PartnersModel.call('im_search', [query.term, self.limit]).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.$('.o_mail_chat_partner_invite_input').select2('data'); + if(data.length >= 1){ + this.ChannelModel.call('channel_invite', [], {"ids" : [this.channel.id], 'partner_ids': _.pluck(data, 'id')}).then(function(){ + var names = _.pluck(data, 'text').join(', '); + self.do_notify(_t('New people'), _.str.sprintf(_t('You added %s to the conversation.'), names)); + self.close(); + }); + }else{ + self.close(); + } + }, +}); + +/** + * Client Action : Instant Messaging View, inspired by Slack.com + * + * Action replacing the Inbox, and the list of group (mailing list, multiple conversation, rooms, ...) + * Includes real time messages (received and sent), creating group, channel, chat conversation, ... + **/ +var ChatMailThread = Widget.extend(mail_thread.MailThreadMixin, ControlPanelMixin, { + template: 'mail.chat.ChatMailThread', + events: { + // events from MailThreadMixin + "click .o_mail_redirect": "on_click_redirect", + "click .o_mail_thread_message_star": "on_message_star", + // events specific for ChatMailThread + "click .o_mail_chat_channel_item": "on_click_channel", + "click .o_mail_chat_partner_item": "on_click_partner", + "click .o_mail_chat_partner_unpin": "on_click_partner_unpin", + "click .o_mail_thread_message_needaction": "on_message_needaction", + }, + init: function (parent, action) { + this._super.apply(this, arguments); + // attributes + this.action_manager = parent; + this.help_message = action.help || ''; + this.context = action.context; + this.action = action; + this.loading_history = false; + this.chatter_needaction_auto = false; + this.options = this.action.params; + // mail thread mixin + mail_thread.MailThreadMixin.init.call(this); + // components : conversation manager and search widget (channel_type + '_search_widget') + this.conv_manager = web_client.mail_conversation_manager; + this.channel_search_widget = new ChannelAddMoreSearch(this, {'label': _t('+ Subscribe'), 'can_create': true}); + this.group_search_widget = new PrivateGroupAddMoreSearch(this, {'label': _t('+ New private group'), 'can_create': true}); + this.partner_search_widget = new PartnerAddMoreSeach(this); + // options (update the default of MailThreadMixin) + this.options = _.extend(this.options, { + 'display_document_link': true, + 'display_needaction_button': true, + 'emoji_list': this.conv_manager.emoji_list, + }); + this.emoji_set_substitution(this.conv_manager.emoji_list); + // channel business + this.channels = {}; + this.mapping = {}; // mapping partner_id/channel_id for 'direct message' channel + this.set('current_channel_id', false); + this.set('needaction_inbox_counter', 0); + this.search_domain = []; + // channel slots + this.set('channel_channel', []); + this.set('channel_direct_message', []); + this.set('channel_private_group', []); + this.set('partners', []); + // models + this.ChannelModel = new Model('mail.channel', this.context); + // control panel items + this.control_elements = {}; + }, + willStart: function(){ + var self = this; + return session.rpc('/mail/client_action').then(function(result){ + self.chatter_needaction_auto = result['chatter_needaction_auto']; + self.set('needaction_inbox_counter', result['needaction_inbox_counter']); + self.set('partners', result['channel_slots']['partners']); + self.mapping = result['channel_slots']['mapping']; + self._channel_slot(_.omit(result['channel_slots'], 'partners', 'mapping')); + }); + }, + start: function(){ + var self = this; + this._super.apply(this, arguments); + mail_thread.MailThreadMixin.start.call(this); + // channel business events + this.on("change:current_channel_id", this, this.channel_change); + this.on("change:channel_channel", this, function(){ + self.channel_render('channel_channel'); + }); + this.on("change:channel_private_group", this, function(){ + self.channel_render('channel_private_group'); + }); + this.on("change:partners", this, this.partner_render); + + // search widget for channel + this.channel_search_widget.insertAfter(this.$('.o_mail_chat_channel_slot_channel_channel')); + this.channel_search_widget.on('item_create', this, function(name){ + self.channel_create(name, 'public'); + }); + this.channel_search_widget.on('item_clicked', this, function(item){ + self.channel_join_and_get_info(item.id).then(function(channel){ + self.channel_apply(channel); + }); + }); + // search widget for direct message + this.partner_search_widget.insertAfter(this.$('.o_mail_chat_channel_slot_partners')); + this.partner_search_widget.on('item_clicked', this, function(item){ + self.channel_get([item.id]); + self.partner_add(item); + }); + // search widget for private group + this.group_search_widget.insertAfter(this.$('.o_mail_chat_channel_slot_channel_private_group')); + this.group_search_widget.on('item_create', this, function(name){ + self.channel_create(name, 'private'); + }); + + // bind event to load history when scrolling near top + this.$('.o_mail_chat_messages').scroll(function() { + if ($(this).scrollTop() <= 50 && self.loading_history) { // at 50px of the top, load history + self.message_load_history().then(function(messages){ + if(messages.length < mail_thread.LIMIT_MESSAGE){ + self.loading_history = false; + } + }); + } + }); + + // needaction inbox counter + this.on('change:needaction_inbox_counter', this, this.needaction_inbox_change); + + return $.when(this._super.apply(this, arguments), this.cp_render_searchview()).then(function(res){ + // update control panel + self.cp_render_buttons(); + // apply default channel + var channel_id = self.context.active_id || self.action.params['default_active_id'] || 'channel_inbox'; + if(!_.isString(channel_id)){ + if(_.contains(_.keys(self.channels), channel_id)){ + self.set('current_channel_id', channel_id); + }else{ + self.channel_info(channel_id).then(function(channel){ + self.channel_apply(channel); + }); + } + }else{ + self.set('current_channel_id', channel_id); + } + // internal communication (bind here to avoid receving message from bus when client action still not totally ready) + internal_bus.on('mail_message_receive', self, self.message_receive); + internal_bus.on('mail_session_receive', self, self.channel_receive); + // needaction : the inbox has the same counter as the needaction top counter + internal_bus.on('mail_needaction_new', this, function(inc){ + self.set('needaction_inbox_counter' , self.get('needaction_inbox_counter') + inc); + }); + internal_bus.on('mail_needaction_done', this, function(dec){ + self.set('needaction_inbox_counter' , self.get('needaction_inbox_counter') - dec); + }); + }); + }, + // event actions + on_click_channel: function(event){ + event.preventDefault(); + var channel_id = this.$(event.currentTarget).data('channel-id'); + this.set('current_channel_id', channel_id); + }, + on_click_partner: function(event){ + if(!this.$(event.target).hasClass('o_mail_chat_channel_unpin')){ + event.preventDefault(); + var partner_id = this.$(event.currentTarget).data('partner-id'); + if(this.mapping[partner_id]){ // don't fetch if channel already in local + this.set('current_channel_id', this.mapping[partner_id]); + }else{ + this.channel_get([partner_id]); + } + } + }, + on_click_partner_unpin: function(event){ + event.preventDefault(); + var self = this; + var $source = this.$(event.currentTarget); + var partner_id = $source.data('partner-id'); + var channel_id = this.mapping[partner_id]; + var channel = this.channels[channel_id]; + this.channel_pin(channel.uuid, false).then(function(){ + self.set('partners', _.filter(self.get('partners'), function(p){ return p.id !== partner_id; })); + self.channel_remove(channel_id); + delete self.mapping[partner_id]; + // if unpin current channel, switch to inbox + if(self.get('current_channel_id') === channel_id){ + self.set('current_channel_id', 'channel_inbox'); + } + }); + }, + on_click_button_minimize: function(event){ + event.preventDefault(); + var current_channel = this.channels[this.get('current_channel_id')]; + if(current_channel){ + var channel_uuid = current_channel.uuid; + return this.ChannelModel.call("channel_minimize", [channel_uuid, true]); + } + return $.Deferred.reject(); + }, + on_click_button_invite: function(event){ + var dialog = new PartnerInviteDialog(this, { + title: _.str.sprintf(_t('Invite people to %s.'), this.get_current_channel_name()), + channel: this.channels[this.get('current_channel_id')], + }).open(); + }, + on_click_button_unsubscribe: function(event){ + var self = this; + this.ChannelModel.call('action_unfollow', [[this.get('current_channel_id')]]).then(function(){ + var channel = self.channels[self.get('current_channel_id')] + var slot = self.get_channel_slot(channel); + self.set(slot, _.filter(self.get(slot), function(c){ return c.id !== channel.id; })); + self.do_notify(_t('Unsubscribe'), _.str.sprintf(_t('You unsubscribe from %s.'), self.get_current_channel_name())); + self.set('current_channel_id', 'channel_inbox'); // jump to inbox + }); + }, + on_click_button_settings: function(event){ + this.do_action({ + type: 'ir.actions.act_window', + res_model: "mail.channel", + res_id: this.get('current_channel_id'), + views: [[false, 'form']], + target: 'current' + }, { + clear_breadcrumbs: true + }); + }, + on_message_needaction: function(event){ + var self = this; + mail_thread.MailThreadMixin.on_message_needaction.call(this, event).then(function(message_id){ + // decrement the needaction top counter + internal_bus.trigger('mail_needaction_done', 1); + // decrement the channel labels + var treated_messages = _.filter(self.get('messages'), function(m){ return m.id == message_id; }); + if(treated_messages.length){ + self.needaction_decrement(treated_messages[0].channel_ids || []); + } + }); + }, + // control panel + cp_update: function(){ + var self = this; + var current_channel_id = this.get('current_channel_id'); + var current_channel_name = this.get_current_channel_name(); + // toggle cp buttons + if(_.isString(current_channel_id)){ + self.control_elements.$buttons.find('button').hide(); + }else{ + self.control_elements.$buttons.find('button').show(); + var current_channel = this.channels[current_channel_id]; + if(current_channel.channel_type == 'chat'){ + self.control_elements.$buttons.find('.o_mail_chat_button_invite, .o_mail_chat_button_more').hide(); + } + } + // update control panel + var status = { + breadcrumbs: this.action_manager.get_breadcrumbs().concat([{'title': current_channel_name, 'action': _.clone(this)}]), + cp_content: { + $buttons: self.control_elements.$buttons, + $searchview: self.control_elements.$searchview, + $searchview_buttons: self.control_elements.$searchview_buttons, + $switch_buttons: false, + $pager: false, + }, + searchview: self.searchview, + }; + this.update_control_panel(status, {clear: false}); + }, + cp_render_buttons: function() { + var self = this; + this.control_elements.$buttons = $(QWeb.render("mail.chat.ControlButtons", {})); + this.control_elements.$buttons.on('click', '.o_mail_chat_button_minimize', _.bind(this.on_click_button_minimize, this)); + this.control_elements.$buttons.on('click', '.o_mail_chat_button_invite', _.bind(this.on_click_button_invite, this)); + this.control_elements.$buttons.on('click', '.o_mail_chat_button_unsubscribe', _.bind(this.on_click_button_unsubscribe, this)); + this.control_elements.$buttons.on('click', '.o_mail_chat_button_settings', _.bind(this.on_click_button_settings, this)); + }, + cp_render_searchview: function(){ + var self = this; + var options = { + $buttons: $("
"), + action: this.action, + }; + var view_id = (this.action && this.action.search_view_id && this.action.search_view_id[0]) || false; + this.searchview = new SearchView(this, this.MessageDatasetSearch, view_id, {}, options); + + this.searchview.on('search_data', this, this.on_search); + return $.when(this.searchview.appendTo($("
"))).done(function() { + self.control_elements.$searchview = self.searchview.$el; + self.control_elements.$searchview_buttons = self.searchview.$buttons.contents(); + // hack to hide 'group by' button, since it is not relevant + self.searchview.$buttons.find('.oe-groupby-menu').hide(); + }); + }, + /** + * Method call when action if done after redirecting (using 'on_click_redirect') + * @override + */ + on_reverse_breadcrumb: function(){ + var current_channel_id = this.get('current_channel_id'); + this.cp_update(); // do not reload the client action, just display it, but a refresh of the control panel is needed. + // push state + this.action_manager.do_push_state({ + action: this.action.id, + active_id: current_channel_id, + }); + }, + // messages search methods + on_search: function(domains, contexts, groupbys){ + var self = this; + return pyeval.eval_domains_and_contexts({ + domains: [[]].concat(domains || []), + contexts: [this.context].concat(contexts || []), + group_by_seq: groupbys || [] + }).done(function(results){ + if (results.error) { + throw new Error(_.str.sprintf(_t("Failed to evaluate search criterions")+": \n%s", JSON.stringify(results.error))); + } + // modify the search domain and do search + self.search_domain = results.domain; + return self.message_load_new(); + }); + }, + // channels + /** + * Set the channel slot + * @param {Object[]} fetch_result : should contains only the slot name (as key) and the list of channel header as value. + */ + _channel_slot: function(fetch_result){ + var self = this; + var channel_slots = _.keys(fetch_result); + _.each(channel_slots, function(slot){ + // update the channel slot + self.set(slot, fetch_result[slot]); + // flatten the result : update the complete channel list + _.each(fetch_result[slot], function(channel){ + self.channels[channel.id] = channel; + }); + }); + }, + /** + * Apply a channel means adding it, and swith to it + * @param {Object} channel : channel header + */ + channel_apply: function(channel){ + this.channel_add(channel); + this.set('current_channel_id', channel.id); + }, + /** + * Add the given channel, or update it if already exists and loaded + * @param {Object} channel : channel header to add + */ + channel_add: function(channel){ + var channel_slot = this.get_channel_slot(channel); + var existing = this.get(channel_slot); + if(_.contains(_.pluck(existing, 'id'), channel.id)){ + // update the old channel + var filtered_channels = _.filter(existing, function(item){ return item.id != channel.id; }); + this.set(channel_slot, filtered_channels.concat([channel])); + }else{ + // simply add the reveiced channel + this.set(channel_slot, existing.concat([channel])); + } + // also update the flatten list + this.channels[channel.id] = channel; + + // update the mapping for 'direct message' channel, and the partner list + if(channel_slot === 'channel_direct_message'){ + var partner = channel.direct_partner[0]; + this.mapping[partner.id] = channel.id; + this.partner_add(partner); + } + }, + channel_remove: function(channel_id){ + var channel = this.channels[channel_id]; + var slot = this.get_channel_slot(channel); + this.set(slot, _.filter(this.get(slot), function(c){ return c.id !== channel_id; })); + delete this.channels[channel_id]; + }, + /** + * Get the channel the current user has with the given partner, and get the channel header + * @param {Number[]} partner_ids : list of res.partner identifier + */ + channel_get: function(partner_ids){ + var self = this; + return this.ChannelModel.call('channel_get', [partner_ids]).then(function(channel){ + self.channel_apply(channel); + }); + }, + /** + * Create a channel with the given name and type, and apply it + * @param {String} channel_name : the name of the channel + * @param {String} privacy : the privacy of the channel (groups, public, ...) + */ + channel_create: function(channel_name, privacy){ + var self = this; + return this.ChannelModel.call('channel_create', [channel_name, privacy]).then(function(channel){ + self.channel_apply(channel); + }); + }, + channel_info: function(channel_id){ + var self = this; + return this.ChannelModel.call('channel_info', [[channel_id]]).then(function(channels){ + return channels[0]; + }); + }, + channel_pin: function(uuid, pinned){ + return this.ChannelModel.call('channel_pin', [uuid, pinned]); + }, + channel_join_and_get_info: function(channel_id){ + return this.ChannelModel.call('channel_join_and_get_info', [[channel_id]]); + }, + channel_change: function(){ + var self = this; + var current_channel_id = this.get('current_channel_id'); + var current_channel = this.channels[current_channel_id]; + // mail chat compose message (destroy and replace it) + this.mail_chat_compose_message && this.mail_chat_compose_message.destroy(); + this.mail_chat_compose_message = new mail_thread.MailComposeMessage(this, new data.DataSetSearch(this, 'mail.channel', this.context), { + 'emoji_list': this.options.emoji_list, + 'context': _.extend(this.context, { + 'default_res_id': current_channel_id, + }), + 'display_mode': 'chat', + }); + if(_.isString(current_channel_id)){ + this.$('.o_mail_chat_composer_wrapper').hide(); + }else{ + this.$('.o_mail_chat_composer_wrapper').show(); + } + this.mail_chat_compose_message.appendTo(this.$('.o_mail_chat_composer_wrapper')); + this.mail_chat_compose_message.focus(); + + // push state (the action is referred by action_manager, and no reloaded when jumping + // channel, so update context is requried) + this.action.context['active_id'] = current_channel_id; + this.action.context['active_ids'] = [current_channel_id]; + web_client.action_manager.do_push_state({ + action: this.action.id, + active_id: current_channel_id, + }); + + // update control panel + this.cp_update(); + + // fetch the messages + return this.message_load_new().then(function(raw_messages){ + // allow loading history + self.loading_history = true; + // if normal channel, update last message id, and unbold + var def = $.Deferred().resolve(); + if(_.isNumber(current_channel_id)){ + def = self.ChannelModel.call('channel_seen', [[current_channel_id]]); + }else{ + // auto treat needaction from inbox + if(current_channel_id === 'channel_inbox' && self.chatter_needaction_auto){ + def = self.MessageDatasetSearch._model.query(['id', 'channel_ids']).filter([['needaction', '=', true]]).all().then(function(messages){ + // get needaction message ids + var message_ids = _.pluck(messages, 'id'); + if(message_ids){ + // call to unlink + self.MessageDatasetSearch._model.call('set_message_done',[message_ids]).then(function(){ + // decrement channel items in batch + var flatten_channel_ids = _.flatten(_.pluck(messages, 'channel_ids')); + var channel_count = _.countBy(flatten_channel_ids, function(cid) { + return cid; + }); + _.each(_.keys(channel_count), function(cid){ + self.needaction_decrement([cid], channel_count[cid]); + }); + // decrement the needaction top counter + internal_bus.trigger('mail_needaction_done', message_ids.length); + }); + } + }); + } + } + def.then(function(){ + self._toggle_unread_message(current_channel_id, false, true); + self._scroll(); + }); + }); + }, + channel_render: function(channel_slot){ + this.$('.o_mail_chat_channel_slot_' + channel_slot).replaceWith(QWeb.render("mail.chat.ChatMailThread.channels", {'widget': this, 'channel_slot': channel_slot})); + }, + // partners + partner_add: function(partner){ + var partners = _.filter(this.get('partners'), function(p){ return p.id != partner.id; }); + this.set('partners', partners.concat([partner])); + }, + partner_render: function(){ + this.$('.o_mail_chat_channel_slot_partners').replaceWith(QWeb.render("mail.chat.ChatMailThread.partners", {'widget': this})); + }, + // needaction + needaction_increment: function(channel_ids, inc){ + var self = this; + _.each(channel_ids, function(channel_id){ + if(self.channels[channel_id]){ + var count = (self.channels[channel_id].message_needaction_counter || 0) + (inc || 1); + self.channels[channel_id].message_needaction_counter = count; + self.$('.o_mail_chat_needaction[data-channel-id='+channel_id+']').html(count); + self.$('.o_mail_chat_needaction[data-channel-id='+channel_id+']').show(); + } + }); + }, + needaction_decrement: function(channel_ids, dec){ + var self = this; + _.each(channel_ids, function(channel_id){ + if(self.channels[channel_id]){ + var count = (self.channels[channel_id].message_needaction_counter || 0) - (dec || 1); + self.channels[channel_id].message_needaction_counter = count; + self.$('.o_mail_chat_needaction[data-channel-id='+channel_id+']').html(count); + if(count <= 0){ + self.$('.o_mail_chat_needaction[data-channel-id='+channel_id+']').show(); + } + } + }); + }, + needaction_inbox_change: function(inc){ + var count = this.get('needaction_inbox_counter'); + var $elem = this.$('.o_mail_chat_needaction[data-channel-id="channel_inbox"]'); + $elem.html(count); + if(count > 0){ + $elem.show(); + }else{ + $elem.hide(); + } + }, + // from bus + channel_receive: function(channel){ + this.channel_add(channel); + }, + message_receive: function(message){ + var self = this; + // if current channel should reveice message, give it to it + if(_.contains(message['channel_ids'], this.get('current_channel_id'))){ + this.message_insert([message]); + } + // for other message channel, get the channel if not loaded yet, and bolded them + var other_message_channel_ids = _.without(message['channel_ids'], this.get('current_channel_id')); + var active_channel_ids = _.map(_.keys(this.channels), function(cid){ + return parseInt(cid); + }); // integer as key of a dict is cast as string in javascript + var channel_to_fetch = _.difference(other_message_channel_ids, active_channel_ids); + // fetch unloaded channels and add it + var def = $.Deferred(); + if(channel_to_fetch.length >= 1){ + def = this.ChannelModel.call("channel_info", [], {"ids" : channel_to_fetch}).then(function(channels){ + _.each(channels, function(channel){ + self.channel_add(channel); + }); + }); + }else{ + def.resolve([]); + } + // bold the channel to indicate unread messages + def.then(function(){ + // bold channel having unread messages + _.each(other_message_channel_ids, function(channel_id){ + self._toggle_unread_message(channel_id, true); + }); + // auto scroll to bottom + self._scroll(); + }); + // if needaction, then increment the label + if(message.needaction_partner_ids && _.contains(message.needaction_partner_ids, session.partner_id)){ + this.needaction_increment(message.channel_ids || []); + } + }, + // utils function + get_channel_slot: function(channel){ + if(channel.channel_type === 'channel'){ + if(channel.public === 'private'){ + return 'channel_private_group'; + } + return 'channel_channel'; + } + if(channel.channel_type === 'chat'){ + return 'channel_direct_message'; + } + }, + get_current_channel_name: function(){ + var current_channel_id = this.get('current_channel_id'); + var current_channel = this.channels[current_channel_id]; + var current_channel_name = current_channel && current_channel.name || _t('Unknown'); + // virtual channel id (for inbox, or starred channel) + if(_.isString(current_channel_id)){ + if(current_channel_id == 'channel_inbox'){ + current_channel_name = _t('Inbox'); + } + if(current_channel_id == 'channel_starred'){ + current_channel_name = _t('Starred'); + } + } + return current_channel_name; + }, + _toggle_unread_message: function(channel_id, add_or_remove, set_as_active){ + var partner_id = false; + var inverse_mapping = _.invert(this.mapping); + // toggle bold channel/partner item + this.$('.o_mail_chat_sidebar .o_mail_chat_channel_item[data-channel-id="'+channel_id+'"]').toggleClass('o_mail_chat_channel_unread', add_or_remove); + if(_.contains(_.keys(inverse_mapping), channel_id.toString())){ + partner_id = parseInt(inverse_mapping[channel_id]); + this.$('.o_mail_chat_sidebar .o_mail_chat_partner_item[data-partner-id="'+partner_id+'"]').toggleClass('o_mail_chat_channel_unread', add_or_remove); + } + // set the channel/partner item as the active one + if(set_as_active){ + this.$('.o_mail_chat_sidebar .o_mail_chat_channel_item, .o_mail_chat_sidebar .o_mail_chat_partner_item').find('a').removeClass('active'); + this.$('.o_mail_chat_sidebar .o_mail_chat_channel_item[data-channel-id="'+channel_id+'"]').find('a').addClass('active'); + if(partner_id){ + this.$('.o_mail_chat_sidebar .o_mail_chat_partner_item[data-partner-id="'+partner_id+'"]').find('a').addClass('active'); + } + } + }, + _scroll: function(){ + var current_channel_id = this.get('current_channel_id'); + var current_channel = this.channels[current_channel_id]; + if(_.isNumber(current_channel_id)){ + if(current_channel.seen_message_id){ + if(this.$(".o_mail_thread_message[data-message-id="+current_channel.seen_message_id+"]")){ + this.$('.o_mail_chat_messages').scrollTop(this.$(".o_mail_thread_message[data-message-id="+current_channel.seen_message_id+"]").offset().top); + }else{ + this.$('.o_mail_chat_messages').scrollTop(0); + } + }else{ + this.$('.o_mail_chat_messages').scrollTop(this.$('.o_mail_chat_messages')[0].scrollHeight); + } + current_channel.seen_message_id = false; + }else{ + this.$('.o_mail_chat_messages').scrollTop(this.$('.o_mail_chat_messages')[0].scrollHeight); + } + }, + /** + * Render the messages + * @override + */ + message_render: function(){ + this.$('.o_mail_chat_messages_content').html(QWeb.render('mail.chat.ChatMailThread.content', {'widget': this})); + }, + /** + * Return the message domain for the current channel + * @override + */ + get_message_domain: function(){ + // default channel domain + var current_channel_id = this.get('current_channel_id'); + var domain = [['channel_ids', 'in', current_channel_id]]; + // virtual channel id (for inbox, or starred channel) + if(_.isString(current_channel_id)){ + if(current_channel_id == 'channel_inbox'){ + domain = [['needaction', '=', true]]; + } + if(current_channel_id == 'channel_starred'){ + domain = [['starred', '=', true]]; + } + } + // add search domain + domain = domain.concat(this.search_domain); + return domain; + }, +}); + +core.action_registry.add('mail.chat.instant_messaging', ChatMailThread); + + return { - InstantMessaging: InstantMessaging, - UserWidget: UserWidget, + ChatMailThread: ChatMailThread, }; }); diff --git a/addons/mail/static/src/js/mail_chat_common.js b/addons/mail/static/src/js/mail_chat_common.js index 0aaa0e68d16..a5f52ddf4b1 100644 --- a/addons/mail/static/src/js/mail_chat_common.js +++ b/addons/mail/static/src/js/mail_chat_common.js @@ -1,13 +1,12 @@ -odoo.define('im_chat.im_chat_common', function (require) { +odoo.define('mail.chat_common', function (require) { "use strict"; -// to do: make this work in website - var bus = require('bus.bus'); var core = require('web.core'); var session = require('web.session'); var time = require('web.time'); var Widget = require('web.Widget'); +var mail_utils = require('mail.utils'); var _t = core._t; var QWeb = core.qweb; @@ -15,140 +14,130 @@ var QWeb = core.qweb; var NBR_LIMIT_HISTORY = 20; +/** + * Widget handeling the sessions/conversations + * + * Responsible to listen the bus and do the correct action according to the received notifications (fetch + * conversation, add message, ...) + **/ var ConversationManager = Widget.extend({ init: function(parent, options) { var self = this; this._super(parent); this.options = _.clone(options) || {}; _.defaults(this.options, { - inputPlaceholder: _t("Say something..."), - defaultMessage: null, - defaultUsername: _t("Visitor"), - force_open: false, + placeholder: _t("Say something..."), + default_username: _t("Visitor"), load_history: true, focus: false, }); + this.emoji_list = []; // business this.sessions = {}; this.bus = bus.bus; this.bus.on("notification", this, this.on_notification); - this.bus.options.im_presence = true; - // ui this.set("right_offset", 0); this.set("bottom_offset", 0); this.on("change:right_offset", this, this.calc_positions); this.on("change:bottom_offset", this, this.calc_positions); - this.set("window_focus", true); - this.on("change:window_focus", self, function(e) { - self.bus.options.im_presence = self.get("window_focus"); - }); this.set("waiting_messages", 0); this.on("change:waiting_messages", this, this.window_title_change); $(window).on("focus", _.bind(this.window_focus, this)); - $(window).on("blur", _.bind(this.window_blur, this)); this.window_title_change(); + // session widgets deferred (for their creation) + this._session_defs = {}; + }, + start: function(){ + var self = this; + return $.when(session.rpc("/mail/chat_init"), this._super.apply(this, arguments)).then(function(result){ + self._setup(result); + }); + }, + _setup: function(init_data){ + this.emoji_list = init_data['emoji']; + this.options['emoji'] = mail_utils.shortcode_substitution(init_data['emoji']); }, on_notification: function(notification, options) { var self = this; var channel = notification[0]; var message = notification[1]; var regex_uuid = new RegExp(/(\w{8}(-\w{4}){3}-\w{12}?)/g); + var is_channel_uuid = regex_uuid.test(channel); - // Concern im_chat : if the channel is the im_chat.session or im_chat.status, or a 'private' channel (aka the UUID of a session) - if((Array.isArray(channel) && (channel[1] === 'im_chat.session')) || (regex_uuid.test(channel))){ - // message to display in the chatview - if (message.type === "message" || message.type === "meta") { - self.message_receive(message); - } + // (dbname, 'res.partner', partner_id) receive only the new mail.channel header + if((Array.isArray(channel) && (channel[1] === 'res.partner')) || is_channel_uuid){ // activate the received session if(message.uuid){ - this.session_apply(message, options); + this.session_apply(message, options); + } + } + // (dbname, 'mail.channel', channel_id) receive only the mail.message of the channel + if((Array.isArray(channel) && (channel[1] === 'mail.channel')) || is_channel_uuid){ + // message to display in the chatview + if (message.message_type) { + self.message_receive(message); } } }, - // window focus unfocus beep and title window_focus: function() { - this.set("window_focus", true); this.set("waiting_messages", 0); }, - window_blur: function() { - this.set("window_focus", false); - }, window_beep: function() { - if (typeof(Audio) === "undefined") { - return; - } - var audio = new Audio(); - var ext = audio.canPlayType("audio/ogg; codecs=vorbis") ? ".ogg" : ".mp3"; - audio.src = session.url("/im_chat/static/src/audio/ting") + ext; - audio.play(); + mail_utils.beep(session); }, window_title_change: function() { var title = undefined; if (this.get("waiting_messages") !== 0) { - title = _.str.sprintf(_t("%d Messages"), this.get("waiting_messages")); this.window_beep(); } }, - // sessions and messages - session_apply: function(active_session, options){ - // options used by this function : force_open and focus (load_history can be usefull) + // sessions methods + session_apply: function(session, options){ + // options used by this function : focus (load_history can be usefull) var self = this; - options = _.extend(_.clone(this.options), options || {}); - // force open - var session = _.clone(active_session); - if(options['force_open']){ - session.state = 'open'; - } + options = _.defaults(options || {}, { + 'focus': true, + }, this.options); // create/get the conversation widget - var conv = this.sessions[session.uuid]; + var conv = this.sessions[session.id]; if (! conv) { if(session.state !== 'closed'){ - conv = new Conversation(this, this, session, options); - conv.appendTo($("body")); + conv = new Conversation(this, session, options); + // store this deferred to apply next action (notifications) when it is resolved. + this._session_defs[session.id] = conv.appendTo($("body")); + this._session_defs[session.id].then(function() { + self.calc_positions(); + }); conv.on("destroyed", this, _.bind(this.session_delete, this)); - this.sessions[session.uuid] = conv; - this.calc_positions(); + this.sessions[session.id] = conv; } }else{ - conv.set("session", session); - } - // if force open, broadcast it - if(options['force_open'] && active_session.state !== 'open'){ - conv.session_update_state('open'); - } - // apply the focus - if (options['focus']){ - conv.focus(); + // update the state only when the 'contruction' deferred is resolved. + this._session_defs[session.id].then(function(){ + conv.set('session', session); + }); } return conv; }, - session_delete: function(uuid){ - delete this.sessions[uuid]; + session_delete: function(channel_id){ + delete this.sessions[channel_id]; this.calc_positions(); }, - message_receive: function(message) { + /** + * Adding a message in all its channels + * /!\ Suppose the channel are already loaded in the ConversationManager /!\ + * @param {Object} message : the received message (formatted server side) + */ + message_receive: function(message){ var self = this; - var session_id = message.to_id[0]; - var uuid = message.to_id[1]; - var from_id = message['from_id'] ? message['from_id'][0] : false; - if (! this.get("window_focus") && from_id != this.get_current_uid()) { - this.set("waiting_messages", this.get("waiting_messages") + 1); - } - this._message_receive(message); - }, - _message_receive: function(message){ - var uuid = message.to_id[1]; - var conv = this.sessions[uuid]; - conv.message_receive(message); + _.each(message.channel_ids, function(channel_id){ + self.sessions[channel_id].message_receive(message); + }); }, // others - get_current_uid: function(){ - return session && !_.isUndefined(session.uid) ? session.uid : false; - }, calc_positions: function() { var self = this; var current = this.get("right_offset"); @@ -159,84 +148,72 @@ var ConversationManager = Widget.extend({ }); }, destroy: function() { - $(window).off("unload", this.unload); $(window).off("focus", this.window_focus); $(window).off("blur", this.window_blur); return this._super(); } }); - +/** + * Widget Conversation + * + * Display the conversation in a 'portable' window, manage sending and receiving message, + * and the fold state of the conversation. + **/ var Conversation = Widget.extend({ - className: "openerp_style oe_im_chatview", + template: "mail.chat.im.Conversation", events: { "keydown input": "keydown", - "click .oe_im_chatview_close": "click_close", - "click .oe_im_chatview_header": "click_header" + "click .o_mail_chat_im_window_close": "on_click_close", + "click .o_mail_chat_im_window_header": "on_click_header" }, - init: function(parent, c_manager, session, options) { + init: function(parent, session, options) { this._super(parent); - this.c_manager = c_manager; - this.options = options || {}; - this.loading_history = true; + this.options = _.defaults(options || {}, { + 'emoji': {}, + 'placeholder': _t('Write something ...'), + 'loading_history': true, + 'focus': true, + }); this.set("messages", []); this.set("session", session); + this.set("pending", 0); this.set("right_position", 0); this.set("bottom_position", 0); - this.set("pending", 0); - this.inputPlaceholder = this.options.defaultInputPlaceholder; + }, + willStart: function(){ + if(this.options.loading_history){ + return $.when(this.message_history(), this._super.apply(this, arguments)); + } + return this._super.apply(this, arguments); }, start: function() { var self = this; - self._super.apply(this, arguments); - self.prepare_action_menu(); // ui - self.$().append(QWeb.render("im_chat.Conversation", {widget: self})); self.on("change:right_position", self, self.calc_pos); self.on("change:bottom_position", self, self.calc_pos); - self.on("change:pending", self, _.bind(function() { - if (self.get("pending") === 0) { - self.$(".nbr_messages").text(""); - } else { - self.$(".nbr_messages").text("(" + self.get("pending") + ")"); - } - }, self)); + self.on("change:pending", self, self.message_pending_change); + self.full_height = self.$().height(); - self.calc_pos(); - // business - self.bind_action_menu(); - self.on("change:session", self, self.session_update); - self.on("change:messages", self, self.render_messages); - self.$('.oe_im_chatview_content').on('scroll',function(){ + self.$('.o_mail_chat_im_window_content').on('scroll',function(){ if($(this).scrollTop() === 0){ self.message_history(); } }); - if(self.options['load_history']){ // load history if asked - self.message_history(); - } - // prepare the header and the correct state - self.session_update(); - }, - // action menu - _add_action: function(label, style_class, icon_fa_class, callback){ - this.actions.push({ - 'label': label, - 'class': style_class, - 'icon_class': icon_fa_class, - 'callback': callback, - }); - }, - prepare_action_menu: function(){ - // override this method to add action with _add_action() - this.actions = []; - }, - bind_action_menu: function(){ - var self = this; - _.each(this.actions, function(action){ - if(action.callback){ - self.$('.button_option_group .' + action.class).on('click', _.bind(action.callback, self)); + // business + self.on("change:session", self, self.session_change); + self.on("change:messages", self, self.message_render); + + return self._super.apply(this, arguments).then(function(){ + // place the conversation window correctly on the screen + self.calc_pos(); + // focus if needed + if(self.options.focus){ + self.focus(); } + // re-apply the session to display the correct state + self.set('session', _.clone(self.get('session'))); + self._go_bottom(); }); }, // ui @@ -248,7 +225,7 @@ var Conversation = Widget.extend({ }, hide: function(){ this.$().animate({ - height: this.$(".oe_im_chatview_header").outerHeight() + height: this.$(".o_mail_chat_im_window_header").outerHeight() }); }, calc_pos: function() { @@ -256,21 +233,25 @@ var Conversation = Widget.extend({ this.$().css("bottom", this.get("bottom_position")); }, // session - session_update_state: function(state){ + /** + * Set the fold state LOCALLY for the session. This will + * trigger a ui update + * @param {string} state : the fold state ('fold', 'closed' or 'open') + */ + session_fold: function(state){ var session = this.get('session'); session.state = state; this.set('session', session); }, - session_update: function(){ - // built the name - var names = []; - _.each(this.get("session").users, function(user){ - if( (session.uid !== user.id) && !(_.isUndefined(session.uid) && !user.id) ){ - names.push(user.name); - } - }); - this.$(".header_name").text(names.join(", ")); - this.$(".header_name").attr('title', names.join(", ")); + /** + * Triggered when the session has changed : it update the header name + * and fold the window (ui) according to its state. + */ + session_change: function(){ + // update conversation name in header + var name = this.get("session").name || _t('Unknown Channel'); + this.$(".o_mail_chat_im_window_header_name").text(name); + this.$(".o_mail_chat_im_window_header_name").attr('title', name); // update the fold state if(this.get("session").state){ if(this.get("session").state === 'closed'){ @@ -285,39 +266,53 @@ var Conversation = Widget.extend({ } }, // messages + /** + * Load the history of the current session, and insert it into already loaded messages + * @returns {Deferred} resolved with the new loaded messages (Object[]) + */ message_history: function(){ var self = this; - if(this.loading_history){ - var data = {uuid: self.get("session").uuid, limit: NBR_LIMIT_HISTORY}; - var lastid = _.first(this.get("messages")) ? _.first(this.get("messages")).id : false; - if(lastid){ - data.last_id = lastid; - } - session.rpc("/im_chat/history", data).then(function(messages){ + if(this.options.loading_history){ + var data = { + uuid: self.get("session").uuid, + limit: NBR_LIMIT_HISTORY, + last_id: _.first(this.get("messages")) ? _.first(this.get("messages")).id : false, + }; + return session.rpc("/mail/chat_history", data).then(function(messages){ if(messages){ - self.insert_messages(messages); + self.message_insert(messages); if(messages.length != NBR_LIMIT_HISTORY){ - self.loading_history = false; + self.options.loading_history = false; } }else{ - self.loading_history = false; + self.options.loading_history = false; } + return messages; }); } + return $.Deferred().resolve([]); }, message_receive: function(message) { - if (this.get('session').state === 'open') { - this.set("pending", 0); - } else { - this.set("pending", this.get("pending") + 1); + // don't add duplicated message (this can happen when refreshing and receiving already recieved + // message that are still in the bus queue/buffer). + if(!_.contains(_.pluck(this.get('messages'), 'id'), message.id)){ + // is pending message ? + if (this.get('session').state === 'open') { + this.set("pending", 0); + } else { + if(!this.message_am_i_author(message)){ + this.set("pending", this.get("pending") + 1); + } + } + // insert it + this.message_insert([message]); + this._go_bottom(); } - this.insert_messages([message]); - this._go_bottom(); }, - message_send: function(message, type) { + message_send: function(message) { var self = this; var send_it = function() { - return session.rpc("/im_chat/post", {uuid: self.get("session").uuid, message_type: type, message_content: message}); + return session.rpc("/mail/chat_post", {uuid: self.get("session").uuid, message_content: message}); }; var tries = 0; send_it().fail(function(error, e) { @@ -327,71 +322,65 @@ var Conversation = Widget.extend({ return send_it(); }); }, - insert_messages: function(messages){ + /** + * Preporcess the message (add informations in the Object, ...) + * @param {Object[]} messages : server side formatted message (from message_format method) + */ + _message_preprocess: function(raw_messages){ var self = this; - // avoid duplicated messages - messages = _.filter(messages, function(m){ return !_.contains(_.pluck(self.get("messages"), 'id'), m.id) ; }); - // escape the message content and set the timezone - _.map(messages, function(m){ - if(!m.from_id){ - m.from_id = [false,self. get_anonymous_name()]; + _.each(raw_messages, function(m){ + m.date = moment(time.str_to_datetime(m.date)).format('YYYY-MM-DD HH:mm:ss'); // set the date in the correct browser user timezone + if(m.body){ + m.body = mail_utils.shortcode_apply(m.body, self.options.emoji); } - m.create_date = moment(time.str_to_datetime(m.create_date)).format('YYYY-MM-DD HH:mm:ss'); - return m; + _.each(m.attachment_ids, function(a){ + a.url = mail_utils.get_attachment_url(session, m.id, a.id); + a.fa_class = mail_utils.attachment_filetype_to_fa_class(a.file_type_icon); + }); }); + return raw_messages; + }, + /** + * Insert new raw message into the current ones + * @param {Object[]} messages : server side formatted message (from message_format method) + */ + message_insert: function(raw_messages){ + var messages = this._message_preprocess(raw_messages); this.set("messages", _.sortBy(this.get("messages").concat(messages), function(m){ return m.id; })); }, - render_messages: function(){ + message_render: function(){ var self = this; - var res = {}; - var last_date_day, last_user_id = -1; - _.each(this.get("messages"), function(current){ - // add the url of the avatar for all users in the conversation - current.from_id[2] = session.url(_.str.sprintf("/im_chat/image/%s/%s", self.get('session').uuid, current.from_id[0])); - var date_day = current.create_date.split(" ")[0]; - if(date_day !== last_date_day){ - res[date_day] = []; - last_user_id = -1; - } - last_date_day = date_day; - if(current.type == "message"){ // traditionnal message - if(last_user_id === current.from_id[0]){ - _.last(res[date_day]).push(current); - }else{ - res[date_day].push([current]); - } - last_user_id = current.from_id[0]; - }else{ // meta message - res[date_day].push([current]); - last_user_id = -1; - } - }); - // render and set the content of the chatview - // TODO jem : when refactoring this, don't forget to pre-process date in this function before render quweb template - // since, moment will not be define in qweb on the website pages, because the helper (see csn) is in core.js and cannot be - // imported in the frontend. - this.$('.oe_im_chatview_content_bubbles').html($(QWeb.render("im_chat.Conversation_content", {"list": res}))); - this._go_bottom(); + var message_html = $(QWeb.render("mail.chat.im.Conversation.messages", {"widget": this})); + this.$('.o_mail_chat_im_window_content').html(message_html); + }, + message_am_i_author: function(message){ + return message.author_id && message.author_id[0] === session.partner_id; + }, + message_pending_change: function(event){ + if (this.get("pending") === 0) { + this.$(".o_mail_chat_im_window_header_counter").text(""); + } else { + this.$(".o_mail_chat_im_window_header_counter").text("(" + this.get("pending") + ")"); + } }, // utils and event - get_anonymous_name: function(){ - var name = this.options["defaultUsername"]; - _.each(this.get('session').users, function(u){ - if(!u.id){ - name = u.name; - } - }); - return name; + _go_bottom: function() { + if(this.$(".o_mail_chat_im_window_content").length){ + this.$(".o_mail_chat_im_window_content").scrollTop(this.$(".o_mail_chat_im_window_content").get(0).scrollHeight); + } + }, + focus: function() { + this.$(".o_mail_chat_im_window_input").focus(); }, keydown: function(e) { if(e && e.which == 27) { - if(this.$el.prev().find('.oe_im_chatview_input').length > 0){ - this.$el.prev().find('.oe_im_chatview_input').focus(); + if(this.$el.prev().find('.o_mail_chat_im_window_input').length > 0){ + this.$el.prev().find('.o_mail_chat_im_window_input').focus(); }else{ - this.$el.next().find('.oe_im_chatview_input').focus(); + this.$el.next().find('.o_mail_chat_im_window_input').focus(); } e.stopPropagation(); - this.session_update_state('closed'); + this.session_fold('closed'); } if(e && e.which !== 13) { return; @@ -401,25 +390,17 @@ var Conversation = Widget.extend({ return; } this.$("input").val(""); - this.message_send(mes, "message"); + this.message_send(mes); }, - _go_bottom: function() { - this.$(".oe_im_chatview_content").scrollTop(this.$(".oe_im_chatview_content").get(0).scrollHeight); + on_click_header: function(event){ + this.session_fold(); }, - focus: function() { - this.$(".oe_im_chatview_input").focus(); - }, - click_header: function(event){ - var classes = event.target.className.split(' '); - if(_.contains(classes, 'header_name') || _.contains(classes, 'oe_im_chatview_header')){ - this.session_update_state(); - } - }, - click_close: function(event) { - this.session_update_state('closed'); + on_click_close: function(event) { + event.stopPropagation(); // avoid triggering event for 'on_click_header' + this.session_fold('closed'); }, destroy: function() { - this.trigger("destroyed", this.get('session').uuid); + this.trigger("destroyed", this.get('session').id); return this._super(); } }); @@ -430,4 +411,4 @@ return { ConversationManager: ConversationManager, }; -}); \ No newline at end of file +}); diff --git a/addons/mail/static/src/js/mail_chatter.js b/addons/mail/static/src/js/mail_chatter.js new file mode 100644 index 00000000000..7032f864eb4 --- /dev/null +++ b/addons/mail/static/src/js/mail_chatter.js @@ -0,0 +1,471 @@ +odoo.define('mail.chatter', function (require) { +"use strict"; + +var core = require('web.core'); +var data = require('web.data'); +var session = require('web.session'); +var web_client = require('web.web_client'); +var Model = require('web.Model'); +var Widget = require('web.Widget'); +var form_common = require('web.form_common'); +var SystrayMenu = require('web.SystrayMenu'); +var mail_utils = require('mail.utils'); +var mail_thread = require('mail.thread'); + +var QWeb = core.qweb; +var _t = core._t; +var internal_bus = core.bus; + + +/** + * Mail Compose Message for Chatter + * + * Extends the basic MailComposeMessage Widget to add 'suggested partner' layer (open popup + * when suggested partner is selected without email, or other informations), and the button + * to open the 'mail.compose.message' wizard. + **/ +var ChatterMailComposeMessage = mail_thread.MailComposeMessage.extend({ + template: 'mail.chatter.ComposeMessage', + init: function(parent, dataset, options){ + this._super.apply(this, arguments); + this.suggested_partners = []; + this.context = options.context || {}; + // options + this.options = _.defaults(options, { + 'record_name': false, + 'is_log': false, + 'internal_subtypes': [], + }); + }, + willStart: function(){ + if(this.options.is_log){ + return this._super.apply(this, arguments); + } + return $.when(this._super.apply(this, arguments), this.message_get_suggested_recipients()); + }, + start: function(){ + this.$input = this.$('.o_mail_compose_message_input'); + this.$input.focus(); + // add mail compose message full button + this.$('.o_mail_compose_message_button_group').append('') + this.$('.o_mail_compose_message_button_full_message').on('click', this, this.on_compose_message); + return this._super.apply(this, arguments); + }, + message_get_suggested_recipients: function(){ + var self = this; + var email_addresses = _.pluck(this.suggested_partners, 'email_address'); + return this.thread_dataset.call('message_get_suggested_recipients', [[this.context.default_res_id], this.context]).done(function (suggested_recipients) { + var thread_recipients = suggested_recipients[self.context.default_res_id]; + _.each(thread_recipients, function (recipient) { + var parsed_email = mail_utils.parse_email(recipient[1]); + if (_.indexOf(email_addresses, parsed_email[1]) == -1) { + self.suggested_partners.push({ + 'checked': true, + 'partner_id': recipient[0], + 'full_name': recipient[1], + 'name': parsed_email[0], + 'email_address': parsed_email[1], + 'reason': recipient[2], + }); + } + }); + }); + }, + /** + * Get the list of selected suggested partner + * @returns Array() : list of 'recipient' selected partner (may not be created in db) + **/ + get_checked_suggested_partners: function(){ + var self = this; + var checked_partners = []; + this.$('.o_mail_chatter_compose_message_suggested_partners input:checked').each(function(index){ + var $input = $(this); + var full_name = $input.data('fullname'); + checked_partners = checked_partners.concat(_.filter(self.suggested_partners, function(item){ + return full_name === item.full_name; + })); + }); + return checked_partners; + }, + /** + * Check the additionnal partners (not necessary registered partners), and open a popup form view + * for the ones who informations is missing. + * @param Array : list of 'recipient' partners to complete informations or validate + * @returns Deferred resolved with the list of checked suggested partners (real partner) + **/ + check_suggested_partners: function (checked_suggested_partners) { + var self = this; + var check_done = $.Deferred(); + + var recipients = _.filter(checked_suggested_partners, function (recipient) { return recipient.checked; }); + var recipients_to_find = _.filter(recipients, function (recipient) { return (! recipient.partner_id); }); + var names_to_find = _.pluck(recipients_to_find, 'full_name'); + var recipients_to_check = _.filter(recipients, function (recipient) { return (recipient.partner_id && ! recipient.email_address); }); + var recipient_ids = _.pluck(_.filter(recipients, function (recipient) { return recipient.partner_id && recipient.email_address; }), 'partner_id'); + + var names_to_remove = []; + var recipient_ids_to_remove = []; + + // have unknown names -> call message_get_partner_info_from_emails to try to find partner_id + var find_done = $.Deferred(); + if (names_to_find.length > 0) { + find_done = self.thread_dataset.call('message_partner_info_from_emails', [[this.context.default_res_id], names_to_find]); + } else { + find_done.resolve([]); + } + + // for unknown names + incomplete partners -> open popup - cancel = remove from recipients + $.when(find_done).pipe(function (result) { + var emails_deferred = []; + var recipient_popups = result.concat(recipients_to_check); + + _.each(recipient_popups, function (partner_info) { + var deferred = $.Deferred(); + emails_deferred.push(deferred); + + var partner_name = partner_info.full_name; + var partner_id = partner_info.partner_id; + var parsed_email = mail_utils.parse_email(partner_name); + + var pop = new form_common.FormViewDialog(this, { + res_model: 'res.partner', + res_id: partner_id, + context: { + 'force_email': true, + 'ref': "compound_context", + 'default_name': parsed_email[0], + 'default_email': parsed_email[1], + }, + title: _t("Please complete partner's informations"), + }).open(); + pop.on('closed', self, function () { + deferred.resolve(); + }); + pop.view_form.on('on_button_cancel', self, function () { + names_to_remove.push(partner_name); + if (partner_id) { + recipient_ids_to_remove.push(partner_id); + } + }); + }); + $.when.apply($, emails_deferred).then(function () { + var new_names_to_find = _.difference(names_to_find, names_to_remove); + find_done = $.Deferred(); + if (new_names_to_find.length > 0) { + find_done = self.thread_dataset.call('message_partner_info_from_emails', [[self.context.default_res_id], new_names_to_find, true]); + } else { + find_done.resolve([]); + } + $.when(find_done).pipe(function (result) { + var recipient_popups = result.concat(recipients_to_check); + _.each(recipient_popups, function (partner_info) { + if (partner_info.partner_id && _.indexOf(partner_info.partner_id, recipient_ids_to_remove) == -1) { + recipient_ids.push(partner_info.partner_id); + } + }); + }).pipe(function () { + check_done.resolve(recipient_ids); + }); + }); + }); + return check_done; + }, + message_post: function(body, attachment_ids, values){ + var self = this; + var _super = self._super; + // default values + values = _.defaults(values || {}, { + 'message_type': 'comment', + 'content_subtype': 'html', + 'partner_ids': [], + 'subtype_id': false, + 'subtype': false, + }); + // update subtype + if(this.options.is_log) { + var subtype_id = parseInt(this.$('.o_mail_chatter_compose_message_subtype_select').val()); + if(_.indexOf(_.pluck(this.internal_subtypes, 'id'), subtype_id) == -1) { + values['subtype'] = 'mail.mt_note' + }else{ + values['subtype_id'] = subtype_id; + } + }else{ + values['subtype'] = 'mail.mt_comment'; + } + // update partner_ids + var def = $.when(); + if(this.options.is_log){ + // directly call the super() + def = _.bind(_super, self)(body, attachment_ids, values); + }else{ + // call the super() when all partner popup are treated + var checked_suggested_partners = this.get_checked_suggested_partners(); + this.check_suggested_partners(checked_suggested_partners).done(function(partner_ids){ + // update context + values['partner_ids'] = partner_ids; + values['context'] = _.defaults(self.context, { + 'mail_post_autofollow': true, + 'mail_post_autofollow_partner_ids': partner_ids, + }); + def = _.bind(_super, self)(body, attachment_ids, values); + }); + } + return def + }, + is_empty: function(){ + return this.$input.val() === ''; + }, + on_compose_message: function(event){ + var self = this; + if (!this.do_check_attachment_upload()){ + return false; + } + + var recipient_done = $.Deferred(); + if (this.options.is_log){ + recipient_done.resolve([]); + }else{ + var checked_suggested_partners = this.get_checked_suggested_partners(); + recipient_done = this.check_suggested_partners(checked_suggested_partners); + } + recipient_done.then(function(partner_ids){ + var context = { + 'default_parent_id': self.id, + 'default_body': mail_utils.get_text2html(self.$('.o_mail_compose_message_input').val()), + 'default_attachment_ids': _.pluck(self.get('attachment_ids'), 'id'), + 'default_partner_ids': partner_ids, + 'default_is_log': self.options.is_log, + 'mail_post_autofollow': true, + 'mail_post_autofollow_partner_ids': partner_ids, + }; + + if (self.context.default_model && self.context.default_res_id) { + context.default_model = self.context.default_model; + context.default_res_id = self.context.default_res_id; + } + + self.do_action({ + type: 'ir.actions.act_window', + res_model: 'mail.compose.message', + view_mode: 'form', + view_type: 'form', + views: [[false, 'form']], + target: 'new', + context: context, + }, { + on_close: function() { + self.trigger('need_refresh'); + }, + }); + }); + } +}); + + +/** + * Document Chatter ('mail_thread' widget) + * + * Displayed on every document. Since it is displayed on a form view, it extends 'AbstractField' widget, and + * required the behaviour of 'MailThreadMixin' to manage mail.message (using common methods). + * This widget is responsible of the toggle the message composer and the custom message rendering. + **/ +var ChatterMailThread = form_common.AbstractField.extend(mail_thread.MailThreadMixin, { + template: 'mail.chatter.ChatterMailThread', + events: { + "click .o_mail_redirect": "on_click_redirect", + "click .o_mail_thread_message_star": "on_message_star", + "click .o_mail_thread_message_needaction": "on_message_needaction", + "click .o_mail_chatter_show_more_message": "on_message_show_more", + // toggle message composer (!! declaration order is important !!) + "click .o_mail_chatter_button_new_message": "on_open_composer", + "click .o_mail_chatter_button_log_note": "on_open_composer", + "blur .o_mail_compose_message_input": "on_close_composer", + "mousedown .o_mail_chatter_compose_message": function(event) { + this.stay_open = true; + }, + "focus .o_mail_compose_message_input": function(event) { + this.stay_open = false; + }, + "keydown .o_mail_compose_message_input": function(event) { + this.stay_open = false; + }, + }, + init: function (parent, node){ + this._super.apply(this, arguments); + mail_thread.MailThreadMixin.init.call(this); + this.message_composer = undefined; + this.record_name = ''; + this.context = this.options.context || {}; + // options from 'message_ids' node in field_view_get + this.options = _.defaults(this.options, { + 'display_log_button': true, + 'display_document_link': false, + 'internal_subtypes': [], + 'emoji_list': [], + }); + this.emoji_set_substitution(this.options.emoji_list); + }, + start: function(){ + mail_thread.MailThreadMixin.start.call(this); + return this._super.apply(this, arguments); + }, + on_message_show_more: function(event){ + var self = this; + this.message_load_history().then(function(messages){ + if(messages.length < mail_thread.LIMIT_MESSAGE){ + self.$('.o_mail_chatter_show_more_message').hide(); + } + }); + }, + /** + * When a message is correctly posted, fetch its data to render it + * @param {Number} message_id : the identifier of the new posted message + * @returns {Deferred} + */ + on_message_post: function(message_id){ + var self = this; + return this.message_format([message_id]).then(function(messages){ + self.stay_open = false; + self.message_insert(messages); + self.on_close_composer(); + }); + }, + /** + * The value of the field has change (modification or switch to next document). Form view is + * not re-rendered, simply updated. A re-fetch is needed. + * @override + */ + render_value: function(){ + var self = this; + // re-rendering for mail composer message + this.options.display_show_more_message = this.get_value().length > mail_thread.LIMIT_MESSAGE; + this.renderElement(); + // reset dataset + this.ThreadDataset = this.view.dataset; + // update context (require for mail.compose.message wizard) + this.context = _.extend({ + 'mail_read_set_read': true, // set messages as read in Chatter TODO JEM : not used anymore + 'default_res_id': this.view.datarecord.id || false, + 'default_model': this.view.model || false, + }, this.options.context || {}); + // fetch messages of current document + this.message_format(this.get_value().slice(0, mail_thread.LIMIT_MESSAGE)).then(function(raw_messages){ + self._message_replace(self._message_preprocess(raw_messages)); + self.record_name = _.last(raw_messages) ? _.last(raw_messages).record_name : ''; + // set the thread as read + self.ThreadDataset.call('message_set_read', [[self.view.datarecord.id]]).then(function(message_ids){ + // decrement the needaction top counter + internal_bus.trigger('mail_needaction_done', message_ids.length); + }); + }); + }, + // composer toggle + on_open_composer: function(event){ + var self = this; + this.$('.o_mail_chatter_compose_buttons').hide(); + this.$('.o_mail_chatter_compose_message_wrap').show(); + // destroy existing composer + if(this.message_composer){ + this.message_composer.destroy(); + } + // create the new composer + this.message_composer = new ChatterMailComposeMessage(this, this.ThreadDataset, { + 'internal_subtypes': this.options.internal_subtypes, + 'is_log': this.$(event.currentTarget).hasClass('o_mail_chatter_button_log_note'), + 'emoji_list': this.options.emoji_list, + 'context': this.context, + 'record_name': this.record_name, + }); + this.message_composer.appendTo(this.$('.o_mail_chatter_compose_message_wrap')); + this.message_composer.on('message_sent', this, this.on_message_post); + this.message_composer.on('need_refresh', this, function(e){ + self.message_refresh(); + self.on_close_composer(); + }); + }, + on_close_composer: function(event){ + if(this.message_composer && this.message_composer.is_empty() && !this.stay_open){ + this.$('.o_mail_chatter_compose_buttons').show(); + this.$('.o_mail_chatter_compose_message_wrap').hide(); + } + }, + /** + * Refresh the current message, and set them as current message. Use 'message_fetch', since its + * domain don't use 'ids', so if there is new messages, they'll be fetched. + * @returns {Deferred} + */ + message_refresh: function(){ + return this.message_load_new(); + }, + /** + * Render the messages + * @override + */ + message_render: function(){ + this.$('.o_mail_chatter_messages').html(QWeb.render('mail.chatter.ChatterMailThread.messages', {'widget': this})); + }, + /** + * Order the messages + * @param {Object[]} messages : messages to order + * @override + */ + _message_order: function(messages){ + var messages = mail_thread.MailThreadMixin._message_order.call(this, messages); + return messages.reverse(); + }, + /** + * Return the message domain for the current document mail thread + * @override + */ + get_message_domain: function(){ + var res_id = this.ThreadDataset.ids[this.ThreadDataset.index]; + return [['model', '=', this.ThreadDataset.model], ['res_id', '=', res_id]]; + } +}); + +core.form_widget_registry.add('mail_thread', ChatterMailThread); + + + +/** + * Global ComposeMessage Top Button + * + * Add a link on the top user bar to write a full mail. It opens the form view + * of the mail.compose.message (in a modal). + */ +var ComposeMessageTopButton = Widget.extend({ + template:'mail.ComposeMessageTopButton', + events: { + "click": "on_compose_message", + }, + on_compose_message: function (ev) { + ev.preventDefault(); + var ctx = {}; + if ($('button.o_timeline_compose_post') && $('button.o_timeline_compose_post').is(":visible") == true && + (this.getParent()).getParent().action_manager.inner_widget.active_view.type == 'form'){ + ctx = { + 'default_res_id': (this.getParent()).getParent().action_manager.inner_widget.active_view.controller.datarecord.id, + 'default_model': (this.getParent()).getParent().action_manager.inner_widget.active_view.controller.model, + }; + } + web_client.action_manager.do_action({ + type: 'ir.actions.act_window', + res_model: 'mail.compose.message', + view_mode: 'form', + view_type: 'form', + views: [[false, 'form']], + target: 'new', + context: ctx, + }); + }, +}); + +// Put the ComposeMessageTopButton widget in the systray menu +SystrayMenu.Items.push(ComposeMessageTopButton); + + +return { + ChatterMailThread: ChatterMailThread, +}; + +}); diff --git a/addons/mail/static/src/js/mail_followers.js b/addons/mail/static/src/js/mail_followers.js index 0f2353c729e..124ed218a7e 100644 --- a/addons/mail/static/src/js/mail_followers.js +++ b/addons/mail/static/src/js/mail_followers.js @@ -13,9 +13,9 @@ var web_client = require('web.web_client'); var _t = core._t; var qweb = core.qweb; -/** +/** * ------------------------------------------------------------ - * timeline_followers Widget + * Mail Followers Widget * ------------------------------------------------------------ * * This widget handles the display of a list of records as a vertical @@ -27,7 +27,7 @@ var qweb = core.qweb; var Followers = form_common.AbstractField.extend({ - template: 'mail_followers', + template: 'mail.Followers', init: function() { this._super.apply(this, arguments); @@ -40,7 +40,7 @@ var Followers = form_common.AbstractField.extend({ this.value = []; this.followers = []; this.data_subtype = {}; - + this.view_is_editable = this.__parentedParent.is_action_enabled('edit'); }, @@ -112,7 +112,7 @@ var Followers = form_common.AbstractField.extend({ size: 'small', title: _t('Edit Subscription of ') + $currentTarget.siblings('a').text(), buttons: [ - { text: _t("Apply"), click: function() { + { text: _t("Apply"), click: function() { self.do_update_subscription(event, user_pid); this.parents('.modal').modal('hide'); }}, @@ -236,11 +236,11 @@ var Followers = form_common.AbstractField.extend({ var user_follower = _.filter(this.followers, function (rec) { return rec['is_uid'];}); this.message_is_follower = user_follower.length >= 1; this.follower_id = this.message_is_follower ? user_follower[0]['id'] : undefined; - $(qweb.render('mail_followers_add_more', {'widget': self})).appendTo(node_user_list); + $(qweb.render('mail.Followers.add_more', {'widget': self})).appendTo(node_user_list); // truncate number of displayed followers _(this.followers).each(function (record) { - $(qweb.render('mail_followers_partner', { + $(qweb.render('mail.Followers.partner', { 'record': _.extend(record, {'avatar_url': '/web/image/' + record['res_model'] + '/' + record['res_id'] + '/image_small'}), 'widget': self}) ).appendTo(node_user_list); @@ -279,7 +279,7 @@ var Followers = form_common.AbstractField.extend({ if (user_pid) { dialog = true; - } + } else { var subtype_list_ul = this.$('.o_timeline_subtype_list ul').empty(); if (! this.message_is_follower) { @@ -302,7 +302,7 @@ var Followers = form_common.AbstractField.extend({ display_subtypes:function (data, dialog) { var self = this; var $list = this.$('.o_timeline_subtype_list ul'); - + if (_.isEmpty(this.data_subtype)) { this.data_subtype = data; @@ -326,7 +326,7 @@ var Followers = form_common.AbstractField.extend({ } old_parent_model = record.parent_model; record.followed = record.followed || undefined; - $(qweb.render('mail_followers_subtype', {'record': record, + $(qweb.render('mail.Followers.subtype', {'record': record, 'dialog': dialog})) .appendTo($list); }); @@ -336,9 +336,9 @@ var Followers = form_common.AbstractField.extend({ do_follow: function () { var context = new data.CompoundContext(this.build_context(), {}); this.$('.o_timeline_subtype_list > .dropdown-toggle').attr('disabled', false); - this.ds_model.call('message_subscribe_users', [[this.view.datarecord.id], - [session.uid], - undefined, + this.ds_model.call('message_subscribe_users', [[this.view.datarecord.id], + [session.uid], + undefined, context]) .then(this.proxy('read_value')); @@ -403,9 +403,9 @@ var Followers = form_common.AbstractField.extend({ $(event.target).attr("checked", "checked"); } else { - self.$('.o_timeline_subtype_list ul').empty(); + self.$('.o_timeline_subtype_list ul').empty(); } - } + } else { var context = new data.CompoundContext(this.build_context(), {}); return this.ds_model.call(action_subscribe, [[this.view.datarecord.id], follower_ids, undefined, checklist, context]) diff --git a/addons/mail/static/src/js/mail_thread.js b/addons/mail/static/src/js/mail_thread.js new file mode 100644 index 00000000000..b40e1df5ced --- /dev/null +++ b/addons/mail/static/src/js/mail_thread.js @@ -0,0 +1,430 @@ +odoo.define('mail.thread', function (require) { +"use strict"; + +var core = require('web.core'); +var data = require('web.data'); +var time = require('web.time'); +var Widget = require('web.Widget'); +var session = require('web.session'); +var web_client = require('web.web_client'); +var mail_utils = require('mail.utils'); + +var _t = core._t; +var QWeb = core.qweb; + + +var LIMIT_MESSAGE = 20; + +/** + * Widget : Input textbox to post messages + * + * Input with 2 buttons to manage attachment and emoji + * - Attachment : upload selected attachment (one at a time) and display them below the textbox. Prevent + posting message while uploading + * - Emoji : popover exposing emoji list, and append emoji shortcode to the textbox when click on emoji image + * It triggers 'message_sent' event with the posted message, when it is sent to the server. + */ +var MailComposeMessage = Widget.extend({ + template: 'mail.ComposeMessage', + events: { + "keydown .o_mail_compose_message_input": "on_keydown", + "change input.oe_form_binary_file": "on_attachment_change", + "click .o_mail_compose_message_attachment_list .o_mail_attachment_delete": "on_attachment_delete", + "click .o_mail_compose_message_button_attachment": 'on_click_attachment', + "click .o_mail_compose_message_button_send": 'message_send', + }, + /** + * Contructor + * @param {Object} options : the options of the MailComposeMessage + * @param {Object[]} [options.emoji_list] : the list of emoji + * @param {Object} options.context : the context. It should contains the default_res_id' (id of the current document). + * @param {chat|mail} options.display_mode : the 'chat' mode will display an input, when the 'mail' mode will display a texarea. + */ + init: function(parent, dataset, options){ + this._super.apply(this, arguments); + this.thread_dataset = dataset; + this.options = _.defaults(options || {}, { + 'emoji_list': {}, + 'context': {}, + 'display_mode': 'mail', + }); + this.emoji_list = this.options.emoji_list; + this.context = this.options.context; + // attachment handeling + this.AttachmentDataSet = new data.DataSetSearch(this, 'ir.attachment', this.context); + this.fileupload_id = _.uniqueId('o_mail_chat_fileupload'); + this.set('attachment_ids', []); + }, + start: function(){ + var self = this; + this.$input = this.$('.o_mail_compose_message_input'); + this.$input.focus(); + this.$attachment_button = this.$(".o_mail_compose_message_button_attachment"); + // attachments + $(window).on(this.fileupload_id, this.on_attachment_loaded); + this.on("change:attachment_ids", this, this.attachment_render); + // emoji + self.$('.o_mail_compose_message_button_emoji').popover({ + placement: 'top', + content: function(){ + if(!self.$emoji){ // lazy rendering + self.$emoji = $(QWeb.render('mail.ComposeMessage.emoji', {'widget': self})); + self.$emoji.find('.o_mail_compose_message_emoji_img').on('click', self, self.on_click_emoji_img); + } + return self.$emoji; + }, + html: true, + container: '.o_mail_compose_message_emoji', + trigger: 'focus', + }); + return this._super(); + }, + // events + on_click_attachment: function(event){ + event.preventDefault(); + this.$('input.oe_form_binary_file').click(); + }, + on_click_emoji_img: function(event){ + this.$input.val(this.$input.val() + " " + $(event.currentTarget).data('emoji')+ " "); + this.$input.focus(); + }, + on_keydown: function(event){ + if(event.which === 13 && this.options.display_mode === 'chat') { + this.message_send(); + } + }, + message_send: function(){ + var $input = this.$('.o_mail_compose_message_input'); + var mes = mail_utils.get_text2html($input.val()); + if (! mes.trim() && this.do_check_attachment_upload()) { + return; + } + $input.val(""); + this.message_post(mes, _.pluck(this.get('attachment_ids'), 'id')); + }, + /** + * Sent the message to the server to create a mail.message, using 'message_post' method + * @param {String} body : the core of the message (html, or plaintext) + * @param {Number[]} attachment_ids : attachment_ids linked to the message + * @param {Object} kwargs : other parameters (in a dictionnary) + * @returns {Deferred} resolved when the message is posted, and returning the new message id + */ + message_post: function(body, attachment_ids, kwargs){ + var self = this; + var values = _.defaults(kwargs || {}, { + 'body': body, + 'attachment_ids': attachment_ids || [], + 'message_type': 'comment', + 'content_subtype': 'html', + 'partner_ids': [], + 'subtype': 'mail.mt_comment', + }); + return this.thread_dataset._model.call('message_post', [this.context.default_res_id], values).then(function(message_id){ + self.clean_attachments(); // empty attachment list + self.trigger('message_sent', message_id); + return message_id; + }); + }, + // attachment business + on_attachment_change: function(event){ + var $target = $(event.target); + if ($target.val() !== '') { + var filename = $target.val().replace(/.*[\\\/]/,''); + // if the files exits for this answer, delete the file before upload + var attachments = []; + for (var i in this.get('attachment_ids')) { + if ((this.get('attachment_ids')[i].filename || this.get('attachment_ids')[i].name) == filename) { + if (this.get('attachment_ids')[i].upload) { + return false; + } + this.AttachmentDataSet.unlink([this.get('attachment_ids')[i].id]); + } else { + attachments.push(this.get('attachment_ids')[i]); + } + } + // submit filename + this.$('form.oe_form_binary_form').submit(); + this.$attachment_button.prop('disabled', true); + + attachments.push({ + 'id': 0, + 'name': filename, + 'filename': filename, + 'url': '', + 'upload': true + }); + this.set('attachment_ids', attachments); + } + }, + on_attachment_loaded: function(event, result){ + var attachment_ids = []; + if (result.error || !result.id ) { + this.do_warn(result.error); + attachment_ids = _.filter(this.get('attachment_ids'), function (val) { return !val.upload; }); + }else{ + _.each(this.get('attachment_ids'), function(a){ + if (a.filename == result.filename && a.upload) { + attachment_ids.push({ + 'id': result.id, + 'name': result.name || result.filename, + 'filename': result.filename, + 'url': session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'name', 'id': result.id}), + }); + }else{ + attachment_ids.push(a); + } + }); + } + this.set('attachment_ids', attachment_ids); + + // TODO JEM : understand the 2 lines below .... + var $input = this.$('input.oe_form_binary_file'); + $input.after($input.clone(true)).remove(); + + this.$attachment_button.prop('disabled', false); + }, + on_attachment_delete: function(event){ + event.stopPropagation(); + var self = this; + var attachment_id = $(event.target).data("id"); + if (attachment_id) { + var attachments = []; + _.each(this.get('attachment_ids'), function(attachment){ + if (attachment_id != attachment.id) { + attachments.push(attachment); + } else { + self.AttachmentDataSet.unlink([attachment_id]); + } + }); + this.set('attachment_ids', attachments); + } + }, + do_check_attachment_upload: function () { + if (_.find(this.get('attachment_ids'), function (file) {return file.upload;})) { + this.do_warn(_t("Uploading error"), _t("Please, wait while the file is uploading.")); + return false; + } + return true; + }, + clean_attachments: function(){ + this.set('attachment_ids', []); + }, + // ui + attachment_render: function(){ + this.$('.o_mail_compose_message_attachment_list').html(QWeb.render('mail.ComposeMessage.attachments', {'widget': this})); + }, + focus: function(){ + this.$input.focus(); + } +}); + + +/** + * Mail Thread Mixin : Messages Managment + * + * Load, Fetch, Display mail.message + * This is a mixin since it will be inherit by a form_common.AbstractField and a Wigdet (Client Action) + * + * @mixin + **/ +var MailThreadMixin = { + /** + * Constructor of the 'mail_thread' mixin + * @param {Boolean} [options.display_log_button] : display of the 'log a note' button + * @param {Boolean} [options.display_document_link] : display document link on message + * @param {Boolean} [options.display_needaction_button] : display the needaction 'check' button on messages + * @param {Object[]} [options.internal_subtypes]: list of internal subtype (for employee only) + * @param {Object[]} [options.emoji_list] : list of emoji + */ + init: function(){ + this.MessageDatasetSearch = new data.DataSetSearch(this, 'mail.message'); + this.set('messages', []); + this.emoji_substitution = {}; + // options + this.options = _.defaults(this.options || {}, { + 'display_log_button': false, + 'display_document_link': false, + 'display_needaction_button': false, + 'internal_subtypes': [], + 'emoji_list': [], + }); + }, + start: function(){ + this.on("change:messages", this, this.message_render); + }, + // Common Actions (They should be bind on the implementing widget, the 'events' dict) + /** + * Generic redirect action : redirect to the form view if the + * click node contains 'data-oe-model' and 'data-oe-id'. + */ + on_click_redirect: function(event){ + event.preventDefault(); + var res_id = $(event.target).data('oe-id'); + var res_model = $(event.target).data('oe-model'); + web_client.action_manager.do_push_state({ + 'model': res_model, + 'id': res_id, + 'title': this.record_name, + }); + this.do_action({ + type:'ir.actions.act_window', + view_type: 'form', + view_mode: 'form', + res_model: res_model, + views: [[false, 'form']], + res_id: res_id, + }, { + 'on_reverse_breadcrumb': this.on_reverse_breadcrumb, + }); + }, + /** + * Will be called when clicking on the Breadcrumb (mainly of client action) + * Need to be override + */ + on_reverse_breadcrumb: function(){ + + }, + /** + * Toggle a message as 'starred' (or not), by toggling the 'o_mail_message_starred' css class. + * Trigger when clicking on selector '.o_mail_thread_message_star'. + */ + on_message_star: function(event){ + var $source = this.$(event.currentTarget); + var mid = $source.data('message-id'); + var is_starred = !$source.hasClass('o_mail_message_starred'); + return this.MessageDatasetSearch.call('set_message_starred', [[mid], is_starred]).then(function(res){ + $source.toggleClass('o_mail_message_starred'); + return mid; + }); + }, + on_message_needaction: function(event){ + var $source = this.$(event.currentTarget); + var mid = $source.data('message-id'); + return this.MessageDatasetSearch.call('set_message_done', [[mid]]).then(function(res){ + $source.remove(); + return mid; + }); + }, + // Message functions + /** + * Fetch given message + * @param {Number[]} message_ids : list of mail.message identifiers to fetch + * @returns {Deferred} resolved when the messages are loaded + */ + message_format: function(message_ids){ + return this.MessageDatasetSearch._model.call('message_format', [message_ids]); + }, + /** + * Fetch mail.message in the format defined server side + * @param {Array} domain : Odoo Domain of the message to fetch + * @param {Number} limit : the limit of messages to fetch + * @returns {Deferred} resolved when the messages are loaded + */ + message_fetch: function(domain, limit){ + domain = domain || this.get_message_domain(); + limit = limit || LIMIT_MESSAGE; + return this.MessageDatasetSearch._model.call('message_fetch', [domain], {limit: limit}); + }, + _message_replace: function(messages){ + this.set('messages', this._message_order(messages)); + }, + /** + * Order the message in a particular order, to be defined by implementation + * @param {Object[]} messages : messages to order + */ + _message_order: function(messages){ + return _.sortBy(messages, 'date'); + }, + /** + * Insert messages in the current list + * @param {Object[]} messages : list of mail.message (formatted server side) + */ + message_insert: function(raw_messages){ + var current_messages = this.get('messages'); + current_messages = current_messages.concat(this._message_preprocess(raw_messages)); + this._message_replace(current_messages); + }, + /** + * Load history of message, with the 'history' domain, and insert them in the current list + * @returns {Deferred} resolved when the messages are fetched and returning the raw + * messages (formatted server side) + */ + message_load_history: function(){ + var self = this; + return this.message_fetch(this.get_message_domain_history()).then(function(raw_messages){ + self.message_insert(raw_messages); + return raw_messages + }); + }, + /** + * Load messages according to the current domain, and set that new list as current messages, + * erasing the old ones. + * @returns {Deferred} resolved when the messages are fetched and returning the raw + * messages (formatted server side) + */ + message_load_new: function(){ + var self = this; + return this.message_fetch(this.get_message_domain()).then(function(raw_messages){ + self._message_replace(self._message_preprocess(raw_messages)); + return raw_messages + }); + }, + /** + * Preprocess the list of messages before rendering, add 'computed' field (is_needaction, + * is_starred, ...), and apply image shortcode to the message body. + * @param {Object[]} messages : list of raw mail.message (formatted server side) + * @returns list of messages. It can be sorted, grouped, ... + */ + _message_preprocess: function(messages){ + var self = this; + _.each(messages, function(m){ + m.is_needaction = _.contains(m.needaction_partner_ids, session.partner_id); + m.is_starred = _.contains(m.starred_partner_ids, session.partner_id); + m.date = moment(time.str_to_datetime(m.date)).format('YYYY-MM-DD HH:mm:ss'); // set the date in the correct browser user timezone + if(m.body){ + m.body = mail_utils.shortcode_apply(m.body, self.emoji_substitution); + } + _.each(m.attachment_ids, function(a){ + a.url = mail_utils.get_attachment_url(session, m.id, a.id); + }); + }); + return _.sortBy(messages, 'date'); + }, + /** + * Take the current messages, render them, and insert the rendering in the DOM. + * This is triggered when the mesasge list change. + * Must be redefined, since it depends on the complete DOM widget + */ + message_render: function(){ + + }, + // Message Domains + /** + * Return the current domain to fetch the message of the thread. this should + * be redifined by the thread implementation. + */ + get_message_domain: function(){ + return []; + }, + get_message_domain_history: function(){ + return this.get_message_domain().concat([['id', '<', _.min(_.pluck(this.get('messages'), 'id'))]]); + }, + // Others + /** + * Set the list of emoji to be substituted in message body + * @param {Object[]} emoji_list : list of emoji Object + */ + emoji_set_substitution: function(emoji_list){ + this.emoji_substitution = mail_utils.shortcode_substitution(emoji_list); + }, +}; + + +return { + MailComposeMessage: MailComposeMessage, + MailThreadMixin: MailThreadMixin, + LIMIT_MESSAGE: LIMIT_MESSAGE, +} + + +}); diff --git a/addons/mail/static/src/js/mail_utils.js b/addons/mail/static/src/js/mail_utils.js index 656dc941a75..a18dc449d6b 100644 --- a/addons/mail/static/src/js/mail_utils.js +++ b/addons/mail/static/src/js/mail_utils.js @@ -5,7 +5,7 @@ odoo.define('mail.utils', function () { * ------------------------------------------------------------ * ChatterUtils * ------------------------------------------------------------ - * + * * This class holds a few tools method for Chatter. * Some regular expressions not used anymore, kept because I want to * - (^|\s)@((\w|@|\.)*): @login@log.log @@ -27,24 +27,6 @@ function parse_email(text) { return [text, false]; } -/** - * Replaces some expressions - * - :name - shortcut to an image - */ -function do_replace_expressions(string) { - var icon_list = ['al', 'pinky']; - /* special shortcut: :name, try to find an icon if in list */ - var regex_login = new RegExp(/(^|\s):((\w)*)/g); - var regex_res = regex_login.exec(string); - while (regex_res !== null) { - var icon_name = regex_res[2]; - if (_.include(icon_list, icon_name)) - string = string.replace(regex_res[0], regex_res[1] + '' + icon_name + ''); - regex_res = regex_login.exec(string); - } - return string; -} - /** * Replaces textarea text into html text (add

, ) * TDE note : should be done server-side, in Python -> use mail.compose.message ? @@ -55,7 +37,7 @@ function get_text2html(text) { .replace(/[\n\r]/g,'
'); } -/* Returns the complete domain with "&" +/* Returns the complete domain with "&" * TDE note: please add some comments to explain how/why */ function expand_domain(domain) { @@ -90,6 +72,8 @@ function breakword(str){ return out; } + +// TODO JEM : remove me in master function bindTooltipTo($el, value, position) { $el.tooltip({ 'title': value, @@ -106,13 +90,92 @@ function bindTooltipTo($el, value, position) { }); } + +/** + * ------------------------------------------------------------ + * MailChat Utils + * ------------------------------------------------------------ + */ + + /** + * Apply the given shortcode to the 'str' message. + * @param str : the text to substitute (html). + * @param shortcodes : dict where key is the shortcode, and the value is the substitution + */ +function shortcode_apply(str, shortcodes){ + var re_escape = function(str){ + return String(str).replace(/([.*+?=^!:${}()|[\]\/\\])/g, '\\$1'); + }; + _.each(_.keys(shortcodes), function(key){ + str = str.replace( new RegExp("(?:^|\\s|<[a-z]*>)(" + re_escape(key) + ")(?:\\s|$|)"), ' '+shortcodes[key]+' '); + }); + return str; +} + +/** + * Transform the list of emoji (shortcode Object) into a key-array representing + * the substitution to apply + * @param {Ojbect[]} emoji_list : list of emoji object + * @returns {Object} mapping between the code (as key) and the substitution (as value) + */ +function shortcode_substitution(emoji_list){ + var emoji_substitution = {}; + _.each(emoji_list, function(emoji){ + emoji_substitution[emoji.source] = emoji.substitution; + }); + return emoji_substitution; +} + +/** + * Associate a font awesome class to a attachment file type + * @param {string} file_type : file type to analyse + * @returns {string} the fa class associated + */ +function attachment_filetype_to_fa_class(file_type){ + var mapping = { + 'text': 'fa fa-file-text-o', + 'document': 'fa fa-file-word-o', + 'audio': 'fa fa-file-audio-o', + 'archive': 'fa fa-file-archive-o', + 'disk': 'fa fa-file-archive-o', + 'image': 'fa fa-file-photo-o', + 'webimage': 'fa fa-file-photo-o', + 'script': 'fa fa-file-code-o', + 'html': 'fa fa-file-code-o', + 'video': 'fa fa-file-movie-o', + 'spreadsheet': 'fa fa-file-excel-o', + 'print': 'fa fa-file-pdf-o', + 'presentation': 'fa fa-file-powerpoint-o', + } + if(_.contains(_.keys(mapping), file_type)){ + return mapping[file_type]; + } + return 'fa fa-file-o'; +} + +/** + * Play the noise 'ting' to notify user + */ +function beep(session){ + if (typeof(Audio) === "undefined") { + return; + } + var audio = new Audio(); + var ext = audio.canPlayType("audio/ogg; codecs=vorbis") ? ".ogg" : ".mp3"; + audio.src = session.url("/mail/static/src/audio/ting") + ext; + audio.play(); +} + return { parse_email: parse_email, - do_replace_expressions: do_replace_expressions, get_text2html: get_text2html, expand_domain: expand_domain, breakword: breakword, bindTooltipTo: bindTooltipTo, + shortcode_apply: shortcode_apply, + shortcode_substitution: shortcode_substitution, + attachment_filetype_to_fa_class: attachment_filetype_to_fa_class, + beep: beep, }; }); diff --git a/addons/mail/static/src/less/mail_chat_backend.less b/addons/mail/static/src/less/mail_chat_backend.less new file mode 100644 index 00000000000..80fe41c541c --- /dev/null +++ b/addons/mail/static/src/less/mail_chat_backend.less @@ -0,0 +1,212 @@ + +// ------------------------------------------------------------------ +// Layout +// ------------------------------------------------------------------ +.o_mail_chat { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + height: 100%; + + @media (max-width: @screen-xs-max) { + position: static; + height: auto; + } + + .o-flex-display(); + .o-flex-flow(row, nowrap); + + .o_mail_chat_sidebar { + .o-flex(0, 0, 15%); + overflow: auto; + } + + .o_mail_chat_content { + .o-flex(0, 0, 85%); + height: 100%; + .o-flex-display(); + .o-flex-flow(column, nowrap); + @media (max-width: @screen-xs-max) { + height: auto; + } + + .o_mail_chat_messages { + .o-flex(1, 0, 0); + overflow: auto; + } + + .o_mail_chat_composer_wrapper { + .o-flex(0, 0, auto); + .o-flex-display(); + /* + > img { + .o-flex(0, 0, auto); + width: 64px; + height: 64px; + } + + > input { + .o-flex(1, 0, auto); + } + */ + } + } +} + +// ------------------------------------------------------------------ +// Style +// ------------------------------------------------------------------ +.o_mail_chat { + .o_mail_chat_sidebar { + background-color: @gray; + padding: 1%; + + > .nav-pills { + > li { + > a { + color: @odoo-brand-optional; // community compatibility: override base.css + &:hover, &:focus { + background-color: @gray-dark; + } + &.active { + background-color: @gray-dark; + } + } + + &.o_mail_chat_channel_item, &.o_mail_chat_partner_item { + > a { + color: @gray-lighter; + > .badge { + color: white; + background-color: @odoo-brand-optional; + } + + > i.fa { + padding-top: 3px; + padding-right: 3px; + + &.o_online { + color: @odoo-brand-optional; + } + } + } + } + } + } + + .o_mail_chat_channel_item.o_mail_chat_channel_unread, .o_mail_chat_partner_item.o_mail_chat_channel_unread{ + font-weight: bold; + } + + .o_mail_chat_sidebar_title { + text-transform: uppercase; + color: @gray-lighter; + margin-top: 25px; + .user-select(none); + cursor: default; + } + + .o_mail_chat_add_more_search{ + padding: @padding-base-vertical @padding-base-horizontal; + .o_mail_chat_add_more_text{ + color: @gray-lighter; + display: inline-block; + font-style: italic; + cursor: pointer; + font-size: @font-size-base; + } + .o_mail_chat_add_more_text:hover{ + text-decoration: underline; + color: white; + cursor: pointer; + } + } + + } + + .o_mail_chat_content { + background-color: white; + + .o_mail_chat_help_message{ + padding: 25px; + color: @gray-light; + } + + .o_mail_thread_date_separator{ + margin: auto; + width: 90%; + .o_mail_thread_date_wrapper { + position: relative; + height: 20px; + border-bottom: solid WhiteSmoke 2px; + margin-bottom: 15px; + .o_mail_thread_date_separator_title { + display: table; + position: relative; + top: 10px; + left: 0; + right: 0; + margin: 0 auto; + padding: 0 10px 0 10px; + font-weight: bold; + background: white; + } + } + } + + .o_mail_thread_message{ + //border-bottom: solid WhiteSmoke 1px; + } + /* + .o_mail_chat_messages { + .o_slack_message { + .o_slack_message_avatar { + > img { + + } + } + .o_slack_message_content { + .o_slack_message_author { + font-weight: bold; + .o_slack_message_time { + font-weight: normal; + color: @gray-light; + } + } + + } + } + } + */ + + .o_mail_chat_composer_wrapper { + border-top: 1px solid @gray-lighter; + background-color: @gray-lighter; + padding: 10px; + .o_mail_compose_message{ + margin-bottom: 0px; + } + /* + > img { + background-image: url(/web/static/src/img/placeholder.png); + } + > input { + padding-left: 15px; + resize: none; + border: 0; + outline: none; + } + */ + } + } +} + + +// ------------------------------------------------------------------ +// Other components +// ------------------------------------------------------------------ +.o_mail_chat_partner_invite{ + height: 50px; +} diff --git a/addons/mail/static/src/less/mail_chatter.less b/addons/mail/static/src/less/mail_chatter.less new file mode 100644 index 00000000000..1f7e9b29018 --- /dev/null +++ b/addons/mail/static/src/less/mail_chatter.less @@ -0,0 +1,54 @@ + +.o_mail_chatter{ + /* Layout for chatter header (buttons and compose message) */ + .o_mail_chatter_compose_buttons, .o_mail_chatter_compose_message_wrap{ + padding-left:15px; + margin-bottom:20px; + //background-color: lightGrey; + } + + /* Specific message style for chatter */ + .o_mail_chatter_messages{ + + .o_mail_thread_message{ + .o_mail_thread_message_type_comment{ // message comment + &:not(.o_mail_thread_message_subtype_note){ + background-color: white; + box-shadow: 0px 1px #CACACA; + } + } + .o_mail_thread_message_type_email{ // message email + background-color: white; + box-shadow: 0px 1px #CACACA; + } + .o_mail_thread_message_type_notification{ // message notification + + } + } + + .o_mail_thread_date_separator{ + .o_mail_thread_date_wrapper { + height: 20px; + .o_mail_thread_date_separator_title { + text-align: center; + font-weight: bold; + color: @gray-light; + } + } + } + } + .o_mail_chatter_show_more_message{ + text-align: center; + cursor: pointer; + } + +} + + +.o_mail_chatter_compose_message{ + .o_mail_chatter_compose_message_suggested_partners label{ + vertical-align: middle; + display: block; + line-height: 14px; + } +} \ No newline at end of file diff --git a/addons/mail/static/src/less/mail_thread.less b/addons/mail/static/src/less/mail_thread.less new file mode 100644 index 00000000000..15cfc5cdf94 --- /dev/null +++ b/addons/mail/static/src/less/mail_thread.less @@ -0,0 +1,184 @@ +// ------------------------------------------------------------------ +// Mail Thread 'Mixin' +// ------------------------------------------------------------------ +.o_mail_thread{ + + /* Starred and Needaction on message */ + .o_mail_thread_message_star, .o_mail_thread_message_needaction { + opacity: 0.5; + color: black; + } + .o_mail_thread_message_star:hover, .o_mail_thread_message_needaction:hover{ + opacity: 1; + cursor: pointer; + } + .o_mail_thread_message_star.o_mail_message_starred{ + color: orange; + } + /* author avatar */ + .o_mail_thread_avatar{ + width: 4.5em; + max-width: 50px; + } + /* message layout */ + .o_mail_thread_message{ + margin-bottom: 10px; + } + .o_mail_thread_message_avatar{ + text-align: center; + } + .o_mail_thread_message_core{ + padding: 5px; + } + .o_mail_thread_message_info{ + margin-top: 0; + } + + /* mail redirect */ + .o_mail_redirect{ + cursor: pointer; + } +} + +// ------------------------------------------------------------------ +// Mail Compose Message +// ------------------------------------------------------------------ +.o_mail_compose_message{ + + // Emoji size in the popover + .o_mail_compose_message_emoji_img img{ + height: 26px; + } + + .o_mail_compose_message_mode_mail{ + textarea { + resize: vertical; + width: 100%; + } + } +} + +// ------------------------------------------------------------------ +// Attachment Icons (common for mail thread message and compose message) +// ------------------------------------------------------------------ + +@-webkit-keyframes oe_mail_attach_loading_anim { + 0% { background: #4BBD00 } + 50% { background: #009123 } + 100% { background: #4BBD00 } +} +@-moz-keyframes oe_mail_attach_loading_anim { + 0% { background: #4BBD00 } + 50% { background: #009123 } + 100% { background: #4BBD00 } +} +@-o-keyframes oe_mail_attach_loading_anim { + 0% { background: #4BBD00 } + 50% { background: #009123 } + 100% { background: #4BBD00 } +} +@keyframes oe_mail_attach_loading_anim { + 0% { background: #4BBD00 } + 50% { background: #009123 } + 100% { background: #4BBD00 } +} + +.o_mail_attachment{ + display: inline-block; + width: 100px; + margin: 4px 2px; + min-height: 80px; + position: relative; + border-radius: 3px; + text-align: center; + vertical-align: top; + + img { + position: absolute; + width: 48px; + height: 48px; + top: 0px; + left: 50%; + margin-left: -24px; + } + + .o_mail_attachment_name { + display: inline-block; + max-width: 100%; + padding: 1px 3px; + margin-top: 50px; + margin-bottom: 5px; + background: #F2F7FF; + overflow: hidden; + color: #4c4c4c; + text-shadow: none; + border-radius: 3px; + } + + &.o_mail_attachment_preview { + background: url( data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAJ0lEQVQYV2MsLS39z4AGLCws0IUYGIeCwrVr12J45sSJE5ieGQIKAbuZKf/EMCs7AAAAAElFTkSuQmCC ); + } + + &.o_mail_attachment_preview img { + display: block; + position: relative; + margin:0px; + width: 100px; + height: 80px; + border-radius: 1px; + border: solid 3px #FFF; + margin-left: -50px; + box-shadow: 0px 3px 10px rgba(0, 0, 0, 0.19); + } + + .o_mail_attachment_progress_bar { + display: none; + position: absolute; + top: 18px; + left: 16px; + right: 16px; + height: 17px; + line-height: 13px; + padding: 0px; + background: #4BBD00; + color: white; + text-align: center; + border-radius: 3px; + border: solid 1px rgba(0,0,0,0.2); + box-shadow: 0px 3px 10px rgba(0, 0, 0, 0.34); + -webkit-animation: oe_mail_attach_loading_anim 0.75s infinite linear; + -moz-animation: oe_mail_attach_loading_anim 0.75s infinite linear; + -o-animation: oe_mail_attach_loading_anim 0.75s infinite linear; + animation: oe_mail_attach_loading_anim 0.75s infinite linear; + } + + &.o_mail_uploading .o_mail_attachment_progress_bar { + display: block; + } + .o_mail_attachment_delete .fa { + display: none; + } + + .o_mail_attachment_delete .fa { + display: block; + position: absolute; + top: -7px; + right: 0px; + color: black; + text-shadow: 1px 0px white, -1px 0px white, 0px 1px white, 0px -1px white; + cursor: pointer; + opacity: 0; + -webkit-transition: opacity 0.2s linear; + -moz-transition: opacity 0.2s linear; + -o-transition: opacity 0.2s linear; + transition: opacity 0.2s linear; + } + + &:hover .o_mail_attachment_delete .fa { + opacity: 1; + -webkit-transition: opacity 0.2s linear; + -moz-transition: opacity 0.2s linear; + -o-transition: opacity 0.2s linear; + transition: opacity 0.2s linear; + } +} diff --git a/addons/mail/static/src/xml/mail_chat_backend.xml b/addons/mail/static/src/xml/mail_chat_backend.xml new file mode 100644 index 00000000000..9dc42749767 --- /dev/null +++ b/addons/mail/static/src/xml/mail_chat_backend.xml @@ -0,0 +1,155 @@ + + + + + +

  • + + + +
  • + + + + + + + + + +
    + +
    +
    + + + + +
    + +
    + + + +

    + + + + +

    + + +

    + + + +
    + +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +
    + + + + + + + + + + + + + + + + + +
    + +
    +
    +
    + + + +
    + + +
    + + +
    + +
    +
    + + + diff --git a/addons/mail/static/src/xml/mail_chat_common.xml b/addons/mail/static/src/xml/mail_chat_common.xml index eb479863b3e..745893c11e1 100644 --- a/addons/mail/static/src/xml/mail_chat_common.xml +++ b/addons/mail/static/src/xml/mail_chat_common.xml @@ -1,103 +1,92 @@ - -
    - - - - -
    - - - - -
    -
    ×
    -
    -
    -
    -
    -
    -
    - - - - -
    - -
    - - - - - - - - - - - - - - - -
    -
    - -
    -
    -
    - -
    -
    -
    -
    -
    -
    - -
    -
    -
    - -
    -
    -
    -
    -
    -
    - -
    - - - - - -
    -
    - -
    -
    - - + +
    +
    + + + + + +
    ×
    +
    +
    +
    + +
    +
    -
    -
    -
    - - -
  • - - - -
  • -
    - + + + +
    + + +
    + + +
    +
    +
    + + avatar + + + avatar + +
    +
    + + + + - + + + - + + + + +

    + + + + + + +

    + +
    + + + + + +
    +
    +
    +
    +
    +
    + + +
    +
      + +
    • + : + + + + + +
    • +
      +
    +
    +
    + + \ No newline at end of file diff --git a/addons/mail/static/src/xml/mail_chatter.xml b/addons/mail/static/src/xml/mail_chatter.xml new file mode 100644 index 00000000000..2d2c150aaba --- /dev/null +++ b/addons/mail/static/src/xml/mail_chatter.xml @@ -0,0 +1,123 @@ + + + + +
    + + +
    + + Log an internal note which will not be sent to followers.
    + Note that users reading the document can read internal notes. +
    +
    + +
    + +
    + +
    +
    +
    + + + +
    + + To: + + Followers of + + + + Followers of this document + + + +
    + +
    + +
    +
    +
    +
    +
    + + +
    +
    + + + +
    + +
    +
    +
    + +
    + + or +
    + +
    +
    +
    +
    + + + + +
    +
    + +
    +
    + +
    + ----------- Show more message ----------- +
    +
    +
    + + + + + + + No Messages. + + + + + +
  • + + + +
  • +
    + + +
    diff --git a/addons/mail/static/src/xml/mail_followers.xml b/addons/mail/static/src/xml/mail_followers.xml index dd6d9883abd..b7548f861fd 100644 --- a/addons/mail/static/src/xml/mail_followers.xml +++ b/addons/mail/static/src/xml/mail_followers.xml @@ -2,76 +2,76 @@ -
    -
    - - -
    -
    -
    - - - + +
    +
    +
    + + + +
    - -
    -
    + -
  • - - - - - - - -
  • +--> + +
  • + + + + + + + +
  • +
    - -
  • + +
  • Add followers
  • - +
  • - - + diff --git a/addons/mail/static/src/xml/mail_thread.xml b/addons/mail/static/src/xml/mail_thread.xml new file mode 100644 index 00000000000..10baacc0577 --- /dev/null +++ b/addons/mail/static/src/xml/mail_thread.xml @@ -0,0 +1,186 @@ + + + + + +
    + +
    + + + + + -
    +
    diff --git a/addons/im_livechat/static/src/xml/im_livechat_backend.xml b/addons/im_livechat/static/src/xml/im_livechat_backend.xml new file mode 100644 index 00000000000..a48027b6047 --- /dev/null +++ b/addons/im_livechat/static/src/xml/im_livechat_backend.xml @@ -0,0 +1,22 @@ + + diff --git a/addons/im_livechat/views/im_livechat_channel_templates.xml b/addons/im_livechat/views/im_livechat_channel_templates.xml index 767d3986e84..c4a3aa3351a 100644 --- a/addons/im_livechat/views/im_livechat_channel_templates.xml +++ b/addons/im_livechat/views/im_livechat_channel_templates.xml @@ -2,6 +2,15 @@ + + + diff --git a/addons/im_livechat/views/im_livechat_channel_views.xml b/addons/im_livechat/views/im_livechat_channel_views.xml index 06f8e3ec6de..9a49b91580a 100644 --- a/addons/im_livechat/views/im_livechat_channel_views.xml +++ b/addons/im_livechat/views/im_livechat_channel_views.xml @@ -29,7 +29,7 @@ - +
    @@ -44,7 +44,7 @@ Operators
    - Sessions + Sessions

    @@ -66,8 +66,8 @@
    -