Pārlūkot izejas kodu

Use ModelWithVCard to extend models

JC Brand 4 mēneši atpakaļ
vecāks
revīzija
51c7f3cb9e

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

@@ -8,6 +8,7 @@ import _converse from '../../shared/_converse.js';
 import api from '../../shared/api/index.js';
 import api from '../../shared/api/index.js';
 import { SUCCESS, FAILURE } from '../../shared/constants.js';
 import { SUCCESS, FAILURE } from '../../shared/constants.js';
 import ModelWithContact from '../../shared/model-with-contact.js';
 import ModelWithContact from '../../shared/model-with-contact.js';
+import ModelWithVCard from '../../shared/model-with-vcard';
 import ColorAwareModel from '../../shared/color.js';
 import ColorAwareModel from '../../shared/color.js';
 import { getUniqueId } from '../../utils/index.js';
 import { getUniqueId } from '../../utils/index.js';
 
 
@@ -18,7 +19,7 @@ import { getUniqueId } from '../../utils/index.js';
  * @memberOf _converse
  * @memberOf _converse
  * @example const msg = new Message({'message': 'hello world!'});
  * @example const msg = new Message({'message': 'hello world!'});
  */
  */
-class Message extends ModelWithContact(ColorAwareModel(Model)) {
+class Message extends ModelWithVCard(ModelWithContact(ColorAwareModel(Model))) {
 
 
     defaults () {
     defaults () {
         return {
         return {

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

@@ -7,6 +7,7 @@ import log from '../../log.js';
 import { isUniView } from '../../utils/session.js';
 import { isUniView } from '../../utils/session.js';
 import { sendChatState, sendMarker } from '../../shared/actions.js';
 import { sendChatState, sendMarker } from '../../shared/actions.js';
 import ModelWithMessages from "../../shared/model-with-messages.js";
 import ModelWithMessages from "../../shared/model-with-messages.js";
+import ModelWithVCard from '../../shared/model-with-vcard';
 import ModelWithContact from '../../shared/model-with-contact.js';
 import ModelWithContact from '../../shared/model-with-contact.js';
 import ColorAwareModel from '../../shared/color.js';
 import ColorAwareModel from '../../shared/color.js';
 import ChatBoxBase from '../../shared/chatbox.js';
 import ChatBoxBase from '../../shared/chatbox.js';
@@ -17,7 +18,7 @@ const { Strophe, u } = converse.env;
 /**
 /**
  * Represents a one-on-one chat conversation.
  * Represents a one-on-one chat conversation.
  */
  */
-class ChatBox extends ModelWithMessages(ModelWithContact(ColorAwareModel(ChatBoxBase))) {
+class ChatBox extends ModelWithVCard(ModelWithMessages(ModelWithContact(ColorAwareModel(ChatBoxBase)))) {
     /**
     /**
      * @typedef {import('./message.js').default} Message
      * @typedef {import('./message.js').default} Message
      * @typedef {import('../muc/muc.js').default} MUC
      * @typedef {import('../muc/muc.js').default} MUC

+ 3 - 2
src/headless/plugins/muc/message.js

@@ -1,10 +1,11 @@
 import { Strophe } from 'strophe.js';
 import { Strophe } from 'strophe.js';
-import Message from '../chat/message.js';
 import _converse from '../../shared/_converse.js';
 import _converse from '../../shared/_converse.js';
 import api from '../../shared/api/index.js';
 import api from '../../shared/api/index.js';
+import ModelWithVCard from '../../shared/model-with-vcard';
+import Message from '../chat/message.js';
 
 
 
 
-class MUCMessage extends Message {
+class MUCMessage extends ModelWithVCard(Message) {
     /**
     /**
      * @typedef {import('./occupant').default} MUCOccupant
      * @typedef {import('./occupant').default} MUCOccupant
      */
      */

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

@@ -32,6 +32,7 @@ import { isUniView } from '../../utils/session.js';
 import { parseMUCMessage, parseMUCPresence } from './parsers.js';
 import { parseMUCMessage, parseMUCPresence } from './parsers.js';
 import { sendMarker } from '../../shared/actions.js';
 import { sendMarker } from '../../shared/actions.js';
 import ModelWithMessages from '../../shared/model-with-messages';
 import ModelWithMessages from '../../shared/model-with-messages';
+import ModelWithVCard from '../../shared/model-with-vcard';
 import ColorAwareModel from '../../shared/color';
 import ColorAwareModel from '../../shared/color';
 import ChatBoxBase from '../../shared/chatbox';
 import ChatBoxBase from '../../shared/chatbox';
 import { shouldCreateGroupchatMessage, isInfoVisible } from './utils.js';
 import { shouldCreateGroupchatMessage, isInfoVisible } from './utils.js';
@@ -42,7 +43,7 @@ const { u, stx } = converse.env;
 /**
 /**
  * Represents a groupchat conversation.
  * Represents a groupchat conversation.
  */
  */
-class MUC extends ModelWithMessages(ColorAwareModel(ChatBoxBase)) {
+class MUC extends ModelWithVCard(ModelWithMessages(ColorAwareModel(ChatBoxBase))) {
     /**
     /**
      * @typedef {import('../vcard/vcard').default} VCard
      * @typedef {import('../vcard/vcard').default} VCard
      * @typedef {import('../chat/message.js').default} Message
      * @typedef {import('../chat/message.js').default} Message
@@ -87,12 +88,6 @@ class MUC extends ModelWithMessages(ColorAwareModel(ChatBoxBase)) {
     async initialize () {
     async initialize () {
         super.initialize();
         super.initialize();
 
 
-        /**
-         * @public
-         * @type {VCard}
-         */
-        this.vcard = null;
-
         this.initialized = getOpenPromise();
         this.initialized = getOpenPromise();
 
 
         this.debouncedRejoin = debounce(this.rejoin, 250);
         this.debouncedRejoin = debounce(this.rejoin, 250);

+ 58 - 55
src/headless/plugins/muc/occupant.js

@@ -1,48 +1,46 @@
-import { Model } from '@converse/skeletor';
-import log from '../../log';
-import api from '../../shared/api/index.js';
-import _converse from '../../shared/_converse.js';
-import converse from '../../shared/api/public.js';
-import ColorAwareModel from '../../shared/color.js';
-import ModelWithMessages from '../../shared/model-with-messages.js';
-import { AFFILIATIONS, ROLES } from './constants.js';
-import MUCMessages from './messages.js';
-import u from '../../utils/index.js';
-import { shouldCreateGroupchatMessage } from './utils';
-import { sendChatState } from '../../shared/actions';
+import { Model } from "@converse/skeletor";
+import log from "../../log";
+import api from "../../shared/api/index.js";
+import _converse from "../../shared/_converse.js";
+import converse from "../../shared/api/public.js";
+import ColorAwareModel from "../../shared/color.js";
+import ModelWithMessages from "../../shared/model-with-messages.js";
+import ModelWithVCard from "../../shared/model-with-vcard";
+import { AFFILIATIONS, ROLES } from "./constants.js";
+import MUCMessages from "./messages.js";
+import u from "../../utils/index.js";
+import { shouldCreateGroupchatMessage } from "./utils";
+import { sendChatState } from "../../shared/actions";
 
 
 const { Strophe, stx } = converse.env;
 const { Strophe, stx } = converse.env;
 
 
 /**
 /**
  * Represents a participant in a MUC
  * Represents a participant in a MUC
  */
  */
-class MUCOccupant extends ModelWithMessages(ColorAwareModel(Model)) {
+class MUCOccupant extends ModelWithVCard(ModelWithMessages(ColorAwareModel(Model))) {
     /**
     /**
      * @typedef {import('../chat/types').MessageAttributes} MessageAttributes
      * @typedef {import('../chat/types').MessageAttributes} MessageAttributes
      * @typedef {import('../../shared/errors').StanzaParseError} StanzaParseError
      * @typedef {import('../../shared/errors').StanzaParseError} StanzaParseError
      */
      */
 
 
-    constructor(attributes, options) {
-        super(attributes, options);
-        this.vcard = null;
-    }
-
     async initialize() {
     async initialize() {
-        await super.initialize();
+        this.lazy_load_vcard = true;
+        super.initialize();
+
         await this.fetchMessages();
         await this.fetchMessages();
-        this.on('change:nick', () => this.setColor());
-        this.on('change:jid', () => this.setColor());
-        this.on('change:chat_state', () => sendChatState(this.get('jid'), this.get('chat_state')));
+        this.on("change:nick", () => this.setColor());
+        this.on("change:jid", () => this.setColor());
+        this.on("change:chat_state", () => sendChatState(this.get("jid"), this.get("chat_state")));
     }
     }
 
 
     defaults() {
     defaults() {
         return {
         return {
             hats: [],
             hats: [],
-            show: 'offline',
+            show: "offline",
             states: [],
             states: [],
             hidden: true,
             hidden: true,
             num_unread: 0,
             num_unread: 0,
-            message_type: 'chat',
+            message_type: "chat",
         };
         };
     }
     }
 
 
@@ -51,7 +49,7 @@ class MUCOccupant extends ModelWithMessages(ColorAwareModel(Model)) {
         if (key == null) {
         if (key == null) {
             // eslint-disable-line no-eq-null
             // eslint-disable-line no-eq-null
             return super.save(key, val, options);
             return super.save(key, val, options);
-        } else if (typeof key === 'object') {
+        } else if (typeof key === "object") {
             attrs = key;
             attrs = key;
             options = val;
             options = val;
         } else {
         } else {
@@ -75,13 +73,13 @@ class MUCOccupant extends ModelWithMessages(ColorAwareModel(Model)) {
      */
      */
     async onMessage(attrs_or_error) {
     async onMessage(attrs_or_error) {
         if (u.isErrorObject(attrs_or_error)) {
         if (u.isErrorObject(attrs_or_error)) {
-            const { stanza, message } = /** @type {StanzaParseError} */(attrs_or_error);
+            const { stanza, message } = /** @type {StanzaParseError} */ (attrs_or_error);
             if (stanza) log.error(stanza);
             if (stanza) log.error(stanza);
             return log.error(message);
             return log.error(message);
         }
         }
 
 
-        const attrs = /** @type {MessageAttributes} */(attrs_or_error);
-        if (attrs.type === 'error' && !(await this.shouldShowErrorMessage(attrs))) {
+        const attrs = /** @type {MessageAttributes} */ (attrs_or_error);
+        if (attrs.type === "error" && !(await this.shouldShowErrorMessage(attrs))) {
             return;
             return;
         }
         }
 
 
@@ -105,7 +103,7 @@ class MUCOccupant extends ModelWithMessages(ColorAwareModel(Model)) {
      * @returns {string}
      * @returns {string}
      */
      */
     getDisplayName() {
     getDisplayName() {
-        return this.get('nick') || this.get('jid') || '';
+        return this.get("nick") || this.get("jid") || "";
     }
     }
 
 
     /**
     /**
@@ -113,11 +111,11 @@ class MUCOccupant extends ModelWithMessages(ColorAwareModel(Model)) {
      * @returns {typeof ROLES} - An array of assignable roles
      * @returns {typeof ROLES} - An array of assignable roles
      */
      */
     getAssignableRoles() {
     getAssignableRoles() {
-        let disabled = api.settings.get('modtools_disable_assign');
+        let disabled = api.settings.get("modtools_disable_assign");
         if (!Array.isArray(disabled)) {
         if (!Array.isArray(disabled)) {
             disabled = disabled ? ROLES : [];
             disabled = disabled ? ROLES : [];
         }
         }
-        if (this.get('role') === 'moderator') {
+        if (this.get("role") === "moderator") {
             return ROLES.filter((r) => !disabled.includes(r));
             return ROLES.filter((r) => !disabled.includes(r));
         } else {
         } else {
             return [];
             return [];
@@ -129,56 +127,61 @@ class MUCOccupant extends ModelWithMessages(ColorAwareModel(Model)) {
      * @returns {typeof AFFILIATIONS} An array of assignable affiliations
      * @returns {typeof AFFILIATIONS} An array of assignable affiliations
      */
      */
     getAssignableAffiliations() {
     getAssignableAffiliations() {
-        let disabled = api.settings.get('modtools_disable_assign');
+        let disabled = api.settings.get("modtools_disable_assign");
         if (!Array.isArray(disabled)) {
         if (!Array.isArray(disabled)) {
             disabled = disabled ? AFFILIATIONS : [];
             disabled = disabled ? AFFILIATIONS : [];
         }
         }
-        if (this.get('affiliation') === 'owner') {
+        if (this.get("affiliation") === "owner") {
             return AFFILIATIONS.filter((a) => !disabled.includes(a));
             return AFFILIATIONS.filter((a) => !disabled.includes(a));
-        } else if (this.get('affiliation') === 'admin') {
-            return AFFILIATIONS.filter((a) => !['owner', 'admin', ...disabled].includes(a));
+        } else if (this.get("affiliation") === "admin") {
+            return AFFILIATIONS.filter((a) => !["owner", "admin", ...disabled].includes(a));
         } else {
         } else {
             return [];
             return [];
         }
         }
     }
     }
 
 
     isMember() {
     isMember() {
-        return ['admin', 'owner', 'member'].includes(this.get('affiliation'));
+        return ["admin", "owner", "member"].includes(this.get("affiliation"));
     }
     }
 
 
     isModerator() {
     isModerator() {
-        return ['admin', 'owner'].includes(this.get('affiliation')) || this.get('role') === 'moderator';
+        return ["admin", "owner"].includes(this.get("affiliation")) || this.get("role") === "moderator";
     }
     }
 
 
     isSelf() {
     isSelf() {
-        return this.get('states').includes('110');
+        return this.get("states").includes("110");
     }
     }
 
 
     /**
     /**
      * @param {MessageAttributes} [attrs]
      * @param {MessageAttributes} [attrs]
      * @return {Promise<MessageAttributes>}
      * @return {Promise<MessageAttributes>}
      */
      */
-    async getOutgoingMessageAttributes (attrs) {
+    async getOutgoingMessageAttributes(attrs) {
         const origin_id = u.getUniqueId();
         const origin_id = u.getUniqueId();
         const text = attrs?.body;
         const text = attrs?.body;
         const body = text ? u.shortnamesToUnicode(text) : undefined;
         const body = text ? u.shortnamesToUnicode(text) : undefined;
         const muc = this.collection.chatroom;
         const muc = this.collection.chatroom;
         const own_occupant = muc.getOwnOccupant();
         const own_occupant = muc.getOwnOccupant();
-        attrs = Object.assign({}, attrs, {
-            body,
-            from: own_occupant.get('from'),
-            fullname: _converse.state.xmppstatus.get('fullname'),
-            id: origin_id,
-            jid: this.get('jid'),
-            message: body,
-            msgid: origin_id,
-            nick: own_occupant.get('nickname'),
-            origin_id,
-            sender: 'me',
-            time: (new Date()).toISOString(),
-            to: this.get('from') ?? `${muc.get('jid')}/${this.get('nick')}`,
-            type: 'chat',
-        }, u.getMediaURLsMetadata(text));
+        attrs = Object.assign(
+            {},
+            attrs,
+            {
+                body,
+                from: own_occupant.get("from"),
+                fullname: _converse.state.xmppstatus.get("fullname"),
+                id: origin_id,
+                jid: this.get("jid"),
+                message: body,
+                msgid: origin_id,
+                nick: own_occupant.get("nickname"),
+                origin_id,
+                sender: "me",
+                time: new Date().toISOString(),
+                to: this.get("from") ?? `${muc.get("jid")}/${this.get("nick")}`,
+                type: "chat",
+            },
+            u.getMediaURLsMetadata(text)
+        );
 
 
         /**
         /**
          * *Hook* which allows plugins to update the attributes of an outgoing message.
          * *Hook* which allows plugins to update the attributes of an outgoing message.
@@ -189,7 +192,7 @@ class MUCOccupant extends ModelWithMessages(ColorAwareModel(Model)) {
          * @param {MessageAttributes} attrs
          * @param {MessageAttributes} attrs
          *      The message attributes, from which the stanza will be created.
          *      The message attributes, from which the stanza will be created.
          */
          */
-        attrs = await api.hook('getOutgoingMessageAttributes', this, attrs);
+        attrs = await api.hook("getOutgoingMessageAttributes", this, attrs);
         return attrs;
         return attrs;
     }
     }
 
 

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

@@ -5,21 +5,16 @@ import _converse from '../../shared/_converse.js';
 import api from '../../shared/api/index.js';
 import api from '../../shared/api/index.js';
 import converse from '../../shared/api/public.js';
 import converse from '../../shared/api/public.js';
 import ColorAwareModel from '../../shared/color.js';
 import ColorAwareModel from '../../shared/color.js';
+import ModelWithVCard from '../../shared/model-with-vcard.js';
 import { rejectPresenceSubscription } from './utils.js';
 import { rejectPresenceSubscription } from './utils.js';
 
 
 const { Strophe, $iq, $pres, stx } = converse.env;
 const { Strophe, $iq, $pres, stx } = converse.env;
 
 
-class RosterContact extends ColorAwareModel(Model) {
+class RosterContact extends ModelWithVCard(ColorAwareModel(Model)) {
     get idAttribute () {
     get idAttribute () {
         return 'jid';
         return 'jid';
     }
     }
 
 
-    constructor (attrs, options) {
-        super(attrs, options);
-        /** @type {import('../vcard/vcard').default} */
-        this.vcard = null;
-    }
-
     defaults () {
     defaults () {
         return {
         return {
             groups: [],
             groups: [],

+ 4 - 6
src/headless/plugins/status/status.js

@@ -2,19 +2,16 @@ import { Model } from '@converse/skeletor';
 import _converse from '../../shared/_converse.js';
 import _converse from '../../shared/_converse.js';
 import api from '../../shared/api/index.js';
 import api from '../../shared/api/index.js';
 import converse from '../../shared/api/public.js';
 import converse from '../../shared/api/public.js';
+import ModelWithVCard from '../../shared/model-with-vcard';
 import ColorAwareModel from '../../shared/color.js';
 import ColorAwareModel from '../../shared/color.js';
 import { isIdle, getIdleSeconds } from './utils.js';
 import { isIdle, getIdleSeconds } from './utils.js';
 
 
 const { Strophe, $pres } = converse.env;
 const { Strophe, $pres } = converse.env;
 
 
-export default class XMPPStatus extends ColorAwareModel(Model) {
-    constructor(attributes, options) {
-        super(attributes, options);
-        this.vcard = null;
-    }
+export default class XMPPStatus extends ModelWithVCard(ColorAwareModel(Model)) {
 
 
     defaults() {
     defaults() {
-        return { 'status': api.settings.get('default_state') };
+        return { status: api.settings.get('default_state') };
     }
     }
 
 
     getStatus() {
     getStatus() {
@@ -46,6 +43,7 @@ export default class XMPPStatus extends ColorAwareModel(Model) {
     }
     }
 
 
     initialize() {
     initialize() {
+        super.initialize();
         this.on('change', (item) => {
         this.on('change', (item) => {
             if (!(item.changed instanceof Object)) {
             if (!(item.changed instanceof Object)) {
                 return;
                 return;

+ 88 - 103
src/headless/plugins/vcard/utils.js

@@ -7,113 +7,108 @@
  * @typedef {import('../muc/occupant.js').default} MUCOccupant
  * @typedef {import('../muc/occupant.js').default} MUCOccupant
  * @typedef {import('@converse/skeletor/src/types/helpers.js').Model} Model
  * @typedef {import('@converse/skeletor/src/types/helpers.js').Model} Model
  */
  */
-import _converse from '../../shared/_converse.js';
-import api from '../../shared/api/index.js';
-import converse from '../../shared/api/public.js';
+import _converse from "../../shared/_converse.js";
+import api from "../../shared/api/index.js";
+import converse from "../../shared/api/public.js";
 import log from "../../log.js";
 import log from "../../log.js";
-import { initStorage } from '../../utils/storage.js';
-import { shouldClearCache } from '../../utils/session.js';
-import { isElement } from '../../utils/html.js';
+import { initStorage } from "../../utils/storage.js";
+import { shouldClearCache } from "../../utils/session.js";
+import { isElement } from "../../utils/html.js";
 
 
 const { Strophe, $iq, u } = converse.env;
 const { Strophe, $iq, u } = converse.env;
 
 
-
 /**
 /**
  * @param {Element} iq
  * @param {Element} iq
  */
  */
-async function onVCardData (iq) {
-    const vcard = iq.querySelector('vCard');
+async function onVCardData(iq) {
+    const vcard = iq.querySelector("vCard");
     let result = {};
     let result = {};
     if (vcard !== null) {
     if (vcard !== null) {
         result = {
         result = {
-            'stanza': iq,
-            'fullname': vcard.querySelector('FN')?.textContent,
-            'nickname': vcard.querySelector('NICKNAME')?.textContent,
-            'image': vcard.querySelector('PHOTO BINVAL')?.textContent,
-            'image_type': vcard.querySelector('PHOTO TYPE')?.textContent,
-            'url': vcard.querySelector('URL')?.textContent,
-            'role': vcard.querySelector('ROLE')?.textContent,
-            'email': vcard.querySelector('EMAIL USERID')?.textContent,
-            'vcard_updated': (new Date()).toISOString(),
-            'vcard_error': undefined,
+            "stanza": iq,
+            "fullname": vcard.querySelector("FN")?.textContent,
+            "nickname": vcard.querySelector("NICKNAME")?.textContent,
+            "image": vcard.querySelector("PHOTO BINVAL")?.textContent,
+            "image_type": vcard.querySelector("PHOTO TYPE")?.textContent,
+            "url": vcard.querySelector("URL")?.textContent,
+            "role": vcard.querySelector("ROLE")?.textContent,
+            "email": vcard.querySelector("EMAIL USERID")?.textContent,
+            "vcard_updated": new Date().toISOString(),
+            "vcard_error": undefined,
             image_hash: undefined,
             image_hash: undefined,
         };
         };
     }
     }
     if (result.image) {
     if (result.image) {
-        const buffer = u.base64ToArrayBuffer(result['image']);
-        const ab = await crypto.subtle.digest('SHA-1', buffer);
-        result['image_hash'] = u.arrayBufferToHex(ab);
+        const buffer = u.base64ToArrayBuffer(result["image"]);
+        const ab = await crypto.subtle.digest("SHA-1", buffer);
+        result["image_hash"] = u.arrayBufferToHex(ab);
     }
     }
     return result;
     return result;
 }
 }
 
 
-
 /**
 /**
  * @param {"get"|"set"|"result"} type
  * @param {"get"|"set"|"result"} type
  * @param {string} jid
  * @param {string} jid
  * @param {Element} [vcard_el]
  * @param {Element} [vcard_el]
  */
  */
-export function createStanza (type, jid, vcard_el) {
-    const iq = $iq(jid ? {'type': type, 'to': jid} : {'type': type});
+export function createStanza(type, jid, vcard_el) {
+    const iq = $iq(jid ? { "type": type, "to": jid } : { "type": type });
     if (!vcard_el) {
     if (!vcard_el) {
-        iq.c("vCard", {'xmlns': Strophe.NS.VCARD});
+        iq.c("vCard", { "xmlns": Strophe.NS.VCARD });
     } else {
     } else {
         iq.cnode(vcard_el);
         iq.cnode(vcard_el);
     }
     }
     return iq;
     return iq;
 }
 }
 
 
-
 /**
 /**
  * @param {MUCOccupant} occupant
  * @param {MUCOccupant} occupant
  */
  */
-export function onOccupantAvatarChanged (occupant) {
-    const hash = occupant.get('image_hash');
+export function onOccupantAvatarChanged(occupant) {
+    const hash = occupant.get("image_hash");
     const vcards = [];
     const vcards = [];
-    if (occupant.get('jid')) {
-        vcards.push(_converse.state.vcards.get(occupant.get('jid')));
+    if (occupant.get("jid")) {
+        vcards.push(_converse.state.vcards.get(occupant.get("jid")));
     }
     }
-    vcards.push(_converse.state.vcards.get(occupant.get('from')));
-    vcards.forEach(v => (hash && v && v?.get('image_hash') !== hash) && api.vcard.update(v, true));
+    vcards.push(_converse.state.vcards.get(occupant.get("from")));
+    vcards.forEach((v) => hash && v && v?.get("image_hash") !== hash && api.vcard.update(v, true));
 }
 }
 
 
-
 /**
 /**
- * @param {Model} model
+ * @param {Model|MUCOccupant|MUCMessage} model
  * @param {boolean} [create=true]
  * @param {boolean} [create=true]
- * @returns {Promise<VCard>}
+ * @returns {Promise<VCard|null>}
  */
  */
-export async function getVCardForModel (model, create=true) {
+export async function getVCardForModel(model, create = true) {
     await initVCardCollection();
     await initVCardCollection();
 
 
     let vcard;
     let vcard;
     if (model instanceof _converse.exports.MUCOccupant) {
     if (model instanceof _converse.exports.MUCOccupant) {
-        return getVCardForOccupant(/** @type {MUCOccupant} */(model), create);
-    }
-
-    if (model instanceof _converse.exports.MUCMessage) {
-        return getVCardForMUCMessage(/** @type {MUCMessage} */(model), create);
-    }
+        vcard = await getVCardForOccupant(/** @type {MUCOccupant} */ (model), create);
+    } else if (model instanceof _converse.exports.MUCMessage) {
+        vcard = await getVCardForMUCMessage(/** @type {MUCMessage} */ (model), create);
+    } else {
+        let jid;
+        if (model instanceof _converse.exports.Message) {
+            if (["error", "info"].includes(model.get("type"))) {
+                return;
+            }
+            jid = Strophe.getBareJidFromJid(model.get("from"));
+        } else {
+            jid = model.get("jid");
+        }
 
 
-    let jid;
-    if (model instanceof _converse.exports.Message) {
-        if (['error', 'info'].includes(model.get('type'))) {
-            return;
+        if (!jid) {
+            log.warn(`Could not set VCard on model because no JID found!`);
+            return null;
         }
         }
-        jid = Strophe.getBareJidFromJid(model.get('from'));
-    } else {
-        jid = model.get('jid');
+        const { vcards } = _converse.state;
+        vcard = vcards.get(jid) || (create ? vcards.create({ jid }) : null);
     }
     }
 
 
-    if (!jid) {
-        log.warn(`Could not set VCard on model because no JID found!`);
-        return;
+    if (vcard) {
+        vcard.on("change", () => model.trigger("vcard:change"));
     }
     }
-
-    const { vcards } = _converse.state;
-    vcard = vcards.get(jid) || create && vcards.create({ jid });
-
-    vcard.on('change', () => model.trigger('vcard:change'));
     return vcard;
     return vcard;
 }
 }
 
 
@@ -122,92 +117,82 @@ export async function getVCardForModel (model, create=true) {
  * @param {boolean} [create=true]
  * @param {boolean} [create=true]
  * @returns {Promise<VCard|null>}
  * @returns {Promise<VCard|null>}
  */
  */
-export async function getVCardForOccupant(occupant, create=true) {
-    await api.waitUntil('VCardsInitialized');
+export async function getVCardForOccupant(occupant, create = true) {
+    await api.waitUntil("VCardsInitialized");
 
 
     const { vcards, xmppstatus } = _converse.state;
     const { vcards, xmppstatus } = _converse.state;
     const muc = occupant?.collection?.chatroom;
     const muc = occupant?.collection?.chatroom;
-    const nick = occupant.get('nick');
-    let vcard;
+    const nick = occupant.get("nick");
 
 
-    if (nick && muc?.get('nick') === nick) {
-        vcard = xmppstatus.vcard;
+    if (nick && muc?.get("nick") === nick) {
+        return xmppstatus.vcard;
     } else {
     } else {
-        const jid = occupant.get('jid') || occupant.get('from');
+        const jid = occupant.get("jid") || occupant.get("from");
         if (jid) {
         if (jid) {
-            vcard = vcards.get(jid) || create && vcards.create({ jid });
+            return vcards.get(jid) || (create ? vcards.create({ jid }) : null);
         } else {
         } else {
             log.warn(`Could not get VCard for occupant because no JID found!`);
             log.warn(`Could not get VCard for occupant because no JID found!`);
             return null;
             return null;
         }
         }
     }
     }
-
-    if (vcard) {
-        vcard.on('change', () => occupant.trigger('vcard:change'));
-    }
-    return vcard;
 }
 }
 
 
-
 /**
 /**
  * @param {MUCMessage} message
  * @param {MUCMessage} message
  * @param {boolean} [create=true]
  * @param {boolean} [create=true]
  * @returns {Promise<VCard|null>}
  * @returns {Promise<VCard|null>}
  */
  */
-async function getVCardForMUCMessage (message, create=true) {
-    if (['error', 'info'].includes(message.get('type'))) return;
+async function getVCardForMUCMessage(message, create = true) {
+    if (["error", "info"].includes(message.get("type"))) return;
 
 
-    await api.waitUntil('VCardsInitialized');
+    await api.waitUntil("VCardsInitialized");
     const { vcards, xmppstatus } = _converse.state;
     const { vcards, xmppstatus } = _converse.state;
     const muc = message?.collection?.chatbox;
     const muc = message?.collection?.chatbox;
-    const nick = Strophe.getResourceFromJid(message.get('from'));
-    let vcard;
+    const nick = Strophe.getResourceFromJid(message.get("from"));
 
 
-    if (nick && muc?.get('nick') === nick) {
-        vcard = xmppstatus.vcard;
+    if (nick && muc?.get("nick") === nick) {
+        return xmppstatus.vcard;
     } else {
     } else {
-        const jid = message.occupant?.get('jid') || message.get('from');
+        const jid = message.occupant?.get("jid") || message.get("from");
         if (jid) {
         if (jid) {
-            vcard = vcards.get(jid) || create && vcards.create({ jid });
+            return vcards.get(jid) || (create ? vcards.create({ jid }) : null);
         } else {
         } else {
-            log.warn(`Could not get VCard for message because no JID found! msgid: ${message.get('msgid')}`);
+            debugger;
+            log.warn(`Could not get VCard for message because no JID found! msgid: ${message.get("msgid")}`);
             return null;
             return null;
         }
         }
     }
     }
-
-    if (vcard) {
-        vcard.on('change', () => message.trigger('vcard:change'));
-    }
-    return vcard;
 }
 }
 
 
-async function initVCardCollection () {
+async function initVCardCollection() {
     if (_converse.state.vcards) return _converse.state.vcards;
     if (_converse.state.vcards) return _converse.state.vcards;
 
 
     const vcards = new _converse.exports.VCards();
     const vcards = new _converse.exports.VCards();
     _converse.state.vcards = vcards;
     _converse.state.vcards = vcards;
     Object.assign(_converse, { vcards }); // XXX DEPRECATED
     Object.assign(_converse, { vcards }); // XXX DEPRECATED
 
 
-    const bare_jid = _converse.session.get('bare_jid');
+    const bare_jid = _converse.session.get("bare_jid");
     const id = `${bare_jid}-converse.vcards`;
     const id = `${bare_jid}-converse.vcards`;
     initStorage(vcards, id);
     initStorage(vcards, id);
-    await new Promise(resolve => {
-        vcards.fetch({
-            success: resolve,
-            error: resolve
-        }, {'silent': true});
+    await new Promise((resolve) => {
+        vcards.fetch(
+            {
+                success: resolve,
+                error: resolve,
+            },
+            { "silent": true }
+        );
     });
     });
     /**
     /**
      * Triggered as soon as the `_converse.vcards` collection has been initialized and populated from cache.
      * Triggered as soon as the `_converse.vcards` collection has been initialized and populated from cache.
      * @event _converse#VCardsInitialized
      * @event _converse#VCardsInitialized
      */
      */
-    api.trigger('VCardsInitialized');
+    api.trigger("VCardsInitialized");
 }
 }
 
 
-
-export function clearVCardsSession () {
+export function clearVCardsSession() {
     if (shouldClearCache(_converse)) {
     if (shouldClearCache(_converse)) {
-        api.promises.add('VCardsInitialized');
+        api.promises.add("VCardsInitialized");
         if (_converse.state.vcards) {
         if (_converse.state.vcards) {
             _converse.state.vcards.clearStore();
             _converse.state.vcards.clearStore();
             Object.assign(_converse, { vcards: undefined }); // XXX DEPRECATED
             Object.assign(_converse, { vcards: undefined }); // XXX DEPRECATED
@@ -219,19 +204,19 @@ export function clearVCardsSession () {
 /**
 /**
  * @param {string} jid
  * @param {string} jid
  */
  */
-export async function getVCard (jid) {
-    const bare_jid = _converse.session.get('bare_jid');
+export async function getVCard(jid) {
+    const bare_jid = _converse.session.get("bare_jid");
     const to = Strophe.getBareJidFromJid(jid) === bare_jid ? null : jid;
     const to = Strophe.getBareJidFromJid(jid) === bare_jid ? null : jid;
     let iq;
     let iq;
     try {
     try {
-        iq = await api.sendIQ(createStanza("get", to))
+        iq = await api.sendIQ(createStanza("get", to));
     } catch (error) {
     } catch (error) {
         return {
         return {
             jid,
             jid,
             stanza: isElement(error) ? error : null,
             stanza: isElement(error) ? error : null,
             error: isElement(error) ? null : error,
             error: isElement(error) ? null : error,
-            vcard_error: (new Date()).toISOString()
-        }
+            vcard_error: new Date().toISOString(),
+        };
     }
     }
     return onVCardData(iq);
     return onVCardData(iq);
 }
 }

+ 0 - 7
src/headless/shared/model-with-contact.js

@@ -10,7 +10,6 @@ import api from './api/index.js';
 export default function ModelWithContact(BaseModel) {
 export default function ModelWithContact(BaseModel) {
     return class ModelWithContact extends BaseModel {
     return class ModelWithContact extends BaseModel {
         /**
         /**
-         * @typedef {import('../plugins/vcard/vcard').default} VCard
          * @typedef {import('../plugins/roster/contact').default} RosterContact
          * @typedef {import('../plugins/roster/contact').default} RosterContact
          * @typedef {import('./_converse.js').XMPPStatus} XMPPStatus
          * @typedef {import('./_converse.js').XMPPStatus} XMPPStatus
          */
          */
@@ -23,12 +22,6 @@ export default function ModelWithContact(BaseModel) {
              * @type {RosterContact|XMPPStatus}
              * @type {RosterContact|XMPPStatus}
              */
              */
             this.contact = null;
             this.contact = null;
-
-            /**
-             * @public
-             * @type {VCard}
-             */
-            this.vcard = null;
         }
         }
 
 
         /**
         /**

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

@@ -1,10 +1,78 @@
 export default Message;
 export default Message;
 declare const Message_base: {
 declare const Message_base: {
+    new (...args: any[]): {
+        _vcard: import("../vcard/vcard.js").default;
+        lazy_load_vcard: boolean;
+        initialize(): void;
+        readonly vcard: import("../vcard/vcard.js").default;
+        getVCard(create?: boolean): Promise<import("../vcard/vcard.js").default | null>;
+        cid: any;
+        attributes: {};
+        validationError: string;
+        collection: any;
+        changed: {};
+        browserStorage: Storage;
+        _browserStorage: Storage;
+        readonly idAttribute: string;
+        readonly cidPrefix: string;
+        preinitialize(): void;
+        validate(attrs: object, options?: object): string;
+        toJSON(): any;
+        sync(method: "create" | "update" | "patch" | "delete" | "read", model: Model, options: import("@converse/skeletor/src/types/model.js").Options): any;
+        get(attr: string): any;
+        keys(): string[];
+        values(): any[];
+        pairs(): [string, any][];
+        entries(): [string, any][];
+        invert(): any;
+        pick(...args: any[]): any;
+        omit(...args: any[]): any;
+        isEmpty(): any;
+        has(attr: string): boolean;
+        matches(attrs: import("@converse/skeletor/src/types/model.js").Attributes): boolean;
+        set(key: string | any, val?: string | any, options?: import("@converse/skeletor/src/types/model.js").Options): false | any;
+        _changing: boolean;
+        _previousAttributes: any;
+        id: any;
+        _pending: boolean | import("@converse/skeletor/src/types/model.js").Options;
+        unset(attr: string, options?: import("@converse/skeletor/src/types/model.js").Options): false | any;
+        clear(options: import("@converse/skeletor/src/types/model.js").Options): false | any;
+        hasChanged(attr?: string): any;
+        changedAttributes(diff: any): any;
+        previous(attr?: string): any;
+        previousAttributes(): any;
+        fetch(options?: import("@converse/skeletor/src/types/model.js").Options): any;
+        save(key?: string | import("@converse/skeletor/src/types/model.js").Attributes, val?: boolean | number | string | import("@converse/skeletor/src/types/model.js").Options, options?: import("@converse/skeletor/src/types/model.js").Options): any;
+        destroy(options?: import("@converse/skeletor/src/types/model.js").Options): boolean;
+        url(): any;
+        parse(resp: import("@converse/skeletor/src/types/model.js").Options, options?: import("@converse/skeletor/src/types/model.js").Options): import("@converse/skeletor/src/types/model.js").Options;
+        isNew(): boolean;
+        isValid(options?: import("@converse/skeletor/src/types/model.js").Options): boolean;
+        _validate(attrs: import("@converse/skeletor/src/types/model.js").Attributes, options?: import("@converse/skeletor/src/types/model.js").Options): boolean;
+        on(name: string, callback: (event: any, model: Model, collection: import("@converse/skeletor").Collection, options: Record<string, any>) => any, context: any): any;
+        _events: any;
+        _listeners: {};
+        listenTo(obj: any, name: string, callback?: (event: any, model: Model, collection: import("@converse/skeletor").Collection, options: Record<string, any>) => any): any;
+        _listeningTo: {};
+        _listenId: any;
+        off(name: string, callback: (event: any, model: Model, collection: import("@converse/skeletor").Collection, options: Record<string, any>) => any, context?: any): any;
+        stopListening(obj?: any, name?: string, callback?: (event: any, model: Model, collection: import("@converse/skeletor").Collection, options: Record<string, any>) => any): any;
+        once(name: string, callback: (event: any, model: Model, collection: import("@converse/skeletor").Collection, options: Record<string, any>) => any, context: any): any;
+        listenToOnce(obj: any, name: string, callback?: (event: any, model: Model, collection: import("@converse/skeletor").Collection, options: Record<string, any>) => any): any;
+        trigger(name: string, ...args: any[]): any;
+        constructor: Function;
+        toString(): string;
+        toLocaleString(): string;
+        valueOf(): Object;
+        hasOwnProperty(v: PropertyKey): boolean;
+        isPrototypeOf(v: Object): boolean;
+        propertyIsEnumerable(v: PropertyKey): boolean;
+    };
+} & {
     new (...args: any[]): {
     new (...args: any[]): {
         initialize(): void;
         initialize(): void;
         rosterContactAdded: any;
         rosterContactAdded: any;
         contact: import("../roster/contact.js").default | import("../status/status.js").default;
         contact: import("../roster/contact.js").default | import("../status/status.js").default;
-        vcard: import("../vcard/vcard.js").default;
         setModelContact(jid: string): Promise<void>;
         setModelContact(jid: string): Promise<void>;
         cid: any;
         cid: any;
         attributes: {};
         attributes: {};

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

@@ -1,5 +1,74 @@
 export default ChatBox;
 export default ChatBox;
 declare const ChatBox_base: {
 declare const ChatBox_base: {
+    new (...args: any[]): {
+        _vcard: import("../vcard/vcard.js").default;
+        lazy_load_vcard: boolean;
+        initialize(): void;
+        readonly vcard: import("../vcard/vcard.js").default;
+        getVCard(create?: boolean): Promise<import("../vcard/vcard.js").default | null>;
+        cid: any;
+        attributes: {};
+        validationError: string;
+        collection: any;
+        changed: {};
+        browserStorage: Storage;
+        _browserStorage: Storage;
+        readonly idAttribute: string;
+        readonly cidPrefix: string;
+        preinitialize(): void;
+        validate(attrs: object, options?: object): string;
+        toJSON(): any;
+        sync(method: "create" | "update" | "patch" | "delete" | "read", model: import("@converse/skeletor").Model, options: import("@converse/skeletor/src/types/model.js").Options): any;
+        get(attr: string): any;
+        keys(): string[];
+        values(): any[];
+        pairs(): [string, any][];
+        entries(): [string, any][];
+        invert(): any;
+        pick(...args: any[]): any;
+        omit(...args: any[]): any;
+        isEmpty(): any;
+        has(attr: string): boolean;
+        matches(attrs: import("@converse/skeletor/src/types/model.js").Attributes): boolean;
+        set(key: string | any, val?: string | any, options?: import("@converse/skeletor/src/types/model.js").Options): false | any;
+        _changing: boolean;
+        _previousAttributes: any;
+        id: any;
+        _pending: boolean | import("@converse/skeletor/src/types/model.js").Options;
+        unset(attr: string, options?: import("@converse/skeletor/src/types/model.js").Options): false | any;
+        clear(options: import("@converse/skeletor/src/types/model.js").Options): false | any;
+        hasChanged(attr?: string): any;
+        changedAttributes(diff: any): any;
+        previous(attr?: string): any;
+        previousAttributes(): any;
+        fetch(options?: import("@converse/skeletor/src/types/model.js").Options): any;
+        save(key?: string | import("@converse/skeletor/src/types/model.js").Attributes, val?: boolean | number | string | import("@converse/skeletor/src/types/model.js").Options, options?: import("@converse/skeletor/src/types/model.js").Options): any;
+        destroy(options?: import("@converse/skeletor/src/types/model.js").Options): boolean;
+        url(): any;
+        parse(resp: import("@converse/skeletor/src/types/model.js").Options, options?: import("@converse/skeletor/src/types/model.js").Options): import("@converse/skeletor/src/types/model.js").Options;
+        isNew(): boolean;
+        isValid(options?: import("@converse/skeletor/src/types/model.js").Options): boolean;
+        _validate(attrs: import("@converse/skeletor/src/types/model.js").Attributes, options?: import("@converse/skeletor/src/types/model.js").Options): boolean;
+        on(name: string, callback: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options: Record<string, any>) => any, context: any): any;
+        _events: any;
+        _listeners: {};
+        listenTo(obj: any, name: string, callback?: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options: Record<string, any>) => any): any;
+        _listeningTo: {};
+        _listenId: any;
+        off(name: string, callback: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options: Record<string, any>) => any, context?: any): any;
+        stopListening(obj?: any, name?: string, callback?: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options: Record<string, any>) => any): any;
+        once(name: string, callback: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options: Record<string, any>) => any, context: any): any;
+        listenToOnce(obj: any, name: string, callback?: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options: Record<string, any>) => any): any;
+        trigger(name: string, ...args: any[]): any;
+        constructor: Function;
+        toString(): string;
+        toLocaleString(): string;
+        valueOf(): Object;
+        hasOwnProperty(v: PropertyKey): boolean;
+        isPrototypeOf(v: Object): boolean;
+        propertyIsEnumerable(v: PropertyKey): boolean;
+    };
+} & {
     new (...args: any[]): {
     new (...args: any[]): {
         disable_mam: boolean;
         disable_mam: boolean;
         initialize(): Promise<void>;
         initialize(): Promise<void>;
@@ -132,7 +201,6 @@ declare const ChatBox_base: {
         initialize(): void;
         initialize(): void;
         rosterContactAdded: any;
         rosterContactAdded: any;
         contact: import("../roster/contact.js").default | import("../status/status.js").default;
         contact: import("../roster/contact.js").default | import("../status/status.js").default;
-        vcard: import("../vcard/vcard.js").default;
         setModelContact(jid: string): Promise<void>;
         setModelContact(jid: string): Promise<void>;
         cid: any;
         cid: any;
         attributes: {};
         attributes: {};

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

@@ -1,5 +1,79 @@
 export default MUCMessage;
 export default MUCMessage;
-declare class MUCMessage extends Message {
+declare const MUCMessage_base: {
+    new (...args: any[]): {
+        _vcard: import("../vcard/vcard.js").default;
+        lazy_load_vcard: boolean;
+        initialize(): void;
+        readonly vcard: import("../vcard/vcard.js").default;
+        getVCard(create?: boolean): Promise<import("../vcard/vcard.js").default | null>;
+        cid: any;
+        attributes: {};
+        validationError: string;
+        collection: any;
+        changed: {};
+        browserStorage: Storage;
+        _browserStorage: Storage;
+        readonly idAttribute: string;
+        readonly cidPrefix: string;
+        preinitialize(): void;
+        validate(attrs: object, options?: object): string;
+        toJSON(): any;
+        sync(method: "create" | "update" | "patch" | "delete" | "read", model: import("@converse/skeletor").Model, options: import("@converse/skeletor/src/types/model.js").Options): any;
+        get(attr: string): any;
+        keys(): string[];
+        values(): any[];
+        pairs(): [string, any][];
+        entries(): [string, any][];
+        invert(): any;
+        pick(...args: any[]): any;
+        omit(...args: any[]): any;
+        isEmpty(): any;
+        has(attr: string): boolean;
+        matches(attrs: import("@converse/skeletor/src/types/model.js").Attributes): boolean;
+        set(key: string | any, val?: string | any, options?: import("@converse/skeletor/src/types/model.js").Options): false | any;
+        _changing: boolean;
+        _previousAttributes: any;
+        id: any;
+        _pending: boolean | import("@converse/skeletor/src/types/model.js").Options;
+        unset(attr: string, options?: import("@converse/skeletor/src/types/model.js").Options): false | any;
+        clear(options: import("@converse/skeletor/src/types/model.js").Options): false | any;
+        hasChanged(attr?: string): any;
+        changedAttributes(diff: any): any;
+        previous(attr?: string): any;
+        previousAttributes(): any;
+        fetch(options?: import("@converse/skeletor/src/types/model.js").Options): any;
+        save(key?: string | import("@converse/skeletor/src/types/model.js").Attributes, val?: boolean | number | string | import("@converse/skeletor/src/types/model.js").Options, options?: import("@converse/skeletor/src/types/model.js").Options): any;
+        destroy(options?: import("@converse/skeletor/src/types/model.js").Options): boolean;
+        url(): any;
+        parse(resp: import("@converse/skeletor/src/types/model.js").Options, options?: import("@converse/skeletor/src/types/model.js").Options): import("@converse/skeletor/src/types/model.js").Options;
+        isNew(): boolean;
+        isValid(options?: import("@converse/skeletor/src/types/model.js").Options): boolean;
+        _validate(attrs: import("@converse/skeletor/src/types/model.js").Attributes, options?: import("@converse/skeletor/src/types/model.js").Options): boolean;
+        on(name: string, callback: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options: Record<string, any>) => any, context: any): any;
+        _events: any;
+        _listeners: {};
+        listenTo(obj: any, name: string, callback?: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options: Record<string, any>) => any): any;
+        _listeningTo: {};
+        _listenId: any;
+        off(name: string, callback: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options: Record<string, any>) => any, context?: any): any;
+        stopListening(obj?: any, name?: string, callback?: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options: Record<string, any>) => any): any;
+        once(name: string, callback: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options: Record<string, any>) => any, context: any): any;
+        listenToOnce(obj: any, name: string, callback?: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options: Record<string, any>) => any): any;
+        trigger(name: string, ...args: any[]): any;
+        constructor: Function;
+        toString(): string;
+        toLocaleString(): string;
+        valueOf(): Object;
+        hasOwnProperty(v: PropertyKey): boolean;
+        isPrototypeOf(v: Object): boolean;
+        propertyIsEnumerable(v: PropertyKey): boolean;
+    };
+} & typeof Message;
+declare class MUCMessage extends MUCMessage_base {
+    /**
+     * @typedef {import('./occupant').default} MUCOccupant
+     */
+    initialize(): Promise<void>;
     get occupants(): any;
     get occupants(): any;
     /**
     /**
      * Determines whether this messsage may be moderated,
      * Determines whether this messsage may be moderated,

+ 69 - 5
src/headless/types/plugins/muc/muc.d.ts

@@ -1,5 +1,74 @@
 export default MUC;
 export default MUC;
 declare const MUC_base: {
 declare const MUC_base: {
+    new (...args: any[]): {
+        _vcard: import("../vcard/vcard").default;
+        lazy_load_vcard: boolean;
+        initialize(): void;
+        readonly vcard: import("../vcard/vcard").default;
+        getVCard(create?: boolean): Promise<import("../vcard/vcard").default | null>;
+        cid: any;
+        attributes: {};
+        validationError: string;
+        collection: any;
+        changed: {};
+        browserStorage: Storage;
+        _browserStorage: Storage;
+        readonly idAttribute: string;
+        readonly cidPrefix: string;
+        preinitialize(): void;
+        validate(attrs: object, options?: object): string;
+        toJSON(): any;
+        sync(method: "create" | "update" | "patch" | "delete" | "read", model: Model, options: import("@converse/skeletor/src/types/model").Options): any;
+        get(attr: string): any;
+        keys(): string[];
+        values(): any[];
+        pairs(): [string, any][];
+        entries(): [string, any][];
+        invert(): any;
+        pick(...args: any[]): any;
+        omit(...args: any[]): any;
+        isEmpty(): any;
+        has(attr: string): boolean;
+        matches(attrs: import("@converse/skeletor/src/types/model").Attributes): boolean;
+        set(key: string | any, val?: string | any, options?: import("@converse/skeletor/src/types/model").Options): false | any;
+        _changing: boolean;
+        _previousAttributes: any;
+        id: any;
+        _pending: boolean | import("@converse/skeletor/src/types/model").Options;
+        unset(attr: string, options?: import("@converse/skeletor/src/types/model").Options): false | any;
+        clear(options: import("@converse/skeletor/src/types/model").Options): false | any;
+        hasChanged(attr?: string): any;
+        changedAttributes(diff: any): any;
+        previous(attr?: string): any;
+        previousAttributes(): any;
+        fetch(options?: import("@converse/skeletor/src/types/model").Options): any;
+        save(key?: string | import("@converse/skeletor/src/types/model").Attributes, val?: boolean | number | string | import("@converse/skeletor/src/types/model").Options, options?: import("@converse/skeletor/src/types/model").Options): any;
+        destroy(options?: import("@converse/skeletor/src/types/model").Options): boolean;
+        url(): any;
+        parse(resp: import("@converse/skeletor/src/types/model").Options, options?: import("@converse/skeletor/src/types/model").Options): import("@converse/skeletor/src/types/model").Options;
+        isNew(): boolean;
+        isValid(options?: import("@converse/skeletor/src/types/model").Options): boolean;
+        _validate(attrs: import("@converse/skeletor/src/types/model").Attributes, options?: import("@converse/skeletor/src/types/model").Options): boolean;
+        on(name: string, callback: (event: any, model: Model, collection: import("@converse/skeletor").Collection, options: Record<string, any>) => any, context: any): any;
+        _events: any;
+        _listeners: {};
+        listenTo(obj: any, name: string, callback?: (event: any, model: Model, collection: import("@converse/skeletor").Collection, options: Record<string, any>) => any): any;
+        _listeningTo: {};
+        _listenId: any;
+        off(name: string, callback: (event: any, model: Model, collection: import("@converse/skeletor").Collection, options: Record<string, any>) => any, context?: any): any;
+        stopListening(obj?: any, name?: string, callback?: (event: any, model: Model, collection: import("@converse/skeletor").Collection, options: Record<string, any>) => any): any;
+        once(name: string, callback: (event: any, model: Model, collection: import("@converse/skeletor").Collection, options: Record<string, any>) => any, context: any): any;
+        listenToOnce(obj: any, name: string, callback?: (event: any, model: Model, collection: import("@converse/skeletor").Collection, options: Record<string, any>) => any): any;
+        trigger(name: string, ...args: any[]): any;
+        constructor: Function;
+        toString(): string;
+        toLocaleString(): string;
+        valueOf(): Object;
+        hasOwnProperty(v: PropertyKey): boolean;
+        isPrototypeOf(v: Object): boolean;
+        propertyIsEnumerable(v: PropertyKey): boolean;
+    };
+} & {
     new (...args: any[]): {
     new (...args: any[]): {
         disable_mam: boolean;
         disable_mam: boolean;
         initialize(): Promise<void>;
         initialize(): Promise<void>;
@@ -231,11 +300,6 @@ declare class MUC extends MUC_base {
         type: string;
         type: string;
     };
     };
     initialize(): Promise<void>;
     initialize(): Promise<void>;
-    /**
-     * @public
-     * @type {VCard}
-     */
-    public vcard: import("../vcard/vcard").default;
     initialized: any;
     initialized: any;
     debouncedRejoin: import("lodash").DebouncedFunc<() => Promise<void>>;
     debouncedRejoin: import("lodash").DebouncedFunc<() => Promise<void>>;
     isEntered(): boolean;
     isEntered(): boolean;

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

@@ -1,5 +1,74 @@
 export default MUCOccupant;
 export default MUCOccupant;
 declare const MUCOccupant_base: {
 declare const MUCOccupant_base: {
+    new (...args: any[]): {
+        _vcard: import("../vcard").VCard;
+        lazy_load_vcard: boolean;
+        initialize(): void;
+        readonly vcard: import("../vcard").VCard;
+        getVCard(create?: boolean): Promise<import("../vcard").VCard | null>;
+        cid: any;
+        attributes: {};
+        validationError: string;
+        collection: any;
+        changed: {};
+        browserStorage: Storage;
+        _browserStorage: Storage;
+        readonly idAttribute: string;
+        readonly cidPrefix: string;
+        preinitialize(): void;
+        validate(attrs: object, options?: object): string;
+        toJSON(): any;
+        sync(method: "create" | "update" | "patch" | "delete" | "read", model: Model, options: import("@converse/skeletor/src/types/model").Options): any;
+        get(attr: string): any;
+        keys(): string[];
+        values(): any[];
+        pairs(): [string, any][];
+        entries(): [string, any][];
+        invert(): any;
+        pick(...args: any[]): any;
+        omit(...args: any[]): any;
+        isEmpty(): any;
+        has(attr: string): boolean;
+        matches(attrs: import("@converse/skeletor/src/types/model").Attributes): boolean;
+        set(key: string | any, val?: string | any, options?: import("@converse/skeletor/src/types/model").Options): false | any;
+        _changing: boolean;
+        _previousAttributes: any;
+        id: any;
+        _pending: boolean | import("@converse/skeletor/src/types/model").Options;
+        unset(attr: string, options?: import("@converse/skeletor/src/types/model").Options): false | any;
+        clear(options: import("@converse/skeletor/src/types/model").Options): false | any;
+        hasChanged(attr?: string): any;
+        changedAttributes(diff: any): any;
+        previous(attr?: string): any;
+        previousAttributes(): any;
+        fetch(options?: import("@converse/skeletor/src/types/model").Options): any;
+        save(key?: string | import("@converse/skeletor/src/types/model").Attributes, val?: boolean | number | string | import("@converse/skeletor/src/types/model").Options, options?: import("@converse/skeletor/src/types/model").Options): any;
+        destroy(options?: import("@converse/skeletor/src/types/model").Options): boolean;
+        url(): any;
+        parse(resp: import("@converse/skeletor/src/types/model").Options, options?: import("@converse/skeletor/src/types/model").Options): import("@converse/skeletor/src/types/model").Options;
+        isNew(): boolean;
+        isValid(options?: import("@converse/skeletor/src/types/model").Options): boolean;
+        _validate(attrs: import("@converse/skeletor/src/types/model").Attributes, options?: import("@converse/skeletor/src/types/model").Options): boolean;
+        on(name: string, callback: (event: any, model: Model, collection: import("@converse/skeletor").Collection, options: Record<string, any>) => any, context: any): any;
+        _events: any;
+        _listeners: {};
+        listenTo(obj: any, name: string, callback?: (event: any, model: Model, collection: import("@converse/skeletor").Collection, options: Record<string, any>) => any): any;
+        _listeningTo: {};
+        _listenId: any;
+        off(name: string, callback: (event: any, model: Model, collection: import("@converse/skeletor").Collection, options: Record<string, any>) => any, context?: any): any;
+        stopListening(obj?: any, name?: string, callback?: (event: any, model: Model, collection: import("@converse/skeletor").Collection, options: Record<string, any>) => any): any;
+        once(name: string, callback: (event: any, model: Model, collection: import("@converse/skeletor").Collection, options: Record<string, any>) => any, context: any): any;
+        listenToOnce(obj: any, name: string, callback?: (event: any, model: Model, collection: import("@converse/skeletor").Collection, options: Record<string, any>) => any): any;
+        trigger(name: string, ...args: any[]): any;
+        constructor: Function;
+        toString(): string;
+        toLocaleString(): string;
+        valueOf(): Object;
+        hasOwnProperty(v: PropertyKey): boolean;
+        isPrototypeOf(v: Object): boolean;
+        propertyIsEnumerable(v: PropertyKey): boolean;
+    };
+} & {
     new (...args: any[]): {
     new (...args: any[]): {
         disable_mam: boolean;
         disable_mam: boolean;
         initialize(): Promise<void>;
         initialize(): Promise<void>;
@@ -205,8 +274,6 @@ declare class MUCOccupant extends MUCOccupant_base {
      * @typedef {import('../chat/types').MessageAttributes} MessageAttributes
      * @typedef {import('../chat/types').MessageAttributes} MessageAttributes
      * @typedef {import('../../shared/errors').StanzaParseError} StanzaParseError
      * @typedef {import('../../shared/errors').StanzaParseError} StanzaParseError
      */
      */
-    constructor(attributes: any, options: any);
-    vcard: any;
     initialize(): Promise<void>;
     initialize(): Promise<void>;
     defaults(): {
     defaults(): {
         hats: any[];
         hats: any[];
@@ -238,8 +305,8 @@ declare class MUCOccupant extends MUCOccupant_base {
     isModerator(): boolean;
     isModerator(): boolean;
     isSelf(): any;
     isSelf(): any;
 }
 }
-import { Model } from '@converse/skeletor';
-import MUCMessages from './messages.js';
-import { ROLES } from './constants.js';
-import { AFFILIATIONS } from './constants.js';
+import { Model } from "@converse/skeletor";
+import MUCMessages from "./messages.js";
+import { ROLES } from "./constants.js";
+import { AFFILIATIONS } from "./constants.js";
 //# sourceMappingURL=occupant.d.ts.map
 //# sourceMappingURL=occupant.d.ts.map

+ 69 - 3
src/headless/types/plugins/roster/contact.d.ts

@@ -1,5 +1,74 @@
 export default RosterContact;
 export default RosterContact;
 declare const RosterContact_base: {
 declare const RosterContact_base: {
+    new (...args: any[]): {
+        _vcard: import("../vcard/vcard.js").default;
+        lazy_load_vcard: boolean;
+        initialize(): void;
+        readonly vcard: import("../vcard/vcard.js").default;
+        getVCard(create?: boolean): Promise<import("../vcard/vcard.js").default | null>;
+        cid: any;
+        attributes: {};
+        validationError: string;
+        collection: any;
+        changed: {};
+        browserStorage: Storage;
+        _browserStorage: Storage;
+        readonly idAttribute: string;
+        readonly cidPrefix: string;
+        preinitialize(): void;
+        validate(attrs: object, options?: object): string;
+        toJSON(): any;
+        sync(method: "create" | "update" | "patch" | "delete" | "read", model: Model, options: import("@converse/skeletor/src/types/model.js").Options): any;
+        get(attr: string): any;
+        keys(): string[];
+        values(): any[];
+        pairs(): [string, any][];
+        entries(): [string, any][];
+        invert(): any;
+        pick(...args: any[]): any;
+        omit(...args: any[]): any;
+        isEmpty(): any;
+        has(attr: string): boolean;
+        matches(attrs: import("@converse/skeletor/src/types/model.js").Attributes): boolean;
+        set(key: string | any, val?: string | any, options?: import("@converse/skeletor/src/types/model.js").Options): false | any;
+        _changing: boolean;
+        _previousAttributes: any;
+        id: any;
+        _pending: boolean | import("@converse/skeletor/src/types/model.js").Options;
+        unset(attr: string, options?: import("@converse/skeletor/src/types/model.js").Options): false | any;
+        clear(options: import("@converse/skeletor/src/types/model.js").Options): false | any;
+        hasChanged(attr?: string): any;
+        changedAttributes(diff: any): any;
+        previous(attr?: string): any;
+        previousAttributes(): any;
+        fetch(options?: import("@converse/skeletor/src/types/model.js").Options): any;
+        save(key?: string | import("@converse/skeletor/src/types/model.js").Attributes, val?: boolean | number | string | import("@converse/skeletor/src/types/model.js").Options, options?: import("@converse/skeletor/src/types/model.js").Options): any;
+        destroy(options?: import("@converse/skeletor/src/types/model.js").Options): boolean;
+        url(): any;
+        parse(resp: import("@converse/skeletor/src/types/model.js").Options, options?: import("@converse/skeletor/src/types/model.js").Options): import("@converse/skeletor/src/types/model.js").Options;
+        isNew(): boolean;
+        isValid(options?: import("@converse/skeletor/src/types/model.js").Options): boolean;
+        _validate(attrs: import("@converse/skeletor/src/types/model.js").Attributes, options?: import("@converse/skeletor/src/types/model.js").Options): boolean;
+        on(name: string, callback: (event: any, model: Model, collection: import("@converse/skeletor").Collection, options: Record<string, any>) => any, context: any): any;
+        _events: any;
+        _listeners: {};
+        listenTo(obj: any, name: string, callback?: (event: any, model: Model, collection: import("@converse/skeletor").Collection, options: Record<string, any>) => any): any;
+        _listeningTo: {};
+        _listenId: any;
+        off(name: string, callback: (event: any, model: Model, collection: import("@converse/skeletor").Collection, options: Record<string, any>) => any, context?: any): any;
+        stopListening(obj?: any, name?: string, callback?: (event: any, model: Model, collection: import("@converse/skeletor").Collection, options: Record<string, any>) => any): any;
+        once(name: string, callback: (event: any, model: Model, collection: import("@converse/skeletor").Collection, options: Record<string, any>) => any, context: any): any;
+        listenToOnce(obj: any, name: string, callback?: (event: any, model: Model, collection: import("@converse/skeletor").Collection, options: Record<string, any>) => any): any;
+        trigger(name: string, ...args: any[]): any;
+        constructor: Function;
+        toString(): string;
+        toLocaleString(): string;
+        valueOf(): Object;
+        hasOwnProperty(v: PropertyKey): boolean;
+        isPrototypeOf(v: Object): boolean;
+        propertyIsEnumerable(v: PropertyKey): boolean;
+    };
+} & {
     new (...args: any[]): {
     new (...args: any[]): {
         setColor(): Promise<void>;
         setColor(): Promise<void>;
         getIdentifier(): any;
         getIdentifier(): any;
@@ -70,9 +139,6 @@ declare const RosterContact_base: {
     };
     };
 } & typeof Model;
 } & typeof Model;
 declare class RosterContact extends RosterContact_base {
 declare class RosterContact extends RosterContact_base {
-    constructor(attrs: any, options: any);
-    /** @type {import('../vcard/vcard').default} */
-    vcard: import("../vcard/vcard").default;
     defaults(): {
     defaults(): {
         groups: any[];
         groups: any[];
         num_unread: number;
         num_unread: number;

+ 70 - 2
src/headless/types/plugins/status/status.d.ts

@@ -1,4 +1,73 @@
 declare const XMPPStatus_base: {
 declare const XMPPStatus_base: {
+    new (...args: any[]): {
+        _vcard: import("../vcard/vcard.js").default;
+        lazy_load_vcard: boolean;
+        initialize(): void;
+        readonly vcard: import("../vcard/vcard.js").default;
+        getVCard(create?: boolean): Promise<import("../vcard/vcard.js").default | null>;
+        cid: any;
+        attributes: {};
+        validationError: string;
+        collection: any;
+        changed: {};
+        browserStorage: Storage;
+        _browserStorage: Storage;
+        readonly idAttribute: string;
+        readonly cidPrefix: string;
+        preinitialize(): void;
+        validate(attrs: object, options?: object): string;
+        toJSON(): any;
+        sync(method: "create" | "update" | "patch" | "delete" | "read", model: Model, options: import("@converse/skeletor/src/types/model.js").Options): any;
+        get(attr: string): any;
+        keys(): string[];
+        values(): any[];
+        pairs(): [string, any][];
+        entries(): [string, any][];
+        invert(): any;
+        pick(...args: any[]): any;
+        omit(...args: any[]): any;
+        isEmpty(): any;
+        has(attr: string): boolean;
+        matches(attrs: import("@converse/skeletor/src/types/model.js").Attributes): boolean;
+        set(key: string | any, val?: string | any, options?: import("@converse/skeletor/src/types/model.js").Options): false | any;
+        _changing: boolean;
+        _previousAttributes: any;
+        id: any;
+        _pending: boolean | import("@converse/skeletor/src/types/model.js").Options;
+        unset(attr: string, options?: import("@converse/skeletor/src/types/model.js").Options): false | any;
+        clear(options: import("@converse/skeletor/src/types/model.js").Options): false | any;
+        hasChanged(attr?: string): any;
+        changedAttributes(diff: any): any;
+        previous(attr?: string): any;
+        previousAttributes(): any;
+        fetch(options?: import("@converse/skeletor/src/types/model.js").Options): any;
+        save(key?: string | import("@converse/skeletor/src/types/model.js").Attributes, val?: boolean | number | string | import("@converse/skeletor/src/types/model.js").Options, options?: import("@converse/skeletor/src/types/model.js").Options): any;
+        destroy(options?: import("@converse/skeletor/src/types/model.js").Options): boolean;
+        url(): any;
+        parse(resp: import("@converse/skeletor/src/types/model.js").Options, options?: import("@converse/skeletor/src/types/model.js").Options): import("@converse/skeletor/src/types/model.js").Options;
+        isNew(): boolean;
+        isValid(options?: import("@converse/skeletor/src/types/model.js").Options): boolean;
+        _validate(attrs: import("@converse/skeletor/src/types/model.js").Attributes, options?: import("@converse/skeletor/src/types/model.js").Options): boolean;
+        on(name: string, callback: (event: any, model: Model, collection: import("@converse/skeletor").Collection, options: Record<string, any>) => any, context: any): any;
+        _events: any;
+        _listeners: {};
+        listenTo(obj: any, name: string, callback?: (event: any, model: Model, collection: import("@converse/skeletor").Collection, options: Record<string, any>) => any): any;
+        _listeningTo: {};
+        _listenId: any;
+        off(name: string, callback: (event: any, model: Model, collection: import("@converse/skeletor").Collection, options: Record<string, any>) => any, context?: any): any;
+        stopListening(obj?: any, name?: string, callback?: (event: any, model: Model, collection: import("@converse/skeletor").Collection, options: Record<string, any>) => any): any;
+        once(name: string, callback: (event: any, model: Model, collection: import("@converse/skeletor").Collection, options: Record<string, any>) => any, context: any): any;
+        listenToOnce(obj: any, name: string, callback?: (event: any, model: Model, collection: import("@converse/skeletor").Collection, options: Record<string, any>) => any): any;
+        trigger(name: string, ...args: any[]): any;
+        constructor: Function;
+        toString(): string;
+        toLocaleString(): string;
+        valueOf(): Object;
+        hasOwnProperty(v: PropertyKey): boolean;
+        isPrototypeOf(v: Object): boolean;
+        propertyIsEnumerable(v: PropertyKey): boolean;
+    };
+} & {
     new (...args: any[]): {
     new (...args: any[]): {
         setColor(): Promise<void>;
         setColor(): Promise<void>;
         getIdentifier(): any;
         getIdentifier(): any;
@@ -69,8 +138,6 @@ declare const XMPPStatus_base: {
     };
     };
 } & typeof Model;
 } & typeof Model;
 export default class XMPPStatus extends XMPPStatus_base {
 export default class XMPPStatus extends XMPPStatus_base {
-    constructor(attributes: any, options: any);
-    vcard: any;
     defaults(): {
     defaults(): {
         status: any;
         status: any;
     };
     };
@@ -81,6 +148,7 @@ export default class XMPPStatus extends XMPPStatus_base {
      * @param {Object} [options]
      * @param {Object} [options]
      */
      */
     set(key: string | any, val?: string | any, options?: any): false | this;
     set(key: string | any, val?: string | any, options?: any): false | this;
+    initialize(): void;
     getDisplayName(): any;
     getDisplayName(): any;
     getNickname(): any;
     getNickname(): any;
     /** Constructs a presence stanza
     /** Constructs a presence stanza

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

@@ -9,11 +9,11 @@ export function createStanza(type: "get" | "set" | "result", jid: string, vcard_
  */
  */
 export function onOccupantAvatarChanged(occupant: MUCOccupant): void;
 export function onOccupantAvatarChanged(occupant: MUCOccupant): void;
 /**
 /**
- * @param {Model} model
+ * @param {Model|MUCOccupant|MUCMessage} model
  * @param {boolean} [create=true]
  * @param {boolean} [create=true]
  * @returns {Promise<VCard>}
  * @returns {Promise<VCard>}
  */
  */
-export function getVCardForModel(model: Model, create?: boolean): Promise<VCard>;
+export function getVCardForModel(model: Model | MUCOccupant | MUCMessage, create?: boolean): Promise<VCard>;
 /**
 /**
  * @param {MUCOccupant} occupant
  * @param {MUCOccupant} occupant
  * @param {boolean} [create=true]
  * @param {boolean} [create=true]

+ 0 - 6
src/headless/types/shared/model-with-contact.d.ts

@@ -5,7 +5,6 @@
 export default function ModelWithContact<T extends import("./types").ModelExtender>(BaseModel: T): {
 export default function ModelWithContact<T extends import("./types").ModelExtender>(BaseModel: T): {
     new (...args: any[]): {
     new (...args: any[]): {
         /**
         /**
-         * @typedef {import('../plugins/vcard/vcard').default} VCard
          * @typedef {import('../plugins/roster/contact').default} RosterContact
          * @typedef {import('../plugins/roster/contact').default} RosterContact
          * @typedef {import('./_converse.js').XMPPStatus} XMPPStatus
          * @typedef {import('./_converse.js').XMPPStatus} XMPPStatus
          */
          */
@@ -16,11 +15,6 @@ export default function ModelWithContact<T extends import("./types").ModelExtend
          * @type {RosterContact|XMPPStatus}
          * @type {RosterContact|XMPPStatus}
          */
          */
         contact: import("../plugins/roster/contact").default | import("../index.js").XMPPStatus;
         contact: import("../plugins/roster/contact").default | import("../index.js").XMPPStatus;
-        /**
-         * @public
-         * @type {VCard}
-         */
-        vcard: import("../plugins/vcard/vcard").default;
         /**
         /**
          * @param {string} jid
          * @param {string} jid
          */
          */

+ 1 - 1
src/plugins/chatview/tests/message-avatar.js

@@ -68,7 +68,7 @@ describe("A Chat Message", function () {
         const image_type = 'image/svg+xml';
         const image_type = 'image/svg+xml';
 
 
         // Change contact avatar and check that it reflects
         // Change contact avatar and check that it reflects
-        contact.vcard.set({
+        (await contact.getVCard()).set({
             image,
             image,
             image_type,
             image_type,
             vcard_updated: (new Date()).toISOString()
             vcard_updated: (new Date()).toISOString()