From c3c56c199c832f86f03528a1a470ff84a83bb220 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Mon, 4 Aug 2025 16:40:24 +0200 Subject: [PATCH 1/3] Fix exception when bookmark name is not defined --- src/headless/plugins/bookmarks/model.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/headless/plugins/bookmarks/model.js b/src/headless/plugins/bookmarks/model.js index 363b803d0d..904eacd18c 100644 --- a/src/headless/plugins/bookmarks/model.js +++ b/src/headless/plugins/bookmarks/model.js @@ -4,12 +4,12 @@ import converse from '../../shared/api/public.js'; const { Strophe } = converse.env; class Bookmark extends Model { - get idAttribute () { + get idAttribute() { return 'jid'; } - getDisplayName () { - return Strophe.xmlunescape(this.get('name')) || this.get('jid'); + getDisplayName() { + return this.get('name') && Strophe.xmlunescape(this.get('name')) || this.get('jid'); } } From badae393275ade5895e2975d52888636cd500d35 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Mon, 4 Aug 2025 16:48:12 +0200 Subject: [PATCH 2/3] Add a timeout option to the `api.disco.refresh` function And use it to set a lower timeout for disco info calls when joining a MUC. Updates #3778 --- src/headless/plugins/disco/api.js | 12 +++++++----- src/headless/plugins/disco/entity.js | 12 +++++++++--- src/headless/plugins/disco/types.ts | 8 ++++++++ src/headless/plugins/muc/muc.js | 14 ++++++++++---- src/headless/shared/api/send.js | 1 + src/headless/types/plugins/disco/api.d.ts | 6 ++++-- src/headless/types/plugins/disco/entity.d.ts | 10 ++++++++-- src/headless/types/plugins/disco/types.d.ts | 8 ++++++++ src/headless/types/plugins/muc/muc.d.ts | 3 ++- src/types/shared/avatar/avatar.d.ts | 15 ++++++++++++--- 10 files changed, 69 insertions(+), 20 deletions(-) create mode 100644 src/headless/plugins/disco/types.ts create mode 100644 src/headless/types/plugins/disco/types.d.ts diff --git a/src/headless/plugins/disco/api.js b/src/headless/plugins/disco/api.js index b25307908f..43f121117d 100644 --- a/src/headless/plugins/disco/api.js +++ b/src/headless/plugins/disco/api.js @@ -152,9 +152,10 @@ export default { * @method api.disco.info * @param {string} jid The Jabber ID of the entity to query * @param {string} [node] A specific node identifier associated with the JID + * @param {import('./types').DiscoInfoOptions} [options] * @returns {promise} Promise which resolves once we have a result from the server. */ - info(jid, node) { + info(jid, node, options) { const attrs = { xmlns: Strophe.NS.DISCO_INFO }; if (node) { attrs.node = node; @@ -164,7 +165,7 @@ export default { 'to': jid, 'type': 'get', }).c('query', attrs); - return api.sendIQ(info); + return api.sendIQ(info, options?.timeout); }, /** @@ -381,11 +382,12 @@ export default { * disco entity by refetching them from the server * @method api.disco.refresh * @param {string} jid The JID of the entity whose features are refreshed. + * @param {import('./types').DiscoInfoOptions} [options] * @returns {Promise} A promise which resolves once the features have been refreshed * @example * await api.disco.refresh('room@conference.example.org'); */ - async refresh(jid) { + async refresh(jid, options) { if (!jid) throw new TypeError('api.disco.refresh: You need to provide an entity JID'); await api.waitUntil('discoInitialized'); @@ -400,10 +402,10 @@ export default { if (!entity.waitUntilItemsFetched.isPending) { entity.waitUntilItemsFetched = getOpenPromise(); } - entity.queryInfo(); + entity.queryInfo(options); } else { // Create it if it doesn't exist - entity = await api.disco.entities.create({ jid }, { ignore_cache: true }); + entity = await api.disco.entities.create({ jid }, { ignore_cache: true, timeout: options.timeout }); } return entity.waitUntilItemsFetched; }, diff --git a/src/headless/plugins/disco/entity.js b/src/headless/plugins/disco/entity.js index a9ed48e6e9..ccfbc69aee 100644 --- a/src/headless/plugins/disco/entity.js +++ b/src/headless/plugins/disco/entity.js @@ -102,9 +102,12 @@ class DiscoEntity extends Model { api.trigger('discoExtensionFieldDiscovered', field); } + /** + * @param {import('./types').FetchEntityFeaturesOptions} options + */ async fetchFeatures(options) { if (options.ignore_cache) { - await this.queryInfo(); + await this.queryInfo(options); } else { const store_id = this.features.browserStorage.name; @@ -144,10 +147,13 @@ class DiscoEntity extends Model { } } - async queryInfo() { + /** + * @param {import('./types').DiscoInfoOptions} [options] + */ + async queryInfo(options) { let stanza; try { - stanza = await api.disco.info(this.get('jid'), null); + stanza = await api.disco.info(this.get('jid'), null, options); } catch (iq) { if (u.isElement(iq)) { const e = await parseErrorStanza(iq); diff --git a/src/headless/plugins/disco/types.ts b/src/headless/plugins/disco/types.ts new file mode 100644 index 0000000000..f76f060696 --- /dev/null +++ b/src/headless/plugins/disco/types.ts @@ -0,0 +1,8 @@ +export type DiscoInfoOptions = { + timeout?: number; +} + +export type FetchEntityFeaturesOptions = { + timeout?: number; + ignore_cache?: boolean; +} diff --git a/src/headless/plugins/muc/muc.js b/src/headless/plugins/muc/muc.js index e695ddd82c..725dc11805 100644 --- a/src/headless/plugins/muc/muc.js +++ b/src/headless/plugins/muc/muc.js @@ -50,6 +50,8 @@ import MUCSession from './session'; const { u, stx } = converse.env; +const DISCO_INFO_TIMEOUT_ON_JOIN = 5000; + /** * Represents a groupchat conversation. */ @@ -192,7 +194,9 @@ class MUC extends ModelWithVCard(ModelWithMessages(ColorAwareModel(ChatBoxBase)) // Set this early, so we don't rejoin in onHiddenChange this.session.save('connection_status', ROOMSTATUS.CONNECTING); - const is_new = (await this.refreshDiscoInfo()) instanceof ItemNotFoundError; + const result = await this.refreshDiscoInfo({ timeout: DISCO_INFO_TIMEOUT_ON_JOIN }); + const is_new = result instanceof ItemNotFoundError; + nick = await this.getAndPersistNickname(nick); if (!nick) { safeSave(this.session, { 'connection_status': ROOMSTATUS.NICKNAME_REQUIRED }); @@ -1254,10 +1258,11 @@ class MUC extends ModelWithVCard(ModelWithMessages(ColorAwareModel(ChatBoxBase)) * Refresh the disco identity, features and fields for this {@link MUC}. * *features* are stored on the features {@link Model} attribute on this {@link MUC}. * *fields* are stored on the config {@link Model} attribute on this {@link MUC}. + * @param {import('@converse/headless/plugins/disco/types').DiscoInfoOptions} [options] * @returns {Promise} */ - async refreshDiscoInfo() { - const result = await api.disco.refresh(this.get('jid')); + async refreshDiscoInfo(options) { + const result = await api.disco.refresh(this.get('jid'), options); if (result instanceof StanzaError) { return result; } @@ -1738,7 +1743,8 @@ class MUC extends ModelWithVCard(ModelWithMessages(ColorAwareModel(ChatBoxBase)) `; - const result = await api.sendIQ(stanza, null, false); + + const result = await api.sendIQ(stanza, DISCO_INFO_TIMEOUT_ON_JOIN, false); if (u.isErrorObject(result)) { throw result; } diff --git a/src/headless/shared/api/send.js b/src/headless/shared/api/send.js index 9558a00735..5ab03d56c7 100644 --- a/src/headless/shared/api/send.js +++ b/src/headless/shared/api/send.js @@ -19,6 +19,7 @@ export default { send(stanza) { const { api } = _converse; if (!api.connection.connected()) { + // TODO: queue unsent messages and send once we're connected again log.warn("Not sending stanza because we're not connected!"); log.warn(Strophe.serialize(stanza)); return; diff --git a/src/headless/types/plugins/disco/api.d.ts b/src/headless/types/plugins/disco/api.d.ts index be4493a588..4c73d60044 100644 --- a/src/headless/types/plugins/disco/api.d.ts +++ b/src/headless/types/plugins/disco/api.d.ts @@ -65,9 +65,10 @@ declare namespace _default { * @method api.disco.info * @param {string} jid The Jabber ID of the entity to query * @param {string} [node] A specific node identifier associated with the JID + * @param {import('./types').DiscoInfoOptions} [options] * @returns {promise} Promise which resolves once we have a result from the server. */ - export function info(jid: string, node?: string): Promise; + export function info(jid: string, node?: string, options?: import("./types").DiscoInfoOptions): Promise; /** * Query for items associated with an XMPP entity * @@ -190,11 +191,12 @@ declare namespace _default { * disco entity by refetching them from the server * @method api.disco.refresh * @param {string} jid The JID of the entity whose features are refreshed. + * @param {import('./types').DiscoInfoOptions} [options] * @returns {Promise} A promise which resolves once the features have been refreshed * @example * await api.disco.refresh('room@conference.example.org'); */ - export function refresh(jid: string): Promise; + export function refresh(jid: string, options?: import("./types").DiscoInfoOptions): Promise; /** * Return all the features associated with a disco entity * diff --git a/src/headless/types/plugins/disco/entity.d.ts b/src/headless/types/plugins/disco/entity.d.ts index 1e8160f426..242e2fe05d 100644 --- a/src/headless/types/plugins/disco/entity.d.ts +++ b/src/headless/types/plugins/disco/entity.d.ts @@ -46,8 +46,14 @@ declare class DiscoEntity extends Model { getFeature(feature: string): Promise; onFeatureAdded(feature: any): void; onFieldAdded(field: any): void; - fetchFeatures(options: any): Promise; - queryInfo(): Promise; + /** + * @param {import('./types').FetchEntityFeaturesOptions} options + */ + fetchFeatures(options: import("./types").FetchEntityFeaturesOptions): Promise; + /** + * @param {import('./types').DiscoInfoOptions} [options] + */ + queryInfo(options?: import("./types").DiscoInfoOptions): Promise; /** * @param {Element} stanza */ diff --git a/src/headless/types/plugins/disco/types.d.ts b/src/headless/types/plugins/disco/types.d.ts new file mode 100644 index 0000000000..9bbc6f9461 --- /dev/null +++ b/src/headless/types/plugins/disco/types.d.ts @@ -0,0 +1,8 @@ +export type DiscoInfoOptions = { + timeout?: number; +}; +export type FetchEntityFeaturesOptions = { + timeout?: number; + ignore_cache?: boolean; +}; +//# sourceMappingURL=types.d.ts.map \ No newline at end of file diff --git a/src/headless/types/plugins/muc/muc.d.ts b/src/headless/types/plugins/muc/muc.d.ts index a8a23554ea..12dc18c617 100644 --- a/src/headless/types/plugins/muc/muc.d.ts +++ b/src/headless/types/plugins/muc/muc.d.ts @@ -539,9 +539,10 @@ declare class MUC extends MUC_base { * Refresh the disco identity, features and fields for this {@link MUC}. * *features* are stored on the features {@link Model} attribute on this {@link MUC}. * *fields* are stored on the config {@link Model} attribute on this {@link MUC}. + * @param {import('@converse/headless/plugins/disco/types').DiscoInfoOptions} [options] * @returns {Promise} */ - refreshDiscoInfo(): Promise; + refreshDiscoInfo(options?: import("@converse/headless/plugins/disco/types").DiscoInfoOptions): Promise; /** * Fetch the *extended* MUC info from the server and cache it locally * https://xmpp.org/extensions/xep-0045.html#disco-roominfo diff --git a/src/types/shared/avatar/avatar.d.ts b/src/types/shared/avatar/avatar.d.ts index e69964e5ab..8fbd08f65f 100644 --- a/src/types/shared/avatar/avatar.d.ts +++ b/src/types/shared/avatar/avatar.d.ts @@ -1,4 +1,7 @@ -export default class Avatar extends CustomElement { +export default class Avatar extends ObservableElement { + /** + * @typedef {import('shared/components/types').ObservableProperty} ObservableProperty + */ static get properties(): { model: { type: ObjectConstructor; @@ -18,18 +21,24 @@ export default class Avatar extends CustomElement { nonce: { type: StringConstructor; }; + observable: { + type: StringConstructor; + }; + intersectionRatio: { + type: NumberConstructor; + }; }; - model: any; pickerdata: any; width: number; height: number; name: string; render(): import("lit-html").TemplateResult<1> | ""; + onVisibilityChanged(): void; /** * @param {string} name * @returns {string} */ getInitials(name: string): string; } -import { CustomElement } from 'shared/components/element.js'; +import { ObservableElement } from 'shared/components/observable.js'; //# sourceMappingURL=avatar.d.ts.map \ No newline at end of file From a523d4111a514305e452a92dafcb558d81561d55 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Sun, 3 Aug 2025 22:32:49 +0200 Subject: [PATCH 3/3] Only render the avatar colors when it becomes observable Updates #3778 --- src/shared/avatar/avatar.js | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/shared/avatar/avatar.js b/src/shared/avatar/avatar.js index 1dc900300b..b06caad22e 100644 --- a/src/shared/avatar/avatar.js +++ b/src/shared/avatar/avatar.js @@ -2,14 +2,18 @@ import { html } from 'lit'; import { until } from 'lit/directives/until.js'; import { api } from '@converse/headless'; import { __ } from 'i18n'; -import { CustomElement } from 'shared/components/element.js'; +import { ObservableElement } from 'shared/components/observable.js'; import tplAvatar from './templates/avatar.js'; import './avatar.scss'; -export default class Avatar extends CustomElement { +export default class Avatar extends ObservableElement { + /** + * @typedef {import('shared/components/types').ObservableProperty} ObservableProperty + */ static get properties() { return { + ...super.properties, model: { type: Object }, pickerdata: { type: Object }, name: { type: String }, @@ -26,6 +30,7 @@ export default class Avatar extends CustomElement { this.width = 36; this.height = 36; this.name = ''; + this.observable = /** @type {ObservableProperty} */ ("once"); } render() { @@ -57,14 +62,17 @@ export default class Avatar extends CustomElement { height: ${this.height}px; font: ${this.width / 2}px Arial; line-height: ${this.height}px;`; + const style = this.isVisible ? until(this.model.getAvatarStyle(css), default_bg_css + css) : default_bg_css + css; - const author_style = this.model.getAvatarStyle(css); - return html`
- ${this.getInitials(this.name)} + return html`
+ ${this.getInitials(this.name)}
`; } + onVisibilityChanged() { + this.requestUpdate(); + } + /** * @param {string} name * @returns {string}