Commit c41bdac6 authored by JC Brand's avatar JC Brand

Some work on componentizing the minimized chats UI

parent d5c93eb0
......@@ -3199,7 +3199,8 @@
"dependencies": {
"filesize": {
"version": "6.1.0",
"resolved": false
"resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz",
"integrity": "sha512-LpCHtPQ3sFx67z+uh2HnSyWSLLu5Jxo21795uRDuar/EOuYWXib5EmPaGIBuSnRqH2IODiKA2k5re/K9OnN/Yg=="
},
"fs-extra": {
"version": "8.1.0",
......@@ -3233,7 +3234,8 @@
},
"jed": {
"version": "1.1.1",
"resolved": false
"resolved": "https://registry.npmjs.org/jed/-/jed-1.1.1.tgz",
"integrity": "sha1-elSbvZ/+FYWwzQoZHiAwVb7ldLQ="
},
"jsonfile": {
"version": "5.0.0",
......@@ -3254,7 +3256,8 @@
},
"localforage": {
"version": "1.7.3",
"resolved": false,
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.7.3.tgz",
"integrity": "sha512-1TulyYfc4udS7ECSBT2vwJksWbkwwTX8BzeUIiq8Y07Riy7bDAAnxDaPU/tWyOVmQAcWJIEIFP9lPfBGqVoPgQ==",
"requires": {
"lie": "3.1.1"
}
......@@ -3266,13 +3269,14 @@
},
"pluggable.js": {
"version": "2.0.1",
"resolved": false,
"resolved": "https://registry.npmjs.org/pluggable.js/-/pluggable.js-2.0.1.tgz",
"integrity": "sha512-SBt6v6Tbp20Jf8hU0cpcc/+HBHGMY8/Q+yA6Ih0tBQE8tfdZ6U4PRG0iNvUUjLx/hVyOP53n0UfGBymlfaaXCg==",
"requires": {
"lodash": "^4.17.11"
}
},
"skeletor.js": {
"version": "0.0.1",
"version": "github:skeletorjs/skeletor#bf6d9c86f9fcf224fa9d9af5a25380b77aa4b561",
"from": "github:skeletorjs/skeletor#bf6d9c86f9fcf224fa9d9af5a25380b77aa4b561",
"requires": {
"lodash": "^4.17.14"
......@@ -3280,7 +3284,11 @@
},
"strophe.js": {
"version": "github:strophe/strophejs#c4a94e59877c06dc2395f4ccbd26f3fee67a4c9f",
"from": "strophe.js@github:strophe/strophejs#c4a94e59877c06dc2395f4ccbd26f3fee67a4c9f"
"from": "strophe.js@github:strophe/strophejs#c4a94e59877c06dc2395f4ccbd26f3fee67a4c9f",
"requires": {
"abab": "^2.0.3",
"xmldom": "^0.1.27"
}
},
"twemoji": {
"version": "12.1.5",
......
......@@ -193,82 +193,6 @@ describe("Chatboxes", function () {
done();
}));
it("can be trimmed to conserve space",
mock.initConverse(['rosterGroupsFetched'], {},
async function (done, _converse) {
spyOn(_converse.chatboxviews, 'trimChats');
const trimmed_chatboxes = _converse.minimized_chats;
spyOn(trimmed_chatboxes, 'addChat').and.callThrough();
spyOn(trimmed_chatboxes, 'removeChat').and.callThrough();
await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse);
expect(_converse.chatboxviews.trimChats.calls.count()).toBe(1);
let jid, chatboxview;
// openControlBox was called earlier, so the controlbox is
// visible, but no other chat boxes have been created.
expect(_converse.chatboxes.length).toEqual(1);
expect(document.querySelectorAll("#conversejs .chatbox").length).toBe(1); // Controlbox is open
_converse.rosterview.update(); // XXX: Hack to make sure $roster element is attached.
await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group li').length);
// Test that they can be maximized again
const online_contacts = _converse.rosterview.el.querySelectorAll('.roster-group .current-xmpp-contact a.open-chat');
expect(online_contacts.length).toBe(17);
let i;
for (i=0; i<online_contacts.length; i++) {
const el = online_contacts[i];
el.click();
}
await u.waitUntil(() => _converse.chatboxes.length == 16);
expect(_converse.chatboxviews.trimChats.calls.count()).toBe(16);
_converse.api.chatviews.get().forEach(v => spyOn(v, 'onMinimized').and.callThrough());
for (i=0; i<online_contacts.length; i++) {
const el = online_contacts[i];
jid = _.trim(el.textContent.trim()).replace(/ /g,'.').toLowerCase() + '@montague.lit';
chatboxview = _converse.chatboxviews.get(jid);
chatboxview.model.set({'minimized': true});
expect(trimmed_chatboxes.addChat).toHaveBeenCalled();
expect(chatboxview.onMinimized).toHaveBeenCalled();
}
await u.waitUntil(() => _converse.chatboxviews.keys().length);
var key = _converse.chatboxviews.keys()[1];
const trimmedview = trimmed_chatboxes.get(key);
const chatbox = trimmedview.model;
spyOn(chatbox, 'maximize').and.callThrough();
spyOn(trimmedview, 'restore').and.callThrough();
trimmedview.delegateEvents();
trimmedview.el.querySelector("a.restore-chat").click();
expect(trimmedview.restore).toHaveBeenCalled();
expect(chatbox.maximize).toHaveBeenCalled();
expect(_converse.chatboxviews.trimChats.calls.count()).toBe(17);
done();
}));
it("can be opened in minimized mode initially",
mock.initConverse(
['rosterGroupsFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await _converse.api.chats.create(sender_jid, {'minimized': true});
await u.waitUntil(() => _converse.chatboxes.length > 1);
const chatBoxView = _converse.chatboxviews.get(sender_jid);
expect(u.isVisible(chatBoxView.el)).toBeFalsy();
const minimized_chat = _converse.minimized_chats.get(sender_jid);
expect(minimized_chat).toBeTruthy();
expect(u.isVisible(minimized_chat.el)).toBeTruthy();
done();
}));
it("is focused if its already open and you click on its corresponding roster item",
mock.initConverse(['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
......@@ -364,42 +288,6 @@ describe("Chatboxes", function () {
done();
}));
it("can be minimized by clicking a DOM element with class 'toggle-chatbox-button'",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse);
const contact_jid = mock.cur_names[7].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length);
await mock.openChatBoxFor(_converse, contact_jid);
const trimmed_chatboxes = _converse.minimized_chats;
const chatview = _converse.chatboxviews.get(contact_jid);
spyOn(chatview, 'minimize').and.callThrough();
spyOn(_converse.api, "trigger").and.callThrough();
// We need to rebind all events otherwise our spy won't be called
chatview.delegateEvents();
chatview.el.querySelector('.toggle-chatbox-button').click();
expect(chatview.minimize).toHaveBeenCalled();
expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxMinimized', jasmine.any(Object));
expect(_converse.api.trigger.calls.count(), 2);
expect(u.isVisible(chatview.el)).toBeFalsy();
expect(chatview.model.get('minimized')).toBeTruthy();
chatview.el.querySelector('.toggle-chatbox-button').click();
const trimmedview = trimmed_chatboxes.get(chatview.model.get('id'));
spyOn(trimmedview, 'restore').and.callThrough();
trimmedview.delegateEvents();
trimmedview.el.querySelector("a.restore-chat").click();
expect(trimmedview.restore).toHaveBeenCalled();
expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxMaximized', jasmine.any(Object));
expect(chatview.model.get('minimized')).toBeFalsy();
done();
}));
it("will be removed from browserStorage when closed",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
......@@ -1591,82 +1479,4 @@ describe("Chatboxes", function () {
done();
}));
});
describe("A Minimized ChatBoxView's Unread Message Count", function () {
it("is displayed when scrolled up chatbox is minimized after receiving unread messages",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 1);
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await mock.openChatBoxFor(_converse, sender_jid);
const msgFactory = function () {
return mock.createChatMessage(_converse, sender_jid, 'This message will be received as unread, but eventually will be read');
};
const selectUnreadMsgCount = function () {
const minimizedChatBoxView = _converse.minimized_chats.get(sender_jid);
return minimizedChatBoxView.el.querySelector('.message-count');
};
const chatbox = _converse.chatboxes.get(sender_jid);
chatbox.save('scrolled', true);
_converse.handleMessageStanza(msgFactory());
await u.waitUntil(() => chatbox.messages.length);
const chatboxview = _converse.chatboxviews.get(sender_jid);
chatboxview.minimize();
const unread_count = selectUnreadMsgCount();
expect(u.isVisible(unread_count)).toBeTruthy();
expect(unread_count.innerHTML.replace(/<!---->/g, '')).toBe('1');
done();
}));
it("is incremented when message is received and windows is not focused",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 1);
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
const view = await mock.openChatBoxFor(_converse, sender_jid)
const msgFactory = function () {
return mock.createChatMessage(_converse, sender_jid,
'This message will be received as unread, but eventually will be read');
};
const selectUnreadMsgCount = function () {
const minimizedChatBoxView = _converse.minimized_chats.get(sender_jid);
return minimizedChatBoxView.el.querySelector('.message-count');
};
view.minimize();
_converse.handleMessageStanza(msgFactory());
await u.waitUntil(() => view.model.messages.length);
const unread_count = selectUnreadMsgCount();
expect(u.isVisible(unread_count)).toBeTruthy();
expect(unread_count.innerHTML.replace(/<!---->/g, '')).toBe('1');
done();
}));
it("will render Openstreetmap-URL from geo-URI",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 1);
const message = "geo:37.786971,-122.399677";
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await mock.openChatBoxFor(_converse, contact_jid);
const view = _converse.chatboxviews.get(contact_jid);
spyOn(view.model, 'sendMessage').and.callThrough();
mock.sendMessage(view, message);
await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-msg').length, 1000);
expect(view.model.sendMessage).toHaveBeenCalled();
const msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
await u.waitUntil(() => msg.innerHTML.replace(/\<!----\>/g, '') ===
'<a target="_blank" rel="noopener" href="https://www.openstreetmap.org/?mlat=37.786971&amp;'+
'mlon=-122.399677#map=18/37.786971/-122.399677">https://www.openstreetmap.org/?mlat=37.786971&amp;mlon=-122.399677#map=18/37.786971/-122.399677</a>');
done();
}));
});
});
......@@ -700,68 +700,6 @@ describe("A Chat Message", function () {
done();
}));
it("received for a minimized chat box will increment a counter on its header",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
if (_converse.view_mode === 'fullscreen') {
return done();
}
await mock.waitForRoster(_converse, 'current');
const contact_name = mock.cur_names[0];
const contact_jid = contact_name.replace(/ /g,'.').toLowerCase() + '@montague.lit';
await mock.openControlBox(_converse);
spyOn(_converse.api, "trigger").and.callThrough();
await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length);
await mock.openChatBoxFor(_converse, contact_jid);
const chatview = _converse.api.chatviews.get(contact_jid);
expect(u.isVisible(chatview.el)).toBeTruthy();
expect(chatview.model.get('minimized')).toBeFalsy();
chatview.el.querySelector('.toggle-chatbox-button').click();
expect(chatview.model.get('minimized')).toBeTruthy();
var message = 'This message is sent to a minimized chatbox';
var sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
var msg = $msg({
from: sender_jid,
to: _converse.connection.jid,
type: 'chat',
id: u.getUniqueId()
}).c('body').t(message).up()
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
await _converse.handleMessageStanza(msg);
await u.waitUntil(() => chatview.model.messages.length);
expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object));
const trimmed_chatboxes = _converse.minimized_chats;
const trimmedview = trimmed_chatboxes.get(contact_jid);
let count = trimmedview.el.querySelector('.message-count');
expect(u.isVisible(chatview.el)).toBeFalsy();
expect(trimmedview.model.get('minimized')).toBeTruthy();
expect(u.isVisible(count)).toBeTruthy();
expect(count.textContent).toBe('1');
_converse.handleMessageStanza(
$msg({
from: mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit',
to: _converse.connection.jid,
type: 'chat',
id: u.getUniqueId()
}).c('body').t('This message is also sent to a minimized chatbox').up()
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()
);
await u.waitUntil(() => (chatview.model.messages.length > 1));
expect(u.isVisible(chatview.el)).toBeFalsy();
expect(trimmedview.model.get('minimized')).toBeTruthy();
count = trimmedview.el.querySelector('.message-count');
expect(u.isVisible(count)).toBeTruthy();
expect(count.textContent).toBe('2');
trimmedview.el.querySelector('.restore-chat').click();
expect(trimmed_chatboxes.keys().length).toBe(0);
done();
}));
it("will indicate when it has a time difference of more than a day between it and its predecessor",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
......
/*global mock */
/*global mock, converse */
const _ = converse.env._;
const $msg = converse.env.$msg;
const u = converse.env.utils;
const sizzle = converse.env.sizzle;
describe("A chat message", function () {
it("received for a minimized chat box will increment a counter on its header",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
if (_converse.view_mode === 'fullscreen') {
return done();
}
await mock.waitForRoster(_converse, 'current');
const contact_name = mock.cur_names[0];
const contact_jid = contact_name.replace(/ /g,'.').toLowerCase() + '@montague.lit';
await mock.openControlBox(_converse);
spyOn(_converse.api, "trigger").and.callThrough();
await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length);
await mock.openChatBoxFor(_converse, contact_jid);
const chatview = _converse.api.chatviews.get(contact_jid);
expect(u.isVisible(chatview.el)).toBeTruthy();
expect(chatview.model.get('minimized')).toBeFalsy();
chatview.el.querySelector('.toggle-chatbox-button').click();
expect(chatview.model.get('minimized')).toBeTruthy();
var message = 'This message is sent to a minimized chatbox';
var sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
var msg = $msg({
from: sender_jid,
to: _converse.connection.jid,
type: 'chat',
id: u.getUniqueId()
}).c('body').t(message).up()
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
await _converse.handleMessageStanza(msg);
await u.waitUntil(() => chatview.model.messages.length);
expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object));
const trimmed_chatboxes = _converse.minimized_chats;
let count = trimmed_chatboxes.el.querySelector('converse-minimized-chat .message-count');
expect(u.isVisible(chatview.el)).toBeFalsy();
expect(chatview.model.get('minimized')).toBeTruthy();
expect(u.isVisible(count)).toBeTruthy();
expect(count.textContent).toBe('1');
_converse.handleMessageStanza(
$msg({
from: mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit',
to: _converse.connection.jid,
type: 'chat',
id: u.getUniqueId()
}).c('body').t('This message is also sent to a minimized chatbox').up()
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()
);
await u.waitUntil(() => (chatview.model.messages.length > 1));
expect(u.isVisible(chatview.el)).toBeFalsy();
expect(chatview.model.get('minimized')).toBeTruthy();
count = trimmed_chatboxes.el.querySelector('converse-minimized-chat .message-count');
expect(u.isVisible(count)).toBeTruthy();
expect(count.textContent).toBe('2');
_converse.minimized_chats.el.querySelector("a.restore-chat").click();
expect(_converse.chatboxes.filter('minimized').length).toBe(0);
done();
}));
});
describe("A Groupcaht", function () {
it("can be minimized by clicking a DOM element with class 'toggle-chatbox-button'",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.openChatRoom(_converse, 'lounge', 'montague.lit', 'romeo');
const view = _converse.chatboxviews.get('lounge@montague.lit');
spyOn(view, 'onMinimized').and.callThrough();
spyOn(view, 'onMaximized').and.callThrough();
spyOn(_converse.api, "trigger").and.callThrough();
view.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
const button = await u.waitUntil(() => view.el.querySelector('.toggle-chatbox-button'));
button.click();
expect(view.onMinimized).toHaveBeenCalled();
expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxMinimized', jasmine.any(Object));
expect(u.isVisible(view.el)).toBeFalsy();
expect(view.model.get('minimized')).toBeTruthy();
expect(view.onMinimized).toHaveBeenCalled();
const el = await u.waitUntil(() => _converse.minimized_chats.el.querySelector("a.restore-chat"));
el.click();
expect(view.onMaximized).toHaveBeenCalled();
expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxMaximized', jasmine.any(Object));
expect(view.model.get('minimized')).toBeFalsy();
expect(_converse.api.trigger.calls.count(), 3);
done();
}));
});
describe("A Chatbox", function () {
it("can be minimized by clicking a DOM element with class 'toggle-chatbox-button'",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse);
const contact_jid = mock.cur_names[7].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length);
await mock.openChatBoxFor(_converse, contact_jid);
const chatview = _converse.chatboxviews.get(contact_jid);
spyOn(chatview, 'minimize').and.callThrough();
spyOn(_converse.api, "trigger").and.callThrough();
// We need to rebind all events otherwise our spy won't be called
chatview.delegateEvents();
chatview.el.querySelector('.toggle-chatbox-button').click();
expect(chatview.minimize).toHaveBeenCalled();
expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxMinimized', jasmine.any(Object));
expect(_converse.api.trigger.calls.count(), 2);
expect(u.isVisible(chatview.el)).toBeFalsy();
expect(chatview.model.get('minimized')).toBeTruthy();
chatview.el.querySelector('.toggle-chatbox-button').click();
await u.waitUntil(() => _converse.chatboxviews.keys().length);
_converse.minimized_chats.el.querySelector("a.restore-chat").click();
expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxMaximized', jasmine.any(Object));
expect(chatview.model.get('minimized')).toBeFalsy();
done();
}));
it("can be opened in minimized mode initially",
mock.initConverse(
['rosterGroupsFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
expect(u.isVisible(_converse.minimized_chats.el.firstElementChild)).toBe(false);
await _converse.api.chats.create(sender_jid, {'minimized': true});
await u.waitUntil(() => _converse.chatboxes.length > 1);
const chatBoxView = _converse.chatboxviews.get(sender_jid);
expect(u.isVisible(chatBoxView.el)).toBeFalsy();
expect(u.isVisible(_converse.minimized_chats.el.firstElementChild)).toBe(true);
expect(_converse.minimized_chats.el.firstElementChild.querySelectorAll('converse-minimized-chat').length).toBe(1);
expect(_converse.chatboxes.filter('minimized').length).toBe(1);
done();
}));
it("can be trimmed to conserve space",
mock.initConverse(['rosterGroupsFetched'], {},
async function (done, _converse) {
spyOn(_converse.chatboxviews, 'trimChats');
await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse);
expect(_converse.chatboxviews.trimChats.calls.count()).toBe(1);
let jid, chatboxview;
// openControlBox was called earlier, so the controlbox is
// visible, but no other chat boxes have been created.
expect(_converse.chatboxes.length).toEqual(1);
expect(document.querySelectorAll("#conversejs .chatbox").length).toBe(1); // Controlbox is open
_converse.rosterview.update(); // XXX: Hack to make sure $roster element is attached.
await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group li').length);
// Test that they can be maximized again
const online_contacts = _converse.rosterview.el.querySelectorAll('.roster-group .current-xmpp-contact a.open-chat');
expect(online_contacts.length).toBe(17);
let i;
for (i=0; i<online_contacts.length; i++) {
const el = online_contacts[i];
el.click();
}
await u.waitUntil(() => _converse.chatboxes.length == 16);
expect(_converse.chatboxviews.trimChats.calls.count()).toBe(16);
_converse.api.chatviews.get().forEach(v => spyOn(v, 'onMinimized').and.callThrough());
for (i=0; i<online_contacts.length; i++) {
const el = online_contacts[i];
jid = el.textContent.trim().replace(/ /g,'.').toLowerCase() + '@montague.lit';
chatboxview = _converse.chatboxviews.get(jid);
chatboxview.model.set({'minimized': true});
expect(chatboxview.onMinimized).toHaveBeenCalled();
}
await u.waitUntil(() => _converse.chatboxviews.keys().length);
var key = _converse.chatboxviews.keys()[1];
const chatbox = _converse.chatboxes.get(key);
spyOn(chatbox, 'maximize').and.callThrough();
_converse.minimized_chats.el.querySelector("a.restore-chat").click();
expect(chatbox.maximize).toHaveBeenCalled();
expect(_converse.chatboxviews.trimChats.calls.count()).toBe(17);
done();
}));
});
describe("A Minimized ChatBoxView's Unread Message Count", function () {
it("is displayed when scrolled up chatbox is minimized after receiving unread messages",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 1);
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await mock.openChatBoxFor(_converse, sender_jid);
const msgFactory = () => mock.createChatMessage(_converse, sender_jid, 'This message will be received as unread, but eventually will be read');
const selectUnreadMsgCount = () => _converse.minimized_chats.el.querySelector('#toggle-minimized-chats .unread-message-count');
const chatbox = _converse.chatboxes.get(sender_jid);
chatbox.save('scrolled', true);
_converse.handleMessageStanza(msgFactory());
await u.waitUntil(() => chatbox.messages.length);
const chatboxview = _converse.chatboxviews.get(sender_jid);
chatboxview.minimize();
const unread_count = selectUnreadMsgCount();
expect(u.isVisible(unread_count)).toBeTruthy();
expect(unread_count.innerHTML.replace(/<!---->/g, '')).toBe('1');
done();
}));
it("is incremented when message is received and windows is not focused",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 1);
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
const view = await mock.openChatBoxFor(_converse, sender_jid)
const msgFactory = () => mock.createChatMessage(_converse, sender_jid, 'This message will be received as unread, but eventually will be read');
const selectUnreadMsgCount = () => _converse.minimized_chats.el.querySelector('#toggle-minimized-chats .unread-message-count');
view.minimize();
_converse.handleMessageStanza(msgFactory());
await u.waitUntil(() => view.model.messages.length);
const unread_count = selectUnreadMsgCount();
expect(u.isVisible(unread_count)).toBeTruthy();
expect(unread_count.innerHTML.replace(/<!---->/g, '')).toBe('1');
done();
}));
it("will render Openstreetmap-URL from geo-URI",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 1);
const message = "geo:37.786971,-122.399677";
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await mock.openChatBoxFor(_converse, contact_jid);
const view = _converse.chatboxviews.get(contact_jid);
spyOn(view.model, 'sendMessage').and.callThrough();
mock.sendMessage(view, message);
await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-msg').length, 1000);
expect(view.model.sendMessage).toHaveBeenCalled();
const msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
await u.waitUntil(() => msg.innerHTML.replace(/\<!----\>/g, '') ===
'<a target="_blank" rel="noopener" href="https://www.openstreetmap.org/?mlat=37.786971&amp;'+
'mlon=-122.399677#map=18/37.786971/-122.399677">https://www.openstreetmap.org/?mlat=37.786971&amp;mlon=-122.399677#map=18/37.786971/-122.399677</a>');
done();
}));
});
describe("The Minimized Chats Widget", function () {
......@@ -19,12 +291,12 @@ describe("The Minimized Chats Widget", function () {
await mock.openChatBoxFor(_converse, contact_jid)
let chatview = _converse.chatboxviews.get(contact_jid);
expect(chatview.model.get('minimized')).toBeFalsy();
expect(u.isVisible(_converse.minimized_chats.el)).toBe(false);
expect(u.isVisible(_converse.minimized_chats.el.firstElementChild)).toBe(false);
chatview.el.querySelector('.toggle-chatbox-button').click();
expect(chatview.model.get('minimized')).toBeTruthy();
expect(u.isVisible(_converse.minimized_chats.el)).toBe(true);
expect(_converse.minimized_chats.keys().length).toBe(1);
expect(_converse.minimized_chats.keys()[0]).toBe(contact_jid);
expect(_converse.chatboxes.filter('minimized').length).toBe(1);
expect(_converse.chatboxes.models.filter(c => c.get('minimized')).pop().get('jid')).toBe(contact_jid);
contact_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await mock.openChatBoxFor(_converse, contact_jid);
......@@ -33,8 +305,8 @@ describe("The Minimized Chats Widget", function () {
chatview.el.querySelector('.toggle-chatbox-button').click();
expect(chatview.model.get('minimized')).toBeTruthy();
expect(u.isVisible(_converse.minimized_chats.el)).toBe(true);
expect(_converse.minimized_chats.keys().length).toBe(2);
expect(_.includes(_converse.minimized_chats.keys(), contact_jid)).toBeTruthy();
expect(_converse.chatboxes.filter('minimized').length).toBe(2);
expect(_converse.chatboxes.filter('minimized').map(c => c.get('jid')).includes(contact_jid)).toBeTruthy();
done();
}));
......@@ -50,16 +322,18 @@ describe("The Minimized Chats Widget", function () {
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await mock.openChatBoxFor(_converse, contact_jid);
const chatview = _converse.chatboxviews.get(contact_jid);
expect(u.isVisible(_converse.minimized_chats.el)).toBeFalsy();
expect(u.isVisible(_converse.minimized_chats.el.firstElementChild)).toBe(false);
chatview.model.set({'minimized': true});
expect(u.isVisible(_converse.minimized_chats.el)).toBeTruthy();
expect(_converse.minimized_chats.keys().length).toBe(1);
expect(_converse.minimized_chats.keys()[0]).toBe(contact_jid);
expect(_converse.chatboxes.filter('minimized').length).toBe(1);
expect(_converse.chatboxes.models.filter(c => c.get('minimized')).pop().get('jid')).toBe(contact_jid);
expect(u.isVisible(_converse.minimized_chats.el.querySelector('.minimized-chats-flyout'))).toBeTruthy();
expect(_converse.minimized_chats.toggleview.model.get('collapsed')).toBeFalsy();
expect(_converse.minimized_chats.minchats.get('collapsed')).toBeFalsy();
_converse.minimized_chats.el.querySelector('#toggle-minimized-chats').click();
await u.waitUntil(() => u.isVisible(_converse.minimized_chats.el.querySelector('.minimized-chats-flyout')));
expect(_converse.minimized_chats.toggleview.model.get('collapsed')).toBeTruthy();
expect(_converse.minimized_chats.minchats.get('collapsed')).toBeTruthy();
done();
}));
......@@ -72,22 +346,22 @@ describe("The Minimized Chats Widget", function () {
await mock.openControlBox(_converse);
_converse.minimized_chats.initToggle();
var i, contact_jid, chatview, msg;
_converse.minimized_chats.toggleview.model.set({'collapsed': true});
_converse.minimized_chats.minchats.set({'collapsed': true});
const unread_el = _converse.minimized_chats.toggleview.el.querySelector('.unread-message-count');
expect(unread_el === null).toBe(true);
const unread_el = _converse.minimized_chats.el.querySelector('.unread-message-count');
expect(u.isVisible(unread_el)).toBe(false);
let i, contact_jid;
for (i=0; i<3; i++) {
contact_jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@montague.lit';
mock.openChatBoxFor(_converse, contact_jid);
}
await u.waitUntil(() => _converse.chatboxes.length == 4);
chatview = _converse.chatboxviews.get(contact_jid);
const chatview = _converse.chatboxviews.get(contact_jid);
chatview.model.set({'minimized': true});
for (i=0; i<3; i++) {
msg = $msg({
const msg = $msg({
from: contact_jid,
to: _converse.connection.jid,
type: 'chat',
......@@ -98,8 +372,8 @@ describe("The Minimized Chats Widget", function () {
}
await u.waitUntil(() => chatview.model.messages.length === 3, 500);
expect(u.isVisible(_converse.minimized_chats.toggleview.el.querySelector('.unread-message-count'))).toBeTruthy();
expect(_converse.minimized_chats.toggleview.el.querySelector('.unread-message-count').textContent).toBe((3).toString());
expect(u.isVisible(_converse.minimized_chats.el.querySelector('.unread-message-count'))).toBeTruthy();
expect(_converse.minimized_chats.el.querySelector('.unread-message-count').textContent).toBe((3).toString());
// Chat state notifications don't increment the unread messages counter
// <composing> state
_converse.handleMessageStanza($msg({
......@@ -108,7 +382,7 @@ describe("The Minimized Chats Widget", function () {
type: 'chat',
id: u.getUniqueId()
}).c('composing', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
expect(_converse.minimized_chats.toggleview.el.querySelector('.unread-message-count').textContent).toBe((i).toString());
expect(_converse.minimized_chats.el.querySelector('.unread-message-count').textContent).toBe((i).toString());
// <paused> state
_converse.handleMessageStanza($msg({
......@@ -117,7 +391,7 @@ describe("The Minimized Chats Widget", function () {
type: 'chat',
id: u.getUniqueId()
}).c('paused', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
expect(_converse.minimized_chats.toggleview.el.querySelector('.unread-message-count').textContent).toBe((i).toString());
expect(_converse.minimized_chats.el.querySelector('.unread-message-count').textContent).toBe((i).toString());
// <gone> state
_converse.handleMessageStanza($msg({
......@@ -126,7 +400,7 @@ describe("The Minimized Chats Widget", function () {
type: 'chat',
id: u.getUniqueId()
}).c('gone', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
expect(_converse.minimized_chats.toggleview.el.querySelector('.unread-message-count').textContent).toBe((i).toString());
expect(_converse.minimized_chats.el.querySelector('.unread-message-count').textContent).toBe((i).toString());
// <inactive> state
_converse.handleMessageStanza($msg({
......@@ -135,7 +409,7 @@ describe("The Minimized Chats Widget", function () {
type: 'chat',
id: u.getUniqueId()
}).c('inactive', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
expect(_converse.minimized_chats.toggleview.el.querySelector('.unread-message-count').textContent).toBe((i).toString());
expect(_converse.minimized_chats.el.querySelector('.unread-message-count').textContent).toBe((i).toString());
done();
}));
......@@ -158,8 +432,8 @@ describe("The Minimized Chats Widget", function () {
}).c('body').t(message).tree();
view.model.handleMessageStanza(msg);
await u.waitUntil(() => view.model.messages.length);
expect(u.isVisible(_converse.minimized_chats.toggleview.el.querySelector('.unread-message-count'))).toBeTruthy();
expect(_converse.minimized_chats.toggleview.el.querySelector('.unread-message-count').textContent).toBe('1');
expect(u.isVisible(_converse.minimized_chats.el.querySelector('.unread-message-count'))).toBeTruthy();
expect(_converse.minimized_chats.el.querySelector('.unread-message-count').textContent).toBe('1');
done();
}));
});
......@@ -2862,38 +2862,6 @@ describe("Groupchats", function () {
done();
}));
it("can be minimized by clicking a DOM element with class 'toggle-chatbox-button'",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.openChatRoom(_converse, 'lounge', 'montague.lit', 'romeo');
const view = _converse.chatboxviews.get('lounge@montague.lit'),
trimmed_chatboxes = _converse.minimized_chats;
spyOn(view, 'onMinimized').and.callThrough();
spyOn(view, 'onMaximized').and.callThrough();
spyOn(_converse.api, "trigger").and.callThrough();
view.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
const button = await u.waitUntil(() => view.el.querySelector('.toggle-chatbox-button'));
button.click();
expect(view.onMinimized).toHaveBeenCalled();
expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxMinimized', jasmine.any(Object));
expect(u.isVisible(view.el)).toBeFalsy();
expect(view.model.get('minimized')).toBeTruthy();
expect(view.onMinimized).toHaveBeenCalled();
await u.waitUntil(() => trimmed_chatboxes.get(view.model.get('id')));
const trimmedview = trimmed_chatboxes.get(view.model.get('id'));
trimmedview.el.querySelector("a.restore-chat").click();
expect(view.onMaximized).toHaveBeenCalled();
expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxMaximized', jasmine.any(Object));
expect(view.model.get('minimized')).toBeFalsy();
expect(_converse.api.trigger.calls.count(), 3);
done();
}));
it("can be closed again by clicking a DOM element with class 'close-chatbox-button'",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
......
import { CustomElement } from './element.js';
import tpl_trimmed_chat from "templates/trimmed_chat.js";
import { api, _converse } from "@converse/headless/converse-core";
export default class MinimizedChat extends CustomElement {
static get properties () {
return {
model: { type: Object },
title: { type: String },
type: { type: String },
num_unread: { type: Number }
}
}
render () {
const data = {
'close': ev => this.close(ev),
'num_unread': this.num_unread,
'restore': ev => this.restore(ev),
'title': this.title,
'type': this.type
};
return tpl_trimmed_chat(data);
}
close (ev) {
ev?.preventDefault();
const view = _converse.chatboxviews.get(this.model.get('id'));
if (view) {
// This will call model.destroy(), removing it from the
// collection and will also emit 'chatBoxClosed'
view.close();
} else {
this.model.destroy();
api.trigger('chatBoxClosed', this);
}
}
restore (ev) {
ev?.preventDefault();
this.model.maximize();
}
}
api.elements.define('converse-minimized-chat', MinimizedChat);
......@@ -3,16 +3,14 @@
* @copyright 2020, the Converse.js contributors
* @license Mozilla Public License (MPLv2)
*/
import "converse-chatview";
import tpl_chats_panel from "templates/chats_panel.html";
import tpl_toggle_chats from "templates/toggle_chats.js";
import tpl_trimmed_chat from "templates/trimmed_chat.js";
import './components/minimized_chat.js';
import 'converse-chatview';
import tpl_chats_panel from 'templates/chats_panel.js';
import { Model } from '@converse/skeletor/src/model.js';
import { Overview } from "@converse/skeletor/src/overview";
import { View } from "@converse/skeletor/src/view";
import { View } from '@converse/skeletor/src/view';
import { __ } from '@converse/headless/i18n';
import { _converse, api, converse } from "@converse/headless/converse-core";
import { debounce, sum } from 'lodash-es';
import { _converse, api, converse } from '@converse/headless/converse-core';
import { debounce } from 'lodash-es';
import { render } from 'lit-html';
const { dayjs } = converse.env;
......@@ -324,183 +322,58 @@ converse.plugins.add('converse-minimize', {
api.promises.add('minimizedChatsInitialized');
_converse.MinimizedChatBoxView = View.extend({
tagName: 'div',
events: {
'click .close-chatbox-button': 'close',
'click .restore-chat': 'restore'
},
initialize () {
this.listenTo(this.model, 'change:num_unread', this.render)
this.listenTo(this.model, 'change:name', this.render)
this.listenTo(this.model, 'change:fullname', this.render)
this.listenTo(this.model, 'change:jid', this.render)
this.listenTo(this.model, 'destroy', this.remove)
/**
* Triggered once a {@link _converse.MinimizedChatBoxView } has been initialized
* @event _converse#minimizedChatViewInitialized
* @type { _converse.MinimizedChatBoxView }
* @example _converse.api.listen.on('minimizedChatViewInitialized', view => { ... });
*/
api.trigger('minimizedChatViewInitialized', this);
},
render () {
const data = Object.assign(this.model.toJSON(), {'title': this.model.getDisplayName()});
render(tpl_trimmed_chat(data), this.el);
return this.el;
},
close (ev) {
if (ev && ev.preventDefault) { ev.preventDefault(); }
this.remove();
const view = _converse.chatboxviews.get(this.model.get('id'));
if (view) {
// This will call model.destroy(), removing it from the
// collection and will also emit 'chatBoxClosed'
view.close();
} else {
this.model.destroy();
api.trigger('chatBoxClosed', this);
}
return this;
},
restore: debounce(function (ev) {
if (ev && ev.preventDefault) { ev.preventDefault(); }
this.model.off('change:num_unread', null, this);
this.remove();
this.model.maximize();
}, 200, {'leading': true})
_converse.MinimizedChatsToggle = Model.extend({
defaults: {
'collapsed': false
}
});
_converse.MinimizedChats = Overview.extend({
tagName: 'div',
id: "minimized-chats",
className: 'hidden',
events: {
"click #toggle-minimized-chats": "toggle"
},
_converse.MinimizedChats = View.extend({
tagName: 'span',
async initialize () {
this.render();
await this.initToggle();
const chats = this.model.where({'minimized': true});
chats.length && this.addMultipleChats(chats);
this.listenTo(this.model, "add", this.onChanged)
this.listenTo(this.model, "destroy", this.removeChat)
this.listenTo(this.model, "change:minimized", this.onChanged)
this.listenTo(this.model, 'change:num_unread', this.updateUnreadMessagesCounter)
this.render();
this.listenTo(this.minchats, 'change:collapsed', this.render)
this.listenTo(this.model, 'add', this.render)
this.listenTo(this.model, 'change:fullname', this.render)
this.listenTo(this.model, 'change:jid', this.render)
this.listenTo(this.model, 'change:minimized', this.render)
this.listenTo(this.model, 'change:name', this.render)
this.listenTo(this.model, 'change:num_unread', this.render)
this.listenTo(this.model, 'remove', this.render)
},
render () {
const chats = this.model.where({'minimized': true});
const num_unread = chats.reduce((acc, chat) => (acc + chat.get('num_unread')), 0);
const num_minimized = chats.reduce((acc, chat) => (acc + (chat.get('minimized') ? 1 : 0)), 0);
const collapsed = this.minchats.get('collapsed');
const data = { chats, num_unread, num_minimized, collapsed };
data.toggle = ev => this.toggle(ev);
render(tpl_chats_panel(data), this.el);
if (!this.el.parentElement) {
this.el.innerHTML = tpl_chats_panel();
_converse.chatboxviews.insertRowColumn(this.el);
}
if (this.keys().length === 0) {
this.el.classList.add('hidden');
} else if (this.keys().length > 0 && !u.isVisible(this.el)) {
this.el.classList.remove('hidden');
}
return this.el;
},
async initToggle () {
const id = `converse.minchatstoggle-${_converse.bare_jid}`;
const model = new _converse.MinimizedChatsToggle({id});
model.browserStorage = _converse.createStore(id);
await new Promise(resolve => model.fetch({'success': resolve, 'error': resolve}));
this.toggleview = new _converse.MinimizedChatsToggleView({model});
this.minchats = new _converse.MinimizedChatsToggle({id});
this.minchats.browserStorage = _converse.createStore(id);
await new Promise(resolve => this.minchats.fetch({'success': resolve, 'error': resolve}));
},
toggle (ev) {
if (ev && ev.preventDefault) { ev.preventDefault(); }
this.toggleview.model.save({'collapsed': !this.toggleview.model.get('collapsed')});
u.slideToggleElement(this.el.querySelector('.minimized-chats-flyout'), 200);
},
onChanged (item) {
if (item.get('id') === 'controlbox') {
// The ControlBox has it's own minimize toggle
return;
}
if (item.get('minimized')) {
this.addChat(item);
} else if (this.get(item.get('id'))) {
this.removeChat(item);
}
},
addChatView (item) {
const existing = this.get(item.get('id'));
if (existing && existing.el.parentNode) {
return;
}
const view = new _converse.MinimizedChatBoxView({model: item});
this.el.querySelector('.minimized-chats-flyout').insertAdjacentElement('beforeEnd', view.render());
this.add(item.get('id'), view);
},
addMultipleChats (items) {
items.forEach(item => this.addChatView(item));
this.toggleview.model.set({'num_minimized': this.keys().length});
this.render();
},
addChat (item) {
this.addChatView(item);
this.toggleview.model.set({'num_minimized': this.keys().length});
this.render();
},
removeChat (item) {
this.remove(item.get('id'));
this.toggleview.model.set({'num_minimized': this.keys().length});
this.render();
},
updateUnreadMessagesCounter () {
this.toggleview.model.save({'num_unread': sum(this.model.pluck('num_unread'))});
this.render();
ev?.preventDefault();
this.minchats.save({'collapsed': !this.minchats.get('collapsed')});
}
});
_converse.MinimizedChatsToggle = Model.extend({
defaults: {
'collapsed': false,
'num_minimized': 0,
'num_unread': 0
}
});
_converse.MinimizedChatsToggleView = View.extend({
_setElement (){
this.el = _converse.root.querySelector('#toggle-minimized-chats');
},
initialize () {
this.listenTo(this.model, 'change:num_minimized', this.render)
this.listenTo(this.model, 'change:num_unread', this.render)
this.flyout = this.el.parentElement.querySelector('.minimized-chats-flyout');
},
render () {
render(tpl_toggle_chats(Object.assign(this.model.toJSON())), this.el);
if (this.model.get('collapsed')) {
u.hideElement(this.flyout);
} else {
u.showElement(this.flyout);
}
return this.el;
}
});
function initMinimizedChats () {
_converse.minimized_chats?.remove();
_converse.minimized_chats = new _converse.MinimizedChats({model: _converse.chatboxes});
......
<a id="toggle-minimized-chats" href="#" class="row no-gutters"></a>
<div class="flyout minimized-chats-flyout row no-gutters"></div>
import { html } from "lit-html";
import { __ } from '@converse/headless/i18n';
export default (o) =>
html`<div id="minimized-chats" class="${o.chats.length ? '' : 'hidden'}">
<a id="toggle-minimized-chats" class="row no-gutters" @click=${o.toggle}>
${o.num_minimized} ${__('Minimized')}
<span class="unread-message-count ${!o.num_unread ? 'unread-message-count-hidden' : ''}" href="#">${o.num_unread}</span>
</a>
<div class="flyout minimized-chats-flyout row no-gutters ${o.collapsed ? 'hidden' : ''}">
${o.chats.map(chat =>
html`<converse-minimized-chat
.model=${chat}
title=${chat.getDisplayName()}
type=${chat.get('type')}
num_unread=${chat.get('num_unread')}></converse-minimized-chat>`)}
</div>
</div>`;
import { html } from "lit-html";
import { __ } from '@converse/headless/i18n';
export default (o) => html`
${o.num_minimized} ${__('Minimized')}
<span class="unread-message-count ${!o.num_unread ? 'unread-message-count-hidden' : ''}" href="#">${o.num_unread}</span>
`;
......@@ -6,10 +6,10 @@ export default (o) => {
const i18n_tooltip = __('Click to restore this chat');
return html`
<div class="chat-head-${o.type} chat-head row no-gutters">
<a class="restore-chat w-100 align-self-center" title="${i18n_tooltip}">
<a class="restore-chat w-100 align-self-center" title="${i18n_tooltip}" @click=${o.restore}>
${o.num_unread ? html`<span class="message-count badge badge-light">${o.num_unread}</span>` : '' }
${o.title}
</a>
<a class="chatbox-btn close-chatbox-button fa fa-times"></a>
<a class="chatbox-btn close-chatbox-button fa fa-times" @click=${o.close}></a>
</div>`;
}
......@@ -20,6 +20,7 @@
}
});
converse.initialize({
// root: new DocumentFragment(),
show_send_button: true,
auto_away: 300,
auto_register_muc_nickname: true,
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment