diff --git a/addons/im_livechat/controllers/main.py b/addons/im_livechat/controllers/main.py
index b90a418f8cd..6803f637e93 100644
--- a/addons/im_livechat/controllers/main.py
+++ b/addons/im_livechat/controllers/main.py
@@ -98,7 +98,7 @@ class LivechatController(http.Controller):
if previous_operator_id:
previous_operator_id = int(previous_operator_id)
- return request.env["im_livechat.channel"].with_context(lang=False).sudo().browse(channel_id)._get_mail_channel(anonymous_name, previous_operator_id, user_id, country_id)
+ return request.env["im_livechat.channel"].with_context(lang=False).sudo().browse(channel_id)._open_livechat_mail_channel(anonymous_name, previous_operator_id, user_id, country_id)
@http.route('/im_livechat/feedback', type='json', auth='public', cors="*")
def feedback(self, uuid, rate, reason=None, **kwargs):
diff --git a/addons/im_livechat/models/im_livechat_channel.py b/addons/im_livechat/models/im_livechat_channel.py
index ed7a6a73252..5f9db25f65e 100644
--- a/addons/im_livechat/models/im_livechat_channel.py
+++ b/addons/im_livechat/models/im_livechat_channel.py
@@ -104,7 +104,28 @@ class ImLivechatChannel(models.Model):
self.ensure_one()
return self.user_ids.filtered(lambda user: user.im_status == 'online')
- def _get_mail_channel(self, anonymous_name, previous_operator_id=None, user_id=None, country_id=None):
+ def _get_livechat_mail_channel_vals(self, anonymous_name, operator, user_id=None, country_id=None):
+ # partner to add to the mail.channel
+ operator_partner_id = operator.partner_id.id
+ channel_partner_to_add = [(4, operator_partner_id)]
+ visitor_user = False
+ if user_id:
+ visitor_user = self.env['res.users'].browse(user_id)
+ if visitor_user and visitor_user.active: # valid session user (not public)
+ channel_partner_to_add.append((4, visitor_user.partner_id.id))
+ return {
+ 'channel_partner_ids': channel_partner_to_add,
+ 'livechat_operator_id': operator_partner_id,
+ 'livechat_channel_id': self.id,
+ 'anonymous_name': False if user_id else anonymous_name,
+ 'country_id': country_id,
+ 'channel_type': 'livechat',
+ 'name': ', '.join([visitor_user.name if visitor_user else anonymous_name, operator.livechat_username if operator.livechat_username else operator.name]),
+ 'public': 'private',
+ 'email_send': False,
+ }
+
+ def _open_livechat_mail_channel(self, anonymous_name, previous_operator_id=None, user_id=None, country_id=None):
""" Return a mail.channel given a livechat channel. It creates one with a connected operator, or return false otherwise
:param anonymous_name : the name of the anonymous person of the channel
:param previous_operator_id : partner_id.id of the previous operator that this visitor had in the past
@@ -118,7 +139,6 @@ class ImLivechatChannel(models.Model):
the system will first try to assign that operator if he's available (to improve user experience).
"""
self.ensure_one()
-
operator = False
if previous_operator_id:
available_users = self._get_available_users()
@@ -131,27 +151,10 @@ class ImLivechatChannel(models.Model):
# no one available
return False
- operator_partner_id = operator.partner_id.id
- # partner to add to the mail.channel
- channel_partner_to_add = [(4, operator_partner_id)]
- visitor_user = False
- if user_id:
- visitor_user = self.env['res.users'].browse(user_id)
- if visitor_user and visitor_user.active: # valid session user (not public)
- channel_partner_to_add.append((4, visitor_user.partner_id.id))
# create the session, and add the link with the given channel
- mail_channel = self.env["mail.channel"].with_context(mail_create_nosubscribe=False).sudo().create({
- 'channel_partner_ids': channel_partner_to_add,
- 'livechat_operator_id': operator_partner_id,
- 'livechat_channel_id': self.id,
- 'anonymous_name': False if user_id else anonymous_name,
- 'country_id': country_id,
- 'channel_type': 'livechat',
- 'name': ', '.join([visitor_user.name if visitor_user else anonymous_name, operator.livechat_username if operator.livechat_username else operator.name]),
- 'public': 'private',
- 'email_send': False,
- })
- mail_channel._broadcast([operator_partner_id])
+ mail_channel_vals = self._get_livechat_mail_channel_vals(anonymous_name, operator, user_id=user_id, country_id=country_id)
+ mail_channel = self.env["mail.channel"].with_context(mail_create_nosubscribe=False).sudo().create(mail_channel_vals)
+ mail_channel._broadcast([operator.partner_id.id])
return mail_channel.sudo().channel_info()[0]
def _get_random_operator(self):
diff --git a/addons/im_livechat/tests/test_get_mail_channel.py b/addons/im_livechat/tests/test_get_mail_channel.py
index ad464de559d..2ac615dca4a 100644
--- a/addons/im_livechat/tests/test_get_mail_channel.py
+++ b/addons/im_livechat/tests/test_get_mail_channel.py
@@ -47,20 +47,20 @@ class TestGetMailChannel(TransactionCase):
"""
for i in range(5):
- mail_channels = self._get_mail_channels()
+ mail_channels = self._open_livechat_mail_channel()
channel_operators = [channel_info['operator_pid'] for channel_info in mail_channels]
channel_operator_ids = [channel_operator[0] for channel_operator in channel_operators]
self.assertTrue(all(partner_id in channel_operator_ids for partner_id in self.operators.mapped('partner_id').ids))
- visitor_user_channel = self.livechat_channel._get_mail_channel('Visitor', user_id=self.visitor_user.id)
+ visitor_user_channel = self.livechat_channel._open_livechat_mail_channel('Visitor', user_id=self.visitor_user.id)
chat_title = '%s (%s)' % (self.visitor_user.name, self.visitor_user.country_id.name)
self.assertEqual(visitor_user_channel['correspondent_name'], chat_title, "Chat title should be correct and should contain visitor's country name")
- def _get_mail_channels(self):
+ def _open_livechat_mail_channel(self):
mail_channels = []
for i in range(5):
- mail_channel = self.livechat_channel._get_mail_channel('Anonymous')
+ mail_channel = self.livechat_channel._open_livechat_mail_channel('Anonymous')
mail_channels.append(mail_channel)
# send a message to mark this channel as 'active'
self.env['mail.channel'].browse(mail_channel['id']).write({
diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py
index 0131981abe3..550a438b5ea 100644
--- a/addons/mail/models/mail_channel.py
+++ b/addons/mail/models/mail_channel.py
@@ -696,9 +696,15 @@ class Channel(models.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)])
+ channel._execute_channel_pin(pinned)
+
+ def _execute_channel_pin(self, pinned=False):
+ """ Hook for website_livechat channel unpin and cleaning """
+ self.ensure_one()
+ channel_partners = self.env['mail.channel.partner'].search(
+ [('partner_id', '=', self.env.user.partner_id.id), ('channel_id', '=', self.id)])
if not pinned:
- self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), channel.channel_info('unsubscribe')[0])
+ self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), self.channel_info('unsubscribe')[0])
if channel_partners:
channel_partners.write({'is_pinned': pinned})
diff --git a/addons/mail/static/src/js/services/mail_notification_manager.js b/addons/mail/static/src/js/services/mail_notification_manager.js
index cbaaff8194b..f3c6fdfb26c 100644
--- a/addons/mail/static/src/js/services/mail_notification_manager.js
+++ b/addons/mail/static/src/js/services/mail_notification_manager.js
@@ -202,6 +202,22 @@ MailManager.include({
_handlePartnerActivityUpdateNotification: function (data) {
this._mailBus.trigger('activity_updated', data);
},
+ /**
+ * Called to open the channel in detach mode (minimized) even if no new message:
+ *
+ * @private
+ * @param {Object} channelData
+ * @param {integer} channelData.id
+ * @param {string} [channelData.info]
+ * @param {boolean} channelData.is_minimized
+ * @param {string} channelData.state
+ */
+ _handlePartnerChannelMinimizeNotification: function (channelData) {
+ var self = this;
+ this._addChannel(channelData).then(function (channelID){
+ self.getChannel(channelID).detach()
+ });
+ },
/**
* Called when receiving a channel state as a partner notification:
*
@@ -422,6 +438,8 @@ MailManager.include({
} else if (data.type === 'simple_notification') {
var title = _.escape(data.title), message = _.escape(data.message);
data.warning ? this.do_warn(title, message, data.sticky) : this.do_notify(title, message, data.sticky);
+ } else if (data.info === 'channel_minimize') {
+ this._handlePartnerChannelMinimizeNotification(data);
} else {
this._handlePartnerChannelNotification(data);
}
diff --git a/addons/website_livechat/__manifest__.py b/addons/website_livechat/__manifest__.py
index 2a4154a3d63..9677350cb25 100644
--- a/addons/website_livechat/__manifest__.py
+++ b/addons/website_livechat/__manifest__.py
@@ -15,8 +15,12 @@ Allow website visitors to chat with the collaborators. This module also brings a
'views/website_livechat.xml',
'views/res_config_settings_views.xml',
'views/website_livechat_view.xml',
+ 'views/website_visitor_views.xml',
'security/ir.model.access.csv',
'security/website_livechat.xml',
'data/website_livechat_data.xml',
],
+ 'qweb': [
+ 'static/src/xml/thread.xml',
+ ],
}
diff --git a/addons/website_livechat/controllers/main.py b/addons/website_livechat/controllers/main.py
index 8b5a6fba317..4525191b916 100644
--- a/addons/website_livechat/controllers/main.py
+++ b/addons/website_livechat/controllers/main.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from odoo import http
+from odoo import http, _
from odoo.http import request
@@ -55,3 +55,22 @@ class WebsiteLivechat(http.Controller):
'ratings_per_user': ratings_per_partner
}
return request.render("website_livechat.channel_page", values)
+
+ @http.route('/im_livechat/visitor_leave_session', type='json', auth="public")
+ def visitor_leave_session(self, uuid):
+ """ Called when the livechat visitor leaves the conversation.
+ This will clean the chat request and warn the operator that the conversation is over.
+ This allows also to re-send a new chat request to the visitor, as while the visitor is
+ in conversation with an operator, it's not possible to send the visitor a chat request."""
+ mail_channel = request.env['mail.channel'].sudo().search([('uuid', '=', uuid)])
+ if mail_channel:
+ mail_channel.close_livechat_request_session(_('has left the conversation.'))
+
+ @http.route('/im_livechat/close_empty_livechat', type='json', auth="public")
+ def close_empty_livechat(self, uuid):
+ """ Called when an operator send a chat request to a visitor but does not speak to him and closes
+ the chatter. (when the operator does not complete the 'send chat request' flow in other terms)
+ This will clean the chat request and allows operators to send the visitor a new chat request."""
+ mail_channel = request.env['mail.channel'].sudo().search([('uuid', '=', uuid)])
+ if mail_channel:
+ mail_channel.channel_pin(uuid, False)
diff --git a/addons/website_livechat/models/__init__.py b/addons/website_livechat/models/__init__.py
index 18f7d71a23d..7da0d975cd0 100644
--- a/addons/website_livechat/models/__init__.py
+++ b/addons/website_livechat/models/__init__.py
@@ -1,6 +1,9 @@
# -*- coding: utf-8 -*-
from . import im_livechat
-from . import website
-from . import res_config_settings
+from . import im_livechat_channel
from . import ir_http
+from . import mail_channel
+from . import res_config_settings
+from . import website
+from . import website_visitor
diff --git a/addons/website_livechat/models/im_livechat_channel.py b/addons/website_livechat/models/im_livechat_channel.py
new file mode 100644
index 00000000000..db41f626a83
--- /dev/null
+++ b/addons/website_livechat/models/im_livechat_channel.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import models, _
+
+
+class ImLivechatChannel(models.Model):
+ _inherit = 'im_livechat.channel'
+
+ def _get_livechat_mail_channel_vals(self, anonymous_name, operator, user_id=None, country_id=None):
+ mail_channel_vals = super(ImLivechatChannel, self)._get_livechat_mail_channel_vals(anonymous_name, operator, user_id=user_id, country_id=country_id)
+ visitor_sudo = self.env['website.visitor']._get_visitor_from_request()
+ if visitor_sudo:
+ mail_channel_vals.update({
+ 'livechat_visitor_id': visitor_sudo.id,
+ 'livechat_active': True
+ })
+ if not user_id:
+ mail_channel_vals['anonymous_name'] = visitor_sudo.name + (' (%s)' % visitor_sudo.country_id.name if visitor_sudo.country_id else '')
+ # As chat requested by the visitor, delete the chat requested by an operator if any to avoid conflicts between two flows
+ chat_request_channel = self.env['mail.channel'].sudo().search([('livechat_visitor_id', '=', visitor_sudo.id), ('livechat_active', '=', True)])
+ for mail_channel in chat_request_channel:
+ mail_channel.close_livechat_request_session(_('has started a conversation with %s. The chat request has been canceled.') % operator.name)
+
+ return mail_channel_vals
diff --git a/addons/website_livechat/models/mail_channel.py b/addons/website_livechat/models/mail_channel.py
new file mode 100644
index 00000000000..ba8fb18fec7
--- /dev/null
+++ b/addons/website_livechat/models/mail_channel.py
@@ -0,0 +1,78 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import fields, models, _
+
+
+class MailChannel(models.Model):
+ _inherit = 'mail.channel'
+
+ livechat_visitor_id = fields.Many2one('website.visitor', string='Visitor')
+ livechat_active = fields.Boolean('Is livechat ongoing?', help='Livechat session is not considered as active if the visitor left the conversation.')
+
+ def _execute_channel_pin(self, pinned=False):
+ """ Override to clean an empty livechat channel.
+ This is typically called when the operator send a chat request to a website.visitor
+ but don't speak to him and closes the chatter.
+ This allows operators to send the visitor a new chat request.
+ If active empty livechat channel,
+ delete mail_channel as not useful to keep empty chat
+ """
+ super(MailChannel, self)._execute_channel_pin(pinned)
+ if self.livechat_active and not self.channel_message_ids:
+ self.unlink()
+
+ def channel_info(self, extra_info=False):
+ """
+ Override to add visitor information on the mail channel infos.
+ This will be used to display a banner with visitor informations
+ at the top of the livechat channel discussion view in discuss module.
+ """
+ channel_infos = super(MailChannel, self).channel_info(extra_info)
+ channel_infos_dict = dict((c['id'], c) for c in channel_infos)
+ for channel in self:
+ visitor = channel.livechat_visitor_id
+ if visitor:
+ channel_infos_dict[channel.id]['visitor'] = {
+ 'name': visitor.name,
+ 'country_code': visitor.country_id.code.lower() if visitor.country_id else False,
+ 'is_connected': visitor.is_connected,
+ 'history': self._get_visitor_history(visitor),
+ 'website': visitor.website_id.name,
+ 'lang': visitor.lang_id.name,
+ 'partner_id': visitor.user_partner_id.id,
+ }
+ return list(channel_infos_dict.values())
+
+ def _get_visitor_history(self, visitor):
+ """
+ Prepare history string to render it in the visitor info div on discuss livechat channel view.
+ :param visitor: website.visitor of the channel
+ :return: arrow separated string containing navigation history information
+ """
+ recent_history = visitor.visitor_page_ids[:3] if len(visitor.visitor_page_ids) >= 3 else visitor.visitor_page_ids
+ return ' → '.join(page.page_id.name + ' (' + page.visit_datetime.strftime('%H:%M') + ')' for page in recent_history)
+
+ def close_livechat_request_session(self, message):
+ self.ensure_one()
+ if self.livechat_active:
+ self.livechat_active = False
+ # Notify that the visitor has left the conversation
+ name = _('The visitor') if not self.livechat_visitor_id else self.livechat_visitor_id.name
+ leave_message = '%s %s' % (name, message)
+ self.message_post(author_id=self.env.ref('base.user_root').sudo().partner_id.id,
+ body=leave_message, message_type='comment', subtype='mt_comment')
+
+ def message_post(self, **kwargs):
+ """Override to mark the visitor as still connected.
+ If the message sent is not from the operator (so if it's the visitor or
+ odoobot sending closing chat notification, the visitor last action date is updated."""
+ message = super(MailChannel, self).message_post(**kwargs)
+ message_author_id = message.author_id
+ visitor = self.livechat_visitor_id
+ if len(self) == 1 and visitor and message_author_id != self.livechat_operator_id:
+ visitor.sudo().write({
+ 'last_connection_datetime': fields.datetime.now(),
+ })
+ return message
+
diff --git a/addons/website_livechat/models/website_visitor.py b/addons/website_livechat/models/website_visitor.py
new file mode 100644
index 00000000000..c27c002a99a
--- /dev/null
+++ b/addons/website_livechat/models/website_visitor.py
@@ -0,0 +1,102 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import models, api, fields, _
+from odoo.exceptions import UserError
+import json
+
+
+class WebsiteVisitor(models.Model):
+ _inherit = 'website.visitor'
+
+ livechat_operator_id = fields.Many2one('res.partner', compute='_compute_livechat_operator_id', store=True, string='Speaking with')
+ livechat_operator_name = fields.Char('Operator Name', related="livechat_operator_id.name")
+ mail_channel_ids = fields.One2many('mail.channel', 'livechat_visitor_id',
+ string="Visitor's livechat channels", readonly=True)
+ session_count = fields.Integer('# Sessions', compute="_compute_session_count")
+
+ @api.depends('mail_channel_ids.livechat_active', 'mail_channel_ids.livechat_operator_id')
+ def _compute_livechat_operator_id(self):
+ results = self.env['mail.channel'].search_read(
+ [('livechat_visitor_id', 'in', self.ids), ('livechat_active', '=', True)],
+ ['livechat_visitor_id', 'livechat_operator_id']
+ )
+ visitor_operator_map = {int(result['livechat_visitor_id'][0]): int(result['livechat_operator_id'][0]) for result in results}
+ for visitor in self:
+ visitor.livechat_operator_id = visitor_operator_map.get(visitor.id, False)
+
+ @api.depends('mail_channel_ids')
+ def _compute_session_count(self):
+ sessions = self.env['mail.channel'].read_group([('livechat_visitor_id', 'in', self.ids)], ['livechat_visitor_id'], ['livechat_visitor_id'])
+ sessions_count = {session['livechat_visitor_id'][0]: session['livechat_visitor_id_count'] for session in sessions}
+ for visitor in self:
+ visitor.session_count = sessions_count.get(visitor.id, 0)
+
+ def action_send_chat_request(self):
+ """ Send a chat request to website_visitor(s).
+ This creates a chat_request and a mail_channel with livechat active flag.
+ But for the visitor to get the chat request, the operator still has to speak to the visitor.
+ The visitor will receive the chat request the next time he navigates to a website page.
+ (see _handle_webpage_dispatch for next step)"""
+ # check if visitor is available
+ unavailable_visitors_count = self.env['mail.channel'].search_count([('livechat_visitor_id', 'in', self.ids), ('livechat_active', '=', True)])
+ if unavailable_visitors_count:
+ raise UserError(_('Recipients are not available. Please refresh the page to get latest visitors status.'))
+ # check if user is available as operator
+ for website in self.mapped('website_id'):
+ if not website.channel_id:
+ raise UserError(_('No Livechat Channel allows you to send a chat request for website %s.' % website.name))
+ self.website_id.channel_id.write({'user_ids': [(4, self.env.user.id)]})
+ # Create chat_requests and linked mail_channels
+ mail_channel_vals_list = []
+ for visitor in self:
+ operator = self.env.user
+ country = visitor.country_id
+ visitor_name = "%s (%s)" % (visitor.name, country.name) if country else visitor.name
+ mail_channel_vals_list.append({
+ 'channel_partner_ids': [(4, operator.partner_id.id)],
+ 'livechat_channel_id': visitor.website_id.channel_id.id,
+ 'livechat_operator_id': self.env.user.partner_id.id,
+ 'channel_type': 'livechat',
+ 'public': 'private',
+ 'email_send': False,
+ 'country_id': country.id,
+ 'anonymous_name': visitor_name,
+ 'name': ', '.join([visitor_name, operator.livechat_username if operator.livechat_username else operator.name]),
+ 'livechat_visitor_id': visitor.id,
+ 'livechat_active': True,
+ })
+ if mail_channel_vals_list:
+ mail_channels = self.env['mail.channel'].create(mail_channel_vals_list)
+ # Open empty chatter to allow the operator to start chatting with the visitor.
+ mail_channels_info = mail_channels.channel_info('channel_minimize')
+ for mail_channel_info in mail_channels_info:
+ self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', operator.partner_id.id), mail_channel_info)
+
+ def _handle_website_page_visit(self, response, website_page, visitor_sudo):
+ """ Called when the visitor navigates to a website page.
+ This checks if there is a chat request for the visitor.
+ It will set the livechat session cookie of the visitor with the mail channel information
+ to make the usual livechat mechanism do the rest.
+ (opening the chatter if a livechat session exist for the visitor)
+ This will only happen if the mail channel linked to the chat request already has a message.
+ So that empty livechat channel won't pop up at client side. """
+ super(WebsiteVisitor, self)._handle_website_page_visit(response, website_page, visitor_sudo)
+ visitor_id = self.env['website.visitor']._get_visitor_from_request().id if not visitor_sudo else visitor_sudo.id
+ if visitor_id:
+ # get active chat_request linked to visitor
+ chat_request_channel = self.env['mail.channel'].sudo().search([('livechat_visitor_id', '=', visitor_id), ('livechat_active', '=', True)], order='create_date desc', limit=1)
+ if chat_request_channel and chat_request_channel.channel_message_ids:
+ livechat_session = json.dumps({
+ "folded": False,
+ "id": chat_request_channel.id,
+ "message_unread_counter": 0,
+ "operator_pid": [
+ chat_request_channel.livechat_operator_id.id,
+ chat_request_channel.livechat_operator_id.display_name
+ ],
+ "name": chat_request_channel.name,
+ "uuid": chat_request_channel.uuid,
+ "type": "chat_request"
+ })
+ response.set_cookie('im_livechat_session', livechat_session)
diff --git a/addons/website_livechat/security/ir.model.access.csv b/addons/website_livechat/security/ir.model.access.csv
index f33a4ac2f2d..90de53247be 100644
--- a/addons/website_livechat/security/ir.model.access.csv
+++ b/addons/website_livechat/security/ir.model.access.csv
@@ -1,2 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
-access_im_livechat_channel_public,im_livechat.channel.public,im_livechat.model_im_livechat_channel,,1,0,0,0
\ No newline at end of file
+access_im_livechat_channel_public,im_livechat.channel.public,im_livechat.model_im_livechat_channel,,1,0,0,0
+access_website_visitor_livechat_users,website.visitor.livechat.users,model_website_visitor,im_livechat.im_livechat_group_user,1,0,0,0
diff --git a/addons/website_livechat/static/src/js/channel.js b/addons/website_livechat/static/src/js/channel.js
new file mode 100644
index 00000000000..24d679e8593
--- /dev/null
+++ b/addons/website_livechat/static/src/js/channel.js
@@ -0,0 +1,34 @@
+odoo.define('website.livechat.mail.model.Channel', function (require) {
+"use strict";
+
+var Channel = require('mail.model.Channel');
+var session = require('web.session');
+
+/**
+ * This class represent channels in JS. In this context, the word channel
+ * has the same meaning of channel on the server, meaning that direct messages
+ * (DM) and livechats are also channels.
+ *
+ * Any piece of code in JS that make use of channels must ideally interact with
+ * such objects, instead of direct data from the server.
+ */
+Channel.include({
+ /**
+ * @override
+ * Add the visitor if is set on the channel
+ * @param {Object} params
+ * @param {Object} params.data
+ * @param {string} params.data.visitor
+ */
+ init: function (params) {
+ var self = this;
+ var data = params.data;
+ this._visitor = data.visitor;
+ this._super.apply(this, arguments);
+ },
+
+});
+
+return Channel;
+
+});
diff --git a/addons/website_livechat/static/src/js/im_livechat.js b/addons/website_livechat/static/src/js/im_livechat.js
new file mode 100644
index 00000000000..d5ce27af2e1
--- /dev/null
+++ b/addons/website_livechat/static/src/js/im_livechat.js
@@ -0,0 +1,69 @@
+odoo.define('website_livechat.livechat_request', function (require) {
+"use strict";
+
+var utils = require('web.utils');
+var session = require('web.session');
+var LivechatButton = require('im_livechat.im_livechat').LivechatButton;
+
+
+LivechatButton.include({
+
+ /**
+ * @override
+ * This will will correctly format the livechat session cookie
+ * that comes from server side (and that is not properly formatted)
+ * This is used for chat request mechanism, when an operator send a chat request
+ * from backend to a website visitor.
+ */
+ willStart: function () {
+ var self = this;
+ var cookie = utils.get_cookie('im_livechat_session');
+ var ready;
+ if (cookie) {
+ var cleanedLivechatSessionCookie = this.decode_server_cookie(cookie);
+ utils.set_cookie('im_livechat_session', cleanedLivechatSessionCookie, 60*60);
+ }
+ return this._super();
+ },
+
+ /**
+ * @override
+ * Called when the visitor closes the livechat chatter
+ * (no matter the way : close, send feedback, ..)
+ * this will deactivate the mail_channel, clean the chat request if any
+ * and allow the operators to send the visitor a new chat request
+ */
+ _closeChat: function () {
+ var self = this;
+ var cookie = utils.get_cookie('im_livechat_session');
+ if (cookie) {
+ var channel = JSON.parse(cookie);
+ var ready = session.rpc('/im_livechat/visitor_leave_session', {uuid: channel.uuid});
+ ready.then(self._super());
+ }
+ else {
+ this._super();
+ }
+ },
+
+ /**
+ * Utils to correctly re-encode json string sent by server.
+ * Copied from StackOverflow.
+ */
+ decode_server_cookie: function (val) {
+ if (val.indexOf('\\') === -1) {
+ return val; // not encoded
+ }
+ val = val.slice(1, -1).replace(/\\"/g, '"');
+ val = val.replace(/\\(\d{3})/g, function(match, octal) {
+ return String.fromCharCode(parseInt(octal, 8));
+ });
+ return val.replace(/\\\\/g, '\\');
+ },
+});
+
+return {
+ LivechatButton: LivechatButton,
+};
+
+});
diff --git a/addons/website_livechat/static/src/js/thread_window.js b/addons/website_livechat/static/src/js/thread_window.js
new file mode 100644
index 00000000000..a53d2bcddd5
--- /dev/null
+++ b/addons/website_livechat/static/src/js/thread_window.js
@@ -0,0 +1,34 @@
+odoo.define('website_livechat.ThreadWindow', function (require) {
+"use strict";
+
+var ThreadWindow = require('mail.ThreadWindow');
+var session = require('web.session');
+
+/**
+ * This is the main widget for rendering small windows for mail.model.Thread.
+ * Almost all instances of this class are linked to a thread. The sole
+ * exception is the "blank" thread window. This window let us open another
+ * thread window, using this "blank" thread window.
+ */
+ThreadWindow.include({
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ close: function () {
+ var self = this;
+ if (this.hasThread() && this._thread._type === "livechat" && this._threadWidget._messages.length == 0) {
+ session.rpc('/im_livechat/close_empty_livechat', {uuid: this._thread._uuid});
+ }
+ else {
+ this._super();
+ }
+ },
+});
+
+return ThreadWindow;
+
+});
diff --git a/addons/website_livechat/static/src/scss/mail_thread_visitor_info.scss b/addons/website_livechat/static/src/scss/mail_thread_visitor_info.scss
new file mode 100644
index 00000000000..2520b54fde7
--- /dev/null
+++ b/addons/website_livechat/static/src/scss/mail_thread_visitor_info.scss
@@ -0,0 +1,39 @@
+.o_web_client {
+ &:not(.o_no_thread_window) {
+ .o_w_visitor_info {
+ display:none !important;
+ }
+ }
+ &.o_no_thread_window {
+ .o_mail_thread {
+ position: sticky;
+ }
+ .o_w_visitor_info {
+ height: 87px;
+ border-bottom: 1px solid #ced4da;
+ position: fixed;
+ background: white;
+ width: 100%;
+ z-index: 1;
+ .o_w_visitor_status {
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ &.o_w_visitor_disconnected {
+ color: red;
+ }
+ &.o_w_visitor_connected {
+ color: #00A09D;
+ }
+ }
+ }
+ .o_sub_w_visitor_info {
+ padding-top: 80px;
+ }
+ }
+}
+
+.o_country_flag {
+ width:24px;
+ height: 16px;
+}
diff --git a/addons/website_livechat/static/src/xml/thread.xml b/addons/website_livechat/static/src/xml/thread.xml
new file mode 100644
index 00000000000..a8872290b2a
--- /dev/null
+++ b/addons/website_livechat/static/src/xml/thread.xml
@@ -0,0 +1,60 @@
+
+
+
+
+
+