2
0
Эх сурвалжийг харах

Refine XHR user search

- Use one input for both the name and XMPP address, instead of two.
- Show both name and JID in the search results and the input.
- Remove the `autocomplete_add_contact` config setting. Auto-complete is
  now always enabled.
JC Brand 3 сар өмнө
parent
commit
89053ebb54

+ 1 - 0
CHANGES.md

@@ -89,6 +89,7 @@
 - The `allow_non_roster_messaging` setting now defaults to `true`.
 - The `allow_non_roster_messaging` setting now defaults to `true`.
 
 
 ### Breaking changes:
 ### Breaking changes:
+- Removed the `autocomplete_add_contact` config setting. Auto-complete is now always enabled.
 - Remove the old `_converse.BootstrapModal` in favor of `_converse.BaseModal` which is a web component.
 - Remove the old `_converse.BootstrapModal` in favor of `_converse.BaseModal` which is a web component.
 - The connection is no longer available on the `_converse` object. Instead, use `api.connection.get()`.
 - The connection is no longer available on the `_converse` object. Instead, use `api.connection.get()`.
 - Add a new `exports` attribute on the `_converse` object which is meant for
 - Add a new `exports` attribute on the `_converse` object which is meant for

+ 0 - 7
docs/source/configuration.rst

@@ -321,13 +321,6 @@ You will be able to query for even older messages by scrolling upwards in the ch
 (the so-called infinite scrolling pattern).
 (the so-called infinite scrolling pattern).
 
 
 
 
-autocomplete_add_contact
-------------------------
-
-* Default: ``true``
-
-Determines whether search suggestions are shown in the "Add Contact" modal.
-
 auto_fill_history_gaps
 auto_fill_history_gaps
 ----------------------
 ----------------------
 
 

+ 0 - 1
src/plugins/rosterview/index.js

@@ -21,7 +21,6 @@ converse.plugins.add('converse-rosterview', {
 
 
     initialize () {
     initialize () {
         api.settings.extend({
         api.settings.extend({
-            'autocomplete_add_contact': true,
             'allow_contact_removal': true,
             'allow_contact_removal': true,
             'hide_offline_users': false,
             'hide_offline_users': false,
             'roster_groups': true,
             'roster_groups': true,

+ 42 - 32
src/plugins/rosterview/modals/add-contact.js

@@ -1,43 +1,42 @@
-import { Strophe } from 'strophe.js';
-import { _converse, api, log } from '@converse/headless';
-import 'shared/autocomplete/index.js';
-import BaseModal from 'plugins/modal/modal.js';
-import tplAddContactModal from './templates/add-contact.js';
-import { __ } from 'i18n';
-import { getNamesAutoCompleteList } from '../utils.js';
+import { Strophe } from "strophe.js";
+import { _converse, api, log, u } from "@converse/headless";
+import "shared/autocomplete/index.js";
+import BaseModal from "plugins/modal/modal.js";
+import tplAddContactModal from "./templates/add-contact.js";
+import { __ } from "i18n";
 
 
 export default class AddContactModal extends BaseModal {
 export default class AddContactModal extends BaseModal {
-    initialize () {
+    initialize() {
         super.initialize();
         super.initialize();
-        this.listenTo(this.model, 'change', () => this.requestUpdate());
+        this.listenTo(this.model, "change", () => this.requestUpdate());
         this.requestUpdate();
         this.requestUpdate();
         this.addEventListener(
         this.addEventListener(
-            'shown.bs.modal',
+            "shown.bs.modal",
             () => /** @type {HTMLInputElement} */ (this.querySelector('input[name="jid"]'))?.focus(),
             () => /** @type {HTMLInputElement} */ (this.querySelector('input[name="jid"]'))?.focus(),
             false
             false
         );
         );
     }
     }
 
 
-    renderModal () {
+    renderModal() {
         return tplAddContactModal(this);
         return tplAddContactModal(this);
     }
     }
 
 
-    getModalTitle () {
-        return __('Add a Contact');
+    getModalTitle() {
+        return __("Add a Contact");
     }
     }
 
 
     /**
     /**
      * @param {string} jid
      * @param {string} jid
      */
      */
-    validateSubmission (jid) {
-        if (!jid || jid.split('@').filter((s) => !!s).length < 2) {
-            this.model.set('error', __('Please enter a valid XMPP address'));
+    validateSubmission(jid) {
+        if (!jid || jid.split("@").filter((s) => !!s).length < 2) {
+            this.model.set("error", __("Please enter a valid XMPP address"));
             return false;
             return false;
         } else if (_converse.state.roster.get(Strophe.getBareJidFromJid(jid))) {
         } else if (_converse.state.roster.get(Strophe.getBareJidFromJid(jid))) {
-            this.model.set('error', __('This contact has already been added'));
+            this.model.set("error", __("This contact has already been added"));
             return false;
             return false;
         }
         }
-        this.model.set('error', null);
+        this.model.set("error", null);
         return true;
         return true;
     }
     }
 
 
@@ -47,12 +46,12 @@ export default class AddContactModal extends BaseModal {
      * @param {string} name
      * @param {string} name
      * @param {string[]} groups
      * @param {string[]} groups
      */
      */
-    async afterSubmission (_form, jid, name, groups) {
+    async afterSubmission(_form, jid, name, groups) {
         try {
         try {
             await api.contacts.add({ jid, name, groups });
             await api.contacts.add({ jid, name, groups });
         } catch (e) {
         } catch (e) {
             log.error(e);
             log.error(e);
-            this.model.set('error', __('Sorry, something went wrong'));
+            this.model.set("error", __("Sorry, something went wrong"));
             return;
             return;
         }
         }
         this.model.clear();
         this.model.clear();
@@ -62,22 +61,33 @@ export default class AddContactModal extends BaseModal {
     /**
     /**
      * @param {Event} ev
      * @param {Event} ev
      */
      */
-    async addContactFromForm (ev) {
+    async addContactFromForm(ev) {
         ev.preventDefault();
         ev.preventDefault();
-        const form = /** @type {HTMLFormElement} */(ev.target);
+        const form = /** @type {HTMLFormElement} */ (ev.target);
         const data = new FormData(form);
         const data = new FormData(form);
-        let name = /** @type {string} */ (data.get('name') || '').trim();
-        let jid = /** @type {string} */ (data.get('jid') || '').trim();
+        let jid = /** @type {string} */ (data.get("jid") || "").trim();
 
 
-        if (!jid && typeof api.settings.get('xhr_user_search_url') === 'string') {
-            const list = await getNamesAutoCompleteList(name);
-            if (list.length !== 1) {
-                this.model.set('error', __('Sorry, could not find a contact with that name'));
-                this.requestUpdate();
+        let name;
+        if (api.settings.get("xhr_user_search_url")) {
+            // In this case, the value of `jid` is something like `John Doe <john@chat.com>`
+            // So we want to get `name` which is `John Doe` and reset `jid` to
+            // what's inside the arrow brackets, so in this case
+            // `john@chat.com`.
+            const match = jid.match(/^(.*) <(.*)>$/);
+            if (match) {
+                name = match[1].trim();
+                jid = match[2].trim();
+            } else {
+                this.model.set(
+                    "error",
+                    __(
+                        'Invalid value for the name and XMPP address. Please use the format "Name <username@example.org>".'
+                    )
+                );
                 return;
                 return;
             }
             }
-            jid = list[0].value;
-            name = list[0].label;
+        } else {
+            name = /** @type {string} */ (data.get("name") || "").trim();
         }
         }
 
 
         if (this.validateSubmission(jid)) {
         if (this.validateSubmission(jid)) {
@@ -91,4 +101,4 @@ export default class AddContactModal extends BaseModal {
     }
     }
 }
 }
 
 
-api.elements.define('converse-add-contact-modal', AddContactModal);
+api.elements.define("converse-add-contact-modal", AddContactModal);

+ 28 - 29
src/plugins/rosterview/modals/templates/add-contact.js

@@ -2,7 +2,7 @@ import { __ } from "i18n";
 import { api } from "@converse/headless";
 import { api } from "@converse/headless";
 import { getGroupsAutoCompleteList, getJIDsAutoCompleteList, getNamesAutoCompleteList } from "../../utils.js";
 import { getGroupsAutoCompleteList, getJIDsAutoCompleteList, getNamesAutoCompleteList } from "../../utils.js";
 import { html } from "lit";
 import { html } from "lit";
-import { FILTER_STARTSWITH } from "shared/autocomplete/utils";
+import { FILTER_STARTSWITH, FILTER_CONTAINS } from "shared/autocomplete/utils";
 
 
 /**
 /**
  * @param {import('../add-contact.js').default} el
  * @param {import('../add-contact.js').default} el
@@ -13,7 +13,8 @@ export default (el) => {
     const i18n_groups = __("Groups");
     const i18n_groups = __("Groups");
     const i18n_groups_help = __("Use commas to separate multiple values");
     const i18n_groups_help = __("Use commas to separate multiple values");
     const i18n_nickname = __("Name");
     const i18n_nickname = __("Name");
-    const i18n_xmpp_address = __("XMPP Address");
+    const using_xhr = api.settings.get("xhr_user_search_url");
+    const i18n_xmpp_address = using_xhr ? __("Search name or XMPP address") : __("XMPP Address");
     const error = el.model.get("error");
     const error = el.model.get("error");
 
 
     return html` <div class="modal-body">
     return html` <div class="modal-body">
@@ -21,44 +22,42 @@ export default (el) => {
         <form class="converse-form add-xmpp-contact" @submit=${(ev) => el.addContactFromForm(ev)}>
         <form class="converse-form add-xmpp-contact" @submit=${(ev) => el.addContactFromForm(ev)}>
             <div class="mb-3">
             <div class="mb-3">
                 <label class="form-label clearfix" for="jid">${i18n_xmpp_address}:</label>
                 <label class="form-label clearfix" for="jid">${i18n_xmpp_address}:</label>
-                ${api.settings.get("autocomplete_add_contact")
+                ${using_xhr
                     ? html`<converse-autocomplete
                     ? html`<converse-autocomplete
-                          .list=${getJIDsAutoCompleteList()}
-                          .data=${(text, input) => `${input.slice(0, input.indexOf("@"))}@${text}`}
+                          .getAutoCompleteList=${getNamesAutoCompleteList}
                           position="below"
                           position="below"
-                          filter=${FILTER_STARTSWITH}
-                          ?required=${!api.settings.get("xhr_user_search_url")}
+                          filter=${FILTER_CONTAINS}
+                          ?required=${true}
                           value="${el.model.get("jid") || ""}"
                           value="${el.model.get("jid") || ""}"
                           placeholder="${i18n_contact_placeholder}"
                           placeholder="${i18n_contact_placeholder}"
                           name="jid"
                           name="jid"
                       ></converse-autocomplete>`
                       ></converse-autocomplete>`
-                    : html`<input
-                          type="text"
-                          name="jid"
+                    : html`<converse-autocomplete
+                          .list=${getJIDsAutoCompleteList()}
+                          .data=${(text, input) => `${input.slice(0, input.indexOf("@"))}@${text}`}
+                          position="below"
+                          filter=${FILTER_STARTSWITH}
                           ?required=${!api.settings.get("xhr_user_search_url")}
                           ?required=${!api.settings.get("xhr_user_search_url")}
                           value="${el.model.get("jid") || ""}"
                           value="${el.model.get("jid") || ""}"
-                          class="form-control"
                           placeholder="${i18n_contact_placeholder}"
                           placeholder="${i18n_contact_placeholder}"
-                      />`}
+                          name="jid"
+                      ></converse-autocomplete>`}
             </div>
             </div>
 
 
-            <div class="mb-3">
-                <label class="form-label clearfix" for="name">${i18n_nickname}:</label>
-                ${api.settings.get("autocomplete_add_contact") &&
-                typeof api.settings.get("xhr_user_search_url") === "string"
-                    ? html`<converse-autocomplete
-                          .getAutoCompleteList=${(query) => getNamesAutoCompleteList(query, "fullname")}
-                          filter=${FILTER_STARTSWITH}
-                          value="${el.model.get("nickname") || ""}"
-                          name="name"
-                      ></converse-autocomplete>`
-                    : html`<input
-                          type="text"
-                          name="name"
-                          value="${el.model.get("nickname") || ""}"
-                          class="form-control"
-                      />`}
-            </div>
+            ${!using_xhr
+                ? html`
+                      <div class="mb-3">
+                          <label class="form-label clearfix" for="name">${i18n_nickname}:</label>
+                          <input
+                              type="text"
+                              name="name"
+                              value="${el.model.get("nickname") || ""}"
+                              class="form-control"
+                          />
+                      </div>
+                  `
+                : ""}
+
             <div class="mb-3">
             <div class="mb-3">
                 <label class="form-label clearfix" for="name">${i18n_groups}:</label>
                 <label class="form-label clearfix" for="name">${i18n_groups}:</label>
                 <div class="mb-1">
                 <div class="mb-1">

+ 7 - 101
src/plugins/rosterview/tests/add-contact-modal.js

@@ -52,35 +52,8 @@ describe("The 'Add Contact' widget", function () {
             </iq>`);
             </iq>`);
     }));
     }));
 
 
-    it("can be configured to not provide search suggestions",
-            mock.initConverse([], {'autocomplete_add_contact': false}, async function (_converse) {
-
-        await mock.waitForRoster(_converse, 'all', 0);
-        await mock.openControlBox(_converse);
-        const cbview = _converse.chatboxviews.get('controlbox');
-        cbview.querySelector('.add-contact').click()
-        const modal = _converse.api.modal.get('converse-add-contact-modal');
-        expect(modal.jid_auto_complete).toBe(undefined);
-        expect(modal.name_auto_complete).toBe(undefined);
-
-        await u.waitUntil(() => u.isVisible(modal), 1000);
-        expect(modal.querySelector('form.add-xmpp-contact')).not.toBe(null);
-        const input_jid = modal.querySelector('input[name="jid"]');
-        input_jid.value = 'someone@montague.lit';
-        modal.querySelector('button[type="submit"]').click();
-
-        const IQ_stanzas = _converse.api.connection.get().IQ_stanzas;
-        const sent_stanza = await u.waitUntil(
-            () => IQ_stanzas.filter(s => sizzle(`iq[type="set"] query[xmlns="${Strophe.NS.ROSTER}"]`, s).length).pop()
-        );
-        expect(sent_stanza).toEqualStanza(stx`
-            <iq id="${sent_stanza.getAttribute('id')}" type="set" xmlns="jabber:client">
-                <query xmlns="jabber:iq:roster"><item jid="someone@montague.lit"/></query>
-            </iq>`);
-    }));
-
     it("integrates with xhr_user_search_url to search for contacts",
     it("integrates with xhr_user_search_url to search for contacts",
-            mock.initConverse([], { 'xhr_user_search_url': 'http://example.org/?' },
+            mock.initConverse([], { xhr_user_search_url: 'http://example.org/?' },
             async function (_converse) {
             async function (_converse) {
 
 
         await mock.waitForRoster(_converse, 'all', 0);
         await mock.waitForRoster(_converse, 'all', 0);
@@ -99,96 +72,29 @@ describe("The 'Add Contact' widget", function () {
         const modal = _converse.api.modal.get('converse-add-contact-modal');
         const modal = _converse.api.modal.get('converse-add-contact-modal');
         await u.waitUntil(() => u.isVisible(modal), 1000);
         await u.waitUntil(() => u.isVisible(modal), 1000);
 
 
-
-        const input_el = modal.querySelector('input[name="name"]');
+        const input_el = modal.querySelector('input[name="jid"]');
         input_el.value = 'marty';
         input_el.value = 'marty';
         input_el.dispatchEvent(new Event('input'));
         input_el.dispatchEvent(new Event('input'));
+
         await u.waitUntil(() => modal.querySelector('.suggestion-box li'), 1000);
         await u.waitUntil(() => modal.querySelector('.suggestion-box li'), 1000);
         expect(modal.querySelectorAll('.suggestion-box li').length).toBe(1);
         expect(modal.querySelectorAll('.suggestion-box li').length).toBe(1);
         const suggestion = modal.querySelector('.suggestion-box li');
         const suggestion = modal.querySelector('.suggestion-box li');
-        expect(suggestion.textContent).toBe('Marty McFly');
+        expect(suggestion.textContent).toBe('Marty McFly <marty@mcfly.net>');
 
 
         const el = u.ancestor(suggestion, 'converse-autocomplete');
         const el = u.ancestor(suggestion, 'converse-autocomplete');
         el.auto_complete.select(suggestion);
         el.auto_complete.select(suggestion);
 
 
-        expect(input_el.value.trim()).toBe('Marty McFly');
-        // TODO: We only have autocomplete for the name input
-        return;
+        expect(input_el.value.trim()).toBe('Marty McFly <marty@mcfly.net>');
 
 
-        expect(modal.querySelector('input[name="jid"]').value).toBe('marty@mcfly.net');
-        modal.querySelector('button[type="submit"]').click();
-
-        const sent_IQs = _converse.api.connection.get().IQ_stanzas;
-        const sent_stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq[type="set"] query[xmlns="${Strophe.NS.ROSTER}"]`)).pop());
-        expect(Strophe.serialize(sent_stanza)).toEqual(
-        `<iq id="${sent_stanza.getAttribute('id')}" type="set" xmlns="jabber:client">`+
-            `<query xmlns="jabber:iq:roster"><item jid="marty@mcfly.net" name="Marty McFly"/></query>`+
-        `</iq>`);
-        window.XMLHttpRequest = XMLHttpRequestBackup;
-    }));
-
-    it("can be configured to not provide search suggestions for XHR search results",
-        mock.initConverse([],
-            { 'autocomplete_add_contact': false,
-              'xhr_user_search_url': 'http://example.org/?' },
-            async function (_converse) {
-
-        await mock.waitForRoster(_converse, 'all');
-        await mock.openControlBox(_converse);
-
-        spyOn(window, 'fetch').and.callFake(() => {
-            let json;
-            const value = modal.querySelector('input[name="name"]').value;
-            if (value === 'existing') {
-                const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
-                json = [{"jid": contact_jid, "fullname": mock.cur_names[0]}];
-            } else if (value === 'romeo') {
-                json = [{"jid": "romeo@montague.lit", "fullname": "Romeo Montague"}];
-            } else if (value === 'ambiguous') {
-                json = [
-                    {"jid": "marty@mcfly.net", "fullname": "Marty McFly"},
-                    {"jid": "doc@brown.com", "fullname": "Doc Brown"}
-                ];
-            } else if (value === 'insufficient') {
-                json = [];
-            } else {
-                json = [{"jid": "marty@mcfly.net", "fullname": "Marty McFly"}];
-            }
-            return { json };
-        });
-
-        const cbview = _converse.chatboxviews.get('controlbox');
-        cbview.querySelector('.add-contact').click()
-        const modal = _converse.api.modal.get('converse-add-contact-modal');
-        await u.waitUntil(() => u.isVisible(modal), 1000);
-
-        expect(modal.jid_auto_complete).toBe(undefined);
-        expect(modal.name_auto_complete).toBe(undefined);
-
-        const input_el = modal.querySelector('input[name="name"]');
-        input_el.value = 'ambiguous';
-        modal.querySelector('button[type="submit"]').click();
-
-        const feedback_el = await u.waitUntil(() => modal.querySelector('.alert-danger'));
-        expect(feedback_el.textContent).toBe('Sorry, could not find a contact with that name');
-
-        input_el.value = 'existing';
-        modal.querySelector('button[type="submit"]').click();
-        await u.waitUntil(() => feedback_el.textContent === 'This contact has already been added');
-
-        input_el.value = 'insufficient';
-        modal.querySelector('button[type="submit"]').click();
-        await u.waitUntil(() => feedback_el.textContent === 'Sorry, could not find a contact with that name');
-
-        input_el.value = 'Marty McFly';
         modal.querySelector('button[type="submit"]').click();
         modal.querySelector('button[type="submit"]').click();
 
 
         const sent_IQs = _converse.api.connection.get().IQ_stanzas;
         const sent_IQs = _converse.api.connection.get().IQ_stanzas;
         const sent_stanza = await u.waitUntil(() => sent_IQs.filter(
         const sent_stanza = await u.waitUntil(() => sent_IQs.filter(
             iq => sizzle(`iq[type="set"] query[xmlns="${Strophe.NS.ROSTER}"]`, iq).length).pop());
             iq => sizzle(`iq[type="set"] query[xmlns="${Strophe.NS.ROSTER}"]`, iq).length).pop());
+
         expect(sent_stanza).toEqualStanza(stx`
         expect(sent_stanza).toEqualStanza(stx`
             <iq id="${sent_stanza.getAttribute('id')}" type="set" xmlns="jabber:client">
             <iq id="${sent_stanza.getAttribute('id')}" type="set" xmlns="jabber:client">
-                <query xmlns="jabber:iq:roster"><item jid="marty@mcfly.net" name="Marty McFly"></item></query>
+                <query xmlns="jabber:iq:roster"><item jid="marty@mcfly.net" name="Marty McFly"/></query>
             </iq>`);
             </iq>`);
     }));
     }));
 });
 });

+ 6 - 6
src/plugins/rosterview/utils.js

@@ -300,11 +300,11 @@ export function getJIDsAutoCompleteList() {
 /**
 /**
  * @param {string} query
  * @param {string} query
  */
  */
-export async function getNamesAutoCompleteList(query, value_attr='jid') {
+export async function getNamesAutoCompleteList(query) {
     const options = {
     const options = {
-        'mode': /** @type {RequestMode} */ ('cors'),
-        'headers': {
-            'Accept': 'text/json',
+        mode: /** @type {RequestMode} */ ('cors'),
+        headers: {
+            Accept: 'text/json',
         },
         },
     };
     };
     const url = `${api.settings.get('xhr_user_search_url')}q=${encodeURIComponent(query)}`;
     const url = `${api.settings.get('xhr_user_search_url')}q=${encodeURIComponent(query)}`;
@@ -323,7 +323,7 @@ export async function getNamesAutoCompleteList(query, value_attr='jid') {
         return [];
         return [];
     }
     }
     return json.map((i) => ({
     return json.map((i) => ({
-        label: i.fullname || i.jid,
-        value: i[value_attr]
+        label: `${i.fullname} <${i.jid}>`,
+        value: `${i.fullname} <${i.jid}>`
     }));
     }));
 }
 }

+ 1 - 1
src/types/plugins/rosterview/modals/add-contact.d.ts

@@ -17,5 +17,5 @@ export default class AddContactModal extends BaseModal {
      */
      */
     addContactFromForm(ev: Event): Promise<void>;
     addContactFromForm(ev: Event): Promise<void>;
 }
 }
-import BaseModal from 'plugins/modal/modal.js';
+import BaseModal from "plugins/modal/modal.js";
 //# sourceMappingURL=add-contact.d.ts.map
 //# sourceMappingURL=add-contact.d.ts.map

+ 3 - 3
src/types/plugins/rosterview/utils.d.ts

@@ -64,9 +64,9 @@ export function getJIDsAutoCompleteList(): any[];
 /**
 /**
  * @param {string} query
  * @param {string} query
  */
  */
-export function getNamesAutoCompleteList(query: string, value_attr?: string): Promise<{
-    label: any;
-    value: any;
+export function getNamesAutoCompleteList(query: string): Promise<{
+    label: string;
+    value: string;
 }[]>;
 }[]>;
 export type Model = import("@converse/skeletor").Model;
 export type Model = import("@converse/skeletor").Model;
 export type RosterContact = import("@converse/headless").RosterContact;
 export type RosterContact = import("@converse/headless").RosterContact;