From 4af96310721237d3640bbd162ff9ea84e627028d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9rome=20Maes?= Date: Mon, 1 Jun 2015 16:37:07 +0200 Subject: [MOV] mail, im_chat : move files and code from im_chat to mail and bus - im_chat.session will be replaced by mail.channel - im_chat.shortode is renamed into mail.shortcode - im_chat.presence is moved to bus module - js and controller code is moved from im_chat to mail module This commit only move files, and modify manifests, bundles, ... The code will be adapt in the next commits. --- addons/bus/controllers/main.py | 3 + addons/bus/models/__init__.py | 2 + addons/bus/models/bus_presence.py | 87 +++++ addons/bus/models/res_users.py | 78 ++++ addons/bus/security/ir.model.access.csv | 4 +- addons/bus/static/src/js/bus.js | 16 + addons/im_chat/controllers/main.py | 63 --- addons/im_chat/data/im_chat_session_data.xml | 158 -------- addons/im_chat/models/res_users.py | 78 ---- addons/im_chat/static/src/audio/purr.mp3 | Bin 85262 -> 0 bytes addons/im_chat/static/src/audio/purr.ogg | Bin 47390 -> 0 bytes addons/im_chat/static/src/audio/ting.mp3 | Bin 10866 -> 0 bytes addons/im_chat/static/src/audio/ting.ogg | Bin 14654 -> 0 bytes addons/im_chat/static/src/css/im_chat_backend.css | 114 ------ addons/im_chat/static/src/css/im_chat_common.css | 259 ------------ addons/im_chat/static/src/img/avatar.jpg | Bin 6462 -> 0 bytes addons/im_chat/static/src/img/beer.png | Bin 6097 -> 0 bytes addons/im_chat/static/src/img/boar.png | Bin 4798 -> 0 bytes addons/im_chat/static/src/img/cat.png | Bin 6083 -> 0 bytes addons/im_chat/static/src/img/confused.png | Bin 4633 -> 0 bytes addons/im_chat/static/src/img/cookie.png | Bin 8077 -> 0 bytes addons/im_chat/static/src/img/ghost.png | Bin 4513 -> 0 bytes addons/im_chat/static/src/img/green.png | Bin 1239 -> 0 bytes addons/im_chat/static/src/img/heart.png | Bin 3302 -> 0 bytes addons/im_chat/static/src/img/laugh.png | Bin 6347 -> 0 bytes addons/im_chat/static/src/img/man.png | Bin 5985 -> 0 bytes addons/im_chat/static/src/img/man_with_turban.png | Bin 6526 -> 0 bytes addons/im_chat/static/src/img/musti.png | Bin 1017 -> 0 bytes addons/im_chat/static/src/img/neutral.png | Bin 4843 -> 0 bytes addons/im_chat/static/src/img/open_mouth.png | Bin 4519 -> 0 bytes addons/im_chat/static/src/img/pinky.png | Bin 2978 -> 0 bytes addons/im_chat/static/src/img/poop.png | Bin 4754 -> 0 bytes addons/im_chat/static/src/img/postal_horn.png | Bin 4815 -> 0 bytes addons/im_chat/static/src/img/sad.png | Bin 4764 -> 0 bytes addons/im_chat/static/src/img/scream.png | Bin 6503 -> 0 bytes addons/im_chat/static/src/img/smile.png | Bin 5890 -> 0 bytes addons/im_chat/static/src/img/stuck_out_tongue.png | Bin 5955 -> 0 bytes addons/im_chat/static/src/img/thumbsdown.png | Bin 5110 -> 0 bytes addons/im_chat/static/src/img/thumbsup.png | Bin 5045 -> 0 bytes addons/im_chat/static/src/img/trollface.png | Bin 2828 -> 0 bytes addons/im_chat/static/src/img/wink.png | Bin 5253 -> 0 bytes addons/im_chat/static/src/img/yellow.png | Bin 1235 -> 0 bytes addons/im_chat/static/src/img/yum.png | Bin 5886 -> 0 bytes addons/im_chat/static/src/js/im_chat_backend.js | 301 -------------- addons/im_chat/static/src/js/im_chat_common.js | 433 --------------------- addons/im_chat/static/src/xml/im_chat.xml | 103 ----- addons/im_chat/views/im_chat_session_views.xml | 48 --- addons/mail/__openerp__.py | 3 + addons/mail/controllers/__init__.py | 2 + addons/mail/controllers/bus.py | 63 +++ addons/mail/data/mail_shortcode_data.xml | 158 ++++++++ addons/mail/models/__init__.py | 3 +- addons/mail/models/mail_channel.py | 138 +++++++ addons/mail/models/mail_shortcode.py | 48 +++ addons/mail/static/src/audio/purr.mp3 | Bin 0 -> 85262 bytes addons/mail/static/src/audio/purr.ogg | Bin 0 -> 47390 bytes addons/mail/static/src/audio/ting.mp3 | Bin 0 -> 10866 bytes addons/mail/static/src/audio/ting.ogg | Bin 0 -> 14654 bytes addons/mail/static/src/css/mail_chat_backend.css | 114 ++++++ addons/mail/static/src/css/mail_chat_common.css | 259 ++++++++++++ addons/mail/static/src/img/smiley/avatar.jpg | Bin 0 -> 6462 bytes addons/mail/static/src/img/smiley/beer.png | Bin 0 -> 6097 bytes addons/mail/static/src/img/smiley/boar.png | Bin 0 -> 4798 bytes addons/mail/static/src/img/smiley/cat.png | Bin 0 -> 6083 bytes addons/mail/static/src/img/smiley/confused.png | Bin 0 -> 4633 bytes addons/mail/static/src/img/smiley/cookie.png | Bin 0 -> 8077 bytes addons/mail/static/src/img/smiley/ghost.png | Bin 0 -> 4513 bytes addons/mail/static/src/img/smiley/green.png | Bin 0 -> 1239 bytes addons/mail/static/src/img/smiley/heart.png | Bin 0 -> 3302 bytes addons/mail/static/src/img/smiley/laugh.png | Bin 0 -> 6347 bytes addons/mail/static/src/img/smiley/man.png | Bin 0 -> 5985 bytes .../mail/static/src/img/smiley/man_with_turban.png | Bin 0 -> 6526 bytes addons/mail/static/src/img/smiley/musti.png | Bin 0 -> 1017 bytes addons/mail/static/src/img/smiley/neutral.png | Bin 0 -> 4843 bytes addons/mail/static/src/img/smiley/open_mouth.png | Bin 0 -> 4519 bytes addons/mail/static/src/img/smiley/pinky.png | Bin 0 -> 2978 bytes addons/mail/static/src/img/smiley/poop.png | Bin 0 -> 4754 bytes addons/mail/static/src/img/smiley/postal_horn.png | Bin 0 -> 4815 bytes addons/mail/static/src/img/smiley/sad.png | Bin 0 -> 4764 bytes addons/mail/static/src/img/smiley/scream.png | Bin 0 -> 6503 bytes addons/mail/static/src/img/smiley/smile.png | Bin 0 -> 5890 bytes .../static/src/img/smiley/stuck_out_tongue.png | Bin 0 -> 5955 bytes addons/mail/static/src/img/smiley/thumbsdown.png | Bin 0 -> 5110 bytes addons/mail/static/src/img/smiley/thumbsup.png | Bin 0 -> 5045 bytes addons/mail/static/src/img/smiley/trollface.png | Bin 0 -> 2828 bytes addons/mail/static/src/img/smiley/wink.png | Bin 0 -> 5253 bytes addons/mail/static/src/img/smiley/yellow.png | Bin 0 -> 1235 bytes addons/mail/static/src/img/smiley/yum.png | Bin 0 -> 5886 bytes addons/mail/static/src/js/mail_chat_backend.js | 301 ++++++++++++++ addons/mail/static/src/js/mail_chat_common.js | 433 +++++++++++++++++++++ addons/mail/static/src/xml/mail_chat_common.xml | 103 +++++ addons/mail/views/mail_shortcode_views.xml | 48 +++ addons/mail/views/mail_templates.xml | 5 + openerp/tools/mail.py | 20 +- 94 files changed, 1885 insertions(+), 1560 deletions(-) create mode 100644 addons/bus/models/bus_presence.py create mode 100644 addons/bus/models/res_users.py delete mode 100644 addons/im_chat/controllers/main.py delete mode 100644 addons/im_chat/data/im_chat_session_data.xml delete mode 100644 addons/im_chat/models/res_users.py delete mode 100644 addons/im_chat/static/src/audio/purr.mp3 delete mode 100644 addons/im_chat/static/src/audio/purr.ogg delete mode 100644 addons/im_chat/static/src/audio/ting.mp3 delete mode 100644 addons/im_chat/static/src/audio/ting.ogg delete mode 100644 addons/im_chat/static/src/css/im_chat_backend.css delete mode 100644 addons/im_chat/static/src/css/im_chat_common.css delete mode 100644 addons/im_chat/static/src/img/avatar.jpg delete mode 100644 addons/im_chat/static/src/img/beer.png delete mode 100644 addons/im_chat/static/src/img/boar.png delete mode 100644 addons/im_chat/static/src/img/cat.png delete mode 100644 addons/im_chat/static/src/img/confused.png delete mode 100644 addons/im_chat/static/src/img/cookie.png delete mode 100644 addons/im_chat/static/src/img/ghost.png delete mode 100644 addons/im_chat/static/src/img/green.png delete mode 100644 addons/im_chat/static/src/img/heart.png delete mode 100644 addons/im_chat/static/src/img/laugh.png delete mode 100644 addons/im_chat/static/src/img/man.png delete mode 100644 addons/im_chat/static/src/img/man_with_turban.png delete mode 100644 addons/im_chat/static/src/img/musti.png delete mode 100644 addons/im_chat/static/src/img/neutral.png delete mode 100644 addons/im_chat/static/src/img/open_mouth.png delete mode 100644 addons/im_chat/static/src/img/pinky.png delete mode 100644 addons/im_chat/static/src/img/poop.png delete mode 100644 addons/im_chat/static/src/img/postal_horn.png delete mode 100644 addons/im_chat/static/src/img/sad.png delete mode 100644 addons/im_chat/static/src/img/scream.png delete mode 100644 addons/im_chat/static/src/img/smile.png delete mode 100644 addons/im_chat/static/src/img/stuck_out_tongue.png delete mode 100644 addons/im_chat/static/src/img/thumbsdown.png delete mode 100644 addons/im_chat/static/src/img/thumbsup.png delete mode 100644 addons/im_chat/static/src/img/trollface.png delete mode 100644 addons/im_chat/static/src/img/wink.png delete mode 100644 addons/im_chat/static/src/img/yellow.png delete mode 100644 addons/im_chat/static/src/img/yum.png delete mode 100644 addons/im_chat/static/src/js/im_chat_backend.js delete mode 100644 addons/im_chat/static/src/js/im_chat_common.js delete mode 100644 addons/im_chat/static/src/xml/im_chat.xml delete mode 100644 addons/im_chat/views/im_chat_session_views.xml create mode 100644 addons/mail/controllers/bus.py create mode 100644 addons/mail/data/mail_shortcode_data.xml create mode 100644 addons/mail/models/mail_shortcode.py create mode 100644 addons/mail/static/src/audio/purr.mp3 create mode 100644 addons/mail/static/src/audio/purr.ogg create mode 100644 addons/mail/static/src/audio/ting.mp3 create mode 100644 addons/mail/static/src/audio/ting.ogg create mode 100644 addons/mail/static/src/css/mail_chat_backend.css create mode 100644 addons/mail/static/src/css/mail_chat_common.css create mode 100644 addons/mail/static/src/img/smiley/avatar.jpg create mode 100644 addons/mail/static/src/img/smiley/beer.png create mode 100644 addons/mail/static/src/img/smiley/boar.png create mode 100644 addons/mail/static/src/img/smiley/cat.png create mode 100644 addons/mail/static/src/img/smiley/confused.png create mode 100644 addons/mail/static/src/img/smiley/cookie.png create mode 100644 addons/mail/static/src/img/smiley/ghost.png create mode 100644 addons/mail/static/src/img/smiley/green.png create mode 100644 addons/mail/static/src/img/smiley/heart.png create mode 100644 addons/mail/static/src/img/smiley/laugh.png create mode 100644 addons/mail/static/src/img/smiley/man.png create mode 100644 addons/mail/static/src/img/smiley/man_with_turban.png create mode 100644 addons/mail/static/src/img/smiley/musti.png create mode 100644 addons/mail/static/src/img/smiley/neutral.png create mode 100644 addons/mail/static/src/img/smiley/open_mouth.png create mode 100644 addons/mail/static/src/img/smiley/pinky.png create mode 100644 addons/mail/static/src/img/smiley/poop.png create mode 100644 addons/mail/static/src/img/smiley/postal_horn.png create mode 100644 addons/mail/static/src/img/smiley/sad.png create mode 100644 addons/mail/static/src/img/smiley/scream.png create mode 100644 addons/mail/static/src/img/smiley/smile.png create mode 100644 addons/mail/static/src/img/smiley/stuck_out_tongue.png create mode 100644 addons/mail/static/src/img/smiley/thumbsdown.png create mode 100644 addons/mail/static/src/img/smiley/thumbsup.png create mode 100644 addons/mail/static/src/img/smiley/trollface.png create mode 100644 addons/mail/static/src/img/smiley/wink.png create mode 100644 addons/mail/static/src/img/smiley/yellow.png create mode 100644 addons/mail/static/src/img/smiley/yum.png create mode 100644 addons/mail/static/src/js/mail_chat_backend.js create mode 100644 addons/mail/static/src/js/mail_chat_common.js create mode 100644 addons/mail/static/src/xml/mail_chat_common.xml create mode 100644 addons/mail/views/mail_shortcode_views.xml diff --git a/addons/bus/controllers/main.py b/addons/bus/controllers/main.py index ee9902ec983..3da386e80b2 100644 --- a/addons/bus/controllers/main.py +++ b/addons/bus/controllers/main.py @@ -20,6 +20,9 @@ class BusController(openerp.http.Controller): # override to add channels def _poll(self, dbname, channels, last, options): + # update the user presence + if request.session.uid and 'im_presence' in options: + request.env['bus.presence'].update(options.get('im_presence')) request.cr.close() request._cr = None return dispatch.poll(dbname, channels, last) diff --git a/addons/bus/models/__init__.py b/addons/bus/models/__init__.py index c1c365d98c0..208eb0c6c81 100644 --- a/addons/bus/models/__init__.py +++ b/addons/bus/models/__init__.py @@ -1,2 +1,4 @@ # -*- coding: utf-8 -*- import bus +import bus_presence +import res_users diff --git a/addons/bus/models/bus_presence.py b/addons/bus/models/bus_presence.py new file mode 100644 index 00000000000..d89cfc9a104 --- /dev/null +++ b/addons/bus/models/bus_presence.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +import datetime +import random +import time + +from openerp import api, fields, models +from openerp import tools +from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT + +from openerp.addons.bus.models.bus import TIMEOUT + +DISCONNECTION_TIMER = TIMEOUT + 5 +AWAY_TIMER = 600 # 10 minutes + + +class ImChatPresence(models.Model): + """ User Chat Presence + Its status is 'online', 'away' or 'offline'. This model should be a one2one, but is not + attached to res_users to avoid database concurrence errors. Since the 'update' method is executed + at each poll, if the user have multiple opened tabs, concurrence errors can happend, but are 'muted-logged'. + """ + + _name = 'im_chat.presence' + _description = 'User Chat Presence' + + _sql_constraints = [('im_chat_user_status_unique', 'unique(user_id)', 'A user can only have one IM status.')] + + user_id = fields.Many2one('res.users', 'Users', required=True, index=True, ondelete='cascade') + last_poll = fields.Datetime('Last Poll', default=lambda self: fields.Datetime.now()) + last_presence = fields.Datetime('Last Presence', default=lambda self: fields.Datetime.now()) + status = fields.Selection([('online', 'Online'), ('away', 'Away'), ('offline', 'Offline')], 'IM Status', default='offline') + + + @api.model + def update(self, user_presence=True): + """ Register the given presence of the current user, and trigger a im_status change if necessary. + The status will not be written or sent if not necessary. + :param user_presence : True, if the user (self._uid) is still detected using its browser. + :type user_presence : boolean + """ + presence = self.search([('user_id', '=', self._uid)], limit=1) + # set the default values + send_notification = True + values = { + 'last_poll': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT), + 'status' : presence and presence.status or 'offline' + } + # update the user or a create a new one + if not presence: # create a new presence for the user + values['status'] = 'online' + values['user_id'] = self._uid + self.create(values) + else: # write the user presence if necessary + if user_presence: + values['last_presence'] = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT) + values['status'] = 'online' + else: + threshold = datetime.datetime.now() - datetime.timedelta(seconds=AWAY_TIMER) + if datetime.datetime.strptime(presence.last_presence, DEFAULT_SERVER_DATETIME_FORMAT) < threshold: + values['status'] = 'away' + send_notification = presence.status != values['status'] + # write only if the last_poll is passed TIMEOUT, or if the status has changed + delta = datetime.datetime.utcnow() - datetime.datetime.strptime(presence.last_poll, DEFAULT_SERVER_DATETIME_FORMAT) + if delta > datetime.timedelta(seconds=TIMEOUT) or send_notification: + # Hide transaction serialization errors, which can be ignored, the presence update is not essential + with tools.mute_logger('openerp.sql_db'): + presence.write(values) + # avoid TransactionRollbackError + self.env.cr.commit() # TODO : check if still necessary + # notify if the status has changed + if send_notification: # TODO : add user_id to the channel tuple to allow using user_watch in controller presence + self.env['bus.bus'].sendone((self._cr.dbname, 'im_chat.presence'), {'id': self._uid, 'im_status': values['status']}) + # gc : disconnect the users having a too old last_poll. 1 on 100 chance to do it. + if random.random() < 0.01: + self.check_users_disconnection() + return True + + @api.model + def check_users_disconnection(self): + """ Disconnect the users having a too old last_poll """ + limit_date = (datetime.datetime.utcnow() - datetime.timedelta(0, DISCONNECTION_TIMER)).strftime(DEFAULT_SERVER_DATETIME_FORMAT) + presences = self.search([('last_poll', '<', limit_date), ('status', '!=', 'offline')]) + presences.write({'status': 'offline'}) + notifications = [] + for presence in presences: + notifications.append([(self._cr.dbname, 'im_chat.presence'), {'id': presence.user_id.id, 'im_status': presence.status}]) + self.env['bus.bus'].sendmany(notifications) diff --git a/addons/bus/models/res_users.py b/addons/bus/models/res_users.py new file mode 100644 index 00000000000..5e9d664b6a8 --- /dev/null +++ b/addons/bus/models/res_users.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +from openerp import api, fields, models + + +class ResUsers(models.Model): + + _inherit = "res.users" + + im_status = fields.Char('IM Status', compute='_compute_im_status') + + @api.multi + def _compute_im_status(self): + """ Compute the im_status of the users """ + res = dict((uid, 'offline') for uid in self.ids) + presences = self.env['im_chat.presence'].search([('user_id', 'in', self.ids)]) + for presence in presences: + res[presence.user_id.id] = presence.status + for user in self: + user.im_status = res.get(user.id) + + @api.model + def im_search(self, name, limit=20): + """ Search users with a name and return its id, name and im_status. + Note : the user must be logged + :param name : the user name to search + :param limit : the limit of result to return + """ + result = [] + # find the employee group + group_employee = self.env['ir.model.data'].get_object_reference('base', 'group_user')[1] + + where_clause_base = " U.active = 't' " + query_params = () + if name: + where_clause_base += " AND P.name ILIKE %s " + query_params = query_params + ('%'+name+'%',) + + # first query to find online employee + self._cr.execute('''SELECT U.id as id, P.name as name, COALESCE(S.status, 'offline') as im_status + FROM im_chat_presence S + JOIN res_users U ON S.user_id = U.id + JOIN res_partner P ON P.id = U.partner_id + WHERE '''+where_clause_base+''' + AND U.id != %s + AND EXISTS (SELECT 1 FROM res_groups_users_rel G WHERE G.gid = %s AND G.uid = U.id) + AND S.status = 'online' + ORDER BY P.name + LIMIT %s + ''', query_params + (self._uid, group_employee, limit)) + result = result + self._cr.dictfetchall() + + # second query to find other online people + if len(result) < limit: + self._cr.execute('''SELECT U.id as id, P.name as name, COALESCE(S.status, 'offline') as im_status + FROM im_chat_presence S + JOIN res_users U ON S.user_id = U.id + JOIN res_partner P ON P.id = U.partner_id + WHERE '''+where_clause_base+''' + AND U.id NOT IN %s + AND S.status = 'online' + ORDER BY P.name + LIMIT %s + ''', query_params + (tuple([u["id"] for u in result]) + (self._uid,), limit-len(result))) + result = result + self._cr.dictfetchall() + + # third query to find all other people + if len(result) < limit: + self._cr.execute('''SELECT U.id as id, P.name as name, COALESCE(S.status, 'offline') as im_status + FROM res_users U + LEFT JOIN im_chat_presence S ON S.user_id = U.id + LEFT JOIN res_partner P ON P.id = U.partner_id + WHERE '''+where_clause_base+''' + AND U.id NOT IN %s + ORDER BY P.name + LIMIT %s + ''', query_params + (tuple([u["id"] for u in result]) + (self._uid,), limit-len(result))) + result = result + self._cr.dictfetchall() + return result diff --git a/addons/bus/security/ir.model.access.csv b/addons/bus/security/ir.model.access.csv index 8a679b74f81..4a626a0eb5a 100644 --- a/addons/bus/security/ir.model.access.csv +++ b/addons/bus/security/ir.model.access.csv @@ -1,2 +1,4 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_bus_bus,bus.bus public,model_bus_bus,,0,0,0,0 \ No newline at end of file +access_bus_bus,bus.bus public,model_bus_bus,,0,0,0,0 +access_bus_presence,bus.presence,model_bus_presence,base.group_user,1,1,1,1 +access_bus_presence_portal,bus.presence,model_bus_presence,base.group_portal,1,1,1,1 diff --git a/addons/bus/static/src/js/bus.js b/addons/bus/static/src/js/bus.js index b0756132012..3c828668308 100644 --- a/addons/bus/static/src/js/bus.js +++ b/addons/bus/static/src/js/bus.js @@ -9,12 +9,21 @@ bus.ERROR_DELAY = 10000; bus.Bus = Widget.extend({ init: function(){ + var self = this; this._super(); this.options = {}; this.activated = false; this.channels = []; this.last = 0; this.stop = false; + + // bus presence + this.set("window_focus", true); + this.on("change:window_focus", self, function(e) { + self.options.im_presence = self.get("window_focus"); + }); + $(window).on("focus", _.bind(this.window_focus, this)); + $(window).on("blur", _.bind(this.window_blur, this)); }, start_polling: function(){ if(!this.activated){ @@ -57,6 +66,13 @@ bus.Bus = Widget.extend({ delete_channel: function(channel){ this.channels = _.without(this.channels, channel); }, + // bus presence : window focus/unfocus + window_focus: function() { + this.set("window_focus", true); + }, + window_blur: function() { + this.set("window_focus", false); + }, }); // singleton diff --git a/addons/im_chat/controllers/main.py b/addons/im_chat/controllers/main.py deleted file mode 100644 index 35ede172690..00000000000 --- a/addons/im_chat/controllers/main.py +++ /dev/null @@ -1,63 +0,0 @@ -# -*- coding: utf-8 -* -import base64 -import openerp - -from openerp import SUPERUSER_ID -from openerp.http import request - - -class ChatController(openerp.addons.bus.controllers.main.BusController): - - def _default_request_uid(self): - """ For Anonymous people, they receive the access right of SUPERUSER_ID since they have NO access (auth=none) - !!! Each time a method from this controller is call, there is a check if the user (who can be anonymous and Sudo access) - can access to the ressource, thanks to 'is_in_session', (for instance). - """ - return request.session.uid and request.session.uid or SUPERUSER_ID - - # -------------------------- - # Extends BUS Controller Poll - # -------------------------- - def _poll(self, dbname, channels, last, options): - if request.session.uid: - request.env['im_chat.presence'].update(options.get('im_presence')) - # channel to receive message - channels.append((request.db, 'im_chat.session', request.session.uid)) - ## For performance issue, the real time status notification is disabled. This means a change of status are still braoadcasted - ## but not received by anyone. Otherwise, all listening user restart their longpolling at the same time and cause a 'ConnectionPool Full Error' - ## since there is not enought cursors for everyone. Now, when a user open his list of users, an RPC call is made to update his user status list. - ##channels.append((request.db,'im_chat.presence')) - return super(ChatController, self)._poll(dbname, channels, last, options) - - # -------------------------- - # Anonymous routes (Common Methods) - # -------------------------- - @openerp.http.route('/im_chat/post', type="json", auth="none") - def post(self, uuid, message_content, message_type): - request_uid = self._default_request_uid() - return request.env["im_chat.message"].sudo(request_uid).send_message(request.session.uid, uuid, message_content, message_type) - - @openerp.http.route(['/im_chat/image//', '/im_chat/image//'], type='http', auth="none") - def image(self, uuid, user_id): - request_uid = self._default_request_uid() - # anonymous will have a user_id = False, interpreted by string - if isinstance(user_id, (basestring, unicode)): - user_id = False - # get image for the people in the channel - image_b64 = request.env['im_chat.session'].sudo(request_uid).session_user_image(uuid, user_id) - image_data = base64.b64decode(image_b64) - headers = [('Content-Type', 'image/png')] - headers.append(('Content-Length', len(image_data))) - return request.make_response(image_data, headers) - - @openerp.http.route(['/im_chat/history'], type="json", auth="none") - def history(self, uuid, last_id=False, limit=20): - request_uid = self._default_request_uid() - return request.env["im_chat.message"].sudo(request_uid).fetch_message(uuid, last_id, limit) - - # -------------------------- - # User routes (Only Logged User Methods) - # -------------------------- - @openerp.http.route('/im_chat/init', type="json", auth="user") - def init(self): - return request.env['im_chat.message'].get_init_notifications() diff --git a/addons/im_chat/data/im_chat_session_data.xml b/addons/im_chat/data/im_chat_session_data.xml deleted file mode 100644 index bd77ecbf8da..00000000000 --- a/addons/im_chat/data/im_chat_session_data.xml +++ /dev/null @@ -1,158 +0,0 @@ - - - - - - - :) - <img src='/im_chat/static/src/img/smile.png'/> - Emoji Smile - - - - :( - <img src='/im_chat/static/src/img/sad.png'/> - Emoji Sad - - - - :D - <img src='/im_chat/static/src/img/laugh.png'/> - Emoji Laugh - - - - ;) - <img src='/im_chat/static/src/img/wink.png'/> - Emoji Wink - - - - O_0 - <img src='/im_chat/static/src/img/stuck_out_tongue.png'/> - Emoji Stuck Out Tongue Winking Eye - - - - :@ - <img src='/im_chat/static/src/img/scream.png'/> - Emoji Scream - - - - :| - <img src='/im_chat/static/src/img/neutral.png'/> - Emoji Speechless/Neutral face - - - - :p - <img src='/im_chat/static/src/img/yum.png'/> - Emoji Cheeky - - - - :/ - <img src='/im_chat/static/src/img/confused.png'/> - Emoji Confused - - - - :O - <img src='/im_chat/static/src/img/open_mouth.png'/> - Emoji Open Mouth - - - - &lt;3 - <img src='/im_chat/static/src/img/heart.png'/> - Emoji Heart - - - - - :+1 - <img src='/im_chat/static/src/img/thumbsup.png'/> - Emoji Thumbs Up - - - - :-1 - <img src='/im_chat/static/src/img/thumbsdown.png'/> - Emoji Thumbs Down - - - - :kitten - <img src='/im_chat/static/src/img/cat.png'/> - Emoji Kitten - - - - :pinky - <img src='/im_chat/static/src/img/pinky.png'/> - Emoji Pinky - - - - :musti - <img src='/im_chat/static/src/img/musti.png'/> - Emoji Musti - - - - :turban - <img src='/im_chat/static/src/img/man_with_turban.png'/> - Emoji Man With Turban - - - - :troll - <img src='/im_chat/static/src/img/trollface.png'/> - Emoji Troll Face - - - - :boar - <img src='/im_chat/static/src/img/boar.png'/> - Emoji Boar - - - - :poop - <img src='/im_chat/static/src/img/poop.png'/> - Emoji Poop - - - - :cookie - <img src='/im_chat/static/src/img/cookie.png'/> - Emoji Cookie - - - - :postal_horn - <img src='/im_chat/static/src/img/postal_horn.png'/> - Emoji Postal Horn - - - - :ghost - <img src='/im_chat/static/src/img/ghost.png'/> - Emoji Ghost - - - - :beer - <img src='/im_chat/static/src/img/beer.png'/> - Emoji Beer - - - - :man - <img src='/im_chat/static/src/img/man.png'/> - Emoji Man - - - - diff --git a/addons/im_chat/models/res_users.py b/addons/im_chat/models/res_users.py deleted file mode 100644 index 5e9d664b6a8..00000000000 --- a/addons/im_chat/models/res_users.py +++ /dev/null @@ -1,78 +0,0 @@ -# -*- coding: utf-8 -*- -from openerp import api, fields, models - - -class ResUsers(models.Model): - - _inherit = "res.users" - - im_status = fields.Char('IM Status', compute='_compute_im_status') - - @api.multi - def _compute_im_status(self): - """ Compute the im_status of the users """ - res = dict((uid, 'offline') for uid in self.ids) - presences = self.env['im_chat.presence'].search([('user_id', 'in', self.ids)]) - for presence in presences: - res[presence.user_id.id] = presence.status - for user in self: - user.im_status = res.get(user.id) - - @api.model - def im_search(self, name, limit=20): - """ Search users with a name and return its id, name and im_status. - Note : the user must be logged - :param name : the user name to search - :param limit : the limit of result to return - """ - result = [] - # find the employee group - group_employee = self.env['ir.model.data'].get_object_reference('base', 'group_user')[1] - - where_clause_base = " U.active = 't' " - query_params = () - if name: - where_clause_base += " AND P.name ILIKE %s " - query_params = query_params + ('%'+name+'%',) - - # first query to find online employee - self._cr.execute('''SELECT U.id as id, P.name as name, COALESCE(S.status, 'offline') as im_status - FROM im_chat_presence S - JOIN res_users U ON S.user_id = U.id - JOIN res_partner P ON P.id = U.partner_id - WHERE '''+where_clause_base+''' - AND U.id != %s - AND EXISTS (SELECT 1 FROM res_groups_users_rel G WHERE G.gid = %s AND G.uid = U.id) - AND S.status = 'online' - ORDER BY P.name - LIMIT %s - ''', query_params + (self._uid, group_employee, limit)) - result = result + self._cr.dictfetchall() - - # second query to find other online people - if len(result) < limit: - self._cr.execute('''SELECT U.id as id, P.name as name, COALESCE(S.status, 'offline') as im_status - FROM im_chat_presence S - JOIN res_users U ON S.user_id = U.id - JOIN res_partner P ON P.id = U.partner_id - WHERE '''+where_clause_base+''' - AND U.id NOT IN %s - AND S.status = 'online' - ORDER BY P.name - LIMIT %s - ''', query_params + (tuple([u["id"] for u in result]) + (self._uid,), limit-len(result))) - result = result + self._cr.dictfetchall() - - # third query to find all other people - if len(result) < limit: - self._cr.execute('''SELECT U.id as id, P.name as name, COALESCE(S.status, 'offline') as im_status - FROM res_users U - LEFT JOIN im_chat_presence S ON S.user_id = U.id - LEFT JOIN res_partner P ON P.id = U.partner_id - WHERE '''+where_clause_base+''' - AND U.id NOT IN %s - ORDER BY P.name - LIMIT %s - ''', query_params + (tuple([u["id"] for u in result]) + (self._uid,), limit-len(result))) - result = result + self._cr.dictfetchall() - return result diff --git a/addons/im_chat/static/src/audio/purr.mp3 b/addons/im_chat/static/src/audio/purr.mp3 deleted file mode 100644 index 849d303c3b1..00000000000 Binary files a/addons/im_chat/static/src/audio/purr.mp3 and /dev/null differ diff --git a/addons/im_chat/static/src/audio/purr.ogg b/addons/im_chat/static/src/audio/purr.ogg deleted file mode 100644 index 2ddc77537f1..00000000000 Binary files a/addons/im_chat/static/src/audio/purr.ogg and /dev/null differ diff --git a/addons/im_chat/static/src/audio/ting.mp3 b/addons/im_chat/static/src/audio/ting.mp3 deleted file mode 100644 index fd29bf599d6..00000000000 Binary files a/addons/im_chat/static/src/audio/ting.mp3 and /dev/null differ diff --git a/addons/im_chat/static/src/audio/ting.ogg b/addons/im_chat/static/src/audio/ting.ogg deleted file mode 100644 index cc8c0f01eff..00000000000 Binary files a/addons/im_chat/static/src/audio/ting.ogg and /dev/null differ diff --git a/addons/im_chat/static/src/css/im_chat_backend.css b/addons/im_chat/static/src/css/im_chat_backend.css deleted file mode 100644 index d01577eb56a..00000000000 --- a/addons/im_chat/static/src/css/im_chat_backend.css +++ /dev/null @@ -1,114 +0,0 @@ -.oe_im { - position: fixed; - background-color: #eaeaea; - width: 250px; - border-left: 1px solid #AEB9BD; - z-index: 500; - -webkit-box-shadow: -2px 0px 5px 0px rgba(50, 50, 50, 0.25); - -moz-box-shadow: -2px 0px 5px 0px rgba(50, 50, 50, 0.25); - box-shadow: -2px 0px 5px 0px rgba(50, 50, 50, 0.25); -} - -@media print { - .oe_im, .oe_im_chatview { - visibility: hidden; - } -} -/* button */ -.oe_systray #oe_topbar_imbutton_icon { - color: white; -} -.oe_topbar_item.oe_topbar_imbutton .fa { - position: relative; - top: -8px; -} - -/* search stuff */ -.oe_im_frame_header { - position: relative; - padding: 16px; -} -.oe_im_frame_header .oe_im_searchbox { - width: 200px; - padding: 1px 21px 1px 19px; - font-size: 13px; - border: none; - border-bottom: 1px solid #c6c6c6; - background: none; - margin-left: 20px; - height: 38px; -} -.oe_im_frame_header .oe_im_searchbox:focus { - outline: none; -} -.oe_im_frame_header .oe_im_search_icon { - position: absolute; - color: #888; - top: 28px; - left: 8px; - font-size: 16px; - font-weight: 300 !important; -} -.oe_im_frame_header .oe_im_search_clear { - display: none; - position: absolute; - right: 11px; - top: 4px; - font-size: 26px; - color: #b6b6b6; - cursor: pointer; -} -.oe_im_frame_header .oe_im_search_clear:hover { - color: #888; -} - -/* users */ - -.oe_im_users { - padding-bottom: 38px; - height: 100%; - overflow-y: scroll; -} -.oe_im_user { - position: relative; - padding: 2px 8px; - cursor: pointer; - font-size: 13px; - margin-bottom: 10px; -} -.oe_im_user:hover { - background: lightGrey; -} -.oe_im_user_clip { - display: inline-block; - width: 26px; - height: 26px; - margin-right: 4px; - -moz-box-shadow: 0 0 2px 1px rgba(0,0,0,0.25); - -webkit-box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.25); - box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.25); -} -.oe_im_user_avatar { - width: 26px; - height: auto; -} -.oe_im_user_name { - width: 162px; - line-height: 26px; - padding-right: 15px; - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - position: relative; -} - -.oe_im_user_online { - display: none; - position: absolute; - top: 9.5px; - right: 11px; - width: 11px; - height: 11px; - vertical-align: middle; - border: 0; -} diff --git a/addons/im_chat/static/src/css/im_chat_common.css b/addons/im_chat/static/src/css/im_chat_common.css deleted file mode 100644 index 1ad2981eb41..00000000000 --- a/addons/im_chat/static/src/css/im_chat_common.css +++ /dev/null @@ -1,259 +0,0 @@ - - - -.openerp_style { /* base style of openerp */ - font-family: "Lucida Grande", Helvetica, Verdana, Arial, sans-serif; - color: #4c4c4c; - font-size: 13px; - background: white; -} - -/* button */ - -.oe_chat_button { - position: fixed; - bottom: 0px; - right: 6px; - display: inline-block; - min-width: 100px; - background-color: rgba(60, 60, 60, 0.6); - font-family: 'Lucida Grande', 'Lucida Sans Unicode', Arial, Verdana, sans-serif; - font-size: 14px; - font-weight: bold; - padding: 10px; - color: white; - text-shadow: rgb(59, 76, 88) 1px 1px 0px; - border: 1px solid rgb(80, 80, 80); - border-bottom: 0px; - border-top-left-radius: 5px; - border-top-right-radius: 5px; - cursor: pointer; - z-index:500; -} - -/* conversations */ - -.oe_im_chatview { - position: fixed; - overflow: hidden; - margin-bottom: 5px; - margin-right: 6px; - background-color: #fff; - border: 1px solid #DEDEDE; - width: 240px; - z-index:500; -} -@media screen and (max-width:360px) { - .oe_im_chatview { - width: 100%; - margin-right: 0; - } - .oe_im_chatview .oe_im_chatview_content { - width: 100% !important; - } - .oe_im_chatview .oe_im_chatview_input { - width: 100% !important; - } -} -.oe_im_chatview .oe_im_chatview_disconnected { - display:none; - z-index: 100; - width: 100%; - background: #E8EBEF; - padding: 5px; - font-size: 11px; - color: #999; - line-height: 14px; - height: 28px; - overflow: hidden; -} -.oe_im_chatview.oe_im_chatview_disconnected_status .oe_im_chatview_disconnected { - display: block; -} - - - -/* ####### Chat View : Header ####### */ -.oe_im_chatview_header { - padding: 3px 6px 2px; - background: #DEDEDE; - background: -moz-linear-gradient(#FCFCFC, #DEDEDE); - background: -webkit-gradient(linear, left top, left bottom, from(#FCFCFC), to(#DEDEDE)); - border-radius: 3px 3px 0 0; - -moz-border-radius: 3px 3px 0 0; - -webkit-border-radius: 3px 3px 0 0; - border-bottom: 1px solid #AEB9BD; - cursor: pointer; -} - -.oe_im_chatview_header .header_name{ - max-width: 75%; - word-wrap: break-word; - display: inline-block; - height: 15px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.oe_im_chatview_header .right_buttons { - float: right; -} -.oe_im_chatview_header .right_buttons > * { - display: inline-block; - border: 0; - cursor: pointer; - color: gray ; - background: transparent; - font-size: 16px; - line-height: 16px; - margin-left: 5px; - -webkit-appearance: none; -} - -.oe_im_chatview_header .right_buttons .dropdown-menu{ - right: 0; - left: auto; -} -.oe_im_chatview_header .right_buttons .dropdown-menu > li{ - display: block; - padding: 3px 20px; - clear: both; - font-weight: normal; - line-height: 1.428571429; - color: #4c4c4c; - white-space: nowrap; -} - - - -.oe_im_chatview .oe_im_chatview_content { - overflow: auto; - height: 287px; - width: 240px; -} -.oe_im_chatview.oe_im_chatview_disconnected_status .oe_im_chatview_content { - height: 249px; -} -.oe_im_chatview .oe_im_chatview_footer { - position: relative; - padding: 3px; - border-top: 1px solid #AEB9BD; - background: #DEDEDE; - background: -moz-linear-gradient(#FCFCFC, #DEDEDE); - background: -webkit-gradient(linear, left top, left bottom, from(#FCFCFC), to(#DEDEDE)); - -moz-border-radius: 0 0 3px 3px; - -webkit-border-radius: 0 0 3px 3px; - border-radius: 0 0 3px 3px; -} -.oe_im_chatview .oe_im_chatview_input { - width: 222px; - font-family: Lato, Helvetica, sans-serif; - font-size: 13px; - color: #333; - padding: 1px 5px; - border: 1px solid #AEB9BD; - -moz-border-radius: 3px; - -webkit-border-radius: 3px; - border-radius: 3px; - -moz-box-shadow: inset 0 1px 4px rgba(0,0,0,0.2); - -webkit-box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.2); - box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.2); -} -.oe_im_chatview .oe_im_chatview_message_bubble { - background: white; - position: relative; - min-height: 32px; - padding: 3px; - margin: 3px; - -moz-border-radius: 3px; - -webkit-border-radius: 3px; - border-radius: 3px; -} - -/* Smiley size */ -.oe_im_chatview .oe_im_chatview_bubble_item img{ - width: 18px; -} - -.oe_im_chatview .oe_im_chatview_technical_bubble { - background: #D8D8D8; - position: relative; - min-height: 32px; - padding: 3px; - margin: 3px; - -moz-border-radius: 3px; - -webkit-border-radius: 3px; - border-radius: 3px; - - font-style:italic; -} - -.oe_im_chatview .oe_im_chatview_clip { - position: relative; - float: left; - width: 26px; - height: 26px; - margin-right: 4px; - -moz-box-shadow: 0 0 2px 1px rgba(0,0,0,0.25); - -webkit-box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.25); - box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.25); -} -.oe_im_chatview .oe_im_chatview_avatar { - float: left; - width: 26px; - height: auto; - clip: rect(0, 26px, 26px, 0); - max-width: 100%; - width: auto 9; - height: auto; - vertical-align: middle; - border: 0; - -ms-interpolation-mode: bicubic; -} -.oe_im_chatview .oe_im_chatview_time { - position: absolute; - right: 0px; - top: 0px; - margin: 3px; - text-align: right; - line-height: 13px; - font-size: 11px; - color: #999; - width: 60px; - overflow: hidden; -} -.oe_im_chatview .oe_im_chatview_from { - margin: 0 0 2px 0; - line-height: 14px; - font-weight: bold; - font-size: 12px; - width: 140px; - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - color: #3A87AD; -} -.oe_im_chatview .oe_im_chatview_bubble_list { -} -.oe_im_chatview .oe_im_chatview_bubble_item { - margin: 0 0 2px 30px; - line-height: 14px; - word-wrap: break-word; -} - -.oe_im_chatview_online { - margin-top: -4px; - width: 11px; - height: 11px; -} - -.oe_im_chatview .oe_im_chatview_date_separator { - background: transparent; - position: relative; - padding: 3px; - color: #aaa; - margin: 3px; - text-align: right; - font-size: 12px; -} \ No newline at end of file diff --git a/addons/im_chat/static/src/img/avatar.jpg b/addons/im_chat/static/src/img/avatar.jpg deleted file mode 100644 index 7168794022e..00000000000 Binary files a/addons/im_chat/static/src/img/avatar.jpg and /dev/null differ diff --git a/addons/im_chat/static/src/img/beer.png b/addons/im_chat/static/src/img/beer.png deleted file mode 100644 index cd78bed7440..00000000000 Binary files a/addons/im_chat/static/src/img/beer.png and /dev/null differ diff --git a/addons/im_chat/static/src/img/boar.png b/addons/im_chat/static/src/img/boar.png deleted file mode 100644 index 3ef50325016..00000000000 Binary files a/addons/im_chat/static/src/img/boar.png and /dev/null differ diff --git a/addons/im_chat/static/src/img/cat.png b/addons/im_chat/static/src/img/cat.png deleted file mode 100644 index dbf1b0276ab..00000000000 Binary files a/addons/im_chat/static/src/img/cat.png and /dev/null differ diff --git a/addons/im_chat/static/src/img/confused.png b/addons/im_chat/static/src/img/confused.png deleted file mode 100644 index 8dc494db083..00000000000 Binary files a/addons/im_chat/static/src/img/confused.png and /dev/null differ diff --git a/addons/im_chat/static/src/img/cookie.png b/addons/im_chat/static/src/img/cookie.png deleted file mode 100644 index 177d0f9ad86..00000000000 Binary files a/addons/im_chat/static/src/img/cookie.png and /dev/null differ diff --git a/addons/im_chat/static/src/img/ghost.png b/addons/im_chat/static/src/img/ghost.png deleted file mode 100644 index 671dd0c9e2e..00000000000 Binary files a/addons/im_chat/static/src/img/ghost.png and /dev/null differ diff --git a/addons/im_chat/static/src/img/green.png b/addons/im_chat/static/src/img/green.png deleted file mode 100644 index 0700a5ef71b..00000000000 Binary files a/addons/im_chat/static/src/img/green.png and /dev/null differ diff --git a/addons/im_chat/static/src/img/heart.png b/addons/im_chat/static/src/img/heart.png deleted file mode 100644 index 7d7790ce4df..00000000000 Binary files a/addons/im_chat/static/src/img/heart.png and /dev/null differ diff --git a/addons/im_chat/static/src/img/laugh.png b/addons/im_chat/static/src/img/laugh.png deleted file mode 100644 index 11c91eb22e6..00000000000 Binary files a/addons/im_chat/static/src/img/laugh.png and /dev/null differ diff --git a/addons/im_chat/static/src/img/man.png b/addons/im_chat/static/src/img/man.png deleted file mode 100644 index c4a4217f4d4..00000000000 Binary files a/addons/im_chat/static/src/img/man.png and /dev/null differ diff --git a/addons/im_chat/static/src/img/man_with_turban.png b/addons/im_chat/static/src/img/man_with_turban.png deleted file mode 100644 index 8a0bda1ca77..00000000000 Binary files a/addons/im_chat/static/src/img/man_with_turban.png and /dev/null differ diff --git a/addons/im_chat/static/src/img/musti.png b/addons/im_chat/static/src/img/musti.png deleted file mode 100644 index f6843ddf95c..00000000000 Binary files a/addons/im_chat/static/src/img/musti.png and /dev/null differ diff --git a/addons/im_chat/static/src/img/neutral.png b/addons/im_chat/static/src/img/neutral.png deleted file mode 100644 index 682a1ba066d..00000000000 Binary files a/addons/im_chat/static/src/img/neutral.png and /dev/null differ diff --git a/addons/im_chat/static/src/img/open_mouth.png b/addons/im_chat/static/src/img/open_mouth.png deleted file mode 100644 index daf914274a6..00000000000 Binary files a/addons/im_chat/static/src/img/open_mouth.png and /dev/null differ diff --git a/addons/im_chat/static/src/img/pinky.png b/addons/im_chat/static/src/img/pinky.png deleted file mode 100644 index 4466946601c..00000000000 Binary files a/addons/im_chat/static/src/img/pinky.png and /dev/null differ diff --git a/addons/im_chat/static/src/img/poop.png b/addons/im_chat/static/src/img/poop.png deleted file mode 100644 index 73a4dc84008..00000000000 Binary files a/addons/im_chat/static/src/img/poop.png and /dev/null differ diff --git a/addons/im_chat/static/src/img/postal_horn.png b/addons/im_chat/static/src/img/postal_horn.png deleted file mode 100644 index 5b7ed85d4fc..00000000000 Binary files a/addons/im_chat/static/src/img/postal_horn.png and /dev/null differ diff --git a/addons/im_chat/static/src/img/sad.png b/addons/im_chat/static/src/img/sad.png deleted file mode 100644 index 82552008719..00000000000 Binary files a/addons/im_chat/static/src/img/sad.png and /dev/null differ diff --git a/addons/im_chat/static/src/img/scream.png b/addons/im_chat/static/src/img/scream.png deleted file mode 100644 index 76bfc6b8a65..00000000000 Binary files a/addons/im_chat/static/src/img/scream.png and /dev/null differ diff --git a/addons/im_chat/static/src/img/smile.png b/addons/im_chat/static/src/img/smile.png deleted file mode 100644 index 81a83968996..00000000000 Binary files a/addons/im_chat/static/src/img/smile.png and /dev/null differ diff --git a/addons/im_chat/static/src/img/stuck_out_tongue.png b/addons/im_chat/static/src/img/stuck_out_tongue.png deleted file mode 100644 index ea9aeb99e29..00000000000 Binary files a/addons/im_chat/static/src/img/stuck_out_tongue.png and /dev/null differ diff --git a/addons/im_chat/static/src/img/thumbsdown.png b/addons/im_chat/static/src/img/thumbsdown.png deleted file mode 100644 index 74a5005c893..00000000000 Binary files a/addons/im_chat/static/src/img/thumbsdown.png and /dev/null differ diff --git a/addons/im_chat/static/src/img/thumbsup.png b/addons/im_chat/static/src/img/thumbsup.png deleted file mode 100644 index 116388fc3e2..00000000000 Binary files a/addons/im_chat/static/src/img/thumbsup.png and /dev/null differ diff --git a/addons/im_chat/static/src/img/trollface.png b/addons/im_chat/static/src/img/trollface.png deleted file mode 100644 index eb222e2ad69..00000000000 Binary files a/addons/im_chat/static/src/img/trollface.png and /dev/null differ diff --git a/addons/im_chat/static/src/img/wink.png b/addons/im_chat/static/src/img/wink.png deleted file mode 100644 index 756766dd3e9..00000000000 Binary files a/addons/im_chat/static/src/img/wink.png and /dev/null differ diff --git a/addons/im_chat/static/src/img/yellow.png b/addons/im_chat/static/src/img/yellow.png deleted file mode 100644 index 726570e518f..00000000000 Binary files a/addons/im_chat/static/src/img/yellow.png and /dev/null differ diff --git a/addons/im_chat/static/src/img/yum.png b/addons/im_chat/static/src/img/yum.png deleted file mode 100644 index fc39637ecd8..00000000000 Binary files a/addons/im_chat/static/src/img/yum.png and /dev/null differ diff --git a/addons/im_chat/static/src/js/im_chat_backend.js b/addons/im_chat/static/src/js/im_chat_backend.js deleted file mode 100644 index cd1cc00fdf2..00000000000 --- a/addons/im_chat/static/src/js/im_chat_backend.js +++ /dev/null @@ -1,301 +0,0 @@ -odoo.define('im_chat.im_chat', function (require) { -"use strict"; - -// to do: make this work in website - -var bus = require('bus.bus'); -var core = require('web.core'); -var data = require('web.data'); -var Model = require('web.DataModel'); -var session = require('web.session'); -var SystrayMenu = require('web.SystrayMenu'); -var utils = require('web.utils'); -var web_client = require('web.web_client'); -var Widget = require('web.Widget'); -var im_chat_common = require('im_chat.im_chat_common'); - -var _t = core._t; -var QWeb = core.qweb; - -var USERS_LIMIT = 20; - - -// ########## CONVERSATION extentions ############### - -im_chat_common.ConversationManager.include({ - _message_receive: function(message){ - var self = this; - var session_id = message.to_id[0]; - var conv = this.sessions[message.to_id[1]]; - if(!conv){ - // fetch the session, and init it with the message - var def_session = new Model("im_chat.session").call("session_info", [], {"ids" : [session_id]}).then(function(session){ - conv = self.session_apply(session, {'force_open': true}); - conv.message_receive(message); - }); - }else{ - this._super(message); - } - }, -}); - -im_chat_common.Conversation.include({ - // user actions - prepare_action_menu: function(){ - this._super(); - this._add_action(_t('Shortcuts'), 'im_chat_option_shortcut', 'fa fa-info-circle', this.action_shorcode); - this._add_action(_t('Quit discussion'), 'im_chat_option_quit', 'fa fa-minus-square', this.action_quit_session); - }, - action_shorcode: function(e){ - return web_client.action_manager.do_action({ - type: 'ir.actions.act_window', - name : _t('Shortcode'), - res_model: 'im_chat.shortcode', - view_mode: 'tree,form', - view_type: 'tree', - views: [[false, 'list'], [false, 'form']], - target: "new", - limit: 80, - flags: { - action_buttons: true, - pager: true, - } - }); - }, - action_quit_session: function(e){ - var self = this; - var Session = new Model("im_chat.session"); - return Session.call("quit_user", [this.get("session").uuid]).then(function(res) { - if(! res){ - self.do_warn(_t("Warning"), _t("You are only 2 identified users. Just close the conversation to leave.")); - } - }); - }, - // session - session_update_state: function(state){ - var self = this; - var args = arguments; - var super_call = this._super; - // broadcast the state changing - return new Model("im_chat.session").call("update_state", [], {"uuid" : this.get("session").uuid, "state" : state}).then(function(){ - super_call.apply(self, args); - }); - }, - // window title - window_title_change: function() { - this.super(); - var title = undefined; - if (this.get("waiting_messages") !== 0) { - title = _.str.sprintf(_t("%d Messages"), this.get("waiting_messages")) - } - web_client.set_title_part("im_messages", title); - }, - // TODO : change this way - add_user: function(user){ - return new Model("im_chat.session").call("add_user", [this.get("session").uuid , user.id]); - }, -}); - - - -// ###### BACKEND : contact panel, top menu button ######### - -var UserWidget = Widget.extend({ - template: "im_chat.UserWidget", - events: { - "click": "activate_user", - }, - init: function(parent, user) { - this._super(parent); - this.set("id", user.id); - this.set("name", user.name); - this.set("im_status", user.im_status); - this.set("image_url", user.image_url); - }, - start: function() { - this.$el.data("user", {id:this.get("id"), name:this.get("name")}); - this.$el.draggable({helper: "clone"}); - this.on("change:im_status", this, this.update_status); - this.update_status(); - }, - update_status: function(){ - this.$(".oe_im_user_online").toggle(this.get('im_status') !== 'offline'); - var img_src = (this.get('im_status') == 'away' ? '/im_chat/static/src/img/yellow.png' : '/im_chat/static/src/img/green.png'); - this.$(".oe_im_user_online").attr('src', img_src); - }, - activate_user: function() { - this.trigger("user_clicked", this.get("id")); - }, -}); - -var InstantMessaging = Widget.extend({ - template: "im_chat.InstantMessaging", - events: { - "keydown .oe_im_searchbox": "input_change", - "keyup .oe_im_searchbox": "input_change", - "change .oe_im_searchbox": "input_change", - }, - init: function(parent) { - this._super(parent); - this.shown = false; - this.set("right_offset", 0); - this.set("current_search", ""); - this.users = []; - this.widgets = {}; - - // listen bus - this.bus = bus.bus; - this.bus.on("notification", this, this.on_notification); - }, - start: function() { - var self = this; - // ui - this.conv_manager = new im_chat_common.ConversationManager(this); - this.on("change:right_offset", this.conv_manager, _.bind(function() { - this.conv_manager.set("right_offset", this.get("right_offset")); - }, this)); - $(window).scroll(_.bind(this.position_compute, this)); - $(window).resize(_.bind(this.position_compute, this)); - this.$el.css("right", -this.$el.outerWidth()); - this.position_compute(); - - // business - this.on("change:current_search", this, this.user_search); - - // add a drag & drop listener - self.conv_manager.on("im_session_activated", self, function(conv) { - conv.$el.droppable({ - drop: function(event, ui) { - conv.add_user(ui.draggable.data("user")); - } - }); - }); - - // fetch the unread message and the recent activity (e.i. to re-init in case of refreshing page) - return session.rpc("/im_chat/init", {}).then(function(notifications) { - _.each(notifications, function(notif){ - self.conv_manager.on_notification(notif, {'load_history': true}); - }); - // start polling - self.bus.start_polling(); - }); - }, - on_notification: function(notification){ - var channel = notification[0]; - var message = notification[1]; - // user status notification - if(channel[1] === 'im_chat.presence'){ - if(message.im_status){ - this.user_update_status([message]); - } - } - }, - // ui - position_compute: function() { - var top = 48; - this.$el.css("top", top); - this.$el.css("bottom", 0); - }, - input_change: function() { - this.set("current_search", this.$(".oe_im_searchbox").val()); - }, - switch_display: function() { - this.position_compute(); - var fct = _.bind(function(place) { - this.set("right_offset", place + this.$el.outerWidth()); - this.$(".oe_im_searchbox").focus(); - }, this); - var opt = { - step: fct, - }; - if (this.shown) { - this.$el.animate({ - right: -this.$el.outerWidth(), - }, opt); - } else { - if (! bus.bus.activated) { - this.do_warn("Instant Messaging is not activated on this server. Try later.", ""); - return; - } - // update the list of user status when show the IM - this.user_search(); - this.$el.animate({ - right: 0, - }, opt); - } - this.shown = ! this.shown; - }, - // user methods - user_search: function(e) { - var self = this; - var user_model = new Model("res.users"); - return user_model.call('im_search', [this.get("current_search"), USERS_LIMIT]).then(function(result){ - self.$(".oe_im_input").val(""); - var old_widgets = self.widgets; - self.widgets = {}; - self.users = []; - _.each(result, function(user) { - user.image_url = session.url('/web/image', {model:'res.users', field: 'image_small', id: user.id}); - var widget = new UserWidget(self, user); - widget.appendTo(self.$(".oe_im_users")); - widget.on("user_clicked", self, self.user_clicked); - self.widgets[user.id] = widget; - self.users.push(user); - }); - _.each(old_widgets, function(w) { - w.destroy(); - }); - }); - }, - user_clicked: function(user_id) { - var self = this; - var sessions = new Model("im_chat.session"); - return sessions.call("session_get", [user_id]).then(function(session) { - self.conv_manager.session_apply(session, {'focus': true, 'force_open': true}); - }); - }, - user_update_status: function(users_list){ - var self = this; - _.each(users_list, function(el) { - if (self.widgets[el.id]) { - self.widgets[el.id].set("im_status", el.im_status); - } - }); - } -}); - - -var ImTopButton = Widget.extend({ - template:'im_chat.ImTopButton', - events: { - "click": "clicked", - }, - start: function() { - // Create the InstantMessaging widget and put it in the DOM - var im = new InstantMessaging(this); - web_client.im_messaging = im; - im.appendTo(web_client.$el); - // Bind the click action to the ImTopButton - this.on("clicked", im, im.switch_display); - return this._super(); - }, - clicked: function(ev) { - ev.preventDefault(); - this.trigger("clicked"); - }, -}); - -// Put the ImTopButton widget in the systray menu if the user is an employee -var Users = new Model('res.users'); -Users.call('has_group', ['base.group_user']).done(function(is_employee) { - if (is_employee) { - SystrayMenu.Items.push(ImTopButton); - } -}); - -return { - InstantMessaging: InstantMessaging, - UserWidget: UserWidget, -}; - -}); diff --git a/addons/im_chat/static/src/js/im_chat_common.js b/addons/im_chat/static/src/js/im_chat_common.js deleted file mode 100644 index 0aaa0e68d16..00000000000 --- a/addons/im_chat/static/src/js/im_chat_common.js +++ /dev/null @@ -1,433 +0,0 @@ -odoo.define('im_chat.im_chat_common', function (require) { -"use strict"; - -// to do: make this work in website - -var bus = require('bus.bus'); -var core = require('web.core'); -var session = require('web.session'); -var time = require('web.time'); -var Widget = require('web.Widget'); - -var _t = core._t; -var QWeb = core.qweb; - -var NBR_LIMIT_HISTORY = 20; - - -var ConversationManager = Widget.extend({ - init: function(parent, options) { - var self = this; - this._super(parent); - this.options = _.clone(options) || {}; - _.defaults(this.options, { - inputPlaceholder: _t("Say something..."), - defaultMessage: null, - defaultUsername: _t("Visitor"), - force_open: false, - load_history: true, - focus: false, - }); - // business - this.sessions = {}; - this.bus = bus.bus; - this.bus.on("notification", this, this.on_notification); - this.bus.options.im_presence = true; - - // ui - this.set("right_offset", 0); - this.set("bottom_offset", 0); - this.on("change:right_offset", this, this.calc_positions); - this.on("change:bottom_offset", this, this.calc_positions); - - this.set("window_focus", true); - this.on("change:window_focus", self, function(e) { - self.bus.options.im_presence = self.get("window_focus"); - }); - this.set("waiting_messages", 0); - this.on("change:waiting_messages", this, this.window_title_change); - $(window).on("focus", _.bind(this.window_focus, this)); - $(window).on("blur", _.bind(this.window_blur, this)); - this.window_title_change(); - }, - on_notification: function(notification, options) { - var self = this; - var channel = notification[0]; - var message = notification[1]; - var regex_uuid = new RegExp(/(\w{8}(-\w{4}){3}-\w{12}?)/g); - - // Concern im_chat : if the channel is the im_chat.session or im_chat.status, or a 'private' channel (aka the UUID of a session) - if((Array.isArray(channel) && (channel[1] === 'im_chat.session')) || (regex_uuid.test(channel))){ - // message to display in the chatview - if (message.type === "message" || message.type === "meta") { - self.message_receive(message); - } - // activate the received session - if(message.uuid){ - this.session_apply(message, options); - } - } - }, - - // window focus unfocus beep and title - window_focus: function() { - this.set("window_focus", true); - this.set("waiting_messages", 0); - }, - window_blur: function() { - this.set("window_focus", false); - }, - window_beep: function() { - if (typeof(Audio) === "undefined") { - return; - } - var audio = new Audio(); - var ext = audio.canPlayType("audio/ogg; codecs=vorbis") ? ".ogg" : ".mp3"; - audio.src = session.url("/im_chat/static/src/audio/ting") + ext; - audio.play(); - }, - window_title_change: function() { - var title = undefined; - if (this.get("waiting_messages") !== 0) { - title = _.str.sprintf(_t("%d Messages"), this.get("waiting_messages")); - this.window_beep(); - } - }, - // sessions and messages - session_apply: function(active_session, options){ - // options used by this function : force_open and focus (load_history can be usefull) - var self = this; - options = _.extend(_.clone(this.options), options || {}); - // force open - var session = _.clone(active_session); - if(options['force_open']){ - session.state = 'open'; - } - // create/get the conversation widget - var conv = this.sessions[session.uuid]; - if (! conv) { - if(session.state !== 'closed'){ - conv = new Conversation(this, this, session, options); - conv.appendTo($("body")); - conv.on("destroyed", this, _.bind(this.session_delete, this)); - this.sessions[session.uuid] = conv; - this.calc_positions(); - } - }else{ - conv.set("session", session); - } - // if force open, broadcast it - if(options['force_open'] && active_session.state !== 'open'){ - conv.session_update_state('open'); - } - // apply the focus - if (options['focus']){ - conv.focus(); - } - return conv; - }, - session_delete: function(uuid){ - delete this.sessions[uuid]; - this.calc_positions(); - }, - message_receive: function(message) { - var self = this; - var session_id = message.to_id[0]; - var uuid = message.to_id[1]; - var from_id = message['from_id'] ? message['from_id'][0] : false; - if (! this.get("window_focus") && from_id != this.get_current_uid()) { - this.set("waiting_messages", this.get("waiting_messages") + 1); - } - this._message_receive(message); - }, - _message_receive: function(message){ - var uuid = message.to_id[1]; - var conv = this.sessions[uuid]; - conv.message_receive(message); - }, - // others - get_current_uid: function(){ - return session && !_.isUndefined(session.uid) ? session.uid : false; - }, - calc_positions: function() { - var self = this; - var current = this.get("right_offset"); - _.each(this.sessions, function(s) { - s.set("bottom_position", self.get("bottom_offset")); - s.set("right_position", current); - current += s.$().outerWidth(true); - }); - }, - destroy: function() { - $(window).off("unload", this.unload); - $(window).off("focus", this.window_focus); - $(window).off("blur", this.window_blur); - return this._super(); - } -}); - - -var Conversation = Widget.extend({ - className: "openerp_style oe_im_chatview", - events: { - "keydown input": "keydown", - "click .oe_im_chatview_close": "click_close", - "click .oe_im_chatview_header": "click_header" - }, - init: function(parent, c_manager, session, options) { - this._super(parent); - this.c_manager = c_manager; - this.options = options || {}; - this.loading_history = true; - this.set("messages", []); - this.set("session", session); - this.set("right_position", 0); - this.set("bottom_position", 0); - this.set("pending", 0); - this.inputPlaceholder = this.options.defaultInputPlaceholder; - }, - start: function() { - var self = this; - self._super.apply(this, arguments); - self.prepare_action_menu(); - // ui - self.$().append(QWeb.render("im_chat.Conversation", {widget: self})); - self.on("change:right_position", self, self.calc_pos); - self.on("change:bottom_position", self, self.calc_pos); - self.on("change:pending", self, _.bind(function() { - if (self.get("pending") === 0) { - self.$(".nbr_messages").text(""); - } else { - self.$(".nbr_messages").text("(" + self.get("pending") + ")"); - } - }, self)); - self.full_height = self.$().height(); - self.calc_pos(); - // business - self.bind_action_menu(); - self.on("change:session", self, self.session_update); - self.on("change:messages", self, self.render_messages); - self.$('.oe_im_chatview_content').on('scroll',function(){ - if($(this).scrollTop() === 0){ - self.message_history(); - } - }); - if(self.options['load_history']){ // load history if asked - self.message_history(); - } - // prepare the header and the correct state - self.session_update(); - }, - // action menu - _add_action: function(label, style_class, icon_fa_class, callback){ - this.actions.push({ - 'label': label, - 'class': style_class, - 'icon_class': icon_fa_class, - 'callback': callback, - }); - }, - prepare_action_menu: function(){ - // override this method to add action with _add_action() - this.actions = []; - }, - bind_action_menu: function(){ - var self = this; - _.each(this.actions, function(action){ - if(action.callback){ - self.$('.button_option_group .' + action.class).on('click', _.bind(action.callback, self)); - } - }); - }, - // ui - show: function(){ - this.$().animate({ - height: this.full_height - }); - this.set("pending", 0); - }, - hide: function(){ - this.$().animate({ - height: this.$(".oe_im_chatview_header").outerHeight() - }); - }, - calc_pos: function() { - this.$().css("right", this.get("right_position")); - this.$().css("bottom", this.get("bottom_position")); - }, - // session - session_update_state: function(state){ - var session = this.get('session'); - session.state = state; - this.set('session', session); - }, - session_update: function(){ - // built the name - var names = []; - _.each(this.get("session").users, function(user){ - if( (session.uid !== user.id) && !(_.isUndefined(session.uid) && !user.id) ){ - names.push(user.name); - } - }); - this.$(".header_name").text(names.join(", ")); - this.$(".header_name").attr('title', names.join(", ")); - // update the fold state - if(this.get("session").state){ - if(this.get("session").state === 'closed'){ - this.destroy(); - }else{ - if(this.get("session").state === 'open'){ - this.show(); - }else{ - this.hide(); - } - } - } - }, - // messages - message_history: function(){ - var self = this; - if(this.loading_history){ - var data = {uuid: self.get("session").uuid, limit: NBR_LIMIT_HISTORY}; - var lastid = _.first(this.get("messages")) ? _.first(this.get("messages")).id : false; - if(lastid){ - data.last_id = lastid; - } - session.rpc("/im_chat/history", data).then(function(messages){ - if(messages){ - self.insert_messages(messages); - if(messages.length != NBR_LIMIT_HISTORY){ - self.loading_history = false; - } - }else{ - self.loading_history = false; - } - }); - } - }, - message_receive: function(message) { - if (this.get('session').state === 'open') { - this.set("pending", 0); - } else { - this.set("pending", this.get("pending") + 1); - } - this.insert_messages([message]); - this._go_bottom(); - }, - message_send: function(message, type) { - var self = this; - var send_it = function() { - return session.rpc("/im_chat/post", {uuid: self.get("session").uuid, message_type: type, message_content: message}); - }; - var tries = 0; - send_it().fail(function(error, e) { - e.preventDefault(); - tries += 1; - if (tries < 3) - return send_it(); - }); - }, - insert_messages: function(messages){ - var self = this; - // avoid duplicated messages - messages = _.filter(messages, function(m){ return !_.contains(_.pluck(self.get("messages"), 'id'), m.id) ; }); - // escape the message content and set the timezone - _.map(messages, function(m){ - if(!m.from_id){ - m.from_id = [false,self. get_anonymous_name()]; - } - m.create_date = moment(time.str_to_datetime(m.create_date)).format('YYYY-MM-DD HH:mm:ss'); - return m; - }); - this.set("messages", _.sortBy(this.get("messages").concat(messages), function(m){ return m.id; })); - }, - render_messages: function(){ - var self = this; - var res = {}; - var last_date_day, last_user_id = -1; - _.each(this.get("messages"), function(current){ - // add the url of the avatar for all users in the conversation - current.from_id[2] = session.url(_.str.sprintf("/im_chat/image/%s/%s", self.get('session').uuid, current.from_id[0])); - var date_day = current.create_date.split(" ")[0]; - if(date_day !== last_date_day){ - res[date_day] = []; - last_user_id = -1; - } - last_date_day = date_day; - if(current.type == "message"){ // traditionnal message - if(last_user_id === current.from_id[0]){ - _.last(res[date_day]).push(current); - }else{ - res[date_day].push([current]); - } - last_user_id = current.from_id[0]; - }else{ // meta message - res[date_day].push([current]); - last_user_id = -1; - } - }); - // render and set the content of the chatview - // TODO jem : when refactoring this, don't forget to pre-process date in this function before render quweb template - // since, moment will not be define in qweb on the website pages, because the helper (see csn) is in core.js and cannot be - // imported in the frontend. - this.$('.oe_im_chatview_content_bubbles').html($(QWeb.render("im_chat.Conversation_content", {"list": res}))); - this._go_bottom(); - }, - // utils and event - get_anonymous_name: function(){ - var name = this.options["defaultUsername"]; - _.each(this.get('session').users, function(u){ - if(!u.id){ - name = u.name; - } - }); - return name; - }, - keydown: function(e) { - if(e && e.which == 27) { - if(this.$el.prev().find('.oe_im_chatview_input').length > 0){ - this.$el.prev().find('.oe_im_chatview_input').focus(); - }else{ - this.$el.next().find('.oe_im_chatview_input').focus(); - } - e.stopPropagation(); - this.session_update_state('closed'); - } - if(e && e.which !== 13) { - return; - } - var mes = this.$("input").val(); - if (! mes.trim()) { - return; - } - this.$("input").val(""); - this.message_send(mes, "message"); - }, - _go_bottom: function() { - this.$(".oe_im_chatview_content").scrollTop(this.$(".oe_im_chatview_content").get(0).scrollHeight); - }, - focus: function() { - this.$(".oe_im_chatview_input").focus(); - }, - click_header: function(event){ - var classes = event.target.className.split(' '); - if(_.contains(classes, 'header_name') || _.contains(classes, 'oe_im_chatview_header')){ - this.session_update_state(); - } - }, - click_close: function(event) { - this.session_update_state('closed'); - }, - destroy: function() { - this.trigger("destroyed", this.get('session').uuid); - return this._super(); - } -}); - - -return { - Conversation: Conversation, - ConversationManager: ConversationManager, -}; - -}); \ No newline at end of file diff --git a/addons/im_chat/static/src/xml/im_chat.xml b/addons/im_chat/static/src/xml/im_chat.xml deleted file mode 100644 index eb479863b3e..00000000000 --- a/addons/im_chat/static/src/xml/im_chat.xml +++ /dev/null @@ -1,103 +0,0 @@ - - - -
- - - - -
- - - - -
-
×
-
-
-
-
-
-
- - - - -
- -
- - - - - - - - - - - - - - - -
-
- -
-
-
- -
-
-
-
-
-
- -
-
-
- -
-
-
-
-
-
- - -
- - - - - -
-
- -
-
- - -
-
-
-
-
- -
  • - - - -
  • -
    - diff --git a/addons/im_chat/views/im_chat_session_views.xml b/addons/im_chat/views/im_chat_session_views.xml deleted file mode 100644 index e281950d102..00000000000 --- a/addons/im_chat/views/im_chat_session_views.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - Chat Shortcode - im_chat.shortcode - tree,form - -

    - Click to define a new chat shortcode. -

    - A shortcode is a keyboard shortcut. For instance, you type #gm and it will be transformed into "Good Morning". -

    -
    -
    - - - im_chat.shortcode.tree - im_chat.shortcode - - - - - - - - - - - im_chat.shortcode.form - im_chat.shortcode - -
    - - - - - - - -
    -
    -
    - -
    -
    diff --git a/addons/mail/__openerp__.py b/addons/mail/__openerp__.py index e1f124c77c5..1e9149dc4fb 100644 --- a/addons/mail/__openerp__.py +++ b/addons/mail/__openerp__.py @@ -36,9 +36,11 @@ Main Features 'views/mail_followers_views.xml', 'views/mail_thread_views.xml', 'views/mail_channel_views.xml', + 'views/mail_shortcode_views.xml', 'views/res_config_views.xml', 'data/mail_data.xml', 'data/mail_channel_data.xml', + 'data/mail_shortcode.xml', 'security/mail_security.xml', 'security/ir.model.access.csv', 'views/mail_alias_views.xml', @@ -58,6 +60,7 @@ Main Features 'application': True, 'qweb': [ 'static/src/xml/mail.xml', + 'static/src/xml/mail_chat_common.xml', 'static/src/xml/mail_followers.xml', 'static/src/xml/announcement.xml', ], diff --git a/addons/mail/controllers/__init__.py b/addons/mail/controllers/__init__.py index 8ee9bae18d9..c59e37b30c8 100644 --- a/addons/mail/controllers/__init__.py +++ b/addons/mail/controllers/__init__.py @@ -1 +1,3 @@ +# -*- coding: utf-8 -* import main +import bus diff --git a/addons/mail/controllers/bus.py b/addons/mail/controllers/bus.py new file mode 100644 index 00000000000..35ede172690 --- /dev/null +++ b/addons/mail/controllers/bus.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -* +import base64 +import openerp + +from openerp import SUPERUSER_ID +from openerp.http import request + + +class ChatController(openerp.addons.bus.controllers.main.BusController): + + def _default_request_uid(self): + """ For Anonymous people, they receive the access right of SUPERUSER_ID since they have NO access (auth=none) + !!! Each time a method from this controller is call, there is a check if the user (who can be anonymous and Sudo access) + can access to the ressource, thanks to 'is_in_session', (for instance). + """ + return request.session.uid and request.session.uid or SUPERUSER_ID + + # -------------------------- + # Extends BUS Controller Poll + # -------------------------- + def _poll(self, dbname, channels, last, options): + if request.session.uid: + request.env['im_chat.presence'].update(options.get('im_presence')) + # channel to receive message + channels.append((request.db, 'im_chat.session', request.session.uid)) + ## For performance issue, the real time status notification is disabled. This means a change of status are still braoadcasted + ## but not received by anyone. Otherwise, all listening user restart their longpolling at the same time and cause a 'ConnectionPool Full Error' + ## since there is not enought cursors for everyone. Now, when a user open his list of users, an RPC call is made to update his user status list. + ##channels.append((request.db,'im_chat.presence')) + return super(ChatController, self)._poll(dbname, channels, last, options) + + # -------------------------- + # Anonymous routes (Common Methods) + # -------------------------- + @openerp.http.route('/im_chat/post', type="json", auth="none") + def post(self, uuid, message_content, message_type): + request_uid = self._default_request_uid() + return request.env["im_chat.message"].sudo(request_uid).send_message(request.session.uid, uuid, message_content, message_type) + + @openerp.http.route(['/im_chat/image//', '/im_chat/image//'], type='http', auth="none") + def image(self, uuid, user_id): + request_uid = self._default_request_uid() + # anonymous will have a user_id = False, interpreted by string + if isinstance(user_id, (basestring, unicode)): + user_id = False + # get image for the people in the channel + image_b64 = request.env['im_chat.session'].sudo(request_uid).session_user_image(uuid, user_id) + image_data = base64.b64decode(image_b64) + headers = [('Content-Type', 'image/png')] + headers.append(('Content-Length', len(image_data))) + return request.make_response(image_data, headers) + + @openerp.http.route(['/im_chat/history'], type="json", auth="none") + def history(self, uuid, last_id=False, limit=20): + request_uid = self._default_request_uid() + return request.env["im_chat.message"].sudo(request_uid).fetch_message(uuid, last_id, limit) + + # -------------------------- + # User routes (Only Logged User Methods) + # -------------------------- + @openerp.http.route('/im_chat/init', type="json", auth="user") + def init(self): + return request.env['im_chat.message'].get_init_notifications() diff --git a/addons/mail/data/mail_shortcode_data.xml b/addons/mail/data/mail_shortcode_data.xml new file mode 100644 index 00000000000..bd77ecbf8da --- /dev/null +++ b/addons/mail/data/mail_shortcode_data.xml @@ -0,0 +1,158 @@ + + + + + + + :) + <img src='/im_chat/static/src/img/smile.png'/> + Emoji Smile + + + + :( + <img src='/im_chat/static/src/img/sad.png'/> + Emoji Sad + + + + :D + <img src='/im_chat/static/src/img/laugh.png'/> + Emoji Laugh + + + + ;) + <img src='/im_chat/static/src/img/wink.png'/> + Emoji Wink + + + + O_0 + <img src='/im_chat/static/src/img/stuck_out_tongue.png'/> + Emoji Stuck Out Tongue Winking Eye + + + + :@ + <img src='/im_chat/static/src/img/scream.png'/> + Emoji Scream + + + + :| + <img src='/im_chat/static/src/img/neutral.png'/> + Emoji Speechless/Neutral face + + + + :p + <img src='/im_chat/static/src/img/yum.png'/> + Emoji Cheeky + + + + :/ + <img src='/im_chat/static/src/img/confused.png'/> + Emoji Confused + + + + :O + <img src='/im_chat/static/src/img/open_mouth.png'/> + Emoji Open Mouth + + + + &lt;3 + <img src='/im_chat/static/src/img/heart.png'/> + Emoji Heart + + + + + :+1 + <img src='/im_chat/static/src/img/thumbsup.png'/> + Emoji Thumbs Up + + + + :-1 + <img src='/im_chat/static/src/img/thumbsdown.png'/> + Emoji Thumbs Down + + + + :kitten + <img src='/im_chat/static/src/img/cat.png'/> + Emoji Kitten + + + + :pinky + <img src='/im_chat/static/src/img/pinky.png'/> + Emoji Pinky + + + + :musti + <img src='/im_chat/static/src/img/musti.png'/> + Emoji Musti + + + + :turban + <img src='/im_chat/static/src/img/man_with_turban.png'/> + Emoji Man With Turban + + + + :troll + <img src='/im_chat/static/src/img/trollface.png'/> + Emoji Troll Face + + + + :boar + <img src='/im_chat/static/src/img/boar.png'/> + Emoji Boar + + + + :poop + <img src='/im_chat/static/src/img/poop.png'/> + Emoji Poop + + + + :cookie + <img src='/im_chat/static/src/img/cookie.png'/> + Emoji Cookie + + + + :postal_horn + <img src='/im_chat/static/src/img/postal_horn.png'/> + Emoji Postal Horn + + + + :ghost + <img src='/im_chat/static/src/img/ghost.png'/> + Emoji Ghost + + + + :beer + <img src='/im_chat/static/src/img/beer.png'/> + Emoji Beer + + + + :man + <img src='/im_chat/static/src/img/man.png'/> + Emoji Man + + + + diff --git a/addons/mail/models/__init__.py b/addons/mail/models/__init__.py index ca72296a6fb..d24e5295cf0 100644 --- a/addons/mail/models/__init__.py +++ b/addons/mail/models/__init__.py @@ -9,9 +9,10 @@ import mail_mail import mail_thread import mail_channel import mail_template +import mail_shortcode import res_partner import res_users import res_config import update import ir_actions -import html2text \ No newline at end of file +import html2text diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py index f118133ddf9..635a4999cce 100644 --- a/addons/mail/models/mail_channel.py +++ b/addons/mail/models/mail_channel.py @@ -14,6 +14,8 @@ class ChannelPartner(models.Model): partner_id = fields.Many2one('res.partner', string='Recipient') channel_id = fields.Many2one('mail.channel', string='Channel') seen_message_id = fields.Many2one('mail.message', string='Last Seen') + state = fields.Selection([('open', 'Open'), ('folded', 'Folded'), ('closed', 'Closed')], string='Status', default='open') + seen_datetime = fields.Datetime('Last Seen Datetime') class Channel(models.Model): @@ -161,3 +163,139 @@ class Channel(models.Model): headers['X-Forge-To'] = list_to res['headers'] = repr(headers) return res + + + # -------------------------- + # Session Methods + # -------------------------- + @api.multi + def session_info(self): + """ Get the header of the current session + :returns the values of the session + :rtype : dict + """ + self.ensure_one() + info = { + 'uuid': self.uuid, + 'users': self.session_user_info(), + 'state': 'open', + } + # add user session state, if available and if user is logged + if self._uid: + domain = [('user_id', '=', self._uid), ('session_id', 'in', self.ids)] + session_state = self.env['im_chat.conversation_state'].search(domain, limit=1) + if session_state: + info['state'] = session_state.state + return info + + @api.multi + def session_user_info(self): + """ Get the user infos for all the identified user in the session + :returns a list of user infos + :rtype : list(dict) + """ + self.ensure_one() + return self.env['res.users'].browse(self.user_ids.ids).read(['id', 'name', 'im_status']) + + @api.model + def session_user_image(self, uuid, user_id): + """ Get the avatar of a user in the given session + :param uuid : the uuid of the session + :param user_id : the user identifier + """ + # get the session + image_b64 = False + if user_id: + session = self.env["im_chat.session"].search([('uuid', '=', uuid)], limit=1) + if session and session.sudo(user_id).is_in_session(): + image_b64 = self.env["res.users"].sudo().browse(user_id).image_small + # set default image if not found + if not image_b64: + image_b64 = 'R0lGODlhAQABAIABAP///wAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==' + return image_b64 + + @api.model + def session_get(self, user_to): + """ Get the canonical session between 2 users, create it if needed. + To reuse an old session, this one must be private, and contains only 2 users. + :param user_to : the identifier of the user + :returns a session header, or False if the user_to was False + :rtype : dict + """ + if user_to: + session = self.search([('user_ids', 'in', user_to), ('user_ids', 'in', [self._uid])], limit=1) + if not (session and len(session.user_ids) == 2 and session.is_private()): + session = self.create({'user_ids': [(6, 0, (user_to, self._uid))]}) + return session.session_info() + + # -------------------------- + # Utils Methods + # -------------------------- + @api.multi + def is_private(self): + """ Return true if the session is private between identified users (no external messages). + The only way to do that is to check if there is a message without author (from_id is False). + """ + self.ensure_one() + message_ids = self.env["im_chat.message"].search([('to_id', 'in', self.ids), ('from_id', '=', None)]) + return len(message_ids) == 0 + + @api.multi + def is_in_session(self): + """ Return True if the current user is in the user_ids of the session. False otherwise. + Note : the user need to be logged + """ + self.ensure_one() + user_id = self._uid + return user_id and user_id in [u.id for u in self.user_ids] + + @api.model + def update_state(self, uuid, state=None): + """ Update the fold_state of the given session. In order to syncronize web browser + tabs, the change will be broadcast to himself (the current user channel). + Note : the user need to be logged + :param status : the new status of the session for the current user. + """ + domain = [('user_id', '=', self._uid), ('session_id.uuid', '=', uuid)] + for session_state in self.env['im_chat.conversation_state'].search(domain): + if not state: + state = session_state.state + if session_state.state == 'open': + state = 'folded' + else: + state = 'open' + session_state.write({'state': state}) + self.env['bus.bus'].sendone((self._cr.dbname, 'im_chat.session', self._uid), session_state.session_id.session_info()) + + @api.model + def get_init_notifications(self): + """ Get unread messages and old messages received less than AWAY_TIMER + ago and the session_info for open or folded window + Note : the user need to be logged + """ + # get the message since the AWAY_TIMER + threshold = datetime.datetime.now() - datetime.timedelta(seconds=AWAY_TIMER) + threshold = threshold.strftime(DEFAULT_SERVER_DATETIME_FORMAT) + domain = [('to_id.user_ids', 'in', [self._uid]), ('create_date', '>', threshold)] + + # get the message since the last poll of the user + presence = self.env['im_chat.presence'].search([('user_id', '=', self._uid)], limit=1) + if presence: + domain.append(('create_date', '>', presence.last_poll)) + messages = self.search_read(domain, ['from_id', 'to_id', 'create_date', 'type', 'message'], order='id asc') + + # get the session of the messages and the not-closed ones + session_ids = [m['to_id'][0] for m in messages] + domain = [('user_id', '=', self._uid), '|', ('state', '!=', 'closed'), ('session_id', 'in', session_ids)] + session_states = self.env['im_chat.conversation_state'].search(domain) + # re-open the session where a message have been recieved recently + session_states.filtered(lambda r: r.state == 'closed').write({'state': 'folded'}) + + # create the notifications (session infos first, then messages) + notifications = [] + for state in session_states: + session_infos = state.session_id.session_info() + notifications.append([(self._cr.dbname, 'im_chat.session', self._uid), session_infos]) + for message in messages: + notifications.append([(self._cr.dbname, 'im_chat.session', self._uid), message]) + return notifications diff --git a/addons/mail/models/mail_shortcode.py b/addons/mail/models/mail_shortcode.py new file mode 100644 index 00000000000..42627e5abe5 --- /dev/null +++ b/addons/mail/models/mail_shortcode.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +import cgi +import re + +from openerp import api, fields, models + + +class ImChatShortcode(models.Model): + """ Shortcode + Canned Responses, allowing the user to defined shortcuts in its chat message. + These shortcode are globals and are available for every user. Smiley use this mecanism. + """ + + _name = 'im_chat.shortcode' + _description = 'Canned Response / Shortcode' + + source = fields.Char('Shortcut', required=True, index=True, help="The shortcut which must be replace in the Chat Messages") + substitution = fields.Char('Substitution', required=True, index=True, help="The excaped html code replacing the shortcut") + description = fields.Char('Description') + + @api.model + def create(self, values): + if values.get('substitution'): + values['substitution'] = self._sanitize_shorcode(values['substitution']) + return super(ImChatShortcode, self).create(values) + + @api.multi + def write(self, values): + if values.get('substitution'): + values['substitution'] = self._sanitize_shorcode(values['substitution']) + return super(ImChatShortcode, self).write(values) + + def _sanitize_shorcode(self, substitution): + """ Sanitize the shortcode substitution : + - HTML substitution : only allow the img tag (smiley) + - escape other substitutions to avoid XSS + """ + is_img_tag = re.match(r'''^$''', substitution, re.M|re.I) + if is_img_tag: + return substitution + return cgi.escape(substitution) + + @api.model + def replace_shortcode(self, message): + for shortcode in self.search([]): + regex = '(?:^|\s)(%s)(?:\s|$)' % re.escape(shortcode.source) + message = re.sub(regex, " " + shortcode.substitution + " ", message) + return message diff --git a/addons/mail/static/src/audio/purr.mp3 b/addons/mail/static/src/audio/purr.mp3 new file mode 100644 index 00000000000..849d303c3b1 Binary files /dev/null and b/addons/mail/static/src/audio/purr.mp3 differ diff --git a/addons/mail/static/src/audio/purr.ogg b/addons/mail/static/src/audio/purr.ogg new file mode 100644 index 00000000000..2ddc77537f1 Binary files /dev/null and b/addons/mail/static/src/audio/purr.ogg differ diff --git a/addons/mail/static/src/audio/ting.mp3 b/addons/mail/static/src/audio/ting.mp3 new file mode 100644 index 00000000000..fd29bf599d6 Binary files /dev/null and b/addons/mail/static/src/audio/ting.mp3 differ diff --git a/addons/mail/static/src/audio/ting.ogg b/addons/mail/static/src/audio/ting.ogg new file mode 100644 index 00000000000..cc8c0f01eff Binary files /dev/null and b/addons/mail/static/src/audio/ting.ogg differ diff --git a/addons/mail/static/src/css/mail_chat_backend.css b/addons/mail/static/src/css/mail_chat_backend.css new file mode 100644 index 00000000000..d01577eb56a --- /dev/null +++ b/addons/mail/static/src/css/mail_chat_backend.css @@ -0,0 +1,114 @@ +.oe_im { + position: fixed; + background-color: #eaeaea; + width: 250px; + border-left: 1px solid #AEB9BD; + z-index: 500; + -webkit-box-shadow: -2px 0px 5px 0px rgba(50, 50, 50, 0.25); + -moz-box-shadow: -2px 0px 5px 0px rgba(50, 50, 50, 0.25); + box-shadow: -2px 0px 5px 0px rgba(50, 50, 50, 0.25); +} + +@media print { + .oe_im, .oe_im_chatview { + visibility: hidden; + } +} +/* button */ +.oe_systray #oe_topbar_imbutton_icon { + color: white; +} +.oe_topbar_item.oe_topbar_imbutton .fa { + position: relative; + top: -8px; +} + +/* search stuff */ +.oe_im_frame_header { + position: relative; + padding: 16px; +} +.oe_im_frame_header .oe_im_searchbox { + width: 200px; + padding: 1px 21px 1px 19px; + font-size: 13px; + border: none; + border-bottom: 1px solid #c6c6c6; + background: none; + margin-left: 20px; + height: 38px; +} +.oe_im_frame_header .oe_im_searchbox:focus { + outline: none; +} +.oe_im_frame_header .oe_im_search_icon { + position: absolute; + color: #888; + top: 28px; + left: 8px; + font-size: 16px; + font-weight: 300 !important; +} +.oe_im_frame_header .oe_im_search_clear { + display: none; + position: absolute; + right: 11px; + top: 4px; + font-size: 26px; + color: #b6b6b6; + cursor: pointer; +} +.oe_im_frame_header .oe_im_search_clear:hover { + color: #888; +} + +/* users */ + +.oe_im_users { + padding-bottom: 38px; + height: 100%; + overflow-y: scroll; +} +.oe_im_user { + position: relative; + padding: 2px 8px; + cursor: pointer; + font-size: 13px; + margin-bottom: 10px; +} +.oe_im_user:hover { + background: lightGrey; +} +.oe_im_user_clip { + display: inline-block; + width: 26px; + height: 26px; + margin-right: 4px; + -moz-box-shadow: 0 0 2px 1px rgba(0,0,0,0.25); + -webkit-box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.25); + box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.25); +} +.oe_im_user_avatar { + width: 26px; + height: auto; +} +.oe_im_user_name { + width: 162px; + line-height: 26px; + padding-right: 15px; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + position: relative; +} + +.oe_im_user_online { + display: none; + position: absolute; + top: 9.5px; + right: 11px; + width: 11px; + height: 11px; + vertical-align: middle; + border: 0; +} diff --git a/addons/mail/static/src/css/mail_chat_common.css b/addons/mail/static/src/css/mail_chat_common.css new file mode 100644 index 00000000000..1ad2981eb41 --- /dev/null +++ b/addons/mail/static/src/css/mail_chat_common.css @@ -0,0 +1,259 @@ + + + +.openerp_style { /* base style of openerp */ + font-family: "Lucida Grande", Helvetica, Verdana, Arial, sans-serif; + color: #4c4c4c; + font-size: 13px; + background: white; +} + +/* button */ + +.oe_chat_button { + position: fixed; + bottom: 0px; + right: 6px; + display: inline-block; + min-width: 100px; + background-color: rgba(60, 60, 60, 0.6); + font-family: 'Lucida Grande', 'Lucida Sans Unicode', Arial, Verdana, sans-serif; + font-size: 14px; + font-weight: bold; + padding: 10px; + color: white; + text-shadow: rgb(59, 76, 88) 1px 1px 0px; + border: 1px solid rgb(80, 80, 80); + border-bottom: 0px; + border-top-left-radius: 5px; + border-top-right-radius: 5px; + cursor: pointer; + z-index:500; +} + +/* conversations */ + +.oe_im_chatview { + position: fixed; + overflow: hidden; + margin-bottom: 5px; + margin-right: 6px; + background-color: #fff; + border: 1px solid #DEDEDE; + width: 240px; + z-index:500; +} +@media screen and (max-width:360px) { + .oe_im_chatview { + width: 100%; + margin-right: 0; + } + .oe_im_chatview .oe_im_chatview_content { + width: 100% !important; + } + .oe_im_chatview .oe_im_chatview_input { + width: 100% !important; + } +} +.oe_im_chatview .oe_im_chatview_disconnected { + display:none; + z-index: 100; + width: 100%; + background: #E8EBEF; + padding: 5px; + font-size: 11px; + color: #999; + line-height: 14px; + height: 28px; + overflow: hidden; +} +.oe_im_chatview.oe_im_chatview_disconnected_status .oe_im_chatview_disconnected { + display: block; +} + + + +/* ####### Chat View : Header ####### */ +.oe_im_chatview_header { + padding: 3px 6px 2px; + background: #DEDEDE; + background: -moz-linear-gradient(#FCFCFC, #DEDEDE); + background: -webkit-gradient(linear, left top, left bottom, from(#FCFCFC), to(#DEDEDE)); + border-radius: 3px 3px 0 0; + -moz-border-radius: 3px 3px 0 0; + -webkit-border-radius: 3px 3px 0 0; + border-bottom: 1px solid #AEB9BD; + cursor: pointer; +} + +.oe_im_chatview_header .header_name{ + max-width: 75%; + word-wrap: break-word; + display: inline-block; + height: 15px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.oe_im_chatview_header .right_buttons { + float: right; +} +.oe_im_chatview_header .right_buttons > * { + display: inline-block; + border: 0; + cursor: pointer; + color: gray ; + background: transparent; + font-size: 16px; + line-height: 16px; + margin-left: 5px; + -webkit-appearance: none; +} + +.oe_im_chatview_header .right_buttons .dropdown-menu{ + right: 0; + left: auto; +} +.oe_im_chatview_header .right_buttons .dropdown-menu > li{ + display: block; + padding: 3px 20px; + clear: both; + font-weight: normal; + line-height: 1.428571429; + color: #4c4c4c; + white-space: nowrap; +} + + + +.oe_im_chatview .oe_im_chatview_content { + overflow: auto; + height: 287px; + width: 240px; +} +.oe_im_chatview.oe_im_chatview_disconnected_status .oe_im_chatview_content { + height: 249px; +} +.oe_im_chatview .oe_im_chatview_footer { + position: relative; + padding: 3px; + border-top: 1px solid #AEB9BD; + background: #DEDEDE; + background: -moz-linear-gradient(#FCFCFC, #DEDEDE); + background: -webkit-gradient(linear, left top, left bottom, from(#FCFCFC), to(#DEDEDE)); + -moz-border-radius: 0 0 3px 3px; + -webkit-border-radius: 0 0 3px 3px; + border-radius: 0 0 3px 3px; +} +.oe_im_chatview .oe_im_chatview_input { + width: 222px; + font-family: Lato, Helvetica, sans-serif; + font-size: 13px; + color: #333; + padding: 1px 5px; + border: 1px solid #AEB9BD; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + border-radius: 3px; + -moz-box-shadow: inset 0 1px 4px rgba(0,0,0,0.2); + -webkit-box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.2); + box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.2); +} +.oe_im_chatview .oe_im_chatview_message_bubble { + background: white; + position: relative; + min-height: 32px; + padding: 3px; + margin: 3px; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + border-radius: 3px; +} + +/* Smiley size */ +.oe_im_chatview .oe_im_chatview_bubble_item img{ + width: 18px; +} + +.oe_im_chatview .oe_im_chatview_technical_bubble { + background: #D8D8D8; + position: relative; + min-height: 32px; + padding: 3px; + margin: 3px; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + border-radius: 3px; + + font-style:italic; +} + +.oe_im_chatview .oe_im_chatview_clip { + position: relative; + float: left; + width: 26px; + height: 26px; + margin-right: 4px; + -moz-box-shadow: 0 0 2px 1px rgba(0,0,0,0.25); + -webkit-box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.25); + box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.25); +} +.oe_im_chatview .oe_im_chatview_avatar { + float: left; + width: 26px; + height: auto; + clip: rect(0, 26px, 26px, 0); + max-width: 100%; + width: auto 9; + height: auto; + vertical-align: middle; + border: 0; + -ms-interpolation-mode: bicubic; +} +.oe_im_chatview .oe_im_chatview_time { + position: absolute; + right: 0px; + top: 0px; + margin: 3px; + text-align: right; + line-height: 13px; + font-size: 11px; + color: #999; + width: 60px; + overflow: hidden; +} +.oe_im_chatview .oe_im_chatview_from { + margin: 0 0 2px 0; + line-height: 14px; + font-weight: bold; + font-size: 12px; + width: 140px; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + color: #3A87AD; +} +.oe_im_chatview .oe_im_chatview_bubble_list { +} +.oe_im_chatview .oe_im_chatview_bubble_item { + margin: 0 0 2px 30px; + line-height: 14px; + word-wrap: break-word; +} + +.oe_im_chatview_online { + margin-top: -4px; + width: 11px; + height: 11px; +} + +.oe_im_chatview .oe_im_chatview_date_separator { + background: transparent; + position: relative; + padding: 3px; + color: #aaa; + margin: 3px; + text-align: right; + font-size: 12px; +} \ No newline at end of file diff --git a/addons/mail/static/src/img/smiley/avatar.jpg b/addons/mail/static/src/img/smiley/avatar.jpg new file mode 100644 index 00000000000..7168794022e Binary files /dev/null and b/addons/mail/static/src/img/smiley/avatar.jpg differ diff --git a/addons/mail/static/src/img/smiley/beer.png b/addons/mail/static/src/img/smiley/beer.png new file mode 100644 index 00000000000..cd78bed7440 Binary files /dev/null and b/addons/mail/static/src/img/smiley/beer.png differ diff --git a/addons/mail/static/src/img/smiley/boar.png b/addons/mail/static/src/img/smiley/boar.png new file mode 100644 index 00000000000..3ef50325016 Binary files /dev/null and b/addons/mail/static/src/img/smiley/boar.png differ diff --git a/addons/mail/static/src/img/smiley/cat.png b/addons/mail/static/src/img/smiley/cat.png new file mode 100644 index 00000000000..dbf1b0276ab Binary files /dev/null and b/addons/mail/static/src/img/smiley/cat.png differ diff --git a/addons/mail/static/src/img/smiley/confused.png b/addons/mail/static/src/img/smiley/confused.png new file mode 100644 index 00000000000..8dc494db083 Binary files /dev/null and b/addons/mail/static/src/img/smiley/confused.png differ diff --git a/addons/mail/static/src/img/smiley/cookie.png b/addons/mail/static/src/img/smiley/cookie.png new file mode 100644 index 00000000000..177d0f9ad86 Binary files /dev/null and b/addons/mail/static/src/img/smiley/cookie.png differ diff --git a/addons/mail/static/src/img/smiley/ghost.png b/addons/mail/static/src/img/smiley/ghost.png new file mode 100644 index 00000000000..671dd0c9e2e Binary files /dev/null and b/addons/mail/static/src/img/smiley/ghost.png differ diff --git a/addons/mail/static/src/img/smiley/green.png b/addons/mail/static/src/img/smiley/green.png new file mode 100644 index 00000000000..0700a5ef71b Binary files /dev/null and b/addons/mail/static/src/img/smiley/green.png differ diff --git a/addons/mail/static/src/img/smiley/heart.png b/addons/mail/static/src/img/smiley/heart.png new file mode 100644 index 00000000000..7d7790ce4df Binary files /dev/null and b/addons/mail/static/src/img/smiley/heart.png differ diff --git a/addons/mail/static/src/img/smiley/laugh.png b/addons/mail/static/src/img/smiley/laugh.png new file mode 100644 index 00000000000..11c91eb22e6 Binary files /dev/null and b/addons/mail/static/src/img/smiley/laugh.png differ diff --git a/addons/mail/static/src/img/smiley/man.png b/addons/mail/static/src/img/smiley/man.png new file mode 100644 index 00000000000..c4a4217f4d4 Binary files /dev/null and b/addons/mail/static/src/img/smiley/man.png differ diff --git a/addons/mail/static/src/img/smiley/man_with_turban.png b/addons/mail/static/src/img/smiley/man_with_turban.png new file mode 100644 index 00000000000..8a0bda1ca77 Binary files /dev/null and b/addons/mail/static/src/img/smiley/man_with_turban.png differ diff --git a/addons/mail/static/src/img/smiley/musti.png b/addons/mail/static/src/img/smiley/musti.png new file mode 100644 index 00000000000..f6843ddf95c Binary files /dev/null and b/addons/mail/static/src/img/smiley/musti.png differ diff --git a/addons/mail/static/src/img/smiley/neutral.png b/addons/mail/static/src/img/smiley/neutral.png new file mode 100644 index 00000000000..682a1ba066d Binary files /dev/null and b/addons/mail/static/src/img/smiley/neutral.png differ diff --git a/addons/mail/static/src/img/smiley/open_mouth.png b/addons/mail/static/src/img/smiley/open_mouth.png new file mode 100644 index 00000000000..daf914274a6 Binary files /dev/null and b/addons/mail/static/src/img/smiley/open_mouth.png differ diff --git a/addons/mail/static/src/img/smiley/pinky.png b/addons/mail/static/src/img/smiley/pinky.png new file mode 100644 index 00000000000..4466946601c Binary files /dev/null and b/addons/mail/static/src/img/smiley/pinky.png differ diff --git a/addons/mail/static/src/img/smiley/poop.png b/addons/mail/static/src/img/smiley/poop.png new file mode 100644 index 00000000000..73a4dc84008 Binary files /dev/null and b/addons/mail/static/src/img/smiley/poop.png differ diff --git a/addons/mail/static/src/img/smiley/postal_horn.png b/addons/mail/static/src/img/smiley/postal_horn.png new file mode 100644 index 00000000000..5b7ed85d4fc Binary files /dev/null and b/addons/mail/static/src/img/smiley/postal_horn.png differ diff --git a/addons/mail/static/src/img/smiley/sad.png b/addons/mail/static/src/img/smiley/sad.png new file mode 100644 index 00000000000..82552008719 Binary files /dev/null and b/addons/mail/static/src/img/smiley/sad.png differ diff --git a/addons/mail/static/src/img/smiley/scream.png b/addons/mail/static/src/img/smiley/scream.png new file mode 100644 index 00000000000..76bfc6b8a65 Binary files /dev/null and b/addons/mail/static/src/img/smiley/scream.png differ diff --git a/addons/mail/static/src/img/smiley/smile.png b/addons/mail/static/src/img/smiley/smile.png new file mode 100644 index 00000000000..81a83968996 Binary files /dev/null and b/addons/mail/static/src/img/smiley/smile.png differ diff --git a/addons/mail/static/src/img/smiley/stuck_out_tongue.png b/addons/mail/static/src/img/smiley/stuck_out_tongue.png new file mode 100644 index 00000000000..ea9aeb99e29 Binary files /dev/null and b/addons/mail/static/src/img/smiley/stuck_out_tongue.png differ diff --git a/addons/mail/static/src/img/smiley/thumbsdown.png b/addons/mail/static/src/img/smiley/thumbsdown.png new file mode 100644 index 00000000000..74a5005c893 Binary files /dev/null and b/addons/mail/static/src/img/smiley/thumbsdown.png differ diff --git a/addons/mail/static/src/img/smiley/thumbsup.png b/addons/mail/static/src/img/smiley/thumbsup.png new file mode 100644 index 00000000000..116388fc3e2 Binary files /dev/null and b/addons/mail/static/src/img/smiley/thumbsup.png differ diff --git a/addons/mail/static/src/img/smiley/trollface.png b/addons/mail/static/src/img/smiley/trollface.png new file mode 100644 index 00000000000..eb222e2ad69 Binary files /dev/null and b/addons/mail/static/src/img/smiley/trollface.png differ diff --git a/addons/mail/static/src/img/smiley/wink.png b/addons/mail/static/src/img/smiley/wink.png new file mode 100644 index 00000000000..756766dd3e9 Binary files /dev/null and b/addons/mail/static/src/img/smiley/wink.png differ diff --git a/addons/mail/static/src/img/smiley/yellow.png b/addons/mail/static/src/img/smiley/yellow.png new file mode 100644 index 00000000000..726570e518f Binary files /dev/null and b/addons/mail/static/src/img/smiley/yellow.png differ diff --git a/addons/mail/static/src/img/smiley/yum.png b/addons/mail/static/src/img/smiley/yum.png new file mode 100644 index 00000000000..fc39637ecd8 Binary files /dev/null and b/addons/mail/static/src/img/smiley/yum.png differ diff --git a/addons/mail/static/src/js/mail_chat_backend.js b/addons/mail/static/src/js/mail_chat_backend.js new file mode 100644 index 00000000000..cd1cc00fdf2 --- /dev/null +++ b/addons/mail/static/src/js/mail_chat_backend.js @@ -0,0 +1,301 @@ +odoo.define('im_chat.im_chat', function (require) { +"use strict"; + +// to do: make this work in website + +var bus = require('bus.bus'); +var core = require('web.core'); +var data = require('web.data'); +var Model = require('web.DataModel'); +var session = require('web.session'); +var SystrayMenu = require('web.SystrayMenu'); +var utils = require('web.utils'); +var web_client = require('web.web_client'); +var Widget = require('web.Widget'); +var im_chat_common = require('im_chat.im_chat_common'); + +var _t = core._t; +var QWeb = core.qweb; + +var USERS_LIMIT = 20; + + +// ########## CONVERSATION extentions ############### + +im_chat_common.ConversationManager.include({ + _message_receive: function(message){ + var self = this; + var session_id = message.to_id[0]; + var conv = this.sessions[message.to_id[1]]; + if(!conv){ + // fetch the session, and init it with the message + var def_session = new Model("im_chat.session").call("session_info", [], {"ids" : [session_id]}).then(function(session){ + conv = self.session_apply(session, {'force_open': true}); + conv.message_receive(message); + }); + }else{ + this._super(message); + } + }, +}); + +im_chat_common.Conversation.include({ + // user actions + prepare_action_menu: function(){ + this._super(); + this._add_action(_t('Shortcuts'), 'im_chat_option_shortcut', 'fa fa-info-circle', this.action_shorcode); + this._add_action(_t('Quit discussion'), 'im_chat_option_quit', 'fa fa-minus-square', this.action_quit_session); + }, + action_shorcode: function(e){ + return web_client.action_manager.do_action({ + type: 'ir.actions.act_window', + name : _t('Shortcode'), + res_model: 'im_chat.shortcode', + view_mode: 'tree,form', + view_type: 'tree', + views: [[false, 'list'], [false, 'form']], + target: "new", + limit: 80, + flags: { + action_buttons: true, + pager: true, + } + }); + }, + action_quit_session: function(e){ + var self = this; + var Session = new Model("im_chat.session"); + return Session.call("quit_user", [this.get("session").uuid]).then(function(res) { + if(! res){ + self.do_warn(_t("Warning"), _t("You are only 2 identified users. Just close the conversation to leave.")); + } + }); + }, + // session + session_update_state: function(state){ + var self = this; + var args = arguments; + var super_call = this._super; + // broadcast the state changing + return new Model("im_chat.session").call("update_state", [], {"uuid" : this.get("session").uuid, "state" : state}).then(function(){ + super_call.apply(self, args); + }); + }, + // window title + window_title_change: function() { + this.super(); + var title = undefined; + if (this.get("waiting_messages") !== 0) { + title = _.str.sprintf(_t("%d Messages"), this.get("waiting_messages")) + } + web_client.set_title_part("im_messages", title); + }, + // TODO : change this way + add_user: function(user){ + return new Model("im_chat.session").call("add_user", [this.get("session").uuid , user.id]); + }, +}); + + + +// ###### BACKEND : contact panel, top menu button ######### + +var UserWidget = Widget.extend({ + template: "im_chat.UserWidget", + events: { + "click": "activate_user", + }, + init: function(parent, user) { + this._super(parent); + this.set("id", user.id); + this.set("name", user.name); + this.set("im_status", user.im_status); + this.set("image_url", user.image_url); + }, + start: function() { + this.$el.data("user", {id:this.get("id"), name:this.get("name")}); + this.$el.draggable({helper: "clone"}); + this.on("change:im_status", this, this.update_status); + this.update_status(); + }, + update_status: function(){ + this.$(".oe_im_user_online").toggle(this.get('im_status') !== 'offline'); + var img_src = (this.get('im_status') == 'away' ? '/im_chat/static/src/img/yellow.png' : '/im_chat/static/src/img/green.png'); + this.$(".oe_im_user_online").attr('src', img_src); + }, + activate_user: function() { + this.trigger("user_clicked", this.get("id")); + }, +}); + +var InstantMessaging = Widget.extend({ + template: "im_chat.InstantMessaging", + events: { + "keydown .oe_im_searchbox": "input_change", + "keyup .oe_im_searchbox": "input_change", + "change .oe_im_searchbox": "input_change", + }, + init: function(parent) { + this._super(parent); + this.shown = false; + this.set("right_offset", 0); + this.set("current_search", ""); + this.users = []; + this.widgets = {}; + + // listen bus + this.bus = bus.bus; + this.bus.on("notification", this, this.on_notification); + }, + start: function() { + var self = this; + // ui + this.conv_manager = new im_chat_common.ConversationManager(this); + this.on("change:right_offset", this.conv_manager, _.bind(function() { + this.conv_manager.set("right_offset", this.get("right_offset")); + }, this)); + $(window).scroll(_.bind(this.position_compute, this)); + $(window).resize(_.bind(this.position_compute, this)); + this.$el.css("right", -this.$el.outerWidth()); + this.position_compute(); + + // business + this.on("change:current_search", this, this.user_search); + + // add a drag & drop listener + self.conv_manager.on("im_session_activated", self, function(conv) { + conv.$el.droppable({ + drop: function(event, ui) { + conv.add_user(ui.draggable.data("user")); + } + }); + }); + + // fetch the unread message and the recent activity (e.i. to re-init in case of refreshing page) + return session.rpc("/im_chat/init", {}).then(function(notifications) { + _.each(notifications, function(notif){ + self.conv_manager.on_notification(notif, {'load_history': true}); + }); + // start polling + self.bus.start_polling(); + }); + }, + on_notification: function(notification){ + var channel = notification[0]; + var message = notification[1]; + // user status notification + if(channel[1] === 'im_chat.presence'){ + if(message.im_status){ + this.user_update_status([message]); + } + } + }, + // ui + position_compute: function() { + var top = 48; + this.$el.css("top", top); + this.$el.css("bottom", 0); + }, + input_change: function() { + this.set("current_search", this.$(".oe_im_searchbox").val()); + }, + switch_display: function() { + this.position_compute(); + var fct = _.bind(function(place) { + this.set("right_offset", place + this.$el.outerWidth()); + this.$(".oe_im_searchbox").focus(); + }, this); + var opt = { + step: fct, + }; + if (this.shown) { + this.$el.animate({ + right: -this.$el.outerWidth(), + }, opt); + } else { + if (! bus.bus.activated) { + this.do_warn("Instant Messaging is not activated on this server. Try later.", ""); + return; + } + // update the list of user status when show the IM + this.user_search(); + this.$el.animate({ + right: 0, + }, opt); + } + this.shown = ! this.shown; + }, + // user methods + user_search: function(e) { + var self = this; + var user_model = new Model("res.users"); + return user_model.call('im_search', [this.get("current_search"), USERS_LIMIT]).then(function(result){ + self.$(".oe_im_input").val(""); + var old_widgets = self.widgets; + self.widgets = {}; + self.users = []; + _.each(result, function(user) { + user.image_url = session.url('/web/image', {model:'res.users', field: 'image_small', id: user.id}); + var widget = new UserWidget(self, user); + widget.appendTo(self.$(".oe_im_users")); + widget.on("user_clicked", self, self.user_clicked); + self.widgets[user.id] = widget; + self.users.push(user); + }); + _.each(old_widgets, function(w) { + w.destroy(); + }); + }); + }, + user_clicked: function(user_id) { + var self = this; + var sessions = new Model("im_chat.session"); + return sessions.call("session_get", [user_id]).then(function(session) { + self.conv_manager.session_apply(session, {'focus': true, 'force_open': true}); + }); + }, + user_update_status: function(users_list){ + var self = this; + _.each(users_list, function(el) { + if (self.widgets[el.id]) { + self.widgets[el.id].set("im_status", el.im_status); + } + }); + } +}); + + +var ImTopButton = Widget.extend({ + template:'im_chat.ImTopButton', + events: { + "click": "clicked", + }, + start: function() { + // Create the InstantMessaging widget and put it in the DOM + var im = new InstantMessaging(this); + web_client.im_messaging = im; + im.appendTo(web_client.$el); + // Bind the click action to the ImTopButton + this.on("clicked", im, im.switch_display); + return this._super(); + }, + clicked: function(ev) { + ev.preventDefault(); + this.trigger("clicked"); + }, +}); + +// Put the ImTopButton widget in the systray menu if the user is an employee +var Users = new Model('res.users'); +Users.call('has_group', ['base.group_user']).done(function(is_employee) { + if (is_employee) { + SystrayMenu.Items.push(ImTopButton); + } +}); + +return { + InstantMessaging: InstantMessaging, + UserWidget: UserWidget, +}; + +}); diff --git a/addons/mail/static/src/js/mail_chat_common.js b/addons/mail/static/src/js/mail_chat_common.js new file mode 100644 index 00000000000..0aaa0e68d16 --- /dev/null +++ b/addons/mail/static/src/js/mail_chat_common.js @@ -0,0 +1,433 @@ +odoo.define('im_chat.im_chat_common', function (require) { +"use strict"; + +// to do: make this work in website + +var bus = require('bus.bus'); +var core = require('web.core'); +var session = require('web.session'); +var time = require('web.time'); +var Widget = require('web.Widget'); + +var _t = core._t; +var QWeb = core.qweb; + +var NBR_LIMIT_HISTORY = 20; + + +var ConversationManager = Widget.extend({ + init: function(parent, options) { + var self = this; + this._super(parent); + this.options = _.clone(options) || {}; + _.defaults(this.options, { + inputPlaceholder: _t("Say something..."), + defaultMessage: null, + defaultUsername: _t("Visitor"), + force_open: false, + load_history: true, + focus: false, + }); + // business + this.sessions = {}; + this.bus = bus.bus; + this.bus.on("notification", this, this.on_notification); + this.bus.options.im_presence = true; + + // ui + this.set("right_offset", 0); + this.set("bottom_offset", 0); + this.on("change:right_offset", this, this.calc_positions); + this.on("change:bottom_offset", this, this.calc_positions); + + this.set("window_focus", true); + this.on("change:window_focus", self, function(e) { + self.bus.options.im_presence = self.get("window_focus"); + }); + this.set("waiting_messages", 0); + this.on("change:waiting_messages", this, this.window_title_change); + $(window).on("focus", _.bind(this.window_focus, this)); + $(window).on("blur", _.bind(this.window_blur, this)); + this.window_title_change(); + }, + on_notification: function(notification, options) { + var self = this; + var channel = notification[0]; + var message = notification[1]; + var regex_uuid = new RegExp(/(\w{8}(-\w{4}){3}-\w{12}?)/g); + + // Concern im_chat : if the channel is the im_chat.session or im_chat.status, or a 'private' channel (aka the UUID of a session) + if((Array.isArray(channel) && (channel[1] === 'im_chat.session')) || (regex_uuid.test(channel))){ + // message to display in the chatview + if (message.type === "message" || message.type === "meta") { + self.message_receive(message); + } + // activate the received session + if(message.uuid){ + this.session_apply(message, options); + } + } + }, + + // window focus unfocus beep and title + window_focus: function() { + this.set("window_focus", true); + this.set("waiting_messages", 0); + }, + window_blur: function() { + this.set("window_focus", false); + }, + window_beep: function() { + if (typeof(Audio) === "undefined") { + return; + } + var audio = new Audio(); + var ext = audio.canPlayType("audio/ogg; codecs=vorbis") ? ".ogg" : ".mp3"; + audio.src = session.url("/im_chat/static/src/audio/ting") + ext; + audio.play(); + }, + window_title_change: function() { + var title = undefined; + if (this.get("waiting_messages") !== 0) { + title = _.str.sprintf(_t("%d Messages"), this.get("waiting_messages")); + this.window_beep(); + } + }, + // sessions and messages + session_apply: function(active_session, options){ + // options used by this function : force_open and focus (load_history can be usefull) + var self = this; + options = _.extend(_.clone(this.options), options || {}); + // force open + var session = _.clone(active_session); + if(options['force_open']){ + session.state = 'open'; + } + // create/get the conversation widget + var conv = this.sessions[session.uuid]; + if (! conv) { + if(session.state !== 'closed'){ + conv = new Conversation(this, this, session, options); + conv.appendTo($("body")); + conv.on("destroyed", this, _.bind(this.session_delete, this)); + this.sessions[session.uuid] = conv; + this.calc_positions(); + } + }else{ + conv.set("session", session); + } + // if force open, broadcast it + if(options['force_open'] && active_session.state !== 'open'){ + conv.session_update_state('open'); + } + // apply the focus + if (options['focus']){ + conv.focus(); + } + return conv; + }, + session_delete: function(uuid){ + delete this.sessions[uuid]; + this.calc_positions(); + }, + message_receive: function(message) { + var self = this; + var session_id = message.to_id[0]; + var uuid = message.to_id[1]; + var from_id = message['from_id'] ? message['from_id'][0] : false; + if (! this.get("window_focus") && from_id != this.get_current_uid()) { + this.set("waiting_messages", this.get("waiting_messages") + 1); + } + this._message_receive(message); + }, + _message_receive: function(message){ + var uuid = message.to_id[1]; + var conv = this.sessions[uuid]; + conv.message_receive(message); + }, + // others + get_current_uid: function(){ + return session && !_.isUndefined(session.uid) ? session.uid : false; + }, + calc_positions: function() { + var self = this; + var current = this.get("right_offset"); + _.each(this.sessions, function(s) { + s.set("bottom_position", self.get("bottom_offset")); + s.set("right_position", current); + current += s.$().outerWidth(true); + }); + }, + destroy: function() { + $(window).off("unload", this.unload); + $(window).off("focus", this.window_focus); + $(window).off("blur", this.window_blur); + return this._super(); + } +}); + + +var Conversation = Widget.extend({ + className: "openerp_style oe_im_chatview", + events: { + "keydown input": "keydown", + "click .oe_im_chatview_close": "click_close", + "click .oe_im_chatview_header": "click_header" + }, + init: function(parent, c_manager, session, options) { + this._super(parent); + this.c_manager = c_manager; + this.options = options || {}; + this.loading_history = true; + this.set("messages", []); + this.set("session", session); + this.set("right_position", 0); + this.set("bottom_position", 0); + this.set("pending", 0); + this.inputPlaceholder = this.options.defaultInputPlaceholder; + }, + start: function() { + var self = this; + self._super.apply(this, arguments); + self.prepare_action_menu(); + // ui + self.$().append(QWeb.render("im_chat.Conversation", {widget: self})); + self.on("change:right_position", self, self.calc_pos); + self.on("change:bottom_position", self, self.calc_pos); + self.on("change:pending", self, _.bind(function() { + if (self.get("pending") === 0) { + self.$(".nbr_messages").text(""); + } else { + self.$(".nbr_messages").text("(" + self.get("pending") + ")"); + } + }, self)); + self.full_height = self.$().height(); + self.calc_pos(); + // business + self.bind_action_menu(); + self.on("change:session", self, self.session_update); + self.on("change:messages", self, self.render_messages); + self.$('.oe_im_chatview_content').on('scroll',function(){ + if($(this).scrollTop() === 0){ + self.message_history(); + } + }); + if(self.options['load_history']){ // load history if asked + self.message_history(); + } + // prepare the header and the correct state + self.session_update(); + }, + // action menu + _add_action: function(label, style_class, icon_fa_class, callback){ + this.actions.push({ + 'label': label, + 'class': style_class, + 'icon_class': icon_fa_class, + 'callback': callback, + }); + }, + prepare_action_menu: function(){ + // override this method to add action with _add_action() + this.actions = []; + }, + bind_action_menu: function(){ + var self = this; + _.each(this.actions, function(action){ + if(action.callback){ + self.$('.button_option_group .' + action.class).on('click', _.bind(action.callback, self)); + } + }); + }, + // ui + show: function(){ + this.$().animate({ + height: this.full_height + }); + this.set("pending", 0); + }, + hide: function(){ + this.$().animate({ + height: this.$(".oe_im_chatview_header").outerHeight() + }); + }, + calc_pos: function() { + this.$().css("right", this.get("right_position")); + this.$().css("bottom", this.get("bottom_position")); + }, + // session + session_update_state: function(state){ + var session = this.get('session'); + session.state = state; + this.set('session', session); + }, + session_update: function(){ + // built the name + var names = []; + _.each(this.get("session").users, function(user){ + if( (session.uid !== user.id) && !(_.isUndefined(session.uid) && !user.id) ){ + names.push(user.name); + } + }); + this.$(".header_name").text(names.join(", ")); + this.$(".header_name").attr('title', names.join(", ")); + // update the fold state + if(this.get("session").state){ + if(this.get("session").state === 'closed'){ + this.destroy(); + }else{ + if(this.get("session").state === 'open'){ + this.show(); + }else{ + this.hide(); + } + } + } + }, + // messages + message_history: function(){ + var self = this; + if(this.loading_history){ + var data = {uuid: self.get("session").uuid, limit: NBR_LIMIT_HISTORY}; + var lastid = _.first(this.get("messages")) ? _.first(this.get("messages")).id : false; + if(lastid){ + data.last_id = lastid; + } + session.rpc("/im_chat/history", data).then(function(messages){ + if(messages){ + self.insert_messages(messages); + if(messages.length != NBR_LIMIT_HISTORY){ + self.loading_history = false; + } + }else{ + self.loading_history = false; + } + }); + } + }, + message_receive: function(message) { + if (this.get('session').state === 'open') { + this.set("pending", 0); + } else { + this.set("pending", this.get("pending") + 1); + } + this.insert_messages([message]); + this._go_bottom(); + }, + message_send: function(message, type) { + var self = this; + var send_it = function() { + return session.rpc("/im_chat/post", {uuid: self.get("session").uuid, message_type: type, message_content: message}); + }; + var tries = 0; + send_it().fail(function(error, e) { + e.preventDefault(); + tries += 1; + if (tries < 3) + return send_it(); + }); + }, + insert_messages: function(messages){ + var self = this; + // avoid duplicated messages + messages = _.filter(messages, function(m){ return !_.contains(_.pluck(self.get("messages"), 'id'), m.id) ; }); + // escape the message content and set the timezone + _.map(messages, function(m){ + if(!m.from_id){ + m.from_id = [false,self. get_anonymous_name()]; + } + m.create_date = moment(time.str_to_datetime(m.create_date)).format('YYYY-MM-DD HH:mm:ss'); + return m; + }); + this.set("messages", _.sortBy(this.get("messages").concat(messages), function(m){ return m.id; })); + }, + render_messages: function(){ + var self = this; + var res = {}; + var last_date_day, last_user_id = -1; + _.each(this.get("messages"), function(current){ + // add the url of the avatar for all users in the conversation + current.from_id[2] = session.url(_.str.sprintf("/im_chat/image/%s/%s", self.get('session').uuid, current.from_id[0])); + var date_day = current.create_date.split(" ")[0]; + if(date_day !== last_date_day){ + res[date_day] = []; + last_user_id = -1; + } + last_date_day = date_day; + if(current.type == "message"){ // traditionnal message + if(last_user_id === current.from_id[0]){ + _.last(res[date_day]).push(current); + }else{ + res[date_day].push([current]); + } + last_user_id = current.from_id[0]; + }else{ // meta message + res[date_day].push([current]); + last_user_id = -1; + } + }); + // render and set the content of the chatview + // TODO jem : when refactoring this, don't forget to pre-process date in this function before render quweb template + // since, moment will not be define in qweb on the website pages, because the helper (see csn) is in core.js and cannot be + // imported in the frontend. + this.$('.oe_im_chatview_content_bubbles').html($(QWeb.render("im_chat.Conversation_content", {"list": res}))); + this._go_bottom(); + }, + // utils and event + get_anonymous_name: function(){ + var name = this.options["defaultUsername"]; + _.each(this.get('session').users, function(u){ + if(!u.id){ + name = u.name; + } + }); + return name; + }, + keydown: function(e) { + if(e && e.which == 27) { + if(this.$el.prev().find('.oe_im_chatview_input').length > 0){ + this.$el.prev().find('.oe_im_chatview_input').focus(); + }else{ + this.$el.next().find('.oe_im_chatview_input').focus(); + } + e.stopPropagation(); + this.session_update_state('closed'); + } + if(e && e.which !== 13) { + return; + } + var mes = this.$("input").val(); + if (! mes.trim()) { + return; + } + this.$("input").val(""); + this.message_send(mes, "message"); + }, + _go_bottom: function() { + this.$(".oe_im_chatview_content").scrollTop(this.$(".oe_im_chatview_content").get(0).scrollHeight); + }, + focus: function() { + this.$(".oe_im_chatview_input").focus(); + }, + click_header: function(event){ + var classes = event.target.className.split(' '); + if(_.contains(classes, 'header_name') || _.contains(classes, 'oe_im_chatview_header')){ + this.session_update_state(); + } + }, + click_close: function(event) { + this.session_update_state('closed'); + }, + destroy: function() { + this.trigger("destroyed", this.get('session').uuid); + return this._super(); + } +}); + + +return { + Conversation: Conversation, + ConversationManager: ConversationManager, +}; + +}); \ No newline at end of file diff --git a/addons/mail/static/src/xml/mail_chat_common.xml b/addons/mail/static/src/xml/mail_chat_common.xml new file mode 100644 index 00000000000..eb479863b3e --- /dev/null +++ b/addons/mail/static/src/xml/mail_chat_common.xml @@ -0,0 +1,103 @@ + + + +
    + + + + +
    + + + + +
    +
    ×
    +
    +
    +
    +
    +
    +
    + + + + +
    + +
    + + + + + + + + + + + + + + + +
    +
    + +
    +
    +
    + +
    +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +
    +
    +
    + + +
    + + + + + +
    +
    + +
    +
    + + +
    +
    +
    +
    +
    + +
  • + + + +
  • +
    + diff --git a/addons/mail/views/mail_shortcode_views.xml b/addons/mail/views/mail_shortcode_views.xml new file mode 100644 index 00000000000..e281950d102 --- /dev/null +++ b/addons/mail/views/mail_shortcode_views.xml @@ -0,0 +1,48 @@ + + + + + + + Chat Shortcode + im_chat.shortcode + tree,form + +

    + Click to define a new chat shortcode. +

    + A shortcode is a keyboard shortcut. For instance, you type #gm and it will be transformed into "Good Morning". +

    +
    +
    + + + im_chat.shortcode.tree + im_chat.shortcode + + + + + + + + + + + im_chat.shortcode.form + im_chat.shortcode + +
    + + + + + + + +
    +
    +
    + +
    +
    diff --git a/addons/mail/views/mail_templates.xml b/addons/mail/views/mail_templates.xml index 7472cdd39ff..8fa17f867d7 100644 --- a/addons/mail/views/mail_templates.xml +++ b/addons/mail/views/mail_templates.xml @@ -11,6 +11,11 @@ + + + + + diff --git a/openerp/tools/mail.py b/openerp/tools/mail.py index 3c22262e14c..f358044bd08 100644 --- a/openerp/tools/mail.py +++ b/openerp/tools/mail.py @@ -514,6 +514,24 @@ def html2plaintext(html, body_id=None, encoding='utf-8'): return html +def html_escape_keep_url(self, message): + """ Escape the message and transform the url into clickable link + :param string message: the message to escape url and transform them into clickable link + :returns the escaped message + :rtype : string + """ + safe_message = "" + first = 0 + last = 0 + for m in re.finditer('(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?', message): + last = m.start() + safe_message += cgi.escape(message[first:last]) + safe_message += '%s' % (cgi.escape(m.group(0)), m.group(0)) + first = m.end() + last = m.end() + safe_message += cgi.escape(message[last:]) + return safe_message + def plaintext2html(text, container_tag=False): """ Convert plaintext into html. Content of the text is escaped to manage html entities, using cgi.escape(). @@ -587,7 +605,7 @@ def append_content_to_html(html, content, plaintext=True, preserve=False, contai #---------------------------------------------------------- # matches any email in a body of text -email_re = re.compile(r"""([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6})""", re.VERBOSE) +email_re = re.compile(r"""([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6})""", re.VERBOSE) # matches a string containing only one email single_email_re = re.compile(r"""^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$""", re.VERBOSE) -- cgit v1.2.3