Explorar o código

Add support for `normal` messages in 1:1 MAM

JC Brand hai 2 meses
pai
achega
16875b91ab

+ 1 - 0
karma.conf.js

@@ -41,6 +41,7 @@ module.exports = function(config) {
       { pattern: "src/headless/plugins/bookmarks/tests/deprecated.js", type: 'module' },
       { pattern: "src/headless/plugins/caps/tests/caps.js", type: 'module' },
       { pattern: "src/headless/plugins/chat/tests/api.js", type: 'module' },
+      { pattern: "src/headless/plugins/chat/tests/chat.js", type: 'module' },
       { pattern: "src/headless/plugins/disco/tests/disco.js", type: 'module' },
       { pattern: "src/headless/plugins/mam/tests/api.js", type: 'module' },
       { pattern: "src/headless/plugins/muc/tests/affiliations.js", type: 'module' },

+ 8 - 0
src/headless/plugins/chat/model.js

@@ -236,6 +236,14 @@ class ChatBox extends ModelWithVCard(ModelWithMessages(ModelWithContact(ColorAwa
     canPostMessages() {
         return true;
     }
+
+    /**
+     * @param {import('../../shared/message').default} message
+     */
+    isChatMessage(message) {
+        const type = message.get('type');
+        return type === this.get('message_type') || type === 'normal';
+    }
 }
 
 export default ChatBox;

+ 26 - 0
src/headless/plugins/chat/tests/chat.js

@@ -0,0 +1,26 @@
+
+/* global mock, converse */
+
+describe("A ChatBox", function() {
+
+    it("considers both 'chat' and 'normal' messages as chat messages", mock.initConverse(
+            ['rosterInitialized', 'chatBoxesInitialized'], {},
+            async (_converse) => {
+
+        await mock.waitForRoster(_converse, 'current', 1);
+        const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
+        await mock.openChatBoxFor(_converse, contact_jid);
+
+        const { handleMessageStanza } = _converse;
+        await handleMessageStanza(mock.createChatMessage(_converse, contact_jid, 'This is an info message', 'info'));
+        await handleMessageStanza(mock.createChatMessage(_converse, contact_jid, 'This is a normal message', 'normal'));
+        await handleMessageStanza(mock.createChatMessage(_converse, contact_jid, 'This is a chat message', 'chat'));
+
+        const chatbox = _converse.state.chatboxes.get(contact_jid);
+        const oldest_msg = chatbox.getOldestMessage();
+        expect(oldest_msg.get('message')).toBe('This is a normal message');
+
+        const newest_msg = chatbox.getMostRecentMessage();
+        expect(newest_msg.get('message')).toBe('This is a chat message');
+    }));
+});

+ 8 - 0
src/headless/plugins/headlines/feed.js

@@ -39,4 +39,12 @@ export default class HeadlinesFeed extends ChatBoxBase {
     canPostMessages() {
         return false;
     }
+
+    /**
+     * @param {import('../../shared/message').default} message
+     */
+    isChatMessage(message) {
+        const type = message.get('type');
+        return type === this.get('message_type') || type === 'normal';
+    }
 }

+ 7 - 0
src/headless/plugins/muc/muc.js

@@ -1039,6 +1039,13 @@ class MUC extends ModelWithVCard(ModelWithMessages(ColorAwareModel(ChatBoxBase))
         return this.isEntered() && !(this.features.get('moderated') && this.getOwnRole() === 'visitor');
     }
 
+    /**
+     * @param {import('../../shared/message').default} message
+     */
+    isChatMessage(message) {
+        return message.get('type') === this.get('message_type');
+    }
+
     /**
      * Return an array of unique nicknames based on all occupants and messages in this MUC.
      * @returns {String[]}

+ 7 - 0
src/headless/plugins/muc/occupant.js

@@ -210,6 +210,13 @@ class MUCOccupant extends ModelWithVCard(ModelWithMessages(ColorAwareModel(Model
         stanza.cnode(stx`<x xmlns="${Strophe.NS.MUC}#user"/>`).root();
         return stanza;
     }
+
+    /**
+     * @param {import('../../shared/message').default} message
+     */
+    isChatMessage(message) {
+        return message.get('type') === this.get('message_type');
+    }
 }
 
 export default MUCOccupant;

+ 170 - 159
src/headless/shared/model-with-messages.js

@@ -1,18 +1,18 @@
-import { filesize } from "filesize";
-import pick from "lodash-es/pick";
-import debounce from "lodash-es/debounce.js";
-import { getOpenPromise } from "@converse/openpromise";
-import { Model } from "@converse/skeletor";
-import log from "@converse/log";
-import { initStorage } from "../utils/storage.js";
-import * as constants from "./constants.js";
-import converse from "./api/public.js";
-import api from "./api/index.js";
-import { isNewMessage } from "../plugins/chat/utils.js";
-import _converse from "./_converse.js";
-import { MethodNotImplementedError } from "./errors.js";
-import { sendMarker, sendReceiptStanza, sendRetractionMessage } from "./actions.js";
-import { parseMessage } from "../plugins/chat/parsers";
+import { filesize } from 'filesize';
+import pick from 'lodash-es/pick';
+import debounce from 'lodash-es/debounce.js';
+import { getOpenPromise } from '@converse/openpromise';
+import { Model } from '@converse/skeletor';
+import log from '@converse/log';
+import { initStorage } from '../utils/storage.js';
+import * as constants from './constants.js';
+import converse from './api/public.js';
+import api from './api/index.js';
+import { isNewMessage } from '../plugins/chat/utils.js';
+import _converse from './_converse.js';
+import * as errors from './errors.js';
+import { sendMarker, sendReceiptStanza, sendRetractionMessage } from './actions.js';
+import { parseMessage } from '../plugins/chat/parsers';
 
 const { Strophe, stx, u } = converse.env;
 
@@ -33,7 +33,6 @@ export default function ModelWithMessages(BaseModel) {
      * @typedef {import('../plugins/muc/muc').default} MUC
      * @typedef {import('../plugins/muc/parsers').MUCMessageAttributes} MUCMessageAttributes
      * @typedef {import('../shared/types').MessageAttributes} MessageAttributes
-     * @typedef {import('./errors').StanzaParseError} StanzaParseError
      * @typedef {import('./message').default} BaseMessage
      * @typedef {import('strophe.js').Builder} Builder
      */
@@ -52,7 +51,7 @@ export default function ModelWithMessages(BaseModel) {
             this.initMessages();
             this.initNotifications();
 
-            this.ui.on("change:scrolled", () => this.onScrolledChanged());
+            this.ui.on('change:scrolled', () => this.onScrolledChanged());
         }
 
         initNotifications() {
@@ -67,7 +66,7 @@ export default function ModelWithMessages(BaseModel) {
          * @returns {string}
          */
         getDisplayName() {
-            return this.get("jid");
+            return this.get('jid');
         }
 
         canPostMessages() {
@@ -89,7 +88,7 @@ export default function ModelWithMessages(BaseModel) {
         }
 
         getMessagesCacheKey() {
-            return `converse.messages-${this.get("jid")}-${_converse.session.get("bare_jid")}`;
+            return `converse.messages-${this.get('jid')}-${_converse.session.get('bare_jid')}`;
         }
 
         getMessagesCollection() {
@@ -98,14 +97,14 @@ export default function ModelWithMessages(BaseModel) {
 
         getNotificationsText() {
             const { __ } = _converse;
-            if (this.notifications?.get("chat_state") === constants.COMPOSING) {
-                return __("%1$s is typing", this.getDisplayName());
-            } else if (this.notifications?.get("chat_state") === constants.PAUSED) {
-                return __("%1$s has stopped typing", this.getDisplayName());
-            } else if (this.notifications?.get("chat_state") === constants.GONE) {
-                return __("%1$s has gone away", this.getDisplayName());
+            if (this.notifications?.get('chat_state') === constants.COMPOSING) {
+                return __('%1$s is typing', this.getDisplayName());
+            } else if (this.notifications?.get('chat_state') === constants.PAUSED) {
+                return __('%1$s has stopped typing', this.getDisplayName());
+            } else if (this.notifications?.get('chat_state') === constants.GONE) {
+                return __('%1$s has gone away', this.getDisplayName());
             } else {
-                return "";
+                return '';
             }
         }
 
@@ -115,14 +114,14 @@ export default function ModelWithMessages(BaseModel) {
             this.messages.chatbox = this;
             initStorage(this.messages, this.getMessagesCacheKey());
 
-            this.listenTo(this.messages, "add", (m) => this.onMessageAdded(m));
-            this.listenTo(this.messages, "change:upload", (m) => this.onMessageUploadChanged(m));
-            this.listenTo(this.messages, "change:correcting", (m) => this.onMessageCorrecting(m));
+            this.listenTo(this.messages, 'add', (m) => this.onMessageAdded(m));
+            this.listenTo(this.messages, 'change:upload', (m) => this.onMessageUploadChanged(m));
+            this.listenTo(this.messages, 'change:correcting', (m) => this.onMessageCorrecting(m));
         }
 
         fetchMessages() {
             if (this.messages.fetched_flag) {
-                log.info(`Not re-fetching messages for ${this.get("jid")}`);
+                log.info(`Not re-fetching messages for ${this.get('jid')}`);
                 return;
             }
             this.messages.fetched_flag = true;
@@ -150,14 +149,14 @@ export default function ModelWithMessages(BaseModel) {
              * @type {ModelWithMessages}
              * @example _converse.api.listen.on('afterMessagesFetched', (model) => { ... });
              */
-            api.trigger("afterMessagesFetched", this);
+            api.trigger('afterMessagesFetched', this);
         }
 
         /**
          * @param {MessageAttributes|Error} _attrs_or_error
          */
         async onMessage(_attrs_or_error) {
-            throw new MethodNotImplementedError("onMessage is not implemented");
+            throw new errors.MethodNotImplementedError('onMessage is not implemented');
         }
 
         /**
@@ -166,7 +165,7 @@ export default function ModelWithMessages(BaseModel) {
          * @returns {object}
          */
         getUpdatedMessageAttributes(message, attrs) {
-            if (!attrs.error_type && message.get("error_type") === "Decryption") {
+            if (!attrs.error_type && message.get('error_type') === 'Decryption') {
                 // Looks like we have a failed decrypted message stored, and now
                 // we have a properly decrypted version of the same message.
                 // See issue: https://github.com/conversejs/converse.js/issues/2733#issuecomment-1035493594
@@ -182,7 +181,7 @@ export default function ModelWithMessages(BaseModel) {
             } else {
                 return {
                     is_archived: attrs.is_archived,
-                    time: attrs.time ? attrs.time : message.get("time"),
+                    time: attrs.time ? attrs.time : message.get('time'),
                 };
             }
         }
@@ -210,7 +209,7 @@ export default function ModelWithMessages(BaseModel) {
             }
 
             let query;
-            if (attrs.type === "groupchat") {
+            if (attrs.type === 'groupchat') {
                 const { occupant_id, replace_id } = /** @type {MUCMessageAttributes} */ (attrs);
                 query = occupant_id
                     ? ({ attributes: m }) => m.msgid === replace_id && m.occupant_id == occupant_id
@@ -223,26 +222,26 @@ export default function ModelWithMessages(BaseModel) {
 
             const message = this.messages.models.find(query);
             if (!message) {
-                attrs["older_versions"] = {};
+                attrs['older_versions'] = {};
                 return await this.createMessage(attrs); // eslint-disable-line no-return-await
             }
 
-            const older_versions = message.get("older_versions") || {};
-            if (attrs.time < message.get("time") && message.get("edited")) {
+            const older_versions = message.get('older_versions') || {};
+            if (attrs.time < message.get('time') && message.get('edited')) {
                 // This is an older message which has been corrected afterwards
-                older_versions[attrs.time] = attrs["message"];
-                message.save({ "older_versions": older_versions });
+                older_versions[attrs.time] = attrs['message'];
+                message.save({ 'older_versions': older_versions });
             } else {
                 // This is a correction of an earlier message we already received
                 if (Object.keys(older_versions).length) {
-                    older_versions[message.get("edited")] = message.getMessageText();
+                    older_versions[message.get('edited')] = message.getMessageText();
                 } else {
-                    older_versions[message.get("time")] = message.getMessageText();
+                    older_versions[message.get('time')] = message.getMessageText();
                 }
                 attrs = Object.assign(attrs, { older_versions });
-                delete attrs["msgid"]; // We want to keep the msgid of the original message
-                delete attrs["id"]; // Delete id, otherwise a new cache entry gets created
-                attrs["time"] = message.get("time");
+                delete attrs['msgid']; // We want to keep the msgid of the original message
+                delete attrs['id']; // Delete id, otherwise a new cache entry gets created
+                attrs['time'] = message.get('time');
                 message.save(attrs);
             }
             return message;
@@ -264,7 +263,7 @@ export default function ModelWithMessages(BaseModel) {
          * @return {Promise<MessageAttributes>}
          */
         async getOutgoingMessageAttributes(_attrs) {
-            throw new MethodNotImplementedError("getOutgoingMessageAttributes is not implemented");
+            throw new errors.MethodNotImplementedError('getOutgoingMessageAttributes is not implemented');
         }
 
         /**
@@ -279,19 +278,19 @@ export default function ModelWithMessages(BaseModel) {
             await converse.emojis?.initialized_promise;
 
             if (!this.canPostMessages()) {
-                log.warn("sendMessage was called but canPostMessages is false");
+                log.warn('sendMessage was called but canPostMessages is false');
                 return;
             }
 
             attrs = await this.getOutgoingMessageAttributes(attrs);
-            let message = this.messages.findWhere("correcting");
+            let message = this.messages.findWhere('correcting');
             if (message) {
-                const older_versions = message.get("older_versions") || {};
-                const edited_time = message.get("edited") || message.get("time");
+                const older_versions = message.get('older_versions') || {};
+                const edited_time = message.get('edited') || message.get('time');
                 older_versions[edited_time] = message.getMessageText();
 
                 message.save({
-                    ...["body", "is_only_emojis", "media_urls", "references", "is_encrypted"].reduce((obj, k) => {
+                    ...['body', 'is_only_emojis', 'media_urls', 'references', 'is_encrypted'].reduce((obj, k) => {
                         if (attrs.hasOwnProperty(k)) obj[k] = attrs[k];
                         return obj;
                     }, {}),
@@ -328,7 +327,7 @@ export default function ModelWithMessages(BaseModel) {
              * @property {(ChatBox|MUC)} data.chatbox
              * @property {(BaseMessage)} data.message
              */
-            api.trigger("sendMessage", { "chatbox": this, message });
+            api.trigger('sendMessage', { 'chatbox': this, message });
             return message;
         }
 
@@ -338,13 +337,13 @@ export default function ModelWithMessages(BaseModel) {
          */
         retractOwnMessage(message) {
             const retraction_id = u.getUniqueId();
-            sendRetractionMessage(this.get("jid"), message, retraction_id);
+            sendRetractionMessage(this.get('jid'), message, retraction_id);
             message.save({
-                "retracted": new Date().toISOString(),
-                "retracted_id": message.get("origin_id"),
-                "retraction_id": retraction_id,
-                "is_ephemeral": true,
-                "editable": false,
+                'retracted': new Date().toISOString(),
+                'retracted_id': message.get('origin_id'),
+                'retraction_id': retraction_id,
+                'is_ephemeral': true,
+                'editable': false,
             });
         }
 
@@ -353,27 +352,27 @@ export default function ModelWithMessages(BaseModel) {
          */
         async sendFiles(files) {
             const { __, session } = _converse;
-            const result = await api.disco.features.get(Strophe.NS.HTTPUPLOAD, session.get("domain"));
+            const result = await api.disco.features.get(Strophe.NS.HTTPUPLOAD, session.get('domain'));
             const item = result.pop();
             if (!item) {
                 this.createMessage({
-                    "message": __("Sorry, looks like file upload is not supported by your server."),
-                    "type": "error",
-                    "is_ephemeral": true,
+                    'message': __('Sorry, looks like file upload is not supported by your server.'),
+                    'type': 'error',
+                    'is_ephemeral': true,
                 });
                 return;
             }
             const data = item.dataforms
-                .where({ "FORM_TYPE": { "value": Strophe.NS.HTTPUPLOAD, "type": "hidden" } })
+                .where({ 'FORM_TYPE': { 'value': Strophe.NS.HTTPUPLOAD, 'type': 'hidden' } })
                 .pop();
-            const max_file_size = parseInt((data?.attributes || {})["max-file-size"]?.value, 10);
+            const max_file_size = parseInt((data?.attributes || {})['max-file-size']?.value, 10);
             const slot_request_url = item?.id;
 
             if (!slot_request_url) {
                 this.createMessage({
-                    "message": __("Sorry, looks like file upload is not supported by your server."),
-                    "type": "error",
-                    "is_ephemeral": true,
+                    'message': __('Sorry, looks like file upload is not supported by your server.'),
+                    'type': 'error',
+                    'is_ephemeral': true,
                 });
                 return;
             }
@@ -385,33 +384,33 @@ export default function ModelWithMessages(BaseModel) {
                  * @param {ChatBox|MUC} chat - The chat from which this file will be uploaded.
                  * @param {File} file - The file that will be uploaded
                  */
-                file = await api.hook("beforeFileUpload", this, file);
+                file = await api.hook('beforeFileUpload', this, file);
 
                 if (!isNaN(max_file_size) && file.size > max_file_size) {
                     const size = filesize(max_file_size);
                     const message = Array.isArray(size)
-                        ? __("The size of your file, %1$s, exceeds the maximum allowed by your server.", file.name)
+                        ? __('The size of your file, %1$s, exceeds the maximum allowed by your server.', file.name)
                         : __(
-                              "The size of your file, %1$s, exceeds the maximum allowed by your server, which is %2$s.",
+                              'The size of your file, %1$s, exceeds the maximum allowed by your server, which is %2$s.',
                               file.name,
                               size
                           );
                     return this.createMessage({
                         message,
-                        type: "error",
+                        type: 'error',
                         is_ephemeral: true,
                     });
                 } else {
                     const initial_attrs = await this.getOutgoingMessageAttributes();
                     const attrs = Object.assign(initial_attrs, {
-                        "file": true,
-                        "progress": 0,
-                        "slot_request_url": slot_request_url,
+                        'file': true,
+                        'progress': 0,
+                        'slot_request_url': slot_request_url,
                     });
                     this.setEditable(attrs, new Date().toISOString());
-                    const message = await this.createMessage(attrs, { "silent": true });
+                    const message = await this.createMessage(attrs, { 'silent': true });
                     message.file = file;
-                    this.messages.trigger("add", message);
+                    this.messages.trigger('add', message);
                     message.getRequestSlotURL();
                 }
             });
@@ -426,15 +425,15 @@ export default function ModelWithMessages(BaseModel) {
          * @param {String} send_time - time when the message was sent
          */
         setEditable(attrs, send_time) {
-            if (attrs.is_headline || u.isEmptyMessage(attrs) || attrs.sender !== "me") {
+            if (attrs.is_headline || u.isEmptyMessage(attrs) || attrs.sender !== 'me') {
                 return;
             }
-            if (api.settings.get("allow_message_corrections") === "all") {
-                attrs.editable = !(attrs.file || attrs.retracted || "oob_url" in attrs);
-            } else if (api.settings.get("allow_message_corrections") === "last" && send_time > this.get("time_sent")) {
-                this.set({ "time_sent": send_time });
-                this.messages.findWhere({ "editable": true })?.save({ "editable": false });
-                attrs.editable = !(attrs.file || attrs.retracted || "oob_url" in attrs);
+            if (api.settings.get('allow_message_corrections') === 'all') {
+                attrs.editable = !(attrs.file || attrs.retracted || 'oob_url' in attrs);
+            } else if (api.settings.get('allow_message_corrections') === 'last' && send_time > this.get('time_sent')) {
+                this.set({ 'time_sent': send_time });
+                this.messages.findWhere({ 'editable': true })?.save({ 'editable': false });
+                attrs.editable = !(attrs.file || attrs.retracted || 'oob_url' in attrs);
             }
         }
 
@@ -466,7 +465,7 @@ export default function ModelWithMessages(BaseModel) {
                     constants.INACTIVE
                 );
             }
-            this.set("chat_state", state, options);
+            this.set('chat_state', state, options);
             return this;
         }
 
@@ -475,8 +474,8 @@ export default function ModelWithMessages(BaseModel) {
          */
         onMessageAdded(message) {
             if (
-                api.settings.get("prune_messages_above") &&
-                (api.settings.get("pruning_behavior") === "scrolled" || !this.ui.get("scrolled")) &&
+                api.settings.get('prune_messages_above') &&
+                (api.settings.get('pruning_behavior') === 'scrolled' || !this.ui.get('scrolled')) &&
                 !u.isEmptyMessage(message)
             ) {
                 this.debouncedPruneHistory();
@@ -487,11 +486,11 @@ export default function ModelWithMessages(BaseModel) {
          * @param {BaseMessage} message
          */
         async onMessageUploadChanged(message) {
-            if (message.get("upload") === constants.SUCCESS) {
+            if (message.get('upload') === constants.SUCCESS) {
                 const attrs = {
-                    "body": message.get("body"),
-                    "spoiler_hint": message.get("spoiler_hint"),
-                    "oob_url": message.get("oob_url"),
+                    'body': message.get('body'),
+                    'spoiler_hint': message.get('spoiler_hint'),
+                    'oob_url': message.get('oob_url'),
                 };
                 await this.sendMessage(attrs);
                 message.destroy();
@@ -510,7 +509,7 @@ export default function ModelWithMessages(BaseModel) {
         }
 
         onScrolledChanged() {
-            if (!this.ui.get("scrolled")) {
+            if (!this.ui.get('scrolled')) {
                 this.clearUnreadMsgCounter();
                 this.pruneHistoryWhenScrolledDown();
             }
@@ -518,9 +517,9 @@ export default function ModelWithMessages(BaseModel) {
 
         pruneHistoryWhenScrolledDown() {
             if (
-                api.settings.get("prune_messages_above") &&
-                api.settings.get("pruning_behavior") === "unscrolled" &&
-                !this.ui.get("scrolled")
+                api.settings.get('prune_messages_above') &&
+                api.settings.get('pruning_behavior') === 'unscrolled' &&
+                !this.ui.get('scrolled')
             ) {
                 this.debouncedPruneHistory();
             }
@@ -548,7 +547,7 @@ export default function ModelWithMessages(BaseModel) {
             try {
                 await this.messages.clearStore();
             } catch (e) {
-                this.messages.trigger("reset");
+                this.messages.trigger('reset');
                 log.error(e);
             } finally {
                 // No point in fetching messages from the cache if it's been cleared.
@@ -559,13 +558,13 @@ export default function ModelWithMessages(BaseModel) {
 
         editEarlierMessage() {
             let message;
-            let idx = this.messages.findLastIndex("correcting");
+            let idx = this.messages.findLastIndex('correcting');
             if (idx >= 0) {
-                this.messages.at(idx).save("correcting", false);
+                this.messages.at(idx).save('correcting', false);
                 while (idx > 0) {
                     idx -= 1;
                     const candidate = this.messages.at(idx);
-                    if (candidate.get("editable")) {
+                    if (candidate.get('editable')) {
                         message = candidate;
                         break;
                     }
@@ -574,24 +573,24 @@ export default function ModelWithMessages(BaseModel) {
             message =
                 message ||
                 this.messages
-                    .filter({ sender: "me" })
+                    .filter({ sender: 'me' })
                     .reverse()
-                    .find((m) => m.get("editable"));
+                    .find((m) => m.get('editable'));
 
-            message?.save("correcting", true);
+            message?.save('correcting', true);
         }
 
         editLaterMessage() {
             let message;
-            let idx = this.messages.findLastIndex("correcting");
+            let idx = this.messages.findLastIndex('correcting');
             if (idx >= 0) {
-                this.messages.at(idx).save("correcting", false);
+                this.messages.at(idx).save('correcting', false);
                 while (idx < this.messages.length - 1) {
                     idx += 1;
                     const candidate = this.messages.at(idx);
-                    if (candidate.get("editable")) {
+                    if (candidate.get('editable')) {
                         message = candidate;
-                        message.save("correcting", true);
+                        message.save('correcting', true);
                         break;
                     }
                 }
@@ -599,19 +598,31 @@ export default function ModelWithMessages(BaseModel) {
             return message;
         }
 
+        /**
+         * Used by sub-classes to indicate wether a message is a chat
+         * message, as opposed to error or info messages.
+         * @param {BaseMessage} _message
+         * @returns {boolean}
+         */
+        isChatMessage(_message) {
+            throw new errors.MethodNotImplementedError();
+        }
+
+        /** @returns {BaseMessage} */
         getOldestMessage() {
             for (let i = 0; i < this.messages.length; i++) {
                 const message = this.messages.at(i);
-                if (message.get("type") === this.get("message_type")) {
+                if (this.isChatMessage(message)) {
                     return message;
                 }
             }
         }
 
+        /** @returns {BaseMessage} */
         getMostRecentMessage() {
             for (let i = this.messages.length - 1; i >= 0; i--) {
                 const message = this.messages.at(i);
-                if (message.get("type") === this.get("message_type")) {
+                if (this.isChatMessage(message)) {
                     return message;
                 }
             }
@@ -624,7 +635,7 @@ export default function ModelWithMessages(BaseModel) {
          */
         getMessageReferencedByError(attrs) {
             const id = attrs.msgid;
-            return id && this.messages.models.find((m) => [m.get("msgid"), m.get("retraction_id")].includes(id));
+            return id && this.messages.models.find((m) => [m.get('msgid'), m.get('retraction_id')].includes(id));
         }
 
         /**
@@ -642,7 +653,7 @@ export default function ModelWithMessages(BaseModel) {
             }
             // Only look for dangling retractions if there are newer
             // messages than this one, since retractions come after.
-            if (this.messages.last().get("time") > attrs.time) {
+            if (this.messages.last().get('time') > attrs.time) {
                 // Search from latest backwards
                 const messages = Array.from(this.messages.models);
                 messages.reverse();
@@ -687,9 +698,9 @@ export default function ModelWithMessages(BaseModel) {
          * @param {object} attrs - Attributes representing a received
          */
         getStanzaIdQueryAttrs(attrs) {
-            const keys = Object.keys(attrs).filter((k) => k.startsWith("stanza_id "));
+            const keys = Object.keys(attrs).filter((k) => k.startsWith('stanza_id '));
             return keys.map((key) => {
-                const by_jid = key.replace(/^stanza_id /, "");
+                const by_jid = key.replace(/^stanza_id /, '');
                 const query = {};
                 query[`stanza_id ${by_jid}`] = attrs[key];
                 return query;
@@ -709,7 +720,7 @@ export default function ModelWithMessages(BaseModel) {
                 if (!attrs.is_encrypted && attrs.body) {
                     // We can't match the message if it's a reflected
                     // encrypted message (e.g. via MAM or in a MUC)
-                    query["body"] = attrs.body;
+                    query['body'] = attrs.body;
                 }
                 return query;
             }
@@ -722,22 +733,22 @@ export default function ModelWithMessages(BaseModel) {
          * @param {boolean} [force=false] - Whether a marker should be sent for the
          *  message, even if it didn't include a `markable` element.
          */
-        async sendMarkerForMessage(msg, type = "displayed", force = false) {
-            if (!msg || msg?.get("type") === "groupchat" || !api.settings.get("send_chat_markers").includes(type)) {
+        async sendMarkerForMessage(msg, type = 'displayed', force = false) {
+            if (!msg || msg?.get('type') === 'groupchat' || !api.settings.get('send_chat_markers').includes(type)) {
                 return;
             }
 
             // Don't send chat markers to contacts that are not subscribed
             // to our presence.
-            const contact = await api.contacts.get(this.get("jid"));
-            const subscription = contact?.get("subscription");
-            if (!contact || subscription === "none" || subscription === "to") {
+            const contact = await api.contacts.get(this.get('jid'));
+            const subscription = contact?.get('subscription');
+            if (!contact || subscription === 'none' || subscription === 'to') {
                 return;
             }
 
-            if (msg?.get("is_markable") || force) {
-                const from_jid = Strophe.getBareJidFromJid(msg.get("from"));
-                sendMarker(from_jid, msg.get("msgid"), type, msg.get("type"));
+            if (msg?.get('is_markable') || force) {
+                const from_jid = Strophe.getBareJidFromJid(msg.get('from'));
+                sendMarker(from_jid, msg.get('msgid'), type, msg.get('type'));
             }
         }
 
@@ -747,17 +758,17 @@ export default function ModelWithMessages(BaseModel) {
          * @param {BaseMessage} message
          */
         handleUnreadMessage(message) {
-            if (!message?.get("body")) {
+            if (!message?.get('body')) {
                 return;
             }
 
             if (isNewMessage(message)) {
-                if (message.get("sender") === "me") {
+                if (message.get('sender') === 'me') {
                     // We remove the "scrolled" flag so that the chat area
                     // gets scrolled down. We always want to scroll down
                     // when the user writes a message as opposed to when a
                     // message is received.
-                    this.ui.set("scrolled", false);
+                    this.ui.set('scrolled', false);
                 } else if (this.isHidden()) {
                     this.incrementUnreadMsgsCounter(message);
                 } else {
@@ -780,28 +791,28 @@ export default function ModelWithMessages(BaseModel) {
                 error_type: attrs.error_type,
                 is_error: true,
             };
-            if (attrs.msgid === message.get("retraction_id")) {
+            if (attrs.msgid === message.get('retraction_id')) {
                 // The error message refers to a retraction
                 new_attrs.retraction_id = undefined;
                 if (!attrs.error) {
-                    if (attrs.error_condition === "forbidden") {
+                    if (attrs.error_condition === 'forbidden') {
                         new_attrs.error = __("You're not allowed to retract your message.");
                     } else {
-                        new_attrs.error = __("Sorry, an error occurred while trying to retract your message.");
+                        new_attrs.error = __('Sorry, an error occurred while trying to retract your message.');
                     }
                 }
             } else if (!attrs.error) {
-                if (attrs.error_condition === "forbidden") {
+                if (attrs.error_condition === 'forbidden') {
                     new_attrs.error = __("You're not allowed to send a message.");
                 } else {
-                    new_attrs.error = __("Sorry, an error occurred while trying to send your message.");
+                    new_attrs.error = __('Sorry, an error occurred while trying to send your message.');
                 }
             }
             /**
              * *Hook* which allows plugins to add application-specific attributes
              * @event _converse#getErrorAttributesForMessage
              */
-            return await api.hook("getErrorAttributesForMessage", attrs, new_attrs);
+            return await api.hook('getErrorAttributesForMessage', attrs, new_attrs);
         }
 
         /**
@@ -810,7 +821,7 @@ export default function ModelWithMessages(BaseModel) {
         async handleErrorMessageStanza(stanza) {
             const attrs_or_error = await parseMessage(stanza);
             if (u.isErrorObject(attrs_or_error)) {
-                const { stanza, message } = /** @type {StanzaParseError} */ (attrs_or_error);
+                const { stanza, message } = /** @type {errors.StanzaParseError} */ (attrs_or_error);
                 if (stanza) log.error(stanza);
                 return log.error(message);
             }
@@ -834,16 +845,16 @@ export default function ModelWithMessages(BaseModel) {
          */
         incrementUnreadMsgsCounter(message) {
             const settings = {
-                "num_unread": this.get("num_unread") + 1,
+                'num_unread': this.get('num_unread') + 1,
             };
-            if (this.get("num_unread") === 0) {
-                settings["first_unread_id"] = message.get("id");
+            if (this.get('num_unread') === 0) {
+                settings['first_unread_id'] = message.get('id');
             }
             this.save(settings);
         }
 
         clearUnreadMsgCounter() {
-            if (this.get("num_unread") > 0) {
+            if (this.get('num_unread') > 0) {
                 this.sendMarkerForMessage(this.messages.last());
             }
             u.safeSave(this, { num_unread: 0 });
@@ -857,19 +868,19 @@ export default function ModelWithMessages(BaseModel) {
          *  whether a message was retracted or not.
          */
         async handleRetraction(attrs) {
-            const RETRACTION_ATTRIBUTES = ["retracted", "retracted_id", "editable"];
+            const RETRACTION_ATTRIBUTES = ['retracted', 'retracted_id', 'editable'];
             if (attrs.retracted) {
                 if (attrs.is_tombstone) return false;
 
                 for (const m of this.messages.models) {
-                    if (m.get("from") !== attrs.from) continue;
-                    if (m.get("origin_id") === attrs.retracted_id || m.get("msgid") === attrs.retracted_id) {
+                    if (m.get('from') !== attrs.from) continue;
+                    if (m.get('origin_id') === attrs.retracted_id || m.get('msgid') === attrs.retracted_id) {
                         m.save(pick(attrs, RETRACTION_ATTRIBUTES));
                         return true;
                     }
                 }
 
-                attrs["dangling_retraction"] = true;
+                attrs['dangling_retraction'] = true;
                 await this.createMessage(attrs);
                 return true;
             } else {
@@ -878,7 +889,7 @@ export default function ModelWithMessages(BaseModel) {
                 if (message) {
                     const retraction_attrs = pick(message.attributes, RETRACTION_ATTRIBUTES);
                     const new_attrs = Object.assign({ dangling_retraction: false }, attrs, retraction_attrs);
-                    delete new_attrs["id"]; // Delete id, otherwise a new cache entry gets created
+                    delete new_attrs['id']; // Delete id, otherwise a new cache entry gets created
                     message.save(new_attrs);
                     return true;
                 }
@@ -890,13 +901,13 @@ export default function ModelWithMessages(BaseModel) {
          * @param {MessageAttributes} attrs
          */
         handleReceipt(attrs) {
-            if (attrs.sender === "them") {
+            if (attrs.sender === 'them') {
                 if (attrs.is_valid_receipt_request) {
                     sendReceiptStanza(attrs.from, attrs.msgid);
                 } else if (attrs.receipt_id) {
-                    const message = this.messages.findWhere({ "msgid": attrs.receipt_id });
-                    if (message && !message.get("received")) {
-                        message.save({ "received": new Date().toISOString() });
+                    const message = this.messages.findWhere({ 'msgid': attrs.receipt_id });
+                    if (message && !message.get('received')) {
+                        message.save({ 'received': new Date().toISOString() });
                     }
                     return true;
                 }
@@ -925,15 +936,15 @@ export default function ModelWithMessages(BaseModel) {
 
             const stanza = stx`
                 <message xmlns="jabber:client"
-                        from="${message.get("type") === "groupchat" ? api.connection.get().jid : message.get("from")}"
-                        to="${message.get("to") || this.get("jid")}"
-                        type="${this.get("message_type")}"
+                        from="${message.get('type') === 'groupchat' ? api.connection.get().jid : message.get('from')}"
+                        to="${message.get('to') || this.get('jid')}"
+                        type="${this.get('message_type')}"
                         id="${(edited && u.getUniqueId()) || msgid}">
-                    ${body ? stx`<body>${body}</body>` : ""}
+                    ${body ? stx`<body>${body}</body>` : ''}
                     <active xmlns="${Strophe.NS.CHATSTATES}"/>
-                    ${type === "chat" ? stx`<request xmlns="${Strophe.NS.RECEIPTS}"></request>` : ""}
-                    ${!is_encrypted && oob_url ? stx`<x xmlns="${Strophe.NS.OUTOFBAND}"><url>${oob_url}</url></x>` : ""}
-                    ${!is_encrypted && is_spoiler ? stx`<spoiler xmlns="${Strophe.NS.SPOILER}">${spoiler_hint ?? ""}</spoiler>` : ""}
+                    ${type === 'chat' ? stx`<request xmlns="${Strophe.NS.RECEIPTS}"></request>` : ''}
+                    ${!is_encrypted && oob_url ? stx`<x xmlns="${Strophe.NS.OUTOFBAND}"><url>${oob_url}</url></x>` : ''}
+                    ${!is_encrypted && is_spoiler ? stx`<spoiler xmlns="${Strophe.NS.SPOILER}">${spoiler_hint ?? ''}</spoiler>` : ''}
                     ${
                         !is_encrypted
                             ? references?.map(
@@ -943,10 +954,10 @@ export default function ModelWithMessages(BaseModel) {
                                                 type="${ref.type}"
                                                 uri="${ref.uri}"></reference>`
                               )
-                            : ""
+                            : ''
                     }
-                    ${edited ? stx`<replace xmlns="${Strophe.NS.MESSAGE_CORRECT}" id="${msgid}"></replace>` : ""}
-                    ${origin_id ? stx`<origin-id xmlns="${Strophe.NS.SID}" id="${origin_id}"></origin-id>` : ""}
+                    ${edited ? stx`<replace xmlns="${Strophe.NS.MESSAGE_CORRECT}" id="${msgid}"></replace>` : ''}
+                    ${origin_id ? stx`<origin-id xmlns="${Strophe.NS.SID}" id="${origin_id}"></origin-id>` : ''}
                 </message>`;
 
             /**
@@ -962,7 +973,7 @@ export default function ModelWithMessages(BaseModel) {
              *      You can use the Strophe.Builder functions to extend the stanza.
              *      See http://strophe.im/strophejs/doc/1.4.3/files/strophe-umd-js.html#Strophe.Builder.Functions
              */
-            const data = await api.hook("createMessageStanza", this, { message, stanza });
+            const data = await api.hook('createMessageStanza', this, { message, stanza });
             return data.stanza;
         }
 
@@ -971,8 +982,8 @@ export default function ModelWithMessages(BaseModel) {
          * number of messages specified in the settings.
          */
         pruneHistory() {
-            const max_history = api.settings.get("prune_messages_above");
-            if (max_history && typeof max_history === "number") {
+            const max_history = api.settings.get('prune_messages_above');
+            if (max_history && typeof max_history === 'number') {
                 if (this.messages.length > max_history) {
                     const non_empty_messages = this.messages.filter((m) => !u.isEmptyMessage(m));
                     if (non_empty_messages.length > max_history) {
@@ -984,7 +995,7 @@ export default function ModelWithMessages(BaseModel) {
                          * once older messages have been removed to keep the
                          * number of messages below the value set in `prune_messages_above`.
                          */
-                        this.trigger("historyPruned");
+                        this.trigger('historyPruned');
                     }
                 }
             }
@@ -992,7 +1003,7 @@ export default function ModelWithMessages(BaseModel) {
         debouncedPruneHistory = debounce(() => this.pruneHistory(), 500, { maxWait: 2000 });
 
         isScrolledUp() {
-            return this.ui.get("scrolled");
+            return this.ui.get('scrolled');
         }
 
         /**
@@ -1001,7 +1012,7 @@ export default function ModelWithMessages(BaseModel) {
          * @returns {boolean}
          */
         isHidden() {
-            return this.get("hidden") || this.isScrolledUp() || document.hidden;
+            return this.get('hidden') || this.isScrolledUp() || document.hidden;
         }
     };
 }

+ 22 - 17
src/headless/types/plugins/chat/model.d.ts

@@ -87,32 +87,33 @@ declare const ChatBox_base: {
         fetchMessages(): any;
         afterMessagesFetched(): void;
         onMessage(_attrs_or_error: import("../../shared/types").MessageAttributes | Error): Promise<void>;
-        getUpdatedMessageAttributes(message: import("../../index.js").BaseMessage<any>, attrs: import("../../shared/types").MessageAttributes): object;
-        updateMessage(message: import("../../index.js").BaseMessage<any>, attrs: import("../../shared/types").MessageAttributes): void;
-        handleCorrection(attrs: import("../../shared/types").MessageAttributes | import("../muc/types.js").MUCMessageAttributes): Promise<import("../../index.js").BaseMessage<any> | void>;
+        getUpdatedMessageAttributes(message: import("../../shared/message").default<any>, attrs: import("../../shared/types").MessageAttributes): object;
+        updateMessage(message: import("../../shared/message").default<any>, attrs: import("../../shared/types").MessageAttributes): void;
+        handleCorrection(attrs: import("../../shared/types").MessageAttributes | import("../muc/types.js").MUCMessageAttributes): Promise<import("../../shared/message").default<any> | void>;
         queueMessage(attrs: import("../../shared/types").MessageAttributes): any;
         msg_chain: any;
         getOutgoingMessageAttributes(_attrs?: import("../../shared/types").MessageAttributes): Promise<import("../../shared/types").MessageAttributes>;
-        sendMessage(attrs?: any): Promise<import("../../index.js").BaseMessage<any>>;
-        retractOwnMessage(message: import("../../index.js").BaseMessage<any>): void;
+        sendMessage(attrs?: any): Promise<import("../../shared/message").default<any>>;
+        retractOwnMessage(message: import("../../shared/message").default<any>): void;
         sendFiles(files: File[]): Promise<void>;
         setEditable(attrs: any, send_time: string): void;
         setChatState(state: string, options?: object): any;
         chat_state_timeout: NodeJS.Timeout;
-        onMessageAdded(message: import("../../index.js").BaseMessage<any>): void;
-        onMessageUploadChanged(message: import("../../index.js").BaseMessage<any>): Promise<void>;
-        onMessageCorrecting(message: import("../../index.js").BaseMessage<any>): void;
+        onMessageAdded(message: import("../../shared/message").default<any>): void;
+        onMessageUploadChanged(message: import("../../shared/message").default<any>): Promise<void>;
+        onMessageCorrecting(message: import("../../shared/message").default<any>): void;
         onScrolledChanged(): void;
         pruneHistoryWhenScrolledDown(): void;
         shouldShowErrorMessage(attrs: import("../../shared/types").MessageAttributes): Promise<boolean>;
         clearMessages(): Promise<void>;
         editEarlierMessage(): void;
         editLaterMessage(): any;
-        getOldestMessage(): any;
-        getMostRecentMessage(): any;
+        isChatMessage(_message: import("../../shared/message").default<any>): boolean;
+        getOldestMessage(): import("../../shared/message").default<any>;
+        getMostRecentMessage(): import("../../shared/message").default<any>;
         getMessageReferencedByError(attrs: object): any;
-        findDanglingRetraction(attrs: object): import("../../index.js").BaseMessage<any> | null;
-        getDuplicateMessage(attrs: object): import("../../index.js").BaseMessage<any>;
+        findDanglingRetraction(attrs: object): import("../../shared/message").default<any> | null;
+        getDuplicateMessage(attrs: object): import("../../shared/message").default<any>;
         getOriginIdQueryAttrs(attrs: object): {
             origin_id: any;
             from: any;
@@ -122,15 +123,15 @@ declare const ChatBox_base: {
             from: any;
             msgid: any;
         };
-        sendMarkerForMessage(msg: import("../../index.js").BaseMessage<any>, type?: ("received" | "displayed" | "acknowledged"), force?: boolean): Promise<void>;
-        handleUnreadMessage(message: import("../../index.js").BaseMessage<any>): void;
-        getErrorAttributesForMessage(message: import("../../index.js").BaseMessage<any>, attrs: import("../../shared/types").MessageAttributes): Promise<any>;
+        sendMarkerForMessage(msg: import("../../shared/message").default<any>, type?: ("received" | "displayed" | "acknowledged"), force?: boolean): Promise<void>;
+        handleUnreadMessage(message: import("../../shared/message").default<any>): void;
+        getErrorAttributesForMessage(message: import("../../shared/message").default<any>, attrs: import("../../shared/types").MessageAttributes): Promise<any>;
         handleErrorMessageStanza(stanza: Element): Promise<void>;
-        incrementUnreadMsgsCounter(message: import("../../index.js").BaseMessage<any>): void;
+        incrementUnreadMsgsCounter(message: import("../../shared/message").default<any>): void;
         clearUnreadMsgCounter(): void;
         handleRetraction(attrs: import("../../shared/types").MessageAttributes): Promise<boolean>;
         handleReceipt(attrs: import("../../shared/types").MessageAttributes): boolean;
-        createMessageStanza(message: import("../../index.js").BaseMessage<any>): Promise<any>;
+        createMessageStanza(message: import("../../shared/message").default<any>): Promise<any>;
         pruneHistory(): void;
         debouncedPruneHistory: import("lodash").DebouncedFunc<() => void>;
         isScrolledUp(): any;
@@ -402,6 +403,10 @@ declare class ChatBox extends ChatBox_base {
      */
     getOutgoingMessageAttributes(attrs?: import("../../shared/types").MessageAttributes): Promise<import("../../shared/types").MessageAttributes>;
     canPostMessages(): boolean;
+    /**
+     * @param {import('../../shared/message').default} message
+     */
+    isChatMessage(message: import("../../shared/message").default<any>): boolean;
 }
 import ChatBoxBase from '../../shared/chatbox.js';
 //# sourceMappingURL=model.d.ts.map

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

@@ -108,8 +108,9 @@ declare const MUC_base: {
         clearMessages(): Promise<void>;
         editEarlierMessage(): void;
         editLaterMessage(): any;
-        getOldestMessage(): any;
-        getMostRecentMessage(): any;
+        isChatMessage(_message: import("../../shared/message.js").default<any>): boolean;
+        getOldestMessage(): import("../../shared/message.js").default<any>;
+        getMostRecentMessage(): import("../../shared/message.js").default<any>;
         getMessageReferencedByError(attrs: object): any;
         findDanglingRetraction(attrs: object): import("../../shared/message.js").default<any> | null;
         getDuplicateMessage(attrs: object): import("../../shared/message.js").default<any>;
@@ -484,6 +485,10 @@ declare class MUC extends MUC_base {
     }): Promise<void>;
     canModerateMessages(): any;
     canPostMessages(): boolean;
+    /**
+     * @param {import('../../shared/message').default} message
+     */
+    isChatMessage(message: import("../../shared/message.js").default<any>): boolean;
     /**
      * Return an array of unique nicknames based on all occupants and messages in this MUC.
      * @returns {String[]}

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

@@ -87,32 +87,33 @@ declare const MUCOccupant_base: {
         fetchMessages(): any;
         afterMessagesFetched(): void;
         onMessage(_attrs_or_error: import("../../shared/types").MessageAttributes | Error): Promise<void>;
-        getUpdatedMessageAttributes(message: import("../../index.js").BaseMessage<any>, attrs: import("../../shared/types").MessageAttributes): object;
-        updateMessage(message: import("../../index.js").BaseMessage<any>, attrs: import("../../shared/types").MessageAttributes): void;
-        handleCorrection(attrs: import("../../shared/types").MessageAttributes | import("./types.js").MUCMessageAttributes): Promise<import("../../index.js").BaseMessage<any> | void>;
+        getUpdatedMessageAttributes(message: import("../../shared/message").default<any>, attrs: import("../../shared/types").MessageAttributes): object;
+        updateMessage(message: import("../../shared/message").default<any>, attrs: import("../../shared/types").MessageAttributes): void;
+        handleCorrection(attrs: import("../../shared/types").MessageAttributes | import("./types.js").MUCMessageAttributes): Promise<import("../../shared/message").default<any> | void>;
         queueMessage(attrs: import("../../shared/types").MessageAttributes): any;
         msg_chain: any;
         getOutgoingMessageAttributes(_attrs?: import("../../shared/types").MessageAttributes): Promise<import("../../shared/types").MessageAttributes>;
-        sendMessage(attrs?: any): Promise<import("../../index.js").BaseMessage<any>>;
-        retractOwnMessage(message: import("../../index.js").BaseMessage<any>): void;
+        sendMessage(attrs?: any): Promise<import("../../shared/message").default<any>>;
+        retractOwnMessage(message: import("../../shared/message").default<any>): void;
         sendFiles(files: File[]): Promise<void>;
         setEditable(attrs: any, send_time: string): void;
         setChatState(state: string, options?: object): any;
         chat_state_timeout: NodeJS.Timeout;
-        onMessageAdded(message: import("../../index.js").BaseMessage<any>): void;
-        onMessageUploadChanged(message: import("../../index.js").BaseMessage<any>): Promise<void>;
-        onMessageCorrecting(message: import("../../index.js").BaseMessage<any>): void;
+        onMessageAdded(message: import("../../shared/message").default<any>): void;
+        onMessageUploadChanged(message: import("../../shared/message").default<any>): Promise<void>;
+        onMessageCorrecting(message: import("../../shared/message").default<any>): void;
         onScrolledChanged(): void;
         pruneHistoryWhenScrolledDown(): void;
         shouldShowErrorMessage(attrs: import("../../shared/types").MessageAttributes): Promise<boolean>;
         clearMessages(): Promise<void>;
         editEarlierMessage(): void;
         editLaterMessage(): any;
-        getOldestMessage(): any;
-        getMostRecentMessage(): any;
+        isChatMessage(_message: import("../../shared/message").default<any>): boolean;
+        getOldestMessage(): import("../../shared/message").default<any>;
+        getMostRecentMessage(): import("../../shared/message").default<any>;
         getMessageReferencedByError(attrs: object): any;
-        findDanglingRetraction(attrs: object): import("../../index.js").BaseMessage<any> | null;
-        getDuplicateMessage(attrs: object): import("../../index.js").BaseMessage<any>;
+        findDanglingRetraction(attrs: object): import("../../shared/message").default<any> | null;
+        getDuplicateMessage(attrs: object): import("../../shared/message").default<any>;
         getOriginIdQueryAttrs(attrs: object): {
             origin_id: any;
             from: any;
@@ -122,15 +123,15 @@ declare const MUCOccupant_base: {
             from: any;
             msgid: any;
         };
-        sendMarkerForMessage(msg: import("../../index.js").BaseMessage<any>, type?: ("received" | "displayed" | "acknowledged"), force?: boolean): Promise<void>;
-        handleUnreadMessage(message: import("../../index.js").BaseMessage<any>): void;
-        getErrorAttributesForMessage(message: import("../../index.js").BaseMessage<any>, attrs: import("../../shared/types").MessageAttributes): Promise<any>;
+        sendMarkerForMessage(msg: import("../../shared/message").default<any>, type?: ("received" | "displayed" | "acknowledged"), force?: boolean): Promise<void>;
+        handleUnreadMessage(message: import("../../shared/message").default<any>): void;
+        getErrorAttributesForMessage(message: import("../../shared/message").default<any>, attrs: import("../../shared/types").MessageAttributes): Promise<any>;
         handleErrorMessageStanza(stanza: Element): Promise<void>;
-        incrementUnreadMsgsCounter(message: import("../../index.js").BaseMessage<any>): void;
+        incrementUnreadMsgsCounter(message: import("../../shared/message").default<any>): void;
         clearUnreadMsgCounter(): void;
         handleRetraction(attrs: import("../../shared/types").MessageAttributes): Promise<boolean>;
         handleReceipt(attrs: import("../../shared/types").MessageAttributes): boolean;
-        createMessageStanza(message: import("../../index.js").BaseMessage<any>): Promise<any>;
+        createMessageStanza(message: import("../../shared/message").default<any>): Promise<any>;
         pruneHistory(): void;
         debouncedPruneHistory: import("lodash").DebouncedFunc<() => void>;
         isScrolledUp(): any;

+ 3 - 2
src/headless/types/shared/chatbox.d.ts

@@ -38,8 +38,9 @@ declare const ChatBoxBase_base: {
         clearMessages(): Promise<void>;
         editEarlierMessage(): void;
         editLaterMessage(): any;
-        getOldestMessage(): any;
-        getMostRecentMessage(): any;
+        isChatMessage(_message: import("./message.js").default<any>): boolean;
+        getOldestMessage(): import("./message.js").default<any>;
+        getMostRecentMessage(): import("./message.js").default<any>;
         getMessageReferencedByError(attrs: object): any;
         findDanglingRetraction(attrs: object): import("./message.js").default<any> | null;
         getDuplicateMessage(attrs: object): import("./message.js").default<any>;

+ 12 - 3
src/headless/types/shared/model-with-messages.d.ts

@@ -133,8 +133,17 @@ export default function ModelWithMessages<T extends import("./types").ModelExten
         clearMessages(): Promise<void>;
         editEarlierMessage(): void;
         editLaterMessage(): any;
-        getOldestMessage(): any;
-        getMostRecentMessage(): any;
+        /**
+         * Used by sub-classes to indicate wether a message is a chat
+         * message, as opposed to error or info messages.
+         * @param {BaseMessage} _message
+         * @returns {boolean}
+         */
+        isChatMessage(_message: import("./message").default<any>): boolean;
+        /** @returns {BaseMessage} */
+        getOldestMessage(): import("./message").default<any>;
+        /** @returns {BaseMessage} */
+        getMostRecentMessage(): import("./message").default<any>;
         /**
          * Given an error `<message>` stanza's attributes, find the saved message model which is
          * referenced by that error.
@@ -299,5 +308,5 @@ export default function ModelWithMessages<T extends import("./types").ModelExten
         propertyIsEnumerable(v: PropertyKey): boolean;
     };
 } & T;
-import { Model } from "@converse/skeletor";
+import { Model } from '@converse/skeletor';
 //# sourceMappingURL=model-with-messages.d.ts.map

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

@@ -532,11 +532,11 @@ async function waitForRoster (_converse, type='current', length=-1, include_nick
     await _converse.api.waitUntil('rosterContactsFetched');
 }
 
-function createChatMessage (_converse, sender_jid, message) {
+function createChatMessage (_converse, sender_jid, message, type='chat') {
     return $msg({
                 from: sender_jid,
                 to: _converse.api.connection.get().jid,
-                type: 'chat',
+                type,
                 id: (new Date()).getTime()
             })
             .c('body').t(message).up()