diff options
| author | David Beguin <dbe@odoo.com> | 2019-07-08 08:13:29 +0000 |
|---|---|---|
| committer | David Beguin <dbe@odoo.com> | 2019-08-19 06:33:37 +0000 |
| commit | 78e6ce3c3fe565c488b956450ed875629cdd2de0 (patch) | |
| tree | 989fc02ac9592d3c7bc950c5fd9e31e755019d4f | |
| parent | a8ce2886be2e02315fa3526257a941c91c5385b9 (diff) | |
[IMP] website_livechat : allow to send chat request to a website_visitor
This commit allows a livechat operator to send a chat request to a
connected and available website_visitor.
A visitor is considered as connected if his last tracked website.page request
was within the last 5 minutes.
A visitor is available if he doesn't have an active livechat conversation
(mail_channel with type = livechat).
- If another operator sent him a chat request
- Or if the visitor asked himself to speak with an operator
(via the normal and existing flow)
A livechat conversation is active while the visitor haven't left the conversation.
An operator cannot leave a livechat conversation, only the visitor can.
The flow to send a chat request:
- On the visitor view (tree or form), operator click on 'send chat request'
(button or livechat icon)
- A empty conversation with the visitor pops up at operator side.
- While the operator didn't send a message, the visitor won't see the conversation.
- The operator can type a message to the user
- If the operator close the chat without sending any messages,
the chat request AND the mail_channel are both deleted.
In this case, the visitor is then available to send him a new chat request.
- If the operator send a message, at the visitor's next action
(page navigation on pages that allows livechat, based on livechat rules),
the conversation will pop up at visitor side, using the livechat button widget.
The visitor won't be able to request a livechat conversation with an operator until
he leaves the chat requested by the operator.
- Once the visitor leaves (with or without rating) the conversation :
- the operator is notified that the visitor has left the conversation
- the chat request is deleted to keep the chat request table clean and minimal
- the livechat conversation if set to inactive.
- The visitor is now available again to send him a chat request.
This feature uses the already existing livechat_session cookie mechanism,
so no further code modification was needed to make this work.
It's directly integrated is existing livechat flow.
The chat_request model is only useful to quickly check if a visitor has a chat request
and to send the livechat conversation info to the visitor via the livechat_session cookie.
This commit also add a website_visitor banner info on discuss view :
To be able to quickly see all the relevant information of a website visitor
while talking with in discuss view, a fixed banner have been added.
Only the discuss view will benefit from this because detached chatter window
is to small to display such banner.
Task ID: 34624
PR #2028059
19 files changed, 667 insertions, 33 deletions
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 @@ -203,6 +203,22 @@ MailManager.include({ 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: * * - if it is a new channel, it means we have been invited to this channel @@ -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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates xml:space="preserve"> + <t t-extend="mail.widget.Thread.Content.ASC"> + <t t-jquery=".o_mail_thread_content" t-operation="replace"> + <t t-call="mail.widget.Thread.website.visitor.banner"/> + <div class="o_mail_thread_content o_sub_w_visitor_info"> + <t t-if="options.displayLoadMore" t-call="mail.widget.Thread.LoadMore"/> + <t t-call="mail.widget.Thread.Messages"/> + <t t-if="options.displayBottomThreadFreeSpace"> + <div class="o_thread_bottom_free_space"/> + </t> + </div> + </t> + </t> + + <t t-extend="mail.widget.Thread.Content.DESC"> + <t t-jquery=".o_mail_thread_content" t-operation="replace"> + <t t-call="mail.widget.Thread.website.visitor.banner"/> + <div class="o_mail_thread_content"> + <t t-if="options.messagesSeparatorPosition == 'top'" t-call="mail.MessagesSeparator"/> + <t t-set="messages" t-value="messages.slice().reverse()"/> + <t t-call="mail.widget.Thread.Messages"/> + <t t-if="options.displayLoadMore" t-call="mail.widget.Thread.LoadMore"/> + </div> + </t> + </t> + + <t t-name="mail.widget.Thread.website.visitor.banner"> + <t t-set="visitor" t-value="thread._visitor"/> + <t t-if="visitor"> + <div class="o_w_visitor_info o_thread_message o_mail_discussion d-flex"> + <div class="o_thread_message_sidebar align-self-center"> + <div class="o_thread_message_sidebar_image"> + <img alt="" + t-att-src="visitor.partner_id ? '/web/image/res.partner/' + visitor.partner_id + '/image_small' : '/mail/static/src/img/smiley/avatar.jpg'" + class="o_thread_message_avatar rounded-circle"/> + <span> + <i t-if="visitor.is_connected" aria-label="Visitor is connected" class="o_w_visitor_status o_w_visitor_connected fa fa-circle" role="img" title="Online"></i> + <i t-else="" aria-label="Visitor is offline" class="o_w_visitor_status o_w_visitor_disconnected fa fa-circle" role="img" title="Offline"></i> + </span> + </div> + </div> + <div class="o_thread_message_core pt-2 align-self-center"> + <div class="d-flex"> + <img t-if="visitor.country_code" t-attf-src='base/static/img/country_flags/#{visitor.country_code}.png' class="o_country_flag mr-2 mb-1"/> + <strong class="o_thread_author" t-esc="visitor.name"/> + <span><i class="fa fa-comment-o ml-4 mr-2" aria-label="Lang"/><t t-esc="visitor.lang"/></span> + <span t-if="visitor.website"><i class="fa fa-globe ml-4 mr-2" aria-label="Website"/><span t-esc="visitor.website"/></span> + </div> + <div class="o_thread_message_content"> + <div class="d-flex"> + <span><i class="fa fa-history mr-2" aria-label="History"/></span> + <p t-esc="visitor.history"/> + </div> + </div> + </div> + </div> + </t> + </t> +</templates> 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 @@ <?xml version="1.0" encoding="utf-8"?> <odoo> <data> + <template id="assets_backend" inherit_id="website.assets_backend" name="website_livechat assets backend"> + <xpath expr="." position="inside"> + <!-- Visitor Chat Request --> + <script type="text/javascript" src="/website_livechat/static/src/js/thread_window.js"></script> + <script type="text/javascript" src="/website_livechat/static/src/js/channel.js"></script> + <link rel="stylesheet" type="text/scss" href="/website_livechat/static/src/scss/mail_thread_visitor_info.scss"/> + </xpath> + </template> <!-- Integrate Livechat in Common Frontend for Website @@ -32,6 +40,9 @@ <script type="text/javascript" src="/mail/static/src/js/models/utils/timer.js"></script> <script type="text/javascript" src="/mail/static/src/js/models/utils/timers.js"></script> <script type="text/javascript" src="/mail/static/src/js/models/utils/cc_throttle_function.js"></script> + <!--Chat Request--> + <script type="text/javascript" src="/website_livechat/static/src/js/im_livechat.js"></script> + <!--Stylesheets--> <link rel="stylesheet" type="text/scss" href="/mail/static/src/scss/abstract_thread_window.scss"></link> <link rel="stylesheet" type="text/scss" href="/mail/static/src/scss/thread.scss"></link> <link rel="stylesheet" type="text/scss" href="/im_livechat/static/src/scss/im_livechat.scss"/> 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 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo><data> + <record id="website_visitor_livechat_session_action" model="ir.actions.act_window"> + <field name="name">Visitor's Sessions</field> + <field name="res_model">mail.channel</field> + <field name="view_mode">tree,form</field> + <field name="view_id" ref="im_livechat.mail_channel_view_tree"/> + <field name="domain">[('livechat_visitor_id', '=', active_id)]</field> + </record> + + <record id="website_visitor_livechat_session_action_tree" model="ir.actions.act_window.view"> + <field name="sequence">1</field> + <field name="view_mode">tree</field> + <field name="view_id" ref="im_livechat.mail_channel_view_tree"/> + <field name="act_window_id" ref="website_livechat.website_visitor_livechat_session_action"/> + </record> + + <record id="website_visitor_livechat_session_action_form" model="ir.actions.act_window.view"> + <field name="sequence">2</field> + <field name="view_mode">form</field> + <field name="view_id" ref="im_livechat.mail_channel_view_form"/> + <field name="act_window_id" ref="website_livechat.website_visitor_livechat_session_action"/> + </record> + + <!-- website visitor views --> + <record id="website_visitor_view_kanban" model="ir.ui.view"> + <field name="name">website.visitor.view.kanban.inherit.website.livechat</field> + <field name="model">website.visitor</field> + <field name="inherit_id" ref="website.website_visitor_view_kanban"/> + <field name="arch" type="xml"> + <field name="page_ids" position="after"> + <field name="livechat_operator_id"/> + </field> + <xpath expr="//div[@id='w_visitor_kanban_actions']" position="before"> + <div t-if="record.livechat_operator_id.raw_value"> + <span class="fa fa-comments mr-2"/>Speaking With <span class="font-weight-bold"><field name="livechat_operator_name"/></span> + </div> + <t t-else=""> + <br attrs="{'invisible': ['|', ('livechat_operator_id', '!=', False), ('is_connected', '=', False)]}"/> + </t> + </xpath> + <xpath expr="//div[@id='w_visitor_kanban_actions']" position="inside"> + <button name="action_send_chat_request" type="object" + class="btn btn-primary" + attrs="{'invisible': ['|', ('livechat_operator_id', '!=', False), ('is_connected', '=', False)]}"> + <i class="fa fa-comments"/> Chat + </button> + </xpath> + </field> + </record> + + <record id="website_visitor_view_form" model="ir.ui.view"> + <field name="name">website.visitor.view.form.inherit.website.livechat</field> + <field name="model">website.visitor</field> + <field name="inherit_id" ref="website.website_visitor_view_form"/> + <field name="arch" type="xml"> + <xpath expr="//header" position="inside"> + <button name="action_send_chat_request" string="Send chat request" type="object" class="oe_highlight" + attrs="{'invisible': ['|', ('livechat_operator_id', '!=', False), ('is_connected', '=', False)]}"/> + </xpath> + <xpath expr="//group[@id='general_info']" position="before"> + <group id="livechat_info"> + <field name="livechat_operator_id"/> + </group> + </xpath> + <xpath expr="//div[hasclass('oe_button_box')]" position="inside"> + <button name="%(website_visitor_livechat_session_action)d" type="action" class="oe_stat_button" icon="fa-comment" + attrs="{'invisible': [('session_count', '=', 0)]}"> + <field name="session_count" widget="statinfo" string="Sessions"/> + </button> + </xpath> + </field> + </record> + + <record id="website_visitor_view_tree" model="ir.ui.view"> + <field name="name">website.visitor.view.tree.inherit.website.livechat</field> + <field name="model">website.visitor</field> + <field name="inherit_id" ref="website.website_visitor_view_tree"/> + <field name="arch" type="xml"> + <xpath expr="//field[@name='is_connected']" position="after"> + <field name="livechat_operator_id"/> + <button name="action_send_chat_request" string="Send chat request" type="object" icon="fa-comments" + attrs="{'invisible': ['|', ('livechat_operator_id', '!=', False), ('is_connected', '=', False)]}"/> + </xpath> + </field> + </record> + + <record id="website_visitor_view_search" model="ir.ui.view"> + <field name="name">website.visitor.view.search.website.livechat</field> + <field name="model">website.visitor</field> + <field name="inherit_id" ref="website.website_visitor_view_search"/> + <field name="arch" type="xml"> + <xpath expr="//search" position="inside"> + <filter string="Busy" name="in_conversation" domain="[('livechat_operator_id', '!=', False)]"/> + <filter string="Available" name="not_in_conversation" domain="[('livechat_operator_id', '=', False)]"/> + </xpath> + </field> + </record> + + <record id="website_livechat_send_chat_request_action_server" model="ir.actions.server"> + <field name="name">Send Chat Requests</field> + <field name="type">ir.actions.server</field> + <field name="model_id" ref="model_website_visitor"/> + <field name="binding_model_id" ref="model_website_visitor"/> + <field name="binding_view_types">list</field> + <field name="state">code</field> + <field name="code"> + if records: + action = records.action_send_chat_request() + </field> + </record> + + <record id="website.website_visitors_action" model="ir.actions.act_window"> + <field name="help" type="html"> + <p class="o_view_nocontent_smiling_face"> + Wait for visitors to come to your website to see their history.</p> + <p>Interact with them by sending them messages.</p> + </field> + </record> + + <menuitem + id="website_livechat_visitor_menu" + name="Visitors" + parent="im_livechat.menu_livechat_root" + action="website.website_visitors_action" + groups="im_livechat.im_livechat_group_user" + sequence="15"/> +</data></odoo> |
