diff --git a/src/headless/plugins/disco/api.js b/src/headless/plugins/disco/api.js index 1e218d0116..cc713c1979 100644 --- a/src/headless/plugins/disco/api.js +++ b/src/headless/plugins/disco/api.js @@ -1,18 +1,19 @@ -/** - * @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'; import converse from '../../shared/api/public.js'; import log from '../../log.js'; -import DiscoEntity from './entity.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 * diff --git a/src/headless/plugins/muc/utils.js b/src/headless/plugins/muc/utils.js index eb2e2857a9..0cddedf9cf 100644 --- a/src/headless/plugins/muc/utils.js +++ b/src/headless/plugins/muc/utils.js @@ -13,13 +13,14 @@ const { Strophe, sizzle, u } = converse.env; * @returns {Promise} */ export async function getDefaultMUCService () { - let muc_service = api.settings.get('muc_domain'); + 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; } } diff --git a/src/headless/types/plugins/disco/api.d.ts b/src/headless/types/plugins/disco/api.d.ts index 279d763070..be4493a588 100644 --- a/src/headless/types/plugins/disco/api.d.ts +++ b/src/headless/types/plugins/disco/api.d.ts @@ -87,7 +87,7 @@ declare namespace _default { * @return {Promise} * @example _converse.api.disco.entities.get(jid); */ - function get(jid: string, create?: boolean): Promise; + function get(jid: string, create?: boolean): Promise; /** * Return any disco items advertised on this entity * @@ -252,8 +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; -import DiscoEntity from './entity.js'; //# sourceMappingURL=api.d.ts.map \ No newline at end of file diff --git a/src/plugins/muc-views/modals/add-muc.js b/src/plugins/muc-views/modals/add-muc.js index ee684aa4a0..a5d177a959 100644 --- a/src/plugins/muc-views/modals/add-muc.js +++ b/src/plugins/muc-views/modals/add-muc.js @@ -1,5 +1,4 @@ import { _converse, api, converse } from '@converse/headless'; -import AutoCompleteComponent from 'shared/autocomplete/component.js'; import tplAddMuc from './templates/add-muc.js'; import BaseModal from 'plugins/modal/modal.js'; import { __ } from 'i18n'; @@ -60,7 +59,7 @@ export default class AddMUCModal extends BaseModal { return s .trim() .replace(/\s+/g, '-') - .replace(/\u0142/g, "l") + .replace(/\u0142/g, 'l') .replace(/[^\x00-\x7F]/g, (c) => c.normalize('NFD').replace(/[\u0300-\u036f]/g, '')) .replace(/[^a-zA-Z0-9-]/g, '-') .replace(/-+/g, '-') @@ -74,8 +73,8 @@ export default class AddMUCModal extends BaseModal { async openChatRoom(ev) { ev.preventDefault(); - const autocomplete_el = /** @type {AutoCompleteComponent} */ (this.querySelector('converse-autocomplete')); - if (autocomplete_el.onChange().error_message) return; + const autocomplete_el = /** @type {import('shared/autocomplete/component').default} */ (this.querySelector('converse-autocomplete')); + if ((await autocomplete_el.onChange()).error_message) return; const { escapeNode, getNodeFromJid, getDomainFromJid } = Strophe; const form = /** @type {HTMLFormElement} */ (ev.target); @@ -109,9 +108,9 @@ export default class AddMUCModal extends BaseModal { /** * @param {string} jid - * @return {string} + * @return {Promise} */ - validateMUCJID(jid) { + async validateMUCJID(jid) { if (jid.length === 0) { return __('Invalid groupchat address, it cannot be empty.'); } @@ -130,6 +129,16 @@ export default class AddMUCModal extends BaseModal { return __('Invalid groupchat address, it cannot start or end with an @ sign.'); } + if (!jid.includes('@')) { + const muc_service = await u.muc.getDefaultMUCService(); + if (!muc_service) { + return __( + "No default groupchat service found. "+ + "You'll need to specify the full address, for example room@conference.example.org" + ); + } + } + const policy = api.settings.get('muc_roomid_policy'); if (policy && api.settings.get('muc_domain')) { if (api.settings.get('locked_muc_domain') || !u.isValidJID(jid)) { diff --git a/src/plugins/muc-views/modals/templates/add-muc.js b/src/plugins/muc-views/modals/templates/add-muc.js index 84fa62a474..9603e1ae17 100644 --- a/src/plugins/muc-views/modals/templates/add-muc.js +++ b/src/plugins/muc-views/modals/templates/add-muc.js @@ -28,14 +28,10 @@ const nickname_input = () => { */ export default (el) => { const i18n_join = __('Join'); - const muc_domain = api.settings.get('muc_domain'); - - let placeholder = ''; let label_name; if (api.settings.get('locked_muc_domain')) { label_name = __('Groupchat name'); } else { - placeholder = muc_domain ? `name@${muc_domain}` : __('name@conference.example.org'); label_name = __('Groupchat name or address'); } @@ -58,7 +54,6 @@ export default (el) => { class="add-muc-autocomplete" min_chars="3" name="chatroom" - placeholder="${placeholder}" position="below" required >` diff --git a/src/plugins/muc-views/tests/muc-add-modal.js b/src/plugins/muc-views/tests/muc-add-modal.js index ba7c314bb6..1102e38949 100644 --- a/src/plugins/muc-views/tests/muc-add-modal.js +++ b/src/plugins/muc-views/tests/muc-add-modal.js @@ -1,7 +1,6 @@ /*global mock, converse */ const { Promise, sizzle, u } = converse.env; - describe('The "Groupchats" Add modal', function () { beforeAll(() => jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza })); @@ -12,9 +11,6 @@ describe('The "Groupchats" Add modal', function () { let label_name = modal.querySelector('label[for="chatroom"]'); expect(label_name.textContent.trim()).toBe('Groupchat name or address:'); - const name_input = modal.querySelector('input[name="chatroom"]'); - expect(name_input.placeholder).toBe('name@conference.example.org'); - const label_nick = modal.querySelector('label[for="nickname"]'); expect(label_nick.textContent.trim()).toBe('Nickname:'); const nick_input = modal.querySelector('input[name="nickname"]'); @@ -39,7 +35,6 @@ describe('The "Groupchats" Add modal', function () { const label_name = modal.querySelector('label[for="chatroom"]'); expect(label_name.textContent.trim()).toBe('Groupchat name or address:'); let name_input = modal.querySelector('input[name="chatroom"]'); - expect(name_input.placeholder).toBe('name@muc.example.org'); name_input.value = 'lounge'; let nick_input = modal.querySelector('input[name="nickname"]'); nick_input.value = 'max'; @@ -66,7 +61,7 @@ describe('The "Groupchats" Add modal', function () { }) ); - it('only uses the muc_domain if locked_muc_domain is true', mock.initConverse( + it('uses the muc_domain if locked_muc_domain is true', mock.initConverse( ['chatBoxesFetched'], { muc_domain: 'muc.example.org', locked_muc_domain: true }, async function (_converse) { const modal = await mock.openAddMUCModal(_converse); @@ -104,7 +99,7 @@ describe('The "Groupchats" Add modal', function () { }) ); - fit("lets you create a MUC with only the name", + it("lets you create a MUC with only the name", mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) { const { domain } = _converse; await mock.waitUntilDiscoConfirmed( @@ -210,6 +205,37 @@ describe('The "Groupchats" Add modal', function () { }) ); + it("shows a validation error when only the name was specified and there's no default MUC service", + mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) { + const { domain } = _converse; + await mock.waitUntilDiscoConfirmed( + _converse, + domain, + [{ category: 'server', type: 'IM' }], + [], + ); + + const nick = 'max'; + const modal = await mock.openAddMUCModal(_converse); + spyOn(_converse.ChatRoom.prototype, 'getDiscoInfo').and.callFake(() => Promise.resolve()); + + const name_input = modal.querySelector('input[name="chatroom"]'); + name_input.value = 'The Lounge'; + + const nick_input = modal.querySelector('input[name="nickname"]'); + nick_input.value = nick; + + modal.querySelector('form input[type="submit"]').click(); + + await u.waitUntil(() => name_input.classList.contains('error')); + expect(name_input.classList.contains('is-invalid')).toBe(true); + expect(modal.querySelector('.invalid-feedback')?.textContent).toBe( + "No default groupchat service found. "+ + "You'll need to specify the full address, for example room@conference.example.org" + ); + }) + ); + it("normalizes the MUC name when creating the corresponding JID", mock.initConverse(['chatBoxesFetched'], {muc_domain: 'montague.lit'}, async function (_converse) { const modal = await mock.openAddMUCModal(_converse); diff --git a/src/shared/autocomplete/component.js b/src/shared/autocomplete/component.js index 2a21bfc945..e3ee520dab 100644 --- a/src/shared/autocomplete/component.js +++ b/src/shared/autocomplete/component.js @@ -3,7 +3,6 @@ import { CustomElement } from 'shared/components/element.js'; import { FILTER_CONTAINS, FILTER_STARTSWITH } from './utils.js'; import { api } from '@converse/headless'; import { html } from 'lit'; -import {ancestor} from 'utils/html.js'; /** * A custom element that can be used to add auto-completion suggestions to a form input. @@ -147,9 +146,9 @@ export default class AutoCompleteComponent extends CustomElement { this.auto_complete.evaluate(ev); } - onChange() { + async onChange() { const input = this.querySelector('input'); - this.error_message = this.validate?.(input.value); + this.error_message = await this.validate?.(input.value); if (this.error_message) this.requestUpdate(); return this; } diff --git a/src/types/plugins/muc-views/modals/add-muc.d.ts b/src/types/plugins/muc-views/modals/add-muc.d.ts index 8ec3d06557..92bd0f061e 100644 --- a/src/types/plugins/muc-views/modals/add-muc.d.ts +++ b/src/types/plugins/muc-views/modals/add-muc.d.ts @@ -23,9 +23,9 @@ export default class AddMUCModal extends BaseModal { openChatRoom(ev: Event): Promise; /** * @param {string} jid - * @return {string} + * @return {Promise} */ - validateMUCJID(jid: string): string; + validateMUCJID(jid: string): Promise; } import BaseModal from 'plugins/modal/modal.js'; //# sourceMappingURL=add-muc.d.ts.map \ No newline at end of file diff --git a/src/types/shared/autocomplete/component.d.ts b/src/types/shared/autocomplete/component.d.ts index 1a677a9cd2..6728cf75b3 100644 --- a/src/types/shared/autocomplete/component.d.ts +++ b/src/types/shared/autocomplete/component.d.ts @@ -119,7 +119,7 @@ export default class AutoCompleteComponent extends CustomElement { onKeyDown(ev: KeyboardEvent): void; /** @param {KeyboardEvent} ev */ onKeyUp(ev: KeyboardEvent): void; - onChange(): this; + onChange(): Promise; } import { CustomElement } from 'shared/components/element.js'; import AutoComplete from './autocomplete.js';