Turn converse-roster and converse-roster-filter into Lit elements

This commit is contained in:
JC Brand 2021-07-15 16:36:42 +02:00
parent 8de4671603
commit d2a35d4ce1
4 changed files with 56 additions and 56 deletions

View File

@ -1,10 +1,9 @@
import debounce from "lodash-es/debounce"; import debounce from "lodash-es/debounce";
import tpl_roster_filter from "./templates/roster_filter.js"; 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 { Model } from '@converse/skeletor/src/model.js';
import { _converse, api } from "@converse/headless/core"; import { _converse, api } from "@converse/headless/core";
import { initStorage } from '@converse/headless/shared/utils.js'; import { initStorage } from '@converse/headless/shared/utils.js';
import { render } from 'lit';
export const RosterFilter = Model.extend({ export const RosterFilter = Model.extend({
initialize () { initialize () {
@ -16,8 +15,12 @@ export const RosterFilter = Model.extend({
} }
}); });
export class RosterFilterView extends ElementView { export class RosterFilterView extends CustomElement {
tagName = 'span';
constructor () {
super();
this.initialize();
}
initialize () { initialize () {
const model = new _converse.RosterFilter(); const model = new _converse.RosterFilter();
@ -30,24 +33,19 @@ export class RosterFilterView extends ElementView {
this.model.save({'filter_text': this.querySelector('.roster-filter').value}); this.model.save({'filter_text': this.querySelector('.roster-filter').value});
}, 250); }, 250);
this.listenTo(this.model, 'change', this.render); this.listenTo(_converse, 'rosterContactsFetched', this.requestUpdate);
this.listenTo( this.listenTo(_converse.presences, 'change:show', this.requestUpdate);
this.model, this.listenTo(_converse.roster, "add", this.requestUpdate);
'change', this.listenTo(_converse.roster, "destroy", this.requestUpdate);
() => this.dispatchEvent(new CustomEvent('update', { 'detail': this.model.changed })) this.listenTo(_converse.roster, "remove", this.requestUpdate);
); this.listenTo(this.model, 'change', this.dispatchUpdateEvent);
this.listenTo(this.model, 'change', this.requestUpdate);
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.model.fetch(); this.model.fetch();
this.render();
} }
render () { render () {
render(tpl_roster_filter( return tpl_roster_filter(
Object.assign(this.model.toJSON(), { Object.assign(this.model.toJSON(), {
visible: this.shouldBeVisible(), visible: this.shouldBeVisible(),
changeChatStateFilter: ev => this.changeChatStateFilter(ev), changeChatStateFilter: ev => this.changeChatStateFilter(ev),
@ -55,8 +53,11 @@ export class RosterFilterView extends ElementView {
clearFilter: ev => this.clearFilter(ev), clearFilter: ev => this.clearFilter(ev),
liveFilter: ev => this.liveFilter(ev), liveFilter: ev => this.liveFilter(ev),
submitFilter: ev => this.submitFilter(ev), submitFilter: ev => this.submitFilter(ev),
})), this); }));
return this; }
dispatchUpdateEvent () {
this.dispatchEvent(new CustomEvent('update', { 'detail': this.model.changed }));
} }
changeChatStateFilter (ev) { changeChatStateFilter (ev) {
@ -96,7 +97,7 @@ export class RosterFilterView extends ElementView {
} }
shouldBeVisible () { shouldBeVisible () {
return _converse.roster && _converse.roster.length >= 5 || this.isActive(); return _converse.roster?.length >= 5 || this.isActive();
} }
clearFilter (ev) { clearFilter (ev) {

View File

@ -1,9 +1,7 @@
import debounce from 'lodash-es/debounce';
import tpl_roster from "./templates/roster.js"; 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 { Model } from '@converse/skeletor/src/model.js';
import { _converse, api } from "@converse/headless/core"; import { _converse, api } from "@converse/headless/core";
import { render } from 'lit';
/** /**
@ -11,21 +9,22 @@ import { render } from 'lit';
* @namespace _converse.RosterView * @namespace _converse.RosterView
* @memberOf _converse * @memberOf _converse
*/ */
export default class RosterView extends ElementView { export default class RosterView extends CustomElement {
constructor () {
super();
this.initialize();
}
async initialize () { async initialize () {
await api.waitUntil('rosterInitialized') await api.waitUntil('rosterInitialized')
this.debouncedRender = debounce(this.render, 100); this.listenTo(_converse, 'rosterContactsFetched', this.requestUpdate);
this.listenTo(_converse, 'rosterContactsFetched', this.render); this.listenTo(_converse.presences, 'change:show', this.requestUpdate);
this.listenTo(_converse.roster, "add", this.debouncedRender); this.listenTo(_converse.roster, 'add', this.requestUpdate);
this.listenTo(_converse.roster, "destroy", this.debouncedRender); this.listenTo(_converse.roster, 'destroy', this.requestUpdate);
this.listenTo(_converse.roster, "remove", this.debouncedRender); this.listenTo(_converse.roster, 'remove', this.requestUpdate);
this.listenTo(_converse.roster, 'change', this.renderIfRelevantChange); this.listenTo(_converse.roster, 'change', this.requestUpdate);
this.listenTo(_converse.roster.state, "change", this.render); this.listenTo(_converse.roster.state, 'change', this.requestUpdate);
_converse.presences.on('change:show', () => this.debouncedRender());
this.render();
this.listenToRosterFilter();
/** /**
* Triggered once the _converse.RosterView instance has been created and initialized. * Triggered once the _converse.RosterView instance has been created and initialized.
* @event _converse#rosterViewInitialized * @event _converse#rosterViewInitialized
@ -34,21 +33,17 @@ export default class RosterView extends ElementView {
api.trigger('rosterViewInitialized'); api.trigger('rosterViewInitialized');
} }
render () { firstUpdated () {
render(tpl_roster(this), this); this.listenToRosterFilter();
} }
renderIfRelevantChange (model) { render () {
const attrs = ['ask', 'requesting', 'groups', 'num_unread']; return tpl_roster(this);
const changed = model.changed || {};
if (Object.keys(changed).filter(m => attrs.includes(m)).length) {
this.render();
}
} }
listenToRosterFilter () { listenToRosterFilter () {
this.filter_view = this.querySelector('converse-roster-filter'); 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 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 async syncContacts (ev) { // eslint-disable-line class-methods-use-this
ev.preventDefault(); ev.preventDefault();
this.syncing_contacts = true; this.syncing_contacts = true;
this.render(); this.requestUpdate();
_converse.roster.data.save('version', null); _converse.roster.data.save('version', null);
await _converse.roster.fetchFromServer(); await _converse.roster.fetchFromServer();
api.user.presence.send(); api.user.presence.send();
this.syncing_contacts = false; this.syncing_contacts = false;
this.render(); this.requestUpdate();
} }
} }

View File

@ -150,7 +150,7 @@ describe("The Protocol", function () {
stanza = $iq({'type': 'result', 'id': roster_fetch_stanza.getAttribute('id')}); stanza = $iq({'type': 'result', 'id': roster_fetch_stanza.getAttribute('id')});
_converse.connection._dataRecv(mock.createRequest(stanza)); _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 // A contact should now have been created
expect(_converse.roster.get('contact@example.org') instanceof _converse.RosterContact).toBeTruthy(); expect(_converse.roster.get('contact@example.org') instanceof _converse.RosterContact).toBeTruthy();
@ -206,7 +206,7 @@ describe("The Protocol", function () {
// contact in the roster. // contact in the roster.
await u.waitUntil(() => { await u.waitUntil(() => {
const header = sizzle('a:contains("Pending contacts")', rosterview).pop(); 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; return contacts.length;
}, 600); }, 600);
@ -286,7 +286,8 @@ describe("The Protocol", function () {
expect(u.hasClass('to', contacts[0])).toBeTruthy(); expect(u.hasClass('to', contacts[0])).toBeTruthy();
expect(u.hasClass('both', contacts[0])).toBeFalsy(); expect(u.hasClass('both', contacts[0])).toBeFalsy();
expect(u.hasClass('current-xmpp-contact', contacts[0])).toBeTruthy(); 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'); expect(contact.presence.get('show')).toBe('offline');

View File

@ -139,7 +139,6 @@ describe("The Contacts Roster", function () {
const rosterview = document.querySelector('converse-roster'); const rosterview = document.querySelector('converse-roster');
const filter = rosterview.querySelector('.roster-filter'); const filter = rosterview.querySelector('.roster-filter');
const roster = rosterview.querySelector('.roster-contacts'); const roster = rosterview.querySelector('.roster-contacts');
rosterview.filter_view.delegateEvents();
await u.waitUntil(() => (sizzle('li', roster).filter(u.isVisible).length === 17), 800); await u.waitUntil(() => (sizzle('li', roster).filter(u.isVisible).length === 17), 800);
filter.value = "la"; filter.value = "la";
@ -251,7 +250,6 @@ describe("The Contacts Roster", function () {
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
const rosterview = document.querySelector('converse-roster'); const rosterview = document.querySelector('converse-roster');
rosterview.filter_view.delegateEvents();
const roster = rosterview.querySelector('.roster-contacts'); const roster = rosterview.querySelector('.roster-contacts');
const button = rosterview.querySelector('span[data-type="groups"]'); const button = rosterview.querySelector('span[data-type="groups"]');
@ -299,7 +297,7 @@ describe("The Contacts Roster", function () {
const isHidden = (el) => u.hasClass('hidden', el); const isHidden = (el) => u.hasClass('hidden', el);
await u.waitUntil(() => !isHidden(rosterview.querySelector('.roster-filter-form .clear-input')), 900); await u.waitUntil(() => !isHidden(rosterview.querySelector('.roster-filter-form .clear-input')), 900);
rosterview.querySelector('.clear-input').click(); 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 // 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'); const contact = _converse.roster.get('groupchanger@montague.lit');
contact.set({'groups': ['secondgroup']}); contact.set({'groups': ['secondgroup']});
await u.waitUntil(() => sizzle('.roster-group[data-group="secondgroup"] a.group-toggle', rosterview).length);
group_titles = await u.waitUntil(() => { group_titles = await u.waitUntil(() => {
const toggles = sizzle('.roster-group[data-group="secondgroup"] a.group-toggle', rosterview); const toggles = sizzle('.roster-group[data-group="secondgroup"] a.group-toggle', rosterview);
if (toggles.reduce((result, t) => result && u.isVisible(t), true)) { 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 // Check that they are sorted alphabetically
const el = await u.waitUntil(() => rosterview.querySelector(`ul[data-group="Pending contacts"]`)); const el = await u.waitUntil(() => rosterview.querySelector(`ul[data-group="Pending contacts"]`));
const spans = el.querySelectorAll('.pending-xmpp-contact span'); 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, requesting: false,
subscription: 'both' subscription: 'both'
}); });
const el = rosterview.querySelector(`ul[data-group="My contacts"]`); await u.waitUntil(() => u.hasClass('collapsed', rosterview.querySelector(`ul[data-group="My contacts"]`)) === true);
expect(u.hasClass('collapsed', el)).toBe(true); expect(true).toBe(true);
})); }));
it("can be added to the roster and they will be sorted alphabetically", it("can be added to the roster and they will be sorted alphabetically",