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 @@ + + + + + +
+ + + +
+ +
+
+
+ + + + +
+ + + + +
+
+
+ + + + +
+
+
+ + + + + +
+
+
+
+ + + + +
+
+
+ +

+

+
+
+
+
+
+ diff --git a/addons/website_livechat/views/website_livechat.xml b/addons/website_livechat/views/website_livechat.xml index 75e884bdcf1..452c19c4f7e 100644 --- a/addons/website_livechat/views/website_livechat.xml +++ b/addons/website_livechat/views/website_livechat.xml @@ -1,6 +1,14 @@ + + + diff --git a/addons/website_livechat/views/website_visitor_views.xml b/addons/website_livechat/views/website_visitor_views.xml new file mode 100644 index 00000000000..96386ac5d5e --- /dev/null +++ b/addons/website_livechat/views/website_visitor_views.xml @@ -0,0 +1,128 @@ + + + + Visitor's Sessions + mail.channel + tree,form + + [('livechat_visitor_id', '=', active_id)] + + + + 1 + tree + + + + + + 2 + form + + + + + + + website.visitor.view.kanban.inherit.website.livechat + website.visitor + + + + + + +
+ Speaking With +
+ +
+
+
+ + + +
+
+ + + website.visitor.view.form.inherit.website.livechat + website.visitor + + + + + + + + + + website.visitor.view.tree.inherit.website.livechat + website.visitor + + + + +