Преглед изворни кода

Fixes #2623: Automatically bookmark MUCs

Merge MUC join and bookmark, leave and unset autojoin
JC Brand пре 7 месеци
родитељ
комит
d464fb9a5a

+ 1 - 0
CHANGES.md

@@ -12,6 +12,7 @@
 - #1195: Add actions to quote and copy messages
 - #1349: XEP-0392 Consistent Color Generation
 - #2586: Add support for XEP-0402 Bookmarks
+- #2623: Merge MUC join and bookmark, leave and unset autojoin 
 - #2716: Fix issue with chat display when opening via URL
 - #2980: Allow setting an avatar for MUCs
 - #3033: Add the `muc_grouped_by_domain` option to display MUCs on the same domain in collapsible groups

+ 27 - 4
src/headless/plugins/bookmarks/collection.js

@@ -13,9 +13,13 @@ import { initStorage } from '../../utils/storage.js';
 import { parseStanzaForBookmarks } from './parsers.js';
 import '../../plugins/muc/index.js';
 
-const { Strophe, sizzle, stx } = converse.env;
+const { Strophe, stx } = converse.env;
 
 class Bookmarks extends Collection {
+    get idAttribute() {
+        return 'jid';
+    }
+
     async initialize() {
         this.on('add', (bm) =>
             this.openBookmarkedRoom(bm)
@@ -90,9 +94,28 @@ class Bookmarks extends Collection {
     /**
      * @param {import('./types').BookmarkAttrs} attrs
      */
-    createBookmark(attrs) {
-        this.create(attrs);
-        this.sendBookmarkStanza().catch((iq) => this.onBookmarkError(iq, attrs));
+    setBookmark(attrs, create=true) {
+        if (!attrs.jid) return log.warn('No JID provided for setBookmark');
+
+        let send_stanza = false;
+
+        const existing = this.get(attrs.jid);
+        if (existing) {
+            // Check if any attrs changed
+            const has_changed = Object.keys(attrs).reduce((result, k) => {
+                return result || (attrs[k] ?? '') !== (existing.attributes[k] ?? '');
+            }, false);
+            if (has_changed) {
+                existing.save(attrs);
+                send_stanza = true;
+            }
+        } else if (create) {
+            this.create(attrs);
+            send_stanza = true;
+        }
+        if (send_stanza) {
+            this.sendBookmarkStanza().catch((iq) => this.onBookmarkError(iq, attrs));
+        }
     }
 
     /**

+ 35 - 0
src/headless/plugins/bookmarks/plugin.js

@@ -61,6 +61,41 @@ converse.plugins.add('converse-bookmarks', {
         Object.assign(_converse, exports); // TODO: DEPRECATED
         Object.assign(_converse.exports, exports);
 
+        api.listen.on(
+            'enteredNewRoom',
+            /** @param {import('../muc/muc').default} muc */
+            ({ attributes }) => {
+                const { bookmarks } = _converse.state;
+                if (!bookmarks) return;
+
+                const { jid, nick, password, name } = /** @type {import("../muc/types").MUCAttributes} */(attributes);
+
+                bookmarks.setBookmark({
+                    jid,
+                    autojoin: true,
+                    nick,
+                    ...(password ? { password } : {}),
+                    ...(name ? { name } : {}),
+                });
+            }
+        );
+
+        api.listen.on(
+            'leaveRoom',
+            /** @param {import('../muc/muc').default} muc */
+            ({ attributes }) => {
+                const { bookmarks } = _converse.state;
+                if (!bookmarks) return;
+
+                const { jid } = /** @type {import("../muc/types").MUCAttributes} */(attributes);
+
+                bookmarks.setBookmark({
+                    jid,
+                    autojoin: false,
+                }, false);
+            }
+        );
+
         api.listen.on('addClientFeatures', () => {
             if (api.settings.get('allow_bookmarks')) {
                 api.disco.own.features.add(Strophe.NS.BOOKMARKS + '+notify')

+ 167 - 15
src/headless/plugins/bookmarks/tests/bookmarks.js

@@ -3,39 +3,191 @@ const { Strophe, sizzle, stx, u } = converse.env;
 
 describe("A chat room", function () {
 
+    beforeEach(() => jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza }));
+
+    it("is automatically bookmarked when opened", mock.initConverse(['chatBoxesFetched'], {}, async (_converse) => {
+        const { bare_jid } = _converse;
+        await mock.waitForRoster(_converse, 'current', 0);
+        await mock.waitUntilDiscoConfirmed(
+            _converse, bare_jid,
+            [{'category': 'pubsub', 'type': 'pep'}],
+            [
+                'http://jabber.org/protocol/pubsub#publish-options',
+                'urn:xmpp:bookmarks:1#compat'
+            ]
+        );
+
+        const nick = 'JC';
+        const muc_jid = 'theplay@conference.shakespeare.lit';
+        const settings = { name: "Play's the thing", password: 'secret' };
+        const muc = await mock.openAndEnterChatRoom(_converse, muc_jid, nick, [], [], true, settings);
+
+        const IQ_stanzas = _converse.api.connection.get().IQ_stanzas;
+        const sent_stanza = await u.waitUntil(
+            () => IQ_stanzas.filter(s => sizzle('iq publish[node="urn:xmpp:bookmarks:1"]', s).length).pop());
+
+        expect(sent_stanza).toEqualStanza(
+            stx`<iq from="${_converse.bare_jid}"
+                    to="${_converse.bare_jid}"
+                    id="${sent_stanza.getAttribute('id')}"
+                    type="set"
+                    xmlns="jabber:client">
+                <pubsub xmlns="http://jabber.org/protocol/pubsub">
+                    <publish node="urn:xmpp:bookmarks:1">
+                        <item id="${muc.get('jid')}">
+                            <conference xmlns="urn:xmpp:bookmarks:1" autojoin="true" name="${settings.name}">
+                                <nick>${nick}</nick>
+                                <password>${settings.password}</password>
+                            </conference>
+                        </item>
+                    </publish>
+                    <publish-options>
+                        <x type="submit" xmlns="jabber:x:data">
+                            <field type="hidden" var="FORM_TYPE">
+                                <value>http://jabber.org/protocol/pubsub#publish-options</value>
+                            </field>
+                            <field var='pubsub#persist_items'>
+                                <value>true</value>
+                            </field>
+                            <field var='pubsub#max_items'>
+                                <value>max</value>
+                            </field>
+                            <field var='pubsub#send_last_published_item'>
+                                <value>never</value>
+                            </field>
+                            <field var='pubsub#access_model'>
+                                <value>whitelist</value>
+                            </field>
+                        </x>
+                    </publish-options>
+                </pubsub>
+            </iq>`
+        );
+
+        /* Server acknowledges successful storage
+         * <iq to='juliet@capulet.lit/balcony' type='result' id='pip1'/>
+         */
+        const stanza = stx`<iq
+            xmlns="jabber:client"
+            to="${_converse.api.connection.get().jid}"
+            type="result"
+            id="${sent_stanza.getAttribute('id')}"/>`;
+        _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
+
+        expect(muc.get('bookmarked')).toBeTruthy();
+    }));
+});
+
+
+describe("A bookmark", function () {
+
+    beforeEach(() => jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza }));
+
     describe("when autojoin is set", function () {
 
-        it("will be be opened and joined automatically upon login", mock.initConverse(
+        it("will cause a MUC to be opened and joined automatically upon login", mock.initConverse(
                 [], {}, async function (_converse) {
 
+            const { api } = _converse;
             await mock.waitForRoster(_converse, 'current', 0);
             await mock.waitUntilBookmarksReturned(_converse);
             spyOn(_converse.api.rooms, 'create').and.callThrough();
-            const jid = 'theplay@conference.shakespeare.lit';
             const { bookmarks } = _converse.state;
+
+            let jid = 'theplay@conference.shakespeare.lit';
             const model = bookmarks.create({
                 jid,
-                'autojoin': false,
-                'name':  'The Play',
-                'nick': ''
+                autojoin: false,
+                name:  'The Play',
+                nick: ''
             });
             expect(_converse.api.rooms.create).not.toHaveBeenCalled();
+
+            // Check that we don't auto-join if muc_respect_autojoin is false
+            api.settings.set('muc_respect_autojoin', false);
+            bookmarks.create({
+                jid,
+                autojoin: true,
+                name:  'The Play',
+                nick: ''
+            });
+            expect(_converse.api.rooms.create).not.toHaveBeenCalled();
+
+            api.settings.set('muc_respect_autojoin', true);
             bookmarks.remove(model);
             bookmarks.create({
                 jid,
-                'autojoin': true,
-                'name':  'Hamlet',
-                'nick': ''
+                autojoin: true,
+                name:  'Hamlet',
+                nick: ''
             });
             expect(_converse.api.rooms.create).toHaveBeenCalled();
+
         }));
-    });
-});
 
+        it("has autojoin set to false upon leaving", mock.initConverse([], {}, async function (_converse) {
+            const { u } = converse.env;
+            await mock.waitForRoster(_converse, 'current', 0);
+            await mock.waitUntilBookmarksReturned(_converse);
 
-describe("A bookmark", function () {
+            const nick = 'romeo';
+            const muc_jid = 'theplay@conference.shakespeare.lit';
+            const settings = { name:  'The Play' };
+            const muc = await mock.openAndEnterChatRoom(_converse, muc_jid, nick, [], [], true, settings);
 
-    beforeEach(() => jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza }));
+            const { bookmarks } = _converse.state;
+            await u.waitUntil(() => bookmarks.length);
+            await u.waitUntil(() => muc.get('bookmarked'));
+            spyOn(bookmarks, 'sendBookmarkStanza').and.callThrough();
+
+            const sent_IQs = _converse.api.connection.get().IQ_stanzas;
+            while (sent_IQs.length) { sent_IQs.pop(); }
+
+            await muc.close();
+            await u.waitUntil(() => sent_IQs.length);
+
+            // Check that an IQ stanza is sent out, containing no
+            // conferences to bookmark (since we removed the one and
+            // only bookmark).
+            const sent_stanza = sent_IQs.pop();
+            expect(sent_stanza).toEqualStanza(
+                stx`<iq from="${_converse.bare_jid}"
+                        to="${_converse.bare_jid}"
+                        id="${sent_stanza.getAttribute('id')}"
+                        type="set"
+                        xmlns="jabber:client">
+                    <pubsub xmlns="http://jabber.org/protocol/pubsub">
+                        <publish node="urn:xmpp:bookmarks:1">
+                            <item id="${muc_jid}">
+                                <conference xmlns="urn:xmpp:bookmarks:1" name="${settings.name}" autojoin="false">
+                                    <nick>${nick}</nick>
+                                </conference>
+                            </item>
+                        </publish>
+                        <publish-options>
+                            <x type="submit" xmlns="jabber:x:data">
+                                <field type="hidden" var="FORM_TYPE">
+                                    <value>http://jabber.org/protocol/pubsub#publish-options</value>
+                                </field>
+                                <field var='pubsub#persist_items'>
+                                    <value>true</value>
+                                </field>
+                                <field var='pubsub#max_items'>
+                                    <value>max</value>
+                                </field>
+                                <field var='pubsub#send_last_published_item'>
+                                    <value>never</value>
+                                </field>
+                                <field var='pubsub#access_model'>
+                                    <value>whitelist</value>
+                                </field>
+                            </x>
+                        </publish-options>
+                    </pubsub>
+                </iq>`
+            );
+        }));
+    });
 
     it("can be created and sends out a stanza", mock.initConverse(
             ['connected', 'chatBoxesFetched'], {}, async function (_converse) {
@@ -47,7 +199,7 @@ describe("A bookmark", function () {
         const muc1_jid = 'theplay@conference.shakespeare.lit';
         const { bookmarks } = _converse.state;
 
-        bookmarks.createBookmark({
+        bookmarks.setBookmark({
             jid: muc1_jid,
             autojoin: true,
             name:  'Hamlet',
@@ -90,7 +242,7 @@ describe("A bookmark", function () {
 
 
         const muc2_jid = 'balcony@conference.shakespeare.lit';
-        bookmarks.createBookmark({
+        bookmarks.setBookmark({
             jid: muc2_jid,
             autojoin: true,
             name:  'Balcony',
@@ -136,7 +288,7 @@ describe("A bookmark", function () {
             </iq>`);
 
         const muc3_jid = 'garden@conference.shakespeare.lit';
-        bookmarks.createBookmark({
+        bookmarks.setBookmark({
             jid: muc3_jid,
             autojoin: false,
             name:  'Garden',

+ 148 - 2
src/headless/plugins/bookmarks/tests/deprecated.js

@@ -1,9 +1,155 @@
 const { sizzle, stx, u } = converse.env;
 
+describe("A chat room", function () {
+
+    beforeEach(() => jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza }));
+
+    it("is automatically bookmarked when opened", mock.initConverse(['chatBoxesFetched'], {}, async (_converse) => {
+        const { bare_jid } = _converse;
+        await mock.waitForRoster(_converse, 'current', 0);
+        await mock.waitUntilDiscoConfirmed(
+            _converse, bare_jid,
+            [{'category': 'pubsub', 'type': 'pep'}],
+            [ 'http://jabber.org/protocol/pubsub#publish-options' ]
+        );
+
+        const nick = 'JC';
+        const muc_jid = 'theplay@conference.shakespeare.lit';
+        const settings = { name: "Play's the thing", password: 'secret' };
+        const muc = await mock.openAndEnterChatRoom(_converse, muc_jid, nick, [], [], true, settings);
+
+        const IQ_stanzas = _converse.api.connection.get().IQ_stanzas;
+        const sent_stanza = await u.waitUntil(
+            () => IQ_stanzas.filter(s => sizzle('iq publish[node="storage:bookmarks"]', s).length).pop());
+
+        expect(sent_stanza).toEqualStanza(
+            stx`<iq from="${_converse.bare_jid}"
+                    to="${_converse.bare_jid}"
+                    id="${sent_stanza.getAttribute('id')}"
+                    type="set"
+                    xmlns="jabber:client">
+                <pubsub xmlns="http://jabber.org/protocol/pubsub">
+                    <publish node="storage:bookmarks">
+                        <item id="current">
+                            <storage xmlns="storage:bookmarks">
+                                <conference autojoin="true" jid="${muc_jid}" name="${settings.name}">
+                                    <nick>${nick}</nick>
+                                    <password>${settings.password}</password>
+                                </conference>
+                            </storage>
+                        </item>
+                    </publish>
+                    <publish-options>
+                        <x type="submit" xmlns="jabber:x:data">
+                            <field type="hidden" var="FORM_TYPE">
+                                <value>http://jabber.org/protocol/pubsub#publish-options</value>
+                            </field>
+                            <field var='pubsub#persist_items'>
+                                <value>true</value>
+                            </field>
+                            <field var='pubsub#max_items'>
+                                <value>max</value>
+                            </field>
+                            <field var='pubsub#send_last_published_item'>
+                                <value>never</value>
+                            </field>
+                            <field var='pubsub#access_model'>
+                                <value>whitelist</value>
+                            </field>
+                        </x>
+                    </publish-options>
+                </pubsub>
+            </iq>`
+        );
+
+        /* Server acknowledges successful storage
+         * <iq to='juliet@capulet.lit/balcony' type='result' id='pip1'/>
+         */
+        const stanza = stx`<iq
+            xmlns="jabber:client"
+            to="${_converse.api.connection.get().jid}"
+            type="result"
+            id="${sent_stanza.getAttribute('id')}"/>`;
+        _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
+
+        expect(muc.get('bookmarked')).toBeTruthy();
+    }));
+});
+
 describe("A bookmark", function () {
 
     beforeEach(() => jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza }));
 
+    it("has autojoin set to false upon leaving", mock.initConverse([], {}, async function (_converse) {
+        const { u } = converse.env;
+        await mock.waitForRoster(_converse, 'current', 0);
+        await mock.waitUntilBookmarksReturned(
+            _converse,
+            [],
+            ['http://jabber.org/protocol/pubsub#publish-options'],
+            'storage:bookmarks'
+        );
+
+        const nick = 'romeo';
+        const muc_jid = 'theplay@conference.shakespeare.lit';
+        const settings = { name:  'The Play' };
+        const muc = await mock.openAndEnterChatRoom(_converse, muc_jid, nick, [], [], true, settings);
+
+        const { bookmarks } = _converse.state;
+        await u.waitUntil(() => bookmarks.length);
+        await u.waitUntil(() => muc.get('bookmarked'));
+        spyOn(bookmarks, 'sendBookmarkStanza').and.callThrough();
+
+        const sent_IQs = _converse.api.connection.get().IQ_stanzas;
+        while (sent_IQs.length) { sent_IQs.pop(); }
+
+        await muc.close();
+        await u.waitUntil(() => sent_IQs.length);
+
+        // Check that an IQ stanza is sent out, containing no
+        // conferences to bookmark (since we removed the one and
+        // only bookmark).
+        const sent_stanza = sent_IQs.pop();
+        expect(sent_stanza).toEqualStanza(
+            stx`<iq from="${_converse.bare_jid}"
+                    to="${_converse.bare_jid}"
+                    id="${sent_stanza.getAttribute('id')}"
+                    type="set"
+                    xmlns="jabber:client">
+                <pubsub xmlns="http://jabber.org/protocol/pubsub">
+                    <publish node="storage:bookmarks">
+                    <item id="current">
+                        <storage xmlns="storage:bookmarks">
+                            <conference jid="${muc_jid}" name="${settings.name}" autojoin="false">
+                                <nick>${nick}</nick>
+                            </conference>
+                        </storage>
+                    </item>
+                    </publish>
+                    <publish-options>
+                        <x type="submit" xmlns="jabber:x:data">
+                            <field type="hidden" var="FORM_TYPE">
+                                <value>http://jabber.org/protocol/pubsub#publish-options</value>
+                            </field>
+                            <field var='pubsub#persist_items'>
+                                <value>true</value>
+                            </field>
+                            <field var='pubsub#max_items'>
+                                <value>max</value>
+                            </field>
+                            <field var='pubsub#send_last_published_item'>
+                                <value>never</value>
+                            </field>
+                            <field var='pubsub#access_model'>
+                                <value>whitelist</value>
+                            </field>
+                        </x>
+                    </publish-options>
+                </pubsub>
+            </iq>`
+        );
+    }));
+
     it("can be created and sends out a stanza", mock.initConverse(
             ['connected', 'chatBoxesFetched'], {}, async function (_converse) {
 
@@ -19,7 +165,7 @@ describe("A bookmark", function () {
         const muc1_jid = 'theplay@conference.shakespeare.lit';
         const { bookmarks } = _converse.state;
 
-        bookmarks.createBookmark({
+        bookmarks.setBookmark({
             jid: muc1_jid,
             autojoin: true,
             name:  'Hamlet',
@@ -64,7 +210,7 @@ describe("A bookmark", function () {
 
 
         const muc2_jid = 'balcony@conference.shakespeare.lit';
-        bookmarks.createBookmark({
+        bookmarks.setBookmark({
             jid: muc2_jid,
             autojoin: true,
             name:  'Balcony',

+ 1 - 1
src/headless/plugins/disco/entity.js

@@ -19,7 +19,7 @@ const { Strophe } = converse.env;
  * See XEP-0030: https://xmpp.org/extensions/xep-0030.html
  */
 class DiscoEntity extends Model {
-    get idAttribute () { // eslint-disable-line class-methods-use-this
+    get idAttribute () {
         return 'jid';
     }
 

+ 16 - 9
src/headless/plugins/muc/muc.js

@@ -56,6 +56,7 @@ class MUC extends ModelWithMessages(ColorAwareModel(ChatBoxBase)) {
      */
 
     defaults () {
+        /** @type {import('./types').DefaultMUCAttributes} */
         return {
             'bookmarked': false,
             'chat_state': undefined,
@@ -922,17 +923,25 @@ class MUC extends ModelWithMessages(ColorAwareModel(ChatBoxBase)) {
 
     /**
      * Leave the groupchat.
-     * @param { string } [exit_msg] - Message to indicate your reason for leaving
+     * @param {string} [exit_msg] - Message to indicate your reason for leaving
      */
     async leave (exit_msg) {
+        /**
+         * Triggered when the user leaves a MUC
+         * @event _converse#leaveRoom
+         * @type {MUC}
+         * @example _converse.api.listen.on('leaveRoom', model => { ... });
+         */
+        api.trigger('leaveRoom', this);
+
         api.connection.connected() && api.user.presence.send('unavailable', this.getRoomJIDAndNick(), exit_msg);
 
         // Delete the features model
         if (this.features) {
             await new Promise(resolve =>
                 this.features.destroy({
-                    'success': resolve,
-                    'error': (_, e) => { log.error(e); resolve(); }
+                    success: resolve,
+                    error: (_, e) => { log.error(e); resolve(); }
                 })
             );
         }
@@ -940,8 +949,8 @@ class MUC extends ModelWithMessages(ColorAwareModel(ChatBoxBase)) {
         const disco_entity = _converse.state.disco_entities?.get(this.get('jid'));
         if (disco_entity) {
             await new Promise(resolve => disco_entity.destroy({
-                'success': resolve,
-                'error': (_, e) => { log.error(e); resolve(); }
+                success: resolve,
+                error: (_, e) => { log.error(e); resolve(); }
             }));
         }
         safeSave(this.session, { 'connection_status': ROOMSTATUS.DISCONNECTED });
@@ -1132,10 +1141,8 @@ class MUC extends ModelWithMessages(ColorAwareModel(ChatBoxBase)) {
         }
         api.send(
             $msg({ 'to': this.get('jid'), 'type': 'groupchat' })
-                .c(chat_state, { 'xmlns': Strophe.NS.CHATSTATES })
-                .up()
-                .c('no-store', { 'xmlns': Strophe.NS.HINTS })
-                .up()
+                .c(chat_state, { 'xmlns': Strophe.NS.CHATSTATES }).up()
+                .c('no-store', { 'xmlns': Strophe.NS.HINTS }).up()
                 .c('no-permanent-store', { 'xmlns': Strophe.NS.HINTS })
         );
     }

+ 18 - 19
src/headless/plugins/muc/plugin.js

@@ -83,22 +83,22 @@ converse.plugins.add('converse-muc', {
         // Refer to docs/source/configuration.rst for explanations of these
         // configuration settings.
         api.settings.extend({
-            'allow_muc_invitations': true,
-            'auto_join_on_invite': false,
-            'auto_join_rooms': [],
-            'auto_register_muc_nickname': false,
-            'colorize_username': false,
-            'hide_muc_participants': false,
-            'locked_muc_domain': false,
-            'modtools_disable_assign': false,
-            'muc_clear_messages_on_leave': true,
-            'muc_domain': undefined,
-            'muc_fetch_members': true,
-            'muc_history_max_stanzas': undefined,
-            'muc_instant_rooms': true,
-            'muc_nickname_from_jid': false,
-            'muc_send_probes': false,
-            'muc_show_info_messages': [
+            allow_muc_invitations: true,
+            auto_join_on_invite: false,
+            auto_join_rooms: [],
+            auto_register_muc_nickname: false,
+            colorize_username: false,
+            hide_muc_participants: false,
+            locked_muc_domain: false,
+            modtools_disable_assign: false,
+            muc_clear_messages_on_leave: true,
+            muc_domain: undefined,
+            muc_fetch_members: true,
+            muc_history_max_stanzas: undefined,
+            muc_instant_rooms: true,
+            muc_nickname_from_jid: false,
+            muc_send_probes: false,
+            muc_show_info_messages: [
                 ...converse.MUC.INFO_CODES.visibility_changes,
                 ...converse.MUC.INFO_CODES.self,
                 ...converse.MUC.INFO_CODES.non_privacy_changes,
@@ -109,8 +109,8 @@ converse.plugins.add('converse-muc', {
                 ...converse.MUC.INFO_CODES.join_leave_events,
                 ...converse.MUC.INFO_CODES.role_changes,
             ],
-            'muc_show_logs_before_join': false,
-            'muc_subscribe_to_rai': false,
+            muc_show_logs_before_join: false,
+            muc_subscribe_to_rai: false,
         });
         api.promises.add(['roomsAutoJoined']);
 
@@ -211,7 +211,6 @@ converse.plugins.add('converse-muc', {
 
         /** @type {module:shared-api.APIEndpoint} */(api.chatboxes.registry).add(CHATROOMS_TYPE, MUC);
 
-
         if (api.settings.get('allow_muc_invitations')) {
             api.listen.on('connected', registerDirectInvitationHandler);
             api.listen.on('reconnected', registerDirectInvitationHandler);

+ 23 - 0
src/headless/plugins/muc/types.ts

@@ -1,5 +1,28 @@
+import { CHAT_STATES } from '../../shared/constants';
 import { MessageAttributes } from '../chat/types';
 
+export type DefaultMUCAttributes = {
+    bookmarked: boolean;
+    chat_state: typeof CHAT_STATES;
+    has_activity: boolean; // XEP-437
+    hidden: boolean;
+    hidden_occupants: boolean;
+    message_type: 'groupchat';
+    name: string;
+    num_unread: number;
+    num_unread_general: number;
+    roomconfig: Object;
+    time_opened: number;
+    time_sent: string;
+    type: 'chatroom';
+};
+
+export type MUCAttributes = DefaultMUCAttributes & {
+    jid: string;
+    nick: string;
+    password: string;
+};
+
 type ExtraMUCAttributes = {
     activities: Array<Object>; // A list of objects representing XEP-0316 MEP notification data
     from_muc: string; // The JID of the MUC from which this message was sent

+ 2 - 1
src/headless/types/plugins/bookmarks/collection.d.ts

@@ -3,6 +3,7 @@ export type MUC = import("../muc/muc.js").default;
 declare class Bookmarks extends Collection {
     static checkBookmarksSupport(): Promise<any>;
     constructor();
+    get idAttribute(): string;
     initialize(): Promise<void>;
     fetched_flag: string;
     model: typeof Bookmark;
@@ -14,7 +15,7 @@ declare class Bookmarks extends Collection {
     /**
      * @param {import('./types').BookmarkAttrs} attrs
      */
-    createBookmark(attrs: import("./types").BookmarkAttrs): void;
+    setBookmark(attrs: import("./types").BookmarkAttrs, create?: boolean): void;
     /**
      * @param {'urn:xmpp:bookmarks:1'|'storage:bookmarks'} node
      * @returns {Stanza|Stanza[]}

+ 1 - 1
src/headless/types/plugins/muc/muc.d.ts

@@ -390,7 +390,7 @@ declare class MUC extends MUC_base {
     sendDestroyIQ(reason?: string, new_jid?: string): any;
     /**
      * Leave the groupchat.
-     * @param { string } [exit_msg] - Message to indicate your reason for leaving
+     * @param {string} [exit_msg] - Message to indicate your reason for leaving
      */
     leave(exit_msg?: string): Promise<void>;
     /**

+ 21 - 0
src/headless/types/plugins/muc/types.d.ts

@@ -1,4 +1,25 @@
+import { CHAT_STATES } from '../../shared/constants';
 import { MessageAttributes } from '../chat/types';
+export type DefaultMUCAttributes = {
+    bookmarked: boolean;
+    chat_state: typeof CHAT_STATES;
+    has_activity: boolean;
+    hidden: boolean;
+    hidden_occupants: boolean;
+    message_type: 'groupchat';
+    name: string;
+    num_unread: number;
+    num_unread_general: number;
+    roomconfig: Object;
+    time_opened: number;
+    time_sent: string;
+    type: 'chatroom';
+};
+export type MUCAttributes = DefaultMUCAttributes & {
+    jid: string;
+    nick: string;
+    password: string;
+};
 type ExtraMUCAttributes = {
     activities: Array<Object>;
     from_muc: string;

+ 1 - 1
src/plugins/bookmark-views/components/bookmark-form.js

@@ -38,7 +38,7 @@ class MUCBookmarkForm extends CustomElement {
         ev.preventDefault();
         const { bookmarks } = _converse.state;
         const form = /** @type {HTMLFormElement} */ (ev.target);
-        bookmarks.createBookmark({
+        bookmarks.setBookmark({
             jid: this.jid,
             autojoin: /** @type {HTMLInputElement} */ (form.querySelector('input[name="autojoin"]'))?.checked || false,
             name: /** @type {HTMLInputElement} */ (form.querySelector('input[name=name]'))?.value,

+ 13 - 13
src/plugins/bookmark-views/index.js

@@ -1,6 +1,5 @@
 /**
- * @description Converse.js plugin which adds views for XEP-0048 bookmarks
- * @copyright 2022, the Converse.js contributors
+ * @copyright 2025, the Converse.js contributors
  * @license Mozilla Public License (MPLv2)
  */
 import { _converse, api, converse } from '@converse/headless';
@@ -8,12 +7,11 @@ import './modals/bookmark-list.js';
 import './modals/bookmark-form.js';
 import BookmarkForm from './components/bookmark-form.js';
 import BookmarksView from './components/bookmarks-list.js';
-import { bookmarkableChatRoomView } from './mixins.js';
-import { getHeadingButtons, removeBookmarkViaEvent, addBookmarkViaEvent } from './utils.js';
+import { BookmarkableChatRoomView } from './mixins.js';
+import { removeBookmarkViaEvent, addBookmarkViaEvent } from './utils.js';
 
 import './styles/bookmarks.scss';
 
-
 converse.plugins.add('converse-bookmark-views', {
     /* Plugin dependencies are other plugins which might be
      * overridden or relied upon, and therefore need to be loaded before
@@ -25,27 +23,29 @@ converse.plugins.add('converse-bookmark-views', {
      */
     dependencies: ['converse-chatboxes', 'converse-muc', 'converse-muc-views'],
 
-    initialize () {
+    initialize() {
         // Configuration values for this plugin
         // ====================================
         // Refer to docs/source/configuration.rst for explanations of these
         // configuration settings.
         api.settings.extend({
-            hide_open_bookmarks: true
+            hide_open_bookmarks: true,
         });
 
-        const exports =  {
+        const exports = {
             removeBookmarkViaEvent,
             addBookmarkViaEvent,
             MUCBookmarkForm: BookmarkForm,
             BookmarksView,
-        }
+        };
 
         Object.assign(_converse, exports); // DEPRECATED
         Object.assign(_converse.exports, exports);
-        Object.assign(_converse.exports.ChatRoomView.prototype, bookmarkableChatRoomView);
+        Object.assign(_converse.exports.ChatRoomView.prototype, BookmarkableChatRoomView);
 
-        api.listen.on('getHeadingButtons', getHeadingButtons);
-        api.listen.on('chatRoomViewInitialized', view => view.setBookmarkState());
-    }
+        api.listen.on(
+            'chatRoomViewInitialized',
+            /** @param {BookmarkableChatRoomView} view */ (view) => view.setBookmarkState()
+        );
+    },
 });

+ 1 - 1
src/plugins/bookmark-views/mixins.js

@@ -2,7 +2,7 @@ import { _converse, api, converse } from '@converse/headless';
 
 const { u } = converse.env;
 
-export const bookmarkableChatRoomView = {
+export const BookmarkableChatRoomView = {
     /**
      * Set whether the groupchat is bookmarked or not.
      * @private

+ 0 - 227
src/plugins/bookmark-views/tests/bookmarks.js

@@ -1,152 +1,10 @@
 /* global mock, converse */
 const { Strophe, sizzle, stx, u } = converse.env;
 
-
 describe("A chat room", function () {
 
     beforeEach(() => jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza }));
 
-    it("can be bookmarked", mock.initConverse(['chatBoxesFetched'], {}, async (_converse) => {
-        await mock.waitForRoster(_converse, 'current', 0);
-        await mock.waitUntilDiscoConfirmed(
-            _converse, _converse.bare_jid,
-            [{'category': 'pubsub', 'type': 'pep'}],
-            [
-                'http://jabber.org/protocol/pubsub#publish-options',
-                'urn:xmpp:bookmarks:1#compat'
-            ]
-        );
-
-        const nick = 'JC';
-        const muc_jid = 'theplay@conference.shakespeare.lit';
-        await mock.openChatRoom(_converse, 'theplay', 'conference.shakespeare.lit', 'JC');
-        await mock.getRoomFeatures(_converse, muc_jid, []);
-        await mock.waitForReservedNick(_converse, muc_jid, nick);
-        await mock.receiveOwnMUCPresence(_converse, muc_jid, nick);
-        const view = _converse.chatboxviews.get(muc_jid);
-        await u.waitUntil(() => (view.model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED));
-        await mock.returnMemberLists(_converse, muc_jid, [], ['member', 'admin', 'owner']);
-
-        await u.waitUntil(() => view.querySelector('.toggle-bookmark') !== null);
-
-        const toggle = view.querySelector('.toggle-bookmark');
-        expect(toggle.title).toBe('Bookmark this groupchat');
-        toggle.click();
-
-        const modal = _converse.api.modal.get('converse-bookmark-form-modal');
-        await u.waitUntil(() => u.isVisible(modal), 1000);
-
-        expect(view.model.get('bookmarked')).toBeFalsy();
-        const form = await u.waitUntil(() => modal.querySelector('.chatroom-form'));
-        form.querySelector('input[name="name"]').value = "Play's the Thing";
-        form.querySelector('input[name="autojoin"]').checked = 'checked';
-        form.querySelector('input[name="nick"]').value = 'JC';
-        form.querySelector('input[name="password"]').value = 'secret';
-
-        const IQ_stanzas = _converse.api.connection.get().IQ_stanzas;
-        modal.querySelector('converse-muc-bookmark-form .btn-primary').click();
-
-        const sent_stanza = await u.waitUntil(
-            () => IQ_stanzas.filter(s => sizzle('iq publish[node="urn:xmpp:bookmarks:1"]', s).length).pop());
-        expect(sent_stanza).toEqualStanza(
-        stx`<iq from="${_converse.bare_jid}"
-                    to="${_converse.bare_jid}"
-                    id="${sent_stanza.getAttribute('id')}"
-                    type="set"
-                    xmlns="jabber:client">
-                <pubsub xmlns="http://jabber.org/protocol/pubsub">
-                    <publish node="urn:xmpp:bookmarks:1">
-                        <item id="${view.model.get('jid')}">
-                            <conference xmlns="urn:xmpp:bookmarks:1" autojoin="true" name="Play's the Thing">
-                                <nick>JC</nick>
-                                <password>secret</password>
-                            </conference>
-                        </item>
-                    </publish>
-                    <publish-options>
-                        <x type="submit" xmlns="jabber:x:data">
-                            <field type="hidden" var="FORM_TYPE">
-                                <value>http://jabber.org/protocol/pubsub#publish-options</value>
-                            </field>
-                            <field var='pubsub#persist_items'>
-                                <value>true</value>
-                            </field>
-                            <field var='pubsub#max_items'>
-                                <value>max</value>
-                            </field>
-                            <field var='pubsub#send_last_published_item'>
-                                <value>never</value>
-                            </field>
-                            <field var='pubsub#access_model'>
-                                <value>whitelist</value>
-                            </field>
-                        </x>
-                    </publish-options>
-                </pubsub>
-            </iq>`
-        );
-        /* Server acknowledges successful storage
-         * <iq to='juliet@capulet.lit/balcony' type='result' id='pip1'/>
-         */
-        const stanza = stx`<iq
-            xmlns="jabber:client"
-            to="${_converse.api.connection.get().jid}"
-            type="result"
-            id="${sent_stanza.getAttribute('id')}"/>`;
-        _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
-        await u.waitUntil(() => view.model.get('bookmarked'));
-        expect(view.model.get('bookmarked')).toBeTruthy();
-        expect(u.hasClass('on-button', view.querySelector('.toggle-bookmark')), true);
-        // We ignore this IQ stanza... (unless it's an error stanza), so
-        // nothing to test for here.
-    }));
-
-
-    it("will be automatically opened if 'autojoin' is set on the bookmark", mock.initConverse(
-            ['chatBoxesFetched'], {}, async function (_converse) {
-
-        const { u } = converse.env;
-        const { api } = _converse;
-        await mock.waitForRoster(_converse, 'current', 0);
-        await mock.waitUntilDiscoConfirmed(
-            _converse, _converse.bare_jid,
-            [{'category': 'pubsub', 'type': 'pep'}],
-            ['http://jabber.org/protocol/pubsub#publish-options']
-        );
-        await u.waitUntil(() => _converse.state.bookmarks);
-        const { bookmarks } = _converse.state;
-        let jid = 'lounge@montague.lit';
-        bookmarks.create({
-            'jid': jid,
-            'autojoin': false,
-            'name':  'The Lounge',
-            'nick': ' Othello'
-        });
-        expect(_converse.chatboxviews.get(jid) === undefined).toBeTruthy();
-
-        jid = 'theplay@conference.shakespeare.lit';
-        bookmarks.create({
-            'jid': jid,
-            'autojoin': true,
-            'name':  'The Play',
-            'nick': ' Othello'
-        });
-        await new Promise(resolve => _converse.api.listen.once('chatRoomViewInitialized', resolve));
-        expect(!!_converse.chatboxviews.get(jid)).toBe(true);
-
-        // Check that we don't auto-join if muc_respect_autojoin is false
-        api.settings.set('muc_respect_autojoin', false);
-        jid = 'balcony@conference.shakespeare.lit';
-        bookmarks.create({
-            'jid': jid,
-            'autojoin': true,
-            'name':  'Balcony',
-            'nick': ' Othello'
-        });
-        expect(_converse.chatboxviews.get(jid) === undefined).toBe(true);
-    }));
-
-
     describe("when bookmarked", function () {
 
         it("will use the nickname from the bookmark", mock.initConverse([], {}, async function (_converse) {
@@ -204,91 +62,6 @@ describe("A chat room", function () {
             view.model.set('bookmarked', false);
             await u.waitUntil(() => view.querySelector('.chatbox-title__text .fa-bookmark') === null);
         }));
-
-        it("can be unbookmarked", mock.initConverse([], {}, async function (_converse) {
-            const { u } = converse.env;
-            await mock.waitForRoster(_converse, 'current', 0);
-            await mock.waitUntilBookmarksReturned(_converse);
-
-            const nick = 'romeo';
-            const muc_jid = 'theplay@conference.shakespeare.lit';
-            await _converse.api.rooms.open(muc_jid);
-            await mock.getRoomFeatures(_converse, muc_jid);
-            await mock.waitForReservedNick(_converse, muc_jid, nick);
-
-            const view = _converse.chatboxviews.get(muc_jid);
-            await u.waitUntil(() => view.querySelector('.toggle-bookmark'));
-
-            const { bookmarks } = _converse.state;
-
-            spyOn(view, 'showBookmarkModal').and.callThrough();
-            spyOn(bookmarks, 'sendBookmarkStanza').and.callThrough();
-
-            bookmarks.create({
-                'jid': view.model.get('jid'),
-                'autojoin': false,
-                'name':  'The Play',
-                'nick': 'Othello'
-            });
-
-            expect(bookmarks.length).toBe(1);
-            await u.waitUntil(() => _converse.chatboxes.length >= 1);
-            expect(view.model.get('bookmarked')).toBeTruthy();
-            await u.waitUntil(() => view.querySelector('.chatbox-title__text .fa-bookmark') !== null);
-            spyOn(_converse.api.connection.get(), 'getUniqueId').and.callThrough();
-            const bookmark_icon = view.querySelector('.toggle-bookmark');
-            bookmark_icon.click();
-            expect(view.showBookmarkModal).toHaveBeenCalled();
-
-            const modal = _converse.api.modal.get('converse-bookmark-form-modal');
-            await u.waitUntil(() => u.isVisible(modal), 1000);
-            const form = await u.waitUntil(() => modal.querySelector('.chatroom-form'));
-
-            expect(form.querySelector('input[name="name"]').value).toBe('The Play');
-            expect(form.querySelector('input[name="autojoin"]').checked).toBeFalsy();
-            expect(form.querySelector('input[name="nick"]').value).toBe('Othello');
-
-            // Remove the bookmark
-            modal.querySelector('.button-remove').click();
-
-            await u.waitUntil(() => view.querySelector('.chatbox-title__text .fa-bookmark') === null);
-            expect(bookmarks.length).toBe(0);
-
-            // Check that an IQ stanza is sent out, containing no
-            // conferences to bookmark (since we removed the one and
-            // only bookmark).
-            const sent_stanza = _converse.api.connection.get().IQ_stanzas.pop();
-            expect(sent_stanza).toEqualStanza(
-                stx`<iq from="${_converse.bare_jid}"
-                        to="${_converse.bare_jid}"
-                        id="${sent_stanza.getAttribute('id')}"
-                        type="set"
-                        xmlns="jabber:client">
-                    <pubsub xmlns="http://jabber.org/protocol/pubsub">
-                        <publish node="urn:xmpp:bookmarks:1"/>
-                        <publish-options>
-                            <x type="submit" xmlns="jabber:x:data">
-                                <field type="hidden" var="FORM_TYPE">
-                                    <value>http://jabber.org/protocol/pubsub#publish-options</value>
-                                </field>
-                                <field var='pubsub#persist_items'>
-                                    <value>true</value>
-                                </field>
-                                <field var='pubsub#max_items'>
-                                    <value>max</value>
-                                </field>
-                                <field var='pubsub#send_last_published_item'>
-                                    <value>never</value>
-                                </field>
-                                <field var='pubsub#access_model'>
-                                    <value>whitelist</value>
-                                </field>
-                            </x>
-                        </publish-options>
-                    </pubsub>
-                </iq>`
-            );
-        }));
     });
 });
 

+ 0 - 191
src/plugins/bookmark-views/tests/deprecated.js

@@ -1,197 +1,6 @@
 /* global mock, converse */
 const { Strophe, sizzle, stx, u } = converse.env;
 
-describe("A chat room", function () {
-
-    beforeEach(() => jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza }));
-
-    it("can be bookmarked", mock.initConverse(['chatBoxesFetched'], {}, async (_converse) => {
-
-        await mock.waitForRoster(_converse, 'current', 0);
-        await mock.waitUntilDiscoConfirmed(
-            _converse, _converse.bare_jid,
-            [{'category': 'pubsub', 'type': 'pep'}],
-            ['http://jabber.org/protocol/pubsub#publish-options'],
-        );
-
-        const nick = 'JC';
-        const muc_jid = 'theplay@conference.shakespeare.lit';
-        await mock.openChatRoom(_converse, 'theplay', 'conference.shakespeare.lit', 'JC');
-        await mock.getRoomFeatures(_converse, muc_jid, []);
-        await mock.waitForReservedNick(_converse, muc_jid, nick);
-        await mock.receiveOwnMUCPresence(_converse, muc_jid, nick);
-        const view = _converse.chatboxviews.get(muc_jid);
-        await u.waitUntil(() => (view.model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED));
-        await mock.returnMemberLists(_converse, muc_jid, [], ['member', 'admin', 'owner']);
-
-        await u.waitUntil(() => view.querySelector('.toggle-bookmark') !== null);
-
-        const toggle = view.querySelector('.toggle-bookmark');
-        expect(toggle.title).toBe('Bookmark this groupchat');
-        toggle.click();
-
-        const modal = _converse.api.modal.get('converse-bookmark-form-modal');
-        await u.waitUntil(() => u.isVisible(modal), 1000);
-
-        expect(view.model.get('bookmarked')).toBeFalsy();
-        const form = await u.waitUntil(() => modal.querySelector('.chatroom-form'));
-        form.querySelector('input[name="name"]').value = "Play's the Thing";
-        form.querySelector('input[name="autojoin"]').checked = 'checked';
-        form.querySelector('input[name="nick"]').value = 'JC';
-
-        const IQ_stanzas = _converse.api.connection.get().IQ_stanzas;
-        modal.querySelector('converse-muc-bookmark-form .btn-primary').click();
-
-        const sent_stanza = await u.waitUntil(
-            () => IQ_stanzas.filter(s => sizzle('iq publish[node="storage:bookmarks"]', s).length).pop());
-        expect(sent_stanza).toEqualStanza(
-            stx`<iq to="romeo@montague.lit"
-                    from="romeo@montague.lit"
-                    id="${sent_stanza.getAttribute('id')}"
-                    type="set"
-                    xmlns="jabber:client">
-                <pubsub xmlns="http://jabber.org/protocol/pubsub">
-                    <publish node="storage:bookmarks">
-                        <item id="current">
-                            <storage xmlns="storage:bookmarks">
-                                <conference autojoin="true" jid="theplay@conference.shakespeare.lit" name="Play's the Thing">
-                                    <nick>JC</nick>
-                                </conference>
-                            </storage>
-                        </item>
-                    </publish>
-                    <publish-options>
-                        <x type="submit" xmlns="jabber:x:data">
-                            <field type="hidden" var="FORM_TYPE">
-                                <value>http://jabber.org/protocol/pubsub#publish-options</value>
-                            </field>
-                            <field var='pubsub#persist_items'>
-                                <value>true</value>
-                            </field>
-                            <field var='pubsub#max_items'>
-                                <value>max</value>
-                            </field>
-                            <field var='pubsub#send_last_published_item'>
-                                <value>never</value>
-                            </field>
-                            <field var='pubsub#access_model'>
-                                <value>whitelist</value>
-                            </field>
-                        </x>
-                    </publish-options>
-                </pubsub>
-            </iq>`
-        );
-        // Server acknowledges successful storage
-        const stanza = stx`<iq
-            xmlns="jabber:client"
-            to="${_converse.api.connection.get().jid}"
-            type="result"
-            id="${sent_stanza.getAttribute('id')}"/>`;
-        _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
-        await u.waitUntil(() => view.model.get('bookmarked'));
-        expect(view.model.get('bookmarked')).toBeTruthy();
-        expect(u.hasClass('on-button', view.querySelector('.toggle-bookmark')), true);
-        // We ignore this IQ stanza... (unless it's an error stanza), so
-        // nothing to test for here.
-    }));
-
-
-    describe("when bookmarked", function () {
-
-        it("can be unbookmarked", mock.initConverse([], {}, async function (_converse) {
-            const { u } = converse.env;
-            await mock.waitForRoster(_converse, 'current', 0);
-            await mock.waitUntilBookmarksReturned(
-                _converse,
-                [],
-                ['http://jabber.org/protocol/pubsub#publish-options'],
-                'storage:bookmarks'
-            );
-            const nick = 'romeo';
-            const muc_jid = 'theplay@conference.shakespeare.lit';
-            await _converse.api.rooms.open(muc_jid);
-            await mock.getRoomFeatures(_converse, muc_jid);
-            await mock.waitForReservedNick(_converse, muc_jid, nick);
-
-            const view = _converse.chatboxviews.get(muc_jid);
-            await u.waitUntil(() => view.querySelector('.toggle-bookmark'));
-
-            const { bookmarks } = _converse.state;
-
-            spyOn(view, 'showBookmarkModal').and.callThrough();
-            spyOn(bookmarks, 'sendBookmarkStanza').and.callThrough();
-
-            bookmarks.create({
-                'jid': view.model.get('jid'),
-                'autojoin': false,
-                'name':  'The Play',
-                'nick': 'Othello'
-            });
-
-            expect(bookmarks.length).toBe(1);
-            await u.waitUntil(() => _converse.chatboxes.length >= 1);
-            expect(view.model.get('bookmarked')).toBeTruthy();
-            await u.waitUntil(() => view.querySelector('.chatbox-title__text .fa-bookmark') !== null);
-            spyOn(_converse.api.connection.get(), 'getUniqueId').and.callThrough();
-            const bookmark_icon = view.querySelector('.toggle-bookmark');
-            bookmark_icon.click();
-            expect(view.showBookmarkModal).toHaveBeenCalled();
-
-            const modal = _converse.api.modal.get('converse-bookmark-form-modal');
-            await u.waitUntil(() => u.isVisible(modal), 1000);
-            const form = await u.waitUntil(() => modal.querySelector('.chatroom-form'));
-
-            expect(form.querySelector('input[name="name"]').value).toBe('The Play');
-            expect(form.querySelector('input[name="autojoin"]').checked).toBeFalsy();
-            expect(form.querySelector('input[name="nick"]').value).toBe('Othello');
-
-            // Remove the bookmark
-            modal.querySelector('.button-remove').click();
-
-            await u.waitUntil(() => view.querySelector('.chatbox-title__text .fa-bookmark') === null);
-            expect(bookmarks.length).toBe(0);
-
-            // Check that an IQ stanza is sent out, containing no
-            // conferences to bookmark (since we removed the one and
-            // only bookmark).
-            const sent_stanza = _converse.api.connection.get().IQ_stanzas.pop();
-            expect(sent_stanza).toEqualStanza(
-                stx`<iq from="${_converse.bare_jid}"
-                        to="${_converse.bare_jid}"
-                        id="${sent_stanza.getAttribute('id')}"
-                        type="set"
-                        xmlns="jabber:client">
-                    <pubsub xmlns="http://jabber.org/protocol/pubsub">
-                        <publish node="storage:bookmarks">
-                            <item id="current"><storage xmlns="storage:bookmarks"/></item>
-                        </publish>
-                        <publish-options>
-                            <x type="submit" xmlns="jabber:x:data">
-                                <field type="hidden" var="FORM_TYPE">
-                                    <value>http://jabber.org/protocol/pubsub#publish-options</value>
-                                </field>
-                                <field var='pubsub#persist_items'>
-                                    <value>true</value>
-                                </field>
-                                <field var='pubsub#max_items'>
-                                    <value>max</value>
-                                </field>
-                                <field var='pubsub#send_last_published_item'>
-                                    <value>never</value>
-                                </field>
-                                <field var='pubsub#access_model'>
-                                    <value>whitelist</value>
-                                </field>
-                            </x>
-                        </publish-options>
-                    </pubsub>
-                </iq>`
-            );
-        }));
-    });
-});
-
 describe("Bookmarks", function () {
 
     beforeEach(() => jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza }));

+ 25 - 34
src/plugins/bookmark-views/utils.js

@@ -1,52 +1,43 @@
-import { _converse, api, converse, constants, Bookmarks } from '@converse/headless';
+import { _converse, api, converse } from '@converse/headless';
 import { __ } from 'i18n';
 
-const { CHATROOMS_TYPE } = constants;
-
-export function getHeadingButtons (view, buttons) {
-    if (api.settings.get('allow_bookmarks') && view.model.get('type') === CHATROOMS_TYPE) {
-        const data = {
-            'i18n_title': __('Bookmark this groupchat'),
-            'i18n_text': __('Bookmark'),
-            'handler': (ev) => view.showBookmarkModal(ev),
-            'a_class': 'toggle-bookmark',
-            'icon_class': 'fa-bookmark',
-            'name': 'bookmark'
-        };
-        const names = buttons.map(t => t.name);
-        const idx = names.indexOf('details');
-        const data_promise = Bookmarks.checkBookmarksSupport().then((s) => (s ? data : null));
-        return idx > -1
-            ? [...buttons.slice(0, idx+1), data_promise, ...buttons.slice(idx+1)]
-            : [data_promise, ...buttons];
-    }
-    return buttons;
-}
-
-export async function removeBookmarkViaEvent (ev) {
+/**
+ * @param {Event} ev
+ */
+export async function removeBookmarkViaEvent(ev) {
     ev.preventDefault();
-    const name = ev.currentTarget.getAttribute('data-bookmark-name');
-    const jid = ev.currentTarget.getAttribute('data-room-jid');
+    const el = /** @type {Element} */ (ev.currentTarget);
+    const name = el.getAttribute('data-bookmark-name');
+    const jid = el.getAttribute('data-room-jid');
     const result = await api.confirm(__('Are you sure you want to remove the bookmark "%1$s"?', name));
     if (result) {
-        _converse.state.bookmarks.where({ jid }).forEach(b => b.destroy());
+        _converse.state.bookmarks
+            .where({ jid })
+            .forEach(/** @param {import('@converse/headless').Bookmark} b */ (b) => b.destroy());
     }
 }
 
-export function addBookmarkViaEvent (ev) {
+/**
+ * @param {Event} ev
+ */
+export function addBookmarkViaEvent(ev) {
     ev.preventDefault();
-    const jid = ev.currentTarget.getAttribute('data-room-jid');
+    const el = /** @type {Element} */ (ev.currentTarget);
+    const jid = el.getAttribute('data-room-jid');
     api.modal.show('converse-bookmark-form-modal', { jid }, ev);
 }
 
-
-export function openRoomViaEvent (ev) {
+/**
+ * @param {Event} ev
+ */
+export function openRoomViaEvent(ev) {
     ev.preventDefault();
     const { Strophe } = converse.env;
-    const name = ev.target.textContent;
-    const jid = ev.target.getAttribute('data-room-jid');
+    const el = /** @type {Element} */ (ev.currentTarget);
+    const name = el.textContent;
+    const jid = el.getAttribute('data-room-jid');
     const data = {
-        'name': name || Strophe.unescapeNode(Strophe.getNodeFromJid(jid)) || jid
+        'name': name || Strophe.unescapeNode(Strophe.getNodeFromJid(jid)) || jid,
     };
     api.rooms.open(jid, data, true);
 }

+ 3 - 3
src/shared/tests/mock.js

@@ -209,7 +209,7 @@ function openChatRoom (_converse, room, server) {
     return _converse.api.rooms.open(`${room}@${server}`);
 }
 
-async function getRoomFeatures (_converse, muc_jid, features=[]) {
+async function getRoomFeatures (_converse, muc_jid, features=[], settings={}) {
     const room = Strophe.getNodeFromJid(muc_jid);
     muc_jid = muc_jid.toLowerCase();
     const stanzas = _converse.api.connection.get().IQ_stanzas;
@@ -226,7 +226,7 @@ async function getRoomFeatures (_converse, muc_jid, features=[]) {
     }).c('query', { 'xmlns': 'http://jabber.org/protocol/disco#info'})
         .c('identity', {
             'category': 'conference',
-            'name': room[0].toUpperCase() + room.slice(1),
+            'name': settings.name ?? `${room[0].toUpperCase()}${room.slice(1)}`,
             'type': 'text'
         }).up();
 
@@ -373,7 +373,7 @@ async function openAndEnterChatRoom (
     const { api } = _converse;
     muc_jid = muc_jid.toLowerCase();
     const room_creation_promise = api.rooms.open(muc_jid, settings, force_open);
-    await getRoomFeatures(_converse, muc_jid, features);
+    await getRoomFeatures(_converse, muc_jid, features, settings);
     await waitForReservedNick(_converse, muc_jid, nick);
     // The user has just entered the room (because join was called)
     // and receives their own presence from the server.

+ 1 - 1
src/types/plugins/bookmark-views/mixins.d.ts

@@ -1,4 +1,4 @@
-export namespace bookmarkableChatRoomView {
+export namespace BookmarkableChatRoomView {
     /**
      * Set whether the groupchat is bookmarked or not.
      * @private

+ 12 - 4
src/types/plugins/bookmark-views/utils.d.ts

@@ -1,5 +1,13 @@
-export function getHeadingButtons(view: any, buttons: any): any;
-export function removeBookmarkViaEvent(ev: any): Promise<void>;
-export function addBookmarkViaEvent(ev: any): void;
-export function openRoomViaEvent(ev: any): void;
+/**
+ * @param {Event} ev
+ */
+export function removeBookmarkViaEvent(ev: Event): Promise<void>;
+/**
+ * @param {Event} ev
+ */
+export function addBookmarkViaEvent(ev: Event): void;
+/**
+ * @param {Event} ev
+ */
+export function openRoomViaEvent(ev: Event): void;
 //# sourceMappingURL=utils.d.ts.map