Browse Source

Use XMPP to search for MUCs via search.jabber.network

Also refactor AutoComplete somewhat to not compute `this._list` too
eagerly and to also pass the query string to `this._list`.
JC Brand 3 years ago
parent
commit
320f11f795

+ 66 - 0
src/plugins/muc-views/search.js

@@ -0,0 +1,66 @@
+import log from "@converse/headless/log";
+import { _converse, api, converse } from "@converse/headless/core";
+
+const { Strophe, $iq, sizzle } = converse.env;
+
+Strophe.addNamespace('MUCSEARCH', 'https://xmlns.zombofant.net/muclumbus/search/1.0');
+
+const rooms_cache = {};
+
+async function searchRooms (query) {
+    let iq = $iq({
+        'type': 'get',
+        'from': _converse.bare_jid,
+        'to': 'api@search.jabber.network'
+    }).c('search', { 'xmlns': Strophe.NS.MUCSEARCH })
+
+    try {
+        await api.sendIQ(iq);
+    } catch (e) {
+        log.error(e);
+        return [];
+    }
+
+    iq = $iq({
+        'type': 'get',
+        'from': _converse.bare_jid,
+        'to': 'api@search.jabber.network'
+    }).c('search', { 'xmlns': Strophe.NS.MUCSEARCH })
+        .c('set', { 'xmlns': Strophe.NS.RSM })
+            .c('max').t(10).up().up()
+        .c('x', { 'xmlns': Strophe.NS.XFORM, 'type': 'submit' })
+            .c('field', { 'var': 'FORM_TYPE', 'type': 'hidden' })
+                .c('value').t('https://xmlns.zombofant.net/muclumbus/search/1.0#params').up().up()
+            .c('field', { 'var': 'q', 'type': 'text-single' })
+                .c('value').t(query).up().up()
+            .c('field', { 'var': 'sinname', 'type': 'boolean' })
+                .c('value').t('true').up().up()
+            .c('field', { 'var': 'sindescription', 'type': 'boolean' })
+                .c('value').t('false').up().up()
+            .c('field', { 'var': 'sinaddr', 'type': 'boolean' })
+                .c('value').t('true').up().up()
+            .c('field', { 'var': 'min_users', 'type': 'text-single' })
+                .c('value').t('1').up().up()
+            .c('field', { 'var': 'key', 'type': 'list-single' })
+                .c('value').t('address').up()
+                .c('option').c('value').t('nusers').up().up()
+                .c('option').c('value').t('address')
+
+    let iq_result;
+    try {
+        iq_result = await api.sendIQ(iq);
+    } catch (e) {
+        log.error(e);
+        return [];
+    }
+    const s = `result[xmlns="${Strophe.NS.MUCSEARCH}"] item`;
+    return sizzle(s, iq_result).map(i => `${i.querySelector('name')?.textContent} (${i.getAttribute('address')})`);
+}
+
+export function getAutoCompleteList (query) {
+    if (!rooms_cache[query]) {
+        rooms_cache[query] = searchRooms(query);
+    }
+    return rooms_cache[query];
+}
+

+ 2 - 1
src/plugins/muc-views/templates/ad-hoc.js

@@ -22,7 +22,8 @@ export default (o) => {
                     <converse-autocomplete
                         .getAutoCompleteList="${getAutoCompleteList}"
                         placeholder="${i18n_jid_placeholder}"
-                        name="jid"></converse-autocomplete>
+                        name="jid">
+                    </converse-autocomplete>
                 </label>
             </fieldset>
             <fieldset class="form-group">

+ 2 - 1
src/plugins/muc-views/templates/add-muc.js

@@ -4,7 +4,7 @@ import { api } from '@converse/headless/core.js';
 import { html } from "lit";
 import { modal_header_close_button } from "plugins/modal/templates/buttons.js"
 import { unsafeHTML } from "lit/directives/unsafe-html.js";
-import { getAutoCompleteList } from "../utils.js";
+import { getAutoCompleteList } from "../search.js";
 
 
 const nickname_input = (o) => {
@@ -38,6 +38,7 @@ export default (o) => {
                             <converse-autocomplete
                                 .getAutoCompleteList="${getAutoCompleteList}"
                                 ?autofocus=${true}
+                                min_chars="3"
                                 position="below"
                                 placeholder="${o.chatroom_placeholder}"
                                 class="add-muc-autocomplete"

+ 1 - 1
src/plugins/muc-views/tests/autocomplete.js

@@ -258,7 +258,7 @@ describe("The nickname autocomplete feature", function () {
             'preventDefault': function preventDefault () {},
             'keyCode': 8
         }
-        for (var i=0; i<3; i++) {
+        for (let i=0; i<3; i++) {
             // Press backspace 3 times to remove "som"
             message_form.onKeyDown(backspace_event);
             textarea.value = textarea.value.slice(0, textarea.value.length-1)

+ 3 - 15
src/plugins/muc-views/utils.js

@@ -127,22 +127,10 @@ export function getAutoCompleteListItem (text, input) {
     return element;
 }
 
-let fetched_room_jids = [];
-let timestamp = null;
-
-async function fetchListOfRooms () {
-    const response = await fetch('https://search.jabber.network/api/1.0/rooms');
-    const data = await response.json();
-    const popular_mucs = data.items.map(item => item.address);
-    fetched_room_jids = [...new Set(popular_mucs)];
-}
-
 export async function getAutoCompleteList () {
-    if (!timestamp || converse.env.dayjs().isAfter(timestamp, 'day')) {
-        await fetchListOfRooms();
-        timestamp = (new Date()).toISOString();
-    }
-    return fetched_room_jids;
+    const models = [...(await api.rooms.get()), ...(await api.contacts.get())];
+    const jids = [...new Set(models.map(o => Strophe.getDomainFromJid(o.get('jid'))))];
+    return jids;
 }
 
 export async function fetchCommandForm (command) {

+ 16 - 14
src/shared/autocomplete/autocomplete.js

@@ -64,7 +64,7 @@ export class AutoComplete {
             "blur": () => this.close({'reason': 'blur'})
         }
         if (this.auto_evaluate) {
-            input["input"] = () => this.evaluate();
+            input["input"] = (e) => this.evaluate(e);
         }
 
         this._events = {
@@ -265,25 +265,27 @@ export class AutoComplete {
             return;
         }
 
-        const list = typeof this._list === "function" ? await this._list() : this._list;
-        if (list.length === 0) {
-            return;
-        }
-
         let value = this.match_current_word ? u.getCurrentWord(this.input) : this.input.value;
+
         const contains_trigger = helpers.isMention(value, this.ac_triggers);
-        if (contains_trigger) {
+        if (contains_trigger && !this.include_triggers.includes(ev.key)) {
+            value = u.isMentionBoundary(value[0])
+                ? value.slice('2')
+                : value.slice('1');
+        }
+
+        const is_long_enough = value.length && value.length >= this.min_chars;
+
+        if (contains_trigger || is_long_enough) {
             this.auto_completing = true;
-            if (!this.include_triggers.includes(ev.key)) {
-                value = u.isMentionBoundary(value[0])
-                    ? value.slice('2')
-                    : value.slice('1');
+
+            const list = typeof this._list === "function" ? await this._list(value) : this._list;
+            if (list.length === 0 || !this.auto_completing) {
+                this.close({'reason': 'nomatches'});
+                return;
             }
-        }
 
-        if ((contains_trigger || value.length) && value.length >= this.min_chars) {
             this.index = -1;
-            // Populate list with options that match
             this.ul.innerHTML = "";
 
             this.suggestions = list

+ 1 - 1
src/shared/autocomplete/component.js

@@ -100,7 +100,7 @@ export default class AutoCompleteComponent extends CustomElement {
             'auto_first': this.auto_first,
             'filter': this.filter == 'contains' ? FILTER_CONTAINS : FILTER_STARTSWITH,
             'include_triggers': [],
-            'list': () => this.getAutoCompleteList(),
+            'list': (q) => this.getAutoCompleteList(q),
             'match_current_word': true,
             'max_items': this.max_items,
             'min_chars': this.min_chars,