Prechádzať zdrojové kódy

Render the roster container and filter with lit-html

This is the first commit that makes use of the `ElementView` from
Skeletor, which lets us turn views into custom elements.
JC Brand 4 rokov pred
rodič
commit
bb3ac36098

+ 3 - 3
package-lock.json

@@ -13380,9 +13380,9 @@
 			"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
 			"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
 		},
 		},
 		"lodash-es": {
 		"lodash-es": {
-			"version": "4.17.15",
-			"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.15.tgz",
-			"integrity": "sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ=="
+			"version": "4.17.20",
+			"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.20.tgz",
+			"integrity": "sha512-JD1COMZsq8maT6mnuz1UMV0jvYD0E0aUsSOdrr1/nAG3dhqQXwRRgeW0cSqH1U43INKcqxaiVIQNOUDld7gRDA=="
 		},
 		},
 		"lodash-template-webpack-loader": {
 		"lodash-template-webpack-loader": {
 			"version": "github:jcbrand/lodash-template-webpack-loader#258c095ab22130dfde454fa59ee0986f302bb733",
 			"version": "github:jcbrand/lodash-template-webpack-loader#258c095ab22130dfde454fa59ee0986f302bb733",

+ 1 - 0
package.json

@@ -65,6 +65,7 @@
   "devDependencies": {
   "devDependencies": {
     "@babel/cli": "^7.10.3",
     "@babel/cli": "^7.10.3",
     "@babel/core": "^7.10.5",
     "@babel/core": "^7.10.5",
+    "@babel/plugin-proposal-class-properties": "^7.12.1",
     "@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.1",
     "@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.1",
     "@babel/plugin-proposal-optional-chaining": "^7.12.1",
     "@babel/plugin-proposal-optional-chaining": "^7.12.1",
     "@babel/plugin-syntax-dynamic-import": "^7.2.0",
     "@babel/plugin-syntax-dynamic-import": "^7.2.0",

+ 41 - 35
src/plugins/rosterview/filterview.js

@@ -1,9 +1,9 @@
 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 { Model } from '@converse/skeletor/src/model.js';
 import { Model } from '@converse/skeletor/src/model.js';
-import { View } from '@converse/skeletor/src/view.js';
-import { __ } from 'i18n';
-import { _converse } from "@converse/headless/core";
+import { _converse, api } from "@converse/headless/core";
 import { debounce } from "lodash-es";
 import { debounce } from "lodash-es";
+import { render } from 'lit-html';
 
 
 
 
 export const RosterFilter = Model.extend({
 export const RosterFilter = Model.extend({
@@ -17,42 +17,49 @@ export const RosterFilter = Model.extend({
 });
 });
 
 
 
 
-export const RosterFilterView = View.extend({
-    tagName: 'span',
+export class RosterFilterView extends ElementView {
+    tagName = 'span';
 
 
     initialize () {
     initialize () {
+        const model = new _converse.RosterFilter();
+        model.id = `_converse.rosterfilter-${_converse.bare_jid}`;
+        model.browserStorage = _converse.createStore(model.id);
+        this.model = model;
+
+        this.liveFilter = debounce(() => {
+            this.model.save({'filter_text': this.querySelector('.roster-filter').value});
+        }, 250);
+
         this.listenTo(this.model, 'change:filter_type', this.render);
         this.listenTo(this.model, 'change:filter_type', this.render);
         this.listenTo(this.model, 'change:filter_text', this.render);
         this.listenTo(this.model, 'change:filter_text', this.render);
-    },
 
 
-    toHTML () {
-        return tpl_roster_filter(
+        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);
+        api.listen.on('rosterContactsFetchedAndProcessed', () => this.render());
+
+        this.model.fetch();
+        this.render();
+    }
+
+    render () {
+        render(tpl_roster_filter(
             Object.assign(this.model.toJSON(), {
             Object.assign(this.model.toJSON(), {
                 visible: this.shouldBeVisible(),
                 visible: this.shouldBeVisible(),
-                placeholder: __('Filter'),
-                title_contact_filter: __('Filter by contact name'),
-                title_group_filter: __('Filter by group name'),
-                title_status_filter: __('Filter by status'),
-                label_any: __('Any'),
-                label_unread_messages: __('Unread'),
-                label_online: __('Online'),
-                label_chatty: __('Chatty'),
-                label_busy: __('Busy'),
-                label_away: __('Away'),
-                label_xa: __('Extended Away'),
-                label_offline: __('Offline'),
                 changeChatStateFilter: ev => this.changeChatStateFilter(ev),
                 changeChatStateFilter: ev => this.changeChatStateFilter(ev),
                 changeTypeFilter: ev => this.changeTypeFilter(ev),
                 changeTypeFilter: ev => this.changeTypeFilter(ev),
                 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;
+    }
 
 
     changeChatStateFilter (ev) {
     changeChatStateFilter (ev) {
         ev && ev.preventDefault();
         ev && ev.preventDefault();
-        this.model.save({'chat_state': this.el.querySelector('.state-type').value});
-    },
+        this.model.save({'chat_state': this.querySelector('.state-type').value});
+    }
 
 
     changeTypeFilter (ev) {
     changeTypeFilter (ev) {
         ev && ev.preventDefault();
         ev && ev.preventDefault();
@@ -60,24 +67,20 @@ export const RosterFilterView = View.extend({
         if (type === 'state') {
         if (type === 'state') {
             this.model.save({
             this.model.save({
                 'filter_type': type,
                 'filter_type': type,
-                'chat_state': this.el.querySelector('.state-type').value
+                'chat_state': this.querySelector('.state-type').value
             });
             });
         } else {
         } else {
             this.model.save({
             this.model.save({
                 'filter_type': type,
                 'filter_type': type,
-                'filter_text': this.el.querySelector('.roster-filter').value
+                'filter_text': this.querySelector('.roster-filter').value
             });
             });
         }
         }
-    },
-
-    liveFilter: debounce(function () {
-        this.model.save({'filter_text': this.el.querySelector('.roster-filter').value});
-    }, 250),
+    }
 
 
     submitFilter (ev) {
     submitFilter (ev) {
         ev && ev.preventDefault();
         ev && ev.preventDefault();
         this.liveFilter();
         this.liveFilter();
-    },
+    }
 
 
     /**
     /**
      * Returns true if the filter is enabled (i.e. if the user
      * Returns true if the filter is enabled (i.e. if the user
@@ -87,14 +90,17 @@ export const RosterFilterView = View.extend({
      */
      */
     isActive () {
     isActive () {
         return (this.model.get('filter_type') === 'state' || this.model.get('filter_text'));
         return (this.model.get('filter_type') === 'state' || this.model.get('filter_text'));
-    },
+    }
 
 
     shouldBeVisible () {
     shouldBeVisible () {
         return _converse.roster && _converse.roster.length >= 5 || this.isActive();
         return _converse.roster && _converse.roster.length >= 5 || this.isActive();
-    },
+    }
 
 
     clearFilter (ev) {
     clearFilter (ev) {
         ev && ev.preventDefault();
         ev && ev.preventDefault();
         this.model.save({'filter_text': ''});
         this.model.save({'filter_text': ''});
     }
     }
-});
+}
+
+
+api.elements.define('converse-roster-filter', RosterFilterView);

+ 2 - 5
src/plugins/rosterview/groupview.js

@@ -1,7 +1,7 @@
 import RosterContactView from './contactview.js';
 import RosterContactView from './contactview.js';
 import tpl_group_header from "./templates/group_header.html";
 import tpl_group_header from "./templates/group_header.html";
 import { OrderedListView } from "@converse/skeletor/src/overview";
 import { OrderedListView } from "@converse/skeletor/src/overview";
-import { _converse, converse } from "@converse/headless/core";
+import { _converse, api, converse } from "@converse/headless/core";
 
 
 const u = converse.env.utils;
 const u = converse.env.utils;
 
 
@@ -49,10 +49,7 @@ const RosterGroupView = OrderedListView.extend({
         // just this group's) have been fetched from browser
         // just this group's) have been fetched from browser
         // storage or the XMPP server and once they've been
         // storage or the XMPP server and once they've been
         // assigned to their various groups.
         // assigned to their various groups.
-        _converse.rosterview.on(
-            'rosterContactsFetchedAndProcessed',
-            () => this.sortAndPositionAllItems()
-        );
+        api.listen.on('rosterContactsFetchedAndProcessed', () => this.sortAndPositionAllItems());
     },
     },
 
 
     render () {
     render () {

+ 3 - 42
src/plugins/rosterview/index.js

@@ -10,7 +10,7 @@ import "modals/add-contact.js";
 import RosterContactView from './contactview.js';
 import RosterContactView from './contactview.js';
 import RosterGroupView from './groupview.js';
 import RosterGroupView from './groupview.js';
 import RosterView from './rosterview.js';
 import RosterView from './rosterview.js';
-import log from "@converse/headless/log";
+import { initRosterView, highlightRosterItem, insertRoster } from './utils.js';
 import { RosterFilter, RosterFilterView } from './filterview.js';
 import { RosterFilter, RosterFilterView } from './filterview.js';
 import { _converse, api, converse } from "@converse/headless/core";
 import { _converse, api, converse } from "@converse/headless/core";
 
 
@@ -20,10 +20,6 @@ converse.plugins.add('converse-rosterview', {
     dependencies: ["converse-roster", "converse-modal", "converse-chatboxviews"],
     dependencies: ["converse-roster", "converse-modal", "converse-chatboxviews"],
 
 
     initialize () {
     initialize () {
-        /* The initialize function gets called as soon as the plugin is
-         * loaded by converse.js's plugin machinery.
-         */
-
         api.settings.extend({
         api.settings.extend({
             'autocomplete_add_contact': true,
             'autocomplete_add_contact': true,
             'allow_chat_pending_contacts': true,
             'allow_chat_pending_contacts': true,
@@ -42,50 +38,15 @@ converse.plugins.add('converse-rosterview', {
 
 
         /* -------- Event Handlers ----------- */
         /* -------- Event Handlers ----------- */
         api.listen.on('chatBoxesInitialized', () => {
         api.listen.on('chatBoxesInitialized', () => {
-            function highlightRosterItem (chatbox) {
-                const contact = _converse.roster && _converse.roster.findWhere({'jid': chatbox.get('jid')});
-                if (contact !== undefined) {
-                    contact.trigger('highlight');
-                }
-            }
             _converse.chatboxes.on('destroy', chatbox => highlightRosterItem(chatbox));
             _converse.chatboxes.on('destroy', chatbox => highlightRosterItem(chatbox));
             _converse.chatboxes.on('change:hidden', chatbox => highlightRosterItem(chatbox));
             _converse.chatboxes.on('change:hidden', chatbox => highlightRosterItem(chatbox));
         });
         });
 
 
-
         api.listen.on('controlBoxInitialized', (view) => {
         api.listen.on('controlBoxInitialized', (view) => {
-            function insertRoster () {
-                if (!view.model.get('connected') || api.settings.get("authentication") === _converse.ANONYMOUS) {
-                    return;
-                }
-                /* 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));
-            }
-            insertRoster();
-            view.model.on('change:connected', insertRoster);
+            insertRoster(view);
+            view.model.on('change:connected', () => insertRoster(view));
         });
         });
 
 
-
-        function initRosterView () {
-            /* Create an instance of RosterView once the RosterGroups
-             * collection has been created (in @converse/headless/core.js)
-             */
-            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');
-        }
         api.listen.on('rosterInitialized', initRosterView);
         api.listen.on('rosterInitialized', initRosterView);
         api.listen.on('rosterReadyAfterReconnection', initRosterView);
         api.listen.on('rosterReadyAfterReconnection', initRosterView);
 
 

+ 11 - 17
src/plugins/rosterview/rosterview.js

@@ -1,11 +1,12 @@
 import RosterGroupView from './groupview.js';
 import RosterGroupView from './groupview.js';
 import log from "@converse/headless/log";
 import log from "@converse/headless/log";
-import tpl_roster from "./templates/roster.html";
+import tpl_roster from "./templates/roster.js";
 import { Model } from '@converse/skeletor/src/model.js';
 import { Model } from '@converse/skeletor/src/model.js';
 import { OrderedListView } from "@converse/skeletor/src/overview";
 import { OrderedListView } from "@converse/skeletor/src/overview";
 import { __ } from 'i18n';
 import { __ } from 'i18n';
 import { _converse, api, converse } from "@converse/headless/core";
 import { _converse, api, converse } from "@converse/headless/core";
 import { debounce, has } from "lodash-es";
 import { debounce, has } from "lodash-es";
+import { render } from 'lit-html';
 
 
 const u = converse.env.utils;
 const u = converse.env.utils;
 
 
@@ -57,20 +58,18 @@ const RosterView = OrderedListView.extend({
             _converse.roster.each(contact => this.addRosterContact(contact, {'silent': true}));
             _converse.roster.each(contact => this.addRosterContact(contact, {'silent': true}));
             this.update();
             this.update();
             this.updateFilter();
             this.updateFilter();
-            this.trigger('rosterContactsFetchedAndProcessed');
+            api.trigger('rosterContactsFetchedAndProcessed');
         });
         });
-        this.createRosterFilter();
+        this.render();
+        this.listenToRosterFilter();
     },
     },
 
 
     render () {
     render () {
-        this.el.innerHTML = tpl_roster({
-            'allow_contact_requests': _converse.allow_contact_requests,
+        render(tpl_roster({
             'heading_contacts': __('Contacts'),
             'heading_contacts': __('Contacts'),
             'title_add_contact': __('Add a contact'),
             'title_add_contact': __('Add a contact'),
             'title_sync_contacts': __('Re-sync your contacts')
             'title_sync_contacts': __('Re-sync your contacts')
-        });
-        const form = this.el.querySelector('.roster-filter-form');
-        this.el.replaceChild(this.filter_view.render().el, form);
+        }), this.el);
         this.roster_el = this.el.querySelector('.roster-contacts');
         this.roster_el = this.el.querySelector('.roster-contacts');
         return this;
         return this;
     },
     },
@@ -79,14 +78,9 @@ const RosterView = OrderedListView.extend({
         api.modal.show(_converse.AddContactModal, {'model': new Model()}, ev);
         api.modal.show(_converse.AddContactModal, {'model': new Model()}, ev);
     },
     },
 
 
-    createRosterFilter () {
-        // Create a model on which we can store filter properties
-        const model = new _converse.RosterFilter();
-        model.id = `_converse.rosterfilter-${_converse.bare_jid}`;
-        model.browserStorage = _converse.createStore(model.id);
-        this.filter_view = new _converse.RosterFilterView({model});
+    listenToRosterFilter () {
+        this.filter_view = this.el.querySelector('converse-roster-filter');
         this.listenTo(this.filter_view.model, 'change', this.updateFilter);
         this.listenTo(this.filter_view.model, 'change', this.updateFilter);
-        this.filter_view.model.fetch();
     },
     },
 
 
     /**
     /**
@@ -109,7 +103,7 @@ const RosterView = OrderedListView.extend({
         if (!u.isVisible(this.roster_el)) {
         if (!u.isVisible(this.roster_el)) {
             u.showElement(this.roster_el);
             u.showElement(this.roster_el);
         }
         }
-        this.filter_view.render();
+        // this.filter_view.render();
         return this;
         return this;
     },
     },
 
 
@@ -217,7 +211,7 @@ const RosterView = OrderedListView.extend({
         if (contact.get('subscription') === 'both' || contact.get('subscription') === 'to' || this.isSelf(jid)) {
         if (contact.get('subscription') === 'both' || contact.get('subscription') === 'to' || this.isSelf(jid)) {
             this.addExistingContact(contact, options);
             this.addExistingContact(contact, options);
         } else {
         } else {
-            if (!_converse.allow_contact_requests) {
+            if (!api.settings.get('allow_contact_requests')) {
                 log.debug(
                 log.debug(
                     `Not adding requesting or pending contact ${jid} `+
                     `Not adding requesting or pending contact ${jid} `+
                     `because allow_contact_requests is false`
                     `because allow_contact_requests is false`

+ 0 - 14
src/plugins/rosterview/templates/roster.html

@@ -1,14 +0,0 @@
-<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>
-    {[ if (o.allow_contact_requests) { ]}
-        <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>
-
-<form class="roster-filter-form"></form>
-
-<div class="list-container roster-contacts"></div>

+ 16 - 0
src/plugins/rosterview/templates/roster.js

@@ -0,0 +1,16 @@
+import { html } from "lit-html";
+import { api } from "@converse/headless/core";
+
+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>
+`;

+ 46 - 31
src/plugins/rosterview/templates/roster_filter.js

@@ -1,35 +1,50 @@
 import { html } from "lit-html";
 import { html } from "lit-html";
+import { __ } from 'i18n';
 
 
 
 
-export default (o) => html`
-    <form class="controlbox-padded roster-filter-form input-button-group ${ (!o.visible) ? 'hidden' : 'fade-in' }"
-          @submit=${o.submitFilter}>
-        <div class="form-inline flex-nowrap">
-            <div class="filter-by d-flex flex-nowrap">
-                <span @click=${o.changeTypeFilter} class="fa fa-user ${ (o.filter_type === 'contacts') ? 'selected' : '' }" data-type="contacts" title="${o.title_contact_filter}"></span>
-                <span @click=${o.changeTypeFilter} class="fa fa-users ${ (o.filter_type === 'groups') ? 'selected' : '' }" data-type="groups" title="${o.title_group_filter}"></span>
-                <span @click=${o.changeTypeFilter} class="fa fa-circle ${ (o.filter_type === 'state') ? 'selected' : '' }" data-type="state" title="${o.title_status_filter}"></span>
-            </div>
-            <div class="btn-group">
-                <input .value="${o.filter_text || ''}"
-                       @keydown=${o.liveFilter}
-                       class="roster-filter form-control ${ (o.filter_type === 'state') ? 'hidden' : '' }"
-                       placeholder="${o.placeholder}"/>
-                <span class="clear-input fa fa-times ${ (!o.filter_text || o.filter_type === 'state') ? 'hidden' : '' }"
-                      @click=${o.clearFilter}>
-                </span>
+export default (o) => {
+    const i18n_placeholder = __('Filter');
+    const title_contact_filter = __('Filter by contact name');
+    const title_group_filter = __('Filter by group name');
+    const title_status_filter = __('Filter by status');
+    const label_any = __('Any');
+    const label_unread_messages = __('Unread');
+    const label_online = __('Online');
+    const label_chatty = __('Chatty');
+    const label_busy = __('Busy');
+    const label_away = __('Away');
+    const label_xa = __('Extended Away');
+    const label_offline = __('Offline');
+
+    return html`
+        <form class="controlbox-padded roster-filter-form input-button-group ${ (!o.visible) ? 'hidden' : 'fade-in' }"
+            @submit=${o.submitFilter}>
+            <div class="form-inline flex-nowrap">
+                <div class="filter-by d-flex flex-nowrap">
+                    <span @click=${o.changeTypeFilter} class="fa fa-user ${ (o.filter_type === 'contacts') ? 'selected' : '' }" data-type="contacts" title="${title_contact_filter}"></span>
+                    <span @click=${o.changeTypeFilter} class="fa fa-users ${ (o.filter_type === 'groups') ? 'selected' : '' }" data-type="groups" title="${title_group_filter}"></span>
+                    <span @click=${o.changeTypeFilter} class="fa fa-circle ${ (o.filter_type === 'state') ? 'selected' : '' }" data-type="state" title="${title_status_filter}"></span>
+                </div>
+                <div class="btn-group">
+                    <input .value="${o.filter_text || ''}"
+                        @keydown=${o.liveFilter}
+                        class="roster-filter form-control ${ (o.filter_type === 'state') ? 'hidden' : '' }"
+                        placeholder="${i18n_placeholder}"/>
+                    <span class="clear-input fa fa-times ${ (!o.filter_text || o.filter_type === 'state') ? 'hidden' : '' }"
+                        @click=${o.clearFilter}>
+                    </span>
+                </div>
+                <select class="form-control state-type ${ (o.filter_type !== 'state') ? 'hidden' : '' }"
+                        @change=${o.changeChatStateFilter}>
+                    <option value="">${label_any}</option>
+                    <option ?selected=${o.chat_state === 'unread_messages'} value="unread_messages">${label_unread_messages}</option>
+                    <option ?selected=${o.chat_state === 'online'} value="online">${label_online}</option>
+                    <option ?selected=${o.chat_state === 'chat'} value="chat">${label_chatty}</option>
+                    <option ?selected=${o.chat_state === 'dnd'} value="dnd">${label_busy}</option>
+                    <option ?selected=${o.chat_state === 'away'} value="away">${label_away}</option>
+                    <option ?selected=${o.chat_state === 'xa'} value="xa">${label_xa}</option>
+                    <option ?selected=${o.chat_state === 'offline'} value="offline">${label_offline}</option>
+                </select>
             </div>
             </div>
-            <select class="form-control state-type ${ (o.filter_type !== 'state') ? 'hidden' : '' }"
-                    @change=${o.changeChatStateFilter}>
-                <option value="">${o.label_any}</option>
-                <option ?selected=${o.chat_state === 'unread_messages'} value="unread_messages">${o.label_unread_messages}</option>
-                <option ?selected=${o.chat_state === 'online'} value="online">${o.label_online}</option>
-                <option ?selected=${o.chat_state === 'chat'} value="chat">${o.label_chatty}</option>
-                <option ?selected=${o.chat_state === 'dnd'} value="dnd">${o.label_busy}</option>
-                <option ?selected=${o.chat_state === 'away'} value="away">${o.label_away}</option>
-                <option ?selected=${o.chat_state === 'xa'} value="xa">${o.label_xa}</option>
-                <option ?selected=${o.chat_state === 'offline'} value="offline">${o.label_offline}</option>
-            </select>
-        </div>
-    </form>
-`;
+        </form>`
+};

+ 33 - 0
src/plugins/rosterview/utils.js

@@ -0,0 +1,33 @@
+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;
+    }
+    /* 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));
+}

+ 1 - 0
webpack.common.js

@@ -101,6 +101,7 @@ module.exports = {
                         }]
                         }]
                     ],
                     ],
                     plugins: [
                     plugins: [
+                        '@babel/plugin-proposal-class-properties',
                         '@babel/plugin-proposal-nullish-coalescing-operator',
                         '@babel/plugin-proposal-nullish-coalescing-operator',
                         '@babel/plugin-proposal-optional-chaining',
                         '@babel/plugin-proposal-optional-chaining',
                         '@babel/plugin-syntax-dynamic-import'
                         '@babel/plugin-syntax-dynamic-import'