فهرست منبع

Refactor `_converse.XMPPStatus` out into `headless/converse-status.js`

Also move some other methods out of `converse-core` into the plugins
that use them.
JC Brand 5 سال پیش
والد
کامیت
b6d5077d04

+ 2 - 0
CHANGES.md

@@ -10,6 +10,7 @@
 - Initial support for sending custom emojis. Currently only between Converse
   instances. Still working out a wire protocol for compatibility with other clients.
   To add custom emojis, edit the `emojis.json` file.
+- Refactor some presence and status handling code from `converse-core` into `@converse/headless/converse-status`.
 
 ### Breaking changes
 
@@ -26,6 +27,7 @@
   * `_converse.api.rooms.create`
 
 - The `show_only_online_users` setting has been removed.
+- The order of certain events have now changed: `statusInitialized` is now triggered after `initialized` and `connected` and `reconnected`.
 
 ## 5.0.4 (2019-10-08)
 - New config option [allow_message_corrections](https://conversejs.org/docs/html/configuration.html#allow-message-corrections)

+ 11 - 12
spec/chatbox.js

@@ -1138,15 +1138,14 @@
         describe("A Message Counter", function () {
 
             it("is incremented when the message is received and the window is not focused",
-                mock.initConverse(
-                    ['rosterGroupsFetched'], {},
-                    async function (done, _converse) {
-
+                    mock.initConverse(
+                        ['rosterGroupsFetched'], {},
+                        async function (done, _converse) {
 
                 await test_utils.waitForRoster(_converse, 'current');
                 test_utils.openControlBox();
 
-                expect(_converse.msg_counter).toBe(0);
+                expect(document.title).toBe('Converse Tests');
 
                 const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
                 const view = await test_utils.openChatBoxFor(_converse, sender_jid)
@@ -1170,7 +1169,7 @@
                 await new Promise(resolve => view.once('messageInserted', resolve));
                 expect(_converse.incrementMsgCounter).toHaveBeenCalled();
                 expect(_converse.clearMsgCounter).not.toHaveBeenCalled();
-                expect(_converse.msg_counter).toBe(1);
+                expect(document.title).toBe('Messages (1) Converse Tests');
                 expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object));
                 _converse.windowSate = previous_state;
                 done();
@@ -1199,7 +1198,7 @@
                 await test_utils.waitForRoster(_converse, 'current');
                 test_utils.openControlBox();
 
-                expect(_converse.msg_counter).toBe(0);
+                expect(document.title).toBe('Converse Tests');
                 spyOn(_converse, 'incrementMsgCounter').and.callThrough();
                 _converse.saveWindowState(null, 'focus');
                 const message = 'This message will not increment the message counter';
@@ -1213,7 +1212,7 @@
                       .c('active', {'xmlns': Strophe.NS.CHATSTATES}).tree();
                 await _converse.chatboxes.onMessage(msg);
                 expect(_converse.incrementMsgCounter).not.toHaveBeenCalled();
-                expect(_converse.msg_counter).toBe(0);
+                expect(document.title).toBe('Converse Tests');
                 done();
             }));
 
@@ -1224,7 +1223,7 @@
 
                 await test_utils.waitForRoster(_converse, 'current');
                 // initial state
-                expect(_converse.msg_counter).toBe(0);
+                expect(document.title).toBe('Converse Tests');
                 const message = 'This message will always increment the message counter from zero',
                     sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit',
                     msgFactory = function () {
@@ -1244,12 +1243,12 @@
                 _converse.chatboxes.onMessage(msgFactory());
                 await u.waitUntil(() => _converse.api.chats.get().length === 2)
                 let view = _converse.chatboxviews.get(sender_jid);
-                expect(_converse.msg_counter).toBe(1);
+                expect(document.title).toBe('Messages (1) Converse Tests');
 
                 // come back to converse-chat page
                 _converse.saveWindowState(null, 'focus');
                 expect(u.isVisible(view.el)).toBeTruthy();
-                expect(_converse.msg_counter).toBe(0);
+                expect(document.title).toBe('Converse Tests');
 
                 // close chatbox and leave converse-chat page again
                 view.close();
@@ -1260,7 +1259,7 @@
                 await u.waitUntil(() => _converse.api.chats.get().length === 2)
                 view = _converse.chatboxviews.get(sender_jid);
                 expect(u.isVisible(view.el)).toBeTruthy();
-                expect(_converse.msg_counter).toBe(1);
+                expect(document.title).toBe('Messages (1) Converse Tests');
                 done();
             }));
         });

+ 6 - 5
spec/smacks.js

@@ -50,16 +50,17 @@
 
             await test_utils.waitForRoster(_converse, 'current', 1);
 
-            iq = IQ_stanzas.pop();
+            IQ_stanzas.pop();
+            const disco_iq = IQ_stanzas.pop();
+            expect(Strophe.serialize(disco_iq)).toBe(
+                `<iq from="romeo@montague.lit" id="${disco_iq.getAttribute('id')}" to="romeo@montague.lit" type="get" xmlns="jabber:client">`+
+                    `<pubsub xmlns="http://jabber.org/protocol/pubsub"><items node="eu.siacs.conversations.axolotl.devicelist"/></pubsub></iq>`);
+
             iq = IQ_stanzas.pop();
             expect(Strophe.serialize(iq)).toBe(
                 `<iq from="romeo@montague.lit/orchard" id="${iq.getAttribute('id')}" to="montague.lit" type="get" xmlns="jabber:client">`+
                     `<query xmlns="http://jabber.org/protocol/disco#info"/></iq>`);
 
-            const disco_iq = IQ_stanzas.pop();
-            expect(Strophe.serialize(disco_iq)).toBe(
-                `<iq from="romeo@montague.lit" id="${disco_iq.getAttribute('id')}" to="romeo@montague.lit" type="get" xmlns="jabber:client">`+
-                    `<pubsub xmlns="http://jabber.org/protocol/pubsub"><items node="eu.siacs.conversations.axolotl.devicelist"/></pubsub></iq>`);
 
             expect(sent_stanzas.filter(s => (s.nodeName === 'r')).length).toBe(2);
             expect(_converse.session.get('unacked_stanzas').length).toBe(5);

+ 2 - 1
src/converse-profile.js

@@ -7,6 +7,7 @@
 /**
  * @module converse-profile
  */
+import "@converse/headless/converse-status";
 import "@converse/headless/converse-vcard";
 import "converse-modal";
 import "formdata-polyfill";
@@ -23,7 +24,7 @@ const u = converse.env.utils;
 
 converse.plugins.add('converse-profile', {
 
-    dependencies: ["converse-modal", "converse-vcard", "converse-chatboxviews"],
+    dependencies: ["converse-status", "converse-modal", "converse-vcard", "converse-chatboxviews"],
 
     initialize () {
         /* The initialize function gets called as soon as the plugin is

+ 28 - 0
src/headless/converse-chatboxes.js

@@ -49,6 +49,33 @@ converse.plugins.add('converse-chatboxes', {
             'privateChatsAutoJoined'
         ]);
 
+        let msg_counter = 0;
+
+        _converse.incrementMsgCounter = function () {
+            msg_counter += 1;
+            const title = document.title;
+            if (!title) {
+                return;
+            }
+            if (title.search(/^Messages \(\d+\) /) === -1) {
+                document.title = `Messages (${msg_counter}) ${title}`;
+            } else {
+                document.title = title.replace(/^Messages \(\d+\) /, `Messages (${msg_counter})`);
+            }
+        };
+
+        _converse.clearMsgCounter = function () {
+            msg_counter = 0;
+            const title = document.title;
+            if (!title) {
+                return;
+            }
+            if (title.search(/^Messages \(\d+\) /) !== -1) {
+                document.title = title.replace(/^Messages \(\d+\) /, "");
+            }
+        };
+
+
         function openChat (jid) {
             if (!utils.isValidJID(jid)) {
                 return _converse.log(
@@ -1405,6 +1432,7 @@ converse.plugins.add('converse-chatboxes', {
 
         _converse.api.listen.on('presencesInitialized', (reconnecting) => _converse.chatboxes.onConnected(reconnecting));
         _converse.api.listen.on('reconnected', () => _converse.chatboxes.forEach(m => m.onReconnection()));
+        _converse.api.listen.on('windowStateChanged', d => (d.state === 'visible') && _converse.clearMsgCounter());
         /************************ END Event Handlers ************************/
 
 

+ 63 - 375
src/headless/converse-core.js

@@ -86,6 +86,7 @@ const CORE_PLUGINS = [
     'converse-roster',
     'converse-rsm',
     'converse-smacks',
+    'converse-status',
     'converse-vcard'
 ];
 
@@ -560,11 +561,6 @@ function clearSession  () {
         _converse.session.browserStorage._clear();
         delete _converse.session;
     }
-    if (_converse.shouldClearCache() && _converse.xmppstatus) {
-        _converse.xmppstatus.destroy();
-        _converse.xmppstatus.browserStorage._clear();
-        delete _converse.xmppstatus;
-    }
     /**
      * Triggered once the user session has been cleared,
      * for example when the user has logged out or when Converse has
@@ -739,6 +735,33 @@ _converse.setUserJID = async function (jid) {
 }
 
 
+function enableCarbons () {
+    /* Ask the XMPP server to enable Message Carbons
+     * See XEP-0280 https://xmpp.org/extensions/xep-0280.html#enabling
+     */
+    if (!_converse.message_carbons || !_converse.session || _converse.session.get('carbons_enabled')) {
+        return;
+    }
+    const carbons_iq = new Strophe.Builder('iq', {
+        'from': _converse.connection.jid,
+        'id': 'enablecarbons',
+        'type': 'set'
+      })
+      .c('enable', {xmlns: Strophe.NS.CARBONS});
+    _converse.connection.addHandler((iq) => {
+        if (iq.querySelectorAll('error').length > 0) {
+            _converse.log(
+                'An error occurred while trying to enable message carbons.',
+                Strophe.LogLevel.WARN);
+        } else {
+            _converse.session.save({'carbons_enabled': true});
+            _converse.log('Message carbons have been enabled.');
+        }
+    }, null, "iq", null, "enablecarbons");
+    _converse.connection.send(carbons_iq);
+}
+
+
 async function onConnected (reconnecting) {
     /* Called as soon as a new connection has been established, either
      * by logging in or by attaching to an existing BOSH session.
@@ -751,9 +774,33 @@ async function onConnected (reconnecting) {
      * user's JID resource for this session.
      * @event _converse#afterResourceBinding
      */
-    await _converse.api.trigger('afterResourceBinding', {'synchronous': true});
-    _converse.enableCarbons();
-    _converse.initStatus(reconnecting)
+    await _converse.api.trigger('afterResourceBinding', reconnecting, {'synchronous': true});
+    enableCarbons();
+
+    if (reconnecting) {
+        /**
+         * After the connection has dropped and converse.js has reconnected.
+         * Any Strophe stanza handlers (as registered via `converse.listen.stanza`) will
+         * have to be registered anew.
+         * @event _converse#reconnected
+         * @example _converse.api.listen.on('reconnected', () => { ... });
+         */
+        _converse.api.trigger('reconnected');
+    } else {
+        /**
+         * Triggered once converse.js has been initialized.
+         * See also {@link _converse#event:pluginsInitialized}.
+         * @event _converse#initialized
+         */
+        _converse.api.trigger('initialized');
+        /**
+         * Triggered after the connection has been established and Converse
+         * has got all its ducks in a row.
+         * @event _converse#initialized
+         */
+        _converse.api.trigger('connected');
+    }
+
 }
 
 
@@ -895,7 +942,6 @@ _converse.initialize = async function (settings, callback) {
     cleanup();
 
     settings = settings !== undefined ? settings : {};
-    const init_promise = u.getResolveablePromise();
     PROMISES.forEach(addPromise);
 
     if ('onpagehide' in window) {
@@ -955,7 +1001,6 @@ _converse.initialize = async function (settings, callback) {
      * https://github.com/jcbrand/converse.js/issues/521
      */
     this.send_initial_presence = true;
-    this.msg_counter = 0;
     this.user_settings = settings; // Save the user settings so that they can be used by plugins
 
     // Module-level functions
@@ -963,107 +1008,6 @@ _converse.initialize = async function (settings, callback) {
 
     this.generateResource = () => `/converse.js-${Math.floor(Math.random()*139749528).toString()}`;
 
-    /**
-     * Send out a Client State Indication (XEP-0352)
-     * @private
-     * @method sendCSI
-     * @memberOf _converse
-     * @param { String } stat - The user's chat status
-     */
-    this.sendCSI = function (stat) {
-        _converse.api.send($build(stat, {xmlns: Strophe.NS.CSI}));
-        _converse.inactive = (stat === _converse.INACTIVE) ? true : false;
-    };
-
-    this.onUserActivity = function () {
-        /* Resets counters and flags relating to CSI and auto_away/auto_xa */
-        if (_converse.idle_seconds > 0) {
-            _converse.idle_seconds = 0;
-        }
-        if (!_.get(_converse.connection, 'authenticated')) {
-            // We can't send out any stanzas when there's no authenticated connection.
-            // This can happen when the connection reconnects.
-            return;
-        }
-        if (_converse.inactive) {
-            _converse.sendCSI(_converse.ACTIVE);
-        }
-        if (_converse.idle) {
-            _converse.idle = false;
-            _converse.xmppstatus.sendPresence();
-        }
-        if (_converse.auto_changed_status === true) {
-            _converse.auto_changed_status = false;
-            // XXX: we should really remember the original state here, and
-            // then set it back to that...
-            _converse.xmppstatus.set('status', _converse.default_state);
-        }
-    };
-
-    this.onEverySecond = function () {
-        /* An interval handler running every second.
-         * Used for CSI and the auto_away and auto_xa features.
-         */
-        if (!_.get(_converse.connection, 'authenticated')) {
-            // We can't send out any stanzas when there's no authenticated connection.
-            // This can happen when the connection reconnects.
-            return;
-        }
-        const stat = _converse.xmppstatus.get('status');
-        _converse.idle_seconds++;
-        if (_converse.csi_waiting_time > 0 &&
-                _converse.idle_seconds > _converse.csi_waiting_time &&
-                !_converse.inactive) {
-            _converse.sendCSI(_converse.INACTIVE);
-        }
-        if (_converse.idle_presence_timeout > 0 &&
-                _converse.idle_seconds > _converse.idle_presence_timeout &&
-                !_converse.idle) {
-            _converse.idle = true;
-            _converse.xmppstatus.sendPresence();
-        }
-        if (_converse.auto_away > 0 &&
-                _converse.idle_seconds > _converse.auto_away &&
-                stat !== 'away' && stat !== 'xa' && stat !== 'dnd') {
-            _converse.auto_changed_status = true;
-            _converse.xmppstatus.set('status', 'away');
-        } else if (_converse.auto_xa > 0 &&
-                _converse.idle_seconds > _converse.auto_xa &&
-                stat !== 'xa' && stat !== 'dnd') {
-            _converse.auto_changed_status = true;
-            _converse.xmppstatus.set('status', 'xa');
-        }
-    };
-
-    this.registerIntervalHandler = function () {
-        /* Set an interval of one second and register a handler for it.
-         * Required for the auto_away, auto_xa and csi_waiting_time features.
-         */
-        if (
-            _converse.auto_away < 1 &&
-            _converse.auto_xa < 1 &&
-            _converse.csi_waiting_time < 1 &&
-            _converse.idle_presence_timeout < 1
-        ) {
-            // Waiting time of less then one second means features aren't used.
-            return;
-        }
-        _converse.idle_seconds = 0;
-        _converse.auto_changed_status = false; // Was the user's status changed by Converse?
-        window.addEventListener('click', _converse.onUserActivity);
-        window.addEventListener('focus', _converse.onUserActivity);
-        window.addEventListener('keypress', _converse.onUserActivity);
-        window.addEventListener('mousemove', _converse.onUserActivity);
-        const options = {'once': true, 'passive': true};
-        window.addEventListener(_converse.unloadevent, _converse.onUserActivity, options);
-        window.addEventListener(_converse.unloadevent, () => {
-            if (_converse.session) {
-                _converse.session.save('active', false);
-            }
-        });
-        _converse.everySecondTrigger = window.setInterval(_converse.onEverySecond, 1000);
-    };
-
     this.setConnectionStatus = function (connection_status, message) {
         _converse.connfeedback.set({
             'connection_status': connection_status,
@@ -1071,21 +1015,6 @@ _converse.initialize = async function (settings, callback) {
         });
     };
 
-    /**
-     * Reject or cancel another user's subscription to our presence updates.
-     * @method rejectPresenceSubscription
-     * @private
-     * @memberOf _converse
-     * @param { String } jid - The Jabber ID of the user whose subscription is being canceled
-     * @param { String } message - An optional message to the user
-     */
-    this.rejectPresenceSubscription = function (jid, message) {
-        const pres = $pres({to: jid, type: "unsubscribed"});
-        if (message && message !== "") { pres.c("status").t(message); }
-        _converse.api.send(pres);
-    };
-
-
     /**
      * Gets called once strophe's status reaches Strophe.Status.DISCONNECTED.
      * Will either start a teardown process for converse.js or attempt
@@ -1134,11 +1063,15 @@ _converse.initialize = async function (settings, callback) {
         }
     };
 
+    /**
+     * Callback method called by Strophe as the Strophe.Connection goes
+     * through various states while establishing or tearing down a
+     * connection.
+     * @method _converse#onConnectStatusChanged
+     * @private
+     * @memberOf _converse
+     */
     this.onConnectStatusChanged = function (status, message) {
-        /* Callback method called by Strophe as the Strophe.Connection goes
-         * through various states while establishing or tearing down a
-         * connection.
-         */
         _converse.log(`Status changed to: ${_converse.CONNECTION_STATUS[status]}`);
         if (status === Strophe.Status.CONNECTED || status === Strophe.Status.ATTACHED) {
             _converse.setConnectionStatus(status);
@@ -1193,49 +1126,6 @@ _converse.initialize = async function (settings, callback) {
         }
     };
 
-    this.incrementMsgCounter = function () {
-        this.msg_counter += 1;
-        const unreadMsgCount = this.msg_counter;
-        let title = document.title;
-        if (!title) {
-            return;
-        }
-        if (title.search(/^Messages \(\d+\) /) === -1) {
-            title = `Messages (${unreadMsgCount}) ${title}`;
-        } else {
-            title = title.replace(/^Messages \(\d+\) /, `Messages (${unreadMsgCount})`);
-        }
-    };
-
-    this.clearMsgCounter = function () {
-        this.msg_counter = 0;
-        let title = document.title;
-        if (!title) {
-            return;
-        }
-        if (title.search(/^Messages \(\d+\) /) !== -1) {
-            title = title.replace(/^Messages \(\d+\) /, "");
-        }
-    };
-
-    this.initStatus = (reconnecting) => {
-        // If there's no xmppstatus obj, then we were never connected to
-        // begin with, so we set reconnecting to false.
-        reconnecting = _converse.xmppstatus === undefined ? false : reconnecting;
-        if (reconnecting) {
-            _converse.onStatusInitialized(reconnecting);
-        } else {
-            const id = `converse.xmppstatus-${_converse.bare_jid}`;
-            _converse.xmppstatus = new this.XMPPStatus({'id': id});
-            _converse.xmppstatus.browserStorage = _converse.createStore(id, "session");
-            _converse.xmppstatus.fetch({
-                'success': () => _converse.onStatusInitialized(reconnecting),
-                'error': () => _converse.onStatusInitialized(reconnecting),
-                'silent': true
-            });
-        }
-    }
-
     this.saveWindowState = function (ev) {
         // XXX: eventually we should be able to just use
         // document.visibilityState (when we drop support for older
@@ -1255,9 +1145,6 @@ _converse.initialize = async function (settings, callback) {
         } else {
             state = document.hidden ? "hidden" : "visible";
         }
-        if (state  === 'visible') {
-            _converse.clearMsgCounter();
-        }
         _converse.windowState = state;
         /**
          * Triggered when window state has changed.
@@ -1284,73 +1171,6 @@ _converse.initialize = async function (settings, callback) {
         _converse.api.trigger('registeredGlobalEventHandlers');
     };
 
-    this.enableCarbons = function () {
-        /* Ask the XMPP server to enable Message Carbons
-         * See XEP-0280 https://xmpp.org/extensions/xep-0280.html#enabling
-         */
-        if (!this.message_carbons || !this.session || this.session.get('carbons_enabled')) {
-            return;
-        }
-        const carbons_iq = new Strophe.Builder('iq', {
-            'from': this.connection.jid,
-            'id': 'enablecarbons',
-            'type': 'set'
-          })
-          .c('enable', {xmlns: Strophe.NS.CARBONS});
-        this.connection.addHandler((iq) => {
-            if (iq.querySelectorAll('error').length > 0) {
-                _converse.log(
-                    'An error occurred while trying to enable message carbons.',
-                    Strophe.LogLevel.WARN);
-            } else {
-                this.session.save({'carbons_enabled': true});
-                _converse.log('Message carbons have been enabled.');
-            }
-        }, null, "iq", null, "enablecarbons");
-        this.connection.send(carbons_iq);
-    };
-
-
-    this.sendInitialPresence = function () {
-        if (_converse.send_initial_presence) {
-            _converse.xmppstatus.sendPresence();
-        }
-    };
-
-    this.onStatusInitialized = function (reconnecting) {
-        /**
-         * Triggered when the user's own chat status has been initialized.
-         * @event _converse#statusInitialized
-         * @example _converse.api.listen.on('statusInitialized', status => { ... });
-         * @example _converse.api.waitUntil('statusInitialized').then(() => { ... });
-         */
-        _converse.api.trigger('statusInitialized', reconnecting);
-        if (reconnecting) {
-            /**
-             * After the connection has dropped and converse.js has reconnected.
-             * Any Strophe stanza handlers (as registered via `converse.listen.stanza`) will
-             * have to be registered anew.
-             * @event _converse#reconnected
-             * @example _converse.api.listen.on('reconnected', () => { ... });
-             */
-            _converse.api.trigger('reconnected');
-        } else {
-            init_promise.resolve();
-            /**
-             * Triggered once converse.js has been initialized.
-             * See also {@link _converse#event:pluginsInitialized}.
-             * @event _converse#initialized
-             */
-            _converse.api.trigger('initialized');
-            /**
-             * Triggered after the connection has been established and Converse
-             * has got all its ducks in a row.
-             * @event _converse#initialized
-             */
-            _converse.api.trigger('connected');
-        }
-    };
-
     this.bindResource = async function () {
         /**
          * Synchronous event triggered before we send an IQ to bind the user's
@@ -1373,79 +1193,11 @@ _converse.initialize = async function (settings, callback) {
     });
     this.connfeedback = new this.ConnectionFeedback();
 
-
-    this.XMPPStatus = Backbone.Model.extend({
-        defaults: {
-            "status":  _converse.default_state
-        },
-
-        initialize () {
-            this.on('change', item => {
-                if (!_.isObject(item.changed)) {
-                    return;
-                }
-                if ('status' in item.changed || 'status_message' in item.changed) {
-                    this.sendPresence(this.get('status'), this.get('status_message'));
-                }
-            });
-        },
-
-        getNickname () {
-            return _converse.nickname;
-        },
-
-        getFullname () {
-            // Gets overridden in converse-vcard
-            return '';
-        },
-
-        constructPresence (type, status_message) {
-            let presence;
-            type = _.isString(type) ? type : (this.get('status') || _converse.default_state);
-            status_message = _.isString(status_message) ? status_message : this.get('status_message');
-            // Most of these presence types are actually not explicitly sent,
-            // but I add all of them here for reference and future proofing.
-            if ((type === 'unavailable') ||
-                    (type === 'probe') ||
-                    (type === 'error') ||
-                    (type === 'unsubscribe') ||
-                    (type === 'unsubscribed') ||
-                    (type === 'subscribe') ||
-                    (type === 'subscribed')) {
-                presence = $pres({'type': type});
-            } else if (type === 'offline') {
-                presence = $pres({'type': 'unavailable'});
-            } else if (type === 'online') {
-                presence = $pres();
-            } else {
-                presence = $pres().c('show').t(type).up();
-            }
-            if (status_message) {
-                presence.c('status').t(status_message).up();
-            }
-            presence.c('priority').t(
-                _.isNaN(Number(_converse.priority)) ? 0 : _converse.priority
-            ).up();
-            if (_converse.idle) {
-                const idle_since = new Date();
-                idle_since.setSeconds(idle_since.getSeconds() - _converse.idle_seconds);
-                presence.c('idle', {xmlns: Strophe.NS.IDLE, since: idle_since.toISOString()});
-            }
-            return presence;
-        },
-
-        sendPresence (type, status_message) {
-            _converse.api.send(this.constructPresence(type, status_message));
-        }
-    });
-
     // Initialization
     // --------------
     await finishInitialization();
     if (_converse.isTestEnv()) {
         return _converse;
-    } else {
-        return init_promise;
     }
 };
 
@@ -1671,70 +1423,6 @@ _converse.api = {
             return promise;
         },
 
-        /**
-         * Set and get the user's chat status, also called their *availability*.
-         *
-         * @namespace _converse.api.user.status
-         * @memberOf _converse.api.user
-         */
-        status: {
-            /** Return the current user's availability status.
-             *
-             * @method _converse.api.user.status.get
-             * @example _converse.api.user.status.get();
-             */
-            get () {
-                return _converse.xmppstatus.get('status');
-            },
-            /**
-             * The user's status can be set to one of the following values:
-             *
-             * @method _converse.api.user.status.set
-             * @param {string} value The user's chat status (e.g. 'away', 'dnd', 'offline', 'online', 'unavailable' or 'xa')
-             * @param {string} [message] A custom status message
-             *
-             * @example this._converse.api.user.status.set('dnd');
-             * @example this._converse.api.user.status.set('dnd', 'In a meeting');
-             */
-            set (value, message) {
-                const data = {'status': value};
-                if (!_.includes(Object.keys(_converse.STATUS_WEIGHTS), value)) {
-                    throw new Error(
-                        'Invalid availability value. See https://xmpp.org/rfcs/rfc3921.html#rfc.section.2.2.2.1'
-                    );
-                }
-                if (_.isString(message)) {
-                    data.status_message = message;
-                }
-                _converse.xmppstatus.sendPresence(value);
-                _converse.xmppstatus.save(data);
-            },
-
-            /**
-             * Set and retrieve the user's custom status message.
-             *
-             * @namespace _converse.api.user.status.message
-             * @memberOf _converse.api.user.status
-             */
-            message: {
-                /**
-                 * @method _converse.api.user.status.message.get
-                 * @returns {string} The status message
-                 * @example const message = _converse.api.user.status.message.get()
-                 */
-                get () {
-                    return _converse.xmppstatus.get('status_message');
-                },
-                /**
-                 * @method _converse.api.user.status.message.set
-                 * @param {string} status The status message
-                 * @example _converse.api.user.status.message.set('In a meeting');
-                 */
-                set (status) {
-                    _converse.xmppstatus.save({ status_message: status });
-                }
-            }
-        }
     },
 
     /**

+ 26 - 2
src/headless/converse-roster.js

@@ -6,6 +6,7 @@
 /**
  * @module converse-roster
  */
+import "@converse/headless/converse-status";
 import converse from "@converse/headless/converse-core";
 
 const { Backbone, Strophe, $iq, $pres, dayjs, sizzle, _ } = converse.env;
@@ -14,7 +15,7 @@ const u = converse.env.utils;
 
 converse.plugins.add('converse-roster', {
 
-    dependencies: [],
+    dependencies: ['converse-status'],
 
     initialize () {
         /* The initialize function gets called as soon as the plugin is
@@ -58,6 +59,21 @@ converse.plugins.add('converse-roster', {
         };
 
 
+        /**
+         * Reject or cancel another user's subscription to our presence updates.
+         * @method rejectPresenceSubscription
+         * @private
+         * @memberOf _converse
+         * @param { String } jid - The Jabber ID of the user whose subscription is being canceled
+         * @param { String } message - An optional message to the user
+         */
+        _converse.rejectPresenceSubscription = function (jid, message) {
+            const pres = $pres({to: jid, type: "unsubscribed"});
+            if (message && message !== "") { pres.c("status").t(message); }
+            _converse.api.send(pres);
+        };
+
+
         /**
          * Initialize the Bakcbone collections that represent the contats
          * roster and the roster groups.
@@ -91,6 +107,13 @@ converse.plugins.add('converse-roster', {
         };
 
 
+        _converse.sendInitialPresence = function () {
+            if (_converse.send_initial_presence) {
+                _converse.xmppstatus.sendPresence();
+            }
+        };
+
+
         /**
          * Fetch all the roster groups, and then the roster contacts.
          * Emit an event after fetching is done in each case.
@@ -714,6 +737,7 @@ converse.plugins.add('converse-roster', {
                 _converse.api.trigger('contactRequest', this.create(user_data));
             },
 
+
             handleIncomingSubscription (presence) {
                 const jid = presence.getAttribute('from'),
                     bare_jid = Strophe.getBareJidFromJid(jid),
@@ -972,6 +996,7 @@ converse.plugins.add('converse-roster', {
             _converse.api.trigger('presencesInitialized', reconnecting);
         });
 
+
         _converse.api.listen.on('presencesInitialized', (reconnecting) => {
             if (reconnecting) {
                 /**
@@ -983,7 +1008,6 @@ converse.plugins.add('converse-roster', {
                  */
                 _converse.api.trigger('rosterReadyAfterReconnection');
             } else {
-                _converse.registerIntervalHandler();
                 _converse.initRoster();
             }
             _converse.roster.onConnected();

+ 306 - 0
src/headless/converse-status.js

@@ -0,0 +1,306 @@
+// Converse.js
+// https://conversejs.org
+//
+// Copyright (c) 2013-2019, the Converse.js developers
+// Licensed under the Mozilla Public License (MPLv2)
+/**
+ * @module converse-status
+ */
+import { get, isNaN, isObject, isString } from "lodash";
+import converse from "@converse/headless/converse-core";
+
+const { Backbone, Strophe, $build, $pres } = converse.env;
+
+
+converse.plugins.add('converse-status', {
+
+    initialize () {
+        const { _converse } = this;
+
+        _converse.XMPPStatus = Backbone.Model.extend({
+            defaults () {
+                return {"status":  _converse.default_state}
+            },
+
+            initialize () {
+                this.on('change', item => {
+                    if (!isObject(item.changed)) {
+                        return;
+                    }
+                    if ('status' in item.changed || 'status_message' in item.changed) {
+                        this.sendPresence(this.get('status'), this.get('status_message'));
+                    }
+                });
+            },
+
+            getNickname () {
+                return _converse.nickname;
+            },
+
+            getFullname () {
+                // Gets overridden in converse-vcard
+                return '';
+            },
+
+            constructPresence (type, status_message) {
+                let presence;
+                type = isString(type) ? type : (this.get('status') || _converse.default_state);
+                status_message = isString(status_message) ? status_message : this.get('status_message');
+                // Most of these presence types are actually not explicitly sent,
+                // but I add all of them here for reference and future proofing.
+                if ((type === 'unavailable') ||
+                        (type === 'probe') ||
+                        (type === 'error') ||
+                        (type === 'unsubscribe') ||
+                        (type === 'unsubscribed') ||
+                        (type === 'subscribe') ||
+                        (type === 'subscribed')) {
+                    presence = $pres({'type': type});
+                } else if (type === 'offline') {
+                    presence = $pres({'type': 'unavailable'});
+                } else if (type === 'online') {
+                    presence = $pres();
+                } else {
+                    presence = $pres().c('show').t(type).up();
+                }
+                if (status_message) {
+                    presence.c('status').t(status_message).up();
+                }
+                presence.c('priority').t(isNaN(Number(_converse.priority)) ? 0 : _converse.priority).up();
+                if (_converse.idle) {
+                    const idle_since = new Date();
+                    idle_since.setSeconds(idle_since.getSeconds() - _converse.idle_seconds);
+                    presence.c('idle', {xmlns: Strophe.NS.IDLE, since: idle_since.toISOString()});
+                }
+                return presence;
+            },
+
+            sendPresence (type, status_message) {
+                _converse.api.send(this.constructPresence(type, status_message));
+            }
+        });
+
+
+        /**
+         * Send out a Client State Indication (XEP-0352)
+         * @private
+         * @method sendCSI
+         * @memberOf _converse
+         * @param { String } stat - The user's chat status
+         */
+        _converse.sendCSI = function (stat) {
+            _converse.api.send($build(stat, {xmlns: Strophe.NS.CSI}));
+            _converse.inactive = (stat === _converse.INACTIVE) ? true : false;
+        };
+
+
+        _converse.onUserActivity = function () {
+            /* Resets counters and flags relating to CSI and auto_away/auto_xa */
+            if (_converse.idle_seconds > 0) {
+                _converse.idle_seconds = 0;
+            }
+            if (!get(_converse.connection, 'authenticated')) {
+                // We can't send out any stanzas when there's no authenticated connection.
+                // This can happen when the connection reconnects.
+                return;
+            }
+            if (_converse.inactive) {
+                _converse.sendCSI(_converse.ACTIVE);
+            }
+            if (_converse.idle) {
+                _converse.idle = false;
+                _converse.xmppstatus.sendPresence();
+            }
+            if (_converse.auto_changed_status === true) {
+                _converse.auto_changed_status = false;
+                // XXX: we should really remember the original state here, and
+                // then set it back to that...
+                _converse.xmppstatus.set('status', _converse.default_state);
+            }
+        };
+
+        _converse.onEverySecond = function () {
+            /* An interval handler running every second.
+             * Used for CSI and the auto_away and auto_xa features.
+             */
+            if (!get(_converse.connection, 'authenticated')) {
+                // We can't send out any stanzas when there's no authenticated connection.
+                // This can happen when the connection reconnects.
+                return;
+            }
+            const stat = _converse.xmppstatus.get('status');
+            _converse.idle_seconds++;
+            if (_converse.csi_waiting_time > 0 &&
+                    _converse.idle_seconds > _converse.csi_waiting_time &&
+                    !_converse.inactive) {
+                _converse.sendCSI(_converse.INACTIVE);
+            }
+            if (_converse.idle_presence_timeout > 0 &&
+                    _converse.idle_seconds > _converse.idle_presence_timeout &&
+                    !_converse.idle) {
+                _converse.idle = true;
+                _converse.xmppstatus.sendPresence();
+            }
+            if (_converse.auto_away > 0 &&
+                    _converse.idle_seconds > _converse.auto_away &&
+                    stat !== 'away' && stat !== 'xa' && stat !== 'dnd') {
+                _converse.auto_changed_status = true;
+                _converse.xmppstatus.set('status', 'away');
+            } else if (_converse.auto_xa > 0 &&
+                    _converse.idle_seconds > _converse.auto_xa &&
+                    stat !== 'xa' && stat !== 'dnd') {
+                _converse.auto_changed_status = true;
+                _converse.xmppstatus.set('status', 'xa');
+            }
+        };
+
+        _converse.registerIntervalHandler = function () {
+            /* Set an interval of one second and register a handler for it.
+             * Required for the auto_away, auto_xa and csi_waiting_time features.
+             */
+            if (
+                _converse.auto_away < 1 &&
+                _converse.auto_xa < 1 &&
+                _converse.csi_waiting_time < 1 &&
+                _converse.idle_presence_timeout < 1
+            ) {
+                // Waiting time of less then one second means features aren't used.
+                return;
+            }
+            _converse.idle_seconds = 0;
+            _converse.auto_changed_status = false; // Was the user's status changed by Converse?
+            window.addEventListener('click', _converse.onUserActivity);
+            window.addEventListener('focus', _converse.onUserActivity);
+            window.addEventListener('keypress', _converse.onUserActivity);
+            window.addEventListener('mousemove', _converse.onUserActivity);
+            const options = {'once': true, 'passive': true};
+            window.addEventListener(_converse.unloadevent, _converse.onUserActivity, options);
+            window.addEventListener(_converse.unloadevent, () => {
+                if (_converse.session) {
+                    _converse.session.save('active', false);
+                }
+            });
+            _converse.everySecondTrigger = window.setInterval(_converse.onEverySecond, 1000);
+        };
+
+
+        _converse.api.listen.on('presencesInitialized', (reconnecting) => {
+            if (!reconnecting) {
+                _converse.registerIntervalHandler();
+            }
+        });
+
+
+        function onStatusInitialized (reconnecting) {
+            /**
+             * Triggered when the user's own chat status has been initialized.
+             * @event _converse#statusInitialized
+             * @example _converse.api.listen.on('statusInitialized', status => { ... });
+             * @example _converse.api.waitUntil('statusInitialized').then(() => { ... });
+             */
+            _converse.api.trigger('statusInitialized', reconnecting);
+        }
+
+
+        function initStatus (reconnecting) {
+            // If there's no xmppstatus obj, then we were never connected to
+            // begin with, so we set reconnecting to false.
+            reconnecting = _converse.xmppstatus === undefined ? false : reconnecting;
+            if (reconnecting) {
+                onStatusInitialized(reconnecting);
+            } else {
+                const id = `converse.xmppstatus-${_converse.bare_jid}`;
+                _converse.xmppstatus = new _converse.XMPPStatus({'id': id});
+                _converse.xmppstatus.browserStorage = _converse.createStore(id, "session");
+                _converse.xmppstatus.fetch({
+                    'success': () => onStatusInitialized(reconnecting),
+                    'error': () => onStatusInitialized(reconnecting),
+                    'silent': true
+                });
+            }
+        }
+
+
+        /************************ BEGIN Event Handlers ************************/
+        _converse.api.listen.on('clearSession', () => {
+            if (_converse.shouldClearCache() && _converse.xmppstatus) {
+                _converse.xmppstatus.destroy();
+                _converse.xmppstatus.browserStorage._clear();
+                delete _converse.xmppstatus;
+            }
+        });
+
+        _converse.api.listen.on('connected', () => initStatus(false));
+        _converse.api.listen.on('reconnected', () => initStatus(true));
+        /************************ END Event Handlers ************************/
+
+
+        /************************ BEGIN API ************************/
+        Object.assign(_converse.api.user, {
+            /**
+             * Set and get the user's chat status, also called their *availability*.
+             *
+             * @namespace _converse.api.user.status
+             * @memberOf _converse.api.user
+             */
+            status: {
+                /** Return the current user's availability status.
+                 *
+                 * @method _converse.api.user.status.get
+                 * @example _converse.api.user.status.get();
+                 */
+                get () {
+                    return _converse.xmppstatus.get('status');
+                },
+                /**
+                 * The user's status can be set to one of the following values:
+                 *
+                 * @method _converse.api.user.status.set
+                 * @param {string} value The user's chat status (e.g. 'away', 'dnd', 'offline', 'online', 'unavailable' or 'xa')
+                 * @param {string} [message] A custom status message
+                 *
+                 * @example this._converse.api.user.status.set('dnd');
+                 * @example this._converse.api.user.status.set('dnd', 'In a meeting');
+                 */
+                set (value, message) {
+                    const data = {'status': value};
+                    if (!Object.keys(_converse.STATUS_WEIGHTS).includes(value)) {
+                        throw new Error(
+                            'Invalid availability value. See https://xmpp.org/rfcs/rfc3921.html#rfc.section.2.2.2.1'
+                        );
+                    }
+                    if (isString(message)) {
+                        data.status_message = message;
+                    }
+                    _converse.xmppstatus.sendPresence(value);
+                    _converse.xmppstatus.save(data);
+                },
+
+                /**
+                 * Set and retrieve the user's custom status message.
+                 *
+                 * @namespace _converse.api.user.status.message
+                 * @memberOf _converse.api.user.status
+                 */
+                message: {
+                    /**
+                     * @method _converse.api.user.status.message.get
+                     * @returns {string} The status message
+                     * @example const message = _converse.api.user.status.message.get()
+                     */
+                    get () {
+                        return _converse.xmppstatus.get('status_message');
+                    },
+                    /**
+                     * @method _converse.api.user.status.message.set
+                     * @param {string} status The status message
+                     * @example _converse.api.user.status.message.set('In a meeting');
+                     */
+                    set (status) {
+                        _converse.xmppstatus.save({ status_message: status });
+                    }
+                }
+            }
+        });
+    }
+});

+ 3 - 3
src/headless/converse-vcard.js

@@ -6,6 +6,7 @@
 /**
  * @module converse-vcard
  */
+import "./converse-status";
 import converse from "./converse-core";
 import tpl_vcard from "./templates/vcard.html";
 
@@ -15,7 +16,7 @@ const u = converse.env.utils;
 
 converse.plugins.add('converse-vcard', {
 
-    dependencies: ["converse-roster"],
+    dependencies: ["converse-status", "converse-roster"],
 
     overrides: {
         XMPPStatus: {
@@ -162,10 +163,9 @@ converse.plugins.add('converse-vcard', {
             _converse.vcards.browserStorage = _converse.createStore(id, _converse.config.get('storage'));
             _converse.vcards.fetch();
         }
-        _converse.api.listen.on('afterResourceBinding', _converse.initVCardCollection);
-
 
         _converse.api.listen.on('statusInitialized', () => {
+            _converse.initVCardCollection();
             const vcards = _converse.vcards;
             if (_converse.session) {
                 const jid = _converse.session.get('bare_jid');

+ 1 - 0
src/headless/headless.js

@@ -14,6 +14,7 @@ import "./converse-pubsub";      // XEP-0060 Pubsub
 import "./converse-roster";      // Contacts Roster
 import "./converse-rsm";         // XEP-0059 Result Set management
 import "./converse-smacks";      // XEP-0198 Stream Management
+import "./converse-status";   // XEP-0199 XMPP Ping
 import "./converse-vcard";       // XEP-0054 VCard-temp
 /* END: Removable components */
 

+ 1 - 0
tests/mock.js

@@ -256,6 +256,7 @@
                 if (el) {
                     el.parentElement.removeChild(el);
                 }
+                document.title = "Converse Tests";
                 done();
             }
             await Promise.all((promise_names || []).map(_converse.api.waitUntil));