diff --git a/karma.conf.js b/karma.conf.js index 6733d9737..db115f597 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -26,7 +26,7 @@ module.exports = function(config) { { pattern: "spec/spoilers.js", type: 'module' }, { pattern: "spec/emojis.js", type: 'module' }, - { pattern: "spec/roomslist.js", type: 'module' }, + { pattern: "spec/muclist.js", type: 'module' }, { pattern: "spec/utils.js", type: 'module' }, { pattern: "spec/converse.js", type: 'module' }, { pattern: "spec/bookmarks.js", type: 'module' }, diff --git a/spec/muc.js b/spec/muc.js index 06ba66889..8f3978e9c 100644 --- a/spec/muc.js +++ b/spec/muc.js @@ -12,7 +12,6 @@ const u = converse.env.utils; describe("Groupchats", function () { - describe("The \"rooms\" API", function () { it("has a method 'close' which closes rooms by JID or all rooms when called with no arguments", @@ -4862,7 +4861,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.list_rooms_modal; + const modal = roomspanel.muc_list_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 @@ -4939,7 +4938,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.list_rooms_modal; + const modal = roomspanel.muc_list_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'); @@ -4956,7 +4955,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.list_rooms_modal; + const modal = roomspanel.muc_list_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/roomslist.js b/spec/muclist.js similarity index 99% rename from spec/roomslist.js rename to spec/muclist.js index 2457d9e69..0e27abe4e 100644 --- a/spec/roomslist.js +++ b/spec/muclist.js @@ -1,4 +1,4 @@ -/* global mock */ +/* global mock, converse */ describe("A list of open groupchats", function () { diff --git a/src/converse-muc-views.js b/src/converse-muc-views.js index 2784b2580..97938366d 100644 --- a/src/converse-muc-views.js +++ b/src/converse-muc-views.js @@ -6,10 +6,10 @@ */ import "@converse/headless/utils/muc"; import "converse-modal"; +import MUCListModal from 'modals/muc-list.js'; import ModeratorToolsModal from "./modals/moderator-tools.js"; import RoomDetailsModal from 'modals/muc-details.js'; import log from "@converse/headless/log"; -import st from "@converse/headless/utils/stanza"; import tpl_add_chatroom_modal from "templates/add_chatroom_modal.js"; import tpl_chatroom from "templates/chatroom.js"; import tpl_chatroom_bottom_panel from "templates/chatroom_bottom_panel.html"; @@ -17,12 +17,10 @@ import tpl_chatroom_destroyed from "templates/chatroom_destroyed.html"; import tpl_chatroom_disconnect from "templates/chatroom_disconnect.html"; import tpl_chatroom_head from "templates/chatroom_head.js"; import tpl_chatroom_nickname_form from "templates/chatroom_nickname_form.html"; -import tpl_list_chatrooms_modal from "templates/list_chatrooms_modal.js"; import tpl_muc_config_form from "templates/muc_config_form.js"; import tpl_muc_invite_modal from "templates/muc_invite_modal.js"; import tpl_muc_password_form from "templates/muc_password_form.js"; import tpl_muc_sidebar from "templates/muc_sidebar.js"; -import tpl_room_description from "templates/room_description.html"; import tpl_room_panel from "templates/room_panel.html"; import tpl_spinner from "templates/spinner.html"; import { BootstrapModal } from "./converse-modal.js"; @@ -30,10 +28,10 @@ import { Model } from '@converse/skeletor/src/model.js'; import { View } from '@converse/skeletor/src/view.js'; import { __ } from '@converse/headless/i18n'; import { api, converse } from "@converse/headless/converse-core"; -import { debounce, head, isString, isUndefined } from "lodash-es"; +import { debounce, isString, isUndefined } from "lodash-es"; import { render } from "lit-html"; -const { Strophe, sizzle, $iq, $pres } = converse.env; +const { Strophe, sizzle, $pres } = converse.env; const u = converse.env.utils; const OWNER_COMMANDS = ['owner']; @@ -151,184 +149,6 @@ converse.plugins.add('converse-muc-views', { Object.assign(_converse.ControlBoxView.prototype, viewWithRoomsPanel); } - /* Insert groupchat info (based on returned #disco IQ stanza) - * @function insertRoomInfo - * @param { HTMLElement } el - The HTML DOM element that contains the info. - * @param { XMLElement } stanza - The IQ stanza containing the groupchat info. - */ - function insertRoomInfo (el, stanza) { - // All MUC features found here: https://xmpp.org/registrar/disco-features.html - el.querySelector('span.spinner').remove(); - el.querySelector('a.room-info').classList.add('selected'); - el.insertAdjacentHTML( - 'beforeEnd', - tpl_room_description({ - 'jid': stanza.getAttribute('from'), - 'desc': head(sizzle('field[var="muc#roominfo_description"] value', stanza))?.textContent, - 'occ': head(sizzle('field[var="muc#roominfo_occupants"] value', stanza))?.textContent, - 'hidden': sizzle('feature[var="muc_hidden"]', stanza).length, - 'membersonly': sizzle('feature[var="muc_membersonly"]', stanza).length, - 'moderated': sizzle('feature[var="muc_moderated"]', stanza).length, - 'nonanonymous': sizzle('feature[var="muc_nonanonymous"]', stanza).length, - 'open': sizzle('feature[var="muc_open"]', stanza).length, - 'passwordprotected': sizzle('feature[var="muc_passwordprotected"]', stanza).length, - 'persistent': sizzle('feature[var="muc_persistent"]', stanza).length, - 'publicroom': sizzle('feature[var="muc_publicroom"]', stanza).length, - 'semianonymous': sizzle('feature[var="muc_semianonymous"]', stanza).length, - 'temporary': sizzle('feature[var="muc_temporary"]', stanza).length, - 'unmoderated': sizzle('feature[var="muc_unmoderated"]', stanza).length, - 'label_desc': __('Description:'), - 'label_jid': __('Groupchat Address (JID):'), - 'label_occ': __('Participants:'), - 'label_features': __('Features:'), - 'label_requires_auth': __('Requires authentication'), - 'label_hidden': __('Hidden'), - 'label_requires_invite': __('Requires an invitation'), - 'label_moderated': __('Moderated'), - 'label_non_anon': __('Non-anonymous'), - 'label_open_room': __('Open'), - 'label_permanent_room': __('Permanent'), - 'label_public': __('Public'), - 'label_semi_anon': __('Semi-anonymous'), - 'label_temp_room': __('Temporary'), - 'label_unmoderated': __('Unmoderated') - })); - } - - /** - * Show/hide extra information about a groupchat in a listing. - * @function toggleRoomInfo - * @param { Event } - */ - function toggleRoomInfo (ev) { - const parent_el = u.ancestor(ev.target, '.room-item'); - const div_el = parent_el.querySelector('div.room-info'); - if (div_el) { - u.slideIn(div_el).then(u.removeElement) - parent_el.querySelector('a.room-info').classList.remove('selected'); - } else { - parent_el.insertAdjacentHTML('beforeend', tpl_spinner()); - api.disco.info(ev.target.getAttribute('data-room-jid'), null) - .then(stanza => insertRoomInfo(parent_el, stanza)) - .catch(e => log.error(e)); - } - } - - - _converse.ListChatRoomsModal = BootstrapModal.extend({ - id: "list-chatrooms-modal", - - initialize () { - this.items = []; - this.loading_items = false; - - BootstrapModal.prototype.initialize.apply(this, arguments); - if (api.settings.get('muc_domain') && !this.model.get('muc_domain')) { - this.model.save('muc_domain', api.settings.get('muc_domain')); - } - this.listenTo(this.model, 'change:muc_domain', this.onDomainChange); - }, - - toHTML () { - const muc_domain = this.model.get('muc_domain') || api.settings.get('muc_domain'); - return tpl_list_chatrooms_modal( - Object.assign(this.model.toJSON(), { - 'show_form': !api.settings.get('locked_muc_domain'), - 'server_placeholder': muc_domain ? muc_domain : __('conference.example.org'), - 'items': this.items, - 'loading_items': this.loading_items, - 'openRoom': ev => this.openRoom(ev), - 'setDomainFromEvent': ev => this.setDomainFromEvent(ev), - 'submitForm': ev => this.showRooms(ev), - 'toggleRoomInfo': ev => this.toggleRoomInfo(ev) - })); - }, - - afterRender () { - if (api.settings.get('locked_muc_domain')) { - this.updateRoomsList(); - } else { - this.el.addEventListener('shown.bs.modal', - () => this.el.querySelector('input[name="server"]').focus(), - false - ); - } - }, - - openRoom (ev) { - ev.preventDefault(); - const jid = ev.target.getAttribute('data-room-jid'); - const name = ev.target.getAttribute('data-room-name'); - this.modal.hide(); - api.rooms.open(jid, {'name': name}, true); - }, - - toggleRoomInfo (ev) { - ev.preventDefault(); - toggleRoomInfo(ev); - }, - - onDomainChange () { - api.settings.get('auto_list_rooms') && this.updateRoomsList(); - }, - - /** - * Handle the IQ stanza returned from the server, containing - * all its public groupchats. - * @private - * @method _converse.ChatRoomView#onRoomsFound - * @param { HTMLElement } iq - */ - onRoomsFound (iq) { - this.loading_items = false; - const rooms = iq ? sizzle('query item', iq) : []; - if (rooms.length) { - this.model.set({'feedback_text': __('Groupchats found')}, {'silent': true}); - this.items = rooms.map(st.getAttributes); - } else { - this.items = []; - this.model.set({'feedback_text': __('No groupchats found')}, {'silent': true}); - } - this.render(); - return true; - }, - - /** - * Send an IQ stanza to the server asking for all groupchats - * @private - * @method _converse.ChatRoomView#updateRoomsList - */ - updateRoomsList () { - const iq = $iq({ - 'to': this.model.get('muc_domain'), - 'from': _converse.connection.jid, - 'type': "get" - }).c("query", {xmlns: Strophe.NS.DISCO_ITEMS}); - api.sendIQ(iq) - .then(iq => this.onRoomsFound(iq)) - .catch(() => this.onRoomsFound()) - }, - - showRooms (ev) { - ev.preventDefault(); - this.loading_items = true; - this.render(); - - const data = new FormData(ev.target); - this.model.setDomain(data.get('server')); - this.updateRoomsList(); - }, - - setDomainFromEvent (ev) { - this.model.setDomain(ev.target.value); - }, - - setNick (ev) { - this.model.save({nick: ev.target.value}); - } - }); - - _converse.AddChatRoomModal = BootstrapModal.extend({ id: 'add-chatroom-modal', @@ -1716,7 +1536,7 @@ converse.plugins.add('converse-muc-views', { id: 'chatrooms', events: { 'click a.controlbox-heading__btn.show-add-muc-modal': 'showAddRoomModal', - 'click a.controlbox-heading__btn.show-list-muc-modal': 'showListRoomsModal' + 'click a.controlbox-heading__btn.show-list-muc-modal': 'showMUCListModal' }, render () { @@ -1735,11 +1555,11 @@ converse.plugins.add('converse-muc-views', { this.add_room_modal.show(ev); }, - showListRoomsModal(ev) { - if (this.list_rooms_modal === undefined) { - this.list_rooms_modal = new _converse.ListChatRoomsModal({'model': this.model}); + showMUCListModal(ev) { + if (this.muc_list_modal === undefined) { + this.muc_list_modal = new MUCListModal({'model': this.model}); } - this.list_rooms_modal.show(ev); + this.muc_list_modal.show(ev); } }); diff --git a/src/modals/muc-list.js b/src/modals/muc-list.js new file mode 100644 index 000000000..b04e10636 --- /dev/null +++ b/src/modals/muc-list.js @@ -0,0 +1,192 @@ +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_room_description from "templates/room_description.html"; +import tpl_spinner from "templates/spinner.html"; +import { BootstrapModal } from "../converse-modal.js"; +import { Strophe, $iq } from 'strophe.js/src/strophe'; +import { __ } from '@converse/headless/i18n'; +import { _converse, api, converse } from "@converse/headless/converse-core"; +import { head } from "lodash-es"; + +const u = converse.env.utils; + + +/* Insert groupchat info (based on returned #disco IQ stanza) + * @function insertRoomInfo + * @param { HTMLElement } el - The HTML DOM element that contains the info. + * @param { XMLElement } stanza - The IQ stanza containing the groupchat info. + */ +function insertRoomInfo (el, stanza) { + // All MUC features found here: https://xmpp.org/registrar/disco-features.html + el.querySelector('span.spinner').remove(); + el.querySelector('a.room-info').classList.add('selected'); + el.insertAdjacentHTML( + 'beforeEnd', + tpl_room_description({ + 'jid': stanza.getAttribute('from'), + 'desc': head(sizzle('field[var="muc#roominfo_description"] value', stanza))?.textContent, + 'occ': head(sizzle('field[var="muc#roominfo_occupants"] value', stanza))?.textContent, + 'hidden': sizzle('feature[var="muc_hidden"]', stanza).length, + 'membersonly': sizzle('feature[var="muc_membersonly"]', stanza).length, + 'moderated': sizzle('feature[var="muc_moderated"]', stanza).length, + 'nonanonymous': sizzle('feature[var="muc_nonanonymous"]', stanza).length, + 'open': sizzle('feature[var="muc_open"]', stanza).length, + 'passwordprotected': sizzle('feature[var="muc_passwordprotected"]', stanza).length, + 'persistent': sizzle('feature[var="muc_persistent"]', stanza).length, + 'publicroom': sizzle('feature[var="muc_publicroom"]', stanza).length, + 'semianonymous': sizzle('feature[var="muc_semianonymous"]', stanza).length, + 'temporary': sizzle('feature[var="muc_temporary"]', stanza).length, + 'unmoderated': sizzle('feature[var="muc_unmoderated"]', stanza).length, + 'label_desc': __('Description:'), + 'label_jid': __('Groupchat Address (JID):'), + 'label_occ': __('Participants:'), + 'label_features': __('Features:'), + 'label_requires_auth': __('Requires authentication'), + 'label_hidden': __('Hidden'), + 'label_requires_invite': __('Requires an invitation'), + 'label_moderated': __('Moderated'), + 'label_non_anon': __('Non-anonymous'), + 'label_open_room': __('Open'), + 'label_permanent_room': __('Permanent'), + 'label_public': __('Public'), + 'label_semi_anon': __('Semi-anonymous'), + 'label_temp_room': __('Temporary'), + 'label_unmoderated': __('Unmoderated') + })); +} + + +/** + * Show/hide extra information about a groupchat in a listing. + * @function toggleRoomInfo + * @param { Event } + */ +function toggleRoomInfo (ev) { + const parent_el = u.ancestor(ev.target, '.room-item'); + const div_el = parent_el.querySelector('div.room-info'); + if (div_el) { + u.slideIn(div_el).then(u.removeElement) + parent_el.querySelector('a.room-info').classList.remove('selected'); + } else { + parent_el.insertAdjacentHTML('beforeend', tpl_spinner()); + api.disco.info(ev.target.getAttribute('data-room-jid'), null) + .then(stanza => insertRoomInfo(parent_el, stanza)) + .catch(e => log.error(e)); + } +} + + +export default BootstrapModal.extend({ + id: "list-chatrooms-modal", + + initialize () { + this.items = []; + this.loading_items = false; + + BootstrapModal.prototype.initialize.apply(this, arguments); + if (api.settings.get('muc_domain') && !this.model.get('muc_domain')) { + this.model.save('muc_domain', api.settings.get('muc_domain')); + } + this.listenTo(this.model, 'change:muc_domain', this.onDomainChange); + }, + + toHTML () { + const muc_domain = this.model.get('muc_domain') || api.settings.get('muc_domain'); + return tpl_list_chatrooms_modal( + Object.assign(this.model.toJSON(), { + 'show_form': !api.settings.get('locked_muc_domain'), + 'server_placeholder': muc_domain ? muc_domain : __('conference.example.org'), + 'items': this.items, + 'loading_items': this.loading_items, + 'openRoom': ev => this.openRoom(ev), + 'setDomainFromEvent': ev => this.setDomainFromEvent(ev), + 'submitForm': ev => this.showRooms(ev), + 'toggleRoomInfo': ev => this.toggleRoomInfo(ev) + })); + }, + + afterRender () { + if (api.settings.get('locked_muc_domain')) { + this.updateRoomsList(); + } else { + this.el.addEventListener('shown.bs.modal', + () => this.el.querySelector('input[name="server"]').focus(), + false + ); + } + }, + + openRoom (ev) { + ev.preventDefault(); + const jid = ev.target.getAttribute('data-room-jid'); + const name = ev.target.getAttribute('data-room-name'); + this.modal.hide(); + api.rooms.open(jid, {'name': name}, true); + }, + + toggleRoomInfo (ev) { + ev.preventDefault(); + toggleRoomInfo(ev); + }, + + onDomainChange () { + api.settings.get('auto_list_rooms') && this.updateRoomsList(); + }, + + /** + * Handle the IQ stanza returned from the server, containing + * all its public groupchats. + * @private + * @method _converse.ChatRoomView#onRoomsFound + * @param { HTMLElement } iq + */ + onRoomsFound (iq) { + this.loading_items = false; + const rooms = iq ? sizzle('query item', iq) : []; + if (rooms.length) { + this.model.set({'feedback_text': __('Groupchats found')}, {'silent': true}); + this.items = rooms.map(st.getAttributes); + } else { + this.items = []; + this.model.set({'feedback_text': __('No groupchats found')}, {'silent': true}); + } + this.render(); + return true; + }, + + /** + * Send an IQ stanza to the server asking for all groupchats + * @private + * @method _converse.ChatRoomView#updateRoomsList + */ + updateRoomsList () { + const iq = $iq({ + 'to': this.model.get('muc_domain'), + 'from': _converse.connection.jid, + 'type': "get" + }).c("query", {xmlns: Strophe.NS.DISCO_ITEMS}); + api.sendIQ(iq) + .then(iq => this.onRoomsFound(iq)) + .catch(() => this.onRoomsFound()) + }, + + showRooms (ev) { + ev.preventDefault(); + this.loading_items = true; + this.render(); + + const data = new FormData(ev.target); + this.model.setDomain(data.get('server')); + this.updateRoomsList(); + }, + + setDomainFromEvent (ev) { + this.model.setDomain(ev.target.value); + }, + + setNick (ev) { + this.model.save({nick: ev.target.value}); + } +});