diff --git a/src/plugins/muc-views/search.js b/src/plugins/muc-views/search.js new file mode 100644 index 000000000..fda65537f --- /dev/null +++ b/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]; +} + diff --git a/src/plugins/muc-views/templates/ad-hoc.js b/src/plugins/muc-views/templates/ad-hoc.js index ecfd97108..a6a1c695c 100644 --- a/src/plugins/muc-views/templates/ad-hoc.js +++ b/src/plugins/muc-views/templates/ad-hoc.js @@ -22,7 +22,8 @@ export default (o) => { + name="jid"> +
diff --git a/src/plugins/muc-views/templates/add-muc.js b/src/plugins/muc-views/templates/add-muc.js index b7a289d5d..822f79d15 100644 --- a/src/plugins/muc-views/templates/add-muc.js +++ b/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) => { 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) { diff --git a/src/shared/autocomplete/autocomplete.js b/src/shared/autocomplete/autocomplete.js index 801f75e35..4241725f2 100644 --- a/src/shared/autocomplete/autocomplete.js +++ b/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) { - this.auto_completing = true; - if (!this.include_triggers.includes(ev.key)) { - value = u.isMentionBoundary(value[0]) - ? value.slice('2') - : value.slice('1'); - } + if (contains_trigger && !this.include_triggers.includes(ev.key)) { + value = u.isMentionBoundary(value[0]) + ? value.slice('2') + : value.slice('1'); } - if ((contains_trigger || value.length) && value.length >= this.min_chars) { + const is_long_enough = value.length && value.length >= this.min_chars; + + if (contains_trigger || is_long_enough) { + this.auto_completing = true; + + const list = typeof this._list === "function" ? await this._list(value) : this._list; + if (list.length === 0 || !this.auto_completing) { + this.close({'reason': 'nomatches'}); + return; + } + this.index = -1; - // Populate list with options that match this.ul.innerHTML = ""; this.suggestions = list diff --git a/src/shared/autocomplete/component.js b/src/shared/autocomplete/component.js index acb8ef8de..2366824ef 100644 --- a/src/shared/autocomplete/component.js +++ b/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,