瀏覽代碼

Replace roster buttons with dropdown

JC Brand 1 年之前
父節點
當前提交
2a59556066

+ 1 - 0
CHANGES.md

@@ -10,6 +10,7 @@
 - #3307: Fix inconsistency between browsers on textarea outlines
 - #3307: Fix inconsistency between browsers on textarea outlines
 - #3337: Correctly display multiline nested quotes
 - #3337: Correctly display multiline nested quotes
 - Add an occupants filter to the MUC sidebar
 - Add an occupants filter to the MUC sidebar
+- Change contacts filter to rename the anachronistic `Online` state to `Available`.
 - Fix: MUC occupant list does not sort itself on nicknames or roles changes
 - Fix: MUC occupant list does not sort itself on nicknames or roles changes
 - Fix: refresh the MUC sidebar when participants collection is sorted
 - Fix: refresh the MUC sidebar when participants collection is sorted
 - Fix: room information not correctly refreshed when modifications are made by other users
 - Fix: room information not correctly refreshed when modifications are made by other users

+ 26 - 15
src/headless/shared/api/events.js

@@ -17,7 +17,7 @@ export default {
      *  event handlers' promises have been resolved.
      *  event handlers' promises have been resolved.
      *
      *
      * @method _converse.api.trigger
      * @method _converse.api.trigger
-     * @param { string } name - The event name
+     * @param {string} name - The event name
      */
      */
     async trigger (name) {
     async trigger (name) {
         if (!_converse._events) {
         if (!_converse._events) {
@@ -45,8 +45,9 @@ export default {
      * A hook is a special kind of event which allows you to intercept a data
      * A hook is a special kind of event which allows you to intercept a data
      * structure in order to modify it, before passing it back.
      * structure in order to modify it, before passing it back.
      * @async
      * @async
-     * @param { string } name - The hook name
-     * @param {...any} context - The context to which the hook applies (could be for example, a {@link _converse.ChatBox})).
+     * @param {string} name - The hook name
+     * @param {...any} context - The context to which the hook applies
+     *  (could be for example, a {@link _converse.ChatBox}).
      * @param {...any} data - The data structure to be intercepted and modified by the hook listeners.
      * @param {...any} data - The data structure to be intercepted and modified by the hook listeners.
      * @returns {Promise<any>} - A promise that resolves with the modified data structure.
      * @returns {Promise<any>} - A promise that resolves with the modified data structure.
      */
      */
@@ -75,9 +76,9 @@ export default {
         /**
         /**
          * Lets you listen to an event exactly once.
          * Lets you listen to an event exactly once.
          * @method _converse.api.listen.once
          * @method _converse.api.listen.once
-         * @param { string } name The event's name
-         * @param { function } callback The callback method to be called when the event is emitted.
-         * @param { object } [context] The value of the `this` parameter for the callback.
+         * @param {string} name The event's name
+         * @param {function} callback The callback method to be called when the event is emitted.
+         * @param {object} [context] The value of the `this` parameter for the callback.
          * @example _converse.api.listen.once('message', function (messageXML) { ... });
          * @example _converse.api.listen.once('message', function (messageXML) { ... });
          */
          */
         once: _converse.once.bind(_converse),
         once: _converse.once.bind(_converse),
@@ -86,9 +87,9 @@ export default {
          * Lets you subscribe to an event.
          * Lets you subscribe to an event.
          * Every time the event fires, the callback method specified by `callback` will be called.
          * Every time the event fires, the callback method specified by `callback` will be called.
          * @method _converse.api.listen.on
          * @method _converse.api.listen.on
-         * @param { string } name The event's name
-         * @param { function } callback The callback method to be called when the event is emitted.
-         * @param { object } [context] The value of the `this` parameter for the callback.
+         * @param {string} name The event's name
+         * @param {function} callback The callback method to be called when the event is emitted.
+         * @param {object} [context] The value of the `this` parameter for the callback.
          * @example _converse.api.listen.on('message', function (messageXML) { ... });
          * @example _converse.api.listen.on('message', function (messageXML) { ... });
          */
          */
         on: _converse.on.bind(_converse),
         on: _converse.on.bind(_converse),
@@ -96,24 +97,34 @@ export default {
         /**
         /**
          * To stop listening to an event, you can use the `not` method.
          * To stop listening to an event, you can use the `not` method.
          * @method _converse.api.listen.not
          * @method _converse.api.listen.not
-         * @param { string } name The event's name
-         * @param { function } callback The callback method that is to no longer be called when the event fires
+         * @param {string} name The event's name
+         * @param {function} callback The callback method that is to no longer be called when the event fires
          * @example _converse.api.listen.not('message', function (messageXML);
          * @example _converse.api.listen.not('message', function (messageXML);
          */
          */
         not: _converse.off.bind(_converse),
         not: _converse.off.bind(_converse),
 
 
+        /**
+         * An options object which lets you set filter criteria for matching
+         * against stanzas.
+         * @typedef {object} MatchingOptions
+         * @property {string} [ns] - The namespace to match against
+         * @property {string} [type] - The stanza type to match against
+         * @property {string} [id] - The stanza id to match against
+         * @property {string} [from] - The stanza sender to match against
+         */
+
         /**
         /**
          * Subscribe to an incoming stanza
          * Subscribe to an incoming stanza
          * Every a matched stanza is received, the callback method specified by
          * Every a matched stanza is received, the callback method specified by
          * `callback` will be called.
          * `callback` will be called.
          * @method _converse.api.listen.stanza
          * @method _converse.api.listen.stanza
-         * @param { string } name The stanza's name
-         * @param { object } options Matching options (e.g. 'ns' for namespace, 'type' for stanza type, also 'id' and 'from');
-         * @param { function } handler The callback method to be called when the stanza appears
+         * @param {string} name The stanza's name
+         * @param {MatchingOptions|Function} options Matching options or callback
+         * @param {function} handler The callback method to be called when the stanza appears
          */
          */
         stanza (name, options, handler) {
         stanza (name, options, handler) {
             if (isFunction(options)) {
             if (isFunction(options)) {
-                handler = options;
+                handler = /** @type {Function} */(options);
                 options = {};
                 options = {};
             } else {
             } else {
                 options = options || {};
                 options = options || {};

+ 1 - 0
src/headless/types/plugins/chat/model.d.ts

@@ -241,6 +241,7 @@ declare class ChatBox extends ModelWithContact {
     incrementUnreadMsgsCounter(message: Message): void;
     incrementUnreadMsgsCounter(message: Message): void;
     clearUnreadMsgCounter(): void;
     clearUnreadMsgCounter(): void;
     isScrolledUp(): any;
     isScrolledUp(): any;
+    canPostMessages(): boolean;
 }
 }
 import ModelWithContact from "./model-with-contact.js";
 import ModelWithContact from "./model-with-contact.js";
 import { Model } from "@converse/skeletor";
 import { Model } from "@converse/skeletor";

+ 34 - 7
src/headless/types/shared/api/events.d.ts

@@ -13,7 +13,7 @@ declare namespace _default {
      *  event handlers' promises have been resolved.
      *  event handlers' promises have been resolved.
      *
      *
      * @method _converse.api.trigger
      * @method _converse.api.trigger
-     * @param { string } name - The event name
+     * @param {string} name - The event name
      */
      */
     function trigger(name: string, ...args: any[]): Promise<void>;
     function trigger(name: string, ...args: any[]): Promise<void>;
     /**
     /**
@@ -23,8 +23,9 @@ declare namespace _default {
      * A hook is a special kind of event which allows you to intercept a data
      * A hook is a special kind of event which allows you to intercept a data
      * structure in order to modify it, before passing it back.
      * structure in order to modify it, before passing it back.
      * @async
      * @async
-     * @param { string } name - The hook name
-     * @param {...any} context - The context to which the hook applies (could be for example, a {@link _converse.ChatBox})).
+     * @param {string} name - The hook name
+     * @param {...any} context - The context to which the hook applies
+     *  (could be for example, a {@link _converse.ChatBox}).
      * @param {...any} data - The data structure to be intercepted and modified by the hook listeners.
      * @param {...any} data - The data structure to be intercepted and modified by the hook listeners.
      * @returns {Promise<any>} - A promise that resolves with the modified data structure.
      * @returns {Promise<any>} - A promise that resolves with the modified data structure.
      */
      */
@@ -33,16 +34,42 @@ declare namespace _default {
         const once: any;
         const once: any;
         const on: any;
         const on: any;
         const not: any;
         const not: any;
+        /**
+         * An options object which lets you set filter criteria for matching
+         * against stanzas.
+         * @typedef {object} MatchingOptions
+         * @property {string} [ns] - The namespace to match against
+         * @property {string} [type] - The stanza type to match against
+         * @property {string} [id] - The stanza id to match against
+         * @property {string} [from] - The stanza sender to match against
+         */
         /**
         /**
          * Subscribe to an incoming stanza
          * Subscribe to an incoming stanza
          * Every a matched stanza is received, the callback method specified by
          * Every a matched stanza is received, the callback method specified by
          * `callback` will be called.
          * `callback` will be called.
          * @method _converse.api.listen.stanza
          * @method _converse.api.listen.stanza
-         * @param { string } name The stanza's name
-         * @param { object } options Matching options (e.g. 'ns' for namespace, 'type' for stanza type, also 'id' and 'from');
-         * @param { function } handler The callback method to be called when the stanza appears
+         * @param {string} name The stanza's name
+         * @param {MatchingOptions|Function} options Matching options or callback
+         * @param {function} handler The callback method to be called when the stanza appears
          */
          */
-        function stanza(name: string, options: any, handler: Function): void;
+        function stanza(name: string, options: Function | {
+            /**
+             * - The namespace to match against
+             */
+            ns?: string;
+            /**
+             * - The stanza type to match against
+             */
+            type?: string;
+            /**
+             * - The stanza id to match against
+             */
+            id?: string;
+            /**
+             * - The stanza sender to match against
+             */
+            from?: string;
+        }, handler: Function): void;
     }
     }
 }
 }
 export default _default;
 export default _default;

+ 1 - 1
src/plugins/muc-views/templates/muc-sidebar.js

@@ -65,7 +65,7 @@ export default (el, o) => {
             </a>
             </a>
         `);
         `);
         btns.push(html`
         btns.push(html`
-            <a href="#" class="dropdown-item" @click=${(/** @type {MouseEvent} */ev) => el.toggleFilter(ev)}>
+            <a href="#" class="dropdown-item toggle-filter" @click=${(/** @type {MouseEvent} */ev) => el.toggleFilter(ev)}>
                 <converse-icon size="1em" class="fa fa-filter"></converse-icon>
                 <converse-icon size="1em" class="fa fa-filter"></converse-icon>
                 ${is_filter_visible ? i18n_hide_filter : i18n_show_filter}
                 ${is_filter_visible ? i18n_hide_filter : i18n_show_filter}
             </a>
             </a>

+ 11 - 2
src/plugins/muc-views/tests/occupants-filter.js

@@ -53,7 +53,14 @@ describe("The MUC occupants filter", function () {
             expect(occupants.querySelectorAll('li .occupant-nick')[index].textContent.trim()).toBe(name);
             expect(occupants.querySelectorAll('li .occupant-nick')[index].textContent.trim()).toBe(name);
         });
         });
 
 
-        filter_el = view.querySelector('converse-list-filter');
+        const dropdown = await u.waitUntil(() => view.querySelector('.occupants-header converse-dropdown'));
+
+        expect(view.querySelector('converse-list-filter')).toBe(null);
+
+        dropdown.click();
+        dropdown.querySelector('.toggle-filter').click();
+
+        filter_el = await u.waitUntil(() => view.querySelector('converse-list-filter'));
         expect(u.isVisible(filter_el.firstElementChild)).toBe(true);
         expect(u.isVisible(filter_el.firstElementChild)).toBe(true);
 
 
         const filter = filter_el.querySelector('.items-filter');
         const filter = filter_el.querySelector('.items-filter');
@@ -62,7 +69,9 @@ describe("The MUC occupants filter", function () {
         await u.waitUntil(() => [...view.querySelectorAll('li')].filter(u.isVisible).length === 1);
         await u.waitUntil(() => [...view.querySelectorAll('li')].filter(u.isVisible).length === 1);
 
 
         filter_el.querySelector('.fa-times').click();
         filter_el.querySelector('.fa-times').click();
-        await u.waitUntil(() => [...view.querySelectorAll('li')].filter(u.isVisible).length === 3+mock.chatroom_names.length);
+        await u.waitUntil(
+            () => [...view.querySelectorAll('li')].filter(u.isVisible).length === 3+mock.chatroom_names.length
+        );
 
 
         filter_el.querySelector('.fa-circle').click();
         filter_el.querySelector('.fa-circle').click();
         const state_select = view.querySelector('.state-type');
         const state_select = view.querySelector('.state-type');

+ 10 - 1
src/plugins/rosterview/rosterview.js

@@ -45,12 +45,14 @@ export default class RosterView extends CustomElement {
         return tplRoster(this);
         return tplRoster(this);
     }
     }
 
 
+    /** @param {MouseEvent} ev */
     showAddContactModal (ev) {
     showAddContactModal (ev) {
         api.modal.show('converse-add-contact-modal', {'model': new Model()}, ev);
         api.modal.show('converse-add-contact-modal', {'model': new Model()}, ev);
     }
     }
 
 
+    /** @param {MouseEvent} [ev] */
     async syncContacts (ev) {
     async syncContacts (ev) {
-        ev.preventDefault();
+        ev?.preventDefault();
         const { roster } = _converse.state;
         const { roster } = _converse.state;
         this.syncing_contacts = true;
         this.syncing_contacts = true;
         this.requestUpdate();
         this.requestUpdate();
@@ -63,6 +65,7 @@ export default class RosterView extends CustomElement {
         this.requestUpdate();
         this.requestUpdate();
     }
     }
 
 
+    /** @param {MouseEvent} [ev] */
     toggleRoster (ev) {
     toggleRoster (ev) {
         ev?.preventDefault?.();
         ev?.preventDefault?.();
         const list_el = /** @type {HTMLElement} */(this.querySelector('.list-container.roster-contacts'));
         const list_el = /** @type {HTMLElement} */(this.querySelector('.list-container.roster-contacts'));
@@ -72,6 +75,12 @@ export default class RosterView extends CustomElement {
             slideIn(list_el).then(() => this.model.save({'toggle_state': CLOSED}));
             slideIn(list_el).then(() => this.model.save({'toggle_state': CLOSED}));
         }
         }
     }
     }
+
+    /** @param {MouseEvent} [ev] */
+    toggleFilter (ev) {
+        ev?.preventDefault?.();
+        this.model.save({ filter_visible: !this.model.get('filter_visible') });
+    }
 }
 }
 
 
 api.elements.define('converse-roster', RosterView);
 api.elements.define('converse-roster', RosterView);

+ 59 - 32
src/plugins/rosterview/templates/roster.js

@@ -18,13 +18,59 @@ export default (el) => {
     const i18n_heading_contacts = __('Contacts');
     const i18n_heading_contacts = __('Contacts');
     const i18n_toggle_contacts = __('Click to toggle contacts');
     const i18n_toggle_contacts = __('Click to toggle contacts');
     const i18n_title_add_contact = __('Add a contact');
     const i18n_title_add_contact = __('Add a contact');
-    const i18n_title_sync_contacts = __('Re-sync your contacts');
     const roster = _converse.state.roster || [];
     const roster = _converse.state.roster || [];
     const contacts_map = roster.reduce((acc, contact) => populateContactsMap(acc, contact), {});
     const contacts_map = roster.reduce((acc, contact) => populateContactsMap(acc, contact), {});
-    const groupnames = Object.keys(contacts_map).filter(shouldShowGroup);
+    const groupnames = Object.keys(contacts_map).filter((contact) => shouldShowGroup(contact, el.model));
     const is_closed = el.model.get('toggle_state') === CLOSED;
     const is_closed = el.model.get('toggle_state') === CLOSED;
     groupnames.sort(groupsComparator);
     groupnames.sort(groupsComparator);
 
 
+    const i18n_show_filter = __('Show filter');
+    const i18n_hide_filter = __('Hide filter');
+    const is_filter_visible = el.model.get('filter_visible');
+
+    const btns = /** @type {TemplateResult[]} */ [];
+    if (api.settings.get('allow_contact_requests')) {
+        btns.push(html`
+            <a
+                href="#"
+                class="dropdown-item add-contact"
+                @click=${(/** @type {MouseEvent} */ ev) => el.showAddContactModal(ev)}
+                title="${i18n_title_add_contact}"
+                data-toggle="modal"
+                data-target="#add-contact-modal"
+            >
+                <converse-icon class="fa fa-user-plus" size="1em"></converse-icon>
+                ${i18n_title_add_contact}
+            </a>
+        `);
+    }
+
+    if (roster.length > 5) {
+        btns.push(html`
+            <a href="#"
+               class="dropdown-item toggle-filter"
+               @click=${(/** @type {MouseEvent} */ ev) => el.toggleFilter(ev)}>
+                <converse-icon size="1em" class="fa fa-filter"></converse-icon>
+                ${is_filter_visible ? i18n_hide_filter : i18n_show_filter}
+            </a>
+        `);
+    }
+
+    if (api.settings.get("loglevel") === 'debug') {
+        const i18n_title_sync_contacts = __('Re-sync contacts');
+        btns.push(html`
+            <a
+                href="#"
+                class="dropdown-item"
+                @click=${(/** @type {MouseEvent} */ ev) => el.syncContacts(ev)}
+                title="${i18n_title_sync_contacts}"
+            >
+                <converse-icon class="fa fa-sync sync-contacts" size="1em"></converse-icon>
+                ${i18n_title_sync_contacts}
+            </a>
+        `);
+    }
+
     return html`
     return html`
         <div class="d-flex controlbox-padded">
         <div class="d-flex controlbox-padded">
             <span class="w-100 controlbox-heading controlbox-heading--contacts">
             <span class="w-100 controlbox-heading controlbox-heading--contacts">
@@ -37,43 +83,24 @@ export default (el) => {
                     ${i18n_heading_contacts}
                     ${i18n_heading_contacts}
                 </a>
                 </a>
             </span>
             </span>
-            <a
-                class="controlbox-heading__btn sync-contacts"
-                @click=${(ev) => el.syncContacts(ev)}
-                title="${i18n_title_sync_contacts}"
-            >
-                <converse-icon
-                    class="fa fa-sync right ${el.syncing_contacts ? 'fa-spin' : ''}"
-                    size="1em"
-                ></converse-icon>
-            </a>
-            ${api.settings.get('allow_contact_requests')
-                ? html` <a
-                      class="controlbox-heading__btn add-contact"
-                      @click=${(ev) => el.showAddContactModal(ev)}
-                      title="${i18n_title_add_contact}"
-                      data-toggle="modal"
-                      data-target="#add-contact-modal"
-                  >
-                      <converse-icon class="fa fa-user-plus right" size="1.25em"></converse-icon>
-                  </a>`
-                : ''}
+            <converse-dropdown class="chatbox-btn dropleft dropdown--contacts" .items=${btns}></converse-dropdown>
         </div>
         </div>
 
 
         <div class="list-container roster-contacts ${is_closed ? 'hidden' : ''}">
         <div class="list-container roster-contacts ${is_closed ? 'hidden' : ''}">
-            <converse-list-filter
-                @update=${() => el.requestUpdate()}
-                .promise=${api.waitUntil('rosterInitialized')}
-                .items=${_converse.state.roster}
-                .template=${tplRosterFilter}
-                .model=${_converse.state.roster_filter}
-            ></converse-list-filter>
-
+            ${is_filter_visible
+                ? html` <converse-list-filter
+                      @update=${() => el.requestUpdate()}
+                      .promise=${api.waitUntil('rosterInitialized')}
+                      .items=${_converse.state.roster}
+                      .template=${tplRosterFilter}
+                      .model=${_converse.state.roster_filter}
+                  ></converse-list-filter>`
+                : ''}
             ${repeat(
             ${repeat(
                 groupnames,
                 groupnames,
                 (n) => n,
                 (n) => n,
                 (name) => {
                 (name) => {
-                    const contacts = contacts_map[name].filter((c) => shouldShowContact(c, name));
+                    const contacts = contacts_map[name].filter((c) => shouldShowContact(c, name, el.model));
                     contacts.sort(contactsComparator);
                     contacts.sort(contactsComparator);
                     return contacts.length ? tplGroup({ contacts, name }) : '';
                     return contacts.length ? tplGroup({ contacts, name }) : '';
                 }
                 }

+ 2 - 2
src/plugins/rosterview/templates/roster_filter.js

@@ -14,7 +14,7 @@ export default (el) => {
     const title_status_filter = __('Filter by status');
     const title_status_filter = __('Filter by status');
     const label_any = __('Any');
     const label_any = __('Any');
     const label_unread_messages = __('Unread');
     const label_unread_messages = __('Unread');
-    const label_online = __('Online');
+    const label_available = __('Available');
     const label_chatty = __('Chatty');
     const label_chatty = __('Chatty');
     const label_busy = __('Busy');
     const label_busy = __('Busy');
     const label_away = __('Away');
     const label_away = __('Away');
@@ -48,7 +48,7 @@ export default (el) => {
                         @change=${ev => el.changeChatStateFilter(ev)}>
                         @change=${ev => el.changeChatStateFilter(ev)}>
                     <option value="">${label_any}</option>
                     <option value="">${label_any}</option>
                     <option ?selected=${chat_state === 'unread_messages'} value="unread_messages">${label_unread_messages}</option>
                     <option ?selected=${chat_state === 'unread_messages'} value="unread_messages">${label_unread_messages}</option>
-                    <option ?selected=${chat_state === 'online'} value="online">${label_online}</option>
+                    <option ?selected=${chat_state === 'online'} value="online">${label_available}</option>
                     <option ?selected=${chat_state === 'chat'} value="chat">${label_chatty}</option>
                     <option ?selected=${chat_state === 'chat'} value="chat">${label_chatty}</option>
                     <option ?selected=${chat_state === 'dnd'} value="dnd">${label_busy}</option>
                     <option ?selected=${chat_state === 'dnd'} value="dnd">${label_busy}</option>
                     <option ?selected=${chat_state === 'away'} value="away">${label_away}</option>
                     <option ?selected=${chat_state === 'away'} value="away">${label_away}</option>

+ 6 - 1
src/plugins/rosterview/tests/add-contact-modal.js

@@ -13,7 +13,12 @@ describe("The 'Add Contact' widget", function () {
         await mock.openControlBox(_converse);
         await mock.openControlBox(_converse);
 
 
         const cbview = _converse.chatboxviews.get('controlbox');
         const cbview = _converse.chatboxviews.get('controlbox');
-        cbview.querySelector('.add-contact').click()
+
+        const dropdown = await u.waitUntil(
+            () => cbview.querySelector('.dropdown--contacts')
+        );
+        dropdown.querySelector('.add-contact').click()
+
         const modal = _converse.api.modal.get('converse-add-contact-modal');
         const modal = _converse.api.modal.get('converse-add-contact-modal');
         await u.waitUntil(() => u.isVisible(modal), 1000);
         await u.waitUntil(() => u.isVisible(modal), 1000);
         expect(modal.querySelector('form.add-xmpp-contact')).not.toBe(null);
         expect(modal.querySelector('form.add-xmpp-contact')).not.toBe(null);

+ 70 - 30
src/plugins/rosterview/tests/roster.js

@@ -165,11 +165,13 @@ describe("The Contacts Roster", function () {
         expect(_converse.roster.get('lord.capulet@example.net').get('subscription')).toBe('none');
         expect(_converse.roster.get('lord.capulet@example.net').get('subscription')).toBe('none');
     }));
     }));
 
 
-    it("can be refreshed", mock.initConverse(
-        [], {}, async function (_converse) {
+    it("can be refreshed if loglevel is set to debug", mock.initConverse(
+        [], {loglevel: 'debug'}, async function (_converse) {
 
 
         const sent_IQs = _converse.api.connection.get().IQ_stanzas;
         const sent_IQs = _converse.api.connection.get().IQ_stanzas;
-        let stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector('iq query[xmlns="jabber:iq:roster"]')).pop());
+        let stanza = await u.waitUntil(
+            () => sent_IQs.filter(iq => iq.querySelector('iq query[xmlns="jabber:iq:roster"]')).pop());
+
         _converse.api.connection.get()._dataRecv(mock.createRequest($iq({
         _converse.api.connection.get()._dataRecv(mock.createRequest($iq({
             to: _converse.api.connection.get().jid,
             to: _converse.api.connection.get().jid,
             type: 'result',
             type: 'result',
@@ -193,10 +195,17 @@ describe("The Contacts Roster", function () {
         expect(_converse.roster.pluck('jid')).toEqual(['juliet@example.net', 'mercutio@example.net']);
         expect(_converse.roster.pluck('jid')).toEqual(['juliet@example.net', 'mercutio@example.net']);
 
 
         const rosterview = document.querySelector('converse-roster');
         const rosterview = document.querySelector('converse-roster');
-        const sync_button = rosterview.querySelector('.sync-contacts');
+
+        const dropdown = await u.waitUntil(
+            () => rosterview.querySelector('.dropdown--contacts')
+        );
+        const sync_button = dropdown.querySelector('.sync-contacts');
         sync_button.click();
         sync_button.click();
 
 
-        stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector('iq query[xmlns="jabber:iq:roster"]')).pop());
+        stanza = await u.waitUntil(
+            () => sent_IQs.filter(iq => iq.querySelector('iq query[xmlns="jabber:iq:roster"]')).pop()
+        );
+
         _converse.api.connection.get()._dataRecv(mock.createRequest($iq({
         _converse.api.connection.get()._dataRecv(mock.createRequest($iq({
             to: _converse.api.connection.get().jid,
             to: _converse.api.connection.get().jid,
             type: 'result',
             type: 'result',
@@ -223,9 +232,14 @@ describe("The Contacts Roster", function () {
         await mock.waitForRoster(_converse, 'current');
         await mock.waitForRoster(_converse, 'current');
 
 
         const rosterview = document.querySelector('converse-roster');
         const rosterview = document.querySelector('converse-roster');
-        const filter = rosterview.querySelector('.items-filter');
         const roster = rosterview.querySelector('.roster-contacts');
         const roster = rosterview.querySelector('.roster-contacts');
 
 
+        const dropdown = await u.waitUntil(
+            () => rosterview.querySelector('.dropdown--contacts')
+        );
+        dropdown.querySelector('.toggle-filter').click();
+
+        const filter = await u.waitUntil(() => rosterview.querySelector('.items-filter'));
         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";
         u.triggerEvent(filter, "keydown", "KeyboardEvent");
         u.triggerEvent(filter, "keydown", "KeyboardEvent");
@@ -261,23 +275,25 @@ describe("The Contacts Roster", function () {
 
 
     describe("The live filter", function () {
     describe("The live filter", function () {
 
 
-        it("will only appear when roster contacts flow over the visible area",
+        it("will only be an option when there are more than 5 contacts",
                 mock.initConverse([], {}, async function (_converse) {
                 mock.initConverse([], {}, async function (_converse) {
 
 
             expect(document.querySelector('converse-roster')).toBe(null);
             expect(document.querySelector('converse-roster')).toBe(null);
-            await mock.waitForRoster(_converse, 'current');
+            await mock.waitForRoster(_converse, 'current', 5);
             await mock.openControlBox(_converse);
             await mock.openControlBox(_converse);
 
 
             const view = _converse.chatboxviews.get('controlbox');
             const view = _converse.chatboxviews.get('controlbox');
-            const flyout = view.querySelector('.box-flyout');
-            const panel = flyout.querySelector('.controlbox-pane');
-            function hasScrollBar (el) {
-                return el.isConnected && flyout.offsetHeight < panel.scrollHeight;
-            }
-            const rosterview = document.querySelector('converse-roster');
-            const filter = rosterview.querySelector('.items-filter');
-            const el = rosterview.querySelector('.roster-contacts');
-            await u.waitUntil(() => hasScrollBar(el) ? u.isVisible(filter) : !u.isVisible(filter), 900);
+            const dropdown = await u.waitUntil(
+                () => view.querySelector('.dropdown--contacts')
+            );
+            expect(dropdown.querySelector('.toggle-filter')).toBe(null);
+
+            mock.createContact(_converse, 'Slowpoke', 'subscribe');
+            const el = await u.waitUntil(() => dropdown.querySelector('.toggle-filter'));
+            expect(el).toBeDefined();
+
+            el.click();
+            await u.waitUntil(() => view.querySelector('.roster-contacts converse-list-filter'));
         }));
         }));
 
 
         it("can be used to filter the contacts shown",
         it("can be used to filter the contacts shown",
@@ -288,11 +304,15 @@ 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');
-            let filter = rosterview.querySelector('.items-filter');
             const roster = rosterview.querySelector('.roster-contacts');
             const roster = rosterview.querySelector('.roster-contacts');
 
 
             await u.waitUntil(() => (sizzle('li', roster).filter(u.isVisible).length === 17), 600);
             await u.waitUntil(() => (sizzle('li', roster).filter(u.isVisible).length === 17), 600);
             expect(sizzle('ul.roster-group-contacts', roster).filter(u.isVisible).length).toBe(5);
             expect(sizzle('ul.roster-group-contacts', roster).filter(u.isVisible).length).toBe(5);
+
+            const filter_toggle = await u.waitUntil(() => rosterview.querySelector('.toggle-filter'));
+            filter_toggle.click();
+
+            let filter = await u.waitUntil(() => rosterview.querySelector('.items-filter'));
             filter.value = "juliet";
             filter.value = "juliet";
             u.triggerEvent(filter, "keydown", "KeyboardEvent");
             u.triggerEvent(filter, "keydown", "KeyboardEvent");
             await u.waitUntil(() => (sizzle('li', roster).filter(u.isVisible).length === 1), 600);
             await u.waitUntil(() => (sizzle('li', roster).filter(u.isVisible).length === 1), 600);
@@ -338,7 +358,10 @@ describe("The Contacts Roster", function () {
             const rosterview = document.querySelector('converse-roster');
             const rosterview = document.querySelector('converse-roster');
             const roster = rosterview.querySelector('.roster-contacts');
             const roster = rosterview.querySelector('.roster-contacts');
 
 
-            const button =  rosterview.querySelector('converse-icon[data-type="groups"]');
+            const filter_toggle = await u.waitUntil(() => rosterview.querySelector('.toggle-filter'));
+            filter_toggle.click();
+
+            const button =  await u.waitUntil(() => rosterview.querySelector('converse-icon[data-type="groups"]'));
             button.click();
             button.click();
 
 
             await u.waitUntil(() => (sizzle('li', roster).filter(u.isVisible).length === 17), 600);
             await u.waitUntil(() => (sizzle('li', roster).filter(u.isVisible).length === 17), 600);
@@ -374,7 +397,11 @@ describe("The Contacts Roster", function () {
             await mock.waitForRoster(_converse, 'current');
             await mock.waitForRoster(_converse, 'current');
 
 
             const rosterview = document.querySelector('converse-roster');
             const rosterview = document.querySelector('converse-roster');
-            const filter = rosterview.querySelector('.items-filter');
+
+            const filter_toggle = await u.waitUntil(() => rosterview.querySelector('.toggle-filter'));
+            filter_toggle.click();
+
+            const filter = await u.waitUntil(() => rosterview.querySelector('.items-filter'));
             filter.value = "xxx";
             filter.value = "xxx";
             u.triggerEvent(filter, "keydown", "KeyboardEvent");
             u.triggerEvent(filter, "keydown", "KeyboardEvent");
             expect(_.includes(filter.classList, "x")).toBeFalsy();
             expect(_.includes(filter.classList, "x")).toBeFalsy();
@@ -386,37 +413,50 @@ describe("The Contacts Roster", function () {
             await u.waitUntil(() => document.querySelector('.items-filter').value == '');
             await u.waitUntil(() => document.querySelector('.items-filter').value == '');
         }));
         }));
 
 
-        // Disabling for now, because since recently this test consistently
-        // fails on Travis and I couldn't get it to pass there.
-        xit("can be used to filter contacts by their chat state",
+        it("can be used to filter contacts by their chat state",
             mock.initConverse(
             mock.initConverse(
                 [], {},
                 [], {},
                 async function (_converse) {
                 async function (_converse) {
 
 
-            mock.waitForRoster(_converse, 'all');
+            await mock.openControlBox(_converse);
+            await mock.waitForRoster(_converse, 'all');
+
             let jid = mock.cur_names[3].replace(/ /g,'.').toLowerCase() + '@montague.lit';
             let jid = mock.cur_names[3].replace(/ /g,'.').toLowerCase() + '@montague.lit';
             _converse.roster.get(jid).presence.set('show', 'online');
             _converse.roster.get(jid).presence.set('show', 'online');
             jid = mock.cur_names[4].replace(/ /g,'.').toLowerCase() + '@montague.lit';
             jid = mock.cur_names[4].replace(/ /g,'.').toLowerCase() + '@montague.lit';
             _converse.roster.get(jid).presence.set('show', 'dnd');
             _converse.roster.get(jid).presence.set('show', 'dnd');
             await mock.openControlBox(_converse);
             await mock.openControlBox(_converse);
             const rosterview = document.querySelector('converse-roster');
             const rosterview = document.querySelector('converse-roster');
-            const button = rosterview.querySelector('span[data-type="state"]');
+
+            const filter_toggle = await u.waitUntil(() => rosterview.querySelector('.toggle-filter'));
+            filter_toggle.click();
+
+            const button = await u.waitUntil(() => rosterview.querySelector('converse-icon[data-type="state"]'));
             button.click();
             button.click();
-            const roster = rosterview.querySelector('.roster-contacts');
-            await u.waitUntil(() => sizzle('li', roster).filter(u.isVisible).length === 15, 900);
+
             const filter = rosterview.querySelector('.state-type');
             const filter = rosterview.querySelector('.state-type');
+            filter.value = "";
+            u.triggerEvent(filter, 'change');
+
+            const roster = rosterview.querySelector('.roster-contacts');
+            await u.waitUntil(() => sizzle('li', roster).filter(u.isVisible).length === 20, 900);
             expect(sizzle('ul.roster-group-contacts', roster).filter(u.isVisible).length).toBe(5);
             expect(sizzle('ul.roster-group-contacts', roster).filter(u.isVisible).length).toBe(5);
+
             filter.value = "online";
             filter.value = "online";
             u.triggerEvent(filter, 'change');
             u.triggerEvent(filter, 'change');
 
 
             await u.waitUntil(() => sizzle('li', roster).filter(u.isVisible).length === 1, 900);
             await u.waitUntil(() => sizzle('li', roster).filter(u.isVisible).length === 1, 900);
             expect(sizzle('li', roster).filter(u.isVisible).pop().textContent.trim()).toBe('Lord Montague');
             expect(sizzle('li', roster).filter(u.isVisible).pop().textContent.trim()).toBe('Lord Montague');
-            await u.waitUntil(() => sizzle('ul.roster-group-contacts', roster).filter(u.isVisible).length === 1, 900);
-            const ul = sizzle('ul.roster-group-contacts', roster).filter(u.isVisible).pop();
-            expect(ul.parentElement.firstElementChild.textContent.trim()).toBe('friends & acquaintences');
+
+            let ul = sizzle('ul.roster-group-contacts', roster).filter(u.isVisible).pop();
+            expect(ul.parentElement.firstElementChild.textContent.trim()).toBe('Family');
+
             filter.value = "dnd";
             filter.value = "dnd";
             u.triggerEvent(filter, 'change');
             u.triggerEvent(filter, 'change');
+
             await u.waitUntil(() => sizzle('li', roster).filter(u.isVisible).pop().textContent.trim() === 'Friar Laurence', 900);
             await u.waitUntil(() => sizzle('li', roster).filter(u.isVisible).pop().textContent.trim() === 'Friar Laurence', 900);
+            ul = sizzle('ul.roster-group-contacts', roster).filter(u.isVisible).pop();
+            expect(ul.parentElement.firstElementChild.textContent.trim()).toBe('friends & acquaintences');
             expect(sizzle('ul.roster-group-contacts', roster).filter(u.isVisible).length).toBe(1);
             expect(sizzle('ul.roster-group-contacts', roster).filter(u.isVisible).length).toBe(1);
         }));
         }));
     });
     });

+ 22 - 3
src/plugins/rosterview/utils.js

@@ -1,3 +1,7 @@
+/**
+ * @typedef {import('@converse/skeletor').Model} Model
+ * @typedef {import('@converse/headless/plugins/roster/contact').default} RosterContact
+ */
 import { __ } from 'i18n';
 import { __ } from 'i18n';
 import { _converse, api, log } from "@converse/headless";
 import { _converse, api, log } from "@converse/headless";
 
 
@@ -29,6 +33,11 @@ export function toggleGroup (ev, name) {
     }
     }
 }
 }
 
 
+/**
+ * @param {RosterContact} contact
+ * @param {string} groupname
+ * @returns {boolean}
+ */
 export function isContactFiltered (contact, groupname) {
 export function isContactFiltered (contact, groupname) {
     const filter = _converse.state.roster_filter;
     const filter = _converse.state.roster_filter;
     const type = filter.get('type');
     const type = filter.get('type');
@@ -48,7 +57,7 @@ export function isContactFiltered (contact, groupname) {
         } else if (q === 'unread_messages') {
         } else if (q === 'unread_messages') {
             return contact.get('num_unread') === 0;
             return contact.get('num_unread') === 0;
         } else if (q === 'online') {
         } else if (q === 'online') {
-            return ["offline", "unavailable"].includes(contact.presence.get('show'));
+            return ["offline", "unavailable", "dnd", "away", "xa"].includes(contact.presence.get('show'));
         } else {
         } else {
             return !contact.presence.get('show').includes(q);
             return !contact.presence.get('show').includes(q);
         }
         }
@@ -57,7 +66,15 @@ export function isContactFiltered (contact, groupname) {
     }
     }
 }
 }
 
 
-export function shouldShowContact (contact, groupname) {
+/**
+ * @param {RosterContact} contact
+ * @param {string} groupname
+ * @param {Model} model
+ * @returns {boolean}
+ */
+export function shouldShowContact (contact, groupname, model) {
+    if (!model.get('filter_visible')) return true;
+
     const chat_status = contact.presence.get('show');
     const chat_status = contact.presence.get('show');
     if (api.settings.get('hide_offline_users') && chat_status === 'offline') {
     if (api.settings.get('hide_offline_users') && chat_status === 'offline') {
         // If pending or requesting, show
         // If pending or requesting, show
@@ -71,7 +88,9 @@ export function shouldShowContact (contact, groupname) {
     return !isContactFiltered(contact, groupname);
     return !isContactFiltered(contact, groupname);
 }
 }
 
 
-export function shouldShowGroup (group) {
+export function shouldShowGroup (group, model) {
+    if (!model.get('filter_visible')) return true;
+
     const filter = _converse.state.roster_filter;
     const filter = _converse.state.roster_filter;
     const type = filter.get('type');
     const type = filter.get('type');
     if (type === 'groups') {
     if (type === 'groups') {

+ 17 - 8
src/shared/chat/message-actions.js

@@ -11,13 +11,13 @@ import { until } from 'lit/directives/until.js';
 import './styles/message-actions.scss';
 import './styles/message-actions.scss';
 
 
 /**
 /**
- * @typedef { Object } MessageActionAttributes
+ * @typedef {Object} MessageActionAttributes
  * An object which represents a message action (as shown in the message dropdown);
  * An object which represents a message action (as shown in the message dropdown);
- * @property { String } i18n_text
- * @property { Function } handler
- * @property { String } button_class
- * @property { String } icon_class
- * @property { String } name
+ * @property {String} i18n_text
+ * @property {Function} handler
+ * @property {String} button_class
+ * @property {String} icon_class
+ * @property {String} name
  */
  */
 
 
 const { u } = converse.env;
 const { u } = converse.env;
@@ -52,7 +52,10 @@ class MessageActions extends CustomElement {
     }
     }
 
 
     updateIfOwnOccupant (o) {
     updateIfOwnOccupant (o) {
-        (o.get('jid') === _converse.bare_jid) && this.requestUpdate();
+        const bare_jid = _converse.session.get('bare_jid');
+        if (o.get('jid') === bare_jid) {
+            this.requestUpdate();
+        }
     }
     }
 
 
     render () {
     render () {
@@ -96,6 +99,7 @@ class MessageActions extends CustomElement {
         `;
         `;
     }
     }
 
 
+    /** @param {MouseEvent} ev */
     async onMessageEditButtonClicked (ev) {
     async onMessageEditButtonClicked (ev) {
         ev.preventDefault();
         ev.preventDefault();
         const currently_correcting = this.model.collection.findWhere('correcting');
         const currently_correcting = this.model.collection.findWhere('correcting');
@@ -193,6 +197,7 @@ class MessageActions extends CustomElement {
         }
         }
     }
     }
 
 
+    /** @param {MouseEvent} [ev] */
     onMessageRetractButtonClicked (ev) {
     onMessageRetractButtonClicked (ev) {
         ev?.preventDefault?.();
         ev?.preventDefault?.();
         const chatbox = this.model.collection.chatbox;
         const chatbox = this.model.collection.chatbox;
@@ -203,6 +208,7 @@ class MessageActions extends CustomElement {
         }
         }
     }
     }
 
 
+    /** @param {MouseEvent} [ev] */
     onMediaToggleClicked (ev) {
     onMediaToggleClicked (ev) {
         ev?.preventDefault?.();
         ev?.preventDefault?.();
 
 
@@ -285,14 +291,17 @@ class MessageActions extends CustomElement {
         }
         }
     }
     }
 
 
+    /** @param {MouseEvent} [ev] */
     async onMessageCopyButtonClicked (ev) {
     async onMessageCopyButtonClicked (ev) {
         ev?.preventDefault?.();
         ev?.preventDefault?.();
         await navigator.clipboard.writeText(this.model.getMessageText());
         await navigator.clipboard.writeText(this.model.getMessageText());
     }
     }
 
 
+    /** @param {MouseEvent} [ev] */
     onMessageQuoteButtonClicked (ev) {
     onMessageQuoteButtonClicked (ev) {
         ev?.preventDefault?.();
         ev?.preventDefault?.();
-        const view = _converse.chatboxviews.get(this.model.collection.chatbox.get('jid'));
+        const { chatboxviews } = _converse.state;
+        const view = chatboxviews.get(this.model.collection.chatbox.get('jid'));
         view?.getMessageForm().insertIntoTextArea(
         view?.getMessageForm().insertIntoTextArea(
             this.model.getMessageText().replaceAll(/^/gm, '> '),
             this.model.getMessageText().replaceAll(/^/gm, '> '),
             false, false, null, '\n'
             false, false, null, '\n'

+ 1 - 0
src/types/headless/plugins/chat/model.d.ts

@@ -241,6 +241,7 @@ declare class ChatBox extends ModelWithContact {
     incrementUnreadMsgsCounter(message: Message): void;
     incrementUnreadMsgsCounter(message: Message): void;
     clearUnreadMsgCounter(): void;
     clearUnreadMsgCounter(): void;
     isScrolledUp(): any;
     isScrolledUp(): any;
+    canPostMessages(): boolean;
 }
 }
 import ModelWithContact from "./model-with-contact.js";
 import ModelWithContact from "./model-with-contact.js";
 import { Model } from "@converse/skeletor";
 import { Model } from "@converse/skeletor";

+ 34 - 7
src/types/headless/shared/api/events.d.ts

@@ -13,7 +13,7 @@ declare namespace _default {
      *  event handlers' promises have been resolved.
      *  event handlers' promises have been resolved.
      *
      *
      * @method _converse.api.trigger
      * @method _converse.api.trigger
-     * @param { string } name - The event name
+     * @param {string} name - The event name
      */
      */
     function trigger(name: string, ...args: any[]): Promise<void>;
     function trigger(name: string, ...args: any[]): Promise<void>;
     /**
     /**
@@ -23,8 +23,9 @@ declare namespace _default {
      * A hook is a special kind of event which allows you to intercept a data
      * A hook is a special kind of event which allows you to intercept a data
      * structure in order to modify it, before passing it back.
      * structure in order to modify it, before passing it back.
      * @async
      * @async
-     * @param { string } name - The hook name
-     * @param {...any} context - The context to which the hook applies (could be for example, a {@link _converse.ChatBox})).
+     * @param {string} name - The hook name
+     * @param {...any} context - The context to which the hook applies
+     *  (could be for example, a {@link _converse.ChatBox}).
      * @param {...any} data - The data structure to be intercepted and modified by the hook listeners.
      * @param {...any} data - The data structure to be intercepted and modified by the hook listeners.
      * @returns {Promise<any>} - A promise that resolves with the modified data structure.
      * @returns {Promise<any>} - A promise that resolves with the modified data structure.
      */
      */
@@ -33,16 +34,42 @@ declare namespace _default {
         const once: any;
         const once: any;
         const on: any;
         const on: any;
         const not: any;
         const not: any;
+        /**
+         * An options object which lets you set filter criteria for matching
+         * against stanzas.
+         * @typedef {object} MatchingOptions
+         * @property {string} [ns] - The namespace to match against
+         * @property {string} [type] - The stanza type to match against
+         * @property {string} [id] - The stanza id to match against
+         * @property {string} [from] - The stanza sender to match against
+         */
         /**
         /**
          * Subscribe to an incoming stanza
          * Subscribe to an incoming stanza
          * Every a matched stanza is received, the callback method specified by
          * Every a matched stanza is received, the callback method specified by
          * `callback` will be called.
          * `callback` will be called.
          * @method _converse.api.listen.stanza
          * @method _converse.api.listen.stanza
-         * @param { string } name The stanza's name
-         * @param { object } options Matching options (e.g. 'ns' for namespace, 'type' for stanza type, also 'id' and 'from');
-         * @param { function } handler The callback method to be called when the stanza appears
+         * @param {string} name The stanza's name
+         * @param {MatchingOptions|Function} options Matching options or callback
+         * @param {function} handler The callback method to be called when the stanza appears
          */
          */
-        function stanza(name: string, options: any, handler: Function): void;
+        function stanza(name: string, options: Function | {
+            /**
+             * - The namespace to match against
+             */
+            ns?: string;
+            /**
+             * - The stanza type to match against
+             */
+            type?: string;
+            /**
+             * - The stanza id to match against
+             */
+            id?: string;
+            /**
+             * - The stanza sender to match against
+             */
+            from?: string;
+        }, handler: Function): void;
     }
     }
 }
 }
 export default _default;
 export default _default;

+ 1 - 1
src/types/plugins/chatview/message-form.d.ts

@@ -13,7 +13,7 @@ export default class MessageForm extends CustomElement {
      * @param { number } [position] - The end index of the string to be
      * @param { number } [position] - The end index of the string to be
      *  replaced with the new value.
      *  replaced with the new value.
      */
      */
-    insertIntoTextArea(value: string, replace?: (boolean | string), correcting?: boolean, position?: number): void;
+    insertIntoTextArea(value: string, replace?: (boolean | string), correcting?: boolean, position?: number, separator?: string): void;
     onMessageCorrecting(message: any): void;
     onMessageCorrecting(message: any): void;
     onEscapePressed(ev: any): void;
     onEscapePressed(ev: any): void;
     onPaste(ev: any): void;
     onPaste(ev: any): void;

+ 8 - 3
src/types/plugins/rosterview/rosterview.d.ts

@@ -7,10 +7,15 @@ export default class RosterView extends CustomElement {
     initialize(): Promise<void>;
     initialize(): Promise<void>;
     model: Model;
     model: Model;
     render(): import("lit-html").TemplateResult<1>;
     render(): import("lit-html").TemplateResult<1>;
-    showAddContactModal(ev: any): void;
-    syncContacts(ev: any): Promise<void>;
+    /** @param {MouseEvent} ev */
+    showAddContactModal(ev: MouseEvent): void;
+    /** @param {MouseEvent} [ev] */
+    syncContacts(ev?: MouseEvent): Promise<void>;
     syncing_contacts: boolean;
     syncing_contacts: boolean;
-    toggleRoster(ev: any): void;
+    /** @param {MouseEvent} [ev] */
+    toggleRoster(ev?: MouseEvent): void;
+    /** @param {MouseEvent} [ev] */
+    toggleFilter(ev?: MouseEvent): void;
 }
 }
 import { CustomElement } from "shared/components/element.js";
 import { CustomElement } from "shared/components/element.js";
 import { Model } from "@converse/skeletor";
 import { Model } from "@converse/skeletor";

+ 9 - 2
src/types/plugins/rosterview/utils.d.ts

@@ -2,7 +2,14 @@ export function removeContact(contact: any): void;
 export function highlightRosterItem(chatbox: any): void;
 export function highlightRosterItem(chatbox: any): void;
 export function toggleGroup(ev: any, name: any): void;
 export function toggleGroup(ev: any, name: any): void;
 export function isContactFiltered(contact: any, groupname: any): boolean;
 export function isContactFiltered(contact: any, groupname: any): boolean;
-export function shouldShowContact(contact: any, groupname: any): boolean;
-export function shouldShowGroup(group: any): boolean;
+/**
+ * @param {RosterContact} contact
+ * @param {string} groupname
+ * @param {Model} model
+ */
+export function shouldShowContact(contact: any, groupname: string, model: Model): boolean;
+export function shouldShowGroup(group: any, model: any): boolean;
 export function populateContactsMap(contacts_map: any, contact: any): any;
 export function populateContactsMap(contacts_map: any, contact: any): any;
+export type Model = import('@converse/skeletor').Model;
+export type RosterContact = any;
 //# sourceMappingURL=utils.d.ts.map
 //# sourceMappingURL=utils.d.ts.map