diff --git a/addons/im_livechat/static/src/js/im_livechat.js b/addons/im_livechat/static/src/js/im_livechat.js
index b8941eb4239..1445ba79eae 100644
--- a/addons/im_livechat/static/src/js/im_livechat.js
+++ b/addons/im_livechat/static/src/js/im_livechat.js
@@ -360,7 +360,7 @@ var LivechatButton = Widget.extend({
*/
_onUpdatedTypingPartners: function (ev) {
ev.stopPropagation();
- this._chatWindow.renderTypingNotificationBar();
+ this._chatWindow.renderHeader();
},
/**
* @private
diff --git a/addons/mail/static/src/js/composers/basic_composer.js b/addons/mail/static/src/js/composers/basic_composer.js
index c8f7d702af4..1a40e3f48fa 100644
--- a/addons/mail/static/src/js/composers/basic_composer.js
+++ b/addons/mail/static/src/js/composers/basic_composer.js
@@ -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'));
@@ -793,6 +796,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;
diff --git a/addons/mail/static/src/js/discuss.js b/addons/mail/static/src/js/discuss.js
index 2cb9fe749d7..dd03f08028c 100644
--- a/addons/mail/static/src/js/discuss.js
+++ b/addons/mail/static/src/js/discuss.js
@@ -235,12 +235,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);
@@ -1391,19 +1393,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
diff --git a/addons/mail/static/src/js/models/threads/thread_typing_mixin/thread_typing_mixin.js b/addons/mail/static/src/js/models/threads/thread_typing_mixin/thread_typing_mixin.js
index fb1a3a91cd1..9a5b5abb4cc 100644
--- a/addons/mail/static/src/js/models/threads/thread_typing_mixin/thread_typing_mixin.js
+++ b/addons/mail/static/src/js/models/threads/thread_typing_mixin/thread_typing_mixin.js
@@ -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
diff --git a/addons/mail/static/src/js/services/mail_window_manager.js b/addons/mail/static/src/js/services/mail_window_manager.js
index 79ea668e55e..64cf6bb88c9 100644
--- a/addons/mail/static/src/js/services/mail_window_manager.js
+++ b/addons/mail/static/src/js/services/mail_window_manager.js
@@ -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.
*
diff --git a/addons/mail/static/src/js/thread_widget.js b/addons/mail/static/src/js/thread_widget.js
index e6da87c6dba..da74028903e 100644
--- a/addons/mail/static/src/js/thread_widget.js
+++ b/addons/mail/static/src/js/thread_widget.js
@@ -185,12 +185,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());
@@ -253,28 +247,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
*/
diff --git a/addons/mail/static/src/js/thread_windows/abstract_thread_window.js b/addons/mail/static/src/js/thread_windows/abstract_thread_window.js
index f4bf9533ff9..3e4e2e16325 100644
--- a/addons/mail/static/src/js/thread_windows/abstract_thread_window.js
+++ b/addons/mail/static/src/js/thread_windows/abstract_thread_window.js
@@ -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,
diff --git a/addons/mail/static/src/js/thread_windows/thread_window.js b/addons/mail/static/src/js/thread_windows/thread_window.js
index 62468e9ea16..f4b0137c476 100644
--- a/addons/mail/static/src/js/thread_windows/thread_window.js
+++ b/addons/mail/static/src/js/thread_windows/thread_window.js
@@ -55,6 +55,9 @@ 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);
+
var composerDef;
if (!this.hasThread()) {
this._startWithoutThread();
@@ -353,7 +356,19 @@ 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();
+ },
});
return ThreadWindow;
diff --git a/addons/mail/static/src/scss/abstract_thread_window.scss b/addons/mail/static/src/scss/abstract_thread_window.scss
index a4ad6d366cc..b148afa111f 100644
--- a/addons/mail/static/src/scss/abstract_thread_window.scss
+++ b/addons/mail/static/src/scss/abstract_thread_window.scss
@@ -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 {
diff --git a/addons/mail/static/src/scss/composer.scss b/addons/mail/static/src/scss/composer.scss
index e076629f667..f12e195abe9 100644
--- a/addons/mail/static/src/scss/composer.scss
+++ b/addons/mail/static/src/scss/composer.scss
@@ -184,6 +184,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');
diff --git a/addons/mail/static/src/scss/discuss.scss b/addons/mail/static/src/scss/discuss.scss
index 8d328b028bf..39c30aefa05 100644
--- a/addons/mail/static/src/scss/discuss.scss
+++ b/addons/mail/static/src/scss/discuss.scss
@@ -44,6 +44,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;
}
@@ -129,7 +132,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 +146,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;
diff --git a/addons/mail/static/src/scss/thread.scss b/addons/mail/static/src/scss/thread.scss
index 81f6862ca9f..a56f54bad78 100644
--- a/addons/mail/static/src/scss/thread.scss
+++ b/addons/mail/static/src/scss/thread.scss
@@ -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;
@@ -239,3 +219,40 @@
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);
+ }
+}
diff --git a/addons/mail/static/src/xml/abstract_thread_window.xml b/addons/mail/static/src/xml/abstract_thread_window.xml
index de0418af3df..89c45323c7a 100644
--- a/addons/mail/static/src/xml/abstract_thread_window.xml
+++ b/addons/mail/static/src/xml/abstract_thread_window.xml
@@ -11,6 +11,7 @@
first message
", - 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: "test
", - 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: "test
", + 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: "test
", - 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: "test
", + 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(); }); }); diff --git a/addons/mail/static/tests/thread_window/typing_thread_window_tests.js b/addons/mail/static/tests/thread_window/typing_thread_window_tests.js new file mode 100644 index 00000000000..549cfdb7184 --- /dev/null +++ b/addons/mail/static/tests/thread_window/typing_thread_window_tests.js @@ -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(); +}); + +}); +}); +}); diff --git a/addons/mail/views/mail_templates.xml b/addons/mail/views/mail_templates.xml index 6ca4da7bbf8..efe7657e192 100644 --- a/addons/mail/views/mail_templates.xml +++ b/addons/mail/views/mail_templates.xml @@ -110,6 +110,7 @@ +