[MERGE] mail, *: several UX/UI improvements
*: bus, im_livechat, mail_bot, website_livechat
Several improvements for Discuss app:
1. Improved "is typing..." notification
2. Merged private & public channels
3. Quick channel search
4. No push notification right after accepting them, and transparent odoo bot icon in native notification
5. Selected emojis are inserted in composer based on cursor position
6. Set surname to people for DMs name
7. Discard replying to a message in Inbox
8. Broadcast "seen" notification ('v' and 'vv' on messages)
[1] [image1](https://drive.google.com/file/d/11_H6ZJh0wFaegAV0fTLdc68z37nhVKnW/view?usp=sharing), [image2](https://drive.google.com/file/d/1s9g_hUCMesuTLAqpeGMwLWzHCYnzanr6/view?usp=sharing)
[2] [image1](https://drive.google.com/file/d/1WUBqc2lScfWV8Yrtq6Zk4MO4jC0CdaiG/view?usp=sharing), [image2](https://drive.google.com/file/d/13XVcW0Nookwea_4xz02mIwPi5dPhXu18/view?usp=sharing)
[3] [image1](https://drive.google.com/file/d/1FuV3xH9cK6qR7ljd5vELOHc4icvHIJTF/view?usp=sharing), [image2](https://drive.google.com/file/d/1hwuu5ejlj9ou02NF5XmL-XHTiD-Ty6XT/view?usp=sharing)
[4] [image](https://drive.google.com/file/d/1si1TYtCpdyOgw9IRaMWmxuUb1d8M4ccF/view?usp=sharing)
[5] [video](https://drive.google.com/file/d/19knQ47F1wzlNvxT2nJH3A9V34wO1D9Bn/view?usp=sharing)
[6] [image1](https://drive.google.com/file/d/11ot_DtySN2cIN_bM8n7fhFV2JXNoz0lH/view?usp=sharing), [image2](https://drive.google.com/file/d/1aNyapzKePIRg903woCo3kXJhy-ZFwfEG/view?usp=sharing), [image3](https://drive.google.com/file/d/1AtVlOG67YjsS8ax1ErWhXfSDOtKhLQhC/view?usp=sharing)
[7] [image](https://drive.google.com/file/d/1U-VEut7M7-t9WzdKVagfyy6ZrbOJrflp/view?usp=sharing)
[8] [image1](https://drive.google.com/file/d/1_32q8iqPBhW4sPhAtY5mIO968gj1o9o-/view?usp=sharing), [image2](https://drive.google.com/file/d/1Ne6HMEgJxHAWYN3e3SlrF6Qqg70LDvAW/view?usp=sharing)
Task-ID 1881001
closes odoo/odoo#29191
This commit is contained in:
@@ -80,7 +80,7 @@ var BusService = CrossTab.extend(ServicesMixin, {
|
||||
* @param {function} [callback] if given callback will be called when user clicks on notification
|
||||
*/
|
||||
_sendNativeNotification: function (title, content, callback) {
|
||||
var notification = new Notification(title, {body: content, icon: "/mail/static/src/img/odoobot.png"});
|
||||
var notification = new Notification(title, {body: content, icon: "/mail/static/src/img/odoobot_transparent.png"});
|
||||
notification.onclick = function () {
|
||||
window.focus();
|
||||
if (this.cancel) {
|
||||
|
||||
@@ -223,7 +223,7 @@ QUnit.module('Bus', {
|
||||
var pollDeferredMaster = $.Deferred();
|
||||
|
||||
var parentMaster = new Widget();
|
||||
testUtils.addMockEnvironment(parentMaster, {
|
||||
testUtils.mock.addMockEnvironment(parentMaster, {
|
||||
data: {},
|
||||
services: {
|
||||
bus_service: BusService,
|
||||
@@ -255,7 +255,7 @@ QUnit.module('Bus', {
|
||||
setTimeout(function () {
|
||||
var parentSlave = new Widget();
|
||||
var pollDeferredSlave = $.Deferred();
|
||||
testUtils.addMockEnvironment(parentSlave, {
|
||||
testUtils.mock.addMockEnvironment(parentSlave, {
|
||||
data: {},
|
||||
services: {
|
||||
bus_service: BusService,
|
||||
|
||||
@@ -360,7 +360,7 @@ var LivechatButton = Widget.extend({
|
||||
*/
|
||||
_onUpdatedTypingPartners: function (ev) {
|
||||
ev.stopPropagation();
|
||||
this._chatWindow.renderTypingNotificationBar();
|
||||
this._chatWindow.renderHeader();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
|
||||
@@ -4,12 +4,19 @@ odoo.define('im_livechat.Discuss', function (require) {
|
||||
var Discuss = require('mail.Discuss');
|
||||
|
||||
Discuss.include({
|
||||
_renderSidebar: function (options) {
|
||||
// Override to sort livechat threads by last message's date
|
||||
var threadPartition = _.partition(options.threads, function (thread) {
|
||||
return thread.getType() === 'livechat';
|
||||
/**
|
||||
* Override to sort livechats by last message's date
|
||||
*
|
||||
* @override
|
||||
* @private
|
||||
* @param {mail.model.Channel[]} channels
|
||||
* @returns {mail.model.Channel[]}
|
||||
*/
|
||||
_sortChannels: function (channels) {
|
||||
var partition = _.partition(channels, function (channel) {
|
||||
return channel.getType() === 'livechat';
|
||||
});
|
||||
threadPartition[0].sort(function (c1, c2) {
|
||||
partition[0].sort(function (c1, c2) {
|
||||
if (!c1.hasMessages()) {
|
||||
return -1;
|
||||
} else if (!c2.hasMessages()) {
|
||||
@@ -17,8 +24,8 @@ Discuss.include({
|
||||
}
|
||||
return c2.getLastMessage().getDate().diff(c1.getLastMessage().getDate());
|
||||
});
|
||||
options.threads = threadPartition[0].concat(threadPartition[1]);
|
||||
return this._super(options);
|
||||
channels = partition[0].concat(partition[1]);
|
||||
return this._super(channels);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -166,14 +166,14 @@
|
||||
<!-- threads -->
|
||||
<script type="text/javascript" src="/mail/static/src/js/models/threads/abstract_thread.js"></script>
|
||||
<script type="text/javascript" src="/im_livechat/static/src/js/models/website_livechat.js"></script>
|
||||
<!-- thread typing mixin -->
|
||||
<script type="text/javascript" src="/mail/static/src/js/models/threads/thread_typing_mixin/thread_typing_mixin.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/models/threads/thread_typing_mixin/timer.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/models/threads/thread_typing_mixin/timers.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/models/threads/thread_typing_mixin/cc_throttle_function.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/models/threads/mixins/thread_typing_mixin.js"></script>
|
||||
<!-- messages -->
|
||||
<script type="text/javascript" src="/mail/static/src/js/models/messages/abstract_message.js"></script>
|
||||
<script type="text/javascript" src="/im_livechat/static/src/js/models/website_livechat_message.js"></script>
|
||||
<!-- utils -->
|
||||
<script type="text/javascript" src="/mail/static/src/js/models/utils/timer.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/models/utils/timers.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/models/utils/cc_throttle_function.js"></script>
|
||||
<t t-call="web._assets_helpers"/>
|
||||
<link rel="stylesheet" type="text/scss" href="/im_livechat/static/src/scss/im_livechat_bootstrap.scss"/>
|
||||
<link rel="stylesheet" type="text/scss" href="/mail/static/src/scss/abstract_thread_window.scss"></link>
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<template>
|
||||
|
||||
<!--
|
||||
@param {string|integer} activeThreadID
|
||||
@param {mail.model.Channel[]} channels
|
||||
@param {boolean} isMyselfModerator
|
||||
@param {mail.model.Mailbox} inbox
|
||||
@param {mail.model.Mailbox} starred
|
||||
@param {mail.model.Mailbox|undefined} moderation set if current user is moderator
|
||||
@param {boolean} displayQuickSearch
|
||||
@param {Object} options
|
||||
-->
|
||||
<t t-extend="mail.discuss.Sidebar">
|
||||
<t t-jquery="hr" t-operation="before">
|
||||
<t t-foreach="threads" t-as="thread">
|
||||
<t t-if="thread.getType() == 'support_channel'">
|
||||
<div t-attf-class="o_mail_discuss_title_main o_mail_discuss_item #{(activeThreadID == thread.getID()) ? 'o_active': ''}"
|
||||
t-att-data-thread-id="thread.getID()">
|
||||
<t t-foreach="channels" t-as="channel">
|
||||
<t t-if="channel.getType() == 'support_channel'">
|
||||
<div t-attf-class="o_mail_discuss_title_main o_mail_discuss_item #{(activeThreadID == channel.getID()) ? 'o_active': ''}"
|
||||
t-att-data-thread-id="channel.getID()">
|
||||
<span class="o_thread_name"><i class="fa fa-question-circle mr8"/>Odoo Support</span>
|
||||
<t t-set="counter" t-value="thread.getUnreadCounter()"/>
|
||||
<t t-set="counter" t-value="channel.getUnreadCounter()"/>
|
||||
<t t-call="mail.discuss.SidebarCounter"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
@@ -24,9 +24,11 @@ class ChannelPartner(models.Model):
|
||||
_table = 'mail_channel_partner'
|
||||
_rec_name = 'partner_id'
|
||||
|
||||
custom_channel_name = fields.Char('Custom channel name')
|
||||
partner_id = fields.Many2one('res.partner', string='Recipient', ondelete='cascade')
|
||||
partner_email = fields.Char('Email', related='partner_id.email', readonly=False)
|
||||
channel_id = fields.Many2one('mail.channel', string='Channel', ondelete='cascade')
|
||||
fetched_message_id = fields.Many2one('mail.message', string='Last Fetched')
|
||||
seen_message_id = fields.Many2one('mail.message', string='Last Seen')
|
||||
fold_state = fields.Selection([('open', 'Open'), ('folded', 'Folded'), ('closed', 'Closed')], string='Conversation Fold State', default='open')
|
||||
is_minimized = fields.Boolean("Conversation is minimized")
|
||||
@@ -537,10 +539,13 @@ class Channel(models.Model):
|
||||
:rtype : list(dict)
|
||||
"""
|
||||
channel_infos = []
|
||||
partner_channels = self.env['mail.channel.partner']
|
||||
# find the channel partner state, if logged user
|
||||
if self.env.user and self.env.user.partner_id:
|
||||
partner_channels = self.env['mail.channel.partner'].search([('partner_id', '=', self.env.user.partner_id.id), ('channel_id', 'in', self.ids)])
|
||||
|
||||
# all relations partner_channel on those channels
|
||||
all_partner_channel = self.env['mail.channel.partner'].search([('channel_id', 'in', self.ids)])
|
||||
|
||||
# all partner infos on those channels
|
||||
partner_infos = all_partner_channel.mapped('partner_id').read(['id', 'name', 'email'])
|
||||
|
||||
# for each channel, build the information header and include the logged partner information
|
||||
for channel in self:
|
||||
info = {
|
||||
@@ -573,17 +578,34 @@ class Channel(models.Model):
|
||||
if last_message:
|
||||
info['last_message'] = last_message[0].get('last_message')
|
||||
|
||||
# add user session state, if available and if user is logged
|
||||
if partner_channels.ids:
|
||||
partner_channel = partner_channels.filtered(lambda c: channel.id == c.channel_id.id)
|
||||
if len(partner_channel) >= 1:
|
||||
partner_channel = partner_channel[0]
|
||||
info['state'] = partner_channel.fold_state or 'open'
|
||||
info['is_minimized'] = partner_channel.is_minimized
|
||||
info['seen_message_id'] = partner_channel.seen_message_id.id
|
||||
# listeners of the channel
|
||||
channel_partners = all_partner_channel.filtered(lambda pc: channel.id == pc.channel_id.id)
|
||||
|
||||
# find the channel partner state, if logged user
|
||||
partner_channel = self.env['mail.channel.partner']
|
||||
if self.env.user and self.env.user.partner_id:
|
||||
partner_channel = channel_partners.filtered(lambda pc: pc.partner_id.id == self.env.user.partner_id.id)
|
||||
# add needaction and unread counter, since the user is logged
|
||||
info['message_needaction_counter'] = channel.message_needaction_counter
|
||||
info['message_unread_counter'] = channel.message_unread_counter
|
||||
|
||||
# add user session state, if available and if user is logged
|
||||
if len(partner_channel.ids):
|
||||
partner_channel = partner_channel[0]
|
||||
info['state'] = partner_channel.fold_state or 'open'
|
||||
info['is_minimized'] = partner_channel.is_minimized
|
||||
info['seen_message_id'] = partner_channel.seen_message_id.id
|
||||
info['custom_channel_name'] = partner_channel.custom_channel_name
|
||||
|
||||
# add members infos
|
||||
partner_ids = channel_partners.mapped('partner_id').ids
|
||||
info['members'] = [partner_info for partner_info in partner_infos if partner_info['id'] in partner_ids]
|
||||
info['seen_partners_info'] = [{
|
||||
'partner_id': cp.partner_id.id,
|
||||
'fetched_message_id': cp.fetched_message_id.id,
|
||||
'seen_message_id': cp.seen_message_id.id,
|
||||
} for cp in channel_partners]
|
||||
|
||||
channel_infos.append(info)
|
||||
return channel_infos
|
||||
|
||||
@@ -700,10 +722,36 @@ class Channel(models.Model):
|
||||
self.ensure_one()
|
||||
if self.channel_message_ids.ids:
|
||||
last_message_id = self.channel_message_ids.ids[0] # zero is the index of the last message
|
||||
self.env['mail.channel.partner'].search([('channel_id', 'in', self.ids), ('partner_id', '=', self.env.user.partner_id.id)]).write({'seen_message_id': last_message_id})
|
||||
self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), {'info': 'channel_seen', 'id': self.id, 'last_message_id': last_message_id})
|
||||
self.env['mail.channel.partner'].search([('channel_id', 'in', self.ids), ('partner_id', '=', self.env.user.partner_id.id)]).write({
|
||||
'seen_message_id': last_message_id,
|
||||
'fetched_message_id': last_message_id,
|
||||
})
|
||||
data = {
|
||||
'info': 'channel_seen',
|
||||
'last_message_id': last_message_id,
|
||||
'partner_id': self.env.user.partner_id.id,
|
||||
}
|
||||
self.env['bus.bus'].sendmany([[(self._cr.dbname, 'mail.channel', self.id), data]])
|
||||
return last_message_id
|
||||
|
||||
@api.multi
|
||||
def channel_fetched(self):
|
||||
""" Broadcast the channel_fetched notification to channel members
|
||||
"""
|
||||
self.ensure_one()
|
||||
if not self.channel_message_ids.ids:
|
||||
return
|
||||
last_message_id = self.channel_message_ids.ids[0] # zero is the index of the last message
|
||||
self.env['mail.channel.partner'].search([('channel_id', 'in', self.ids), ('partner_id', '=', self.env.user.partner_id.id)]).write({
|
||||
'fetched_message_id': last_message_id,
|
||||
})
|
||||
data = {
|
||||
'info': 'channel_fetched',
|
||||
'last_message_id': last_message_id,
|
||||
'partner_id': self.env.user.partner_id.id,
|
||||
}
|
||||
self.env['bus.bus'].sendmany([[(self._cr.dbname, 'mail.channel', self.id), data]])
|
||||
|
||||
@api.multi
|
||||
def channel_invite(self, partner_ids):
|
||||
""" Add the given partner_ids to the current channels and broadcast the channel header to them.
|
||||
@@ -729,6 +777,14 @@ class Channel(models.Model):
|
||||
# broadcast the channel header to the added partner
|
||||
self._broadcast(partner_ids)
|
||||
|
||||
@api.model
|
||||
def channel_set_custom_name(self, channel_id, name=False):
|
||||
domain = [('partner_id', '=', self.env.user.partner_id.id), ('channel_id.id', '=', channel_id)]
|
||||
channel_partners = self.env['mail.channel.partner'].search(domain, limit=1)
|
||||
channel_partners.write({
|
||||
'custom_channel_name': name,
|
||||
})
|
||||
|
||||
@api.multi
|
||||
def notify_typing(self, is_typing, is_website_user=False):
|
||||
""" Broadcast the typing notification to channel members
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
@@ -123,6 +123,9 @@ var BasicComposer = Widget.extend({
|
||||
$(window).on(this.fileuploadID, this._onAttachmentLoaded.bind(this));
|
||||
this.on('change:attachment_ids', this, this._renderAttachments);
|
||||
|
||||
this.call('mail_service', 'getMailBus')
|
||||
.on('update_typing_partners', this, this._onUpdateTypingPartners);
|
||||
|
||||
// Mention
|
||||
this._mentionManager.prependTo(this.$('.o_composer'));
|
||||
|
||||
@@ -161,6 +164,15 @@ var BasicComposer = Widget.extend({
|
||||
focus: function () {
|
||||
this.$input.focus();
|
||||
},
|
||||
/**
|
||||
* Get cursor position and selection
|
||||
*
|
||||
* @returns {Object} a current cursor position as { start: {integer}, end: {integer} }
|
||||
*/
|
||||
getSelectionPositions: function () {
|
||||
var InputElement = this.$input.get(0);
|
||||
return InputElement ? dom.getSelectionRange(InputElement) : { start: 0, end: 0 };
|
||||
},
|
||||
/**
|
||||
* Get the state of the composer.
|
||||
*
|
||||
@@ -663,15 +675,24 @@ var BasicComposer = Widget.extend({
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Called when an emoji is clicked -> adds it in the <input/>, focuses the
|
||||
* <input/> and closes the emoji panel.
|
||||
* Called when an emoji is clicked from the emoji panel.
|
||||
* Emoji is inserted in the composer based on the position of the cursor,
|
||||
* and it automatically focuses the composer and set the cursor position
|
||||
* just after the newly inserted emoji.
|
||||
*
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onEmojiImageClick: function (ev) {
|
||||
this.$input.val(this.$input.val() + " " + $(ev.currentTarget).data('emoji') + " ");
|
||||
var cursorPosition = this.getSelectionPositions();
|
||||
var inputVal = this.$input.val();
|
||||
var leftSubstring = inputVal.substring(0, cursorPosition.start);
|
||||
var rightSubstring = inputVal.substring(cursorPosition.end);
|
||||
var newInputVal = [leftSubstring , $(ev.currentTarget).data('emoji'), rightSubstring].join(" ");
|
||||
var newCursorPosition = newInputVal.length - rightSubstring.length;
|
||||
this.$input.val(newInputVal);
|
||||
this.$input.focus();
|
||||
this.$input[0].setSelectionRange(newCursorPosition, newCursorPosition);
|
||||
this._hideEmojis();
|
||||
},
|
||||
/**
|
||||
@@ -793,6 +814,24 @@ var BasicComposer = Widget.extend({
|
||||
this._mentionManager.detectDelimiter();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {integer|string} threadID
|
||||
*/
|
||||
_onUpdateTypingPartners: function (threadID) {
|
||||
if (!this.options.showTyping) {
|
||||
return;
|
||||
}
|
||||
if (!this.options.thread) {
|
||||
return;
|
||||
}
|
||||
if (this.options.thread.getID() !== threadID) {
|
||||
return;
|
||||
}
|
||||
this.$('.o_composer_thread_typing').html(QWeb.render('mail.Composer.ThreadTyping', {
|
||||
thread: this.options.thread,
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
return BasicComposer;
|
||||
|
||||
@@ -3,7 +3,14 @@ odoo.define('mail.composer.Extended', function (require) {
|
||||
|
||||
var BasicComposer = require('mail.composer.Basic');
|
||||
|
||||
var core = require('web.core');
|
||||
|
||||
var QWeb = core.qweb;
|
||||
|
||||
var ExtendedComposer = BasicComposer.extend({
|
||||
events: _.extend({}, BasicComposer.prototype.events, {
|
||||
'click .o_composer_button_discard': '_onClickDiscard',
|
||||
}),
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
@@ -67,6 +74,26 @@ var ExtendedComposer = BasicComposer.extend({
|
||||
setSubject: function (subject) {
|
||||
this.$('.o_composer_subject input').val(subject);
|
||||
},
|
||||
/**
|
||||
* Show the button 'discard' next to the send button. This is useful while
|
||||
* replying to a message, when in the end we do not want to reply.
|
||||
*/
|
||||
showDiscardButton: function () {
|
||||
var $discard = this.$('.o_composer_button_discard');
|
||||
$discard.removeClass('d-none');
|
||||
$discard.addClass('d-md-inline-block');
|
||||
},
|
||||
/**
|
||||
* Hide the button 'discard' next to the send button. This is useful when
|
||||
* we were replying to a message from a certain thread, then we discard it
|
||||
* with some other actions (i.e. switch thread). The discard button only
|
||||
* makes sense during the selection of a message.
|
||||
*/
|
||||
hideDiscardButton: function () {
|
||||
var $discard = this.$('.o_composer_button_discard');
|
||||
$discard.removeClass('d-md-inline-block');
|
||||
$discard.addClass('d-none');
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
@@ -93,6 +120,17 @@ var ExtendedComposer = BasicComposer.extend({
|
||||
_shouldSend: function () {
|
||||
return false;
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onClickDiscard: function () {
|
||||
this.trigger_up('discard_extended_composer');
|
||||
},
|
||||
});
|
||||
|
||||
return ExtendedComposer;
|
||||
|
||||
@@ -77,7 +77,7 @@ var MentionManager = Widget.extend({
|
||||
detectDelimiter: function () {
|
||||
var self = this;
|
||||
var textVal = this._composer.$input.val();
|
||||
var cursorPosition = this._getSelectionPositions().start;
|
||||
var cursorPosition = this._getComposerSelectionPositions().start;
|
||||
var leftString = textVal.substring(0, cursorPosition);
|
||||
|
||||
function validateKeyword(delimiter, beginningOnly) {
|
||||
@@ -290,9 +290,8 @@ var MentionManager = Widget.extend({
|
||||
* @private
|
||||
* @returns a current cursor position
|
||||
*/
|
||||
_getSelectionPositions: function () {
|
||||
var InputElement = this._composer.$input.get(0);
|
||||
return InputElement ? dom.getSelectionRange(InputElement) : { start: 0, end: 0 };
|
||||
_getComposerSelectionPositions: function () {
|
||||
return this._composer.getSelectionPositions();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
@@ -402,14 +401,14 @@ var MentionManager = Widget.extend({
|
||||
if (this._activeListener.selection.length) {
|
||||
// get mention matches (ordered by index in the text)
|
||||
var matches = this._getMatch(textInput, this._activeListener);
|
||||
var index = getMentionIndex(matches, this._getSelectionPositions().start);
|
||||
var index = getMentionIndex(matches, this._getComposerSelectionPositions().start);
|
||||
this._activeListener.selection.splice(index, 0, selectedSuggestion);
|
||||
} else {
|
||||
this._activeListener.selection.push(selectedSuggestion);
|
||||
}
|
||||
|
||||
// update input text, and reset dropdown
|
||||
var cursorPosition = this._getSelectionPositions().start;
|
||||
var cursorPosition = this._getComposerSelectionPositions().start;
|
||||
var textLeft = textInput.substring(0, cursorPosition-(this._mentionWord.length+1));
|
||||
var textRight = textInput.substring(cursorPosition, textInput.length);
|
||||
var textInputNew = textLeft + substitution + ' ' + textRight;
|
||||
|
||||
@@ -111,6 +111,72 @@ var PartnerInviteDialog = Dialog.extend({
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Widget : Rename Conversation Dialog
|
||||
*/
|
||||
var RenameConversationDialog = Dialog.extend({
|
||||
dialog_title: _t("Rename conversation"),
|
||||
template: 'mail.RenameConversationDialog',
|
||||
/**
|
||||
* @override
|
||||
* @param {integer|string} channelID id of the channel
|
||||
* @param {function} callback to call when successfully renaming
|
||||
* conversation.
|
||||
*/
|
||||
init: function (parent, channelID, callback) {
|
||||
this._channelID = channelID;
|
||||
this._callback = callback;
|
||||
|
||||
this._super(parent, {
|
||||
title: 'Rename conversation',
|
||||
size: 'medium',
|
||||
buttons: [{
|
||||
text: _t("Rename"),
|
||||
close: true,
|
||||
classes: 'btn-primary o_mail_conversation_rename',
|
||||
click: this._rename.bind(this),
|
||||
}, {
|
||||
text: _t("Discard"),
|
||||
close: true,
|
||||
}],
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
* @returns {$.Promise}
|
||||
*/
|
||||
start: function () {
|
||||
var channel = this.call('mail_service', 'getChannel', this._channelID);
|
||||
this.$('input').val(channel.getName());
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @returns {$.Promise}
|
||||
*/
|
||||
_rename: function () {
|
||||
var self = this;
|
||||
var name = this.$('input').val();
|
||||
return this._rpc({
|
||||
model: 'mail.channel',
|
||||
method: 'channel_set_custom_name',
|
||||
args: [this._channelID],
|
||||
kwargs: {
|
||||
name: name,
|
||||
}
|
||||
}).then(function (updatedName) {
|
||||
var channel = self.call('mail_service', 'getThread', self._channelID);
|
||||
channel.setName(updatedName);
|
||||
self._callback();
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Widget : Moderator reject message dialog
|
||||
*
|
||||
@@ -177,6 +243,7 @@ var ModeratorRejectMessageDialog = Dialog.extend({
|
||||
var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
template: 'mail.discuss',
|
||||
custom_events: {
|
||||
discard_extended_composer: '_onDiscardExtendedComposer',
|
||||
message_moderation: '_onMessageModeration',
|
||||
search: '_onSearch',
|
||||
update_moderation_buttons: '_onUpdateModerationButtons',
|
||||
@@ -189,11 +256,13 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
'keydown': '_onKeydown',
|
||||
'click .o_mail_open_channels': '_onPublicChannelsClick',
|
||||
'click .o_mail_partner_unpin': '_onUnpinChannel',
|
||||
'input .o_discuss_sidebar_quick_search input': '_onInputSidebarQuickSearchInput',
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @param {Object} [options]
|
||||
* @param {integer} [options.channelQuickSearchThreshold=20] amount of
|
||||
* channels (dm inluded) for which a quick search appears in the sidebar.
|
||||
*/
|
||||
init: function (parent, action, options) {
|
||||
this._super.apply(this, arguments);
|
||||
@@ -204,6 +273,10 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
this.domain = [];
|
||||
this.options = options || {};
|
||||
|
||||
if (!('channelQuickSearchThreshold' in this.options)) {
|
||||
this.options.channelQuickSearchThreshold = 20;
|
||||
}
|
||||
|
||||
this._threadsScrolltop = {};
|
||||
this._composerStates = {};
|
||||
this._defaultThreadID = this.options.active_id ||
|
||||
@@ -235,12 +308,14 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
start: function () {
|
||||
var self = this;
|
||||
|
||||
this._basicComposer = new BasicComposer(this,
|
||||
{ mentionPartnersRestricted: true }
|
||||
);
|
||||
this._extendedComposer = new ExtendedComposer(this,
|
||||
{ mentionPartnersRestricted: true }
|
||||
);
|
||||
this._basicComposer = new BasicComposer(this, {
|
||||
mentionPartnersRestricted: true,
|
||||
showTyping: true,
|
||||
});
|
||||
this._extendedComposer = new ExtendedComposer(this, {
|
||||
mentionPartnersRestricted: true,
|
||||
showTyping: true,
|
||||
});
|
||||
this._basicComposer
|
||||
.on('post_message', this, this._onPostMessage)
|
||||
.on('input_focused', this, this._onComposerFocused);
|
||||
@@ -264,7 +339,7 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
return self.alive(self._setThread(self._defaultThreadID));
|
||||
})
|
||||
.then(function () {
|
||||
self._updateThreads();
|
||||
self._initThreads();
|
||||
self._startListening();
|
||||
self._threadWidget.$el.on('scroll', null, _.debounce(function () {
|
||||
if (
|
||||
@@ -475,6 +550,20 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Renders the mainside bar with current threads
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_initThreads: function () {
|
||||
var self = this;
|
||||
var $sidebar = this._renderSidebar();
|
||||
this.$('.o_mail_discuss_sidebar').html($sidebar.contents());
|
||||
_.each(['dm_chat', 'multi_user_channel'], function (type) {
|
||||
var $input = self.$('.o_mail_add_thread[data-type=' + type + '] input');
|
||||
self._prepareAddThreadInput($input, type);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Ensures that enough messages have been loaded to fill the entire screen
|
||||
* (this is particularily important because remaining messages can only be
|
||||
@@ -551,30 +640,42 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
*
|
||||
* @private
|
||||
* @param {JQuery} $input the input to prepare
|
||||
* @param {string} type the type of thread to create ('dm_chat', 'public' or
|
||||
* 'private' channel)
|
||||
* @param {string} type the type of thread to create ('dm_chat',
|
||||
* 'multi_user_channel')
|
||||
*/
|
||||
_prepareAddThreadInput: function ($input, type) {
|
||||
var self = this;
|
||||
if (type === 'public') {
|
||||
if (type === 'multi_user_channel') {
|
||||
$input.autocomplete({
|
||||
source: function (request, response) {
|
||||
self._lastSearchVal = _.escape(request.term);
|
||||
self._searchChannel(self._lastSearchVal).done(function (result){
|
||||
result.push({
|
||||
label: _.str.sprintf(
|
||||
'<strong>' + _t("Create %s") + '</strong>',
|
||||
'<strong>' + _t("Create %s (Public)") + '</strong>',
|
||||
'<em>"#' + self._lastSearchVal + '"</em>'
|
||||
),
|
||||
value: '_create',
|
||||
value: self._lastSearchVal,
|
||||
special: 'public',
|
||||
}, {
|
||||
label: _.str.sprintf(
|
||||
'<strong>' + _t("Create %s (Private)") + '</strong>',
|
||||
'<em>"#' + self._lastSearchVal + '"</em>'
|
||||
),
|
||||
value: self._lastSearchVal,
|
||||
special: 'private',
|
||||
});
|
||||
response(result);
|
||||
});
|
||||
},
|
||||
select: function (ev, ui) {
|
||||
if (self._lastSearchVal) {
|
||||
if (ui.item.value === '_create') {
|
||||
self.call('mail_service', 'createChannel', self._lastSearchVal, 'public');
|
||||
if (ui.item.special) {
|
||||
if (ui.item.special === 'public') {
|
||||
self.call('mail_service', 'createChannel', self._lastSearchVal, 'public');
|
||||
} else {
|
||||
self.call('mail_service', 'createChannel', self._lastSearchVal, 'private');
|
||||
}
|
||||
} else {
|
||||
self.call( 'mail_service', 'joinChannel', ui.item.id);
|
||||
}
|
||||
@@ -585,13 +686,6 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
},
|
||||
html: true,
|
||||
});
|
||||
} else if (type === 'private') {
|
||||
$input.on('keyup', this, function (ev) {
|
||||
var name = _.escape($(ev.target).val());
|
||||
if (ev.which === $.ui.keyCode.ENTER && name) {
|
||||
self.call('mail_service', 'createChannel', name, 'private');
|
||||
}
|
||||
});
|
||||
} else if (type === 'dm_chat') {
|
||||
$input.autocomplete({
|
||||
source: function (request, response) {
|
||||
@@ -672,24 +766,59 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
* Render the sidebar of discuss app
|
||||
*
|
||||
* @private
|
||||
* @param {Object} options
|
||||
* @param {mail.model.Thread[]} [options.threads=[]]
|
||||
* @returns {jQueryElement}
|
||||
*/
|
||||
_renderSidebar: function (options) {
|
||||
var inbox = this.call('mail_service', 'getMailbox', 'inbox');
|
||||
var starred = this.call('mail_service', 'getMailbox', 'starred');
|
||||
var moderation = this.call('mail_service', 'getMailbox', 'moderation');
|
||||
_renderSidebar: function () {
|
||||
var channels = this.call('mail_service', 'getChannels');
|
||||
channels = this._sortChannels(channels);
|
||||
var $sidebar = $(QWeb.render('mail.discuss.Sidebar', {
|
||||
activeThreadID: this._thread ? this._thread.getID() : undefined,
|
||||
threads: options.threads,
|
||||
needactionCounter: inbox.getMailboxCounter(),
|
||||
starredCounter: starred.getMailboxCounter(),
|
||||
moderationCounter: moderation ? moderation.getMailboxCounter() : 0,
|
||||
inbox: this.call('mail_service', 'getMailbox', 'inbox'),
|
||||
starred: this.call('mail_service', 'getMailbox', 'starred'),
|
||||
moderation: this.call('mail_service', 'getMailbox', 'moderation'),
|
||||
channels: channels,
|
||||
isMyselfModerator: this.call('mail_service', 'isMyselfModerator'),
|
||||
displayQuickSearch: channels.length >= this.options.channelQuickSearchThreshold,
|
||||
options: this.options,
|
||||
}));
|
||||
return $sidebar;
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Object} options
|
||||
* @param {string} [options.searchChannelVal='']
|
||||
*/
|
||||
_renderSidebarChannels: function (options) {
|
||||
options.searchChannelVal = options.searchChannelVal || '';
|
||||
var channels = this.call('mail_service', 'getChannels');
|
||||
var searchChannelValLowerCase = options.searchChannelVal.toLowerCase();
|
||||
channels = _.filter(channels, function (channel) {
|
||||
var channelNameLowerCase = channel.getName().toLowerCase();
|
||||
return channelNameLowerCase.indexOf(searchChannelValLowerCase) !== -1;
|
||||
});
|
||||
channels = this._sortChannels(channels);
|
||||
this.$('.o_mail_discuss_sidebar_channels').html(
|
||||
QWeb.render('mail.discuss.SidebarChannels', {
|
||||
activeThreadID: this._thread ? this._thread.getID() : undefined,
|
||||
channels: channels,
|
||||
displayQuickSearch: channels.length >= this.options.channelQuickSearchThreshold,
|
||||
})
|
||||
);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_renderSidebarMailboxes: function () {
|
||||
this.$('.o_mail_discuss_sidebar_mailboxes').html(
|
||||
QWeb.render('mail.discuss.SidebarMailboxes', {
|
||||
activeThreadID: this._thread ? this._thread.getID() : undefined,
|
||||
inbox: this.call('mail_service', 'getMailbox', 'inbox'),
|
||||
starred: this.call('mail_service', 'getMailbox', 'starred'),
|
||||
moderation: this.call('mail_service', 'getMailbox', 'moderation'),
|
||||
isMyselfModerator: this.call('mail_service', 'isMyselfModerator'),
|
||||
})
|
||||
);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {string} template
|
||||
@@ -799,6 +928,7 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
this._selectedMessage = message;
|
||||
var subject = "Re: " + message.getDocumentName();
|
||||
this._extendedComposer.setSubject(subject);
|
||||
this._extendedComposer.showDiscardButton();
|
||||
|
||||
if (this._thread.getType() !== 'mailbox') {
|
||||
this._basicComposer.do_hide();
|
||||
@@ -867,6 +997,14 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
});
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {mail.model.Channel[]} channels
|
||||
* @returns {mail.model.Channel[]}
|
||||
*/
|
||||
_sortChannels: function (channels) {
|
||||
return channels;
|
||||
},
|
||||
/**
|
||||
* Binds handlers on mail bus events
|
||||
*
|
||||
@@ -880,13 +1018,14 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
.on('new_channel', this, this._onNewChannel)
|
||||
.on('is_thread_bottom_visible', this, this._onIsThreadBottomVisible)
|
||||
.on('unsubscribe_from_channel', this, this._onChannelLeft)
|
||||
.on('update_needaction', this, this._throttledUpdateThreads)
|
||||
.on('update_starred', this, this._throttledUpdateThreads)
|
||||
.on('update_thread_unread_counter', this, this._throttledUpdateThreads)
|
||||
.on('update_dm_presence', this, this._throttledUpdateThreads)
|
||||
.on('activity_updated', this, this._throttledUpdateThreads)
|
||||
.on('update_moderation_counter', this, this._throttledUpdateThreads)
|
||||
.on('update_typing_partners', this, this._onTypingPartnersUpdated);
|
||||
.on('update_needaction', this, this._onUpdateNeedaction)
|
||||
.on('update_starred', this, this._onUpdateStarred)
|
||||
.on('update_thread_unread_counter', this, this._onUpdateThreadUnreadCounter)
|
||||
.on('update_dm_presence', this, this._onUpdateDmPresence)
|
||||
.on('activity_updated', this, this._onActivityUpdated)
|
||||
.on('update_moderation_counter', this, this._onUpdateModerationCounter)
|
||||
.on('update_typing_partners', this, this._onTypingPartnersUpdated)
|
||||
.on('update_channel', this, this._onUpdateChannel);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
@@ -917,6 +1056,7 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
_unselectMessage: function () {
|
||||
this._basicComposer.do_toggle(this._thread.getType() !== 'mailbox' && !this._thread.isMassMailing());
|
||||
this._extendedComposer.do_toggle(this._thread.isMassMailing());
|
||||
this._extendedComposer.hideDiscardButton();
|
||||
|
||||
if (!config.device.isMobile) {
|
||||
var composer = this._thread.getType() !== 'mailbox' && this._thread.isMassMailing() ?
|
||||
@@ -1093,21 +1233,18 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Renders the mainside bar with current threads
|
||||
* Re-renders the mainside bar with current threads
|
||||
*
|
||||
* @private
|
||||
* @param {Object} [options={}]
|
||||
* @param {string} [options.searchChannelVal='']
|
||||
*/
|
||||
_updateThreads: function () {
|
||||
_updateThreads: function (options) {
|
||||
var self = this;
|
||||
|
||||
var $sidebar = this._renderSidebar({
|
||||
threads: _.filter(this.call('mail_service', 'getThreads'), function (thread) {
|
||||
return thread.getType() !== 'document_thread';
|
||||
}),
|
||||
});
|
||||
|
||||
this.$('.o_mail_discuss_sidebar').html($sidebar.contents());
|
||||
_.each(['dm_chat', 'public', 'private'], function (type) {
|
||||
options = options || {};
|
||||
this._renderSidebarMailboxes(options);
|
||||
this._renderSidebarChannels(options);
|
||||
_.each(['dm_chat', 'multi_user_channel'], function (type) {
|
||||
var $input = self.$('.o_mail_add_thread[data-type=' + type + '] input');
|
||||
self._prepareAddThreadInput($input, type);
|
||||
});
|
||||
@@ -1117,6 +1254,12 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onActivityUpdated: function () {
|
||||
this._throttleUpdateThreads();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
@@ -1150,7 +1293,13 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onChannelSettingsClicked: function (ev) {
|
||||
var threadID = $(ev.target).data('thread-id');
|
||||
ev.stopPropagation();
|
||||
var threadID = $(ev.currentTarget).data('thread-id');
|
||||
var thread = this.call('mail_service', 'getThread', threadID);
|
||||
if (thread.getType() === 'dm_chat') {
|
||||
new RenameConversationDialog(this, threadID, this._updateThreads.bind(this)).open();
|
||||
return;
|
||||
}
|
||||
this.do_action({
|
||||
type: 'ir.actions.act_window',
|
||||
res_model: 'mail.channel',
|
||||
@@ -1169,6 +1318,14 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
composer.mentionSetCommands(commands);
|
||||
composer.mentionSetPrefetchedPartners(partners);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
*/
|
||||
_onDiscardExtendedComposer: function (ev) {
|
||||
ev.stopPropagation();
|
||||
this._unselectMessage();
|
||||
},
|
||||
/**
|
||||
* When clicking on an item in the sidebar
|
||||
*
|
||||
@@ -1180,6 +1337,16 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
var threadID = $(ev.currentTarget).data('thread-id');
|
||||
this._setThread(threadID);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onInputSidebarQuickSearchInput: function (ev) {
|
||||
ev.preventDefault();
|
||||
this._updateThreads({
|
||||
searchChannelVal: $(ev.currentTarget).val(),
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Invite button is only for channels (not mailboxes)
|
||||
*
|
||||
@@ -1391,19 +1558,9 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {integer|string} threadID
|
||||
*/
|
||||
_onTypingPartnersUpdated: function (threadID) {
|
||||
var self = this;
|
||||
if (this._thread.getID() !== threadID) {
|
||||
return;
|
||||
}
|
||||
if (this._thread.hasTypingNotification()) {
|
||||
// call getMentionpartnerSuggestions in order to correctly fetch members
|
||||
this._thread.getMentionPartnerSuggestions().then(function () {
|
||||
self._threadWidget.renderTypingNotificationBar(self._thread);
|
||||
});
|
||||
}
|
||||
_onTypingPartnersUpdated: function () {
|
||||
this._updateThreads();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
@@ -1436,6 +1593,22 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
_onUnstarAllClicked: function () {
|
||||
this.call('mail_service', 'unstarAll');
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {integer} channelID
|
||||
*/
|
||||
_onUpdateChannel: function (channelID) {
|
||||
if (this._thread.getID() !== channelID) {
|
||||
return;
|
||||
}
|
||||
this._fetchAndRenderThread();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onUpdateDmPresence: function () {
|
||||
this._throttledUpdateThreads();
|
||||
},
|
||||
/**
|
||||
* Update the moderation buttons.
|
||||
* This is triggered when a moderation checkbox
|
||||
@@ -1446,6 +1619,30 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
_onUpdateModerationButtons: function () {
|
||||
this._updateModerationButtons();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onUpdateModerationCounter: function () {
|
||||
this._throttledUpdateThreads();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onUpdateNeedaction: function () {
|
||||
this._throttledUpdateThreads();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onUpdateStarred: function () {
|
||||
this._throttledUpdateThreads();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onUpdateThreadUnreadCounter: function () {
|
||||
this._throttledUpdateThreads();
|
||||
},
|
||||
});
|
||||
|
||||
core.action_registry.add('mail.discuss', Discuss);
|
||||
|
||||
@@ -59,6 +59,13 @@ Discuss.include({
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
_initThreads: function () {
|
||||
return this._updateThreads();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @returns {Boolean} true iff we currently are in the Inbox tab
|
||||
@@ -73,7 +80,7 @@ Discuss.include({
|
||||
_renderButtons: function () {
|
||||
var self = this;
|
||||
this._super.apply(this, arguments);
|
||||
_.each(['dm_chat', 'public', 'private'], function (type) {
|
||||
_.each(['dm_chat', 'multi_user_channel'], function (type) {
|
||||
var selector = '.o_mail_discuss_button_' + type;
|
||||
self.$buttons.on('click', selector, self._onAddThread.bind(self));
|
||||
});
|
||||
|
||||
@@ -102,6 +102,16 @@ var AbstractThread = Class.extend(Mixins.EventDispatcherMixin, {
|
||||
hasMessages: function () {
|
||||
return !_.isEmpty(this.getMessages());
|
||||
},
|
||||
/**
|
||||
* States whether this thread is compatible with the 'seen' feature.
|
||||
* By default, threads do not have thsi feature active.
|
||||
* @see {mail.model.ThreadSeenMixin} to enable this feature on a thread.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasSeenFeature: function () {
|
||||
return false;
|
||||
},
|
||||
/**
|
||||
* States whether this thread is compatible with the 'is typing...' feature.
|
||||
* By default, threads do not have this feature active.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
odoo.define('mail.model.Channel', function (require) {
|
||||
"use strict";
|
||||
|
||||
var ChannelSeenMixin = require('mail.model.ChannelSeenMixin');
|
||||
var SearchableThread = require('mail.model.SearchableThread');
|
||||
var ThreadTypingMixin = require('mail.model.ThreadTypingMixin');
|
||||
var mailUtils = require('mail.utils');
|
||||
@@ -16,7 +17,7 @@ var time = require('web.time');
|
||||
* Any piece of code in JS that make use of channels must ideally interact with
|
||||
* such objects, instead of direct data from the server.
|
||||
*/
|
||||
var Channel = SearchableThread.extend(ThreadTypingMixin, {
|
||||
var Channel = SearchableThread.extend(ChannelSeenMixin, ThreadTypingMixin, {
|
||||
/**
|
||||
* @override
|
||||
* @param {Object} params
|
||||
@@ -29,9 +30,17 @@ var Channel = SearchableThread.extend(ThreadTypingMixin, {
|
||||
* @param {boolean} params.data.is_moderator whether the current user is
|
||||
* moderator of this channel.
|
||||
* @param {string} [params.data.last_message_date] date in server-format
|
||||
* @param {Object[]} [params.data.members=[]]
|
||||
* @param {integer} [params.data.members[i].id]
|
||||
* @param {string} [params.data.members[i].name]
|
||||
* @param {string} [params.data.members[i].email]
|
||||
* @param {integer} [params.data.message_unread_counter]
|
||||
* @param {boolean} [params.data.moderation=false] whether the channel is
|
||||
* moderated or not
|
||||
* @param {Object[]} [params.data.partners_info=[]]
|
||||
* @param {integer} [params.data.partners_info[i].partner_id]
|
||||
* @param {integer} [params.data.partners_info[i].fetched_message_id]
|
||||
* @param {integer} [params.data.partners_info[i].seen_message_id]
|
||||
* @param {string} params.data.state
|
||||
* @param {string} [params.data.uuid]
|
||||
* @param {Object} params.options
|
||||
@@ -41,7 +50,8 @@ var Channel = SearchableThread.extend(ThreadTypingMixin, {
|
||||
init: function (params) {
|
||||
var self = this;
|
||||
this._super.apply(this, arguments);
|
||||
ThreadTypingMixin.init.call(this, arguments);
|
||||
ChannelSeenMixin.init.apply(this, arguments);
|
||||
ThreadTypingMixin.init.apply(this, arguments);
|
||||
|
||||
var data = params.data;
|
||||
var options = params.options;
|
||||
@@ -61,14 +71,13 @@ var Channel = SearchableThread.extend(ThreadTypingMixin, {
|
||||
this._isModerated = data.moderation;
|
||||
this._isMyselfModerator = data.is_moderator;
|
||||
this._lastMessageDate = undefined;
|
||||
this._members = [];
|
||||
this._members = data.members || [];
|
||||
// Deferred that is resolved on fetched members of this channel.
|
||||
this._membersDef = undefined;
|
||||
// number of messages that are 'needaction', which is equivalent to the
|
||||
// number of messages in this channel that are in inbox.
|
||||
this._needactionCounter = data.message_needaction_counter || 0;
|
||||
this._serverType = data.channel_type;
|
||||
this._throttleFetchSeen = _.throttle(this._fetchSeen.bind(this), 3000);
|
||||
// unique identifier for this channel, which is required for some rpc
|
||||
this._uuid = data.uuid;
|
||||
|
||||
@@ -331,23 +340,6 @@ var Channel = SearchableThread.extend(ThreadTypingMixin, {
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @returns {$.Promise<integer>} resolved with ID of last seen message
|
||||
*/
|
||||
_fetchSeen: function () {
|
||||
var self = this;
|
||||
return this._rpc({
|
||||
model: 'mail.channel',
|
||||
method: 'channel_seen',
|
||||
args: [[this._id]],
|
||||
}, {
|
||||
shadow: true
|
||||
}).then(function (lastSeenMessageID) {
|
||||
self._lastSeenMessageID = lastSeenMessageID;
|
||||
return lastSeenMessageID;
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Override so that it tells whether the channel is moderated or not. This
|
||||
* is useful in order to display pending moderation messages when the
|
||||
@@ -395,9 +387,23 @@ var Channel = SearchableThread.extend(ThreadTypingMixin, {
|
||||
*/
|
||||
_markAsRead: function () {
|
||||
var superDef = this._super.apply(this, arguments);
|
||||
var seenDef = this._throttleFetchSeen();
|
||||
var seenDef = this._throttleNotifySeen();
|
||||
return $.when(superDef, seenDef);
|
||||
},
|
||||
/**
|
||||
* @override {mail.model.ThreadSeenMixin}
|
||||
* @private
|
||||
* @returns {$.Promise}
|
||||
*/
|
||||
_notifyFetched: function () {
|
||||
return this._rpc({
|
||||
model: 'mail.channel',
|
||||
method: 'channel_fetched',
|
||||
args: [[this._id]],
|
||||
}, {
|
||||
shadow: true
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @override {mail.model.ThreadTypingMixin}
|
||||
* @private
|
||||
@@ -413,6 +419,25 @@ var Channel = SearchableThread.extend(ThreadTypingMixin, {
|
||||
kwargs: { is_typing: params.typing },
|
||||
}, { shadow: true });
|
||||
},
|
||||
/**
|
||||
* @override {mail.model.ThreadSeenMixin}
|
||||
* @private
|
||||
* @returns {$.Promise<integer>} resolved with ID of last seen message
|
||||
*/
|
||||
_notifySeen: function () {
|
||||
var self = this;
|
||||
this._cancelThrottledNotifyFetched();
|
||||
return this._rpc({
|
||||
model: 'mail.channel',
|
||||
method: 'channel_seen',
|
||||
args: [[this._id]],
|
||||
}, {
|
||||
shadow: true
|
||||
}).then(function (lastSeenMessageID) {
|
||||
self._lastSeenMessageID = lastSeenMessageID;
|
||||
return lastSeenMessageID;
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Prepare and send a message to the server on this channel.
|
||||
*
|
||||
|
||||
@@ -12,6 +12,8 @@ var DMChat = TwoUserChannel.extend({
|
||||
* @override
|
||||
* @param {Object} params
|
||||
* @param {Object} params.data
|
||||
* @param {string|undefined} [params.custom_channel_name] if set, use this
|
||||
* custom name for this DM
|
||||
* @param {Object[]} params.data.direct_partner
|
||||
* @param {integer} params.data.direct_partner[0].id
|
||||
* @param {string} params.data.direct_partner[0].im_status
|
||||
@@ -23,7 +25,7 @@ var DMChat = TwoUserChannel.extend({
|
||||
var data = params.data;
|
||||
|
||||
this._directPartnerID = data.direct_partner[0].id;
|
||||
this._name = data.direct_partner[0].name;
|
||||
this._name = data.custom_channel_name || data.direct_partner[0].name;
|
||||
this._status = data.direct_partner[0].im_status;
|
||||
this._type = 'dm_chat';
|
||||
},
|
||||
|
||||
@@ -168,17 +168,6 @@ var DocumentThread = Thread.extend({
|
||||
this._mustFetchMessageIDs = false;
|
||||
this._messageIDs = messageIDs;
|
||||
},
|
||||
/**
|
||||
* Set the name of this document thread
|
||||
*
|
||||
* This is useful if the name of the document related to the document thread
|
||||
* has changed
|
||||
*
|
||||
* @param {string} newName
|
||||
*/
|
||||
setName: function (newName) {
|
||||
this._name = newName;
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
odoo.define('mail.model.ChannelSeenMixin', function (require) {
|
||||
"use strict";
|
||||
|
||||
var CCThrottleFunction = require('mail.model.CCThrottleFunction');
|
||||
|
||||
var session = require('web.session');
|
||||
|
||||
/**
|
||||
* Mixin for enabling the "seen" feature on channels.
|
||||
*/
|
||||
var ChannelSeenMixin = {
|
||||
/**
|
||||
* Initialize the internal data for seen feature on channels.
|
||||
*
|
||||
* Also listens on some internal events of the channel:
|
||||
*
|
||||
* - 'message_added': used to notify channel has been fetched
|
||||
*
|
||||
* @param {Object} params
|
||||
* @param {Object[]} [params.data.partners_info=[]]
|
||||
* @param {integer} [params.data.seen_partners_info[i].partner_id]
|
||||
* @param {integer|boolean} [params.data.seen_partners_info[i].fetched_message_id=false]
|
||||
* @param {integer|boolean} [params.data.seen_partners_info[i].seen_message_id=false]
|
||||
*/
|
||||
init: function (params) {
|
||||
this._seenPartnersInfo = params.data.seen_partners_info || [];
|
||||
|
||||
this._throttleNotifySeen = _.throttle(this._notifySeen.bind(this), 3000);
|
||||
this._throttleNotifyFetched = CCThrottleFunction({
|
||||
duration: 3000,
|
||||
func: this._notifyFetched.bind(this),
|
||||
});
|
||||
|
||||
this.on('message_added', this, this._onSeenMessageAdded);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Get list of members that have fetched the provided message on this
|
||||
* channel, but not yet seen it. Ignore current user in this list.
|
||||
*
|
||||
* @param {mail.model.Message} message
|
||||
* @returns {Object[]} list of members
|
||||
*/
|
||||
getFetchedNotSeenMembers: function (message) {
|
||||
var otherPartnersInfo = _.filter(this._seenPartnersInfo, function (info) {
|
||||
return info.partner_id !== session.partner_id;
|
||||
});
|
||||
|
||||
// seen partner IDs
|
||||
var seenPartnersInfo = _.filter(otherPartnersInfo, function (info) {
|
||||
return info.seen_message_id && info.seen_message_id >= message.getID();
|
||||
});
|
||||
var seenPartnerIDs = _.map(seenPartnersInfo, function (info) {
|
||||
return info.partner_id;
|
||||
});
|
||||
|
||||
// fetched partner IDs (ignore seen)
|
||||
var fetchedPartnersInfo = _.filter(otherPartnersInfo, function (info) {
|
||||
var lastFetchedID = info.fetched_message_id || info.seen_message_id || 0;
|
||||
return lastFetchedID >= message.getID();
|
||||
});
|
||||
var fetchedPartnerIDs = _.map(fetchedPartnersInfo, function (info) {
|
||||
return info.partner_id;
|
||||
});
|
||||
fetchedPartnerIDs = _.reject(fetchedPartnerIDs, function (fetchedPartnerID) {
|
||||
return _.contains(seenPartnerIDs, fetchedPartnerID);
|
||||
});
|
||||
|
||||
// fetched members
|
||||
var fetchedMembers = _.filter(this._members, function (member) {
|
||||
return _.contains(fetchedPartnerIDs, member.id);
|
||||
});
|
||||
|
||||
return fetchedMembers;
|
||||
},
|
||||
/**
|
||||
* @returns {integer}
|
||||
*/
|
||||
getLastMessageIDSeenByEveryone: function () {
|
||||
var otherPartnersInfo = _.filter(this._seenPartnersInfo, function (info) {
|
||||
return info.partner_id !== session.partner_id;
|
||||
});
|
||||
var seenMessageIDs = _.map(otherPartnersInfo, function (info) {
|
||||
return info.seen_message_id || 0;
|
||||
});
|
||||
if (_.isEmpty(seenMessageIDs)) {
|
||||
return 0;
|
||||
}
|
||||
return _.min(seenMessageIDs);
|
||||
},
|
||||
/**
|
||||
* Get list of members that have seen the provided message on this channel.
|
||||
* Ignore current user in this list.
|
||||
*
|
||||
* @param {mail.model.Message} message
|
||||
* @returns {Object[]} list of members
|
||||
*/
|
||||
getSeenMembers: function (message) {
|
||||
var otherPartnersInfo = _.filter(this._seenPartnersInfo, function (info) {
|
||||
return info.partner_id !== session.partner_id;
|
||||
});
|
||||
|
||||
// seen partner IDs
|
||||
var seenPartnersInfo = _.filter(otherPartnersInfo, function (info) {
|
||||
return info.seen_message_id >= message.getID();
|
||||
});
|
||||
var seenPartnerIDs = _.map(seenPartnersInfo, function (info) {
|
||||
return info.partner_id;
|
||||
});
|
||||
|
||||
// seen members
|
||||
var seenMembers = _.filter(this._members, function (member) {
|
||||
return _.contains(seenPartnerIDs, member.id);
|
||||
});
|
||||
|
||||
return seenMembers;
|
||||
},
|
||||
/**
|
||||
* @param {mail.model.Message} message
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasEveryoneFetched: function (message) {
|
||||
var otherPartnersInfo = _.filter(this._seenPartnersInfo, function (info) {
|
||||
return info.partner_id !== session.partner_id;
|
||||
});
|
||||
var lastFetchedMessageIDs = _.map(otherPartnersInfo, function (info) {
|
||||
return info.fetched_message_id || info.seen_message_id || 0;
|
||||
});
|
||||
if (_.isEmpty(lastFetchedMessageIDs)) {
|
||||
return 0;
|
||||
}
|
||||
return message.getID() <= _.min(lastFetchedMessageIDs);
|
||||
},
|
||||
/**
|
||||
* @param {mail.model.Message} message
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasEveryoneSeen: function (message) {
|
||||
var otherPartnersInfo = _.filter(this._seenPartnersInfo, function (info) {
|
||||
return info.partner_id !== session.partner_id;
|
||||
});
|
||||
var lastSeenMessageIDs = _.map(otherPartnersInfo, function (info) {
|
||||
return info.seen_message_id || 0;
|
||||
});
|
||||
if (_.isEmpty(lastSeenMessageIDs)) {
|
||||
return 0;
|
||||
}
|
||||
return message.getID() <= _.min(lastSeenMessageIDs);
|
||||
},
|
||||
/**
|
||||
* @param {mail.model.Message} message
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasSomeoneFetched: function (message) {
|
||||
var otherPartnersInfo = _.filter(this._seenPartnersInfo, function (info) {
|
||||
return info.partner_id !== session.partner_id;
|
||||
});
|
||||
var lastFetchedMessageIDs = _.map(otherPartnersInfo, function (info) {
|
||||
return info.fetched_message_id || info.seen_message_id || 0;
|
||||
});
|
||||
if (_.isEmpty(lastFetchedMessageIDs)) {
|
||||
return false;
|
||||
}
|
||||
var fetched = _.find(lastFetchedMessageIDs, function (msgID) {
|
||||
return msgID >= message.getID();
|
||||
});
|
||||
return fetched;
|
||||
},
|
||||
/**
|
||||
* @param {mail.model.Message} message
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasSomeoneSeen: function (message) {
|
||||
var otherPartnersInfo = _.filter(this._seenPartnersInfo, function (info) {
|
||||
return info.partner_id !== session.partner_id;
|
||||
});
|
||||
var lastSeenMessageIDs = _.map(otherPartnersInfo, function (info) {
|
||||
return info.seen_message_id || 0;
|
||||
});
|
||||
if (_.isEmpty(lastSeenMessageIDs)) {
|
||||
return false;
|
||||
}
|
||||
var seen = _.find(lastSeenMessageIDs, function (msgID) {
|
||||
return msgID >= message.getID();
|
||||
});
|
||||
return seen;
|
||||
},
|
||||
/**
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasSeenFeature: function () {
|
||||
return true;
|
||||
},
|
||||
/**
|
||||
* @param {Object} data
|
||||
* @param {string} data.info either 'channel_seen' or 'channel_fetched'
|
||||
* @param {integer} data.partner_id
|
||||
* @param {integer} data.last_message_id
|
||||
*/
|
||||
updateSeenPartnersInfo: function (data) {
|
||||
var seenInfo = _.findWhere(this._seenPartnersInfo, { partner_id: data.partner_id });
|
||||
if (!seenInfo) {
|
||||
seenInfo = {
|
||||
partner_id: data.partner_id,
|
||||
seen_message_id: 0,
|
||||
fetched_message_id: 0,
|
||||
};
|
||||
this._seenPartnersInfo.push(seenInfo);
|
||||
}
|
||||
if (data.info === 'channel_fetched') {
|
||||
seenInfo.fetched_message_id = data.last_message_id;
|
||||
} else if (data.info === 'channel_seen') {
|
||||
seenInfo.fetched_message_id = data.last_message_id;
|
||||
seenInfo.seen_message_id = data.last_message_id;
|
||||
}
|
||||
this.call('mail_service', 'getMailBus').trigger('update_channel', this.getID());
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_cancelThrottledNotifyFetched: function () {
|
||||
this._throttleNotifyFetched.cancel();
|
||||
this._throttleNotifyFetched.clear();
|
||||
},
|
||||
/**
|
||||
* @abstract
|
||||
* @private
|
||||
* @returns {$.Promise}
|
||||
*/
|
||||
_notifyFetched: function () {
|
||||
return $.when();
|
||||
},
|
||||
/**
|
||||
* @abstract
|
||||
* @private
|
||||
* @returns {$.Promise<integer>} resolved with ID of last seen message
|
||||
*/
|
||||
_notifySeen: function () {
|
||||
return $.when();
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onSeenMessageAdded: function () {
|
||||
this._throttleNotifyFetched();
|
||||
},
|
||||
};
|
||||
|
||||
return ChannelSeenMixin;
|
||||
|
||||
});
|
||||
+9
@@ -147,6 +147,15 @@ var ThreadTypingMixin = {
|
||||
hasTypingNotification: function () {
|
||||
return true;
|
||||
},
|
||||
/**
|
||||
* Tells if someone other than current user is typing something on this
|
||||
* thread.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isSomeoneTyping: function () {
|
||||
return !(_.isEmpty(this._typingPartnerIDs));
|
||||
},
|
||||
/**
|
||||
* Register someone that is currently typing something in this thread.
|
||||
* If this is the current user that is typing something, don't do anything
|
||||
@@ -18,7 +18,8 @@ var MultiUserChannel = Channel.extend({
|
||||
var data = params.data;
|
||||
|
||||
this._isMassMailing = data.mass_mailing || false;
|
||||
this._type = data.public !== 'private' ? 'public' : 'private';
|
||||
this._type = 'multi_user_channel';
|
||||
this._public = data.public !== 'private';
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -45,6 +46,12 @@ var MultiUserChannel = Channel.extend({
|
||||
isMassMailing: function () {
|
||||
return this._isMassMailing;
|
||||
},
|
||||
/**
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isPublic: function () {
|
||||
return this._public;
|
||||
},
|
||||
/**
|
||||
* Unsubscribes from channel
|
||||
*
|
||||
|
||||
@@ -271,6 +271,14 @@ var Thread = AbstractThread.extend(ServicesMixin, {
|
||||
markAsPreviewed: function () {
|
||||
this._previewed = true;
|
||||
},
|
||||
/**
|
||||
* Set the name of this thread
|
||||
*
|
||||
* @param {string} newName
|
||||
*/
|
||||
setName: function (newName) {
|
||||
this._name = newName;
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
|
||||
@@ -56,10 +56,32 @@ MailManager.include({
|
||||
_handleChannelNotification: function (params) {
|
||||
if (params.data && params.data.info === 'typing_status') {
|
||||
this._handleChannelTypingNotification(params.channelID, params.data);
|
||||
} else if (params.data && params.data.info === 'channel_fetched') {
|
||||
this._handleChannelFetchedNotification(params.channelID, params.data);
|
||||
} else if (params.data && params.data.info === 'channel_seen') {
|
||||
this._handleChannelSeenNotification(params.channelID, params.data);
|
||||
} else {
|
||||
this._handleChannelMessageNotification(params.data);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Called when a channel has been fetched, and the server responses with the
|
||||
* last message fetched. Useful in order to track last message fetched.
|
||||
*
|
||||
* @private
|
||||
* @param {integer} channelID
|
||||
* @param {Object} data
|
||||
* @param {string} data.info 'channel_fetched'
|
||||
* @param {integer} data.last_message_id
|
||||
* @param {integer} data.partner_id
|
||||
*/
|
||||
_handleChannelFetchedNotification: function (channelID, data) {
|
||||
var channel = this.getChannel(channelID);
|
||||
if (!channel) {
|
||||
return;
|
||||
}
|
||||
channel.updateSeenPartnersInfo(data);
|
||||
},
|
||||
/**
|
||||
* Called when a new or updated message is received on a channel
|
||||
*
|
||||
@@ -88,6 +110,31 @@ MailManager.include({
|
||||
});
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Called when a channel has been seen, and the server responses with the
|
||||
* last message seen. Useful in order to track last message seen.
|
||||
*
|
||||
* @private
|
||||
* @param {integer} channelID
|
||||
* @param {Object} data
|
||||
* @param {string} data.info 'channel_seen'
|
||||
* @param {integer} data.last_message_id
|
||||
* @param {integer} data.partner_id
|
||||
*/
|
||||
_handleChannelSeenNotification: function (channelID, data) {
|
||||
var channel = this.getChannel(channelID);
|
||||
if (!channel) {
|
||||
return;
|
||||
}
|
||||
channel.updateSeenPartnersInfo(data);
|
||||
if (session.partner_id !== data.partner_id) {
|
||||
return;
|
||||
}
|
||||
channel.setLastSeenMessageID(data.last_message_id);
|
||||
if (channel.hasUnreadMessages()) {
|
||||
channel.resetUnreadCounter();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Called when someone starts or stops typing a message in a channel
|
||||
*
|
||||
@@ -150,25 +197,6 @@ MailManager.include({
|
||||
_handlePartnerActivityUpdateNotification: function (data) {
|
||||
this._mailBus.trigger('activity_updated', data);
|
||||
},
|
||||
/**
|
||||
* Called when a channel has been seen, and the server responses with the
|
||||
* last message seen. Useful in order to track last message seen.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} data
|
||||
* @param {integer} data.id ID of a channel
|
||||
* @param {integer} [data.last_message_id] mandatory if 'id' refers to an
|
||||
* existing channel.
|
||||
*/
|
||||
_handlePartnerChannelSeenNotification: function (data) {
|
||||
var channel = this.getChannel(data.id);
|
||||
if (channel) {
|
||||
channel.setLastSeenMessageID(data.last_message_id);
|
||||
if (channel.hasUnreadMessages()) {
|
||||
channel.resetUnreadCounter();
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Called when receiving a channel state as a partner notification:
|
||||
*
|
||||
@@ -351,8 +379,6 @@ MailManager.include({
|
||||
this._handlePartnerMessageAuthorNotification(data);
|
||||
} else if (data.type === 'deletion') {
|
||||
this._handlePartnerMessageDeletionNotification(data);
|
||||
} else if (data.info === 'channel_seen') {
|
||||
this._handlePartnerChannelSeenNotification(data);
|
||||
} else if (data.info === 'transient_message') {
|
||||
this._handlePartnerTransientMessageNotification(data);
|
||||
} else if (data.type === 'activity_updated') {
|
||||
|
||||
@@ -329,8 +329,7 @@ MailManager.include({
|
||||
.on('is_thread_bottom_visible', this, this._onIsThreadBottomVisible)
|
||||
.on('unsubscribe_from_channel', this, this._onUnsubscribeFromChannel)
|
||||
.on('update_thread_unread_counter', this, this._onUpdateThreadUnreadCounter)
|
||||
.on('update_dm_presence', this, this._onUpdateDmPresence)
|
||||
.on('update_typing_partners', this, this._onTypingPartnersUpdated);
|
||||
.on('update_dm_presence', this, this._onUpdateDmPresence);
|
||||
|
||||
core.bus.on('resize', this, _.debounce(this._repositionThreadWindows.bind(this), 100));
|
||||
},
|
||||
@@ -655,23 +654,6 @@ MailManager.include({
|
||||
_onNewMessage: function (message) {
|
||||
this._updateThreadWindowsFromMessage(message, { keepBottom: true, passively: true });
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {integer|string} threadID
|
||||
*/
|
||||
_onTypingPartnersUpdated: function (threadID) {
|
||||
var threadWindow = this._getThreadWindow(threadID);
|
||||
if (!threadWindow) {
|
||||
return;
|
||||
}
|
||||
var thread = this.getThread(threadID);
|
||||
if (thread.isChannel()) {
|
||||
// call getMentionpartnerSuggestions in order to correctly fetch members
|
||||
thread.getMentionPartnerSuggestions().then(function () {
|
||||
threadWindow.renderTypingNotificationBar();
|
||||
});
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Close the thread window when unsusbscribe from a channel.
|
||||
*
|
||||
|
||||
@@ -83,6 +83,9 @@ var ThreadWidget = Widget.extend({
|
||||
this._selectedMessageID = null;
|
||||
this._currentThreadID = null;
|
||||
this._messageMailPopover = null;
|
||||
this._messageSeenPopover = null;
|
||||
// used to track popover IDs to destroy on re-rendering of popovers
|
||||
this._openedSeenPopoverIDs = [];
|
||||
},
|
||||
/**
|
||||
* The message mail popover may still be shown at this moment. If we do not
|
||||
@@ -95,6 +98,10 @@ var ThreadWidget = Widget.extend({
|
||||
if (this._messageMailPopover) {
|
||||
this._messageMailPopover.popover('hide');
|
||||
}
|
||||
if (this._messageSeenPopover) {
|
||||
this._messageSeenPopover.popover('hide');
|
||||
}
|
||||
this._destroyOpenSeenPopoverIDs();
|
||||
},
|
||||
/**
|
||||
* @param {mail.model.AbstractThread} thread the thread to render.
|
||||
@@ -185,12 +192,6 @@ var ThreadWidget = Widget.extend({
|
||||
dateFormat: time.getLangDatetimeFormat(),
|
||||
}));
|
||||
|
||||
// must be after mail.widget.Thread rendering, so that there is the
|
||||
// DOM element for the 'is typing' notification bar
|
||||
if (thread.hasTypingNotification()) {
|
||||
this.renderTypingNotificationBar(thread);
|
||||
}
|
||||
|
||||
_.each(messages, function (message) {
|
||||
var $message = self.$('.o_thread_message[data-message-id="'+ message.getID() +'"]');
|
||||
$message.find('.o_mail_timestamp').data('date', message.getDate());
|
||||
@@ -209,6 +210,9 @@ var ThreadWidget = Widget.extend({
|
||||
}
|
||||
|
||||
this._renderMessageMailPopover(messages);
|
||||
if (thread.hasSeenFeature()) {
|
||||
this._renderMessageSeenPopover(thread, messages);
|
||||
}
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -253,28 +257,6 @@ var ThreadWidget = Widget.extend({
|
||||
});
|
||||
return done;
|
||||
},
|
||||
/**
|
||||
* Render the 'is typing...' text on the typing notification bar of the
|
||||
* thread. This is called when there is a change in the list of users
|
||||
* typing something on this thread.
|
||||
*
|
||||
* @param {mail.model.AbstractThread} thread with ThreadTypingMixin
|
||||
*/
|
||||
renderTypingNotificationBar: function (thread) {
|
||||
if (this._currentThreadID === thread.getID()) {
|
||||
var shouldScrollToBottomAfterRendering = this.isAtBottom();
|
||||
|
||||
// typing notification bar rendering
|
||||
var $typingBar = this.$('.o_thread_typing_notification_bar');
|
||||
var text = thread.getTypingMembersToText();
|
||||
$typingBar.toggleClass('o_hidden', !text); // hide if no text, because of padding
|
||||
$typingBar.text(text);
|
||||
|
||||
if (shouldScrollToBottomAfterRendering) {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Scroll to the bottom of the thread
|
||||
*/
|
||||
@@ -337,6 +319,15 @@ var ThreadWidget = Widget.extend({
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_destroyOpenSeenPopoverIDs: function () {
|
||||
_.each(this._openedSeenPopoverIDs, function (popoverID) {
|
||||
$('#' + popoverID).remove();
|
||||
});
|
||||
this._openedSeenPopoverIDs = [];
|
||||
},
|
||||
/**
|
||||
* Modifies $element to add the 'read more/read less' functionality
|
||||
* All element nodes with 'data-o-mail-quote' attribute are concerned.
|
||||
@@ -466,6 +457,46 @@ var ThreadWidget = Widget.extend({
|
||||
},
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Render the popover when mouse hovering on the seen icon of a message
|
||||
* in the thread. Only seen icons in non-squashed message have popover,
|
||||
* because squashed messages hides this icon on message mouseover.
|
||||
*
|
||||
* @private
|
||||
* @param {mail.model.AbstractThread} thread with thread seen mixin,
|
||||
* @see {mail.model.ThreadSeenMixin}
|
||||
* @param {mail.model.Message[]} messages list of messages in the
|
||||
* rendered thread.
|
||||
*/
|
||||
_renderMessageSeenPopover: function (thread, messages) {
|
||||
var self = this;
|
||||
this._destroyOpenSeenPopoverIDs();
|
||||
if (this._messageSeenPopover) {
|
||||
this._messageSeenPopover.popover('hide');
|
||||
}
|
||||
if (!this.$('.o_thread_message_core .o_mail_thread_message_seen_icon').length) {
|
||||
return;
|
||||
}
|
||||
this._messageSeenPopover = this.$('.o_thread_message_core .o_mail_thread_message_seen_icon').popover({
|
||||
html: true,
|
||||
boundary: 'viewport',
|
||||
placement: 'auto',
|
||||
trigger: 'hover',
|
||||
offset: '0, 1',
|
||||
content: function () {
|
||||
var $this = $(this);
|
||||
self._openedSeenPopoverIDs.push($this.attr('aria-describedby'));
|
||||
var messageID = $this.data('message-id');
|
||||
var message = _.find(messages, function (message) {
|
||||
return message.getID() === messageID;
|
||||
});
|
||||
return QWeb.render('mail.widget.Thread.Message.SeenIconPopoverContent', {
|
||||
thread: thread,
|
||||
message: message,
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
|
||||
@@ -138,6 +138,15 @@ var AbstractThreadWindow = Widget.extend({
|
||||
getID: function () {
|
||||
return this._getThreadID();
|
||||
},
|
||||
/**
|
||||
* @returns {mail.model.Thread|undefined}
|
||||
*/
|
||||
getThread: function () {
|
||||
if (!this.hasThread) {
|
||||
return undefined;
|
||||
}
|
||||
return this._thread;
|
||||
},
|
||||
/**
|
||||
* Get the status of the thread, such as the im status of a DM chat
|
||||
* ('online', 'offline', etc.). If this window has no thread, returns
|
||||
@@ -256,14 +265,6 @@ var AbstractThreadWindow = Widget.extend({
|
||||
this.$header.html(
|
||||
QWeb.render('mail.AbstractThreadWindow.HeaderContent', options));
|
||||
},
|
||||
/**
|
||||
* Render the 'is typing...' notification bar text on the thread in this
|
||||
* thread window. This is called when there is a change in the list of users
|
||||
* currently typing something on this thread.
|
||||
*/
|
||||
renderTypingNotificationBar: function () {
|
||||
this._threadWidget.renderTypingNotificationBar(this._thread);
|
||||
},
|
||||
/**
|
||||
* Scroll to the bottom of the thread in the thread window
|
||||
*/
|
||||
@@ -339,6 +340,7 @@ var AbstractThreadWindow = Widget.extend({
|
||||
_getHeaderRenderingOptions: function () {
|
||||
return {
|
||||
status: this.getThreadStatus(),
|
||||
thread: this.getThread(),
|
||||
title: this.getTitle(),
|
||||
unreadCounter: this.getUnreadCounter(),
|
||||
widget: this,
|
||||
|
||||
@@ -55,6 +55,10 @@ var ThreadWindow = AbstractThreadWindow.extend({
|
||||
|
||||
var superDef = this._super().then(this._listenThreadWidget.bind(this));
|
||||
|
||||
this.call('mail_service', 'getMailBus')
|
||||
.on('update_typing_partners', this, this._onUpdateTypingPartners)
|
||||
.on('update_channel', this, this._onUpdateChannel);
|
||||
|
||||
var composerDef;
|
||||
if (!this.hasThread()) {
|
||||
this._startWithoutThread();
|
||||
@@ -353,7 +357,29 @@ var ThreadWindow = AbstractThreadWindow.extend({
|
||||
var message = this.call('mail_service', 'getMessage', messageID);
|
||||
message.toggleStarStatus();
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {integer|string} threadID
|
||||
*/
|
||||
_onUpdateTypingPartners: function (threadID) {
|
||||
if (!this.hasThread()) {
|
||||
return;
|
||||
}
|
||||
if (this._thread.getID() !== threadID) {
|
||||
return;
|
||||
}
|
||||
this.renderHeader();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {integer} channelID
|
||||
*/
|
||||
_onUpdateChannel: function (channelID) {
|
||||
if (this._thread.getID() !== channelID) {
|
||||
return;
|
||||
}
|
||||
this.render();
|
||||
},
|
||||
});
|
||||
|
||||
return ThreadWindow;
|
||||
|
||||
@@ -57,6 +57,14 @@ $o-mail-thread-window-zindex: $zindex-modal + 1 !default;
|
||||
cursor: pointer;
|
||||
flex: 1 1 auto;
|
||||
@include o-text-overflow;
|
||||
|
||||
.o_mail_thread_typing_icon {
|
||||
padding-left: 2px;
|
||||
|
||||
.o_mail_thread_typing_icon_dot {
|
||||
background: gray('300');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_thread_window_buttons {
|
||||
@@ -76,10 +84,6 @@ $o-mail-thread-window-zindex: $zindex-modal + 1 !default;
|
||||
overflow: auto;
|
||||
-webkit-overflow-scrolling: touch; // smooth scrolling in iOS app (Safari)
|
||||
|
||||
.o_thread_typing_notification_bar {
|
||||
background-color: rgba($o-mail-thread-window-bg, 0.75);
|
||||
}
|
||||
|
||||
.o_thread_date_separator {
|
||||
margin: 0px 0px 15px 0px;
|
||||
.o_thread_date {
|
||||
|
||||
@@ -147,6 +147,11 @@
|
||||
}
|
||||
.o_composer_send {
|
||||
margin-top: $o-mail-chatter-gap;
|
||||
|
||||
> .o_composer_button_discard:not(:hover) {
|
||||
background: theme-color('light');
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,6 +189,11 @@
|
||||
flex: 1 0 100%;
|
||||
}
|
||||
|
||||
.o_composer_thread_typing {
|
||||
font-size: 0.9em;
|
||||
color: gray('600');
|
||||
}
|
||||
|
||||
.o_chatter_composer_tools {
|
||||
border: none;
|
||||
border-left: 1px solid gray('300');
|
||||
|
||||
@@ -18,6 +18,16 @@
|
||||
width: $o-mail-chat-sidebar-width + 50px;
|
||||
}
|
||||
|
||||
.o_discuss_sidebar_quick_search {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
> input {
|
||||
border-radius: 15px;
|
||||
flex-basis: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
.o_mail_sidebar_title {
|
||||
display: flex;
|
||||
padding: 6px $o-horizontal-padding 0 $o-horizontal-padding;
|
||||
@@ -44,6 +54,9 @@
|
||||
flex: 1 1 auto;
|
||||
@include o-text-overflow;
|
||||
}
|
||||
.o_mail_thread_typing_icon_dot {
|
||||
background: gray('600');;
|
||||
}
|
||||
.o_mail_sidebar_needaction, .o_mail_partner_unpin, .o_mail_channel_settings {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
@@ -122,6 +135,23 @@
|
||||
background-color: gray('900');
|
||||
color: gray('300');
|
||||
|
||||
.o_discuss_sidebar_quick_search {
|
||||
|
||||
> input:focus {
|
||||
&::placeholder {
|
||||
color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
> input {
|
||||
outline: none;
|
||||
background-color: gray('800');
|
||||
border: gray('600');
|
||||
text-align: center;
|
||||
color: gray('400');
|
||||
}
|
||||
}
|
||||
|
||||
.o_mail_discuss_item {
|
||||
cursor: pointer;
|
||||
|
||||
@@ -129,7 +159,7 @@
|
||||
background-color: darken(gray('900'),2%);
|
||||
|
||||
.o_mail_partner_unpin, .o_mail_channel_settings {
|
||||
opacity: $o-mail-sidebar-icon-opacity;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,7 +173,7 @@
|
||||
}
|
||||
|
||||
.o_mail_partner_unpin, .o_mail_channel_settings {
|
||||
@include o-hover-opacity($default-opacity: 0, $hover-opacity: 1);
|
||||
display: none;
|
||||
}
|
||||
&.o_unread_message {
|
||||
color: white;
|
||||
|
||||
@@ -13,26 +13,6 @@
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
.o_thread_typing_notification_free_space {
|
||||
flex-grow: 1,
|
||||
}
|
||||
|
||||
.o_thread_typing_notification_bar {
|
||||
flex: 0, 0, 20px;
|
||||
background-color: rgba($white, 0.75);
|
||||
padding: 5px;
|
||||
text-align: center;
|
||||
color: gray('600');
|
||||
|
||||
&.o_thread_order_asc {
|
||||
@include o-position-sticky($bottom: 0px);
|
||||
}
|
||||
|
||||
&.o_thread_order_desc {
|
||||
@include o-position-sticky($top: 0px);
|
||||
}
|
||||
}
|
||||
|
||||
.o_thread_tooltip_container {
|
||||
display: inline;
|
||||
position: relative;
|
||||
@@ -98,15 +78,21 @@
|
||||
max-width: $o-mail-thread-avatar-size;
|
||||
}
|
||||
.o_thread_message_side_date {
|
||||
display: none;
|
||||
margin-left: -5px;
|
||||
}
|
||||
.o_thread_message_star {
|
||||
display: none;
|
||||
margin-right: -5px;
|
||||
}
|
||||
|
||||
.o_thread_message_side_date {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.o_mail_thread_message_seen_icon:not(.o_all_seen) {
|
||||
opacity: $o-mail-thread-icon-opacity*0.5;
|
||||
}
|
||||
}
|
||||
.o_thread_icon {
|
||||
cursor: pointer;
|
||||
@@ -119,14 +105,21 @@
|
||||
|
||||
&:hover, &.o_thread_selected_message {
|
||||
.o_thread_message_side_date {
|
||||
display: inline-block;
|
||||
opacity: $o-mail-thread-side-date-opacity;
|
||||
}
|
||||
.o_thread_icon {
|
||||
display: inline-block;
|
||||
opacity: $o-mail-thread-icon-opacity;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.o_thread_message_sidebar {
|
||||
.o_mail_thread_message_seen_icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_mail_redirect {
|
||||
@@ -239,3 +232,68 @@
|
||||
opacity: $o-mail-thread-icon-opacity;
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Thread typing icon: shared between discuss and chat windows
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
.o_mail_thread_typing_icon {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
||||
.o_mail_thread_typing_icon_dot {
|
||||
display: inline-block;
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
border-radius: 50%;
|
||||
background: gray('800');
|
||||
animation: o_mail_thread_typing_icon_dot 1.5s linear infinite;
|
||||
|
||||
&:nth-child(2) {
|
||||
animation-delay: -1.35s;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
animation-delay: -1.2s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes o_mail_thread_typing_icon_dot {
|
||||
0%, 40%, 100% {
|
||||
transform: initial;
|
||||
}
|
||||
20% {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Thread seen icon: shared between discuss and chat windows
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
.o_mail_thread_message_seen_icon {
|
||||
position: relative;
|
||||
opacity: 0.6;
|
||||
|
||||
&.o_all_seen {
|
||||
color: $o-enterprise-primary-color;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.fa-check:nth-child(1) {
|
||||
padding-left: 3px;
|
||||
}
|
||||
|
||||
.fa-check:nth-child(2) {
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
left: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<t t-set="status" t-value="widget.getThreadStatus()"/>
|
||||
<t t-set="title" t-value="widget.getTitle()"/>
|
||||
<t t-set="unreadCounter" t-value="widget.getUnreadCounter()"/>
|
||||
<t t-set="thread" t-value="widget.getThread()"/>
|
||||
</t>
|
||||
</div>
|
||||
<div class="o_thread_window_content">
|
||||
@@ -58,6 +59,7 @@
|
||||
name of the document.
|
||||
@param {integer} [unreadCounter] the number of unread messages on the
|
||||
thread.
|
||||
@param {mail.model.Thread|undefined} thread
|
||||
@param {Object} widget
|
||||
@param {function} widget.isMobile function without any param that
|
||||
states whether it should render for desktop or mobile screen.
|
||||
@@ -70,6 +72,7 @@
|
||||
<t t-if="status" t-call="mail.UserStatus"/>
|
||||
<t t-esc="title"/>
|
||||
<span t-if="unreadCounter"> (<t t-esc="unreadCounter"/>)</span>
|
||||
<t t-if="thread and thread.hasTypingNotification() and thread.isSomeoneTyping()" t-call="mail.ThreadTypingIcon"/>
|
||||
</span>
|
||||
<span t-if="!widget.isMobile()" class="o_thread_window_buttons">
|
||||
<a href="#" class="o_thread_window_close fa fa-close"/>
|
||||
|
||||
@@ -24,9 +24,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_composer_attachments_list"/>
|
||||
<div class="o_composer_thread_typing"/>
|
||||
</div>
|
||||
<div class="o_composer_send">
|
||||
<button tabindex="3" class="btn btn-primary o_composer_button_send d-none d-md-inline-block" type="button"><t t-esc="widget.options.sendText"/></button>
|
||||
<button tabindex="3" class="btn btn-secondary o_composer_button_discard d-none" type="button">Discard</button>
|
||||
</div>
|
||||
<span class="d-none">
|
||||
<t t-call="HiddenInputFile">
|
||||
@@ -40,6 +42,13 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
@param {mail.model.Thread} thread
|
||||
-->
|
||||
<t t-name="mail.Composer.ThreadTyping">
|
||||
<t t-esc="thread.getTypingMembersToText()"/>
|
||||
</t>
|
||||
|
||||
<div t-name="mail.Composer.emojis" class="o_mail_emoji_container">
|
||||
<t t-foreach="emojis" t-as="emoji">
|
||||
<button t-att-data-emoji="emoji.sources[0]" class="btn btn-link o_mail_emoji" t-att-title="emoji.description" t-att-aria-label="emoji.description">
|
||||
|
||||
@@ -16,64 +16,84 @@
|
||||
|
||||
<!--
|
||||
@param {string|integer} activeThreadID
|
||||
@param {integer} moderationCounter
|
||||
@param {integer} needactionCounter
|
||||
@param {integer} starredCounter
|
||||
@param {mail.model.Channel[]} channels
|
||||
@param {boolean} isMyselfModerator
|
||||
@param {mail.model.Mailbox} inbox
|
||||
@param {mail.model.Mailbox} starred
|
||||
@param {mail.model.Mailbox|undefined} moderation set if current user is moderator
|
||||
@param {boolean} displayQuickSearch
|
||||
@param {Object} options
|
||||
-->
|
||||
<t t-name="mail.discuss.Sidebar">
|
||||
<div class="o_mail_discuss_sidebar">
|
||||
<div t-attf-class="o_mail_discuss_title_main o_mail_discuss_item #{(activeThreadID === 'mailbox_inbox') ? 'o_active': ''}"
|
||||
data-thread-id="mailbox_inbox">
|
||||
<span class="o_thread_name"><i class="fa fa-inbox mr8"/>Inbox</span>
|
||||
<t t-set="counter" t-value="needactionCounter"/>
|
||||
<t t-call="mail.discuss.SidebarCounter"/>
|
||||
</div>
|
||||
<div t-attf-class="o_mail_discuss_title_main o_mail_mailbox_title_starred o_mail_discuss_item #{(activeThreadID === 'mailbox_starred') ? 'o_active': ''}"
|
||||
data-thread-id="mailbox_starred">
|
||||
<span class="o_thread_name"><i class="fa fa-star-o mr8"/>Starred</span>
|
||||
<t t-set="counter" t-value="starredCounter"/>
|
||||
<t t-call="mail.discuss.SidebarCounter"/>
|
||||
</div>
|
||||
<div t-if="isMyselfModerator" t-attf-class="o_mail_discuss_title_main o_mail_discuss_item #{(activeThreadID == 'mailbox_moderation') ? 'o_active': ''}"
|
||||
data-thread-id="mailbox_moderation">
|
||||
<span class="o_thread_name"> <i class="fa fa-envelope mr8"/>Moderation Queue</span>
|
||||
<t t-set="counter" t-value="moderationCounter"/>
|
||||
<t t-call="mail.discuss.SidebarCounter"/>
|
||||
<div class="o_mail_discuss_sidebar_mailboxes">
|
||||
<t t-call="mail.discuss.SidebarMailboxes"/>
|
||||
</div>
|
||||
<hr class="mb8"/>
|
||||
|
||||
<t t-set="type" t-value="'public'"/>
|
||||
<t t-call="mail.discuss.SidebarTitle">
|
||||
<t t-set="title">Channels</t>
|
||||
<t t-set="icon" t-value="fa-users"/>
|
||||
</t>
|
||||
<t t-call="mail.discuss.SidebarItems">
|
||||
<t t-set="displayHash" t-value="true"/>
|
||||
<t t-set="inputPlaceholder">Add a channel</t>
|
||||
</t>
|
||||
|
||||
<t t-set="type" t-value="'dm_chat'"/>
|
||||
<t t-call="mail.discuss.SidebarTitle">
|
||||
<t t-set="title">Direct Messages</t>
|
||||
<t t-set="icon" t-value="fa-user"/>
|
||||
</t>
|
||||
<t t-call="mail.discuss.SidebarItems">
|
||||
<t t-set="displayStatus" t-value="true"/>
|
||||
<t t-set="inputPlaceholder">User name</t>
|
||||
</t>
|
||||
|
||||
<t t-set="type" t-value="'private'"/>
|
||||
<t t-call="mail.discuss.SidebarTitle">
|
||||
<t t-set="title">Private Channels</t>
|
||||
<t t-set="icon" t-value="fa-eye-slash"/>
|
||||
</t>
|
||||
<t t-call="mail.discuss.SidebarItems">
|
||||
<t t-set="inputPlaceholder">Add a private channel</t>
|
||||
<t t-set="displayHash" t-value="true"/>
|
||||
</t>
|
||||
<div t-if="displayQuickSearch" class="o_discuss_sidebar_quick_search">
|
||||
<t t-set="inputPlaceholder">Quick search...</t>
|
||||
<input t-att-placeholder="inputPlaceholder"/>
|
||||
</div>
|
||||
<div class="o_mail_discuss_sidebar_channels">
|
||||
<t t-call="mail.discuss.SidebarChannels"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!--
|
||||
@param {string|integer} activeThreadID
|
||||
@param {boolean} isMyselfModerator
|
||||
@param {mail.model.Mailbox} inbox
|
||||
@param {mail.model.Mailbox} starred
|
||||
@param {mail.model.Mailbox|undefined} moderation set if current user is moderator
|
||||
-->
|
||||
<t t-name="mail.discuss.SidebarMailboxes">
|
||||
<div t-attf-class="o_mail_discuss_title_main o_mail_discuss_item #{(activeThreadID === 'mailbox_inbox') ? 'o_active': ''}"
|
||||
data-thread-id="mailbox_inbox">
|
||||
<span class="o_thread_name"><i class="fa fa-inbox mr8"/>Inbox</span>
|
||||
<t t-set="counter" t-value="inbox.getMailboxCounter()"/>
|
||||
<t t-call="mail.discuss.SidebarCounter"/>
|
||||
</div>
|
||||
<div t-attf-class="o_mail_discuss_title_main o_mail_mailbox_title_starred o_mail_discuss_item #{(activeThreadID === 'mailbox_starred') ? 'o_active': ''}"
|
||||
data-thread-id="mailbox_starred">
|
||||
<span class="o_thread_name"><i class="fa fa-star-o mr8"/>Starred</span>
|
||||
<t t-set="counter" t-value="starred.getMailboxCounter()"/>
|
||||
<t t-call="mail.discuss.SidebarCounter"/>
|
||||
</div>
|
||||
<div t-if="isMyselfModerator" t-attf-class="o_mail_discuss_title_main o_mail_discuss_item #{(activeThreadID == 'mailbox_moderation') ? 'o_active': ''}"
|
||||
data-thread-id="mailbox_moderation">
|
||||
<span class="o_thread_name"> <i class="fa fa-envelope mr8"/>Moderation Queue</span>
|
||||
<t t-set="counter" t-value="moderation.getMailboxCounter()"/>
|
||||
<t t-call="mail.discuss.SidebarCounter"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!--
|
||||
@param {string|integer} activeThreadID
|
||||
@param {mail.model.Channel[]} channels
|
||||
-->
|
||||
<t t-name="mail.discuss.SidebarChannels">
|
||||
<t t-set="type" t-value="'multi_user_channel'"/>
|
||||
<t t-call="mail.discuss.SidebarTitle">
|
||||
<t t-set="title">Channels</t>
|
||||
<t t-set="icon" t-value="fa-users"/>
|
||||
</t>
|
||||
<t t-call="mail.discuss.SidebarItems">
|
||||
<t t-set="displayHash" t-value="true"/>
|
||||
<t t-set="inputPlaceholder">Add a channel</t>
|
||||
</t>
|
||||
|
||||
<t t-set="type" t-value="'dm_chat'"/>
|
||||
<t t-call="mail.discuss.SidebarTitle">
|
||||
<t t-set="title">Direct Messages</t>
|
||||
<t t-set="icon" t-value="fa-user"/>
|
||||
</t>
|
||||
<t t-call="mail.discuss.SidebarItems">
|
||||
<t t-set="displayStatus" t-value="true"/>
|
||||
<t t-set="inputPlaceholder">User name</t>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<!--
|
||||
@param {integer} counter
|
||||
-->
|
||||
@@ -87,24 +107,24 @@
|
||||
Don't display the thread type if there is no thread of such type
|
||||
and the 'add a thread' option is disabled for this type
|
||||
|
||||
@param {mail.model.Thread[]} threads
|
||||
@param {mail.model.Channel[]} channels
|
||||
@param {boolean} [disableAddThread] whether the 'Add thread' action should disabled or not for the considered discuss item category
|
||||
@param {string} [icon] classname to add on title to display icon
|
||||
@param {string} title the displayed type of the discuss items (e.g. "Private Channels")
|
||||
@param {string} title the displayed type of the discuss items (e.g. "Channels")
|
||||
@param {string} type the type of the thread
|
||||
-->
|
||||
<t t-name="mail.discuss.SidebarTitle">
|
||||
<t t-if="disableAddThread">
|
||||
<t t-set="empty" t-value="true"/>
|
||||
<t t-foreach="threads" t-as="thread">
|
||||
<t t-if="thread.getType() === type">
|
||||
<t t-foreach="channels" t-as="channel">
|
||||
<t t-if="channel.getType() === type">
|
||||
<t t-set="empty" t-value="false"/>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
<t t-if="!disableAddThread || !empty">
|
||||
<div class="o_mail_sidebar_title">
|
||||
<h4 t-att-class="type == 'public' ? 'o_mail_open_channels' : ''">
|
||||
<h4 t-att-class="type == 'multi_user_channel' ? 'o_mail_open_channels' : ''">
|
||||
<i t-if="icon" t-attf-class="mr4 fa-fw fa #{icon}" role="img" aria-label="Channel" title="Channel"></i>
|
||||
<b><t t-esc="title"/></b>
|
||||
</h4>
|
||||
@@ -115,7 +135,7 @@
|
||||
|
||||
<!--
|
||||
@param {string|integer} activeThreadID
|
||||
@param {mail.model.Thread[]} threads
|
||||
@param {mail.model.Channel[]} channels
|
||||
@param {boolean} disableAddThread whether the 'Add thread' action should be disable or not for the considered discuss item category
|
||||
@param {boolean} [displayHash] whether the hash ('#') character should be displayed as a prefix for item's name
|
||||
@param {boolean} [displayStatus] whether the item should display its status (should be the case only for chats)
|
||||
@@ -126,25 +146,30 @@
|
||||
<span t-if="displayHash">#</span>
|
||||
<input type="text" class="o_input" t-attf-placeholder="#{inputPlaceholder}"/>
|
||||
</div>
|
||||
<t t-foreach="threads" t-as="thread">
|
||||
<t t-set="counter" t-value="thread.getType() === 'mailbox' ? thread.getMailboxCounter() : thread.isTwoUserThread() ? thread.getUnreadCounter() : thread.getNeedactionCounter()"/>
|
||||
<div t-if="thread.getType() === type" t-att-data-thread-id="thread.getID()" t-att-title="thread.getName()"
|
||||
t-attf-class="o_mail_discuss_item #{thread.hasUnreadMessages() ? ' o_unread_message' : ''} #{(activeThreadID == thread.getID()) ? 'o_active': ''}">
|
||||
<t t-foreach="channels" t-as="channel">
|
||||
<t t-set="counter" t-value="channel.isTwoUserThread() ? channel.getUnreadCounter() : channel.getNeedactionCounter()"/>
|
||||
<div t-if="channel.getType() === type" t-att-data-thread-id="channel.getID()" t-att-title="channel.getName()"
|
||||
t-attf-class="o_mail_discuss_item #{channel.hasUnreadMessages() ? ' o_unread_message' : ''} #{(activeThreadID == channel.getID()) ? 'o_active': ''}">
|
||||
<span class="o_thread_name">
|
||||
<span t-if="displayStatus">
|
||||
<t t-call="mail.UserStatus">
|
||||
<t t-set="status" t-value="thread.getStatus()"/>
|
||||
<t t-set="status" t-value="channel.getStatus()"/>
|
||||
</t>
|
||||
</span>
|
||||
<span t-if="displayHash" class="o_mail_hash">#</span>
|
||||
<t t-esc="_.unescape(thread.getName())"/>
|
||||
<i t-if="thread.isMassMailing()" class="fa fa-envelope-o" title="Sends messages by email" role="img" aria-label="Send by mail"/>
|
||||
<i t-if="channel.getType() === 'multi_user_channel' and !channel.isPublic()" class="fa fa-lock" title="Private channel" role="img" aria-label="Private channel"/>
|
||||
<t t-esc="_.unescape(channel.getName())"/>
|
||||
<i t-if="channel.isMassMailing()" class="fa fa-envelope-o" title="Sends messages by email" role="img" aria-label="Send by mail"/>
|
||||
</span>
|
||||
<t t-if="channel.hasTypingNotification() and channel.isSomeoneTyping()">
|
||||
<t t-set="thread" t-value="channel"/>
|
||||
<t t-call="mail.ThreadTypingIcon"/>
|
||||
</t>
|
||||
<span t-attf-class="fa fa-cog o_mail_channel_settings"
|
||||
title="Channel settings" role="img" aria-label="Channel settings" t-att-data-thread-id="thread.getID()"/>
|
||||
title="Channel settings" role="img" aria-label="Channel settings" t-att-data-thread-id="channel.getID()"/>
|
||||
<t t-call="mail.discuss.SidebarCounter"/>
|
||||
<span t-if="!thread.isGroupBasedSubscription()" t-attf-class="fa fa-times o_mail_partner_unpin #{counter ? 'd-none' : ''}"
|
||||
title="Leave this channel" role="img" aria-label="Leave this channel" t-att-data-thread-id="thread.getID()"/>
|
||||
<span t-if="!channel.isGroupBasedSubscription()" t-attf-class="fa fa-times o_mail_partner_unpin #{counter ? 'd-none' : ''}"
|
||||
title="Leave this channel" role="img" aria-label="Leave this channel" t-att-data-thread-id="channel.getID()"/>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
@@ -179,7 +204,7 @@
|
||||
<button type="button" class="btn btn-secondary o_mail_discuss_button_mark_all_read d-none d-md-inline-block" title="Mark all as read">Mark all read</button>
|
||||
<button type="button" class="btn btn-secondary o_mail_discuss_button_unstar_all" title="Unstar all messages">Unstar all</button>
|
||||
<button type="button" class="btn btn-secondary o_mail_discuss_button_dm_chat o_hidden d-md-none" title="New Message">New Message</button>
|
||||
<button t-if="!disableAddThread" type="button" class="btn btn-secondary o_mail_discuss_button_public o_mail_discuss_button_private d-block d-md-none" title="New Channel">New Channel</button>
|
||||
<button t-if="!disableAddThread" type="button" class="btn btn-secondary o_mail_discuss_button_multi_user_channel d-block d-md-none" title="New Channel">New Channel</button>
|
||||
<button type="button" class="btn btn-secondary o_hidden o_mail_discuss_button_select_all" title="Select all messages to moderate">Select All</button>
|
||||
<button type="button" class="btn btn-secondary o_hidden o_mail_discuss_button_unselect_all" title="Unselect all messages to moderate">Unselect All</button>
|
||||
<button type="button" data-decision="accept" class="btn btn-secondary o_mail_discuss_button_moderate_all" title="Accept selected messages">Accept</button>
|
||||
@@ -193,6 +218,11 @@
|
||||
<input type="text" class="o_input o_mail_discuss_partner_invite_input" id="mail_search_partners"/>
|
||||
</div>
|
||||
|
||||
<!-- Rename channel dialog's content -->
|
||||
<div t-name="mail.RenameConversationDialog">
|
||||
<input type="text" class="o_input o_mail_discuss_rename_channel_input"/>
|
||||
</div>
|
||||
|
||||
<div t-name="mail.ModeratorRejectMessageDialog">
|
||||
<input class="form-control" type="text" id="message_title" placeholder="Subject" value="Message Rejected"/>
|
||||
<textarea class="form-control mt16" id="reject_message" placeholder="Mail Body">Your message was rejected by moderator.</textarea>
|
||||
@@ -219,14 +249,10 @@
|
||||
<span class="fa fa-user"/>
|
||||
<span class="o_tab_title">Chat</span>
|
||||
</div>
|
||||
<div class="o_mail_mobile_tab" data-type="public">
|
||||
<div class="o_mail_mobile_tab" data-type="multi_user_channel">
|
||||
<span class="fa fa-users"/>
|
||||
<span class="o_tab_title">Channels</span>
|
||||
</div>
|
||||
<div class="o_mail_mobile_tab" data-type="private">
|
||||
<span class="fa fa-eye-slash"/>
|
||||
<span class="o_tab_title">Private Channels</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
@@ -238,8 +264,8 @@
|
||||
<t t-name="mail.discuss.MobileTabPane">
|
||||
<div class="o_mail_discuss_tab_pane" t-att-data-type="type">
|
||||
<div t-if="!disableAddThread" class="o_mail_add_thread" t-att-data-type="type">
|
||||
<span t-if="type == 'private' || type == 'public'">#</span>
|
||||
<t t-set="inputPlaceholder" t-if="type == 'private' || type == 'public'">Add a channel</t>
|
||||
<span t-if="type == 'multi_user_channel'">#</span>
|
||||
<t t-set="inputPlaceholder" t-if="type == 'multi_user_channel'">Add a channel</t>
|
||||
<t t-set="inputPlaceholder" t-if="type == 'dm_chat'">Open chat</t>
|
||||
<input type="text" t-attf-placeholder="#{inputPlaceholder}"/>
|
||||
</div>
|
||||
|
||||
@@ -191,7 +191,6 @@
|
||||
<t t-if="options.displayBottomThreadFreeSpace">
|
||||
<div class="o_thread_bottom_free_space"/>
|
||||
</t>
|
||||
<t t-if="thread.hasTypingNotification()" t-call="mail.widget.Thread.TypingNotificationBar"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
@@ -217,26 +216,11 @@
|
||||
<div class="o_mail_thread_content">
|
||||
<t t-if="options.messagesSeparatorPosition == 'top'" t-call="mail.MessagesSeparator"/>
|
||||
<t t-set="messages" t-value="messages.slice().reverse()"/>
|
||||
<t t-if="thread.hasTypingNotification()" t-call="mail.widget.Thread.TypingNotificationBar"/>
|
||||
<t t-call="mail.widget.Thread.Messages"/>
|
||||
<t t-if="options.displayLoadMore" t-call="mail.widget.Thread.LoadMore"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!--
|
||||
@param {mail.model.AbstractThread} thread
|
||||
-->
|
||||
<t t-name="mail.widget.Thread.TypingNotificationBar">
|
||||
<t t-if="options.displayOrder === ORDER.ASC">
|
||||
<div class="o_thread_typing_notification_free_space"/>
|
||||
<div class="o_thread_typing_notification_bar o_thread_order_asc"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="o_thread_typing_notification_free_space"/>
|
||||
<div class="o_thread_typing_notification_bar o_thread_order_desc"/>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<!--
|
||||
@param {mail.model.AbstractMessage[]} messages messages are ordered based
|
||||
on desired display order
|
||||
@@ -275,7 +259,7 @@
|
||||
@param {integer} [options.selectedMessageID]
|
||||
-->
|
||||
<t t-name="mail.widget.Thread.Message">
|
||||
<div t-att-class="'o_thread_message' + (message.getID() === options.selectedMessageID ? 'o_thread_selected_message ' : ' ') + (message.isDiscussion() or message.isNotification() ? ' o_mail_discussion' : ' o_mail_not_discussion')" t-att-data-message-id="message.getID()">
|
||||
<div t-att-class="'o_thread_message ' + (message.getID() === options.selectedMessageID ? 'o_thread_selected_message ' : ' ') + (message.isDiscussion() or message.isNotification() ? ' o_mail_discussion ' : ' o_mail_not_discussion ')" t-att-data-message-id="message.getID()">
|
||||
<div t-if="options.displayAvatars" class="o_thread_message_sidebar">
|
||||
<t t-if="message.hasAuthor()">
|
||||
<img t-if="displayAuthorMessages[message.getID()]"
|
||||
@@ -297,6 +281,7 @@
|
||||
<i t-if="!displayAuthorMessages[message.getID()] and options.displayStars and message.getType() !== 'notification'"
|
||||
t-att-class="'fa o_thread_message_star o_thread_icon ' + (message.isStarred() ? 'fa-star' : 'fa-star-o')"
|
||||
t-att-data-message-id="message.getID()" title="Mark as Todo" role="img" aria-label="Mark as todo"/>
|
||||
<t t-if="!displayAuthorMessages[message.getID()] and thread.hasSeenFeature()" t-call="mail.widget.Thread.Message.SeenIcon"/>
|
||||
</div>
|
||||
<div class="o_thread_message_core">
|
||||
<p t-if="displayAuthorMessages[message.getID()]" class="o_mail_info text-muted">
|
||||
@@ -335,6 +320,7 @@
|
||||
<i t-att-class="'o_thread_tooltip o_thread_message_email o_thread_message_email_' + message.getCustomerEmailStatus() + ' fa fa-envelope-o'" t-att-data-message-id="message.getID()"/>
|
||||
</span>
|
||||
<span t-attf-class="o_thread_icons">
|
||||
<t t-if="thread.hasSeenFeature()" t-call="mail.widget.Thread.Message.SeenIcon"/>
|
||||
<i t-if="options.displayStars and !message.isSystemNotification() and !message.needsModeration()"
|
||||
t-att-class="'fa fa-lg o_thread_icon o_thread_message_star ' + (message.isStarred() ? 'fa-star' : 'fa-star-o')"
|
||||
t-att-data-message-id="message.getID()" title="Mark as Todo" role="img" aria-label="Mark as Todo"/>
|
||||
@@ -379,6 +365,70 @@
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<!--
|
||||
Display the seen icon of a message in the thread.
|
||||
It shows the fetch 'check' before the seen 'check'. We change the order in the template
|
||||
so that fetch 'check' is on top of 'seen' check (z-index does not work with absolute positioning)
|
||||
|
||||
@param {mail.model.Thread} thread
|
||||
@param {mail.model.Message} message
|
||||
-->
|
||||
<t t-name="mail.widget.Thread.Message.SeenIcon">
|
||||
<t t-if="message.isMyselfAuthor() and message.getID() >= thread.getLastMessageIDSeenByEveryone()">
|
||||
<span t-attf-class="o_mail_thread_message_seen_icon #{thread.hasEveryoneSeen(message) ? 'o_all_seen' : ''}" t-att-data-message-id="message.getID()">
|
||||
<t t-if="thread.hasSomeoneFetched(message)">
|
||||
<i class="fa fa-check"/>
|
||||
</t>
|
||||
<t t-if="thread.hasSomeoneSeen(message)">
|
||||
<i class="fa fa-check"/>
|
||||
</t>
|
||||
</span>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<!--
|
||||
Display the popover content when clicking the seen icion of a message in the thread.
|
||||
|
||||
List the members that have received the messages, and the members that have seen it.
|
||||
|
||||
@param {mail.model.Thread} thread
|
||||
@param {mail.model.Message} message
|
||||
-->
|
||||
<t t-name="mail.widget.Thread.Message.SeenIconPopoverContent">
|
||||
<div class="o_mail_thread_message_seen_icon_content">
|
||||
<t t-if="thread.hasEveryoneSeen(message)">
|
||||
<p>Seen by Everyone</p>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-if="thread.hasSomeoneSeen(message)">
|
||||
<t t-set="seen_members" t-value="thread.getSeenMembers(message)"/>
|
||||
Seen by:
|
||||
<ul>
|
||||
<li t-foreach="seen_members" t-as="member">
|
||||
<t t-esc="member.name"/> (<t t-esc="member.email"/>)
|
||||
</li>
|
||||
</ul>
|
||||
</t>
|
||||
<t t-if="thread.hasSomeoneFetched(message)">
|
||||
<t t-if="thread.hasEveryoneFetched(message)">
|
||||
<p>Received by Everyone</p>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-set="fetched_members" t-value="thread.getFetchedNotSeenMembers(message)"/>
|
||||
<t t-if="!_.isEmpty(fetched_members)">
|
||||
Received by:
|
||||
<ul>
|
||||
<li t-foreach="fetched_members" t-as="member">
|
||||
<t t-esc="member.name"/> (<t t-esc="member.email"/>)
|
||||
</li>
|
||||
</ul>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!--
|
||||
@param {Object[]|undefined} list of customer email data of a given message.
|
||||
A customer email data is a list with the following format:
|
||||
@@ -550,4 +600,16 @@
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!--
|
||||
@param {mail.model.Thread} thread with typing feature
|
||||
-->
|
||||
<t t-name="mail.ThreadTypingIcon">
|
||||
<span class="o_mail_thread_typing_icon" t-att-title="thread.getTypingMembersToText()">
|
||||
<span class="o_mail_thread_typing_icon_dot"/>
|
||||
<span class="o_mail_thread_typing_icon_dot"/>
|
||||
<span class="o_mail_thread_typing_icon_dot"/>
|
||||
</span>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
||||
@@ -26,7 +26,7 @@ QUnit.test('mobile basic rendering', function (assert) {
|
||||
// /mail/client_action route is always called when the test suite is
|
||||
// launched), and we must wait for this RPC to be done before starting to
|
||||
// test the interface. This should be refactored to facilitate the testing.
|
||||
assert.expect(19);
|
||||
assert.expect(16);
|
||||
var done = assert.async();
|
||||
|
||||
createDiscuss({
|
||||
@@ -35,6 +35,7 @@ QUnit.test('mobile basic rendering', function (assert) {
|
||||
params: {},
|
||||
data: this.data,
|
||||
services: this.services,
|
||||
debug: true,
|
||||
}).then(function (discuss) {
|
||||
// test basic rendering in mobile
|
||||
assert.containsOnce(discuss, '.o_mail_discuss_content .o_mail_no_content',
|
||||
@@ -49,7 +50,7 @@ QUnit.test('mobile basic rendering', function (assert) {
|
||||
"should be in inbox");
|
||||
assert.hasClass($('.o_mail_discuss_button_dm_chat'),'d-none',
|
||||
"should have invisible button 'New Message'");
|
||||
assert.hasClass($('.o_mail_discuss_button_public'),'d-none',
|
||||
assert.hasClass($('.o_mail_discuss_button_multi_user_channel'),'d-none',
|
||||
"should have invisible button 'New Channel'");
|
||||
|
||||
// move to 'Chat' tab
|
||||
@@ -62,29 +63,19 @@ QUnit.test('mobile basic rendering', function (assert) {
|
||||
"should have a button to open DM chat in 'Chat' tab");
|
||||
assert.doesNotHaveClass($('.o_mail_discuss_button_dm_chat'), 'd-none',
|
||||
"should be visible in 'Chat' tab");
|
||||
assert.hasClass($('.o_mail_discuss_button_public'),'d-none',
|
||||
assert.hasClass($('.o_mail_discuss_button_multi_user_channel'),'d-none',
|
||||
"should have invisible button 'New Channel' in 'Chat' tab");
|
||||
testUtils.dom.click($('.o_mail_discuss_button_dm_chat'));
|
||||
assert.containsOnce(discuss, '.o_mail_add_thread input:visible',
|
||||
"should display the input to add a channel");
|
||||
|
||||
// move to 'Channels' tab
|
||||
testUtils.dom.click(discuss.$('.o_mail_mobile_tab[data-type=public]'));
|
||||
testUtils.dom.click(discuss.$('.o_mail_mobile_tab[data-type=multi_user_channel]'));
|
||||
assert.hasClass($('.o_mail_discuss_button_dm_chat'),'d-none',
|
||||
"should have invisible button 'New Message' in 'Channels' tab");
|
||||
assert.doesNotHaveClass($('.o_mail_discuss_button_public'), 'd-none',
|
||||
assert.doesNotHaveClass($('.o_mail_discuss_button_multi_user_channel'), 'd-none',
|
||||
"should have visible button 'New Channel' in 'Channels' tab");
|
||||
testUtils.dom.click($('.o_mail_discuss_button_public'));
|
||||
assert.containsOnce(discuss, '.o_mail_add_thread input:visible',
|
||||
"should display the input to add a channel");
|
||||
|
||||
// move to Private Channels tab
|
||||
testUtils.dom.click(discuss.$('.o_mail_mobile_tab[data-type=private]'));
|
||||
assert.hasClass($('.o_mail_discuss_button_dm_chat'),'d-none',
|
||||
"should have invisible button 'New Message' in 'Private Channels' tab");
|
||||
assert.doesNotHaveClass($('.o_mail_discuss_button_private'), 'd-none',
|
||||
"should have invisible button 'New Channel' in 'Private Channels' tab");
|
||||
testUtils.dom.click($('.o_mail_discuss_button_private'));
|
||||
testUtils.dom.click($('.o_mail_discuss_button_multi_user_channel'));
|
||||
assert.containsOnce(discuss, '.o_mail_add_thread input:visible',
|
||||
"should display the input to add a channel");
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -906,6 +906,10 @@ QUnit.test('confirm dialog when administrator leave (not chat) channel', functio
|
||||
})
|
||||
.then(function (discuss) {
|
||||
// Unsubscribe on MyChannel as administrator
|
||||
// The unpin icon is only shown when hovering on sidebar item.
|
||||
// helpter dom.click needs the element to be visible, hence the
|
||||
// hack on display of this icon.
|
||||
discuss.$('.o_mail_partner_unpin').css('display', 'block');
|
||||
testUtils.dom.click(discuss.$('.o_mail_partner_unpin'));
|
||||
|
||||
assert.strictEqual($('.modal-dialog').length, 1,
|
||||
@@ -1219,5 +1223,348 @@ QUnit.test('reply to message from inbox', function (assert) {
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('discard replying to message from inbox', function (assert) {
|
||||
var done = assert.async();
|
||||
assert.expect(4);
|
||||
|
||||
var self = this;
|
||||
this.data['mail.message'].records = [{
|
||||
author_id: [5, 'Demo User'],
|
||||
body: '<p>test 1</p>',
|
||||
id: 1,
|
||||
needaction: true,
|
||||
needaction_partner_ids: [3],
|
||||
res_id: 100,
|
||||
model: 'some.document',
|
||||
record_name: 'SomeDocument',
|
||||
}];
|
||||
this.data.initMessaging = {
|
||||
needaction_inbox_counter: 1,
|
||||
};
|
||||
|
||||
createDiscuss({
|
||||
id: 1,
|
||||
context: {},
|
||||
params: {},
|
||||
data: this.data,
|
||||
services: this.services,
|
||||
session: { partner_id: 3 },
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'message_post') {
|
||||
assert.step(args.method);
|
||||
assert.strictEqual(args.model, 'some.document',
|
||||
"should post message to correct document model");
|
||||
assert.strictEqual(args.args[0], 100,
|
||||
"should post message to correct document ID");
|
||||
|
||||
self.data['mail.message'].records.push({
|
||||
author_id: [3, 'Me'],
|
||||
body: args.kwargs.body,
|
||||
id: 2,
|
||||
res_id: 100,
|
||||
model: 'some.document',
|
||||
record_name: 'SomeDocument',
|
||||
});
|
||||
return $.when(2);
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
})
|
||||
.then(function (discuss) {
|
||||
testUtils.dom.click(discuss.$('.o_thread_message_reply'));
|
||||
assert.containsOnce(discuss, '.o_thread_selected_message',
|
||||
"should have a message selected");
|
||||
|
||||
var $composer = discuss.$('.o_thread_composer_extended');
|
||||
assert.containsOnce($composer, '.o_composer_button_discard',
|
||||
"should have button to discard replying to message");
|
||||
|
||||
testUtils.dom.click($composer.find('.o_composer_button_discard'));
|
||||
assert.isNotVisible($composer,
|
||||
"extended composer should become hidden on discard");
|
||||
assert.containsNone(discuss, '.o_thread_selected_message',
|
||||
"should not longer have a message selected");
|
||||
|
||||
discuss.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('no quick search channels in the sidebar with less than 20 channels', function (assert) {
|
||||
assert.expect(3);
|
||||
var done = assert.async();
|
||||
|
||||
var channelsData = [];
|
||||
_.each(_.range(0, 19), function (num) {
|
||||
channelsData.push({
|
||||
id: num,
|
||||
channel_type: "channel",
|
||||
name: "Channel" + num,
|
||||
});
|
||||
});
|
||||
|
||||
this.data.initMessaging = {
|
||||
channel_slots: {
|
||||
channel_channel: channelsData,
|
||||
},
|
||||
};
|
||||
|
||||
createDiscuss({
|
||||
id: 1,
|
||||
context: {},
|
||||
params: {},
|
||||
data: this.data,
|
||||
services: this.services,
|
||||
})
|
||||
.then(function (discuss) {
|
||||
var $sidebar = discuss.$('.o_mail_discuss_sidebar');
|
||||
|
||||
var $channelsSidebar = $sidebar.find('.o_mail_discuss_sidebar_channels');
|
||||
assert.strictEqual($channelsSidebar.length, 1,
|
||||
"should show channels and dms in sidebar");
|
||||
assert.strictEqual($channelsSidebar.find('.o_mail_discuss_item').length, 19,
|
||||
"should show 19 channels in sidebar");
|
||||
assert.strictEqual(discuss.$('.o_discuss_sidebar_quick_search').length, 0,
|
||||
"should not display a quick search");
|
||||
|
||||
discuss.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('quick search channels in the sidebar with more than 20 channels', function (assert) {
|
||||
assert.expect(7);
|
||||
var done = assert.async();
|
||||
|
||||
var channelsData = [];
|
||||
var channelANum = 15;
|
||||
_.each(_.range(0, channelANum), function (num) {
|
||||
var channel = {
|
||||
id: num,
|
||||
channel_type: "channel",
|
||||
name: "ChannelA" + num,
|
||||
};
|
||||
channelsData.push(channel);
|
||||
});
|
||||
var channelBNum = 5;
|
||||
_.each(_.range(0, channelBNum), function (num) {
|
||||
var channel = {
|
||||
id: channelANum + num,
|
||||
channel_type: "channel",
|
||||
name: "ChannelB" + num,
|
||||
};
|
||||
channelsData.push(channel);
|
||||
});
|
||||
|
||||
this.data.initMessaging = {
|
||||
channel_slots: {
|
||||
channel_channel: channelsData,
|
||||
},
|
||||
};
|
||||
|
||||
createDiscuss({
|
||||
id: 1,
|
||||
context: {},
|
||||
params: {},
|
||||
data: this.data,
|
||||
services: this.services,
|
||||
})
|
||||
.then(function (discuss) {
|
||||
var $sidebar = discuss.$('.o_mail_discuss_sidebar');
|
||||
|
||||
var $channelsSidebar = $sidebar.find('.o_mail_discuss_sidebar_channels');
|
||||
assert.strictEqual($channelsSidebar.length, 1,
|
||||
"should show channels in sidebar");
|
||||
assert.strictEqual($channelsSidebar.find('.o_mail_discuss_item').length, 20,
|
||||
"should show 20 channels in sidebar");
|
||||
assert.strictEqual(discuss.$('.o_discuss_sidebar_quick_search').length, 1,
|
||||
"should display a quick search");
|
||||
|
||||
discuss.$('.o_discuss_sidebar_quick_search input').val('channelA').trigger('input');
|
||||
$channelsSidebar = $sidebar.find('.o_mail_discuss_sidebar_channels');
|
||||
assert.strictEqual($channelsSidebar.find('.o_mail_discuss_item').length, 15,
|
||||
"should now show 15 channels in sidebar");
|
||||
|
||||
discuss.$('.o_discuss_sidebar_quick_search input').val('channelB').trigger('input');
|
||||
|
||||
$channelsSidebar = $sidebar.find('.o_mail_discuss_sidebar_channels');
|
||||
assert.strictEqual($channelsSidebar.find('.o_mail_discuss_item').length, 5,
|
||||
"should now show 5 channels in sidebar");
|
||||
|
||||
discuss.$('.o_discuss_sidebar_quick_search input').val('channelB4').trigger('input');
|
||||
|
||||
$channelsSidebar = $sidebar.find('.o_mail_discuss_sidebar_channels');
|
||||
assert.strictEqual($channelsSidebar.find('.o_mail_discuss_item').length, 1,
|
||||
"should now show a single channel in sidebar");
|
||||
assert.strictEqual($channelsSidebar.find('.o_thread_name').text().replace(/\s/g, ''),
|
||||
'#ChannelB4',
|
||||
"should have searched the correct channel B4");
|
||||
|
||||
discuss.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('select emoji replaces cursor position', function (assert) {
|
||||
assert.expect(5);
|
||||
var done = assert.async();
|
||||
|
||||
this.data.initMessaging = {
|
||||
channel_slots: {
|
||||
channel_channel: [{
|
||||
id: 1,
|
||||
channel_type: "channel",
|
||||
name: "general",
|
||||
}],
|
||||
},
|
||||
};
|
||||
|
||||
createDiscuss({
|
||||
id: 1,
|
||||
context: {},
|
||||
params: {},
|
||||
data: this.data,
|
||||
services: this.services,
|
||||
})
|
||||
.then(function (discuss) {
|
||||
var $general = discuss.$('.o_mail_discuss_sidebar')
|
||||
.find('.o_mail_discuss_item[data-thread-id=1]');
|
||||
assert.strictEqual($general.length, 1,
|
||||
"should have the channel item with id 1");
|
||||
assert.hasAttrValue($general, 'title', 'general',
|
||||
"should have the title 'general'");
|
||||
|
||||
// click on general
|
||||
testUtils.dom.click($general);
|
||||
|
||||
// general uses basic composer, so this is the 1st composer
|
||||
// (2nd composer is hidden)
|
||||
var $composer = discuss.$('.o_composer_text_field').first();
|
||||
|
||||
testUtils.fields.editInput($composer, 'abcdefgh');
|
||||
|
||||
assert.strictEqual($composer.val(), "abcdefgh");
|
||||
|
||||
// cursor position: ab|cd|efgh (selecting c and d)
|
||||
$composer[0].setSelectionRange(2, 4);
|
||||
|
||||
testUtils.dom.click(discuss.$('.o_composer_button_emoji'));
|
||||
testUtils.dom.click(discuss.$('.o_mail_emoji[data-emoji=":)"]'));
|
||||
|
||||
assert.strictEqual($composer.val(), "ab :) efgh",
|
||||
"should have inserted emoji ");
|
||||
|
||||
testUtils.dom.click(discuss.$('.o_composer_button_emoji'));
|
||||
testUtils.dom.click(discuss.$('.o_mail_emoji[data-emoji=":)"]'));
|
||||
|
||||
assert.strictEqual($composer.val(), "ab :) :) efgh",
|
||||
"should have inserted emoji after previously inserted emoji");
|
||||
|
||||
discuss.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('rename DM conversation', function (assert) {
|
||||
assert.expect(11);
|
||||
var done = assert.async();
|
||||
|
||||
this.data.initMessaging = {
|
||||
channel_slots: {
|
||||
channel_direct_message: [{
|
||||
id: 1,
|
||||
channel_type: "chat",
|
||||
create_uid: 3,
|
||||
direct_partner: [{
|
||||
id: 7,
|
||||
im_status: 'online',
|
||||
name: 'Demo User',
|
||||
}],
|
||||
}],
|
||||
},
|
||||
};
|
||||
|
||||
createDiscuss({
|
||||
id: 1,
|
||||
context: {},
|
||||
params: {},
|
||||
data: this.data,
|
||||
services: this.services,
|
||||
session: { partner_id: 3 },
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'channel_set_custom_name') {
|
||||
assert.step(args.method);
|
||||
assert.strictEqual(args.args[0], 1);
|
||||
assert.strictEqual(args.kwargs.name, "Demo");
|
||||
return $.when("Demo");
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
})
|
||||
.then(function (discuss) {
|
||||
var $dm = discuss.$('.o_mail_discuss_item[data-thread-id=1]');
|
||||
assert.isVisible($dm, "should display DM in the discuss sidebar");
|
||||
assert.strictEqual($dm.find('.o_thread_name').text().trim(), "Demo User");
|
||||
|
||||
// The settings icon is only shown when hovering on sidebar item.
|
||||
// helpter dom.click needs the element to be visible, hence the
|
||||
// hack on display of this icon.
|
||||
discuss.$('.o_mail_channel_settings').css('display', 'block');
|
||||
testUtils.dom.click(discuss.$('.o_mail_channel_settings'));
|
||||
|
||||
assert.isVisible($('.modal-dialog'));
|
||||
assert.strictEqual($('.modal-title').text(), "Rename conversation");
|
||||
assert.isVisible($('.o_mail_discuss_rename_channel_input'));
|
||||
assert.strictEqual($('.o_mail_discuss_rename_channel_input').val(), "Demo User");
|
||||
|
||||
testUtils.fields.editInput($('.o_mail_discuss_rename_channel_input'), "Demo");
|
||||
testUtils.dom.click($('.o_mail_conversation_rename'));
|
||||
$dm = discuss.$('.o_mail_discuss_item[data-thread-id=1]');
|
||||
assert.verifySteps(['channel_set_custom_name']);
|
||||
assert.strictEqual($dm.find('.o_thread_name').text().trim(), "Demo");
|
||||
|
||||
discuss.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('custom-named DM conversation', function (assert) {
|
||||
assert.expect(2);
|
||||
var done = assert.async();
|
||||
|
||||
this.data.initMessaging = {
|
||||
channel_slots: {
|
||||
channel_direct_message: [{
|
||||
id: 1,
|
||||
channel_type: "chat",
|
||||
create_uid: 3,
|
||||
direct_partner: [{
|
||||
id: 7,
|
||||
im_status: 'online',
|
||||
name: 'Demo User',
|
||||
}],
|
||||
custom_channel_name: 'My Buddy',
|
||||
}],
|
||||
},
|
||||
};
|
||||
|
||||
createDiscuss({
|
||||
id: 1,
|
||||
context: {},
|
||||
params: {},
|
||||
data: this.data,
|
||||
services: this.services,
|
||||
session: { partner_id: 3 },
|
||||
})
|
||||
.then(function (discuss) {
|
||||
var $dm = discuss.$('.o_mail_discuss_item[data-thread-id=1]');
|
||||
assert.isVisible($dm, "should display DM in the discuss sidebar");
|
||||
assert.strictEqual($dm.find('.o_thread_name').text().trim(), "My Buddy");
|
||||
|
||||
discuss.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -54,7 +54,6 @@ QUnit.module('Discuss (Typing Notifications)', {
|
||||
this.patch();
|
||||
|
||||
// Frequently used data for tests
|
||||
this.generalChannelID = 1;
|
||||
this.myName = "Myself";
|
||||
this.myPartnerID = 30;
|
||||
|
||||
@@ -62,7 +61,7 @@ QUnit.module('Discuss (Typing Notifications)', {
|
||||
initMessaging: {
|
||||
channel_slots: {
|
||||
channel_channel: [{
|
||||
id: this.generalChannelID,
|
||||
id: 1,
|
||||
channel_type: "channel",
|
||||
name: "general",
|
||||
}],
|
||||
@@ -114,10 +113,10 @@ QUnit.module('Discuss (Typing Notifications)', {
|
||||
records: [{
|
||||
author_id: [this.myPartnerID, this.myName],
|
||||
body: "<p>first message</p>",
|
||||
channel_ids: [this.generalChannelID],
|
||||
channel_ids: [1],
|
||||
id: 100,
|
||||
model: 'mail.channel',
|
||||
res_id: this.generalChannelID,
|
||||
res_id: 1,
|
||||
}],
|
||||
},
|
||||
};
|
||||
@@ -158,7 +157,6 @@ QUnit.test('notify is typing', function (assert) {
|
||||
assert.expect(11);
|
||||
var done = assert.async();
|
||||
|
||||
var self = this;
|
||||
// this is to track the step that we are testing in the `notify_typing`
|
||||
// mocked RPC
|
||||
var step = 1;
|
||||
@@ -175,13 +173,13 @@ QUnit.test('notify is typing', function (assert) {
|
||||
var channelID = args.args[0];
|
||||
if (step === 1) {
|
||||
// user notify that he is typing
|
||||
assert.strictEqual(channelID, self.generalChannelID,
|
||||
assert.strictEqual(channelID, 1,
|
||||
"should send a typing notification on general channel");
|
||||
assert.strictEqual(args.kwargs.is_typing, true,
|
||||
"should send a currently typing notification");
|
||||
step++;
|
||||
} else if (step === 2) {
|
||||
assert.strictEqual(channelID, self.generalChannelID,
|
||||
assert.strictEqual(channelID, 1,
|
||||
"should send a typing notification on general channel");
|
||||
assert.strictEqual(args.kwargs.is_typing, false,
|
||||
"should send a stopped typing notification");
|
||||
@@ -224,12 +222,9 @@ QUnit.test('notify is typing', function (assert) {
|
||||
});
|
||||
|
||||
QUnit.test('receive is typing notification', function (assert) {
|
||||
assert.expect(7);
|
||||
var done = assert.async();
|
||||
assert.expect(10);
|
||||
|
||||
var self = this;
|
||||
var fetchListenersDef = $.Deferred();
|
||||
|
||||
createDiscuss({
|
||||
id: 1,
|
||||
context: {},
|
||||
@@ -240,7 +235,6 @@ QUnit.test('receive is typing notification', function (assert) {
|
||||
// when receiving an 'is typing' notification, fetch the list of
|
||||
// members of this channel if we haven't done yet.
|
||||
if (args.method === 'channel_fetch_listeners') {
|
||||
fetchListenersDef.resolve();
|
||||
return $.when([
|
||||
{ id: self.myPartnerID, name: self.myName },
|
||||
{ id: 42, name: "Someone" },
|
||||
@@ -257,49 +251,58 @@ QUnit.test('receive is typing notification', function (assert) {
|
||||
assert.hasAttrValue($general, 'title', 'general',
|
||||
"should have the title 'general'");
|
||||
|
||||
// pick 1st composer (basic), not 2nd composer (extended, hidden)
|
||||
var $composer = discuss.$('.o_thread_composer').first();
|
||||
|
||||
// click on general
|
||||
testUtils.dom.click($general);
|
||||
assert.containsOnce(discuss, '.o_thread_typing_notification_bar',
|
||||
"there should be a typing notification bar on the thread");
|
||||
assert.isNotVisible(discuss.$('.o_thread_typing_notification_bar'),
|
||||
"the typing notification bar should be hidden by default");
|
||||
assert.containsNone($general, '.o_mail_thread_typing_icon',
|
||||
"should no longer show someone is typing in sidebar of discuss");
|
||||
assert.strictEqual($composer.find('.o_composer_thread_typing').text().trim(), "",
|
||||
"should no longer show someone is typing on the composer of active thread");
|
||||
|
||||
self.simulateIsTyping({
|
||||
channelID: self.generalChannelID,
|
||||
channelID: 1,
|
||||
isTyping: true,
|
||||
partnerID: 42,
|
||||
widget: discuss,
|
||||
});
|
||||
|
||||
fetchListenersDef.then(function () {
|
||||
assert.isVisible(discuss.$('.o_thread_typing_notification_bar'),
|
||||
"the typing notification bar should be visible when someone is typing something");
|
||||
assert.strictEqual(discuss.$('.o_thread_typing_notification_bar').text(),
|
||||
"Someone is typing...",
|
||||
"It should display that the other user is typing something");
|
||||
$general = discuss.$('.o_mail_discuss_sidebar')
|
||||
.find('.o_mail_discuss_item[data-thread-id=1]');
|
||||
assert.containsOnce($general, '.o_mail_thread_typing_icon',
|
||||
"should have a thread typing icon next to general icon in the sidebar");
|
||||
assert.hasAttrValue($general.find('.o_mail_thread_typing_icon'), 'title',
|
||||
"Someone is typing...",
|
||||
"should show who is typing on hover on this thread typing icon");
|
||||
assert.containsOnce($composer, '.o_composer_thread_typing',
|
||||
"should show typing info on the composer of active thread");
|
||||
assert.strictEqual($composer.find('.o_composer_thread_typing').text().trim(),
|
||||
"Someone is typing...",
|
||||
"should show who is typing on the composer of active thread");
|
||||
|
||||
self.simulateIsTyping({
|
||||
channelID: self.generalChannelID,
|
||||
isTyping: false,
|
||||
partnerID: 42,
|
||||
widget: discuss,
|
||||
});
|
||||
assert.isNotVisible(discuss.$('.o_thread_typing_notification_bar'),
|
||||
"the typing notification bar should be hidden when someone stops typing something");
|
||||
|
||||
discuss.destroy();
|
||||
done();
|
||||
self.simulateIsTyping({
|
||||
channelID: 1,
|
||||
isTyping: false,
|
||||
partnerID: 42,
|
||||
widget: discuss,
|
||||
});
|
||||
|
||||
$general = discuss.$('.o_mail_discuss_sidebar')
|
||||
.find('.o_mail_discuss_item[data-thread-id=1]');
|
||||
assert.containsNone($general, '.o_mail_thread_typing_icon',
|
||||
"should no longer show someone is typing in sidebar of discuss");
|
||||
assert.strictEqual($composer.find('.o_composer_thread_typing').text().trim(), "",
|
||||
"should no longer show someone is typing on the composer of active thread");
|
||||
|
||||
discuss.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('receive message of someone that was typing something', function (assert) {
|
||||
assert.expect(4);
|
||||
var done = assert.async();
|
||||
assert.expect(6);
|
||||
|
||||
var self = this;
|
||||
var fetchListenersDef = $.Deferred();
|
||||
|
||||
createDiscuss({
|
||||
id: 1,
|
||||
context: {},
|
||||
@@ -310,7 +313,6 @@ QUnit.test('receive message of someone that was typing something', function (ass
|
||||
// when receiving an 'is typing' notification, fetch the list of
|
||||
// members of this channel if we haven't done yet.
|
||||
if (args.method === 'channel_fetch_listeners') {
|
||||
fetchListenersDef.resolve();
|
||||
return $.when([
|
||||
{ id: self.myPartnerID, name: self.myName },
|
||||
{ id: 42, name: "Someone" },
|
||||
@@ -324,51 +326,56 @@ QUnit.test('receive message of someone that was typing something', function (ass
|
||||
var $general = discuss.$('.o_mail_discuss_sidebar .o_mail_discuss_item[data-thread-id=1]');
|
||||
testUtils.dom.click($general);
|
||||
|
||||
assert.isNotVisible(discuss.$('.o_thread_typing_notification_bar'),
|
||||
"the typing notification bar should be hidden by default");
|
||||
// pick 1st composer (basic), not 2nd composer (extended, hidden)
|
||||
var $composer = discuss.$('.o_thread_composer').first();
|
||||
|
||||
self.simulateIsTyping({
|
||||
channelID: self.generalChannelID,
|
||||
channelID: 1,
|
||||
isTyping: true,
|
||||
partnerID: 42,
|
||||
widget: discuss,
|
||||
});
|
||||
|
||||
fetchListenersDef.then(function () {
|
||||
assert.isVisible(discuss.$('.o_thread_typing_notification_bar'),
|
||||
"the typing notification bar should be visible when someone is typing something");
|
||||
assert.strictEqual(discuss.$('.o_thread_typing_notification_bar').text(),
|
||||
"Someone is typing...",
|
||||
"It should display that the other user is typing something");
|
||||
$general = discuss.$('.o_mail_discuss_sidebar')
|
||||
.find('.o_mail_discuss_item[data-thread-id=1]');
|
||||
assert.containsOnce($general, '.o_mail_thread_typing_icon',
|
||||
"should have a thread typing icon next to general icon in the sidebar");
|
||||
assert.hasAttrValue($general.find('.o_mail_thread_typing_icon'), 'title',
|
||||
"Someone is typing...",
|
||||
"should show who is typing on hover on this thread typing icon");
|
||||
assert.containsOnce($composer, '.o_composer_thread_typing',
|
||||
"should show typing info on the composer of active thread");
|
||||
assert.strictEqual($composer.find('.o_composer_thread_typing').text().trim(),
|
||||
"Someone is typing...",
|
||||
"should show who is typing on the composer of active thread");
|
||||
|
||||
// Simulate receive message from the person typing something
|
||||
var messageData = {
|
||||
author_id: [42, 'Someone'],
|
||||
body: "<p>test</p>",
|
||||
channel_ids: [self.generalChannelID],
|
||||
id: 101,
|
||||
model: 'mail.channel',
|
||||
res_id: self.generalChannelID,
|
||||
};
|
||||
var notification = [[false, 'mail.channel', self.generalChannelID], messageData];
|
||||
discuss.call('bus_service', 'trigger', 'notification', [notification]);
|
||||
// Simulate receive message from the person typing something
|
||||
var messageData = {
|
||||
author_id: [42, 'Someone'],
|
||||
body: "<p>test</p>",
|
||||
channel_ids: [1],
|
||||
id: 101,
|
||||
model: 'mail.channel',
|
||||
res_id: 1,
|
||||
};
|
||||
var notification = [[false, 'mail.channel', 1], messageData];
|
||||
discuss.call('bus_service', 'trigger', 'notification', [notification]);
|
||||
|
||||
assert.isNotVisible(discuss.$('.o_thread_typing_notification_bar'),
|
||||
"the typing notification bar should be hidden when receiving message from someone that was typing some text");
|
||||
$general = discuss.$('.o_mail_discuss_sidebar')
|
||||
.find('.o_mail_discuss_item[data-thread-id=1]');
|
||||
assert.containsNone($general, '.o_mail_thread_typing_icon',
|
||||
"should no longer show someone is typing in sidebar of discuss, after receiving message");
|
||||
assert.strictEqual($composer.find('.o_composer_thread_typing').text().trim(), "",
|
||||
"should no longer show someone is typing on the composer of active thread, after receiving message");
|
||||
|
||||
discuss.destroy();
|
||||
done();
|
||||
});
|
||||
discuss.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('do not display myself as typing', function (assert) {
|
||||
assert.expect(3);
|
||||
var done = assert.async();
|
||||
assert.expect(2);
|
||||
|
||||
var self = this;
|
||||
var fetchListenersDef = $.Deferred();
|
||||
|
||||
createDiscuss({
|
||||
id: 1,
|
||||
context: {},
|
||||
@@ -379,7 +386,6 @@ QUnit.test('do not display myself as typing', function (assert) {
|
||||
// when receiving an 'is typing' notification, fetch the list of
|
||||
// members of this channel if we haven't done yet.
|
||||
if (args.method === 'channel_fetch_listeners') {
|
||||
fetchListenersDef.resolve();
|
||||
return $.when([
|
||||
{ id: self.myPartnerID, name: self.myName },
|
||||
]);
|
||||
@@ -388,29 +394,28 @@ QUnit.test('do not display myself as typing', function (assert) {
|
||||
},
|
||||
session: { partner_id: this.myPartnerID },
|
||||
}).then(function (discuss) {
|
||||
// click on general
|
||||
// click on general channel
|
||||
var $general = discuss.$('.o_mail_discuss_sidebar')
|
||||
.find('.o_mail_discuss_item[data-thread-id=1]');
|
||||
testUtils.dom.click($general);
|
||||
assert.containsOnce(discuss, '.o_thread_typing_notification_bar',
|
||||
"there should be a typing notification bar on the thread");
|
||||
assert.isNotVisible(discuss.$('.o_thread_typing_notification_bar'),
|
||||
"the typing notification bar should be hidden by default");
|
||||
// pick 1st composer (basic), not 2nd composer (extended, hidden)
|
||||
var $composer = discuss.$('.o_thread_composer').first();
|
||||
|
||||
self.simulateIsTyping({
|
||||
channelID: self.generalChannelID,
|
||||
channelID: 1,
|
||||
isTyping: true,
|
||||
partnerID: self.myPartnerID,
|
||||
widget: discuss,
|
||||
});
|
||||
|
||||
fetchListenersDef.then(function () {
|
||||
assert.isNotVisible(discuss.$('.o_thread_typing_notification_bar'),
|
||||
"the typing notification bar should still be hidden when I am typing something");
|
||||
$general = discuss.$('.o_mail_discuss_sidebar')
|
||||
.find('.o_mail_discuss_item[data-thread-id=1]');
|
||||
assert.containsNone($general, '.o_mail_thread_typing_icon',
|
||||
"should not show current user is typing in sidebar of discuss");
|
||||
assert.strictEqual($composer.find('.o_composer_thread_typing').text().trim(), "",
|
||||
"should not show current user is typing on the composer of active thread");
|
||||
|
||||
discuss.destroy();
|
||||
done();
|
||||
});
|
||||
discuss.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -420,12 +425,9 @@ QUnit.test('several users typing something at the same time', function (assert)
|
||||
// have been typing something for the longest time.
|
||||
// @see mail.model.ThreadTypingMixin:getTypingMembersToText for the rule
|
||||
// applied for displaying the typing notification text.
|
||||
assert.expect(9);
|
||||
var done = assert.async();
|
||||
assert.expect(10);
|
||||
|
||||
var self = this;
|
||||
var fetchListenersDef = $.Deferred();
|
||||
|
||||
createDiscuss({
|
||||
id: 1,
|
||||
context: {},
|
||||
@@ -436,7 +438,6 @@ QUnit.test('several users typing something at the same time', function (assert)
|
||||
// when receiving an 'is typing' notification, fetch the list of
|
||||
// members of this channel if we haven't done yet.
|
||||
if (args.method === 'channel_fetch_listeners') {
|
||||
fetchListenersDef.resolve();
|
||||
return $.when([
|
||||
{ id: self.myPartnerID, name: self.myName },
|
||||
{ id: 42, name: "Someone" },
|
||||
@@ -448,70 +449,85 @@ QUnit.test('several users typing something at the same time', function (assert)
|
||||
},
|
||||
session: { partner_id: this.myPartnerID },
|
||||
}).then(function (discuss) {
|
||||
|
||||
// click on general channel
|
||||
var $general = discuss.$('.o_mail_discuss_sidebar')
|
||||
.find('.o_mail_discuss_item[data-thread-id=1]');
|
||||
assert.strictEqual($general.length, 1,
|
||||
"should have the channel item with id 1");
|
||||
assert.hasAttrValue($general, 'title', 'general',
|
||||
"should have the title 'general'");
|
||||
|
||||
// click on general
|
||||
testUtils.dom.click($general);
|
||||
assert.containsOnce(discuss, '.o_thread_typing_notification_bar',
|
||||
"there should be a typing notification bar on the thread");
|
||||
assert.isNotVisible(discuss.$('.o_thread_typing_notification_bar'),
|
||||
"the typing notification bar should be hidden by default");
|
||||
|
||||
// pick 1st composer (basic), not 2nd composer (extended, hidden)
|
||||
var $composer = discuss.$('.o_thread_composer').first();
|
||||
|
||||
self.simulateIsTyping({
|
||||
channelID: self.generalChannelID,
|
||||
channelID: 1,
|
||||
isTyping: true,
|
||||
partnerID: 42,
|
||||
widget: discuss,
|
||||
});
|
||||
|
||||
fetchListenersDef.then(function () {
|
||||
assert.isVisible(discuss.$('.o_thread_typing_notification_bar'),
|
||||
"the typing notification bar should be visible when someone is typing something");
|
||||
assert.strictEqual(discuss.$('.o_thread_typing_notification_bar').text(),
|
||||
"Someone is typing...",
|
||||
"It should display that the other user is typing something");
|
||||
$general = discuss.$('.o_mail_discuss_sidebar')
|
||||
.find('.o_mail_discuss_item[data-thread-id=1]');
|
||||
assert.containsOnce($general, '.o_mail_thread_typing_icon',
|
||||
"should have a thread typing icon next to general icon in the sidebar");
|
||||
assert.hasAttrValue($general.find('.o_mail_thread_typing_icon'), 'title',
|
||||
"Someone is typing...",
|
||||
"should show someone is typing on hover on this thread typing icon");
|
||||
assert.containsOnce($composer, '.o_composer_thread_typing',
|
||||
"should show typing info on the composer of active thread");
|
||||
assert.strictEqual($composer.find('.o_composer_thread_typing').text().trim(),
|
||||
"Someone is typing...",
|
||||
"should show someone is typing on the composer of active thread");
|
||||
|
||||
self.simulateIsTyping({
|
||||
channelID: self.generalChannelID,
|
||||
isTyping: true,
|
||||
partnerID: 43,
|
||||
widget: discuss,
|
||||
});
|
||||
assert.strictEqual(discuss.$('.o_thread_typing_notification_bar').text(),
|
||||
"Someone and Anonymous are typing...",
|
||||
"It should display the two users that are typing something");
|
||||
|
||||
self.simulateIsTyping({
|
||||
channelID: self.generalChannelID,
|
||||
isTyping: true,
|
||||
partnerID: 44,
|
||||
widget: discuss,
|
||||
});
|
||||
assert.strictEqual(discuss.$('.o_thread_typing_notification_bar').text(),
|
||||
"Someone, Anonymous and more are typing...",
|
||||
"It should display that several users are typing something");
|
||||
|
||||
self.simulateIsTyping({
|
||||
channelID: self.generalChannelID,
|
||||
isTyping: false,
|
||||
partnerID: 42,
|
||||
widget: discuss,
|
||||
});
|
||||
|
||||
assert.strictEqual(discuss.$('.o_thread_typing_notification_bar').text(),
|
||||
"Anonymous and Shy Guy are typing...",
|
||||
"It should omit Someone that is typing");
|
||||
|
||||
discuss.destroy();
|
||||
done();
|
||||
self.simulateIsTyping({
|
||||
channelID: 1,
|
||||
isTyping: true,
|
||||
partnerID: 43,
|
||||
widget: discuss,
|
||||
});
|
||||
|
||||
$general = discuss.$('.o_mail_discuss_sidebar')
|
||||
.find('.o_mail_discuss_item[data-thread-id=1]');
|
||||
assert.hasAttrValue($general.find('.o_mail_thread_typing_icon'), 'title',
|
||||
"Someone and Anonymous are typing...",
|
||||
"should show someone and anonymous are simultaneously typing on hover on this thread typing icon");
|
||||
assert.strictEqual($composer.find('.o_composer_thread_typing').text().trim(),
|
||||
"Someone and Anonymous are typing...",
|
||||
"should show someone and anonymous are simultaneously typing on the composer of active thread");
|
||||
|
||||
self.simulateIsTyping({
|
||||
channelID: 1,
|
||||
isTyping: true,
|
||||
partnerID: 44,
|
||||
widget: discuss,
|
||||
});
|
||||
|
||||
$general = discuss.$('.o_mail_discuss_sidebar')
|
||||
.find('.o_mail_discuss_item[data-thread-id=1]');
|
||||
assert.hasAttrValue($general.find('.o_mail_thread_typing_icon'), 'title',
|
||||
"Someone, Anonymous and more are typing...",
|
||||
"should show 2 and more users are simultaneously typing on hover on this thread typing icon");
|
||||
assert.strictEqual($composer.find('.o_composer_thread_typing').text().trim(),
|
||||
"Someone, Anonymous and more are typing...",
|
||||
"should show 2 and more users are simultaneously typing on the composer of active thread");
|
||||
|
||||
self.simulateIsTyping({
|
||||
channelID: 1,
|
||||
isTyping: false,
|
||||
partnerID: 42,
|
||||
widget: discuss,
|
||||
});
|
||||
|
||||
$general = discuss.$('.o_mail_discuss_sidebar')
|
||||
.find('.o_mail_discuss_item[data-thread-id=1]');
|
||||
assert.hasAttrValue($general.find('.o_mail_thread_typing_icon'), 'title',
|
||||
"Anonymous and Shy Guy are typing...",
|
||||
"should no longer show than someone is typing on hover on this thread typing icon");
|
||||
assert.strictEqual($composer.find('.o_composer_thread_typing').text().trim(),
|
||||
"Anonymous and Shy Guy are typing...",
|
||||
"should no longer show than someone is typing on the composer of active thread");
|
||||
|
||||
discuss.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -520,12 +536,9 @@ QUnit.test('long typing partner A and in-between short typing partner B', functi
|
||||
// Let's suppose that A is typing a very long message. If B types a short
|
||||
// and sends it, it should display that B is typing, remove B from display
|
||||
// when receiving the message, but still keep A in the typing notification.
|
||||
assert.expect(4);
|
||||
var done = assert.async();
|
||||
assert.expect(8);
|
||||
|
||||
var self = this;
|
||||
var fetchListenersDef = $.Deferred();
|
||||
|
||||
createDiscuss({
|
||||
id: 1,
|
||||
context: {},
|
||||
@@ -536,7 +549,6 @@ QUnit.test('long typing partner A and in-between short typing partner B', functi
|
||||
// when receiving an 'is typing' notification, fetch the list of
|
||||
// members of this channel if we haven't done yet.
|
||||
if (args.method === 'channel_fetch_listeners') {
|
||||
fetchListenersDef.resolve();
|
||||
return $.when([
|
||||
{ id: self.myPartnerID, name: self.myName },
|
||||
{ id: 42, name: "A" },
|
||||
@@ -550,52 +562,136 @@ QUnit.test('long typing partner A and in-between short typing partner B', functi
|
||||
// click on general channel
|
||||
var $general = discuss.$('.o_mail_discuss_sidebar .o_mail_discuss_item[data-thread-id=1]');
|
||||
testUtils.dom.click($general);
|
||||
// pick 1st composer (basic), not 2nd composer (extended, hidden)
|
||||
var $composer = discuss.$('.o_thread_composer').first();
|
||||
|
||||
self.simulateIsTyping({
|
||||
channelID: self.generalChannelID,
|
||||
channelID: 1,
|
||||
isTyping: true,
|
||||
partnerID: 42,
|
||||
widget: discuss,
|
||||
});
|
||||
|
||||
fetchListenersDef.then(function () {
|
||||
assert.isVisible(discuss.$('.o_thread_typing_notification_bar'),
|
||||
"the typing notification bar should be visible when someone is typing something");
|
||||
assert.strictEqual(discuss.$('.o_thread_typing_notification_bar').text(),
|
||||
"A is typing...",
|
||||
"It should display that A is typing something");
|
||||
$general = discuss.$('.o_mail_discuss_sidebar')
|
||||
.find('.o_mail_discuss_item[data-thread-id=1]');
|
||||
assert.containsOnce($general, '.o_mail_thread_typing_icon',
|
||||
"should have a thread typing icon next to general icon in the sidebar");
|
||||
assert.hasAttrValue($general.find('.o_mail_thread_typing_icon'), 'title',
|
||||
"A is typing...",
|
||||
"should show A is typing on hover on this thread typing icon");
|
||||
assert.containsOnce($composer, '.o_composer_thread_typing',
|
||||
"should show typing info on the composer of active thread");
|
||||
assert.strictEqual($composer.find('.o_composer_thread_typing').text().trim(),
|
||||
"A is typing...",
|
||||
"should show A is typing on the composer of active thread");
|
||||
|
||||
self.simulateIsTyping({
|
||||
channelID: self.generalChannelID,
|
||||
isTyping: true,
|
||||
partnerID: 43,
|
||||
widget: discuss,
|
||||
});
|
||||
|
||||
// B comes after A in the display, because A is oldest typing partner.
|
||||
assert.strictEqual(discuss.$('.o_thread_typing_notification_bar').text(),
|
||||
"A and B are typing...",
|
||||
"It should display that A and B are typing something");
|
||||
|
||||
// Simulate receive message from B
|
||||
var messageData = {
|
||||
author_id: [43, 'Someone'],
|
||||
body: "<p>test</p>",
|
||||
channel_ids: [self.generalChannelID],
|
||||
id: 101,
|
||||
model: 'mail.channel',
|
||||
res_id: self.generalChannelID,
|
||||
};
|
||||
var notification = [[false, 'mail.channel', self.generalChannelID], messageData];
|
||||
discuss.call('bus_service', 'trigger', 'notification', [notification]);
|
||||
|
||||
assert.strictEqual(discuss.$('.o_thread_typing_notification_bar').text(),
|
||||
"A is typing...",
|
||||
"It should display that A is still typing something");
|
||||
|
||||
discuss.destroy();
|
||||
done();
|
||||
self.simulateIsTyping({
|
||||
channelID: 1,
|
||||
isTyping: true,
|
||||
partnerID: 43,
|
||||
widget: discuss,
|
||||
});
|
||||
|
||||
// B comes after A in the display, because A is oldest typing partner.
|
||||
$general = discuss.$('.o_mail_discuss_sidebar')
|
||||
.find('.o_mail_discuss_item[data-thread-id=1]');
|
||||
assert.hasAttrValue($general.find('.o_mail_thread_typing_icon'), 'title',
|
||||
"A and B are typing...",
|
||||
"should show A and B are typing on hover on this thread typing icon");
|
||||
assert.strictEqual($composer.find('.o_composer_thread_typing').text().trim(),
|
||||
"A and B are typing...",
|
||||
"should show A and B are typing on the composer of active thread");
|
||||
|
||||
// Simulate receive message from B
|
||||
var messageData = {
|
||||
author_id: [43, 'Someone'],
|
||||
body: "<p>test</p>",
|
||||
channel_ids: [1],
|
||||
id: 101,
|
||||
model: 'mail.channel',
|
||||
res_id: 1,
|
||||
};
|
||||
var notification = [[false, 'mail.channel', 1], messageData];
|
||||
discuss.call('bus_service', 'trigger', 'notification', [notification]);
|
||||
|
||||
$general = discuss.$('.o_mail_discuss_sidebar')
|
||||
.find('.o_mail_discuss_item[data-thread-id=1]');
|
||||
assert.hasAttrValue($general.find('.o_mail_thread_typing_icon'), 'title',
|
||||
"A is typing...",
|
||||
"should show A is still typing on hover on this thread typing icon");
|
||||
assert.strictEqual($composer.find('.o_composer_thread_typing').text().trim(),
|
||||
"A is typing...",
|
||||
"should show A is still typing on the composer of active thread");
|
||||
|
||||
discuss.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('receive is typing notification from unselected thread', function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
this.data.initMessaging.channel_slots.channel_channel.push({
|
||||
id: 2,
|
||||
channel_type: "channel",
|
||||
name: "other",
|
||||
});
|
||||
|
||||
var self = this;
|
||||
createDiscuss({
|
||||
id: 1,
|
||||
context: {},
|
||||
params: {},
|
||||
data: this.data,
|
||||
services: this.services,
|
||||
mockRPC: function (route, args) {
|
||||
// when receiving an 'is typing' notification, fetch the list of
|
||||
// members of this channel if we haven't done yet.
|
||||
if (args.method === 'channel_fetch_listeners') {
|
||||
return $.when([
|
||||
{ id: self.myPartnerID, name: self.myName },
|
||||
{ id: 42, name: "Someone" },
|
||||
]);
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
session: { partner_id: this.myPartnerID },
|
||||
}).then(function (discuss) {
|
||||
var $general = discuss.$('.o_mail_discuss_sidebar')
|
||||
.find('.o_mail_discuss_item[data-thread-id=1]');
|
||||
var $other = discuss.$('.o_mail_discuss_sidebar')
|
||||
.find('.o_mail_discuss_item[data-thread-id=2]');
|
||||
assert.strictEqual($general.length, 1,
|
||||
"should have the channel item with id 1");
|
||||
assert.hasAttrValue($general, 'title', 'general',
|
||||
"should have the title 'general'");
|
||||
assert.strictEqual($other.length, 1,
|
||||
"should have the channel item with id 2");
|
||||
assert.hasAttrValue($other, 'title', 'other',
|
||||
"should have the title 'other'");
|
||||
|
||||
// click on general
|
||||
testUtils.dom.click($general);
|
||||
|
||||
assert.containsNone($general, '.o_mail_thread_typing_icon',
|
||||
"should no longer show someone is typing in sidebar of discuss");
|
||||
|
||||
self.simulateIsTyping({
|
||||
channelID: 2,
|
||||
isTyping: true,
|
||||
partnerID: 42,
|
||||
widget: discuss,
|
||||
});
|
||||
|
||||
$general = discuss.$('.o_mail_discuss_sidebar')
|
||||
.find('.o_mail_discuss_item[data-thread-id=1]');
|
||||
$other = discuss.$('.o_mail_discuss_sidebar')
|
||||
.find('.o_mail_discuss_item[data-thread-id=2]');
|
||||
assert.containsNone($general, '.o_mail_thread_typing_icon',
|
||||
"should not have a thread typing icon next to general icon in the sidebar");
|
||||
assert.containsOnce($other, '.o_mail_thread_typing_icon',
|
||||
"should have a thread typing icon next to other icon in the sidebar");
|
||||
|
||||
discuss.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -198,6 +198,9 @@ MockServer.include({
|
||||
if (args.method === 'channel_seen') {
|
||||
return $.when();
|
||||
}
|
||||
if (args.method === 'channel_fetched') {
|
||||
return $.when();
|
||||
}
|
||||
if (args.method === 'message_fetch') {
|
||||
return $.when(this._mockMessageFetch(args));
|
||||
}
|
||||
|
||||
@@ -592,9 +592,10 @@ QUnit.test("messaging menu widget: channel seen notification", function ( assert
|
||||
this.data.initMessaging.channel_slots.channel_channel[0].message_unread_counter = 1;
|
||||
|
||||
var messagingMenu = new MessagingMenu();
|
||||
testUtils.addMockEnvironment(messagingMenu, {
|
||||
testUtils.mock.addMockEnvironment(messagingMenu, {
|
||||
services: this.services,
|
||||
data: this.data,
|
||||
session: { partner_id: 3 },
|
||||
});
|
||||
|
||||
messagingMenu.appendTo($('#qunit-fixture'));
|
||||
@@ -609,11 +610,11 @@ QUnit.test("messaging menu widget: channel seen notification", function ( assert
|
||||
// Simulate received channel seen notification
|
||||
var message = {
|
||||
info: 'channel_seen',
|
||||
id: 1,
|
||||
last_message_id: 1,
|
||||
partner_id: 3,
|
||||
};
|
||||
var notifications = [
|
||||
[['myDB', 'res.partner'], message]
|
||||
[['myDB', 'mail.channel', 1], message]
|
||||
];
|
||||
messagingMenu.call('bus_service', 'trigger', 'notification', notifications);
|
||||
|
||||
@@ -633,7 +634,7 @@ QUnit.test("messaging menu widget: preview with no message should be undated", f
|
||||
this.data['mail.message'].records[0].channel_ids = [];
|
||||
|
||||
var messagingMenu = new MessagingMenu();
|
||||
testUtils.addMockEnvironment(messagingMenu, {
|
||||
testUtils.mock.addMockEnvironment(messagingMenu, {
|
||||
services: this.services,
|
||||
data: this.data,
|
||||
});
|
||||
@@ -664,7 +665,7 @@ QUnit.test("messaging menu widget: sort dated previews before undated previews",
|
||||
this.data.initMessaging.channel_slots.direct_message = [dm];
|
||||
|
||||
var messagingMenu = new MessagingMenu();
|
||||
testUtils.addMockEnvironment(messagingMenu, {
|
||||
testUtils.mock.addMockEnvironment(messagingMenu, {
|
||||
services: this.services,
|
||||
data: this.data,
|
||||
});
|
||||
@@ -752,7 +753,7 @@ QUnit.test('global counter with channel previews', function (assert) {
|
||||
this.data['mail.message'].records = messages;
|
||||
|
||||
var messagingMenu = new MessagingMenu();
|
||||
testUtils.addMockEnvironment(messagingMenu, {
|
||||
testUtils.mock.addMockEnvironment(messagingMenu, {
|
||||
services: this.services,
|
||||
data: this.data,
|
||||
session: {
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
odoo.define('mail.typingThreadWindowTests', function (require) {
|
||||
"use strict";
|
||||
|
||||
var mailTestUtils = require('mail.testUtils');
|
||||
|
||||
var testUtils = require('web.test_utils');
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
QUnit.module('mail', {}, function () {
|
||||
QUnit.module('Thread Window', {}, function () {
|
||||
QUnit.module('Typing', {
|
||||
beforeEach: function () {
|
||||
var self = this;
|
||||
|
||||
// define channel to link to chat window
|
||||
this.data = {
|
||||
'mail.message': {
|
||||
fields: {},
|
||||
records: [],
|
||||
},
|
||||
initMessaging: {
|
||||
channel_slots: {
|
||||
channel_channel: [{
|
||||
id: 1,
|
||||
channel_type: "channel",
|
||||
name: "general",
|
||||
}],
|
||||
},
|
||||
},
|
||||
};
|
||||
this.services = mailTestUtils.getMailServices();
|
||||
this.ORIGINAL_THREAD_WINDOW_APPENDTO = this.services.mail_service.prototype.THREAD_WINDOW_APPENDTO;
|
||||
|
||||
/**
|
||||
* Simulate that someone typing something (or stops typing)
|
||||
*
|
||||
* @param {Object} params
|
||||
* @param {integer} params.channelID
|
||||
* @param {boolean} params.isTyping
|
||||
* @param {integer} params.partnerID
|
||||
* @param {Widget} params.widget a widget that can call the bus_service
|
||||
*/
|
||||
this.simulateIsTyping = function (params) {
|
||||
var typingData = {
|
||||
info: 'typing_status',
|
||||
partner_id: params.partnerID,
|
||||
is_typing: params.isTyping,
|
||||
};
|
||||
var notification = [[false, 'mail.channel', params.channelID], typingData];
|
||||
params.widget.call('bus_service', 'trigger', 'notification', [notification]);
|
||||
};
|
||||
|
||||
this.createParent = function (params) {
|
||||
var widget = new Widget();
|
||||
|
||||
// in non-debug mode, append thread windows in qunit-fixture
|
||||
// note that it does not hide thread window because it uses fixed
|
||||
// position, and qunit-fixture uses absolute...
|
||||
if (params.debug) {
|
||||
self.services.mail_service.prototype.THREAD_WINDOW_APPENDTO = 'body';
|
||||
} else {
|
||||
self.services.mail_service.prototype.THREAD_WINDOW_APPENDTO = '#qunit-fixture';
|
||||
}
|
||||
|
||||
testUtils.mock.addMockEnvironment(widget, params);
|
||||
return widget;
|
||||
};
|
||||
},
|
||||
afterEach: function () {
|
||||
// reset thread window append to body
|
||||
this.services.mail_service.prototype.THREAD_WINDOW_APPENDTO = 'body';
|
||||
},
|
||||
});
|
||||
|
||||
QUnit.test('receive typing notification', function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
var parent = this.createParent({
|
||||
data: this.data,
|
||||
services: this.services,
|
||||
mockRPC: function (route, args) {
|
||||
// when receiving an 'is typing' notification, fetch the list of
|
||||
// members of this channel if we haven't done yet.
|
||||
if (args.method === 'channel_fetch_listeners') {
|
||||
return $.when([
|
||||
{ id: 42, name: "Someone" },
|
||||
]);
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
// detach channel 1, so that it opens corresponding thread window.
|
||||
parent.call('mail_service', 'getChannel', 1).detach();
|
||||
|
||||
var $threadWindow = $('.o_thread_window');
|
||||
assert.containsNone($threadWindow, '.o_mail_thread_typing_icon',
|
||||
"should not display an icon that someone is typing something on this thread");
|
||||
|
||||
this.simulateIsTyping({
|
||||
channelID: 1,
|
||||
isTyping: true,
|
||||
partnerID: 42,
|
||||
widget: parent,
|
||||
});
|
||||
|
||||
assert.containsOnce($threadWindow, '.o_mail_thread_typing_icon',
|
||||
"should display an icon that someone is typing something on this thread");
|
||||
assert.hasAttrValue($threadWindow.find('.o_mail_thread_typing_icon'), 'title',
|
||||
"Someone is typing...",
|
||||
"should display someone is typing something on hover of this icon");
|
||||
|
||||
this.simulateIsTyping({
|
||||
channelID: 1,
|
||||
isTyping: false,
|
||||
partnerID: 42,
|
||||
widget: parent,
|
||||
});
|
||||
|
||||
assert.containsNone($threadWindow, '.o_mail_thread_typing_icon',
|
||||
"should no longer display an icon that someone is typing something on this thread");
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -35,11 +35,11 @@
|
||||
<script type="text/javascript" src="/mail/static/src/js/models/threads/mailbox.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/models/threads/searchable_thread.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/models/threads/thread.js"></script>
|
||||
<!-- thread typing mixin -->
|
||||
<script type="text/javascript" src="/mail/static/src/js/models/threads/thread_typing_mixin/thread_typing_mixin.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/models/threads/thread_typing_mixin/timer.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/models/threads/thread_typing_mixin/timers.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/models/threads/thread_typing_mixin/cc_throttle_function.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/models/threads/mixins/channel_seen_mixin.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/models/threads/mixins/thread_typing_mixin.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/models/utils/timer.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/models/utils/timers.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/models/utils/cc_throttle_function.js"></script>
|
||||
<!-- Widgets -->
|
||||
<!-- thread windows -->
|
||||
<script type="text/javascript" src="/mail/static/src/js/thread_windows/abstract_thread_window.js"></script>
|
||||
@@ -110,6 +110,7 @@
|
||||
<script type="text/javascript" src="/mail/static/tests/thread_window/blank_thread_window_tests.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/tests/thread_window/hidden_thread_window_tests.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/tests/thread_window/moderation_thread_window_tests.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/tests/thread_window/typing_thread_window_tests.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/tests/document_thread_window_tests.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/tests/chatter_tests.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/tests/mail_utils_tests.js"></script>
|
||||
@@ -120,6 +121,7 @@
|
||||
<script type="text/javascript" src="/mail/static/tests/systray/systray_messaging_menu_tests.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/tests/systray/systray_messaging_menu_mail_failure_tests.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/tests/discuss_moderation_tests.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/tests/discuss_seen_indicator_tests.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/tests/discuss_typing_notification_tests.js"></script>
|
||||
<!-- tools -->
|
||||
<script type="text/javascript" src="/mail/static/tests/tools/debug_manager_tests.js"></script>
|
||||
|
||||
@@ -23,6 +23,11 @@ var MailBotService = AbstractService.extend({
|
||||
}, 2*60*1000);
|
||||
}
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Get the previews related to the OdooBot (conversation not included).
|
||||
* For instance, when there is no conversation with OdooBot and OdooBot has
|
||||
@@ -33,25 +38,28 @@ var MailBotService = AbstractService.extend({
|
||||
* 'mail.Preview' template.
|
||||
*/
|
||||
getPreviews: function (filter) {
|
||||
var previews = [];
|
||||
if (this.hasRequest() && (filter === 'mailbox_inbox' || !filter)) {
|
||||
previews.push({
|
||||
title: _t("OdooBot has a request"),
|
||||
imageSRC: "/mail/static/src/img/odoobot.png",
|
||||
status: 'bot',
|
||||
body: _t("Enable desktop notifications to chat"),
|
||||
id: 'request_notification',
|
||||
unreadCounter: 1,
|
||||
});
|
||||
if (!this.isRequestingForNativeNotifications()) {
|
||||
return [];
|
||||
}
|
||||
if (filter && filter !== 'mailbox_inbox') {
|
||||
return [];
|
||||
}
|
||||
var previews = [{
|
||||
title: _t("OdooBot has a request"),
|
||||
imageSRC: "/mail/static/src/img/odoobot.png",
|
||||
status: 'bot',
|
||||
body: _t("Enable desktop notifications to chat"),
|
||||
id: 'request_notification',
|
||||
unreadCounter: 1,
|
||||
}];
|
||||
return previews;
|
||||
},
|
||||
/**
|
||||
* Tell whether OdooBot has a request or not.
|
||||
* Tell whether OdooBot is requesting to enable push notifications.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasRequest: function () {
|
||||
isRequestingForNativeNotifications: function () {
|
||||
return window.Notification && window.Notification.permission === "default";
|
||||
},
|
||||
});
|
||||
|
||||
@@ -22,7 +22,7 @@ return MessagingMenu.include({
|
||||
*/
|
||||
_computeCounter: function () {
|
||||
var counter = this._super.apply(this, arguments);
|
||||
if (this.call('mailbot_service', 'hasRequest')) {
|
||||
if (this.call('mailbot_service', 'isRequestingForNativeNotifications')) {
|
||||
counter++;
|
||||
}
|
||||
return counter;
|
||||
@@ -59,9 +59,6 @@ return MessagingMenu.include({
|
||||
if (value !== 'granted') {
|
||||
this.call('bus_service', 'sendNotification', _t('Permission denied'),
|
||||
_t('Odoo will not have the permission to send native notifications on this device.'));
|
||||
} else {
|
||||
this.call('bus_service', 'sendNotification', _t('Yay, push notifications are enabled!'),
|
||||
_t('Odoo has now the permission to send you native notifications on this device.'));
|
||||
}
|
||||
this._updateCounter();
|
||||
},
|
||||
|
||||
@@ -27,7 +27,7 @@ QUnit.module('MessagingMenu', {
|
||||
// Patch mailbot_service so that it does do not do any RPC, and
|
||||
// the detection of push notifications permissions can be simulated in
|
||||
// the test cases. By default, shows 'OdooBot has a request'
|
||||
this.hasMailbotRequest = true;
|
||||
this.isMailbotRequesting = true;
|
||||
testUtils.mock.patch(MailBotService, {
|
||||
/**
|
||||
* @override
|
||||
@@ -37,8 +37,8 @@ QUnit.module('MessagingMenu', {
|
||||
* @override
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasRequest: function () {
|
||||
return self.hasMailbotRequest;
|
||||
isRequestingForNativeNotifications: function () {
|
||||
return self.isMailbotRequesting;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -82,7 +82,7 @@ QUnit.test('messaging menu widget: rendering with OdooBot has a request', functi
|
||||
QUnit.test('messaging menu widget: rendering without OdooBot has a request', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
this.hasMailbotRequest = false;
|
||||
this.isMailbotRequesting = false;
|
||||
|
||||
var messagingMenu = new MessagingMenu();
|
||||
testUtils.mock.addMockEnvironment(messagingMenu, {
|
||||
|
||||
@@ -24,14 +24,14 @@
|
||||
<!-- threads -->
|
||||
<script type="text/javascript" src="/mail/static/src/js/models/threads/abstract_thread.js"></script>
|
||||
<script type="text/javascript" src="/im_livechat/static/src/js/models/website_livechat.js"></script>
|
||||
<!-- thread typing mixin -->
|
||||
<script type="text/javascript" src="/mail/static/src/js/models/threads/thread_typing_mixin/thread_typing_mixin.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/models/threads/thread_typing_mixin/timer.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/models/threads/thread_typing_mixin/timers.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/models/threads/thread_typing_mixin/cc_throttle_function.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/models/threads/mixins/thread_typing_mixin.js"></script>
|
||||
<!-- messages -->
|
||||
<script type="text/javascript" src="/mail/static/src/js/models/messages/abstract_message.js"></script>
|
||||
<script type="text/javascript" src="/im_livechat/static/src/js/models/website_livechat_message.js"></script>
|
||||
<!-- utils -->
|
||||
<script type="text/javascript" src="/mail/static/src/js/models/utils/timer.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/models/utils/timers.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/models/utils/cc_throttle_function.js"></script>
|
||||
<link rel="stylesheet" type="text/scss" href="/mail/static/src/scss/abstract_thread_window.scss"></link>
|
||||
<link rel="stylesheet" type="text/scss" href="/mail/static/src/scss/thread.scss"></link>
|
||||
<link rel="stylesheet" type="text/scss" href="/im_livechat/static/src/scss/im_livechat.scss"/>
|
||||
|
||||
Reference in New Issue
Block a user