Turn roster into a custom element

This commit is contained in:
JC Brand 2021-01-26 14:01:37 +01:00
parent 7407a5a681
commit 9f5dbad589
61 changed files with 1210 additions and 1719 deletions

View File

@ -27,6 +27,7 @@ OMEMO messages are gone and cannot be recovered on that device. See [muc_clear_m
Removed events:
* `chatBoxInsertedIntoDOM`
* `bookmarkViewsInitialized`
* `rosterGroupsFetched`
## 7.0.2 (2020-11-23)

View File

@ -25,49 +25,48 @@ module.exports = function(config) {
{ pattern: "node_modules/sinon/pkg/sinon.js", type: 'module' },
{ pattern: "spec/mock.js", type: 'module' },
{ pattern: "spec/user-details-modal.js", type: 'module' },
{ pattern: "spec/spoilers.js", type: 'module' },
{ pattern: "spec/emojis.js", type: 'module' },
{ pattern: "spec/muclist.js", type: 'module' },
{ pattern: "spec/utils.js", type: 'module' },
{ pattern: "spec/converse.js", type: 'module' },
{ pattern: "spec/bookmarks.js", type: 'module' },
{ pattern: "spec/headline.js", type: 'module' },
{ pattern: "spec/disco.js", type: 'module' },
{ pattern: "spec/protocol.js", type: 'module' },
{ pattern: "spec/presence.js", type: 'module' },
{ pattern: "spec/eventemitter.js", type: 'module' },
{ pattern: "spec/smacks.js", type: 'module' },
{ pattern: "spec/ping.js", type: 'module' },
{ pattern: "spec/push.js", type: 'module' },
{ pattern: "spec/xmppstatus.js", type: 'module' },
{ pattern: "spec/mam.js", type: 'module' },
{ pattern: "spec/omemo.js", type: 'module' },
{ pattern: "spec/controlbox.js", type: 'module' },
{ pattern: "spec/roster.js", type: 'module' },
// { pattern: "spec/user-details-modal.js", type: 'module' },
// { pattern: "spec/spoilers.js", type: 'module' },
// { pattern: "spec/emojis.js", type: 'module' },
// { pattern: "spec/muclist.js", type: 'module' },
// { pattern: "spec/converse.js", type: 'module' },
// { pattern: "spec/bookmarks.js", type: 'module' },
// { pattern: "spec/headline.js", type: 'module' },
// { pattern: "spec/disco.js", type: 'module' },
// { pattern: "spec/protocol.js", type: 'module' },
// { pattern: "spec/presence.js", type: 'module' },
// { pattern: "spec/eventemitter.js", type: 'module' },
// { pattern: "spec/smacks.js", type: 'module' },
// { pattern: "spec/ping.js", type: 'module' },
// { pattern: "spec/push.js", type: 'module' },
// { pattern: "spec/xmppstatus.js", type: 'module' },
// { pattern: "spec/mam.js", type: 'module' },
// { pattern: "spec/omemo.js", type: 'module' },
// { pattern: "spec/controlbox.js", type: 'module' },
// { pattern: "spec/roster.js", type: 'module' },
{ pattern: "spec/chatbox.js", type: 'module' },
{ pattern: "spec/messages.js", type: 'module' },
{ pattern: "spec/corrections.js", type: 'module' },
{ pattern: "spec/styling.js", type: 'module' },
{ pattern: "spec/receipts.js", type: 'module' },
{ pattern: "spec/markers.js", type: 'module' },
{ pattern: "spec/rai.js", type: 'module' },
{ pattern: "spec/muc_messages.js", type: 'module' },
{ pattern: "spec/muc-mentions.js", type: 'module' },
{ pattern: "spec/me-messages.js", type: 'module' },
{ pattern: "spec/mentions.js", type: 'module' },
{ pattern: "spec/retractions.js", type: 'module' },
{ pattern: "spec/muc.js", type: 'module' },
{ pattern: "spec/modtools.js", type: 'module' },
{ pattern: "spec/room_registration.js", type: 'module' },
{ pattern: "spec/autocomplete.js", type: 'module' },
{ pattern: "spec/minchats.js", type: 'module' },
{ pattern: "spec/notification.js", type: 'module' },
{ pattern: "spec/login.js", type: 'module' },
{ pattern: "spec/register.js", type: 'module' },
{ pattern: "spec/hats.js", type: 'module' },
{ pattern: "spec/http-file-upload.js", type: 'module' },
{ pattern: "spec/xss.js", type: 'module' }
// { pattern: "spec/messages.js", type: 'module' },
// { pattern: "spec/corrections.js", type: 'module' },
// { pattern: "spec/styling.js", type: 'module' },
// { pattern: "spec/receipts.js", type: 'module' },
// { pattern: "spec/markers.js", type: 'module' },
// { pattern: "spec/rai.js", type: 'module' },
// { pattern: "spec/muc_messages.js", type: 'module' },
// { pattern: "spec/muc-mentions.js", type: 'module' },
// { pattern: "spec/me-messages.js", type: 'module' },
// { pattern: "spec/mentions.js", type: 'module' },
// { pattern: "spec/retractions.js", type: 'module' },
// { pattern: "spec/muc.js", type: 'module' },
// { pattern: "spec/modtools.js", type: 'module' },
// { pattern: "spec/room_registration.js", type: 'module' },
// { pattern: "spec/autocomplete.js", type: 'module' },
// { pattern: "spec/minchats.js", type: 'module' },
// { pattern: "spec/notification.js", type: 'module' },
// { pattern: "spec/login.js", type: 'module' },
// { pattern: "spec/register.js", type: 'module' },
// { pattern: "spec/hats.js", type: 'module' },
// { pattern: "spec/http-file-upload.js", type: 'module' },
// { pattern: "spec/xss.js", type: 'module' }
],
proxies: {

View File

@ -53,126 +53,119 @@
height: 100%;
overflow-x: hidden;
overflow-y: auto;
color: var(--text-color);
.roster-group {
border: none;
color: var(--text-color);
font-weight: normal;
text-shadow: 0 1px 0 var(--text-shadow-color);
margin: 0.75em 0 0.75em 0;
.group-toggle {
font-family: var(--heading-font);
display: block;
width: 100%;
margin: 0.75em 0 0.25em 0;
}
.group-toggle {
font-family: var(--heading-font);
display: block;
width: 100%;
padding-top: 0;
padding-bottom: 0.3rem;
.group-toggle, .group-toggle .fa {
color: var(--chat-head-color-dark) !important;
&:hover {
color: var(--chat-head-color-darker) !important;
}
}
.group-toggle, .group-toggle .fa {
color: var(--chat-head-color-dark) !important;
&:hover {
color: var(--chat-head-color-darker) !important;
.current-xmpp-contact {
margin: 0.25em 0;
.chat-status {
vertical-align: middle;
font-size: 0.6em;
margin-right: 0;
margin-left: -0.7em;
margin-bottom: -1.5em;
border-radius: 50%;
border: 2px solid var(--occupants-background-color);
}
.chat-status--offline {
margin-right: 0.8em;
}
.chat-status--online {
color: var(--chat-status-online);
}
.chat-status--busy {
color: var(--chat-status-busy);
}
.chat-status--away {
color: var(--chat-status-away);
}
.chat-status--offline {
display: none;
}
.far.fa-circle,
.fa-times-circle {
color: var(--subdued-color);
}
}
li {
&.requesting-xmpp-contact {
a {
line-height: var(--line-height);
}
.req-contact-name {
padding: 0 0.2em 0 0;
}
}
.current-xmpp-contact {
margin: 0.25em 0;
.chat-status {
vertical-align: middle;
font-size: 0.6em;
margin-right: 0;
margin-left: -0.7em;
margin-bottom: -1.5em;
border-radius: 50%;
border: 2px solid var(--occupants-background-color);
}
.chat-status--offline {
margin-right: 0.8em;
}
.chat-status--online {
color: var(--chat-status-online);
}
.chat-status--busy {
color: var(--chat-status-busy);
}
.chat-status--away {
color: var(--chat-status-away);
}
.chat-status--offline {
display: none;
}
.far.fa-circle,
.fa-times-circle {
color: var(--subdued-color);
}
}
li {
&.requesting-xmpp-contact {
a {
line-height: var(--line-height);
}
.req-contact-name {
padding: 0 0.2em 0 0;
}
}
.open-chat {
margin: 0;
padding: 0;
&.unread-msgs {
font-weight: bold;
.contact-name {
width: 70%;
}
}
.msgs-indicator {
color: white;
background-color: var(--chat-head-color);
opacity: 1;
border-radius: 10%;
padding: 0.2em 0.4em;
font-size: var(--font-size-small);
}
.open-chat {
margin: 0;
padding: 0;
&.unread-msgs {
font-weight: bold;
.contact-name {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
padding: 0;
margin: 0;
max-width: 85%;
float: none;
height: 100%;
&.unread-msgs {
max-width: 60%;
}
&.contact-name--offline {
margin-left: 0.7em;
}
width: 70%;
}
}
&.odd {
background-color: #DCEAC5;
/* Make this difference */
.msgs-indicator {
color: white;
background-color: var(--chat-head-color);
opacity: 1;
border-radius: 10%;
padding: 0.2em 0.4em;
font-size: var(--font-size-small);
margin-right: 0;
}
a, span {
.contact-name {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
padding: 0;
margin: 0;
max-width: 85%;
float: none;
height: 100%;
&.unread-msgs {
max-width: 60%;
}
&.contact-name--offline {
margin-left: 0.25em;
}
}
.span {
display: inline-block;
}
.decline-xmpp-request {
margin-left: 5px;
}
&:hover {
background-color: var(controlbox-pane-bg-hover-color);
}
}
&.odd {
background-color: #DCEAC5;
/* Make this difference */
}
a, span {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.span {
display: inline-block;
}
.decline-xmpp-request {
margin-left: 5px;
}
&:hover {
background-color: var(controlbox-pane-bg-hover-color);
}
}
}

View File

@ -9,7 +9,7 @@ describe("The nickname autocomplete feature", function () {
it("shows all autocompletion options when the user presses @",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'tom');
@ -63,7 +63,7 @@ describe("The nickname autocomplete feature", function () {
it("shows all autocompletion options when the user presses @ right after a new line",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'tom');
@ -118,7 +118,7 @@ describe("The nickname autocomplete feature", function () {
it("shows all autocompletion options when the user presses @ right after an allowed character",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {'opening_mention_characters':['(']},
['rosterContactsFetched', 'chatBoxesFetched'], {'opening_mention_characters':['(']},
async function (done, _converse) {
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'tom');
@ -172,7 +172,7 @@ describe("The nickname autocomplete feature", function () {
}));
it("should order by query index position and length", mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {}, async function (done, _converse) {
['rosterContactsFetched', 'chatBoxesFetched'], {}, async function (done, _converse) {
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'tom');
const view = _converse.chatboxviews.get('lounge@montague.lit');
@ -220,7 +220,7 @@ describe("The nickname autocomplete feature", function () {
it("autocompletes when the user presses tab",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
@ -332,7 +332,7 @@ describe("The nickname autocomplete feature", function () {
it("autocompletes when the user presses backspace",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');

View File

@ -6,8 +6,9 @@ const { Strophe, u, sizzle, $iq } = converse.env;
describe("A chat room", function () {
it("can be bookmarked", mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {}, async function (done, _converse) {
['chatBoxesFetched'], {}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 0);
await mock.waitUntilDiscoConfirmed(
_converse, _converse.bare_jid,
[{'category': 'pubsub', 'type': 'pep'}],
@ -133,10 +134,10 @@ describe("A chat room", function () {
it("will be automatically opened if 'autojoin' is set on the bookmark", mock.initConverse(
['rosterGroupsFetched'], {},
async function (done, _converse) {
[], {}, async function (done, _converse) {
const { u } = converse.env;
await mock.waitForRoster(_converse, 'current', 0);
await mock.waitUntilDiscoConfirmed(
_converse, _converse.bare_jid,
[{'category': 'pubsub', 'type': 'pep'}],
@ -178,10 +179,9 @@ describe("A chat room", function () {
describe("when bookmarked", function () {
it("will use the nickname from the bookmark", mock.initConverse(
['rosterGroupsFetched'], {}, async function (done, _converse) {
it("will use the nickname from the bookmark", mock.initConverse([], {}, async function (done, _converse) {
const { u } = converse.env;
await mock.waitForRoster(_converse, 'current', 0);
await mock.waitUntilBookmarksReturned(_converse);
const muc_jid = 'coven@chat.shakespeare.lit';
_converse.bookmarks.create({
@ -199,11 +199,10 @@ describe("A chat room", function () {
done();
}));
it("displays that it's bookmarked through its bookmark icon", mock.initConverse(
['rosterGroupsFetched'], {},
async function (done, _converse) {
it("displays that it's bookmarked through its bookmark icon", mock.initConverse([], {}, async function (done, _converse) {
const { u } = converse.env;
await mock.waitForRoster(_converse, 'current', 0);
mock.waitUntilDiscoConfirmed(
_converse, _converse.bare_jid,
[{'category': 'pubsub', 'type': 'pep'}],
@ -225,10 +224,10 @@ describe("A chat room", function () {
done();
}));
it("can be unbookmarked", mock.initConverse(
['rosterGroupsFetched'], {}, async function (done, _converse) {
it("can be unbookmarked", mock.initConverse([], {}, async function (done, _converse) {
const { u, Strophe } = converse.env;
await mock.waitForRoster(_converse, 'current', 0);
await mock.waitUntilBookmarksReturned(_converse);
const muc_jid = 'theplay@conference.shakespeare.lit';
await _converse.api.rooms.open(muc_jid);
@ -292,9 +291,9 @@ describe("A chat room", function () {
describe("and when autojoin is set", function () {
it("will be be opened and joined automatically upon login", mock.initConverse(
['rosterGroupsFetched'], {},
async function (done, _converse) {
[], {}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 0);
await mock.waitUntilBookmarksReturned(_converse);
spyOn(_converse.api.rooms, 'create').and.callThrough();
const jid = 'theplay@conference.shakespeare.lit';
@ -321,9 +320,10 @@ describe("A chat room", function () {
describe("Bookmarks", function () {
it("can be pushed from the XMPP server", mock.initConverse(
['connected', 'rosterGroupsFetched', 'chatBoxesFetched'], {}, async function (done, _converse) {
['connected', 'chatBoxesFetched'], {}, async function (done, _converse) {
const { $msg, u } = converse.env;
await mock.waitForRoster(_converse, 'current', 0);
await mock.waitUntilBookmarksReturned(_converse);
/* The stored data is automatically pushed to all of the user's
@ -410,10 +410,11 @@ describe("Bookmarks", function () {
it("can be retrieved from the XMPP server", mock.initConverse(
['chatBoxesFetched', 'rosterGroupsFetched'], {},
['chatBoxesFetched'], {},
async function (done, _converse) {
const { Strophe, sizzle, u, $iq } = converse.env;
await mock.waitForRoster(_converse, 'current', 0);
await mock.waitUntilDiscoConfirmed(
_converse, _converse.bare_jid,
[{'category': 'pubsub', 'type': 'pep'}],
@ -490,14 +491,14 @@ describe("Bookmarks", function () {
describe("The bookmarks list", function () {
it("shows a list of bookmarks", mock.initConverse(
['rosterGroupsFetched'], {},
async function (done, _converse) {
[], {}, async function (done, _converse) {
await mock.waitUntilDiscoConfirmed(
_converse, _converse.bare_jid,
[{'category': 'pubsub', 'type': 'pep'}],
['http://jabber.org/protocol/pubsub#publish-options']
);
await mock.waitForRoster(_converse, 'current', 0);
mock.openControlBox(_converse);
const IQ_stanzas = _converse.connection.IQ_stanzas;
@ -565,7 +566,7 @@ describe("Bookmarks", function () {
}));
it("can be used to open a MUC from a bookmark", mock.initConverse(
['rosterGroupsFetched'], {'view_mode': 'fullscreen'}, async function (done, _converse) {
[], {'view_mode': 'fullscreen'}, async function (done, _converse) {
const api = _converse.api;
await mock.waitUntilDiscoConfirmed(
@ -573,6 +574,7 @@ describe("Bookmarks", function () {
[{'category': 'pubsub', 'type': 'pep'}],
['http://jabber.org/protocol/pubsub#publish-options']
);
await mock.waitForRoster(_converse, 'current', 0);
await mock.openControlBox(_converse);
const view = await _converse.chatboxviews.get('controlbox');
const IQ_stanzas = _converse.connection.IQ_stanzas;
@ -614,8 +616,9 @@ describe("Bookmarks", function () {
}));
it("remembers the toggle state of the bookmarks list", mock.initConverse(
['rosterGroupsFetched'], {}, async function (done, _converse) {
[], {}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 0);
await mock.openControlBox(_converse);
await mock.waitUntilDiscoConfirmed(
_converse, _converse.bare_jid,
@ -672,10 +675,9 @@ describe("Bookmarks", function () {
describe("When hide_open_bookmarks is true and a bookmarked room is opened", function () {
it("can be closed", mock.initConverse(
['rosterGroupsFetched'],
{ hide_open_bookmarks: true },
async function (done, _converse) {
[], { hide_open_bookmarks: true }, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 0);
await mock.openControlBox(_converse);
await mock.waitUntilBookmarksReturned(_converse);

View File

@ -13,7 +13,7 @@ describe("Chatboxes", function () {
describe("A Chatbox", function () {
it("has a /help command to show the available commands", mock.initConverse(['rosterGroupsFetched', 'chatBoxesFetched'], {}, async function (done, _converse) {
it("has a /help command to show the available commands", mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 1);
await mock.openControlBox(_converse);
@ -72,8 +72,7 @@ describe("Chatboxes", function () {
it("is created when you click on a roster item", mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
['chatBoxesFetched'], {}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse);
@ -84,8 +83,9 @@ describe("Chatboxes", function () {
spyOn(_converse.minimize, 'trimChats');
expect(document.querySelectorAll("#conversejs .chatbox").length).toBe(1); // Controlbox is open
await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group li').length, 700);
const online_contacts = _converse.rosterview.querySelectorAll('.roster-group .current-xmpp-contact a.open-chat');
const rosterview = document.querySelector('converse-roster');
await u.waitUntil(() => rosterview.querySelectorAll('.roster-group li').length, 700);
const online_contacts = rosterview.querySelectorAll('.roster-group .current-xmpp-contact a.open-chat');
expect(online_contacts.length).toBe(17);
let el = online_contacts[0];
el.click();
@ -102,8 +102,8 @@ describe("Chatboxes", function () {
}));
it("opens when a new message is received", mock.initConverse(
['rosterGroupsFetched'], {'allow_non_roster_messaging': true},
async function (done, _converse) {
[], {'allow_non_roster_messaging': true},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 0);
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@ -122,10 +122,7 @@ describe("Chatboxes", function () {
done();
}));
it("doesn't open when a message without body is received", mock.initConverse(
['rosterGroupsFetched'], {},
async function (done, _converse) {
it("doesn't open when a message without body is received", mock.initConverse([], {}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 1);
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
const stanza = u.toStanza(`
@ -142,8 +139,7 @@ describe("Chatboxes", function () {
}));
it("is focused if its already open and you click on its corresponding roster item",
mock.initConverse(['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse);
@ -151,7 +147,8 @@ describe("Chatboxes", function () {
const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit';
const view = await mock.openChatBoxFor(_converse, contact_jid);
const el = sizzle('a.open-chat:contains("'+view.model.getDisplayName()+'")', _converse.rosterview.el).pop();
const rosterview = document.querySelector('converse-roster');
const el = sizzle('a.open-chat:contains("'+view.model.getDisplayName()+'")', rosterview.el).pop();
await u.waitUntil(() => u.isVisible(el));
const textarea = view.querySelector('.chat-textarea');
await u.waitUntil(() => u.isVisible(textarea));
@ -167,9 +164,7 @@ describe("Chatboxes", function () {
}));
it("can be saved to, and retrieved from, browserStorage",
mock.initConverse(
['rosterGroupsFetched'], {},
async function (done, _converse) {
mock.initConverse([], {}, async function (done, _converse) {
spyOn(_converse.minimize, 'trimChats');
await mock.waitForRoster(_converse, 'current');
@ -193,23 +188,21 @@ describe("Chatboxes", function () {
// have the same attributes values as the original ones.
const attrs = ['id', 'box_id', 'visible'];
let new_attrs, old_attrs;
for (var i=0; i<attrs.length; i++) {
for (let i=0; i<attrs.length; i++) {
new_attrs = _.map(_.map(newchatboxes.models, 'attributes'), attrs[i]);
old_attrs = _.map(_.map(_converse.chatboxes.models, 'attributes'), attrs[i]);
expect(_.isEqual(new_attrs, old_attrs)).toEqual(true);
}
_converse.rosterview.render();
done();
}));
it("can be closed by clicking a DOM element with class 'close-chatbox-button'",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
const contact_jid = mock.cur_names[7].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length);
const rosterview = document.querySelector('converse-roster');
await u.waitUntil(() => rosterview.querySelectorAll('.roster-group').length);
await mock.openChatBoxFor(_converse, contact_jid);
const chatview = _converse.chatboxviews.get(contact_jid);
spyOn(chatview, 'close').and.callThrough();
@ -224,14 +217,13 @@ describe("Chatboxes", function () {
}));
it("will be removed from browserStorage when closed",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
spyOn(_converse.minimize, 'trimChats');
await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse);
await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length);
const rosterview = document.querySelector('converse-roster');
await u.waitUntil(() => rosterview.querySelectorAll('.roster-group').length);
spyOn(_converse.api, "trigger").and.callThrough();
mock.closeControlBox();
@ -265,9 +257,7 @@ describe("Chatboxes", function () {
describe("A chat toolbar", function () {
it("shows the remaining character count if a message_limit is configured",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {'message_limit': 200},
async function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {'message_limit': 200}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 3);
await mock.openControlBox(_converse);
@ -305,9 +295,7 @@ describe("Chatboxes", function () {
it("does not show a remaining character count if message_limit is zero",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {'message_limit': 0},
async function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {'message_limit': 0}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 3);
await mock.openControlBox(_converse);
@ -321,9 +309,7 @@ describe("Chatboxes", function () {
it("can contain a button for starting a call",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse);
@ -355,11 +341,7 @@ describe("Chatboxes", function () {
describe("A Chat Status Notification", function () {
it("does not open a new chatbox",
mock.initConverse(
['rosterGroupsFetched'], {},
async function (done, _converse) {
it("does not open a new chatbox", mock.initConverse([], {}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse);
@ -383,14 +365,13 @@ describe("Chatboxes", function () {
describe("An active notification", function () {
it("is sent when the user opens a chat box",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await mock.openControlBox(_converse);
u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length);
const rosterview = document.querySelector('converse-roster');
u.waitUntil(() => rosterview.querySelectorAll('.roster-group').length);
spyOn(_converse.connection, 'send');
await mock.openChatBoxFor(_converse, contact_jid);
const view = _converse.chatboxviews.get(contact_jid);
@ -406,14 +387,14 @@ describe("Chatboxes", function () {
}));
it("is sent when the user maximizes a minimized a chat box", mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
['chatBoxesFetched'], {}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 1);
await mock.openControlBox(_converse);
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length);
const rosterview = document.querySelector('converse-roster');
await u.waitUntil(() => rosterview.querySelectorAll('.roster-group').length);
await mock.openChatBoxFor(_converse, contact_jid);
const view = _converse.chatboxviews.get(contact_jid);
view.model.minimize();
@ -439,15 +420,14 @@ describe("Chatboxes", function () {
describe("A composing notification", function () {
it("is sent as soon as the user starts typing a message which is not a command",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse);
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length);
const rosterview = document.querySelector('converse-roster');
await u.waitUntil(() => rosterview.querySelectorAll('.roster-group').length);
await mock.openChatBoxFor(_converse, contact_jid);
var view = _converse.chatboxviews.get(contact_jid);
expect(view.model.get('chat_state')).toBe('active');
@ -478,15 +458,15 @@ describe("Chatboxes", function () {
}));
it("is NOT sent out if send_chat_state_notifications doesn't allow it",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {'send_chat_state_notifications': []},
mock.initConverse(['chatBoxesFetched'], {'send_chat_state_notifications': []},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse);
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length);
const rosterview = document.querySelector('converse-roster');
await u.waitUntil(() => rosterview.querySelectorAll('.roster-group').length);
await mock.openChatBoxFor(_converse, contact_jid);
var view = _converse.chatboxviews.get(contact_jid);
expect(view.model.get('chat_state')).toBe('active');
@ -501,17 +481,14 @@ describe("Chatboxes", function () {
done();
}));
it("will be shown if received",
mock.initConverse(
['rosterGroupsFetched'], {},
async function (done, _converse) {
it("will be shown if received", mock.initConverse([], {}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse);
// See XEP-0085 https://xmpp.org/extensions/xep-0085.html#definitions
const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length);
const rosterview = document.querySelector('converse-roster');
await u.waitUntil(() => rosterview.querySelectorAll('.roster-group').length);
await mock.openChatBoxFor(_converse, sender_jid);
// <composing> state
@ -553,9 +530,7 @@ describe("Chatboxes", function () {
}));
it("is ignored if it's a composing carbon message sent by this user from a different client",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
await mock.waitUntilDiscoConfirmed(_converse, 'montague.lit', [], ['vcard-temp']);
await u.waitUntil(() => _converse.xmppstatus.vcard.get('fullname'));
@ -593,14 +568,13 @@ describe("Chatboxes", function () {
describe("A paused notification", function () {
it("is sent if the user has stopped typing since 30 seconds",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await mock.openControlBox(_converse);
await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group li').length, 700);
const rosterview = document.querySelector('converse-roster');
await u.waitUntil(() => rosterview.querySelectorAll('.roster-group li').length, 700);
_converse.TIMEOUTS.PAUSED = 200; // Make the timeout shorter so that we can test
await mock.openChatBoxFor(_converse, contact_jid);
const view = _converse.chatboxviews.get(contact_jid);
@ -646,14 +620,11 @@ describe("Chatboxes", function () {
done();
}));
it("will be shown if received",
mock.initConverse(
['rosterGroupsFetched'], {},
async function (done, _converse) {
it("will be shown if received", mock.initConverse([], {}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse);
await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length);
const rosterview = document.querySelector('converse-roster');
await u.waitUntil(() => rosterview.querySelectorAll('.roster-group').length);
// TODO: only show paused state if the previous state was composing
// See XEP-0085 https://xmpp.org/extensions/xep-0085.html#definitions
spyOn(_converse.api, "trigger").and.callThrough();
@ -675,9 +646,7 @@ describe("Chatboxes", function () {
}));
it("will not be shown if it's a paused carbon message that this user sent from a different client",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
await mock.waitUntilDiscoConfirmed(_converse, 'montague.lit', [], ['vcard-temp']);
await u.waitUntil(() => _converse.xmppstatus.vcard.get('fullname'));
@ -713,9 +682,7 @@ describe("Chatboxes", function () {
describe("An inactive notification", function () {
it("is sent if the user has stopped typing since 2 minutes",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
const sent_stanzas = _converse.connection.sent_stanzas;
// Make the timeouts shorter so that we can test
@ -725,7 +692,8 @@ describe("Chatboxes", function () {
await mock.waitForRoster(_converse, 'current');
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await mock.openControlBox(_converse);
await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length, 1000);
const rosterview = document.querySelector('converse-roster');
await u.waitUntil(() => rosterview.querySelectorAll('.roster-group').length, 1000);
await mock.openChatBoxFor(_converse, contact_jid);
const view = _converse.chatboxviews.get(contact_jid);
await u.waitUntil(() => view.model.get('chat_state') === 'active');
@ -776,9 +744,7 @@ describe("Chatboxes", function () {
}));
it("is sent when the user a minimizes a chat box",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse);
@ -797,14 +763,13 @@ describe("Chatboxes", function () {
}));
it("is sent if the user closes a chat box",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await mock.openControlBox(_converse);
await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length);
const rosterview = document.querySelector('converse-roster');
await u.waitUntil(() => rosterview.querySelectorAll('.roster-group').length);
const view = await mock.openChatBoxFor(_converse, contact_jid);
expect(view.model.get('chat_state')).toBe('active');
spyOn(_converse.connection, 'send');
@ -821,9 +786,7 @@ describe("Chatboxes", function () {
}));
it("will clear any other chat status notifications",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse);
@ -862,11 +825,7 @@ describe("Chatboxes", function () {
describe("A gone notification", function () {
it("will be shown if received",
mock.initConverse(
['rosterGroupsFetched'], {},
async function (done, _converse) {
it("will be shown if received", mock.initConverse([], {}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 3);
await mock.openControlBox(_converse);
const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@ -889,17 +848,14 @@ describe("Chatboxes", function () {
describe("On receiving a message correction", function () {
it("will be removed",
mock.initConverse(
['rosterGroupsFetched'], {},
async function (done, _converse) {
it("will be removed", mock.initConverse([], {}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse);
// See XEP-0085 https://xmpp.org/extensions/xep-0085.html#definitions
const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length);
const rosterview = document.querySelector('converse-roster');
await u.waitUntil(() => rosterview.querySelectorAll('.roster-group').length);
await mock.openChatBoxFor(_converse, sender_jid);
// Original message
@ -953,9 +909,7 @@ describe("Chatboxes", function () {
describe("Special Messages", function () {
it("'/clear' can be used to clear messages in a conversation",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse);
@ -998,9 +952,7 @@ describe("Chatboxes", function () {
describe("A ChatBox's Unread Message Count", function () {
it("is incremented when the message is received and ChatBoxView is scrolled up",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 1);
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit',
@ -1021,9 +973,7 @@ describe("Chatboxes", function () {
}));
it("is not incremented when the message is received and ChatBoxView is scrolled down",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 1);
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@ -1040,11 +990,9 @@ describe("Chatboxes", function () {
}));
it("is incremented when message is received, chatbox is scrolled down and the window is not focused",
mock.initConverse(['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
const msgFactory = function () {
return mock.createChatMessage(_converse, sender_jid, 'This message will be unread');
@ -1066,9 +1014,7 @@ describe("Chatboxes", function () {
}));
it("is incremented when message is received, chatbox is scrolled up and the window is not focused",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 1);
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@ -1091,9 +1037,7 @@ describe("Chatboxes", function () {
}));
it("is cleared when ChatBoxView was scrolled down and the window become focused",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 1);
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@ -1119,9 +1063,7 @@ describe("Chatboxes", function () {
}));
it("is not cleared when ChatBoxView was scrolled up and the windows become focused",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 1);
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@ -1151,14 +1093,13 @@ describe("Chatboxes", function () {
describe("A RosterView's Unread Message Count", function () {
it("is updated when message is received and chatbox is scrolled up",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 1);
let msg, indicator_el;
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length, 500);
const rosterview = document.querySelector('converse-roster');
await u.waitUntil(() => rosterview.querySelectorAll('.roster-group').length, 500);
await mock.openChatBoxFor(_converse, sender_jid);
const chatbox = _converse.chatboxes.get(sender_jid);
chatbox.save('scrolled', true);
@ -1166,26 +1107,25 @@ describe("Chatboxes", function () {
await _converse.handleMessageStanza(msg);
await u.waitUntil(() => chatbox.messages.length);
const selector = 'a.open-chat:contains("' + chatbox.get('nickname') + '") .msgs-indicator';
indicator_el = sizzle(selector, _converse.rosterview.el).pop();
indicator_el = sizzle(selector, rosterview.el).pop();
expect(indicator_el.textContent).toBe('1');
msg = mock.createChatMessage(_converse, sender_jid, 'This message will be unread too');
await _converse.handleMessageStanza(msg);
await u.waitUntil(() => chatbox.messages.length > 1);
indicator_el = sizzle(selector, _converse.rosterview.el).pop();
indicator_el = sizzle(selector, rosterview.el).pop();
expect(indicator_el.textContent).toBe('2');
done();
}));
it("is updated when message is received and chatbox is minimized",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 1);
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
let indicator_el, msg;
await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length, 500);
const rosterview = document.querySelector('converse-roster');
await u.waitUntil(() => rosterview.querySelectorAll('.roster-group').length, 500);
await mock.openChatBoxFor(_converse, sender_jid);
const chatbox = _converse.chatboxes.get(sender_jid);
var chatboxview = _converse.chatboxviews.get(sender_jid);
@ -1195,31 +1135,30 @@ describe("Chatboxes", function () {
await _converse.handleMessageStanza(msg);
await u.waitUntil(() => chatbox.messages.length);
const selector = 'a.open-chat:contains("' + chatbox.get('nickname') + '") .msgs-indicator';
indicator_el = sizzle(selector, _converse.rosterview.el).pop();
indicator_el = sizzle(selector, rosterview.el).pop();
expect(indicator_el.textContent).toBe('1');
msg = mock.createChatMessage(_converse, sender_jid, 'This message will be unread too');
await _converse.handleMessageStanza(msg);
await u.waitUntil(() => chatbox.messages.length === 2);
indicator_el = sizzle(selector, _converse.rosterview.el).pop();
indicator_el = sizzle(selector, rosterview.el).pop();
expect(indicator_el.textContent).toBe('2');
done();
}));
it("is cleared when chatbox is maximzied after receiving messages in minimized mode",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 1);
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
const msgFactory = () => mock.createChatMessage(_converse, sender_jid, 'This message will be received as unread, but eventually will be read');
await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length, 500);
const rosterview = document.querySelector('converse-roster');
await u.waitUntil(() => rosterview.querySelectorAll('.roster-group').length, 500);
await mock.openChatBoxFor(_converse, sender_jid);
const chatbox = _converse.chatboxes.get(sender_jid);
const view = _converse.chatboxviews.get(sender_jid);
const selector = 'a.open-chat:contains("' + chatbox.get('nickname') + '") .msgs-indicator';
const select_msgs_indicator = () => sizzle(selector, _converse.rosterview.el).pop();
const select_msgs_indicator = () => sizzle(selector, rosterview.el).pop();
view.minimize();
_converse.handleMessageStanza(msgFactory());
await u.waitUntil(() => chatbox.messages.length);
@ -1233,45 +1172,43 @@ describe("Chatboxes", function () {
}));
it("is cleared when unread messages are viewed which were received in scrolled-up chatbox",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
await mock.openControlBox(_converse);
await mock.waitForRoster(_converse, 'current', 1);
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length, 500);
const rosterview = document.querySelector('converse-roster');
await u.waitUntil(() => rosterview.querySelectorAll('.roster-group').length, 500);
await mock.openChatBoxFor(_converse, sender_jid);
const chatbox = _converse.chatboxes.get(sender_jid);
const msgFactory = () => mock.createChatMessage(_converse, sender_jid, 'This message will be received as unread, but eventually will be read');
const selector = `a.open-chat:contains("${chatbox.get('nickname')}") .msgs-indicator`;
const select_msgs_indicator = () => sizzle(selector, _converse.rosterview.el).pop();
const select_msgs_indicator = () => sizzle(selector, rosterview.el).pop();
chatbox.save('scrolled', true);
_converse.handleMessageStanza(msgFactory());
const view = _converse.chatboxviews.get(sender_jid);
await u.waitUntil(() => view.model.messages.length);
expect(select_msgs_indicator().textContent).toBe('1');
view.viewUnreadMessages();
_converse.rosterview.render();
rosterview.render();
expect(select_msgs_indicator()).toBeUndefined();
done();
}));
it("is not cleared after user clicks on roster view when chatbox is already opened and scrolled up",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 1);
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length, 500);
const rosterview = document.querySelector('converse-roster');
await u.waitUntil(() => rosterview.querySelectorAll('.roster-group').length, 500);
await mock.openChatBoxFor(_converse, sender_jid);
const chatbox = _converse.chatboxes.get(sender_jid);
const view = _converse.chatboxviews.get(sender_jid);
const msg = 'This message will be received as unread, but eventually will be read';
const msgFactory = () => mock.createChatMessage(_converse, sender_jid, msg);
const selector = 'a.open-chat:contains("' + chatbox.get('nickname') + '") .msgs-indicator';
const select_msgs_indicator = () => sizzle(selector, _converse.rosterview.el).pop();
const select_msgs_indicator = () => sizzle(selector, rosterview.el).pop();
chatbox.save('scrolled', true);
_converse.handleMessageStanza(msgFactory());
await u.waitUntil(() => view.model.messages.length);

View File

@ -10,7 +10,7 @@ describe("The Controlbox", function () {
it("can be opened by clicking a DOM element with class 'toggle-controlbox'",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
function (done, _converse) {
// This spec will only pass if the controlbox is not currently
@ -57,7 +57,7 @@ describe("The Controlbox", function () {
it("can be used to add contact and it checks for case-sensivity",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
spyOn(_converse.api, "trigger").and.callThrough();
@ -85,7 +85,7 @@ describe("The Controlbox", function () {
it("shows the number of unread mentions received",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'all');
@ -135,7 +135,7 @@ describe("The Controlbox", function () {
it("shows the user's chat status, which is online by default",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
function (done, _converse) {
mock.openControlBox(_converse);
@ -147,7 +147,7 @@ describe("The Controlbox", function () {
it("can be used to set the current user's chat status",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
await mock.openControlBox(_converse);
@ -175,7 +175,7 @@ describe("The Controlbox", function () {
it("can be used to set a custom status message",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
await mock.openControlBox(_converse);
@ -209,7 +209,7 @@ describe("The 'Add Contact' widget", function () {
it("opens up an add modal when you click on it",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'all');
@ -243,7 +243,7 @@ describe("The 'Add Contact' widget", function () {
it("can be configured to not provide search suggestions",
mock.initConverse(
['rosterGroupsFetched'], {'autocomplete_add_contact': false},
['rosterContactsFetched'], {'autocomplete_add_contact': false},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'all', 0);
@ -275,7 +275,7 @@ describe("The 'Add Contact' widget", function () {
it("integrates with xhr_user_search_url to search for contacts",
mock.initConverse(
['rosterGroupsFetched'],
['rosterContactsFetched'],
{ 'xhr_user_search_url': 'http://example.org/?' },
async function (done, _converse) {
@ -331,7 +331,7 @@ describe("The 'Add Contact' widget", function () {
it("can be configured to not provide search suggestions for XHR search results",
mock.initConverse(
['rosterGroupsFetched'],
['rosterContactsFetched'],
{ 'autocomplete_add_contact': false,
'xhr_user_search_url': 'http://example.org/?' },
async function (done, _converse) {

View File

@ -305,8 +305,7 @@ describe("Converse", function() {
}));
it("has a method 'open' which opens and returns a promise that resolves to a chat model", mock.initConverse(
['rosterGroupsFetched', 'chatBoxesInitialized'], {},
async (done, _converse) => {
['chatBoxesInitialized'], {}, async (done, _converse) => {
const u = converse.env.utils;
await mock.openControlBox(_converse);

View File

@ -7,7 +7,7 @@ describe("A Chat Message", function () {
it("can be sent as a correction by using the up arrow",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 1);
@ -165,7 +165,7 @@ describe("A Chat Message", function () {
it("can be sent as a correction by clicking the pencil icon",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 1);
@ -292,7 +292,7 @@ describe("A Chat Message", function () {
it("can be replaced with a correction",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 1);
@ -357,7 +357,7 @@ describe("A Groupchat Message", function () {
it("can be replaced with a correction",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
const muc_jid = 'lounge@montague.lit';
@ -426,7 +426,7 @@ describe("A Groupchat Message", function () {
it("keeps the same position in history after a correction",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
const muc_jid = 'lounge@montague.lit';
@ -522,7 +522,7 @@ describe("A Groupchat Message", function () {
it("can be sent as a correction by using the up arrow",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
const muc_jid = 'lounge@montague.lit';

View File

@ -12,9 +12,7 @@ describe("Emojis", function () {
afterEach(() => (jasmine.DEFAULT_TIMEOUT_INTERVAL = original_timeout));
it("can be opened by clicking a button in the chat toolbar",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await mock.waitForRoster(_converse, 'current');
@ -32,10 +30,9 @@ describe("Emojis", function () {
}));
it("is opened to autocomplete emojis in the textarea",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 0);
const muc_jid = 'lounge@montague.lit';
await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
const view = _converse.chatboxviews.get(muc_jid);
@ -100,11 +97,10 @@ describe("Emojis", function () {
}));
it("is focused to autocomplete emojis in the textarea",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
const muc_jid = 'lounge@montague.lit';
await mock.waitForRoster(_converse, 'current', 0);
await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
const view = _converse.chatboxviews.get(muc_jid);
await u.waitUntil(() => view.querySelector('converse-emoji-dropdown'));
@ -150,11 +146,10 @@ describe("Emojis", function () {
it("properly inserts emojis into the chat textarea",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
const muc_jid = 'lounge@montague.lit';
await mock.waitForRoster(_converse, 'current', 0);
await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
const view = _converse.chatboxviews.get(muc_jid);
await u.waitUntil(() => view.querySelector('converse-emoji-dropdown'));
@ -195,11 +190,10 @@ describe("Emojis", function () {
it("allows you to search for particular emojis",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
const muc_jid = 'lounge@montague.lit';
await mock.waitForRoster(_converse, 'current', 0);
await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
const view = _converse.chatboxviews.get(muc_jid);
await u.waitUntil(() => view.querySelector('converse-emoji-dropdown'));
@ -256,9 +250,7 @@ describe("Emojis", function () {
describe("A Chat Message", function () {
it("will display larger if it's only emojis",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {'use_system_emojis': true},
async function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {'use_system_emojis': true}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@ -342,9 +334,9 @@ describe("Emojis", function () {
}));
it("can render emojis as images",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {'use_system_emojis': false},
async function (done, _converse) {
mock.initConverse(
['chatBoxesFetched'], {'use_system_emojis': false},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
const contact_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@ -391,7 +383,7 @@ describe("Emojis", function () {
it("can show custom emojis",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'],
['chatBoxesFetched'],
{ emoji_categories: {
"smileys": ":grinning:",
"people": ":thumbsup:",

View File

@ -6,7 +6,7 @@ describe("A XEP-0317 MUC Hat", function () {
it("can be included in a presence stanza",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
const muc_jid = 'lounge@montague.lit';

View File

@ -3,9 +3,9 @@
describe("A headlines box", function () {
it("will not open nor display non-headline messages",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {}, function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 0);
const { $msg } = converse.env;
/* XMPP spam message:
*
@ -31,8 +31,9 @@ describe("A headlines box", function () {
}));
it("will open and display headline messages", mock.initConverse(
['rosterGroupsFetched'], {}, async function (done, _converse) {
[], {}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 0);
const { u, $msg} = converse.env;
/* <message from='notify.example.com'
* to='romeo@im.example.com'
@ -67,8 +68,9 @@ describe("A headlines box", function () {
}));
it("will show headline messages in the controlbox", mock.initConverse(
['rosterGroupsFetched'], {}, async function (done, _converse) {
[], {}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 0);
const { u, $msg} = converse.env;
/* <message from='notify.example.com'
* to='romeo@im.example.com'
@ -103,9 +105,10 @@ describe("A headlines box", function () {
}));
it("will remove headline messages from the controlbox if closed", mock.initConverse(
['rosterGroupsFetched'], {}, async function (done, _converse) {
[], {}, async function (done, _converse) {
const { u, $msg} = converse.env;
await mock.waitForRoster(_converse, 'current', 0);
await mock.openControlBox(_converse);
/* <message from='notify.example.com'
* to='romeo@im.example.com'
@ -145,8 +148,9 @@ describe("A headlines box", function () {
it("will not show a headline messages from a full JID if allow_non_roster_messaging is false",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {}, function (done, _converse) {
['chatBoxesFetched'], {}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 0);
const { $msg } = converse.env;
_converse.allow_non_roster_messaging = false;
const stanza = $msg({

View File

@ -11,7 +11,7 @@ describe("XEP-0363: HTTP File Upload", function () {
it("is done automatically",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
const IQ_stanzas = _converse.connection.IQ_stanzas;
await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, [], []);
@ -163,7 +163,7 @@ describe("XEP-0363: HTTP File Upload", function () {
}));
it("does not appear in MUC chats", mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async (done, _converse) => {
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
@ -204,7 +204,7 @@ describe("XEP-0363: HTTP File Upload", function () {
}));
it("appears in MUC chats", mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async (done, _converse) => {
await mock.waitUntilDiscoConfirmed(
@ -224,7 +224,7 @@ describe("XEP-0363: HTTP File Upload", function () {
describe("when clicked and a file chosen", function () {
it("is uploaded and sent out", mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {} ,async (done, _converse) => {
['rosterContactsFetched', 'chatBoxesFetched'], {} ,async (done, _converse) => {
const base_url = 'https://conversejs.org';
await mock.waitUntilDiscoConfirmed(
@ -561,7 +561,7 @@ describe("XEP-0363: HTTP File Upload", function () {
describe("While a file is being uploaded", function () {
it("shows a progress bar", mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.waitUntilDiscoConfirmed(

View File

@ -9,7 +9,7 @@ describe("A XEP-0333 Chat Marker", function () {
it("is sent when a markable message is received from a roster contact",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 1);
@ -40,7 +40,7 @@ describe("A XEP-0333 Chat Marker", function () {
it("is not sent when a markable message is received from someone not on the roster",
mock.initConverse(
['rosterGroupsFetched'], {'allow_non_roster_messaging': true},
['rosterContactsFetched'], {'allow_non_roster_messaging': true},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 0);
@ -75,7 +75,7 @@ describe("A XEP-0333 Chat Marker", function () {
it("is ignored if it's a carbon copy of one that I sent from a different client",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 1);
@ -124,7 +124,7 @@ describe("A XEP-0333 Chat Marker", function () {
it("may be returned for a MUC message",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');

View File

@ -6,7 +6,7 @@ describe("A Groupchat Message", function () {
it("supports the /me command",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
await mock.waitUntilDiscoConfirmed(_converse, 'montague.lit', [], ['vcard-temp']);
@ -61,7 +61,7 @@ describe("A Groupchat Message", function () {
describe("A Message", function () {
it("supports the /me command", mock.initConverse(['rosterGroupsFetched'], {}, async function (done, _converse) {
it("supports the /me command", mock.initConverse(['rosterContactsFetched'], {}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
await mock.waitUntilDiscoConfirmed(_converse, 'montague.lit', [], ['vcard-temp']);
await u.waitUntil(() => _converse.xmppstatus.vcard.get('fullname'));

View File

@ -8,7 +8,7 @@ describe("An incoming groupchat message", function () {
it("is specially marked when you are mentioned in it",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
const muc_jid = 'lounge@montague.lit';
@ -32,7 +32,7 @@ describe("An incoming groupchat message", function () {
it("highlights all users mentioned via XEP-0372 references",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
const muc_jid = 'lounge@montague.lit';
@ -88,7 +88,7 @@ describe("An incoming groupchat message", function () {
it("highlights all users mentioned via XEP-0372 references in a quoted message",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
const muc_jid = 'lounge@montague.lit';
@ -134,7 +134,7 @@ describe("A sent groupchat message", function () {
it("gets parsed for mentions which get turned into references",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
const muc_jid = 'lounge@montague.lit';
@ -266,7 +266,7 @@ describe("A sent groupchat message", function () {
it("gets parsed for mentions as indicated with an @ preceded by a space or at the start of the text",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
const muc_jid = 'lounge@montague.lit';
@ -300,7 +300,7 @@ describe("A sent groupchat message", function () {
it("properly encodes the URIs in sent out references",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
const muc_jid = 'lounge@montague.lit';
@ -344,7 +344,7 @@ describe("A sent groupchat message", function () {
it("can get corrected and given new references",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
const muc_jid = 'lounge@montague.lit';
@ -443,7 +443,7 @@ describe("A sent groupchat message", function () {
it("includes a XEP-0372 references to that person",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
const muc_jid = 'lounge@montague.lit';
@ -494,7 +494,7 @@ describe("A sent groupchat message", function () {
it("highlights all users mentioned via XEP-0372 references in a quoted message",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
const members = [{'jid': 'gibson@gibson.net', 'nick': 'gibson', 'affiliation': 'member'}];

View File

@ -7,7 +7,7 @@ const u = converse.env.utils;
describe("A Chat Message", function () {
it("will be demarcated if it's the first newly received message",
mock.initConverse(['rosterGroupsFetched', 'chatBoxesFetched'], {},
mock.initConverse(['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 1);
@ -35,7 +35,7 @@ describe("A Chat Message", function () {
it("is rejected if it's an unencapsulated forwarded message",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 2);
@ -80,7 +80,7 @@ describe("A Chat Message", function () {
it("can be received out of order, and will still be displayed in the right order",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
@ -245,7 +245,7 @@ describe("A Chat Message", function () {
it("is ignored if it's a malformed headline message",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
@ -274,7 +274,7 @@ describe("A Chat Message", function () {
it("can be a carbon message, as defined in XEP-0280",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
const include_nick = false;
@ -325,7 +325,7 @@ describe("A Chat Message", function () {
it("can be a carbon message that this user sent from a different client, as defined in XEP-0280",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
await mock.waitUntilDiscoConfirmed(_converse, 'montague.lit', [], ['vcard-temp']);
@ -372,7 +372,7 @@ describe("A Chat Message", function () {
it("will be discarded if it's a malicious message meant to look like a carbon copy",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
@ -418,7 +418,7 @@ describe("A Chat Message", function () {
it("will indicate when it has a time difference of more than a day between it and its predecessor",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
const include_nick = false;
@ -510,7 +510,7 @@ describe("A Chat Message", function () {
it("is sanitized to prevent Javascript injection attacks",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
@ -530,7 +530,7 @@ describe("A Chat Message", function () {
it("can contain hyperlinks, which will be clickable",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
@ -552,7 +552,7 @@ describe("A Chat Message", function () {
it("will remove url query parameters from hyperlinks as set",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'],
['rosterContactsFetched', 'chatBoxesFetched'],
{'filter_url_query_params': ['utm_medium', 'utm_content', 's']},
async function (done, _converse) {
@ -584,7 +584,7 @@ describe("A Chat Message", function () {
it("will render newlines",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
@ -635,7 +635,7 @@ describe("A Chat Message", function () {
it("will render images from their URLs",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
@ -687,7 +687,7 @@ describe("A Chat Message", function () {
it("will render images from approved URLs only",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {'show_images_inline': ['conversejs.org']},
['rosterContactsFetched', 'chatBoxesFetched'], {'show_images_inline': ['conversejs.org']},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
@ -711,7 +711,7 @@ describe("A Chat Message", function () {
it("will fall back to rendering images as URLs",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
@ -755,7 +755,7 @@ describe("A Chat Message", function () {
it("will render the message time as configured",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
@ -781,7 +781,7 @@ describe("A Chat Message", function () {
it("will be correctly identified and rendered as a followup message",
mock.initConverse(
['rosterGroupsFetched'], {'debounced_content_rendering': false},
['rosterContactsFetched'], {'debounced_content_rendering': false},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
@ -943,7 +943,7 @@ describe("A Chat Message", function () {
it("will appear inside the chatbox it was sent from",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
@ -965,7 +965,7 @@ describe("A Chat Message", function () {
it("will be trimmed of leading and trailing whitespace",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 1);
@ -986,7 +986,7 @@ describe("A Chat Message", function () {
it("will open a chatbox and be displayed inside it",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
const include_nick = false;
@ -1032,7 +1032,7 @@ describe("A Chat Message", function () {
it("will be trimmed of leading and trailing whitespace",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 1, false);
@ -1063,7 +1063,7 @@ describe("A Chat Message", function () {
it("the VCard for that user is fetched and the chatbox updated with the results",
mock.initConverse(
['rosterGroupsFetched'], {'allow_non_roster_messaging': true},
['rosterContactsFetched'], {'allow_non_roster_messaging': true},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 0);
@ -1117,7 +1117,7 @@ describe("A Chat Message", function () {
it("will open a chatbox and be displayed inside it if allow_non_roster_messaging is true",
mock.initConverse(
['rosterGroupsFetched'], {'allow_non_roster_messaging': false},
['rosterContactsFetched'], {'allow_non_roster_messaging': false},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 0);
@ -1173,7 +1173,7 @@ describe("A Chat Message", function () {
it("will have the error message displayed after itself",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 1);
@ -1298,7 +1298,7 @@ describe("A Chat Message", function () {
it("will not show to the user an error message for a CSI message",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
// See #1317
@ -1339,7 +1339,7 @@ describe("A Chat Message", function () {
it("will cause the chat area to be scrolled down only if it was at the bottom originally",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
@ -1376,7 +1376,7 @@ describe("A Chat Message", function () {
it("is ignored if it's intended for a different resource and filter_by_resource is set to true",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
@ -1426,7 +1426,7 @@ describe("A Chat Message", function () {
it("will render audio from oob mp3 URLs",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 1);
@ -1476,7 +1476,7 @@ describe("A Chat Message", function () {
it("will render video from oob mp4 URLs",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 1);
@ -1522,7 +1522,7 @@ describe("A Chat Message", function () {
it("will render download links for files from oob URLs",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 1);
@ -1551,7 +1551,7 @@ describe("A Chat Message", function () {
it("will render images from oob URLs",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
const base_url = 'https://conversejs.org';

View File

@ -9,7 +9,7 @@ describe("A chat message", function () {
it("received for a minimized chat box will increment a counter on its header",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
if (_converse.view_mode === 'fullscreen') {
@ -76,7 +76,7 @@ describe("A Groupcaht", function () {
it("can be minimized by clicking a DOM element with class 'toggle-chatbox-button'",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.openChatRoom(_converse, 'lounge', 'montague.lit', 'romeo');
@ -108,7 +108,7 @@ describe("A Chatbox", function () {
it("can be minimized by clicking a DOM element with class 'toggle-chatbox-button'",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
@ -141,7 +141,7 @@ describe("A Chatbox", function () {
it("can be opened in minimized mode initially",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
@ -159,7 +159,7 @@ describe("A Chatbox", function () {
it("can be trimmed to conserve space",
mock.initConverse(['rosterGroupsFetched'], {},
mock.initConverse(['rosterContactsFetched'], {},
async function (done, _converse) {
spyOn(_converse.minimize, 'trimChats');
@ -212,7 +212,7 @@ describe("A Minimized ChatBoxView's Unread Message Count", function () {
it("is displayed when scrolled up chatbox is minimized after receiving unread messages",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 1);
@ -235,7 +235,7 @@ describe("A Minimized ChatBoxView's Unread Message Count", function () {
it("is incremented when message is received and windows is not focused",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 1);
@ -254,7 +254,7 @@ describe("A Minimized ChatBoxView's Unread Message Count", function () {
it("will render Openstreetmap-URL from geo-URI",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 1);
@ -280,7 +280,7 @@ describe("The Minimized Chats Widget", function () {
it("shows chats that have been minimized",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
@ -312,7 +312,7 @@ describe("The Minimized Chats Widget", function () {
it("can be toggled to hide or show minimized chats",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
@ -339,7 +339,7 @@ describe("The Minimized Chats Widget", function () {
it("shows the number messages received to minimized chats",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 4);
@ -415,7 +415,7 @@ describe("The Minimized Chats Widget", function () {
it("shows the number messages received to minimized groupchats",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
const muc_jid = 'kitchen@conference.shakespeare.lit';

View File

@ -22,7 +22,7 @@ describe("The groupchat moderator tool", function () {
it("allows you to set affiliations and roles",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
spyOn(_converse.ChatRoomView.prototype, 'showModeratorToolsModal').and.callThrough();
@ -143,7 +143,7 @@ describe("The groupchat moderator tool", function () {
it("allows you to filter affiliation search results",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
spyOn(_converse.ChatRoomView.prototype, 'showModeratorToolsModal').and.callThrough();
@ -199,7 +199,7 @@ describe("The groupchat moderator tool", function () {
it("allows you to filter role search results",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
spyOn(_converse.ChatRoomView.prototype, 'showModeratorToolsModal').and.callThrough();
@ -310,7 +310,7 @@ describe("The groupchat moderator tool", function () {
it("shows an error message if a particular affiliation list may not be retrieved",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
spyOn(_converse.ChatRoomView.prototype, 'showModeratorToolsModal').and.callThrough();
@ -362,7 +362,7 @@ describe("The groupchat moderator tool", function () {
it("shows an error message if a particular affiliation may not be set",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
spyOn(_converse.ChatRoomView.prototype, 'showModeratorToolsModal').and.callThrough();
@ -429,7 +429,7 @@ describe("The groupchat moderator tool", function () {
it("doesn't allow admins to make more admins",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
spyOn(_converse.ChatRoomView.prototype, 'showModeratorToolsModal').and.callThrough();
@ -466,7 +466,7 @@ describe("The groupchat moderator tool", function () {
it("lets the assignable affiliations and roles be configured via modtools_disable_assign",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
spyOn(_converse.ChatRoomView.prototype, 'showModeratorToolsModal').and.callThrough();

View File

@ -9,7 +9,7 @@ describe("MUC Mention Notfications", function () {
it("may be received from a MUC in which the user is not currently present",
mock.initConverse(
['rosterGroupsFetched'], {
['rosterContactsFetched'], {
'allow_bookmarks': false, // Hack to get the rooms list to render
'muc_subscribe_to_rai': true,
'view_mode': 'fullscreen'},

View File

@ -15,7 +15,7 @@ describe("Groupchats", function () {
it("has a method 'close' which closes rooms by JID or all rooms when called with no arguments",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
@ -50,7 +50,7 @@ describe("Groupchats", function () {
it("has a method 'get' which returns a wrapped groupchat (if it exists)",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
@ -96,7 +96,7 @@ describe("Groupchats", function () {
it("has a method 'open' which opens (optionally configures) and returns a wrapped chat box",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
// Mock 'getDiscoInfo', otherwise the room won't be
@ -266,7 +266,7 @@ describe("Groupchats", function () {
it("will be created when muc_instant_rooms is set to true",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
let IQ_stanzas = _converse.connection.IQ_stanzas;
@ -403,7 +403,7 @@ describe("Groupchats", function () {
it("maintains its state across reloads",
mock.initConverse(
['rosterGroupsFetched'], {
['rosterContactsFetched'], {
'clear_messages_on_reconnection': true,
'enable_smacks': false
}, async function (done, _converse) {
@ -511,7 +511,7 @@ describe("Groupchats", function () {
it("will fetch the member list if muc_fetch_members is true",
mock.initConverse(
['rosterGroupsFetched'], {'muc_fetch_members': true},
['rosterContactsFetched'], {'muc_fetch_members': true},
async function (done, _converse) {
let sent_IQs = _converse.connection.IQ_stanzas;
@ -572,7 +572,7 @@ describe("Groupchats", function () {
it("gracefully handles being forbidden from fetching the lists for certain affiliations",
mock.initConverse(
['rosterGroupsFetched'], {'muc_fetch_members': true},
['rosterContactsFetched'], {'muc_fetch_members': true},
async function (done, _converse) {
const sent_IQs = _converse.connection.IQ_stanzas;
@ -655,7 +655,7 @@ describe("Groupchats", function () {
it("is shown the header",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
await mock.openAndEnterChatRoom(_converse, 'jdev@conference.jabber.org', 'jc');
@ -689,7 +689,7 @@ describe("Groupchats", function () {
it("can be toggled by the user",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
await mock.openAndEnterChatRoom(_converse, 'jdev@conference.jabber.org', 'jc');
@ -731,7 +731,7 @@ describe("Groupchats", function () {
it("will always be shown when it's new",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
await mock.openAndEnterChatRoom(_converse, 'jdev@conference.jabber.org', 'jc');
@ -770,7 +770,7 @@ describe("Groupchats", function () {
it("causes an info message to be shown when received in real-time",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
spyOn(_converse.ChatRoom.prototype, 'handleSubjectChange').and.callThrough();
@ -833,7 +833,7 @@ describe("Groupchats", function () {
it("restores cached messages when it reconnects and clear_messages_on_reconnection and muc_clear_messages_on_leave are false",
mock.initConverse(
['rosterGroupsFetched'],
['rosterContactsFetched'],
{
'clear_messages_on_reconnection': false,
'muc_clear_messages_on_leave': false
@ -866,7 +866,7 @@ describe("Groupchats", function () {
it("clears cached messages when it reconnects and clear_messages_on_reconnection is true",
mock.initConverse(
['rosterGroupsFetched'], {'clear_messages_on_reconnection': true},
['rosterContactsFetched'], {'clear_messages_on_reconnection': true},
async function (done, _converse) {
const muc_jid = 'lounge@montague.lit';
@ -893,7 +893,7 @@ describe("Groupchats", function () {
it("is opened when an xmpp: URI is clicked inside another groupchat",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
@ -922,7 +922,7 @@ describe("Groupchats", function () {
it("shows a notification if it's not anonymous",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
const muc_jid = 'coven@chat.shakespeare.lit';
@ -969,7 +969,7 @@ describe("Groupchats", function () {
it("shows join/leave messages when users enter or exit a groupchat",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {'muc_fetch_members': false},
['rosterContactsFetched', 'chatBoxesFetched'], {'muc_fetch_members': false},
async function (done, _converse) {
const muc_jid = 'coven@chat.shakespeare.lit';
@ -1241,7 +1241,7 @@ describe("Groupchats", function () {
it("combines subsequent join/leave messages when users enter or exit a groupchat",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.openAndEnterChatRoom(_converse, 'coven@chat.shakespeare.lit', 'romeo')
@ -1379,7 +1379,7 @@ describe("Groupchats", function () {
it("doesn't show the disconnection messages when join_leave_events is not in muc_show_info_messages setting",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {'muc_show_info_messages': []},
['rosterContactsFetched', 'chatBoxesFetched'], {'muc_show_info_messages': []},
async function (done, _converse) {
spyOn(_converse.ChatRoom.prototype, 'onOccupantAdded').and.callThrough();
@ -1420,7 +1420,7 @@ describe("Groupchats", function () {
it("role-change messages that follow a MUC leave are left out",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
// See https://github.com/conversejs/converse.js/issues/1259
@ -1477,7 +1477,7 @@ describe("Groupchats", function () {
it("can be configured if you're its owner",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
let sent_IQ, IQ_id;
@ -1688,7 +1688,7 @@ describe("Groupchats", function () {
it("shows all members even if they're not currently present in the groupchat",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
const muc_jid = 'lounge@montague.lit'
@ -1771,7 +1771,7 @@ describe("Groupchats", function () {
it("shows users currently present in the groupchat",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
@ -1825,7 +1825,7 @@ describe("Groupchats", function () {
it("indicates moderators and visitors by means of a special css class and tooltip",
mock.initConverse(
['rosterGroupsFetched'], {'view_mode': 'fullscreen'},
['rosterContactsFetched'], {'view_mode': 'fullscreen'},
async function (done, _converse) {
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
@ -1891,7 +1891,7 @@ describe("Groupchats", function () {
it("properly handles notification that a room has been destroyed",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
await mock.openChatRoomViaModal(_converse, 'problematic@muc.montague.lit', 'romeo')
@ -1921,7 +1921,7 @@ describe("Groupchats", function () {
it("will use the user's reserved nickname, if it exists",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
const IQ_stanzas = _converse.connection.IQ_stanzas;
@ -2014,7 +2014,7 @@ describe("Groupchats", function () {
it("allows the user to invite their roster contacts to enter the groupchat",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {'view_mode': 'fullscreen'},
['rosterContactsFetched', 'chatBoxesFetched'], {'view_mode': 'fullscreen'},
async function (done, _converse) {
// We need roster contacts, so that we have someone to invite
@ -2093,7 +2093,7 @@ describe("Groupchats", function () {
it("can be joined automatically, based upon a received invite",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current'); // We need roster contacts, who can invite us
@ -2129,7 +2129,7 @@ describe("Groupchats", function () {
it("shows received groupchat messages",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
const text = 'This is a received message';
@ -2161,7 +2161,7 @@ describe("Groupchats", function () {
it("shows sent groupchat messages",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
@ -2207,7 +2207,7 @@ describe("Groupchats", function () {
it("will cause the chat area to be scrolled down only if it was at the bottom already",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
const message = 'This message is received while the chat area is scrolled up';
@ -2249,7 +2249,7 @@ describe("Groupchats", function () {
it("reconnects when no-acceptable error is returned when sending a message",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
const muc_jid = 'coven@chat.shakespeare.lit';
@ -2304,7 +2304,7 @@ describe("Groupchats", function () {
it("informs users if the room configuration has changed",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
const muc_jid = 'coven@chat.shakespeare.lit';
@ -2331,7 +2331,7 @@ describe("Groupchats", function () {
it("informs users if their nicknames have been changed.",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
/* The service then sends two presence stanzas to the full JID
@ -2436,7 +2436,7 @@ describe("Groupchats", function () {
it("queries for the groupchat information before attempting to join the user",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
const nick = "some1";
@ -2512,7 +2512,7 @@ describe("Groupchats", function () {
it("updates the shown features when the groupchat configuration has changed",
mock.initConverse(
['rosterGroupsFetched'], {'view_mode': 'fullscreen'},
['rosterContactsFetched'], {'view_mode': 'fullscreen'},
async function (done, _converse) {
let features = [
@ -2718,7 +2718,7 @@ describe("Groupchats", function () {
it("indicates when a room is no longer anonymous",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
let IQ_id;
@ -2767,7 +2767,7 @@ describe("Groupchats", function () {
it("informs users if they have been kicked out of the groupchat",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
/* <presence
@ -2824,7 +2824,7 @@ describe("Groupchats", function () {
it("informs users if they have exited the groupchat due to a technical reason",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
/* <presence
@ -2876,7 +2876,7 @@ describe("Groupchats", function () {
it("can be saved to, and retrieved from, browserStorage",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.openChatRoom(_converse, 'lounge', 'montague.lit', 'romeo');
@ -2910,7 +2910,7 @@ describe("Groupchats", function () {
it("can be closed again by clicking a DOM element with class 'close-chatbox-button'",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.openChatRoom(_converse, 'lounge', 'montague.lit', 'romeo');
@ -2931,7 +2931,7 @@ describe("Groupchats", function () {
it("informs users of role and affiliation changes",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
const muc_jid = 'lounge@montague.lit';
@ -3002,7 +3002,7 @@ describe("Groupchats", function () {
it("notifies users of role and affiliation changes for members not currently in the groupchat",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
const muc_jid = 'lounge@montague.lit';
@ -3049,7 +3049,7 @@ describe("Groupchats", function () {
it("takes /help to show the available commands",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
spyOn(window, 'confirm').and.callFake(() => true);
@ -3148,7 +3148,7 @@ describe("Groupchats", function () {
it("takes /help to show the available commands and commands can be disabled by config",
mock.initConverse(
['rosterGroupsFetched'], {muc_disable_slash_commands: ['mute', 'voice']},
['rosterContactsFetched'], {muc_disable_slash_commands: ['mute', 'voice']},
async function (done, _converse) {
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
@ -3186,7 +3186,7 @@ describe("Groupchats", function () {
it("takes /member to make an occupant a member",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
let iq_stanza;
@ -3331,7 +3331,7 @@ describe("Groupchats", function () {
it("takes /topic to set the groupchat topic",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
@ -3395,7 +3395,7 @@ describe("Groupchats", function () {
it("takes /clear to clear messages",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
@ -3414,7 +3414,7 @@ describe("Groupchats", function () {
it("takes /owner to make a user an owner",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
let sent_IQ, IQ_id;
@ -3506,7 +3506,7 @@ describe("Groupchats", function () {
it("takes /ban to ban a user",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
let sent_IQ, IQ_id;
@ -3606,7 +3606,7 @@ describe("Groupchats", function () {
it("takes a /kick command to kick a user",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
let sent_IQ, IQ_id;
@ -3698,7 +3698,7 @@ describe("Groupchats", function () {
it("takes /op and /deop to make a user a moderator or not",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
const muc_jid = 'lounge@montague.lit';
@ -3839,7 +3839,7 @@ describe("Groupchats", function () {
it("takes /mute and /voice to mute and unmute a user",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
const muc_jid = 'lounge@montague.lit';
@ -3977,7 +3977,7 @@ describe("Groupchats", function () {
it("takes /destroy to destroy a muc",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
const muc_jid = 'lounge@montague.lit';
@ -4072,7 +4072,7 @@ describe("Groupchats", function () {
it("will use the nickname set in the global settings if the user doesn't have a VCard nickname",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {'nickname': 'Benedict-Cucumberpatch'},
['rosterContactsFetched', 'chatBoxesFetched'], {'nickname': 'Benedict-Cucumberpatch'},
async function (done, _converse) {
await mock.openChatRoomViaModal(_converse, 'roomy@muc.montague.lit');
@ -4083,7 +4083,7 @@ describe("Groupchats", function () {
it("will show an error message if the groupchat requires a password",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
const muc_jid = 'protected';
@ -4119,7 +4119,7 @@ describe("Groupchats", function () {
it("will show an error message if the groupchat is members-only and the user not included",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
const muc_jid = 'members-only@muc.montague.lit'
@ -4168,7 +4168,7 @@ describe("Groupchats", function () {
it("will show an error message if the user has been banned",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
const muc_jid = 'off-limits@muc.montague.lit'
@ -4213,7 +4213,7 @@ describe("Groupchats", function () {
it("will render a nickname form if a nickname conflict happens and muc_nickname_from_jid=false",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
const muc_jid = 'conflicted@muc.montague.lit';
@ -4258,7 +4258,7 @@ describe("Groupchats", function () {
it("will automatically choose a new nickname if a nickname conflict happens and muc_nickname_from_jid=true",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
const muc_jid = 'conflicting@muc.montague.lit'
@ -4318,7 +4318,7 @@ describe("Groupchats", function () {
it("will show an error message if the user is not allowed to have created the groupchat",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
const muc_jid = 'impermissable@muc.montague.lit'
@ -4358,7 +4358,7 @@ describe("Groupchats", function () {
it("will show an error message if the user's nickname doesn't conform to groupchat policy",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
const muc_jid = 'conformist@muc.montague.lit'
@ -4399,7 +4399,7 @@ describe("Groupchats", function () {
it("will show an error message if the groupchat doesn't yet exist",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
const muc_jid = 'nonexistent@muc.montague.lit'
@ -4440,7 +4440,7 @@ describe("Groupchats", function () {
it("will show an error message if the groupchat has reached its maximum number of participants",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
const muc_jid = 'maxed-out@muc.montague.lit'
@ -4484,7 +4484,7 @@ describe("Groupchats", function () {
it("will first be added to the member list if the groupchat is members only",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 0);
@ -4632,7 +4632,7 @@ describe("Groupchats", function () {
it("can be computed in various ways",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
await mock.openChatRoom(_converse, 'coven', 'chat.shakespeare.lit', 'romeo');
@ -4699,7 +4699,7 @@ describe("Groupchats", function () {
it("can be opened from a link in the \"Groupchats\" section of the controlbox",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.openControlBox(_converse);
@ -4741,7 +4741,7 @@ describe("Groupchats", function () {
it("doesn't show the nickname field if locked_muc_nickname is true",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {'locked_muc_nickname': true, 'muc_nickname_from_jid': true},
['rosterContactsFetched', 'chatBoxesFetched'], {'locked_muc_nickname': true, 'muc_nickname_from_jid': true},
async function (done, _converse) {
await mock.openControlBox(_converse);
@ -4764,7 +4764,7 @@ describe("Groupchats", function () {
it("uses the JID node if muc_nickname_from_jid is set to true",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {'muc_nickname_from_jid': true},
['rosterContactsFetched', 'chatBoxesFetched'], {'muc_nickname_from_jid': true},
async function (done, _converse) {
await mock.openControlBox(_converse);
@ -4783,7 +4783,7 @@ describe("Groupchats", function () {
it("uses the nickname passed in to converse.initialize",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {'nickname': 'st.nick'},
['rosterContactsFetched', 'chatBoxesFetched'], {'nickname': 'st.nick'},
async function (done, _converse) {
await mock.openControlBox(_converse);
@ -4802,7 +4802,7 @@ describe("Groupchats", function () {
it("doesn't require the domain when muc_domain is set",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {'muc_domain': 'muc.example.org'},
['rosterContactsFetched', 'chatBoxesFetched'], {'muc_domain': 'muc.example.org'},
async function (done, _converse) {
await mock.openControlBox(_converse);
@ -4842,7 +4842,7 @@ describe("Groupchats", function () {
it("only uses the muc_domain is locked_muc_domain is true",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {'muc_domain': 'muc.example.org', 'locked_muc_domain': true},
['rosterContactsFetched', 'chatBoxesFetched'], {'muc_domain': 'muc.example.org', 'locked_muc_domain': true},
async function (done, _converse) {
await mock.openControlBox(_converse);
@ -4884,7 +4884,7 @@ describe("Groupchats", function () {
it("can be opened from a link in the \"Groupchats\" section of the controlbox",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.openControlBox(_converse);
@ -4960,7 +4960,7 @@ describe("Groupchats", function () {
it("is pre-filled with the muc_domain",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'],
['rosterContactsFetched', 'chatBoxesFetched'],
{'muc_domain': 'muc.example.org'},
async function (done, _converse) {
@ -4977,7 +4977,7 @@ describe("Groupchats", function () {
it("doesn't let you set the MUC domain if it's locked",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'],
['rosterContactsFetched', 'chatBoxesFetched'],
{'muc_domain': 'chat.shakespeare.lit', 'locked_muc_domain': true},
async function (done, _converse) {
@ -5028,7 +5028,7 @@ describe("Groupchats", function () {
it("shows the number of unread mentions received",
mock.initConverse(
['rosterGroupsFetched'], {'allow_bookmarks': false},
['rosterContactsFetched'], {'allow_bookmarks': false},
async function (done, _converse) {
await mock.openControlBox(_converse);
@ -5078,7 +5078,7 @@ describe("Groupchats", function () {
it("is is not sent out to a MUC if the user is a visitor in a moderated room",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
spyOn(_converse.ChatRoom.prototype, 'sendChatState').and.callThrough();
@ -5132,7 +5132,7 @@ describe("Groupchats", function () {
it("will be shown if received",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
const muc_jid = 'coven@chat.shakespeare.lit';
@ -5257,7 +5257,7 @@ describe("Groupchats", function () {
it("will be shown if received",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
const muc_jid = 'coven@chat.shakespeare.lit';
@ -5360,7 +5360,7 @@ describe("Groupchats", function () {
it("will receive a user-friendly error message when trying to send a message",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
const muc_jid = 'trollbox@montague.lit';
@ -5410,7 +5410,7 @@ describe("Groupchats", function () {
it("will see an explanatory message instead of a textarea",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
const features = [
@ -5488,7 +5488,7 @@ describe("Groupchats", function () {
it("sends presence probes when muc_send_probes is true",
mock.initConverse(
['rosterGroupsFetched'], {'muc_send_probes': true},
['rosterContactsFetched'], {'muc_send_probes': true},
async function (done, _converse) {
const muc_jid = 'lounge@montague.lit';

View File

@ -13,7 +13,7 @@ describe("A Groupchat Message", function () {
it("will have the error displayed below it",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
const muc_jid = 'lounge@montague.lit';
@ -58,7 +58,7 @@ describe("A Groupchat Message", function () {
it("is not rendered as a followup message",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
const muc_jid = 'lounge@montague.lit';
@ -96,7 +96,7 @@ describe("A Groupchat Message", function () {
it("is not shown if its a duplicate",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
const muc_jid = 'lounge@montague.lit';
@ -131,7 +131,7 @@ describe("A Groupchat Message", function () {
it("is rejected if it's an unencapsulated forwarded message",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
const muc_jid = 'lounge@montague.lit';
@ -166,7 +166,7 @@ describe("A Groupchat Message", function () {
it("can contain a chat state notification and will still be shown",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
const muc_jid = 'lounge@montague.lit';
@ -191,7 +191,7 @@ describe("A Groupchat Message", function () {
it("can not be expected to have a unique id attribute",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
const muc_jid = 'lounge@montague.lit';
@ -222,7 +222,7 @@ describe("A Groupchat Message", function () {
it("is ignored if it has the same archive-id of an already received one",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
const muc_jid = 'room@muc.example.com';
@ -271,7 +271,7 @@ describe("A Groupchat Message", function () {
it("is ignored if it has the same stanza-id of an already received one",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
const muc_jid = 'room@muc.example.com';
@ -319,7 +319,7 @@ describe("A Groupchat Message", function () {
it("will be discarded if it's a malicious message meant to look like a carbon copy",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
@ -378,7 +378,7 @@ describe("A Groupchat Message", function () {
it("keeps track of the sender's role and affiliation",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
const muc_jid = 'lounge@montague.lit';
@ -506,7 +506,7 @@ describe("A Groupchat Message", function () {
it("keeps track whether you are the sender or not",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
const muc_jid = 'lounge@montague.lit';
@ -526,7 +526,7 @@ describe("A Groupchat Message", function () {
it("will be shown as received upon MUC reflection",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
@ -569,7 +569,7 @@ describe("A Groupchat Message", function () {
it("gets updated with its stanza-id upon MUC reflection",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
const muc_jid = 'room@muc.example.com';
@ -604,7 +604,7 @@ describe("A Groupchat Message", function () {
it("can cause a delivery receipt to be returned",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');

View File

@ -6,11 +6,12 @@ const u = converse.env.utils;
describe("A list of open groupchats", function () {
it("is shown in controlbox", mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'],
['chatBoxesFetched'],
{ allow_bookmarks: false // Makes testing easier, otherwise we
// have to mock stanza traffic.
}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 0);
await mock.openControlBox(_converse);
const controlbox = _converse.chatboxviews.get('controlbox');
let list = controlbox.querySelector('.list-container--openrooms');
@ -48,13 +49,14 @@ describe("A list of open groupchats", function () {
it("uses bookmarks to determine groupchat names",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'],
['chatBoxesFetched'],
{'view_mode': 'fullscreen'},
async function (done, _converse) {
const { Strophe, $iq, $pres, sizzle } = converse.env;
const u = converse.env.utils;
await mock.waitForRoster(_converse, 'current', 0);
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
let stanza = $pres({
to: 'romeo@montague.lit/orchard',
@ -111,11 +113,12 @@ describe("A list of open groupchats", function () {
describe("A groupchat shown in the groupchats list", function () {
it("is highlighted if it's currently open", mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'],
['chatBoxesFetched'],
{ view_mode: 'fullscreen',
allow_bookmarks: false // Makes testing easier, otherwise we have to mock stanza traffic.
}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 0);
const controlbox = _converse.chatboxviews.get('controlbox');
const u = converse.env.utils;
const muc_jid = 'coven@chat.shakespeare.lit';
@ -142,7 +145,7 @@ describe("A groupchat shown in the groupchats list", function () {
}));
it("has an info icon which opens a details modal when clicked", mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'],
['chatBoxesFetched'],
{ whitelisted_plugins: ['converse-roomslist'],
allow_bookmarks: false // Makes testing easier, otherwise we
// have to mock stanza traffic.
@ -152,6 +155,7 @@ describe("A groupchat shown in the groupchats list", function () {
const u = converse.env.utils;
const IQ_stanzas = _converse.connection.IQ_stanzas;
const room_jid = 'coven@chat.shakespeare.lit';
await mock.waitForRoster(_converse, 'current', 0);
await mock.openControlBox(_converse);
await _converse.api.rooms.open(room_jid, {'nick': 'some1'});
const view = _converse.chatboxviews.get(room_jid);
@ -250,7 +254,7 @@ describe("A groupchat shown in the groupchats list", function () {
}));
it("can be closed", mock.initConverse(
['rosterGroupsFetched'],
[],
{ whitelisted_plugins: ['converse-roomslist'],
allow_bookmarks: false // Makes testing easier, otherwise we have to mock stanza traffic.
},
@ -259,6 +263,7 @@ describe("A groupchat shown in the groupchats list", function () {
const u = converse.env.utils;
spyOn(window, 'confirm').and.callFake(() => true);
expect(_converse.chatboxes.length).toBe(1);
await mock.waitForRoster(_converse, 'current', 0);
await mock.openChatRoom(_converse, 'lounge', 'conference.shakespeare.lit', 'JC');
expect(_converse.chatboxes.length).toBe(2);

View File

@ -12,7 +12,7 @@ describe("Notifications", function () {
describe("an HTML5 Notification", function () {
it("is shown when a new private message is received",
mock.initConverse(['rosterGroupsFetched'], {}, async (done, _converse) => {
mock.initConverse(['rosterContactsFetched'], {}, async (done, _converse) => {
await mock.waitForRoster(_converse, 'current');
const stub = jasmine.createSpyObj('MyNotification', ['onclick', 'close']);
@ -34,7 +34,7 @@ describe("Notifications", function () {
}));
it("is shown when you are mentioned in a groupchat",
mock.initConverse(['rosterGroupsFetched'], {}, async (done, _converse) => {
mock.initConverse(['rosterContactsFetched'], {}, async (done, _converse) => {
await mock.waitForRoster(_converse, 'current');
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
@ -79,7 +79,7 @@ describe("Notifications", function () {
}));
it("is shown for headline messages",
mock.initConverse(['rosterGroupsFetched'], {}, async (done, _converse) => {
mock.initConverse(['rosterContactsFetched'], {}, async (done, _converse) => {
const stub = jasmine.createSpyObj('MyNotification', ['onclick', 'close']);
spyOn(window, 'Notification').and.returnValue(stub);
@ -124,7 +124,7 @@ describe("Notifications", function () {
}));
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},
mock.initConverse(['rosterContactsFetched'], {show_chat_state_notifications: true},
async (done, _converse) => {
await mock.waitForRoster(_converse, 'current', 3);
@ -153,7 +153,7 @@ describe("Notifications", function () {
describe("A notification sound", function () {
it("is played when the current user is mentioned in a groupchat",
mock.initConverse(['rosterGroupsFetched'], {}, async (done, _converse) => {
mock.initConverse(['rosterContactsFetched'], {}, async (done, _converse) => {
mock.createContacts(_converse, 'current');
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
@ -209,7 +209,7 @@ describe("Notifications", function () {
it("is incremented when the message is received and the window is not focused",
mock.initConverse(
['rosterGroupsFetched'], {'show_tab_notifications': false},
['rosterContactsFetched'], {'show_tab_notifications': false},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
@ -265,7 +265,7 @@ describe("Notifications", function () {
it("is not incremented when the message is received and the window is focused",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
@ -296,7 +296,7 @@ describe("Notifications", function () {
it("is incremented from zero when chatbox was closed after viewing previously received messages and the window is not focused now",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');

View File

@ -73,9 +73,7 @@ async function initializedOMEMO (_converse) {
describe("The OMEMO module", function() {
it("adds methods for encrypting and decrypting messages via AES GCM",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
const message = 'This message will be encrypted'
await mock.waitForRoster(_converse, 'current', 1);
@ -86,9 +84,7 @@ describe("The OMEMO module", function() {
}));
it("enables encrypted messages to be sent and received",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
let sent_stanza;
await mock.waitForRoster(_converse, 'current', 1);
@ -224,9 +220,7 @@ describe("The OMEMO module", function() {
}));
it("enables encrypted groupchat messages to be sent and received",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
// MEMO encryption works only in members only conferences
// that are non-anonymous.
@ -371,9 +365,7 @@ describe("The OMEMO module", function() {
}));
it("will create a new device based on a received carbon message",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, [], [Strophe.NS.SID]);
await mock.waitForRoster(_converse, 'current', 1);
@ -481,9 +473,7 @@ describe("The OMEMO module", function() {
}));
it("gracefully handles auth errors when trying to send encrypted groupchat messages",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
// MEMO encryption works only in members only conferences
// that are non-anonymous.
@ -610,9 +600,7 @@ describe("The OMEMO module", function() {
}));
it("can receive a PreKeySignalMessage",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
_converse.NUM_PREKEYS = 5; // Restrict to 5, otherwise the resulting stanza is too large to easily test
await mock.waitForRoster(_converse, 'current', 1);
@ -707,9 +695,7 @@ describe("The OMEMO module", function() {
}));
it("updates device lists based on PEP messages",
mock.initConverse(
['rosterGroupsFetched'], {'allow_non_roster_messaging': true},
async function (done, _converse) {
mock.initConverse([], {'allow_non_roster_messaging': true}, async function (done, _converse) {
await mock.waitUntilDiscoConfirmed(
_converse, _converse.bare_jid,
@ -881,9 +867,7 @@ describe("The OMEMO module", function() {
it("updates device bundles based on PEP messages",
mock.initConverse(
['rosterGroupsFetched'], {},
async function (done, _converse) {
mock.initConverse([], {}, async function (done, _converse) {
await mock.waitUntilDiscoConfirmed(
_converse, _converse.bare_jid,
@ -1031,9 +1015,7 @@ describe("The OMEMO module", function() {
}));
it("publishes a bundle with which an encrypted session can be created",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
await mock.waitUntilDiscoConfirmed(
_converse, _converse.bare_jid,
@ -1109,9 +1091,7 @@ describe("The OMEMO module", function() {
it("adds a toolbar button for starting an encrypted chat session",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
await mock.waitUntilDiscoConfirmed(
_converse, _converse.bare_jid,
@ -1230,7 +1210,7 @@ describe("The OMEMO module", function() {
expect(devicelist.devices.at(1).get('id')).toBe('3300659945416e274474e469a1f0154c');
expect(devicelist.devices.at(2).get('id')).toBe('4e30f35051b7b8b42abe083742187228');
expect(devicelist.devices.at(3).get('id')).toBe('ae890ac52d0df67ed7cfdf51b644e901');
await u.waitUntil(() => _converse.chatboxviews.get(contact_jid).el.querySelector('.chat-toolbar'));
await u.waitUntil(() => _converse.chatboxviews.get(contact_jid).querySelector('.chat-toolbar'));
const view = _converse.chatboxviews.get(contact_jid);
const toolbar = view.querySelector('.chat-toolbar');
expect(view.model.get('omemo_active')).toBe(undefined);
@ -1238,7 +1218,6 @@ describe("The OMEMO module", function() {
expect(toggle === null).toBe(false);
expect(u.hasClass('fa-unlock', toggle.querySelector('converse-icon'))).toBe(true);
expect(u.hasClass('fa-lock', toggle.querySelector('.converse-icon'))).toBe(false);
view.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
toolbar.querySelector('.toggle-omemo').click();
expect(view.model.get('omemo_active')).toBe(true);
@ -1271,10 +1250,9 @@ describe("The OMEMO module", function() {
}));
it("adds a toolbar button for starting an encrypted groupchat session",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 0);
await mock.waitUntilDiscoConfirmed(
_converse, _converse.bare_jid,
[{'category': 'pubsub', 'type': 'pep'}],
@ -1444,9 +1422,7 @@ describe("The OMEMO module", function() {
it("shows OMEMO device fingerprints in the user details modal",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
await mock.waitUntilDiscoConfirmed(
_converse, _converse.bare_jid,

View File

@ -10,10 +10,9 @@ describe("A sent presence stanza", function () {
afterEach(() => (jasmine.DEFAULT_TIMEOUT_INTERVAL = original_timeout));
it("includes a entity capabilities node",
mock.initConverse(
['rosterGroupsFetched'], {},
(done, _converse) => {
mock.initConverse([], {}, async (done, _converse) => {
await mock.waitForRoster(_converse, 'current', 0);
_converse.api.disco.own.identities.clear();
_converse.api.disco.own.features.clear();
@ -66,9 +65,7 @@ describe("A sent presence stanza", function () {
}));
it("includes the saved status message",
mock.initConverse(
['rosterGroupsFetched'], {},
async (done, _converse) => {
mock.initConverse([], {}, async (done, _converse) => {
const { u, Strophe } = converse.env;
mock.openControlBox(_converse);
@ -116,9 +113,7 @@ describe("A sent presence stanza", function () {
describe("A received presence stanza", function () {
it("has its priority taken into account",
mock.initConverse(
['rosterGroupsFetched'], {},
async (done, _converse) => {
mock.initConverse([], {}, async (done, _converse) => {
const u = converse.env.utils;
mock.openControlBox(_converse);

View File

@ -39,13 +39,11 @@ describe("The Protocol", function () {
* stanza of type "result".
*/
it("Subscribe to contact, contact accepts and subscribes back",
mock.initConverse(
['rosterGroupsFetched'],
{ roster_groups: false },
async function (done, _converse) {
mock.initConverse([], { roster_groups: false }, async function (done, _converse) {
const { u, $iq, $pres, sizzle, Strophe } = converse.env;
let contact, sent_stanza, IQ_id, stanza;
await mock.waitForRoster(_converse, 'current', 0);
await mock.waitUntilDiscoConfirmed(_converse, 'montague.lit', [], ['vcard-temp']);
await u.waitUntil(() => _converse.xmppstatus.vcard.get('fullname'), 300);
/* The process by which a user subscribes to a contact, including
@ -191,6 +189,7 @@ describe("The Protocol", function () {
* </query>
* </iq>
*/
spyOn(_converse.roster, "updateContact").and.callThrough();
stanza = $iq({'type': 'set', 'from': _converse.bare_jid})
.c('query', {'xmlns': 'jabber:iq:roster'})
@ -201,15 +200,17 @@ describe("The Protocol", function () {
'name': 'contact@example.org'});
_converse.connection._dataRecv(mock.createRequest(stanza));
expect(_converse.roster.updateContact).toHaveBeenCalled();
const rosterview = document.querySelector('converse-roster');
// Check that the user is now properly shown as a pending
// contact in the roster.
await u.waitUntil(() => {
const header = sizzle('a:contains("Pending contacts")', _converse.rosterview.el).pop();
const header = sizzle('a:contains("Pending contacts")', rosterview).pop();
const contacts = Array.from(header.parentElement.querySelectorAll('li')).filter(u.isVisible);
return contacts.length;
}, 600);
let header = sizzle('a:contains("Pending contacts")', _converse.rosterview.el).pop();
let header = sizzle('a:contains("Pending contacts")', rosterview).pop();
let contacts = header.parentElement.querySelectorAll('li');
expect(contacts.length).toBe(1);
expect(u.isVisible(contacts[0])).toBe(true);
@ -271,13 +272,12 @@ describe("The Protocol", function () {
);
expect(_converse.roster.updateContact).toHaveBeenCalled();
// The contact should now be visible as an existing
// contact (but still offline).
// The contact should now be visible as an existing contact (but still offline).
await u.waitUntil(() => {
const header = sizzle('a:contains("My contacts")', _converse.rosterview.el);
return sizzle('li', header[0].parentNode).filter(l => u.isVisible(l)).length;
const header = sizzle('a:contains("My contacts")', rosterview).pop();
return sizzle('li', header?.parentNode).filter(l => u.isVisible(l)).length;
}, 600);
header = sizzle('a:contains("My contacts")', _converse.rosterview.el);
header = sizzle('a:contains("My contacts")', rosterview);
expect(header.length).toBe(1);
expect(u.isVisible(header[0])).toBeTruthy();
contacts = header[0].parentNode.querySelectorAll('li');
@ -359,15 +359,14 @@ describe("The Protocol", function () {
}));
it("Alternate Flow: Contact Declines Subscription Request",
mock.initConverse(
['rosterGroupsFetched'], {},
function (done, _converse) {
mock.initConverse([], {}, async function (done, _converse) {
const { $iq, $pres } = converse.env;
/* The process by which a user subscribes to a contact, including
* the interaction between roster items and subscription states.
*/
var contact, stanza, sent_stanza, sent_IQ;
await mock.waitForRoster(_converse, 'current', 0);
mock.openControlBox(_converse);
// Add a new roster contact via roster push
stanza = $iq({'type': 'set'}).c('query', {'xmlns': 'jabber:iq:roster'})
@ -443,10 +442,7 @@ describe("The Protocol", function () {
}));
it("Unsubscribe to a contact when subscription is mutual",
mock.initConverse(
['rosterGroupsFetched'],
{ roster_groups: false },
async function (done, _converse) {
mock.initConverse([], { roster_groups: false }, async function (done, _converse) {
const { u, $iq, sizzle, Strophe } = converse.env;
const jid = 'abram@montague.lit';
@ -456,7 +452,8 @@ describe("The Protocol", function () {
// We now have a contact we want to remove
expect(_converse.roster.get(jid) instanceof _converse.RosterContact).toBeTruthy();
const header = sizzle('a:contains("My contacts")', _converse.rosterview.el).pop();
const rosterview = document.querySelector('converse-roster');
const header = sizzle('a:contains("My contacts")', rosterview).pop();
await u.waitUntil(() => header.parentElement.querySelectorAll('li').length);
// remove the first user
@ -483,6 +480,7 @@ describe("The Protocol", function () {
* </iq>
*/
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">`+
@ -500,8 +498,7 @@ describe("The Protocol", function () {
}));
it("Receiving a subscription request", mock.initConverse(
['rosterGroupsFetched'], {},
async function (done, _converse) {
[], {}, async function (done, _converse) {
const { u, $pres, sizzle, Strophe } = converse.env;
spyOn(_converse.api, "trigger").and.callThrough();
@ -519,16 +516,19 @@ describe("The Protocol", function () {
}).c('nick', {
'xmlns': Strophe.NS.NICK,
}).t('Clint Contact');
_converse.connection._dataRecv(mock.createRequest(stanza));
const rosterview = document.querySelector('converse-roster');
await u.waitUntil(() => {
const header = sizzle('a:contains("Contact requests")', _converse.rosterview.el).pop();
const contacts = Array.from(header.parentElement.querySelectorAll('li')).filter(u.isVisible);
return contacts.length;
const header = sizzle('a:contains("Contact requests")', rosterview).pop();
return Array.from(header?.parentElement.querySelectorAll('li') ?? []).filter(u.isVisible)?.length;
}, 500);
expect(_converse.api.trigger).toHaveBeenCalledWith('contactRequest', jasmine.any(Object));
const header = sizzle('a:contains("Contact requests")', _converse.rosterview.el).pop();
const header = sizzle('a:contains("Contact requests")', rosterview).pop();
expect(u.isVisible(header)).toBe(true);
const contacts = header.parentElement.querySelectorAll('li');
const contacts = header.nextElementSibling.querySelectorAll('li');
expect(contacts.length).toBe(1);
done();
}));

View File

@ -13,7 +13,7 @@ describe("XEP-0357 Push Notifications", function () {
it("can be enabled",
mock.initConverse(
['rosterGroupsFetched'], {
[], {
'push_app_servers': [{
'jid': 'push-5@client.example',
'node': 'yxs32uqsflafdk3iuqo'
@ -51,7 +51,7 @@ describe("XEP-0357 Push Notifications", function () {
it("can be enabled for a MUC domain",
mock.initConverse(
['rosterGroupsFetched'], {
[], {
'enable_muc_push': true,
'push_app_servers': [{
'jid': 'push-5@client.example',
@ -68,10 +68,7 @@ describe("XEP-0357 Push Notifications", function () {
_converse, _converse.bare_jid, [],
['urn:xmpp:push:0']);
let iq = await u.waitUntil(() => _.filter(
IQ_stanzas,
iq => sizzle(`iq[type="set"] enable[xmlns="${Strophe.NS.PUSH}"]`, iq).length
).pop());
let iq = await u.waitUntil(() => IQ_stanzas.filter(iq => sizzle(`iq[type="set"] enable[xmlns="${Strophe.NS.PUSH}"]`, iq).length).pop());
expect(Strophe.serialize(iq)).toBe(
`<iq id="${iq.getAttribute('id')}" type="set" xmlns="jabber:client">`+
@ -111,7 +108,7 @@ describe("XEP-0357 Push Notifications", function () {
it("can be disabled",
mock.initConverse(
['rosterGroupsFetched'], {
[], {
'push_app_servers': [{
'jid': 'push-5@client.example',
'node': 'yxs32uqsflafdk3iuqo',
@ -127,9 +124,7 @@ describe("XEP-0357 Push Notifications", function () {
_converse.bare_jid,
[{'category': 'account', 'type':'registered'}],
['urn:xmpp:push:0'], [], 'info');
const stanza = await u.waitUntil(
() => _.filter(IQ_stanzas, iq => iq.querySelector('iq[type="set"] disable[xmlns="urn:xmpp:push:0"]')).pop()
);
const stanza = await u.waitUntil(() => IQ_stanzas.filter(iq => iq.querySelector('iq[type="set"] disable[xmlns="urn:xmpp:push:0"]')).pop());
expect(Strophe.serialize(stanza)).toEqual(
`<iq id="${stanza.getAttribute('id')}" type="set" xmlns="jabber:client">`+
'<disable jid="push-5@client.example" node="yxs32uqsflafdk3iuqo" xmlns="urn:xmpp:push:0"/>'+
@ -146,8 +141,7 @@ describe("XEP-0357 Push Notifications", function () {
it("can require a secret token to be included",
mock.initConverse(
['rosterGroupsFetched'], {
mock.initConverse([], {
'push_app_servers': [{
'jid': 'push-5@client.example',
'node': 'yxs32uqsflafdk3iuqo',
@ -168,9 +162,7 @@ describe("XEP-0357 Push Notifications", function () {
[{'category': 'account', 'type':'registered'}],
['urn:xmpp:push:0'], [], 'info');
const stanza = await u.waitUntil(
() => _.filter(IQ_stanzas, iq => iq.querySelector('iq[type="set"] enable[xmlns="urn:xmpp:push:0"]')).pop()
);
const stanza = await u.waitUntil(() => IQ_stanzas.filter(iq => iq.querySelector('iq[type="set"] enable[xmlns="urn:xmpp:push:0"]')).pop());
expect(Strophe.serialize(stanza)).toEqual(
`<iq id="${stanza.getAttribute('id')}" type="set" xmlns="jabber:client">`+
'<enable jid="push-5@client.example" node="yxs32uqsflafdk3iuqo" xmlns="urn:xmpp:push:0">'+

View File

@ -9,7 +9,7 @@ describe("XEP-0437 Room Activity Indicators", function () {
it("will be activated for a MUC that becomes hidden",
mock.initConverse(
['rosterGroupsFetched'], {
['rosterContactsFetched'], {
'allow_bookmarks': false, // Hack to get the rooms list to render
'muc_subscribe_to_rai': true,
'view_mode': 'fullscreen'},
@ -116,7 +116,7 @@ describe("XEP-0437 Room Activity Indicators", function () {
it("will be activated for a MUC that starts out hidden",
mock.initConverse(
['rosterGroupsFetched'], {
['rosterContactsFetched'], {
'allow_bookmarks': false, // Hack to get the rooms list to render
'muc_subscribe_to_rai': true,
'view_mode': 'fullscreen'},
@ -181,7 +181,7 @@ describe("XEP-0437 Room Activity Indicators", function () {
it("may not be activated due to server resource constraints",
mock.initConverse(
['rosterGroupsFetched'], {
['rosterContactsFetched'], {
'allow_bookmarks': false, // Hack to get the rooms list to render
'muc_subscribe_to_rai': true,
'view_mode': 'fullscreen'},

View File

@ -8,7 +8,7 @@ describe("A delivery receipt", function () {
it("is emitted for a received message which requests it",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
@ -34,7 +34,7 @@ describe("A delivery receipt", function () {
it("is not emitted for a carbon message",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 1);
@ -64,7 +64,7 @@ describe("A delivery receipt", function () {
it("is not emitted for an archived message",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 1);
@ -101,7 +101,7 @@ describe("A delivery receipt", function () {
it("can be received for a sent message",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 1);

View File

@ -38,7 +38,7 @@ describe("Message Retractions", function () {
it("is not applied if it's not from the right author",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
const muc_jid = 'lounge@montague.lit';
@ -82,7 +82,7 @@ describe("Message Retractions", function () {
it("can be received before the message it pertains to",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
const date = (new Date()).toISOString();
@ -138,7 +138,7 @@ describe("Message Retractions", function () {
it("can be received before the message it pertains to",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
const date = (new Date()).toISOString();
@ -199,7 +199,7 @@ describe("Message Retractions", function () {
it("can be received before the message it pertains to",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
const date = (new Date()).toISOString();
@ -257,7 +257,7 @@ describe("Message Retractions", function () {
it("can be followed up by a retraction",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 1);
@ -327,7 +327,7 @@ describe("Message Retractions", function () {
it("can be retracted by its author",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 1);
@ -377,7 +377,7 @@ describe("Message Retractions", function () {
it("can be followed up by a retraction by the author",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
const muc_jid = 'lounge@montague.lit';
@ -421,7 +421,7 @@ describe("Message Retractions", function () {
it("can be retracted by a moderator, with the IQ response received before the retraction message",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
const muc_jid = 'lounge@montague.lit';
@ -507,7 +507,7 @@ describe("Message Retractions", function () {
it("can not be retracted if the MUC doesn't support message moderation",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
const muc_jid = 'lounge@montague.lit';
@ -533,7 +533,7 @@ describe("Message Retractions", function () {
it("can be retracted by a moderator, with the retraction message received before the IQ response",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
const muc_jid = 'lounge@montague.lit';
@ -604,7 +604,7 @@ describe("Message Retractions", function () {
it("can be retracted by its author",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
const muc_jid = 'lounge@montague.lit';
@ -662,7 +662,7 @@ describe("Message Retractions", function () {
it("can be retracted by its author, causing an error message in response",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
const muc_jid = 'lounge@montague.lit';
@ -711,7 +711,7 @@ describe("Message Retractions", function () {
it("can be retracted by its author, causing a timeout error in response",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
_converse.STANZA_TIMEOUT = 1;
@ -748,7 +748,7 @@ describe("Message Retractions", function () {
it("can be retracted by a moderator",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
const muc_jid = 'lounge@montague.lit';
@ -800,7 +800,7 @@ describe("Message Retractions", function () {
it("can be retracted by the sender if they're a moderator",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {'allow_message_retraction': 'moderator'},
['rosterContactsFetched', 'chatBoxesFetched'], {'allow_message_retraction': 'moderator'},
async function (done, _converse) {
const muc_jid = 'lounge@montague.lit';

View File

@ -12,7 +12,7 @@ describe("Chatrooms", function () {
it("allows you to register your nickname in a room",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {'auto_register_muc_nickname': true},
['rosterContactsFetched', 'chatBoxesFetched'], {'auto_register_muc_nickname': true},
async function (done, _converse) {
const muc_jid = 'coven@chat.shakespeare.lit';
@ -69,7 +69,7 @@ describe("Chatrooms", function () {
it("allows you to automatically register your nickname when joining a room",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {'auto_register_muc_nickname': true},
['rosterContactsFetched', 'chatBoxesFetched'], {'auto_register_muc_nickname': true},
async function (done, _converse) {
const muc_jid = 'coven@chat.shakespeare.lit';

File diff suppressed because it is too large Load Diff

View File

@ -18,9 +18,6 @@ describe("XEP-0198 Stream Management", function () {
},
async function (done, _converse) {
const view = _converse.chatboxviews.get('controlbox');
spyOn(view, 'renderControlBoxPane').and.callThrough();
await _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'), 1000).pop());
@ -32,8 +29,6 @@ describe("XEP-0198 Stream Management", function () {
_converse.connection._dataRecv(mock.createRequest(result));
expect(_converse.session.get('smacks_enabled')).toBe(true);
await u.waitUntil(() => view.renderControlBoxPane.calls?.count());
let IQ_stanzas = _converse.connection.IQ_stanzas;
await u.waitUntil(() => IQ_stanzas.length === 4);

View File

@ -8,9 +8,7 @@ describe("A spoiler message", function () {
afterEach(() => (jasmine.DEFAULT_TIMEOUT_INTERVAL = original_timeout));
it("can be received with a hint",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async (done, _converse) => {
mock.initConverse(['chatBoxesFetched'], {}, async (done, _converse) => {
await mock.waitForRoster(_converse, 'current');
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@ -48,9 +46,7 @@ describe("A spoiler message", function () {
}));
it("can be received without a hint",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async (done, _converse) => {
mock.initConverse(['chatBoxesFetched'], {}, async (done, _converse) => {
await mock.waitForRoster(_converse, 'current');
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@ -87,9 +83,7 @@ describe("A spoiler message", function () {
}));
it("can be sent without a hint",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async (done, _converse) => {
mock.initConverse(['chatBoxesFetched'], {}, async (done, _converse) => {
await mock.waitForRoster(_converse, 'current', 1);
mock.openControlBox(_converse);
@ -166,9 +160,7 @@ describe("A spoiler message", function () {
}));
it("can be sent with a hint",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async (done, _converse) => {
mock.initConverse(['chatBoxesFetched'], {}, async (done, _converse) => {
await mock.waitForRoster(_converse, 'current', 1);
mock.openControlBox(_converse);

View File

@ -5,7 +5,7 @@ const { u, Promise, $msg } = converse.env;
describe("An incoming chat Message", function () {
it("can have styling disabled via an \"unstyled\" element",
mock.initConverse(['rosterGroupsFetched', 'chatBoxesFetched'], {},
mock.initConverse(['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
const include_nick = false;
@ -37,7 +37,7 @@ describe("An incoming chat Message", function () {
it("can have styling disabled via the allow_message_styling setting",
mock.initConverse(['rosterGroupsFetched', 'chatBoxesFetched'], {'allow_message_styling': false},
mock.initConverse(['rosterContactsFetched', 'chatBoxesFetched'], {'allow_message_styling': false},
async function (done, _converse) {
const include_nick = false;
@ -67,7 +67,7 @@ describe("An incoming chat Message", function () {
}));
it("can be styled with span XEP-0393 message styling hints",
mock.initConverse(['rosterGroupsFetched', 'chatBoxesFetched'], {},
mock.initConverse(['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
let msg_text, msg, msg_el;
@ -192,7 +192,7 @@ describe("An incoming chat Message", function () {
}));
it("can be styled with block XEP-0393 message styling hints",
mock.initConverse(['rosterGroupsFetched', 'chatBoxesFetched'], {},
mock.initConverse(['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
let msg_text, msg, msg_el;
@ -240,7 +240,7 @@ describe("An incoming chat Message", function () {
}));
it("can be styled with quote XEP-0393 message styling hints",
mock.initConverse(['rosterGroupsFetched', 'chatBoxesFetched'], {},
mock.initConverse(['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
let msg_text, msg, msg_el;
@ -392,7 +392,7 @@ describe("An incoming chat Message", function () {
describe("A outgoing groupchat Message", function () {
it("can be styled with span XEP-0393 message styling hints that contain mentions",
mock.initConverse(['rosterGroupsFetched', 'chatBoxesFetched'], {},
mock.initConverse(['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
const muc_jid = 'lounge@montague.lit';

View File

@ -5,9 +5,7 @@ const u = converse.env.utils;
describe("The User Details Modal", function () {
it("can be used to remove a contact",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 1);
_converse.api.trigger('rosterContactsFetched');
@ -36,7 +34,7 @@ describe("The User Details Modal", function () {
}));
it("shows an alert when an error happened while removing the contact",
mock.initConverse(['rosterGroupsFetched'], {}, async function (done, _converse) {
mock.initConverse([], {}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 1);
_converse.api.trigger('rosterContactsFetched');

View File

@ -9,7 +9,7 @@ describe("XSS", function () {
it("will escape IMG payload XSS attempts",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
spyOn(window, 'alert').and.callThrough();
@ -69,7 +69,7 @@ describe("XSS", function () {
it("will escape SVG payload XSS attempts",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
spyOn(window, 'alert').and.callThrough();
@ -128,7 +128,7 @@ describe("XSS", function () {
it("will have properly escaped URLs",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
@ -186,7 +186,7 @@ describe("XSS", function () {
it("will avoid malformed and unsafe urls urls from rendering as anchors",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
['rosterContactsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
@ -270,7 +270,7 @@ describe("XSS", function () {
describe("A Groupchat", function () {
it("escapes occupant nicknames when rendering them, to avoid JS-injection attacks",
mock.initConverse(['rosterGroupsFetched'], {},
mock.initConverse(['rosterContactsFetched'], {},
async function (done, _converse) {
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
@ -303,7 +303,7 @@ describe("XSS", function () {
it("escapes the subject before rendering it, to avoid JS-injection attacks",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterContactsFetched'], {},
async function (done, _converse) {
await mock.openAndEnterChatRoom(_converse, 'jdev@conference.jabber.org', 'jc');

View File

@ -12,7 +12,7 @@ import { CustomElement } from './element.js';
import { __ } from '../i18n';
import { _converse, api, converse } from '@converse/headless/core';
import { html } from 'lit-element';
import { renderAvatar } from './../templates/directives/avatar';
import { renderAvatar } from 'templates/directives/avatar';
const { Strophe } = converse.env;
const u = converse.env.utils;

View File

@ -2,6 +2,7 @@ import RosterContact from './contact.js';
import log from "@converse/headless/log";
import sum from 'lodash/sum';
import { Collection } from "@converse/skeletor/src/collection";
import { Model } from "@converse/skeletor/src/model";
import { __ } from 'i18n';
import { _converse, api, converse } from "@converse/headless/core";
@ -12,19 +13,11 @@ const u = converse.env.utils;
const RosterContacts = Collection.extend({
model: RosterContact,
comparator (contact1, contact2) {
// Groups are sorted alphabetically, ignoring case.
// However, Ungrouped, Requesting Contacts and Pending Contacts
// appear last and in that order.
const status1 = contact1.presence.get('show') || 'offline';
const status2 = contact2.presence.get('show') || 'offline';
if (_converse.STATUS_WEIGHTS[status1] === _converse.STATUS_WEIGHTS[status2]) {
const name1 = (contact1.getDisplayName()).toLowerCase();
const name2 = (contact2.getDisplayName()).toLowerCase();
return name1 < name2 ? -1 : (name1 > name2? 1 : 0);
} else {
return _converse.STATUS_WEIGHTS[status1] < _converse.STATUS_WEIGHTS[status2] ? -1 : 1;
}
initialize () {
const id = `roster.state-${_converse.bare_jid}-${this.get('jid')}`;
this.state = new Model({ id, 'collapsed_groups': [] });
this.state.browserStorage = _converse.createStore(id);
this.state.fetch();
},
onConnected () {
@ -302,7 +295,6 @@ const RosterContacts = Collection.extend({
*/
updateContact (item) {
const jid = item.getAttribute('jid');
const contact = this.get(jid);
const subscription = item.getAttribute("subscription");
const ask = item.getAttribute("ask");

View File

@ -1,18 +0,0 @@
import { Model } from '@converse/skeletor/src/model.js';
import { __ } from 'i18n';
import { _converse } from "@converse/headless/core";
const RosterGroup = Model.extend({
initialize (attributes) {
this.set(Object.assign({
description: __('Click to hide these contacts'),
state: _converse.OPENED
}, attributes));
// Collection of contacts belonging to this group.
this.contacts = new _converse.RosterContacts();
}
});
export default RosterGroup;

View File

@ -1,58 +0,0 @@
import RosterGroup from './group.js';
import { Collection } from "@converse/skeletor/src/collection";
import { _converse } from "@converse/headless/core";
/**
* @class
*/
const RosterGroups = Collection.extend({
model: RosterGroup,
comparator (a, b) {
const HEADER_WEIGHTS = {};
HEADER_WEIGHTS[_converse.HEADER_UNREAD] = 0;
HEADER_WEIGHTS[_converse.HEADER_REQUESTING_CONTACTS] = 1;
HEADER_WEIGHTS[_converse.HEADER_CURRENT_CONTACTS] = 2;
HEADER_WEIGHTS[_converse.HEADER_UNGROUPED] = 3;
HEADER_WEIGHTS[_converse.HEADER_PENDING_CONTACTS] = 4;
a = a.get('name');
b = b.get('name');
const WEIGHTS = HEADER_WEIGHTS;
const special_groups = Object.keys(HEADER_WEIGHTS);
const a_is_special = special_groups.includes(a);
const b_is_special = special_groups.includes(b);
if (!a_is_special && !b_is_special ) {
return a.toLowerCase() < b.toLowerCase() ? -1 : (a.toLowerCase() > b.toLowerCase() ? 1 : 0);
} else if (a_is_special && b_is_special) {
return WEIGHTS[a] < WEIGHTS[b] ? -1 : (WEIGHTS[a] > WEIGHTS[b] ? 1 : 0);
} else if (!a_is_special && b_is_special) {
const a_header = _converse.HEADER_CURRENT_CONTACTS;
return WEIGHTS[a_header] < WEIGHTS[b] ? -1 : (WEIGHTS[a_header] > WEIGHTS[b] ? 1 : 0);
} else if (a_is_special && !b_is_special) {
const b_header = _converse.HEADER_CURRENT_CONTACTS;
return WEIGHTS[a] < WEIGHTS[b_header] ? -1 : (WEIGHTS[a] > WEIGHTS[b_header] ? 1 : 0);
}
},
/**
* Fetches all the roster groups from sessionStorage.
* @private
* @method _converse.RosterGroups#fetchRosterGroups
* @returns { Promise } - A promise which resolves once the groups have been fetched.
*/
fetchRosterGroups () {
return new Promise(success => {
this.fetch({
success,
// We need to first have all groups before
// we can start positioning them, so we set
// 'silent' to true.
silent: true,
});
});
}
});
export default RosterGroups;

View File

@ -6,8 +6,6 @@
import "@converse/headless/plugins/status";
import RosterContact from './contact.js';
import RosterContacts from './contacts.js';
import RosterGroup from './group.js';
import RosterGroups from './groups.js';
import invoke from 'lodash/invoke';
import log from "@converse/headless/log";
import roster_api from './api.js';
@ -37,7 +35,6 @@ converse.plugins.add('converse-roster', {
'cachedRoster',
'roster',
'rosterContactsFetched',
'rosterGroupsFetched',
'rosterInitialized',
]);
@ -96,16 +93,6 @@ converse.plugins.add('converse-roster', {
_converse.send_initial_presence = true;
}
try {
await _converse.rostergroups.fetchRosterGroups();
/**
* Triggered once roster groups have been fetched. Used by the
* `converse-rosterview.js` plugin to know when it can start alphabetically
* position roster groups.
* @event _converse#rosterGroupsFetched
* @example _converse.api.listen.on('rosterGroupsFetched', () => { ... });
* @example _converse.api.waitUntil('rosterGroupsFetched').then(() => { ... });
*/
api.trigger('rosterGroupsFetched');
await _converse.roster.fetchRosterContacts();
api.trigger('rosterContactsFetched');
} catch (reason) {
@ -119,8 +106,6 @@ converse.plugins.add('converse-roster', {
_converse.Presences = Presences;
_converse.RosterContact = RosterContact;
_converse.RosterContacts = RosterContacts;
_converse.RosterGroup = RosterGroup;
_converse.RosterGroups = RosterGroups;
_converse.unregisterPresenceHandler = function () {
if (_converse.presence_ref !== undefined) {

View File

@ -15,12 +15,8 @@ export async function initRoster () {
_converse.roster.data.id = id;
_converse.roster.data.browserStorage = _converse.createStore(id);
_converse.roster.data.fetch();
id = `converse.roster.groups${_converse.bare_jid}`;
_converse.rostergroups = new _converse.RosterGroups();
_converse.rostergroups.browserStorage = _converse.createStore(id);
/**
* Triggered once the `_converse.RosterContacts` and `_converse.RosterGroups` have
* Triggered once the `_converse.RosterContacts`
* been created, but not yet populated with data.
* This event is useful when you want to create views for these collections.
* @event _converse#chatBoxMaximized
@ -42,3 +38,42 @@ export function updateUnreadCounter (chatbox) {
export async function clearPresences () {
await _converse.presences?.clearStore();
}
export function contactsComparator (contact1, contact2) {
const status1 = contact1.presence.get('show') || 'offline';
const status2 = contact2.presence.get('show') || 'offline';
if (_converse.STATUS_WEIGHTS[status1] === _converse.STATUS_WEIGHTS[status2]) {
const name1 = (contact1.getDisplayName()).toLowerCase();
const name2 = (contact2.getDisplayName()).toLowerCase();
return name1 < name2 ? -1 : (name1 > name2? 1 : 0);
} else {
return _converse.STATUS_WEIGHTS[status1] < _converse.STATUS_WEIGHTS[status2] ? -1 : 1;
}
}
export function groupsComparator (a, b) {
const HEADER_WEIGHTS = {};
HEADER_WEIGHTS[_converse.HEADER_UNREAD] = 0;
HEADER_WEIGHTS[_converse.HEADER_REQUESTING_CONTACTS] = 1;
HEADER_WEIGHTS[_converse.HEADER_CURRENT_CONTACTS] = 2;
HEADER_WEIGHTS[_converse.HEADER_UNGROUPED] = 3;
HEADER_WEIGHTS[_converse.HEADER_PENDING_CONTACTS] = 4;
const WEIGHTS = HEADER_WEIGHTS;
const special_groups = Object.keys(HEADER_WEIGHTS);
const a_is_special = special_groups.includes(a);
const b_is_special = special_groups.includes(b);
if (!a_is_special && !b_is_special ) {
return a.toLowerCase() < b.toLowerCase() ? -1 : (a.toLowerCase() > b.toLowerCase() ? 1 : 0);
} else if (a_is_special && b_is_special) {
return WEIGHTS[a] < WEIGHTS[b] ? -1 : (WEIGHTS[a] > WEIGHTS[b] ? 1 : 0);
} else if (!a_is_special && b_is_special) {
const a_header = _converse.HEADER_CURRENT_CONTACTS;
return WEIGHTS[a_header] < WEIGHTS[b] ? -1 : (WEIGHTS[a_header] > WEIGHTS[b] ? 1 : 0);
} else if (a_is_special && !b_is_special) {
const b_header = _converse.HEADER_CURRENT_CONTACTS;
return WEIGHTS[a] < WEIGHTS[b_header] ? -1 : (WEIGHTS[a] > WEIGHTS[b_header] ? 1 : 0);
}
}

View File

@ -85,7 +85,7 @@ const UserDetailsModal = BootstrapModal.extend({
},
removeContact (ev) {
if (ev && ev.preventDefault) { ev.preventDefault(); }
ev?.preventDefault?.();
if (!api.settings.get('allow_contact_removal')) { return; }
const result = confirm(__("Are you sure you want to remove this contact?"));
if (result === true) {

View File

@ -1,4 +1,5 @@
import { html } from 'lit-html';
import { _converse, api } from "@converse/headless/core";
export default o => html`
<div class="flyout box-flyout">
@ -19,7 +20,10 @@ export default o => html`
<div id="chatrooms" class="controlbox-section">
<converse-rooms-list></converse-rooms-list>
<converse-bookmarks></converse-bookmarks>
</div>`
</div>
${ api.settings.get("authentication") === _converse.ANONYMOUS ? '' :
html`<div id="converse-roster" class="controlbox-section"><converse-roster></converse-roster></div>`
}`
: o['active-form'] === 'register'
? html`<converse-register-panel></converse-register-panel>`
: html`<converse-login-panel></converse-login-panel>`

View File

@ -12,13 +12,13 @@ import MUCConfigForm from './config-form.js';
import MUCPasswordForm from './password-form.js';
import log from '@converse/headless/log';
import muc_api from './api.js';
import { RoomsPanel, RoomsPanelViewMixin } from './rooms-panel.js';
import { api, converse, _converse } from '@converse/headless/core';
const { Strophe } = converse.env;
function setMUCDomain (domain, controlboxview) {
controlboxview.getRoomsPanel().model.save('muc_domain', Strophe.getDomainFromJid(domain));
controlboxview.querySelector('converse-rooms-list')
.model.save('muc_domain', Strophe.getDomainFromJid(domain));
}
function setMUCDomainFromDisco (controlboxview) {
@ -50,7 +50,7 @@ function setMUCDomainFromDisco (controlboxview) {
function fetchAndSetMUCDomain (controlboxview) {
if (controlboxview.model.get('connected')) {
if (!controlboxview.getRoomsPanel().model.get('muc_domain')) {
if (!controlboxview.querySelector('converse-rooms-list').model.get('muc_domain')) {
if (api.settings.get('muc_domain') === undefined) {
setMUCDomainFromDisco(controlboxview);
} else {
@ -120,8 +120,6 @@ converse.plugins.add('converse-muc-views', {
_converse.MUCConfigForm = MUCConfigForm;
_converse.MUCPasswordForm = MUCPasswordForm;
_converse.ChatRoomView = MUCView;
_converse.RoomsPanel = RoomsPanel;
_converse.ControlBoxView && Object.assign(_converse.ControlBoxView.prototype, RoomsPanelViewMixin);
Object.assign(_converse.api, muc_api);

View File

@ -1,17 +0,0 @@
import { converse } from '@converse/headless/core';
const u = converse.env.utils;
/**
* Mixin which adds the ability to a ControlBox to render a list of open groupchats
* @mixin
*/
export const RoomsPanelViewMixin = {
getRoomsPanel () {
if (this.roomspanel && u.isInDOM(this.roomspanel.el)) {
return this.roomspanel;
} else {
return this.renderRoomsPanel();
}
}
};

View File

@ -2,79 +2,33 @@ import log from "@converse/headless/log";
import tpl_pending_contact from "./templates/pending_contact.js";
import tpl_requesting_contact from "./templates/requesting_contact.js";
import tpl_roster_item from "./templates/roster_item.js";
import { ViewWithAvatar } from 'shared/avatar.js';
import { CustomElement } from 'components/element.js';
import { __ } from 'i18n';
import { _converse, api, converse } from "@converse/headless/core";
import { debounce, without } from "lodash-es";
import { render } from 'lit-html';
const u = converse.env.utils;
const STATUSES = {
'dnd': __('This contact is busy'),
'online': __('This contact is online'),
'offline': __('This contact is offline'),
'unavailable': __('This contact is unavailable'),
'xa': __('This contact is away for an extended period'),
'away': __('This contact is away')
};
class RosterContact extends CustomElement {
const RosterContactView = ViewWithAvatar.extend({
tagName: 'li',
className: 'list-item d-flex hidden controlbox-padded',
static get properties () {
return {
model: { type: Object }
}
}
events: {
"click .accept-xmpp-request": "acceptRequest",
"click .decline-xmpp-request": "declineRequest",
"click .open-chat": "openChat",
"click .remove-xmpp-contact": "removeContact"
},
async initialize () {
await this.model.initialized;
this.debouncedRender = debounce(this.render, 50);
this.listenTo(this.model, "change", this.debouncedRender);
this.listenTo(this.model, "destroy", this.remove);
this.listenTo(this.model, "highlight", this.highlight);
this.listenTo(this.model, "remove", this.remove);
this.listenTo(this.model, 'vcard:change', this.debouncedRender);
this.listenTo(this.model.presence, "change:show", this.debouncedRender);
this.render();
},
connectedCallback () {
super.connectedCallback();
this.listenTo(this.model, "change", this.requestUpdate);
this.listenTo(this.model, "highlight", this.requestUpdate);
this.listenTo(this.model, 'vcard:change', this.requestUpdate);
}
render () {
if (!this.mayBeShown()) {
u.hideElement(this.el);
return this;
}
const ask = this.model.get('ask'),
show = this.model.presence.get('show'),
requesting = this.model.get('requesting'),
subscription = this.model.get('subscription'),
jid = this.model.get('jid');
const classes_to_remove = [
'current-xmpp-contact',
'pending-xmpp-contact',
'requesting-xmpp-contact'
].concat(Object.keys(STATUSES));
classes_to_remove.forEach(c => u.removeClass(c, this.el));
this.el.classList.add(show);
this.el.setAttribute('data-status', show);
this.highlight();
if (_converse.isUniView()) {
const chatbox = _converse.chatboxes.get(this.model.get('jid'));
if (chatbox) {
if (chatbox.get('hidden')) {
this.el.classList.remove('open');
} else {
this.el.classList.add('open');
}
}
}
const ask = this.model.get('ask');
const requesting = this.model.get('requesting');
const subscription = this.model.get('subscription');
const jid = this.model.get('jid');
if ((ask === 'subscribe') || (subscription === 'from')) {
/* ask === 'subscribe'
@ -89,46 +43,41 @@ const RosterContactView = ViewWithAvatar.extend({
* So in both cases the user is a "pending" contact.
*/
const display_name = this.model.getDisplayName();
this.el.classList.add('pending-xmpp-contact');
render(tpl_pending_contact(Object.assign(this.model.toJSON(), { display_name })), this.el);
return tpl_pending_contact(Object.assign(
this.model.toJSON(), {
display_name,
'openChat': ev => this.openChat(ev),
'removeContact': ev => this.removeContact(ev)
}));
} else if (requesting === true) {
const display_name = this.model.getDisplayName();
this.el.classList.add('requesting-xmpp-contact');
render(tpl_requesting_contact(
return tpl_requesting_contact(
Object.assign(this.model.toJSON(), {
display_name,
'openChat': ev => this.openChat(ev),
'acceptRequest': ev => this.acceptRequest(ev),
'declineRequest': ev => this.declineRequest(ev),
'desc_accept': __("Click to accept the contact request from %1$s", display_name),
'desc_decline': __("Click to decline the contact request from %1$s", display_name),
'allow_chat_pending_contacts': api.settings.get('allow_chat_pending_contacts')
})
), this.el);
} else if (subscription === 'both' || subscription === 'to' || _converse.rosterview.isSelf(jid)) {
this.el.classList.add('current-xmpp-contact');
this.el.classList.remove(without(['both', 'to'], subscription)[0]);
this.el.classList.add(subscription);
this.renderRosterItem(this.model);
);
} else if (subscription === 'both' || subscription === 'to' || u.isSameBareJID(jid, _converse.connection.jid)) {
return this.renderRosterItem(this.model);
}
return this;
},
}
/**
* If appropriate, highlight the contact (by adding the 'open' class).
* @private
* @method _converse.RosterContactView#highlight
*/
highlight () {
if (_converse.isUniView()) {
const chatbox = _converse.chatboxes.get(this.model.get('jid'));
if ((chatbox && chatbox.get('hidden')) || !chatbox) {
this.el.classList.remove('open');
} else {
this.el.classList.add('open');
}
}
},
renderRosterItem (item) { // eslint-disable-line class-methods-use-this
const STATUSES = {
'dnd': __('This contact is busy'),
'online': __('This contact is online'),
'offline': __('This contact is offline'),
'unavailable': __('This contact is unavailable'),
'xa': __('This contact is away for an extended period'),
'away': __('This contact is away')
};
renderRosterItem (item) {
const show = item.presence.get('show') || 'offline';
let status_icon;
if (show === 'online') {
@ -143,55 +92,45 @@ const RosterContactView = ViewWithAvatar.extend({
status_icon = 'fa fa-times-circle chat-status chat-status--offline';
}
const display_name = item.getDisplayName();
render(tpl_roster_item(
return tpl_roster_item(
Object.assign(item.toJSON(), {
show,
display_name,
status_icon,
'openChat': ev => this.openChat(ev),
'removeContact': ev => this.removeContact(ev),
'getAvatarData': () => this.getAvatarData(),
'desc_status': STATUSES[show],
'num_unread': item.get('num_unread') || 0,
classes: ''
})
), this.el);
this.renderAvatar();
return this;
},
);
}
/**
* Returns a boolean indicating whether this contact should
* generally be visible in the roster.
* It doesn't check for the more specific case of whether
* the group it's in is collapsed.
* @private
* @method _converse.RosterContactView#mayBeShown
*/
mayBeShown () {
const chatStatus = this.model.presence.get('show');
if (api.settings.get('hide_offline_users') && chatStatus === 'offline') {
// If pending or requesting, show
if ((this.model.get('ask') === 'subscribe') ||
(this.model.get('subscription') === 'from') ||
(this.model.get('requesting') === true)) {
return true;
}
return false;
}
return true;
},
getAvatarData () {
const image_type = this.model.vcard?.get('image_type') || _converse.DEFAULT_IMAGE_TYPE;
const image_data = this.model.vcard?.get('image') || _converse.DEFAULT_IMAGE;
const image = "data:" + image_type + ";base64," + image_data;
return {
'classes': 'avatar',
'height': 30,
'width': 30,
image,
};
}
openChat (ev) {
if (ev && ev.preventDefault) { ev.preventDefault(); }
ev?.preventDefault?.();
this.model.openChat();
},
}
async removeContact (ev) {
if (ev && ev.preventDefault) { ev.preventDefault(); }
removeContact (ev) {
ev?.preventDefault?.();
if (!api.settings.get('allow_contact_removal')) { return; }
if (!confirm(__("Are you sure you want to remove this contact?"))) { return; }
try {
await this.model.removeFromRoster();
this.remove();
this.model.removeFromRoster();
if (this.model.collection) {
// The model might have already been removed as
// result of a roster push.
@ -203,10 +142,10 @@ const RosterContactView = ViewWithAvatar.extend({
[__('Sorry, there was an error while trying to remove %1$s as a contact.', this.model.getDisplayName())]
);
}
},
}
async acceptRequest (ev) {
if (ev && ev.preventDefault) { ev.preventDefault(); }
ev?.preventDefault?.();
await _converse.roster.sendContactAddIQ(
this.model.get('jid'),
@ -214,7 +153,7 @@ const RosterContactView = ViewWithAvatar.extend({
[]
);
this.model.authorize().subscribe();
},
}
declineRequest (ev) {
if (ev && ev.preventDefault) { ev.preventDefault(); }
@ -224,6 +163,6 @@ const RosterContactView = ViewWithAvatar.extend({
}
return this;
}
});
}
export default RosterContactView;
api.elements.define('converse-roster-contact', RosterContact);

View File

@ -23,6 +23,7 @@ export class RosterFilterView extends ElementView {
model.id = `_converse.rosterfilter-${_converse.bare_jid}`;
model.browserStorage = _converse.createStore(model.id);
this.model = model;
_converse.roster_filter = model;
this.liveFilter = debounce(() => {
this.model.save({'filter_text': this.querySelector('.roster-filter').value});
@ -39,7 +40,6 @@ export class RosterFilterView extends ElementView {
this.listenTo(_converse.roster, "destroy", this.render);
this.listenTo(_converse.roster, "remove", this.render);
_converse.presences.on('change:show', this.render, this);
api.listen.on('rosterContactsFetchedAndProcessed', () => this.render());
this.model.fetch();
this.render();

View File

@ -1,206 +0,0 @@
import RosterContactView from './contactview.js';
import tpl_group_header from "./templates/group_header.js";
import { OrderedListView } from "@converse/skeletor/src/overview";
import { _converse, api, converse } from "@converse/headless/core";
import { render } from 'lit-html';
const u = converse.env.utils;
/**
* @class
* @namespace _converse.RosterGroupView
* @memberOf _converse
*/
const RosterGroupView = OrderedListView.extend({
tagName: 'div',
className: 'roster-group hidden',
events: {
"click a.group-toggle": "toggle"
},
sortImmediatelyOnAdd: true,
ItemView: RosterContactView,
listItems: 'model.contacts',
listSelector: '.roster-group-contacts',
sortEvent: 'presenceChanged',
initialize () {
OrderedListView.prototype.initialize.apply(this, arguments);
if (this.model.get('name') === _converse.HEADER_UNREAD) {
this.listenTo(this.model.contacts, "change:num_unread",
c => !this.model.get('unread_messages') && this.removeContact(c)
);
}
if (this.model.get('name') === _converse.HEADER_REQUESTING_CONTACTS) {
this.listenTo(this.model.contacts, "change:requesting",
c => !c.get('requesting') && this.removeContact(c)
);
}
if (this.model.get('name') === _converse.HEADER_PENDING_CONTACTS) {
this.listenTo(this.model.contacts, "change:subscription",
c => (c.get('subscription') !== 'from') && this.removeContact(c)
);
}
this.listenTo(this.model.contacts, "remove", this.onRemove);
this.listenTo(_converse.roster, 'change:groups', this.onContactGroupChange);
// This event gets triggered once *all* contacts (i.e. not
// just this group's) have been fetched from browser
// storage or the XMPP server and once they've been
// assigned to their various groups.
api.listen.on('rosterContactsFetchedAndProcessed', () => this.sortAndPositionAllItems());
},
render () {
this.el.setAttribute('data-group', this.model.get('name'));
render(tpl_group_header({
'label_group': this.model.get('name'),
'desc_group_toggle': this.model.get('description'),
'toggle_state': this.model.get('state')
}), this.el);
this.contacts_el = this.el.querySelector('.roster-group-contacts');
return this;
},
show () {
u.showElement(this.el);
if (this.model.get('state') === _converse.OPENED) {
Object.values(this.getAll())
.filter(v => v.mayBeShown())
.forEach(v => u.showElement(v.el));
}
return this;
},
collapse () {
return u.slideIn(this.contacts_el);
},
/* Given a list of contacts, make sure they're filtered out
* (aka hidden) and that all other contacts are visible.
* If all contacts are hidden, then also hide the group title.
* @private
* @method _converse.RosterGroupView#filterOutContacts
* @param { Array } contacts
*/
filterOutContacts (contacts=[]) {
let shown = 0;
this.model.contacts.forEach(contact => {
const contact_view = this.get(contact.get('id'));
if (contacts.includes(contact)) {
u.hideElement(contact_view.el);
} else if (contact_view.mayBeShown()) {
u.showElement(contact_view.el);
shown += 1;
}
});
if (shown) {
u.showElement(this.el);
} else {
u.hideElement(this.el);
}
},
/**
* Given the filter query "q" and the filter type "type",
* return a list of contacts that need to be filtered out.
* @private
* @method _converse.RosterGroupView#getFilterMatches
* @param { String } q - The filter query
* @param { String } type - The filter type
*/
getFilterMatches (q, type) {
if (q.length === 0) {
return [];
}
q = q.toLowerCase();
const contacts = this.model.contacts;
if (type === 'state') {
const sticky_groups = [_converse.HEADER_REQUESTING_CONTACTS, _converse.HEADER_UNREAD];
if (sticky_groups.includes(this.model.get('name'))) {
// When filtering by chat state, we still want to
// show sticky groups, even though they don't
// match the state in question.
return [];
} else if (q === 'unread_messages') {
return contacts.filter({'num_unread': 0});
} else if (q === 'online') {
return contacts.filter(c => ["offline", "unavailable"].includes(c.presence.get('show')));
} else {
return contacts.filter(c => !c.presence.get('show').includes(q));
}
} else {
return contacts.filter(c => !c.getFilterCriteria().includes(q));
}
},
/**
* Filter the group's contacts based on the query "q".
*
* If all contacts are filtered out (i.e. hidden), then the
* group must be filtered out as well.
* @private
* @method _converse.RosterGroupView#filter
* @param { string } q - The query to filter against
* @param { string } type
*/
filter (q, type) {
if (q === null || q === undefined) {
type = type || _converse.rosterview.filter_view.model.get('filter_type');
if (type === 'state') {
q = _converse.rosterview.filter_view.model.get('chat_state');
} else {
q = _converse.rosterview.filter_view.model.get('filter_text');
}
}
this.filterOutContacts(this.getFilterMatches(q, type));
},
async toggle (ev) {
if (ev && ev.preventDefault) { ev.preventDefault(); }
const icon_el = ev.target.matches('.fa') ? ev.target : ev.target.querySelector('.fa');
if (u.hasClass("fa-caret-down", icon_el)) {
this.model.save({state: _converse.CLOSED});
await this.collapse();
icon_el.classList.remove("fa-caret-down");
icon_el.classList.add("fa-caret-right");
} else {
icon_el.classList.remove("fa-caret-right");
icon_el.classList.add("fa-caret-down");
this.model.save({state: _converse.OPENED});
this.filter();
u.showElement(this.el);
u.slideOut(this.contacts_el);
}
},
onContactGroupChange (contact) {
const in_this_group = contact.get('groups').includes(this.model.get('name'));
const cid = contact.get('id');
const in_this_overview = !this.get(cid);
if (in_this_group && !in_this_overview) {
this.items.trigger('add', contact);
} else if (!in_this_group) {
this.removeContact(contact);
}
},
removeContact (contact) {
// We suppress events, otherwise the remove event will
// also cause the contact's view to be removed from the
// "Pending Contacts" group.
this.model.contacts.remove(contact, {'silent': true});
this.onRemove(contact);
},
onRemove (contact) {
this.remove(contact.get('jid'));
if (this.model.contacts.length === 0) {
this.remove();
}
}
});
export default RosterGroupView;

View File

@ -7,12 +7,11 @@ import "../modal";
import "@converse/headless/plugins/chatboxes";
import "@converse/headless/plugins/roster/index.js";
import "modals/add-contact.js";
import './rosterview.js';
import RosterContactView from './contactview.js';
import RosterGroupView from './groupview.js';
import RosterView from './rosterview.js';
import { initRosterView, highlightRosterItem, insertRoster } from './utils.js';
import { RosterFilter, RosterFilterView } from './filterview.js';
import { _converse, api, converse } from "@converse/headless/core";
import { highlightRosterItem } from './utils.js';
converse.plugins.add('converse-rosterview', {
@ -33,8 +32,6 @@ converse.plugins.add('converse-rosterview', {
_converse.RosterFilter = RosterFilter;
_converse.RosterFilterView = RosterFilterView;
_converse.RosterContactView = RosterContactView;
_converse.RosterGroupView = RosterGroupView;
_converse.RosterView = RosterView;
/* -------- Event Handlers ----------- */
api.listen.on('chatBoxesInitialized', () => {
@ -42,21 +39,6 @@ converse.plugins.add('converse-rosterview', {
_converse.chatboxes.on('change:hidden', chatbox => highlightRosterItem(chatbox));
});
api.listen.on('controlBoxInitialized', (view) => {
insertRoster(view);
view.model.on('change:connected', () => insertRoster(view));
});
api.listen.on('rosterInitialized', initRosterView);
api.listen.on('rosterReadyAfterReconnection', initRosterView);
api.listen.on('afterTearDown', () => {
if (converse.rosterview) {
converse.rosterview.model.off().reset();
converse.rosterview.each(groupview => groupview.removeAll().remove());
converse.rosterview.removeAll().remove();
delete converse.rosterview;
}
});
api.listen.on('afterTearDown', () => _converse.rotergroups?.off().reset());
}
});

View File

@ -1,11 +1,8 @@
import RosterGroupView from './groupview.js';
import log from "@converse/headless/log";
import debounce from 'lodash/debounce';
import tpl_roster from "./templates/roster.js";
import { ElementView } from "@converse/skeletor/src/element";
import { Model } from '@converse/skeletor/src/model.js';
import { OrderedListView } from "@converse/skeletor/src/overview";
import { __ } from 'i18n';
import { _converse, api, converse } from "@converse/headless/core";
import { debounce, has } from "lodash-es";
import { render } from 'lit-html';
const u = converse.env.utils;
@ -16,217 +13,65 @@ const u = converse.env.utils;
* @namespace _converse.RosterView
* @memberOf _converse
*/
const RosterView = OrderedListView.extend({
tagName: 'div',
id: 'converse-roster',
className: 'controlbox-section',
ItemView: RosterGroupView,
listItems: 'model',
listSelector: '.roster-contacts',
sortEvent: null, // Groups are immutable, so they don't get re-sorted
subviewIndex: 'name',
sortImmediatelyOnAdd: true,
events: {
export default class RosterView extends ElementView {
events = {
'click a.controlbox-heading__btn.add-contact': 'showAddContactModal',
'click a.controlbox-heading__btn.sync-contacts': 'syncContacts'
},
}
initialize () {
OrderedListView.prototype.initialize.apply(this, arguments);
async initialize () {
await api.waitUntil('rosterInitialized')
this.debouncedRender = debounce(this.render, 100);
this.listenTo(_converse.roster, "add", this.debouncedRender);
this.listenTo(_converse.roster, "destroy", this.debouncedRender);
this.listenTo(_converse.roster, "remove", this.debouncedRender);
this.listenTo(_converse.roster, 'change', this.renderIfRelevantChange);
this.listenTo(_converse.roster, "add", this.onContactAdded);
this.listenTo(_converse.roster, 'change:groups', this.onContactAdded);
this.listenTo(_converse.roster, 'change', this.onContactChange);
this.listenTo(_converse.roster, "destroy", this.update);
this.listenTo(_converse.roster, "remove", this.update);
_converse.presences.on('change:show', () => {
this.update();
this.updateFilter();
});
// FIXME Need to find a fix for this on the contact.presence
// this.listenTo(this.model.presence, "change:show", this.requestUpdate);
this.listenTo(this.model, "reset", this.reset);
// This event gets triggered once *all* contacts (i.e. not
// just this group's) have been fetched from browser
// storage or the XMPP server and once they've been
// assigned to their various groups.
api.listen.on('rosterGroupsFetched', this.sortAndPositionAllItems.bind(this));
api.listen.on('rosterContactsFetched', () => {
_converse.roster.each(contact => this.addRosterContact(contact, {'silent': true}));
this.update();
this.updateFilter();
api.trigger('rosterContactsFetchedAndProcessed');
});
this.listenTo(_converse.roster.state, "change", this.render);
_converse.presences.on('change:show', () => this.debouncedRender());
api.listen.on('rosterContactsFetched', () => this.render());
this.render();
this.listenToRosterFilter();
},
/**
* Triggered once the _converse.RosterView instance has been created and initialized.
* @event _converse#rosterViewInitialized
* @example _converse.api.listen.on('rosterViewInitialized', () => { ... });
*/
api.trigger('rosterViewInitialized');
}
render () {
render(tpl_roster({
'heading_contacts': __('Contacts'),
'title_add_contact': __('Add a contact'),
'title_sync_contacts': __('Re-sync your contacts')
}), this.el);
this.roster_el = this.el.querySelector('.roster-contacts');
return this;
},
render(tpl_roster(), this);
}
showAddContactModal (ev) {
api.modal.show(_converse.AddContactModal, {'model': new Model()}, ev);
},
renderIfRelevantChange (model) {
const attrs = ['ask', 'requesting', 'groups', 'num_unread'];
const changed = model.changed || {};
if (Object.keys(changed).filter(m => attrs.includes(m)).length) {
this.render();
}
}
listenToRosterFilter () {
this.filter_view = this.el.querySelector('converse-roster-filter');
this.filter_view.addEventListener('update', () => this.updateFilter());
},
this.filter_view = this.querySelector('converse-roster-filter');
this.filter_view.addEventListener('update', () => this.render());
}
/**
* Called whenever the filter settings have been changed or
* when contacts have been added, removed or changed.
*
* Debounced for 100ms so that it doesn't get called for every
* contact fetched from browser storage.
*/
updateFilter: debounce(function () {
const filter = new _converse.RosterFilter();
const type = filter.get('filter_type');
if (type === 'state') {
this.filter(filter.get('chat_state'), type);
} else {
this.filter(filter.get('filter_text'), type);
}
}, 100),
showAddContactModal (ev) { // eslint-disable-line class-methods-use-this
api.modal.show(_converse.AddContactModal, {'model': new Model()}, ev);
}
update () {
if (!u.isVisible(this.roster_el)) {
u.showElement(this.roster_el);
}
return this;
},
filter (query, type) {
const views = Object.values(this.getAll());
// First ensure the filter is restored to its original state
views.forEach(v => (v.model.contacts.length > 0) && v.show().filter(''));
// Now we can filter
query = query.toLowerCase();
if (type === 'groups') {
views.forEach(view => {
if (!view.model.get('name').toLowerCase().includes(query)) {
u.slideIn(view.el);
} else if (view.model.contacts.length > 0) {
u.slideOut(view.el);
}
});
} else {
views.forEach(v => v.filter(query, type));
}
},
async syncContacts (ev) {
async syncContacts (ev) { // eslint-disable-line class-methods-use-this
ev.preventDefault();
u.addClass('fa-spin', ev.target);
_converse.roster.data.save('version', null);
await _converse.roster.fetchFromServer();
api.user.presence.send();
u.removeClass('fa-spin', ev.target);
},
reset () {
this.removeAll();
this.render().update();
return this;
},
onContactAdded (contact) {
this.addRosterContact(contact)
this.update();
this.updateFilter();
},
onContactChange (contact) {
this.update();
if (has(contact.changed, 'subscription')) {
if (contact.changed.subscription === 'from') {
this.addContactToGroup(contact, _converse.HEADER_PENDING_CONTACTS);
} else if (['both', 'to'].includes(contact.get('subscription'))) {
this.addExistingContact(contact);
}
}
if (has(contact.changed, 'num_unread') && contact.get('num_unread')) {
this.addContactToGroup(contact, _converse.HEADER_UNREAD);
}
if (has(contact.changed, 'ask') && contact.changed.ask === 'subscribe') {
this.addContactToGroup(contact, _converse.HEADER_PENDING_CONTACTS);
}
if (has(contact.changed, 'subscription') && contact.changed.requesting === 'true') {
this.addContactToGroup(contact, _converse.HEADER_REQUESTING_CONTACTS);
}
this.updateFilter();
},
/**
* Returns the group as specified by name.
* Creates the group if it doesn't exist.
* @method _converse.RosterView#getGroup
* @private
* @param {string} name
*/
getGroup (name) {
const view = this.get(name);
if (view) {
return view.model;
}
return this.model.create({name});
},
addContactToGroup (contact, name, options) {
this.getGroup(name).contacts.add(contact, options);
this.sortAndPositionAllItems();
},
addExistingContact (contact, options) {
let groups;
if (api.settings.get('roster_groups')) {
groups = contact.get('groups');
groups = (groups.length === 0) ? [_converse.HEADER_UNGROUPED] : groups;
} else {
groups = [_converse.HEADER_CURRENT_CONTACTS];
}
if (contact.get('num_unread')) {
groups.push(_converse.HEADER_UNREAD);
}
groups.forEach(g => this.addContactToGroup(contact, g, options));
},
isSelf (jid) {
return u.isSameBareJID(jid, _converse.connection.jid);
},
addRosterContact (contact, options) {
const jid = contact.get('jid');
if (contact.get('subscription') === 'both' || contact.get('subscription') === 'to' || this.isSelf(jid)) {
this.addExistingContact(contact, options);
} else {
if (!api.settings.get('allow_contact_requests')) {
log.debug(
`Not adding requesting or pending contact ${jid} `+
`because allow_contact_requests is false`
);
return;
}
if ((contact.get('ask') === 'subscribe') || (contact.get('subscription') === 'from')) {
this.addContactToGroup(contact, _converse.HEADER_PENDING_CONTACTS, options);
} else if (contact.get('requesting') === true) {
this.addContactToGroup(contact, _converse.HEADER_REQUESTING_CONTACTS, options);
}
}
return this;
}
});
}
export default RosterView;
api.elements.define('converse-roster', RosterView);

View File

@ -0,0 +1,60 @@
import { __ } from 'i18n';
import { _converse, converse } from "@converse/headless/core";
import { html } from "lit-html";
import { toggleGroup } from '../utils.js';
const { u } = converse.env;
function renderContact (contact) {
const jid = contact.get('jid');
const extra_classes = [];
if (_converse.isUniView()) {
const chatbox = _converse.chatboxes.get(jid);
if (chatbox && !chatbox.get('hidden')) {
extra_classes.push('open');
}
}
const ask = contact.get('ask');
const requesting = contact.get('requesting');
const subscription = contact.get('subscription');
if ((ask === 'subscribe') || (subscription === 'from')) {
/* ask === 'subscribe'
* Means we have asked to subscribe to them.
*
* subscription === 'from'
* They are subscribed to us, but not vice versa.
* We assume that there is a pending subscription
* from us to them (otherwise we're in a state not
* supported by converse.js).
*
* So in both cases the user is a "pending" contact.
*/
extra_classes.push('pending-xmpp-contact');
} else if (requesting === true) {
extra_classes.push('requesting-xmpp-contact');
} else if (subscription === 'both' || subscription === 'to' || u.isSameBareJID(jid, _converse.connection.jid)) {
extra_classes.push('current-xmpp-contact');
extra_classes.push(subscription);
extra_classes.push(contact.presence.get('show'));
}
return html`
<li class="list-item d-flex controlbox-padded ${extra_classes.join(' ')}" data-status="${contact.presence.get('show')}">
<converse-roster-contact .model=${contact}></converse-roster-contact>
</li>`;
}
export default (o) => {
const i18n_title = __('Click to hide these contacts');
const collapsed = _converse.roster.state.get('collapsed_groups');
return html`
<div class="roster-group" data-group="${o.name}">
<a href="#" class="list-toggle group-toggle controlbox-padded" title="${i18n_title}" @click=${ev => toggleGroup(ev, o.name)}>
<span class="fa ${ (collapsed.includes(o.name)) ? 'fa-caret-right' : 'fa-caret-down' }"></span> ${o.name}
</a>
<ul class="items-list roster-group-contacts ${ (collapsed.includes(o.name)) ? 'collapsed' : '' }" data-group="${o.name}">
${ o.contacts.map(renderContact) }
</ul>
</div>`;
}

View File

@ -1,9 +0,0 @@
import { html } from "lit-html";
import { _converse } from "@converse/headless/core";
export default (o) => html`
<a href="#" class="list-toggle group-toggle controlbox-padded" title="${o.desc_group_toggle}">
<span class="fa ${ (o.toggle_state === _converse.OPENED) ? 'fa-caret-down' : 'fa-caret-right' }">
</span> ${o.label_group}</a>
<ul class="items-list roster-group-contacts ${ (o.toggle_state === _converse.CLOSED) ? 'collapsed' : '' }"></ul>
`;

View File

@ -7,6 +7,6 @@ const tpl_pending_contact = o => html`<span class="pending-contact-name" title="
export default (o) => {
const i18n_remove = __('Click to remove %1$s as a contact', o.display_name);
return html`
${ api.settings.get('allow_chat_pending_contacts') ? html`<a class="list-item-link open-chat w-100" href="#">${tpl_pending_contact(o)}</a>` : tpl_pending_contact(o) };
<a class="list-item-action remove-xmpp-contact far fa-trash-alt" title="${i18n_remove}" href="#"></a>`;
${ api.settings.get('allow_chat_pending_contacts') ? html`<a class="list-item-link open-chat w-100" href="#" @click=${o.openChat}>${tpl_pending_contact(o)}</a>` : tpl_pending_contact(o) }
<a class="list-item-action remove-xmpp-contact far fa-trash-alt" @click=${o.removeContact} title="${i18n_remove}" href="#"></a>`;
}

View File

@ -4,8 +4,10 @@ import { html } from "lit-html";
const tpl_requesting_contact = o => html`<span class="req-contact-name w-100" title="JID: ${o.jid}">${o.display_name}</span>`;
export default (o) => html`
${ api.settings.get('allow_chat_pending_contacts') ? html`<a class="open-chat w-100" href="#">${tpl_requesting_contact(o) }</a>` : tpl_requesting_contact(o) }
${ api.settings.get('allow_chat_pending_contacts') ? html`<a class="open-chat w-100" href="#" @click=${o.openChat}>${tpl_requesting_contact(o) }</a>` : tpl_requesting_contact(o) }
<a class="accept-xmpp-request list-item-action list-item-action--visible fa fa-check"
@click=${o.acceptRequest}
aria-label="${o.desc_accept}" title="${o.desc_accept}" href="#"></a>
<a class="decline-xmpp-request list-item-action list-item-action--visible fa fa-times"
@click=${o.declineRequest}
aria-label="${o.desc_decline}" title="${o.desc_decline}" href="#"></a>`;

View File

@ -1,16 +1,71 @@
import tpl_group from "./group.js";
import { __ } from 'i18n';
import { _converse, api } from "@converse/headless/core";
import { contactsComparator, groupsComparator } from '@converse/headless/plugins/roster/utils.js';
import { html } from "lit-html";
import { api } from "@converse/headless/core";
import { shouldShowContact, shouldShowGroup } from '../utils.js';
export default (o) => html`
<div class="d-flex controlbox-padded">
<span class="w-100 controlbox-heading controlbox-heading--contacts">${o.heading_contacts}</span>
<a class="controlbox-heading__btn sync-contacts fa fa-sync" title="${o.title_sync_contacts}"></a>
${ api.settings.get('allow_contact_requests') ? html`
<a class="controlbox-heading__btn add-contact fa fa-user-plus"
title="${o.title_add_contact}"
data-toggle="modal"
data-target="#add-contact-modal"></a>` : '' }
</div>
<converse-roster-filter></converse-roster-filter>
<div class="list-container roster-contacts"></div>
`;
function populateContactsMap (contacts_map, contact) {
if (contact.get('ask') === 'subscribe') {
const name = _converse.HEADER_PENDING_CONTACTS;
contacts_map[name] ? contacts_map[name].push(contact) : (contacts_map[name] = [contact]);
} else if (contact.get('requesting')) {
const name = _converse.HEADER_REQUESTING_CONTACTS;
contacts_map[name] ? contacts_map[name].push(contact) : (contacts_map[name] = [contact]);
} else {
let contact_groups;
if (api.settings.get('roster_groups')) {
contact_groups = contact.get('groups');
contact_groups = (contact_groups.length === 0) ? [_converse.HEADER_UNGROUPED] : contact_groups;
} else {
contact_groups = [_converse.HEADER_CURRENT_CONTACTS];
}
for (const name of contact_groups) {
contacts_map[name] ? contacts_map[name].push(contact) : (contacts_map[name] = [contact]);
}
}
if (contact.get('num_unread')) {
const name = _converse.HEADER_UNREAD;
contacts_map[name] ? contacts_map[name].push(contact) : (contacts_map[name] = [contact]);
}
return contacts_map;
}
export default () => {
const i18n_heading_contacts = __('Contacts');
const i18n_title_add_contact = __('Add a contact');
const i18n_title_sync_contacts = __('Re-sync your contacts');
const roster = _converse.roster || [];
const contacts_map = roster.reduce((acc, contact) => populateContactsMap(acc, contact), {});
const groupnames = Object.keys(contacts_map).filter(shouldShowGroup);
groupnames.sort(groupsComparator);
return html`
<div class="d-flex controlbox-padded">
<span class="w-100 controlbox-heading controlbox-heading--contacts">${i18n_heading_contacts}</span>
<a class="controlbox-heading__btn sync-contacts fa fa-sync" title="${i18n_title_sync_contacts}"></a>
${ api.settings.get('allow_contact_requests') ? html`
<a class="controlbox-heading__btn add-contact fa fa-user-plus"
title="${i18n_title_add_contact}"
data-toggle="modal"
data-target="#add-contact-modal"></a>` : '' }
</div>
<converse-roster-filter></converse-roster-filter>
<div class="list-container roster-contacts">
${ groupnames?.map(name => {
const contacts = contacts_map[name].filter(c => shouldShowContact(c));
contacts.sort(contactsComparator);
if (contacts.length) {
return tpl_group({
'contacts': contacts,
'name': name,
});
} else {
return '';
}
}) }
</div>
`;
}

View File

@ -1,16 +1,17 @@
import { __ } from 'i18n';
import { api } from "@converse/headless/core";
import { html } from "lit-html";
import { renderAvatar } from 'templates/directives/avatar';
export default (o) => {
const i18n_chat = __('Click to chat with %1$s (XMPP address: %2$s)', o.display_name, o.jid);
const i18n_remove = __('Click to remove %1$s as a contact', o.display_name);
return html`
<a class="list-item-link cbox-list-item open-chat w-100 ${ o.num_unread ? 'unread-msgs' : '' }" title="${i18n_chat}" href="#">
<canvas class="avatar" height="30" width="30"></canvas>
<a class="list-item-link cbox-list-item open-chat w-100 ${ o.num_unread ? 'unread-msgs' : '' }" title="${i18n_chat}" href="#" @click=${o.openChat}>
${ renderAvatar(o.getAvatarData()) }
<span class="${o.status_icon}" title="${o.desc_status}"></span>
${ o.num_unread ? html`<span class="msgs-indicator">${ o.num_unread }</span>` : '' }
<span class="contact-name contact-name--${o.show} ${ o.num_unread ? 'unread-msgs' : ''}">${o.display_name}</span>
</a>
${ api.settings.get('allow_contact_removal') ? html`<a class="list-item-action remove-xmpp-contact far fa-trash-alt" title="${i18n_remove}" href="#"></a>` : '' }`;
${ api.settings.get('allow_contact_removal') ? html`<a class="list-item-action remove-xmpp-contact far fa-trash-alt" @click=${o.removeContact} title="${i18n_remove}" href="#"></a>` : '' }`;
}

View File

@ -1,33 +1,75 @@
import log from "@converse/headless/log";
import { _converse, api } from "@converse/headless/core";
export function initRosterView () {
if (api.settings.get("authentication") === _converse.ANONYMOUS) {
return;
}
_converse.rosterview = new _converse.RosterView({'model': _converse.rostergroups });
_converse.rosterview.render();
/**
* Triggered once the _converse.RosterView instance has been created and initialized.
* @event _converse#rosterViewInitialized
* @example _converse.api.listen.on('rosterViewInitialized', () => { ... });
*/
api.trigger('rosterViewInitialized');
}
export function highlightRosterItem (chatbox) {
_converse.roster?.findWhere({'jid': chatbox.get('jid')})?.trigger('highlight');
}
export function insertRoster (view) {
if (!view.model.get('connected') || api.settings.get("authentication") === _converse.ANONYMOUS) {
return;
export function toggleGroup (ev, name) {
ev?.preventDefault?.();
const collapsed = _converse.roster.state.get('collapsed_groups');
if (collapsed.includes(name)) {
_converse.roster.state.save('collapsed_groups', collapsed.filter(n => n !== name));
} else {
_converse.roster.state.save('collapsed_groups', [...collapsed, name]);
}
/* Place the rosterview inside the "Contacts" panel. */
api.waitUntil('rosterViewInitialized')
.then(() => view.controlbox_pane.el.insertAdjacentElement('beforeEnd', _converse.rosterview.el))
.catch(e => log.fatal(e));
}
export function isContactFiltered (contact) {
const filter = _converse.roster_filter;
const type = filter.get('filter_type');
const q = (type === 'state') ?
filter.get('chat_state').toLowerCase() :
filter.get('filter_text').toLowerCase();
if (!q) return false;
if (type === 'state') {
const sticky_groups = [_converse.HEADER_REQUESTING_CONTACTS, _converse.HEADER_UNREAD];
if (sticky_groups.includes(this.model.get('name'))) {
// When filtering by chat state, we still want to
// show sticky groups, even though they don't
// match the state in question.
return false;
} else if (q === 'unread_messages') {
return contact.get('num_unread') === 0;
} else if (q === 'online') {
return ["offline", "unavailable"].includes(contact.presence.get('show'));
} else {
return !contact.presence.get('show').includes(q);
}
} else if (type === 'contacts') {
return !contact.getFilterCriteria().includes(q);
}
}
export function shouldShowContact (contact) {
const chat_status = contact.presence.get('show');
if (api.settings.get('hide_offline_users') && chat_status === 'offline') {
// If pending or requesting, show
if ((contact.get('ask') === 'subscribe') ||
(contact.get('subscription') === 'from') ||
(contact.get('requesting') === true)) {
return !isContactFiltered(contact);
}
return false;
}
return !isContactFiltered(contact);
}
export function shouldShowGroup (group) {
const filter = _converse.roster_filter;
const type = filter.get('filter_type');
if (type === 'groups') {
const q = filter.get('filter_text')?.toLowerCase();
if (!q) {
return true;
}
if (!group.toLowerCase().includes(q)) {
return false;
}
}
return true;
}

View File

@ -1,6 +1,6 @@
import { html } from "lit-html";
export default (o) => html`
export default (o) => html`
<fieldset class="form-group">
${o.label ? html`<label>${o.label}</label>` : '' }
<img src="data:${o.type};base64,${o.data}">