[IMP] im_livechat, *: Show Livechat Button with notification

*: mail, website_livechat

This commit adds new option in Livechat Button visibility
"Show with notifcation", which allow to see livechat button
and also display a customizable floating text next to button.

This text appears 1 second after the livechat button become visible.

Task-2937993

closes odoo/odoo#102930

X-original-commit: 785356f46b49d1b3d9b975319c87f39266694b82
Signed-off-by: Louis Wicket (wil) <wil@odoo.com>
This commit is contained in:
Alexandre Kühn
2022-10-10 18:08:48 +02:00
parent bfc1d414db
commit 36346eb1d0
20 changed files with 304 additions and 97 deletions
+3 -3
View File
@@ -50,7 +50,7 @@ Help your customers with this chat, and analyse their feedback.
'im_livechat/static/src/components/*/*',
],
'web.assets_frontend': [
('include', 'mail.assets_public_livechat'),
('include', 'im_livechat.assets_public_livechat'),
'im_livechat/static/src/public/main.js',
'im_livechat/static/src/services/*.js',
'im_livechat/static/src/legacy/public_livechat_chatbot.js',
@@ -82,7 +82,7 @@ Help your customers with this chat, and analyse their feedback.
'mail.assets_messaging': [
'im_livechat/static/src/models/*.js',
],
'mail.assets_public_livechat': [
'im_livechat.assets_public_livechat': [
('include', 'mail.assets_core_messaging'),
'im_livechat/static/src/legacy/models/*',
'im_livechat/static/src/legacy/widgets/*',
@@ -202,7 +202,7 @@ Help your customers with this chat, and analyse their feedback.
'mail/static/src/utils/*.js',
'mail/static/src/js/emojis.js',
'mail/static/src/component_hooks/*.js',
('include', 'mail.assets_public_livechat'),
('include', 'im_livechat.assets_public_livechat'),
'mail/static/src/services/messaging_service.js',
# Framework JS
'bus/static/src/*.js',
+5 -2
View File
@@ -28,13 +28,16 @@ class LivechatController(http.Controller):
@http.route('/im_livechat/load_templates', type='json', auth='none', cors="*")
def load_templates(self, **kwargs):
templates = [
templates = self._livechat_templates_get()
return [tools.file_open(tmpl, 'rb').read() for tmpl in templates]
def _livechat_templates_get(self):
return [
'im_livechat/static/src/legacy/widgets/feedback/feedback.xml',
'im_livechat/static/src/legacy/widgets/public_livechat_window/public_livechat_window.xml',
'im_livechat/static/src/legacy/widgets/public_livechat_view/public_livechat_view.xml',
'im_livechat/static/src/legacy/public_livechat_chatbot.xml',
]
return [tools.file_open(tmpl, 'rb').read() for tmpl in templates]
@http.route('/im_livechat/support/<int:channel_id>', type='http', auth='public')
def support_page(self, channel_id, **kwargs):
@@ -283,21 +283,23 @@ class ImLivechatChannelRule(models.Model):
regex_url = fields.Char('URL Regex',
help="Regular expression specifying the web pages this rule will be applied on.")
action = fields.Selection([
('display_button', 'Display the LiveChat Button'),
('auto_popup', 'Autopop LiveChat'),
('hide_button', 'Hide all buttons')], string='Action', required=True, default='display_button',
help="* 'Display the button' displays the chat button on the pages.\n"\
"* 'Auto popup' displays the button and automatically open the conversation pane.\n"\
"* 'Hide the button' hides the chat button on the pages.\n")
auto_popup_timer = fields.Integer('Auto popup timer', default=0,
help="Delay (in seconds) to automatically open the conversation window. Note: the selected action must be 'Auto popup' otherwise this parameter will not be taken into account.")
('display_button', 'Show'),
('display_button_and_text', 'Show with notification'),
('auto_popup', 'Open automatically'),
('hide_button', 'Hide')], string='Live Chat Button', required=True, default='display_button',
help="* 'Show' displays the chat button on the pages.\n"\
"* 'Show with notification' is 'Show' in addition to a floating text just next to the button.\n"\
"* 'Open automatically' displays the button and automatically opens the conversation pane.\n"\
"* 'Hide' hides the chat button on the pages.\n")
auto_popup_timer = fields.Integer('Open automatically timer', default=0,
help="Delay (in seconds) to automatically open the conversation window. Note: the selected action must be 'Open automatically' otherwise this parameter will not be taken into account.")
chatbot_script_id = fields.Many2one('chatbot.script', string='Chatbot')
chatbot_only_if_no_operator = fields.Boolean(
string='Enabled only if no operator', help='Enable the bot only if there is no operator available')
channel_id = fields.Many2one('im_livechat.channel', 'Channel',
help="The channel of the rule")
country_ids = fields.Many2many('res.country', 'im_livechat_channel_country_rel', 'channel_id', 'country_id', 'Country',
help="The rule will only be applied for these countries. Example: if you select 'Belgium' and 'United States' and that you set the action to 'Hide Button', the chat button will be hidden on the specified URL from the visitors located in these 2 countries. This feature requires GeoIP installed on your server.")
help="The rule will only be applied for these countries. Example: if you select 'Belgium' and 'United States' and that you set the action to 'Hide', the chat button will be hidden on the specified URL from the visitors located in these 2 countries. This feature requires GeoIP installed on your server.")
sequence = fields.Integer('Matching order', default=10,
help="Given the order to find a matching rule. If 2 rules are matching for the given url/country, the one with the lowest sequence will be chosen.")
@@ -146,7 +146,10 @@ registerModel({
this._sendMessageChatbotAfter();
},
start() {
this.widget.$el.text(this.buttonText);
if (!this.messaging.publicLivechatGlobal.hasWebsiteLivechatFeature) {
this.widget.$el.text(this.buttonText);
}
this.update({ isWidgetMounted: true });
if (this.messaging.publicLivechatGlobal.history) {
for (const m of this.messaging.publicLivechatGlobal.history) {
this.addMessage(m);
@@ -169,7 +172,6 @@ registerModel({
if (this.buttonTextColor) {
this.widget.$el.css('color', this.buttonTextColor);
}
// If website_event_track installed, put the livechat banner above the PWA banner.
const pwaBannerHeight = $('.o_pwa_install_banner').outerHeight(true);
if (pwaBannerHeight) {
@@ -434,6 +436,9 @@ registerModel({
default: false,
}),
isTypingTimeout: attr(),
isWidgetMounted: attr({
default: false,
}),
openChatDebounced: attr({
compute() {
return _.debounce(this._openChat, 200, true);
@@ -162,6 +162,11 @@ registerModel({
hasLoadedQWebTemplate: attr({
default: false,
}),
hasWebsiteLivechatFeature: attr({
compute() {
return false;
},
}),
history: attr({
default: null,
}),
@@ -0,0 +1,64 @@
/** @odoo-module **/
import { registerModel } from '@mail/model/model_core';
import { attr } from '@mail/model/model_field';
import { clear } from '@mail/model/model_field_command';
registerModel({
name: 'Timer',
identifyingMode: 'xor',
lifecycleHooks: {
_created() {
this.update({ timeoutId: this.messaging.browser.setTimeout(this._onTimeout, this.duration) });
},
_willDelete() {
this.messaging.browser.clearTimeout(this.timeoutId);
},
},
recordMethods: {
/**
* @private
*/
_onTimeout() {
this.update({ timeoutId: clear() });
this.onTimeout();
},
onTimeout() {},
_onChangeDoReset() {
if (!this.doReset) {
return;
}
this.messaging.browser.clearTimeout(this.timeoutId);
this.update({
doReset: clear(),
timeoutId: this.messaging.browser.setTimeout(this._onTimeout, this.duration),
});
},
},
fields: {
doReset: attr({
default: false,
}),
/**
* Duration, in milliseconds, until timer times out and calls the
* timeout function.
*/
duration: attr({
compute() {
return clear();
},
required: true,
}),
/**
* Internal reference of `setTimeout()` that is used to invoke function
* when timer times out. Useful to clear it when timer is cleared/reset.
*/
timeoutId: attr(),
},
onChanges: [
{
dependencies: ["doReset"],
methodName: '_onChangeDoReset',
},
],
});
+2 -10
View File
@@ -558,16 +558,8 @@ export class ModelManager {
*/
_checkProcessedFieldsOnModels() {
for (const model of Object.values(this.models)) {
switch (model.identifyingMode) {
case 'and':
break;
case 'xor':
if (model.__identifyingFieldNames.size === 0) {
throw new Error(`No identifying fields has been specified for 'xor' identifying mode on ${model}`);
}
break;
default:
throw new Error(`Unsupported identifying mode "${model.identifyingMode}" on ${model}. Must be one of 'and' or 'xor'.`);
if (!['and', 'xor'].includes(model.identifyingMode)) {
throw new Error(`Unsupported identifying mode "${model.identifyingMode}" on ${model}. Must be one of 'and' or 'xor'.`);
}
for (const field of model.__fieldList) {
const fieldName = field.fieldName;
+9 -48
View File
@@ -1,26 +1,15 @@
/** @odoo-module **/
import { registerModel } from '@mail/model/model_core';
import { attr, one } from '@mail/model/model_field';
import { clear } from '@mail/model/model_field_command';
import { registerPatch } from '@mail/model/model_core';
import { one } from '@mail/model/model_field';
registerModel({
registerPatch({
name: 'Timer',
identifyingMode: 'xor',
lifecycleHooks: {
_created() {
this.update({ timeoutId: this.messaging.browser.setTimeout(this._onTimeout, this.duration) });
},
_willDelete() {
this.messaging.browser.clearTimeout(this.timeoutId);
},
},
recordMethods: {
/**
* @private
* @override
*/
_onTimeout() {
this.update({ timeoutId: clear() });
onTimeout() {
if (this.blurManagerOwnerAsFrameRequest) {
this.blurManagerOwnerAsFrameRequest.onRequestFrameTimerTimeout();
return;
@@ -57,16 +46,7 @@ registerModel({
this.throttleOwner.onTimeout();
return;
}
},
_onChangeDoReset() {
if (!this.doReset) {
return;
}
this.messaging.browser.clearTimeout(this.timeoutId);
this.update({
doReset: clear(),
timeoutId: this.messaging.browser.setTimeout(this._onTimeout, this.duration),
});
return this._super();
},
},
fields: {
@@ -82,14 +62,7 @@ registerModel({
identifying: true,
inverse: 'attachmentsLoaderTimer',
}),
doReset: attr({
default: false,
}),
/**
* Duration, in milliseconds, until timer times out and calls the
* timeout function.
*/
duration: attr({
duration: {
compute() {
if (this.blurManagerOwnerAsFrameRequest) {
return Math.floor(1000 / 30); // 30 fps
@@ -118,10 +91,9 @@ registerModel({
if (this.throttleOwner) {
return this.throttleOwner.duration;
}
return clear();
return this._super();
},
required: true,
}),
},
messageViewOwnerAsHighlight: one('MessageView', {
identifying: true,
inverse: 'highlightTimer',
@@ -147,16 +119,5 @@ registerModel({
identifying: true,
inverse: 'cooldownTimer',
}),
/**
* Internal reference of `setTimeout()` that is used to invoke function
* when timer times out. Useful to clear it when timer is cleared/reset.
*/
timeoutId: attr(),
},
onChanges: [
{
dependencies: ["doReset"],
methodName: '_onChangeDoReset',
},
],
});
+8 -3
View File
@@ -25,12 +25,13 @@ Allow website visitors to chat with the collaborators. This module also brings a
'data/website_livechat_chatbot_demo.xml',
],
'assets': {
'im_livechat.assets_public_livechat': [
'website_livechat/static/src/public_models/*.js',
'website_livechat/static/src/legacy/widgets/*/*',
],
'mail.assets_messaging': [
'website_livechat/static/src/models/*.js',
],
'mail.assets_public_livechat': [
'website_livechat/static/src/public_models/*.js',
],
'mail.assets_discuss_public': [
'website_livechat/static/src/components/*/*',
],
@@ -59,6 +60,10 @@ Allow website visitors to chat with the collaborators. This module also brings a
'web.qunit_suite_tests': [
'website_livechat/static/tests/qunit_suite_tests/**/*.js',
],
'im_livechat.external_lib': [
'website_livechat/static/src/external_lib/livechat_button.js',
'website_livechat/static/src/legacy/public_livechat.scss',
],
},
'license': 'LGPL-3',
}
@@ -66,3 +66,8 @@ class WebsiteLivechat(LivechatController):
if visitor_sudo:
anonymous_name = visitor_sudo.with_context(lang=visitor_sudo.lang_id.code).display_name
return super(WebsiteLivechat, self).get_session(channel_id, anonymous_name, previous_operator_id=previous_operator_id, chatbot_script_id=chatbot_script_id, **kwargs)
def _livechat_templates_get(self):
return super(WebsiteLivechat, self)._livechat_templates_get() + [
'website_livechat/static/src/legacy/widgets/public_livechat_floating_text_view/public_livechat_floating_text_view.xml',
]
@@ -0,0 +1,7 @@
/** @odoo-module **/
import LivechatButton from '@im_livechat/legacy/widgets/livechat_button';
LivechatButton.include({
className: `${LivechatButton.prototype.className} o_website_livechat_button o-isExternalLib`,
});
@@ -1,30 +1,10 @@
odoo.define('website_livechat.legacy.website_livechat.livechat_request', function (require) {
"use strict";
const config = require('web.config');
var LivechatButton = require('@im_livechat/legacy/widgets/livechat_button')[Symbol.for("default")];
LivechatButton.include({
className: `${LivechatButton.prototype.className} o_bottom_fixed_element o_bottom_fixed_element_move_up o_website_livechat_button fa fa-commenting`,
/**
* @override
*/
async start() {
// We trigger a resize to launch the event that checks if this element hides
// a button when the page is loaded.
$(window).trigger('resize');
await this._super(...arguments);
this.el.innerHTML = "";
if (this.messaging.publicLivechatGlobal.livechatButtonView.buttonText && !config.device.touch) {
this.el.dataset.content = this.messaging.publicLivechatGlobal.livechatButtonView.buttonText;
this.el.dataset.toggle = "popover";
this.el.dataset.trigger = "hover";
this.$el.popover({
animation: true,
});
}
},
});
return {
@@ -11,6 +11,7 @@
justify-content: center;
align-items: center;
transition: filter 0.3s;
padding: 0px;
margin: 0 30px 30px 0;
box-shadow: $box-shadow;
border: none;
@@ -22,6 +23,10 @@
font-weight: normal;
text-shadow: none;
&.o-isExternalLib::before {
content: "💬";
}
&:hover {
filter: brightness(90%);
}
@@ -0,0 +1,12 @@
/** @odoo-module **/
import Widget from 'web.Widget';
export const PublicLivechatFloatingTextView = Widget.extend({
template: 'im_livechat.legacy.PublicLivechatFloatingTextView',
init(parent, messaging) {
this._super(parent);
this.messaging = messaging;
this.publicLivechatFloatingTextView = this.messaging.publicLivechatGlobal.livechatButtonView.floatingTextView;
},
});
@@ -0,0 +1,55 @@
.o_PublicLivechatFloatingTextView {
animation: o_PublicLivechatFloatingTextView_animation 0.6s ease-in-out forwards;
display: flex;
position: absolute;
box-shadow: $box-shadow;
background-color: $white;
color: $gray-800;
right: 4.0rem;
bottom: 2.0rem;
border-radius: 0.5rem 0.5rem 0 0.5rem;
transform-origin: 100% 100%;
user-select: none;
}
@keyframes o_PublicLivechatFloatingTextView_animation {
0% {
opacity: 0;
transform: scale(0);
}
20% {
opacity: 0;
transform: scale(0.5);
}
50% {
opacity: 1;
}
75% {
transform: scale(1.05);
}
100% {
transform: scale(1);
}
}
.o_PublicLivechatFloatingTextView_background {
transition: filter 0.3s;
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
border-radius: 0.5rem 0.5rem 0 0.5rem;
background-color: $white;
}
.o_PublicLivechatFloatingTextView_text {
padding: 0.75rem 1rem;
position: relative;
color: $gray-800;
font-size: 1.2rem;
font-weight: 500;
font-family: var(--body-font-family);
text-shadow: none;
}
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<!--
@param {im_livechat.legacy.PublicLivechatFloatingTextView} widget
-->
<t t-name="im_livechat.legacy.PublicLivechatFloatingTextView">
<div class="o_PublicLivechatFloatingTextView">
<div class="o_PublicLivechatFloatingTextView_background"/>
<span class="o_PublicLivechatFloatingTextView_text" t-out="widget.publicLivechatFloatingTextView.text"/>
</div>
</t>
</templates>
@@ -1,6 +1,7 @@
/** @odoo-module **/
import { registerPatch } from '@mail/model/model_core';
import { attr, one } from '@mail/model/model_field';
import { clear } from '@mail/model/model_field_command';
import {unaccent} from 'web.utils';
@@ -50,6 +51,27 @@ registerPatch({
},
},
fields: {
floatingTextView: one('PublicLivechatFloatingTextView', {
inverse: 'livechatButtonViewOwner',
}),
hasFloatingText: attr({
compute() {
return Boolean(
this.messaging.publicLivechatGlobal.rule &&
this.messaging.publicLivechatGlobal.rule.action === 'display_button_and_text' &&
this.isWidgetMounted
);
},
}),
initialFloatingTextViewVisibilityTimer: one('Timer', {
compute() {
if (!this.floatingTextView && this.hasFloatingText) {
return {};
}
return clear();
},
inverse: 'livechatButtonViewOwnerAsInitialFloatingTextVisibility',
}),
isOpenChatDebounced: {
compute() {
if (this.messaging.publicLivechatGlobal.isTestChatbot) {
@@ -0,0 +1,30 @@
/** @odoo-module **/
import { PublicLivechatFloatingTextView } from '@website_livechat/legacy/widgets/public_livechat_floating_text_view/public_livechat_floating_text_view';
import { registerModel } from '@mail/model/model_core';
import { attr, one } from '@mail/model/model_field';
registerModel({
name: 'PublicLivechatFloatingTextView',
lifecycleHooks: {
_created() {
const widget = new PublicLivechatFloatingTextView(this.livechatButtonViewOwner.widget, this.messaging);
widget.appendTo(this.livechatButtonViewOwner.widget.$el);
this.update({ widget });
},
_willDelete() {
this.widget.destroy();
},
},
fields: {
livechatButtonViewOwner: one('LivechatButtonView', {
identifying: true,
inverse: 'floatingTextView',
}),
text: attr({
related: 'livechatButtonViewOwner.buttonText',
}),
widget: attr(),
},
});
@@ -31,4 +31,11 @@ registerPatch({
return this._super();
},
},
fields: {
hasWebsiteLivechatFeature: {
compute() {
return true;
},
},
},
});
@@ -0,0 +1,34 @@
/** @odoo-module **/
import { registerPatch } from '@mail/model/model_core';
import { one } from '@mail/model/model_field';
registerPatch({
name: 'Timer',
recordMethods: {
/**
* @override
*/
onTimeout() {
if (this.livechatButtonViewOwnerAsInitialFloatingTextVisibility) {
this.livechatButtonViewOwnerAsInitialFloatingTextVisibility.update({ floatingTextView: {} });
return;
}
return this._super();
},
},
fields: {
duration: {
compute() {
if (this.livechatButtonViewOwnerAsInitialFloatingTextVisibility) {
return 1 * 1000;
}
return this._super();
},
},
livechatButtonViewOwnerAsInitialFloatingTextVisibility: one('LivechatButtonView', {
inverse: 'initialFloatingTextViewVisibilityTimer',
identifying: true,
}),
},
});