From b18cc6bcc51632c664fbb776bdc437db4c8189fa Mon Sep 17 00:00:00 2001 From: JC Brand Date: Tue, 1 Dec 2020 18:50:35 +0100 Subject: [PATCH] Move modals and their templates into `./modals/` --- package-lock.json | 17 +- spec/user-details-modal.js | 3 +- src/converse-modal.js | 352 ++++++------------ src/converse-profile.js | 150 +------- src/converse-rosterview.js | 138 +------ src/modals/add-contact.js | 142 +++++++ src/modals/add-muc.js | 7 +- src/modals/alert.js | 18 + src/modals/base.js | 84 +++++ src/modals/chat-status.js | 64 ++++ src/modals/confirm.js | 59 +++ src/modals/image.js | 4 +- src/modals/message-versions.js | 7 +- src/modals/moderator-tools.js | 6 +- src/modals/muc-commands.js | 2 +- src/modals/muc-details.js | 6 +- src/modals/muc-invite.js | 6 +- src/modals/muc-list.js | 5 +- src/modals/occupant.js | 13 +- src/modals/profile.js | 99 +++++ .../templates/add-contact.js} | 4 +- .../templates/add-muc.js} | 2 +- .../templates/alert.js} | 2 +- src/{ => modals}/templates/buttons.js | 3 +- .../templates/chat-status.js} | 2 +- .../templates/image.js} | 4 +- .../templates/message-versions.js} | 6 +- .../templates/moderator-tools.js} | 6 +- .../templates/muc-details.js} | 4 +- .../templates/muc-invite.js} | 4 +- .../templates/muc-list.js} | 6 +- src/modals/templates/occupant.js | 2 +- .../templates/profile.js} | 8 +- src/{ => modals}/templates/prompt.js | 2 +- .../templates/user-details.js} | 6 +- .../templates/user-settings.js} | 6 +- src/modals/user-details.js | 6 +- src/modals/user-settings.js | 5 +- 38 files changed, 654 insertions(+), 606 deletions(-) create mode 100644 src/modals/add-contact.js create mode 100644 src/modals/alert.js create mode 100644 src/modals/base.js create mode 100644 src/modals/chat-status.js create mode 100644 src/modals/confirm.js create mode 100644 src/modals/profile.js rename src/{templates/add_contact_modal.js => modals/templates/add-contact.js} (97%) rename src/{templates/add_chatroom_modal.js => modals/templates/add-muc.js} (98%) rename src/{templates/alert_modal.js => modals/templates/alert.js} (90%) rename src/{ => modals}/templates/buttons.js (91%) rename src/{templates/chat_status_modal.js => modals/templates/chat-status.js} (98%) rename src/{templates/image_modal.js => modals/templates/image.js} (94%) rename src/{templates/message_versions_modal.js => modals/templates/message-versions.js} (95%) rename src/{templates/moderator_tools_modal.js => modals/templates/moderator-tools.js} (98%) rename src/{templates/chatroom_details_modal.js => modals/templates/muc-details.js} (99%) rename src/{templates/muc_invite_modal.js => modals/templates/muc-invite.js} (96%) rename src/{templates/list_chatrooms_modal.js => modals/templates/muc-list.js} (96%) rename src/{templates/profile_modal.js => modals/templates/profile.js} (97%) rename src/{ => modals}/templates/prompt.js (97%) rename src/{templates/user_details_modal.js => modals/templates/user-details.js} (97%) rename src/{templates/user_settings_modal.js => modals/templates/user-settings.js} (96%) diff --git a/package-lock.json b/package-lock.json index 01445e0ff..50a488e1a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3144,7 +3144,8 @@ "dependencies": { "filesize": { "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": { "version": "8.1.0", @@ -3200,20 +3201,22 @@ }, "localforage": { "version": "1.7.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.7.3.tgz", + "integrity": "sha512-1TulyYfc4udS7ECSBT2vwJksWbkwwTX8BzeUIiq8Y07Riy7bDAAnxDaPU/tWyOVmQAcWJIEIFP9lPfBGqVoPgQ==", "requires": { "lie": "3.1.1" } }, "pluggable.js": { "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": { "lodash": "^4.17.11" } }, "skeletor.js": { - "version": "0.0.1", + "version": "github:skeletorjs/skeletor#bf6d9c86f9fcf224fa9d9af5a25380b77aa4b561", "from": "github:skeletorjs/skeletor#bf6d9c86f9fcf224fa9d9af5a25380b77aa4b561", "requires": { "lodash": "^4.17.14" @@ -3221,7 +3224,11 @@ }, "strophe.js": { "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": { "version": "12.1.5", diff --git a/spec/user-details-modal.js b/spec/user-details-modal.js index 3afa98cb9..1eb59727a 100644 --- a/spec/user-details-modal.js +++ b/spec/user-details-modal.js @@ -1,4 +1,4 @@ -/*global mock */ +/*global mock, converse */ 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'); expect(u.isVisible(remove_contact_button)).toBeTruthy(); remove_contact_button.click(); + await u.waitUntil(() => u.isVisible(document.querySelector('.alert-danger')), 2000); const header = document.querySelector('.alert-danger .modal-title'); diff --git a/src/converse-modal.js b/src/converse-modal.js index ea615fc24..801bd4e4a 100644 --- a/src/converse-modal.js +++ b/src/converse-modal.js @@ -3,277 +3,133 @@ * @copyright The Converse.js contributors * @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 { render } from 'lit-html'; -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"; +import { _converse, converse } from "@converse/headless/converse-core"; -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 -export 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); - }, +let alert; - 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; +const modal_api = { + /** + * Show a confirm modal to the user. + * @method _converse.api.confirm + * @param { String } title - The header text for the confirmation dialog + * @param { (String[]|String) } messages - The text to show to the user + * @param { Array } fields - An object representing a fields presented to the user. + * @property { String } Field.label - The form label for the input field. + * @property { String } Field.name - The name for the input field. + * @property { String } [Field.challenge] - A challenge value that must be provided by the user. + * @property { String } [Field.placeholder] - The placeholder for the input field. + * @property { Boolean} [Field.required] - Whether the field is required or not + * @returns { Promise } A promise which resolves with an array of + * filled in fields or `false` if the confirm dialog was closed or canceled. + */ + async confirm (title, messages=[], fields=[]) { + if (typeof messages === 'string') { + messages = [messages]; } + const model = new Model({title, messages, fields, 'type': 'confirm'}) + const confirm = new Confirm({model}); + confirm.show(); + let result; + try { + result = await confirm.confirmation; + } catch (e) { + result = false; + } + confirm.remove(); + return result; }, - 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; + /** + * Show a prompt modal to the user. + * @method _converse.api.prompt + * @param { String } title - The header text for the prompt + * @param { (String[]|String) } messages - The prompt text to show to the user + * @param { String } placeholder - The placeholder text for the prompt input + * @returns { Promise } A promise which resolves with the text provided by the + * user or `false` if the user canceled the prompt. + */ + async prompt (title, messages=[], placeholder='') { + if (typeof messages === 'string') { + messages = [messages]; + } + const model = new Model({ + title, + messages, + 'fields': [{ + 'name': 'reason', + 'placeholder': placeholder, + }], + 'type': 'prompt' + }) + const prompt = new Confirm({model}); + prompt.show(); + let result; + try { + result = (await prompt.confirmation).pop()?.value; + } catch (e) { + result = false; + } + prompt.remove(); + return result; + }, + + /** + * Show an alert modal to the user. + * @method _converse.api.alert + * @param { ('info'|'warn'|'error') } type - The type of alert. + * @param { String } title - The header text for the alert. + * @param { (String[]|String) } messages - The alert text to show to the user. + */ + alert (type, title, messages) { + if (typeof messages === 'string') { + messages = [messages]; + } + let level; + if (type === 'error') { + level = 'alert-danger'; + } else if (type === 'info') { + level = 'alert-info'; + } else if (type === 'warn') { + level = 'alert-warning'; + } + + if (alert === undefined) { + const model = new Model({ + 'title': title, + 'messages': messages, + 'level': level, + 'type': 'alert' + }) + alert = new Alert({model}); + } else { + alert.model.set({ + 'title': title, + 'messages': messages, + 'level': level }); - - 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(); + alert.show(); } -}); - - -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. - * @method _converse.api.confirm - * @param { String } title - The header text for the confirmation dialog - * @param { (String[]|String) } messages - The text to show to the user - * @param { Array } fields - An object representing a fields presented to the user. - * @property { String } Field.label - The form label for the input field. - * @property { String } Field.name - The name for the input field. - * @property { String } [Field.challenge] - A challenge value that must be provided by the user. - * @property { String } [Field.placeholder] - The placeholder for the input field. - * @property { Boolean} [Field.required] - Whether the field is required or not - * @returns { Promise } A promise which resolves with an array of - * filled in fields or `false` if the confirm dialog was closed or canceled. - */ - async confirm (title, messages=[], fields=[]) { - if (typeof messages === 'string') { - messages = [messages]; - } - const model = new Model({title, messages, fields, 'type': 'confirm'}) - const confirm = new Confirm({model}); - confirm.show(); - let result; - try { - result = await confirm.confirmation; - } catch (e) { - result = false; - } - confirm.remove(); - return result; - }, - - /** - * Show a prompt modal to the user. - * @method _converse.api.prompt - * @param { String } title - The header text for the prompt - * @param { (String[]|String) } messages - The prompt text to show to the user - * @param { String } placeholder - The placeholder text for the prompt input - * @returns { Promise } A promise which resolves with the text provided by the - * user or `false` if the user canceled the prompt. - */ - async prompt (title, messages=[], placeholder='') { - if (typeof messages === 'string') { - messages = [messages]; - } - const model = new Model({ - title, - messages, - 'fields': [{ - 'name': 'reason', - 'placeholder': placeholder, - }], - 'type': 'prompt' - }) - const prompt = new Confirm({model}); - prompt.show(); - let result; - try { - result = (await prompt.confirmation).pop()?.value; - } catch (e) { - result = false; - } - prompt.remove(); - return result; - }, - - /** - * Show an alert modal to the user. - * @method _converse.api.alert - * @param { ('info'|'warn'|'error') } type - The type of alert. - * @param { String } title - The header text for the alert. - * @param { (String[]|String) } messages - The alert text to show to the user. - */ - alert (type, title, messages) { - if (typeof messages === 'string') { - messages = [messages]; - } - let level; - if (type === 'error') { - level = 'alert-danger'; - } else if (type === 'info') { - level = 'alert-info'; - } else if (type === 'warn') { - level = 'alert-warning'; - } - - if (alert === undefined) { - const model = new Model({ - 'title': title, - 'messages': messages, - 'level': level, - 'type': 'alert' - }) - alert = new Alert({model}); - } else { - alert.model.set({ - 'title': title, - 'messages': messages, - 'level': level - }); - } - alert.show(); - } - }); + Object.assign(_converse.api, modal_api); } }); - diff --git a/src/converse-profile.js b/src/converse-profile.js index b4fd107e6..90c133894 100644 --- a/src/converse-profile.js +++ b/src/converse-profile.js @@ -3,22 +3,16 @@ * @copyright The Converse.js contributors * @license Mozilla Public License (MPLv2) */ +import "modals/profile.js"; +import "modals/chat-status.js"; import "@converse/headless/converse-status"; import "@converse/headless/converse-vcard"; import "converse-modal"; 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_modal from "templates/profile_modal"; -import { BootstrapModal } from "./converse-modal.js"; import { __ } from './i18n'; import { _converse, api, converse } from "@converse/headless/converse-core"; -const u = converse.env.utils; - 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({ tagName: "div", events: { diff --git a/src/converse-rosterview.js b/src/converse-rosterview.js index ad928b0be..6b538bc96 100644 --- a/src/converse-rosterview.js +++ b/src/converse-rosterview.js @@ -6,23 +6,21 @@ import "@converse/headless/converse-chatboxes"; import "@converse/headless/converse-roster"; import "converse-modal"; +import "modals/add-contact.js"; 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_pending_contact from "templates/pending_contact.html"; import tpl_requesting_contact from "templates/requesting_contact.html"; import tpl_roster from "templates/roster.html"; import tpl_roster_filter from "templates/roster_filter.js"; import tpl_roster_item from "templates/roster_item.html"; -import { BootstrapModal } from "./converse-modal.js"; import { Model } from '@converse/skeletor/src/model.js'; import { OrderedListView } from "@converse/skeletor/src/overview"; import { View } from '@converse/skeletor/src/view.js'; import { __ } from './i18n'; 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; @@ -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({ initialize () { this.set({ @@ -195,6 +63,7 @@ converse.plugins.add('converse-rosterview', { }, }); + _converse.RosterFilterView = View.extend({ tagName: 'span', @@ -980,4 +849,3 @@ converse.plugins.add('converse-rosterview', { }); } }); - diff --git a/src/modals/add-contact.js b/src/modals/add-contact.js new file mode 100644 index 000000000..e5bee2a8e --- /dev/null +++ b/src/modals/add-contact.js @@ -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; diff --git a/src/modals/add-muc.js b/src/modals/add-muc.js index 879889524..04b37eb1f 100644 --- a/src/modals/add-muc.js +++ b/src/modals/add-muc.js @@ -1,5 +1,5 @@ -import tpl_add_chatroom_modal from "templates/add_chatroom_modal.js"; -import { BootstrapModal } from "../converse-modal.js"; +import tpl_add_muc from "./templates/add-muc.js"; +import BootstrapModal from "./base.js"; import { Strophe } from 'strophe.js/src/strophe'; import { __ } from '../i18n'; import { _converse, api, converse } from "@converse/headless/converse-core"; @@ -8,6 +8,7 @@ const u = converse.env.utils; export default BootstrapModal.extend({ + persistent: true, id: 'add-chatroom-modal', events: { @@ -28,7 +29,7 @@ export default BootstrapModal.extend({ const muc_domain = this.model.get('muc_domain') || api.settings.get('muc_domain'); 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, 'label_room_address': api.settings.get('muc_domain') ? __('Groupchat name') : __('Groupchat address'), 'chatroom_placeholder': placeholder, diff --git a/src/modals/alert.js b/src/modals/alert.js new file mode 100644 index 000000000..a9309b1d8 --- /dev/null +++ b/src/modals/alert.js @@ -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; diff --git a/src/modals/base.js b/src/modals/base.js new file mode 100644 index 000000000..597fd6f9f --- /dev/null +++ b/src/modals/base.js @@ -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; diff --git a/src/modals/chat-status.js b/src/modals/chat-status.js new file mode 100644 index 000000000..602fc18e5 --- /dev/null +++ b/src/modals/chat-status.js @@ -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; diff --git a/src/modals/confirm.js b/src/modals/confirm.js new file mode 100644 index 000000000..ce5897d51 --- /dev/null +++ b/src/modals/confirm.js @@ -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; diff --git a/src/modals/image.js b/src/modals/image.js index 00707cb5e..43275a42c 100644 --- a/src/modals/image.js +++ b/src/modals/image.js @@ -1,5 +1,5 @@ -import { BootstrapModal } from "../converse-modal.js"; -import tpl_image_modal from "../templates/image_modal.js"; +import BootstrapModal from "./base.js"; +import tpl_image_modal from "./templates/image.js"; export default BootstrapModal.extend({ diff --git a/src/modals/message-versions.js b/src/modals/message-versions.js index a8fe02842..2db467b16 100644 --- a/src/modals/message-versions.js +++ b/src/modals/message-versions.js @@ -1,10 +1,9 @@ -import { BootstrapModal } from "../converse-modal.js"; -import tpl_message_versions_modal from "../templates/message_versions_modal.js"; +import BootstrapModal from "./base.js"; +import tpl_message_versions_modal from "./templates/message-versions.js"; export default BootstrapModal.extend({ - // FIXME: this isn't globally unique - id: "message-versions-modal", + toHTML () { return tpl_message_versions_modal(this.model.toJSON()); } diff --git a/src/modals/moderator-tools.js b/src/modals/moderator-tools.js index 89e82039a..0b61828b5 100644 --- a/src/modals/moderator-tools.js +++ b/src/modals/moderator-tools.js @@ -1,8 +1,8 @@ +import BootstrapModal from "./base.js"; import log from "@converse/headless/log"; 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 { BootstrapModal } from "../converse-modal.js"; import { __ } from '../i18n'; import { api, converse } from "@converse/headless/converse-core"; @@ -12,7 +12,7 @@ let _converse; export default BootstrapModal.extend({ - id: "converse-modtools-modal", + persistent: true, initialize (attrs) { _converse = attrs._converse; diff --git a/src/modals/muc-commands.js b/src/modals/muc-commands.js index 26b2cf33f..9b0e475b2 100644 --- a/src/modals/muc-commands.js +++ b/src/modals/muc-commands.js @@ -1,4 +1,4 @@ -import { BootstrapModal } from "../converse-modal.js"; +import BootstrapModal from "./base.js"; import { __ } from '../i18n'; import { api, converse } from "@converse/headless/converse-core"; import log from "@converse/headless/log"; diff --git a/src/modals/muc-details.js b/src/modals/muc-details.js index 22ff4f9ff..d9d75276f 100644 --- a/src/modals/muc-details.js +++ b/src/modals/muc-details.js @@ -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 tpl_chatroom_details_modal from "../templates/chatroom_details_modal.js"; export default BootstrapModal.extend({ @@ -15,7 +15,7 @@ export default BootstrapModal.extend({ }, toHTML () { - return tpl_chatroom_details_modal(Object.assign( + return tpl_muc_details(Object.assign( this.model.toJSON(), { 'config': this.model.config.toJSON(), 'display_name': __('Groupchat info for %1$s', this.model.getDisplayName()), diff --git a/src/modals/muc-invite.js b/src/modals/muc-invite.js index 256a6bcac..4f672c333 100644 --- a/src/modals/muc-invite.js +++ b/src/modals/muc-invite.js @@ -1,5 +1,5 @@ -import tpl_muc_invite_modal from "templates/muc_invite_modal.js"; -import { BootstrapModal } from "../converse-modal.js"; +import BootstrapModal from "./base.js"; +import tpl_muc_invite_modal from "./templates/muc-invite.js"; import { _converse, converse } from "@converse/headless/converse-core"; const u = converse.env.utils; @@ -49,5 +49,3 @@ export default BootstrapModal.extend({ } } }); - - diff --git a/src/modals/muc-list.js b/src/modals/muc-list.js index d9f12a58c..06a673222 100644 --- a/src/modals/muc-list.js +++ b/src/modals/muc-list.js @@ -1,10 +1,10 @@ +import BootstrapModal from "./base.js"; import log from "@converse/headless/log"; import sizzle from 'sizzle'; 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_spinner from "templates/spinner.js"; -import { BootstrapModal } from "../converse-modal.js"; import { Strophe, $iq } from 'strophe.js/src/strophe'; import { __ } from '../i18n'; import { _converse, api, converse } from "@converse/headless/converse-core"; @@ -83,6 +83,7 @@ function toggleRoomInfo (ev) { export default BootstrapModal.extend({ id: "list-chatrooms-modal", + persistent: true, initialize () { this.items = []; diff --git a/src/modals/occupant.js b/src/modals/occupant.js index 18e424119..f2c135414 100644 --- a/src/modals/occupant.js +++ b/src/modals/occupant.js @@ -1,20 +1,19 @@ +import BootstrapModal from "./base.js"; import tpl_occupant_modal from "./templates/occupant.js"; -import { BootstrapModal } from "../converse-modal.js"; import { _converse, api } from "@converse/headless/converse-core"; const OccupantModal = BootstrapModal.extend({ - id: "muc-occupant-modal", initialize () { BootstrapModal.prototype.initialize.apply(this, arguments); this.listenTo(this.model, 'change', this.render); /** - * Triggered once the OccupantModal has been initialized - * @event _converse#userDetailsModalInitialized - * @type { _converse.ChatBox } - * @example _converse.api.listen.on('userDetailsModalInitialized', chatbox => { ... }); - */ + * Triggered once the OccupantModal has been initialized + * @event _converse#userDetailsModalInitialized + * @type { _converse.ChatBox } + * @example _converse.api.listen.on('userDetailsModalInitialized', chatbox => { ... }); + */ api.trigger('occupantModalInitialized', this.model); }, diff --git a/src/modals/profile.js b/src/modals/profile.js new file mode 100644 index 000000000..14512e045 --- /dev/null +++ b/src/modals/profile.js @@ -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; diff --git a/src/templates/add_contact_modal.js b/src/modals/templates/add-contact.js similarity index 97% rename from src/templates/add_contact_modal.js rename to src/modals/templates/add-contact.js index d0fad63ae..3587bef64 100644 --- a/src/templates/add_contact_modal.js +++ b/src/modals/templates/add-contact.js @@ -1,6 +1,6 @@ import { html } from "lit-html"; -import { __ } from '../i18n'; -import { modal_header_close_button } from "./buttons" +import { __ } from '../../i18n'; +import { modal_header_close_button } from "./buttons.js" export default (o) => { diff --git a/src/templates/add_chatroom_modal.js b/src/modals/templates/add-muc.js similarity index 98% rename from src/templates/add_chatroom_modal.js rename to src/modals/templates/add-muc.js index a9b3f135b..5cc33a10f 100644 --- a/src/templates/add_chatroom_modal.js +++ b/src/modals/templates/add-muc.js @@ -1,5 +1,5 @@ import xss from "xss/dist/xss"; -import { __ } from '../i18n'; +import { __ } from '../../i18n'; import { html } from "lit-html"; import { modal_header_close_button } from "./buttons" import { unsafeHTML } from "lit-html/directives/unsafe-html.js"; diff --git a/src/templates/alert_modal.js b/src/modals/templates/alert.js similarity index 90% rename from src/templates/alert_modal.js rename to src/modals/templates/alert.js index ef9fdd1d8..4f2c2ae19 100644 --- a/src/templates/alert_modal.js +++ b/src/modals/templates/alert.js @@ -1,5 +1,5 @@ import { html } from "lit-html"; -import { modal_header_close_button } from "./buttons" +import { modal_header_close_button } from "./buttons.js" export default (o) => html` diff --git a/src/templates/buttons.js b/src/modals/templates/buttons.js similarity index 91% rename from src/templates/buttons.js rename to src/modals/templates/buttons.js index a370a85a9..918cf2ad8 100644 --- a/src/templates/buttons.js +++ b/src/modals/templates/buttons.js @@ -1,8 +1,7 @@ -import { __ } from '../i18n'; +import { __ } from '../../i18n'; import { html } from "lit-html"; export const modal_close_button = html``; export const modal_header_close_button = html``; - diff --git a/src/templates/chat_status_modal.js b/src/modals/templates/chat-status.js similarity index 98% rename from src/templates/chat_status_modal.js rename to src/modals/templates/chat-status.js index 23258f686..e1e5f9297 100644 --- a/src/templates/chat_status_modal.js +++ b/src/modals/templates/chat-status.js @@ -1,5 +1,5 @@ import { html } from "lit-html"; -import { modal_header_close_button } from "./buttons" +import { modal_header_close_button } from "./buttons.js" export default (o) => html` diff --git a/src/templates/image_modal.js b/src/modals/templates/image.js similarity index 94% rename from src/templates/image_modal.js rename to src/modals/templates/image.js index 1a5fa256e..93a7ee791 100644 --- a/src/templates/image_modal.js +++ b/src/modals/templates/image.js @@ -1,6 +1,6 @@ import { html } from "lit-html"; -import { __ } from '../i18n'; -import { modal_close_button, modal_header_close_button } from "./buttons" +import { __ } from '../../i18n'; +import { modal_close_button, modal_header_close_button } from "./buttons.js" export default (o) => { diff --git a/src/templates/message_versions_modal.js b/src/modals/templates/message-versions.js similarity index 95% rename from src/templates/message_versions_modal.js rename to src/modals/templates/message-versions.js index fb7e54cfb..465a76616 100644 --- a/src/templates/message_versions_modal.js +++ b/src/modals/templates/message-versions.js @@ -1,7 +1,7 @@ -import { html } from "lit-html"; -import { __ } from '../i18n'; 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` diff --git a/src/templates/moderator_tools_modal.js b/src/modals/templates/moderator-tools.js similarity index 98% rename from src/templates/moderator_tools_modal.js rename to src/modals/templates/moderator-tools.js index f88d31af7..3de0ea552 100644 --- a/src/templates/moderator_tools_modal.js +++ b/src/modals/templates/moderator-tools.js @@ -1,7 +1,7 @@ import { html } from "lit-html"; -import { __ } from '../i18n'; -import spinner from "./spinner.js"; -import { modal_header_close_button } from "./buttons" +import { __ } from '../../i18n'; +import spinner from "../../templates/spinner.js"; +import { modal_header_close_button } from "./buttons.js" function getRoleHelpText (role) { diff --git a/src/templates/chatroom_details_modal.js b/src/modals/templates/muc-details.js similarity index 99% rename from src/templates/chatroom_details_modal.js rename to src/modals/templates/muc-details.js index c3ec44014..bc0309a56 100644 --- a/src/templates/chatroom_details_modal.js +++ b/src/modals/templates/muc-details.js @@ -1,6 +1,6 @@ -import { __ } from '../i18n'; +import { __ } from '../../i18n'; 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 xss from "xss/dist/xss"; diff --git a/src/templates/muc_invite_modal.js b/src/modals/templates/muc-invite.js similarity index 96% rename from src/templates/muc_invite_modal.js rename to src/modals/templates/muc-invite.js index f43e806a6..9e767ed3f 100644 --- a/src/templates/muc_invite_modal.js +++ b/src/modals/templates/muc-invite.js @@ -1,6 +1,6 @@ import { html } from "lit-html"; -import { __ } from '../i18n'; -import { modal_header_close_button } from "./buttons" +import { __ } from '../../i18n'; +import { modal_header_close_button } from "./buttons.js" export default (o) => { diff --git a/src/templates/list_chatrooms_modal.js b/src/modals/templates/muc-list.js similarity index 96% rename from src/templates/list_chatrooms_modal.js rename to src/modals/templates/muc-list.js index 6c2c907ac..91ec2338e 100644 --- a/src/templates/list_chatrooms_modal.js +++ b/src/modals/templates/muc-list.js @@ -1,8 +1,8 @@ -import { __ } from '../i18n'; +import { __ } from '../../i18n'; import { html } from "lit-html"; import { repeat } from 'lit-html/directives/repeat.js'; -import { modal_close_button, modal_header_close_button } from "./buttons" -import spinner from "./spinner.js"; +import { modal_close_button, modal_header_close_button } from "./buttons.js" +import spinner from "../../templates/spinner.js"; const form = (o) => { diff --git a/src/modals/templates/occupant.js b/src/modals/templates/occupant.js index 292ca4da6..45e8ee96b 100644 --- a/src/modals/templates/occupant.js +++ b/src/modals/templates/occupant.js @@ -1,5 +1,5 @@ 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'; diff --git a/src/templates/profile_modal.js b/src/modals/templates/profile.js similarity index 97% rename from src/templates/profile_modal.js rename to src/modals/templates/profile.js index dbcae2f76..79e251016 100644 --- a/src/templates/profile_modal.js +++ b/src/modals/templates/profile.js @@ -1,9 +1,9 @@ -import "../components/image_picker.js"; -import spinner from "./spinner.js"; -import { __ } from '../i18n'; +import "../../components/image_picker.js"; +import spinner from "../../templates/spinner.js"; +import { __ } from '../../i18n'; import { _converse, converse } from "@converse/headless/converse-core"; 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; diff --git a/src/templates/prompt.js b/src/modals/templates/prompt.js similarity index 97% rename from src/templates/prompt.js rename to src/modals/templates/prompt.js index ca2ec6012..5af7e3a06 100644 --- a/src/templates/prompt.js +++ b/src/modals/templates/prompt.js @@ -1,5 +1,5 @@ import { html } from "lit-html"; -import { __ } from '../i18n'; +import { __ } from '../../i18n'; const tpl_field = (f) => html` diff --git a/src/templates/user_details_modal.js b/src/modals/templates/user-details.js similarity index 97% rename from src/templates/user_details_modal.js rename to src/modals/templates/user-details.js index e81a0894b..567b6826d 100644 --- a/src/templates/user_details_modal.js +++ b/src/modals/templates/user-details.js @@ -1,7 +1,7 @@ -import { __ } from '../i18n'; +import { __ } from '../../i18n'; import { html } from "lit-html"; -import avatar from "./avatar.js"; -import { modal_close_button, modal_header_close_button } from "./buttons" +import avatar from "../../templates/avatar.js"; +import { modal_close_button, modal_header_close_button } from "./buttons.js" const device_fingerprint = (o) => { diff --git a/src/templates/user_settings_modal.js b/src/modals/templates/user-settings.js similarity index 96% rename from src/templates/user_settings_modal.js rename to src/modals/templates/user-settings.js index 4f0f19e05..fa606aaea 100644 --- a/src/templates/user_settings_modal.js +++ b/src/modals/templates/user-settings.js @@ -1,9 +1,9 @@ -import '../components/adhoc-commands.js'; +import '../../components/adhoc-commands.js'; import xss from "xss/dist/xss"; -import { __ } from '../i18n'; +import { __ } from '../../i18n'; import { api } from "@converse/headless/converse-core"; 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'; diff --git a/src/modals/user-details.js b/src/modals/user-details.js index 54be90fc9..7c5bcdfa4 100644 --- a/src/modals/user-details.js +++ b/src/modals/user-details.js @@ -1,6 +1,6 @@ +import BootstrapModal from "./base.js"; import log from "@converse/headless/log"; -import tpl_user_details_modal from "../templates/user_details_modal.js"; -import { BootstrapModal } from "../converse-modal.js"; +import tpl_user_details_modal from "./templates/user-details.js"; import { __ } from '../i18n'; import { _converse, api, converse } from "@converse/headless/converse-core"; @@ -8,7 +8,7 @@ const u = converse.env.utils; const UserDetailsModal = BootstrapModal.extend({ - id: "user-details-modal", + persistent: true, events: { 'click button.refresh-contact': 'refreshContact', diff --git a/src/modals/user-settings.js b/src/modals/user-settings.js index c709f86cf..755bc11aa 100644 --- a/src/modals/user-settings.js +++ b/src/modals/user-settings.js @@ -1,5 +1,5 @@ -import { BootstrapModal } from "../converse-modal.js"; -import tpl_user_settings_modal from "templates/user_settings_modal"; +import BootstrapModal from "./base.js"; +import tpl_user_settings_modal from "./templates/user-settings.js"; let _converse; @@ -21,4 +21,3 @@ export default BootstrapModal.extend({ ); } }); -