Commit 1fa203c9 authored by JC Brand's avatar JC Brand

Support for IndexedDB. updates #1105

Depend on latest backbone.browserStorage which has support for IndexedDB
via localforage.

Storage operations are now asynchronous and transactional.

Bugs fixed (mostly by waiting for operations to complete):

* Rooms are now fetched asynchronously, so wait before triggering `show`
  or when closing.
* Make sure chat create/update transactions complete before firing events
* Make sure chats and messages have been fetched before creating new ones.
* When doing a `fetch` with `wait: false` on a collection and then
  creating a model in that collection, then once the read
  operation finishes (after creating the model), the collection is emptied again.
* Patch and wait when saving.
  Otherwise we have previously set attributes overriding later ones.
* Make sure api.roomviews.close returns a promise

Test fixes:

* Chats are now asynchronously returned, so we need to use `await`
* Wait for the storage transaction to complete when creating and updating messages
* Wait for all chatboxes to close
    Otherwise we get sessionStorage inconsistencies due to the async nature of localforage.
* Wait for room views to close in spec/chatroom.js

In the process, remove the `closeAllChatBoxes` override in
converse-controlbox by letting the `close` method decide whether it
should be closed or not.
parent 66c052f3
......@@ -22,13 +22,7 @@
},
"rules": {
"lodash/prefer-lodash-chain": "off",
"lodash/prefer-lodash-method": [2, {
"ignoreMethods": [
"assign", "every", "keys", "find", "endsWith", "startsWith", "filter",
"reduce", "isArray", "create", "map", "replace", "some", "toLower",
"split", "trim", "forEach", "toUpperCase", "includes", "values", "padStart"
]
}],
"lodash/prefer-lodash-method": "off",
"lodash/import-scope": "off",
"lodash/prefer-constant": "off",
"lodash/prefer-get": "off",
......
......@@ -3,6 +3,7 @@
## 6.0.0 (Unreleased)
- #129: Add support for XEP-0156: Disovering Alternative XMPP Connection Methods. Only XML is supported for now.
- #1105: Preliminary support for storing persistent data in IndexedDB instead of localStorage
- #1089: When filtering the roster for `online` users, show all non-offline users.
- #1691: Fix `collection.chatbox is undefined` errors
- #1733: New message notifications for a minimized chat stack on top of each other
......@@ -15,6 +16,10 @@
### Breaking changes
- In contrast to sessionStorage and localStorage, IndexedDB is an asynchronous database.
A lot of code that relied on database access to be synchronous had to be
updated to work with asynchronous access via promises.
- In order to add support for XEP-0156, the XMPP connection needs to be created
only once we know the JID of the user that's logging in. This means that the
[connectionInitialized](https://conversejs.org/docs/html/api/-_converse.html#event:connectionInitialized)
......@@ -26,6 +31,7 @@
* `_converse.api.chats.create`
* `_converse.api.rooms.get`
* `_converse.api.rooms.create`
* `_converse.api.roomviews.close`
- The `show_only_online_users` setting has been removed.
- The order of certain events have now changed: `statusInitialized` is now triggered after `initialized` and `connected` and `reconnected`.
......
This diff is collapsed.
......@@ -168,7 +168,8 @@
'name': 'The Play',
'nick': ' Othello'
});
expect(_converse.chatboxviews.get(jid) === undefined).toBeFalsy();
await u.waitUntil(() => _converse.api.rooms.get().length);
expect(_.isUndefined(_converse.chatboxviews.get(jid))).toBeFalsy();
// Check that we don't auto-join if muc_respect_autojoin is false
_converse.muc_respect_autojoin = false;
......@@ -188,20 +189,20 @@
it("will use the nickname from the bookmark", mock.initConverse(
['rosterGroupsFetched'], {}, async function (done, _converse) {
await test_utils.waitUntilBookmarksReturned(_converse);
const room_jid = 'coven@chat.shakespeare.lit';
await u.waitUntil(() => _converse.bookmarks);
_converse.bookmarks.create({
'jid': room_jid,
'autojoin': false,
'name': 'The Play',
'nick': 'Othello'
});
const model = await _converse.api.rooms.open(room_jid);
spyOn(model, 'join').and.callThrough();
const room = await _converse.api.rooms.open(room_jid);
spyOn(room, 'join').and.callThrough();
await test_utils.getRoomFeatures(_converse, 'coven', 'chat.shakespeare.lit');
await u.waitUntil(() => model.join.calls.count());
expect(model.get('nick')).toBe('Othello');
await u.waitUntil(() => room.join.calls.count());
expect(room.get('nick')).toBe('Othello');
done();
}));
......@@ -234,22 +235,13 @@
}));
it("can be unbookmarked", mock.initConverse(
['rosterGroupsFetched'], {},
async function (done, _converse) {
let sent_stanza, IQ_id;
await test_utils.waitUntilDiscoConfirmed(
_converse, _converse.bare_jid,
[{'category': 'pubsub', 'type': 'pep'}],
['http://jabber.org/protocol/pubsub#publish-options']
);
const sendIQ = _converse.connection.sendIQ;
await test_utils.openChatRoom(_converse, 'theplay', 'conference.shakespeare.lit', 'JC');
['rosterGroupsFetched'], {}, async function (done, _converse) {
const jid = 'theplay@conference.shakespeare.lit';
const view = _converse.chatboxviews.get(jid);
await u.waitUntil(() => !_.isNull(view.el.querySelector('.toggle-bookmark')));
await test_utils.waitUntilBookmarksReturned(_converse);
const muc_jid = 'theplay@conference.shakespeare.lit';
await _converse.api.rooms.open(muc_jid);
const view = _converse.chatboxviews.get(muc_jid);
await u.waitUntil(() => view.el.querySelector('.toggle-bookmark'));
spyOn(view, 'toggleBookmark').and.callThrough();
spyOn(_converse.bookmarks, 'sendBookmarkStanza').and.callThrough();
......@@ -261,16 +253,13 @@
'name': 'The Play',
'nick': ' Othello'
});
expect(_converse.bookmarks.length).toBe(1);
await u.waitUntil(() => _converse.chatboxes.length >= 1);
expect(view.model.get('bookmarked')).toBeTruthy();
let bookmark_icon = await u.waitUntil(() => view.el.querySelector('.toggle-bookmark'));
expect(u.hasClass('button-on', bookmark_icon)).toBeTruthy();
spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
sent_stanza = iq;
IQ_id = sendIQ.bind(this)(iq, callback, errback);
});
spyOn(_converse.connection, 'getUniqueId').and.callThrough();
bookmark_icon.click();
bookmark_icon = await u.waitUntil(() => view.el.querySelector('.toggle-bookmark'));
......@@ -281,8 +270,9 @@
// Check that an IQ stanza is sent out, containing no
// conferences to bookmark (since we removed the one and
// only bookmark).
expect(sent_stanza.toLocaleString()).toBe(
`<iq from="romeo@montague.lit/orchard" id="${IQ_id}" type="set" xmlns="jabber:client">`+
const sent_stanza = _converse.connection.IQ_stanzas.pop();
expect(Strophe.serialize(sent_stanza)).toBe(
`<iq from="romeo@montague.lit/orchard" id="${sent_stanza.getAttribute('id')}" type="set" xmlns="jabber:client">`+
`<pubsub xmlns="http://jabber.org/protocol/pubsub">`+
`<publish node="storage:bookmarks">`+
`<item id="current">`+
......@@ -478,7 +468,7 @@
[{'category': 'pubsub', 'type': 'pep'}],
['http://jabber.org/protocol/pubsub#publish-options']
);
test_utils.openControlBox();
test_utils.openControlBox(_converse);
const IQ_stanzas = _converse.connection.IQ_stanzas;
const sent_stanza = await u.waitUntil(
......@@ -546,10 +536,9 @@
it("remembers the toggle state of the bookmarks list", mock.initConverse(
['rosterGroupsFetched'], {},
async function (done, _converse) {
['rosterGroupsFetched'], {}, async function (done, _converse) {
test_utils.openControlBox();
await test_utils.openControlBox(_converse);
await test_utils.waitUntilDiscoConfirmed(
_converse, _converse.bare_jid,
[{'category': 'pubsub', 'type': 'pep'}],
......@@ -558,7 +547,7 @@
const IQ_stanzas = _converse.connection.IQ_stanzas;
const sent_stanza = await u.waitUntil(
() => IQ_stanzas.filter(s => sizzle('items[node="storage:bookmarks"]', s).length).pop());
() => IQ_stanzas.filter(s => sizzle('iq items[node="storage:bookmarks"]', s).length).pop());
expect(Strophe.serialize(sent_stanza)).toBe(
`<iq from="romeo@montague.lit/orchard" id="${sent_stanza.getAttribute('id')}" type="get" xmlns="jabber:client">`+
......@@ -567,13 +556,13 @@
'</pubsub>'+
'</iq>'
);
const stanza = $iq({'to': _converse.connection.jid, 'type':'result', 'id':sent_stanza.getAttribute('id')})
const stanza = $iq({'to': _converse.connection.jid, 'type':'result', 'id': sent_stanza.getAttribute('id')})
.c('pubsub', {'xmlns': Strophe.NS.PUBSUB})
.c('items', {'node': 'storage:bookmarks'})
.c('item', {'id': 'current'})
.c('storage', {'xmlns': 'storage:bookmarks'});
_converse.connection._dataRecv(test_utils.createRequest(stanza));
await _converse.api.waitUntil('bookmarksInitialized');
_converse.bookmarks.create({
'jid': 'theplay@conference.shakespeare.lit',
......@@ -606,7 +595,7 @@
{ hide_open_bookmarks: true },
async function (done, _converse) {
test_utils.openControlBox();
await test_utils.openControlBox(_converse);
await test_utils.waitUntilBookmarksReturned(_converse);
// Check that it's there
......
This diff is collapsed.
......@@ -43,7 +43,7 @@
spyOn(_converse.api, "trigger").and.callThrough();
spyOn(_converse.rosterview, 'update').and.callThrough();
test_utils.openControlBox();
await test_utils.openControlBox(_converse);
// Adding two contacts one with Capital initials and one with small initials of same JID (Case sensitive check)
_converse.roster.create({
jid: mock.pend_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit',
......@@ -69,8 +69,8 @@
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
test_utils.createContacts(_converse, 'all').openControlBox();
_converse.api.trigger('rosterContactsFetched');
await test_utils.waitForRoster(_converse, 'all');
await test_utils.openControlBox(_converse);
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await test_utils.openChatBoxFor(_converse, sender_jid);
......@@ -78,8 +78,8 @@
const chatview = _converse.chatboxviews.get(sender_jid);
chatview.model.set({'minimized': true});
expect(_.isNull(_converse.chatboxviews.el.querySelector('.restore-chat .message-count'))).toBeTruthy();
expect(_.isNull(_converse.rosterview.el.querySelector('.msgs-indicator'))).toBeTruthy();
expect(_converse.chatboxviews.el.querySelector('.restore-chat .message-count') === null).toBeTruthy();
expect(_converse.rosterview.el.querySelector('.msgs-indicator') === null).toBeTruthy();
let msg = $msg({
from: sender_jid,
......@@ -119,7 +119,7 @@
['rosterGroupsFetched'], {},
function (done, _converse) {
test_utils.openControlBox();
test_utils.openControlBox(_converse);
var view = _converse.xmppstatusview;
expect(u.hasClass('online', view.el.querySelector('.xmpp-status span:first-child'))).toBe(true);
expect(view.el.querySelector('.xmpp-status span.online').textContent.trim()).toBe('I am online');
......@@ -131,8 +131,7 @@
['rosterGroupsFetched'], {},
async function (done, _converse) {
test_utils.openControlBox();
await test_utils.openControlBox(_converse);
var cbview = _converse.chatboxviews.get('controlbox');
cbview.el.querySelector('.change-status').click()
var modal = _converse.xmppstatusview.status_modal;
......@@ -160,8 +159,7 @@
['rosterGroupsFetched'], {},
async function (done, _converse) {
test_utils.openControlBox();
await test_utils.openControlBox(_converse);
const cbview = _converse.chatboxviews.get('controlbox');
cbview.el.querySelector('.change-status').click()
const modal = _converse.xmppstatusview.status_modal;
......@@ -194,7 +192,8 @@
['rosterGroupsFetched'], {},
async function (done, _converse) {
test_utils.createContacts(_converse, 'all').openControlBox();
await test_utils.waitForRoster(_converse, 'all');
await test_utils.openControlBox(_converse);
const cbview = _converse.chatboxviews.get('controlbox');
cbview.el.querySelector('.add-contact').click()
......@@ -229,7 +228,7 @@
['rosterGroupsFetched'], {'autocomplete_add_contact': false},
async function (done, _converse) {
test_utils.openControlBox();
test_utils.openControlBox(_converse);
const cbview = _converse.chatboxviews.get('controlbox');
cbview.el.querySelector('.add-contact').click()
const modal = _converse.rosterview.add_contact_modal;
......@@ -319,7 +318,8 @@
'xhr_user_search_url': 'http://example.org/?' },
async function (done, _converse) {
test_utils.createContacts(_converse, 'all').openControlBox();
await test_utils.waitForRoster(_converse, 'all');
await test_utils.openControlBox(_converse);
var modal;
const xhr = {
'open': function open () {},
......
......@@ -258,9 +258,8 @@
['rosterInitialized', 'chatBoxesInitialized'], {},
async (done, _converse) => {
test_utils.openControlBox();
test_utils.createContacts(_converse, 'current', 2);
_converse.api.trigger('rosterContactsFetched');
await test_utils.openControlBox(_converse);
await test_utils.waitForRoster(_converse, 'current', 2);
// Test on chat that doesn't exist.
let chat = await _converse.api.chats.get('non-existing@jabber.org');
......@@ -294,9 +293,8 @@
['rosterGroupsFetched', 'chatBoxesInitialized'], {},
async (done, _converse) => {
test_utils.openControlBox();
test_utils.createContacts(_converse, 'current', 2);
_converse.api.trigger('rosterContactsFetched');
await test_utils.openControlBox(_converse);
await test_utils.waitForRoster(_converse, 'current', 2);
const jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
const jid2 = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit';
......
......@@ -18,7 +18,7 @@
async function (done, _converse) {
await test_utils.waitForRoster(_converse, 'current');
test_utils.openControlBox();
test_utils.openControlBox(_converse);
const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await test_utils.openChatBoxFor(_converse, contact_jid);
......
......@@ -6,13 +6,16 @@
], factory);
} (this, function (jasmine, mock, test_utils) {
"use strict";
var $msg = converse.env.$msg,
_ = converse.env._,
utils = converse.env.utils;
const $msg = converse.env.$msg,
_ = converse.env._,
u = converse.env.utils;
describe("A headlines box", function () {
it("will not open nor display non-headline messages", mock.initConverse((done, _converse) => {
it("will not open nor display non-headline messages",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {}, async function (done, _converse) {
/* XMPP spam message:
*
* <message xmlns="jabber:client"
......@@ -23,7 +26,7 @@
* <body>SORRY FOR THIS ADVERT</body
* </message
*/
sinon.spy(utils, 'isHeadlineMessage');
sinon.spy(u, 'isHeadlineMessage');
const stanza = $msg({
'xmlns': 'jabber:client',
'to': 'romeo@montague.lit',
......@@ -33,9 +36,10 @@
.c('nick', {'xmlns': "http://jabber.org/protocol/nick"}).t("-wwdmz").up()
.c('body').t('SORRY FOR THIS ADVERT');
_converse.connection._dataRecv(test_utils.createRequest(stanza));
expect(utils.isHeadlineMessage.called).toBeTruthy();
expect(utils.isHeadlineMessage.returned(false)).toBeTruthy();
utils.isHeadlineMessage.restore();
await u.waitUntil(() => _converse.api.chats.get().length);
expect(u.isHeadlineMessage.called).toBeTruthy();
expect(u.isHeadlineMessage.returned(false)).toBeTruthy();
u.isHeadlineMessage.restore();
done();
}));
......@@ -55,7 +59,7 @@
* </x>
* </message>
*/
sinon.spy(utils, 'isHeadlineMessage');
sinon.spy(u, 'isHeadlineMessage');
const stanza = $msg({
'type': 'headline',
'from': 'notify.example.com',
......@@ -73,9 +77,9 @@
_converse.chatboxviews.keys(),
'notify.example.com')
).toBeTruthy();
expect(utils.isHeadlineMessage.called).toBeTruthy();
expect(utils.isHeadlineMessage.returned(true)).toBeTruthy();
utils.isHeadlineMessage.restore(); // unwraps
expect(u.isHeadlineMessage.called).toBeTruthy();
expect(u.isHeadlineMessage.returned(true)).toBeTruthy();
u.isHeadlineMessage.restore(); // unwraps
// Headlines boxes don't show an avatar
const view = _converse.chatboxviews.get('notify.example.com');
expect(view.model.get('show_avatar')).toBeFalsy();
......@@ -84,11 +88,12 @@
}));
it("will not show a headline messages from a full JID if allow_non_roster_messaging is false",
mock.initConverse((done, _converse) => {
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {}, function (done, _converse) {
_converse.allow_non_roster_messaging = false;
sinon.spy(utils, 'isHeadlineMessage');
var stanza = $msg({
sinon.spy(u, 'isHeadlineMessage');
const stanza = $msg({
'type': 'headline',
'from': 'andre5114@jabber.snc.ru/Spark',
'to': 'romeo@montague.lit',
......@@ -98,9 +103,9 @@
.c('body').t('Здравствуйте друзья');
_converse.connection._dataRecv(test_utils.createRequest(stanza));
expect(_.without('controlbox', _converse.chatboxviews.keys()).length).toBe(0);
expect(utils.isHeadlineMessage.called).toBeTruthy();
expect(utils.isHeadlineMessage.returned(true)).toBeTruthy();
utils.isHeadlineMessage.restore(); // unwraps
expect(u.isHeadlineMessage.called).toBeTruthy();
expect(u.isHeadlineMessage.returned(true)).toBeTruthy();
u.isHeadlineMessage.restore(); // unwraps
done();
}));
});
......
......@@ -176,7 +176,7 @@
async function (done, _converse) {
await test_utils.waitForRoster(_converse, 'current', 3);
test_utils.openControlBox();
test_utils.openControlBox(_converse);
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await test_utils.openChatBoxFor(_converse, contact_jid);
await test_utils.waitUntilDiscoConfirmed(
......@@ -222,8 +222,7 @@
await test_utils.waitUntilDiscoConfirmed(_converse, _converse.domain, [], [], ['upload.montague.lit'], 'items')
await test_utils.waitUntilDiscoConfirmed(_converse, 'upload.montague.lit', [], [Strophe.NS.HTTPUPLOAD], []);
test_utils.createContacts(_converse, 'current', 3);
_converse.api.trigger('rosterContactsFetched');
await test_utils.waitForRoster(_converse, 'current', 3);
const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await test_utils.openChatBoxFor(_converse, contact_jid);
const view = _converse.chatboxviews.get(contact_jid);
......@@ -264,8 +263,7 @@
await test_utils.waitUntilDiscoConfirmed(_converse, _converse.domain, [], [], ['upload.montague.tld'], 'items');
await test_utils.waitUntilDiscoConfirmed(_converse, 'upload.montague.tld', [], [Strophe.NS.HTTPUPLOAD], []);
test_utils.createContacts(_converse, 'current');
_converse.api.trigger('rosterContactsFetched');
await test_utils.waitForRoster(_converse, 'current');
const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await test_utils.openChatBoxFor(_converse, contact_jid);
const view = _converse.chatboxviews.get(contact_jid);
......@@ -561,8 +559,7 @@
entities = await _converse.api.disco.entities.get();
expect(entities.get('montague.lit').items.get('upload.montague.lit').identities.where({'category': 'store'}).length).toBe(1);
await _converse.api.disco.supports(Strophe.NS.HTTPUPLOAD, _converse.domain);
test_utils.createContacts(_converse, 'current');
_converse.api.trigger('rosterContactsFetched');
await test_utils.waitForRoster(_converse, 'current');
const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await test_utils.openChatBoxFor(_converse, contact_jid);
......@@ -599,8 +596,7 @@
await test_utils.waitUntilDiscoConfirmed(_converse, _converse.domain, [], [], ['upload.montague.tld'], 'items');
await test_utils.waitUntilDiscoConfirmed(_converse, 'upload.montague.tld', [], [Strophe.NS.HTTPUPLOAD], []);
test_utils.createContacts(_converse, 'current');
_converse.api.trigger('rosterContactsFetched');
await test_utils.waitForRoster(_converse, 'current');
const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await test_utils.openChatBoxFor(_converse, contact_jid);
const view = _converse.chatboxviews.get(contact_jid);
......
......@@ -13,7 +13,7 @@
allow_registration: false },
async function (done, _converse) {
test_utils.openControlBox();
test_utils.openControlBox(_converse);
const cbview = await u.waitUntil(() => _converse.chatboxviews.get('controlbox'));
const checkboxes = cbview.el.querySelectorAll('input[type="checkbox"]');
expect(checkboxes.length).toBe(1);
......@@ -51,7 +51,7 @@
u.waitUntil(() => _converse.chatboxviews.get('controlbox'))
.then(() => {
var cbview = _converse.chatboxviews.get('controlbox');
test_utils.openControlBox();
test_utils.openControlBox(_converse);
const checkboxes = cbview.el.querySelectorAll('input[type="checkbox"]');
expect(checkboxes.length).toBe(1);
......
This diff is collapsed.
......@@ -13,11 +13,8 @@
['rosterGroupsFetched'], {},
async function (done, _converse) {
test_utils.createContacts(_converse, 'current');
_converse.api.trigger('rosterContactsFetched');
test_utils.openControlBox();
_converse.minimized_chats.toggleview.model.browserStorage._clear();
await test_utils.waitForRoster(_converse, 'current');
await test_utils.openControlBox(_converse);
_converse.minimized_chats.initToggle();
let contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
......@@ -48,11 +45,8 @@
['rosterGroupsFetched'], {},
async function (done, _converse) {
test_utils.createContacts(_converse, 'current');
_converse.api.trigger('rosterContactsFetched');
test_utils.openControlBox();
_converse.minimized_chats.toggleview.model.browserStorage._clear();
await test_utils.waitForRoster(_converse, 'current');
await test_utils.openControlBox(_converse);
_converse.minimized_chats.initToggle();
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
......@@ -74,13 +68,10 @@
it("shows the number messages received to minimized chats",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
function (done, _converse) {
test_utils.createContacts(_converse, 'current');
_converse.api.trigger('rosterContactsFetched');
async function (done, _converse) {
test_utils.openControlBox();
_converse.minimized_chats.toggleview.model.browserStorage._clear();
await test_utils.waitForRoster(_converse, 'current');
await test_utils.openControlBox(_converse);
_converse.minimized_chats.initToggle();
var i, contact_jid, chatview, msg;
......
This diff is collapsed.
......@@ -65,7 +65,6 @@
type: 'groupchat'
}).c('body').t(message).tree();
await view.model.onMessage(msg);
await new Promise(resolve => view.once('messageInserted', resolve));
expect(u.hasClass('mentioned', view.el.querySelector('.chat-msg'))).toBeTruthy();
done();
}));
......@@ -87,8 +86,7 @@
type: 'groupchat'
}).c('body').t('First message').tree();
await view.model.onMessage(msg);
await new Promise(resolve => view.once('messageInserted', resolve));
expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
msg = $msg({
from: 'lounge@montague.lit/some2',
......@@ -97,8 +95,8 @@
type: 'groupchat'
}).c('body').t('Another message').tree();
await view.model.onMessage(msg);
await new Promise(resolve => view.once('messageInserted', resolve));
expect(view.el.querySelectorAll('.chat-msg').length).toBe(2);
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 2);
expect(view.model.messages.length).toBe(2);
done();
}));
......@@ -154,7 +152,7 @@
async function (done, _converse) {
await test_utils.waitForRoster(_converse, 'current');
test_utils.openControlBox();
await test_utils.openControlBox(_converse);
const muc_jid = 'xsf@muc.xmpp.org';
const sender_jid = `${muc_jid}/romeo`;
const impersonated_jid = `${muc_jid}/i_am_groot`
......@@ -223,7 +221,6 @@
type: 'groupchat'
}).c('body').t('I wrote this message!').tree();
await view.model.onMessage(msg);
await new Promise(resolve => view.once('messageInserted', resolve));
expect(view.model.messages.last().occupant.get('affiliation')).toBe('owner');
expect(view.model.messages.last().occupant.get('role')).toBe('moderator');
expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
......@@ -250,7 +247,6 @@
type: 'groupchat'
}).c('body').t('Another message!').tree();
await view.model.onMessage(msg);
await new Promise(resolve => view.once('messageInserted', resolve));
expect(view.model.messages.last().occupant.get('affiliation')).toBe('member');
expect(view.model.messages.last().occupant.get('role')).toBe('participant');
expect(view.el.querySelectorAll('.chat-msg').length).toBe(2);
......@@ -286,7 +282,6 @@
type: 'groupchat'
}).c('body').t('Message from someone not in the MUC right now').tree();
await view.model.onMessage(msg);
await new Promise(resolve => view.once('messageInserted', resolve));
expect(view.model.messages.last().occupant).toBeUndefined();
// Check that there's a new "add" event handler, for when the occupant appears.
expect(view.model.occupants._events.add.length).toBe(add_events+1);
......@@ -351,7 +346,6 @@
type: 'groupchat'
}).c('body').t('I wrote this message!').tree();
await view.model.onMessage(msg);
await new Promise(resolve => view.once('messageInserted', resolve));
expect(view.model.messages.last().get('sender')).toBe('me');
done();
}));
......@@ -382,7 +376,6 @@
'type': 'groupchat',
'id': msg_id,
}).c('body').t('But soft, what light through yonder airlock breaks?').tree());
await new Promise(resolve => view.once('messageInserted', resolve));
expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
expect(view.el.querySelector('.chat-msg__text').textContent)
.toBe('But soft, what light through yonder airlock breaks?');
......@@ -445,8 +438,7 @@
preventDefault: function preventDefault () {},
keyCode: 13 // Enter
});
await new Promise(resolve => view.once('messageInserted', resolve));
expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
expect(view.el.querySelector('.chat-msg__text').textContent)
.toBe('But soft, what light through yonder airlock breaks?');
......@@ -503,7 +495,6 @@
'to': 'romeo@montague.lit',
'type': 'groupchat'
}).c('body').t('Hello world').tree());
await new Promise(resolve => view.once('messageInserted', resolve));
expect(view.el.querySelectorAll('.chat-msg').length).toBe(2);
// Test that pressing the down arrow cancels message correction
......@@ -744,7 +735,6 @@
.c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'11', 'end':'14', 'type':'mention', 'uri':'xmpp:romeo@montague.lit'}).up()
.c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'15', 'end':'23', 'type':'mention', 'uri':'xmpp:mr.robot@montague.lit'}).nodeTree;
await view.model.onMessage(msg);
await new Promise(resolve => view.once('messageInserted', resolve));
const messages = view.el.querySelectorAll('.chat-msg__text');
expect(messages.length).toBe(1);
expect(messages[0].classList.length).toEqual(1);
......
......@@ -40,7 +40,7 @@
it("is shown when you are mentioned in a groupchat",
mock.initConverse(['rosterGroupsFetched'], {}, async (done, _converse) => {
await test_utils.createContacts(_converse, 'current');
await test_utils.waitForRoster(_converse, 'current');
await test_utils.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
const view = _converse.api.chatviews.get('lounge@montague.lit');
if (!view.el.querySelectorAll('.chat-area').length) {
......@@ -66,10 +66,10 @@
to: 'romeo@montague.lit',
type: 'groupchat'
}).c('body').t(message).tree();
_converse.connection._dataRecv(test_utils.createRequest(msg));
await new Promise(resolve => view.once('messageInserted', resolve));
expect(_converse.areDesktopNotificationsEnabled).toHaveBeenCalled();
await u.waitUntil(() => _converse.areDesktopNotificationsEnabled.calls.count() === 1);
expect(_converse.showMessageNotification).toHaveBeenCalled();
if (no_notification) {
delete window.Notification;
......@@ -128,16 +128,16 @@
done();
}));
it("is shown when a user changes their chat state (if show_chat_state_notifications is true)", mock.initConverse((done, _converse) => {
// TODO: not yet testing show_desktop_notifications setting
_converse.show_chat_state_notifications = true;
it("is shown when a user changes their chat state (if show_chat_state_notifications is true)",
mock.initConverse(['rosterGroupsFetched'], {show_chat_state_notifications: true},
async (done, _converse) => {
test_utils.createContacts(_converse, 'current');
await test_utils.waitForRoster(_converse, 'current', 3);
spyOn(_converse, 'areDesktopNotificationsEnabled').and.returnValue(true);
spyOn(_converse, 'showChatStateNotification');
const jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit';
_converse.roster.get(jid).presence.set('show', 'busy'); // This will emit 'contactStatusChanged'
expect(_converse.areDesktopNotificationsEnabled).toHaveBeenCalled();
await u.waitUntil(() => _converse.areDesktopNotificationsEnabled.calls.count() === 1);
expect(_converse.showChatStateNotification).toHaveBeenCalled();
done()
}));
......
......@@ -81,8 +81,7 @@
async function (done, _converse) {
const message = 'This message will be encrypted'
test_utils.createContacts(_converse, 'current', 1);
_converse.api.trigger('rosterContactsFetched');
await test_utils.waitForRoster(_converse, 'current', 1);
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
const view = await test_utils.openChatBoxFor(_converse, contact_jid);
const payload = await view.model.encryptMessage(message);
......@@ -98,8 +97,7 @@
async function (done, _converse) {
let sent_stanza;
test_utils.createContacts(_converse, 'current', 1);
_converse.api.trigger('rosterContactsFetched');
await test_utils.waitForRoster(_converse, 'current', 1);
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await u.waitUntil(() => initializedOMEMO(_converse));
await test_utils.openChatBoxFor(_converse, contact_jid);
......@@ -386,8 +384,7 @@
await test_utils.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, [], [Strophe.NS.SID]);
test_utils.createContacts(_converse, 'current', 1);
_converse.api.trigger('rosterContactsFetched');
await test_utils.waitForRoster(_converse, 'current', 1);
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await u.waitUntil(() => initializedOMEMO(_converse));
await test_utils.openChatBoxFor(_converse, contact_jid);
......@@ -605,8 +602,7 @@
async function (done, _converse) {
_converse.NUM_PREKEYS = 5; // Restrict to 5, otherwise the resulting stanza is too large to easily test
test_utils.createContacts(_converse, 'current', 1);
_converse.api.trigger('rosterContactsFetched');
await test_utils.waitForRoster(_converse, 'current', 1);
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await u.waitUntil(() => initializedOMEMO(_converse));
......@@ -710,7 +706,7 @@
['http://jabber.org/protocol/pubsub#publish-options']
);
test_utils.createContacts(_converse, 'current', 1);
await test_utils.waitForRoster(_converse, 'current', 1);
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
// Wait until own devices are fetched
......@@ -884,7 +880,7 @@
['http://jabber.org/protocol/pubsub#publish-options']
);
test_utils.createContacts(_converse, 'current');
await test_utils.waitForRoster(_converse, 'current');
const contact_jid = mock.cur_names[3].replace(/ /g,'.').toLowerCase() + '@montague.lit';
let iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, _converse.bare_jid));
expect(Strophe.serialize(iq_stanza)).toBe(
......@@ -1036,8 +1032,7 @@
_converse.NUM_PREKEYS = 2; // Restrict to 2, otherwise the resulting stanza is too large to easily test
test_utils.createContacts(_converse, 'current', 1);
_converse.api.trigger('rosterContactsFetched');
await test_utils.waitForRoster(_converse, 'current', 1);
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
let iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, _converse.bare_jid));
let stanza = $iq({
......@@ -1113,8 +1108,7 @@
['http://jabber.org/protocol/pubsub#publish-options']
);
test_utils.createContacts(_converse, 'current', 1);
_converse.api.trigger('rosterContactsFetched');
await test_utils.waitForRoster(_converse, 'current', 1);
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
let iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, _converse.bare_jid));
......@@ -1460,8 +1454,7 @@
['http://jabber.org/protocol/pubsub#publish-options']
);
test_utils.createContacts(_converse, 'current', 1);
_converse.api.trigger('rosterContactsFetched');
await test_utils.waitForRoster(_converse, 'current', 1);
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await test_utils.openChatBoxFor(_converse, contact_jid)
// We simply emit, to avoid doing all the setup work
......
......@@ -74,7 +74,7 @@
['rosterGroupsFetched'], {},
async (done, _converse) => {
test_utils.openControlBox();
test_utils.openControlBox(_converse);
const view = _converse.xmppstatusview;
spyOn(view.model, 'sendPresence').and.callThrough();
spyOn(_converse.connection, 'send').and.callThrough();
......@@ -112,12 +112,12 @@
it("has its priority taken into account",
mock.initConverse(
['rosterGroupsFetched'], {},
(done, _converse) => {
async (done, _converse) => {
test_utils.openControlBox();
test_utils.createContacts(_converse, 'current'); // Create some contacts so that we can test positioning
test_utils.openControlBox(_converse);
await test_utils.waitForRoster(_converse, 'current');
const contact_jid = mock.cur_names[8].replace(/ /g,'.').toLowerCase() + '@montague.lit';
const contact = _converse.roster.get(contact_jid);
const contact = await _converse.api.contacts.get(contact_jid);
let stanza = u.toStanza(`
<presence xmlns="jabber:client"
to="romeo@montague.lit/converse.js-21770972"
......
......@@ -13,7 +13,7 @@
['rosterGroupsFetched'], {'muc_show_join_leave': false},
async function (done, _converse) {
test_utils.openControlBox();
test_utils.openControlBox(_converse);
await test_utils.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
_.rangeRight(3000, 0).forEach(i => {
const name = `User ${i.toString().padStart(5, '0')}`;
......@@ -41,7 +41,7 @@
function (done, _converse) {
_converse.roster_groups = false;
test_utils.openControlBox();
test_utils.openControlBox(_converse);
expect(_converse.roster.pluck('jid').length).toBe(0);
var stanza = $iq({
......@@ -86,7 +86,7 @@
// _converse.show_only_online_users = true;
_converse.roster_groups = true;
test_utils.openControlBox();
test_utils.openControlBox(_converse);
expect(_converse.roster.pluck('jid').length).toBe(0);
var stanza = $iq({
......
......@@ -5,9 +5,9 @@
"test-utils"], factory);
} (this, function (jasmine, mock, test_utils) {
"use strict";
const Strophe = converse.env.Strophe;
const $iq = converse.env.$iq;
const $pres = converse.env.$pres;
const Strophe = converse.env.Strophe;
const _ = converse.env._;
const sizzle = converse.env.sizzle;
const u = converse.env.utils;
......@@ -175,7 +175,7 @@
*/
const sent_presence = await u.waitUntil(() => sent_stanzas.filter(s => s.match('presence')).pop());
expect(contact.subscribe).toHaveBeenCalled();
expect(sent_presence).toBe( // Strophe adds the xmlns attr (although not in spec)
expect(sent_presence).toBe(
`<presence to="contact@example.org" type="subscribe" xmlns="jabber:client">`+
`<nick xmlns="http://jabber.org/protocol/nick">Romeo Montague</nick>`+
`</presence>`
......@@ -455,18 +455,13 @@
{ roster_groups: false },
async function (done, _converse) {
var sent_IQ, IQ_id, jid = 'abram@montague.lit';
test_utils.openControlBox(_converse);
test_utils.createContacts(_converse, 'current');
const jid = 'abram@montague.lit';
await test_utils.openControlBox(_converse);
await test_utils.waitForRoster(_converse, 'current');
spyOn(window, 'confirm').and.returnValue(true);
// We now have a contact we want to remove
expect(_converse.roster.get(jid) instanceof _converse.RosterContact).toBeTruthy();
var sendIQ = _converse.connection.sendIQ;
spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
sent_IQ = iq;
IQ_id = sendIQ.bind(this)(iq, callback, errback);
});
const header = sizzle('a:contains("My contacts")', _converse.rosterview.el).pop();
await u.waitUntil(() => header.parentElement.querySelectorAll('li').length);
......@@ -493,8 +488,9 @@
* </query>
* </iq>
*/
expect(sent_IQ.toLocaleString()).toBe(
`<iq id="${IQ_id}" type="set" xmlns="jabber:client">`+
const sent_iq = _converse.connection.IQ_stanzas.pop();
expect(Strophe.serialize(sent_iq)).toBe(
`<iq id="${sent_iq.getAttribute('id')}" type="set" xmlns="jabber:client">`+
`<query xmlns="jabber:iq:roster">`+
`<item jid="abram@montague.lit" subscription="remove"/>`+
`</query>`+
......@@ -502,7 +498,7 @@
// Receive confirmation from the contact's server
// <iq type='result' id='remove1'/>
const stanza = $iq({'type': 'result', 'id':IQ_id});
const stanza = $iq({'type': 'result', 'id': sent_iq.getAttribute('id')});
_converse.connection._dataRecv(test_utils.createRequest(stanza));
// Our contact has now been removed
await u.waitUntil(() => typeof _converse.roster.get(jid) === "undefined");
......@@ -514,9 +510,8 @@
async function (done, _converse) {
spyOn(_converse.api, "trigger").and.callThrough();
test_utils.openControlBox(_converse);
// Create some contacts so that we can test positioning
test_utils.createContacts(_converse, 'current');
await test_utils.openControlBox(_converse);
await test_utils.waitForRoster(_converse, 'current');
/* <presence
* from='user@example.com'
* to='contact@example.org'
......@@ -534,7 +529,7 @@
const header = sizzle('a:contains("Contact requests")', _converse.rosterview.el).pop();
const contacts = _.filter(header.parentElement.querySelectorAll('li'), u.isVisible);
return contacts.length;
}, 300);
}, 500);
expect(_converse.api.trigger).toHaveBeenCalledWith('contactRequest', jasmine.any(Object));
const header = sizzle('a:contains("Contact requests")', _converse.rosterview.el).pop();
expect(u.isVisible(header)).toBe(true);
......
......@@ -16,8 +16,7 @@
async function (done, _converse) {
await u.waitUntil(() => _converse.chatboxviews.get('controlbox'));
test_utils.openControlBox();
const cbview = _converse.chatboxviews.get('controlbox');
const cbview = _converse.api.controlbox.get();
expect(cbview.el.querySelectorAll('a.register-account').length).toBe(0);
done();
}));
......@@ -29,15 +28,22 @@
allow_registration: true },
async function (done, _converse) {
const toggle = document.querySelector(".toggle-controlbox");
if (!u.isVisible(document.querySelector("#controlbox"))) {
if (!u.isVisible(toggle)) {
u.removeClass('hidden', toggle);
}
toggle.click();
}
await u.waitUntil(() => _.get(_converse.chatboxviews.get('controlbox'), 'registerpanel'), 300);
const cbview = _converse.chatboxviews.get('controlbox');
test_utils.openControlBox();
const panels = cbview.el.querySelector('.controlbox-panes');
const login = panels.firstElementChild;
const registration = panels.childNodes[1];
const register_link = cbview.el.querySelector('a.register-account');
expect(register_link.textContent).toBe("Create an account");
register_link.click();
await u.waitUntil(() => u.isVisible(registration));
expect(u.isVisible(login)).toBe(false);
done();
......@@ -52,8 +58,7 @@
spyOn(Strophe.Connection.prototype, 'connect');
await u.waitUntil(() => _.get(_converse.chatboxviews.get('controlbox'), 'registerpanel'));
test_utils.openControlBox();
const cbview = _converse.chatboxviews.get('controlbox');
const cbview = _converse.api.controlbox.get();
const registerview = cbview.registerpanel;
spyOn(registerview, 'onProviderChosen').and.callThrough();
registerview.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
......@@ -88,8 +93,7 @@
spyOn(Strophe.Connection.prototype, 'connect');
await u.waitUntil(() => _.get(_converse.chatboxviews.get('controlbox'), 'registerpanel'));
test_utils.openControlBox();
const cbview = _converse.chatboxviews.get('controlbox');
const cbview = _converse.api.controlbox.get();
cbview.el.querySelector('.toggle-register-login').click();
const registerview = _converse.chatboxviews.get('controlbox').registerpanel;
......@@ -100,7 +104,7 @@
registerview.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
expect(registerview._registering).toBeFalsy();
expect(_converse.connection.connected).toBeFalsy();
expect(_converse.api.connection.connected()).toBeFalsy();
registerview.el.querySelector('input[name=domain]').value = 'conversejs.org';
registerview.el.querySelector('input[type=submit]').click();
expect(registerview.onProviderChosen).toHaveBeenCalled();
......@@ -143,8 +147,14 @@
async function (done, _converse) {
await u.waitUntil(() => _.get(_converse.chatboxviews.get('controlbox'), 'registerpanel'));
test_utils.openControlBox();
const cbview = _converse.chatboxviews.get('controlbox');
const toggle = document.querySelector(".toggle-controlbox");
if (!u.isVisible(document.querySelector("#controlbox"))) {
if (!u.isVisible(toggle)) {
u.removeClass('hidden', toggle);
}
toggle.click();
}
const cbview = _converse.api.controlbox.get();
cbview.el.querySelector('.toggle-register-login').click();
const registerview = cbview.registerpanel;
......@@ -153,7 +163,6 @@
spyOn(registerview, 'onRegistrationFields').and.callThrough();
spyOn(registerview, 'renderRegistrationForm').and.callThrough();
registerview.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
spyOn(_converse.connection, 'connect').and.callThrough();
registerview.el.querySelector('input[name=domain]').value = 'conversejs.org';
registerview.el.querySelector('input[type=submit]').click();
......@@ -200,8 +209,14 @@
async function (done, _converse) {
await u.waitUntil(() => _.get(_converse.chatboxviews.get('controlbox'), 'registerpanel'));
test_utils.openControlBox();
const cbview = _converse.chatboxviews.get('controlbox');
const toggle = document.querySelector(".toggle-controlbox");
if (!u.isVisible(document.querySelector("#controlbox"))) {
if (!u.isVisible(toggle)) {
u.removeClass('hidden', toggle);
}
toggle.click();
}
const cbview = _converse.api.controlbox.get();
cbview.el.querySelector('.toggle-register-login').click();
const registerview = _converse.chatboxviews.get('controlbox').registerpanel;
spyOn(registerview, 'onProviderChosen').and.callThrough();
......@@ -209,7 +224,6 @@
spyOn(registerview, 'onRegistrationFields').and.callThrough();
spyOn(registerview, 'renderRegistrationForm').and.callThrough();
registerview.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
spyOn(_converse.connection, 'connect').and.callThrough();
registerview.el.querySelector('input[name=domain]').value = 'conversejs.org';
registerview.el.querySelector('input[type=submit]').click();
......@@ -274,7 +288,13 @@
async function (done, _converse) {
await u.waitUntil(() => _.get(_converse.chatboxviews.get('controlbox'), 'registerpanel'));
test_utils.openControlBox();
const toggle = document.querySelector(".toggle-controlbox");
if (!u.isVisible(document.querySelector("#controlbox"))) {
if (!u.isVisible(toggle)) {
u.removeClass('hidden', toggle);
}
toggle.click();
}
const cbview = _converse.chatboxviews.get('controlbox');
cbview.el.querySelector('.toggle-register-login').click();
const registerview = _converse.chatboxviews.get('controlbox').registerpanel;
......
......@@ -13,7 +13,7 @@
// have to mock stanza traffic.
}, async function (done, _converse) {
test_utils.openControlBox();
await test_utils.openControlBox(_converse);
const controlbox = _converse.chatboxviews.get('controlbox');
let list = controlbox.el.querySelector('.list-container--openrooms');
expect(_.includes(list.classList, 'hidden')).toBeTruthy();
......@@ -31,7 +31,7 @@
expect(room_els.length).toBe(2);
let view = _converse.chatboxviews.get('room@conference.shakespeare.lit');
view.close();
await view.close();
room_els = _converse.rooms_list_view.el.querySelectorAll(".open-room");
expect(room_els.length).toBe(1);
expect(room_els[0].innerText).toBe('lounge@montague.lit');
......@@ -39,7 +39,7 @@
u.waitUntil(() => _.includes(list.classList, 'hidden'));
view = _converse.chatboxviews.get('lounge@montague.lit');
view.close();
await view.close();
room_els = _converse.rooms_list_view.el.querySelectorAll(".open-room");
expect(room_els.length).toBe(0);
......@@ -110,19 +110,21 @@
describe("A groupchat shown in the groupchats list", function () {
it("is highlighted if it's currently open", mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched', 'emojisInitialized'],
{ view_mode: 'fullscreen',
allow_bookmarks: false // Makes testing easier, otherwise we have to mock stanza traffic.
}, async function (done, _converse) {
['rosterGroupsFetched', 'chatBoxesFetched', 'emojisInitialized'],
{ view_mode: 'fullscreen',
allow_bookmarks: false // Makes testing easier, otherwise we have to mock stanza traffic.
}, async function (done, _converse) {
await _converse.api.rooms.open('coven@chat.shakespeare.lit', {'nick': 'some1'});
const muc_jid = 'coven@chat.shakespeare.lit';
await _converse.api.rooms.open(muc_jid, {'nick': 'some1'});
const lview = _converse.rooms_list_view
await u.waitUntil(() => lview.el.querySelectorAll(".open-room").length);
let room_els = lview.el.querySelectorAll(".available-chatroom");
expect(room_els.length).toBe(1);
let item = room_els[0];
expect(u.hasClass('open', item)).toBe(true);
await u.waitUntil(() => lview.model.get(muc_jid).get('hidden') === false);
await u.waitUntil(() => u.hasClass('open', item), 1000);
expect(item.textContent.trim()).toBe('coven@chat.shakespeare.lit');
await _converse.api.rooms.open('balcony@chat.shakespeare.lit', {'nick': 'some1'});
await u.waitUntil(() => lview.el.querySelectorAll(".open-room").length > 1);
......@@ -137,15 +139,15 @@
}));
it("has an info icon which opens a details modal when clicked", mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched', 'emojisInitialized'],
{ whitelisted_plugins: ['converse-roomslist'],
allow_bookmarks: false // Makes testing easier, otherwise we
// have to mock stanza traffic.
}, async function (done, _converse) {
['rosterGroupsFetched', 'chatBoxesFetched', 'emojisInitialized'],
{ whitelisted_plugins: ['converse-roomslist'],
allow_bookmarks: false // Makes testing easier, otherwise we
// have to mock stanza traffic.
}, async function (done, _converse) {
const IQ_stanzas = _converse.connection.IQ_stanzas;
const room_jid = 'coven@chat.shakespeare.lit';
test_utils.openControlBox();
await test_utils.openControlBox(_converse);
await _converse.api.rooms.open(room_jid, {'nick': 'some1'});
const view = _converse.chatboxviews.get(room_jid);
......@@ -242,11 +244,11 @@
}));
it("can be closed", mock.initConverse(
['rosterGroupsFetched', 'emojisInitialized'],
{ whitelisted_plugins: ['converse-roomslist'],
allow_bookmarks: false // Makes testing easier, otherwise we have to mock stanza traffic.
},
async function (done, _converse) {
['rosterGroupsFetched', 'emojisInitialized'],
{ whitelisted_plugins: ['converse-roomslist'],
allow_bookmarks: false // Makes testing easier, otherwise we have to mock stanza traffic.
},
async function (done, _converse) {
spyOn(window, 'confirm').and.callFake(() => true);
expect(_converse.chatboxes.length).toBe(1);
......@@ -260,6 +262,8 @@
close_el.click();
expect(window.confirm).toHaveBeenCalledWith(
'Are you sure you want to leave the groupchat lounge@conference.shakespeare.lit?');
await u.waitUntil(() => !_converse.api.rooms.get().length);
room_els = _converse.rooms_list_view.el.querySelectorAll(".open-room");
expect(room_els.length).toBe(0);
expect(_converse.chatboxes.length).toBe(1);
......@@ -272,7 +276,7 @@
allow_bookmarks: false // Makes testing easier, otherwise we have to mock stanza traffic.
}, async (done, _converse) => {
test_utils.openControlBox();
await test_utils.openControlBox(_converse);
const room_jid = 'kitchen@conference.shakespeare.lit';
await u.waitUntil(() => _converse.rooms_list_view !== undefined, 500);
await test_utils.openAndEnterChatRoom(_converse, 'kitchen@conference.shakespeare.lit', 'romeo');
......@@ -287,12 +291,10 @@
type: 'groupchat'
}).c('body').t('foo').tree());
const lview = _converse.rooms_list_view
await u.waitUntil(() => lview.el.querySelectorAll(".available-chatroom").length, 500);
// If the user isn't mentioned, the counter doesn't get incremented, but the text of the groupchat is bold
let room_el = lview.el.querySelector(".available-chatroom");
expect(_.includes(room_el.classList, 'unread-msgs')).toBeTruthy();
const lview = _converse.rooms_list_view
let room_el = await u.waitUntil(() => lview.el.querySelector(".available-chatroom"));
expect(Array.from(room_el.classList).includes('unread-msgs')).toBeTruthy();
// If the user is mentioned, the counter also gets updated
await view.model.onMessage(
......@@ -303,10 +305,11 @@
type: 'groupchat'
}).c('body').t('romeo: Your attention is required').tree()
);
await u.waitUntil(() => _converse.rooms_list_view.el.querySelectorAll(".msgs-indicator").length);
spyOn(view.model, 'incrementUnreadMsgCounter').and.callThrough();
let indicator_el = _converse.rooms_list_view.el.querySelector(".msgs-indicator");
let indicator_el = await u.waitUntil(() => lview.el.querySelector(".msgs-indicator"));
expect(indicator_el.textContent).toBe('1');
spyOn(view.model, 'incrementUnreadMsgCounter').and.callThrough();
await view.model.onMessage(
$msg({
from: room_jid+'/'+nick,
......@@ -316,14 +319,13 @@
}).c('body').t('romeo: and another thing...').tree()
);
await u.waitUntil(() => view.model.incrementUnreadMsgCounter.calls.count());
indicator_el = _converse.rooms_list_view.el.querySelector(".msgs-indicator");
expect(indicator_el.textContent).toBe('2');
await u.waitUntil(() => lview.el.querySelector(".msgs-indicator").textContent === '2', 1000);
// When the chat gets maximized again, the unread indicators are removed
view.model.set({'minimized': false});
indicator_el = _converse.rooms_list_view.el.querySelector(".msgs-indicator");
indicator_el = lview.el.querySelector(".msgs-indicator");
expect(indicator_el === null);
room_el = _converse.rooms_list_view.el.querySelector(".available-chatroom");
room_el = lview.el.querySelector(".available-chatroom");
expect(_.includes(room_el.classList, 'unread-msgs')).toBeFalsy();
done();
}));
......
This diff is collapsed.
......@@ -39,18 +39,12 @@
let IQ_stanzas = _converse.connection.IQ_stanzas;
await u.waitUntil(() => IQ_stanzas.length === 4);
let iq = IQ_stanzas.pop();
expect(Strophe.serialize(iq)).toBe(
`<iq from="romeo@montague.lit/orchard" id="${iq.getAttribute('id')}" to="romeo@montague.lit" type="get" xmlns="jabber:client">`+
`<query xmlns="http://jabber.org/protocol/disco#info"/></iq>`);
iq = IQ_stanzas[IQ_stanzas.length-1];
let iq = IQ_stanzas[IQ_stanzas.length-1];
expect(Strophe.serialize(iq)).toBe(
`<iq id="${iq.getAttribute('id')}" type="get" xmlns="jabber:client"><query xmlns="jabber:iq:roster"/></iq>`);
await test_utils.waitForRoster(_converse, 'current', 1);
IQ_stanzas.pop();
const disco_iq = IQ_stanzas.pop();
expect(Strophe.serialize(disco_iq)).toBe(
`<iq from="romeo@montague.lit" id="${disco_iq.getAttribute('id')}" to="romeo@montague.lit" type="get" xmlns="jabber:client">`+
......@@ -58,9 +52,13 @@
iq = IQ_stanzas.pop();
expect(Strophe.serialize(iq)).toBe(
`<iq from="romeo@montague.lit/orchard" id="${iq.getAttribute('id')}" to="montague.lit" type="get" xmlns="jabber:client">`+
`<iq from="romeo@montague.lit/orchard" id="${iq.getAttribute('id')}" to="romeo@montague.lit" type="get" xmlns="jabber:client">`+
`<query xmlns="http://jabber.org/protocol/disco#info"/></iq>`);
iq = IQ_stanzas.pop();
expect(Strophe.serialize(iq)).toBe(
`<iq from="romeo@montague.lit/orchard" id="${iq.getAttribute('id')}" to="montague.lit" type="get" xmlns="jabber:client">`+
`<query xmlns="http://jabber.org/protocol/disco#info"/></iq>`);
expect(sent_stanzas.filter(s => (s.nodeName === 'r')).length).toBe(2);
expect(_converse.session.get('unacked_stanzas').length).toBe(5);
......@@ -104,7 +102,7 @@
// test session resumption
_converse.connection.IQ_stanzas = [];
IQ_stanzas = _converse.connection.IQ_stanzas;
_converse.api.connection.reconnect();
await _converse.api.connection.reconnect();
stanza = await u.waitUntil(() => sent_stanzas.filter(s => (s.tagName === 'resume')).pop());
expect(Strophe.serialize(stanza)).toEqual('<resume h="2" previd="some-long-sm-id" xmlns="urn:xmpp:sm:3"/>');
......@@ -120,9 +118,7 @@
// Test that unacked stanzas get resent out
iq = IQ_stanzas.pop();
expect(Strophe.serialize(iq)).toBe(
`<iq from="romeo@montague.lit/orchard" id="${iq.getAttribute('id')}" to="romeo@montague.lit" type="get" xmlns="jabber:client">`+
`<query xmlns="http://jabber.org/protocol/disco#info"/></iq>`);
expect(Strophe.serialize(iq)).toBe(`<iq id="${iq.getAttribute('id')}" type="get" xmlns="jabber:client"><query xmlns="jabber:iq:roster"/></iq>`);
expect(IQ_stanzas.filter(iq => sizzle('query[xmlns="jabber:iq:roster"]', iq).pop()).length).toBe(0);
......@@ -141,9 +137,6 @@
},
async function (done, _converse) {
const view = _converse.chatboxviews.get('controlbox');
spyOn(view, 'renderControlBoxPane').and.callThrough();
_converse.api.user.login('romeo@montague.lit/orchard', 'secret');
const sent_stanzas = _converse.connection.sent_stanzas;
let stanza = await u.waitUntil(() => sent_stanzas.filter(s => (s.tagName === 'enable')).pop());
......@@ -154,7 +147,7 @@
await test_utils.waitForRoster(_converse, 'current', 1);
// test session resumption
_converse.api.connection.reconnect();
await _converse.api.connection.reconnect();
stanza = await u.waitUntil(() => sent_stanzas.filter(s => (s.tagName === 'resume')).pop());
expect(Strophe.serialize(stanza)).toEqual('<resume h="1" previd="some-long-sm-id" xmlns="urn:xmpp:sm:3"/>');
......
......@@ -35,6 +35,7 @@
}).t(spoiler_hint)
.tree();
await _converse.chatboxes.onMessage(msg);
await u.waitUntil(() => _converse.api.chats.get().length === 2);
const view = _converse.chatboxviews.get(sender_jid);
await new Promise(resolve => view.once('messageInserted', resolve));
await u.waitUntil(() => view.model.vcard.get('fullname') === 'Mercutio')
......@@ -69,6 +70,7 @@
'xmlns': 'urn:xmpp:spoiler:0',
}).tree();
await _converse.chatboxes.onMessage(msg);
await u.waitUntil(() => _converse.api.chats.get().length === 2);
const view = _converse.chatboxviews.get(sender_jid);
await new Promise(resolve => view.once('messageInserted', resolve));
await u.waitUntil(() => view.model.vcard.get('fullname') === 'Mercutio')
......@@ -86,7 +88,7 @@
async (done, _converse) => {
await test_utils.waitForRoster(_converse, 'current', 1);
test_utils.openControlBox();
test_utils.openControlBox(_converse);
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
// XXX: We need to send a presence from the contact, so that we
......@@ -159,7 +161,7 @@
async (done, _converse) => {
await test_utils.waitForRoster(_converse, 'current', 1);
test_utils.openControlBox();
test_utils.openControlBox(_converse);
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
// XXX: We need to send a presence from the contact, so that we
......@@ -173,7 +175,7 @@
_converse.connection._dataRecv(test_utils.createRequest(presence));
await test_utils.openChatBoxFor(_converse, contact_jid);
await test_utils.waitUntilDiscoConfirmed(_converse, contact_jid+'/phone', [], [Strophe.NS.SPOILER]);
const view = _converse.chatboxviews.get(contact_jid);
const view = _converse.api.chatviews.get(contact_jid);
await u.waitUntil(() => view.el.querySelector('.toggle-compose-spoiler'));
let spoiler_toggle = view.el.querySelector('.toggle-compose-spoiler');
......
(function (root, factory) {
define([
"jasmine",
"converse-core",
"mock",
"test-utils",
"utils",
"transcripts"
], factory
);
} (this, function (jasmine, converse, mock, test_utils, utils, transcripts) {
} (this, function (jasmine, mock, test_utils, utils, transcripts) {
var Strophe = converse.env.Strophe;
var _ = converse.env._;
var IGNORED_TAGS = [
......
......@@ -15,11 +15,11 @@
['rosterGroupsFetched', 'chatBoxesFetched', 'emojisInitialized'], {},
async function (done, _converse) {
test_utils.createContacts(_converse, 'current');
await test_utils.waitForRoster(_converse, 'current', 1);
_converse.api.trigger('rosterContactsFetched');
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
test_utils.openChatBoxFor(_converse, contact_jid);
await test_utils.openChatBoxFor(_converse, contact_jid);
await u.waitUntil(() => _converse.chatboxes.length > 1);
const view = _converse.chatboxviews.get(contact_jid);
let show_modal_button = view.el.querySelector('.show-user-details-modal');
......@@ -48,7 +48,7 @@
['rosterGroupsFetched', 'emojisInitialized'], {},
async function (done, _converse) {
test_utils.createContacts(_converse, 'current');
await test_utils.waitForRoster(_converse, 'current', 1);
_converse.api.trigger('rosterContactsFetched');
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
......@@ -60,9 +60,8 @@
const modal = view.user_details_modal;
await u.waitUntil(() => u.isVisible(modal.el), 2000);
spyOn(window, 'confirm').and.returnValue(true);
spyOn(view.model.contact, 'removeFromRoster').and.callFake(function (callback, errback) {
errback();
});
spyOn(view.model.contact, 'removeFromRoster').and.callFake((callback, errback) => errback());
let remove_contact_button = modal.el.querySelector('button.remove-contact');
expect(u.isVisible(remove_contact_button)).toBeTruthy();
remove_contact_button.click();
......
......@@ -445,6 +445,14 @@ converse.plugins.add('converse-chatview', {
this.insertIntoDOM();
this.scrollDown();
this.content.addEventListener('scroll', this.markScrolled.bind(this));
/**
* Triggered whenever a `_converse.ChatBox` instance has fetched its messages from
* `sessionStorage` but **NOT** from the server.
* @event _converse#afterMessagesFetched
* @type {_converse.ChatBoxView | _converse.ChatRoomView}
* @example _converse.api.listen.on('afterMessagesFetched', view => { ... });
*/
_converse.api.trigger('afterMessagesFetched', this);
},
insertIntoDOM () {
......@@ -589,7 +597,7 @@ converse.plugins.add('converse-chatview', {
}
},
showHelpMessages (msgs, type, spinner) {
showHelpMessages (msgs, type='info', spinner) {
msgs.forEach(msg => {
this.content.insertAdjacentHTML(
'beforeend',
......@@ -997,11 +1005,11 @@ converse.plugins.add('converse-chatview', {
}
},
clearMessages (ev) {
async clearMessages (ev) {
if (ev && ev.preventDefault) { ev.preventDefault(); }
const result = confirm(__("Are you sure you want to clear the messages from this conversation?"));
if (result === true) {
this.model.clearMessages();
await this.model.clearMessages();
}
return this;
},
......@@ -1119,7 +1127,7 @@ converse.plugins.add('converse-chatview', {
}
},
close (ev) {
async close (ev) {
if (ev && ev.preventDefault) { ev.preventDefault(); }
if (Backbone.history.getFragment() === "converse/chat?jid="+this.model.get('jid')) {
_converse.router.navigate('');
......@@ -1130,7 +1138,7 @@ converse.plugins.add('converse-chatview', {
this.model.setChatState(_converse.INACTIVE);
this.model.sendChatState();
}
this.model.close();
await this.model.close();
this.remove();
/**
* Triggered once a chatbox has been closed.
......@@ -1247,10 +1255,7 @@ converse.plugins.add('converse-chatview', {
},
viewUnreadMessages () {
this.model.save({
'scrolled': false,
'top_visible_message': null
});
this.model.save({'scrolled': false, 'top_visible_message': null});
this.scrollDown();
},
......@@ -1315,7 +1320,7 @@ converse.plugins.add('converse-chatview', {
* @namespace _converse.api.chatviews
* @memberOf _converse.api
*/
'chatviews': {
chatviews: {
/**
* Get the view of an already open chat.
* @method _converse.api.chatviews.get
......@@ -1329,13 +1334,9 @@ converse.plugins.add('converse-chatview', {
* // To return an array of views, provide an array of JIDs:
* _converse.api.chatviews.get(['buddy1@example.com', 'buddy2@example.com'])
*/
'get' (jids) {
get (jids) {
if (jids === undefined) {
_converse.log(
"chatviews.get: You need to provide at least one JID",
Strophe.LogLevel.ERROR
);
return null;
return Object.values(_converse.chatboxviews.getAll());
}
if (isString(jids)) {
return _converse.chatboxviews.get(jids);
......
......@@ -85,7 +85,7 @@ converse.plugins.add('converse-controlbox', {
ChatBoxes: {
model (attrs, options) {
const { _converse } = this.__super__;
if (attrs.id == 'controlbox') {
if (attrs && attrs.id == 'controlbox') {
return new _converse.ControlBox(attrs, options);
} else {
return this.__super__.model.apply(this, arguments);
......@@ -292,14 +292,24 @@ converse.plugins.add('converse-controlbox', {
)
},
close (ev) {
async close (ev) {
if (ev && ev.preventDefault) { ev.preventDefault(); }
if (get(ev, 'name') === 'closeAllChatBoxes' &&
(_converse.disconnection_cause !== _converse.LOGOUT ||
_converse.show_controlbox_by_default)) {
return;
}
if (_converse.sticky_controlbox) {
return;
}
const connection = get(_converse, 'connection', {});
if (connection.connected && !connection.disconnecting) {
this.model.save({'closed': true});
await new Promise((resolve, reject) => {
return this.model.save(
{'closed': true},
{'success': resolve, 'error': reject}
);
});
} else {
this.model.trigger('hide');
}
......@@ -591,7 +601,7 @@ converse.plugins.add('converse-controlbox', {
_converse.api.listen.on('chatBoxesFetched', () => {
const controlbox = _converse.chatboxes.get('controlbox') || addControlBox();
controlbox.save({connected:true});
controlbox.save({'connected': true});
});
const disconnect = function () {
......
......@@ -49,9 +49,12 @@ converse.plugins.add('converse-dragresize', {
ChatBox: {
initialize () {
const { _converse } = this.__super__;
const result = this.__super__.initialize.apply(this, arguments),
height = this.get('height'), width = this.get('width'),
save = this.get('id') === 'controlbox' ? this.set.bind(this) : this.save.bind(this);
const result = this.__super__.initialize.apply(this, arguments);
const height = this.get('height'), width = this.get('width');
const save = this.get('id') === 'controlbox' ?
(attrs) => this.set(attrs) :
(attrs) => this.save(attrs);
save({
'height': _converse.applyDragResistance(height, this.get('default_height')),
'width': _converse.applyDragResistance(width, this.get('default_width')),
......
......@@ -885,7 +885,6 @@ converse.plugins.add('converse-muc-views', {
*/
if (u.isPersistableModel(this.model)) {
this.model.clearUnreadMsgCounter();
this.model.save();
}
this.scrollDown();
},
......@@ -925,12 +924,12 @@ converse.plugins.add('converse-muc-views', {
* @private
* @method _converse.ChatRoomView#close
*/
close () {
async close () {
this.hide();
if (Backbone.history.getFragment() === "converse/room?jid="+this.model.get('jid')) {
_converse.router.navigate('');
}
this.model.leave();
await this.model.leave();
return _converse.ChatBoxView.prototype.close.apply(this, arguments);
},
......@@ -2128,7 +2127,6 @@ converse.plugins.add('converse-muc-views', {
const view = _converse.chatboxviews.get('controlbox');
if (view && view.roomspanel) {
view.roomspanel.model.destroy();
view.roomspanel.model.browserStorage._clear();
view.roomspanel.remove();
delete view.roomspanel;
}
......@@ -2197,6 +2195,7 @@ converse.plugins.add('converse-muc-views', {
*
* @method _converse.api.roomviews.close
* @param {(String[]|String)} jids The JID or array of JIDs of the chatroom(s)
* @returns { Promise } - Promise which resolves once the views have been closed.
*/
close (jids) {
let views;
......@@ -2207,11 +2206,7 @@ converse.plugins.add('converse-muc-views', {
} else if (Array.isArray(jids)) {
views = jids.map(jid => _converse.chatboxviews.get(jid));
}
views.forEach(view => {
if (view.is_chatroom && view.model) {
view.close();
}
});
return Promise.all(views.map(v => (v.is_chatroom && v.model && v.close())))
}
}
});
......
......@@ -714,9 +714,8 @@ converse.plugins.add('converse-omemo', {
if (identifier === null || identifier === undefined) {
throw new Error("Can't save identity_key for invalid identifier");
}
const address = new libsignal.SignalProtocolAddress.fromString(identifier),
existing = this.get('identity_key'+address.getName());
const address = new libsignal.SignalProtocolAddress.fromString(identifier);
const existing = this.get('identity_key'+address.getName());
const b64_idkey = u.arrayBufferToBase64(identity_key);
this.save('identity_key'+address.getName(), b64_idkey)
......@@ -986,8 +985,7 @@ converse.plugins.add('converse-omemo', {
initialize () {
this.devices = new _converse.Devices();
const id = `converse.devicelist-${_converse.bare_jid}-${this.get('jid')}`;
const storage = _converse.config.get('storage');
this.devices.browserStorage = _converse.createStore(id, storage);
this.devices.browserStorage = _converse.createStore(id);
this.fetchDevices();
},
......@@ -1096,7 +1094,7 @@ converse.plugins.add('converse-omemo', {
function fetchDeviceLists () {
return new Promise((success, error) => _converse.devicelists.fetch({success, error}));
return new Promise((success, error) => _converse.devicelists.fetch({success, 'error': (m, e) => error(e)}));
}
async function fetchOwnDevices () {
......@@ -1171,10 +1169,9 @@ converse.plugins.add('converse-omemo', {
function restoreOMEMOSession () {
if (_converse.omemo_store === undefined) {
const storage = _converse.config.get('storage'),
id = `converse.omemosession-${_converse.bare_jid}`;
const id = `converse.omemosession-${_converse.bare_jid}`;
_converse.omemo_store = new _converse.OMEMOStore({'id': id});
_converse.omemo_store.browserStorage = _converse.createStore(id, storage);
_converse.omemo_store.browserStorage = _converse.createStore(id);
}
return _converse.omemo_store.fetchSession();
}
......@@ -1184,9 +1181,8 @@ converse.plugins.add('converse-omemo', {
return;
}
_converse.devicelists = new _converse.DeviceLists();
const storage = _converse.config.get('storage'),
id = `converse.devicelists-${_converse.bare_jid}`;
_converse.devicelists.browserStorage = _converse.createStore(id, storage);
const id = `converse.devicelists-${_converse.bare_jid}`;
_converse.devicelists.browserStorage = _converse.createStore(id);
try {
await fetchOwnDevices();
......
......@@ -808,9 +808,8 @@ converse.plugins.add('converse-rosterview', {
createRosterFilter () {
// Create a model on which we can store filter properties
const model = new _converse.RosterFilter();
model.id = `_converse.rosterfilter${_converse.bare_jid}`;
const storage = _converse.config.get('storage');
model.browserStorage = _converse.createStore(this.filter.id, storage);
model.id = `_converse.rosterfilter-${_converse.bare_jid}`;
model.browserStorage = _converse.createStore(model.id);
this.filter_view = new _converse.RosterFilterView({'model': model});
this.listenTo(this.filter_view.model, 'change', this.updateFilter);
this.filter_view.model.fetch();
......
......@@ -96,7 +96,15 @@ converse.plugins.add('converse-uniview', {
.forEach(hideChat);
if (view.model.get('hidden')) {
u.safeSave(view.model, {'hidden': false});
return new Promise(resolve => {
u.safeSave(
view.model,
{'hidden': false}, {
'success': resolve,
'failure': resolve
}
);
});
}
}
});
......
......@@ -127,19 +127,13 @@ converse.plugins.add('converse-bookmarks', {
fetchBookmarks () {
const deferred = u.getResolveablePromise();
if (this.browserStorage.records.length > 0) {
if (window.sessionStorage.getItem(this.fetched_flag)) {
this.fetch({
'success': () => deferred.resolve(),
'error': () => deferred.resolve()
});
} else if (! window.sessionStorage.getItem(this.fetched_flag)) {
// There aren't any cached bookmarks and the
// `fetched_flag` is off, so we query the XMPP server.
// If nothing is returned from the XMPP server, we set
// the `fetched_flag` to avoid calling the server again.
this.fetchBookmarksFromServer(deferred);
} else {
deferred.resolve();
this.fetchBookmarksFromServer(deferred);
}
return deferred;
},
......@@ -231,6 +225,7 @@ converse.plugins.add('converse-bookmarks', {
onBookmarksReceived (deferred, iq) {
this.createBookmarksFromStanza(iq);
window.sessionStorage.setItem(this.fetched_flag, true);
if (deferred !== undefined) {
return deferred.resolve();
}
......@@ -301,6 +296,7 @@ converse.plugins.add('converse-bookmarks', {
if (_converse.bookmarks !== undefined) {
_converse.bookmarks.clearSession({'silent': true});
window.sessionStorage.removeItem(_converse.bookmarks.fetched_flag);
delete _converse.bookmarks;
}
});
......
......@@ -114,7 +114,6 @@ converse.plugins.add('converse-bosh', {
sessionStorage.removeItem(`${id}-${id}`);
} else {
_converse.bosh_session.destroy();
_converse.bosh_session.browserStorage._clear();
delete _converse.bosh_session;
}
});
......
......@@ -18,8 +18,8 @@ function propertySort (array, property) {
}
function generateVerificationString (_converse) {
const identities = _converse.api.disco.own.identities.get(),
features = _converse.api.disco.own.features.get();
const identities = _converse.api.disco.own.identities.get();
const features = _converse.api.disco.own.features.get();
if (identities.length > 1) {
propertySort(identities, "category");
......
......@@ -353,8 +353,7 @@ converse.plugins.add('converse-chatboxes', {
initMessages () {
this.messages = new this.messagesCollection();
this.messages.chatbox = this;
const storage = _converse.config.get('storage');
this.messages.browserStorage = _converse.createStore(this.getMessagesCacheKey(), storage);
this.messages.browserStorage = _converse.createStore(this.getMessagesCacheKey());
this.listenTo(this.messages, 'change:upload', message => {
if (message.get('upload') === _converse.SUCCESS) {
_converse.api.send(this.createMessageStanza(message));
......@@ -388,27 +387,28 @@ converse.plugins.add('converse-chatboxes', {
return this.messages.fetched;
},
clearMessages () {
async clearMessages () {
try {
this.messages.models.forEach(m => m.destroy());
await Promise.all(this.messages.models.map(m => m.destroy()));
this.messages.reset();
} catch (e) {
this.messages.trigger('reset');
_converse.log(e, Strophe.LogLevel.ERROR);
} finally {
delete this.messages.fetched;
this.messages.browserStorage._clear();
}
},
close () {
async close () {
try {
this.destroy();
await new Promise((success, reject) => {
return this.destroy({success, 'error': (m, e) => reject(e)})
});
} catch (e) {
_converse.log(e, Strophe.LogLevel.ERROR);
} finally {
if (_converse.clear_messages_on_reconnection) {
this.clearMessages();
await this.clearMessages();
}
}
},
......@@ -1172,9 +1172,7 @@ converse.plugins.add('converse-chatboxes', {
if (reconnecting) {
return;
}
const storage = _converse.config.get('storage');
const id = `converse.chatboxes-${_converse.bare_jid}`;
this.browserStorage = _converse.createStore(id, storage);
this.browserStorage = _converse.createStore(`converse.chatboxes-${_converse.bare_jid}`);
this.registerMessageHandler();
this.fetch({
'add': true,
......@@ -1202,7 +1200,7 @@ converse.plugins.add('converse-chatboxes', {
return;
}
const attrs = await chatbox.getMessageAttributesFromStanza(stanza, stanza);
chatbox.messages.create(attrs);
await chatbox.messages.create(attrs);
},
/**
......@@ -1353,6 +1351,7 @@ converse.plugins.add('converse-chatboxes', {
}
jid = Strophe.getBareJidFromJid(jid.toLowerCase());
await _converse.api.waitUntil('chatBoxesFetched');
let chatbox = this.get(Strophe.getBareJidFromJid(jid));
if (chatbox) {
chatbox.save(attrs);
......@@ -1519,11 +1518,7 @@ converse.plugins.add('converse-chatboxes', {
* });
*/
async open (jids, attrs, force) {
await Promise.all([
_converse.api.waitUntil('rosterContactsFetched'),
_converse.api.waitUntil('chatBoxesFetched')
]);
await _converse.api.waitUntil('chatBoxesFetched');
if (isString(jids)) {
const chat = await _converse.api.chats.create(jids, attrs);
if (chat) {
......@@ -1542,7 +1537,7 @@ converse.plugins.add('converse-chatboxes', {
},
/**
* Returns a chat model. The chat should already be open.
* Retrieves a chat model. The chat should already be open.
*
* @method _converse.api.chats.get
* @param {String|string[]} jids - e.g. 'buddy@example.com' or ['buddy1@example.com', 'buddy2@example.com']
......
......@@ -26,6 +26,7 @@ const $msg = strophe.default.$msg;
const $pres = strophe.default.$pres;
Backbone = Backbone.noConflict();
BrowserStorage.patch(Backbone);
dayjs.extend(advancedFormat);
......@@ -109,9 +110,20 @@ _converse.VERSION_NAME = "v5.0.5dev";
Object.assign(_converse, Backbone.Events);
_converse.Collection = Backbone.Collection.extend({
clearSession (options) {
Array.from(this.models).forEach(m => m.destroy(options));
this.browserStorage._clear();
async clearSession (options={}) {
await Promise.all(Array.from(this.models).map(m => {
return new Promise(
success => m.destroy(
Object.assign(options, {
success,
'error': (m, e) => {
_converse.log(e, Strophe.LogLevel.ERROR);
success()
}
})
)
);
}));
this.reset();
}
});
......@@ -377,9 +389,40 @@ _converse.isUniView = function () {
return _.includes(['mobile', 'fullscreen', 'embedded'], _converse.view_mode);
};
async function initStorage () {
await BrowserStorage.sessionStorageInitialized;
// Sets up Backbone.BrowserStorage and localForage for the 3 different stores.
_converse.localStorage = BrowserStorage.localForage.createInstance({
'name': 'local',
'description': 'localStorage instance',
'driver': [BrowserStorage.localForage.LOCALSTORAGE]
});
_converse.indexedDB = BrowserStorage.localForage.createInstance({
'name': 'indexed',
'description': 'indexedDB instance',
'driver': [BrowserStorage.localForage.INDEXEDDB]
});
_converse.sessionStorage = BrowserStorage.localForage.createInstance({
'name': 'session',
'description': 'sessionStorage instance',
'driver': ['sessionStorageWrapper']
});
_converse.storage = {
'session': _converse.sessionStorage,
'local': _converse.localStorage,
'indexed': _converse.indexedDB
}
}
_converse.createStore = function (id, storage) {
const s = storage ? storage : _converse.config.get('storage');
return new BrowserStorage[s](id);
const s = _converse.storage[storage ? storage : _converse.config.get('storage')];
return new Backbone.BrowserStorage(id, s);
}
......@@ -455,8 +498,8 @@ function initClientConfig () {
}
function tearDown () {
_converse.api.trigger('beforeTearDown');
async function tearDown () {
await _converse.api.trigger('beforeTearDown', {'synchronous': true});
window.removeEventListener('click', _converse.onUserActivity);
window.removeEventListener('focus', _converse.onUserActivity);
window.removeEventListener('keypress', _converse.onUserActivity);
......@@ -531,7 +574,7 @@ function connect (credentials) {
}
function reconnect () {
async function reconnect () {
_converse.log('RECONNECTING: the connection has dropped, attempting to reconnect.');
_converse.setConnectionStatus(
Strophe.Status.RECONNECTING,
......@@ -546,7 +589,7 @@ function reconnect () {
_converse.api.trigger('will-reconnect');
_converse.connection.reconnecting = true;
tearDown();
await tearDown();
return _converse.api.user.login();
}
......@@ -558,7 +601,6 @@ _converse.shouldClearCache = () => (!_converse.config.get('trusted') || _convers
function clearSession () {
if (_converse.session !== undefined) {
_converse.session.destroy();
_converse.session.browserStorage._clear();
delete _converse.session;
}
/**
......@@ -669,7 +711,7 @@ async function initSession (jid) {
await new Promise(r => _converse.session.fetch({'success': r, 'error': r}));
if (_converse.session.get('active')) {
_converse.session.clear();
_converse.session.save({'id': id});
_converse.session.save({id});
}
saveJIDtoSession(jid);
/**
......@@ -822,6 +864,7 @@ function setUpXMLLogging () {
async function finishInitialization () {
await initStorage();
initClientConfig();
initPlugins();
_converse.registerGlobalEventHandlers();
......@@ -920,10 +963,15 @@ function unregisterGlobalEventHandlers () {
_converse.api.trigger('unregisteredGlobalEventHandlers');
}
function cleanup () {
// Looks like _converse.initialized was called again without logging
// out or disconnecting in the previous session.
// This happens in tests. We therefore first clean up.
async function cleanup () {
// Make sure everything is reset in case this is a subsequent call to
// convesre.initialize (happens during tests).
if (_converse.localStorage) {
await Promise.all([
BrowserStorage.localForage.dropInstance({'name': 'local'}),
BrowserStorage.localForage.dropInstance({'name': 'indexed'}),
BrowserStorage.localForage.dropInstance({'name': 'session'})]);
}
Backbone.history.stop();
unregisterGlobalEventHandlers();
delete _converse.controlboxtoggle;
......@@ -939,7 +987,7 @@ function cleanup () {
_converse.initialize = async function (settings, callback) {
cleanup();
await cleanup();
settings = settings !== undefined ? settings : {};
PROMISES.forEach(addPromise);
......@@ -1256,7 +1304,7 @@ _converse.api = {
const conn_status = _converse.connfeedback.get('connection_status');
if (_converse.authentication === _converse.ANONYMOUS) {
tearDown();
await tearDown();
clearSession();
}
if (conn_status === Strophe.Status.CONNFAIL) {
......@@ -1421,8 +1469,7 @@ _converse.api = {
complete();
}
return promise;
},
}
},
/**
......
......@@ -120,18 +120,24 @@ converse.plugins.add('converse-disco', {
_converse.api.trigger('discoExtensionFieldDiscovered', field);
},
fetchFeatures (options) {
if (options.ignore_cache || this.features.browserStorage.records.length === 0) {
async fetchFeatures (options) {
if (options.ignore_cache) {
this.queryInfo();
} else {
this.features.fetch({
add: true,
success: () => {
this.waitUntilFeaturesDiscovered.resolve(this);
this.trigger('featuresDiscovered');
}
});
this.identities.fetch({add: true});
const store_id = this.features.browserStorage.name;
const result = await this.features.browserStorage.store.getItem(store_id);
if (result && result.length === 0 || result === null) {
this.queryInfo();
} else {
this.features.fetch({
add: true,
success: () => {
this.waitUntilFeaturesDiscovered.resolve(this);
this.trigger('featuresDiscovered');
}
});
this.identities.fetch({add: true});
}
}
},
......@@ -259,7 +265,7 @@ converse.plugins.add('converse-disco', {
function initStreamFeatures () {
const bare_jid = Strophe.getBareJidFromJid(_converse.jid);
const id = `converse.stream-features-${bare_jid}`;
if (!_converse.stream_features || _converse.stream_features.browserStorage.id !== id) {
if (!_converse.stream_features || _converse.stream_features.browserStorage.name !== id) {
_converse.stream_features = new _converse.Collection();
_converse.stream_features.browserStorage = _converse.createStore(id, "session");
_converse.stream_features.fetch({
......@@ -281,6 +287,9 @@ converse.plugins.add('converse-disco', {
* @example _converse.api.listen.on('streamFeaturesAdded', () => { ... });
*/
_converse.api.trigger('streamFeaturesAdded');
},
error (m, e) {
_converse.log(e, Strophe.LogLevel.ERROR);
}
});
}
......@@ -345,15 +354,17 @@ converse.plugins.add('converse-disco', {
/******************** Event Handlers ********************/
// Re-create promise upon reconnection
_converse.api.listen.on('userSessionInitialized', initStreamFeatures);
_converse.api.listen.on('beforeResourceBinding', initStreamFeatures);
_converse.api.listen.on('reconnected', initializeDisco);
_converse.api.listen.on('connected', initializeDisco);
_converse.api.listen.on('beforeTearDown', () => {
_converse.api.listen.on('beforeTearDown', async () => {
_converse.api.promises.add('streamFeaturesAdded')
if (_converse.stream_features) {
_converse.stream_features.clearSession();
await _converse.stream_features.clearSession();
delete _converse.stream_features;
}
});
......
......@@ -388,7 +388,6 @@ converse.plugins.add('converse-emoji', {
/************************ BEGIN Event Handlers ************************/
_converse.api.listen.on('clearSession', () => {
if (_converse.emojipicker) {
_converse.emojipicker.browserStorage._clear();
_converse.emojipicker.destroy();
delete _converse.emojipicker
}
......
......@@ -253,16 +253,14 @@ converse.plugins.add('converse-mam', {
chat.fetchNewestMessages();
}
});
_converse.api.listen.on('afterMessagesFetched', chat => {
// XXX: We don't want to query MAM every time this is triggered
// since it's not necessary when the chat is restored from cache.
// (given that BOSH or SMACKS will ensure that you get messages
// sent during the reload).
//
// With MUCs we can listen for `enteredNewRoom` but for
// one-on-one we have to use this hacky solutoin for now.
// `chat_state` is `undefined` only for newly created chats.
if (chat.get('type') === _converse.PRIVATE_CHAT_TYPE && chat.get('chat_state') === undefined) {
// With MUCs we can listen for `enteredNewRoom`.
if (chat.get('type') === _converse.PRIVATE_CHAT_TYPE && !_converse.connection.restored) {
chat.fetchNewestMessages();
}
});
......
......@@ -84,10 +84,17 @@ converse.plugins.add('converse-muc', {
dependencies: ["converse-chatboxes", "converse-disco", "converse-controlbox"],
overrides: {
tearDown () {
const { _converse } = this.__super__;
const groupchats = this.chatboxes.where({'type': _converse.CHATROOMS_TYPE});
_.each(groupchats, gc => u.safeSave(gc, {'connection_status': converse.ROOMSTATUS.DISCONNECTED}));
this.__super__.tearDown.call(this, arguments);
},
ChatBoxes: {
model (attrs, options) {
const { _converse } = this.__super__;
if (attrs.type == _converse.CHATROOMS_TYPE) {
if (attrs && attrs.type == _converse.CHATROOMS_TYPE) {
return new _converse.ChatRoom(attrs, options);
} else {
return this.__super__.model.apply(this, arguments);
......@@ -528,11 +535,15 @@ converse.plugins.add('converse-muc', {
* registered for this groupchat.
*/
if (this.message_handler) {
_converse.connection.deleteHandler(this.message_handler);
if (_converse.connection) {
_converse.connection.deleteHandler(this.message_handler);
}
delete this.message_handler;
}
if (this.presence_handler) {
_converse.connection.deleteHandler(this.presence_handler);
if (_converse.connection) {
_converse.connection.deleteHandler(this.presence_handler);
}
delete this.presence_handler;
}
return this;
......@@ -612,14 +623,13 @@ converse.plugins.add('converse-muc', {
* @method _converse.ChatRoom#leave
* @param { string } [exit_msg] - Message to indicate your reason for leaving
*/
leave (exit_msg) {
async leave (exit_msg) {
this.features.destroy();
this.occupants.browserStorage._clear();
this.occupants.reset();
this.occupants.clearSession();
if (_converse.disco_entities) {
const disco_entity = _converse.disco_entities.get(this.get('jid'));
if (disco_entity) {
disco_entity.destroy();
await new Promise((success, error) => disco_entity.destroy({success, error}));
}
}
if (_converse.api.connection.connected()) {
......@@ -629,10 +639,11 @@ converse.plugins.add('converse-muc', {
this.removeHandlers();
},
close () {
async close () {
try {
this.features.destroy();
this.features.browserStorage._clear();
await new Promise((success, reject) => {
return this.features.destroy({success, 'error': (m, e) => reject(e)})
});
} catch (e) {
_converse.log(e, Strophe.LogLevel.ERROR);
}
......@@ -838,9 +849,10 @@ converse.plugins.add('converse-muc', {
}
const fields = await _converse.api.disco.getFields(this.get('jid'));
this.save({
'name': identity && identity.get('name'),
'description': _.get(fields.findWhere({'var': "muc#roominfo_description"}), 'attributes.value')
});
'name': identity && identity.get('name'),
'description': _.get(fields.findWhere({'var': "muc#roominfo_description"}), 'attributes.value')
}
);
const features = await _converse.api.disco.getFeatures(this.get('jid'));
const attrs = Object.assign(
......@@ -857,6 +869,7 @@ converse.plugins.add('converse-muc', {
}
attrs[fieldname.replace('muc_', '')] = true;
});
attrs.description = _.get(fields.findWhere({'var': "muc#roominfo_description"}), 'attributes.value');
this.features.save(attrs);
},
......@@ -1012,33 +1025,6 @@ converse.plugins.add('converse-muc', {
return this.occupants.findWhere({'jid': _converse.bare_jid});
},
/**
* Parse the presence stanza for the current user's affiliation and
* role and save them on the relevant {@link _converse.ChatRoomOccupant}
* instance.
* @private
* @method _converse.ChatRoom#saveAffiliationAndRole
* @param { XMLElement } pres - A <presence> stanza.
*/
saveAffiliationAndRole (pres) {
const item = sizzle(`x[xmlns="${Strophe.NS.MUC_USER}"] item`, pres).pop();
const is_self = (pres.querySelector("status[code='110']") !== null);
if (is_self && item) {
const affiliation = item.getAttribute('affiliation');
const role = item.getAttribute('role');
const changes = {};
if (affiliation) {
changes['affiliation'] = affiliation;
}
if (role) {
changes['role'] = role;
}
if (!_.isEmpty(changes)) {
this.getOwnOccupant().save(changes);
}
}
},
/**
* Send an IQ stanza specifying an affiliation change.
* @private
......@@ -1265,8 +1251,7 @@ converse.plugins.add('converse-muc', {
},
/**
* Given a presence stanza, update the occupant model
* based on its contents.
* Given a presence stanza, update the occupant model based on its contents.
* @private
* @method _converse.ChatRoom#updateOccupantsOnPresence
* @param { XMLElement } pres - The presence stanza
......@@ -1572,7 +1557,13 @@ converse.plugins.add('converse-muc', {
!this.ignorableCSN(attrs) &&
(attrs['chat_state'] || !u.isEmptyMessage(attrs))) {
const msg = this.correctMessage(attrs) || this.messages.create(attrs);
const msg = this.correctMessage(attrs) ||
await new Promise((success, reject) => {
this.messages.create(
attrs,
{ success, 'erorr': (m, e) => reject(e) }
)
});
this.incrementUnreadMsgCounter(msg);
}
_converse.api.trigger('message', {'stanza': original_stanza, 'chatbox': this});
......@@ -1802,7 +1793,6 @@ converse.plugins.add('converse-muc', {
this.save('connection_status', converse.ROOMSTATUS.ENTERED);
}
this.updateOccupantsOnPresence(stanza);
this.saveAffiliationAndRole(stanza);
if (stanza.getAttribute('type') === 'unavailable') {
this.handleDisconnection(stanza);
......@@ -1839,6 +1829,7 @@ converse.plugins.add('converse-muc', {
}
}
}
this.save({'connection_status': converse.ROOMSTATUS.ENTERED});
},
/**
......@@ -1983,6 +1974,12 @@ converse.plugins.add('converse-muc', {
this.create(attrs);
}
});
/**
* Triggered once the member lists for this MUC have been fetched and processed.
* @event _converse#membersFetched
* @example _converse.api.listen.on('membersFetched', () => { ... });
*/
_converse.api.trigger('membersFetched');
},
findOccupant (data) {
......@@ -2142,7 +2139,7 @@ converse.plugins.add('converse-muc', {
*/
return _converse.chatboxes
.filter(m => (m.get('type') === _converse.CHATROOMS_TYPE))
.forEach(m => m.save('connection_status', converse.ROOMSTATUS.DISCONNECTED))
.forEach(m => m.save({'connection_status': converse.ROOMSTATUS.DISCONNECTED}));
}
_converse.api.listen.on('disconnected', disconnectChatRooms);
......@@ -2267,15 +2264,14 @@ converse.plugins.add('converse-muc', {
room && room.maybeShow(force);
return room;
} else {
return jids.map(async jid => {
const room = await _converse.api.rooms.create(jid, attrs);
room.maybeShow(force);
});
const rooms = await Promise.all(jids.map(jid => _converse.api.rooms.create(jid, attrs)));
rooms.forEach(r => r.maybeShow(force));
return rooms;
}
},
/**
* Returns an object representing a MUC chatroom (aka groupchat)
* Fetches the object representing a MUC chatroom (aka groupchat)
*
* @method _converse.api.rooms.get
* @param {string} [jid] The room JID (if not specified, all rooms will be returned).
......
......@@ -684,11 +684,10 @@ converse.plugins.add('converse-roster', {
const jid = item.getAttribute('jid');
if (this.isSelf(jid)) { return; }
const contact = this.get(jid),
subscription = item.getAttribute("subscription"),
ask = item.getAttribute("ask"),
groups = _.map(item.getElementsByTagName('group'), Strophe.getText);
const contact = this.get(jid);
const subscription = item.getAttribute("subscription");
const ask = item.getAttribute("ask");
const groups = Array.from(item.getElementsByTagName('group')).map(e => e.textContent);
if (!contact) {
if ((subscription === "none" && ask === null) || (subscription === "remove")) {
return; // We're lazy when adding contacts.
......@@ -959,7 +958,6 @@ converse.plugins.add('converse-roster', {
if (_converse.shouldClearCache()) {
if (_converse.roster) {
_.invoke(_converse, 'roster.data.destroy');
_.invoke(_converse, 'roster.data.browserStorage._clear');
_converse.roster.clearSession();
delete _converse.roster;
}
......@@ -1024,7 +1022,7 @@ converse.plugins.add('converse-roster', {
* @namespace _converse.api.contacts
* @memberOf _converse.api
*/
'contacts': {
contacts: {
/**
* This method is used to retrieve roster contacts.
*
......
......@@ -225,7 +225,6 @@ converse.plugins.add('converse-status', {
_converse.api.listen.on('clearSession', () => {
if (_converse.shouldClearCache() && _converse.xmppstatus) {
_converse.xmppstatus.destroy();
_converse.xmppstatus.browserStorage._clear();
delete _converse.xmppstatus;
}
});
......
{
"name": "@converse/headless",
"version": "4.2.0",
"version": "5.0.3",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"backbone": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/backbone/-/backbone-1.4.0.tgz",
"integrity": "sha512-RLmDrRXkVdouTg38jcgHhyQ/2zjg7a8E6sz2zxfz21Hh17xDJYUHBZimVIt5fUyS8vbfpeSmTL3gUjTEvUV3qQ==",
"dev": true,
"requires": {
"underscore": ">=1.8.3"
}
},
"backbone.browserStorage": {
"version": "github:conversejs/backbone.browserStorage#4fcb17861023becb3b25dec7b3238253873c8cd6",
"from": "github:conversejs/backbone.browserStorage#4fcb17861023becb3b25dec7b3238253873c8cd6",
"dev": true
},
"es6-promise": {
"version": "4.2.8",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
......@@ -15,6 +29,96 @@
"resolved": "https://registry.npmjs.org/filesize/-/filesize-3.6.1.tgz",
"integrity": "sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg==",
"dev": true
},
"immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=",
"dev": true
},
"jed": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/jed/-/jed-1.1.1.tgz",
"integrity": "sha1-elSbvZ/+FYWwzQoZHiAwVb7ldLQ=",
"dev": true
},
"lie": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
"integrity": "sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=",
"dev": true,
"requires": {
"immediate": "~3.0.5"
}
},
"localforage": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.7.3.tgz",
"integrity": "sha512-1TulyYfc4udS7ECSBT2vwJksWbkwwTX8BzeUIiq8Y07Riy7bDAAnxDaPU/tWyOVmQAcWJIEIFP9lPfBGqVoPgQ==",
"dev": true,
"requires": {
"lie": "3.1.1"
}
},
"lodash": {
"version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
"dev": true
},
"moment": {
"version": "2.19.4",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.19.4.tgz",
"integrity": "sha512-1xFTAknSLfc47DIxHDUbnJWC+UwgWxATmymaxIPQpmMh7LBm7ZbwVEsuushqwL2GYZU0jie4xO+TK44hJPjNSQ==",
"dev": true
},
"pluggable.js": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/pluggable.js/-/pluggable.js-2.0.0.tgz",
"integrity": "sha512-FgrSayXWfQQWL+RSDiCAFZbkEsY7hTZCiSuN9Ar/mcHpesxOPfrSzJKp+YbimOt9QFtSd+lR8Uob5tgkdQSOzg==",
"dev": true,
"requires": {
"lodash": "^4.17.4"
}
},
"strophe.js": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/strophe.js/-/strophe.js-1.3.2.tgz",
"integrity": "sha512-N6n93B0+0/qRazWUtgunJNE3DTfEpz363i17Uvnqh0lvl9iATnMtSoZtKjqN3reKPtjtBpbBRMWAQJRc688QkQ==",
"dev": true
},
"strophejs-plugin-ping": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/strophejs-plugin-ping/-/strophejs-plugin-ping-0.0.3.tgz",
"integrity": "sha512-HS/ArEGKXfu36fihjUSfjqmqOSyppQTJUbrkfEtOBRJmnaP3LsRRe5T2S3dmCdsWHKORaJYc/OHSKfFlxHPdqw==",
"dev": true,
"requires": {
"strophe.js": "^1.2.12"
}
},
"strophejs-plugin-rsm": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/strophejs-plugin-rsm/-/strophejs-plugin-rsm-0.0.2.tgz",
"integrity": "sha512-Yn/VpxNz3Gkb790rJkwMyjlwHWCjWA9UxIl5kwGnsr7Ofo1MHztgyQ8XwQF1DGFp3Y4oiXbjZ/whG3S/cIgIew==",
"dev": true
},
"twemoji": {
"version": "11.3.0",
"resolved": "https://registry.npmjs.org/twemoji/-/twemoji-11.3.0.tgz",
"integrity": "sha512-xN/vlR6+gDmfjt6LInAqwGAv3Agwrmzx5TD1jEFwKS19IOGDrX0/3OB8GP1wUYPVIdkaer5hw6qd+52jzvz0Lg==",
"dev": true
},
"underscore": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz",
"integrity": "sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg==",
"dev": true
},
"urijs": {
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.1.tgz",
"integrity": "sha512-xVrGVi94ueCJNrBSTjWqjvtgvl3cyOTThp2zaMaFNGp3F542TR6sM3f2o8RqZl+AwteClSVmoCyt0ka4RjQOQg==",
"dev": true
}
}
}
......@@ -23,9 +23,10 @@
"gitHead": "9641dcdc820e029b05930479c242d2b707bbe8e2",
"devDependencies": {
"backbone": "1.4",
"backbone.browserStorage": "0.0.5",
"backbone.browserStorage": "conversejs/backbone.browserStorage#2d0ceaa2f38eedc60122bb0aa23c826dc37a9194",
"filesize": "^4.1.2",
"jed": "1.1.1",
"localforage": "^1.7.3",
"lodash": "^4.17.15",
"pluggable.js": "2.0.1",
"strophe.js": "1.3.4",
......
......@@ -437,11 +437,11 @@ u.onMultipleEvents = function (events=[], callback) {
events.forEach(e => e.object.on(e.event, handler));
};
u.safeSave = function (model, attributes) {
u.safeSave = function (model, attributes, options) {
if (u.isPersistableModel(model)) {
model.save(attributes);
model.save(attributes, options);
} else {
model.set(attributes);
model.set(attributes, options);
}
};
......
<div class="list-container list-container--openrooms {{{ !o.rooms.length && 'hidden' || '' }}}">
<a href="#" class="list-toggle open-rooms-toggle controlbox-padded" title="{{{o.desc_rooms}}}">
<span class="fa {[ if (o.toggle_state === o._converse.OPENED) { ]} fa-caret-down {[ } else { ]} fa-caret-right {[ } ]}">
</span> {{{o.label_rooms}}}</a>
<span class="fa {[ if (o.toggle_state === o._converse.OPENED) { ]} fa-caret-down {[ } else { ]} fa-caret-right {[ } ]}"></span>{{{o.label_rooms}}}</a>
<div class="items-list rooms-list open-rooms-list {{{ o.collapsed && 'collapsed' }}}">
{[o.rooms.forEach(function (room) { ]}
......
......@@ -76,13 +76,37 @@
'Escalus, prince of Verona', 'The Nurse', 'Paris'
];
mock.pend_names = [
'Lord Capulet', 'Lady Capulet', 'Servant'
];
mock.cur_names = [
'Mercutio', 'Juliet Capulet', 'Lady Montague', 'Lord Montague', 'Friar Laurence',
'Tybalt', 'Lady Capulet', 'Benviolo', 'Balthasar',
'Peter', 'Abram', 'Sampson', 'Gregory', 'Potpan', 'Friar John'
'Lord Capulet', 'Guard', 'Servant'
];
mock.current_contacts_map = {
'Mercutio': ['Colleagues', 'friends & acquaintences'],
'Juliet Capulet': ['friends & acquaintences'],
'Lady Montague': ['Colleagues', 'Family'],
'Lord Montague': ['Family'],
'Friar Laurence': ['friends & acquaintences'],
'Tybalt': ['friends & acquaintences'],
'Lady Capulet': ['ænemies'],
'Benviolo': ['friends & acquaintences'],
'Balthasar': ['Colleagues'],
'Peter': ['Colleagues'],
'Abram': ['Colleagues'],
'Sampson': ['Colleagues'],
'Gregory': ['friends & acquaintences'],
'Potpan': [],
'Friar John': []
};
const map = mock.current_contacts_map;
const groups_map = {};
Object.keys(map).forEach(k => {
const groups = map[k].length ? map[k] : ["Ungrouped"];
Object.values(groups).forEach(g => {
groups_map[g] = groups_map[g] ? [...groups_map[g], k] : [k]
});
});
mock.groups_map = groups_map;
mock.cur_names = Object.keys(mock.current_contacts_map);
mock.num_contacts = mock.req_names.length + mock.pend_names.length + mock.cur_names.length;
mock.groups = {
......@@ -195,42 +219,39 @@
'no_trimming': true,
'play_sounds': false,
'use_emojione': false,
'view_mode': mock.view_mode,
'view_mode': mock.view_mode
}, settings || {}));
_converse.ChatBoxViews.prototype.trimChat = function () {};
_converse.api.vcard.get = function (model, force) {
return new Promise(resolve => {
let jid;
if (_.isString(model)) {
jid = model;
} else if (!model.get('vcard_updated') || force) {
jid = model.get('jid') || model.get('muc_jid');
}
let fullname;
if (!jid || jid == 'romeo@montague.lit') {
jid = 'romeo@montague.lit';
fullname = 'Romeo Montague' ;
} else {
const name = jid.split('@')[0].replace(/\./g, ' ').split(' ');
const last = name.length-1;
name[0] = name[0].charAt(0).toUpperCase()+name[0].slice(1);
name[last] = name[last].charAt(0).toUpperCase()+name[last].slice(1);
fullname = name.join(' ');
}
const vcard = $iq().c('vCard').c('FN').t(fullname).nodeTree;
const result = {
'vcard': vcard,
'fullname': _.get(vcard.querySelector('FN'), 'textContent'),
'image': _.get(vcard.querySelector('PHOTO BINVAL'), 'textContent'),
'image_type': _.get(vcard.querySelector('PHOTO TYPE'), 'textContent'),
'url': _.get(vcard.querySelector('URL'), 'textContent'),
'vcard_updated': dayjs().format(),
'vcard_error': undefined
};
resolve(result);
}).catch(e => _converse.log(e, Strophe.LogLevel.FATAL));
let jid;
if (_.isString(model)) {
jid = model;
} else if (!model.get('vcard_updated') || force) {
jid = model.get('jid') || model.get('muc_jid');
}
let fullname;
if (!jid || jid == 'romeo@montague.lit') {
jid = 'romeo@montague.lit';
fullname = 'Romeo Montague' ;
} else {
const name = jid.split('@')[0].replace(/\./g, ' ').split(' ');
const last = name.length-1;
name[0] = name[0].charAt(0).toUpperCase()+name[0].slice(1);
name[last] = name[last].charAt(0).toUpperCase()+name[last].slice(1);
fullname = name.join(' ');
}
const vcard = $iq().c('vCard').c('FN').t(fullname).nodeTree;
return {
'vcard': vcard,
'fullname': _.get(vcard.querySelector('FN'), 'textContent'),
'image': _.get(vcard.querySelector('PHOTO BINVAL'), 'textContent'),
'image_type': _.get(vcard.querySelector('PHOTO TYPE'), 'textContent'),
'url': _.get(vcard.querySelector('URL'), 'textContent'),
'vcard_updated': dayjs().format(),
'vcard_error': undefined
};
};
if (_.get(settings, 'auto_login') !== false) {
_converse.api.user.login('romeo@montague.lit/orchard', 'secret');
......
......@@ -31,10 +31,10 @@ require.config(config);
var specs = [
"jasmine",
//"spec/transcripts",
// "spec/transcripts",
// "spec/profiling",
"spec/spoilers",
"spec/roomslist",
"spec/profiling",
"spec/utils",
"spec/converse",
"spec/bookmarks",
......
......@@ -47,17 +47,14 @@
return req;
};
utils.closeAllChatBoxes = function (converse) {
var i, chatbox;
for (i=converse.chatboxes.models.length-1; i>-1; i--) {
chatbox = converse.chatboxes.models[i];
converse.chatboxviews.get(chatbox.get('id')).close();
}
return this;
utils.closeAllChatBoxes = function (_converse) {
return Promise.all(_converse.chatboxviews.map(view => view.close()));
};
utils.openControlBox = function () {
const toggle = document.querySelector(".toggle-controlbox");
utils.openControlBox = async function (_converse) {
const model = await _converse.api.chats.open('controlbox');
await u.waitUntil(() => model.get('connected'));
var toggle = document.querySelector(".toggle-controlbox");
if (!u.isVisible(document.querySelector("#controlbox"))) {
if (!u.isVisible(toggle)) {
u.removeClass('hidden', toggle);
......@@ -68,9 +65,9 @@
};
utils.closeControlBox = function () {
var controlbox = document.querySelector("#controlbox");
const controlbox = document.querySelector("#controlbox");
if (u.isVisible(controlbox)) {
var button = controlbox.querySelector(".close-chatbox-button");
const button = controlbox.querySelector(".close-chatbox-button");
if (!_.isNull(button)) {
button.click();
}
......@@ -116,15 +113,19 @@
return views;
};
utils.openChatBoxFor = function (_converse, jid) {
utils.openChatBoxFor = async function (_converse, jid) {
await _converse.api.waitUntil('rosterContactsFetched');
_converse.roster.get(jid).trigger("open");
return u.waitUntil(() => _converse.chatboxviews.get(jid), 1000);
};
utils.openChatRoomViaModal = async function (_converse, jid, nick='') {
// Opens a new chatroom
utils.openControlBox(_converse);
const roomspanel = _converse.chatboxviews.get('controlbox').roomspanel;
const model = await _converse.api.chats.open('controlbox');
await u.waitUntil(() => model.get('connected'));
utils.openControlBox();
const view = await _converse.chatboxviews.get('controlbox');
const roomspanel = view.roomspanel;
roomspanel.el.querySelector('.show-add-muc-modal').click();
utils.closeControlBox(_converse);
const modal = roomspanel.add_room_modal;
......@@ -281,6 +282,7 @@
});
_converse.connection._dataRecv(utils.createRequest(owner_list_stanza));
}
return new Promise(resolve => _converse.api.listen.on('membersFetched', resolve));
};
utils.receiveOwnMUCPresence = function (_converse, muc_jid, nick) {
......@@ -296,7 +298,8 @@
}).up()
.c('status').attrs({code:'110'});
_converse.connection._dataRecv(utils.createRequest(presence));
}
// return utils.waitUntil(() => (view.model.get('connection_status') === converse.ROOMSTATUS.ENTERED));
};
utils.openAndEnterChatRoom = async function (_converse, muc_jid, nick, features=[], members=[]) {
......@@ -318,19 +321,36 @@
};
utils.clearChatBoxMessages = function (converse, jid) {
var view = converse.chatboxviews.get(jid);
const view = converse.chatboxviews.get(jid);
view.el.querySelector('.chat-content').innerHTML = '';
view.model.messages.reset();
view.model.messages.browserStorage._clear();
return view.model.messages.clearSession();
};
utils.createContact = async function (_converse, name, ask, requesting, subscription) {
const jid = name.replace(/ /g,'.').toLowerCase() + '@montague.lit';
if (_converse.roster.get(jid)) {
return Promise.resolve();
}
const contact = await new Promise((success, error) => {
_converse.roster.create({
'ask': ask,
'fullname': name,
'jid': jid,
'requesting': requesting,
'subscription': subscription
}, {success, error});
});
return contact;
};
utils.createContacts = function (converse, type, length) {
utils.createContacts = async function (_converse, type, length) {
/* Create current (as opposed to requesting or pending) contacts
* for the user's roster.
*
* These contacts are not grouped. See below.
*/
var names, jid, subscription, requesting, ask;
await _converse.api.waitUntil('rosterContactsFetched');
let names, subscription, requesting, ask;
if (type === 'requesting') {
names = mock.req_names;
subscription = 'none';
......@@ -347,33 +367,18 @@
requesting = false;
ask = null;
} else if (type === 'all') {
this.createContacts(converse, 'current')
.createContacts(converse, 'requesting')
.createContacts(converse, 'pending');
await this.createContacts(_converse, 'current');
await this.createContacts(_converse, 'requesting')
await this.createContacts(_converse, 'pending');
return this;
} else {
throw Error("Need to specify the type of contact to create");
}
if (typeof length === 'undefined') {
length = names.length;
}
for (var i=0; i<length; i++) {
jid = names[i].replace(/ /g,'.').toLowerCase() + '@montague.lit';
if (!converse.roster.get(jid)) {
converse.roster.create({
'ask': ask,
'name': names[i],
'jid': jid,
'requesting': requesting,
'subscription': subscription
});
}
}
return this;
const promises = names.slice(0, length).map(n => this.createContact(_converse, n, ask, requesting, subscription));
await Promise.all(promises);
};
utils.waitForRoster = async function (_converse, type='current', length, include_nick=true) {
utils.waitForRoster = async function (_converse, type='current', length=-1, include_nick=true, grouped=true) {
const iq = await u.waitUntil(() =>
_.filter(
_converse.connection.IQ_stanzas,
......@@ -388,44 +393,36 @@
'xmlns': 'jabber:iq:roster'
});
if (type === 'pending' || type === 'all') {
mock.pend_names.slice(0, length).map(name =>
const pend_names = (length > -1) ? mock.pend_names.slice(0, length) : mock.pend_names;
pend_names.map(name =>
result.c('item', {
jid: name.replace(/ /g,'.').toLowerCase() + '@montague.lit',
name: include_nick ? name : undefined,
subscription: 'to'
subscription: 'none',
ask: 'subscribe'
}).up()
);
} else if (type === 'current' || type === 'all') {
mock.cur_names.slice(0, length).map(name =>
}
if (type === 'current' || type === 'all') {
const cur_names = Object.keys(mock.current_contacts_map);
const names = (length > -1) ? cur_names.slice(0, length) : cur_names;
names.forEach(name => {
result.c('item', {
jid: name.replace(/ /g,'.').toLowerCase() + '@montague.lit',
name: include_nick ? name : undefined,
subscription: 'both'
}).up()
);
subscription: 'both',
ask: null
});
if (grouped) {
mock.current_contacts_map[name].forEach(g => result.c('group').t(g).up());
}
result.up();
});
}
_converse.connection._dataRecv(utils.createRequest(result));
await _converse.api.waitUntil('rosterContactsFetched');
};
utils.createGroupedContacts = function (converse) {
/* Create grouped contacts
*/
let i=0, j=0;
_.each(_.keys(mock.groups), function (name) {
j = i;
for (i=j; i<j+mock.groups[name]; i++) {
converse.roster.create({
'jid': mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@montague.lit',
'subscription': 'both',
'ask': null,
'groups': name === 'ungrouped'? [] : [name],
'name': mock.cur_names[i]
});
}
});
};
utils.createChatMessage = function (_converse, sender_jid, message) {
return $msg({
from: sender_jid,
......
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