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`.
This commit is contained in:
parent
4237e5b3ae
commit
320f11f795
66
src/plugins/muc-views/search.js
Normal file
66
src/plugins/muc-views/search.js
Normal file
@ -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];
|
||||||
|
}
|
||||||
|
|
@ -22,7 +22,8 @@ export default (o) => {
|
|||||||
<converse-autocomplete
|
<converse-autocomplete
|
||||||
.getAutoCompleteList="${getAutoCompleteList}"
|
.getAutoCompleteList="${getAutoCompleteList}"
|
||||||
placeholder="${i18n_jid_placeholder}"
|
placeholder="${i18n_jid_placeholder}"
|
||||||
name="jid"></converse-autocomplete>
|
name="jid">
|
||||||
|
</converse-autocomplete>
|
||||||
</label>
|
</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset class="form-group">
|
<fieldset class="form-group">
|
||||||
|
@ -4,7 +4,7 @@ import { api } from '@converse/headless/core.js';
|
|||||||
import { html } from "lit";
|
import { html } from "lit";
|
||||||
import { modal_header_close_button } from "plugins/modal/templates/buttons.js"
|
import { modal_header_close_button } from "plugins/modal/templates/buttons.js"
|
||||||
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
||||||
import { getAutoCompleteList } from "../utils.js";
|
import { getAutoCompleteList } from "../search.js";
|
||||||
|
|
||||||
|
|
||||||
const nickname_input = (o) => {
|
const nickname_input = (o) => {
|
||||||
@ -38,6 +38,7 @@ export default (o) => {
|
|||||||
<converse-autocomplete
|
<converse-autocomplete
|
||||||
.getAutoCompleteList="${getAutoCompleteList}"
|
.getAutoCompleteList="${getAutoCompleteList}"
|
||||||
?autofocus=${true}
|
?autofocus=${true}
|
||||||
|
min_chars="3"
|
||||||
position="below"
|
position="below"
|
||||||
placeholder="${o.chatroom_placeholder}"
|
placeholder="${o.chatroom_placeholder}"
|
||||||
class="add-muc-autocomplete"
|
class="add-muc-autocomplete"
|
||||||
|
@ -258,7 +258,7 @@ describe("The nickname autocomplete feature", function () {
|
|||||||
'preventDefault': function preventDefault () {},
|
'preventDefault': function preventDefault () {},
|
||||||
'keyCode': 8
|
'keyCode': 8
|
||||||
}
|
}
|
||||||
for (var i=0; i<3; i++) {
|
for (let i=0; i<3; i++) {
|
||||||
// Press backspace 3 times to remove "som"
|
// Press backspace 3 times to remove "som"
|
||||||
message_form.onKeyDown(backspace_event);
|
message_form.onKeyDown(backspace_event);
|
||||||
textarea.value = textarea.value.slice(0, textarea.value.length-1)
|
textarea.value = textarea.value.slice(0, textarea.value.length-1)
|
||||||
|
@ -127,22 +127,10 @@ export function getAutoCompleteListItem (text, input) {
|
|||||||
return element;
|
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 () {
|
export async function getAutoCompleteList () {
|
||||||
if (!timestamp || converse.env.dayjs().isAfter(timestamp, 'day')) {
|
const models = [...(await api.rooms.get()), ...(await api.contacts.get())];
|
||||||
await fetchListOfRooms();
|
const jids = [...new Set(models.map(o => Strophe.getDomainFromJid(o.get('jid'))))];
|
||||||
timestamp = (new Date()).toISOString();
|
return jids;
|
||||||
}
|
|
||||||
return fetched_room_jids;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchCommandForm (command) {
|
export async function fetchCommandForm (command) {
|
||||||
|
@ -64,7 +64,7 @@ export class AutoComplete {
|
|||||||
"blur": () => this.close({'reason': 'blur'})
|
"blur": () => this.close({'reason': 'blur'})
|
||||||
}
|
}
|
||||||
if (this.auto_evaluate) {
|
if (this.auto_evaluate) {
|
||||||
input["input"] = () => this.evaluate();
|
input["input"] = (e) => this.evaluate(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._events = {
|
this._events = {
|
||||||
@ -265,25 +265,27 @@ export class AutoComplete {
|
|||||||
return;
|
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;
|
let value = this.match_current_word ? u.getCurrentWord(this.input) : this.input.value;
|
||||||
|
|
||||||
const contains_trigger = helpers.isMention(value, this.ac_triggers);
|
const contains_trigger = helpers.isMention(value, this.ac_triggers);
|
||||||
if (contains_trigger) {
|
if (contains_trigger && !this.include_triggers.includes(ev.key)) {
|
||||||
this.auto_completing = true;
|
|
||||||
if (!this.include_triggers.includes(ev.key)) {
|
|
||||||
value = u.isMentionBoundary(value[0])
|
value = u.isMentionBoundary(value[0])
|
||||||
? value.slice('2')
|
? value.slice('2')
|
||||||
: value.slice('1');
|
: value.slice('1');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((contains_trigger || value.length) && value.length >= this.min_chars) {
|
|
||||||
this.index = -1;
|
this.index = -1;
|
||||||
// Populate list with options that match
|
|
||||||
this.ul.innerHTML = "";
|
this.ul.innerHTML = "";
|
||||||
|
|
||||||
this.suggestions = list
|
this.suggestions = list
|
||||||
|
@ -100,7 +100,7 @@ export default class AutoCompleteComponent extends CustomElement {
|
|||||||
'auto_first': this.auto_first,
|
'auto_first': this.auto_first,
|
||||||
'filter': this.filter == 'contains' ? FILTER_CONTAINS : FILTER_STARTSWITH,
|
'filter': this.filter == 'contains' ? FILTER_CONTAINS : FILTER_STARTSWITH,
|
||||||
'include_triggers': [],
|
'include_triggers': [],
|
||||||
'list': () => this.getAutoCompleteList(),
|
'list': (q) => this.getAutoCompleteList(q),
|
||||||
'match_current_word': true,
|
'match_current_word': true,
|
||||||
'max_items': this.max_items,
|
'max_items': this.max_items,
|
||||||
'min_chars': this.min_chars,
|
'min_chars': this.min_chars,
|
||||||
|
Loading…
Reference in New Issue
Block a user