Jelajahi Sumber

Show a validation error in add-muc modal when no default MUC domain is found

JC Brand 5 bulan lalu
induk
melakukan
c13dcd8937

+ 7 - 6
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
      *

+ 2 - 1
src/headless/plugins/muc/utils.js

@@ -13,13 +13,14 @@ const { Strophe, sizzle, u } = converse.env;
  * @returns {Promise<string|undefined>}
  */
 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;
             }
         }

+ 1 - 5
src/headless/types/plugins/disco/api.d.ts

@@ -87,7 +87,7 @@ declare namespace _default {
              * @return {Promise<DiscoEntity|DiscoEntities|undefined>}
              * @example _converse.api.disco.entities.get(jid);
              */
-            function get(jid: string, create?: boolean): Promise<DiscoEntity | DiscoEntities | undefined>;
+            function get(jid: string, create?: boolean): Promise<import("./entity").default | import("./entities").default | undefined>;
             /**
              * 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

+ 15 - 6
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<string>}
      */
-    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)) {

+ 0 - 5
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
                       ></converse-autocomplete>`

+ 33 - 7
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);

+ 2 - 3
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;
     }

+ 2 - 2
src/types/plugins/muc-views/modals/add-muc.d.ts

@@ -23,9 +23,9 @@ export default class AddMUCModal extends BaseModal {
     openChatRoom(ev: Event): Promise<void>;
     /**
      * @param {string} jid
-     * @return {string}
+     * @return {Promise<string>}
      */
-    validateMUCJID(jid: string): string;
+    validateMUCJID(jid: string): Promise<string>;
 }
 import BaseModal from 'plugins/modal/modal.js';
 //# sourceMappingURL=add-muc.d.ts.map

+ 1 - 1
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<this>;
 }
 import { CustomElement } from 'shared/components/element.js';
 import AutoComplete from './autocomplete.js';