Переглянути джерело

Type improvements to MUC and MAM.

Refactor MUC message slightly to reduce code duplication.
JC Brand 1 рік тому
батько
коміт
20e37f5d74

+ 2 - 2
src/headless/plugins/chat/message.js

@@ -120,7 +120,7 @@ class Message extends ModelWithContact {
     /**
      * Returns a boolean indicating whether this message is ephemeral,
      * meaning it will get automatically removed after ten seconds.
-     * @returns { boolean }
+     * @returns {boolean}
      */
     isEphemeral () {
         return this.get('is_ephemeral');
@@ -128,7 +128,7 @@ class Message extends ModelWithContact {
 
     /**
      * Returns a boolean indicating whether this message is a XEP-0245 /me command.
-     * @returns { boolean }
+     * @returns {boolean}
      */
     isMeCommand () {
         const text = this.getMessageText();

+ 5 - 2
src/headless/plugins/chat/model.js

@@ -276,7 +276,10 @@ class ChatBox extends ModelWithContact {
         }
     }
 
-    async close () {
+    /**
+     * @param {Object} [_ev]
+     */
+    async close (_ev) {
         if (api.connection.connected()) {
             // Immediately sending the chat state, because the
             // model is going to be destroyed afterwards.
@@ -285,7 +288,7 @@ class ChatBox extends ModelWithContact {
         }
         try {
             await new Promise((success, reject) => {
-                return this.destroy({success, 'error': (m, e) => reject(e)})
+                return this.destroy({success, 'error': (_m, e) => reject(e)})
             });
         } catch (e) {
             log.error(e);

+ 39 - 18
src/headless/plugins/mam/utils.js

@@ -1,6 +1,7 @@
 /**
  * @typedef {import('../muc/muc.js').default} MUC
  * @typedef {import('../chat/model.js').default} ChatBox
+ * @typedef {import('@converse/skeletor/src/types/helpers.js').Model} Model
  */
 import sizzle from 'sizzle';
 import { Strophe, $iq } from 'strophe.js';
@@ -19,7 +20,7 @@ const u = converse.env.utils;
 /**
  * @param {Element} iq
  */
-export function onMAMError (iq) {
+export function onMAMError(iq) {
     if (iq?.querySelectorAll('feature-not-implemented').length) {
         log.warn(`Message Archive Management (XEP-0313) not supported by ${iq.getAttribute('from')}`);
     } else {
@@ -38,16 +39,19 @@ export function onMAMError (iq) {
  *
  * Per JID preferences will be set in chat boxes, so it'll
  * probbaly be handled elsewhere in any case.
+ *
+ * @param {Element} iq
+ * @param {Model} feature
  */
-export function onMAMPreferences (iq, feature) {
+export function onMAMPreferences(iq, feature) {
     const preference = sizzle(`prefs[xmlns="${NS.MAM}"]`, iq).pop();
     const default_pref = preference.getAttribute('default');
     if (default_pref !== api.settings.get('message_archiving')) {
         const stanza = $iq({ 'type': 'set' }).c('prefs', {
             'xmlns': NS.MAM,
-            'default': api.settings.get('message_archiving')
+            'default': api.settings.get('message_archiving'),
         });
-        Array.from(preference.children).forEach(child => stanza.cnode(child).up());
+        Array.from(preference.children).forEach((child) => stanza.cnode(child).up());
 
         // XXX: Strictly speaking, the server should respond with the updated prefs
         // (see example 18: https://xmpp.org/extensions/xep-0313.html#config)
@@ -60,19 +64,25 @@ export function onMAMPreferences (iq, feature) {
     }
 }
 
-export function getMAMPrefsFromFeature (feature) {
+/**
+ * @param {Model} feature
+ */
+export function getMAMPrefsFromFeature(feature) {
     const prefs = feature.get('preferences') || {};
     if (feature.get('var') !== NS.MAM || api.settings.get('message_archiving') === undefined) {
         return;
     }
     if (prefs['default'] !== api.settings.get('message_archiving')) {
         api.sendIQ($iq({ 'type': 'get' }).c('prefs', { 'xmlns': NS.MAM }))
-            .then(iq => _converse.exports.onMAMPreferences(iq, feature))
+            .then(/** @param {Element} iq */ (iq) => _converse.exports.onMAMPreferences(iq, feature))
             .catch(_converse.exports.onMAMError);
     }
 }
 
-export function preMUCJoinMAMFetch (muc) {
+/**
+ * @param {MUC} muc
+ */
+export function preMUCJoinMAMFetch(muc) {
     if (
         !api.settings.get('muc_show_logs_before_join') ||
         !muc.features.get('mam_enabled') ||
@@ -84,10 +94,19 @@ export function preMUCJoinMAMFetch (muc) {
     muc.save({ 'prejoin_mam_fetched': true });
 }
 
-export async function handleMAMResult (model, result, query, options, should_page) {
+/**
+ * @param {ChatBox|MUC} model
+ * @param {Object} result
+ * @param {Object} query
+ * @param {Object} options
+ * @param {('forwards'|'backwards'|null)} [should_page=null]
+ */
+export async function handleMAMResult(model, result, query, options, should_page) {
     await api.emojis.initialize();
     const is_muc = model.get('type') === CHATROOMS_TYPE;
-    const doParseMessage = s => is_muc ? parseMUCMessage(s, model) : parseMessage(s);
+    const doParseMessage = /** @param {Element} s*/ (s) =>
+        is_muc ? parseMUCMessage(s, /** @type {MUC} */ (model)) : parseMessage(s);
+
     const messages = await Promise.all(result.messages.map(doParseMessage));
     result.messages = messages;
 
@@ -99,7 +118,7 @@ export async function handleMAMResult (model, result, query, options, should_pag
     const data = { query, 'chatbox': model, messages };
     await api.trigger('MAMResult', data, { 'synchronous': true });
 
-    messages.forEach(m => model.queueMessage(m));
+    messages.forEach((m) => model.queueMessage(m));
     if (result.error) {
         const event_id = (result.error.retry_event_id = u.getUniqueId());
         api.listen.once(event_id, () => fetchArchivedMessages(model, options, should_page));
@@ -133,7 +152,7 @@ export async function handleMAMResult (model, result, query, options, should_pag
  *  this function should recursively page through the entire result set if a limited
  *  number of results were returned.
  */
-export async function fetchArchivedMessages (model, options = {}, should_page = null) {
+export async function fetchArchivedMessages(model, options = {}, should_page = null) {
     if (model.disable_mam) {
         return;
     }
@@ -144,11 +163,12 @@ export async function fetchArchivedMessages (model, options = {}, should_page =
         return;
     }
     const max = api.settings.get('archived_messages_page_size');
+
     const query = Object.assign(
         {
             'groupchat': is_muc,
             'max': max,
-            'with': model.get('jid')
+            'with': model.get('jid'),
         },
         options
     );
@@ -176,7 +196,7 @@ export async function fetchArchivedMessages (model, options = {}, should_page =
  * @param {MAMOptions} options
  * @param {object} result - The RSM result object
  */
-async function createPlaceholder (model, options, result) {
+async function createPlaceholder(model, options, result) {
     if (options.before == '' && (model.messages.length === 0 || !options.start)) {
         // Fetching the latest MAM messages with an empty local cache
         return;
@@ -185,7 +205,8 @@ async function createPlaceholder (model, options, result) {
         // Infinite scrolling upward
         return;
     }
-    if (options.before == null) { // eslint-disable-line no-eq-null
+    if (options.before == null) {
+        // eslint-disable-line no-eq-null
         // Adding placeholders when paging forwards is not supported yet,
         // since currently with standard Converse, we only page forwards
         // when fetching the entire history (i.e. no gaps should arise).
@@ -194,15 +215,15 @@ async function createPlaceholder (model, options, result) {
     const msgs = await Promise.all(result.messages);
     const { rsm } = result;
     const key = `stanza_id ${model.get('jid')}`;
-    const adjacent_message = msgs.find(m => m[key] === rsm.result.first);
+    const adjacent_message = msgs.find((m) => m[key] === rsm.result.first);
     const adjacent_message_date = new Date(adjacent_message['time']);
 
     const msg_data = {
         'template_hook': 'getMessageTemplate',
         'time': new Date(adjacent_message_date.getTime() - 1).toISOString(),
         'before': rsm.result.first,
-        'start': options.start
-    }
+        'start': options.start,
+    };
     model.messages.add(new MAMPlaceholderMessage(msg_data));
 }
 
@@ -211,7 +232,7 @@ async function createPlaceholder (model, options, result) {
  * the last archived message in our local cache.
  * @param {ChatBox} model
  */
-export function fetchNewestMessages (model) {
+export function fetchNewestMessages(model) {
     if (model.disable_mam) {
         return;
     }

+ 43 - 27
src/headless/plugins/muc/message.js

@@ -5,6 +5,9 @@ import { Strophe } from 'strophe.js';
 
 
 class MUCMessage extends Message {
+    /**
+     * @typedef {import('./muc.js').MUCOccupant} MUCOccupant
+     */
 
     async initialize () { // eslint-disable-line require-await
         this.chatbox = this.collection?.chatbox;
@@ -64,6 +67,9 @@ class MUCMessage extends Message {
         this.listenTo(this.chatbox.occupants, 'add', this.onOccupantAdded);
     }
 
+    /**
+     * @param {MUCOccupant} [occupant]
+     */
     onOccupantAdded (occupant) {
         if (this.get('occupant_id')) {
             if (occupant.get('occupant_id') !== this.get('occupant_id')) {
@@ -72,48 +78,58 @@ class MUCMessage extends Message {
         } else if (occupant.get('nick') !== Strophe.getResourceFromJid(this.get('from'))) {
             return;
         }
-
-        this.occupant = occupant;
-        if (occupant.get('jid')) {
-            this.save('from_real_jid', occupant.get('jid'));
-        }
-
-        this.trigger('occupantAdded');
-        this.listenTo(this.occupant, 'destroy', this.onOccupantRemoved);
-        this.stopListening(this.chatbox.occupants, 'add', this.onOccupantAdded);
+        this.setOccupant(occupant)
     }
 
     getOccupant() {
-        if (this.occupant) return this.occupant;
-
-        this.setOccupant();
-        return this.occupant;
+        return this.occupant || this.setOccupant();
     }
 
-    setOccupant () {
-        if (this.get('type') !== 'groupchat' || this.isEphemeral() || this.occupant) {
+    /**
+     * @param {MUCOccupant} [occupant]
+     * @return {MUCOccupant}
+     */
+    setOccupant (occupant) {
+        if (this.get('type') !== 'groupchat' || this.isEphemeral()) {
             return;
         }
 
-        const nick = Strophe.getResourceFromJid(this.get('from'));
-        const occupant_id = this.get('occupant_id');
+        if (occupant) {
+            this.occupant = occupant;
+
+        } else {
+            if (this.occupant) return;
+
+            const nick = Strophe.getResourceFromJid(this.get('from'));
+            const occupant_id = this.get('occupant_id');
+            this.occupant = (nick || occupant_id) ? this.chatbox.occupants.findOccupant({ nick, occupant_id }) : null;
 
-        this.occupant = this.chatbox.occupants.findOccupant({ nick, occupant_id });
+            if (!this.occupant) {
+                const jid = this.get('from_real_jid');
+                if (!nick && !occupant_id && !jid) {
+                    // Tombstones of retracted messages might have no occupant info
+                    return;
+                }
 
-        if (!this.occupant) {
-            this.occupant = this.chatbox.occupants.create({
-                nick,
-                occupant_id,
-                jid: this.get('from_real_jid'),
-            });
+                this.occupant = this.chatbox.occupants.create({ nick, occupant_id, jid });
 
-            if (api.settings.get('muc_send_probes')) {
-                const jid = `${this.chatbox.get('jid')}/${nick}`;
-                api.user.presence.send('probe', jid);
+                if (api.settings.get('muc_send_probes')) {
+                    const jid = `${this.chatbox.get('jid')}/${nick}`;
+                    api.user.presence.send('probe', jid);
+                }
             }
         }
 
+        if (this.get('from_real_jid') !== this.occupant.get('jid')) {
+            this.save('from_real_jid', this.occupant.get('jid'));
+        }
+
+        this.trigger('occupant:add');
+        this.listenTo(this.occupant, 'change', (changed) => this.trigger('occupant:change', changed));
         this.listenTo(this.occupant, 'destroy', this.onOccupantRemoved);
+        this.stopListening(this.chatbox.occupants, 'add', this.onOccupantAdded);
+
+        return this.occupant;
     }
 }
 

+ 42 - 19
src/headless/plugins/muc/muc.js

@@ -7,24 +7,24 @@
  * @typedef {module:plugin-chat-parsers.MessageAttributes} MessageAttributes
  * @typedef {module:plugin-muc-parsers.MUCMessageAttributes} MUCMessageAttributes
  * @typedef {module:shared.converse.UserMessage} UserMessage
- * @typedef {import('strophe.js/src/builder.js').Builder} Strophe.Builder
+ * @typedef {import('strophe.js').Builder} Builder
  */
-import _converse from '../../shared/_converse.js';
-import api from '../../shared/api/index.js';
-import converse from '../../shared/api/public.js';
 import debounce from 'lodash-es/debounce';
-import log from '../../log';
-import p from '../../utils/parse-helpers';
 import pick from 'lodash-es/pick';
 import sizzle from 'sizzle';
+import { getOpenPromise } from '@converse/openpromise';
 import { Model } from '@converse/skeletor';
+import log from '../../log';
+import p from '../../utils/parse-helpers';
+import _converse from '../../shared/_converse.js';
+import api from '../../shared/api/index.js';
+import converse from '../../shared/api/public.js';
 import ChatBox from '../chat/model.js';
 import { ROOMSTATUS } from './constants.js';
 import { CHATROOMS_TYPE, GONE } from '../../shared/constants.js';
 import { Strophe, $build, $iq, $msg, $pres } from 'strophe.js';
 import { TimeoutError } from '../../shared/errors.js';
 import { computeAffiliationsDelta, setAffiliations, getAffiliationList }  from './affiliations/utils.js';
-import { getOpenPromise } from '@converse/openpromise';
 import { handleCorrection } from '../../shared/chat/utils.js';
 import { initStorage, createStore } from '../../utils/storage.js';
 import { isArchived } from '../../shared/parsers.js';
@@ -323,6 +323,9 @@ class MUC extends ChatBox {
         }
     }
 
+    /**
+     * @param {MUCOccupant} occupant
+     */
     onOccupantAdded (occupant) {
         if (
             isInfoVisible(converse.MUC_TRAFFIC_STATES.ENTERED) &&
@@ -333,6 +336,9 @@ class MUC extends ChatBox {
         }
     }
 
+    /**
+     * @param {MUCOccupant} occupant
+     */
     onOccupantRemoved (occupant) {
         if (
             isInfoVisible(converse.MUC_TRAFFIC_STATES.EXITED) &&
@@ -343,6 +349,9 @@ class MUC extends ChatBox {
         }
     }
 
+    /**
+     * @param {MUCOccupant} occupant
+     */
     onOccupantShowChanged (occupant) {
         if (occupant.get('states').includes('303')) {
             return;
@@ -583,10 +592,10 @@ class MUC extends ChatBox {
      * Parses an incoming message stanza and queues it for processing.
      * @private
      * @method MUC#handleMessageStanza
-     * @param {Strophe.Builder|Element} stanza
+     * @param {Builder|Element} stanza
      */
     async handleMessageStanza (stanza) {
-        stanza = stanza.tree?.() ?? stanza;
+        stanza = /** @type {Builder} */(stanza).tree?.() ?? /** @type {Element} */(stanza);
 
         const type = stanza.getAttribute('type');
         if (type === 'error') {
@@ -687,6 +696,7 @@ class MUC extends ChatBox {
         );
 
         this.affiliation_message_handler = connection.addHandler(
+            /** @param {Element} stanza */
             (stanza) => {
                 this.handleAffiliationChangedMessage(stanza);
                 return true;
@@ -749,7 +759,7 @@ class MUC extends ChatBox {
      * or error message within a specific timeout period.
      * @private
      * @method MUC#sendTimedMessage
-     * @param {Strophe.Builder|Element } message
+     * @param {Builder|Element } message
      * @returns { Promise<Element>|Promise<TimeoutError> } Returns a promise
      *  which resolves with the reflected message stanza or with an error stanza or
      *  {@link TimeoutError}.
@@ -772,7 +782,8 @@ class MUC extends ChatBox {
             return false;
         });
         const handler = connection.addHandler(
-            stanza => {
+            /** @param {Element} stanza */
+            (stanza) => {
                 timeoutHandler && connection.deleteTimedHandler(timeoutHandler);
                 promise.resolve(stanza);
             }, null, 'message', ['error', 'groupchat'], id);
@@ -889,7 +900,6 @@ class MUC extends ChatBox {
      * Sends an IQ stanza to the XMPP server to destroy this groupchat. Not
      * to be confused with the {@link MUC#destroy}
      * method, which simply removes the room from the local browser storage cache.
-     * @private
      * @method MUC#sendDestroyIQ
      * @param { string } [reason] - The reason for destroying the groupchat.
      * @param { string } [new_jid] - The JID of the new groupchat which replaces this one.
@@ -940,6 +950,9 @@ class MUC extends ChatBox {
         safeSave(this.session, { 'connection_status': ROOMSTATUS.DISCONNECTED });
     }
 
+    /**
+     * @param {{ name: 'closeAllChatBoxes' }} [ev]
+     */
     async close (ev) {
         const { ENTERED, CLOSING } = ROOMSTATUS;
         const was_entered = this.session.get('connection_status') === ENTERED;
@@ -992,18 +1005,27 @@ class MUC extends ChatBox {
         return RegExp(`(?:\\p{P}|\\p{Z}|^)@(${longNickString})(?![\\w@-])`, 'uig');
     }
 
+    /**
+     * @param {string} jid
+     */
     getOccupantByJID (jid) {
         return this.occupants.findOccupant({ jid });
     }
 
+    /**
+     * @param {string} nick
+     */
     getOccupantByNickname (nick) {
         return this.occupants.findOccupant({ nick });
     }
 
-    getReferenceURIFromNickname (nickname) {
+    /**
+     * @param {string} nick
+     */
+    getReferenceURIFromNickname (nick) {
         const muc_jid = this.get('jid');
-        const occupant = this.getOccupant(nickname);
-        const uri = (this.features.get('nonanonymous') && occupant?.get('jid')) || `${muc_jid}/${nickname}`;
+        const occupant = this.getOccupant(nick);
+        const uri = (this.features.get('nonanonymous') && occupant?.get('jid')) || `${muc_jid}/${nick}`;
         return encodeURI(`xmpp:${uri}`);
     }
 
@@ -1020,7 +1042,7 @@ class MUC extends ChatBox {
 
         const getMatchingNickname = p.findFirstMatchInArray(this.getAllKnownNicknames());
 
-        const matchToReference = match => {
+        const matchToReference = (match) => {
             let at_sign_index = match[0].indexOf('@');
             if (match[0][at_sign_index + 1] === '@') {
                 // edge-case
@@ -1124,7 +1146,6 @@ class MUC extends ChatBox {
 
     /**
      * Send a direct invitation as per XEP-0249
-     * @private
      * @method MUC#directInvite
      * @param { String } recipient - JID of the person being invited
      * @param { String } [reason] - Reason for the invitation
@@ -2018,13 +2039,15 @@ class MUC extends ChatBox {
      * @method MUC#sendStatusPresence
      * @param { String } type
      * @param { String } [status] - An optional status message
-     * @param { Element[]|Strophe.Builder[]|Element|Strophe.Builder } [child_nodes]
+     * @param { Element[]|Builder[]|Element|Builder } [child_nodes]
      *  Nodes(s) to be added as child nodes of the `presence` XML element.
      */
     async sendStatusPresence (type, status, child_nodes) {
         if (this.session.get('connection_status') === ROOMSTATUS.ENTERED) {
             const presence = await _converse.state.xmppstatus.constructPresence(type, this.getRoomJIDAndNick(), status);
-            child_nodes?.map(c => c?.tree() ?? c).forEach(c => presence.cnode(c).up());
+            /** @type {Element[]|Builder[]} */(child_nodes)?.map(
+                c => c?.tree() ?? c).forEach(c => presence.cnode(c).up()
+            );
             api.send(presence);
         }
     }

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

@@ -267,7 +267,7 @@ export async function parseMUCMessage (stanza, chatbox) {
     attrs.from_real_jid = attrs.is_archived && getJIDFromMUCUserData(stanza) ||
         chatbox.occupants.findOccupant(attrs)?.get('jid');
 
-    attrs = Object.assign( {
+    attrs = Object.assign({
         'is_only_emojis': attrs.body ? u.isOnlyEmojis(attrs.body) : false,
         'is_valid_receipt_request': isValidReceiptRequest(stanza, attrs),
         'message': attrs.body || attrs.error, // TODO: Should only be used for error and info messages
@@ -287,6 +287,7 @@ export async function parseMUCMessage (stanza, chatbox) {
     } else if (attrs.is_carbon) {
         return new StanzaParseError('Invalid Stanza: MUC messages SHOULD NOT be XEP-0280 carbon copied', stanza);
     }
+
     // We prefer to use one of the XEP-0359 unique and stable stanza IDs as the Model id, to avoid duplicates.
     attrs['id'] = attrs['origin_id'] || attrs[`stanza_id ${attrs.from_muc || attrs.from}`] || u.getUniqueId();
 

+ 10 - 4
src/headless/plugins/muc/utils.js

@@ -1,6 +1,3 @@
-/**
- * @typedef {import('@converse/skeletor').Model} Model
- */
 import _converse from '../../shared/_converse.js';
 import api from '../../shared/api/index.js';
 import converse from '../../shared/api/public.js';
@@ -12,6 +9,9 @@ import { getUnloadEvent } from '../../utils/session.js';
 
 const { Strophe, sizzle, u } = converse.env;
 
+/**
+ * @param {import('@converse/skeletor').Model} model
+ */
 export function isChatRoom (model) {
     return model?.get('type') === 'chatroom';
 }
@@ -20,6 +20,10 @@ export function shouldCreateGroupchatMessage (attrs) {
     return attrs.nick && (u.shouldCreateMessage(attrs) || attrs.is_tombstone);
 }
 
+/**
+ * @param {import('./muc.js').MUCOccupant} occupant1
+ * @param {import('./muc.js').MUCOccupant} occupant2
+ */
 export function occupantsComparator (occupant1, occupant2) {
     const role1 = occupant1.get('role') || 'none';
     const role2 = occupant2.get('role') || 'none';
@@ -82,6 +86,8 @@ export async function routeToRoom (event) {
 /* Opens a groupchat, making sure that certain attributes
  * are correct, for example that the "type" is set to
  * "chatroom".
+ * @param {string} jid
+ * @param {Object} settings
  */
 export async function openChatRoom (jid, settings) {
     settings.type = CHATROOMS_TYPE;
@@ -97,7 +103,7 @@ export async function openChatRoom (jid, settings) {
  * See XEP-0249: Direct MUC invitations.
  * @private
  * @method _converse.ChatRoom#onDirectMUCInvitation
- * @param { Element } message - The message stanza containing the invitation.
+ * @param {Element} message - The message stanza containing the invitation.
  */
 export async function onDirectMUCInvitation (message) {
     const x_el = sizzle('x[xmlns="jabber:x:conference"]', message).pop(),

+ 2 - 2
src/headless/types/plugins/chat/message.d.ts

@@ -39,12 +39,12 @@ declare class Message extends ModelWithContact {
     /**
      * Returns a boolean indicating whether this message is ephemeral,
      * meaning it will get automatically removed after ten seconds.
-     * @returns { boolean }
+     * @returns {boolean}
      */
     isEphemeral(): boolean;
     /**
      * Returns a boolean indicating whether this message is a XEP-0245 /me command.
-     * @returns { boolean }
+     * @returns {boolean}
      */
     isMeCommand(): boolean;
     /**

+ 4 - 1
src/headless/types/plugins/chat/model.d.ts

@@ -56,7 +56,10 @@ declare class ChatBox extends ModelWithContact {
     onMessageUploadChanged(message: any): Promise<void>;
     onMessageAdded(message: any): void;
     clearMessages(): Promise<void>;
-    close(): Promise<void>;
+    /**
+     * @param {Object} [_ev]
+     */
+    close(_ev?: any): Promise<void>;
     announceReconnection(): void;
     onReconnection(): Promise<void>;
     onPresenceChanged(item: any): void;

+ 21 - 4
src/headless/types/plugins/mam/utils.d.ts

@@ -12,11 +12,27 @@ export function onMAMError(iq: Element): void;
  *
  * Per JID preferences will be set in chat boxes, so it'll
  * probbaly be handled elsewhere in any case.
+ *
+ * @param {Element} iq
+ * @param {Model} feature
+ */
+export function onMAMPreferences(iq: Element, feature: Model): void;
+/**
+ * @param {Model} feature
+ */
+export function getMAMPrefsFromFeature(feature: Model): void;
+/**
+ * @param {MUC} muc
+ */
+export function preMUCJoinMAMFetch(muc: MUC): void;
+/**
+ * @param {ChatBox|MUC} model
+ * @param {Object} result
+ * @param {Object} query
+ * @param {Object} options
+ * @param {('forwards'|'backwards'|null)} [should_page=null]
  */
-export function onMAMPreferences(iq: any, feature: any): void;
-export function getMAMPrefsFromFeature(feature: any): void;
-export function preMUCJoinMAMFetch(muc: any): void;
-export function handleMAMResult(model: any, result: any, query: any, options: any, should_page: any): Promise<void>;
+export function handleMAMResult(model: ChatBox | MUC, result: any, query: any, options: any, should_page?: ('forwards' | 'backwards' | null)): Promise<void>;
 /**
  * @typedef {Object} MAMOptions
  * A map of MAM related options that may be passed to fetchArchivedMessages
@@ -55,4 +71,5 @@ export function fetchNewestMessages(model: ChatBox): void;
 export type MAMOptions = any;
 export type MUC = import('../muc/muc.js').default;
 export type ChatBox = import('../chat/model.js').default;
+export type Model = import('@converse/skeletor/src/types/helpers.js').Model;
 //# sourceMappingURL=utils.d.ts.map

+ 10 - 3
src/headless/types/plugins/muc/message.d.ts

@@ -11,10 +11,17 @@ declare class MUCMessage extends Message {
     mayBeModerated(): boolean;
     checkValidity(): any;
     onOccupantRemoved(): void;
-    onOccupantAdded(occupant: any): void;
-    occupant: any;
+    /**
+     * @param {MUCOccupant} [occupant]
+     */
+    onOccupantAdded(occupant?: import("./occupant.js").default): void;
     getOccupant(): any;
-    setOccupant(): void;
+    /**
+     * @param {MUCOccupant} [occupant]
+     * @return {MUCOccupant}
+     */
+    setOccupant(occupant?: import("./occupant.js").default): import("./occupant.js").default;
+    occupant: any;
 }
 import Message from "../chat/message.js";
 //# sourceMappingURL=message.d.ts.map

+ 38 - 20
src/headless/types/plugins/muc/muc.d.ts

@@ -6,9 +6,7 @@ export type MemberListItem = any;
 export type MessageAttributes = any;
 export type MUCMessageAttributes = any;
 export type UserMessage = any;
-export namespace Strophe {
-    type Builder = any;
-}
+export type Builder = import('strophe.js').Builder;
 /**
  * Represents an open/ongoing groupchat conversation.
  * @namespace MUC
@@ -64,7 +62,7 @@ declare class MUC extends ChatBox {
     /**
      * @param {string} password
      */
-    constructJoinPresence(password: string): Promise<import("strophe.js/src/types/builder.js").default>;
+    constructJoinPresence(password: string): Promise<import("strophe.js/src/types/builder").default>;
     clearOccupantsCache(): void;
     /**
      * Given the passed in MUC message, send a XEP-0333 chat marker.
@@ -91,9 +89,18 @@ declare class MUC extends ChatBox {
      * @method MUC#onHiddenChange
      */
     private onHiddenChange;
-    onOccupantAdded(occupant: any): void;
-    onOccupantRemoved(occupant: any): void;
-    onOccupantShowChanged(occupant: any): void;
+    /**
+     * @param {MUCOccupant} occupant
+     */
+    onOccupantAdded(occupant: MUCOccupant): void;
+    /**
+     * @param {MUCOccupant} occupant
+     */
+    onOccupantRemoved(occupant: MUCOccupant): void;
+    /**
+     * @param {MUCOccupant} occupant
+     */
+    onOccupantShowChanged(occupant: MUCOccupant): void;
     onRoomEntered(): Promise<void>;
     onConnectionStatusChanged(): Promise<void>;
     restoreSession(): Promise<any>;
@@ -123,7 +130,7 @@ declare class MUC extends ChatBox {
      * Parses an incoming message stanza and queues it for processing.
      * @private
      * @method MUC#handleMessageStanza
-     * @param {Strophe.Builder|Element} stanza
+     * @param {Builder|Element} stanza
      */
     private handleMessageStanza;
     /**
@@ -144,7 +151,7 @@ declare class MUC extends ChatBox {
      * or error message within a specific timeout period.
      * @private
      * @method MUC#sendTimedMessage
-     * @param {Strophe.Builder|Element } message
+     * @param {Builder|Element } message
      * @returns { Promise<Element>|Promise<TimeoutError> } Returns a promise
      *  which resolves with the reflected message stanza or with an error stanza or
      *  {@link TimeoutError}.
@@ -179,12 +186,11 @@ declare class MUC extends ChatBox {
      * Sends an IQ stanza to the XMPP server to destroy this groupchat. Not
      * to be confused with the {@link MUC#destroy}
      * method, which simply removes the room from the local browser storage cache.
-     * @private
      * @method MUC#sendDestroyIQ
      * @param { string } [reason] - The reason for destroying the groupchat.
      * @param { string } [new_jid] - The JID of the new groupchat which replaces this one.
      */
-    private sendDestroyIQ;
+    sendDestroyIQ(reason?: string, new_jid?: string): any;
     /**
      * Leave the groupchat.
      * @private
@@ -192,7 +198,12 @@ declare class MUC extends ChatBox {
      * @param { string } [exit_msg] - Message to indicate your reason for leaving
      */
     private leave;
-    close(ev: any): Promise<any>;
+    /**
+     * @param {{ name: 'closeAllChatBoxes' }} [ev]
+     */
+    close(ev?: {
+        name: 'closeAllChatBoxes';
+    }): Promise<any>;
     canModerateMessages(): any;
     /**
      * Return an array of unique nicknames based on all occupants and messages in this MUC.
@@ -202,9 +213,18 @@ declare class MUC extends ChatBox {
      */
     private getAllKnownNicknames;
     getAllKnownNicknamesRegex(): RegExp;
-    getOccupantByJID(jid: any): any;
-    getOccupantByNickname(nick: any): any;
-    getReferenceURIFromNickname(nickname: any): string;
+    /**
+     * @param {string} jid
+     */
+    getOccupantByJID(jid: string): any;
+    /**
+     * @param {string} nick
+     */
+    getOccupantByNickname(nick: string): any;
+    /**
+     * @param {string} nick
+     */
+    getReferenceURIFromNickname(nick: string): string;
     /**
      * Given a text message, look for `@` mentions and turn them into
      * XEP-0372 references
@@ -221,12 +241,11 @@ declare class MUC extends ChatBox {
     private getRoomJIDAndNick;
     /**
      * Send a direct invitation as per XEP-0249
-     * @private
      * @method MUC#directInvite
      * @param { String } recipient - JID of the person being invited
      * @param { String } [reason] - Reason for the invitation
      */
-    private directInvite;
+    directInvite(recipient: string, reason?: string): void;
     /**
      * Refresh the disco identity, features and fields for this {@link MUC}.
      * *features* are stored on the features {@link Model} attribute on this {@link MUC}.
@@ -496,10 +515,10 @@ declare class MUC extends ChatBox {
      * @method MUC#sendStatusPresence
      * @param { String } type
      * @param { String } [status] - An optional status message
-     * @param { Element[]|Strophe.Builder[]|Element|Strophe.Builder } [child_nodes]
+     * @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[] | Strophe.Builder[] | Element | Strophe.Builder): Promise<void>;
+    sendStatusPresence(type: string, status?: string, child_nodes?: Element[] | Builder[] | Element | Builder): Promise<void>;
     /**
      * Check whether we're still joined and re-join if not
      * @method MUC#rejoinIfNecessary
@@ -664,5 +683,4 @@ declare class MUCSession extends Model {
     };
 }
 import { Model } from "@converse/skeletor";
-import { Strophe } from "strophe.js";
 //# sourceMappingURL=muc.d.ts.map

+ 10 - 4
src/headless/types/plugins/muc/utils.d.ts

@@ -1,6 +1,13 @@
-export function isChatRoom(model: any): boolean;
+/**
+ * @param {import('@converse/skeletor').Model} model
+ */
+export function isChatRoom(model: import('@converse/skeletor').Model): boolean;
 export function shouldCreateGroupchatMessage(attrs: any): any;
-export function occupantsComparator(occupant1: any, occupant2: any): 0 | 1 | -1;
+/**
+ * @param {import('./muc.js').MUCOccupant} occupant1
+ * @param {import('./muc.js').MUCOccupant} occupant2
+ */
+export function occupantsComparator(occupant1: import('./muc.js').MUCOccupant, occupant2: import('./muc.js').MUCOccupant): 0 | 1 | -1;
 export function registerDirectInvitationHandler(): void;
 export function disconnectChatRooms(): any;
 export function onWindowStateChanged(): Promise<void>;
@@ -14,7 +21,7 @@ export function openChatRoom(jid: any, settings: any): Promise<any>;
  * See XEP-0249: Direct MUC invitations.
  * @private
  * @method _converse.ChatRoom#onDirectMUCInvitation
- * @param { Element } message - The message stanza containing the invitation.
+ * @param {Element} message - The message stanza containing the invitation.
  */
 export function onDirectMUCInvitation(message: Element): Promise<void>;
 export function getDefaultMUCNickname(): any;
@@ -35,5 +42,4 @@ export function onAddClientFeatures(): void;
 export function onBeforeTearDown(): void;
 export function onStatusInitialized(): void;
 export function onBeforeResourceBinding(): void;
-export type Model = import('@converse/skeletor').Model;
 //# sourceMappingURL=utils.d.ts.map

+ 8 - 1
src/plugins/mam-views/utils.js

@@ -1,3 +1,7 @@
+/**
+ * @typedef {import('../chatview/chat.js').default} ChatView
+ * @typedef {import('../muc-views/muc.js').default} MUCView
+ */
 import { html } from 'lit/html.js';
 import { _converse, api, log, constants, u, MAMPlaceholderMessage } from '@converse/headless';
 
@@ -12,6 +16,9 @@ export function getPlaceholderTemplate (message, tpl) {
     }
 }
 
+/**
+ * @param {ChatView|MUCView} view
+ */
 export async function fetchMessagesOnScrollUp (view) {
     if (view.model.ui.get('chat-content-spinner-top')) {
         return;
@@ -22,7 +29,7 @@ export async function fetchMessagesOnScrollUp (view) {
         if (oldest_message) {
             const bare_jid = _converse.session.get('bare_jid');
             const by_jid = is_groupchat ? view.model.get('jid') : bare_jid;
-            const stanza_id = oldest_message && oldest_message.get(`stanza_id ${by_jid}`);
+            const stanza_id = oldest_message.get(`stanza_id ${by_jid}`);
             view.model.ui.set('chat-content-spinner-top', true);
             try {
                 if (stanza_id) {

+ 2 - 12
src/shared/chat/message.js

@@ -55,18 +55,8 @@ export default class Message extends CustomElement {
         this.listenTo(this.model, 'change', () => this.requestUpdate());
         this.listenTo(this.model, 'contact:change', () => this.requestUpdate());
         this.listenTo(this.model, 'vcard:change', () => this.requestUpdate());
-
-        // TODO: refactor MUC to trigger `occupant:change`
-        if (this.model.get('type') === 'groupchat') {
-            if (this.model.occupant) {
-                this.listenTo(this.model.occupant, 'change', () => this.requestUpdate());
-            } else {
-                this.listenTo(this.model, 'occupantAdded', () => {
-                    this.requestUpdate();
-                    this.listenTo(this.model.occupant, 'change', () => this.requestUpdate())
-                });
-            }
-        }
+        this.listenTo(this.model, 'occupant:change', () => this.requestUpdate());
+        this.listenTo(this.model, 'occupant:add', () => this.requestUpdate());
     }
 
     async setModels () {

+ 6 - 1
src/types/plugins/mam-views/utils.d.ts

@@ -1,3 +1,8 @@
 export function getPlaceholderTemplate(message: any, tpl: any): any;
-export function fetchMessagesOnScrollUp(view: any): Promise<void>;
+/**
+ * @param {ChatView|MUCView} view
+ */
+export function fetchMessagesOnScrollUp(view: ChatView | MUCView): Promise<void>;
+export type ChatView = import('../chatview/chat.js').default;
+export type MUCView = import('../muc-views/muc.js').default;
 //# sourceMappingURL=utils.d.ts.map