Indicate to the user when there are unread messages

further down in the chat box.
This commit is contained in:
JC Brand 2016-05-28 08:55:03 +00:00
parent 82ee7f694e
commit c738d085c4
13 changed files with 237 additions and 100 deletions

View File

@ -1369,6 +1369,16 @@
color: #FB5D50; }
#conversejs .chatbox .chat-body .delayed .chat-msg-me {
color: #7EABBB; }
#conversejs .chatbox .new-msgs-indicator {
position: absolute;
width: 100%;
cursor: pointer;
background-color: #F4A261;
color: #FCFDFD;
padding: 0.3em;
font-size: 0.9em;
text-align: center;
z-index: 20; }
#conversejs .chatbox .chat-content {
position: relative;
padding: 0.5em;
@ -1455,7 +1465,7 @@
box-shadow: -1px -1px 2px 0 rgba(0, 0, 0, 0.4);
display: none;
font-size: 12px;
margin: 0 0 1px 0;
margin: 0;
position: absolute;
right: 0; }
#conversejs .chatbox form.sendXMPPMessage .chat-toolbar ul li {
@ -1985,10 +1995,15 @@
max-width: 70%;
float: left;
min-width: 200px; }
#conversejs .chatroom .box-flyout .chatroom-body .chat-area .new-msgs-indicator {
background-color: #E76F51;
max-width: 70%; }
#conversejs .chatroom .box-flyout .chatroom-body .chat-area .chat-content {
padding: 0 0.5em 0 0.5em; }
#conversejs .chatroom .box-flyout .chatroom-body .chat-area.full {
max-width: 100%; }
#conversejs .chatroom .box-flyout .chatroom-body .chat-area.full .new-msgs-indicator {
max-width: 100%; }
#conversejs .chatroom .box-flyout .chatroom-body .mentioned {
font-weight: bold; }
#conversejs .chatroom .box-flyout .chatroom-body .chat-msg-room {

View File

@ -187,6 +187,17 @@
}
}
}
.new-msgs-indicator {
position: absolute;
width: 100%;
cursor: pointer;
background-color: $chat-head-color;
color: $light-background-color;
padding: 0.3em;
font-size: 0.9em;
text-align: center;
z-index: 20;
}
.chat-content {
position: relative;
padding: 0.5em;
@ -283,7 +294,7 @@
box-shadow: -1px -1px 2px 0 rgba(0, 0, 0, 0.4);
display: none;
font-size: 12px;
margin: 0 0 1px 0;
margin: 0;
position: absolute;
right: 0;
li {

View File

@ -50,12 +50,19 @@
max-width: 70%;
float: left;
min-width: $chat-width;
.new-msgs-indicator {
background-color: $chatroom-head-color;
max-width: 70%;
}
.chat-content {
// There's an annoying Chrome box-sizing bug which prevents us from adding 0.5em padding here.
padding: 0 0.5em 0 0.5em;
}
&.full {
max-width: 100%;
.new-msgs-indicator {
max-width: 100%;
}
}
}
.mentioned {

View File

@ -413,10 +413,146 @@
runs(function () {});
});
describe("when received from someone else", function () {
it("can be received which will open a chatbox and be displayed inside it", function () {
spyOn(converse, 'emit');
var message = 'This is a received message';
var sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
var msg = $msg({
from: sender_jid,
to: this.connection.jid,
type: 'chat',
id: (new Date()).getTime()
}).c('body').t(message).up()
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
// We don't already have an open chatbox for this user
expect(this.chatboxes.get(sender_jid)).not.toBeDefined();
runs(function () {
// onMessage is a handler for received XMPP messages
this.chatboxes.onMessage(msg);
expect(converse.emit).toHaveBeenCalledWith('message', msg);
}.bind(converse));
waits(50);
runs(function () {
// Check that the chatbox and its view now exist
var chatbox = this.chatboxes.get(sender_jid);
var chatboxview = this.chatboxviews.get(sender_jid);
expect(chatbox).toBeDefined();
expect(chatboxview).toBeDefined();
// Check that the message was received and check the message parameters
expect(chatbox.messages.length).toEqual(1);
var msg_obj = chatbox.messages.models[0];
expect(msg_obj.get('message')).toEqual(message);
expect(msg_obj.get('fullname')).toEqual(mock.cur_names[0]);
expect(msg_obj.get('sender')).toEqual('them');
expect(msg_obj.get('delayed')).toEqual(false);
// Now check that the message appears inside the chatbox in the DOM
var $chat_content = chatboxview.$el.find('.chat-content');
var msg_txt = $chat_content.find('.chat-message').find('.chat-msg-content').text();
expect(msg_txt).toEqual(message);
var sender_txt = $chat_content.find('span.chat-msg-them').text();
expect(sender_txt.match(/^[0-9][0-9]:[0-9][0-9] /)).toBeTruthy();
}.bind(converse));
}.bind(converse));
it("will cause the chat area to be scrolled down only if it was at the bottom already", function () {
// TODO
var message = 'This message is received while the chat area is scrolled up';
var sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
test_utils.openChatBoxFor(sender_jid);
var chatboxview = converse.chatboxviews.get(sender_jid);
spyOn(chatboxview, 'scrollDown').andCallThrough();
runs(function () {
/* Create enough messages so that there's a
* scrollbar.
*/
for (var i=0; i<20; i++) {
converse.chatboxes.onMessage($msg({
from: sender_jid,
to: converse.connection.jid,
type: 'chat',
id: (new Date()).getTime()
}).c('body').t('Message: '+i).up()
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
}
});
waits(50);
runs(function () {
chatboxview.$content.scrollTop(0);
});
waits(250);
runs(function () {
converse.chatboxes.onMessage($msg({
from: sender_jid,
to: converse.connection.jid,
type: 'chat',
id: (new Date()).getTime()
}).c('body').t(message).up()
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
});
waits(150);
runs(function () {
// Now check that the message appears inside the chatbox in the DOM
var $chat_content = chatboxview.$el.find('.chat-content');
var msg_txt = $chat_content.find('.chat-message:last').find('.chat-msg-content').text();
expect(msg_txt).toEqual(message);
expect(chatboxview.model.get('scrolled')).toBeTruthy();
expect(chatboxview.$content.scrollTop()).toBe(0);
expect(chatboxview.$('.new-msgs-indicator').is(':visible')).toBeTruthy();
// Scroll down again
chatboxview.$content.scrollTop(chatboxview.$content[0].scrollHeight);
});
waits(250);
runs(function () {
expect(chatboxview.$('.new-msgs-indicator').is(':visible')).toBeFalsy();
});
});
it("is ignored if it's intended for a different resource and filter_by_resource is set to true", function () {
// Send a message from a different resource
var message, sender_jid, msg;
spyOn(converse, 'log');
spyOn(converse.chatboxes, 'getChatBox').andCallThrough();
runs(function () {
converse.filter_by_resource = true;
sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
msg = $msg({
from: sender_jid,
to: converse.bare_jid+'/'+"some-other-resource",
type: 'chat',
id: (new Date()).getTime()
}).c('body').t("This message will not be shown").up()
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
converse.chatboxes.onMessage(msg);
});
waits(50);
runs(function () {
expect(converse.log).toHaveBeenCalledWith(
"onMessage: Ignoring incoming message intended for a different resource: dummy@localhost/some-other-resource", "info");
expect(converse.chatboxes.getChatBox).not.toHaveBeenCalled();
converse.filter_by_resource = false;
});
waits(50);
runs(function () {
message = "This message sent to a different resource will be shown";
msg = $msg({
from: sender_jid,
to: converse.bare_jid+'/'+"some-other-resource",
type: 'chat',
id: '134234623462346'
}).c('body').t(message).up()
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
converse.chatboxes.onMessage(msg);
});
waits(50);
runs(function () {
expect(converse.chatboxes.getChatBox).toHaveBeenCalled();
var chatboxview = converse.chatboxviews.get(sender_jid);
var $chat_content = chatboxview.$el.find('.chat-content:last');
var msg_txt = $chat_content.find('.chat-message').find('.chat-msg-content').text();
expect(msg_txt).toEqual(message);
});
});
});
@ -426,95 +562,6 @@
});
});
it("can be received which will open a chatbox and be displayed inside it", function () {
spyOn(converse, 'emit');
var message = 'This is a received message';
var sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
var msg = $msg({
from: sender_jid,
to: this.connection.jid,
type: 'chat',
id: (new Date()).getTime()
}).c('body').t(message).up()
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
// We don't already have an open chatbox for this user
expect(this.chatboxes.get(sender_jid)).not.toBeDefined();
runs(function () {
// onMessage is a handler for received XMPP messages
this.chatboxes.onMessage(msg);
expect(converse.emit).toHaveBeenCalledWith('message', msg);
}.bind(converse));
waits(50);
runs(function () {
// Check that the chatbox and its view now exist
var chatbox = this.chatboxes.get(sender_jid);
var chatboxview = this.chatboxviews.get(sender_jid);
expect(chatbox).toBeDefined();
expect(chatboxview).toBeDefined();
// Check that the message was received and check the message parameters
expect(chatbox.messages.length).toEqual(1);
var msg_obj = chatbox.messages.models[0];
expect(msg_obj.get('message')).toEqual(message);
expect(msg_obj.get('fullname')).toEqual(mock.cur_names[0]);
expect(msg_obj.get('sender')).toEqual('them');
expect(msg_obj.get('delayed')).toEqual(false);
// Now check that the message appears inside the chatbox in the DOM
var $chat_content = chatboxview.$el.find('.chat-content');
var msg_txt = $chat_content.find('.chat-message').find('.chat-msg-content').text();
expect(msg_txt).toEqual(message);
var sender_txt = $chat_content.find('span.chat-msg-them').text();
expect(sender_txt.match(/^[0-9][0-9]:[0-9][0-9] /)).toBeTruthy();
}.bind(converse));
}.bind(converse));
it("is ignored if it's intended for a different resource and filter_by_resource is set to true", function () {
// Send a message from a different resource
var message, sender_jid, msg;
spyOn(converse, 'log');
spyOn(converse.chatboxes, 'getChatBox').andCallThrough();
runs(function () {
converse.filter_by_resource = true;
sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
msg = $msg({
from: sender_jid,
to: converse.bare_jid+'/'+"some-other-resource",
type: 'chat',
id: (new Date()).getTime()
}).c('body').t("This message will not be shown").up()
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
converse.chatboxes.onMessage(msg);
});
waits(50);
runs(function () {
expect(converse.log).toHaveBeenCalledWith(
"onMessage: Ignoring incoming message intended for a different resource: dummy@localhost/some-other-resource", "info");
expect(converse.chatboxes.getChatBox).not.toHaveBeenCalled();
converse.filter_by_resource = false;
});
waits(50);
runs(function () {
message = "This message sent to a different resource will be shown";
msg = $msg({
from: sender_jid,
to: converse.bare_jid+'/'+"some-other-resource",
type: 'chat',
id: '134234623462346'
}).c('body').t(message).up()
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
converse.chatboxes.onMessage(msg);
});
waits(50);
runs(function () {
expect(converse.chatboxes.getChatBox).toHaveBeenCalled();
var chatboxview = converse.chatboxviews.get(sender_jid);
var $chat_content = chatboxview.$el.find('.chat-content:last');
var msg_txt = $chat_content.find('.chat-message').find('.chat-msg-content').text();
expect(msg_txt).toEqual(message);
});
});
it("is ignored if it's a malformed headline message", function () {
/* Ideally we wouldn't have to filter out headline
* messages, but Prosody gives them the wrong 'type' :(

View File

@ -216,6 +216,50 @@
expect(converse.emit.callCount, 1);
});
it("will cause the chat area to be scrolled down only if it was at the bottom already", function () {
var message = 'This message is received while the chat area is scrolled up';
test_utils.openChatRoom('lounge', 'localhost', 'dummy');
var view = converse.chatboxviews.get('lounge@localhost');
spyOn(view, 'scrollDown').andCallThrough();
runs(function () {
/* Create enough messages so that there's a
* scrollbar.
*/
for (var i=0; i<20; i++) {
converse.chatboxes.onMessage(
$msg({
from: 'lounge@localhost/someone',
to: 'dummy@localhost.com',
type: 'groupchat',
id: (new Date()).getTime(),
}).c('body').t('Message: '+i).tree());
}
});
waits(50);
runs(function () {
view.$content.scrollTop(0);
});
waits(250);
runs(function () {
expect(view.model.get('scrolled')).toBeTruthy();
converse.chatboxes.onMessage(
$msg({
from: 'lounge@localhost/someone',
to: 'dummy@localhost.com',
type: 'groupchat',
id: (new Date()).getTime(),
}).c('body').t(message).tree());
});
waits(150);
runs(function () {
// Now check that the message appears inside the chatbox in the DOM
var $chat_content = view.$el.find('.chat-content');
var msg_txt = $chat_content.find('.chat-message:last').find('.chat-msg-content').text();
expect(msg_txt).toEqual(message);
expect(view.$content.scrollTop()).toBe(0);
});
});
it("shows received chatroom subject messages", function () {
var text = 'Jabber/XMPP Development | RFCs and Extensions: http://xmpp.org/ | Protocol and XSF discussions: xsf@muc.xmpp.org';
var stanza = Strophe.xmlHtmlNode(
@ -461,8 +505,8 @@
}.bind(converse));
}.bind(converse));
}.bind(converse));
describe("Each chat room can take special commands", function () {
beforeEach(function () {
runs(function () {

View File

@ -99,6 +99,7 @@
show_toolbar: converse.show_toolbar,
show_textarea: true,
title: this.model.get('fullname'),
unread_msgs: __('You have unread messages'),
info_close: __('Close this chat box'),
label_personal_message: __('Personal message')
}
@ -333,6 +334,9 @@
if (converse.windowState === 'blur' || this.model.get('scrolled', true)) {
converse.incrementMsgCounter();
}
if (this.model.get('scrolled', true)) {
this.$el.find('.new-msgs-indicator').removeClass('hidden');
}
} else {
// We remove the "scrolled" flag so that the chat area
// gets scrolled down. We always want to scroll down
@ -688,15 +692,16 @@
// and the user is scrolled away...
// Should probably take a look at incrementMsgCounter
if (ev && ev.preventDefault) { ev.preventDefault(); }
var is_at_bottom = this.$content.scrollTop() + this.$content.innerHeight() >= this.$content[0].scrollHeight;
var is_at_bottom = this.$content.scrollTop() + this.$content.innerHeight() >= this.$content[0].scrollHeight-10;
if (is_at_bottom) {
this.model.set('scrolled', false);
this.$el.find('.new-msgs-indicator').addClass('hidden');
} else {
// We're not at the bottom of the chat area, so we mark
// that the box is in a scrolled-up state.
this.model.set('scrolled', true);
}
}, 50),
}, 150),
scrollDownMessageHeight: function ($message) {
if (this.$content.is(':visible') && !this.model.get('scrolled')) {
@ -708,6 +713,7 @@
scrollDown: function () {
if (this.$content.is(':visible') && !this.model.get('scrolled')) {
this.$content.scrollTop(this.$content[0].scrollHeight);
this.$el.find('.new-msgs-indicator').addClass('hidden');
}
return this;
}

View File

@ -1577,6 +1577,9 @@
});
this.setUpXMLLogging = function () {
Strophe.log = function (level, msg) {
converse.log(msg, level);
};
if (this.debug) {
this.connection.xmlInput = function (body) { converse.log(body.outerHTML); };
this.connection.xmlOutput = function (body) { converse.log(body.outerHTML); };

View File

@ -27,7 +27,7 @@
// relevant objects or classes.
//
// New functions which don't exist yet can also be added.
registerGlobalEventHandlers: function () {
$(document).on('mousemove', function (ev) {
if (!this.resizing || !this.allow_dragresize) { return true; }

View File

@ -44,7 +44,7 @@
// relevant objects or classes.
//
// New functions which don't exist yet can also be added.
ChatBoxViews: {
onChatBoxAdded: function (item) {
var view = this.get(item.get('id'));
@ -93,6 +93,7 @@
show_toolbar: converse.show_toolbar,
show_textarea: false,
title: this.model.get('fullname'),
unread_msgs: __('You have unread messages'),
info_close: __('Close this box'),
info_minimize: __('Minimize this box'),
label_personal_message: ''

View File

@ -216,6 +216,7 @@
this.$('.chatroom-body').empty()
.append(
converse.templates.chatarea({
'unread_msgs': __('You have unread messages'),
'show_toolbar': converse.show_toolbar,
'label_message': __('Message')
}))

View File

@ -1,5 +1,6 @@
<div class="chat-area">
<div class="chat-content"></div>
<div class="new-msgs-indicator hidden">▼ {{ unread_msgs }} ▼</div>
<form class="sendXMPPMessage" action="" method="post">
{[ if (show_toolbar) { ]}
<ul class="chat-toolbar no-text-select"></ul>

View File

@ -17,6 +17,7 @@
</div>
<div class="chat-body">
<div class="chat-content"></div>
<div class="new-msgs-indicator hidden">▼ {{ unread_msgs }} ▼</div>
{[ if (show_textarea) { ]}
<form class="sendXMPPMessage" action="" method="post">
{[ if (show_toolbar) { ]}

View File

@ -55,7 +55,7 @@ require([
auto_login: true,
jid: 'dummy@localhost',
password: 'secret',
debug: false
debug: true
}, function (converse) {
window.converse = converse;
window.crypto = {