diff --git a/CHANGES.md b/CHANGES.md index f92a26611..85da970ae 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,7 @@ - GIFs don't render inside unfurls and cause a TypeError - Improve how the `muc_domain` setting is populated via service discovery +- Remove local (non-requesting) contacts not returned from a full roster response - #2746: Always reply to all iqs, even those not understood - #2868: Selected emoji is inserted into all open chat boxes diff --git a/src/headless/plugins/roster/contacts.js b/src/headless/plugins/roster/contacts.js index c6d4e08f5..780552044 100644 --- a/src/headless/plugins/roster/contacts.js +++ b/src/headless/plugins/roster/contacts.js @@ -67,7 +67,7 @@ const RosterContacts = Collection.extend({ 'add': true, 'silent': true, 'success': resolve, - 'error': (c, e) => reject(e) + 'error': (_, e) => reject(e) }); }); if (u.isErrorObject(result)) { @@ -262,11 +262,19 @@ const RosterContacts = Collection.extend({ if (this.rosterVersioningSupported()) { stanza.attrs({'ver': this.data.get('version')}); } + const iq = await api.sendIQ(stanza, null, false); - if (iq.getAttribute('type') !== 'error') { + + if (iq.getAttribute('type') === 'result') { const query = sizzle(`query[xmlns="${Strophe.NS.ROSTER}"]`, iq).pop(); if (query) { const items = sizzle(`item`, query); + if (!this.data.get('version')) { + // We're getting the full roster, so remove all cached + // contacts that aren't included in it. + const jids = items.map(item => item.getAttribute('jid')); + this.models.forEach(m => !m.get('requesting') && !jids.includes(m.get('jid')) && m.destroy()); + } items.forEach(item => this.updateContact(item)); this.data.save('version', query.getAttribute('ver')); } @@ -276,6 +284,7 @@ const RosterContacts = Collection.extend({ log.error("Error while trying to fetch roster from the server"); return; } + _converse.session.save('roster_cached', true); /** * When the roster has been received from the XMPP server. @@ -348,7 +357,6 @@ const RosterContacts = Collection.extend({ api.trigger('contactRequest', this.create(user_data)); }, - handleIncomingSubscription (presence) { const jid = presence.getAttribute('from'), bare_jid = Strophe.getBareJidFromJid(jid), diff --git a/src/plugins/rosterview/tests/roster.js b/src/plugins/rosterview/tests/roster.js index a90a49ae4..3f0c7a8ab 100644 --- a/src/plugins/rosterview/tests/roster.js +++ b/src/plugins/rosterview/tests/roster.js @@ -132,6 +132,59 @@ describe("The Contacts Roster", function () { expect(_converse.roster.at(0).get('jid')).toBe('nurse@example.com'); })); + it("can be refreshed", mock.initConverse( + [], {}, async function (_converse) { + + const sent_IQs = _converse.connection.IQ_stanzas; + let stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector('iq query[xmlns="jabber:iq:roster"]')).pop()); + _converse.connection._dataRecv(mock.createRequest($iq({ + to: _converse.connection.jid, + type: 'result', + id: stanza.getAttribute('id') + }).c('query', { + xmlns: 'jabber:iq:roster', + }).c('item', { + jid: 'juliet@example.net', + name: 'Juliet', + subscription:'both' + }).c('group').t('Friends').up().up() + .c('item', { + jid: 'mercutio@example.net', + name: 'Mercutio', + subscription:'from' + }).c('group').t('Friends'))); + + while (sent_IQs.length) sent_IQs.pop(); + + await u.waitUntil(() => _converse.roster.length === 2); + expect(_converse.roster.pluck('jid')).toEqual(['juliet@example.net', 'mercutio@example.net']); + + const rosterview = document.querySelector('converse-roster'); + const sync_button = rosterview.querySelector('.sync-contacts'); + sync_button.click(); + + stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector('iq query[xmlns="jabber:iq:roster"]')).pop()); + _converse.connection._dataRecv(mock.createRequest($iq({ + to: _converse.connection.jid, + type: 'result', + id: stanza.getAttribute('id') + }).c('query', { + xmlns: 'jabber:iq:roster', + }).c('item', { + jid: 'juliet@example.net', + name: 'Juliet', + subscription:'both' + }).c('group').t('Friends').up().up() + .c('item', { + jid: 'lord.capulet@example.net', + name: 'Lord Capulet', + subscription:'from' + }).c('group').t('Acquaintences'))); + + await u.waitUntil(() => _converse.roster.pluck('jid').includes('lord.capulet@example.net')); + expect(_converse.roster.pluck('jid')).toEqual(['juliet@example.net', 'lord.capulet@example.net']); + })); + it("will also show contacts added afterwards", mock.initConverse([], {}, async function (_converse) { await mock.openControlBox(_converse); await mock.waitForRoster(_converse, 'current'); @@ -1175,6 +1228,7 @@ describe("The Contacts Roster", function () { const pres = $pres({from: 'data@enterprise/resource', type: 'subscribe'}); _converse.connection._dataRecv(mock.createRequest(pres)); + expect(_converse.roster.pluck('jid').length).toBe(1); const rosterview = document.querySelector('converse-roster'); await u.waitUntil(() => sizzle('a:contains("Contact requests")', rosterview).length, 700);