Move modals and their templates into ./modals/

This commit is contained in:
JC Brand 2020-12-01 18:50:35 +01:00
parent 34cba68432
commit b18cc6bcc5
38 changed files with 654 additions and 606 deletions

17
package-lock.json generated
View File

@ -3144,7 +3144,8 @@
"dependencies": { "dependencies": {
"filesize": { "filesize": {
"version": "6.1.0", "version": "6.1.0",
"resolved": false "resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz",
"integrity": "sha512-LpCHtPQ3sFx67z+uh2HnSyWSLLu5Jxo21795uRDuar/EOuYWXib5EmPaGIBuSnRqH2IODiKA2k5re/K9OnN/Yg=="
}, },
"fs-extra": { "fs-extra": {
"version": "8.1.0", "version": "8.1.0",
@ -3200,20 +3201,22 @@
}, },
"localforage": { "localforage": {
"version": "1.7.3", "version": "1.7.3",
"resolved": false, "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.7.3.tgz",
"integrity": "sha512-1TulyYfc4udS7ECSBT2vwJksWbkwwTX8BzeUIiq8Y07Riy7bDAAnxDaPU/tWyOVmQAcWJIEIFP9lPfBGqVoPgQ==",
"requires": { "requires": {
"lie": "3.1.1" "lie": "3.1.1"
} }
}, },
"pluggable.js": { "pluggable.js": {
"version": "2.0.1", "version": "2.0.1",
"resolved": false, "resolved": "https://registry.npmjs.org/pluggable.js/-/pluggable.js-2.0.1.tgz",
"integrity": "sha512-SBt6v6Tbp20Jf8hU0cpcc/+HBHGMY8/Q+yA6Ih0tBQE8tfdZ6U4PRG0iNvUUjLx/hVyOP53n0UfGBymlfaaXCg==",
"requires": { "requires": {
"lodash": "^4.17.11" "lodash": "^4.17.11"
} }
}, },
"skeletor.js": { "skeletor.js": {
"version": "0.0.1", "version": "github:skeletorjs/skeletor#bf6d9c86f9fcf224fa9d9af5a25380b77aa4b561",
"from": "github:skeletorjs/skeletor#bf6d9c86f9fcf224fa9d9af5a25380b77aa4b561", "from": "github:skeletorjs/skeletor#bf6d9c86f9fcf224fa9d9af5a25380b77aa4b561",
"requires": { "requires": {
"lodash": "^4.17.14" "lodash": "^4.17.14"
@ -3221,7 +3224,11 @@
}, },
"strophe.js": { "strophe.js": {
"version": "github:strophe/strophejs#c4a94e59877c06dc2395f4ccbd26f3fee67a4c9f", "version": "github:strophe/strophejs#c4a94e59877c06dc2395f4ccbd26f3fee67a4c9f",
"from": "strophe.js@github:strophe/strophejs#c4a94e59877c06dc2395f4ccbd26f3fee67a4c9f" "from": "strophe.js@github:strophe/strophejs#c4a94e59877c06dc2395f4ccbd26f3fee67a4c9f",
"requires": {
"abab": "^2.0.3",
"xmldom": "^0.1.27"
}
}, },
"twemoji": { "twemoji": {
"version": "12.1.5", "version": "12.1.5",

View File

@ -1,4 +1,4 @@
/*global mock */ /*global mock, converse */
const u = converse.env.utils; const u = converse.env.utils;
@ -53,6 +53,7 @@ describe("The User Details Modal", function () {
let remove_contact_button = modal.el.querySelector('button.remove-contact'); let remove_contact_button = modal.el.querySelector('button.remove-contact');
expect(u.isVisible(remove_contact_button)).toBeTruthy(); expect(u.isVisible(remove_contact_button)).toBeTruthy();
remove_contact_button.click(); remove_contact_button.click();
await u.waitUntil(() => u.isVisible(document.querySelector('.alert-danger')), 2000); await u.waitUntil(() => u.isVisible(document.querySelector('.alert-danger')), 2000);
const header = document.querySelector('.alert-danger .modal-title'); const header = document.querySelector('.alert-danger .modal-title');

View File

@ -3,174 +3,19 @@
* @copyright The Converse.js contributors * @copyright The Converse.js contributors
* @license Mozilla Public License (MPLv2) * @license Mozilla Public License (MPLv2)
*/ */
import { View } from '@converse/skeletor/src/view.js'; import Alert from './modals/alert.js';
import BootstrapModal from './modals/base.js';
import Confirm from './modals/confirm.js';
import { Model } from '@converse/skeletor/src/model.js'; import { Model } from '@converse/skeletor/src/model.js';
import { render } from 'lit-html'; import { _converse, converse } from "@converse/headless/converse-core";
import { __ } from './i18n';
import bootstrap from "bootstrap.native";
import { converse } from "@converse/headless/converse-core";
import log from "@converse/headless/log";
import tpl_alert_component from "templates/alert.js";
import tpl_alert_modal from "templates/alert_modal.js";
import tpl_prompt from "templates/prompt.js";
const { sizzle } = converse.env;
const u = converse.env.utils;
let _converse;
export const BootstrapModal = View.extend({
className: "modal",
events: {
'click .nav-item .nav-link': 'switchTab'
},
initialize () {
this.render()
this.el.setAttribute('tabindex', '-1');
this.el.setAttribute('role', 'dialog');
this.el.setAttribute('aria-hidden', 'true');
const label_id = this.el.querySelector('.modal-title').getAttribute('id');
label_id && this.el.setAttribute('aria-labelledby', label_id);
this.insertIntoDOM();
const Modal = bootstrap.Modal;
this.modal = new Modal(this.el, {
backdrop: true,
keyboard: true
});
this.el.addEventListener('hide.bs.modal', () => u.removeClass('selected', this.trigger_el), false);
},
insertIntoDOM () {
const container_el = _converse.chatboxviews.el.querySelector("#converse-modals");
container_el.insertAdjacentElement('beforeEnd', this.el);
},
switchTab (ev) {
ev.stopPropagation();
ev.preventDefault();
sizzle('.nav-link.active', this.el).forEach(el => {
u.removeClass('active', this.el.querySelector(el.getAttribute('href')));
u.removeClass('active', el);
});
u.addClass('active', ev.target);
u.addClass('active', this.el.querySelector(ev.target.getAttribute('href')))
},
alert (message, type='primary') {
const body = this.el.querySelector('.modal-alert');
if (body === null) {
log.error("Could not find a .modal-alert element in the modal to show an alert message in!");
return;
}
// FIXME: Instead of adding the alert imperatively, we should
// find a way to let the modal rerender with an alert message
render(tpl_alert_component({'type': `alert-${type}`, 'message': message}), body);
const el = body.firstElementChild;
setTimeout(() => {
u.addClass('fade-out', el);
setTimeout(() => u.removeElement(el), 600);
}, 5000);
},
show (ev) {
if (ev) {
ev.preventDefault();
this.trigger_el = ev.target;
this.trigger_el.classList.add('selected');
}
this.modal.show();
}
});
converse.env.BootstrapModal = BootstrapModal; // expose to plugins converse.env.BootstrapModal = BootstrapModal; // expose to plugins
export const Confirm = BootstrapModal.extend({
events: {
'submit .confirm': 'onConfimation'
},
initialize () { let alert;
this.confirmation = u.getResolveablePromise();
BootstrapModal.prototype.initialize.apply(this, arguments);
this.listenTo(this.model, 'change', this.render)
this.el.addEventListener('closed.bs.modal', () => this.confirmation.reject(), false);
},
toHTML () { const modal_api = {
return tpl_prompt(this.model.toJSON());
},
afterRender () {
if (!this.close_handler_registered) {
this.el.addEventListener('closed.bs.modal', () => {
if (!this.confirmation.isResolved) {
this.confirmation.reject()
}
}, false);
this.close_handler_registered = true;
}
},
onConfimation (ev) {
ev.preventDefault();
const form_data = new FormData(ev.target);
const fields = (this.model.get('fields') || [])
.map(field => {
const value = form_data.get(field.name).trim();
field.value = value;
if (field.challenge) {
field.challenge_failed = (value !== field.challenge);
}
return field;
});
if (fields.filter(c => c.challenge_failed).length) {
this.model.set('fields', fields);
// Setting an array doesn't trigger a change event
this.model.trigger('change');
return;
}
this.confirmation.resolve(fields);
this.modal.hide();
}
});
export const Alert = BootstrapModal.extend({
initialize () {
BootstrapModal.prototype.initialize.apply(this, arguments);
this.listenTo(this.model, 'change', this.render)
},
toHTML () {
return tpl_alert_modal(Object.assign({__}, this.model.toJSON()));
}
});
converse.plugins.add('converse-modal', {
initialize () {
_converse = this._converse
/************************ BEGIN Event Listeners ************************/
_converse.api.listen.on('disconnect', () => {
const container = document.querySelector("#converse-modals");
if (container) {
container.innerHTML = '';
}
});
/************************ BEGIN API ************************/
// We extend the default converse.js API to add methods specific to MUC chat rooms.
let alert;
Object.assign(_converse.api, {
/** /**
* Show a confirm modal to the user. * Show a confirm modal to the user.
* @method _converse.api.confirm * @method _converse.api.confirm
@ -273,7 +118,18 @@ converse.plugins.add('converse-modal', {
} }
alert.show(); alert.show();
} }
}
converse.plugins.add('converse-modal', {
initialize () {
_converse.api.listen.on('disconnect', () => {
const container = document.querySelector("#converse-modals");
if (container) {
container.innerHTML = '';
}
}); });
Object.assign(_converse.api, modal_api);
} }
}); });

View File

@ -3,22 +3,16 @@
* @copyright The Converse.js contributors * @copyright The Converse.js contributors
* @license Mozilla Public License (MPLv2) * @license Mozilla Public License (MPLv2)
*/ */
import "modals/profile.js";
import "modals/chat-status.js";
import "@converse/headless/converse-status"; import "@converse/headless/converse-status";
import "@converse/headless/converse-vcard"; import "@converse/headless/converse-vcard";
import "converse-modal"; import "converse-modal";
import UserSettingsModal from "modals/user-settings"; import UserSettingsModal from "modals/user-settings";
import bootstrap from "bootstrap.native";
import log from "@converse/headless/log";
import sizzle from 'sizzle';
import tpl_chat_status_modal from "templates/chat_status_modal";
import tpl_profile from "templates/profile.js"; import tpl_profile from "templates/profile.js";
import tpl_profile_modal from "templates/profile_modal";
import { BootstrapModal } from "./converse-modal.js";
import { __ } from './i18n'; import { __ } from './i18n';
import { _converse, api, converse } from "@converse/headless/converse-core"; import { _converse, api, converse } from "@converse/headless/converse-core";
const u = converse.env.utils;
converse.plugins.add('converse-profile', { converse.plugins.add('converse-profile', {
@ -35,146 +29,6 @@ converse.plugins.add('converse-profile', {
}); });
_converse.ProfileModal = BootstrapModal.extend({
id: "user-profile-modal",
events: {
'submit .profile-form': 'onFormSubmitted'
},
initialize () {
this.listenTo(this.model, 'change', this.render);
BootstrapModal.prototype.initialize.apply(this, arguments);
/**
* Triggered when the _converse.ProfileModal has been created and initialized.
* @event _converse#profileModalInitialized
* @type { _converse.XMPPStatus }
* @example _converse.api.listen.on('profileModalInitialized', status => { ... });
*/
api.trigger('profileModalInitialized', this.model);
},
toHTML () {
return tpl_profile_modal(Object.assign(
this.model.toJSON(),
this.model.vcard.toJSON(),
this.getAvatarData(),
{ 'view': this }
));
},
getAvatarData () {
const image_type = this.model.vcard.get('image_type');
const image_data = this.model.vcard.get('image');
const image = "data:" + image_type + ";base64," + image_data;
return {
'height': 128,
'width': 128,
image,
};
},
afterRender () {
this.tabs = sizzle('.nav-item .nav-link', this.el).map(e => new bootstrap.Tab(e));
},
async setVCard (data) {
try {
await api.vcard.set(_converse.bare_jid, data);
} catch (err) {
log.fatal(err);
this.alert([
__("Sorry, an error happened while trying to save your profile data."),
__("You can check your browser's developer console for any error output.")
].join(" "));
return;
}
this.modal.hide();
},
onFormSubmitted (ev) {
ev.preventDefault();
const reader = new FileReader();
const form_data = new FormData(ev.target);
const image_file = form_data.get('image');
const data = {
'fn': form_data.get('fn'),
'nickname': form_data.get('nickname'),
'role': form_data.get('role'),
'email': form_data.get('email'),
'url': form_data.get('url'),
};
if (!image_file.size) {
Object.assign(data, {
'image': this.model.vcard.get('image'),
'image_type': this.model.vcard.get('image_type')
});
this.setVCard(data);
} else {
reader.onloadend = () => {
Object.assign(data, {
'image': btoa(reader.result),
'image_type': image_file.type
});
this.setVCard(data);
};
reader.readAsBinaryString(image_file);
}
}
});
_converse.ChatStatusModal = BootstrapModal.extend({
id: "modal-status-change",
events: {
"submit form#set-xmpp-status": "onFormSubmitted",
"click .clear-input": "clearStatusMessage"
},
toHTML () {
return tpl_chat_status_modal(
Object.assign(
this.model.toJSON(),
this.model.vcard.toJSON(), {
'label_away': __('Away'),
'label_busy': __('Busy'),
'label_cancel': __('Cancel'),
'label_close': __('Close'),
'label_custom_status': __('Custom status'),
'label_offline': __('Offline'),
'label_online': __('Online'),
'label_save': __('Save'),
'label_xa': __('Away for long'),
'modal_title': __('Change chat status'),
'placeholder_status_message': __('Personal status message')
}));
},
afterRender () {
this.el.addEventListener('shown.bs.modal', () => {
this.el.querySelector('input[name="status_message"]').focus();
}, false);
},
clearStatusMessage (ev) {
if (ev && ev.preventDefault) {
ev.preventDefault();
u.hideElement(this.el.querySelector('.clear-input'));
}
const roster_filter = this.el.querySelector('input[name="status_message"]');
roster_filter.value = '';
},
onFormSubmitted (ev) {
ev.preventDefault();
const data = new FormData(ev.target);
this.model.save({
'status_message': data.get('status_message'),
'status': data.get('chat_status')
});
this.modal.hide();
}
});
_converse.XMPPStatusView = _converse.ViewWithAvatar.extend({ _converse.XMPPStatusView = _converse.ViewWithAvatar.extend({
tagName: "div", tagName: "div",
events: { events: {

View File

@ -6,23 +6,21 @@
import "@converse/headless/converse-chatboxes"; import "@converse/headless/converse-chatboxes";
import "@converse/headless/converse-roster"; import "@converse/headless/converse-roster";
import "converse-modal"; import "converse-modal";
import "modals/add-contact.js";
import log from "@converse/headless/log"; import log from "@converse/headless/log";
import tpl_add_contact_modal from "templates/add_contact_modal.js";
import tpl_group_header from "templates/group_header.html"; import tpl_group_header from "templates/group_header.html";
import tpl_pending_contact from "templates/pending_contact.html"; import tpl_pending_contact from "templates/pending_contact.html";
import tpl_requesting_contact from "templates/requesting_contact.html"; import tpl_requesting_contact from "templates/requesting_contact.html";
import tpl_roster from "templates/roster.html"; import tpl_roster from "templates/roster.html";
import tpl_roster_filter from "templates/roster_filter.js"; import tpl_roster_filter from "templates/roster_filter.js";
import tpl_roster_item from "templates/roster_item.html"; import tpl_roster_item from "templates/roster_item.html";
import { BootstrapModal } from "./converse-modal.js";
import { Model } from '@converse/skeletor/src/model.js'; import { Model } from '@converse/skeletor/src/model.js';
import { OrderedListView } from "@converse/skeletor/src/overview"; import { OrderedListView } from "@converse/skeletor/src/overview";
import { View } from '@converse/skeletor/src/view.js'; import { View } from '@converse/skeletor/src/view.js';
import { __ } from './i18n'; import { __ } from './i18n';
import { _converse, api, converse } from "@converse/headless/converse-core"; import { _converse, api, converse } from "@converse/headless/converse-core";
import { compact, debounce, has, without } from "lodash-es"; import { debounce, has, without } from "lodash-es";
const { Strophe } = converse.env;
const u = converse.env.utils; const u = converse.env.utils;
@ -55,136 +53,6 @@ converse.plugins.add('converse-rosterview', {
}; };
_converse.AddContactModal = BootstrapModal.extend({
id: "add-contact-modal",
events: {
'submit form': 'addContactFromForm'
},
initialize () {
BootstrapModal.prototype.initialize.apply(this, arguments);
this.listenTo(this.model, 'change', this.render);
},
toHTML () {
const label_nickname = api.settings.get('xhr_user_search_url') ? __('Contact name') : __('Optional nickname');
return tpl_add_contact_modal(Object.assign(this.model.toJSON(), { _converse, label_nickname }));
},
afterRender () {
if (typeof api.settings.get('xhr_user_search_url') === 'string') {
this.initXHRAutoComplete();
} else {
this.initJIDAutoComplete();
}
const jid_input = this.el.querySelector('input[name="jid"]');
this.el.addEventListener('shown.bs.modal', () => jid_input.focus(), false);
},
initJIDAutoComplete () {
if (!api.settings.get('autocomplete_add_contact')) {
return;
}
const el = this.el.querySelector('.suggestion-box__jid').parentElement;
this.jid_auto_complete = new _converse.AutoComplete(el, {
'data': (text, input) => `${input.slice(0, input.indexOf("@"))}@${text}`,
'filter': _converse.FILTER_STARTSWITH,
'list': [...new Set(_converse.roster.map(item => Strophe.getDomainFromJid(item.get('jid'))))]
});
},
initXHRAutoComplete () {
if (!api.settings.get('autocomplete_add_contact')) {
return this.initXHRFetch();
}
const el = this.el.querySelector('.suggestion-box__name').parentElement;
this.name_auto_complete = new _converse.AutoComplete(el, {
'auto_evaluate': false,
'filter': _converse.FILTER_STARTSWITH,
'list': []
});
const xhr = new window.XMLHttpRequest();
// `open` must be called after `onload` for mock/testing purposes.
xhr.onload = () => {
if (xhr.responseText) {
const r = xhr.responseText;
this.name_auto_complete.list = JSON.parse(r).map(i => ({'label': i.fullname || i.jid, 'value': i.jid}));
this.name_auto_complete.auto_completing = true;
this.name_auto_complete.evaluate();
}
};
const input_el = this.el.querySelector('input[name="name"]');
input_el.addEventListener('input', debounce(() => {
xhr.open("GET", `${api.settings.get('xhr_user_search_url')}q=${encodeURIComponent(input_el.value)}`, true);
xhr.send()
} , 300));
this.name_auto_complete.on('suggestion-box-selectcomplete', ev => {
this.el.querySelector('input[name="name"]').value = ev.text.label;
this.el.querySelector('input[name="jid"]').value = ev.text.value;
});
},
initXHRFetch () {
this.xhr = new window.XMLHttpRequest();
this.xhr.onload = () => {
if (this.xhr.responseText) {
const r = this.xhr.responseText;
const list = JSON.parse(r).map(i => ({'label': i.fullname || i.jid, 'value': i.jid}));
if (list.length !== 1) {
const el = this.el.querySelector('.invalid-feedback');
el.textContent = __('Sorry, could not find a contact with that name')
u.addClass('d-block', el);
return;
}
const jid = list[0].value;
if (this.validateSubmission(jid)) {
const form = this.el.querySelector('form');
const name = list[0].label;
this.afterSubmission(form, jid, name);
}
}
};
},
validateSubmission (jid) {
const el = this.el.querySelector('.invalid-feedback');
if (!jid || compact(jid.split('@')).length < 2) {
u.addClass('is-invalid', this.el.querySelector('input[name="jid"]'));
u.addClass('d-block', el);
return false;
} else if (_converse.roster.get(Strophe.getBareJidFromJid(jid))) {
el.textContent = __('This contact has already been added')
u.addClass('d-block', el);
return false;
}
u.removeClass('d-block', el);
return true;
},
afterSubmission (form, jid, name) {
_converse.roster.addAndSubscribe(jid, name);
this.model.clear();
this.modal.hide();
},
addContactFromForm (ev) {
ev.preventDefault();
const data = new FormData(ev.target),
jid = (data.get('jid') || '').trim();
if (!jid && typeof api.settings.get('xhr_user_search_url') === 'string') {
const input_el = this.el.querySelector('input[name="name"]');
this.xhr.open("GET", `${api.settings.get('xhr_user_search_url')}q=${encodeURIComponent(input_el.value)}`, true);
this.xhr.send()
return;
}
if (this.validateSubmission(jid)) {
this.afterSubmission(ev.target, jid, data.get('name'));
}
}
});
_converse.RosterFilter = Model.extend({ _converse.RosterFilter = Model.extend({
initialize () { initialize () {
this.set({ this.set({
@ -195,6 +63,7 @@ converse.plugins.add('converse-rosterview', {
}, },
}); });
_converse.RosterFilterView = View.extend({ _converse.RosterFilterView = View.extend({
tagName: 'span', tagName: 'span',
@ -980,4 +849,3 @@ converse.plugins.add('converse-rosterview', {
}); });
} }
}); });

142
src/modals/add-contact.js Normal file
View File

@ -0,0 +1,142 @@
import BootstrapModal from "./base.js";
import tpl_add_contact_modal from "./templates/add-contact.js";
import { __ } from '../i18n';
import { _converse, api, converse } from "@converse/headless/converse-core";
import { compact, debounce } from "lodash-es";
const { Strophe } = converse.env;
const u = converse.env.utils;
const AddContactModal = BootstrapModal.extend({
id: "add-contact-modal",
events: {
'submit form': 'addContactFromForm'
},
initialize () {
BootstrapModal.prototype.initialize.apply(this, arguments);
this.listenTo(this.model, 'change', this.render);
},
toHTML () {
const label_nickname = api.settings.get('xhr_user_search_url') ? __('Contact name') : __('Optional nickname');
return tpl_add_contact_modal(Object.assign(this.model.toJSON(), { _converse, label_nickname }));
},
afterRender () {
if (typeof api.settings.get('xhr_user_search_url') === 'string') {
this.initXHRAutoComplete();
} else {
this.initJIDAutoComplete();
}
const jid_input = this.el.querySelector('input[name="jid"]');
this.el.addEventListener('shown.bs.modal', () => jid_input.focus(), false);
},
initJIDAutoComplete () {
if (!api.settings.get('autocomplete_add_contact')) {
return;
}
const el = this.el.querySelector('.suggestion-box__jid').parentElement;
this.jid_auto_complete = new _converse.AutoComplete(el, {
'data': (text, input) => `${input.slice(0, input.indexOf("@"))}@${text}`,
'filter': _converse.FILTER_STARTSWITH,
'list': [...new Set(_converse.roster.map(item => Strophe.getDomainFromJid(item.get('jid'))))]
});
},
initXHRAutoComplete () {
if (!api.settings.get('autocomplete_add_contact')) {
return this.initXHRFetch();
}
const el = this.el.querySelector('.suggestion-box__name').parentElement;
this.name_auto_complete = new _converse.AutoComplete(el, {
'auto_evaluate': false,
'filter': _converse.FILTER_STARTSWITH,
'list': []
});
const xhr = new window.XMLHttpRequest();
// `open` must be called after `onload` for mock/testing purposes.
xhr.onload = () => {
if (xhr.responseText) {
const r = xhr.responseText;
this.name_auto_complete.list = JSON.parse(r).map(i => ({'label': i.fullname || i.jid, 'value': i.jid}));
this.name_auto_complete.auto_completing = true;
this.name_auto_complete.evaluate();
}
};
const input_el = this.el.querySelector('input[name="name"]');
input_el.addEventListener('input', debounce(() => {
xhr.open("GET", `${api.settings.get('xhr_user_search_url')}q=${encodeURIComponent(input_el.value)}`, true);
xhr.send()
} , 300));
this.name_auto_complete.on('suggestion-box-selectcomplete', ev => {
this.el.querySelector('input[name="name"]').value = ev.text.label;
this.el.querySelector('input[name="jid"]').value = ev.text.value;
});
},
initXHRFetch () {
this.xhr = new window.XMLHttpRequest();
this.xhr.onload = () => {
if (this.xhr.responseText) {
const r = this.xhr.responseText;
const list = JSON.parse(r).map(i => ({'label': i.fullname || i.jid, 'value': i.jid}));
if (list.length !== 1) {
const el = this.el.querySelector('.invalid-feedback');
el.textContent = __('Sorry, could not find a contact with that name')
u.addClass('d-block', el);
return;
}
const jid = list[0].value;
if (this.validateSubmission(jid)) {
const form = this.el.querySelector('form');
const name = list[0].label;
this.afterSubmission(form, jid, name);
}
}
};
},
validateSubmission (jid) {
const el = this.el.querySelector('.invalid-feedback');
if (!jid || compact(jid.split('@')).length < 2) {
u.addClass('is-invalid', this.el.querySelector('input[name="jid"]'));
u.addClass('d-block', el);
return false;
} else if (_converse.roster.get(Strophe.getBareJidFromJid(jid))) {
el.textContent = __('This contact has already been added')
u.addClass('d-block', el);
return false;
}
u.removeClass('d-block', el);
return true;
},
afterSubmission (form, jid, name) {
_converse.roster.addAndSubscribe(jid, name);
this.model.clear();
this.modal.hide();
},
addContactFromForm (ev) {
ev.preventDefault();
const data = new FormData(ev.target),
jid = (data.get('jid') || '').trim();
if (!jid && typeof api.settings.get('xhr_user_search_url') === 'string') {
const input_el = this.el.querySelector('input[name="name"]');
this.xhr.open("GET", `${api.settings.get('xhr_user_search_url')}q=${encodeURIComponent(input_el.value)}`, true);
this.xhr.send()
return;
}
if (this.validateSubmission(jid)) {
this.afterSubmission(ev.target, jid, data.get('name'));
}
}
});
_converse.AddContactModal = AddContactModal;
export default AddContactModal;

View File

@ -1,5 +1,5 @@
import tpl_add_chatroom_modal from "templates/add_chatroom_modal.js"; import tpl_add_muc from "./templates/add-muc.js";
import { BootstrapModal } from "../converse-modal.js"; import BootstrapModal from "./base.js";
import { Strophe } from 'strophe.js/src/strophe'; import { Strophe } from 'strophe.js/src/strophe';
import { __ } from '../i18n'; import { __ } from '../i18n';
import { _converse, api, converse } from "@converse/headless/converse-core"; import { _converse, api, converse } from "@converse/headless/converse-core";
@ -8,6 +8,7 @@ const u = converse.env.utils;
export default BootstrapModal.extend({ export default BootstrapModal.extend({
persistent: true,
id: 'add-chatroom-modal', id: 'add-chatroom-modal',
events: { events: {
@ -28,7 +29,7 @@ export default BootstrapModal.extend({
const muc_domain = this.model.get('muc_domain') || api.settings.get('muc_domain'); const muc_domain = this.model.get('muc_domain') || api.settings.get('muc_domain');
placeholder = muc_domain ? `name@${muc_domain}` : __('name@conference.example.org'); placeholder = muc_domain ? `name@${muc_domain}` : __('name@conference.example.org');
} }
return tpl_add_chatroom_modal(Object.assign(this.model.toJSON(), { return tpl_add_muc(Object.assign(this.model.toJSON(), {
'_converse': _converse, '_converse': _converse,
'label_room_address': api.settings.get('muc_domain') ? __('Groupchat name') : __('Groupchat address'), 'label_room_address': api.settings.get('muc_domain') ? __('Groupchat name') : __('Groupchat address'),
'chatroom_placeholder': placeholder, 'chatroom_placeholder': placeholder,

18
src/modals/alert.js Normal file
View File

@ -0,0 +1,18 @@
import BootstrapModal from "./base.js";
import tpl_alert_modal from "./templates/alert.js";
import { __ } from '../i18n';
const Alert = BootstrapModal.extend({
initialize () {
BootstrapModal.prototype.initialize.apply(this, arguments);
this.listenTo(this.model, 'change', this.render)
},
toHTML () {
return tpl_alert_modal(Object.assign({__}, this.model.toJSON()));
}
});
export default Alert;

84
src/modals/base.js Normal file
View File

@ -0,0 +1,84 @@
import bootstrap from "bootstrap.native";
import log from "@converse/headless/log";
import tpl_alert_component from "templates/alert.js";
import { View } from '@converse/skeletor/src/view.js';
import { _converse, converse } from "@converse/headless/converse-core";
import { render } from 'lit-html';
const { sizzle } = converse.env;
const u = converse.env.utils;
const BaseModal = View.extend({
className: "modal",
persistent: false, // Whether this modal should persist in the DOM once it's been closed
events: {
'click .nav-item .nav-link': 'switchTab'
},
initialize () {
this.render()
this.el.setAttribute('tabindex', '-1');
this.el.setAttribute('role', 'dialog');
this.el.setAttribute('aria-hidden', 'true');
const label_id = this.el.querySelector('.modal-title').getAttribute('id');
label_id && this.el.setAttribute('aria-labelledby', label_id);
this.insertIntoDOM();
const Modal = bootstrap.Modal;
this.modal = new Modal(this.el, {
backdrop: true,
keyboard: true
});
this.el.addEventListener('hide.bs.modal', () => this.onHide(), false);
},
onHide () {
u.removeClass('selected', this.trigger_el);
!this.persistent && this.remove();
},
insertIntoDOM () {
const container_el = _converse.chatboxviews.el.querySelector("#converse-modals");
container_el.insertAdjacentElement('beforeEnd', this.el);
},
switchTab (ev) {
ev.stopPropagation();
ev.preventDefault();
sizzle('.nav-link.active', this.el).forEach(el => {
u.removeClass('active', this.el.querySelector(el.getAttribute('href')));
u.removeClass('active', el);
});
u.addClass('active', ev.target);
u.addClass('active', this.el.querySelector(ev.target.getAttribute('href')))
},
alert (message, type='primary') {
const body = this.el.querySelector('.modal-alert');
if (body === null) {
log.error("Could not find a .modal-alert element in the modal to show an alert message in!");
return;
}
// FIXME: Instead of adding the alert imperatively, we should
// find a way to let the modal rerender with an alert message
render(tpl_alert_component({'type': `alert-${type}`, 'message': message}), body);
const el = body.firstElementChild;
setTimeout(() => {
u.addClass('fade-out', el);
setTimeout(() => u.removeElement(el), 600);
}, 5000);
},
show (ev) {
if (ev) {
ev.preventDefault();
this.trigger_el = ev.target;
this.trigger_el.classList.add('selected');
}
this.modal.show();
}
});
export default BaseModal;

64
src/modals/chat-status.js Normal file
View File

@ -0,0 +1,64 @@
import BootstrapModal from "./base.js";
import tpl_chat_status_modal from "./templates/chat-status.js";
import { __ } from '../i18n';
import { _converse, converse } from "@converse/headless/converse-core";
const u = converse.env.utils;
const ChatStatusModal = BootstrapModal.extend({
id: "modal-status-change",
events: {
"submit form#set-xmpp-status": "onFormSubmitted",
"click .clear-input": "clearStatusMessage"
},
toHTML () {
return tpl_chat_status_modal(
Object.assign(
this.model.toJSON(),
this.model.vcard.toJSON(), {
'label_away': __('Away'),
'label_busy': __('Busy'),
'label_cancel': __('Cancel'),
'label_close': __('Close'),
'label_custom_status': __('Custom status'),
'label_offline': __('Offline'),
'label_online': __('Online'),
'label_save': __('Save'),
'label_xa': __('Away for long'),
'modal_title': __('Change chat status'),
'placeholder_status_message': __('Personal status message')
}));
},
afterRender () {
this.el.addEventListener('shown.bs.modal', () => {
this.el.querySelector('input[name="status_message"]').focus();
}, false);
},
clearStatusMessage (ev) {
if (ev && ev.preventDefault) {
ev.preventDefault();
u.hideElement(this.el.querySelector('.clear-input'));
}
const roster_filter = this.el.querySelector('input[name="status_message"]');
roster_filter.value = '';
},
onFormSubmitted (ev) {
ev.preventDefault();
const data = new FormData(ev.target);
this.model.save({
'status_message': data.get('status_message'),
'status': data.get('chat_status')
});
this.modal.hide();
}
});
_converse.ChatStatusModal = ChatStatusModal;
export default ChatStatusModal;

59
src/modals/confirm.js Normal file
View File

@ -0,0 +1,59 @@
import BootstrapModal from './base.js';
import tpl_prompt from "./templates/prompt.js";
import { converse } from "@converse/headless/converse-core";
const u = converse.env.utils;
const Confirm = BootstrapModal.extend({
events: {
'submit .confirm': 'onConfimation'
},
initialize () {
this.confirmation = u.getResolveablePromise();
BootstrapModal.prototype.initialize.apply(this, arguments);
this.listenTo(this.model, 'change', this.render)
this.el.addEventListener('closed.bs.modal', () => this.confirmation.reject(), false);
},
toHTML () {
return tpl_prompt(this.model.toJSON());
},
afterRender () {
if (!this.close_handler_registered) {
this.el.addEventListener('closed.bs.modal', () => {
if (!this.confirmation.isResolved) {
this.confirmation.reject()
}
}, false);
this.close_handler_registered = true;
}
},
onConfimation (ev) {
ev.preventDefault();
const form_data = new FormData(ev.target);
const fields = (this.model.get('fields') || [])
.map(field => {
const value = form_data.get(field.name).trim();
field.value = value;
if (field.challenge) {
field.challenge_failed = (value !== field.challenge);
}
return field;
});
if (fields.filter(c => c.challenge_failed).length) {
this.model.set('fields', fields);
// Setting an array doesn't trigger a change event
this.model.trigger('change');
return;
}
this.confirmation.resolve(fields);
this.modal.hide();
}
});
export default Confirm;

View File

@ -1,5 +1,5 @@
import { BootstrapModal } from "../converse-modal.js"; import BootstrapModal from "./base.js";
import tpl_image_modal from "../templates/image_modal.js"; import tpl_image_modal from "./templates/image.js";
export default BootstrapModal.extend({ export default BootstrapModal.extend({

View File

@ -1,10 +1,9 @@
import { BootstrapModal } from "../converse-modal.js"; import BootstrapModal from "./base.js";
import tpl_message_versions_modal from "../templates/message_versions_modal.js"; import tpl_message_versions_modal from "./templates/message-versions.js";
export default BootstrapModal.extend({ export default BootstrapModal.extend({
// FIXME: this isn't globally unique
id: "message-versions-modal",
toHTML () { toHTML () {
return tpl_message_versions_modal(this.model.toJSON()); return tpl_message_versions_modal(this.model.toJSON());
} }

View File

@ -1,8 +1,8 @@
import BootstrapModal from "./base.js";
import log from "@converse/headless/log"; import log from "@converse/headless/log";
import sizzle from "sizzle"; import sizzle from "sizzle";
import tpl_moderator_tools_modal from "../templates/moderator_tools_modal.js"; import tpl_moderator_tools_modal from "./templates/moderator-tools.js";
import { AFFILIATIONS, ROLES } from "@converse/headless/converse-muc.js"; import { AFFILIATIONS, ROLES } from "@converse/headless/converse-muc.js";
import { BootstrapModal } from "../converse-modal.js";
import { __ } from '../i18n'; import { __ } from '../i18n';
import { api, converse } from "@converse/headless/converse-core"; import { api, converse } from "@converse/headless/converse-core";
@ -12,7 +12,7 @@ let _converse;
export default BootstrapModal.extend({ export default BootstrapModal.extend({
id: "converse-modtools-modal", persistent: true,
initialize (attrs) { initialize (attrs) {
_converse = attrs._converse; _converse = attrs._converse;

View File

@ -1,4 +1,4 @@
import { BootstrapModal } from "../converse-modal.js"; import BootstrapModal from "./base.js";
import { __ } from '../i18n'; import { __ } from '../i18n';
import { api, converse } from "@converse/headless/converse-core"; import { api, converse } from "@converse/headless/converse-core";
import log from "@converse/headless/log"; import log from "@converse/headless/log";

View File

@ -1,6 +1,6 @@
import { BootstrapModal } from "../converse-modal.js"; import BootstrapModal from "./base.js";
import tpl_muc_details from "./templates/muc-details.js";
import { __ } from '../i18n'; import { __ } from '../i18n';
import tpl_chatroom_details_modal from "../templates/chatroom_details_modal.js";
export default BootstrapModal.extend({ export default BootstrapModal.extend({
@ -15,7 +15,7 @@ export default BootstrapModal.extend({
}, },
toHTML () { toHTML () {
return tpl_chatroom_details_modal(Object.assign( return tpl_muc_details(Object.assign(
this.model.toJSON(), { this.model.toJSON(), {
'config': this.model.config.toJSON(), 'config': this.model.config.toJSON(),
'display_name': __('Groupchat info for %1$s', this.model.getDisplayName()), 'display_name': __('Groupchat info for %1$s', this.model.getDisplayName()),

View File

@ -1,5 +1,5 @@
import tpl_muc_invite_modal from "templates/muc_invite_modal.js"; import BootstrapModal from "./base.js";
import { BootstrapModal } from "../converse-modal.js"; import tpl_muc_invite_modal from "./templates/muc-invite.js";
import { _converse, converse } from "@converse/headless/converse-core"; import { _converse, converse } from "@converse/headless/converse-core";
const u = converse.env.utils; const u = converse.env.utils;
@ -49,5 +49,3 @@ export default BootstrapModal.extend({
} }
} }
}); });

View File

@ -1,10 +1,10 @@
import BootstrapModal from "./base.js";
import log from "@converse/headless/log"; import log from "@converse/headless/log";
import sizzle from 'sizzle'; import sizzle from 'sizzle';
import st from "@converse/headless/utils/stanza"; import st from "@converse/headless/utils/stanza";
import tpl_list_chatrooms_modal from "templates/list_chatrooms_modal.js"; import tpl_list_chatrooms_modal from "./templates/muc-list.js";
import tpl_room_description from "templates/room_description.html"; import tpl_room_description from "templates/room_description.html";
import tpl_spinner from "templates/spinner.js"; import tpl_spinner from "templates/spinner.js";
import { BootstrapModal } from "../converse-modal.js";
import { Strophe, $iq } from 'strophe.js/src/strophe'; import { Strophe, $iq } from 'strophe.js/src/strophe';
import { __ } from '../i18n'; import { __ } from '../i18n';
import { _converse, api, converse } from "@converse/headless/converse-core"; import { _converse, api, converse } from "@converse/headless/converse-core";
@ -83,6 +83,7 @@ function toggleRoomInfo (ev) {
export default BootstrapModal.extend({ export default BootstrapModal.extend({
id: "list-chatrooms-modal", id: "list-chatrooms-modal",
persistent: true,
initialize () { initialize () {
this.items = []; this.items = [];

View File

@ -1,10 +1,9 @@
import BootstrapModal from "./base.js";
import tpl_occupant_modal from "./templates/occupant.js"; import tpl_occupant_modal from "./templates/occupant.js";
import { BootstrapModal } from "../converse-modal.js";
import { _converse, api } from "@converse/headless/converse-core"; import { _converse, api } from "@converse/headless/converse-core";
const OccupantModal = BootstrapModal.extend({ const OccupantModal = BootstrapModal.extend({
id: "muc-occupant-modal",
initialize () { initialize () {
BootstrapModal.prototype.initialize.apply(this, arguments); BootstrapModal.prototype.initialize.apply(this, arguments);

99
src/modals/profile.js Normal file
View File

@ -0,0 +1,99 @@
import BootstrapModal from "./base.js";
import bootstrap from "bootstrap.native";
import log from "@converse/headless/log";
import sizzle from 'sizzle';
import tpl_profile_modal from "./templates/profile.js";
import { __ } from '../i18n';
import { _converse, api } from "@converse/headless/converse-core";
const ProfileModal = BootstrapModal.extend({
id: "user-profile-modal",
events: {
'submit .profile-form': 'onFormSubmitted'
},
initialize () {
this.listenTo(this.model, 'change', this.render);
BootstrapModal.prototype.initialize.apply(this, arguments);
/**
* Triggered when the _converse.ProfileModal has been created and initialized.
* @event _converse#profileModalInitialized
* @type { _converse.XMPPStatus }
* @example _converse.api.listen.on('profileModalInitialized', status => { ... });
*/
api.trigger('profileModalInitialized', this.model);
},
toHTML () {
return tpl_profile_modal(Object.assign(
this.model.toJSON(),
this.model.vcard.toJSON(),
this.getAvatarData(),
{ 'view': this }
));
},
getAvatarData () {
const image_type = this.model.vcard.get('image_type');
const image_data = this.model.vcard.get('image');
const image = "data:" + image_type + ";base64," + image_data;
return {
'height': 128,
'width': 128,
image,
};
},
afterRender () {
this.tabs = sizzle('.nav-item .nav-link', this.el).map(e => new bootstrap.Tab(e));
},
async setVCard (data) {
try {
await api.vcard.set(_converse.bare_jid, data);
} catch (err) {
log.fatal(err);
this.alert([
__("Sorry, an error happened while trying to save your profile data."),
__("You can check your browser's developer console for any error output.")
].join(" "));
return;
}
this.modal.hide();
},
onFormSubmitted (ev) {
ev.preventDefault();
const reader = new FileReader();
const form_data = new FormData(ev.target);
const image_file = form_data.get('image');
const data = {
'fn': form_data.get('fn'),
'nickname': form_data.get('nickname'),
'role': form_data.get('role'),
'email': form_data.get('email'),
'url': form_data.get('url'),
};
if (!image_file.size) {
Object.assign(data, {
'image': this.model.vcard.get('image'),
'image_type': this.model.vcard.get('image_type')
});
this.setVCard(data);
} else {
reader.onloadend = () => {
Object.assign(data, {
'image': btoa(reader.result),
'image_type': image_file.type
});
this.setVCard(data);
};
reader.readAsBinaryString(image_file);
}
}
});
_converse.ProfileModal = ProfileModal;
export default ProfileModal;

View File

@ -1,6 +1,6 @@
import { html } from "lit-html"; import { html } from "lit-html";
import { __ } from '../i18n'; import { __ } from '../../i18n';
import { modal_header_close_button } from "./buttons" import { modal_header_close_button } from "./buttons.js"
export default (o) => { export default (o) => {

View File

@ -1,5 +1,5 @@
import xss from "xss/dist/xss"; import xss from "xss/dist/xss";
import { __ } from '../i18n'; import { __ } from '../../i18n';
import { html } from "lit-html"; import { html } from "lit-html";
import { modal_header_close_button } from "./buttons" import { modal_header_close_button } from "./buttons"
import { unsafeHTML } from "lit-html/directives/unsafe-html.js"; import { unsafeHTML } from "lit-html/directives/unsafe-html.js";

View File

@ -1,5 +1,5 @@
import { html } from "lit-html"; import { html } from "lit-html";
import { modal_header_close_button } from "./buttons" import { modal_header_close_button } from "./buttons.js"
export default (o) => html` export default (o) => html`

View File

@ -1,8 +1,7 @@
import { __ } from '../i18n'; import { __ } from '../../i18n';
import { html } from "lit-html"; import { html } from "lit-html";
export const modal_close_button = html`<button type="button" class="btn btn-secondary" data-dismiss="modal">${__('Close')}</button>`; export const modal_close_button = html`<button type="button" class="btn btn-secondary" data-dismiss="modal">${__('Close')}</button>`;
export const modal_header_close_button = html`<button type="button" class="close" data-dismiss="modal" aria-label="${__('Close')}"><span aria-hidden="true">×</span></button>`; export const modal_header_close_button = html`<button type="button" class="close" data-dismiss="modal" aria-label="${__('Close')}"><span aria-hidden="true">×</span></button>`;

View File

@ -1,5 +1,5 @@
import { html } from "lit-html"; import { html } from "lit-html";
import { modal_header_close_button } from "./buttons" import { modal_header_close_button } from "./buttons.js"
export default (o) => html` export default (o) => html`

View File

@ -1,6 +1,6 @@
import { html } from "lit-html"; import { html } from "lit-html";
import { __ } from '../i18n'; import { __ } from '../../i18n';
import { modal_close_button, modal_header_close_button } from "./buttons" import { modal_close_button, modal_header_close_button } from "./buttons.js"
export default (o) => { export default (o) => {

View File

@ -1,7 +1,7 @@
import { html } from "lit-html";
import { __ } from '../i18n';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { modal_close_button, modal_header_close_button } from "./buttons" import { __ } from '../../i18n';
import { html } from "lit-html";
import { modal_close_button, modal_header_close_button } from "./buttons.js"
export default (o) => html` export default (o) => html`

View File

@ -1,7 +1,7 @@
import { html } from "lit-html"; import { html } from "lit-html";
import { __ } from '../i18n'; import { __ } from '../../i18n';
import spinner from "./spinner.js"; import spinner from "../../templates/spinner.js";
import { modal_header_close_button } from "./buttons" import { modal_header_close_button } from "./buttons.js"
function getRoleHelpText (role) { function getRoleHelpText (role) {

View File

@ -1,6 +1,6 @@
import { __ } from '../i18n'; import { __ } from '../../i18n';
import { html } from "lit-html"; import { html } from "lit-html";
import { modal_close_button, modal_header_close_button } from "./buttons" import { modal_close_button, modal_header_close_button } from "./buttons.js"
import { unsafeHTML } from 'lit-html/directives/unsafe-html.js'; import { unsafeHTML } from 'lit-html/directives/unsafe-html.js';
import xss from "xss/dist/xss"; import xss from "xss/dist/xss";

View File

@ -1,6 +1,6 @@
import { html } from "lit-html"; import { html } from "lit-html";
import { __ } from '../i18n'; import { __ } from '../../i18n';
import { modal_header_close_button } from "./buttons" import { modal_header_close_button } from "./buttons.js"
export default (o) => { export default (o) => {

View File

@ -1,8 +1,8 @@
import { __ } from '../i18n'; import { __ } from '../../i18n';
import { html } from "lit-html"; import { html } from "lit-html";
import { repeat } from 'lit-html/directives/repeat.js'; import { repeat } from 'lit-html/directives/repeat.js';
import { modal_close_button, modal_header_close_button } from "./buttons" import { modal_close_button, modal_header_close_button } from "./buttons.js"
import spinner from "./spinner.js"; import spinner from "../../templates/spinner.js";
const form = (o) => { const form = (o) => {

View File

@ -1,5 +1,5 @@
import { html } from "lit-html"; import { html } from "lit-html";
import { modal_close_button, modal_header_close_button } from "../../templates/buttons" import { modal_close_button, modal_header_close_button } from "./buttons.js"
import { renderAvatar } from '../../templates/directives/avatar'; import { renderAvatar } from '../../templates/directives/avatar';

View File

@ -1,9 +1,9 @@
import "../components/image_picker.js"; import "../../components/image_picker.js";
import spinner from "./spinner.js"; import spinner from "../../templates/spinner.js";
import { __ } from '../i18n'; import { __ } from '../../i18n';
import { _converse, converse } from "@converse/headless/converse-core"; import { _converse, converse } from "@converse/headless/converse-core";
import { html } from "lit-html"; import { html } from "lit-html";
import { modal_header_close_button } from "./buttons"; import { modal_header_close_button } from "./buttons.js";
const u = converse.env.utils; const u = converse.env.utils;

View File

@ -1,5 +1,5 @@
import { html } from "lit-html"; import { html } from "lit-html";
import { __ } from '../i18n'; import { __ } from '../../i18n';
const tpl_field = (f) => html` const tpl_field = (f) => html`

View File

@ -1,7 +1,7 @@
import { __ } from '../i18n'; import { __ } from '../../i18n';
import { html } from "lit-html"; import { html } from "lit-html";
import avatar from "./avatar.js"; import avatar from "../../templates/avatar.js";
import { modal_close_button, modal_header_close_button } from "./buttons" import { modal_close_button, modal_header_close_button } from "./buttons.js"
const device_fingerprint = (o) => { const device_fingerprint = (o) => {

View File

@ -1,9 +1,9 @@
import '../components/adhoc-commands.js'; import '../../components/adhoc-commands.js';
import xss from "xss/dist/xss"; import xss from "xss/dist/xss";
import { __ } from '../i18n'; import { __ } from '../../i18n';
import { api } from "@converse/headless/converse-core"; import { api } from "@converse/headless/converse-core";
import { html } from "lit-html"; import { html } from "lit-html";
import { modal_header_close_button } from "./buttons" import { modal_header_close_button } from "./buttons.js"
import { unsafeHTML } from 'lit-html/directives/unsafe-html.js'; import { unsafeHTML } from 'lit-html/directives/unsafe-html.js';

View File

@ -1,6 +1,6 @@
import BootstrapModal from "./base.js";
import log from "@converse/headless/log"; import log from "@converse/headless/log";
import tpl_user_details_modal from "../templates/user_details_modal.js"; import tpl_user_details_modal from "./templates/user-details.js";
import { BootstrapModal } from "../converse-modal.js";
import { __ } from '../i18n'; import { __ } from '../i18n';
import { _converse, api, converse } from "@converse/headless/converse-core"; import { _converse, api, converse } from "@converse/headless/converse-core";
@ -8,7 +8,7 @@ const u = converse.env.utils;
const UserDetailsModal = BootstrapModal.extend({ const UserDetailsModal = BootstrapModal.extend({
id: "user-details-modal", persistent: true,
events: { events: {
'click button.refresh-contact': 'refreshContact', 'click button.refresh-contact': 'refreshContact',

View File

@ -1,5 +1,5 @@
import { BootstrapModal } from "../converse-modal.js"; import BootstrapModal from "./base.js";
import tpl_user_settings_modal from "templates/user_settings_modal"; import tpl_user_settings_modal from "./templates/user-settings.js";
let _converse; let _converse;
@ -21,4 +21,3 @@ export default BootstrapModal.extend({
); );
} }
}); });