Przeglądaj źródła

Refactor chat status

Converse has the concept of `status` for the logged-in user and
contacts, which is a combination of the presence `type` in some
cases and the `show` value.

I was refactoring `constructPresence` to try and be more explicit and
clear about the `type` and `show` values that an outgoing presence
should have, and while doing so run into the issue that those values
were being used together as the `status`.

In the end this turned out to be a much bigger refactor to clean up this
whole mess which has been there from the very early days.
JC Brand 2 miesięcy temu
rodzic
commit
e0dc9cb35b
44 zmienionych plików z 503 dodań i 386 usunięć
  1. 1 0
      CHANGES.md
  2. 0 10
      docs/source/configuration.rst
  3. 3 3
      src/headless/plugins/caps/tests/caps.js
  4. 1 1
      src/headless/plugins/muc/message.js
  5. 225 220
      src/headless/plugins/muc/muc.js
  6. 2 1
      src/headless/plugins/muc/occupant.js
  7. 1 1
      src/headless/plugins/muc/parsers.js
  8. 16 17
      src/headless/plugins/muc/tests/muc.js
  9. 12 2
      src/headless/plugins/roster/contact.js
  10. 7 7
      src/headless/plugins/roster/contacts.js
  11. 7 2
      src/headless/plugins/roster/utils.js
  12. 20 3
      src/headless/plugins/status/api.js
  13. 0 1
      src/headless/plugins/status/plugin.js
  14. 22 15
      src/headless/plugins/status/profile.js
  15. 7 5
      src/headless/plugins/status/types.ts
  16. 13 12
      src/headless/plugins/status/utils.js
  17. 2 2
      src/headless/plugins/vcard/tests/update.js
  18. 12 13
      src/headless/shared/api/presence.js
  19. 12 0
      src/headless/shared/constants.js
  20. 5 5
      src/headless/tests/converse.js
  21. 7 8
      src/headless/types/plugins/muc/muc.d.ts
  22. 1 1
      src/headless/types/plugins/muc/occupant.d.ts
  23. 2 2
      src/headless/types/plugins/roster/contacts.d.ts
  24. 11 6
      src/headless/types/plugins/status/profile.d.ts
  25. 10 0
      src/headless/types/plugins/status/types.d.ts
  26. 4 0
      src/headless/types/plugins/status/utils.d.ts
  27. 2 0
      src/headless/types/plugins/vcard/utils.d.ts
  28. 2 4
      src/headless/types/shared/api/presence.d.ts
  29. 5 1
      src/headless/types/shared/api/user.d.ts
  30. 2 0
      src/headless/types/shared/constants.d.ts
  31. 0 3
      src/plugins/modal/styles/_modal.scss
  32. 1 1
      src/plugins/muc-views/templates/muc-occupant.js
  33. 7 2
      src/plugins/muc-views/templates/muc-occupants.js
  34. 4 5
      src/plugins/muc-views/templates/occupant.js
  35. 1 1
      src/plugins/muc-views/tests/muc.js
  36. 16 2
      src/plugins/profile/modals/chat-status.js
  37. 3 1
      src/plugins/profile/modals/profile.js
  38. 8 6
      src/plugins/profile/templates/chat-status-modal.js
  39. 8 4
      src/plugins/profile/templates/profile.js
  40. 14 13
      src/plugins/rosterview/tests/presence.js
  41. 2 2
      src/shared/tests/mock.js
  42. 12 1
      src/types/plugins/profile/modals/profile.d.ts
  43. 1 1
      src/types/plugins/profile/templates/chat-status-modal.d.ts
  44. 12 2
      src/types/shared/components/image-picker.d.ts

+ 1 - 0
CHANGES.md

@@ -105,6 +105,7 @@
 - Changed the signature of the `api.contacts.add` API method.
 - The deprecated API method `api.settings.update` has been removed. Use
   `api.settings.extend` instead.
+- Removed the `default_state` config option.
 - New config option [rtl_langs](https://conversejs.org/docs/html/configuration.html#rtl-langs) for specifying languages for which
   Converse's UI should be shown in right-to-left order.
 

+ 0 - 10
docs/source/configuration.rst

@@ -795,16 +795,6 @@ JIDs with other domains are still allowed but need to be provided in full.
 To specify only one domain and disallow other domains, see the `locked_domain`_
 option.
 
-default_state
--------------
-
-* Default: ``'online'``
-
-The default chat status that the user wil have. If you for example set this to
-``'chat'``, then Converse will send out a presence stanza with ``"show"``
-set to ``'chat'`` as soon as you've been logged in.
-
-
 discover_connection_methods
 ---------------------------
 

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

@@ -39,7 +39,7 @@ describe('A sent presence stanza', function () {
         mock.initConverse(['statusInitialized'], {}, async (_converse) => {
             const { api } = _converse;
             const { profile } = _converse.state;
-            let pres = await profile.constructPresence('online', null, 'Hello world');
+            let pres = await profile.constructPresence({ status: 'Hello world' });
             expect(pres.node).toEqualStanza(stx`
             <presence xmlns="jabber:client">
                 <status>Hello world</status>
@@ -48,7 +48,7 @@ describe('A sent presence stanza', function () {
             </presence>`);
 
             api.settings.set('priority', 2);
-            pres = await profile.constructPresence('away', null, 'Going jogging');
+            pres = await profile.constructPresence({ show: 'away', status: 'Going jogging' });
             expect(pres.node).toEqualStanza(stx`
             <presence xmlns="jabber:client">
                 <show>away</show>
@@ -58,7 +58,7 @@ describe('A sent presence stanza', function () {
             </presence>`);
 
             api.settings.set('priority', undefined);
-            pres = await profile.constructPresence('dnd', null, 'Doing taxes');
+            pres = await profile.constructPresence({ show: 'dnd', status: 'Doing taxes' });
             expect(pres.node).toEqualStanza(stx`
             <presence xmlns="jabber:client">
                 <show>dnd</show>

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

@@ -113,7 +113,7 @@ class MUCMessage extends BaseMessage {
 
                 if (api.settings.get('muc_send_probes')) {
                     const jid = `${this.chatbox.get('jid')}/${nick}`;
-                    api.user.presence.send('probe', jid);
+                    api.user.presence.send({ to: jid, type: 'probe' });
                 }
             }
         }

Plik diff jest za duży
+ 225 - 220
src/headless/plugins/muc/muc.js


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

@@ -36,7 +36,8 @@ class MUCOccupant extends ModelWithVCard(ModelWithMessages(ColorAwareModel(Model
     defaults() {
         return {
             hats: [],
-            show: "offline",
+            presence: 'offline',
+            show: undefined,
             states: [],
             hidden: true,
             num_unread: 0,

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

@@ -425,7 +425,7 @@ export async function parseMUCPresence(stanza, chatbox) {
         muc_jid: Strophe.getBareJidFromJid(from),
         occupant_id: getOccupantID(stanza, chatbox),
         status: stanza.querySelector(':scope > status')?.textContent ?? undefined,
-        show: stanza.querySelector(':scope > show')?.textContent ?? (type !== 'unavailable' ? 'online' : 'offline'),
+        show: stanza.querySelector(':scope > show')?.textContent ?? undefined,
         image_hash: sizzle(`presence > x[xmlns="${Strophe.NS.VCARDUPDATE}"] photo`, stanza).pop()?.textContent,
         hats: sizzle(`presence > hats[xmlns="${Strophe.NS.MUC_HATS}"] hat`, stanza).map(
             /** @param {Element} h */ (h) => ({

+ 16 - 17
src/headless/plugins/muc/tests/muc.js

@@ -46,7 +46,7 @@ describe("Groupchats", function () {
 
             const { profile } = _converse.state;
             const muc_jid = 'coven@chat.shakespeare.lit';
-            profile.set('status', 'away');
+            profile.set('show', 'away');
 
             const sent_stanzas = _converse.api.connection.get().sent_stanzas;
             while (sent_stanzas.length) sent_stanzas.pop();
@@ -65,31 +65,30 @@ describe("Groupchats", function () {
 
             while (sent_stanzas.length) sent_stanzas.pop();
 
-            profile.set('status', 'xa');
+            profile.set('show', 'xa');
             pres = await u.waitUntil(() => sent_stanzas.filter(s => s.nodeName === 'presence' && s.getAttribute('to') === `${muc_jid}/romeo`).pop());
 
-            expect(Strophe.serialize(pres)).toBe(
-                `<presence to="${muc_jid}/romeo" xmlns="jabber:client">`+
-                    `<show>xa</show>`+
-                    `<priority>0</priority>`+
-                    `<c hash="sha-1" node="https://conversejs.org" ver="TfHz9vOOfqIG0Z9lW5CuPaWGnrQ=" xmlns="http://jabber.org/protocol/caps"/>`+
-                `</presence>`)
+            expect(pres).toEqualStanza(stx`
+                <presence to="${muc_jid}/romeo" xmlns="jabber:client">
+                    <show>xa</show>
+                    <priority>0</priority>
+                    <c hash="sha-1" node="https://conversejs.org" ver="TfHz9vOOfqIG0Z9lW5CuPaWGnrQ=" xmlns="http://jabber.org/protocol/caps"/>
+                </presence>`)
 
-            profile.set('status', 'dnd');
-            profile.set('status_message', 'Do not disturb');
+            profile.set({ show: 'dnd', status_message: 'Do not disturb' });
             while (sent_stanzas.length) sent_stanzas.pop();
 
             const muc2_jid = 'cave@chat.shakespeare.lit';
             const muc2 = await mock.openAndEnterMUC(_converse, muc2_jid, 'romeo');
 
             pres = await u.waitUntil(() => sent_stanzas.filter(s => s.nodeName === 'presence').pop());
-            expect(Strophe.serialize(pres)).toBe(
-                `<presence from="${_converse.jid}" id="${pres.getAttribute('id')}" to="${muc2_jid}/romeo" xmlns="jabber:client">`+
-                    `<x xmlns="http://jabber.org/protocol/muc"><history maxstanzas="0"/></x>`+
-                    `<show>dnd</show>`+
-                    `<status>Do not disturb</status>`+
-                    `<c hash="sha-1" node="https://conversejs.org" ver="TfHz9vOOfqIG0Z9lW5CuPaWGnrQ=" xmlns="http://jabber.org/protocol/caps"/>`+
-                `</presence>`);
+            expect(pres).toEqualStanza(stx`
+                <presence from="${_converse.jid}" id="${pres.getAttribute('id')}" to="${muc2_jid}/romeo" xmlns="jabber:client">
+                    <x xmlns="http://jabber.org/protocol/muc"><history maxstanzas="0"/></x>
+                    <show>dnd</show>
+                    <status>Do not disturb</status>
+                    <c hash="sha-1" node="https://conversejs.org" ver="TfHz9vOOfqIG0Z9lW5CuPaWGnrQ=" xmlns="http://jabber.org/protocol/caps"/>
+                </presence>`);
 
             expect(muc2.getOwnOccupant().get('show')).toBe('dnd');
 

+ 12 - 2
src/headless/plugins/roster/contact.js

@@ -44,6 +44,8 @@ class RosterContact extends ModelWithVCard(ColorAwareModel(Model)) {
          */
         this.listenTo(this.presence, 'change:show', () => api.trigger('contactPresenceChanged', this));
         this.listenTo(this.presence, 'change:show', () => this.trigger('presenceChanged'));
+        this.listenTo(this.presence, 'change:presence', () => api.trigger('contactPresenceChanged', this));
+        this.listenTo(this.presence, 'change:presence', () => this.trigger('presenceChanged'));
         /**
          * Synchronous event which provides a hook for further initializing a RosterContact
          * @event _converse#rosterContactInitialized
@@ -60,7 +62,11 @@ class RosterContact extends ModelWithVCard(ColorAwareModel(Model)) {
     }
 
     getStatus () {
-        return this.presence.get('show') || 'offline';
+        const presence  = this.presence.get('presence');
+        if (presence === 'offline' || presence === 'unavailable') {
+            return 'offline';
+        }
+        return this.presence.get('show') || presence || 'offline';
     }
 
     openChat () {
@@ -81,7 +87,11 @@ class RosterContact extends ModelWithVCard(ColorAwareModel(Model)) {
      *      reason for the subscription request.
      */
     subscribe (message) {
-        api.user.presence.send('subscribe', this.get('jid'), message);
+        api.user.presence.send({
+            type: 'subscribe',
+            to: this.get('jid'),
+            status: message
+        });
         this.save('ask', "subscribe"); // ask === 'subscribe' Means we have asked to subscribe to them.
         return this;
     }

+ 7 - 7
src/headless/plugins/roster/contacts.js

@@ -412,12 +412,12 @@ class RosterContacts extends Collection {
     }
 
     /**
-     * @param {Element} presence
+     * @param {Element} stanza
      */
-    handleOwnPresence (presence) {
-        const jid = presence.getAttribute('from');
+    handleOwnPresence (stanza) {
+        const jid = stanza.getAttribute('from');
         const resource = Strophe.getResourceFromJid(jid);
-        const presence_type = presence.getAttribute('type');
+        const presence_type = stanza.getAttribute('type');
         const { profile } = _converse.state;
 
         if ((api.connection.get().jid !== jid) &&
@@ -427,10 +427,10 @@ class RosterContacts extends Collection {
             // Another resource has changed its status and
             // synchronize_availability option set to update,
             // we'll update ours as well.
-            const show = presence.querySelector('show')?.textContent || 'online';
-            profile.save({ 'status': show }, { silent: true });
+            const show = stanza.querySelector('show')?.textContent;
+            profile.save({ show, presence: 'online' }, { silent: true });
 
-            const status_message = presence.querySelector('status')?.textContent;
+            const status_message = stanza.querySelector('status')?.textContent;
             if (status_message) profile.save({ status_message });
         }
         if (_converse.session.get('jid') === jid && presence_type === 'unavailable') {

+ 7 - 2
src/headless/plugins/roster/utils.js

@@ -66,14 +66,18 @@ async function populateRoster(ignore_cache = false) {
     } catch (reason) {
         log.error(reason);
     } finally {
-        connection.send_initial_presence && api.user.presence.send();
+        if (connection.send_initial_presence) {
+            api.user.presence.send();
+            _converse.state.profile.save({ presence: 'online' });
+
+        }
     }
 }
 
 function updateUnreadCounter(chatbox) {
     const roster = /** @type {RosterContacts} */ (_converse.state.roster);
     const contact = roster?.get(chatbox.get('jid'));
-    contact?.save({ 'num_unread': chatbox.get('num_unread') });
+    contact?.save({ num_unread: chatbox.get('num_unread') });
 }
 
 let presence_ref;
@@ -82,6 +86,7 @@ function registerPresenceHandler() {
     unregisterPresenceHandler();
     const connection = api.connection.get();
     presence_ref = connection.addHandler(
+        /** @param {Element} presence */
         (presence) => {
             const roster = /** @type {RosterContacts} */ (_converse.state.roster);
             roster.presenceHandler(presence);

+ 20 - 3
src/headless/plugins/status/api.js

@@ -1,6 +1,6 @@
 import api from '../../shared/api/index.js';
 import _converse from '../../shared/_converse.js';
-import { STATUS_WEIGHTS } from '../../shared/constants';
+import { PRES_SHOW_VALUES, PRES_TYPE_VALUES, STATUS_WEIGHTS } from '../../shared/constants';
 
 
 export default {
@@ -18,7 +18,16 @@ export default {
          */
         async get () {
             await api.waitUntil('statusInitialized');
-            return _converse.state.profile.get('status');
+
+            const show = _converse.state.profile.get('show');
+            if (show) {
+                return show;
+            }
+            const status = _converse.state.profile.get('status');
+            if (!status) {
+                return 'online';
+            }
+            return status;
         },
 
         /**
@@ -33,12 +42,20 @@ export default {
          * @example _converse.api.user.status.set('dnd', 'In a meeting');
          */
         async set (value, message) {
-            const data = {'status': value};
             if (!Object.keys(STATUS_WEIGHTS).includes(value)) {
                 throw new Error(
                     'Invalid availability value. See https://xmpp.org/rfcs/rfc3921.html#rfc.section.2.2.2.1'
                 );
             }
+
+            let show = PRES_SHOW_VALUES.includes(value) ? value : undefined;
+            if (value === 'away') {
+                show = 'dnd';
+            }
+
+            const type = PRES_TYPE_VALUES.includes(value) ? value : undefined;
+            const data = { show, type };
+
             if (typeof message === 'string') {
                 data.status_message = message;
             }

+ 0 - 1
src/headless/plugins/status/plugin.js

@@ -26,7 +26,6 @@ converse.plugins.add('converse-status', {
             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.
-            default_state: 'online',
             idle_presence_timeout: 300, // Seconds after which an idle presence is sent
             priority: 0,
         });

+ 22 - 15
src/headless/plugins/status/profile.js

@@ -11,13 +11,22 @@ const { Stanza, Strophe, stx } = converse.env;
 export default class Profile extends ModelWithVCard(ColorAwareModel(Model)) {
     defaults() {
         return {
-            status: api.settings.get('default_state'),
+            presence: 'online',
+            status: null,
+            show: null,
             groups: [],
         };
     }
 
-    getStatus() {
-        return this.get('status');
+    /**
+     * @return {import('./types').connection_status}
+     */
+    getStatus () {
+        const presence  = this.get('presence');
+        if (presence === 'offline' || presence === 'unavailable') {
+            return 'offline';
+        }
+        return this.get('show') || presence || 'offline';
     }
 
     /**
@@ -47,11 +56,11 @@ export default class Profile extends ModelWithVCard(ColorAwareModel(Model)) {
     initialize() {
         super.initialize();
         this.on('change', (item) => {
-            if (!(item.changed instanceof Object)) {
-                return;
-            }
-            if ('status' in item.changed || 'status_message' in item.changed) {
-                api.user.presence.send(this.get('status'), null, this.get('status_message'));
+            if (item.changed?.status || item.changed?.status_message || item.changed?.show) {
+                api.user.presence.send({
+                    show: this.get('show'),
+                    status: this.get('status_message'),
+                });
             }
         });
     }
@@ -72,16 +81,14 @@ export default class Profile extends ModelWithVCard(ColorAwareModel(Model)) {
     /**
      * Constructs a presence stanza
      * @param {import('./types').presence_attrs} [attrs={}]
+     * @returns {Promise<Stanza>}
      */
     async constructPresence(attrs = {}) {
-        debugger;
-        const type =
-            typeof attrs.type === 'string' ? attrs.type : this.get('status') || api.settings.get('default_state');
-        const status = typeof attrs.status === 'string' ? attrs.status : this.get('status_message');
-        const include_nick = status === 'subscribe';
-        const { show, to } = attrs;
-
+        const { type, to } = attrs;
         const { profile } = _converse.state;
+        const status = typeof attrs.status === 'string' ? attrs.status : this.get('status_message');
+        const show = attrs.show || this.get('status');
+        const include_nick = type === 'subscribe';
         const nick = include_nick ? profile.getNickname() : null;
         const priority = api.settings.get('priority');
 

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

@@ -1,10 +1,12 @@
+export type connection_status = 'online' | 'unavailable' | 'offline';
+export type profile_show = 'dnd' | 'away' | 'xa' | 'chat';
 
 export type presence_attrs = {
-    type?: presence_type
-    to?: string
-    status?: string
-    show?: string
-}
+    type?: presence_type;
+    to?: string;
+    status?: string;
+    show?: string;
+};
 
 export type presence_type =
     | 'error'

+ 13 - 12
src/headless/plugins/status/utils.js

@@ -79,21 +79,22 @@ export function onUserActivity() {
         auto_changed_status = false;
         // XXX: we should really remember the original state here, and
         // then set it back to that...
-        _converse.state.profile.set('status', api.settings.get('default_state'));
+        _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() {
-    /* An interval handler running every second.
-     * Used for CSI and the auto_away and auto_xa features.
-     */
     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 stat = profile.get('status');
+    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);
@@ -109,20 +110,20 @@ export function onEverySecond() {
     if (
         api.settings.get('auto_away') > 0 &&
         idle_seconds > api.settings.get('auto_away') &&
-        stat !== 'away' &&
-        stat !== 'xa' &&
-        stat !== 'dnd'
+        show !== 'away' &&
+        show !== 'xa' &&
+        show !== 'dnd'
     ) {
         auto_changed_status = true;
-        profile.set('status', 'away');
+        profile.set('show', 'away');
     } else if (
         api.settings.get('auto_xa') > 0 &&
         idle_seconds > api.settings.get('auto_xa') &&
-        stat !== 'xa' &&
-        stat !== 'dnd'
+        show !== 'xa' &&
+        show !== 'dnd'
     ) {
         auto_changed_status = true;
-        profile.set('status', 'xa');
+        profile.set('show', 'xa');
     }
 }
 

+ 2 - 2
src/headless/plugins/vcard/tests/update.js

@@ -24,7 +24,7 @@ describe('A VCard', function () {
                         </presence>`
                 )
             );
-            const sent_stanza = await u.waitUntil(() => IQ_stanzas.filter((s) => sizzle('vCard', s).length).pop(), 500);
+            const sent_stanza = await u.waitUntil(() => IQ_stanzas.filter((s) => sizzle('vCard', s).length).pop(), 1000);
             expect(sent_stanza).toEqualStanza(stx`
                 <iq type="get"
                         to="mercutio@montague.lit"
@@ -102,7 +102,7 @@ describe('A VCard', function () {
             const IQ_stanzas = _converse.api.connection.get().IQ_stanzas;
             let sent_stanza = await u.waitUntil(() =>
                 IQ_stanzas.filter((s) => sizzle(`vCard`, s).length).pop()
-            );
+            , 500);
             _converse.api.connection.get()._dataRecv(
                 mock.createRequest(stx`
                 <iq from='${own_jid}'

+ 12 - 13
src/headless/shared/api/presence.js

@@ -21,13 +21,11 @@ export default {
         /**
          * Send out a presence stanza
          * @method _converse.api.user.presence.send
-         * @param {import('../../plugins/status/types').presence_type} [type]
-         * @param {String} [to]
-         * @param {String} [status] - An optional status message
+         * @param {import('../../plugins/status/types').presence_attrs} [attrs]
          * @param {Array<Element>|Array<Builder>|Element|Builder} [nodes]
          *  Nodes(s) to be added as child nodes of the `presence` XML element.
          */
-        async send (type, to, status, nodes) {
+        async send(attrs, nodes) {
             await waitUntil('statusInitialized');
 
             let children = [];
@@ -35,15 +33,16 @@ export default {
                 children = Array.isArray(nodes) ? nodes : [nodes];
             }
 
-            const model = /** @type {Profile} */(_converse.state.profile);
-            const presence = await model.constructPresence({ type, to, status });
-            children.map(c => c?.tree() ?? c).forEach(c => presence.cnode(c).up());
+            const model = /** @type {Profile} */ (_converse.state.profile);
+            const presence = await model.constructPresence(attrs);
+            children.map((c) => c?.tree() ?? c).forEach((c) => presence.cnode(c).up());
             send(presence);
 
-            if (['away', 'chat', 'dnd', 'online', 'xa', undefined].includes(type)) {
-                const mucs = /** @type {MUC[]} */(await rooms.get());
-                mucs.forEach(muc => muc.sendStatusPresence(type, status, children));
+            const { show, type } = attrs || {};
+            if (show || !type) {
+                const mucs = /** @type {MUC[]} */ (await rooms.get());
+                mucs.forEach((muc) => muc.sendStatusPresence(attrs, children));
             }
-        }
-    }
-}
+        },
+    },
+};

+ 12 - 0
src/headless/shared/constants.js

@@ -3,6 +3,18 @@ import { Strophe } from 'strophe.js';
 export const BOSH_WAIT = 59;
 export const VERSION_NAME = "v11.0.0";
 
+export const PRES_SHOW_VALUES = ['chat', 'dnd', 'away', 'xa'];
+export const PRES_TYPE_VALUES = [
+    'available',
+    'unavailable',
+    'error',
+    'probe',
+    'subscribe',
+    'subscribed',
+    'unsubscribe',
+    'unsubscribed',
+];
+
 export const STATUS_WEIGHTS = {
     offline: 6,
     unavailable: 5,

+ 5 - 5
src/headless/tests/converse.js

@@ -110,13 +110,13 @@ describe("Converse", function() {
             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('status')).toBe('away');
+                expect(await profile.get('show')).toBe('dnd');
                 await _converse.api.user.status.set('dnd');
-                expect(await profile.get('status')).toBe('dnd');
+                expect(await profile.get('show')).toBe('dnd');
                 await _converse.api.user.status.set('xa');
-                expect(await profile.get('status')).toBe('xa');
+                expect(await profile.get('show')).toBe('xa');
                 await _converse.api.user.status.set('chat');
-                expect(await profile.get('status')).toBe('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');
@@ -126,7 +126,7 @@ describe("Converse", function() {
             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('status')).toBe('away');
+                expect(profile.get('show')).toBe('dnd');
                 expect(profile.get('status_message')).toBe("I'm in a meeting");
             }));
 

+ 7 - 8
src/headless/types/plugins/muc/muc.d.ts

@@ -377,7 +377,7 @@ declare class MUC extends MUC_base {
     /**
      * @param {MUCOccupant} occupant
      */
-    onOccupantShowChanged(occupant: import("./occupant.js").default): void;
+    onOccupantPresenceChanged(occupant: import("./occupant.js").default): void;
     onRoomEntered(): Promise<void>;
     onConnectionStatusChanged(): Promise<void>;
     getMessagesCollection(): any;
@@ -763,12 +763,11 @@ declare class MUC extends MUC_base {
     isJoined(): Promise<boolean>;
     /**
      * Sends a status update presence (i.e. based on the `<show>` element)
-     * @param {String} type
-     * @param {String} [status] - An optional status message
+     * @param {import("../status/types").presence_attrs} attrs
      * @param {Element[]|Builder[]|Element|Builder} [child_nodes]
      *  Nodes(s) to be added as child nodes of the `presence` XML element.
      */
-    sendStatusPresence(type: string, status?: string, child_nodes?: Element[] | import("strophe.js").Builder[] | Element | import("strophe.js").Builder): Promise<void>;
+    sendStatusPresence(attrs: import("../status/types").presence_attrs, 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
      */
@@ -933,8 +932,8 @@ declare class MUC extends MUC_base {
     incrementUnreadMsgsCounter(message: import("../../shared/message.js").default<any>): void;
     clearUnreadMsgCounter(): Promise<void>;
 }
-import { Model } from "@converse/skeletor";
-import ChatBoxBase from "../../shared/chatbox";
-import MUCSession from "./session";
-import { TimeoutError } from "../../shared/errors.js";
+import { Model } from '@converse/skeletor';
+import ChatBoxBase from '../../shared/chatbox';
+import MUCSession from './session';
+import { TimeoutError } from '../../shared/errors.js';
 //# sourceMappingURL=muc.d.ts.map

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

@@ -278,7 +278,7 @@ declare class MUCOccupant extends MUCOccupant_base {
     initialize(): Promise<void>;
     defaults(): {
         hats: any[];
-        show: string;
+        show: any;
         states: any[];
         hidden: boolean;
         num_unread: number;

+ 2 - 2
src/headless/types/plugins/roster/contacts.d.ts

@@ -87,9 +87,9 @@ declare class RosterContacts extends Collection {
      */
     handleIncomingSubscription(presence: Element): void;
     /**
-     * @param {Element} presence
+     * @param {Element} stanza
      */
-    handleOwnPresence(presence: Element): void;
+    handleOwnPresence(stanza: Element): void;
     /**
      * @param {Element} presence
      */

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

@@ -139,9 +139,15 @@ declare const Profile_base: {
 } & typeof Model;
 export default class Profile extends Profile_base {
     defaults(): {
+        presence: string;
         status: any;
+        show: any;
+        groups: any[];
     };
-    getStatus(): any;
+    /**
+     * @return {import('./types').connection_status}
+     */
+    getStatus(): import("./types").connection_status;
     /**
      * @param {string|Object} key
      * @param {string|Object} [val]
@@ -154,12 +160,11 @@ export default class Profile extends Profile_base {
      */
     getDisplayName(options?: import("../roster/types.js").ContactDisplayNameOptions): any;
     getNickname(): any;
-    /** Constructs a presence stanza
-     * @param {string} [type]
-     * @param {string} [to] - The JID to which this presence should be sent
-     * @param {string} [status_message]
+    /**
+     * Constructs a presence stanza
+     * @param {import('./types').presence_attrs} [attrs={}]
      */
-    constructPresence(type?: string, to?: string, status_message?: string): Promise<any>;
+    constructPresence(attrs?: import("./types").presence_attrs): Promise<any>;
 }
 import { Model } from '@converse/skeletor';
 export {};

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

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

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

@@ -8,6 +8,10 @@ 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)

+ 2 - 0
src/headless/types/plugins/vcard/utils.d.ts

@@ -35,6 +35,8 @@ export function fetchVCard(jid: string): Promise<import("./types").VCardResult |
     error: any;
     vcard_error: string;
 }>;
+export function unregisterPresenceHandler(): void;
+export function registerPresenceHandler(): void;
 export type MUCMessage = import("../../plugins/muc/message").default;
 export type Profile = import("../../plugins/status/profile").default;
 export type VCards = import("../../plugins/vcard/vcards").default;

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

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

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

@@ -32,7 +32,11 @@ declare namespace api {
          */
         logout(): Promise<any>;
         presence: {
-            send(type?: string, to?: string, status?: string, nodes?: Array<Element> | Array<Builder> | Element | Builder): Promise<void>;
+            send(attrs?: import("../../plugins/status/types.js" /**
+             * @method _converse.api.user.jid
+             * @returns {string} The current user's full JID (Jabber ID)
+             * @example _converse.api.user.jid())
+             */).presence_attrs, nodes?: Array<Element> | Array<Builder> | Element | Builder): Promise<void>;
         };
         settings: {
             getModel(): Promise<Model>;

+ 2 - 0
src/headless/types/shared/constants.d.ts

@@ -1,5 +1,7 @@
 export const BOSH_WAIT: 59;
 export const VERSION_NAME: "v11.0.0";
+export const PRES_SHOW_VALUES: string[];
+export const PRES_TYPE_VALUES: string[];
 export namespace STATUS_WEIGHTS {
     let offline: number;
     let unavailable: number;

+ 0 - 3
src/plugins/modal/styles/_modal.scss

@@ -142,9 +142,6 @@ $prefix: 'converse-';
 
         .set-xmpp-status {
             margin: 1em;
-            .custom-control-label {
-                padding-top: 0.25em;
-            }
         }
 
         #omemo-tabpanel {

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

@@ -52,7 +52,7 @@ export default (el) => {
         </div>
 
         ${el.model
-            ? html` <div class="row">
+            ? html`<div class="row">
                   <div class="col">
                       <converse-avatar
                           .model=${el.model}

+ 7 - 2
src/plugins/muc-views/templates/muc-occupants.js

@@ -21,8 +21,13 @@ function isOccupantFiltered (el, occ) {
     if (!q) return false;
 
     if (type === 'state') {
-        const show = occ.get('show');
-        return q === 'online' ? ["offline", "unavailable"].includes(show) : !show.includes(q);
+        const presence = occ.get('presence');
+        if (q === 'online') {
+            return ["offline", "unavailable"].includes(presence);
+        } else if (q === 'ofline') {
+            return presence === 'online';
+        }
+        return !occ.get('show')?.includes(q);
     } else if (type === 'items')  {
         return !occ.getDisplayName().toLowerCase().includes(q);
     }

+ 4 - 5
src/plugins/muc-views/templates/occupant.js

@@ -150,17 +150,16 @@ async function tplActionButtons(o) {
  */
 export default (el) => {
     const o = el.model;
-    const affiliation = o.get('affiliation');
-    const hint_show = PRETTY_CHAT_STATUS[o.get('show')];
+    const { show, presence, affiliation } = el.model.attributes;
+    const hint_show = PRETTY_CHAT_STATUS[show || presence];
     const role = o.get('role');
 
-    const show = o.get('show');
     let classes, color;
-    if (show === 'online') {
+    if (show === 'chat' || (!show && presence === 'online')) {
         [classes, color] = ['fa fa-circle', 'chat-status-online'];
     } else if (show === 'dnd') {
         [classes, color] = ['fa fa-minus-circle', 'chat-status-busy'];
-    } else if (show === 'away') {
+    } else if (show === 'away' || show === 'xa') {
         [classes, color] = ['fa fa-circle', 'chat-status-away'];
     } else {
         [classes, color] = ['fa fa-circle', 'chat-status-offline'];

+ 1 - 1
src/plugins/muc-views/tests/muc.js

@@ -805,7 +805,7 @@ describe("Groupchats", function () {
                 "some1, newgirl and nomorenicks have entered the groupchat\nnewguy and insider have left the groupchat");
 
             expect(view.model.occupants.length).toBe(5);
-            expect(view.model.occupants.findWhere({'jid': 'insider@montague.lit'}).get('show')).toBe('offline');
+            expect(view.model.occupants.findWhere({'jid': 'insider@montague.lit'}).get('presence')).toBe('offline');
 
             // New girl leaves
             presence = $pres({

+ 16 - 2
src/plugins/profile/modals/chat-status.js

@@ -28,6 +28,9 @@ export default class ChatStatusModal extends BaseModal {
         return __('Change chat status');
     }
 
+    /**
+     * @param {MouseEvent} ev
+     */
     clearStatusMessage(ev) {
         if (ev && ev.preventDefault) {
             ev.preventDefault();
@@ -37,12 +40,23 @@ export default class ChatStatusModal extends BaseModal {
         roster_filter.value = '';
     }
 
+    /**
+     * @param {SubmitEvent} ev
+     */
     onFormSubmitted(ev) {
         ev.preventDefault();
-        const data = new FormData(ev.target);
+        const data = new FormData(/** @type {HTMLFormElement} */ (ev.target));
+        let show, presence;
+        const chat_status = data.get('chat_status');
+        if (chat_status === 'online') {
+            presence = 'online';
+        } else {
+            show = chat_status;
+        }
         this.model.save({
             status_message: data.get('status_message'),
-            status: data.get('chat_status'),
+            presence,
+            show,
         });
         this.close();
     }

+ 3 - 1
src/plugins/profile/modals/profile.js

@@ -1,3 +1,4 @@
+import { Model } from '@converse/skeletor';
 import { _converse, api, log } from "@converse/headless";
 import { compressImage, isImageWithAlphaChannel } from 'utils/file.js';
 import BaseModal from "plugins/modal/modal.js";
@@ -15,7 +16,8 @@ export default class ProfileModal extends BaseModal {
      */
 
     static properties = {
-        _submitting: { state: true }
+        _submitting: { state: true },
+        model: { type: Model },
     }
 
     /**

+ 8 - 6
src/plugins/profile/templates/chat-status-modal.js

@@ -1,7 +1,9 @@
 import { html } from "lit";
 import { __ } from 'i18n';
 
-
+/**
+ * @param {import('../modals/chat-status').default} el
+ */
 export default (el) => {
     const label_away = __('Away');
     const label_busy = __('Busy');
@@ -9,32 +11,32 @@ export default (el) => {
     const label_save = __('Save');
     const label_xa = __('Away for long');
     const placeholder_status_message = __('Personal status message');
-    const status = el.model.get('status');
+    const status = el.model.get('show') || el.model.get('presence');
     const status_message = el.model.get('status_message');
 
     return html`
     <form class="converse-form set-xmpp-status" id="set-xmpp-status" @submit=${ev => el.onFormSubmitted(ev)}>
         <div>
             <div class="custom-control custom-radio">
-                <input ?checked=${status === 'online'}
+                <input ?checked="${status === 'online'}"
                     type="radio" id="radio-online" value="online" name="chat_status" class="custom-control-input"/>
                 <label class="custom-control-label" for="radio-online">
                     <converse-icon size="1em" class="fa fa-circle chat-status chat-status--online"></converse-icon>${label_online}</label>
             </div>
             <div class="custom-control custom-radio">
-                <input ?checked=${status === 'busy'}
+                <input ?checked="${status === 'busy'}"
                     type="radio" id="radio-busy" value="dnd" name="chat_status" class="custom-control-input"/>
                 <label class="custom-control-label" for="radio-busy">
                     <converse-icon size="1em" class="fa fa-minus-circle  chat-status chat-status--busy"></converse-icon>${label_busy}</label>
             </div>
             <div class="custom-control custom-radio">
-                <input ?checked=${status === 'away'}
+                <input ?checked="${status === 'away'}"
                     type="radio" id="radio-away" value="away" name="chat_status" class="custom-control-input"/>
                 <label class="custom-control-label" for="radio-away">
                     <converse-icon size="1em" class="fa fa-circle chat-status chat-status--away"></converse-icon>${label_away}</label>
             </div>
             <div class="custom-control custom-radio">
-                <input ?checked=${status === 'xa'}
+                <input ?checked="${status === 'xa'}"
                     type="radio" id="radio-xa" value="xa" name="chat_status" class="custom-control-input"/>
                 <label class="custom-control-label" for="radio-xa">
                     <converse-icon size="1em" class="far fa-circle chat-status chat-status--xa"></converse-icon>${label_xa}</label>

+ 8 - 4
src/plugins/profile/templates/profile.js

@@ -8,19 +8,23 @@ import { getPrettyStatus } from '../utils.js';
  * @param {import('../statusview').default} el
  */
 export default (el) => {
-    const chat_status = el.model.get('status') || 'offline';
+    const show = el.model.get('show');
+    const presence = el.model.get('presence') || 'online';
+    const chat_status = show || presence;
     const status_message = el.model.get('status_message') || __("I am %1$s", getPrettyStatus(chat_status));
     const i18n_change_status = __('Click to change your chat status');
+
     let classes, color;
-    if (chat_status === 'online') {
+    if (show === 'chat' || (!show && presence === 'online')) {
         [classes, color] = ['fa fa-circle chat-status', 'chat-status-online'];
-    } else if (chat_status === 'dnd') {
+    } else if (show === 'dnd') {
         [classes, color] =  ['fa fa-minus-circle chat-status', 'chat-status-busy'];
-    } else if (chat_status === 'away') {
+    } else if (show === 'away' || show === 'xa') {
         [classes, color] =  ['fa fa-circle chat-status', 'chat-status-away'];
     } else {
         [classes, color] = ['fa fa-circle chat-status', 'comment'];
     }
+
     return html`
         <div class="userinfo">
             <div class="controlbox-section profile d-flex">

+ 14 - 13
src/plugins/rosterview/tests/presence.js

@@ -4,6 +4,7 @@ const original_timeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
 
 describe("A sent presence stanza", function () {
 
+    beforeAll(() => jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza }));
     beforeEach(() => (jasmine.DEFAULT_TIMEOUT_INTERVAL = 7000));
     afterEach(() => (jasmine.DEFAULT_TIMEOUT_INTERVAL = original_timeout));
 
@@ -25,12 +26,12 @@ describe("A sent presence stanza", function () {
 
         const sent_stanzas = _converse.api.connection.get().sent_stanzas;
         let sent_presence = await u.waitUntil(() => sent_stanzas.filter(s => Strophe.serialize(s).match('presence')).pop());
-        expect(Strophe.serialize(sent_presence))
-            .toBe(`<presence xmlns="jabber:client">`+
-                    `<status>My custom status</status>`+
-                    `<priority>0</priority>`+
-                    `<c hash="sha-1" node="https://conversejs.org" ver="TfHz9vOOfqIG0Z9lW5CuPaWGnrQ=" xmlns="http://jabber.org/protocol/caps"/>`+
-                    `</presence>`)
+        expect(sent_presence).toEqualStanza(stx`
+            <presence xmlns="jabber:client">
+                <status>My custom status</status>
+                <priority>0</priority>
+                <c hash="sha-1" node="https://conversejs.org" ver="TfHz9vOOfqIG0Z9lW5CuPaWGnrQ=" xmlns="http://jabber.org/protocol/caps"/>
+            </presence>`)
         await u.waitUntil(() => modal.getAttribute('aria-hidden') === "true");
         await u.waitUntil(() => !u.isVisible(modal));
 
@@ -43,12 +44,12 @@ describe("A sent presence stanza", function () {
         await u.waitUntil(() => sent_stanzas.filter(s => Strophe.serialize(s).match('presence')).length === 2);
         sent_presence = sent_stanzas.filter(s => Strophe.serialize(s).match('presence')).pop();
         expect(Strophe.serialize(sent_presence))
-            .toBe(
-                `<presence xmlns="jabber:client">`+
-                    `<show>dnd</show>`+
-                    `<status>My custom status</status>`+
-                    `<priority>0</priority>`+
-                    `<c hash="sha-1" node="https://conversejs.org" ver="TfHz9vOOfqIG0Z9lW5CuPaWGnrQ=" xmlns="http://jabber.org/protocol/caps"/>`+
-                `</presence>`)
+            .toEqualStanza(stx`
+                <presence xmlns="jabber:client">
+                    <show>dnd</show>
+                    <status>My custom status</status>
+                    <priority>0</priority>
+                    <c hash="sha-1" node="https://conversejs.org" ver="TfHz9vOOfqIG0Z9lW5CuPaWGnrQ=" xmlns="http://jabber.org/protocol/caps"/>
+                </presence>`)
     }));
 });

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

@@ -387,8 +387,8 @@ async function receiveOwnMUCPresence (_converse, muc_jid, nick, affiliation='own
                 ? stx`<occupant-id xmlns="${Strophe.NS.OCCUPANTID}" id="${u.getUniqueId()}"/>`
                 : ''
             }
-            ${ _converse.xmppstatus.get('status')
-                ? stx`<show>${_converse.xmppstatus.get('status')}</show>`
+            ${ _converse.state.profile.get('show')
+                ? stx`<show>${_converse.state.profile.get('show')}</show>`
                 : ''
             }
         </presence>`));

+ 12 - 1
src/types/plugins/profile/modals/profile.d.ts

@@ -1,14 +1,25 @@
 export default class ProfileModal extends BaseModal {
+    /**
+     * @typedef {import('@converse/headless/types/plugins/vcard/api').VCardData} VCardData
+     * @typedef {import("@converse/headless").Profile} Profile
+     */
+    static properties: {
+        _submitting: {
+            state: boolean;
+        };
+        model: any;
+    };
     renderModal(): import("lit-html").TemplateResult<1>;
     getModalTitle(): any;
     /**
      * @param {VCardData} data
      */
-    setVCard(data: import("@converse/headless/types/plugins/vcard/api").VCardData): Promise<void>;
+    setVCard(data: import("@converse/headless/types/plugins/vcard/api").VCardData): Promise<boolean>;
     /**
      * @param {SubmitEvent} ev
      */
     onFormSubmitted(ev: SubmitEvent): Promise<void>;
+    _submitting: boolean;
 }
 import BaseModal from "plugins/modal/modal.js";
 //# sourceMappingURL=profile.d.ts.map

+ 1 - 1
src/types/plugins/profile/templates/chat-status-modal.d.ts

@@ -1,3 +1,3 @@
-declare function _default(el: any): import("lit-html").TemplateResult<1>;
+declare function _default(el: import("../modals/chat-status").default): import("lit-html").TemplateResult<1>;
 export default _default;
 //# sourceMappingURL=chat-status-modal.d.ts.map

+ 12 - 2
src/types/shared/components/image-picker.d.ts

@@ -13,9 +13,20 @@ export default class ImagePicker extends CustomElement {
     model: any;
     width: any;
     height: any;
-    data: Model;
     nonce: string;
     render(): import("lit-html").TemplateResult<1>;
+    /**
+     * Clears the selected image.
+     * @param {Event} ev
+     */
+    clearImage(ev: Event): void;
+    data: {
+        data_uri: any;
+        image_type: any;
+    } | {
+        data_uri: string | ArrayBuffer;
+        image_type: string;
+    };
     /**
      * @param {Event} ev
      */
@@ -26,5 +37,4 @@ export default class ImagePicker extends CustomElement {
     updateFilePreview(ev: InputEvent): void;
 }
 import { CustomElement } from './element.js';
-import { Model } from '@converse/skeletor';
 //# sourceMappingURL=image-picker.d.ts.map

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików