Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor add-muc modal #3586

Merged
merged 6 commits into from
Feb 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
- New "getOccupantActionButtons" hook, so that plugins can add actions on MUC occupants.
- MUC occupants badges: displays short labels, with full label as title.
- New config option [stanza_timeout](https://conversejs.org/docs/html/configuration.html#show-background)
- Update the "Add MUC" modal to add validation and to allow specifying only the MUC name and not the whole address.

### Default config changes
- Make `fullscreen` the default `view_mode`.
Expand Down
6 changes: 0 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,12 +165,6 @@ We accept donations via [Patreon](https://www.patreon.com/jcbrand) and [Liberapa

## Sponsors

<p>
<a href="https://www.dotcom-monitor.com/sponsoring-open-source-projects/?utm_source=conversejs" target="_blank" rel="noopener">
<img alt="Dotcom-Monitor" src="https://raw.githubusercontent.com/conversejs/media/main/logos/dotcom-monitor.svg" width="200">

</a>
</p>
<p>
<a href="https://bairesdev.com/sponsoring-open-source-projects/?utm_source=conversejs" target="_blank" rel="noopener">
<img alt="BairesDev" src="https://raw.githubusercontent.com/conversejs/media/main/logos/bairesdev-primary.png" width="200">
Expand Down
4 changes: 0 additions & 4 deletions docs/source/_templates/sponsors.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,6 @@
<h4 class="sidebar-title">Sponsored by</h4>
</span>
<ul class="sponsors-list">
<li><a href="https://www.dotcom-monitor.com/sponsoring-open-source-projects/?utm_source=conversejs" target="_blank" rel="noopener">
<img style="width: 10em" src="/media/logos/dotcom-monitor.svg" alt="Dotcom-Monitor">
</a>
</li>
<li><a href="https://bairesdev.com/sponsoring-open-source-projects/?utm_source=conversejs" target="_blank" rel="noopener">
<img style="width: 10em" src="/media/logos/bairesdev-primary.png" alt="BairesDev">
</a>
Expand Down
1 change: 0 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,6 @@ <h2 style="text-align: center">Professional support and custom development</h2>
<div class="sponsors">
<h2>Converse is supported by:</h2>
<ul >
<li><a href="https://www.dotcom-monitor.com/sponsoring-open-source-projects/?utm_source=conversejs" target="_blank" rel="noopener"><img style="width: 13em" src="/media/logos/dotcom-monitor.svg" alt="Dotcom-Monitor"></a></li>
<li><a href="https://bairesdev.com/sponsoring-open-source-projects/?utm_source=conversejs" target="_blank" rel="noopener"><img style="width: 13em" src="/media/logos/bairesdev-primary.png" alt="BairesDev"></a></li>
<li><a href="https://blokt.com?utm_source=conversejs" target="_blank" rel="noopener"><img style="width: 12em" src="/logo/blokt.png" alt="Blokt Crypto & Privacy"></a></li>
<li><a href="https://www.keycdn.com?utm_source=conversejs" target="_blank" rel="noopener"><img style="height: 3em" src="/logo/keycdn.svg" alt="KeyCDN"></a></li>
Expand Down
27 changes: 18 additions & 9 deletions src/headless/plugins/disco/api.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
/**
* @typedef {import('./index').DiscoState} DiscoState
* @typedef {import('./entities').default} DiscoEntities
* @typedef {import('@converse/skeletor').Collection} Collection
*/
import { getOpenPromise } from '@converse/openpromise';
import _converse from '../../shared/_converse.js';
import api from '../../shared/api/index.js';
Expand All @@ -12,6 +7,13 @@ import log from '../../log.js';
const { Strophe, $iq } = converse.env;

export default {
/**
* @typedef {import('./entities').default} DiscoEntities
* @typedef {import('./entity').default} DiscoEntity
* @typedef {import('./index').DiscoState} DiscoState
* @typedef {import('@converse/skeletor').Collection} Collection
*/

/**
* The XEP-0030 service discovery API
*
Expand Down Expand Up @@ -174,7 +176,7 @@ export default {
* @returns {promise} Promise which resolves once we have a result from the server.
*/
items(jid, node) {
const attrs = { 'xmlns': Strophe.NS.DISCO_ITEMS };
const attrs = { xmlns: Strophe.NS.DISCO_ITEMS };
if (node) {
attrs.node = node;
}
Expand All @@ -200,6 +202,7 @@ export default {
* @method api.disco.entities.get
* @param {string} jid The Jabber ID of the entity
* @param {boolean} [create] Whether the entity should be created if it doesn't exist.
* @return {Promise<DiscoEntity|DiscoEntities|undefined>}
* @example _converse.api.disco.entities.get(jid);
*/
async get(jid, create = false) {
Expand Down Expand Up @@ -227,7 +230,10 @@ export default {
* @param {string} jid - The Jabber ID of the entity for which we want to fetch items
* @example api.disco.entities.items(jid);
*/
items(jid) {
async items(jid) {
const entity = await api.disco.entities.get(jid);
await entity.waitUntilItemsFetched;

const disco_entities = /** @type {DiscoEntities} */ (_converse.state.disco_entities);
return disco_entities.filter((e) => e.get('parent_jids')?.includes(jid));
},
Expand Down Expand Up @@ -293,9 +299,11 @@ export default {
return [];
}

const items = await api.disco.entities.items(jid);

const promises = [
entity.getFeature(feature),
...api.disco.entities.items(jid).map((i) => i.getFeature(feature)),
...items.map((i) => i.getFeature(feature)),
];
const result = await Promise.all(promises);
return result.filter((f) => f instanceof Object);
Expand Down Expand Up @@ -331,7 +339,8 @@ export default {
return true;
}

const result = await Promise.all(api.disco.entities.items(jid).map((i) => i.getFeature(feature)));
const items = await api.disco.entities.items(jid);
const result = await Promise.all(items.map((i) => i.getFeature(feature)));
return result.map((f) => f instanceof Object).includes(true);
},
},
Expand Down
6 changes: 5 additions & 1 deletion src/headless/plugins/disco/entity.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class DiscoEntity extends Model {
initialize (_, options) {
super.initialize();
this.waitUntilFeaturesDiscovered = getOpenPromise();
this.waitUntilItemsFetched = getOpenPromise();

this.dataforms = new Collection();
let id = `converse.dataforms-${this.get('jid')}`;
Expand Down Expand Up @@ -144,7 +145,8 @@ class DiscoEntity extends Model {
const jid = item.getAttribute('jid');
const entity = _converse.state.disco_entities.get(jid);
if (entity) {
entity.set({ parent_jids: [this.get('jid')] });
const parent_jids = entity.get('parent_jids');
entity.set({ parent_jids: [...parent_jids, this.get('jid')] });
} else {
api.disco.entities.create({
jid,
Expand Down Expand Up @@ -191,6 +193,8 @@ class DiscoEntity extends Model {
if (stanza.querySelector(`feature[var="${Strophe.NS.DISCO_ITEMS}"]`)) {
await this.queryForItems();
}
this.waitUntilItemsFetched.resolve();

Array.from(stanza.querySelectorAll('feature')).forEach(feature => {
this.features.create({
'var': feature.getAttribute('var'),
Expand Down
6 changes: 3 additions & 3 deletions src/headless/plugins/disco/tests/disco.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,9 @@ describe("Service Discovery", function () {
]);
const { api, domain } = _converse;
let entity = entities.get(_converse.domain);
expect(api.disco.entities.items(domain).length).toBe(3);

expect(api.disco.entities.items(domain).map(e => e.get('jid'))).toEqual(
const domain_items = await api.disco.entities.items(domain);
expect(domain_items.length).toBe(3);
expect(domain_items.map(e => e.get('jid'))).toEqual(
['people.shakespeare.lit', 'plays.shakespeare.lit', 'words.shakespeare.lit']
)

Expand Down
3 changes: 0 additions & 3 deletions src/headless/plugins/muc/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,6 @@ const rooms = {
* in the [XEP-0045 MUC specification](https://xmpp.org/extensions/xep-0045.html#registrar-formtype-owner).
* The values should be named without the `muc#roomconfig_` prefix.
* @param {boolean} [attrs.minimized] A boolean, indicating whether the room should be opened minimized or not.
* @param {boolean} [attrs.bring_to_foreground] A boolean indicating whether the room should be
* brought to the foreground and therefore replace the currently shown chat.
* If there is no chat currently open, then this option is ineffective.
* @param {boolean} [force=false] - By default, a minimized
* room won't be maximized (in `overlayed` view mode) and in
* `fullscreen` view mode a newly opened room won't replace
Expand Down
4 changes: 2 additions & 2 deletions src/headless/plugins/muc/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import MUCOccupant from './occupant.js';
import MUCOccupants from './occupants.js';
import './plugin.js';

import { isChatRoom } from './utils.js';
import { getDefaultMUCService, isChatRoom } from './utils.js';
import { setAffiliation } from './affiliations/utils.js';
Object.assign(u, { muc: { isChatRoom, setAffiliation }});
Object.assign(u, { muc: { isChatRoom, setAffiliation, getDefaultMUCService }});

export { MUCMessage, MUCMessages, MUC, MUCOccupant, MUCOccupants };
2 changes: 1 addition & 1 deletion src/headless/plugins/muc/muc.js
Original file line number Diff line number Diff line change
Expand Up @@ -1221,7 +1221,7 @@ class MUC extends ModelWithMessages(ColorAwareModel(ChatBoxBase)) {
getDiscoInfo () {
return api.disco
.getIdentity('conference', 'text', this.get('jid'))
.then((identity) => this.save({ 'name': identity?.get('name') }))
.then((identity) => this.save({ name: identity?.get('name') }))
.then(() => this.getDiscoInfoFields())
.then(() => this.getDiscoInfoFeatures())
.catch((e) => log.error(e));
Expand Down
19 changes: 19 additions & 0 deletions src/headless/plugins/muc/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,25 @@ import { getUnloadEvent } from '../../utils/session.js';

const { Strophe, sizzle, u } = converse.env;

/**
* @returns {Promise<string|undefined>}
*/
export async function getDefaultMUCService () {
let muc_service = api.settings.get('muc_domain') || _converse.session.get('default_muc_service');
if (!muc_service) {
const domain = _converse.session.get('domain');
const items = await api.disco.entities.items(domain);
for (const item of items) {
if (await api.disco.features.has(Strophe.NS.MUC, item.get('jid'))) {
muc_service = item.get('jid');
_converse.session.save({ default_muc_service: muc_service });
break;
}
}
}
return muc_service;
}

/**
* @param {import('@converse/skeletor').Model} model
*/
Expand Down
5 changes: 1 addition & 4 deletions src/headless/plugins/roster/contact.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,7 @@ class RosterContact extends ColorAwareModel(Model) {
}

openChat () {
// XXX: Doubtful whether it's necessary to pass in the contact
// attributes hers. If so, we should perhaps look them up inside the
// `open` API method.
api.chats.open(this.get('jid'), this.attributes, true);
api.chats.open(this.get('jid'), {}, true);
}

getDisplayName () {
Expand Down
8 changes: 3 additions & 5 deletions src/headless/types/plugins/disco/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,17 +84,18 @@ declare namespace _default {
* @method api.disco.entities.get
* @param {string} jid The Jabber ID of the entity
* @param {boolean} [create] Whether the entity should be created if it doesn't exist.
* @return {Promise<DiscoEntity|DiscoEntities|undefined>}
* @example _converse.api.disco.entities.get(jid);
*/
function get(jid: string, create?: boolean): Promise<any>;
function get(jid: string, create?: boolean): Promise<import("./entity").default | import("./entities").default | undefined>;
/**
* Return any disco items advertised on this entity
*
* @method api.disco.entities.items
* @param {string} jid - The Jabber ID of the entity for which we want to fetch items
* @example api.disco.entities.items(jid);
*/
function items(jid: string): any;
function items(jid: string): Promise<any>;
/**
* Create a new disco entity. It's identity and features
* will automatically be fetched from cache or from the
Expand Down Expand Up @@ -251,7 +252,4 @@ declare namespace _default {
}
}
export default _default;
export type DiscoState = import("./index").DiscoState;
export type DiscoEntities = import("./entities").default;
export type Collection = import("@converse/skeletor").Collection;
//# sourceMappingURL=api.d.ts.map
1 change: 1 addition & 0 deletions src/headless/types/plugins/disco/entity.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export default DiscoEntity;
declare class DiscoEntity extends Model {
initialize(_: any, options: any): void;
waitUntilFeaturesDiscovered: any;
waitUntilItemsFetched: any;
dataforms: Collection;
features: Collection;
fields: Collection;
Expand Down
4 changes: 0 additions & 4 deletions src/headless/types/plugins/muc/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,6 @@ declare namespace rooms {
* in the [XEP-0045 MUC specification](https://xmpp.org/extensions/xep-0045.html#registrar-formtype-owner).
* The values should be named without the `muc#roomconfig_` prefix.
* @param {boolean} [attrs.minimized] A boolean, indicating whether the room should be opened minimized or not.
* @param {boolean} [attrs.bring_to_foreground] A boolean indicating whether the room should be
* brought to the foreground and therefore replace the currently shown chat.
* If there is no chat currently open, then this option is ineffective.
* @param {boolean} [force=false] - By default, a minimized
* room won't be maximized (in `overlayed` view mode) and in
* `fullscreen` view mode a newly opened room won't replace
Expand Down Expand Up @@ -83,7 +80,6 @@ declare namespace rooms {
auto_configure?: boolean;
roomconfig?: object;
minimized?: boolean;
bring_to_foreground?: boolean;
}, force?: boolean): Promise<MUC[] | MUC>;
/**
* Fetches the object representing a MUC chatroom (aka groupchat)
Expand Down
4 changes: 4 additions & 0 deletions src/headless/types/plugins/muc/utils.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
/**
* @returns {Promise<string|undefined>}
*/
export function getDefaultMUCService(): Promise<string | undefined>;
/**
* @param {import('@converse/skeletor').Model} model
*/
Expand Down
12 changes: 9 additions & 3 deletions src/headless/utils/jid.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,16 @@ import { Strophe } from 'strophe.js';
* @returns {boolean}
*/
export function isValidJID(jid) {
if (typeof jid === 'string') {
return jid.split('@').filter((s) => !!s).length === 2 && !jid.startsWith('@') && !jid.endsWith('@');
if (!(typeof jid === 'string')) {
return false;
}
return false;

const num_slashes = jid.split('/').length - 1;
if (num_slashes > 1) {
return false;
}

return jid.split('@').filter((s) => !!s).length === 2 && !jid.startsWith('@') && !jid.endsWith('@');
}

/**
Expand Down
15 changes: 6 additions & 9 deletions src/plugins/chatview/tests/http-file-upload.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,9 @@ describe("XEP-0363: HTTP File Upload", function () {
expect(entities.get(_converse.domain).features.length).toBe(2);
expect(entities.get(_converse.domain).identities.length).toBe(1);

api.disco.entities.get().then(entities => {
expect(entities.length).toBe(3);
expect(entities.pluck('jid')).toEqual(['montague.lit', '[email protected]', 'upload.montague.lit']);
expect(api.disco.entities.items('montague.lit').length).toBe(1);
// Converse.js sees that the entity has a disco#info feature, so it will make a query for it.
const selector = 'iq[to="upload.montague.lit"] query[xmlns="http://jabber.org/protocol/disco#info"]';
return u.waitUntil(() => IQ_stanzas.filter(iq => iq.querySelector(selector)).length > 0);
});
const domain_items = await api.disco.entities.items('montague.lit')
expect(domain_items.length).toBe(1);
// Converse.js sees that the entity has a disco#info feature, so it will make a query for it.

selector = 'iq[to="upload.montague.lit"] query[xmlns="http://jabber.org/protocol/disco#info"]';
stanza = await u.waitUntil(() => IQ_stanzas.filter(iq => iq.querySelector(selector)).pop(), 1000);
Expand Down Expand Up @@ -311,7 +306,9 @@ describe("XEP-0363: HTTP File Upload", function () {
expect(entities.get(_converse.domain).features.length).toBe(2);
expect(entities.get(_converse.domain).identities.length).toBe(1);
expect(entities.pluck('jid')).toEqual(['montague.lit', '[email protected]', 'upload.montague.lit']);
expect(api.disco.entities.items('montague.lit').length).toBe(1);

const items = await api.disco.entities.items('montague.lit');
expect(items.length).toBe(1);
await u.waitUntil(function () {
// Converse.js sees that the entity has a disco#info feature,
// so it will make a query for it.
Expand Down
Loading
Loading