From 1a161ad2c7aeda1f818953668b0b79ffa9fdd46b Mon Sep 17 00:00:00 2001 From: JC Brand Date: Wed, 2 Dec 2020 18:37:32 +0100 Subject: [PATCH] Add api for managing modals Set passed-in properties on modal --- karma.conf.js | 2 +- spec/controlbox.js | 13 +++-- spec/mock.js | 2 +- spec/modtools.js | 20 ++++---- spec/muc.js | 22 ++++---- spec/muclist.js | 2 +- spec/omemo.js | 2 +- spec/presence.js | 8 +-- spec/protocol.js | 2 +- spec/user-details-modal.js | 7 +-- src/components/message-body.js | 9 +--- src/converse-chatview.js | 5 +- src/converse-modal.js | 94 +++++++++++++++++++++++++++------- src/converse-muc-views.js | 38 ++++---------- src/converse-profile.js | 15 ++---- src/converse-roomslist.js | 6 +-- src/converse-rosterview.js | 5 +- src/modals/alert.js | 1 + src/modals/base.js | 12 +++-- src/modals/confirm.js | 1 + src/modals/image.js | 2 + src/modals/message-versions.js | 2 +- src/modals/moderator-tools.js | 1 + src/modals/user-details.js | 33 ++++++------ 24 files changed, 168 insertions(+), 136 deletions(-) diff --git a/karma.conf.js b/karma.conf.js index 03c27cc75..8e0939fd3 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -24,6 +24,7 @@ module.exports = function(config) { { pattern: "node_modules/sinon/pkg/sinon.js", type: 'module' }, { pattern: "spec/mock.js", type: 'module' }, + { pattern: "spec/user-details-modal.js", type: 'module' }, { pattern: "spec/spoilers.js", type: 'module' }, { pattern: "spec/emojis.js", type: 'module' }, { pattern: "spec/muclist.js", type: 'module' }, @@ -44,7 +45,6 @@ module.exports = function(config) { { pattern: "spec/controlbox.js", type: 'module' }, { pattern: "spec/roster.js", type: 'module' }, { pattern: "spec/chatbox.js", type: 'module' }, - { pattern: "spec/user-details-modal.js", type: 'module' }, { pattern: "spec/messages.js", type: 'module' }, { pattern: "spec/corrections.js", type: 'module' }, { pattern: "spec/styling.js", type: 'module' }, diff --git a/spec/controlbox.js b/spec/controlbox.js index 794f21841..f4040bdcc 100644 --- a/spec/controlbox.js +++ b/spec/controlbox.js @@ -154,8 +154,7 @@ describe("The Controlbox", function () { await mock.openControlBox(_converse); var cbview = _converse.chatboxviews.get('controlbox'); cbview.el.querySelector('.change-status').click() - var modal = _converse.xmppstatusview.status_modal; - + const modal = _converse.api.modal.get('modal-status-change'); await u.waitUntil(() => u.isVisible(modal.el), 1000); const view = _converse.xmppstatusview; modal.el.querySelector('label[for="radio-busy"]').click(); // Change status to "dnd" @@ -183,7 +182,7 @@ describe("The Controlbox", function () { await mock.openControlBox(_converse); const cbview = _converse.chatboxviews.get('controlbox'); cbview.el.querySelector('.change-status').click() - const modal = _converse.xmppstatusview.status_modal; + const modal = _converse.api.modal.get('modal-status-change'); await u.waitUntil(() => u.isVisible(modal.el), 1000); const view = _converse.xmppstatusview; @@ -219,7 +218,7 @@ describe("The 'Add Contact' widget", function () { const cbview = _converse.chatboxviews.get('controlbox'); cbview.el.querySelector('.add-contact').click() - const modal = _converse.rosterview.add_contact_modal; + const modal = _converse.api.modal.get('add-contact-modal'); await u.waitUntil(() => u.isVisible(modal.el), 1000); expect(modal.el.querySelector('form.add-xmpp-contact')).not.toBe(null); @@ -252,7 +251,7 @@ describe("The 'Add Contact' widget", function () { mock.openControlBox(_converse); const cbview = _converse.chatboxviews.get('controlbox'); cbview.el.querySelector('.add-contact').click() - const modal = _converse.rosterview.add_contact_modal; + const modal = _converse.api.modal.get('add-contact-modal'); expect(modal.jid_auto_complete).toBe(undefined); expect(modal.name_auto_complete).toBe(undefined); @@ -299,7 +298,7 @@ describe("The 'Add Contact' widget", function () { const cbview = _converse.chatboxviews.get('controlbox'); cbview.el.querySelector('.add-contact').click() - const modal = _converse.rosterview.add_contact_modal; + const modal = _converse.api.modal.get('add-contact-modal'); await u.waitUntil(() => u.isVisible(modal.el), 1000); // We only have autocomplete for the name input @@ -369,7 +368,7 @@ describe("The 'Add Contact' widget", function () { const cbview = _converse.chatboxviews.get('controlbox'); cbview.el.querySelector('.add-contact').click() - modal = _converse.rosterview.add_contact_modal; + modal = _converse.api.modal.get('add-contact-modal'); await u.waitUntil(() => u.isVisible(modal.el), 1000); expect(modal.jid_auto_complete).toBe(undefined); diff --git a/spec/mock.js b/spec/mock.js index d4db1678f..ff37782e7 100644 --- a/spec/mock.js +++ b/spec/mock.js @@ -152,7 +152,7 @@ window.addEventListener('converse-loaded', () => { const roomspanel = view.roomspanel; roomspanel.el.querySelector('.show-add-muc-modal').click(); mock.closeControlBox(_converse); - const modal = roomspanel.add_room_modal; + const modal = _converse.api.modal.get('add-chatroom-modal'); await u.waitUntil(() => u.isVisible(modal.el), 1500) modal.el.querySelector('input[name="chatroom"]').value = jid; if (nick) { diff --git a/spec/modtools.js b/spec/modtools.js index 74e247fe6..b936928ea 100644 --- a/spec/modtools.js +++ b/spec/modtools.js @@ -1,4 +1,4 @@ -/*global mock */ +/*global mock, converse */ const _ = converse.env._; const $iq = converse.env.$iq; @@ -8,13 +8,13 @@ const Strophe = converse.env.Strophe; const u = converse.env.utils; -async function openModtools (view) { +async function openModtools (_converse, view) { const textarea = view.el.querySelector('.chat-textarea'); textarea.value = '/modtools'; const enter = { 'target': textarea, 'preventDefault': function preventDefault () {}, 'keyCode': 13 }; view.onKeyDown(enter); await u.waitUntil(() => view.showModeratorToolsModal.calls.count()); - const modal = view.modtools_modal; + const modal = _converse.api.modal.get('converse-modtools-modal'); await u.waitUntil(() => u.isVisible(modal.el), 1000); return modal; } @@ -40,7 +40,7 @@ describe("The groupchat moderator tool", function () { const view = _converse.chatboxviews.get(muc_jid); await u.waitUntil(() => (view.model.occupants.length === 5), 1000); - const modal = await openModtools(view); + const modal = await openModtools(_converse, view); let tab = modal.el.querySelector('#affiliations-tab'); // Clear so that we don't match older stanzas _converse.connection.IQ_stanzas = []; @@ -163,7 +163,7 @@ describe("The groupchat moderator tool", function () { // Clear so that we don't match older stanzas _converse.connection.IQ_stanzas = []; - const modal = await openModtools(view); + const modal = await openModtools(_converse, view); const select = modal.el.querySelector('.select-affiliation'); expect(select.value).toBe('owner'); select.value = 'member'; @@ -270,7 +270,7 @@ describe("The groupchat moderator tool", function () { view.onKeyDown(enter); await u.waitUntil(() => view.showModeratorToolsModal.calls.count()); - const modal = view.modtools_modal; + const modal = _converse.api.modal.get('converse-modtools-modal'); await u.waitUntil(() => u.isVisible(modal.el), 1000); const tab = modal.el.querySelector('#roles-tab'); @@ -326,7 +326,7 @@ describe("The groupchat moderator tool", function () { await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', [], members); const view = _converse.chatboxviews.get(muc_jid); await u.waitUntil(() => (view.model.occupants.length === 5)); - const modal = await openModtools(view); + const modal = await openModtools(_converse, view); const tab = modal.el.querySelector('#affiliations-tab'); // Clear so that we don't match older stanzas _converse.connection.IQ_stanzas = []; @@ -375,7 +375,7 @@ describe("The groupchat moderator tool", function () { await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', [], members); const view = _converse.chatboxviews.get(muc_jid); await u.waitUntil(() => (view.model.occupants.length === 2)); - const modal = await openModtools(view); + const modal = await openModtools(_converse, view); // Clear so that we don't match older stanzas _converse.connection.IQ_stanzas = []; @@ -443,7 +443,7 @@ describe("The groupchat moderator tool", function () { await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', [], members); const view = _converse.chatboxviews.get(muc_jid); await u.waitUntil(() => (view.model.occupants.length === 3)); - const modal = await openModtools(view); + const modal = await openModtools(_converse, view); const tab = modal.el.querySelector('#affiliations-tab'); // Clear so that we don't match older stanzas _converse.connection.IQ_stanzas = []; @@ -481,7 +481,7 @@ describe("The groupchat moderator tool", function () { view.onKeyDown(enter); await u.waitUntil(() => view.showModeratorToolsModal.calls.count()); - const modal = view.modtools_modal; + const modal = _converse.api.modal.get('converse-modtools-modal'); const occupant = view.model.occupants.findWhere({'jid': _converse.bare_jid}); expect(modal.getAssignableAffiliations(occupant)).toEqual(['owner', 'admin', 'member', 'outcast', 'none']); diff --git a/spec/muc.js b/spec/muc.js index 0050f0c6a..9ecff004c 100644 --- a/spec/muc.js +++ b/spec/muc.js @@ -1973,7 +1973,7 @@ describe("Groupchats", function () { await u.waitUntil(() => view.el.querySelector('.open-invite-modal')); view.el.querySelector('.open-invite-modal').click(); - const modal = view.muc_invite_modal; + const modal = _converse.api.modal.get('muc-invite-modal'); await u.waitUntil(() => u.isVisible(modal.el), 1000) expect(modal.el.querySelectorAll('#invitee_jids').length).toBe(1); @@ -2456,7 +2456,7 @@ describe("Groupchats", function () { const info_el = view.el.querySelector(".show-room-details-modal"); info_el.click(); - const modal = view.model.room_details_modal; + const modal = _converse.api.modal.get('room-details-modal'); await u.waitUntil(() => u.isVisible(modal.el), 1000); let features_list = modal.el.querySelector('.features-list'); @@ -4629,7 +4629,7 @@ describe("Groupchats", function () { const roomspanel = _converse.chatboxviews.get('controlbox').roomspanel; roomspanel.el.querySelector('.show-add-muc-modal').click(); mock.closeControlBox(_converse); - const modal = roomspanel.add_room_modal; + const modal = _converse.api.modal.get('add-chatroom-modal'); await u.waitUntil(() => u.isVisible(modal.el), 1000) let label_name = modal.el.querySelector('label[for="chatroom"]'); @@ -4670,7 +4670,7 @@ describe("Groupchats", function () { const roomspanel = _converse.chatboxviews.get('controlbox').roomspanel; roomspanel.el.querySelector('.show-add-muc-modal').click(); mock.closeControlBox(_converse); - const modal = roomspanel.add_room_modal; + const modal = _converse.api.modal.get('add-chatroom-modal'); await u.waitUntil(() => u.isVisible(modal.el), 1000) const name_input = modal.el.querySelector('input[name="chatroom"]'); name_input.value = 'lounge@montague.lit'; @@ -4693,7 +4693,7 @@ describe("Groupchats", function () { const roomspanel = _converse.chatboxviews.get('controlbox').roomspanel; roomspanel.el.querySelector('.show-add-muc-modal').click(); mock.closeControlBox(_converse); - const modal = roomspanel.add_room_modal; + const modal = _converse.api.modal.get('add-chatroom-modal'); await u.waitUntil(() => u.isVisible(modal.el), 1000) const label_nick = modal.el.querySelector('label[for="nickname"]'); expect(label_nick.textContent.trim()).toBe('Nickname:'); @@ -4712,7 +4712,7 @@ describe("Groupchats", function () { const roomspanel = _converse.chatboxviews.get('controlbox').roomspanel; roomspanel.el.querySelector('.show-add-muc-modal').click(); mock.closeControlBox(_converse); - const modal = roomspanel.add_room_modal; + const modal = _converse.api.modal.get('add-chatroom-modal'); await u.waitUntil(() => u.isVisible(modal.el), 1000) const label_nick = modal.el.querySelector('label[for="nickname"]'); expect(label_nick.textContent.trim()).toBe('Nickname:'); @@ -4729,7 +4729,7 @@ describe("Groupchats", function () { await mock.openControlBox(_converse); const roomspanel = _converse.chatboxviews.get('controlbox').roomspanel; roomspanel.el.querySelector('.show-add-muc-modal').click(); - const modal = roomspanel.add_room_modal; + const modal = _converse.api.modal.get('add-chatroom-modal'); await u.waitUntil(() => u.isVisible(modal.el), 1000) expect(modal.el.querySelector('.modal-title').textContent.trim()).toBe('Enter a new Groupchat'); spyOn(_converse.ChatRoom.prototype, 'getDiscoInfo').and.callFake(() => Promise.resolve()); @@ -4769,7 +4769,7 @@ describe("Groupchats", function () { await mock.openControlBox(_converse); const roomspanel = _converse.chatboxviews.get('controlbox').roomspanel; roomspanel.el.querySelector('.show-add-muc-modal').click(); - const modal = roomspanel.add_room_modal; + const modal = _converse.api.modal.get('add-chatroom-modal'); await u.waitUntil(() => u.isVisible(modal.el), 1000) expect(modal.el.querySelector('.modal-title').textContent.trim()).toBe('Enter a new Groupchat'); spyOn(_converse.ChatRoom.prototype, 'getDiscoInfo').and.callFake(() => Promise.resolve()); @@ -4812,7 +4812,7 @@ describe("Groupchats", function () { const roomspanel = _converse.chatboxviews.get('controlbox').roomspanel; roomspanel.el.querySelector('.show-list-muc-modal').click(); mock.closeControlBox(_converse); - const modal = roomspanel.muc_list_modal; + const modal = _converse.api.modal.get('list-chatrooms-modal'); await u.waitUntil(() => u.isVisible(modal.el), 1000); spyOn(_converse.ChatRoom.prototype, 'getDiscoInfo').and.callFake(() => Promise.resolve()); roomspanel.delegateEvents(); // We need to rebind all events otherwise our spy won't be called @@ -4889,7 +4889,7 @@ describe("Groupchats", function () { const roomspanel = _converse.chatboxviews.get('controlbox').roomspanel; roomspanel.el.querySelector('.show-list-muc-modal').click(); mock.closeControlBox(_converse); - const modal = roomspanel.muc_list_modal; + const modal = _converse.api.modal.get('list-chatrooms-modal'); await u.waitUntil(() => u.isVisible(modal.el), 1000); const server_input = modal.el.querySelector('input[name="server"]'); expect(server_input.value).toBe('muc.example.org'); @@ -4906,7 +4906,7 @@ describe("Groupchats", function () { const roomspanel = _converse.chatboxviews.get('controlbox').roomspanel; roomspanel.el.querySelector('.show-list-muc-modal').click(); mock.closeControlBox(_converse); - const modal = roomspanel.muc_list_modal; + const modal = _converse.api.modal.get('list-chatrooms-modal'); await u.waitUntil(() => u.isVisible(modal.el), 1000); spyOn(_converse.ChatRoom.prototype, 'getDiscoInfo').and.callFake(() => Promise.resolve()); roomspanel.delegateEvents(); // We need to rebind all events otherwise our spy won't be called diff --git a/spec/muclist.js b/spec/muclist.js index 265b7629d..e75724804 100644 --- a/spec/muclist.js +++ b/spec/muclist.js @@ -204,7 +204,7 @@ describe("A groupchat shown in the groupchats list", function () { const info_el = _converse.rooms_list_view.el.querySelector(".room-info"); info_el.click(); - const modal = view.model.room_details_modal; + const modal = _converse.api.modal.get('room-details-modal'); await u.waitUntil(() => u.isVisible(modal.el), 1000); let els = modal.el.querySelectorAll('p.room-info'); expect(els[0].textContent).toBe("Name: A Dark Cave") diff --git a/spec/omemo.js b/spec/omemo.js index 484f5260b..318a26e84 100644 --- a/spec/omemo.js +++ b/spec/omemo.js @@ -1463,7 +1463,7 @@ describe("The OMEMO module", function() { const view = _converse.chatboxviews.get(contact_jid); const show_modal_button = view.el.querySelector('.show-user-details-modal'); show_modal_button.click(); - const modal = view.user_details_modal; + const modal = _converse.api.modal.get('user-details-modal'); await u.waitUntil(() => u.isVisible(modal.el), 1000); let iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, contact_jid)); expect(Strophe.serialize(iq_stanza)).toBe( diff --git a/spec/presence.js b/spec/presence.js index 8043c1916..7a39551e4 100644 --- a/spec/presence.js +++ b/spec/presence.js @@ -1,4 +1,5 @@ -/*global mock */ +/*global mock, converse */ + // See: https://xmpp.org/rfcs/rfc3921.html const original_timeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; @@ -76,7 +77,7 @@ describe("A sent presence stanza", function () { const cbview = _converse.chatboxviews.get('controlbox'); const change_status_el = await u.waitUntil(() => cbview.el.querySelector('.change-status')); change_status_el.click() - const modal = _converse.xmppstatusview.status_modal; + let modal = _converse.api.modal.get('modal-status-change'); await u.waitUntil(() => u.isVisible(modal.el), 1000); const msg = 'My custom status'; modal.el.querySelector('input[name="status_message"]').value = msg; @@ -90,10 +91,11 @@ describe("A sent presence stanza", function () { `0`+ ``+ ``) - await u.waitUntil(() => modal.el.getAttribute('aria-hidden') === "true"); await u.waitUntil(() => !u.isVisible(modal.el)); + cbview.el.querySelector('.change-status').click() + modal = _converse.api.modal.get('modal-status-change'); await u.waitUntil(() => modal.el.getAttribute('aria-hidden') === "false", 1000); modal.el.querySelector('label[for="radio-busy"]').click(); // Change status to "dnd" modal.el.querySelector('[type="submit"]').click(); diff --git a/spec/protocol.js b/spec/protocol.js index 39e8aa7ba..7b36cdf5c 100644 --- a/spec/protocol.js +++ b/spec/protocol.js @@ -66,7 +66,7 @@ describe("The Protocol", function () { }); cbview.el.querySelector('.add-contact').click() - const modal = _converse.rosterview.add_contact_modal; + const modal = _converse.api.modal.get('add-contact-modal'); await u.waitUntil(() => u.isVisible(modal.el), 1000); spyOn(modal, "addContactFromForm").and.callThrough(); modal.delegateEvents(); diff --git a/spec/user-details-modal.js b/spec/user-details-modal.js index 1eb59727a..bf1724330 100644 --- a/spec/user-details-modal.js +++ b/spec/user-details-modal.js @@ -18,7 +18,7 @@ describe("The User Details Modal", function () { const view = _converse.chatboxviews.get(contact_jid); let show_modal_button = view.el.querySelector('.show-user-details-modal'); show_modal_button.click(); - const modal = view.user_details_modal; + const modal = _converse.api.modal.get('user-details-modal'); await u.waitUntil(() => u.isVisible(modal.el), 1000); spyOn(window, 'confirm').and.returnValue(true); spyOn(view.model.contact, 'removeFromRoster').and.callFake(callback => callback()); @@ -45,7 +45,7 @@ describe("The User Details Modal", function () { const view = _converse.chatboxviews.get(contact_jid); let show_modal_button = view.el.querySelector('.show-user-details-modal'); show_modal_button.click(); - const modal = view.user_details_modal; + let modal = _converse.api.modal.get('user-details-modal'); await u.waitUntil(() => u.isVisible(modal.el), 2000); spyOn(window, 'confirm').and.returnValue(true); @@ -53,7 +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(modal.el)) await u.waitUntil(() => u.isVisible(document.querySelector('.alert-danger')), 2000); const header = document.querySelector('.alert-danger .modal-title'); @@ -63,6 +63,7 @@ describe("The User Details Modal", function () { document.querySelector('.alert-danger button.close').click(); show_modal_button = view.el.querySelector('.show-user-details-modal'); show_modal_button.click(); + modal = _converse.api.modal.get('user-details-modal'); await u.waitUntil(() => u.isVisible(modal.el), 2000) show_modal_button = view.el.querySelector('.show-user-details-modal'); diff --git a/src/components/message-body.js b/src/components/message-body.js index e2c129dbb..d4f12d85e 100644 --- a/src/components/message-body.js +++ b/src/components/message-body.js @@ -15,14 +15,9 @@ export default class MessageBody extends CustomElement { } } - showImageModal (ev) { + showImageModal (ev) { // eslint-disable-line class-methods-use-this ev.preventDefault(); - if (this.image_modal === undefined) { - this.image_modal = new ImageModal(); - } - this.image_modal.src = ev.target.src; - this.image_modal.render(); - this.image_modal.show(ev); + api.modal.create(ImageModal, {'src': ev.target.src}, ev).show(ev); } render () { diff --git a/src/converse-chatview.js b/src/converse-chatview.js index d96d0a6a2..1f1fd65b0 100644 --- a/src/converse-chatview.js +++ b/src/converse-chatview.js @@ -238,10 +238,7 @@ export const ChatBoxView = View.extend({ showUserDetailsModal (ev) { ev.preventDefault(); - if (this.user_details_modal === undefined) { - this.user_details_modal = new UserDetailsModal({model: this.model}); - } - this.user_details_modal.show(ev); + api.modal.show(UserDetailsModal, {model: this.model}, ev); }, onDragOver (evt) { diff --git a/src/converse-modal.js b/src/converse-modal.js index 801bd4e4a..ca13e5c50 100644 --- a/src/converse-modal.js +++ b/src/converse-modal.js @@ -7,15 +7,77 @@ 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 { _converse, converse } from "@converse/headless/converse-core"; +import { _converse, api, converse } from "@converse/headless/converse-core"; converse.env.BootstrapModal = BootstrapModal; // expose to plugins -let alert; +let modals = []; + const modal_api = { + + /** + * API namespace for methods relating to modals + * @namespace _converse.api.modal + * @memberOf _converse.api + */ + modal: { + /** + * Shows a modal of type `ModalClass` to the user. + * Will create a new instance of that class if an existing one isn't + * found. + * @param { Class } ModalClass + * @param { [Object] } properties - Optional properties that will be + * set on a newly created modal instance (if no pre-existing modal was + * found). + * @param { [Event] } event - The DOM event that causes the modal to be shown. + */ + show (ModalClass, properties, ev) { + const modal = this.get(ModalClass.id) || this.create(ModalClass, properties); + modal.show(ev); + return modal; + }, + + /** + * Return a modal with the passed-in identifier, if it exists. + * @param { String } id + */ + get (id) { + return modals.filter(m => m.id == id).pop(); + }, + + /** + * Create a modal of the passed-in type. + * @param { Class } ModalClass + * @param { [Object] } properties - Optional properties that will be + * set on the modal instance. + */ + create (ModalClass, properties) { + const modal = new ModalClass(properties); + modals.push(modal); + return modal; + }, + + /** + * Remove a particular modal + * @param { View } modal + */ + remove (modal) { + modals = modals.filter(m => m !== modal); + modal.remove(); + }, + + /** + * Remove all modals + */ + removeAll () { + modals.forEach(m => m.remove()); + modals = []; + } + }, + /** * Show a confirm modal to the user. * @method _converse.api.confirm @@ -101,22 +163,13 @@ const modal_api = { 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(); + const model = new Model({ + 'title': title, + 'messages': messages, + 'level': level, + 'type': 'alert' + }) + api.modal.show(Alert, {model}); } } @@ -124,12 +177,15 @@ const modal_api = { converse.plugins.add('converse-modal', { initialize () { - _converse.api.listen.on('disconnect', () => { + api.listen.on('disconnect', () => { const container = document.querySelector("#converse-modals"); if (container) { container.innerHTML = ''; } }); + + api.listen.on('clearSession', () => api.modal.removeAll()); + Object.assign(_converse.api, modal_api); } }); diff --git a/src/converse-muc-views.js b/src/converse-muc-views.js index a61a483d4..9b25f35ce 100644 --- a/src/converse-muc-views.js +++ b/src/converse-muc-views.js @@ -501,29 +501,24 @@ export const ChatRoomView = ChatBoxView.extend({ if (!this.verifyRoles(['moderator'])) { return; } - if (typeof this.model.modtools_modal === 'undefined') { - const model = new Model({'affiliation': affiliation}); - this.modtools_modal = new ModeratorToolsModal({model, _converse, 'chatroomview': this}); + let modal = api.modal.get(ModeratorToolsModal.id); + if (modal) { + modal.model.set('affiliation', affiliation); } else { - this.modtools_modal.set('affiliation', affiliation); + const model = new Model({'affiliation': affiliation}); + modal = api.modal.create(ModeratorToolsModal, {model, _converse, 'chatroomview': this}); } - this.modtools_modal.show(); + modal.show(); }, showRoomDetailsModal (ev) { ev.preventDefault(); - if (this.model.room_details_modal === undefined) { - this.model.room_details_modal = new RoomDetailsModal({'model': this.model}); - } - this.model.room_details_modal.show(ev); + api.modal.show(RoomDetailsModal, {'model': this.model}, ev); }, showOccupantDetailsModal (ev, message) { ev.preventDefault(); - if (this.model.occupant_modal === undefined) { - this.model.occupant_modal = new OccupantModal({'model': message.occupant}); - } - this.model.occupant_modal.show(ev); + api.modal.show(OccupantModal, {'model': message.occupant}, ev); }, showChatStateNotification (message) { @@ -679,12 +674,7 @@ export const ChatRoomView = ChatBoxView.extend({ showInviteModal (ev) { ev.preventDefault(); - if (this.muc_invite_modal === undefined) { - this.muc_invite_modal = new MUCInviteModal({'model': new Model()}); - // TODO: remove once we have API for sending direct invite - this.muc_invite_modal.chatroomview = this; - } - this.muc_invite_modal.show(ev); + api.modal.show(MUCInviteModal, {'model': new Model(), 'chatroomview': this}, ev); }, @@ -1365,17 +1355,11 @@ export const RoomsPanel = View.extend({ }, showAddRoomModal (ev) { - if (this.add_room_modal === undefined) { - this.add_room_modal = new AddMUCModal({'model': this.model}); - } - this.add_room_modal.show(ev); + api.modal.show(AddMUCModal, {'model': this.model}, ev); }, showMUCListModal(ev) { - if (this.muc_list_modal === undefined) { - this.muc_list_modal = new MUCListModal({'model': this.model}); - } - this.muc_list_modal.show(ev); + api.modal.show(MUCListModal, {'model': this.model}, ev); } }); diff --git a/src/converse-profile.js b/src/converse-profile.js index 90c133894..f34600097 100644 --- a/src/converse-profile.js +++ b/src/converse-profile.js @@ -61,26 +61,17 @@ converse.plugins.add('converse-profile', { showProfileModal (ev) { ev.preventDefault(); - if (this.profile_modal === undefined) { - this.profile_modal = new _converse.ProfileModal({model: this.model}); - } - this.profile_modal.show(ev); + api.modal.show(_converse.ProfileModal, {model: this.model}, ev); }, showStatusChangeModal (ev) { ev.preventDefault(); - if (this.status_modal === undefined) { - this.status_modal = new _converse.ChatStatusModal({model: this.model}); - } - this.status_modal.show(ev); + api.modal.show(_converse.ChatStatusModal, {model: this.model}, ev); }, showUserSettingsModal(ev) { ev.preventDefault(); - if (this.user_settings_modal === undefined) { - this.user_settings_modal = new UserSettingsModal({model: this.model, _converse}); - } - this.user_settings_modal.show(ev); + api.modal.show(UserSettingsModal, {model: this.model, _converse}, ev); }, logOut (ev) { diff --git a/src/converse-roomslist.js b/src/converse-roomslist.js index 6324b0ace..a59c0b6e1 100644 --- a/src/converse-roomslist.js +++ b/src/converse-roomslist.js @@ -109,10 +109,7 @@ converse.plugins.add('converse-roomslist', { const jid = ev.target.getAttribute('data-room-jid'); const room = _converse.chatboxes.get(jid); ev.preventDefault(); - if (room.room_details_modal === undefined) { - room.room_details_modal = new RoomDetailsModal({'model': room}); - } - room.room_details_modal.show(ev); + api.modal.show(RoomDetailsModal, {'model': room}, ev); }, async openRoom (ev) { @@ -183,4 +180,3 @@ converse.plugins.add('converse-roomslist', { api.listen.on('reconnected', initRoomsListView); } }); - diff --git a/src/converse-rosterview.js b/src/converse-rosterview.js index 6b538bc96..b8cd0762a 100644 --- a/src/converse-rosterview.js +++ b/src/converse-rosterview.js @@ -629,10 +629,7 @@ converse.plugins.add('converse-rosterview', { }, showAddContactModal (ev) { - if (this.add_contact_modal === undefined) { - this.add_contact_modal = new _converse.AddContactModal({'model': new Model()}); - } - this.add_contact_modal.show(ev); + api.modal.show(_converse.AddContactModal, {'model': new Model()}, ev); }, createRosterFilter () { diff --git a/src/modals/alert.js b/src/modals/alert.js index a9309b1d8..43d05bab9 100644 --- a/src/modals/alert.js +++ b/src/modals/alert.js @@ -4,6 +4,7 @@ import { __ } from '../i18n'; const Alert = BootstrapModal.extend({ + id: 'alert-modal', initialize () { BootstrapModal.prototype.initialize.apply(this, arguments); diff --git a/src/modals/base.js b/src/modals/base.js index 597fd6f9f..5dcc5cab2 100644 --- a/src/modals/base.js +++ b/src/modals/base.js @@ -2,7 +2,7 @@ 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 { _converse, api, converse } from "@converse/headless/converse-core"; import { render } from 'lit-html'; const { sizzle } = converse.env; @@ -16,9 +16,15 @@ const BaseModal = View.extend({ 'click .nav-item .nav-link': 'switchTab' }, - initialize () { + initialize (options) { + if (!this.id) { + throw new Error("Each modal class must have a unique id attribute"); + } this.render() + // Allow properties to be set via passed in options + Object.assign(this, options); + this.el.setAttribute('tabindex', '-1'); this.el.setAttribute('role', 'dialog'); this.el.setAttribute('aria-hidden', 'true'); @@ -36,7 +42,7 @@ const BaseModal = View.extend({ onHide () { u.removeClass('selected', this.trigger_el); - !this.persistent && this.remove(); + !this.persistent && api.modal.remove(this); }, insertIntoDOM () { diff --git a/src/modals/confirm.js b/src/modals/confirm.js index ce5897d51..e38899dfd 100644 --- a/src/modals/confirm.js +++ b/src/modals/confirm.js @@ -6,6 +6,7 @@ const u = converse.env.utils; const Confirm = BootstrapModal.extend({ + id: 'confirm-modal', events: { 'submit .confirm': 'onConfimation' }, diff --git a/src/modals/image.js b/src/modals/image.js index 43275a42c..3a7661470 100644 --- a/src/modals/image.js +++ b/src/modals/image.js @@ -3,6 +3,8 @@ import tpl_image_modal from "./templates/image.js"; export default BootstrapModal.extend({ + id: 'image-modal', + toHTML () { return tpl_image_modal({ 'src': this.src, diff --git a/src/modals/message-versions.js b/src/modals/message-versions.js index 2db467b16..a8e045ca2 100644 --- a/src/modals/message-versions.js +++ b/src/modals/message-versions.js @@ -3,7 +3,7 @@ import tpl_message_versions_modal from "./templates/message-versions.js"; export default BootstrapModal.extend({ - + 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 0b61828b5..1dd7bb49b 100644 --- a/src/modals/moderator-tools.js +++ b/src/modals/moderator-tools.js @@ -12,6 +12,7 @@ let _converse; export default BootstrapModal.extend({ + id: "converse-modtools-modal", persistent: true, initialize (attrs) { diff --git a/src/modals/user-details.js b/src/modals/user-details.js index 7c5bcdfa4..cc7ab4bc7 100644 --- a/src/modals/user-details.js +++ b/src/modals/user-details.js @@ -7,7 +7,22 @@ import { _converse, api, converse } from "@converse/headless/converse-core"; const u = converse.env.utils; +function removeContact (contact) { + contact.removeFromRoster( + () => contact.destroy(), + (e) => { + e && log.error(e); + api.alert('error', __('Error'), [ + __('Sorry, there was an error while trying to remove %1$s as a contact.', + contact.getDisplayName()) + ]); + } + ); +} + + const UserDetailsModal = BootstrapModal.extend({ + id: 'user-details-modal', persistent: true, events: { @@ -74,23 +89,11 @@ const UserDetailsModal = BootstrapModal.extend({ if (!api.settings.get('allow_contact_removal')) { return; } const result = confirm(__("Are you sure you want to remove this contact?")); if (result === true) { - this.modal.hide(); - // XXX: This is annoying but necessary to get tests to pass. - // The `dismissHandler` in bootstrap.native tries to + // XXX: The `dismissHandler` in bootstrap.native tries to // reference the remove button after it's been cleared from // the DOM, so we delay removing the contact to give it time. - setTimeout(() => { - this.model.contact.removeFromRoster( - () => this.model.contact.destroy(), - (err) => { - log.error(err); - api.alert('error', __('Error'), [ - __('Sorry, there was an error while trying to remove %1$s as a contact.', - this.model.contact.getDisplayName()) - ]); - } - ); - }, 1); + setTimeout(() => removeContact(this.model.contact), 1); + this.modal.hide(); } }, });