[IMP] im_livechat, mail: improved "is typing" notification

With this commit, the UI for "is typing..." notifications
has changed.

** before **:

In Discuss & chat windows, "is typing..." notification bar
at the bottom of the thread, inside the conversation.

** after **

In chat windows, animated '...' next to conversation name.
In Discuss, animated '...' next to conversation name in sidebar,
and text below the composer.

Task-ID 1881001
This commit is contained in:
Alexandre Kühn
2018-12-06 14:51:28 +00:00
parent 9117d968e0
commit 7ad45a04e1
20 changed files with 556 additions and 294 deletions
@@ -360,7 +360,7 @@ var LivechatButton = Widget.extend({
*/
_onUpdatedTypingPartners: function (ev) {
ev.stopPropagation();
this._chatWindow.renderTypingNotificationBar();
this._chatWindow.renderHeader();
},
/**
* @private
@@ -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;
+10 -18
View File
@@ -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
@@ -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
@@ -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.
*
@@ -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
*/
@@ -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,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;
@@ -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 {
@@ -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');
+5 -2
View File
@@ -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;
+37 -20
View File
@@ -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);
}
}
@@ -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"/>
+8
View File
@@ -24,6 +24,7 @@
</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>
@@ -40,6 +41,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">
+4
View File
@@ -140,6 +140,10 @@
<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"/>
</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()"/>
<t t-call="mail.discuss.SidebarCounter"/>
+12 -16
View File
@@ -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
@@ -550,4 +534,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>
@@ -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,
@@ -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();
});
});
@@ -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();
});
});
});
});
+1
View File
@@ -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>