diff --git a/src/plugins/rosterview/filterview.js b/src/plugins/rosterview/filterview.js index e30bae098..0724d566c 100644 --- a/src/plugins/rosterview/filterview.js +++ b/src/plugins/rosterview/filterview.js @@ -1,10 +1,9 @@ import debounce from "lodash-es/debounce"; import tpl_roster_filter from "./templates/roster_filter.js"; -import { ElementView } from '@converse/skeletor/src/element.js'; +import { CustomElement } from 'shared/components/element.js'; import { Model } from '@converse/skeletor/src/model.js'; import { _converse, api } from "@converse/headless/core"; import { initStorage } from '@converse/headless/shared/utils.js'; -import { render } from 'lit'; export const RosterFilter = Model.extend({ initialize () { @@ -16,8 +15,12 @@ export const RosterFilter = Model.extend({ } }); -export class RosterFilterView extends ElementView { - tagName = 'span'; +export class RosterFilterView extends CustomElement { + + constructor () { + super(); + this.initialize(); + } initialize () { const model = new _converse.RosterFilter(); @@ -30,24 +33,19 @@ export class RosterFilterView extends ElementView { this.model.save({'filter_text': this.querySelector('.roster-filter').value}); }, 250); - this.listenTo(this.model, 'change', this.render); - this.listenTo( - this.model, - 'change', - () => this.dispatchEvent(new CustomEvent('update', { 'detail': this.model.changed })) - ); - - this.listenTo(_converse.roster, "add", this.render); - this.listenTo(_converse.roster, "destroy", this.render); - this.listenTo(_converse.roster, "remove", this.render); - _converse.presences.on('change:show', this.render, this); + this.listenTo(_converse, 'rosterContactsFetched', this.requestUpdate); + this.listenTo(_converse.presences, 'change:show', this.requestUpdate); + this.listenTo(_converse.roster, "add", this.requestUpdate); + this.listenTo(_converse.roster, "destroy", this.requestUpdate); + this.listenTo(_converse.roster, "remove", this.requestUpdate); + this.listenTo(this.model, 'change', this.dispatchUpdateEvent); + this.listenTo(this.model, 'change', this.requestUpdate); this.model.fetch(); - this.render(); } render () { - render(tpl_roster_filter( + return tpl_roster_filter( Object.assign(this.model.toJSON(), { visible: this.shouldBeVisible(), changeChatStateFilter: ev => this.changeChatStateFilter(ev), @@ -55,8 +53,11 @@ export class RosterFilterView extends ElementView { clearFilter: ev => this.clearFilter(ev), liveFilter: ev => this.liveFilter(ev), submitFilter: ev => this.submitFilter(ev), - })), this); - return this; + })); + } + + dispatchUpdateEvent () { + this.dispatchEvent(new CustomEvent('update', { 'detail': this.model.changed })); } changeChatStateFilter (ev) { @@ -96,7 +97,7 @@ export class RosterFilterView extends ElementView { } shouldBeVisible () { - return _converse.roster && _converse.roster.length >= 5 || this.isActive(); + return _converse.roster?.length >= 5 || this.isActive(); } clearFilter (ev) { diff --git a/src/plugins/rosterview/rosterview.js b/src/plugins/rosterview/rosterview.js index 5ed71ac52..e872133fa 100644 --- a/src/plugins/rosterview/rosterview.js +++ b/src/plugins/rosterview/rosterview.js @@ -1,9 +1,7 @@ -import debounce from 'lodash-es/debounce'; import tpl_roster from "./templates/roster.js"; -import { ElementView } from "@converse/skeletor/src/element"; +import { CustomElement } from 'shared/components/element.js'; import { Model } from '@converse/skeletor/src/model.js'; import { _converse, api } from "@converse/headless/core"; -import { render } from 'lit'; /** @@ -11,21 +9,22 @@ import { render } from 'lit'; * @namespace _converse.RosterView * @memberOf _converse */ -export default class RosterView extends ElementView { +export default class RosterView extends CustomElement { + + constructor () { + super(); + this.initialize(); + } async initialize () { await api.waitUntil('rosterInitialized') - this.debouncedRender = debounce(this.render, 100); - this.listenTo(_converse, 'rosterContactsFetched', this.render); - 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.state, "change", this.render); - _converse.presences.on('change:show', () => this.debouncedRender()); - - this.render(); - this.listenToRosterFilter(); + this.listenTo(_converse, 'rosterContactsFetched', this.requestUpdate); + this.listenTo(_converse.presences, 'change:show', this.requestUpdate); + this.listenTo(_converse.roster, 'add', this.requestUpdate); + this.listenTo(_converse.roster, 'destroy', this.requestUpdate); + this.listenTo(_converse.roster, 'remove', this.requestUpdate); + this.listenTo(_converse.roster, 'change', this.requestUpdate); + this.listenTo(_converse.roster.state, 'change', this.requestUpdate); /** * Triggered once the _converse.RosterView instance has been created and initialized. * @event _converse#rosterViewInitialized @@ -34,21 +33,17 @@ export default class RosterView extends ElementView { api.trigger('rosterViewInitialized'); } - render () { - render(tpl_roster(this), this); + firstUpdated () { + this.listenToRosterFilter(); } - 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(); - } + render () { + return tpl_roster(this); } listenToRosterFilter () { this.filter_view = this.querySelector('converse-roster-filter'); - this.filter_view.addEventListener('update', () => this.render()); + this.filter_view.addEventListener('update', () => this.requestUpdate()); } showAddContactModal (ev) { // eslint-disable-line class-methods-use-this @@ -58,14 +53,14 @@ export default class RosterView extends ElementView { async syncContacts (ev) { // eslint-disable-line class-methods-use-this ev.preventDefault(); this.syncing_contacts = true; - this.render(); + this.requestUpdate(); _converse.roster.data.save('version', null); await _converse.roster.fetchFromServer(); api.user.presence.send(); this.syncing_contacts = false; - this.render(); + this.requestUpdate(); } } diff --git a/src/plugins/rosterview/tests/protocol.js b/src/plugins/rosterview/tests/protocol.js index 15796f9e6..296330c24 100644 --- a/src/plugins/rosterview/tests/protocol.js +++ b/src/plugins/rosterview/tests/protocol.js @@ -150,7 +150,7 @@ describe("The Protocol", function () { stanza = $iq({'type': 'result', 'id': roster_fetch_stanza.getAttribute('id')}); _converse.connection._dataRecv(mock.createRequest(stanza)); - await u.waitUntil(() => _converse.roster.create.calls.count()); + await u.waitUntil(() => _converse.roster.create.calls.count(), 1000); // A contact should now have been created expect(_converse.roster.get('contact@example.org') instanceof _converse.RosterContact).toBeTruthy(); @@ -206,7 +206,7 @@ describe("The Protocol", function () { // contact in the roster. await u.waitUntil(() => { const header = sizzle('a:contains("Pending contacts")', rosterview).pop(); - const contacts = Array.from(header.parentElement.querySelectorAll('li')).filter(u.isVisible); + const contacts = Array.from(header?.parentElement.querySelectorAll('li') ?? []).filter(u.isVisible); return contacts.length; }, 600); @@ -286,7 +286,8 @@ describe("The Protocol", function () { expect(u.hasClass('to', contacts[0])).toBeTruthy(); expect(u.hasClass('both', contacts[0])).toBeFalsy(); expect(u.hasClass('current-xmpp-contact', contacts[0])).toBeTruthy(); - expect(contacts[0].textContent.trim()).toBe('Nicky'); + + await u.waitUntil(() => contacts[0].textContent.trim() === 'Nicky'); expect(contact.presence.get('show')).toBe('offline'); diff --git a/src/plugins/rosterview/tests/roster.js b/src/plugins/rosterview/tests/roster.js index 3aebd5a65..89018c421 100644 --- a/src/plugins/rosterview/tests/roster.js +++ b/src/plugins/rosterview/tests/roster.js @@ -139,7 +139,6 @@ describe("The Contacts Roster", function () { const rosterview = document.querySelector('converse-roster'); const filter = rosterview.querySelector('.roster-filter'); const roster = rosterview.querySelector('.roster-contacts'); - rosterview.filter_view.delegateEvents(); await u.waitUntil(() => (sizzle('li', roster).filter(u.isVisible).length === 17), 800); filter.value = "la"; @@ -251,7 +250,6 @@ describe("The Contacts Roster", function () { await mock.openControlBox(_converse); await mock.waitForRoster(_converse, 'current'); const rosterview = document.querySelector('converse-roster'); - rosterview.filter_view.delegateEvents(); const roster = rosterview.querySelector('.roster-contacts'); const button = rosterview.querySelector('span[data-type="groups"]'); @@ -299,7 +297,7 @@ describe("The Contacts Roster", function () { const isHidden = (el) => u.hasClass('hidden', el); await u.waitUntil(() => !isHidden(rosterview.querySelector('.roster-filter-form .clear-input')), 900); rosterview.querySelector('.clear-input').click(); - expect(document.querySelector('.roster-filter').value).toBe(""); + await u.waitUntil(() => document.querySelector('.roster-filter').value == ''); })); // Disabling for now, because since recently this test consistently @@ -460,6 +458,7 @@ describe("The Contacts Roster", function () { const contact = _converse.roster.get('groupchanger@montague.lit'); contact.set({'groups': ['secondgroup']}); + await u.waitUntil(() => sizzle('.roster-group[data-group="secondgroup"] a.group-toggle', rosterview).length); group_titles = await u.waitUntil(() => { const toggles = sizzle('.roster-group[data-group="secondgroup"] a.group-toggle', rosterview); if (toggles.reduce((result, t) => result && u.isVisible(t), true)) { @@ -688,8 +687,12 @@ describe("The Contacts Roster", function () { // Check that they are sorted alphabetically const el = await u.waitUntil(() => rosterview.querySelector(`ul[data-group="Pending contacts"]`)); const spans = el.querySelectorAll('.pending-xmpp-contact span'); - const t = Array.from(spans).reduce((result, value) => result + value.textContent?.trim(), ''); - expect(t).toEqual(mock.pend_names.slice(0,i+1).sort().join('')); + + await u.waitUntil( + () => Array.from(spans).reduce((result, value) => result + value.textContent?.trim(), '') === + mock.pend_names.slice(0,i+1).sort().join('') + ); + expect(true).toBe(true); })); }); @@ -729,8 +732,8 @@ describe("The Contacts Roster", function () { requesting: false, subscription: 'both' }); - const el = rosterview.querySelector(`ul[data-group="My contacts"]`); - expect(u.hasClass('collapsed', el)).toBe(true); + await u.waitUntil(() => u.hasClass('collapsed', rosterview.querySelector(`ul[data-group="My contacts"]`)) === true); + expect(true).toBe(true); })); it("can be added to the roster and they will be sorted alphabetically",