Fixes #515 Add support for XEP-0050 Ad-Hoc commands

This commit is contained in:
JC Brand 2020-04-15 13:59:55 +02:00
parent 60b3f7ae25
commit 78b60a3bd9
15 changed files with 365 additions and 111 deletions

View File

@ -6,6 +6,7 @@
configuration settings should now be accessed via `_converse.api.settings.get` and not directly on the `_converse` object.
Soon we'll deprecate the latter, so prepare now.
- #515 Add support for XEP-0050 Ad-Hoc commands
- #1313: Stylistic improvements to the send button
- #1490: Busy-loop when fetching registration form fails
- #1535: Add option to destroy a MUC

View File

@ -200,6 +200,7 @@ body.converse-fullscreen {
.dropdown-item {
padding: 0.5rem 1rem;
.fa {
width: 1.25em;
margin-right: 0.75rem;
}
&:active, &.selected {

View File

@ -0,0 +1,216 @@
import "./autocomplete.js"
import { __ } from '@converse/headless/i18n';
import { CustomElement } from './element.js';
import { api } from "@converse/headless/converse-core";
import { html } from "lit-html";
import { unsafeHTML } from 'lit-html/directives/unsafe-html.js';
import log from "@converse/headless/log";
import sizzle from "sizzle";
const { Strophe, $iq } = window.converse.env;
const u = window.converse.env.utils;
const i18n_hide = __('Hide');
const i18n_choose_service = __('On which entity do you want to run commands?');
const i18n_choose_service_instructions = __(
'Certain XMPP services and entities allow privileged users to execute ad-hoc commands on them.');
const i18n_commands_found = __('Commands found');
const i18n_fetch_commands = __('List available commands');
const i18n_jid_placeholder = __('XMPP Address');
const i18n_no_commands_found = __('No commands found');
const i18n_run = __('Execute');
const tpl_command_form = (o, command) => html`
<form @submit=${o.runCommand}>
${ command.alert ? html`<div class="alert alert-${command.alert_type}" role="alert">${command.alert}</div>` : '' }
<fieldset class="form-group">
<input type="hidden" name="command_node" value="${command.node}"/>
<input type="hidden" name="command_jid" value="${command.jid}"/>
<p class="form-help">${command.instructions}</p>
<!-- Fields are generated internally, with xForm2webForm -->
${ command.fields.map(field => unsafeHTML(field)) }
</fieldset>
<fieldset>
<input type="submit" class="btn btn-primary" value="${i18n_run}">
<input type="button" class="btn btn-secondary button-cancel" value="${i18n_hide}" @click=${o.hideCommandForm}>
</fieldset>
</form>
`;
const tpl_command = (o, command) => html`
<li class="room-item list-group-item">
<div class="available-chatroom d-flex flex-row">
<a class="open-room available-room w-100"
@click=${o.toggleCommandForm}
data-command-node="${command.node}"
data-command-jid="${command.jid}"
data-command-name="${command.name}"
title="${command.name}"
href="#">${command.name || command.jid}</a>
</div>
${ command.node === o.showform ? tpl_command_form(o, command) : '' }
</li>
`;
async function getAutoCompleteList () {
const models = [...(await api.rooms.get()), ...(await api.contacts.get())];
const jids = [...new Set(models.map(o => Strophe.getDomainFromJid(o.get('jid'))))];
return jids;
}
const tpl_adhoc = (o) => html`
<form class="converse-form" @submit=${o.fetchCommands}>
<fieldset class="form-group">
<label>
${i18n_choose_service}
<p class="form-help">${i18n_choose_service_instructions}</p>
<converse-autocomplete
.getAutoCompleteList="${getAutoCompleteList}"
placeholder="${i18n_jid_placeholder}"
name="jid"/>
</label>
</fieldset>
<fieldset class="form-group">
<input type="submit" class="btn btn-primary" value="${i18n_fetch_commands}">
</fieldset>
${ o.view === 'list-commands' ? html`
<fieldset class="form-group">
<ul class="list-group">
<li class="list-group-item active">${ o.commands.length ? i18n_commands_found : i18n_no_commands_found }:</li>
${ o.commands.map(cmd => tpl_command(o, cmd)) }
</ul>
</fieldset>`
: '' }
</form>
`;
async function fetchCommandForm (command) {
const node = command.node;
const jid = command.jid;
const stanza = $iq({
'type': 'set',
'to': jid
}).c('command', {
'xmlns': Strophe.NS.ADHOC,
'node': node,
'action': 'execute'
});
try {
const iq = await api.sendIQ(stanza);
const cmd_el = sizzle(`command[xmlns="${Strophe.NS.ADHOC}"]`, iq).pop();
command.sessionid = cmd_el.getAttribute('sessionid');
command.instructions = sizzle('x[type="form"][xmlns="jabber:x:data"] instructions', cmd_el).pop()?.textContent;
command.fields = sizzle('x[type="form"][xmlns="jabber:x:data"] field', cmd_el)
.map(f => u.xForm2webForm(f, cmd_el));
} catch (e) {
if (e === null) {
log.error(`Error: timeout while trying to execute command for ${jid}`);
} else {
log.error(`Error while trying to execute command for ${jid}`);
log.error(e);
}
command.fields = [];
}
}
export class AdHocCommands extends CustomElement {
static get properties () {
return {
'view': { type: String },
'showform': { type: String },
'nonce': { type: String } // Used to force re-rendering
}
}
constructor () {
super();
this.view = 'choose-service';
this.showform = '';
this.commands = [];
}
render () {
return tpl_adhoc({
'commands': this.commands,
'fetchCommands': ev => this.fetchCommands(ev),
'hideCommandForm': ev => this.hideCommandForm(ev),
'runCommand': ev => this.runCommand(ev),
'showform': this.showform,
'toggleCommandForm': ev => this.toggleCommandForm(ev),
'view': this.view,
});
}
async fetchCommands (ev) {
ev.preventDefault();
const form_data = new FormData(ev.target);
const jid = form_data.get('jid').trim();
if (await api.disco.supports(Strophe.NS.ADHOC, jid)) {
this.commands = await api.adhoc.getCommands(jid);
this.view = 'list-commands';
}
}
async toggleCommandForm (ev) {
ev.preventDefault();
const node = ev.target.getAttribute('data-command-node');
const cmd = this.commands.filter(c => c.node === node)[0];
this.showform !== node && await fetchCommandForm(cmd);
this.showform = node;
}
hideCommandForm (ev) {
ev.preventDefault();
this.showform = ''
}
async runCommand (ev) {
ev.preventDefault();
const form_data = new FormData(ev.target);
const jid = form_data.get('command_jid').trim();
const node = form_data.get('command_node').trim();
const cmd = this.commands.filter(c => c.node === node)[0];
const inputs = sizzle(':input:not([type=button]):not([type=submit])', ev.target);
const configArray = inputs
.filter(i => !['command_jid', 'command_node'].includes(i.getAttribute('name')))
.map(u.webForm2xForm);
const iq = $iq({to: jid, type: "set"})
.c("command", {
'sessionid': cmd.session,
'node': cmd.node,
'xmlns': Strophe.NS.ADHOC
}).c("x", {xmlns: Strophe.NS.XFORM, type: "submit"});
configArray.forEach(node => iq.cnode(node).up());
let result;
try {
result = await api.sendIQ(iq);
} catch (e) {
cmd.alert_type = 'danger';
cmd.alert = __('Sorry, an error occurred while trying to execute the command. See the developer console for details');
log.error('Error while trying to execute an ad-hoc command');
log.error(e);
}
if (result) {
cmd.alert = result.querySelector('note')?.textContent;
} else {
cmd.alert = 'Done';
}
cmd.alert_type = 'primary';
this.nonce = u.getUniqueId();
}
}
window.customElements.define('converse-adhoc-commands', AdHocCommands);

View File

@ -12,6 +12,7 @@ import { debounce, head, isString, isUndefined } from "lodash";
import { BootstrapModal } from "./converse-modal.js";
import { render } from "lit-html";
import { __ } from '@converse/headless/i18n';
import RoomDetailsModal from 'modals/muc-details.js';
import converse from "@converse/headless/converse-core";
import log from "@converse/headless/log";
import st from "@converse/headless/utils/stanza";
@ -19,7 +20,6 @@ import tpl_add_chatroom_modal from "templates/add_chatroom_modal.js";
import tpl_chatroom from "templates/chatroom.js";
import tpl_chatroom_bottom_panel from "templates/chatroom_bottom_panel.html";
import tpl_chatroom_destroyed from "templates/chatroom_destroyed.html";
import tpl_chatroom_details_modal from "templates/chatroom_details_modal.js";
import tpl_chatroom_disconnect from "templates/chatroom_disconnect.html";
import tpl_chatroom_head from "templates/chatroom_head.js";
import tpl_chatroom_nickname_form from "templates/chatroom_nickname_form.html";
@ -610,30 +610,6 @@ converse.plugins.add('converse-muc-views', {
});
_converse.RoomDetailsModal = BootstrapModal.extend({
id: "room-details-modal",
initialize () {
BootstrapModal.prototype.initialize.apply(this, arguments);
this.listenTo(this.model, 'change', this.render);
this.listenTo(this.model.features, 'change', this.render);
this.listenTo(this.model.occupants, 'add', this.render);
this.listenTo(this.model.occupants, 'change', this.render);
},
toHTML () {
return tpl_chatroom_details_modal(Object.assign(
this.model.toJSON(), {
'config': this.model.config.toJSON(),
'display_name': __('Groupchat info for %1$s', this.model.getDisplayName()),
'features': this.model.features.toJSON(),
'num_occupants': this.model.occupants.length,
})
);
}
});
/**
* NativeView which renders a groupchat, based upon
* { @link _converse.ChatBoxView } for normal one-on-one chat boxes.
@ -1080,7 +1056,7 @@ converse.plugins.add('converse-muc-views', {
showRoomDetailsModal (ev) {
ev.preventDefault();
if (this.model.room_details_modal === undefined) {
this.model.room_details_modal = new _converse.RoomDetailsModal({'model': this.model});
this.model.room_details_modal = new RoomDetailsModal({'model': this.model});
}
this.model.room_details_modal.show(ev);
},
@ -1106,20 +1082,21 @@ converse.plugins.add('converse-muc-views', {
/**
* Returns a list of objects which represent buttons for the groupchat header.
* @async
* @emits _converse#getHeadingButtons
* @private
* @method _converse.ChatRoomView#getHeadingButtons
*/
getHeadingButtons (subject_hidden) {
const buttons = [{
const buttons = [];
buttons.push({
'i18n_text': __('Details'),
'i18n_title': __('Show more information about this groupchat'),
'handler': ev => this.showRoomDetailsModal(ev),
'a_class': 'show-room-details-modal',
'icon_class': 'fa-info-circle',
'name': 'details'
}];
});
if (this.model.getOwnAffiliation() === 'owner') {
buttons.push({
'i18n_text': __('Configure'),

View File

@ -7,12 +7,12 @@ import "@converse/headless/converse-status";
import "@converse/headless/converse-vcard";
import "converse-modal";
import { BootstrapModal } from "./converse-modal.js";
import UserSettingsModal from "modals/user-settings";
import bootstrap from "bootstrap.native";
import converse from "@converse/headless/converse-core";
import log from "@converse/headless/log";
import sizzle from 'sizzle';
import tpl_chat_status_modal from "templates/chat_status_modal";
import tpl_client_info_modal from "templates/client_info_modal";
import tpl_profile from "templates/profile.js";
import tpl_profile_modal from "templates/profile_modal";
@ -182,26 +182,11 @@ converse.plugins.add('converse-profile', {
}
});
_converse.ClientInfoModal = BootstrapModal.extend({
id: "converse-client-info-modal",
toHTML () {
return tpl_client_info_modal(
Object.assign(
this.model.toJSON(),
this.model.vcard.toJSON(),
{ 'version_name': _converse.VERSION_NAME }
)
);
}
});
_converse.XMPPStatusView = _converse.ViewWithAvatar.extend({
tagName: "div",
events: {
"click a.show-profile": "showProfileModal",
"click a.change-status": "showStatusChangeModal",
"click .show-client-info": "showClientInfoModal",
"click .logout": "logOut"
},
@ -218,8 +203,9 @@ converse.plugins.add('converse-profile', {
_converse,
chat_status,
'fullname': this.model.vcard.get('fullname') || _converse.bare_jid,
"showUserSettingsModal": ev => this.showUserSettingsModal(ev),
'status_message': this.model.get('status_message') ||
__("I am %1$s", this.getPrettyStatus(chat_status))
__("I am %1$s", this.getPrettyStatus(chat_status)),
}));
},
@ -228,6 +214,7 @@ converse.plugins.add('converse-profile', {
},
showProfileModal (ev) {
ev.preventDefault();
if (this.profile_modal === undefined) {
this.profile_modal = new _converse.ProfileModal({model: this.model});
}
@ -235,17 +222,19 @@ converse.plugins.add('converse-profile', {
},
showStatusChangeModal (ev) {
ev.preventDefault();
if (this.status_modal === undefined) {
this.status_modal = new _converse.ChatStatusModal({model: this.model});
}
this.status_modal.show(ev);
},
showClientInfoModal(ev) {
if (this.client_info_modal === undefined) {
this.client_info_modal = new _converse.ClientInfoModal({model: this.model});
showUserSettingsModal(ev) {
ev.preventDefault();
if (this.user_settings_modal === undefined) {
this.user_settings_modal = new UserSettingsModal({model: this.model, _converse});
}
this.client_info_modal.show(ev);
this.user_settings_modal.show(ev);
},
logOut (ev) {

View File

@ -7,11 +7,13 @@
* @license Mozilla Public License (MPLv2)
*/
import "@converse/headless/converse-muc";
import RoomDetailsModal from 'modals/muc-details.js';
import converse from "@converse/headless/converse-core";
import tpl_rooms_list from "templates/rooms_list.js";
import { Model } from 'skeletor.js/src/model.js';
import { View } from 'skeletor.js/src/view.js';
import { __ } from '@converse/headless/i18n';
import converse from "@converse/headless/converse-core";
import tpl_rooms_list from "templates/rooms_list.js";
const { Strophe } = converse.env;
const u = converse.env.utils;
@ -110,7 +112,7 @@ converse.plugins.add('converse-roomslist', {
const room = _converse.chatboxes.get(jid);
ev.preventDefault();
if (room.room_details_modal === undefined) {
room.room_details_modal = new _converse.RoomDetailsModal({'model': room});
room.room_details_modal = new RoomDetailsModal({'model': room});
}
room.room_details_modal.show(ev);
},

View File

@ -4,7 +4,7 @@
* @license Mozilla Public License (MPLv2)
*/
import { __, i18n } from './i18n';
import { assignIn, debounce, invoke, isFunction, isObject, isString, pick } from 'lodash';
import { assignIn, debounce, invoke, isElement, isFunction, isObject, isString, pick } from 'lodash';
import { Collection } from "skeletor.js/src/collection";
import { Events } from 'skeletor.js/src/events.js';
import { Model } from 'skeletor.js/src/model.js';
@ -97,6 +97,7 @@ const PROMISES = [
// These are just the @converse/headless plugins, for the full converse,
// the other plugins are whitelisted in src/converse.js
const CORE_PLUGINS = [
'converse-adhoc',
'converse-bookmarks',
'converse-bosh',
'converse-caps',
@ -307,7 +308,7 @@ function initUserSettings () {
* @namespace _converse.api
* @memberOf _converse
*/
const api = _converse.api = {
export const api = _converse.api = {
/**
* This grouping collects API functions related to the XMPP connection.
*
@ -887,7 +888,8 @@ const api = _converse.api = {
promise = new Promise((resolve, reject) => _converse.connection.sendIQ(stanza, resolve, reject, timeout));
promise.catch(e => {
if (e === null) {
throw new TimeoutError(`Timeout error after ${timeout}ms for the following IQ stanza: ${stanza}`);
const el = isElement(stanza) ? stanza : stanza.nodeTree;
throw new TimeoutError(`Timeout error after ${timeout}ms for the following IQ stanza: ${el}`);
}
});
} else {

View File

@ -2,6 +2,7 @@
* --------------------
* Any of the following components may be removed if they're not needed.
*/
import "./converse-adhoc"; // XEP-0050 Ad Hoc Commands
import "./converse-bookmarks"; // XEP-0199 XMPP Ping
import "./converse-bosh"; // XEP-0206 BOSH
import "./converse-caps"; // XEP-0115 Entity Capabilities

View File

@ -1,9 +1,12 @@
import { BootstrapModal } from "../converse-modal.js";
import { __ } from '@converse/headless/i18n';
import { api } from "@converse/headless/converse-core";
import log from "@converse/headless/log";
import sizzle from "sizzle";
import tpl_muc_commands_modal from "../templates/muc_commands_modal.js";
const { Strophe } = window.converse.env;
const { Strophe, $iq } = window.converse.env;
const u = window.converse.env.utils;
export default BootstrapModal.extend({
@ -19,8 +22,9 @@ export default BootstrapModal.extend({
toHTML () {
return tpl_muc_commands_modal(Object.assign(
this.model.toJSON(), {
'commands': this.commands,
'display_name': __('Ad-hoc commands for %1$s', this.model.getDisplayName()),
'commands': this.commands
'toggleCommandForm': ev => this.toggleCommandForm(ev)
})
);
},
@ -28,5 +32,59 @@ export default BootstrapModal.extend({
async getCommands () {
this.commands = await api.adhoc.getCommands(Strophe.getDomainFromJid(this.model.get('jid')));
this.render();
},
async toggleCommandForm (ev) {
ev.preventDefault();
const node = ev.target.getAttribute('data-command-node');
this.commands.filter(c => (c.node !== node)).forEach(c => (c.show_form = false));
const cmd = this.commands.filter(c => c.node === node)[0];
cmd.show_form = !cmd.show_form;
cmd.show_form && await this.fetchCommandForm(cmd);
this.render();
},
async fetchCommandForm (command) {
const node = command.node;
const jid = command.jid;
const stanza = $iq({
'type': 'set',
'to': jid
}).c('command', {
'xmlns': Strophe.NS.ADHOC,
'node': node,
'action': 'execute'
});
command.fields;
try {
const iq = await api.sendIQ(stanza);
command.fields = sizzle('field', iq).map(f => u.xForm2webForm(f, iq))
} catch (e) {
if (e === null) {
log.error(`Error: timeout while trying to execute command for ${jid}`);
} else {
log.error(`Error while trying to execute command for ${jid}`);
log.error(e);
}
command.fields = [];
}
/*
<iq xmlns="jabber:client" id="72c21b57-5e9f-4b63-9e53-c6e69ed3337e:sendIQ" type="result" from="conference.chat.example.org" to="arzu.horsten@chat.example.org/converse.js-138545405">
<command xmlns="http://jabber.org/protocol/commands" node="http://prosody.im/protocol/hats#add" sessionid="141a571b-37e2-4891-824f-72ca4b64806f" status="executing">
<x xmlns="jabber:x:data" type="form">
<title>Add a hat</title>
<instructions>Assign a hat to a room member</instructions>
<field label="User JID" type="jid-single" var="user"><required/></field>
<field label="Room JID" type="jid-single" var="room"><required/></field>
<field label="Hat title" type="text-single" var="title"/>
<field label="Hat URI" type="text-single" var="uri"><required/></field>
</x>
<actions execute="complete"><next/><complete/></actions>
</command>
</iq>
*/
}
});

View File

@ -0,0 +1,24 @@
import { BootstrapModal } from "../converse-modal.js";
import tpl_client_info_modal from "templates/client_info_modal";
let _converse;
export default BootstrapModal.extend({
id: "converse-client-info-modal",
initialize (settings) {
_converse = settings._converse;
BootstrapModal.prototype.initialize.apply(this, arguments);
},
toHTML () {
return tpl_client_info_modal(
Object.assign(
this.model.toJSON(),
this.model.vcard.toJSON(),
{ 'version_name': _converse.VERSION_NAME }
)
);
}
});

View File

@ -2,10 +2,13 @@ import { __ } from '@converse/headless/i18n';
import { html } from "lit-html";
import { modal_header_close_button } from "./buttons"
import { unsafeHTML } from 'lit-html/directives/unsafe-html.js';
import '../components/adhoc-commands.js';
import xss from "xss/dist/xss";
const modal_title = __('About');
const i18n_modal_title = __('Settings');
const i18n_about = __('About');
const i18n_commands = __('Commands');
const first_subtitle = __(
'%1$s Open Source %2$s XMPP chat client brought to you by %3$s Opkode %2$s',
@ -20,21 +23,43 @@ const second_subtitle = __(
'</a>'
);
const tpl_navigation = (o) => html`
<ul class="nav nav-pills justify-content-center">
<li role="presentation" class="nav-item">
<a class="nav-link active" id="about-tab" href="#about-tabpanel" aria-controls="about-tabpanel" role="tab" data-toggle="tab" @click=${o.switchTab}>${i18n_about}</a>
</li>
<li role="presentation" class="nav-item">
<a class="nav-link" id="commands-tab" href="#commands-tabpanel" aria-controls="commands-tabpanel" role="tab" data-toggle="tab" @click=${o.switchTab}>${i18n_commands}</a>
</li>
</ul>
`;
export default (o) => html`
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="changeStatusModalLabel">${modal_title}</h5>
<h5 class="modal-title" id="converse-modtools-modal-label">${i18n_modal_title}</h5>
${modal_header_close_button}
</div>
<div class="modal-body">
<span class="modal-alert"></span>
<div class="container brand-heading-container">
<h6 class="brand-heading">Converse</h6>
<p class="brand-subtitle">${o.version_name}</p>
<p class="brand-subtitle">${unsafeHTML(xss.filterXSS(first_subtitle, {'whiteList': {'a': []}}))}</p>
<p class="brand-subtitle">${unsafeHTML(xss.filterXSS(second_subtitle, {'whiteList': {'a': []}}))}</p>
${ tpl_navigation(o) }
<div class="tab-content">
<div class="tab-pane tab-pane--columns active" id="about-tabpanel" role="tabpanel" aria-labelledby="about-tab">
<span class="modal-alert"></span>
<br/>
<div class="container brand-heading-container">
<h6 class="brand-heading">Converse</h6>
<p class="brand-subtitle">${o.version_name}</p>
<p class="brand-subtitle">${unsafeHTML(xss.filterXSS(first_subtitle, {'whiteList': {'a': []}}))}</p>
<p class="brand-subtitle">${unsafeHTML(xss.filterXSS(second_subtitle, {'whiteList': {'a': []}}))}</p>
</div>
</div>
<div class="tab-pane tab-pane--columns" id="commands-tabpanel" role="tabpanel" aria-labelledby="commands-tab">
<converse-adhoc-commands/>
</div>
</div>
</div>
</div>

View File

@ -1,41 +0,0 @@
import { __ } from '@converse/headless/i18n';
import { html } from "lit-html";
import { modal_close_button, modal_header_close_button } from "./buttons"
import { repeat } from 'lit-html/directives/repeat.js';
const i18n_commands_found = __('Commands found');
const i18n_no_commands_found = __('No commands found');
const tpl_command = (o, command) => html`
<li class="room-item list-group-item">
<div class="available-chatroom d-flex flex-row">
<a class="open-room available-room w-100"
@click=${o.openRoom}
data-command-node="${command.node}"
data-command-jid="${command.jid}"
data-command-name="${command.name}"
title="${command.name}"
href="#">${command.name || command.jid}</a>
</div>
</li>
`;
export default (o) => html`
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="room-details-modal-label">${o.display_name}</h5>
${modal_header_close_button}
</div>
<div class="modal-body">
<ul class="list-group">
<li class="list-group-item active">${ o.commands.length ? i18n_commands_found : i18n_no_commands_found }:</li>
${repeat(o.commands, item => item.jid, item => tpl_command(o, item))}
</ul>
</div>
<div class="modal-footer">${modal_close_button}</div>
</div>
</div>
`;

View File

@ -14,7 +14,7 @@ export default (o) => html`
<canvas class="avatar align-self-center" height="40" width="40"></canvas>
</a>
<span class="username w-100 align-self-center">${o.fullname}</span>
${o._converse.api.settings.get('show_client_info') ? html`<a class="controlbox-heading__btn show-client-info fa fa-info-circle align-self-center" title="${i18n_details}"></a>` : ''}
${o._converse.api.settings.get('show_client_info') ? html`<a class="controlbox-heading__btn show-client-info fa fa-cog align-self-center" title="${i18n_details}" @click=${o.showUserSettingsModal}></a>` : ''}
${o._converse.api.settings.get('allow_logout') ? html`<a class="controlbox-heading__btn logout fa fa-sign-out-alt align-self-center" title="${i18n_logout}"></a>` : ''}
</div>
<div class="d-flex xmpp-status">

View File

@ -26,7 +26,7 @@ const URL_REGEX = /\b(https?\:\/\/|www\.|https?:\/\/www\.)[^\s<>]{2,200}\b\/?/g;
function getAutoCompleteProperty (name, options) {
return {
'muc#roomconfig_lang': 'language',
'muc#roomconfig_roomsecret': options.new_password ? 'new-password' : 'current-password'
'muc#roomconfig_roomsecret': options?.new_password ? 'new-password' : 'current-password'
}[name];
}
@ -660,7 +660,7 @@ u.xForm2webForm = function (field, stanza, options) {
'id': u.getUniqueId(),
'label': field.getAttribute('label') || '',
'name': name,
'fixed_username': options.fixed_username,
'fixed_username': options?.fixed_username,
'autocomplete': getAutoCompleteProperty(name, options),
'placeholder': null,
'required': !!field.querySelector('required'),

View File

@ -28,7 +28,6 @@
enable_smacks: true,
i18n: 'en',
message_archiving: 'always',
persistent_store: 'IndexedDB',
muc_domain: 'conference.chat.example.org',
muc_respect_autojoin: true,
view_mode: 'fullscreen',