summaryrefslogtreecommitdiff
path: root/addons/mail
diff options
context:
space:
mode:
authorJérome Maes <jem@openerp.com>2015-06-01 16:37:07 +0200
committerAaron Bohy <aab@odoo.com>2015-09-01 20:16:08 +0200
commit4af96310721237d3640bbd162ff9ea84e627028d (patch)
tree2deb74fa477c9ea33b1a0a65199b96e2d6e5f02b /addons/mail
parent5907be1f335dc21934619a34fc928a19e7885211 (diff)
[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.
Diffstat (limited to 'addons/mail')
-rw-r--r--addons/mail/__openerp__.py3
-rw-r--r--addons/mail/controllers/__init__.py2
-rw-r--r--addons/mail/controllers/bus.py63
-rw-r--r--addons/mail/data/mail_shortcode_data.xml158
-rw-r--r--addons/mail/models/__init__.py3
-rw-r--r--addons/mail/models/mail_channel.py138
-rw-r--r--addons/mail/models/mail_shortcode.py48
-rw-r--r--addons/mail/static/src/audio/purr.mp3bin0 -> 85262 bytes
-rw-r--r--addons/mail/static/src/audio/purr.oggbin0 -> 47390 bytes
-rw-r--r--addons/mail/static/src/audio/ting.mp3bin0 -> 10866 bytes
-rw-r--r--addons/mail/static/src/audio/ting.oggbin0 -> 14654 bytes
-rw-r--r--addons/mail/static/src/css/mail_chat_backend.css114
-rw-r--r--addons/mail/static/src/css/mail_chat_common.css259
-rw-r--r--addons/mail/static/src/img/smiley/avatar.jpgbin0 -> 6462 bytes
-rw-r--r--addons/mail/static/src/img/smiley/beer.pngbin0 -> 6097 bytes
-rw-r--r--addons/mail/static/src/img/smiley/boar.pngbin0 -> 4798 bytes
-rw-r--r--addons/mail/static/src/img/smiley/cat.pngbin0 -> 6083 bytes
-rw-r--r--addons/mail/static/src/img/smiley/confused.pngbin0 -> 4633 bytes
-rw-r--r--addons/mail/static/src/img/smiley/cookie.pngbin0 -> 8077 bytes
-rw-r--r--addons/mail/static/src/img/smiley/ghost.pngbin0 -> 4513 bytes
-rw-r--r--addons/mail/static/src/img/smiley/green.pngbin0 -> 1239 bytes
-rw-r--r--addons/mail/static/src/img/smiley/heart.pngbin0 -> 3302 bytes
-rw-r--r--addons/mail/static/src/img/smiley/laugh.pngbin0 -> 6347 bytes
-rw-r--r--addons/mail/static/src/img/smiley/man.pngbin0 -> 5985 bytes
-rw-r--r--addons/mail/static/src/img/smiley/man_with_turban.pngbin0 -> 6526 bytes
-rw-r--r--addons/mail/static/src/img/smiley/musti.pngbin0 -> 1017 bytes
-rw-r--r--addons/mail/static/src/img/smiley/neutral.pngbin0 -> 4843 bytes
-rw-r--r--addons/mail/static/src/img/smiley/open_mouth.pngbin0 -> 4519 bytes
-rw-r--r--addons/mail/static/src/img/smiley/pinky.pngbin0 -> 2978 bytes
-rw-r--r--addons/mail/static/src/img/smiley/poop.pngbin0 -> 4754 bytes
-rw-r--r--addons/mail/static/src/img/smiley/postal_horn.pngbin0 -> 4815 bytes
-rw-r--r--addons/mail/static/src/img/smiley/sad.pngbin0 -> 4764 bytes
-rw-r--r--addons/mail/static/src/img/smiley/scream.pngbin0 -> 6503 bytes
-rw-r--r--addons/mail/static/src/img/smiley/smile.pngbin0 -> 5890 bytes
-rw-r--r--addons/mail/static/src/img/smiley/stuck_out_tongue.pngbin0 -> 5955 bytes
-rw-r--r--addons/mail/static/src/img/smiley/thumbsdown.pngbin0 -> 5110 bytes
-rw-r--r--addons/mail/static/src/img/smiley/thumbsup.pngbin0 -> 5045 bytes
-rw-r--r--addons/mail/static/src/img/smiley/trollface.pngbin0 -> 2828 bytes
-rw-r--r--addons/mail/static/src/img/smiley/wink.pngbin0 -> 5253 bytes
-rw-r--r--addons/mail/static/src/img/smiley/yellow.pngbin0 -> 1235 bytes
-rw-r--r--addons/mail/static/src/img/smiley/yum.pngbin0 -> 5886 bytes
-rw-r--r--addons/mail/static/src/js/mail_chat_backend.js301
-rw-r--r--addons/mail/static/src/js/mail_chat_common.js433
-rw-r--r--addons/mail/static/src/xml/mail_chat_common.xml103
-rw-r--r--addons/mail/views/mail_shortcode_views.xml48
-rw-r--r--addons/mail/views/mail_templates.xml5
46 files changed, 1677 insertions, 1 deletions
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/mail/controllers/bus.py b/addons/mail/controllers/bus.py
new file mode 100644
index 00000000000..35ede172690
--- /dev/null
+++ b/addons/mail/controllers/bus.py
@@ -0,0 +1,63 @@
+# -*- coding: utf-8 -*
+import base64
+import openerp
+
+from openerp import SUPERUSER_ID
+from openerp.http import request
+
+
+class ChatController(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).
+ """
+ return request.session.uid and request.session.uid or SUPERUSER_ID
+
+ # --------------------------
+ # Extends BUS Controller Poll
+ # --------------------------
+ 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)
+
+ # --------------------------
+ # Anonymous routes (Common Methods)
+ # --------------------------
+ @openerp.http.route('/im_chat/post', type="json", auth="none")
+ def post(self, uuid, message_content, message_type):
+ 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)
+
+ @openerp.http.route(['/im_chat/image/<string:uuid>/<int:user_id>', '/im_chat/image/<string:uuid>/<string:user_id>'], type='http', auth="none")
+ def image(self, uuid, user_id):
+ 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)
+
+ @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()
diff --git a/addons/mail/data/mail_shortcode_data.xml b/addons/mail/data/mail_shortcode_data.xml
new file mode 100644
index 00000000000..bd77ecbf8da
--- /dev/null
+++ b/addons/mail/data/mail_shortcode_data.xml
@@ -0,0 +1,158 @@
+<?xml version="1.0"?>
+<odoo>
+ <data>
+
+ <!-- Smileys -->
+ <record id="im_chat_smiley_smile" model="im_chat.shortcode">
+ <field name="source">:)</field>
+ <field name="substitution">&lt;img src='/im_chat/static/src/img/smile.png'/&gt;</field>
+ <field name="description">Emoji Smile</field>
+ </record>
+
+ <record id="im_chat_smiley_sad" model="im_chat.shortcode">
+ <field name="source">:(</field>
+ <field name="substitution">&lt;img src='/im_chat/static/src/img/sad.png'/&gt;</field>
+ <field name="description">Emoji Sad</field>
+ </record>
+
+ <record id="im_chat_smiley_laugh" model="im_chat.shortcode">
+ <field name="source">:D</field>
+ <field name="substitution">&lt;img src='/im_chat/static/src/img/laugh.png'/&gt;</field>
+ <field name="description">Emoji Laugh</field>
+ </record>
+
+ <record id="im_chat_smiley_wink" model="im_chat.shortcode">
+ <field name="source">;)</field>
+ <field name="substitution">&lt;img src='/im_chat/static/src/img/wink.png'/&gt;</field>
+ <field name="description">Emoji Wink</field>
+ </record>
+
+ <record id="im_chat_smiley_stuck" model="im_chat.shortcode">
+ <field name="source">O_0</field>
+ <field name="substitution">&lt;img src='/im_chat/static/src/img/stuck_out_tongue.png'/&gt;</field>
+ <field name="description">Emoji Stuck Out Tongue Winking Eye</field>
+ </record>
+
+ <record id="im_chat_smiley_scream" model="im_chat.shortcode">
+ <field name="source">:@</field>
+ <field name="substitution">&lt;img src='/im_chat/static/src/img/scream.png'/&gt;</field>
+ <field name="description">Emoji Scream</field>
+ </record>
+
+ <record id="im_chat_smiley_neutral" model="im_chat.shortcode">
+ <field name="source">:|</field>
+ <field name="substitution">&lt;img src='/im_chat/static/src/img/neutral.png'/&gt;</field>
+ <field name="description">Emoji Speechless/Neutral face</field>
+ </record>
+
+ <record id="im_chat_smiley_cheeky" model="im_chat.shortcode">
+ <field name="source">:p</field>
+ <field name="substitution">&lt;img src='/im_chat/static/src/img/yum.png'/&gt;</field>
+ <field name="description">Emoji Cheeky</field>
+ </record>
+
+ <record id="im_chat_smiley_confused" model="im_chat.shortcode">
+ <field name="source">:/</field>
+ <field name="substitution">&lt;img src='/im_chat/static/src/img/confused.png'/&gt;</field>
+ <field name="description">Emoji Confused</field>
+ </record>
+
+ <record id="im_chat_smiley_open_mouth" model="im_chat.shortcode">
+ <field name="source">:O</field>
+ <field name="substitution">&lt;img src='/im_chat/static/src/img/open_mouth.png'/&gt;</field>
+ <field name="description">Emoji Open Mouth</field>
+ </record>
+
+ <record id="im_chat_smiley_heart" model="im_chat.shortcode">
+ <field name="source">&amp;lt;3</field>
+ <field name="substitution">&lt;img src='/im_chat/static/src/img/heart.png'/&gt;</field>
+ <field name="description">Emoji Heart</field>
+ </record>
+
+ <!-- Other images -->
+ <record id="im_chat_smiley_thumbsup" model="im_chat.shortcode">
+ <field name="source">:+1</field>
+ <field name="substitution">&lt;img src='/im_chat/static/src/img/thumbsup.png'/&gt;</field>
+ <field name="description">Emoji Thumbs Up</field>
+ </record>
+
+ <record id="im_chat_smiley_thumbsdown" model="im_chat.shortcode">
+ <field name="source">:-1</field>
+ <field name="substitution">&lt;img src='/im_chat/static/src/img/thumbsdown.png'/&gt;</field>
+ <field name="description">Emoji Thumbs Down</field>
+ </record>
+
+ <record id="im_chat_smiley_kitten" model="im_chat.shortcode">
+ <field name="source">:kitten</field>
+ <field name="substitution">&lt;img src='/im_chat/static/src/img/cat.png'/&gt;</field>
+ <field name="description">Emoji Kitten</field>
+ </record>
+
+ <record id="im_chat_smiley_pinky" model="im_chat.shortcode">
+ <field name="source">:pinky</field>
+ <field name="substitution">&lt;img src='/im_chat/static/src/img/pinky.png'/&gt;</field>
+ <field name="description">Emoji Pinky</field>
+ </record>
+
+ <record id="im_chat_smiley_musti" model="im_chat.shortcode">
+ <field name="source">:musti</field>
+ <field name="substitution">&lt;img src='/im_chat/static/src/img/musti.png'/&gt;</field>
+ <field name="description">Emoji Musti</field>
+ </record>
+
+ <record id="im_chat_smiley_man_with_turban" model="im_chat.shortcode">
+ <field name="source">:turban</field>
+ <field name="substitution">&lt;img src='/im_chat/static/src/img/man_with_turban.png'/&gt;</field>
+ <field name="description">Emoji Man With Turban</field>
+ </record>
+
+ <record id="im_chat_smiley_troll" model="im_chat.shortcode">
+ <field name="source">:troll</field>
+ <field name="substitution">&lt;img src='/im_chat/static/src/img/trollface.png'/&gt;</field>
+ <field name="description">Emoji Troll Face</field>
+ </record>
+
+ <record id="im_chat_smiley_boar" model="im_chat.shortcode">
+ <field name="source">:boar</field>
+ <field name="substitution">&lt;img src='/im_chat/static/src/img/boar.png'/&gt;</field>
+ <field name="description">Emoji Boar</field>
+ </record>
+
+ <record id="im_chat_smiley_poop" model="im_chat.shortcode">
+ <field name="source">:poop</field>
+ <field name="substitution">&lt;img src='/im_chat/static/src/img/poop.png'/&gt;</field>
+ <field name="description">Emoji Poop</field>
+ </record>
+
+ <record id="im_chat_smiley_cookie" model="im_chat.shortcode">
+ <field name="source">:cookie</field>
+ <field name="substitution">&lt;img src='/im_chat/static/src/img/cookie.png'/&gt;</field>
+ <field name="description">Emoji Cookie</field>
+ </record>
+
+ <record id="im_chat_smiley_postal_horn" model="im_chat.shortcode">
+ <field name="source">:postal_horn</field>
+ <field name="substitution">&lt;img src='/im_chat/static/src/img/postal_horn.png'/&gt;</field>
+ <field name="description">Emoji Postal Horn</field>
+ </record>
+
+ <record id="im_chat_smiley_ghost" model="im_chat.shortcode">
+ <field name="source">:ghost</field>
+ <field name="substitution">&lt;img src='/im_chat/static/src/img/ghost.png'/&gt;</field>
+ <field name="description">Emoji Ghost</field>
+ </record>
+
+ <record id="im_chat_smiley_beer" model="im_chat.shortcode">
+ <field name="source">:beer</field>
+ <field name="substitution">&lt;img src='/im_chat/static/src/img/beer.png'/&gt;</field>
+ <field name="description">Emoji Beer</field>
+ </record>
+
+ <record id="im_chat_smiley_man" model="im_chat.shortcode">
+ <field name="source">:man</field>
+ <field name="substitution">&lt;img src='/im_chat/static/src/img/man.png'/&gt;</field>
+ <field name="description">Emoji Man</field>
+ </record>
+
+ </data>
+</odoo>
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'''^<img\s+src=('|")([^'"]*)\1\s*/?>$''', 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/mail/static/src/audio/purr.mp3 b/addons/mail/static/src/audio/purr.mp3
new file mode 100644
index 00000000000..849d303c3b1
--- /dev/null
+++ b/addons/mail/static/src/audio/purr.mp3
Binary files differ
diff --git a/addons/mail/static/src/audio/purr.ogg b/addons/mail/static/src/audio/purr.ogg
new file mode 100644
index 00000000000..2ddc77537f1
--- /dev/null
+++ b/addons/mail/static/src/audio/purr.ogg
Binary files differ
diff --git a/addons/mail/static/src/audio/ting.mp3 b/addons/mail/static/src/audio/ting.mp3
new file mode 100644
index 00000000000..fd29bf599d6
--- /dev/null
+++ b/addons/mail/static/src/audio/ting.mp3
Binary files differ
diff --git a/addons/mail/static/src/audio/ting.ogg b/addons/mail/static/src/audio/ting.ogg
new file mode 100644
index 00000000000..cc8c0f01eff
--- /dev/null
+++ b/addons/mail/static/src/audio/ting.ogg
Binary files differ
diff --git a/addons/mail/static/src/css/mail_chat_backend.css b/addons/mail/static/src/css/mail_chat_backend.css
new file mode 100644
index 00000000000..d01577eb56a
--- /dev/null
+++ b/addons/mail/static/src/css/mail_chat_backend.css
@@ -0,0 +1,114 @@
+.oe_im {
+ position: fixed;
+ background-color: #eaeaea;
+ width: 250px;
+ 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 {
+ .oe_im, .oe_im_chatview {
+ visibility: hidden;
+ }
+}
+/* button */
+.oe_systray #oe_topbar_imbutton_icon {
+ color: white;
+}
+.oe_topbar_item.oe_topbar_imbutton .fa {
+ position: relative;
+ 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;
+ height: 100%;
+ overflow-y: scroll;
+}
+.oe_im_user {
+ position: relative;
+ padding: 2px 8px;
+ cursor: pointer;
+ font-size: 13px;
+ margin-bottom: 10px;
+}
+.oe_im_user:hover {
+ background: lightGrey;
+}
+.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);
+}
+.oe_im_user_avatar {
+ width: 26px;
+ height: auto;
+}
+.oe_im_user_name {
+ width: 162px;
+ line-height: 26px;
+ padding-right: 15px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+ position: relative;
+}
+
+.oe_im_user_online {
+ display: none;
+ position: absolute;
+ top: 9.5px;
+ right: 11px;
+ width: 11px;
+ height: 11px;
+ vertical-align: middle;
+ border: 0;
+}
diff --git a/addons/mail/static/src/css/mail_chat_common.css b/addons/mail/static/src/css/mail_chat_common.css
new file mode 100644
index 00000000000..1ad2981eb41
--- /dev/null
+++ b/addons/mail/static/src/css/mail_chat_common.css
@@ -0,0 +1,259 @@
+
+
+
+.openerp_style { /* base style of openerp */
+ font-family: "Lucida Grande", Helvetica, Verdana, Arial, sans-serif;
+ color: #4c4c4c;
+ font-size: 13px;
+ background: white;
+}
+
+/* button */
+
+.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;
+}
+
+/* conversations */
+
+.oe_im_chatview {
+ position: fixed;
+ overflow: hidden;
+ margin-bottom: 5px;
+ margin-right: 6px;
+ background-color: #fff;
+ 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 {
+ padding: 3px 6px 2px;
+ 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{
+ max-width: 75%;
+ word-wrap: break-word;
+ display: inline-block;
+ height: 15px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.oe_im_chatview_header .right_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;
+}
+
+/* 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;
+ 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;
+}
+
+.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
diff --git a/addons/mail/static/src/img/smiley/avatar.jpg b/addons/mail/static/src/img/smiley/avatar.jpg
new file mode 100644
index 00000000000..7168794022e
--- /dev/null
+++ b/addons/mail/static/src/img/smiley/avatar.jpg
Binary files differ
diff --git a/addons/mail/static/src/img/smiley/beer.png b/addons/mail/static/src/img/smiley/beer.png
new file mode 100644
index 00000000000..cd78bed7440
--- /dev/null
+++ b/addons/mail/static/src/img/smiley/beer.png
Binary files differ
diff --git a/addons/mail/static/src/img/smiley/boar.png b/addons/mail/static/src/img/smiley/boar.png
new file mode 100644
index 00000000000..3ef50325016
--- /dev/null
+++ b/addons/mail/static/src/img/smiley/boar.png
Binary files differ
diff --git a/addons/mail/static/src/img/smiley/cat.png b/addons/mail/static/src/img/smiley/cat.png
new file mode 100644
index 00000000000..dbf1b0276ab
--- /dev/null
+++ b/addons/mail/static/src/img/smiley/cat.png
Binary files differ
diff --git a/addons/mail/static/src/img/smiley/confused.png b/addons/mail/static/src/img/smiley/confused.png
new file mode 100644
index 00000000000..8dc494db083
--- /dev/null
+++ b/addons/mail/static/src/img/smiley/confused.png
Binary files differ
diff --git a/addons/mail/static/src/img/smiley/cookie.png b/addons/mail/static/src/img/smiley/cookie.png
new file mode 100644
index 00000000000..177d0f9ad86
--- /dev/null
+++ b/addons/mail/static/src/img/smiley/cookie.png
Binary files differ
diff --git a/addons/mail/static/src/img/smiley/ghost.png b/addons/mail/static/src/img/smiley/ghost.png
new file mode 100644
index 00000000000..671dd0c9e2e
--- /dev/null
+++ b/addons/mail/static/src/img/smiley/ghost.png
Binary files differ
diff --git a/addons/mail/static/src/img/smiley/green.png b/addons/mail/static/src/img/smiley/green.png
new file mode 100644
index 00000000000..0700a5ef71b
--- /dev/null
+++ b/addons/mail/static/src/img/smiley/green.png
Binary files differ
diff --git a/addons/mail/static/src/img/smiley/heart.png b/addons/mail/static/src/img/smiley/heart.png
new file mode 100644
index 00000000000..7d7790ce4df
--- /dev/null
+++ b/addons/mail/static/src/img/smiley/heart.png
Binary files differ
diff --git a/addons/mail/static/src/img/smiley/laugh.png b/addons/mail/static/src/img/smiley/laugh.png
new file mode 100644
index 00000000000..11c91eb22e6
--- /dev/null
+++ b/addons/mail/static/src/img/smiley/laugh.png
Binary files differ
diff --git a/addons/mail/static/src/img/smiley/man.png b/addons/mail/static/src/img/smiley/man.png
new file mode 100644
index 00000000000..c4a4217f4d4
--- /dev/null
+++ b/addons/mail/static/src/img/smiley/man.png
Binary files differ
diff --git a/addons/mail/static/src/img/smiley/man_with_turban.png b/addons/mail/static/src/img/smiley/man_with_turban.png
new file mode 100644
index 00000000000..8a0bda1ca77
--- /dev/null
+++ b/addons/mail/static/src/img/smiley/man_with_turban.png
Binary files differ
diff --git a/addons/mail/static/src/img/smiley/musti.png b/addons/mail/static/src/img/smiley/musti.png
new file mode 100644
index 00000000000..f6843ddf95c
--- /dev/null
+++ b/addons/mail/static/src/img/smiley/musti.png
Binary files differ
diff --git a/addons/mail/static/src/img/smiley/neutral.png b/addons/mail/static/src/img/smiley/neutral.png
new file mode 100644
index 00000000000..682a1ba066d
--- /dev/null
+++ b/addons/mail/static/src/img/smiley/neutral.png
Binary files differ
diff --git a/addons/mail/static/src/img/smiley/open_mouth.png b/addons/mail/static/src/img/smiley/open_mouth.png
new file mode 100644
index 00000000000..daf914274a6
--- /dev/null
+++ b/addons/mail/static/src/img/smiley/open_mouth.png
Binary files differ
diff --git a/addons/mail/static/src/img/smiley/pinky.png b/addons/mail/static/src/img/smiley/pinky.png
new file mode 100644
index 00000000000..4466946601c
--- /dev/null
+++ b/addons/mail/static/src/img/smiley/pinky.png
Binary files differ
diff --git a/addons/mail/static/src/img/smiley/poop.png b/addons/mail/static/src/img/smiley/poop.png
new file mode 100644
index 00000000000..73a4dc84008
--- /dev/null
+++ b/addons/mail/static/src/img/smiley/poop.png
Binary files differ
diff --git a/addons/mail/static/src/img/smiley/postal_horn.png b/addons/mail/static/src/img/smiley/postal_horn.png
new file mode 100644
index 00000000000..5b7ed85d4fc
--- /dev/null
+++ b/addons/mail/static/src/img/smiley/postal_horn.png
Binary files differ
diff --git a/addons/mail/static/src/img/smiley/sad.png b/addons/mail/static/src/img/smiley/sad.png
new file mode 100644
index 00000000000..82552008719
--- /dev/null
+++ b/addons/mail/static/src/img/smiley/sad.png
Binary files differ
diff --git a/addons/mail/static/src/img/smiley/scream.png b/addons/mail/static/src/img/smiley/scream.png
new file mode 100644
index 00000000000..76bfc6b8a65
--- /dev/null
+++ b/addons/mail/static/src/img/smiley/scream.png
Binary files differ
diff --git a/addons/mail/static/src/img/smiley/smile.png b/addons/mail/static/src/img/smiley/smile.png
new file mode 100644
index 00000000000..81a83968996
--- /dev/null
+++ b/addons/mail/static/src/img/smiley/smile.png
Binary files differ
diff --git a/addons/mail/static/src/img/smiley/stuck_out_tongue.png b/addons/mail/static/src/img/smiley/stuck_out_tongue.png
new file mode 100644
index 00000000000..ea9aeb99e29
--- /dev/null
+++ b/addons/mail/static/src/img/smiley/stuck_out_tongue.png
Binary files differ
diff --git a/addons/mail/static/src/img/smiley/thumbsdown.png b/addons/mail/static/src/img/smiley/thumbsdown.png
new file mode 100644
index 00000000000..74a5005c893
--- /dev/null
+++ b/addons/mail/static/src/img/smiley/thumbsdown.png
Binary files differ
diff --git a/addons/mail/static/src/img/smiley/thumbsup.png b/addons/mail/static/src/img/smiley/thumbsup.png
new file mode 100644
index 00000000000..116388fc3e2
--- /dev/null
+++ b/addons/mail/static/src/img/smiley/thumbsup.png
Binary files differ
diff --git a/addons/mail/static/src/img/smiley/trollface.png b/addons/mail/static/src/img/smiley/trollface.png
new file mode 100644
index 00000000000..eb222e2ad69
--- /dev/null
+++ b/addons/mail/static/src/img/smiley/trollface.png
Binary files differ
diff --git a/addons/mail/static/src/img/smiley/wink.png b/addons/mail/static/src/img/smiley/wink.png
new file mode 100644
index 00000000000..756766dd3e9
--- /dev/null
+++ b/addons/mail/static/src/img/smiley/wink.png
Binary files differ
diff --git a/addons/mail/static/src/img/smiley/yellow.png b/addons/mail/static/src/img/smiley/yellow.png
new file mode 100644
index 00000000000..726570e518f
--- /dev/null
+++ b/addons/mail/static/src/img/smiley/yellow.png
Binary files differ
diff --git a/addons/mail/static/src/img/smiley/yum.png b/addons/mail/static/src/img/smiley/yum.png
new file mode 100644
index 00000000000..fc39637ecd8
--- /dev/null
+++ b/addons/mail/static/src/img/smiley/yum.png
Binary files differ
diff --git a/addons/mail/static/src/js/mail_chat_backend.js b/addons/mail/static/src/js/mail_chat_backend.js
new file mode 100644
index 00000000000..cd1cc00fdf2
--- /dev/null
+++ b/addons/mail/static/src/js/mail_chat_backend.js
@@ -0,0 +1,301 @@
+odoo.define('im_chat.im_chat', 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 SystrayMenu = require('web.SystrayMenu');
+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 _t = core._t;
+var QWeb = core.qweb;
+
+var USERS_LIMIT = 20;
+
+
+// ########## CONVERSATION extentions ###############
+
+im_chat_common.ConversationManager.include({
+ _message_receive: function(message){
+ 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);
+ });
+ },
+ // window title
+ window_title_change: function() {
+ this.super();
+ var title = undefined;
+ if (this.get("waiting_messages") !== 0) {
+ 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]);
+ }
+ }
+ },
+ // 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);
+ }
+ 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',
+ events: {
+ "click": "clicked",
+ },
+ 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);
+ return this._super();
+ },
+ clicked: function(ev) {
+ ev.preventDefault();
+ this.trigger("clicked");
+ },
+});
+
+// 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);
+ }
+});
+
+return {
+ InstantMessaging: InstantMessaging,
+ UserWidget: UserWidget,
+};
+
+});
diff --git a/addons/mail/static/src/js/mail_chat_common.js b/addons/mail/static/src/js/mail_chat_common.js
new file mode 100644
index 00000000000..0aaa0e68d16
--- /dev/null
+++ b/addons/mail/static/src/js/mail_chat_common.js
@@ -0,0 +1,433 @@
+odoo.define('im_chat.im_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 _t = core._t;
+var QWeb = core.qweb;
+
+var NBR_LIMIT_HISTORY = 20;
+
+
+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,
+ load_history: true,
+ focus: false,
+ });
+ // 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();
+ },
+ 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);
+
+ // 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);
+ }
+ // activate the received session
+ if(message.uuid){
+ this.session_apply(message, options);
+ }
+ }
+ },
+
+ // 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();
+ },
+ 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)
+ var self = this;
+ options = _.extend(_.clone(this.options), options || {});
+ // force open
+ var session = _.clone(active_session);
+ if(options['force_open']){
+ session.state = 'open';
+ }
+ // create/get the conversation widget
+ var conv = this.sessions[session.uuid];
+ if (! conv) {
+ if(session.state !== 'closed'){
+ conv = new Conversation(this, this, session, options);
+ conv.appendTo($("body"));
+ conv.on("destroyed", this, _.bind(this.session_delete, this));
+ this.sessions[session.uuid] = conv;
+ this.calc_positions();
+ }
+ }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();
+ }
+ return conv;
+ },
+ session_delete: function(uuid){
+ delete this.sessions[uuid];
+ this.calc_positions();
+ },
+ 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);
+ },
+ // 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");
+ _.each(this.sessions, function(s) {
+ s.set("bottom_position", self.get("bottom_offset"));
+ s.set("right_position", current);
+ current += s.$().outerWidth(true);
+ });
+ },
+ destroy: function() {
+ $(window).off("unload", this.unload);
+ $(window).off("focus", this.window_focus);
+ $(window).off("blur", this.window_blur);
+ return this._super();
+ }
+});
+
+
+var Conversation = Widget.extend({
+ className: "openerp_style oe_im_chatview",
+ events: {
+ "keydown input": "keydown",
+ "click .oe_im_chatview_close": "click_close",
+ "click .oe_im_chatview_header": "click_header"
+ },
+ init: function(parent, c_manager, session, options) {
+ this._super(parent);
+ this.c_manager = c_manager;
+ this.options = options || {};
+ this.loading_history = true;
+ this.set("messages", []);
+ this.set("session", session);
+ this.set("right_position", 0);
+ this.set("bottom_position", 0);
+ this.set("pending", 0);
+ this.inputPlaceholder = this.options.defaultInputPlaceholder;
+ },
+ 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.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(){
+ 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));
+ }
+ });
+ },
+ // ui
+ show: function(){
+ this.$().animate({
+ height: this.full_height
+ });
+ this.set("pending", 0);
+ },
+ hide: function(){
+ this.$().animate({
+ height: this.$(".oe_im_chatview_header").outerHeight()
+ });
+ },
+ calc_pos: function() {
+ this.$().css("right", this.get("right_position"));
+ this.$().css("bottom", this.get("bottom_position"));
+ },
+ // session
+ session_update_state: 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(", "));
+ // update the fold state
+ if(this.get("session").state){
+ if(this.get("session").state === 'closed'){
+ this.destroy();
+ }else{
+ if(this.get("session").state === 'open'){
+ this.show();
+ }else{
+ this.hide();
+ }
+ }
+ }
+ },
+ // messages
+ 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(messages){
+ self.insert_messages(messages);
+ if(messages.length != NBR_LIMIT_HISTORY){
+ self.loading_history = false;
+ }
+ }else{
+ self.loading_history = false;
+ }
+ });
+ }
+ },
+ message_receive: function(message) {
+ if (this.get('session').state === 'open') {
+ this.set("pending", 0);
+ } else {
+ this.set("pending", this.get("pending") + 1);
+ }
+ this.insert_messages([message]);
+ this._go_bottom();
+ },
+ message_send: function(message, type) {
+ var self = this;
+ var send_it = function() {
+ return session.rpc("/im_chat/post", {uuid: self.get("session").uuid, message_type: type, message_content: message});
+ };
+ var tries = 0;
+ send_it().fail(function(error, e) {
+ e.preventDefault();
+ tries += 1;
+ if (tries < 3)
+ return send_it();
+ });
+ },
+ insert_messages: function(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()];
+ }
+ m.create_date = moment(time.str_to_datetime(m.create_date)).format('YYYY-MM-DD HH:mm:ss');
+ return m;
+ });
+ this.set("messages", _.sortBy(this.get("messages").concat(messages), function(m){ return m.id; }));
+ },
+ render_messages: 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();
+ },
+ // 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;
+ },
+ 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();
+ }else{
+ this.$el.next().find('.oe_im_chatview_input').focus();
+ }
+ e.stopPropagation();
+ this.session_update_state('closed');
+ }
+ if(e && e.which !== 13) {
+ return;
+ }
+ var mes = this.$("input").val();
+ if (! mes.trim()) {
+ return;
+ }
+ this.$("input").val("");
+ this.message_send(mes, "message");
+ },
+ _go_bottom: function() {
+ this.$(".oe_im_chatview_content").scrollTop(this.$(".oe_im_chatview_content").get(0).scrollHeight);
+ },
+ 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');
+ },
+ destroy: function() {
+ this.trigger("destroyed", this.get('session').uuid);
+ return this._super();
+ }
+});
+
+
+return {
+ Conversation: Conversation,
+ ConversationManager: ConversationManager,
+};
+
+}); \ No newline at end of file
diff --git a/addons/mail/static/src/xml/mail_chat_common.xml b/addons/mail/static/src/xml/mail_chat_common.xml
new file mode 100644
index 00000000000..eb479863b3e
--- /dev/null
+++ b/addons/mail/static/src/xml/mail_chat_common.xml
@@ -0,0 +1,103 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates xml:space="preserve">
+<t t-name="im_chat.Conversation">
+ <div class="oe_im_chatview_header">
+ <span class="header_name"></span>
+ <span class="nbr_messages"/>
+ <span class="right_buttons">
+ <!-- options -->
+ <div t-if="widget.actions.length !== 0" class="btn-group button_option_group">
+ <span class="dropdown-toggle" data-toggle="dropdown">
+ <i class="fa fa-cogs"></i>
+ </span>
+ <ul class="dropdown-menu" role="menu">
+ <t t-foreach="widget.actions" t-as="action">
+ <li t-att-class="action.class">
+ <i t-att-class="action.icon_class"></i> <t t-esc="action.label"/>
+ </li>
+ </t>
+ </ul>
+ </div>
+ <div class="oe_im_chatview_close">×</div>
+ </span>
+ </div>
+ <div class="oe_im_chatview_content">
+ <div class="oe_im_chatview_status"/>
+ <div class="oe_im_chatview_content_bubbles"></div>
+ </div>
+ <div class="oe_im_chatview_footer">
+ <input class="oe_im_chatview_input" t-att-placeholder="widget.inputPlaceholder" />
+ </div>
+</t>
+<t t-name="im_chat.Conversation_content">
+ <t t-foreach="_.keys(list)" t-as="date">
+ <div class="oe_im_chatview_date_separator">
+ <t t-esc="window.moment(date).format('LL')"/>
+ </div>
+ <t t-foreach="list[date]" t-as="bubble">
+ <t t-if="bubble[0].type === 'message'">
+ <t t-call="im_chat.Conversation_message_bubble">
+ <t t-set="messages" t-value="bubble"/>
+ </t>
+ </t>
+ <t t-if="bubble[0].type === 'meta'">
+ <t t-call="im_chat.Conversation_technical_bubble">
+ <t t-set="messages" t-value="bubble"/>
+ </t>
+ </t>
+ </t>
+ </t>
+</t>
+<t t-name="im_chat.Conversation_message_bubble">
+ <div class="oe_im_chatview_message_bubble">
+ <div class="oe_im_chatview_clip">
+ <img class="oe_im_chatview_avatar" t-att-src="_.last(messages).from_id[2]"/>
+ </div>
+ <div class="oe_im_chatview_from"><t t-esc="_.last(messages).from_id[1]"/></div>
+ <div class="oe_im_chatview_bubble_list">
+ <t t-foreach="messages" t-as="m">
+ <div class="oe_im_chatview_bubble_item"><t t-raw="m.message"/></div>
+ </t>
+ </div>
+ <div class="oe_im_chatview_time"><t t-esc="window.moment((_.last(messages).create_date)).format('LT')"/></div>
+ </div>
+</t>
+<t t-name="im_chat.Conversation_technical_bubble">
+ <div class="oe_im_chatview_technical_bubble">
+ <div class="oe_im_chatview_from"><t t-esc="_.last(messages).from_id[1]"/></div>
+ <div>
+ <t t-foreach="messages" t-as="m">
+ <div><t t-raw="m.message"/></div>
+ </t>
+ </div>
+ <div class="oe_im_chatview_time"><t t-esc="window.moment((_.last(messages).create_date)).format('LT')"/></div>
+ </div>
+</t>
+
+<t t-name="im_chat.UserWidget">
+ <div class="oe_im_user ui-draggable">
+ <span class="oe_im_user_clip">
+ <img t-att-src="widget.get('image_url')" class="oe_im_user_avatar"/>
+ </span>
+ <span class="oe_im_user_name"><t t-esc="widget.get('name')"/></span>
+ <img t-att-src="'/im_chat/static/src/img/green.png'" t-att-data-im-user-id="widget.get('id')" class="oe_im_user_online"/>
+ </div>
+</t>
+<t t-name="im_chat.InstantMessaging">
+ <div class="oe_im">
+ <div class="oe_im_frame_header">
+ <i class="fa fa-search oe_im_search_icon"/>
+ <input class="oe_im_searchbox" placeholder="Search users..."/>
+ </div>
+ <div class="oe_im_users"></div>
+ <div class="oe_im_content"></div>
+ </div>
+</t>
+<t t-name="im_chat.ImTopButton">
+ <li t-att-title='_t("Display Instant Messaging")'>
+ <a href="#">
+ <i id="oe_topbar_imbutton_icon" class="fa fa-comments"/>
+ </a>
+ </li>
+</t>
+</templates>
diff --git a/addons/mail/views/mail_shortcode_views.xml b/addons/mail/views/mail_shortcode_views.xml
new file mode 100644
index 00000000000..e281950d102
--- /dev/null
+++ b/addons/mail/views/mail_shortcode_views.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <data>
+
+ <!-- im_chat.shortcode -->
+ <record id="im_chat_shortcode_action" model="ir.actions.act_window">
+ <field name="name">Chat Shortcode</field>
+ <field name="res_model">im_chat.shortcode</field>
+ <field name="view_mode">tree,form</field>
+ <field name="help" type="html">
+ <p class="oe_view_nocontent_create">
+ Click to define a new chat shortcode.
+ </p><p>
+ A shortcode is a keyboard shortcut. For instance, you type #gm and it will be transformed into "Good Morning".
+ </p>
+ </field>
+ </record>
+
+ <record id="im_chat_shortcode_view_tree" model="ir.ui.view">
+ <field name="name">im_chat.shortcode.tree</field>
+ <field name="model">im_chat.shortcode</field>
+ <field name="arch" type="xml">
+ <tree string="Shortcodes">
+ <field name="source"/>
+ <field name="substitution"/>
+ <field name="description"/>
+ </tree>
+ </field>
+ </record>
+
+ <record id="im_chat_shortcode_view_form" model="ir.ui.view">
+ <field name="name">im_chat.shortcode.form</field>
+ <field name="model">im_chat.shortcode</field>
+ <field name="arch" type="xml">
+ <form string="Shortcodes">
+ <sheet>
+ <group>
+ <field name="source"/>
+ <field name="substitution"/>
+ <field name="description"/>
+ </group>
+ </sheet>
+ </form>
+ </field>
+ </record>
+
+ </data>
+</odoo>
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 @@
<script type="text/javascript" src="/mail/static/src/js/mail_followers.js"></script>
<script type="text/javascript" src="/mail/static/src/js/many2many_tags_email.js"></script>
<script type="text/javascript" src="/mail/static/src/js/announcement.js"></script>
+
+ <link rel="stylesheet" href="/mail/static/src/css/mail_chat_common.css"/>
+ <link rel="stylesheet" href="/mail/static/src/css/mail_chat_backend.css"/>
+ <script type="text/javascript" src="/mail/static/src/js/mail_chat_common.js"></script>
+ <script type="text/javascript" src="/mail/static/src/js/mail_chat_backend.js"></script>
</xpath>
</template>
</data>