[IMP] im_livechat : make the feedback for livechat a standart feature using the rating mixin. Create a dependancy to rating.

This commit is contained in:
Jérome Maes
2015-01-08 12:28:22 +01:00
parent c8581af592
commit ade60d54bb
15 changed files with 241 additions and 175 deletions
+1
View File
@@ -1,2 +1,3 @@
import controllers
import im_livechat
import report
+2 -1
View File
@@ -14,6 +14,7 @@ Live Chat Support
Allow to drop instant messaging widgets on any web page that will communicate
with the current server and dispatch visitors request amongst several live
chat operators.
Help your customers with this chat, and analyse their feedback.
""",
'data': [
@@ -27,7 +28,7 @@ chat operators.
'demo': [
"im_livechat_demo.xml",
],
'depends' : ["mail", "im_chat"],
'depends' : ["mail", "rating", "im_chat"],
'installable': True,
'auto_install': False,
'application': True,
@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
import livechat
import rating
@@ -0,0 +1,66 @@
# -*- coding: utf-8 -*-
import json
import openerp
import openerp.addons.im_chat.im_chat
from openerp import http
from openerp.http import request
class LivechatController(http.Controller):
@http.route('/im_livechat/support/<string:dbname>/<int:channel_id>', type='http', auth='none')
def support_page(self, dbname, channel_id, **kwargs):
registry, cr, uid, context = openerp.modules.registry.RegistryManager.get(dbname), request.cr, openerp.SUPERUSER_ID, request.context
info = registry.get('im_livechat.channel').get_info_for_chat_src(cr, uid, channel_id)
info["dbname"] = dbname
info["channel"] = channel_id
info["channel_name"] = registry.get('im_livechat.channel').read(cr, uid, channel_id, ['name'], context=context)["name"]
return request.render('im_livechat.support_page', info)
@http.route('/im_livechat/loader/<string:dbname>/<int:channel_id>', type='http', auth='none')
def loader(self, dbname, channel_id, **kwargs):
registry, cr, uid, context = openerp.modules.registry.RegistryManager.get(dbname), request.cr, openerp.SUPERUSER_ID, request.context
info = registry.get('im_livechat.channel').get_info_for_chat_src(cr, uid, channel_id)
info["dbname"] = dbname
info["channel"] = channel_id
info["username"] = kwargs.get("username", "Visitor")
# find the country from the request
country_id = False
country_code = request.session.geoip and request.session.geoip.get('country_name') or False
if country_code:
country_ids = registry.get('res.country').search(cr, uid, [('code', '=', country_code)], context=context)
if country_ids:
country_id = country_ids[0]
# extract url
url = request.httprequest.headers.get('Referer') or request.httprequest.base_url
# find the match rule for the given country and url
rule = registry.get('im_livechat.channel.rule').match_rule(cr, uid, channel_id, url, country_id, context=context)
if rule:
if rule.action == 'hide_button':
# don't return the initialization script, since its blocked (in the country)
return
rule_data = {
'action' : rule.action,
'auto_popup_timer' : rule.auto_popup_timer,
'regex_url' : rule.regex_url,
}
info['rule'] = json.dumps(rule and rule_data or False)
return request.render('im_livechat.loader', info)
@http.route('/im_livechat/get_session', type="json", auth="none")
def get_session(self, channel_id, anonymous_name, **kwargs):
cr, uid, context, db = request.cr, request.uid or openerp.SUPERUSER_ID, request.context, request.db
reg = openerp.modules.registry.RegistryManager.get(db)
# if geoip, add the country name to the anonymous name
if request.session.geoip:
anonymous_name = anonymous_name + " ("+request.session.geoip.get('country_name', "")+")"
return reg.get("im_livechat.channel").get_channel_session(cr, uid, channel_id, anonymous_name, context=context)
@http.route('/im_livechat/available', type='json', auth="none")
def available(self, db, channel):
cr, uid, context, db = request.cr, request.uid or openerp.SUPERUSER_ID, request.context, request.db
reg = openerp.modules.registry.RegistryManager.get(db)
with reg.cursor() as cr:
return len(reg.get('im_livechat.channel').get_available_users(cr, uid, channel)) > 0
+33
View File
@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
import openerp
from openerp.addons.rating.controllers.main import Rating
from openerp.addons.web import http
from openerp.http import request
class LivechatRatingController(Rating):
@http.route('/rating/livechat/feedback', type='json', auth='none')
def rating_livechat_feedback(self, uuid, rate, reason=None, **kwargs):
Session = request.env['im_chat.session']
Rating = request.env['rating.rating']
session_ids = Session.sudo().search([('uuid', '=', uuid)])
if session_ids:
session = session_ids[0]
# limit the creation : only ONE rating per session
if not session.rating_ids:
values = {
'res_id' : session.id,
'res_model' : 'im_chat.session',
'rating' : rate,
}
# find the partner related to the user of the conversation
rated_partner_id = False
if session.user_ids:
rated_partner_id = session.user_ids[0] and session.user_ids[0].partner_id.id or False
values['rated_partner_id'] = rated_partner_id
if reason:
values['feedback'] = reason
# create the rating
rating = Rating.sudo().create(values)
return rating.id
return False
+23 -77
View File
@@ -19,20 +19,13 @@
#
##############################################################################
import random
import openerp
import json
import openerp.addons.im_chat.im_chat
import datetime
import random
import re
import json
from openerp.osv import osv, fields
from openerp import tools
from openerp import http
from openerp.http import request
from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT
class im_livechat_channel(osv.Model):
@@ -94,6 +87,24 @@ class im_livechat_channel(osv.Model):
res[record.id] = len(record.session_ids)
return res
# RATING METHOD
def _compute_percentage_satisfaction(self, cr, uid, ids, name, arg, context=None):
res = {}
for record in self.browse(cr, uid, ids, context=context):
repartition = record.session_ids.rating_get_grades()
total = sum(repartition.values())
if total > 0:
happy = repartition['great']
res[record.id] = ((happy*100) / total) if happy > 0 else 0
else:
res[record.id] = -1
return res
def action_view_rating(self, cr, uid, ids, context=None):
channels = self.browse(cr, uid, ids, context=context)
action = self.pool['ir.actions.act_window'].for_xml_id(cr, uid, 'rating', 'action_view_rating', context=context)
action['domain'] = [('res_id', 'in', [s.id for s in channels.session_ids]), ('res_model', '=', 'im_chat.session')]
return action
_columns = {
'name': fields.char(string="Channel Name", size=200, required=True),
@@ -127,6 +138,8 @@ class im_livechat_channel(osv.Model):
'session_ids' : fields.one2many('im_chat.session', 'channel_id', 'Sessions'),
'nbr_session' : fields.function(_compute_nbr_session, type='integer', string='Number of session', store=False),
'rule_ids': fields.one2many('im_livechat.channel.rule','channel_id','Rules'),
# rating field
'rating_percentage_satisfaction' : fields.function(_compute_percentage_satisfaction, type='integer', string='% Happy', store=False, default=-1),
}
def _default_user_ids(self, cr, uid, context=None):
@@ -189,7 +202,6 @@ class im_livechat_channel(osv.Model):
return True
class im_livechat_channel_rule(osv.Model):
_name = 'im_livechat.channel.rule'
@@ -227,7 +239,8 @@ class im_livechat_channel_rule(osv.Model):
class im_chat_session(osv.Model):
_inherit = 'im_chat.session'
_name = 'im_chat.session'
_inherit = ['im_chat.session', 'rating.mixin']
def _get_fullname(self, cr, uid, ids, fields, arg, context=None):
""" built the complete name of the session """
@@ -247,8 +260,6 @@ class im_chat_session(osv.Model):
'create_date': fields.datetime('Create Date', required=True, select=True),
'channel_id': fields.many2one("im_livechat.channel", "Channel"),
'fullname' : fields.function(_get_fullname, type="char", string="Complete name"),
'feedback_rating': fields.selection([('10','Good'),('5','Ok'),('1','Bad')], 'Grade', help='Feedback from user (Bad, Ok or Good)'),
'feedback_reason': fields.text('Reason', help="Reason explaining the rating."),
}
def is_in_session(self, cr, uid, uuid, user_id, context=None):
@@ -282,68 +293,3 @@ class im_chat_session(osv.Model):
else:
return super(im_chat_session, self).quit_user(cr, uid, session.id, context=context)
class LiveChatController(http.Controller):
@http.route('/im_livechat/support/<string:dbname>/<int:channel_id>', type='http', auth='none')
def support_page(self, dbname, channel_id, **kwargs):
registry, cr, uid, context = openerp.modules.registry.RegistryManager.get(dbname), request.cr, openerp.SUPERUSER_ID, request.context
info = registry.get('im_livechat.channel').get_info_for_chat_src(cr, uid, channel_id)
info["dbname"] = dbname
info["channel"] = channel_id
info["channel_name"] = registry.get('im_livechat.channel').read(cr, uid, channel_id, ['name'], context=context)["name"]
return request.render('im_livechat.support_page', info)
@http.route('/im_livechat/loader/<string:dbname>/<int:channel_id>', type='http', auth='none')
def loader(self, dbname, channel_id, **kwargs):
registry, cr, uid, context = openerp.modules.registry.RegistryManager.get(dbname), request.cr, openerp.SUPERUSER_ID, request.context
info = registry.get('im_livechat.channel').get_info_for_chat_src(cr, uid, channel_id)
info["dbname"] = dbname
info["channel"] = channel_id
info["username"] = kwargs.get("username", "Visitor")
# find the country from the request
country_id = False
country_code = request.session.geoip and request.session.geoip.get('country_name', False) or False
if country_code:
country_ids = registry.get('res.country').search(cr, uid, [('code', '=', country_code)], context=context)
if country_ids:
country_id = country_ids[0]
# extract url
url = request.httprequest.headers.get('Referer', False) or request.httprequest.base_url
# find the match rule for the given country and url
rule = registry.get('im_livechat.channel.rule').match_rule(cr, uid, channel_id, url, country_id, context=context)
if rule:
if rule.action == 'hide_button':
# don't return the initialization script, since its blocked (in the country)
return
rule_data = {
'action' : rule.action,
'auto_popup_timer' : rule.auto_popup_timer,
'regex_url' : rule.regex_url,
}
info['rule'] = json.dumps(rule and rule_data or False)
return request.render('im_livechat.loader', info)
@http.route('/im_livechat/get_session', type="json", auth="none")
def get_session(self, channel_id, anonymous_name, **kwargs):
cr, uid, context, db = request.cr, request.uid or openerp.SUPERUSER_ID, request.context, request.db
reg = openerp.modules.registry.RegistryManager.get(db)
# if geoip, add the country name to the anonymous name
if request.session.geoip:
anonymous_name = anonymous_name + " ("+request.session.geoip.get('country_name', "")+")"
return reg.get("im_livechat.channel").get_channel_session(cr, uid, channel_id, anonymous_name, context=context)
@http.route('/im_livechat/available', type='json', auth="none")
def available(self, db, channel):
cr, uid, context, db = request.cr, request.uid or openerp.SUPERUSER_ID, request.context, request.db
reg = openerp.modules.registry.RegistryManager.get(db)
with reg.cursor() as cr:
return len(reg.get('im_livechat.channel').get_available_users(cr, uid, channel)) > 0
@http.route('/im_livechat/feedback', type='json', auth="none")
def feedback(self, uuid, rating, reason=None):
cr, uid, context, db = request.cr, request.uid or openerp.SUPERUSER_ID, request.context, request.db
registry = openerp.modules.registry.RegistryManager.get(db)
Session = registry['im_chat.session']
session_ids = Session.search(cr, uid, [('uuid','=',uuid)], context=context)
Session.write(cr, uid, session_ids, {'feedback_rating' : str(rating), 'feedback_reason' : reason}, context=context)
@@ -18,7 +18,6 @@ class im_livechat_report(osv.Model):
'nbr_messages': fields.integer('Average message', readonly=True, group_operator="avg", help="Number of message in the conversation"),
'nbr_user_messages': fields.integer('Average of messages/user', readonly=True, group_operator="avg", help="Average number of message per user"),
'nbr_speakers': fields.integer('# of speakers', readonly=True, group_operator="avg", help="Number of different speakers"),
'rating': fields.float('Rating', readonly=True, group_operator="avg", help="Average Rating"),
'user_id': fields.many2one('res.users', 'User', readonly=True),
'session_id': fields.many2one('im_chat.session', 'Session', readonly=True),
'channel_id': fields.many2one('im_livechat.channel', 'Channel', readonly=True),
@@ -42,7 +41,6 @@ class im_livechat_report(osv.Model):
(SELECT count(distinct COALESCE(from_id, 0)) FROM im_chat_message WHERE to_id=S.id) as nbr_speakers,
(SELECT count(id) FROM im_chat_message WHERE to_id=S.id) as nbr_messages,
count(M.id) as nbr_user_messages,
CAST(S.feedback_rating AS INT) as rating,
U.id as user_id,
S.channel_id as channel_id,
S.id as session_id
@@ -1,45 +1,42 @@
/* feedback */
.oe_im_chatview_feedback{
/* Livechat Rating : feedback smiley */
.oe_im_chatview .oe_livechat_rating{
padding: 15px;
vertical-align: middle;
}
.oe_im_chatview_feedback .oe_im_feedback_text {
.oe_im_chatview .oe_livechat_rating .oe_im_feedback_text {
color: white;
vertical-align: middle;
font-size: 14px;
}
.oe_im_chatview_feedback .oe_im_feedback_small_text {
color: white;
vertical-align: middle;
font-size: 12px;
}
.oe_im_chatview_feedback .oe_im_feedback_choices{
.oe_im_chatview .oe_livechat_rating .oe_livechat_rating_choices{
height:40px;
text-align: center;
margin: 10px 0;
margin: 10px 0 30px 0;
}
.oe_im_chatview_feedback .oe_im_feedback_choices a{
.oe_im_chatview .oe_livechat_rating .oe_livechat_rating_choices img{
width: 50px;
opacity: 0.60;
cursor: pointer;
margin: 5px;
opacity: 0.65;
}
.oe_im_chatview_feedback .oe_im_feedback_choices a:hover{
.oe_im_chatview .oe_livechat_rating .oe_livechat_rating_choices img:hover{
opacity:1;
}
.oe_im_chatview_feedback .oe_im_feedback_choices a.selected{
.oe_im_chatview .oe_livechat_rating .oe_livechat_rating_choices img.selected{
opacity: 1;
}
/* feedback reason */
.oe_im_feedback_reason {
.oe_im_chatview .oe_livechat_rating .oe_livechat_rating_reason {
margin: 10px 0;
display: none; /* hidden by default */
}
.oe_im_feedback_reason textarea {
.oe_im_chatview .oe_livechat_rating .oe_livechat_rating_reason textarea {
width: 100%;
height: 70px;
resize: none;
}
.oe_im_feedback_btn input{
.oe_im_chatview .oe_livechat_rating .oe_livechat_rating_reason_button input{
float: right;
margin-left : 10px;
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 962 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 886 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 739 B

+54 -49
View File
@@ -1,7 +1,3 @@
/*
This file must compile in EcmaScript 3 and work in IE7.
*/
(function() {
"use strict";
@@ -20,9 +16,7 @@
this._super.apply(this, arguments);
this.shown = true;
this.loading_history = true; // since the session is kept after a refresh, anonymous can reload their history
this.feedback = false;
this.rating = false;
this.feedback = false;
},
define_options: function(){
// no options for anonymous user
@@ -57,51 +51,17 @@
openerp.set_cookie(im_livechat.COOKIE_NAME, JSON.stringify(session), 60*60);
},
click_close: function(event) {
this.$('.oe_im_chatview_input').attr('disabled','disabled');
if(!this.feedback && (this.get('messages').length > 1)){
this.feedback = true;
this.$(".oe_im_chatview_content").html(openerp.qweb.render("support_feedback"));
this.$('.oe_im_feedback_choices a').on('click', _.bind(this.choose_feedback, this));
this.$('.oe_im_chatview_feedback #submit').on('click', _.bind(this.submit_feedback, this));
this.feedback = new im_livechat.Feedback(this);
this.$(".oe_im_chatview_content").empty();
this.$(".oe_im_chatview_input").prop('disabled', true);
this.feedback.appendTo( this.$(".oe_im_chatview_content"));
// bind event to close conversation
this.feedback.on("feedback_sent", this, this.click_close);
}else{
if(this.rating){
this.send_feedback(this.rating);
}
// delete cookie
openerp.set_cookie(im_livechat.COOKIE_NAME, "", -1);
this._super.apply(this, arguments);
}
},
choose_feedback: function(e){
var self = this;
this.rating = parseInt($(e.currentTarget).data('value'));
this.$('.oe_im_feedback_choices a').removeClass('selected');
this.$('.oe_im_feedback_choices a[data-value="'+this.rating+'"]').addClass('selected');
},
submit_feedback: function(e){
var self = this;
if(this.rating){
var reason = self.$('.oe_im_chatview_feedback textarea').val();
if(this.rating > 1){
this.send_feedback(this.rating, reason);
}else{
if(reason){
this.send_feedback(this.rating, reason);
}else{
self.$('.oe_im_chatview_feedback textarea').css('border', 'red solid 2px');
}
}
}else{
this.feedback = false;
}
},
send_feedback: function(rate, reason){
var self = this;
openerp.session.rpc("/im_livechat/feedback", {uuid: this.get('session').uuid, rating: rate, reason : reason}).then(function(res) {
self.$(".oe_im_chatview_feedback").html($(document.createElement('div')).addClass("oe_im_feedback_text").html(_t("Thanks you for your feedback. You can close the chat window now.")));
});
this.rating = false;
}
});
// To avoid exeption when the anonymous has close his
@@ -138,11 +98,14 @@
openerp.session = new openerp.Session(null, server_url, { use_cors: false });
this.load_template(db, channel, options, rule);
},
_get_template_list: function(){
return ['/im_chat/static/src/xml/im_chat.xml', '/im_livechat/static/src/xml/im_livechat.xml'];
},
load_template: function(db, channel, options, rule){
var self = this;
// load the qweb templates
var defs = [];
var templates = ['/im_livechat/static/src/xml/im_livechat.xml', '/im_chat/static/src/xml/im_chat.xml'];
var templates = this._get_template_list();
_.each(templates, function(tmpl){
defs.push(openerp.session.rpc('/web/proxy/load', {path: tmpl}).then(function(xml) {
openerp.qweb.add_template(xml);
@@ -191,7 +154,7 @@
this.no_session_message = _t("None of our collaborators seems to be available, please try again later.");
},
start: function() {
this.$().append(openerp.qweb.render("chatButton", {widget: this}));
this.$().append(openerp.qweb.render("im_livechat.chatButton", {widget: this}));
// set up the manager
this.manager = new openerp.im_chat.ConversationManager(this, this.options);
this.manager.set("bottom_offset", $('.oe_chat_button').outerHeight());
@@ -257,6 +220,48 @@
}
});
/* Rating livechat object */
im_livechat.Feedback = openerp.Widget.extend({
template : "im_livechat.feedback",
init: function(parent){
this._super(parent);
this.conversation = parent;
this.reason = false;
this.rating = false;
},
start: function(){
this._super.apply(this.arguments);
// bind events
this.$('.oe_livechat_rating_choices img').on('click', _.bind(this.click_smiley, this));
this.$('#rating_submit').on('click', _.bind(this.click_send, this));
},
click_smiley: function(ev){
this.rating = parseInt($(ev.currentTarget).data('value'));
this.$('.oe_livechat_rating_choices img').removeClass('selected');
this.$('.oe_livechat_rating_choices img[data-value="'+this.rating+'"]').addClass('selected');
// only display textearea if bad smiley selected
if(this.rating == 0){
this.$('.oe_livechat_rating_reason').show();
}else{
this.$('.oe_livechat_rating_reason').hide();
}
this.$('textarea').val(''); // empty the reason each time a click on a smiley is done
},
click_send: function(ev){
this.reason = this.$('textarea').val();
if(_.contains([0,5,10], this.rating)){ // need to use contains, since the rating can 0, evaluate to false
this._send_feedback();
}
},
_send_feedback: function(){
var self = this;
var uuid = this.conversation.get('session').uuid;
openerp.session.rpc("/rating/livechat/feedback", {uuid: uuid, rate: this.rating, reason : this.reason}).then(function(res) {
self.trigger("feedback_sent"); // will close the conversation
});
}
});
return im_livechat;
})();
@@ -1,31 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<templates xml:space="preserve">
<t t-name="chatButton">
<t t-name="im_livechat.chatButton">
<t t-esc="widget.text"/>
</t>
<t t-name="support_feedback">
<div class="oe_im_chatview_feedback">
<t t-name="im_livechat.feedback">
<div class="oe_livechat_rating">
<div class="oe_im_feedback_text">
Please take a second to rate our Livechat Support ...
Did we correctly answer your question ?
</div>
<div class="oe_im_feedback_choices">
<a data-value="10" name="good" title="Good"><img src='/im_livechat/static/src/img/good.png'/></a>
<a data-value="5" name="ok" title="OK"><img src='/im_livechat/static/src/img/ok.png'/></a>
<a data-value="1" name="bad" title="Bad"><img src='/im_livechat/static/src/img/bad.png'/></a>
<div class="oe_livechat_rating_choices">
<img src='/rating/static/src/img/rating_10.png' alt="Good" data-value="10"/>
<img src='/rating/static/src/img/rating_5.png' alt="OK" data-value="5"/>
<img src='/rating/static/src/img/rating_0.png' alt="Bad" data-value="0" />
</div>
<div class="oe_im_feedback_small_text">
Click on the icon that represent the most your feeling.
</div>
<div class="oe_im_feedback_reason">
<div class="oe_livechat_rating_reason">
<textarea id="reason" placeholder="Explain your note"></textarea>
</div>
<div class="oe_im_feedback_btn">
<input type="button" id="submit" value="Send" />
<div class="oe_livechat_rating_reason_button">
<input type="button" id="rating_submit" value="Send" />
</div>
</div>
</t>
</templates>
+1 -2
View File
@@ -113,8 +113,7 @@
<script type="text/javascript" src="/im_livechat/static/src/js/im_livechat.js"></script>
<!-- CSS -->
<link rel="stylesheet" href="/im_chat/static/src/css/im_common.css"></link>
<!-- Style for livechat extern only -->
<link rel="stylesheet" href="/im_livechat/static/src/css/im_livechat.css"></link>
<link rel="stylesheet" href="/im_livechat/static/src/css/im_livechat.css"/>
</template>
<!-- the js code to initialize the LiveSupport object -->
+31 -12
View File
@@ -40,7 +40,6 @@
<field name="id"/>
<field name="fullname"/>
<field name="create_date"/>
<field name="feedback_rating"/>
</tree>
</field>
</record>
@@ -51,14 +50,32 @@
<field name="arch" type="xml">
<form string="Session Form" create="false" edit="false">
<sheet>
<group string="General">
<field name="id"/>
<field name="fullname" />
<field name="create_date" readonly="1"/>
</group>
<group string="Feedback">
<field name="feedback_rating"/>
<field name="feedback_reason"/>
<group>
<group string="General">
<field name="id"/>
<field name="fullname" />
<field name="create_date" readonly="1"/>
</group>
<group string="Feedback">
<field name="rating_ids" mode="kanban" nolabel="1">
<kanban>
<field name="rating"/>
<field name="feedback"/>
<field name="res_name"/>
<field name="create_date"/>
<templates>
<t t-name="kanban-box">
<div class="oe_group_details" style="min-height: 40px">
<img t-attf-src="rating/static/src/img/rating_#{record.rating.raw_value and record.rating.raw_value or 0}.png" class="oe_kanban_image" style="float:left; margin-right: 10px;"/>
<span t-if="record.feedback.raw_value">
<t t-esc="record.feedback.raw_value"/>
</span>
</div>
</t>
</templates>
</kanban>
</field>
</group>
</group>
<field name="message_ids" type="tree">
<tree string="History">
@@ -176,6 +193,9 @@
<button class="oe_inline oe_stat_button" type="action" attrs="{'invisible':[('nbr_session','=', 0)]}" name="%(action_session_channel)d" icon="fa-comments">
<field string="Sessions" name="nbr_session" widget="statinfo"/>
</button>
<button name="action_view_rating" attrs="{'invisible':[('rating_percentage_satisfaction','=', -1)]}" class="oe_stat_button" type="object" icon="fa-smile-o">
<field string="% Happy" name="rating_percentage_satisfaction" widget="statinfo"/>
</button>
</div>
<group>
<group string="Operators">
@@ -266,10 +286,9 @@
<!-- Menu items -->
<menuitem name="Live Chat" parent="mail.mail_feeds_main" id="im_livechat" groups="group_im_livechat"/>
<menuitem name="History" parent="im_livechat" id="session_history" action="action_session_history" groups="group_im_livechat"/>
<menuitem name="Channels" parent="im_livechat" id="support_channels" action="action_support_channels" groups="group_im_livechat"/>
<menuitem name="Channels" parent="im_livechat" id="support_channels" action="action_support_channels" groups="group_im_livechat" sequence="5"/>
<menuitem name="History" parent="im_livechat" id="session_history" action="action_session_history" groups="group_im_livechat" sequence="10"/>
</data>
</openerp>