diff --git a/addons/mail/models/mail_activity.py b/addons/mail/models/mail_activity.py index 771194d4..072e15b8 100644 --- a/addons/mail/models/mail_activity.py +++ b/addons/mail/models/mail_activity.py @@ -2,6 +2,7 @@ # Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details. from datetime import date, datetime, timedelta +import pytz from flectra import api, exceptions, fields, models, _ @@ -77,7 +78,7 @@ class MailActivity(models.Model): summary = fields.Char('Summary') note = fields.Html('Note') feedback = fields.Html('Feedback') - date_deadline = fields.Date('Due Date', index=True, required=True, default=fields.Date.today) + date_deadline = fields.Date('Due Date', index=True, required=True, default=fields.Date.context_today) # description user_id = fields.Many2one( 'res.users', 'Assigned to', @@ -108,8 +109,16 @@ class MailActivity(models.Model): @api.depends('date_deadline') def _compute_state(self): - today = date.today() + today_default = date.today() + for record in self.filtered(lambda activity: activity.date_deadline): + today = today_default + tz = record.user_id.sudo().tz + if tz: + today_utc = pytz.UTC.localize(datetime.utcnow()) + today_tz = today_utc.astimezone(pytz.timezone(tz)) + today = date(year=today_tz.year, month=today_tz.month, day=today_tz.day) + date_deadline = fields.Date.from_string(record.date_deadline) diff = (date_deadline - today) if diff.days == 0: @@ -132,7 +141,8 @@ class MailActivity(models.Model): @api.onchange('recommended_activity_type_id') def _onchange_recommended_activity_type_id(self): - self.activity_type_id = self.recommended_activity_type_id + if self.recommended_activity_type_id: + self.activity_type_id = self.recommended_activity_type_id @api.multi def _check_access(self, operation): diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py index 4b9ab808..1a58d638 100644 --- a/addons/mail/models/mail_channel.py +++ b/addons/mail/models/mail_channel.py @@ -505,7 +505,15 @@ class Channel(models.Model): partners_to_add = partners - channel.channel_partner_ids channel.write({'channel_last_seen_partner_ids': [(0, 0, {'partner_id': partner_id}) for partner_id in partners_to_add.ids]}) for partner in partners_to_add: - notification = _('
joined #%s
') % (self.id, self.name,) + if partner.id != self.env.user.partner_id.id: + notification = _('
%(author)s invited %(new_partner)s to #%(channel_name)s
') % { + 'author': self.env.user.display_name, + 'new_partner': partner.display_name, + 'channel_id': channel.id, + 'channel_name': channel.name, + } + else: + notification = _('
joined #%s
') % (channel.id, channel.name,) self.message_post(body=notification, message_type="notification", subtype="mail.mt_comment", author_id=partner.id) # broadcast the channel header to the added partner @@ -674,7 +682,7 @@ class Channel(models.Model): msg += _(" This channel is private. People must be invited to join it.") else: channel_partners = self.env['mail.channel.partner'].search([('partner_id', '!=', partner.id), ('channel_id', '=', self.id)]) - msg = _("You are in a private conversation with @%s.") % channel_partners[0].partner_id.name + msg = _("You are in a private conversation with @%s.") % (channel_partners[0].partner_id.name if channel_partners else _('Anonymous')) msg += _("""

You can mention someone by typing @username, this will grab its attention.
You can mention a channel by typing #channel.
diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py index fa0b502e..276b11ec 100644 --- a/addons/mail/models/mail_message.py +++ b/addons/mail/models/mail_message.py @@ -5,6 +5,7 @@ import logging import re from email.utils import formataddr +from openerp.http import request from flectra import _, api, fields, models, modules, SUPERUSER_ID, tools from flectra.exceptions import UserError, AccessError @@ -299,11 +300,12 @@ class Message(models.Model): # 2. Attachments as SUPERUSER, because could receive msg and attachments for doc uid cannot see attachments_data = attachments.sudo().read(['id', 'datas_fname', 'name', 'mimetype']) + safari = request and request.httprequest.user_agent.browser == 'safari' attachments_tree = dict((attachment['id'], { 'id': attachment['id'], 'filename': attachment['datas_fname'], 'name': attachment['name'], - 'mimetype': attachment['mimetype'], + 'mimetype': 'application/octet-stream' if safari and 'video' in attachment['mimetype'] else attachment['mimetype'], }) for attachment in attachments_data) # 3. Tracking values @@ -422,7 +424,18 @@ class Message(models.Model): subtype_ids = [msg['subtype_id'][0] for msg in message_values if msg['subtype_id']] subtypes = self.env['mail.message.subtype'].sudo().browse(subtype_ids).read(['internal', 'description']) subtypes_dict = dict((subtype['id'], subtype) for subtype in subtypes) + + # fetch notification status + notif_dict = {} + notifs = self.env['mail.notification'].sudo().search([('mail_message_id', 'in', list(mid for mid in message_tree)), ('is_read', '=', False)]) + for notif in notifs: + mid = notif.mail_message_id.id + if not notif_dict.get(mid): + notif_dict[mid] = {'partner_id': list()} + notif_dict[mid]['partner_id'].append(notif.res_partner_id.id) + for message in message_values: + message['needaction_partner_ids'] = notif_dict.get(message['id'], dict()).get('partner_id', []) message['is_note'] = message['subtype_id'] and subtypes_dict[message['subtype_id'][0]]['internal'] message['subtype_description'] = message['subtype_id'] and subtypes_dict[message['subtype_id'][0]]['description'] if message['model'] and self.env[message['model']]._original_module: diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index 755172e7..cdceb508 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -126,7 +126,8 @@ class MailThread(models.AbstractModel): followers = self.env['mail.followers'].sudo().search([ ('res_model', '=', self._name), ('partner_id', operator, operand)]) - return [('id', 'in', followers.mapped('res_id'))] + # using read() below is much faster than followers.mapped('res_id') + return [('id', 'in', [res['res_id'] for res in followers.read(['res_id'])])] @api.model def _search_follower_channels(self, operator, operand): @@ -139,7 +140,8 @@ class MailThread(models.AbstractModel): followers = self.env['mail.followers'].sudo().search([ ('res_model', '=', self._name), ('channel_id', operator, operand)]) - return [('id', 'in', followers.mapped('res_id'))] + # using read() below is much faster than followers.mapped('res_id') + return [('id', 'in', [res['res_id'] for res in followers.read(['res_id'])])] @api.multi @api.depends('message_follower_ids') @@ -149,7 +151,8 @@ class MailThread(models.AbstractModel): ('res_id', 'in', self.ids), ('partner_id', '=', self.env.user.partner_id.id), ]) - following_ids = followers.mapped('res_id') + # using read() below is much faster than followers.mapped('res_id') + following_ids = [res['res_id'] for res in followers.read(['res_id'])] for record in self: record.message_is_follower = record.id in following_ids @@ -161,9 +164,11 @@ class MailThread(models.AbstractModel): ]) # Cases ('message_is_follower', '=', True) or ('message_is_follower', '!=', False) if (operator == '=' and operand) or (operator == '!=' and not operand): - return [('id', 'in', followers.mapped('res_id'))] + # using read() below is much faster than followers.mapped('res_id') + return [('id', 'in', [res['res_id'] for res in followers.read(['res_id'])])] else: - return [('id', 'not in', followers.mapped('res_id'))] + # using read() below is much faster than followers.mapped('res_id') + return [('id', 'not in', [res['res_id'] for res in followers.read(['res_id'])])] @api.multi def _get_message_unread(self): @@ -1373,7 +1378,13 @@ class MailThread(models.AbstractModel): located in tools. """ if not body: return body, attachments - root = lxml.html.fromstring(body) + try: + root = lxml.html.fromstring(body) + except ValueError: + # In case the email client sent XHTML, fromstring will fail because 'Unicode strings + # with encoding declaration are not supported'. + root = lxml.html.fromstring(body.encode('utf-8')) + postprocessed = False to_remove = [] for node in root.iter(): diff --git a/addons/mail/models/res_users.py b/addons/mail/models/res_users.py index 6b421c88..ffc9dcae 100644 --- a/addons/mail/models/res_users.py +++ b/addons/mail/models/res_users.py @@ -58,6 +58,8 @@ class Users(models.Model): # create a welcome message user._create_welcome_message() + # Auto-subscribe to channels + self.env['mail.channel'].search([('group_ids', 'in', user.groups_id.ids)])._subscribe_users() return user @api.multi @@ -127,16 +129,19 @@ class Users(models.Model): def activity_user_count(self): query = """SELECT m.id, count(*), act.res_model as model, CASE - WHEN now()::date - act.date_deadline::date = 0 Then 'today' - WHEN now()::date - act.date_deadline::date > 0 Then 'overdue' - WHEN now()::date - act.date_deadline::date < 0 Then 'planned' + WHEN %(today)s::date - act.date_deadline::date = 0 Then 'today' + WHEN %(today)s::date - act.date_deadline::date > 0 Then 'overdue' + WHEN %(today)s::date - act.date_deadline::date < 0 Then 'planned' END AS states FROM mail_activity AS act JOIN ir_model AS m ON act.res_model_id = m.id - WHERE user_id = %s + WHERE user_id = %(user_id)s GROUP BY m.id, states, act.res_model; """ - self.env.cr.execute(query, [self.env.uid]) + self.env.cr.execute(query, { + 'today': fields.Date.context_today(self), + 'user_id': self.env.uid, + }) activity_data = self.env.cr.dictfetchall() model_ids = [a['id'] for a in activity_data] model_names = {n[0]:n[1] for n in self.env['ir.model'].browse(model_ids).name_get()} diff --git a/addons/mail/static/src/js/activity.js b/addons/mail/static/src/js/activity.js index 555ab8e9..67880122 100644 --- a/addons/mail/static/src/js/activity.js +++ b/addons/mail/static/src/js/activity.js @@ -32,6 +32,7 @@ function _readActivities(self, ids) { model: 'mail.activity', method: 'read', args: [ids], + context: (self.record && self.record.getContext()) || self.getSession().user_context, }).then(function (activities) { // convert create_date and date_deadline to moments _.each(activities, function (activity) { @@ -108,6 +109,7 @@ var AbstractActivityField = AbstractField.extend({ method: 'action_feedback', args: [[id]], kwargs: {feedback: feedback}, + context: this.record.getContext(), }); }, _scheduleActivity: function (id, previous_activity_type_id, callback) { diff --git a/addons/mail/static/src/js/chat_manager.js b/addons/mail/static/src/js/chat_manager.js index e429054a..020df3d6 100644 --- a/addons/mail/static/src/js/chat_manager.js +++ b/addons/mail/static/src/js/chat_manager.js @@ -149,7 +149,16 @@ function make_message (data) { _.each(_.keys(emoji_substitutions), function (key) { var escaped_key = String(key).replace(/([.*+?=^!:${}()|[\]\/\\])/g, '\\$1'); var regexp = new RegExp("(?:^|\\s|<[a-z]*>)(" + escaped_key + ")(?=\\s|$|)", "g"); + var msg_bak = msg.body; msg.body = msg.body.replace(regexp, ' '+emoji_substitutions[key]+' '); + + // Idiot-proof limit. If the user had the amazing idea of copy-pasting thousands of emojis, + // the image rendering can lead to memory overflow errors on some browsers (e.g. Chrome). + // Set an arbitrary limit to 200 from which we simply don't replace them (anyway, they are + // already replaced by the unicode counterpart). + if (_.str.count(msg.body, 'o_mail_emoji') > 200) { + msg.body = msg_bak; + } }); function property_descr(channel) { diff --git a/addons/mail/static/src/js/chat_window.js b/addons/mail/static/src/js/chat_window.js index 4a0d30ef..7587dc72 100644 --- a/addons/mail/static/src/js/chat_window.js +++ b/addons/mail/static/src/js/chat_window.js @@ -16,7 +16,8 @@ var HEIGHT_FOLDED = '34px'; return Widget.extend({ template: "mail.ChatWindow", custom_events: { - escape_pressed: '_onEscapePressed' + escape_pressed: '_onEscapePressed', + document_viewer_closed: '_onDocumentViewerClose', }, events: { 'click .o_chat_composer': '_onComposerClick', @@ -180,6 +181,9 @@ return Widget.extend({ } this.focus_input(); }, + _onDocumentViewerClose: function (ev) { + this.focus_input(); + }, /** * @private */ diff --git a/addons/mail/static/src/js/composer.js b/addons/mail/static/src/js/composer.js index 110e0f5e..b0eb8065 100644 --- a/addons/mail/static/src/js/composer.js +++ b/addons/mail/static/src/js/composer.js @@ -514,6 +514,9 @@ var BasicComposer = Widget.extend(chat_mixin, { on_click_add_attachment: function () { this.$('input.o_input_file').click(); this.$input.focus(); + // set ignoreEscape to avoid escape_pressed event when file selector dialog is opened + // when user press escape to cancel file selector dialog then escape_pressed event should not be trigerred + this.ignoreEscape = true; }, setState: function (state) { @@ -565,6 +568,8 @@ var BasicComposer = Widget.extend(chat_mixin, { if (this.mention_manager.is_open()) { event.stopPropagation(); this.mention_manager.reset_suggestions(); + } else if (this.ignoreEscape) { + this.ignoreEscape = false; } else { this.trigger_up("escape_pressed"); } @@ -789,6 +794,7 @@ var BasicComposer = Widget.extend(chat_mixin, { * @param {MouseEvent} event */ _onAttachmentView: function (event) { + event.stopPropagation(); var activeAttachmentID = $(event.currentTarget).data('id'); var attachments = this.get('attachment_ids'); if (activeAttachmentID) { diff --git a/addons/mail/static/src/js/document_viewer.js b/addons/mail/static/src/js/document_viewer.js index 32bb0c32..81f2989f 100644 --- a/addons/mail/static/src/js/document_viewer.js +++ b/addons/mail/static/src/js/document_viewer.js @@ -25,6 +25,7 @@ var DocumentViewer = Widget.extend({ 'DOMMouseScroll .o_viewer_content': '_onScroll', // Firefox 'mousewheel .o_viewer_content': '_onScroll', // Chrome, Safari, IE 'keydown': '_onKeydown', + 'keyup': '_onKeyUp', 'mousedown .o_viewer_img': '_onStartDrag', 'mousemove .o_viewer_content': '_onDrag', 'mouseup .o_viewer_content': '_onEndDrag' @@ -152,6 +153,7 @@ var DocumentViewer = Widget.extend({ _onClose: function (e) { e.preventDefault(); this.$el.modal('hide'); + this.trigger_up('document_viewer_closed'); }, /** * When popup close complete destroyed modal even DOM footprint too @@ -232,6 +234,20 @@ var DocumentViewer = Widget.extend({ break; } }, + /** + * Close popup on ESCAPE keyup + * + * @private + * @param {KeyEvent} e + */ + _onKeyUp: function (e) { + switch (e.which) { + case $.ui.keyCode.ESCAPE: + e.preventDefault(); + this._onClose(e); + break; + } + }, /** * @private * @param {MouseEvent} e diff --git a/addons/mail/static/src/js/systray.js b/addons/mail/static/src/js/systray.js index 0f14edd0..a56789ec 100644 --- a/addons/mail/static/src/js/systray.js +++ b/addons/mail/static/src/js/systray.js @@ -122,7 +122,7 @@ var MessagingMenu = Widget.extend({ if (channelID === 'channel_inbox') { var resID = $(event.currentTarget).data('res_id'); var resModel = $(event.currentTarget).data('res_model'); - if (resModel && resID) { + if (resModel && resModel !== 'mail.channel' && resID) { this.do_action({ type: 'ir.actions.act_window', res_model: resModel, @@ -130,7 +130,11 @@ var MessagingMenu = Widget.extend({ res_id: resID }); } else { - this.do_action('mail.mail_channel_action_client_chat', {clear_breadcrumbs: true}) + var clientChatOptions = {clear_breadcrumbs: true}; + if (resModel && resModel === 'mail.channel' && resID) { + clientChatOptions.active_id = resID; + } + this.do_action('mail.mail_channel_action_client_chat', clientChatOptions) .then(function () { self.trigger_up('hide_app_switcher'); core.bus.trigger('change_menu_section', chat_manager.get_discuss_menu_id()); diff --git a/addons/mail/static/src/js/thread.js b/addons/mail/static/src/js/thread.js index 0343b425..257a12d3 100644 --- a/addons/mail/static/src/js/thread.js +++ b/addons/mail/static/src/js/thread.js @@ -328,6 +328,7 @@ var Thread = Widget.extend({ * @param {MouseEvent} event */ _onAttachmentView: function (event) { + event.stopPropagation(); var activeAttachmentID = $(event.currentTarget).data('id'); if (activeAttachmentID) { var attachmentViewer = new DocumentViewer(this, this.attachments, activeAttachmentID); diff --git a/addons/mail/static/src/js/utils.js b/addons/mail/static/src/js/utils.js index dc4c9a6e..5d75e335 100644 --- a/addons/mail/static/src/js/utils.js +++ b/addons/mail/static/src/js/utils.js @@ -60,7 +60,7 @@ function _parse_and_transform(nodes, transform_function) { // Suggested URL Javascript regex of http://stackoverflow.com/questions/3809401/what-is-a-good-regular-expression-to-match-a-url // Adapted to make http(s):// not required if (and only if) www. is given. So `should.notmatch` does not match. -var url_regexp = /\b(?:https?:\/\/|(www\.))[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,13}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/gi; +var url_regexp = /\b(?:https?:\/\/\d{1,3}(?:\.\d{1,3}){3}|(?:https?:\/\/|(?:www\.))[-a-z0-9@:%._\+~#=]{2,256}\.[a-z]{2,13})\b(?:[-a-z0-9@:%_\+.~#?&'$//=]*)/gi; function linkify(text, attrs) { attrs = attrs || {}; if (attrs.target === undefined) { @@ -70,7 +70,7 @@ function linkify(text, attrs) { return key + '="' + _.escape(value) + '"'; }).join(' '); return text.replace(url_regexp, function (url) { - var href = (!/^(f|ht)tps?:\/\//i.test(url)) ? "http://" + url : url; + var href = (!/^https?:\/\//i.test(url)) ? "http://" + url : url; return '' + url + ''; }); } @@ -92,6 +92,7 @@ function strip_html (node, transform_children) { function inline (node, transform_children) { if (node.nodeType === 3) return node.data; + if (node.nodeType === 8) return ""; if (node.tagName === "BR") return " "; if (node.tagName.match(/^(A|P|DIV|PRE|BLOCKQUOTE)$/)) return transform_children(); node.innerHTML = transform_children(); @@ -100,16 +101,23 @@ function inline (node, transform_children) { // Parses text to find email: Tagada -> [Tagada, address@mail.fr] or False function parse_email (text) { - var result = text.match(/(.*)<(.*@.*)>/); - if (result) { - return [_.str.trim(result[1]), _.str.trim(result[2])]; + if (text){ + var result = text.match(/(.*)<(.*@.*)>/); + if (result) { + return [_.str.trim(result[1]), _.str.trim(result[2])]; + } + result = text.match(/(.*@.*)/); + if (result) { + return [_.str.trim(result[1]), _.str.trim(result[1])]; + } + return [text, false]; } - result = text.match(/(.*@.*)/); + /* result = text.match(/(.*@.*)/); if (result) { return [_.str.trim(result[1]), _.str.trim(result[1])]; } return [text, false]; -} +}*/ // Replaces textarea text into html text (add

, ) // TDE note : should be done server-side, in Python -> use mail.compose.message ? diff --git a/addons/mail/static/src/less/composer.less b/addons/mail/static/src/less/composer.less index 74540cc0..6cfbee3f 100644 --- a/addons/mail/static/src/less/composer.less +++ b/addons/mail/static/src/less/composer.less @@ -51,7 +51,7 @@ border: none; } .o_composer_button_full_composer { - .o-position-absolute(0, 0); + .o-position-absolute(auto, 0); } @media (max-width: @screen-xs-max) { .o_composer_button_send { @@ -136,7 +136,7 @@ &.o_chat_inline_composer { .o_composer_container { - .o-flex(1, 0, auto); + .o-flex(1, 1, auto); } .o_composer { padding: @o-mail-chatter-gap @o-mail-chatter-gap 0 @o-mail-chatter-gap; @@ -339,6 +339,7 @@ } .o_modal_fullscreen { + z-index: @o-chat-window-zindex + 1; // To overlap chat window .o_viewer_content { width: 100%; height: 100%; diff --git a/addons/mail/static/src/less/thread.less b/addons/mail/static/src/less/thread.less index 95303612..8b2be015 100644 --- a/addons/mail/static/src/less/thread.less +++ b/addons/mail/static/src/less/thread.less @@ -107,7 +107,7 @@ &.o_mail_note { background-color: @mail-thread-note; padding-left: @grid-gutter-width*0.3; - border-bottom: 1px solid @gray-lighter-dark; + border-bottom: 1px solid @gray-lighter-darker; } .o_mail_subject { diff --git a/addons/mail/static/src/xml/thread.xml b/addons/mail/static/src/xml/thread.xml index 9a707693..180eac17 100644 --- a/addons/mail/static/src/xml/thread.xml +++ b/addons/mail/static/src/xml/thread.xml @@ -76,7 +76,7 @@ -

-
+