소스 검색

Lazy load VCards for messages and roster contacts

JC Brand 4 달 전
부모
커밋
1322d27de2
37개의 변경된 파일587개의 추가작업 그리고 415개의 파일을 삭제
  1. 13 10
      src/headless/plugins/chat/model.js
  2. 2 4
      src/headless/plugins/muc/muc.js
  3. 6 2
      src/headless/plugins/roster/contact.js
  4. 27 13
      src/headless/plugins/vcard/api.js
  5. 5 0
      src/headless/plugins/vcard/plugin.js
  6. 20 0
      src/headless/plugins/vcard/types.ts
  7. 29 33
      src/headless/plugins/vcard/utils.js
  8. 30 10
      src/headless/plugins/vcard/vcard.js
  9. 0 5
      src/headless/plugins/vcard/vcards.js
  10. 2 0
      src/headless/shared/message.js
  11. 7 9
      src/headless/shared/model-with-vcard.js
  12. 10 1
      src/headless/shared/types.ts
  13. 3 3
      src/headless/types/plugins/chat/model.d.ts
  14. 24 26
      src/headless/types/plugins/muc/muc.d.ts
  15. 1 1
      src/headless/types/plugins/muc/occupant.d.ts
  16. 5 2
      src/headless/types/plugins/roster/contact.d.ts
  17. 1 1
      src/headless/types/plugins/status/status.d.ts
  18. 4 4
      src/headless/types/plugins/vcard/api.d.ts
  19. 19 0
      src/headless/types/plugins/vcard/types.d.ts
  20. 10 7
      src/headless/types/plugins/vcard/utils.d.ts
  21. 13 6
      src/headless/types/plugins/vcard/vcard.d.ts
  22. 1 1
      src/headless/types/shared/message.d.ts
  23. 3 4
      src/headless/types/shared/model-with-vcard.d.ts
  24. 8 1
      src/headless/types/shared/types.d.ts
  25. 245 209
      src/i18n/converse.pot
  26. 8 1
      src/plugins/chatview/heading.js
  27. 24 15
      src/plugins/chatview/templates/chat-head.js
  28. 5 4
      src/plugins/chatview/tests/messages.js
  29. 1 1
      src/plugins/muc-views/tests/muc.js
  30. 13 7
      src/plugins/rosterview/contactview.js
  31. 20 17
      src/plugins/rosterview/tests/roster.js
  32. 7 2
      src/shared/chat/message.js
  33. 2 2
      src/shared/components/observable.js
  34. 1 6
      src/shared/modals/user-details.js
  35. 8 2
      src/types/plugins/chatview/heading.d.ts
  36. 5 3
      src/types/plugins/rosterview/contactview.d.ts
  37. 5 3
      src/types/shared/chat/message.d.ts

+ 13 - 10
src/headless/plugins/chat/model.js

@@ -28,13 +28,13 @@ class ChatBox extends ModelWithVCard(ModelWithMessages(ModelWithContact(ColorAwa
 
     defaults () {
         return {
-            'bookmarked': false,
-            'hidden': isUniView() && !api.settings.get('singleton'),
-            'message_type': 'chat',
-            'num_unread': 0,
-            'time_opened': this.get('time_opened') || (new Date()).getTime(),
-            'time_sent': (new Date(0)).toISOString(),
-            'type': PRIVATE_CHAT_TYPE,
+            bookmarked: false,
+            hidden: isUniView() && !api.settings.get('singleton'),
+            message_type: 'chat',
+            num_unread: 0,
+            time_opened: this.get('time_opened') || (new Date()).getTime(),
+            time_sent: (new Date(0)).toISOString(),
+            type: PRIVATE_CHAT_TYPE,
         }
     }
 
@@ -130,12 +130,15 @@ class ChatBox extends ModelWithVCard(ModelWithMessages(ModelWithContact(ColorAwa
     }
 
     /**
-     * @returns {string}
+     * @returns {string|null}
      */
     getDisplayName () {
         if (this.contact) {
-            return this.contact.getDisplayName();
-        } else if (this.vcard) {
+            const display_name = this.contact.getDisplayName(false);
+            if (display_name) return display_name;
+        }
+
+        if (this.vcard) {
             return this.vcard.getDisplayName();
         } else {
             return this.get('jid');

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

@@ -31,7 +31,6 @@ import { getUniqueId, isErrorObject, safeSave } from '../../utils/index.js';
 import { isUniView } from '../../utils/session.js';
 import { parseMUCMessage, parseMUCPresence } from './parsers.js';
 import { sendMarker } from '../../shared/actions.js';
-import BaseMessage from '../../shared/message';
 import ChatBoxBase from '../../shared/chatbox';
 import ColorAwareModel from '../../shared/color';
 import ModelWithMessages from '../../shared/model-with-messages';
@@ -46,8 +45,7 @@ const { u, stx } = converse.env;
  */
 class MUC extends ModelWithVCard(ModelWithMessages(ColorAwareModel(ChatBoxBase))) {
     /**
-     * @typedef {import('../vcard/vcard').default} VCard
-     * @typedef {import('../chat/message.js').default} Message
+     * @typedef {import('../../shared/message.js').default} BaseMessage
      * @typedef {import('./message.js').default} MUCMessage
      * @typedef {import('./occupant.js').default} MUCOccupant
      * @typedef {import('./affiliations/utils.js').NonOutcastAffiliation} NonOutcastAffiliation
@@ -243,7 +241,7 @@ class MUC extends ModelWithVCard(ModelWithMessages(ColorAwareModel(ChatBoxBase))
     /**
      * Given the passed in MUC message, send a XEP-0333 chat marker.
      * @async
-     * @param {Message} msg
+     * @param {BaseMessage} msg
      * @param {('received'|'displayed'|'acknowledged')} [type='displayed']
      * @param {boolean} [force=false] - Whether a marker should be sent for the
      *  message, even if it didn't include a `markable` element.

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

@@ -23,6 +23,7 @@ class RosterContact extends ModelWithVCard(ColorAwareModel(Model)) {
     }
 
     async initialize (attrs) {
+        this.lazy_load_vcard = true;
         super.initialize();
         this.initialized = getOpenPromise();
         this.setPresence();
@@ -66,8 +67,11 @@ class RosterContact extends ModelWithVCard(ColorAwareModel(Model)) {
         api.chats.open(this.get('jid'), {}, true);
     }
 
-    getDisplayName () {
-        return this.get('nickname') || this.vcard?.getDisplayName() || this.get('jid');
+    /**
+     * @returns {string|null}
+     */
+    getDisplayName (jid_fallback=true) {
+        return this.get('nickname') || this.vcard?.getDisplayName() || (jid_fallback ? this.get('jid') : null);
     }
 
     /**

+ 27 - 13
src/headless/plugins/vcard/api.js

@@ -5,7 +5,7 @@ import log from "../../log.js";
 import _converse from '../../shared/_converse.js';
 import api from '../../shared/api/index.js';
 import converse from "../../shared/api/public.js";
-import { createStanza, getVCard } from './utils.js';
+import { createStanza, fetchVCard } from './utils.js';
 
 const { dayjs, u } = converse.env;
 
@@ -86,9 +86,9 @@ export default {
          *     attribute or a `muc_jid` attribute.
          * @param {boolean} [force] A boolean indicating whether the vcard should be
          *     fetched from the server even if it's been fetched before.
-         * @returns {promise} A Promise which resolves with the VCard data for a particular JID or for
-         *     a `Model` instance which represents an entity with a JID (such as a roster contact,
-         *     chat or chatroom occupant).
+         * @returns {Promise<import("./types").VCardResult|null>} A Promise which resolves
+         *     with the VCard data for a particular JID or for a `Model` instance which
+         *     represents an entity with a JID (such as a roster contact, chat or chatroom occupant).
          *
          * @example
          * const { api } = _converse;
@@ -100,20 +100,34 @@ export default {
          *     );
          * });
          */
-         get (model, force) {
-            if (typeof model === 'string') {
-                return getVCard(model);
+        async get(model, force) {
+            if (typeof model === "string") return fetchVCard(model);
+
+            // For a VCard fetch that returned an error, we
+            // check how long ago it was fetched. If it was longer ago than
+            // the last 7 days plus some jitter (to prevent an IQ fetch flood),
+            // then we try again.
+            const { random, round } = Math;
+            const error_date = model.get("vcard_error");
+            const already_tried_recently =
+                error_date &&
+                dayjs(error_date).isBetween(
+                    dayjs().subtract(7, "days").subtract(round(random() * 24), "hours"),
+                    dayjs().subtract(7, "days").add(round(random() * 24), "hours")
+                );
+            if (already_tried_recently) {
+                return;
             }
-            const error_date = model.get('vcard_error');
-            const already_tried_today = error_date && dayjs(error_date).isSame(new Date(), "day");
-            if (force || !model.get('vcard_updated') && !already_tried_today) {
-                const jid = model.get('jid');
+
+
+            if (force || (!model.get("vcard_updated") && !already_tried_recently)) {
+                const jid = model.get("jid");
                 if (!jid) {
                     log.error("No JID to get vcard for");
                 }
-                return getVCard(jid);
+                return fetchVCard(jid);
             } else {
-                return Promise.resolve({});
+                return null;
             }
         },
 

+ 5 - 0
src/headless/plugins/vcard/plugin.js

@@ -39,6 +39,11 @@ converse.plugins.add('converse-vcard', {
         api.listen.on('addClientFeatures', () => api.disco.own.features.add(Strophe.NS.VCARD));
         api.listen.on('clearSession', () => clearVCardsSession());
 
+        api.listen.on('visibilityChanged', ({ el }) => {
+            const { model } = el;
+            if (model?.vcard) model.vcard.trigger('visibilityChanged');
+        });
+
         Object.assign(_converse.api, vcard_api);
     }
 });

+ 20 - 0
src/headless/plugins/vcard/types.ts

@@ -0,0 +1,20 @@
+import { ModelOptions } from '../../shared/types';
+
+export interface VCardModelOptions extends ModelOptions {
+    lazy_load?: boolean; // Should the VCard be fetched only when needed?
+}
+
+export interface VCardResult {
+    email?: string;
+    fullname?: string;
+    image?: string;
+    image_hash?: string;
+    image_type?: string;
+    nickname?: string;
+    role?: string;
+    stanza: Element;
+    error?: Error;
+    url?: string;
+    vcard_error?: string;
+    vcard_updated?: string;
+}

+ 29 - 33
src/headless/plugins/vcard/utils.js

@@ -14,30 +14,26 @@ import log from "../../log.js";
 import { initStorage } from "../../utils/storage.js";
 import { shouldClearCache } from "../../utils/session.js";
 import { isElement } from "../../utils/html.js";
+import { parseErrorStanza } from "../../shared/parsers.js";
 
 const { Strophe, $iq, u } = converse.env;
 
 /**
  * @param {Element} iq
+ * @returns {Promise<import("./types").VCardResult>}
  */
-async function onVCardData(iq) {
-    const vcard = iq.querySelector("vCard");
-    let result = {};
-    if (vcard !== null) {
-        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,
-            image_hash: undefined,
-        };
-    }
+export async function onVCardData(iq) {
+    const result = {
+        email: iq.querySelector("> vCard EMAIL USERID")?.textContent,
+        fullname: iq.querySelector("> vCard FN")?.textContent,
+        image: iq.querySelector("> vCard PHOTO BINVAL")?.textContent,
+        image_type: iq.querySelector("> vCard PHOTO TYPE")?.textContent,
+        nickname: iq.querySelector("vCard NICKNAME")?.textContent,
+        role: iq.querySelector("vCard ROLE")?.textContent,
+        stanza: iq, // TODO: remove?
+        url: iq.querySelector("URL")?.textContent,
+        vcard_updated: new Date().toISOString(),
+    };
     if (result.image) {
         const buffer = u.base64ToArrayBuffer(result["image"]);
         const ab = await crypto.subtle.digest("SHA-1", buffer);
@@ -76,17 +72,17 @@ export function onOccupantAvatarChanged(occupant) {
 
 /**
  * @param {Model|MUCOccupant|MUCMessage} model
- * @param {boolean} [create=true]
+ * @param {boolean} [lazy_load=false]
  * @returns {Promise<VCard|null>}
  */
-export async function getVCardForModel(model, create = true) {
+export async function getVCardForModel(model, lazy_load = false) {
     await initVCardCollection();
 
     let vcard;
     if (model instanceof _converse.exports.MUCOccupant) {
-        vcard = await getVCardForOccupant(/** @type {MUCOccupant} */ (model), create);
+        vcard = await getVCardForOccupant(/** @type {MUCOccupant} */ (model), lazy_load);
     } else if (model instanceof _converse.exports.MUCMessage) {
-        vcard = await getVCardForMUCMessage(/** @type {MUCMessage} */ (model), create);
+        vcard = await getVCardForMUCMessage(/** @type {MUCMessage} */ (model), lazy_load);
     } else {
         let jid;
         if (model instanceof _converse.exports.Message) {
@@ -103,7 +99,7 @@ export async function getVCardForModel(model, create = true) {
             return null;
         }
         const { vcards } = _converse.state;
-        vcard = vcards.get(jid) || (create ? vcards.create({ jid }) : null);
+        vcard = vcards.get(jid) || vcards.create({ jid }, { lazy_load });
     }
 
     if (vcard) {
@@ -114,10 +110,10 @@ export async function getVCardForModel(model, create = true) {
 
 /**
  * @param {MUCOccupant} occupant
- * @param {boolean} [create=true]
+ * @param {boolean} [lazy_load=false]
  * @returns {Promise<VCard|null>}
  */
-export async function getVCardForOccupant(occupant, create = true) {
+export async function getVCardForOccupant(occupant, lazy_load = true) {
     await api.waitUntil("VCardsInitialized");
 
     const { vcards, xmppstatus } = _converse.state;
@@ -129,7 +125,7 @@ export async function getVCardForOccupant(occupant, create = true) {
     } else {
         const jid = occupant.get("jid") || occupant.get("from");
         if (jid) {
-            return vcards.get(jid) || (create ? vcards.create({ jid }) : null);
+            return vcards.get(jid) || vcards.create({ jid }, { lazy_load });
         } else {
             log.debug(`Could not get VCard for occupant because no JID found!`);
             return null;
@@ -139,10 +135,10 @@ export async function getVCardForOccupant(occupant, create = true) {
 
 /**
  * @param {MUCMessage} message
- * @param {boolean} [create=true]
+ * @param {boolean} [lazy_load=true]
  * @returns {Promise<VCard|null>}
  */
-async function getVCardForMUCMessage(message, create = true) {
+async function getVCardForMUCMessage(message, lazy_load = true) {
     if (["error", "info"].includes(message.get("type"))) return;
 
     await api.waitUntil("VCardsInitialized");
@@ -155,7 +151,7 @@ async function getVCardForMUCMessage(message, create = true) {
     } else {
         const jid = message.occupant?.get("jid") || message.get("from");
         if (jid) {
-            return vcards.get(jid) || (create ? vcards.create({ jid }) : null);
+            return vcards.get(jid) || vcards.create({ jid }, { lazy_load });
         } else {
             log.warn(`Could not get VCard for message because no JID found! msgid: ${message.get("msgid")}`);
             return null;
@@ -179,7 +175,7 @@ async function initVCardCollection() {
                 success: resolve,
                 error: resolve,
             },
-            { "silent": true }
+            { silent: true }
         );
     });
     /**
@@ -203,7 +199,7 @@ export function clearVCardsSession() {
 /**
  * @param {string} jid
  */
-export async function getVCard(jid) {
+export async function fetchVCard(jid) {
     const bare_jid = _converse.session.get("bare_jid");
     const to = Strophe.getBareJidFromJid(jid) === bare_jid ? null : jid;
     let iq;
@@ -212,8 +208,8 @@ export async function getVCard(jid) {
     } catch (error) {
         return {
             jid,
-            stanza: isElement(error) ? error : null,
-            error: isElement(error) ? null : error,
+            stanza: isElement(error) ? error : null, // TODO: remove?
+            error: isElement(error) ? parseErrorStanza(error) : error,
             vcard_error: new Date().toISOString(),
         };
     }

+ 30 - 10
src/headless/plugins/vcard/vcard.js

@@ -1,17 +1,37 @@
-import { Model } from '@converse/skeletor';
+import { Model } from "@converse/skeletor";
+import _converse from "../../shared/_converse";
+import api from "../../shared/api/index";
 
-/**
- * Represents a VCard
- * @namespace _converse.VCard
- * @memberOf _converse
- */
 class VCard extends Model {
-    get idAttribute () {
-        return 'jid';
+    /**
+     * @param {import("../../shared/types").ModelAttributes} attrs
+     * @param {import("./types").VCardModelOptions} options
+     */
+    constructor(attrs, options) {
+        super(attrs, options);
+        this._vcard = null;
     }
 
-    getDisplayName () {
-        return this.get('nickname') || this.get('fullname') || this.get('jid');
+    /**
+     * @param {import("../../shared/types").ModelAttributes} [_attrs]
+     * @param {import("./types").VCardModelOptions} [options]
+     */
+    initialize(_attrs, options) {
+        this.lazy_load = !!options?.lazy_load;
+
+        if (this.lazy_load) {
+            this.once("visibilityChanged", () => api.vcard.update(this));
+        } else {
+            api.vcard.update(this);
+        }
+    }
+
+    get idAttribute() {
+        return "jid";
+    }
+
+    getDisplayName() {
+        return this.get("nickname") || this.get("fullname") || this.get("jid");
     }
 }
 

+ 0 - 5
src/headless/plugins/vcard/vcards.js

@@ -1,5 +1,4 @@
 import VCard from "./vcard";
-import api from "./api.js";
 import { Collection } from "@converse/skeletor";
 
 class VCards extends Collection {
@@ -8,10 +7,6 @@ class VCards extends Collection {
         super();
         this.model = VCard;
     }
-
-    initialize () {
-        this.on('add', v => v.get('jid') && api.vcard.update(v));
-    }
 }
 
 export default VCards;

+ 2 - 0
src/headless/shared/message.js

@@ -37,7 +37,9 @@ class BaseMessage extends ModelWithVCard(ModelWithContact(ColorAwareModel(Model)
     }
 
     initialize() {
+        this.lazy_load_vcard = true;
         super.initialize();
+
         if (!this.checkValidity()) return;
         this.chatbox = this.collection?.chatbox;
 

+ 7 - 9
src/headless/shared/model-with-vcard.js

@@ -1,12 +1,15 @@
 import { getVCardForModel } from "../plugins/vcard/utils.js";
 import _converse from "./_converse.js";
-import VCard from "../plugins/vcard/vcard.js";
 
 /**
  * @template {import('./types').ModelExtender} T
  * @param {T} BaseModel
  */
 export default function ModelWithVCard(BaseModel) {
+    /**
+     * @typedef {import('../plugins/vcard/vcard').default} VCard
+     */
+
     return class ModelWithVCard extends BaseModel {
         /**
          * @param {any[]} args
@@ -19,12 +22,7 @@ export default function ModelWithVCard(BaseModel) {
 
         initialize() {
             super.initialize();
-            if (this.lazy_load_vcard) {
-                this.getVCard(false);
-                this.once("visibilityChanged", () => this.getVCard());
-            } else {
-                this.getVCard();
-            }
+            this.getVCard();
         }
 
         get vcard() {
@@ -34,13 +32,13 @@ export default function ModelWithVCard(BaseModel) {
         /**
          * @returns {Promise<VCard|null>}
          */
-        async getVCard(create=true) {
+        async getVCard() {
             const { pluggable } = _converse;
             if (!pluggable.plugins["converse-vcard"]?.enabled(_converse)) return null;
 
             if (this._vcard) return this._vcard;
 
-            this._vcard = await getVCardForModel(this, create);
+            this._vcard = await getVCardForModel(this, this.lazy_load_vcard);
             this.trigger("vcard:add", { vcard: this._vcard });
 
             return this._vcard;

+ 10 - 1
src/headless/shared/types.ts

@@ -1,4 +1,13 @@
-import { Model } from '@converse/skeletor';
+import { Collection, Model } from '@converse/skeletor';
+
+export type ModelAttributes = Record<string, any>;
+
+export interface ModelOptions {
+    collection?: Collection;
+    parse?: boolean;
+    unset?: boolean;
+    silent?: boolean;
+}
 
 // Types for mixins.
 // -----------------

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

@@ -5,7 +5,7 @@ declare const ChatBox_base: {
         lazy_load_vcard: boolean;
         initialize(): void;
         readonly vcard: import("../vcard/vcard.js").default;
-        getVCard(create?: boolean): Promise<import("../vcard/vcard.js").default | null>;
+        getVCard(): Promise<import("../vcard/vcard.js").default | null>;
         cid: any;
         attributes: {};
         validationError: string;
@@ -367,9 +367,9 @@ declare class ChatBox extends ChatBox_base {
     onPresenceChanged(item: import("../roster/presence").default): void;
     close(): Promise<void>;
     /**
-     * @returns {string}
+     * @returns {string|null}
      */
-    getDisplayName(): string;
+    getDisplayName(): string | null;
     /**
      * @param {string} jid1
      * @param {string} jid2

+ 24 - 26
src/headless/types/plugins/muc/muc.d.ts

@@ -1,11 +1,11 @@
 export default MUC;
 declare const MUC_base: {
     new (...args: any[]): {
-        _vcard: import("../vcard/vcard").default;
+        _vcard: import("../vcard").VCard;
         lazy_load_vcard: boolean;
         initialize(): void;
-        readonly vcard: import("../vcard/vcard").default;
-        getVCard(create?: boolean): Promise<import("../vcard/vcard").default | null>;
+        readonly vcard: import("../vcard").VCard;
+        getVCard(): Promise<import("../vcard").VCard | null>;
         cid: any;
         attributes: {};
         validationError: string;
@@ -87,20 +87,20 @@ declare const MUC_base: {
         fetchMessages(): any;
         afterMessagesFetched(): void;
         onMessage(_attrs_or_error: import("../../shared/types").MessageAttributes | Error): Promise<void>;
-        getUpdatedMessageAttributes(message: BaseMessage<any>, attrs: import("../../shared/types").MessageAttributes): object;
-        updateMessage(message: BaseMessage<any>, attrs: import("../../shared/types").MessageAttributes): void;
-        handleCorrection(attrs: import("../../shared/types").MessageAttributes | import("./types").MUCMessageAttributes): Promise<BaseMessage<any> | void>;
+        getUpdatedMessageAttributes(message: import("../../shared/message.js").default<any>, attrs: import("../../shared/types").MessageAttributes): object;
+        updateMessage(message: import("../../shared/message.js").default<any>, attrs: import("../../shared/types").MessageAttributes): void;
+        handleCorrection(attrs: import("../../shared/types").MessageAttributes | import("./types").MUCMessageAttributes): Promise<import("../../shared/message.js").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<BaseMessage<any>>;
-        retractOwnMessage(message: BaseMessage<any>): void;
+        sendMessage(attrs?: any): Promise<import("../../shared/message.js").default<any>>;
+        retractOwnMessage(message: import("../../shared/message.js").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: BaseMessage<any>): void;
-        onMessageUploadChanged(message: BaseMessage<any>): Promise<void>;
+        onMessageAdded(message: import("../../shared/message.js").default<any>): void;
+        onMessageUploadChanged(message: import("../../shared/message.js").default<any>): Promise<void>;
         onScrolledChanged(): void;
         pruneHistoryWhenScrolledDown(): void;
         shouldShowErrorMessage(attrs: import("../../shared/types").MessageAttributes): Promise<boolean>;
@@ -110,8 +110,8 @@ declare const MUC_base: {
         getOldestMessage(): any;
         getMostRecentMessage(): any;
         getMessageReferencedByError(attrs: object): any;
-        findDanglingRetraction(attrs: object): BaseMessage<any> | null;
-        getDuplicateMessage(attrs: object): BaseMessage<any>;
+        findDanglingRetraction(attrs: object): import("../../shared/message.js").default<any> | null;
+        getDuplicateMessage(attrs: object): import("../../shared/message.js").default<any>;
         getOriginIdQueryAttrs(attrs: object): {
             origin_id: any;
             from: any;
@@ -121,15 +121,15 @@ declare const MUC_base: {
             from: any;
             msgid: any;
         };
-        sendMarkerForMessage(msg: BaseMessage<any>, type?: ("received" | "displayed" | "acknowledged"), force?: boolean): Promise<void>;
-        handleUnreadMessage(message: BaseMessage<any>): void;
-        getErrorAttributesForMessage(message: BaseMessage<any>, attrs: import("../../shared/types").MessageAttributes): Promise<any>;
+        sendMarkerForMessage(msg: import("../../shared/message.js").default<any>, type?: ("received" | "displayed" | "acknowledged"), force?: boolean): Promise<void>;
+        handleUnreadMessage(message: import("../../shared/message.js").default<any>): void;
+        getErrorAttributesForMessage(message: import("../../shared/message.js").default<any>, attrs: import("../../shared/types").MessageAttributes): Promise<any>;
         handleErrorMessageStanza(stanza: Element): Promise<void>;
-        incrementUnreadMsgsCounter(message: BaseMessage<any>): void;
+        incrementUnreadMsgsCounter(message: import("../../shared/message.js").default<any>): void;
         clearUnreadMsgCounter(): void;
         handleRetraction(attrs: import("../../shared/types").MessageAttributes): Promise<boolean>;
         handleReceipt(attrs: import("../../shared/types").MessageAttributes): boolean;
-        createMessageStanza(message: BaseMessage<any>): Promise<any>;
+        createMessageStanza(message: import("../../shared/message.js").default<any>): Promise<any>;
         pruneHistory(): void;
         debouncedPruneHistory: import("lodash").DebouncedFunc<() => void>;
         isScrolledUp(): any;
@@ -271,8 +271,7 @@ declare const MUC_base: {
  */
 declare class MUC extends MUC_base {
     /**
-     * @typedef {import('../vcard/vcard').default} VCard
-     * @typedef {import('../chat/message.js').default} Message
+     * @typedef {import('../../shared/message.js').default} BaseMessage
      * @typedef {import('./message.js').default} MUCMessage
      * @typedef {import('./occupant.js').default} MUCOccupant
      * @typedef {import('./affiliations/utils.js').NonOutcastAffiliation} NonOutcastAffiliation
@@ -335,12 +334,12 @@ declare class MUC extends MUC_base {
     /**
      * Given the passed in MUC message, send a XEP-0333 chat marker.
      * @async
-     * @param {Message} msg
+     * @param {BaseMessage} msg
      * @param {('received'|'displayed'|'acknowledged')} [type='displayed']
      * @param {boolean} [force=false] - Whether a marker should be sent for the
      *  message, even if it didn't include a `markable` element.
      */
-    sendMarkerForMessage(msg: import("../chat/message.js").default, type?: ("received" | "displayed" | "acknowledged"), force?: boolean): Promise<void>;
+    sendMarkerForMessage(msg: import("../../shared/message.js").default<any>, type?: ("received" | "displayed" | "acknowledged"), force?: boolean): Promise<void>;
     /**
      * Finds the last eligible message and then sends a XEP-0333 chat marker for it.
      * @param { ('received'|'displayed'|'acknowledged') } [type='displayed']
@@ -431,7 +430,7 @@ declare class MUC extends MUC_base {
      * Retract one of your messages in this groupchat
      * @param {BaseMessage} message - The message which we're retracting.
      */
-    retractOwnMessage(message: BaseMessage<any>): Promise<void>;
+    retractOwnMessage(message: import("../../shared/message.js").default<any>): Promise<void>;
     /**
      * Retract someone else's message in this groupchat.
      * @param {MUCMessage} message - The message which we're retracting.
@@ -824,7 +823,7 @@ declare class MUC extends MUC_base {
      *  message, as returned by {@link parseMUCMessage}
      * @returns {MUCMessage|BaseMessage}
      */
-    getDuplicateMessage(attrs: object): import("./message.js").default | BaseMessage<any>;
+    getDuplicateMessage(attrs: object): import("./message.js").default | import("../../shared/message.js").default<any>;
     /**
      * Handler for all MUC messages sent to this groupchat. This method
      * shouldn't be called directly, instead {@link MUC#queueMessage}
@@ -913,15 +912,14 @@ declare class MUC extends MUC_base {
      * was mentioned in a message.
      * @param {BaseMessage} message - The text message
      */
-    isUserMentioned(message: BaseMessage<any>): any;
+    isUserMentioned(message: import("../../shared/message.js").default<any>): any;
     /**
      * @param {BaseMessage} message - The text message
      */
-    incrementUnreadMsgsCounter(message: BaseMessage<any>): void;
+    incrementUnreadMsgsCounter(message: import("../../shared/message.js").default<any>): void;
     clearUnreadMsgCounter(): Promise<void>;
 }
 import { Model } from '@converse/skeletor';
-import BaseMessage from '../../shared/message';
 import ChatBoxBase from '../../shared/chatbox';
 import MUCSession from './session';
 import { TimeoutError } from '../../shared/errors.js';

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

@@ -5,7 +5,7 @@ declare const MUCOccupant_base: {
         lazy_load_vcard: boolean;
         initialize(): void;
         readonly vcard: import("../vcard").VCard;
-        getVCard(create?: boolean): Promise<import("../vcard").VCard | null>;
+        getVCard(): Promise<import("../vcard").VCard | null>;
         cid: any;
         attributes: {};
         validationError: string;

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

@@ -5,7 +5,7 @@ declare const RosterContact_base: {
         lazy_load_vcard: boolean;
         initialize(): void;
         readonly vcard: import("../vcard/vcard.js").default;
-        getVCard(create?: boolean): Promise<import("../vcard/vcard.js").default | null>;
+        getVCard(): Promise<import("../vcard/vcard.js").default | null>;
         cid: any;
         attributes: {};
         validationError: string;
@@ -149,7 +149,10 @@ declare class RosterContact extends RosterContact_base {
     presence: any;
     getStatus(): any;
     openChat(): void;
-    getDisplayName(): any;
+    /**
+     * @returns {string|null}
+     */
+    getDisplayName(jid_fallback?: boolean): string | null;
     /**
      * Send a presence subscription request to this roster contact
      * @param {string} message - An optional message to explain the

+ 1 - 1
src/headless/types/plugins/status/status.d.ts

@@ -4,7 +4,7 @@ declare const XMPPStatus_base: {
         lazy_load_vcard: boolean;
         initialize(): void;
         readonly vcard: import("../vcard/vcard.js").default;
-        getVCard(create?: boolean): Promise<import("../vcard/vcard.js").default | null>;
+        getVCard(): Promise<import("../vcard/vcard.js").default | null>;
         cid: any;
         attributes: {};
         validationError: string;

+ 4 - 4
src/headless/types/plugins/vcard/api.d.ts

@@ -30,9 +30,9 @@ declare namespace _default {
          *     attribute or a `muc_jid` attribute.
          * @param {boolean} [force] A boolean indicating whether the vcard should be
          *     fetched from the server even if it's been fetched before.
-         * @returns {promise} A Promise which resolves with the VCard data for a particular JID or for
-         *     a `Model` instance which represents an entity with a JID (such as a roster contact,
-         *     chat or chatroom occupant).
+         * @returns {Promise<import("./types").VCardResult|null>} A Promise which resolves
+         *     with the VCard data for a particular JID or for a `Model` instance which
+         *     represents an entity with a JID (such as a roster contact, chat or chatroom occupant).
          *
          * @example
          * const { api } = _converse;
@@ -44,7 +44,7 @@ declare namespace _default {
          *     );
          * });
          */
-        function get(model: Model | string, force?: boolean): Promise<any>;
+        function get(model: Model | string, force?: boolean): Promise<import("./types").VCardResult | null>;
         /**
          * Fetches the VCard associated with a particular `Model` instance
          * (by using its `jid` or `muc_jid` attribute) and then updates the model with the

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

@@ -0,0 +1,19 @@
+import { ModelOptions } from '../../shared/types';
+export interface VCardModelOptions extends ModelOptions {
+    lazy_load?: boolean;
+}
+export interface VCardResult {
+    email?: string;
+    fullname?: string;
+    image?: string;
+    image_hash?: string;
+    image_type?: string;
+    nickname?: string;
+    role?: string;
+    stanza: Element;
+    error?: Error;
+    url?: string;
+    vcard_error?: string;
+    vcard_updated?: string;
+}
+//# sourceMappingURL=types.d.ts.map

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

@@ -1,3 +1,8 @@
+/**
+ * @param {Element} iq
+ * @returns {Promise<import("./types").VCardResult>}
+ */
+export function onVCardData(iq: Element): Promise<import("./types").VCardResult>;
 /**
  * @param {"get"|"set"|"result"} type
  * @param {string} jid
@@ -10,23 +15,21 @@ export function createStanza(type: "get" | "set" | "result", jid: string, vcard_
 export function onOccupantAvatarChanged(occupant: MUCOccupant): void;
 /**
  * @param {Model|MUCOccupant|MUCMessage} model
- * @param {boolean} [create=true]
+ * @param {boolean} [lazy_load=false]
  * @returns {Promise<VCard|null>}
  */
-export function getVCardForModel(model: Model | MUCOccupant | MUCMessage, create?: boolean): Promise<VCard | null>;
+export function getVCardForModel(model: Model | MUCOccupant | MUCMessage, lazy_load?: boolean): Promise<VCard | null>;
 /**
  * @param {MUCOccupant} occupant
- * @param {boolean} [create=true]
+ * @param {boolean} [lazy_load=false]
  * @returns {Promise<VCard|null>}
  */
-export function getVCardForOccupant(occupant: MUCOccupant, create?: boolean): Promise<VCard | null>;
+export function getVCardForOccupant(occupant: MUCOccupant, lazy_load?: boolean): Promise<VCard | null>;
 export function clearVCardsSession(): void;
 /**
  * @param {string} jid
  */
-export function getVCard(jid: string): Promise<{
-    image_hash: any;
-} | {
+export function fetchVCard(jid: string): Promise<import("./types").VCardResult | {
     jid: string;
     stanza: any;
     error: any;

+ 13 - 6
src/headless/types/plugins/vcard/vcard.d.ts

@@ -1,11 +1,18 @@
 export default VCard;
-/**
- * Represents a VCard
- * @namespace _converse.VCard
- * @memberOf _converse
- */
 declare class VCard extends Model {
+    /**
+     * @param {import("../../shared/types").ModelAttributes} attrs
+     * @param {import("./types").VCardModelOptions} options
+     */
+    constructor(attrs: import("../../shared/types").ModelAttributes, options: import("./types").VCardModelOptions);
+    _vcard: any;
+    /**
+     * @param {import("../../shared/types").ModelAttributes} [_attrs]
+     * @param {import("./types").VCardModelOptions} [options]
+     */
+    initialize(_attrs?: import("../../shared/types").ModelAttributes, options?: import("./types").VCardModelOptions): void;
+    lazy_load: boolean;
     getDisplayName(): any;
 }
-import { Model } from '@converse/skeletor';
+import { Model } from "@converse/skeletor";
 //# sourceMappingURL=vcard.d.ts.map

+ 1 - 1
src/headless/types/shared/message.d.ts

@@ -5,7 +5,7 @@ declare const BaseMessage_base: {
         lazy_load_vcard: boolean;
         initialize(): void;
         readonly vcard: import("../index.js").VCard;
-        getVCard(create?: boolean): Promise<import("../index.js").VCard | null>;
+        getVCard(): Promise<import("../index.js").VCard | null>;
         cid: any;
         attributes: {};
         validationError: string;

+ 3 - 4
src/headless/types/shared/model-with-vcard.d.ts

@@ -4,14 +4,14 @@
  */
 export default function ModelWithVCard<T extends import("./types").ModelExtender>(BaseModel: T): {
     new (...args: any[]): {
-        _vcard: VCard;
+        _vcard: import("../plugins/vcard/vcard").default;
         lazy_load_vcard: boolean;
         initialize(): void;
-        readonly vcard: VCard;
+        readonly vcard: import("../plugins/vcard/vcard").default;
         /**
          * @returns {Promise<VCard|null>}
          */
-        getVCard(create?: boolean): Promise<VCard | null>;
+        getVCard(): Promise<import("../plugins/vcard/vcard").default | null>;
         cid: any;
         attributes: {};
         validationError: string;
@@ -75,5 +75,4 @@ export default function ModelWithVCard<T extends import("./types").ModelExtender
         propertyIsEnumerable(v: PropertyKey): boolean;
     };
 } & T;
-import VCard from "../plugins/vcard/vcard.js";
 //# sourceMappingURL=model-with-vcard.d.ts.map

+ 8 - 1
src/headless/types/shared/types.d.ts

@@ -1,4 +1,11 @@
-import { Model } from '@converse/skeletor';
+import { Collection, Model } from '@converse/skeletor';
+export type ModelAttributes = Record<string, any>;
+export interface ModelOptions {
+    collection?: Collection;
+    parse?: boolean;
+    unset?: boolean;
+    silent?: boolean;
+}
 type Constructor<T = {}> = new (...args: any[]) => T;
 export type ModelExtender = Constructor<Model>;
 type EncryptionPayloadAttrs = {

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 245 - 209
src/i18n/converse.pot


+ 8 - 1
src/plugins/chatview/heading.js

@@ -18,7 +18,7 @@ export default class ChatHeading extends CustomElement {
 
     static get properties () {
         return {
-            'jid': { type: String },
+            jid: { type: String },
         }
     }
 
@@ -35,17 +35,24 @@ export default class ChatHeading extends CustomElement {
             this.listenTo(this.model.contact, 'change:nickname', () => this.requestUpdate());
             this.requestUpdate();
         });
+        this.requestUpdate();
     }
 
     render () {
         return tplChatboxHead(this);
     }
 
+    /**
+     * @param {Event} ev
+     */
     showUserDetailsModal (ev) {
         ev.preventDefault();
         api.modal.show('converse-user-details-modal', { model: this.model }, ev);
     }
 
+    /**
+     * @param {Event} ev
+     */
     close (ev) {
         ev.preventDefault();
         this.model.close();

+ 24 - 15
src/plugins/chatview/templates/chat-head.js

@@ -1,8 +1,8 @@
 import { html } from "lit";
-import { until } from 'lit/directives/until.js';
-import { _converse, constants } from '@converse/headless';
-import { __ } from 'i18n';
-import { getStandaloneButtons, getDropdownButtons } from 'shared/chat/utils.js';
+import { until } from "lit/directives/until.js";
+import { _converse, constants } from "@converse/headless";
+import { __ } from "i18n";
+import { getStandaloneButtons, getDropdownButtons } from "shared/chat/utils.js";
 
 const { HEADLINES_TYPE } = constants;
 
@@ -13,7 +13,7 @@ const { HEADLINES_TYPE } = constants;
 export default (el) => {
     const { jid, status, type } = el.model.attributes;
     const heading_buttons_promise = el.getHeadingButtons();
-    const showUserDetailsModal = /** @param {Event} ev */(ev) => el.showUserDetailsModal(ev);
+    const showUserDetailsModal = /** @param {Event} ev */ (ev) => el.showUserDetailsModal(ev);
 
     const i18n_profile = __("The User's Profile Image");
     const display_name = el.model.getDisplayName();
@@ -22,23 +22,32 @@ export default (el) => {
             .model=${el.model.contact || el.model}
             class="avatar chat-msg__avatar"
             name="${display_name}"
-            nonce=${el.model.contact?.vcard?.get('vcard_updated')}
-            height="40" width="40"></converse-avatar></span>`;
+            nonce=${el.model.contact?.vcard?.get("vcard_updated")}
+            height="40"
+            width="40"
+        ></converse-avatar
+    ></span>`;
 
     return html`
-        <div class="chatbox-title ${ status ? '' :  "chatbox-title--no-desc"}">
+        <div class="chatbox-title ${status ? "" : "chatbox-title--no-desc"}">
             <div class="chatbox-title--row">
-                ${ (!_converse.api.settings.get("singleton")) ?  html`<converse-controlbox-navback jid="${jid}"></converse-controlbox-navback>` : '' }
-                ${ (type !== HEADLINES_TYPE) ? html`<a class="show-msg-author-modal" @click=${showUserDetailsModal}>${ avatar }</a>` : '' }
+                ${!_converse.api.settings.get("singleton")
+                    ? html`<converse-controlbox-navback jid="${jid}"></converse-controlbox-navback>`
+                    : ""}
+                ${type !== HEADLINES_TYPE
+                    ? html`<a class="show-msg-author-modal" @click=${showUserDetailsModal}>${avatar}</a>`
+                    : ""}
                 <div class="chatbox-title__text" title="${jid}" role="heading" aria-level="2">
-                    ${ (type !== HEADLINES_TYPE) ? html`<a class="user show-msg-author-modal" @click=${showUserDetailsModal}>${ display_name }</a>` : display_name }
+                    ${type !== HEADLINES_TYPE
+                        ? html`<a class="user show-msg-author-modal" @click=${showUserDetailsModal}>${display_name}</a>`
+                        : display_name}
                 </div>
             </div>
             <div class="chatbox-title__buttons btn-toolbar g-0">
-                ${ until(getDropdownButtons(heading_buttons_promise), '') }
-                ${ until(getStandaloneButtons(heading_buttons_promise), '') }
+                ${until(getDropdownButtons(heading_buttons_promise), "")}
+                ${until(getStandaloneButtons(heading_buttons_promise), "")}
             </div>
         </div>
-        ${ status ? html`<p class="chat-head__desc">${ status }</p>` : '' }
+        ${status ? html`<p class="chat-head__desc">${status}</p>` : ""}
     `;
-}
+};

+ 5 - 4
src/plugins/chatview/tests/messages.js

@@ -326,8 +326,9 @@ describe("A Chat Message", function () {
 
         expect(view.querySelector('.chat-msg .chat-msg__text').textContent).toEqual(msgtext);
         expect(view.querySelector('.chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy();
-        await u.waitUntil(() => chatbox.vcard.get('fullname') === 'Juliet Capulet')
-        expect(view.querySelector('span.chat-msg__author').textContent.trim()).toBe('Juliet Capulet');
+        await u.waitUntil(() => chatbox.vcard.get('fullname') === 'Juliet Capulet');
+        expect(view.querySelector('.chatbox-title__text .show-msg-author-modal').textContent.trim()).toBe('Juliet Capulet');
+        await u.waitUntil(() => view.querySelector('span.chat-msg__author').textContent.trim() === 'Juliet Capulet');
     }));
 
     it("can be a carbon message that this user sent from a different client, as defined in XEP-0280",
@@ -461,7 +462,7 @@ describe("A Chat Message", function () {
         await u.waitUntil(() => chatbox.vcard.get('fullname') === 'Juliet Capulet')
         expect(view.querySelector('.chat-msg .chat-msg__text').textContent).toEqual(message);
         expect(view.querySelector('.chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy();
-        expect(view.querySelector('span.chat-msg__author').textContent.trim()).toBe('Juliet Capulet');
+        await u.waitUntil(() => view.querySelector('span.chat-msg__author').textContent.trim() === 'Juliet Capulet');
 
         expect(view.querySelectorAll('.date-separator').length).toEqual(1);
         let day = view.querySelector('.date-separator');
@@ -933,7 +934,7 @@ describe("A Chat Message", function () {
             expect(mel.textContent).toEqual(message);
             expect(view.querySelector('.chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy();
             await u.waitUntil(() => chatbox.vcard.get('fullname') === mock.cur_names[0]);
-            expect(view.querySelector('span.chat-msg__author').textContent.trim()).toBe('Mercutio');
+            await u.waitUntil(() => view.querySelector('span.chat-msg__author').textContent.trim() === 'Mercutio');
         }));
 
         it("will be trimmed of leading and trailing whitespace",

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

@@ -1575,7 +1575,7 @@ describe("Groupchats", function () {
         }));
 
         it("can be joined automatically, based on a received invite",
-                mock.initConverse([], {}, async function (_converse) {
+                mock.initConverse([], { lazy_load_vcards: false }, async function (_converse) {
 
             await mock.waitForRoster(_converse, 'current'); // We need roster contacts, who can invite us
             const muc_jid = 'lounge@montague.lit';

+ 13 - 7
src/plugins/rosterview/contactview.js

@@ -1,6 +1,6 @@
 import { Model } from '@converse/skeletor';
 import { _converse, converse, api } from '@converse/headless';
-import { CustomElement } from 'shared/components/element.js';
+import { ObservableElement } from 'shared/components/observable.js';
 import tplRequestingContact from './templates/requesting_contact.js';
 import tplRosterItem from './templates/roster_item.js';
 import tplUnsavedContact from './templates/unsaved_contact.js';
@@ -9,16 +9,22 @@ import { blockContact, removeContact } from './utils.js';
 
 const { Strophe } = converse.env;
 
-export default class RosterContact extends CustomElement {
-    static get properties() {
-        return {
-            model: { type: Object },
-        };
-    }
+export default class RosterContact extends ObservableElement {
+    /**
+     * @typedef {import('shared/components/types').ObservableProperty} ObservableProperty
+     */
 
     constructor() {
         super();
         this.model = null;
+        this.observable = /** @type {ObservableProperty} */ ("once");
+    }
+
+    static get properties() {
+        return {
+            ...super.properties,
+            model: { type: Object },
+        };
     }
 
     initialize() {

+ 20 - 17
src/plugins/rosterview/tests/roster.js

@@ -666,7 +666,7 @@ describe("The Contacts Roster", function () {
 
         it("are shown in the roster when hide_offline_users",
             mock.initConverse(
-                [], {'hide_offline_users': true},
+                [], { hide_offline_users: true, lazy_load_vcards: false },
                 async function (_converse) {
 
             await mock.openControlBox(_converse);
@@ -681,7 +681,10 @@ describe("The Contacts Roster", function () {
             expect(el.getAttribute('data-group')).toBe('Ungrouped');
         }));
 
-        it("can be removed by the user", mock.initConverse([], { roster_groups: false }, async function (_converse) {
+        it("can be removed by the user", mock.initConverse([], {
+            roster_groups: false,
+            lazy_load_vcards: false,
+        }, async function (_converse) {
             const { api } = _converse;
             await mock.openControlBox(_converse);
             await mock.waitForRoster(_converse, 'all');
@@ -717,7 +720,7 @@ describe("The Contacts Roster", function () {
         it("can be removed by the user",
                 mock.initConverse(
                     [],
-                    {'roster_groups': false},
+                    { roster_groups: false, lazy_load_vcards: false },
                     async function (_converse) {
 
             spyOn(_converse.api, 'confirm').and.callFake(() => Promise.resolve(true));
@@ -756,7 +759,7 @@ describe("The Contacts Roster", function () {
 
         it("can be collapsed under their own header",
             mock.initConverse(
-                [], {},
+                [], { lazy_load_vcards: false },
                 async function (_converse) {
 
             await _addContacts(_converse);
@@ -767,7 +770,7 @@ describe("The Contacts Roster", function () {
 
         it("will be hidden when appearing under a collapsed group",
             mock.initConverse(
-                [], { roster_groups: false, show_self_in_roster: false },
+                [], { roster_groups: false, show_self_in_roster: false, lazy_load_vcards: false },
                 async function (_converse) {
 
             await _addContacts(_converse);
@@ -789,7 +792,7 @@ describe("The Contacts Roster", function () {
 
         it("will have their online statuses shown correctly",
             mock.initConverse(
-                [], {},
+                [], { lazy_load_vcards: false },
                 async function (_converse) {
 
             await mock.waitForRoster(_converse, 'current', 1);
@@ -820,7 +823,7 @@ describe("The Contacts Roster", function () {
 
         it("can be added to the roster and they will be sorted alphabetically",
             mock.initConverse(
-                [], {},
+                [], { lazy_load_vcards: false },
                 async function (_converse) {
 
             const { api } = _converse;
@@ -847,7 +850,7 @@ describe("The Contacts Roster", function () {
 
         it("can be removed by the user",
             mock.initConverse(
-                [], {},
+                [], { lazy_load_vcards: false },
                 async function (_converse) {
 
             await _addContacts(_converse);
@@ -878,7 +881,7 @@ describe("The Contacts Roster", function () {
 
         it("do not have a header if there aren't any",
             mock.initConverse(
-                [], { show_self_in_roster: false },
+                [], { show_self_in_roster: false, lazy_load_vcards: false },
                 async function (_converse) {
 
             await mock.openControlBox(_converse);
@@ -905,7 +908,7 @@ describe("The Contacts Roster", function () {
 
         it("can change their status to online and be sorted alphabetically",
             mock.initConverse(
-                [], { show_self_in_roster: false },
+                [], { show_self_in_roster: false, lazy_load_vcards: false },
                 async function (_converse) {
 
             await _addContacts(_converse);
@@ -931,7 +934,7 @@ describe("The Contacts Roster", function () {
 
         it("can change their status to busy and be sorted alphabetically",
             mock.initConverse(
-                [], {},
+                [], { lazy_load_vcards: false },
                 async function (_converse) {
 
             await _addContacts(_converse);
@@ -957,7 +960,7 @@ describe("The Contacts Roster", function () {
 
         it("can change their status to away and be sorted alphabetically",
             mock.initConverse(
-                [], {},
+                [], { lazy_load_vcards: false },
                 async function (_converse) {
 
             await _addContacts(_converse);
@@ -983,7 +986,7 @@ describe("The Contacts Roster", function () {
 
         it("can change their status to xa and be sorted alphabetically",
             mock.initConverse(
-                [], {},
+                [], { lazy_load_vcards: false },
                 async function (_converse) {
 
             await _addContacts(_converse);
@@ -1009,7 +1012,7 @@ describe("The Contacts Roster", function () {
 
         it("can change their status to unavailable and be sorted alphabetically",
             mock.initConverse(
-                [], {},
+                [], { lazy_load_vcards: false },
                 async function (_converse) {
 
             await _addContacts(_converse);
@@ -1035,7 +1038,7 @@ describe("The Contacts Roster", function () {
 
         it("are ordered according to status: online, busy, away, xa, unavailable, offline",
             mock.initConverse(
-                [], { show_self_in_roster: false },
+                [], { show_self_in_roster: false, lazy_load_vcards: false },
                 async function (_converse) {
 
             await _addContacts(_converse);
@@ -1196,7 +1199,7 @@ describe("The Contacts Roster", function () {
 
         it("can have their requests accepted by the user",
             mock.initConverse(
-                [], {},
+                [], { lazy_load_vcards: false },
                 async function (_converse) {
 
             await mock.openControlBox(_converse);
@@ -1355,7 +1358,7 @@ describe("The Contacts Roster", function () {
 
         it("is shown upon receiving a message to a previously removed contact",
             mock.initConverse(
-                [], {},
+                [], { lazy_load_vcards: false },
                 async function (_converse) {
 
             const { api } = _converse;

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

@@ -13,23 +13,28 @@ import tplMessage from './templates/message.js';
 import tplMessageText from './templates/message-text.js';
 import tplRetraction from './templates/retraction.js';
 import tplSpinner from 'templates/spinner.js';
-import { CustomElement } from 'shared/components/element.js';
+import { ObservableElement } from 'shared/components/observable.js';
 import { __ } from 'i18n';
 
 const { Strophe } = converse.env;
 const { SUCCESS } = constants;
 
 
-export default class Message extends CustomElement {
+export default class Message extends ObservableElement {
+    /**
+     * @typedef {import('shared/components/types').ObservableProperty} ObservableProperty
+     */
 
     constructor () {
         super();
         this.model_with_messages = null;
         this.model = null;
+        this.observable = /** @type {ObservableProperty} */ ("once");
     }
 
     static get properties () {
         return {
+            ...super.properties,
             model_with_messages: { type: Object },
             model: { type: Object }
         }

+ 2 - 2
src/shared/components/observable.js

@@ -1,3 +1,4 @@
+import { api } from '@converse/headless';
 import { CustomElement } from "./element";
 
 export class ObservableElement extends CustomElement {
@@ -63,8 +64,7 @@ export class ObservableElement extends CustomElement {
             const ratio = Number(entry.intersectionRatio.toFixed(2));
             if (ratio >= this.observableRatio) {
                 this.isVisible = true;
-                this.trigger("visibilityChanged", entry);
-                this.model?.trigger("visibilityChanged", entry);
+                api.trigger('visibilityChanged', { el: this, entry });
 
                 if (this.observable === "once") {
                     this.intersectionObserver.disconnect();

+ 1 - 6
src/shared/modals/user-details.js

@@ -29,15 +29,10 @@ export default class UserDetailsModal extends BaseModal {
     addListeners() {
         this.listenTo(this.model, 'change', () => this.requestUpdate());
 
-        this.model.rosterContactAdded.then(() => {
-            this.registerContactEventHandlers();
-            api.vcard.update(this.model.contact.vcard, true);
-        });
+        this.model.rosterContactAdded.then(() => this.registerContactEventHandlers());
 
         if (this.model.contact !== undefined) {
             this.registerContactEventHandlers();
-            // Refresh the vcard
-            api.vcard.update(this.model.contact.vcard, true);
         }
     }
 

+ 8 - 2
src/types/plugins/chatview/heading.d.ts

@@ -8,8 +8,14 @@ export default class ChatHeading extends CustomElement {
     initialize(): void;
     model: any;
     render(): import("lit").TemplateResult<1 | 2>;
-    showUserDetailsModal(ev: any): void;
-    close(ev: any): void;
+    /**
+     * @param {Event} ev
+     */
+    showUserDetailsModal(ev: Event): void;
+    /**
+     * @param {Event} ev
+     */
+    close(ev: Event): void;
     /**
      * Returns a list of objects which represent buttons for the chat's header.
      * @emits _converse#getHeadingButtons

+ 5 - 3
src/types/plugins/rosterview/contactview.d.ts

@@ -1,10 +1,12 @@
-export default class RosterContact extends CustomElement {
+export default class RosterContact extends ObservableElement {
     static get properties(): {
         model: {
             type: ObjectConstructor;
         };
+        observable: {
+            type: StringConstructor;
+        };
     };
-    model: any;
     initialize(): void;
     render(): import("lit").TemplateResult<1>;
     /**
@@ -32,5 +34,5 @@ export default class RosterContact extends CustomElement {
      */
     declineRequest(ev: MouseEvent): Promise<this>;
 }
-import { CustomElement } from 'shared/components/element.js';
+import { ObservableElement } from 'shared/components/observable.js';
 //# sourceMappingURL=contactview.d.ts.map

+ 5 - 3
src/types/shared/chat/message.d.ts

@@ -1,4 +1,4 @@
-export default class Message extends CustomElement {
+export default class Message extends ObservableElement {
     static get properties(): {
         model_with_messages: {
             type: ObjectConstructor;
@@ -6,9 +6,11 @@ export default class Message extends CustomElement {
         model: {
             type: ObjectConstructor;
         };
+        observable: {
+            type: StringConstructor;
+        };
     };
     model_with_messages: any;
-    model: any;
     initialize(): Promise<void>;
     render(): import("lit").TemplateResult<1> | "";
     renderRetraction(): import("lit").TemplateResult<1>;
@@ -30,5 +32,5 @@ export default class Message extends CustomElement {
     showMessageVersionsModal(ev: any): void;
     toggleSpoilerMessage(ev: any): void;
 }
-import { CustomElement } from 'shared/components/element.js';
+import { ObservableElement } from 'shared/components/observable.js';
 //# sourceMappingURL=message.d.ts.map

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.