Browse Source

Move CSI event handlers out of headless

They are UI related and rely on the DOM.
JC Brand 2 weeks ago
parent
commit
23cd0e97ea

+ 4 - 3
src/headless/plugins/caps/tests/caps.js

@@ -46,9 +46,10 @@ describe('A sent presence stanza', function () {
                 <status>Hello world</status>
                 <status>Hello world</status>
                 <priority>0</priority>
                 <priority>0</priority>
                 <x xmlns="vcard-temp:x:update"/>
                 <x xmlns="vcard-temp:x:update"/>
-                <c hash="sha-1" node="https://conversejs.org" ver="t7NrIuCRhg80cJKAq33v3LKogjI=" xmlns="http://jabber.org/protocol/caps"/>
+                <c hash="sha-1" node="https://conversejs.org" ver="1T0pIfIxYO645OaT9gpXVXOvb9s=" xmlns="http://jabber.org/protocol/caps"/>
             </presence>`);
             </presence>`);
 
 
+
             api.settings.set('priority', 2);
             api.settings.set('priority', 2);
             pres = await profile.constructPresence({ show: 'away', status: 'Going jogging' });
             pres = await profile.constructPresence({ show: 'away', status: 'Going jogging' });
             expect(pres.node).toEqualStanza(stx`
             expect(pres.node).toEqualStanza(stx`
@@ -57,7 +58,7 @@ describe('A sent presence stanza', function () {
                 <status>Going jogging</status>
                 <status>Going jogging</status>
                 <priority>2</priority>
                 <priority>2</priority>
                 <x xmlns="vcard-temp:x:update"/>
                 <x xmlns="vcard-temp:x:update"/>
-                <c hash="sha-1" node="https://conversejs.org" ver="t7NrIuCRhg80cJKAq33v3LKogjI=" xmlns="http://jabber.org/protocol/caps"/>
+                <c hash="sha-1" node="https://conversejs.org" ver="1T0pIfIxYO645OaT9gpXVXOvb9s=" xmlns="http://jabber.org/protocol/caps"/>
             </presence>`);
             </presence>`);
 
 
             api.settings.set('priority', undefined);
             api.settings.set('priority', undefined);
@@ -68,7 +69,7 @@ describe('A sent presence stanza', function () {
                 <status>Doing taxes</status>
                 <status>Doing taxes</status>
                 <priority>0</priority>
                 <priority>0</priority>
                 <x xmlns="vcard-temp:x:update"/>
                 <x xmlns="vcard-temp:x:update"/>
-                <c hash="sha-1" node="https://conversejs.org" ver="t7NrIuCRhg80cJKAq33v3LKogjI=" xmlns="http://jabber.org/protocol/caps"/>
+                <c hash="sha-1" node="https://conversejs.org" ver="1T0pIfIxYO645OaT9gpXVXOvb9s=" xmlns="http://jabber.org/protocol/caps"/>
             </presence>`);
             </presence>`);
         })
         })
     );
     );

+ 1 - 1
src/headless/plugins/muc/muc.js

@@ -2079,7 +2079,7 @@ class MUC extends ModelWithVCard(ModelWithMessages(ColorAwareModel(ChatBoxBase))
 
 
     /**
     /**
      * Sends a status update presence (i.e. based on the `<show>` element)
      * Sends a status update presence (i.e. based on the `<show>` element)
-     * @param {import("../status/types").presence_attrs} attrs
+     * @param {import("../status/types").PresenceAttrs} attrs
      * @param {Element[]|Builder[]|Element|Builder} [child_nodes]
      * @param {Element[]|Builder[]|Element|Builder} [child_nodes]
      *  Nodes(s) to be added as child nodes of the `presence` XML element.
      *  Nodes(s) to be added as child nodes of the `presence` XML element.
      */
      */

+ 4 - 4
src/headless/plugins/muc/tests/muc.js

@@ -59,7 +59,7 @@ describe("Groupchats", function () {
                 <presence from="${_converse.jid}" id="${pres.getAttribute('id')}" to="${muc_jid}/romeo" xmlns="jabber:client">
                 <presence from="${_converse.jid}" id="${pres.getAttribute('id')}" to="${muc_jid}/romeo" xmlns="jabber:client">
                     <x xmlns="http://jabber.org/protocol/muc"><history maxstanzas="0"/></x>
                     <x xmlns="http://jabber.org/protocol/muc"><history maxstanzas="0"/></x>
                     <show>away</show>
                     <show>away</show>
-                    <c hash="sha-1" node="https://conversejs.org" ver="t7NrIuCRhg80cJKAq33v3LKogjI=" xmlns="http://jabber.org/protocol/caps"/>
+                    <c hash="sha-1" node="https://conversejs.org" ver="1T0pIfIxYO645OaT9gpXVXOvb9s=" xmlns="http://jabber.org/protocol/caps"/>
                 </presence>`);
                 </presence>`);
 
 
             expect(muc.getOwnOccupant().get('show')).toBe('away');
             expect(muc.getOwnOccupant().get('show')).toBe('away');
@@ -74,7 +74,7 @@ describe("Groupchats", function () {
                     <show>xa</show>
                     <show>xa</show>
                     <priority>0</priority>
                     <priority>0</priority>
                     <x xmlns="vcard-temp:x:update"/>
                     <x xmlns="vcard-temp:x:update"/>
-                    <c hash="sha-1" node="https://conversejs.org" ver="t7NrIuCRhg80cJKAq33v3LKogjI=" xmlns="http://jabber.org/protocol/caps"/>
+                    <c hash="sha-1" node="https://conversejs.org" ver="1T0pIfIxYO645OaT9gpXVXOvb9s=" xmlns="http://jabber.org/protocol/caps"/>
                 </presence>`)
                 </presence>`)
 
 
             profile.set({ show: 'dnd', status_message: 'Do not disturb' });
             profile.set({ show: 'dnd', status_message: 'Do not disturb' });
@@ -89,7 +89,7 @@ describe("Groupchats", function () {
                     <x xmlns="http://jabber.org/protocol/muc"><history maxstanzas="0"/></x>
                     <x xmlns="http://jabber.org/protocol/muc"><history maxstanzas="0"/></x>
                     <show>dnd</show>
                     <show>dnd</show>
                     <status>Do not disturb</status>
                     <status>Do not disturb</status>
-                    <c hash="sha-1" node="https://conversejs.org" ver="t7NrIuCRhg80cJKAq33v3LKogjI=" xmlns="http://jabber.org/protocol/caps"/>
+                    <c hash="sha-1" node="https://conversejs.org" ver="1T0pIfIxYO645OaT9gpXVXOvb9s=" xmlns="http://jabber.org/protocol/caps"/>
                 </presence>`);
                 </presence>`);
 
 
             expect(muc2.getOwnOccupant().get('show')).toBe('dnd');
             expect(muc2.getOwnOccupant().get('show')).toBe('dnd');
@@ -145,7 +145,7 @@ describe("Groupchats", function () {
             expect(pres).toEqualStanza(stx`
             expect(pres).toEqualStanza(stx`
                 <presence from="${_converse.jid}" id="${pres.getAttribute('id')}" to="coven@chat.shakespeare.lit/romeo" xmlns="jabber:client">
                 <presence from="${_converse.jid}" id="${pres.getAttribute('id')}" to="coven@chat.shakespeare.lit/romeo" xmlns="jabber:client">
                     <x xmlns="http://jabber.org/protocol/muc"><history maxstanzas="0"/></x>
                     <x xmlns="http://jabber.org/protocol/muc"><history maxstanzas="0"/></x>
-                    <c hash="sha-1" node="https://conversejs.org" ver="t7NrIuCRhg80cJKAq33v3LKogjI=" xmlns="http://jabber.org/protocol/caps"/>
+                    <c hash="sha-1" node="https://conversejs.org" ver="1T0pIfIxYO645OaT9gpXVXOvb9s=" xmlns="http://jabber.org/protocol/caps"/>
                 </presence>`);
                 </presence>`);
         }));
         }));
     });
     });

+ 2 - 2
src/headless/plugins/muc/tests/presence.js

@@ -26,7 +26,7 @@ describe('MUC presence history element', function () {
                 <x xmlns="http://jabber.org/protocol/muc">
                 <x xmlns="http://jabber.org/protocol/muc">
                   <history maxstanzas="5"/>
                   <history maxstanzas="5"/>
                 </x>
                 </x>
-                <c xmlns="http://jabber.org/protocol/caps" hash="sha-1" node="https://conversejs.org" ver="t7NrIuCRhg80cJKAq33v3LKogjI="/>
+                <c xmlns="http://jabber.org/protocol/caps" hash="sha-1" node="https://conversejs.org" ver="1T0pIfIxYO645OaT9gpXVXOvb9s="/>
               </presence>`);
               </presence>`);
 
 
             api.settings.set('muc_history_max_stanzas', 0);
             api.settings.set('muc_history_max_stanzas', 0);
@@ -41,7 +41,7 @@ describe('MUC presence history element', function () {
             expect(sent_stanza).toEqualStanza(stx`
             expect(sent_stanza).toEqualStanza(stx`
               <presence to="${muc2_jid}/${nick}" xmlns="jabber:client" id="${sent_stanza.getAttribute('id')}" from="${jid}">
               <presence to="${muc2_jid}/${nick}" xmlns="jabber:client" id="${sent_stanza.getAttribute('id')}" from="${jid}">
                 <x xmlns="http://jabber.org/protocol/muc"><history maxstanzas="0"/></x>
                 <x xmlns="http://jabber.org/protocol/muc"><history maxstanzas="0"/></x>
-                <c xmlns="http://jabber.org/protocol/caps" hash="sha-1" node="https://conversejs.org" ver="t7NrIuCRhg80cJKAq33v3LKogjI="/>
+                <c xmlns="http://jabber.org/protocol/caps" hash="sha-1" node="https://conversejs.org" ver="1T0pIfIxYO645OaT9gpXVXOvb9s="/>
               </presence>`);
               </presence>`);
         })
         })
     );
     );

+ 1 - 1
src/headless/plugins/smacks/tests/smacks.js

@@ -94,7 +94,7 @@ describe("XEP-0198 Stream Management", function () {
         expect(_converse.session.get('unacked_stanzas')[1]).toBe(Strophe.serialize(IQ_stanzas[3]));
         expect(_converse.session.get('unacked_stanzas')[1]).toBe(Strophe.serialize(IQ_stanzas[3]));
         expect(_converse.session.get('unacked_stanzas')[2]).toBe(
         expect(_converse.session.get('unacked_stanzas')[2]).toBe(
             `<presence xmlns="jabber:client"><priority>0</priority><x xmlns="vcard-temp:x:update"/>`+
             `<presence xmlns="jabber:client"><priority>0</priority><x xmlns="vcard-temp:x:update"/>`+
-                `<c hash="sha-1" node="https://conversejs.org" ver="t7NrIuCRhg80cJKAq33v3LKogjI=" xmlns="http://jabber.org/protocol/caps"/>`+
+                `<c hash="sha-1" node="https://conversejs.org" ver="1T0pIfIxYO645OaT9gpXVXOvb9s=" xmlns="http://jabber.org/protocol/caps"/>`+
             `</presence>`);
             `</presence>`);
 
 
         r = stx`<r xmlns="urn:xmpp:sm:3"/>`;
         r = stx`<r xmlns="urn:xmpp:sm:3"/>`;

+ 40 - 14
src/headless/plugins/status/api.js

@@ -2,6 +2,8 @@ import api from '../../shared/api/index.js';
 import _converse from '../../shared/_converse.js';
 import _converse from '../../shared/_converse.js';
 import { PRES_SHOW_VALUES, PRES_TYPE_VALUES, STATUS_WEIGHTS } from '../../shared/constants';
 import { PRES_SHOW_VALUES, PRES_TYPE_VALUES, STATUS_WEIGHTS } from '../../shared/constants';
 
 
+let idle_seconds = 0;
+let idle = false;
 
 
 export default {
 export default {
     /**
     /**
@@ -12,11 +14,10 @@ export default {
     status: {
     status: {
         /**
         /**
          * Return the current user's availability status.
          * Return the current user's availability status.
-         * @async
          * @method _converse.api.user.status.get
          * @method _converse.api.user.status.get
          * @example _converse.api.user.status.get();
          * @example _converse.api.user.status.get();
          */
          */
-        async get () {
+        async get() {
             await api.waitUntil('statusInitialized');
             await api.waitUntil('statusInitialized');
 
 
             const show = _converse.state.profile.get('show');
             const show = _converse.state.profile.get('show');
@@ -32,8 +33,6 @@ export default {
 
 
         /**
         /**
          * The user's status can be set to one of the following values:
          * The user's status can be set to one of the following values:
-         *
-         * @async
          * @method _converse.api.user.status.set
          * @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 } value The user's chat status (e.g. 'away', 'dnd', 'offline', 'online', 'unavailable' or 'xa')
          * @param { string } [message] A custom status message
          * @param { string } [message] A custom status message
@@ -41,7 +40,7 @@ export default {
          * @example _converse.api.user.status.set('dnd');
          * @example _converse.api.user.status.set('dnd');
          * @example _converse.api.user.status.set('dnd', 'In a meeting');
          * @example _converse.api.user.status.set('dnd', 'In a meeting');
          */
          */
-        async set (value, message) {
+        async set(value, message) {
             if (!Object.keys(STATUS_WEIGHTS).includes(value)) {
             if (!Object.keys(STATUS_WEIGHTS).includes(value)) {
                 throw new Error(
                 throw new Error(
                     'Invalid availability value. See https://xmpp.org/rfcs/rfc3921.html#rfc.section.2.2.2.1'
                     'Invalid availability value. See https://xmpp.org/rfcs/rfc3921.html#rfc.section.2.2.2.1'
@@ -71,25 +70,52 @@ export default {
          */
          */
         message: {
         message: {
             /**
             /**
-             * @async
              * @method _converse.api.user.status.message.get
              * @method _converse.api.user.status.message.get
-             * @returns { Promise<string> } The status message
+             * @returns {Promise<string>} The status message
              * @example const message = _converse.api.user.status.message.get()
              * @example const message = _converse.api.user.status.message.get()
              */
              */
-            async get () {
+            async get() {
                 await api.waitUntil('statusInitialized');
                 await api.waitUntil('statusInitialized');
                 return _converse.state.profile.get('status_message');
                 return _converse.state.profile.get('status_message');
             },
             },
             /**
             /**
-             * @async
              * @method _converse.api.user.status.message.set
              * @method _converse.api.user.status.message.set
-             * @param { string } status The status message
+             * @param {string} status The status message
              * @example _converse.api.user.status.message.set('In a meeting');
              * @example _converse.api.user.status.message.set('In a meeting');
              */
              */
-            async set (status) {
+            async set(status) {
                 await api.waitUntil('statusInitialized');
                 await api.waitUntil('statusInitialized');
                 _converse.state.profile.save({ status_message: status });
                 _converse.state.profile.save({ status_message: status });
+            },
+        },
+    },
+
+    /**
+     * Set and get the user's idle status
+     * @namespace _converse.api.user.idle
+     * @memberOf _converse.api.user
+     */
+    idle: {
+        /**
+         * @method _converse.api.user.idle.get
+         * @returns {import('./types').IdleStatus}
+         * @example _converse.api.user.idle.get();
+         */
+        get() {
+            return { idle, seconds: idle_seconds };
+        },
+
+        /**
+         * @method _converse.api.user.idle.set
+         * @param {import('./types').IdleStatus} status
+         */
+        set(status) {
+            if (status.idle) {
+                idle = status.idle;
+            }
+            if (typeof status.seconds === 'number') {
+                idle_seconds = status.seconds;
             }
             }
-        }
-    }
-}
+        },
+    },
+};

+ 4 - 35
src/headless/plugins/status/plugin.js

@@ -4,52 +4,21 @@ import api from '../../shared/api/index.js';
 import converse from '../../shared/api/public.js';
 import converse from '../../shared/api/public.js';
 import status_api from './api.js';
 import status_api from './api.js';
 import { shouldClearCache } from '../../utils/session.js';
 import { shouldClearCache } from '../../utils/session.js';
-import {
-    initStatus,
-    onEverySecond,
-    onUserActivity,
-    registerIntervalHandler,
-    tearDown,
-    sendCSI
-} from './utils.js';
-
-const { Strophe } = converse.env;
-
-Strophe.addNamespace('IDLE', 'urn:xmpp:idle:1');
-
+import { initStatus } from './utils.js';
 
 
 converse.plugins.add('converse-status', {
 converse.plugins.add('converse-status', {
-
-    initialize () {
-
-        api.settings.extend({
-            auto_away: 0, // Seconds after which user status is set to 'away'
-            auto_xa: 0, // Seconds after which user status is set to 'xa'
-            csi_waiting_time: 0, // Support for XEP-0352. Seconds before client is considered idle and CSI is sent out.
-            idle_presence_timeout: 300, // Seconds after which an idle presence is sent
-            priority: 0,
-        });
+    initialize() {
+        api.settings.extend({ priority: 0 });
         api.promises.add(['statusInitialized']);
         api.promises.add(['statusInitialized']);
 
 
         const exports = {
         const exports = {
             XMPPStatus: Profile, // Deprecated
             XMPPStatus: Profile, // Deprecated
             Profile,
             Profile,
-            onUserActivity,
-            onEverySecond,
-            sendCSI,
-            registerIntervalHandler
         };
         };
         Object.assign(_converse, exports); // Deprecated
         Object.assign(_converse, exports); // Deprecated
         Object.assign(_converse.exports, exports);
         Object.assign(_converse.exports, exports);
         Object.assign(_converse.api.user, status_api);
         Object.assign(_converse.api.user, status_api);
 
 
-        if (api.settings.get("idle_presence_timeout") > 0) {
-            api.listen.on('addClientFeatures', () => api.disco.own.features.add(Strophe.NS.IDLE));
-        }
-
-        api.listen.on('presencesInitialized', (reconnecting) => (!reconnecting && registerIntervalHandler()));
-        api.listen.on('beforeTearDown', tearDown);
-
         api.listen.on('clearSession', () => {
         api.listen.on('clearSession', () => {
             if (shouldClearCache(_converse) && _converse.state.profile) {
             if (shouldClearCache(_converse) && _converse.state.profile) {
                 _converse.state.profile.destroy();
                 _converse.state.profile.destroy();
@@ -61,5 +30,5 @@ converse.plugins.add('converse-status', {
 
 
         api.listen.on('connected', () => initStatus(false));
         api.listen.on('connected', () => initStatus(false));
         api.listen.on('reconnected', () => initStatus(true));
         api.listen.on('reconnected', () => initStatus(true));
-    }
+    },
 });
 });

+ 5 - 5
src/headless/plugins/status/profile.js

@@ -4,7 +4,6 @@ import api from '../../shared/api/index.js';
 import converse from '../../shared/api/public.js';
 import converse from '../../shared/api/public.js';
 import ModelWithVCard from '../../shared/model-with-vcard';
 import ModelWithVCard from '../../shared/model-with-vcard';
 import ColorAwareModel from '../../shared/color.js';
 import ColorAwareModel from '../../shared/color.js';
-import { isIdle, getIdleSeconds } from './utils.js';
 
 
 const { Stanza, Strophe, stx } = converse.env;
 const { Stanza, Strophe, stx } = converse.env;
 
 
@@ -19,7 +18,7 @@ export default class Profile extends ModelWithVCard(ColorAwareModel(Model)) {
     }
     }
 
 
     /**
     /**
-     * @return {import('./types').connection_status}
+     * @return {import('./types').ConnectionStatus}
      */
      */
     getStatus () {
     getStatus () {
         const presence  = this.get('presence');
         const presence  = this.get('presence');
@@ -80,7 +79,7 @@ export default class Profile extends ModelWithVCard(ColorAwareModel(Model)) {
 
 
     /**
     /**
      * Constructs a presence stanza
      * Constructs a presence stanza
-     * @param {import('./types').presence_attrs} [attrs={}]
+     * @param {import('./types').PresenceAttrs} [attrs={}]
      * @returns {Promise<Stanza>}
      * @returns {Promise<Stanza>}
      */
      */
     async constructPresence(attrs = {}) {
     async constructPresence(attrs = {}) {
@@ -91,11 +90,12 @@ export default class Profile extends ModelWithVCard(ColorAwareModel(Model)) {
         const include_nick = type === 'subscribe';
         const include_nick = type === 'subscribe';
         const nick = include_nick ? profile.getNickname() : null;
         const nick = include_nick ? profile.getNickname() : null;
         const priority = api.settings.get('priority');
         const priority = api.settings.get('priority');
+        const { idle: is_idle, seconds: idle_seconds } = api.user.idle.get();
 
 
         let idle_since;
         let idle_since;
-        if (isIdle()) {
+        if (is_idle) {
             idle_since = new Date();
             idle_since = new Date();
-            idle_since.setSeconds(idle_since.getSeconds() - getIdleSeconds());
+            idle_since.setSeconds(idle_since.getSeconds() - idle_seconds);
         }
         }
 
 
         const presence = stx`
         const presence = stx`

+ 10 - 5
src/headless/plugins/status/types.ts

@@ -1,14 +1,14 @@
-export type connection_status = 'online' | 'unavailable' | 'offline';
-export type profile_show = 'dnd' | 'away' | 'xa' | 'chat';
+export type ConnectionStatus = 'online' | 'unavailable' | 'offline';
+export type ProfileShow = 'dnd' | 'away' | 'xa' | 'chat';
 
 
-export type presence_attrs = {
-    type?: presence_type;
+export type PresenceAttrs = {
+    type?: PresenceType;
     to?: string;
     to?: string;
     status?: string;
     status?: string;
     show?: string;
     show?: string;
 };
 };
 
 
-export type presence_type =
+type PresenceType =
     | 'error'
     | 'error'
     | 'offline'
     | 'offline'
     | 'online'
     | 'online'
@@ -17,3 +17,8 @@ export type presence_type =
     | 'unavailable'
     | 'unavailable'
     | 'unsubscribe'
     | 'unsubscribe'
     | 'unsubscribed';
     | 'unsubscribed';
+
+export type IdleStatus = {
+    idle?: boolean;
+    seconds?: number;
+};

+ 0 - 140
src/headless/plugins/status/utils.js

@@ -1,11 +1,6 @@
 import _converse from '../../shared/_converse.js';
 import _converse from '../../shared/_converse.js';
 import api from '../../shared/api/index.js';
 import api from '../../shared/api/index.js';
-import converse from '../../shared/api/public.js';
 import { initStorage } from '../../utils/storage.js';
 import { initStorage } from '../../utils/storage.js';
-import { getUnloadEvent } from '../../utils/session.js';
-import { ACTIVE, INACTIVE } from '../../shared/constants.js';
-
-const { Strophe, $build } = converse.env;
 
 
 /**
 /**
  * @param {boolean} reconnecting
  * @param {boolean} reconnecting
@@ -42,138 +37,3 @@ export function initStatus(reconnecting) {
         });
         });
     }
     }
 }
 }
-
-let idle_seconds = 0;
-let idle = false;
-let auto_changed_status = false;
-let inactive = false;
-
-export function isIdle() {
-    return idle;
-}
-
-export function getIdleSeconds() {
-    return idle_seconds;
-}
-
-/**
- * Resets counters and flags relating to CSI and auto_away/auto_xa
- */
-export function onUserActivity() {
-    if (idle_seconds > 0) {
-        idle_seconds = 0;
-    }
-    if (!api.connection.get()?.authenticated) {
-        // We can't send out any stanzas when there's no authenticated connection.
-        // This can happen when the connection reconnects.
-        return;
-    }
-    if (inactive) sendCSI(ACTIVE);
-
-    if (idle) {
-        idle = false;
-        api.user.presence.send();
-    }
-
-    if (auto_changed_status === true) {
-        auto_changed_status = false;
-        // XXX: we should really remember the original state here, and
-        // then set it back to that...
-        _converse.state.profile.set('show', undefined);
-    }
-}
-
-/**
- * An interval handler running every second.
- * Used for CSI and the auto_away and auto_xa features.
- */
-export function onEverySecond() {
-    if (!api.connection.get()?.authenticated) {
-        // We can't send out any stanzas when there's no authenticated connection.
-        // This can happen when the connection reconnects.
-        return;
-    }
-    const { profile } = _converse.state;
-    const show = profile.get('show');
-    idle_seconds++;
-    if (api.settings.get('csi_waiting_time') > 0 && idle_seconds > api.settings.get('csi_waiting_time') && !inactive) {
-        sendCSI(INACTIVE);
-    }
-    if (
-        api.settings.get('idle_presence_timeout') > 0 &&
-        idle_seconds > api.settings.get('idle_presence_timeout') &&
-        !idle
-    ) {
-        idle = true;
-        api.user.presence.send();
-    }
-    if (
-        api.settings.get('auto_away') > 0 &&
-        idle_seconds > api.settings.get('auto_away') &&
-        show !== 'away' &&
-        show !== 'xa' &&
-        show !== 'dnd'
-    ) {
-        auto_changed_status = true;
-        profile.set('show', 'away');
-    } else if (
-        api.settings.get('auto_xa') > 0 &&
-        idle_seconds > api.settings.get('auto_xa') &&
-        show !== 'xa' &&
-        show !== 'dnd'
-    ) {
-        auto_changed_status = true;
-        profile.set('show', 'xa');
-    }
-}
-
-/**
- * Send out a Client State Indication (XEP-0352)
- * @function sendCSI
- * @param { String } stat - The user's chat status
- */
-export function sendCSI(stat) {
-    api.send($build(stat, { xmlns: Strophe.NS.CSI }));
-    inactive = stat === INACTIVE ? true : false;
-}
-
-let everySecondTrigger;
-
-/**
- * Set an interval of one second and register a handler for it.
- * Required for the auto_away, auto_xa and csi_waiting_time features.
- */
-export function registerIntervalHandler() {
-    if (
-        api.settings.get('auto_away') < 1 &&
-        api.settings.get('auto_xa') < 1 &&
-        api.settings.get('csi_waiting_time') < 1 &&
-        api.settings.get('idle_presence_timeout') < 1
-    ) {
-        // Waiting time of less then one second means features aren't used.
-        return;
-    }
-    idle_seconds = 0;
-    auto_changed_status = false; // Was the user's status changed by Converse?
-
-    const { onUserActivity, onEverySecond } = _converse.exports;
-    window.addEventListener('click', onUserActivity);
-    window.addEventListener('focus', onUserActivity);
-    window.addEventListener('keypress', onUserActivity);
-    window.addEventListener('mousemove', onUserActivity);
-    window.addEventListener(getUnloadEvent(), onUserActivity, { 'once': true, 'passive': true });
-    everySecondTrigger = setInterval(onEverySecond, 1000);
-}
-
-export function tearDown() {
-    const { onUserActivity } = _converse.exports;
-    window.removeEventListener('click', onUserActivity);
-    window.removeEventListener('focus', onUserActivity);
-    window.removeEventListener('keypress', onUserActivity);
-    window.removeEventListener('mousemove', onUserActivity);
-    window.removeEventListener(getUnloadEvent(), onUserActivity);
-    if (everySecondTrigger) {
-        clearInterval(everySecondTrigger);
-        everySecondTrigger = null;
-    }
-}

+ 1 - 1
src/headless/shared/api/presence.js

@@ -21,7 +21,7 @@ export default {
         /**
         /**
          * Send out a presence stanza
          * Send out a presence stanza
          * @method _converse.api.user.presence.send
          * @method _converse.api.user.presence.send
-         * @param {import('../../plugins/status/types').presence_attrs} [attrs]
+         * @param {import('../../plugins/status/types').PresenceAttrs} [attrs]
          * @param {Array<Element>|Array<Builder>|Element|Builder} [nodes]
          * @param {Array<Element>|Array<Builder>|Element|Builder} [nodes]
          *  Nodes(s) to be added as child nodes of the `presence` XML element.
          *  Nodes(s) to be added as child nodes of the `presence` XML element.
          */
          */

+ 170 - 243
src/headless/tests/converse.js

@@ -1,254 +1,181 @@
 /* global mock, converse */
 /* global mock, converse */
-import mock from "../tests/mock.js";
-
-const { Strophe } = converse.env;
-
-describe("Converse", function() {
-
-    describe("A chat state indication", function () {
-
-        it("are sent out when the client becomes or stops being idle",
-            mock.initConverse(['discoInitialized'], {}, (_converse) => {
-
-            let i = 0;
-            const domain = _converse.session.get('domain');
-            _converse.disco_entities.get(domain).features['urn:xmpp:csi:0'] = true; // Mock that the server supports CSI
-
-            let sent_stanza = null;
-            spyOn(_converse.api.connection.get(), 'send').and.callFake((stanza) => {
-                sent_stanza = stanza;
-            });
-
-            _converse.api.settings.set('csi_waiting_time', 3);
-            while (i <= _converse.api.settings.get("csi_waiting_time")) {
-                expect(sent_stanza).toBe(null);
-                _converse.exports.onEverySecond();
-                i++;
-            }
-            expect(Strophe.serialize(sent_stanza)).toBe('<inactive xmlns="urn:xmpp:csi:0"/>');
-            _converse.onUserActivity();
-            expect(Strophe.serialize(sent_stanza)).toBe('<active xmlns="urn:xmpp:csi:0"/>');
-        }));
-    });
-
-    describe("Automatic status change", function () {
-
-        it("happens when the client is idle for long enough",
-                mock.initConverse(['chatBoxesFetched'], {}, async (_converse) => {
-
-            const { api } = _converse;
-            let i = 0;
-            // Usually initialized by registerIntervalHandler
-            _converse.api.settings.set('auto_away', 3);
-            _converse.api.settings.set('auto_xa', 6);
-
-            expect(await _converse.api.user.status.get()).toBe('online');
-            while (i <= _converse.api.settings.get("auto_away")) {
-                _converse.onEverySecond(); i++;
-            }
-
-            while (i <= api.settings.get('auto_xa')) {
-                expect(await _converse.api.user.status.get()).toBe('away');
-                _converse.onEverySecond();
-                i++;
-            }
-            expect(await _converse.api.user.status.get()).toBe('xa');
-
-            _converse.onUserActivity();
-            expect(await _converse.api.user.status.get()).toBe('online');
-
-            // Check that it also works for the chat feature
-            await _converse.api.user.status.set('chat')
-            i = 0;
-            while (i <= _converse.api.settings.get("auto_away")) {
-                _converse.onEverySecond();
-                i++;
-            }
-            while (i <= api.settings.get('auto_xa')) {
-                expect(await _converse.api.user.status.get()).toBe('away');
-                _converse.onEverySecond();
-                i++;
-            }
-            expect(await _converse.api.user.status.get()).toBe('xa');
-
-            _converse.onUserActivity();
-            expect(await _converse.api.user.status.get()).toBe('online');
-
-            // Check that it doesn't work for 'dnd'
-            await _converse.api.user.status.set('dnd');
-            i = 0;
-            while (i <= _converse.api.settings.get("auto_away")) {
-                _converse.onEverySecond();
-                i++;
-            }
-            expect(await _converse.api.user.status.get()).toBe('dnd');
-            while (i <= api.settings.get('auto_xa')) {
-                expect(await _converse.api.user.status.get()).toBe('dnd');
-                _converse.onEverySecond();
-                i++;
-            }
-            expect(await _converse.api.user.status.get()).toBe('dnd');
-
-            _converse.onUserActivity();
-            expect(await _converse.api.user.status.get()).toBe('dnd');
-        }));
-    });
-
-    describe("The \"user\" grouping", function () {
-
-        describe("The \"status\" API", function () {
-
-            it("has a method for getting the user's availability",
-                    mock.initConverse(['statusInitialized'], {}, async(_converse) => {
-
-                const { profile } = _converse.state;
-                profile.set('status', 'online');
-                expect(await _converse.api.user.status.get()).toBe('online');
-                profile.set('status', 'dnd');
-                expect(await _converse.api.user.status.get()).toBe('dnd');
-            }));
-
-            it("has a method for setting the user's availability", mock.initConverse(async (_converse) => {
-                await _converse.api.user.status.set('away');
-                const { profile } = _converse.state;
-                expect(await profile.get('show')).toBe('dnd');
-                await _converse.api.user.status.set('dnd');
-                expect(await profile.get('show')).toBe('dnd');
-                await _converse.api.user.status.set('xa');
-                expect(await profile.get('show')).toBe('xa');
-                await _converse.api.user.status.set('chat');
-                expect(await profile.get('show')).toBe('chat');
-                const promise = _converse.api.user.status.set('invalid')
-                promise.catch(e => {
-                    expect(e.message).toBe('Invalid availability value. See https://xmpp.org/rfcs/rfc3921.html#rfc.section.2.2.2.1');
-                });
-            }));
-
-            it("allows setting the status message as well", mock.initConverse(async (_converse) => {
-                await _converse.api.user.status.set('away', "I'm in a meeting");
-                const { profile } = _converse.state;
-                expect(profile.get('show')).toBe('dnd');
-                expect(profile.get('status_message')).toBe("I'm in a meeting");
-            }));
-
-            it("has a method for getting the user's status message",
-                    mock.initConverse(['statusInitialized'], {}, async (_converse) => {
-                const { profile } = _converse.state;
-                await profile.set('status_message', undefined);
-                expect(await _converse.api.user.status.message.get()).toBe(undefined);
-                await profile.set('status_message', "I'm in a meeting");
-                expect(await _converse.api.user.status.message.get()).toBe("I'm in a meeting");
-            }));
-
-            it("has a method for setting the user's status message",
-                    mock.initConverse(['statusInitialized'], {}, async (_converse) => {
-                const { profile } = _converse.state;
-                profile.set('status_message', undefined);
-                await _converse.api.user.status.message.set("I'm in a meeting");
-                expect(profile.get('status_message')).toBe("I'm in a meeting");
-            }));
+import mock from '../tests/mock.js';
+
+describe('Converse', function () {
+    describe('The "user" grouping', function () {
+        describe('The "status" API', function () {
+            it(
+                "has a method for getting the user's availability",
+                mock.initConverse(['statusInitialized'], {}, async (_converse) => {
+                    const { profile } = _converse.state;
+                    profile.set('status', 'online');
+                    expect(await _converse.api.user.status.get()).toBe('online');
+                    profile.set('status', 'dnd');
+                    expect(await _converse.api.user.status.get()).toBe('dnd');
+                })
+            );
+
+            it(
+                "has a method for setting the user's availability",
+                mock.initConverse(async (_converse) => {
+                    await _converse.api.user.status.set('away');
+                    const { profile } = _converse.state;
+                    expect(await profile.get('show')).toBe('dnd');
+                    await _converse.api.user.status.set('dnd');
+                    expect(await profile.get('show')).toBe('dnd');
+                    await _converse.api.user.status.set('xa');
+                    expect(await profile.get('show')).toBe('xa');
+                    await _converse.api.user.status.set('chat');
+                    expect(await profile.get('show')).toBe('chat');
+                    const promise = _converse.api.user.status.set('invalid');
+                    promise.catch((e) => {
+                        expect(e.message).toBe(
+                            'Invalid availability value. See https://xmpp.org/rfcs/rfc3921.html#rfc.section.2.2.2.1'
+                        );
+                    });
+                })
+            );
+
+            it(
+                'allows setting the status message as well',
+                mock.initConverse(async (_converse) => {
+                    await _converse.api.user.status.set('away', "I'm in a meeting");
+                    const { profile } = _converse.state;
+                    expect(profile.get('show')).toBe('dnd');
+                    expect(profile.get('status_message')).toBe("I'm in a meeting");
+                })
+            );
+
+            it(
+                "has a method for getting the user's status message",
+                mock.initConverse(['statusInitialized'], {}, async (_converse) => {
+                    const { profile } = _converse.state;
+                    await profile.set('status_message', undefined);
+                    expect(await _converse.api.user.status.message.get()).toBe(undefined);
+                    await profile.set('status_message', "I'm in a meeting");
+                    expect(await _converse.api.user.status.message.get()).toBe("I'm in a meeting");
+                })
+            );
+
+            it(
+                "has a method for setting the user's status message",
+                mock.initConverse(['statusInitialized'], {}, async (_converse) => {
+                    const { profile } = _converse.state;
+                    profile.set('status_message', undefined);
+                    await _converse.api.user.status.message.set("I'm in a meeting");
+                    expect(profile.get('status_message')).toBe("I'm in a meeting");
+                })
+            );
         });
         });
     });
     });
 
 
-    describe("The \"tokens\" API", function () {
-
-        it("has a method for retrieving the next RID",
-                mock.initConverse(['chatBoxesFetched'], {}, ({ api }) => {
-
-            const connection = api.connection.get();
-            connection._proto.rid = '1234';
-            expect(api.tokens.get('rid')).toBe('1234');
-            connection._proto.rid = '1235';
-            expect(api.tokens.get('rid')).toBe('1235');
-        }));
-
-        it("has a method for retrieving the SID",
-                mock.initConverse(['chatBoxesFetched'], {}, ({ api }) => {
-
-            const connection = api.connection.get();
-            connection._proto.sid = '1234';
-            expect(api.tokens.get('sid')).toBe('1234');
-            connection._proto.sid = '1235';
-            expect(api.tokens.get('sid')).toBe('1235');
-        }));
+    describe('The "tokens" API', function () {
+        it(
+            'has a method for retrieving the next RID',
+            mock.initConverse(['chatBoxesFetched'], {}, ({ api }) => {
+                const connection = api.connection.get();
+                connection._proto.rid = '1234';
+                expect(api.tokens.get('rid')).toBe('1234');
+                connection._proto.rid = '1235';
+                expect(api.tokens.get('rid')).toBe('1235');
+            })
+        );
+
+        it(
+            'has a method for retrieving the SID',
+            mock.initConverse(['chatBoxesFetched'], {}, ({ api }) => {
+                const connection = api.connection.get();
+                connection._proto.sid = '1234';
+                expect(api.tokens.get('sid')).toBe('1234');
+                connection._proto.sid = '1235';
+                expect(api.tokens.get('sid')).toBe('1235');
+            })
+        );
     });
     });
 
 
-    describe("The \"contacts\" API", function () {
-
-        it("has a method 'get' which returns wrapped contacts",
-                mock.initConverse([], {}, async function (_converse) {
-
-            await mock.waitForRoster(_converse, 'current');
-            let contact = await _converse.api.contacts.get('non-existing@jabber.org');
-            expect(contact).toBeFalsy();
-            // Check when a single jid is given
-            const jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
-            contact = await _converse.api.contacts.get(jid);
-            expect(contact.getDisplayName()).toBe(mock.cur_names[0]);
-            expect(contact.get('jid')).toBe(jid);
-            // You can retrieve multiple contacts by passing in an array
-            const jid2 = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit';
-            let list = await _converse.api.contacts.get([jid, jid2]);
-            expect(Array.isArray(list)).toBeTruthy();
-            expect(list[0].getDisplayName()).toBe(mock.cur_names[0]);
-            expect(list[1].getDisplayName()).toBe(mock.cur_names[1]);
-            // Check that all JIDs are returned if you call without any parameters
-            list = await _converse.api.contacts.get();
-            expect(list.length).toBe(mock.cur_names.length);
-        }));
-
-        it("has a method 'add' with which contacts can be added",
-                mock.initConverse(['rosterInitialized'], {}, async (_converse) => {
-
-            const { api } = _converse;
-
-            await mock.waitForRoster(_converse, 'current', 0);
-            try {
-                await api.contacts.add();
-                throw new Error('Call should have failed');
-            } catch (e) {
-                expect(e.message).toBe('api.contacts.add: Valid JID required');
-            }
-            try {
-                await api.contacts.add({ jid: "invalid jid" });
-                throw new Error('Call should have failed');
-            } catch (e) {
-                expect(e.message).toBe('api.contacts.add: Valid JID required');
-            }
-
-            // Create a contact that doesn't get persisted to the
-            // roster, to avoid having to mock stanzas.
-            await api.contacts.add({ jid: "newcontact@example.org" }, false, false);
-            const contacts = await api.contacts.get()
-            expect(contacts.length).toBe(1);
-            expect(contacts[0].get('jid')).toBe("newcontact@example.org");
-            expect(contacts[0].get('subscription')).toBe(undefined);
-            expect(contacts[0].get('ask')).toBeUndefined();
-            expect(contacts[0].get('groups').length).toBe(0);
-        }));
+    describe('The "contacts" API', function () {
+        it(
+            "has a method 'get' which returns wrapped contacts",
+            mock.initConverse([], {}, async function (_converse) {
+                await mock.waitForRoster(_converse, 'current');
+                let contact = await _converse.api.contacts.get('non-existing@jabber.org');
+                expect(contact).toBeFalsy();
+                // Check when a single jid is given
+                const jid = mock.cur_names[0].replace(/ /g, '.').toLowerCase() + '@montague.lit';
+                contact = await _converse.api.contacts.get(jid);
+                expect(contact.getDisplayName()).toBe(mock.cur_names[0]);
+                expect(contact.get('jid')).toBe(jid);
+                // You can retrieve multiple contacts by passing in an array
+                const jid2 = mock.cur_names[1].replace(/ /g, '.').toLowerCase() + '@montague.lit';
+                let list = await _converse.api.contacts.get([jid, jid2]);
+                expect(Array.isArray(list)).toBeTruthy();
+                expect(list[0].getDisplayName()).toBe(mock.cur_names[0]);
+                expect(list[1].getDisplayName()).toBe(mock.cur_names[1]);
+                // Check that all JIDs are returned if you call without any parameters
+                list = await _converse.api.contacts.get();
+                expect(list.length).toBe(mock.cur_names.length);
+            })
+        );
+
+        it(
+            "has a method 'add' with which contacts can be added",
+            mock.initConverse(['rosterInitialized'], {}, async (_converse) => {
+                const { api } = _converse;
+
+                await mock.waitForRoster(_converse, 'current', 0);
+                try {
+                    await api.contacts.add();
+                    throw new Error('Call should have failed');
+                } catch (e) {
+                    expect(e.message).toBe('api.contacts.add: Valid JID required');
+                }
+                try {
+                    await api.contacts.add({ jid: 'invalid jid' });
+                    throw new Error('Call should have failed');
+                } catch (e) {
+                    expect(e.message).toBe('api.contacts.add: Valid JID required');
+                }
+
+                // Create a contact that doesn't get persisted to the
+                // roster, to avoid having to mock stanzas.
+                await api.contacts.add({ jid: 'newcontact@example.org' }, false, false);
+                const contacts = await api.contacts.get();
+                expect(contacts.length).toBe(1);
+                expect(contacts[0].get('jid')).toBe('newcontact@example.org');
+                expect(contacts[0].get('subscription')).toBe(undefined);
+                expect(contacts[0].get('ask')).toBeUndefined();
+                expect(contacts[0].get('groups').length).toBe(0);
+            })
+        );
     });
     });
 
 
-    describe("The \"plugins\" API", function () {
-        it("only has a method 'add' for registering plugins", mock.initConverse((_converse) => {
-            expect(Object.keys(converse.plugins)).toEqual(["add"]);
-            // Cheating a little bit. We clear the plugins to test more easily.
-            const _old_plugins = _converse.pluggable.plugins;
-            _converse.pluggable.plugins = [];
-            converse.plugins.add('plugin1', {});
-            expect(Object.keys(_converse.pluggable.plugins)).toEqual(['plugin1']);
-            converse.plugins.add('plugin2', {});
-            expect(Object.keys(_converse.pluggable.plugins)).toEqual(['plugin1', 'plugin2']);
-            _converse.pluggable.plugins = _old_plugins;
-        }));
-
-        describe("The \"plugins.add\" method", function () {
-            it("throws an error when multiple plugins attempt to register with the same name",
-                    mock.initConverse((_converse) => {  // eslint-disable-line no-unused-vars
-
-                converse.plugins.add('myplugin', {});
-                const error = new TypeError('Error: plugin with name "myplugin" has already been registered!');
-                expect(() => converse.plugins.add('myplugin', {})).toThrow(error);
-            }));
+    describe('The "plugins" API', function () {
+        it(
+            "only has a method 'add' for registering plugins",
+            mock.initConverse((_converse) => {
+                expect(Object.keys(converse.plugins)).toEqual(['add']);
+                // Cheating a little bit. We clear the plugins to test more easily.
+                const _old_plugins = _converse.pluggable.plugins;
+                _converse.pluggable.plugins = [];
+                converse.plugins.add('plugin1', {});
+                expect(Object.keys(_converse.pluggable.plugins)).toEqual(['plugin1']);
+                converse.plugins.add('plugin2', {});
+                expect(Object.keys(_converse.pluggable.plugins)).toEqual(['plugin1', 'plugin2']);
+                _converse.pluggable.plugins = _old_plugins;
+            })
+        );
+
+        describe('The "plugins.add" method', function () {
+            it(
+                'throws an error when multiple plugins attempt to register with the same name',
+                mock.initConverse((_converse) => {
+                    // eslint-disable-line no-unused-vars
+
+                    converse.plugins.add('myplugin', {});
+                    const error = new TypeError('Error: plugin with name "myplugin" has already been registered!');
+                    expect(() => converse.plugins.add('myplugin', {})).toThrow(error);
+                })
+            );
         });
         });
     });
     });
 });
 });

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

@@ -770,11 +770,11 @@ declare class MUC extends MUC_base {
     isJoined(): Promise<boolean>;
     isJoined(): Promise<boolean>;
     /**
     /**
      * Sends a status update presence (i.e. based on the `<show>` element)
      * Sends a status update presence (i.e. based on the `<show>` element)
-     * @param {import("../status/types").presence_attrs} attrs
+     * @param {import("../status/types").PresenceAttrs} attrs
      * @param {Element[]|Builder[]|Element|Builder} [child_nodes]
      * @param {Element[]|Builder[]|Element|Builder} [child_nodes]
      *  Nodes(s) to be added as child nodes of the `presence` XML element.
      *  Nodes(s) to be added as child nodes of the `presence` XML element.
      */
      */
-    sendStatusPresence(attrs: import("../status/types").presence_attrs, child_nodes?: Element[] | import("strophe.js").Builder[] | Element | import("strophe.js").Builder): Promise<void>;
+    sendStatusPresence(attrs: import("../status/types").PresenceAttrs, child_nodes?: Element[] | import("strophe.js").Builder[] | Element | import("strophe.js").Builder): Promise<void>;
     /**
     /**
      * Check whether we're still joined and re-join if not
      * Check whether we're still joined and re-join if not
      */
      */

+ 15 - 7
src/headless/types/plugins/status/api.d.ts

@@ -2,15 +2,12 @@ declare namespace _default {
     namespace status {
     namespace status {
         /**
         /**
          * Return the current user's availability status.
          * Return the current user's availability status.
-         * @async
          * @method _converse.api.user.status.get
          * @method _converse.api.user.status.get
          * @example _converse.api.user.status.get();
          * @example _converse.api.user.status.get();
          */
          */
         function get(): Promise<any>;
         function get(): Promise<any>;
         /**
         /**
          * The user's status can be set to one of the following values:
          * The user's status can be set to one of the following values:
-         *
-         * @async
          * @method _converse.api.user.status.set
          * @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 } value The user's chat status (e.g. 'away', 'dnd', 'offline', 'online', 'unavailable' or 'xa')
          * @param { string } [message] A custom status message
          * @param { string } [message] A custom status message
@@ -21,21 +18,32 @@ declare namespace _default {
         function set(value: string, message?: string): Promise<void>;
         function set(value: string, message?: string): Promise<void>;
         namespace message {
         namespace message {
             /**
             /**
-             * @async
              * @method _converse.api.user.status.message.get
              * @method _converse.api.user.status.message.get
-             * @returns { Promise<string> } The status message
+             * @returns {Promise<string>} The status message
              * @example const message = _converse.api.user.status.message.get()
              * @example const message = _converse.api.user.status.message.get()
              */
              */
             function get(): Promise<string>;
             function get(): Promise<string>;
             /**
             /**
-             * @async
              * @method _converse.api.user.status.message.set
              * @method _converse.api.user.status.message.set
-             * @param { string } status The status message
+             * @param {string} status The status message
              * @example _converse.api.user.status.message.set('In a meeting');
              * @example _converse.api.user.status.message.set('In a meeting');
              */
              */
             function set(status: string): Promise<void>;
             function set(status: string): Promise<void>;
         }
         }
     }
     }
+    namespace idle {
+        /**
+         * @method _converse.api.user.idle.get
+         * @returns {Promise<import('./types').IdleStatus>}
+         * @example _converse.api.user.idle.get();
+         */
+        function get(): Promise<import("./types").IdleStatus>;
+        /**
+         * @method _converse.api.user.idle.set
+         * @param {import('./types').IdleStatus} status
+         */
+        function set(status: import("./types").IdleStatus): void;
+    }
 }
 }
 export default _default;
 export default _default;
 //# sourceMappingURL=api.d.ts.map
 //# sourceMappingURL=api.d.ts.map

+ 4 - 4
src/headless/types/plugins/status/profile.d.ts

@@ -145,9 +145,9 @@ export default class Profile extends Profile_base {
         groups: any[];
         groups: any[];
     };
     };
     /**
     /**
-     * @return {import('./types').connection_status}
+     * @return {import('./types').ConnectionStatus}
      */
      */
-    getStatus(): import("./types").connection_status;
+    getStatus(): import("./types").ConnectionStatus;
     /**
     /**
      * @param {string|Object} key
      * @param {string|Object} key
      * @param {string|Object} [val]
      * @param {string|Object} [val]
@@ -162,10 +162,10 @@ export default class Profile extends Profile_base {
     getNickname(): any;
     getNickname(): any;
     /**
     /**
      * Constructs a presence stanza
      * Constructs a presence stanza
-     * @param {import('./types').presence_attrs} [attrs={}]
+     * @param {import('./types').PresenceAttrs} [attrs={}]
      * @returns {Promise<Stanza>}
      * @returns {Promise<Stanza>}
      */
      */
-    constructPresence(attrs?: import("./types").presence_attrs): Promise<any>;
+    constructPresence(attrs?: import("./types").PresenceAttrs): Promise<any>;
 }
 }
 import { Model } from '@converse/skeletor';
 import { Model } from '@converse/skeletor';
 export {};
 export {};

+ 10 - 5
src/headless/types/plugins/status/types.d.ts

@@ -1,10 +1,15 @@
-export type connection_status = 'online' | 'unavailable' | 'offline';
-export type profile_show = 'dnd' | 'away' | 'xa' | 'chat';
-export type presence_attrs = {
-    type?: presence_type;
+export type ConnectionStatus = 'online' | 'unavailable' | 'offline';
+export type ProfileShow = 'dnd' | 'away' | 'xa' | 'chat';
+export type PresenceAttrs = {
+    type?: PresenceType;
     to?: string;
     to?: string;
     status?: string;
     status?: string;
     show?: string;
     show?: string;
 };
 };
-export type presence_type = 'error' | 'offline' | 'online' | 'probe' | 'subscribe' | 'unavailable' | 'unsubscribe' | 'unsubscribed';
+type PresenceType = 'error' | 'offline' | 'online' | 'probe' | 'subscribe' | 'unavailable' | 'unsubscribe' | 'unsubscribed';
+export type IdleStatus = {
+    idle?: boolean;
+    seconds?: number;
+};
+export {};
 //# sourceMappingURL=types.d.ts.map
 //# sourceMappingURL=types.d.ts.map

+ 0 - 23
src/headless/types/plugins/status/utils.d.ts

@@ -2,27 +2,4 @@
  * @param {boolean} reconnecting
  * @param {boolean} reconnecting
  */
  */
 export function initStatus(reconnecting: boolean): void;
 export function initStatus(reconnecting: boolean): void;
-export function isIdle(): boolean;
-export function getIdleSeconds(): number;
-/**
- * Resets counters and flags relating to CSI and auto_away/auto_xa
- */
-export function onUserActivity(): void;
-/**
- * An interval handler running every second.
- * Used for CSI and the auto_away and auto_xa features.
- */
-export function onEverySecond(): void;
-/**
- * Send out a Client State Indication (XEP-0352)
- * @function sendCSI
- * @param { String } stat - The user's chat status
- */
-export function sendCSI(stat: string): void;
-/**
- * Set an interval of one second and register a handler for it.
- * Required for the auto_away, auto_xa and csi_waiting_time features.
- */
-export function registerIntervalHandler(): void;
-export function tearDown(): void;
 //# sourceMappingURL=utils.d.ts.map
 //# sourceMappingURL=utils.d.ts.map

+ 2 - 2
src/headless/types/shared/api/presence.d.ts

@@ -3,11 +3,11 @@ declare namespace _default {
         /**
         /**
          * Send out a presence stanza
          * Send out a presence stanza
          * @method _converse.api.user.presence.send
          * @method _converse.api.user.presence.send
-         * @param {import('../../plugins/status/types').presence_attrs} [attrs]
+         * @param {import('../../plugins/status/types').PresenceAttrs} [attrs]
          * @param {Array<Element>|Array<Builder>|Element|Builder} [nodes]
          * @param {Array<Element>|Array<Builder>|Element|Builder} [nodes]
          *  Nodes(s) to be added as child nodes of the `presence` XML element.
          *  Nodes(s) to be added as child nodes of the `presence` XML element.
          */
          */
-        function send(attrs?: import("../../plugins/status/types").presence_attrs, nodes?: Array<Element> | Array<Builder> | Element | Builder): Promise<void>;
+        function send(attrs?: import("../../plugins/status/types").PresenceAttrs, nodes?: Array<Element> | Array<Builder> | Element | Builder): Promise<void>;
     }
     }
 }
 }
 export default _default;
 export default _default;

+ 1 - 1
src/headless/types/shared/api/user.d.ts

@@ -36,7 +36,7 @@ declare namespace api {
              * @method _converse.api.user.jid
              * @method _converse.api.user.jid
              * @returns {string} The current user's full JID (Jabber ID)
              * @returns {string} The current user's full JID (Jabber ID)
              * @example _converse.api.user.jid())
              * @example _converse.api.user.jid())
-             */).presence_attrs, nodes?: Array<Element> | Array<Builder> | Element | Builder): Promise<void>;
+             */).PresenceAttrs, nodes?: Array<Element> | Array<Builder> | Element | Builder): Promise<void>;
         };
         };
         settings: {
         settings: {
             getModel(): Promise<Model>;
             getModel(): Promise<Model>;

+ 33 - 3
src/plugins/profile/index.js

@@ -2,12 +2,17 @@
  * @copyright The Converse.js contributors
  * @copyright The Converse.js contributors
  * @license Mozilla Public License (MPLv2)
  * @license Mozilla Public License (MPLv2)
  */
  */
-import { api, converse } from '@converse/headless';
+import { _converse, api, converse } from '@converse/headless';
+import { onEverySecond, onUserActivity, registerIntervalHandler, tearDown, sendCSI } from './utils.js';
 import '../modal/index.js';
 import '../modal/index.js';
 import './modals/profile.js';
 import './modals/profile.js';
 import './modals/user-settings.js';
 import './modals/user-settings.js';
 import './statusview.js';
 import './statusview.js';
 
 
+const { Strophe } = converse.env;
+
+Strophe.addNamespace('IDLE', 'urn:xmpp:idle:1');
+
 converse.plugins.add('converse-profile', {
 converse.plugins.add('converse-profile', {
     dependencies: [
     dependencies: [
         'converse-status',
         'converse-status',
@@ -17,7 +22,32 @@ converse.plugins.add('converse-profile', {
         'converse-adhoc-views',
         'converse-adhoc-views',
     ],
     ],
 
 
-    initialize () {
-        api.settings.extend({ show_client_info: true });
+    initialize() {
+        api.settings.extend({
+            show_client_info: true,
+            auto_away: 0, // Seconds after which user status is set to 'away'
+            auto_xa: 0, // Seconds after which user status is set to 'xa'
+            csi_waiting_time: 0, // Support for XEP-0352. Seconds before client is considered idle and CSI is sent out.
+            idle_presence_timeout: 300, // Seconds after which an idle presence is sent
+        });
+
+        const exports = {
+            onUserActivity,
+            onEverySecond,
+            sendCSI,
+            registerIntervalHandler,
+        };
+        Object.assign(_converse, exports); // Deprecated
+        Object.assign(_converse.exports, exports);
+
+        if (api.settings.get('idle_presence_timeout') > 0) {
+            api.listen.on('addClientFeatures', () => api.disco.own.features.add(Strophe.NS.IDLE));
+        }
+
+        api.listen.on(
+            'presencesInitialized',
+            /** @param {boolean} reconnecting */ (reconnecting) => !reconnecting && registerIntervalHandler()
+        );
+        api.listen.on('beforeTearDown', tearDown);
     },
     },
 });
 });

+ 94 - 0
src/plugins/profile/tests/csi.js

@@ -0,0 +1,94 @@
+const { Strophe } = converse.env;
+
+describe('A chat state indication', function () {
+    it(
+        'are sent out when the client becomes or stops being idle',
+        mock.initConverse(['discoInitialized'], {}, (_converse) => {
+            let i = 0;
+            const domain = _converse.session.get('domain');
+            _converse.disco_entities.get(domain).features['urn:xmpp:csi:0'] = true; // Mock that the server supports CSI
+
+            let sent_stanza = null;
+            spyOn(_converse.api.connection.get(), 'send').and.callFake((stanza) => {
+                sent_stanza = stanza;
+            });
+
+            _converse.api.settings.set('csi_waiting_time', 3);
+            while (i <= _converse.api.settings.get('csi_waiting_time')) {
+                expect(sent_stanza).toBe(null);
+                _converse.exports.onEverySecond();
+                i++;
+            }
+            expect(Strophe.serialize(sent_stanza)).toBe('<inactive xmlns="urn:xmpp:csi:0"/>');
+            _converse.onUserActivity();
+            expect(Strophe.serialize(sent_stanza)).toBe('<active xmlns="urn:xmpp:csi:0"/>');
+        })
+    );
+});
+
+describe('Automatic status change', function () {
+    it(
+        'happens when the client is idle for long enough',
+        mock.initConverse(['chatBoxesFetched'], {}, async (_converse) => {
+            const { api } = _converse;
+            let i = 0;
+            // Usually initialized by registerIntervalHandler
+            _converse.api.settings.set('auto_away', 3);
+            _converse.api.settings.set('auto_xa', 6);
+
+            expect(await _converse.api.user.status.get()).toBe('online');
+            while (i <= _converse.api.settings.get('auto_away')) {
+                expect(await _converse.api.user.status.get()).toBe('online');
+                _converse.onEverySecond();
+                i++;
+            }
+            expect(await _converse.api.user.status.get()).toBe('away');
+
+            while (i <= api.settings.get('auto_xa')) {
+                expect(await _converse.api.user.status.get()).toBe('away');
+                _converse.onEverySecond();
+                i++;
+            }
+            expect(await _converse.api.user.status.get()).toBe('xa');
+
+            _converse.onUserActivity();
+            expect(_converse.api.user.idle.get()).toEqual({ idle: false, seconds: 0 });
+            expect(await _converse.api.user.status.get()).toBe('online');
+
+            // Check that it also works for the chat feature
+            await _converse.api.user.status.set('chat');
+            i = 0;
+            while (i <= _converse.api.settings.get('auto_away')) {
+                _converse.onEverySecond();
+                i++;
+            }
+            while (i <= api.settings.get('auto_xa')) {
+                expect(await _converse.api.user.status.get()).toBe('away');
+                _converse.onEverySecond();
+                i++;
+            }
+            expect(await _converse.api.user.status.get()).toBe('xa');
+
+            _converse.onUserActivity();
+            expect(await _converse.api.user.status.get()).toBe('online');
+
+            // Check that it doesn't work for 'dnd'
+            await _converse.api.user.status.set('dnd');
+            i = 0;
+            while (i <= _converse.api.settings.get('auto_away')) {
+                _converse.onEverySecond();
+                i++;
+            }
+            expect(await _converse.api.user.status.get()).toBe('dnd');
+            while (i <= api.settings.get('auto_xa')) {
+                expect(await _converse.api.user.status.get()).toBe('dnd');
+                _converse.onEverySecond();
+                i++;
+            }
+            expect(await _converse.api.user.status.get()).toBe('dnd');
+
+            _converse.onUserActivity();
+            expect(await _converse.api.user.status.get()).toBe('dnd');
+        })
+    );
+});

+ 133 - 2
src/plugins/profile/utils.js

@@ -1,5 +1,7 @@
 import { __ } from 'i18n';
 import { __ } from 'i18n';
-import { _converse, api } from '@converse/headless';
+import { _converse, api, converse, constants, u } from '@converse/headless';
+
+const { Strophe, $build } = converse.env;
 
 
 /**
 /**
  * @param {string} stat
  * @param {string} stat
@@ -32,6 +34,135 @@ export function shouldShowPasswordResetForm() {
     } else if (['external', 'anonymous'].includes(api.settings.get('authentication'))) {
     } else if (['external', 'anonymous'].includes(api.settings.get('authentication'))) {
         return false;
         return false;
     }
     }
-
     return true;
     return true;
 }
 }
+
+let auto_changed_status = false;
+let inactive = false;
+
+/**
+ * Send out a Client State Indication (XEP-0352)
+ * @function sendCSI
+ * @param { String } stat - The user's chat status
+ */
+export function sendCSI(stat) {
+    api.send($build(stat, { xmlns: Strophe.NS.CSI }));
+    inactive = stat === constants.INACTIVE ? true : false;
+}
+
+/**
+ * Resets counters and flags relating to CSI and auto_away/auto_xa
+ */
+export function onUserActivity() {
+    api.user.idle.set({ seconds: 0 });
+
+    if (!api.connection.get()?.authenticated) {
+        // We can't send out any stanzas when there's no authenticated connection.
+        // This can happen when the connection reconnects.
+        return;
+    }
+    if (inactive) sendCSI(constants.ACTIVE);
+
+    const { idle } = api.user.idle.get();
+    if (idle) {
+        api.user.idle.set({ idle: false });
+        api.user.presence.send();
+    }
+
+    if (auto_changed_status === true) {
+        auto_changed_status = false;
+        // XXX: we should really remember the original state here, and
+        // then set it back to that...
+        _converse.state.profile.set('show', undefined);
+    }
+}
+
+/**
+ * An interval handler running every second.
+ * Used for CSI and the auto_away and auto_xa features.
+ */
+export function onEverySecond() {
+    if (!api.connection.get()?.authenticated) {
+        // We can't send out any stanzas when there's no authenticated connection.
+        // This can happen when the connection reconnects.
+        return;
+    }
+    const { profile } = _converse.state;
+    const show = profile.get('show');
+    const idle_presence_timeout = api.settings.get('idle_presence_timeout');
+    const csi_waiting_time = api.settings.get('csi_waiting_time');
+    const idle_status = api.user.idle.get();
+    let seconds = idle_status.seconds;
+    let idle = idle_status.idle;
+
+    seconds++;
+    if (csi_waiting_time > 0 && seconds > csi_waiting_time && !inactive) {
+        sendCSI(constants.INACTIVE);
+    }
+
+    if (idle_presence_timeout > 0 && seconds > idle_presence_timeout && !idle) {
+        idle = true;
+        api.user.presence.send();
+    }
+    if (
+        api.settings.get('auto_away') > 0 &&
+        seconds > api.settings.get('auto_away') &&
+        show !== 'away' &&
+        show !== 'xa' &&
+        show !== 'dnd'
+    ) {
+        auto_changed_status = true;
+        profile.set('show', 'away');
+    } else if (
+        api.settings.get('auto_xa') > 0 &&
+        seconds > api.settings.get('auto_xa') &&
+        show !== 'xa' &&
+        show !== 'dnd'
+    ) {
+        auto_changed_status = true;
+        profile.set('show', 'xa');
+    }
+
+    api.user.idle.set({ idle, seconds });
+}
+
+let everySecondTrigger;
+
+/**
+ * Set an interval of one second and register a handler for it.
+ * Required for the auto_away, auto_xa and csi_waiting_time features.
+ */
+export function registerIntervalHandler() {
+    if (
+        api.settings.get('auto_away') < 1 &&
+        api.settings.get('auto_xa') < 1 &&
+        api.settings.get('csi_waiting_time') < 1 &&
+        api.settings.get('idle_presence_timeout') < 1
+    ) {
+        // Waiting time of less then one second means features aren't used.
+        return;
+    }
+    api.user.idle.set({ seconds: 0 });
+    auto_changed_status = false; // Was the user's status changed by Converse?
+
+    const { onUserActivity, onEverySecond } = _converse.exports;
+    window.addEventListener('click', onUserActivity);
+    window.addEventListener('focus', onUserActivity);
+    window.addEventListener('keypress', onUserActivity);
+    window.addEventListener('mousemove', onUserActivity);
+    window.addEventListener(u.getUnloadEvent(), onUserActivity, { 'once': true, 'passive': true });
+    everySecondTrigger = setInterval(onEverySecond, 1000);
+}
+
+export function tearDown() {
+    const { onUserActivity } = _converse.exports;
+    window.removeEventListener('click', onUserActivity);
+    window.removeEventListener('focus', onUserActivity);
+    window.removeEventListener('keypress', onUserActivity);
+    window.removeEventListener('mousemove', onUserActivity);
+    window.removeEventListener(u.getUnloadEvent(), onUserActivity);
+    if (everySecondTrigger) {
+        clearInterval(everySecondTrigger);
+        everySecondTrigger = null;
+    }
+}

+ 21 - 0
src/types/plugins/profile/utils.d.ts

@@ -7,4 +7,25 @@ export function getPrettyStatus(stat: string): any;
  * form.
  * form.
  */
  */
 export function shouldShowPasswordResetForm(): boolean;
 export function shouldShowPasswordResetForm(): boolean;
+/**
+ * Send out a Client State Indication (XEP-0352)
+ * @function sendCSI
+ * @param { String } stat - The user's chat status
+ */
+export function sendCSI(stat: string): void;
+/**
+ * Resets counters and flags relating to CSI and auto_away/auto_xa
+ */
+export function onUserActivity(): void;
+/**
+ * An interval handler running every second.
+ * Used for CSI and the auto_away and auto_xa features.
+ */
+export function onEverySecond(): void;
+/**
+ * Set an interval of one second and register a handler for it.
+ * Required for the auto_away, auto_xa and csi_waiting_time features.
+ */
+export function registerIntervalHandler(): void;
+export function tearDown(): void;
 //# sourceMappingURL=utils.d.ts.map
 //# sourceMappingURL=utils.d.ts.map