From 47aad3189973aea90b374349fdad1f34eec160df Mon Sep 17 00:00:00 2001 From: JC Brand Date: Wed, 21 Feb 2018 22:40:51 +0100 Subject: [PATCH] Tricky refactoring. Removed `_converse.chatboxviews.showChat` and trying to simplify how chats are created and when they're shown. Prompted by the work to split the MUC views into a separate plugin --- src/config.js | 2 + src/converse-bookmarks.js | 2 +- src/converse-chatboxes.js | 111 +- src/converse-chatview.js | 10 +- src/converse-controlbox.js | 17 +- src/converse-minimize.js | 18 +- src/converse-muc.js | 2558 ++------------------------------- src/converse-profile.js | 4 +- src/converse-roomslist.js | 13 +- src/converse-rosterview.js | 3 +- src/converse-singleton.js | 69 +- src/templates/room_panel.html | 2 +- 12 files changed, 165 insertions(+), 2644 deletions(-) diff --git a/src/config.js b/src/config.js index 104da444b..a56c64e10 100644 --- a/src/config.js +++ b/src/config.js @@ -135,6 +135,8 @@ require.config({ // define module dependencies for modules not using define shim: { + 'backbone.overview': { deps: ['backbone.nativeview'] }, + 'backbone.orderedlistview': { deps: ['backbone.nativeview'] }, 'awesomplete': { exports: 'Awesomplete'}, 'emojione': { exports: 'emojione'}, 'xss': { diff --git a/src/converse-bookmarks.js b/src/converse-bookmarks.js index b8eba2243..ef47cbf7f 100644 --- a/src/converse-bookmarks.js +++ b/src/converse-bookmarks.js @@ -269,7 +269,7 @@ openBookmarkedRoom (bookmark) { if (bookmark.get('autojoin')) { - _converse.api.rooms.open(bookmark.get('jid'), bookmark.get('nick')); + _converse.api.rooms.create(bookmark.get('jid'), bookmark.get('nick')); } return bookmark; }, diff --git a/src/converse-chatboxes.js b/src/converse-chatboxes.js index 8d7e5f4da..fb00972a9 100644 --- a/src/converse-chatboxes.js +++ b/src/converse-chatboxes.js @@ -95,11 +95,13 @@ _converse.ChatBox = Backbone.Model.extend({ defaults: { - 'type': 'chatbox', - 'show_avatar': true, 'bookmarked': false, 'chat_state': undefined, + 'image': _converse.DEFAULT_IMAGE, + 'image_type': _converse.DEFAULT_IMAGE_TYPE, 'num_unread': 0, + 'show_avatar': true, + 'type': 'chatbox', 'url': '' }, @@ -244,11 +246,7 @@ }, onChatBoxesFetched (collection) { - /* Show chat boxes upon receiving them from sessionStorage - * - * This method gets overridden entirely in src/converse-controlbox.js - * if the controlbox plugin is active. - */ + /* Show chat boxes upon receiving them from sessionStorage */ collection.each((chatbox) => { if (this.chatBoxMayBeShown(chatbox)) { chatbox.trigger('show'); @@ -344,8 +342,8 @@ resource = from_resource; } // Get chat box, but only create a new one when the message has a body. - const chatbox = this.getChatBox(contact_jid, !_.isNull(message.querySelector('body'))), - msgid = message.getAttribute('id'); + const chatbox = this.getChatBox(contact_jid, {}, !_.isNull(message.querySelector('body'))), + msgid = message.getAttribute('id'); if (chatbox) { const messages = msgid && chatbox.messages.findWhere({msgid}) || []; @@ -360,54 +358,30 @@ return true; }, - createChatBox (jid, attrs) { - /* Creates a chat box - * - * Parameters: - * (String) jid - The JID of the user for whom a chat box - * gets created. - * (Object) attrs - Optional chat box atributes. - */ - const bare_jid = Strophe.getBareJidFromJid(jid), - roster_item = _converse.roster.get(bare_jid); - let roster_info = {}; - - if (! _.isUndefined(roster_item)) { - roster_info = { - 'fullname': _.isEmpty(roster_item.get('fullname'))? jid: roster_item.get('fullname'), - 'image_type': roster_item.get('image_type'), - 'image': roster_item.get('image'), - 'url': roster_item.get('url'), - }; - } else if (!_converse.allow_non_roster_messaging) { - _converse.log(`Could not get roster item for JID ${bare_jid}`+ - ' and allow_non_roster_messaging is set to false', - Strophe.LogLevel.ERROR); - return; - } - return this.create(_.assignIn({ - 'id': bare_jid, - 'jid': bare_jid, - 'fullname': jid, - 'image_type': _converse.DEFAULT_IMAGE_TYPE, - 'image': _converse.DEFAULT_IMAGE, - 'url': '', - }, roster_info, attrs || {})); - }, - - getChatBox (jid, create, attrs) { + getChatBox (jid, attrs={}, create) { /* Returns a chat box or optionally return a newly - * created one if one doesn't exist. - * - * Parameters: - * (String) jid - The JID of the user whose chat box we want - * (Boolean) create - Should a new chat box be created if none exists? - * (Object) attrs - Optional chat box atributes. - */ + * created one if one doesn't exist. + * + * Parameters: + * (String) jid - The JID of the user whose chat box we want + * (Boolean) create - Should a new chat box be created if none exists? + * (Object) attrs - Optional chat box atributes. + */ + if (_.isObject(jid)) { + create = attrs; + attrs = jid; + jid = attrs.jid; + } else { + attrs.jid = jid; + } jid = jid.toLowerCase(); let chatbox = this.get(Strophe.getBareJidFromJid(jid)); if (!chatbox && create) { - chatbox = this.createChatBox(jid, attrs); + chatbox = this.create(attrs, { + 'error' (model, response) { + _converse.log(response.responseText); + } + }); } return chatbox; } @@ -449,7 +423,12 @@ }, render () { - this.el.innerHTML = tpl_chatboxes(); + try { + this.el.innerHTML = tpl_chatboxes(); + } catch (e) { + this._ensureElement(); + this.el.innerHTML = tpl_chatboxes(); + } this.row_el = this.el.querySelector('.row'); }, @@ -481,29 +460,6 @@ chatBoxMayBeShown (chatbox) { return this.model.chatBoxMayBeShown(chatbox); - }, - - getChatBox (attrs, create) { - let chatbox = this.model.get(attrs.jid); - if (!chatbox && create) { - chatbox = this.model.create(attrs, { - 'error' (model, response) { - _converse.log(response.responseText); - } - }); - } - return chatbox; - }, - - showChat (attrs) { - /* Find the chat box and show it (if it may be shown). - * If it doesn't exist, create it. - */ - const chatbox = this.getChatBox(attrs, true); - if (this.chatBoxMayBeShown(chatbox)) { - chatbox.trigger('show', true); - } - return chatbox; } }); @@ -575,6 +531,7 @@ } } }); + /************************ END API ************************/ } }); return converse; diff --git a/src/converse-chatview.js b/src/converse-chatview.js index bdd252a17..436e67a1d 100644 --- a/src/converse-chatview.js +++ b/src/converse-chatview.js @@ -737,10 +737,6 @@ return message; }, - shouldShowOnTextMessage () { - return !u.isVisible(this.el); - }, - handleTextMessage (message) { this.showMessage(_.clone(message.attributes)); if (u.isNewMessage(message)) { @@ -754,11 +750,7 @@ this.showNewMessagesIndicator(); } } - if (this.shouldShowOnTextMessage()) { - this.show(); - } else { - this.scrollDown(); - } + this.scrollDown(); }, handleErrorMessage (message) { diff --git a/src/converse-controlbox.js b/src/converse-controlbox.js index e8620f6ca..540b6f5e7 100644 --- a/src/converse-controlbox.js +++ b/src/converse-controlbox.js @@ -86,7 +86,7 @@ * * NB: These plugins need to have already been loaded via require.js. */ - dependencies: ["converse-chatboxes", "converse-rosterview"], + dependencies: ["converse-chatboxes", "converse-rosterview", "converse-chatview"], overrides: { // Overrides mentioned here will be picked up by converse.js's @@ -126,15 +126,6 @@ return this.__super__.chatBoxMayBeShown.apply(this, arguments) && chatbox.get('id') !== 'controlbox'; }, - - onChatBoxesFetched (collection, resp) { - this.__super__.onChatBoxesFetched.apply(this, arguments); - const { _converse } = this.__super__; - if (!_.includes(_.map(collection, 'id'), 'controlbox')) { - _converse.addControlBox(); - } - this.get('controlbox').save({connected:true}); - }, }, ChatBoxViews: { @@ -715,8 +706,10 @@ Promise.all([ _converse.api.waitUntil('connectionInitialized'), _converse.api.waitUntil('chatBoxesInitialized') - ]).then(_converse.addControlBox) - .catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL)); + ]).then(() => { + _converse.addControlBox(); + _converse.chatboxes.get('controlbox').save({connected:true}); + }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL)); const disconnect = function () { /* Upon disconnection, set connected to `false`, so that if diff --git a/src/converse-minimize.js b/src/converse-minimize.js index 1c1b421e6..2bb1a13e0 100644 --- a/src/converse-minimize.js +++ b/src/converse-minimize.js @@ -65,6 +65,8 @@ ChatBox: { initialize () { this.__super__.initialize.apply(this, arguments); + this.on('show', this.maximize, this); + if (this.get('id') === 'controlbox') { return; } @@ -114,11 +116,6 @@ this.__super__.isNewMessageHidden.apply(this, arguments); }, - shouldShowOnTextMessage () { - return !this.model.get('minimized') && - this.__super__.shouldShowOnTextMessage.apply(this, arguments); - }, - setChatBoxHeight (height) { if (!this.model.get('minimized')) { return this.__super__.setChatBoxHeight.apply(this, arguments); @@ -211,17 +208,6 @@ }, ChatBoxViews: { - showChat (attrs) { - /* Find the chat box and show it. If it doesn't exist, create it. - */ - const chatbox = this.__super__.showChat.apply(this, arguments); - const maximize = _.isUndefined(attrs.maximize) ? true : attrs.maximize; - if (chatbox.get('minimized') && maximize) { - chatbox.maximize(); - } - return chatbox; - }, - getChatBoxWidth (view) { if (!view.model.get('minimized') && u.isVisible(view.el)) { return u.getOuterWidth(view.el, true); diff --git a/src/converse-muc.js b/src/converse-muc.js index b06d6edd1..6ab4d3147 100644 --- a/src/converse-muc.js +++ b/src/converse-muc.js @@ -14,60 +14,14 @@ "form-utils", "converse-core", "lodash.fp", - "tpl!chatarea", - "tpl!chatroom", - "tpl!chatroom_disconnect", - "tpl!chatroom_features", - "tpl!chatroom_form", - "tpl!chatroom_head", - "tpl!chatroom_invite", - "tpl!chatroom_join_form", - "tpl!chatroom_nickname_form", - "tpl!chatroom_password_form", - "tpl!chatroom_sidebar", - "tpl!chatroom_toolbar", - "tpl!info", - "tpl!occupant", - "tpl!room_description", - "tpl!room_item", - "tpl!room_panel", - "tpl!rooms_results", - "tpl!spinner", - "awesomplete", "converse-chatview", "converse-disco", "backbone.overview", "backbone.orderedlistview", "backbone.vdomview" ], factory); -}(this, function ( - u, - converse, - fp, - tpl_chatarea, - tpl_chatroom, - tpl_chatroom_disconnect, - tpl_chatroom_features, - tpl_chatroom_form, - tpl_chatroom_head, - tpl_chatroom_invite, - tpl_chatroom_join_form, - tpl_chatroom_nickname_form, - tpl_chatroom_password_form, - tpl_chatroom_sidebar, - tpl_chatroom_toolbar, - tpl_info, - tpl_occupant, - tpl_room_description, - tpl_room_item, - tpl_room_panel, - tpl_rooms_results, - tpl_spinner, - Awesomplete - ) { - +}(this, function (u, converse, fp) { "use strict"; - const CHATROOMS_TYPE = 'chatroom'; const MUC_ROLE_WEIGHTS = { 'moderator': 1, @@ -85,26 +39,14 @@ Strophe.addNamespace('MUC_ROOMCONF', Strophe.NS.MUC + "#roomconfig"); Strophe.addNamespace('MUC_USER', Strophe.NS.MUC + "#user"); - const ROOM_FEATURES = [ + converse.CHATROOMS_TYPE = 'chatroom'; + + converse.ROOM_FEATURES = [ 'passwordprotected', 'unsecured', 'hidden', 'publicroom', 'membersonly', 'open', 'persistent', 'temporary', 'nonanonymous', 'semianonymous', 'moderated', 'unmoderated', 'mam_enabled' ]; - const ROOM_FEATURES_MAP = { - 'passwordprotected': 'unsecured', - 'unsecured': 'passwordprotected', - 'hidden': 'publicroom', - 'publicroom': 'hidden', - 'membersonly': 'open', - 'open': 'membersonly', - 'persistent': 'temporary', - 'temporary': 'persistent', - 'nonanonymous': 'semianonymous', - 'semianonymous': 'nonanonymous', - 'moderated': 'unmoderated', - 'unmoderated': 'moderated' - }; converse.ROOMSTATUS = { CONNECTED: 0, @@ -138,7 +80,7 @@ // New functions which don't exist yet can also be added. _tearDown () { - const rooms = this.chatboxes.where({'type': CHATROOMS_TYPE}); + const rooms = this.chatboxes.where({'type': converse.CHATROOMS_TYPE}); _.each(rooms, function (room) { u.safeSave(room, {'connection_status': converse.ROOMSTATUS.DISCONNECTED}); }); @@ -148,59 +90,12 @@ ChatBoxes: { model (attrs, options) { const { _converse } = this.__super__; - if (attrs.type == CHATROOMS_TYPE) { + if (attrs.type == converse.CHATROOMS_TYPE) { return new _converse.ChatRoom(attrs, options); } else { return this.__super__.model.apply(this, arguments); } }, - }, - - ControlBoxView: { - - renderRoomsPanel () { - const { _converse } = this.__super__; - this.roomspanel = new _converse.RoomsPanel({ - 'model': new (_converse.RoomsPanelModel.extend({ - id: b64_sha1(`converse.roomspanel${_converse.bare_jid}`), // Required by sessionStorage - browserStorage: new Backbone.BrowserStorage[_converse.storage]( - b64_sha1(`converse.roomspanel${_converse.bare_jid}`)) - }))() - }); - this.roomspanel.model.fetch(); - - this.el.querySelector('.controlbox-pane').insertAdjacentElement( - 'beforeEnd', this.roomspanel.render().el); - - if (!this.roomspanel.model.get('nick')) { - this.roomspanel.model.save({ - nick: Strophe.getNodeFromJid(_converse.bare_jid) - }); - } - _converse.emit('roomsPanelRendered'); - }, - - renderControlBoxPane () { - const { _converse } = this.__super__; - this.__super__.renderControlBoxPane.apply(this, arguments); - if (_converse.allow_muc) { - this.renderRoomsPanel(); - } - }, - - }, - - ChatBoxViews: { - onChatBoxAdded (item) { - const { _converse } = this.__super__; - let view = this.get(item.get('id')); - if (!view && item.get('type') === CHATROOMS_TYPE) { - view = new _converse.ChatRoomView({'model': item}); - return this.add(item.get('id'), view); - } else { - return this.__super__.onChatBoxAdded.apply(this, arguments); - } - } } }, @@ -316,7 +211,7 @@ 'toggle_occupants': true }, }); - _converse.api.promises.add(['roomsPanelRendered', 'roomsAutoJoined']); + _converse.api.promises.add(['roomsAutoJoined']); function openRoom (jid) { @@ -337,18 +232,17 @@ _converse.router.route('converse/room?jid=:jid', openRoom); - function openChatRoom (settings, bring_to_foreground) { + _converse.openChatRoom = function (jid, settings, bring_to_foreground) { /* Opens a chat room, making sure that certain attributes * are correct, for example that the "type" is set to * "chatroom". */ - if (_.isUndefined(settings.jid)) { - throw new Error("openChatRoom needs to be called with a JID"); - } - settings.type = CHATROOMS_TYPE; - settings.id = settings.jid; - settings.box_id = b64_sha1(settings.jid) - return _converse.chatboxviews.showChat(settings, bring_to_foreground); + settings.type = converse.CHATROOMS_TYPE; + settings.id = jid; + settings.box_id = b64_sha1(jid) + const chatbox = _converse.chatboxes.getChatBox(jid, settings, true); + chatbox.trigger('show', true); + return chatbox; } _converse.ChatRoom = _converse.ChatBox.extend({ @@ -356,7 +250,7 @@ defaults () { return _.assign( _.clone(_converse.ChatBox.prototype.defaults), - _.zipObject(ROOM_FEATURES, _.map(ROOM_FEATURES, _.stubFalse)), + _.zipObject(converse.ROOM_FEATURES, _.map(converse.ROOM_FEATURES, _.stubFalse)), { // For group chats, we distinguish between generally unread // messages and those ones that specifically mention the @@ -374,7 +268,7 @@ 'description': '', 'features_fetched': false, 'roomconfig': {}, - 'type': CHATROOMS_TYPE, + 'type': converse.CHATROOMS_TYPE, } ); }, @@ -427,1680 +321,6 @@ }); - _converse.ChatRoomOccupants = Backbone.Collection.extend({ - model: _converse.ChatRoomOccupant, - - afterShown (focus) { - /* Override from converse-chatview, specifically to avoid - * the 'active' chat state from being sent out prematurely. - * - * This is instead done in `afterConnected` below. - */ - if (u.isPersistableModel(this.model)) { - this.model.clearUnreadMsgCounter(); - this.model.save(); - } - this.occupantsview.setOccupantsHeight(); - this.scrollDown(); - if (focus) { this.focus(); } - }, - - show (focus) { - if (u.isVisible(this.el)) { - if (focus) { this.focus(); } - return; - } - // Override from converse-chatview in order to not use - // "fadeIn", which causes flashing. - u.showElement(this.el); - this.afterShown(focus); - }, - - afterConnected () { - if (this.model.get('connection_status') === converse.ROOMSTATUS.ENTERED) { - this.setChatState(_converse.ACTIVE); - this.scrollDown(); - this.renderEmojiPicker(); - this.focus(); - } - }, - - getExtraMessageClasses (attrs) { - let extra_classes = _converse.ChatBoxView.prototype - .getExtraMessageClasses.apply(this, arguments); - - if (this.is_chatroom && attrs.sender === 'them' && - this.model.isUserMentioned(attrs.message)) { - // Add special class to mark groupchat messages - // in which we are mentioned. - extra_classes += ' mentioned'; - } - return extra_classes; - }, - - getToolbarOptions () { - return _.extend( - _converse.ChatBoxView.prototype.getToolbarOptions.apply(this, arguments), - { - label_hide_occupants: __('Hide the list of occupants'), - show_occupants_toggle: this.is_chatroom && _converse.visible_toolbar_buttons.toggle_occupants - } - ); - }, - - close (ev) { - /* Close this chat box, which implies leaving the room as - * well. - */ - this.leave(); - }, - - setOccupantsVisibility () { - if (this.model.get('hidden_occupants')) { - const icon_el = this.el.querySelector('.icon-hide-users'); - if (!_.isNull(icon_el)) { - icon_el.classList.remove('icon-hide-users'); - icon_el.classList.add('icon-show-users'); - } - this.el.querySelector('.chat-area').classList.add('full'); - u.hideElement(this.el.querySelector('.occupants')); - } else { - const icon_el = this.el.querySelector('.icon-show-users'); - if (!_.isNull(icon_el)) { - icon_el.classList.remove('icon-show-users'); - icon_el.classList.add('icon-hide-users'); - } - this.el.querySelector('.chat-area').classList.remove('full'); - this.el.querySelector('.occupants').classList.remove('hidden'); - } - this.occupantsview.setOccupantsHeight(); - }, - - toggleOccupants (ev, preserve_state) { - /* Show or hide the right sidebar containing the chat - * occupants (and the invite widget). - */ - if (ev) { - ev.preventDefault(); - ev.stopPropagation(); - } - if (!preserve_state) { - this.model.set({'hidden_occupants': !this.model.get('hidden_occupants')}); - } - this.setOccupantsVisibility(); - this.scrollDown(); - }, - - onOccupantClicked (ev) { - /* When an occupant is clicked, insert their nickname into - * the chat textarea input. - */ - this.insertIntoTextArea(ev.target.textContent); - }, - - requestMemberList (chatroom_jid, affiliation) { - /* Send an IQ stanza to the server, asking it for the - * member-list of this room. - * - * See: http://xmpp.org/extensions/xep-0045.html#modifymember - * - * Parameters: - * (String) chatroom_jid: The JID of the chatroom for - * which the member-list is being requested - * (String) affiliation: The specific member list to - * fetch. 'admin', 'owner' or 'member'. - * - * Returns: - * A promise which resolves once the list has been - * retrieved. - */ - return new Promise((resolve, reject) => { - affiliation = affiliation || 'member'; - const iq = $iq({to: chatroom_jid, type: "get"}) - .c("query", {xmlns: Strophe.NS.MUC_ADMIN}) - .c("item", {'affiliation': affiliation}); - _converse.connection.sendIQ(iq, resolve, reject); - }); - }, - - parseMemberListIQ (iq) { - /* Given an IQ stanza with a member list, create an array of member - * objects. - */ - return _.map( - sizzle(`query[xmlns="${Strophe.NS.MUC_ADMIN}"] item`, iq), - (item) => ({ - 'jid': item.getAttribute('jid'), - 'affiliation': item.getAttribute('affiliation'), - }) - ); - }, - - computeAffiliationsDelta (exclude_existing, remove_absentees, new_list, old_list) { - /* Given two lists of objects with 'jid', 'affiliation' and - * 'reason' properties, return a new list containing - * those objects that are new, changed or removed - * (depending on the 'remove_absentees' boolean). - * - * The affiliations for new and changed members stay the - * same, for removed members, the affiliation is set to 'none'. - * - * The 'reason' property is not taken into account when - * comparing whether affiliations have been changed. - * - * Parameters: - * (Boolean) exclude_existing: Indicates whether JIDs from - * the new list which are also in the old list - * (regardless of affiliation) should be excluded - * from the delta. One reason to do this - * would be when you want to add a JID only if it - * doesn't have *any* existing affiliation at all. - * (Boolean) remove_absentees: Indicates whether JIDs - * from the old list which are not in the new list - * should be considered removed and therefore be - * included in the delta with affiliation set - * to 'none'. - * (Array) new_list: Array containing the new affiliations - * (Array) old_list: Array containing the old affiliations - */ - const new_jids = _.map(new_list, 'jid'); - const old_jids = _.map(old_list, 'jid'); - - // Get the new affiliations - let delta = _.map( - _.difference(new_jids, old_jids), - (jid) => new_list[_.indexOf(new_jids, jid)] - ); - if (!exclude_existing) { - // Get the changed affiliations - delta = delta.concat(_.filter(new_list, function (item) { - const idx = _.indexOf(old_jids, item.jid); - if (idx >= 0) { - return item.affiliation !== old_list[idx].affiliation; - } - return false; - })); - } - if (remove_absentees) { - // Get the removed affiliations - delta = delta.concat( - _.map( - _.difference(old_jids, new_jids), - (jid) => ({'jid': jid, 'affiliation': 'none'}) - ) - ); - } - return delta; - }, - - sendAffiliationIQ (chatroom_jid, affiliation, member) { - /* Send an IQ stanza specifying an affiliation change. - * - * Paremeters: - * (String) chatroom_jid: JID of the relevant room - * (String) affiliation: affiliation (could also be stored - * on the member object). - * (Object) member: Map containing the member's jid and - * optionally a reason and affiliation. - */ - return new Promise((resolve, reject) => { - const iq = $iq({to: chatroom_jid, type: "set"}) - .c("query", {xmlns: Strophe.NS.MUC_ADMIN}) - .c("item", { - 'affiliation': member.affiliation || affiliation, - 'jid': member.jid - }); - if (!_.isUndefined(member.reason)) { - iq.c("reason", member.reason); - } - _converse.connection.sendIQ(iq, resolve, reject); - }); - }, - - setAffiliation (affiliation, members) { - /* Send IQ stanzas to the server to set an affiliation for - * the provided JIDs. - * - * See: http://xmpp.org/extensions/xep-0045.html#modifymember - * - * XXX: Prosody doesn't accept multiple JIDs' affiliations - * being set in one IQ stanza, so as a workaround we send - * a separate stanza for each JID. - * Related ticket: https://prosody.im/issues/issue/795 - * - * Parameters: - * (String) affiliation: The affiliation - * (Object) members: A map of jids, affiliations and - * optionally reasons. Only those entries with the - * same affiliation as being currently set will be - * considered. - * - * Returns: - * A promise which resolves and fails depending on the - * XMPP server response. - */ - members = _.filter(members, (member) => - // We only want those members who have the right - // affiliation (or none, which implies the provided - // one). - _.isUndefined(member.affiliation) || - member.affiliation === affiliation - ); - const promises = _.map( - members, - _.partial(this.sendAffiliationIQ, this.model.get('jid'), affiliation) - ); - return Promise.all(promises); - }, - - setAffiliations (members) { - /* Send IQ stanzas to the server to modify the - * affiliations in this room. - * - * See: http://xmpp.org/extensions/xep-0045.html#modifymember - * - * Parameters: - * (Object) members: A map of jids, affiliations and optionally reasons - * (Function) onSuccess: callback for a succesful response - * (Function) onError: callback for an error response - */ - const affiliations = _.uniq(_.map(members, 'affiliation')); - _.each(affiliations, _.partial(this.setAffiliation.bind(this), _, members)); - }, - - marshallAffiliationIQs () { - /* Marshall a list of IQ stanzas into a map of JIDs and - * affiliations. - * - * Parameters: - * Any amount of XMLElement objects, representing the IQ - * stanzas. - */ - return _.flatMap(arguments[0], this.parseMemberListIQ); - }, - - getJidsWithAffiliations (affiliations) { - /* Returns a map of JIDs that have the affiliations - * as provided. - */ - if (_.isString(affiliations)) { - affiliations = [affiliations]; - } - return new Promise((resolve, reject) => { - const promises = _.map( - affiliations, - _.partial(this.requestMemberList, this.model.get('jid')) - ); - - Promise.all(promises).then( - _.flow(this.marshallAffiliationIQs.bind(this), resolve), - _.flow(this.marshallAffiliationIQs.bind(this), resolve) - ); - }); - }, - - updateMemberLists (members, affiliations, deltaFunc) { - /* Fetch the lists of users with the given affiliations. - * Then compute the delta between those users and - * the passed in members, and if it exists, send the delta - * to the XMPP server to update the member list. - * - * Parameters: - * (Object) members: Map of member jids and affiliations. - * (String|Array) affiliation: An array of affiliations or - * a string if only one affiliation. - * (Function) deltaFunc: The function to compute the delta - * between old and new member lists. - * - * Returns: - * A promise which is resolved once the list has been - * updated or once it's been established there's no need - * to update the list. - */ - this.getJidsWithAffiliations(affiliations).then((old_members) => { - this.setAffiliations(deltaFunc(members, old_members)); - }); - }, - - directInvite (recipient, reason) { - /* Send a direct invitation as per XEP-0249 - * - * Parameters: - * (String) recipient - JID of the person being invited - * (String) reason - Optional reason for the invitation - */ - if (this.model.get('membersonly')) { - // When inviting to a members-only room, we first add - // the person to the member list by giving them an - // affiliation of 'member' (if they're not affiliated - // already), otherwise they won't be able to join. - const map = {}; map[recipient] = 'member'; - const deltaFunc = _.partial(this.computeAffiliationsDelta, true, false); - this.updateMemberLists( - [{'jid': recipient, 'affiliation': 'member', 'reason': reason}], - ['member', 'owner', 'admin'], - deltaFunc - ); - } - const attrs = { - 'xmlns': 'jabber:x:conference', - 'jid': this.model.get('jid') - }; - if (reason !== null) { attrs.reason = reason; } - if (this.model.get('password')) { attrs.password = this.model.get('password'); } - const invitation = $msg({ - from: _converse.connection.jid, - to: recipient, - id: _converse.connection.getUniqueId() - }).c('x', attrs); - _converse.connection.send(invitation); - _converse.emit('roomInviteSent', { - 'room': this, - 'recipient': recipient, - 'reason': reason - }); - }, - - handleChatStateMessage (message) { - /* Override the method on the ChatBoxView base class to - * ignore notifications in groupchats. - * - * As laid out in the business rules in XEP-0085 - * http://xmpp.org/extensions/xep-0085.html#bizrules-groupchat - */ - if (message.get('fullname') === this.model.get('nick')) { - // Don't know about other servers, but OpenFire sends - // back to you your own chat state notifications. - // We ignore them here... - return; - } - if (message.get('chat_state') !== _converse.GONE) { - _converse.ChatBoxView.prototype.handleChatStateMessage.apply(this, arguments); - } - }, - - sendChatState () { - /* Sends a message with the status of the user in this chat session - * as taken from the 'chat_state' attribute of the chat box. - * See XEP-0085 Chat State Notifications. - */ - if (this.model.get('connection_status') !== converse.ROOMSTATUS.ENTERED) { - return; - } - const chat_state = this.model.get('chat_state'); - if (chat_state === _converse.GONE) { - // is not applicable within MUC context - return; - } - _converse.connection.send( - $msg({'to':this.model.get('jid'), 'type': 'groupchat'}) - .c(chat_state, {'xmlns': Strophe.NS.CHATSTATES}).up() - .c('no-store', {'xmlns': Strophe.NS.HINTS}).up() - .c('no-permanent-store', {'xmlns': Strophe.NS.HINTS}) - ); - }, - - sendChatRoomMessage (text) { - /* Constuct a message stanza to be sent to this chat room, - * and send it to the server. - * - * Parameters: - * (String) text: The message text to be sent. - */ - const msgid = _converse.connection.getUniqueId(); - const msg = $msg({ - to: this.model.get('jid'), - from: _converse.connection.jid, - type: 'groupchat', - id: msgid - }).c("body").t(text).up() - .c("x", {xmlns: "jabber:x:event"}).c(_converse.COMPOSING); - _converse.connection.send(msg); - this.model.messages.create({ - fullname: this.model.get('nick'), - sender: 'me', - time: moment().format(), - message: text, - msgid - }); - }, - - modifyRole(room, nick, role, reason, onSuccess, onError) { - const item = $build("item", {nick, role}); - const iq = $iq({to: room, type: "set"}).c("query", {xmlns: Strophe.NS.MUC_ADMIN}).cnode(item.node); - if (reason !== null) { iq.c("reason", reason); } - return _converse.connection.sendIQ(iq, onSuccess, onError); - }, - - validateRoleChangeCommand (command, args) { - /* Check that a command to change a chat room user's role or - * affiliation has anough arguments. - */ - // TODO check if first argument is valid - if (args.length < 1 || args.length > 2) { - this.showStatusNotification( - __('Error: the "%1$s" command takes two arguments, the user\'s nickname and optionally a reason.', - command), - true - ); - return false; - } - return true; - }, - - clearChatRoomMessages (ev) { - /* Remove all messages from the chat room UI. - */ - if (!_.isUndefined(ev)) { ev.stopPropagation(); } - const result = confirm(__("Are you sure you want to clear the messages from this room?")); - if (result === true) { - this.content.innerHTML = ''; - } - return this; - }, - - onCommandError () { - this.showStatusNotification(__("Error: could not execute the command"), true); - }, - - onMessageSubmitted (text) { - /* Gets called when the user presses enter to send off a - * message in a chat room. - * - * Parameters: - * (String) text - The message text. - */ - if (_converse.muc_disable_moderator_commands) { - return this.sendChatRoomMessage(text); - } - const match = text.replace(/^\s*/, "").match(/^\/(.*?)(?: (.*))?$/) || [false, '', ''], - args = match[2] && match[2].splitOnce(' ') || [], - command = match[1].toLowerCase(); - switch (command) { - case 'admin': - if (!this.validateRoleChangeCommand(command, args)) { break; } - this.setAffiliation('admin', - [{ 'jid': args[0], - 'reason': args[1] - }]).then(null, this.onCommandError.bind(this)); - break; - case 'ban': - if (!this.validateRoleChangeCommand(command, args)) { break; } - this.setAffiliation('outcast', - [{ 'jid': args[0], - 'reason': args[1] - }]).then(null, this.onCommandError.bind(this)); - break; - case 'clear': - this.clearChatRoomMessages(); - break; - case 'deop': - if (!this.validateRoleChangeCommand(command, args)) { break; } - this.modifyRole( - this.model.get('jid'), args[0], 'participant', args[1], - undefined, this.onCommandError.bind(this)); - break; - case 'help': - this.showHelpMessages([ - `/admin: ${__("Change user's affiliation to admin")}`, - `/ban: ${__('Ban user from room')}`, - `/clear: ${__('Remove messages')}`, - `/deop: ${__('Change user role to participant')}`, - `/help: ${__('Show this menu')}`, - `/kick: ${__('Kick user from room')}`, - `/me: ${__('Write in 3rd person')}`, - `/member: ${__('Grant membership to a user')}`, - `/mute: ${__("Remove user's ability to post messages")}`, - `/nick: ${__('Change your nickname')}`, - `/op: ${__('Grant moderator role to user')}`, - `/owner: ${__('Grant ownership of this room')}`, - `/revoke: ${__("Revoke user's membership")}`, - `/subject: ${__('Set room subject')}`, - `/topic: ${__('Set room subject (alias for /subject)')}`, - `/voice: ${__('Allow muted user to post messages')}` - ]); - break; - case 'kick': - if (!this.validateRoleChangeCommand(command, args)) { break; } - this.modifyRole( - this.model.get('jid'), args[0], 'none', args[1], - undefined, this.onCommandError.bind(this)); - break; - case 'mute': - if (!this.validateRoleChangeCommand(command, args)) { break; } - this.modifyRole( - this.model.get('jid'), args[0], 'visitor', args[1], - undefined, this.onCommandError.bind(this)); - break; - case 'member': - if (!this.validateRoleChangeCommand(command, args)) { break; } - this.setAffiliation('member', - [{ 'jid': args[0], - 'reason': args[1] - }]).then(null, this.onCommandError.bind(this)); - break; - case 'nick': - _converse.connection.send($pres({ - from: _converse.connection.jid, - to: this.getRoomJIDAndNick(match[2]), - id: _converse.connection.getUniqueId() - }).tree()); - break; - case 'owner': - if (!this.validateRoleChangeCommand(command, args)) { break; } - this.setAffiliation('owner', - [{ 'jid': args[0], - 'reason': args[1] - }]).then(null, this.onCommandError.bind(this)); - break; - case 'op': - if (!this.validateRoleChangeCommand(command, args)) { break; } - this.modifyRole( - this.model.get('jid'), args[0], 'moderator', args[1], - undefined, this.onCommandError.bind(this)); - break; - case 'revoke': - if (!this.validateRoleChangeCommand(command, args)) { break; } - this.setAffiliation('none', - [{ 'jid': args[0], - 'reason': args[1] - }]).then(null, this.onCommandError.bind(this)); - break; - case 'topic': - case 'subject': - _converse.connection.send( - $msg({ - to: this.model.get('jid'), - from: _converse.connection.jid, - type: "groupchat" - }).c("subject", {xmlns: "jabber:client"}).t(match[2]).tree() - ); - break; - case 'voice': - if (!this.validateRoleChangeCommand(command, args)) { break; } - this.modifyRole( - this.model.get('jid'), args[0], 'participant', args[1], - undefined, this.onCommandError.bind(this)); - break; - default: - this.sendChatRoomMessage(text); - break; - } - }, - - handleMUCMessage (stanza) { - /* Handler for all MUC messages sent to this chat room. - * - * Parameters: - * (XMLElement) stanza: The message stanza. - */ - const configuration_changed = stanza.querySelector("status[code='104']"); - const logging_enabled = stanza.querySelector("status[code='170']"); - const logging_disabled = stanza.querySelector("status[code='171']"); - const room_no_longer_anon = stanza.querySelector("status[code='172']"); - const room_now_semi_anon = stanza.querySelector("status[code='173']"); - const room_now_fully_anon = stanza.querySelector("status[code='173']"); - if (configuration_changed || logging_enabled || logging_disabled || - room_no_longer_anon || room_now_semi_anon || room_now_fully_anon) { - this.getRoomFeatures(); - } - _.flow(this.showStatusMessages.bind(this), this.onChatRoomMessage.bind(this))(stanza); - return true; - }, - - getRoomJIDAndNick (nick) { - /* Utility method to construct the JID for the current user - * as occupant of the room. - * - * This is the room JID, with the user's nick added at the - * end. - * - * For example: room@conference.example.org/nickname - */ - if (nick) { - this.model.save({'nick': nick}); - } else { - nick = this.model.get('nick'); - } - const room = this.model.get('jid'); - const jid = Strophe.getBareJidFromJid(room); - return jid + (nick !== null ? `/${nick}` : ""); - }, - - registerHandlers () { - /* Register presence and message handlers for this chat - * room - */ - const room_jid = this.model.get('jid'); - this.removeHandlers(); - this.presence_handler = _converse.connection.addHandler( - this.onChatRoomPresence.bind(this), - Strophe.NS.MUC, 'presence', null, null, room_jid, - {'ignoreNamespaceFragment': true, 'matchBareFromJid': true} - ); - this.message_handler = _converse.connection.addHandler( - this.handleMUCMessage.bind(this), - null, 'message', 'groupchat', null, room_jid, - {'matchBareFromJid': true} - ); - }, - - removeHandlers () { - /* Remove the presence and message handlers that were - * registered for this chat room. - */ - if (this.message_handler) { - _converse.connection.deleteHandler(this.message_handler); - delete this.message_handler; - } - if (this.presence_handler) { - _converse.connection.deleteHandler(this.presence_handler); - delete this.presence_handler; - } - return this; - }, - - join (nick, password) { - /* Join the chat room. - * - * Parameters: - * (String) nick: The user's nickname - * (String) password: Optional password, if required by - * the room. - */ - nick = nick ? nick : this.model.get('nick'); - if (!nick) { - return this.checkForReservedNick(); - } - if (this.model.get('connection_status') === converse.ROOMSTATUS.ENTERED) { - // We have restored a chat room from session storage, - // so we don't send out a presence stanza again. - return this; - } - - const stanza = $pres({ - 'from': _converse.connection.jid, - 'to': this.getRoomJIDAndNick(nick) - }).c("x", {'xmlns': Strophe.NS.MUC}) - .c("history", {'maxstanzas': _converse.muc_history_max_stanzas}).up(); - if (password) { - stanza.cnode(Strophe.xmlElement("password", [], password)); - } - this.model.save('connection_status', converse.ROOMSTATUS.CONNECTING); - _converse.connection.send(stanza); - return this; - }, - - sendUnavailablePresence (exit_msg) { - const presence = $pres({ - type: "unavailable", - from: _converse.connection.jid, - to: this.getRoomJIDAndNick() - }); - if (exit_msg !== null) { - presence.c("status", exit_msg); - } - _converse.connection.sendPresence(presence); - }, - - leave(exit_msg) { - /* Leave the chat room. - * - * Parameters: - * (String) exit_msg: Optional message to indicate your - * reason for leaving. - */ - this.hide(); - if (Backbone.history.getFragment() === "converse/room?jid="+this.model.get('jid')) { - _converse.router.navigate(''); - } - this.occupantsview.model.reset(); - this.occupantsview.model.browserStorage._clear(); - if (_converse.connection.connected) { - this.sendUnavailablePresence(exit_msg); - } - u.safeSave( - this.model, - {'connection_status': converse.ROOMSTATUS.DISCONNECTED} - ); - this.removeHandlers(); - _converse.ChatBoxView.prototype.close.apply(this, arguments); - }, - - renderConfigurationForm (stanza) { - /* Renders a form given an IQ stanza containing the current - * room configuration. - * - * Returns a promise which resolves once the user has - * either submitted the form, or canceled it. - * - * Parameters: - * (XMLElement) stanza: The IQ stanza containing the room - * config. - */ - const container_el = this.el.querySelector('.chatroom-body'); - _.each(container_el.querySelectorAll('.chatroom-form-container'), u.removeElement); - _.each(container_el.children, u.hideElement); - container_el.insertAdjacentHTML('beforeend', tpl_chatroom_form()); - - const form_el = container_el.querySelector('form.chatroom-form'), - fieldset_el = form_el.querySelector('fieldset'), - fields = stanza.querySelectorAll('field'), - title = _.get(stanza.querySelector('title'), 'textContent'), - instructions = _.get(stanza.querySelector('instructions'), 'textContent'); - - u.removeElement(fieldset_el.querySelector('span.spinner')); - fieldset_el.insertAdjacentHTML('beforeend', `${title}`); - - if (instructions && instructions !== title) { - fieldset_el.insertAdjacentHTML('beforeend', `

${instructions}

`); - } - _.each(fields, function (field) { - fieldset_el.insertAdjacentHTML('beforeend', u.xForm2webForm(field, stanza)); - }); - - // Render save/cancel buttons - const last_fieldset_el = document.createElement('fieldset'); - last_fieldset_el.insertAdjacentHTML( - 'beforeend', - ``); - last_fieldset_el.insertAdjacentHTML( - 'beforeend', - ``); - form_el.insertAdjacentElement('beforeend', last_fieldset_el); - - last_fieldset_el.querySelector('input[type=button]').addEventListener('click', (ev) => { - ev.preventDefault(); - this.closeForm(); - }); - - form_el.addEventListener('submit', (ev) => { - ev.preventDefault(); - this.saveConfiguration(ev.target).then( - this.getRoomFeatures.bind(this) - ); - }, - false - ); - }, - - sendConfiguration(config, onSuccess, onError) { - /* Send an IQ stanza with the room configuration. - * - * Parameters: - * (Array) config: The room configuration - * (Function) onSuccess: Callback upon succesful IQ response - * The first parameter passed in is IQ containing the - * room configuration. - * The second is the response IQ from the server. - * (Function) onError: Callback upon error IQ response - * The first parameter passed in is IQ containing the - * room configuration. - * The second is the response IQ from the server. - */ - const iq = $iq({to: this.model.get('jid'), type: "set"}) - .c("query", {xmlns: Strophe.NS.MUC_OWNER}) - .c("x", {xmlns: Strophe.NS.XFORM, type: "submit"}); - _.each(config || [], function (node) { iq.cnode(node).up(); }); - onSuccess = _.isUndefined(onSuccess) ? _.noop : _.partial(onSuccess, iq.nodeTree); - onError = _.isUndefined(onError) ? _.noop : _.partial(onError, iq.nodeTree); - return _converse.connection.sendIQ(iq, onSuccess, onError); - }, - - saveConfiguration (form) { - /* Submit the room configuration form by sending an IQ - * stanza to the server. - * - * Returns a promise which resolves once the XMPP server - * has return a response IQ. - * - * Parameters: - * (HTMLElement) form: The configuration form DOM element. - */ - return new Promise((resolve, reject) => { - const inputs = form ? sizzle(':input:not([type=button]):not([type=submit])', form) : [], - configArray = _.map(inputs, u.webForm2xForm); - this.sendConfiguration(configArray, resolve, reject); - this.closeForm(); - }); - }, - - autoConfigureChatRoom () { - /* Automatically configure room based on the - * 'roomconfig' data on this view's model. - * - * Returns a promise which resolves once a response IQ has - * been received. - * - * Parameters: - * (XMLElement) stanza: IQ stanza from the server, - * containing the configuration. - */ - const that = this; - return new Promise((resolve, reject) => { - this.fetchRoomConfiguration().then(function (stanza) { - const configArray = [], - fields = stanza.querySelectorAll('field'), - config = that.model.get('roomconfig'); - let count = fields.length; - - _.each(fields, function (field) { - const fieldname = field.getAttribute('var').replace('muc#roomconfig_', ''), - type = field.getAttribute('type'); - let value; - if (fieldname in config) { - switch (type) { - case 'boolean': - value = config[fieldname] ? 1 : 0; - break; - case 'list-multi': - // TODO: we don't yet handle "list-multi" types - value = field.innerHTML; - break; - default: - value = config[fieldname]; - } - field.innerHTML = $build('value').t(value); - } - configArray.push(field); - if (!--count) { - that.sendConfiguration(configArray, resolve, reject); - } - }); - }); - }); - }, - - closeForm () { - /* Remove the configuration form without submitting and - * return to the chat view. - */ - u.removeElement(this.el.querySelector('.chatroom-form-container')); - this.renderAfterTransition(); - }, - - fetchRoomConfiguration (handler) { - /* Send an IQ stanza to fetch the room configuration data. - * Returns a promise which resolves once the response IQ - * has been received. - * - * Parameters: - * (Function) handler: The handler for the response IQ - */ - return new Promise((resolve, reject) => { - _converse.connection.sendIQ( - $iq({ - 'to': this.model.get('jid'), - 'type': "get" - }).c("query", {xmlns: Strophe.NS.MUC_OWNER}), - (iq) => { - if (handler) { - handler.apply(this, arguments); - } - resolve(iq); - }, - reject // errback - ); - }); - }, - - parseRoomFeatures (iq) { - /* See http://xmpp.org/extensions/xep-0045.html#disco-roominfo - * - * - * - * - * - * - * - * - * - * - */ - const features = { - 'features_fetched': true, - 'name': iq.querySelector('identity').getAttribute('name') - } - _.each(iq.querySelectorAll('feature'), function (field) { - const fieldname = field.getAttribute('var'); - if (!fieldname.startsWith('muc_')) { - if (fieldname === Strophe.NS.MAM) { - features.mam_enabled = true; - } - return; - } - features[fieldname.replace('muc_', '')] = true; - }); - const desc_field = iq.querySelector('field[var="muc#roominfo_description"] value'); - if (!_.isNull(desc_field)) { - features.description = desc_field.textContent; - } - this.model.save(features); - }, - - getRoomFeatures () { - /* Fetch the room disco info, parse it and then - * save it on the Backbone.Model of this chat rooms. - */ - return new Promise((resolve, reject) => { - _converse.connection.disco.info( - this.model.get('jid'), - null, - _.flow(this.parseRoomFeatures.bind(this), resolve), - () => { reject(new Error("Could not parse the room features")) }, - 5000 - ); - }); - }, - - getAndRenderConfigurationForm (ev) { - /* Start the process of configuring a chat room, either by - * rendering a configuration form, or by auto-configuring - * based on the "roomconfig" data stored on the - * Backbone.Model. - * - * Stores the new configuration on the Backbone.Model once - * completed. - * - * Paremeters: - * (Event) ev: DOM event that might be passed in if this - * method is called due to a user action. In this - * case, auto-configure won't happen, regardless of - * the settings. - */ - this.showSpinner(); - this.fetchRoomConfiguration() - .then(this.renderConfigurationForm.bind(this)) - .catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR)); - }, - - submitNickname (ev) { - /* Get the nickname value from the form and then join the - * chat room with it. - */ - ev.preventDefault(); - const nick_el = ev.target.nick; - const nick = nick_el.value; - if (!nick) { - nick_el.classList.add('error'); - return; - } - else { - nick_el.classList.remove('error'); - } - this.el.querySelector('.chatroom-form-container').outerHTML = tpl_spinner(); - this.join(nick); - }, - - checkForReservedNick () { - /* User service-discovery to ask the XMPP server whether - * this user has a reserved nickname for this room. - * If so, we'll use that, otherwise we render the nickname - * form. - */ - this.showSpinner(); - _converse.connection.sendIQ( - $iq({ - 'to': this.model.get('jid'), - 'from': _converse.connection.jid, - 'type': "get" - }).c("query", { - 'xmlns': Strophe.NS.DISCO_INFO, - 'node': 'x-roomuser-item' - }), - this.onNickNameFound.bind(this), - this.onNickNameNotFound.bind(this) - ); - return this; - }, - - onNickNameFound (iq) { - /* We've received an IQ response from the server which - * might contain the user's reserved nickname. - * If no nickname is found we either render a form for - * them to specify one, or we try to join the room with the - * node of the user's JID. - * - * Parameters: - * (XMLElement) iq: The received IQ stanza - */ - const identity_el = iq.querySelector('query[node="x-roomuser-item"] identity'), - nick = identity_el ? identity_el.getAttribute('name') : null; - if (!nick) { - this.onNickNameNotFound(); - } else { - this.join(nick); - } - }, - - onNickNameNotFound (message) { - if (_converse.muc_nickname_from_jid) { - // We try to enter the room with the node part of - // the user's JID. - this.join(this.getDefaultNickName()); - } else { - this.renderNicknameForm(message); - } - }, - - getDefaultNickName () { - /* The default nickname (used when muc_nickname_from_jid is true) - * is the node part of the user's JID. - * We put this in a separate method so that it can be - * overridden by plugins. - */ - return Strophe.unescapeNode(Strophe.getNodeFromJid(_converse.bare_jid)); - }, - - onNicknameClash (presence) { - /* When the nickname is already taken, we either render a - * form for the user to choose a new nickname, or we - * try to make the nickname unique by adding an integer to - * it. So john will become john-2, and then john-3 and so on. - * - * Which option is take depends on the value of - * muc_nickname_from_jid. - */ - if (_converse.muc_nickname_from_jid) { - const nick = presence.getAttribute('from').split('/')[1]; - if (nick === this.getDefaultNickName()) { - this.join(nick + '-2'); - } else { - const del= nick.lastIndexOf("-"); - const num = nick.substring(del+1, nick.length); - this.join(nick.substring(0, del+1) + String(Number(num)+1)); - } - } else { - this.renderNicknameForm( - __("The nickname you chose is reserved or "+ - "currently in use, please choose a different one.") - ); - } - }, - - hideChatRoomContents () { - const container_el = this.el.querySelector('.chatroom-body'); - if (!_.isNull(container_el)) { - _.each(container_el.children, (child) => { child.classList.add('hidden'); }); - } - }, - - renderNicknameForm (message) { - /* Render a form which allows the user to choose their - * nickname. - */ - this.hideChatRoomContents(); - _.each(this.el.querySelectorAll('span.centered.spinner'), u.removeElement); - if (!_.isString(message)) { - message = ''; - } - const container_el = this.el.querySelector('.chatroom-body'); - container_el.insertAdjacentHTML( - 'beforeend', - tpl_chatroom_nickname_form({ - heading: __('Please choose your nickname'), - label_nickname: __('Nickname'), - label_join: __('Enter room'), - validation_message: message - })); - this.model.save('connection_status', converse.ROOMSTATUS.NICKNAME_REQUIRED); - - const form_el = this.el.querySelector('.chatroom-form'); - form_el.addEventListener('submit', this.submitNickname.bind(this), false); - }, - - submitPassword (ev) { - ev.preventDefault(); - const password = this.el.querySelector('.chatroom-form input[type=password]').value; - this.showSpinner(); - this.join(this.model.get('nick'), password); - }, - - renderPasswordForm () { - const container_el = this.el.querySelector('.chatroom-body'); - _.each(container_el.children, u.hideElement); - _.each(this.el.querySelectorAll('.spinner'), u.removeElement); - - container_el.insertAdjacentHTML('beforeend', - tpl_chatroom_password_form({ - heading: __('This chatroom requires a password'), - label_password: __('Password: '), - label_submit: __('Submit') - })); - - this.model.save('connection_status', converse.ROOMSTATUS.PASSWORD_REQUIRED); - this.el.querySelector('.chatroom-form').addEventListener( - 'submit', this.submitPassword.bind(this), false); - }, - - showDisconnectMessage (msg) { - u.hideElement(this.el.querySelector('.chat-area')); - u.hideElement(this.el.querySelector('.occupants')); - _.each(this.el.querySelectorAll('.spinner'), u.removeElement); - this.el.querySelector('.chatroom-body').insertAdjacentHTML( - 'beforeend', - tpl_chatroom_disconnect({ - 'disconnect_message': msg - }) - ); - }, - - getMessageFromStatus (stat, stanza, is_self) { - /* Parameters: - * (XMLElement) stat: A element. - * (Boolean) is_self: Whether the element refers to the - * current user. - * (XMLElement) stanza: The original stanza received. - */ - const code = stat.getAttribute('code'); - if (code === '110') { return; } - if (code in _converse.muc.info_messages) { - return _converse.muc.info_messages[code]; - } - let nick; - if (!is_self) { - if (code in _converse.muc.action_info_messages) { - nick = Strophe.getResourceFromJid(stanza.getAttribute('from')); - return __(_converse.muc.action_info_messages[code], nick); - } - } else if (code in _converse.muc.new_nickname_messages) { - if (is_self && code === "210") { - nick = Strophe.getResourceFromJid(stanza.getAttribute('from')); - } else if (is_self && code === "303") { - nick = stanza.querySelector('x item').getAttribute('nick'); - } - return __(_converse.muc.new_nickname_messages[code], nick); - } - return; - }, - - saveAffiliationAndRole (pres) { - /* Parse the presence stanza for the current user's - * affiliation. - * - * Parameters: - * (XMLElement) pres: A stanza. - */ - const item = sizzle(`x[xmlns="${Strophe.NS.MUC_USER}"] item`, pres).pop(); - const is_self = pres.querySelector("status[code='110']"); - if (is_self && !_.isNil(item)) { - const affiliation = item.getAttribute('affiliation'); - const role = item.getAttribute('role'); - if (affiliation) { - this.model.save({'affiliation': affiliation}); - } - if (role) { - this.model.save({'role': role}); - } - } - }, - - parseXUserElement (x, stanza, is_self) { - /* Parse the passed-in - * element and construct a map containing relevant - * information. - */ - // 1. Get notification messages based on the elements. - const statuses = x.querySelectorAll('status'); - const mapper = _.partial(this.getMessageFromStatus, _, stanza, is_self); - const notification = {}; - const messages = _.reject(_.map(statuses, mapper), _.isUndefined); - if (messages.length) { - notification.messages = messages; - } - // 2. Get disconnection messages based on the elements - const codes = _.invokeMap(statuses, Element.prototype.getAttribute, 'code'); - const disconnection_codes = _.intersection(codes, _.keys(_converse.muc.disconnect_messages)); - const disconnected = is_self && disconnection_codes.length > 0; - if (disconnected) { - notification.disconnected = true; - notification.disconnection_message = _converse.muc.disconnect_messages[disconnection_codes[0]]; - } - // 3. Find the reason and actor from the element - const item = x.querySelector('item'); - // By using querySelector above, we assume here there is - // one per - // element. This appears to be a safe assumption, since - // each element pertains to a single user. - if (!_.isNull(item)) { - const reason = item.querySelector('reason'); - if (reason) { - notification.reason = reason ? reason.textContent : undefined; - } - const actor = item.querySelector('actor'); - if (actor) { - notification.actor = actor ? actor.getAttribute('nick') : undefined; - } - } - return notification; - }, - - displayNotificationsforUser (notification) { - /* Given the notification object generated by - * parseXUserElement, display any relevant messages and - * information to the user. - */ - if (notification.disconnected) { - this.showDisconnectMessage(notification.disconnection_message); - if (notification.actor) { - this.showDisconnectMessage(__('This action was done by %1$s.', notification.actor)); - } - if (notification.reason) { - this.showDisconnectMessage(__('The reason given is: "%1$s".', notification.reason)); - } - this.model.save('connection_status', converse.ROOMSTATUS.DISCONNECTED); - return; - } - _.each(notification.messages, (message) => { - this.content.insertAdjacentHTML( - 'beforeend', - tpl_info({ - 'data': '', - 'isodate': moment().format(), - 'message': message - })); - }); - if (notification.reason) { - this.showStatusNotification(__('The reason given is: "%1$s".', notification.reason), true); - } - if (_.get(notification.messages, 'length')) { - this.scrollDown(); - } - }, - - displayJoinNotification (stanza) { - const nick = Strophe.getResourceFromJid(stanza.getAttribute('from')); - const stat = stanza.querySelector('status'); - const last_el = this.content.lastElementChild; - - if (_.includes(_.get(last_el, 'classList', []), 'chat-info') && - _.get(last_el, 'dataset', {}).leave === `"${nick}"`) { - last_el.outerHTML = - tpl_info({ - 'data': `data-leavejoin="${nick}"`, - 'isodate': moment().format(), - 'message': __('%1$s has left and re-entered the room.', nick) - }); - } else { - let message; - if (_.get(stat, 'textContent')) { - message = __('%1$s has entered the room. "%2$s"', nick, stat.textContent); - } else { - message = __('%1$s has entered the room.', nick); - } - const data = { - 'data': `data-join="${nick}"`, - 'isodate': moment().format(), - 'message': message - }; - if (_.includes(_.get(last_el, 'classList', []), 'chat-info') && - _.get(last_el, 'dataset', {}).joinleave === `"${nick}"`) { - - last_el.outerHTML = tpl_info(data); - } else { - const el = u.stringToElement(tpl_info(data)); - this.content.insertAdjacentElement('beforeend', el); - this.insertDayIndicator(el); - } - } - this.scrollDown(); - }, - - displayLeaveNotification (stanza) { - const nick = Strophe.getResourceFromJid(stanza.getAttribute('from')); - const stat = stanza.querySelector('status'); - const last_el = this.content.lastElementChild; - if (_.includes(_.get(last_el, 'classList', []), 'chat-info') && - _.get(last_el, 'dataset', {}).join === `"${nick}"`) { - - let message; - if (_.get(stat, 'textContent')) { - message = __('%1$s has entered and left the room. "%2$s"', nick, stat.textContent); - } else { - message = __('%1$s has entered and left the room.', nick); - } - last_el.outerHTML = - tpl_info({ - 'data': `data-joinleave="${nick}"`, - 'isodate': moment().format(), - 'message': message - }); - } else { - let message; - if (_.get(stat, 'textContent')) { - message = __('%1$s has left the room. "%2$s"', nick, stat.textContent); - } else { - message = __('%1$s has left the room.', nick); - } - const data = { - 'message': message, - 'isodate': moment().format(), - 'data': `data-leave="${nick}"` - } - if (_.includes(_.get(last_el, 'classList', []), 'chat-info') && - _.get(last_el, 'dataset', {}).leavejoin === `"${nick}"`) { - - last_el.outerHTML = tpl_info(data); - } else { - const el = u.stringToElement(tpl_info(data)); - this.content.insertAdjacentElement('beforeend', el); - this.insertDayIndicator(el); - } - } - this.scrollDown(); - }, - - displayJoinOrLeaveNotification (stanza) { - if (stanza.getAttribute('type') === 'unavailable') { - this.displayLeaveNotification(stanza); - } else { - const nick = Strophe.getResourceFromJid(stanza.getAttribute('from')); - if (!this.occupantsview.model.find({'nick': nick})) { - // Only show join message if we don't already have the - // occupant model. Doing so avoids showing duplicate - // join messages. - this.displayJoinNotification(stanza); - } - } - }, - - showStatusMessages (stanza) { - /* Check for status codes and communicate their purpose to the user. - * See: http://xmpp.org/registrar/mucstatus.html - * - * Parameters: - * (XMLElement) stanza: The message or presence stanza - * containing the status codes. - */ - const elements = sizzle(`x[xmlns="${Strophe.NS.MUC_USER}"]`, stanza); - const is_self = stanza.querySelectorAll("status[code='110']").length; - const iteratee = _.partial(this.parseXUserElement.bind(this), _, stanza, is_self); - const notifications = _.reject(_.map(elements, iteratee), _.isEmpty); - if (_.isEmpty(notifications)) { - if (_converse.muc_show_join_leave && - stanza.nodeName === 'presence' && - this.model.get('connection_status') === converse.ROOMSTATUS.ENTERED) { - this.displayJoinOrLeaveNotification(stanza); - } - } else { - _.each(notifications, this.displayNotificationsforUser.bind(this)); - } - return stanza; - }, - - showErrorMessage (presence) { - // We didn't enter the room, so we must remove it from the MUC add-on - const error = presence.querySelector('error'); - if (error.getAttribute('type') === 'auth') { - if (!_.isNull(error.querySelector('not-authorized'))) { - this.renderPasswordForm(); - } else if (!_.isNull(error.querySelector('registration-required'))) { - this.showDisconnectMessage(__('You are not on the member list of this room.')); - } else if (!_.isNull(error.querySelector('forbidden'))) { - this.showDisconnectMessage(__('You have been banned from this room.')); - } - } else if (error.getAttribute('type') === 'modify') { - if (!_.isNull(error.querySelector('jid-malformed'))) { - this.showDisconnectMessage(__('No nickname was specified.')); - } - } else if (error.getAttribute('type') === 'cancel') { - if (!_.isNull(error.querySelector('not-allowed'))) { - this.showDisconnectMessage(__('You are not allowed to create new rooms.')); - } else if (!_.isNull(error.querySelector('not-acceptable'))) { - this.showDisconnectMessage(__("Your nickname doesn't conform to this room's policies.")); - } else if (!_.isNull(error.querySelector('conflict'))) { - this.onNicknameClash(presence); - } else if (!_.isNull(error.querySelector('item-not-found'))) { - this.showDisconnectMessage(__("This room does not (yet) exist.")); - } else if (!_.isNull(error.querySelector('service-unavailable'))) { - this.showDisconnectMessage(__("This room has reached its maximum number of occupants.")); - } - } - }, - - renderAfterTransition () { - /* Rerender the room after some kind of transition. For - * example after the spinner has been removed or after a - * form has been submitted and removed. - */ - if (this.model.get('connection_status') == converse.ROOMSTATUS.NICKNAME_REQUIRED) { - this.renderNicknameForm(); - } else if (this.model.get('connection_status') == converse.ROOMSTATUS.PASSWORD_REQUIRED) { - this.renderPasswordForm(); - } else { - this.el.querySelector('.chat-area').classList.remove('hidden'); - this.setOccupantsVisibility(); - this.scrollDown(); - } - }, - - showSpinner () { - u.removeElement(this.el.querySelector('.spinner')); - - const container_el = this.el.querySelector('.chatroom-body'); - const children = Array.prototype.slice.call(container_el.children, 0); - container_el.insertAdjacentHTML('afterbegin', tpl_spinner()); - _.each(children, u.hideElement); - - }, - - hideSpinner () { - /* Check if the spinner is being shown and if so, hide it. - * Also make sure then that the chat area and occupants - * list are both visible. - */ - const spinner = this.el.querySelector('.spinner'); - if (!_.isNull(spinner)) { - u.removeElement(spinner); - this.renderAfterTransition(); - } - return this; - }, - - onOwnChatRoomPresence (pres) { - /* Handles a received presence relating to the current - * user. - * - * For locked rooms (which are by definition "new"), the - * room will either be auto-configured or created instantly - * (with default config) or a configuration room will be - * rendered. - * - * If the room is not locked, then the room will be - * auto-configured only if applicable and if the current - * user is the room's owner. - * - * Parameters: - * (XMLElement) pres: The stanza - */ - this.saveAffiliationAndRole(pres); - - const locked_room = pres.querySelector("status[code='201']"); - if (locked_room) { - if (this.model.get('auto_configure')) { - this.autoConfigureChatRoom().then(this.getRoomFeatures.bind(this)); - } else if (_converse.muc_instant_rooms) { - // Accept default configuration - this.saveConfiguration().then(this.getRoomFeatures.bind(this)); - } else { - this.getAndRenderConfigurationForm(); - return; // We haven't yet entered the room, so bail here. - } - } else if (!this.model.get('features_fetched')) { - // The features for this room weren't fetched. - // That must mean it's a new room without locking - // (in which case Prosody doesn't send a 201 status), - // otherwise the features would have been fetched in - // the "initialize" method already. - if (this.model.get('affiliation') === 'owner' && this.model.get('auto_configure')) { - this.autoConfigureChatRoom().then(this.getRoomFeatures.bind(this)); - } else { - this.getRoomFeatures(); - } - } - this.model.save('connection_status', converse.ROOMSTATUS.ENTERED); - }, - - onChatRoomPresence (pres) { - /* Handles all MUC presence stanzas. - * - * Parameters: - * (XMLElement) pres: The stanza - */ - if (pres.getAttribute('type') === 'error') { - this.model.save('connection_status', converse.ROOMSTATUS.DISCONNECTED); - this.showErrorMessage(pres); - return true; - } - const is_self = pres.querySelector("status[code='110']"); - if (is_self && pres.getAttribute('type') !== 'unavailable') { - this.onOwnChatRoomPresence(pres); - } - this.hideSpinner().showStatusMessages(pres); - // This must be called after showStatusMessages so that - // "join" messages are correctly shown. - this.occupantsview.updateOccupantsOnPresence(pres); - if (this.model.get('role') !== 'none' && - this.model.get('connection_status') === converse.ROOMSTATUS.CONNECTING) { - this.model.save('connection_status', converse.ROOMSTATUS.CONNECTED); - } - return true; - }, - - setChatRoomSubject (sender, subject) { - // For translators: the %1$s and %2$s parts will get - // replaced by the user and topic text respectively - // Example: Topic set by JC Brand to: Hello World! - this.content.insertAdjacentHTML( - 'beforeend', - tpl_info({ - 'data': '', - 'isodate': moment().format(), - 'message': __('Topic set by %1$s to: %2$s', sender, subject) - })); - this.scrollDown(); - }, - - isDuplicateBasedOnTime (message) { - /* Checks whether a received messages is actually a - * duplicate based on whether it has a "ts" attribute - * with a unix timestamp. - * - * This is used for better integration with Slack's XMPP - * gateway, which doesn't use message IDs but instead the - * aforementioned "ts" attributes. - */ - const entity = _converse.disco_entities.get(_converse.domain); - if (entity.identities.where({'name': "Slack-XMPP"})) { - const ts = message.getAttribute('ts'); - if (_.isNull(ts)) { - return false; - } else { - return this.model.messages.where({ - 'sender': 'me', - 'message': this.model.getMessageBody(message) - }).filter( - (msg) => Math.abs(moment(msg.get('time')).diff(moment.unix(ts))) < 5000 - ).length > 0; - } - } - return false; - }, - - isDuplicate (message, original_stanza) { - const msgid = message.getAttribute('id'), - jid = message.getAttribute('from'), - resource = Strophe.getResourceFromJid(jid), - sender = resource && Strophe.unescapeNode(resource) || ''; - if (msgid) { - return this.model.messages.filter( - // Some bots (like HAL in the prosody chatroom) - // respond to commands with the same ID as the - // original message. So we also check the sender. - (msg) => msg.get('msgid') === msgid && msg.get('fullname') === sender - ).length > 0; - } - return this.isDuplicateBasedOnTime(message); - }, - - onChatRoomMessage (message) { - /* Given a stanza, create a message - * Backbone.Model if appropriate. - * - * Parameters: - * (XMLElement) msg: The received message stanza - */ - const original_stanza = message, - forwarded = message.querySelector('forwarded'); - let delay; - if (!_.isNull(forwarded)) { - message = forwarded.querySelector('message'); - delay = forwarded.querySelector('delay'); - } - const jid = message.getAttribute('from'), - resource = Strophe.getResourceFromJid(jid), - sender = resource && Strophe.unescapeNode(resource) || '', - subject = _.propertyOf(message.querySelector('subject'))('textContent'); - - if (this.isDuplicate(message, original_stanza)) { - return true; - } - if (subject) { - this.setChatRoomSubject(sender, subject); - } - if (sender === '') { - return true; - } - this.model.incrementUnreadMsgCounter(original_stanza); - this.model.createMessage(message, delay, original_stanza); - if (sender !== this.model.get('nick')) { - // We only emit an event if it's not our own message - _converse.emit( - 'message', - {'stanza': original_stanza, 'chatbox': this.model} - ); - } - return true; - } - }); - - _converse.ChatRoomOccupant = Backbone.Model.extend({ - initialize (attributes) { - this.set(_.extend({ - 'id': _converse.connection.getUniqueId(), - }, attributes)); - } - }); - - _converse.ChatRoomOccupantView = Backbone.VDOMView.extend({ - tagName: 'li', - initialize () { - this.model.on('change', this.render, this); - }, - - toHTML () { - const show = this.model.get('show') || 'online'; - return tpl_occupant( - _.extend( - { 'jid': '', - 'show': show, - 'hint_show': _converse.PRETTY_CHAT_STATUS[show], - 'hint_occupant': __('Click to mention %1$s in your message.', this.model.get('nick')), - 'desc_moderator': __('This user is a moderator.'), - 'desc_occupant': __('This user can send messages in this room.'), - 'desc_visitor': __('This user can NOT send messages in this room.') - }, this.model.toJSON()) - ); - }, - - destroy () { - this.el.parentElement.removeChild(this.el); - } - }); - _converse.ChatRoomOccupants = Backbone.Collection.extend({ model: _converse.ChatRoomOccupant, @@ -2117,315 +337,6 @@ }, }); - _converse.ChatRoomOccupantsView = Backbone.OrderedListView.extend({ - tagName: 'div', - className: 'occupants', - listItems: 'model', - sortEvent: 'change:role', - listSelector: '.occupant-list', - - ItemView: _converse.ChatRoomOccupantView, - - initialize () { - Backbone.OrderedListView.prototype.initialize.apply(this, arguments); - - this.chatroomview = this.model.chatroomview; - this.chatroomview.model.on('change:open', this.renderInviteWidget, this); - this.chatroomview.model.on('change:affiliation', this.renderInviteWidget, this); - this.chatroomview.model.on('change:hidden', this.onFeatureChanged, this); - this.chatroomview.model.on('change:mam_enabled', this.onFeatureChanged, this); - this.chatroomview.model.on('change:membersonly', this.onFeatureChanged, this); - this.chatroomview.model.on('change:moderated', this.onFeatureChanged, this); - this.chatroomview.model.on('change:nonanonymous', this.onFeatureChanged, this); - this.chatroomview.model.on('change:open', this.onFeatureChanged, this); - this.chatroomview.model.on('change:passwordprotected', this.onFeatureChanged, this); - this.chatroomview.model.on('change:persistent', this.onFeatureChanged, this); - this.chatroomview.model.on('change:publicroom', this.onFeatureChanged, this); - this.chatroomview.model.on('change:semianonymous', this.onFeatureChanged, this); - this.chatroomview.model.on('change:temporary', this.onFeatureChanged, this); - this.chatroomview.model.on('change:unmoderated', this.onFeatureChanged, this); - this.chatroomview.model.on('change:unsecured', this.onFeatureChanged, this); - - const id = b64_sha1(`converse.occupants${_converse.bare_jid}${this.chatroomview.model.get('jid')}`); - this.model.browserStorage = new Backbone.BrowserStorage.session(id); - this.render(); - this.model.fetch({ - 'add': true, - 'silent': true, - 'success': this.sortAndPositionAllItems.bind(this) - }); - }, - - render () { - this.el.innerHTML = tpl_chatroom_sidebar( - _.extend(this.chatroomview.model.toJSON(), { - 'allow_muc_invitations': _converse.allow_muc_invitations, - 'label_occupants': __('Occupants') - }) - ); - if (_converse.allow_muc_invitations) { - _converse.api.waitUntil('rosterContactsFetched').then( - this.renderInviteWidget.bind(this) - ); - } - return this.renderRoomFeatures(); - }, - - renderInviteWidget () { - const form = this.el.querySelector('form.room-invite'); - if (this.shouldInviteWidgetBeShown()) { - if (_.isNull(form)) { - const heading = this.el.querySelector('.occupants-heading'); - heading.insertAdjacentHTML( - 'afterend', - tpl_chatroom_invite({ - 'error_message': null, - 'label_invitation': __('Invite'), - }) - ); - this.initInviteWidget(); - } - } else if (!_.isNull(form)) { - form.remove(); - } - return this; - }, - - renderRoomFeatures () { - const picks = _.pick(this.chatroomview.model.attributes, ROOM_FEATURES), - iteratee = (a, v) => a || v, - el = this.el.querySelector('.chatroom-features'); - - el.innerHTML = tpl_chatroom_features( - _.extend(this.chatroomview.model.toJSON(), { - 'has_features': _.reduce(_.values(picks), iteratee), - 'label_features': __('Features'), - 'label_hidden': __('Hidden'), - 'label_mam_enabled': __('Message archiving'), - 'label_membersonly': __('Members only'), - 'label_moderated': __('Moderated'), - 'label_nonanonymous': __('Non-anonymous'), - 'label_open': __('Open'), - 'label_passwordprotected': __('Password protected'), - 'label_persistent': __('Persistent'), - 'label_public': __('Public'), - 'label_semianonymous': __('Semi-anonymous'), - 'label_temporary': __('Temporary'), - 'label_unmoderated': __('Unmoderated'), - 'label_unsecured': __('No password'), - 'tt_hidden': __('This room is not publicly searchable'), - 'tt_mam_enabled': __('Messages are archived on the server'), - 'tt_membersonly': __('This room is restricted to members only'), - 'tt_moderated': __('This room is being moderated'), - 'tt_nonanonymous': __('All other room occupants can see your XMPP username'), - 'tt_open': __('Anyone can join this room'), - 'tt_passwordprotected': __('This room requires a password before entry'), - 'tt_persistent': __('This room persists even if it\'s unoccupied'), - 'tt_public': __('This room is publicly searchable'), - 'tt_semianonymous': __('Only moderators can see your XMPP username'), - 'tt_temporary': __('This room will disappear once the last person leaves'), - 'tt_unmoderated': __('This room is not being moderated'), - 'tt_unsecured': __('This room does not require a password upon entry') - })); - this.setOccupantsHeight(); - return this; - }, - - onFeatureChanged (model) { - /* When a feature has been changed, it's logical opposite - * must be set to the opposite value. - * - * So for example, if "temporary" was set to "false", then - * "persistent" will be set to "true" in this method. - * - * Additionally a debounced render method is called to make - * sure the features widget gets updated. - */ - if (_.isUndefined(this.debouncedRenderRoomFeatures)) { - this.debouncedRenderRoomFeatures = _.debounce( - this.renderRoomFeatures, 100, {'leading': false} - ); - } - const changed_features = {}; - _.each(_.keys(model.changed), function (k) { - if (!_.isNil(ROOM_FEATURES_MAP[k])) { - changed_features[ROOM_FEATURES_MAP[k]] = !model.changed[k]; - } - }); - this.chatroomview.model.save(changed_features, {'silent': true}); - this.debouncedRenderRoomFeatures(); - }, - - setOccupantsHeight () { - const el = this.el.querySelector('.chatroom-features'); - this.el.querySelector('.occupant-list').style.cssText = - `height: calc(100% - ${el.offsetHeight}px - 5em);`; - }, - - parsePresence (pres) { - const id = Strophe.getResourceFromJid(pres.getAttribute("from")); - const data = { - nick: id, - type: pres.getAttribute("type"), - states: [] - }; - _.each(pres.childNodes, function (child) { - switch (child.nodeName) { - case "status": - data.status = child.textContent || null; - break; - case "show": - data.show = child.textContent || 'online'; - break; - case "x": - if (child.getAttribute("xmlns") === Strophe.NS.MUC_USER) { - _.each(child.childNodes, function (item) { - switch (item.nodeName) { - case "item": - data.affiliation = item.getAttribute("affiliation"); - data.role = item.getAttribute("role"); - data.jid = item.getAttribute("jid"); - data.nick = item.getAttribute("nick") || data.nick; - break; - case "status": - if (item.getAttribute("code")) { - data.states.push(item.getAttribute("code")); - } - } - }); - } - } - }); - return data; - }, - - findOccupant (data) { - /* Try to find an existing occupant based on the passed in - * data object. - * - * If we have a JID, we use that as lookup variable, - * otherwise we use the nick. We don't always have both, - * but should have at least one or the other. - */ - const jid = Strophe.getBareJidFromJid(data.jid); - if (jid !== null) { - return this.model.where({'jid': jid}).pop(); - } else { - return this.model.where({'nick': data.nick}).pop(); - } - }, - - updateOccupantsOnPresence (pres) { - /* Given a presence stanza, update the occupant models - * based on its contents. - * - * Parameters: - * (XMLElement) pres: The presence stanza - */ - const data = this.parsePresence(pres); - if (data.type === 'error') { - return true; - } - const occupant = this.findOccupant(data); - if (data.type === 'unavailable') { - if (occupant) { occupant.destroy(); } - } else { - const jid = Strophe.getBareJidFromJid(data.jid); - const attributes = _.extend(data, { - 'jid': jid ? jid : undefined, - 'resource': data.jid ? Strophe.getResourceFromJid(data.jid) : undefined - }); - if (occupant) { - occupant.save(attributes); - } else { - this.model.create(attributes); - } - } - }, - - promptForInvite (suggestion) { - const reason = prompt( - __('You are about to invite %1$s to the chat room "%2$s". '+ - 'You may optionally include a message, explaining the reason for the invitation.', - suggestion.text.label, this.model.get('id')) - ); - if (reason !== null) { - this.chatroomview.directInvite(suggestion.text.value, reason); - } - const form = suggestion.target.form, - error = form.querySelector('.pure-form-message.error'); - if (!_.isNull(error)) { - error.parentNode.removeChild(error); - } - suggestion.target.value = ''; - }, - - inviteFormSubmitted (evt) { - evt.preventDefault(); - const el = evt.target.querySelector('input.invited-contact'), - jid = el.value; - if (!jid || _.compact(jid.split('@')).length < 2) { - evt.target.outerHTML = tpl_chatroom_invite({ - 'error_message': __('Please enter a valid XMPP username'), - 'label_invitation': __('Invite'), - }); - this.initInviteWidget(); - return; - } - this.promptForInvite({ - 'target': el, - 'text': { - 'label': jid, - 'value': jid - }}); - }, - - shouldInviteWidgetBeShown () { - return _converse.allow_muc_invitations && - (this.chatroomview.model.get('open') || - this.chatroomview.model.get('affiliation') === "owner" - ); - }, - - initInviteWidget () { - const form = this.el.querySelector('form.room-invite'); - if (_.isNull(form)) { - return; - } - form.addEventListener('submit', this.inviteFormSubmitted.bind(this), false); - const el = this.el.querySelector('input.invited-contact'); - const list = _converse.roster.map(function (item) { - const label = item.get('fullname') || item.get('jid'); - return {'label': label, 'value':item.get('jid')}; - }); - const awesomplete = new Awesomplete(el, { - 'minChars': 1, - 'list': list - }); - el.addEventListener('awesomplete-selectcomplete', - this.promptForInvite.bind(this)); - } - }); - - - _converse.MUCJoinForm = Backbone.VDOMView.extend({ - initialize () { - this.model.on('change:muc_domain', this.render, this); - }, - - toHTML () { - return tpl_chatroom_join_form(_.assign(this.model.toJSON(), { - 'server_input_type': _converse.hide_muc_server && 'hidden' || 'text', - 'server_label_global_attr': _converse.hide_muc_server && ' hidden' || '', - 'label_room_name': __('Room name'), - 'label_nickname': __('Nickname'), - 'label_server': __('Server'), - 'label_join': __('Join Room'), - 'label_show_rooms': __('Show rooms') - })); - } - }); - _converse.RoomsPanelModel = Backbone.Model.extend({ defaults: { @@ -2433,253 +344,6 @@ }, }); - _converse.RoomsPanel = Backbone.NativeView.extend({ - /* Backbone.NativeView which renders MUC section of the control box. - * - * Chat rooms can be listed, joined and new rooms can be created. - */ - tagName: 'div', - className: 'controlbox-pane', - id: 'chatrooms', - events: { - 'submit form.add-chatroom': 'openChatRoom', - 'click input#show-rooms': 'showRooms', - 'click a.open-room': 'openChatRoom', - 'click a.room-info': 'toggleRoomInfo', - 'change input[name=server]': 'setDomain', - 'change input[name=nick]': 'setNick' - }, - - initialize (cfg) { - this.join_form = new _converse.MUCJoinForm({'model': this.model}); - this.model.on('change:muc_domain', this.onDomainChange, this); - this.model.on('change:nick', this.onNickChange, this); - }, - - render () { - this.el.innerHTML = tpl_room_panel({ - 'title_new_room': __('Click to add a new room') - }); - this.join_form.setElement(this.el.querySelector('.add-chatroom')); - this.join_form.render(); - return this; - }, - - onDomainChange (model) { - if (_converse.auto_list_rooms) { - this.updateRoomsList(); - } - }, - - onNickChange (model) { - const nick = this.el.querySelector('input.new-chatroom-nick'); - if (!_.isNull(nick)) { - nick.value = model.get('nick'); - } - }, - - removeSpinner () { - _.each(this.el.querySelectorAll('span.spinner'), - (el) => el.parentNode.removeChild(el) - ); - }, - - informNoRoomsFound () { - const chatrooms_el = this.el.querySelector('#available-chatrooms'); - chatrooms_el.innerHTML = tpl_rooms_results({ - 'feedback_text': __('No rooms found') - }); - const input_el = this.el.querySelector('input#show-rooms'); - input_el.classList.remove('hidden') - this.removeSpinner(); - }, - - roomStanzaItemToHTMLElement (room) { - const name = Strophe.unescapeNode( - room.getAttribute('name') || - room.getAttribute('jid') - ); - const div = document.createElement('div'); - div.innerHTML = tpl_room_item({ - 'name': name, - 'jid': room.getAttribute('jid'), - 'open_title': __('Click to open this room'), - 'info_title': __('Show more information on this room') - }); - return div.firstChild; - }, - - onRoomsFound (iq) { - /* Handle the IQ stanza returned from the server, containing - * all its public rooms. - */ - const available_chatrooms = this.el.querySelector('#available-chatrooms'); - this.rooms = iq.querySelectorAll('query item'); - if (this.rooms.length) { - // For translators: %1$s is a variable and will be - // replaced with the XMPP server name - available_chatrooms.innerHTML = tpl_rooms_results({ - 'feedback_text': __('Rooms found') - }); - const fragment = document.createDocumentFragment(); - const children = _.reject(_.map(this.rooms, this.roomStanzaItemToHTMLElement), _.isNil) - _.each(children, (child) => fragment.appendChild(child)); - available_chatrooms.appendChild(fragment); - const input_el = this.el.querySelector('input#show-rooms'); - input_el.classList.remove('hidden') - this.removeSpinner(); - } else { - this.informNoRoomsFound(); - } - return true; - }, - - updateRoomsList () { - /* Send an IQ stanza to the server asking for all rooms - */ - _converse.connection.sendIQ( - $iq({ - to: this.model.get('muc_domain'), - from: _converse.connection.jid, - type: "get" - }).c("query", {xmlns: Strophe.NS.DISCO_ITEMS}), - this.onRoomsFound.bind(this), - this.informNoRoomsFound.bind(this), - 5000 - ); - }, - - showRooms () { - const chatrooms_el = this.el.querySelector('#available-chatrooms'); - const server_el = this.el.querySelector('input.new-chatroom-server'); - const server = server_el.value; - if (!server) { - server_el.classList.add('error'); - return; - } - this.el.querySelector('input.new-chatroom-name').classList.remove('error'); - server_el.classList.remove('error'); - chatrooms_el.innerHTML = ''; - - const input_el = this.el.querySelector('input#show-rooms'); - input_el.classList.add('hidden') - input_el.insertAdjacentHTML('afterend', tpl_spinner()); - - this.model.save({muc_domain: server}); - this.updateRoomsList(); - }, - - insertRoomInfo (el, stanza) { - /* Insert room info (based on returned #disco IQ stanza) - * - * Parameters: - * (HTMLElement) el: The HTML DOM element that should - * contain the info. - * (XMLElement) stanza: The IQ stanza containing the room - * info. - */ - // All MUC features found here: http://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': _.get(_.head(sizzle('field[var="muc#roominfo_description"] value', stanza)), 'textContent'), - 'occ': _.get(_.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': __('Room Address (JID):'), - 'label_occ': __('Occupants:'), - '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 room'), - 'label_permanent_room': __('Permanent room'), - 'label_public': __('Public'), - 'label_semi_anon': __('Semi-anonymous'), - 'label_temp_room': __('Temporary room'), - 'label_unmoderated': __('Unmoderated') - })); - }, - - toggleRoomInfo (ev) { - /* Show/hide extra information about a room in the listing. - */ - const parent_el = u.ancestor(ev.target, '.room-item'), - 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()); - _converse.connection.disco.info( - ev.target.getAttribute('data-room-jid'), - null, - _.partial(this.insertRoomInfo, parent_el) - ); - } - }, - - parseRoomDataFromEvent (ev) { - let name, jid; - if (ev.type === 'click') { - name = ev.target.textContent; - jid = ev.target.getAttribute('data-room-jid'); - } else { - const name_el = this.el.querySelector('input.new-chatroom-name'); - const server_el= this.el.querySelector('input.new-chatroom-server'); - const server = server_el.value; - name = name_el.value.trim(); - name_el.value = ''; // Clear the input - if (name && server) { - jid = Strophe.escapeNode(name.toLowerCase()) + '@' + server.toLowerCase(); - name_el.classList.remove('error'); - server_el.classList.remove('error'); - this.model.save({muc_domain: server}); - } else { - if (!name) { name_el.classList.add('error'); } - if (!server) { server_el.classList.add('error'); } - return; - } - } - return { - 'jid': jid, - 'name': name || Strophe.unescapeNode(Strophe.getNodeFromJid(jid)) || jid - } - }, - - openChatRoom (ev) { - ev.preventDefault(); - const data = this.parseRoomDataFromEvent(ev); - if (!_.isUndefined(data)) { - openChatRoom(data); - } - }, - - setDomain (ev) { - this.model.save({muc_domain: ev.target.value}); - }, - - setNick (ev) { - this.model.save({nick: ev.target.value}); - } - }); - /************************ End of ChatRoomView **********************/ - _converse.onDirectMUCInvitation = function (message) { /* A direct MUC invitation to join a room has been received @@ -2714,10 +378,9 @@ } } if (result === true) { - const chatroom = openChatRoom({ - 'jid': room_jid, - 'password': x_el.getAttribute('password') - }); + const chatroom = _converse.openChatRoom( + room_jid, {'password': x_el.getAttribute('password') }); + if (chatroom.get('connection_status') === converse.ROOMSTATUS.DISCONNECTED) { _converse.chatboxviews.get(room_jid).join(); } @@ -2736,6 +399,18 @@ _converse.on('reconnected', registerDirectInvitationHandler); } + const getChatRoom = function (jid, attrs, create) { + jid = jid.toLowerCase(); + attrs.type = converse.CHATROOMS_TYPE; + attrs.id = jid; + attrs.box_id = b64_sha1(jid) + return _converse.chatboxes.getChatBox(jid, attrs, create); + }; + + const createChatRoom = function (jid, attrs) { + return getChatRoom(jid, attrs, true); + }; + function autoJoinRooms () { /* Automatically join chat rooms, based on the * "auto_join_rooms" configuration setting, which is an array @@ -2758,25 +433,52 @@ }); _converse.emit('roomsAutoJoined'); } + + + function reconnectToChatRooms () { + /* Upon a reconnection event from converse, join again + * all the open chat rooms. + */ + _converse.chatboxviews.each(function (view) { + if (view.model.get('type') === converse.CHATROOMS_TYPE) { + view.model.save('connection_status', converse.ROOMSTATUS.DISCONNECTED); + view.registerHandlers(); + view.join(); + view.fetchMessages(); + } + }); + } + + function disconnectChatRooms () { + /* When disconnecting, or reconnecting, mark all chat rooms as + * disconnected, so that they will be properly entered again + * when fetched from session storage. + */ + _converse.chatboxes.each(function (model) { + if (model.get('type') === converse.CHATROOMS_TYPE) { + model.save('connection_status', converse.ROOMSTATUS.DISCONNECTED); + } + }); + } + + /************************ BEGIN Event Handlers ************************/ + _converse.on('addClientFeatures', () => { + if (_converse.allow_muc) { + _converse.connection.disco.addFeature(Strophe.NS.MUC); + } + if (_converse.allow_muc_invitations) { + _converse.connection.disco.addFeature('jabber:x:conference'); // Invites + } + }); _converse.on('chatBoxesFetched', autoJoinRooms); + _converse.on('reconnected', reconnectToChatRooms); + _converse.on('reconnecting', disconnectChatRooms); + _converse.on('disconnecting', disconnectChatRooms); + /************************ END Event Handlers ************************/ - _converse.getChatRoom = function (jid, attrs, fetcher) { - jid = jid.toLowerCase(); - return _converse.getViewForChatBox( - fetcher(_.extend({ - 'id': jid, - 'jid': jid, - 'name': Strophe.unescapeNode(Strophe.getNodeFromJid(jid)), - 'type': CHATROOMS_TYPE, - 'box_id': b64_sha1(jid) - }, attrs), - attrs.bring_to_foreground - )); - }; - /* We extend the default converse.js API to add methods specific to MUC - * chat rooms. - */ + /************************ BEGIN API ************************/ + // We extend the default converse.js API to add methods specific to MUC chat rooms. _.extend(_converse.api, { 'rooms': { 'close' (jids) { @@ -2796,7 +498,7 @@ }); } }, - 'open' (jids, attrs) { + 'create' (jids, attrs) { if (_.isString(attrs)) { attrs = {'nick': attrs}; } else if (_.isUndefined(attrs)) { @@ -2808,12 +510,20 @@ if (!attrs.nick && _converse.muc_nickname_from_jid) { attrs.nick = Strophe.getNodeFromJid(_converse.bare_jid); } + if (_.isUndefined(jids)) { + throw new TypeError('rooms.create: You need to provide at least one JID'); + } else if (_.isString(jids)) { + return createChatRoom(jids, attrs); + } + return _.map(jids, _.partial(createChatRoom, _, attrs)); + }, + 'open' (jids, attrs) { if (_.isUndefined(jids)) { throw new TypeError('rooms.open: You need to provide at least one JID'); } else if (_.isString(jids)) { - return _converse.getChatRoom(jids, attrs, openChatRoom); + return _converse.api.rooms.create(jids, attrs).trigger('show'); } - return _.map(jids, _.partial(_converse.getChatRoom, _, attrs, openChatRoom)); + return _.map(jids, (jid) => _converse.api.rooms.create(jid, attrs).trigger('show')); }, 'get' (jids, attrs, create) { if (_.isString(attrs)) { @@ -2824,111 +534,23 @@ if (_.isUndefined(jids)) { const result = []; _converse.chatboxes.each(function (chatbox) { - if (chatbox.get('type') === CHATROOMS_TYPE) { - result.push(_converse.getViewForChatBox(chatbox)); + if (chatbox.get('type') === converse.CHATROOMS_TYPE) { + result.push(chatbox); } }); return result; } - const fetcher = _.partial(_converse.chatboxviews.getChatBox.bind(_converse.chatboxviews), _, create); if (!attrs.nick) { attrs.nick = Strophe.getNodeFromJid(_converse.bare_jid); } if (_.isString(jids)) { - return _converse.getChatRoom(jids, attrs, fetcher); + return _converse.getChatRoom(jids, attrs); } - return _.map(jids, _.partial(_converse.getChatRoom, _, attrs, fetcher)); + return _.map(jids, _.partial(_converse.getChatRoom, _, attrs)); } } }); - - /* Event handlers */ - _converse.on('addClientFeatures', () => { - if (_converse.allow_muc) { - _converse.connection.disco.addFeature(Strophe.NS.MUC); - } - if (_converse.allow_muc_invitations) { - _converse.connection.disco.addFeature('jabber:x:conference'); // Invites - } - }); - - _converse.on('reconnected', function reconnectToChatRooms () { - /* Upon a reconnection event from converse, join again - * all the open chat rooms. - */ - _converse.chatboxviews.each(function (view) { - if (view.model.get('type') === CHATROOMS_TYPE) { - view.model.save('connection_status', converse.ROOMSTATUS.DISCONNECTED); - view.registerHandlers(); - view.join(); - view.fetchMessages(); - } - }); - }); - - - function setMUCDomainFromDisco (controlboxview) { - /* Check whether service discovery for the user's domain - * returned MUC information and use that to automatically - * set the MUC domain for the "Rooms" panel of the controlbox. - */ - function featureAdded (feature) { - if ((feature.get('var') === Strophe.NS.MUC)) { - setMUCDomain(feature.get('from'), controlboxview); - } - } - - _converse.api.waitUntil('discoInitialized').then(() => { - _converse.api.listen.on('serviceDiscovered', featureAdded); - // Features could have been added before the controlbox was - // initialized. We're only interested in MUC - _converse.disco_entities.each((entity) => { - const feature = entity.features.findWhere({'var': Strophe.NS.MUC }); - if (feature) { - featureAdded(feature) - } - }); - }).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR)); - } - - function setMUCDomain (domain, controlboxview) { - _converse.muc_domain = domain; - controlboxview.roomspanel.model.save({'muc_domain': domain}); - } - - function fetchAndSetMUCDomain (controlboxview) { - if (controlboxview.model.get('connected')) { - if (!controlboxview.roomspanel.model.get('muc_domain')) { - if (_.isUndefined(_converse.muc_domain)) { - setMUCDomainFromDisco(controlboxview); - } else { - setMUCDomain(_converse.muc_domain, controlboxview); - } - } - } - } - - _converse.on('controlboxInitialized', function (view) { - if (!_converse.allow_muc) { - return; - } - fetchAndSetMUCDomain(view); - view.model.on('change:connected', _.partial(fetchAndSetMUCDomain, view)); - }); - - function disconnectChatRooms () { - /* When disconnecting, or reconnecting, mark all chat rooms as - * disconnected, so that they will be properly entered again - * when fetched from session storage. - */ - _converse.chatboxes.each(function (model) { - if (model.get('type') === CHATROOMS_TYPE) { - model.save('connection_status', converse.ROOMSTATUS.DISCONNECTED); - } - }); - } - _converse.on('reconnecting', disconnectChatRooms); - _converse.on('disconnecting', disconnectChatRooms); + /************************ END API ************************/ } }); })); diff --git a/src/converse-profile.js b/src/converse-profile.js index f2ce0999f..a01824584 100644 --- a/src/converse-profile.js +++ b/src/converse-profile.js @@ -47,8 +47,8 @@ initialize () { this.render().insertIntoDOM(); this.modal = new bootstrap.Modal(this.el, { - backdrop: 'static', // we don't want to dismiss Modal when Modal or backdrop is the click event target - keyboard: true // we want to dismiss Modal on pressing Esc key + backdrop: 'static', + keyboard: true }); }, diff --git a/src/converse-roomslist.js b/src/converse-roomslist.js index 134d1f3f0..42a966a91 100644 --- a/src/converse-roomslist.js +++ b/src/converse-roomslist.js @@ -17,7 +17,7 @@ "tpl!rooms_list_item" ], factory); }(this, function (utils, converse, muc, tpl_rooms_list, tpl_rooms_list_item) { - const { Backbone, Promise, b64_sha1, sizzle, _ } = converse.env; + const { Backbone, Promise, Strophe, b64_sha1, sizzle, _ } = converse.env; const u = converse.env.utils; converse.plugins.add('converse-roomslist', { @@ -137,6 +137,7 @@ 'click .close-room': 'closeRoom', 'click .open-rooms-toggle': 'toggleRoomsList', 'click .remove-bookmark': 'removeBookmark', + 'click a.open-room': 'openRoom', }, listSelector: '.rooms-list', ItemView: _converse.RoomsListElementView, @@ -192,6 +193,16 @@ u.showElement(this.el); }, + openRoom (ev) { + ev.preventDefault(); + const name = ev.target.textContent; + const jid = ev.target.getAttribute('data-room-jid'); + const data = { + 'name': name || Strophe.unescapeNode(Strophe.getNodeFromJid(jid)) || jid + } + _converse.api.rooms.open(jid, data); + }, + closeRoom (ev) { ev.preventDefault(); const name = ev.target.getAttribute('data-room-name'); diff --git a/src/converse-rosterview.js b/src/converse-rosterview.js index b58b0faf4..583709600 100644 --- a/src/converse-rosterview.js +++ b/src/converse-rosterview.js @@ -379,7 +379,8 @@ openChat (ev) { if (ev && ev.preventDefault) { ev.preventDefault(); } - return _converse.chatboxviews.showChat(this.model.attributes, true); + const attrs = this.model.attributes; + _converse.api.chats.open(attrs.jid, attrs); }, removeContact (ev) { diff --git a/src/converse-singleton.js b/src/converse-singleton.js index 07dfdc1bf..b2e1cb837 100644 --- a/src/converse-singleton.js +++ b/src/converse-singleton.js @@ -37,7 +37,7 @@ // an error will be raised if the plugin is not found. // // NB: These plugins need to have already been loaded via require.js. - dependencies: ['converse-muc', 'converse-controlbox', 'converse-rosterview'], + dependencies: ['converse-chatboxes', 'converse-muc', 'converse-controlbox', 'converse-rosterview'], enabled (_converse) { return _.includes(['mobile', 'fullscreen', 'embedded'], _converse.view_mode); @@ -49,80 +49,37 @@ // relevant objects or classes. // // new functions which don't exist yet can also be added. - ChatBoxes: { + chatBoxMayBeShown (chatbox) { + return !chatbox.get('hidden'); + }, + createChatBox (jid, attrs) { - /* Make sure new chat boxes are hidden by default. - */ + /* Make sure new chat boxes are hidden by default. */ attrs = attrs || {}; attrs.hidden = true; return this.__super__.createChatBox.call(this, jid, attrs); } }, - RoomsPanel: { - parseRoomDataFromEvent (ev) { - /* We set hidden to false for rooms opened manually by the - * user. They should always be shown. - */ - const result = this.__super__.parseRoomDataFromEvent.apply(this, arguments); - if (_.isUndefined(result)) { - return - } - result.hidden = false; - return result; - } - }, - - ChatBoxViews: { - showChat (attrs, force) { - /* We only have one chat visible at any one - * time. So before opening a chat, we make sure all other - * chats are hidden. - */ - const { _converse } = this.__super__; - const chatbox = this.getChatBox(attrs, true); - const hidden = _.isUndefined(attrs.hidden) ? chatbox.get('hidden') : attrs.hidden; - if ((force || !hidden) && _converse.connection.authenticated) { - _.each(_converse.chatboxviews.xget(chatbox.get('id')), hideChat); - chatbox.save({'hidden': false}); - } - return this.__super__.showChat.apply(this, arguments); - } - }, - ChatBoxView: { - show (focus) { + _show (focus) { /* We only have one chat visible at any one * time. So before opening a chat, we make sure all other * chats are hidden. */ - if (!this.model.get('hidden')) { - _.each(this.__super__._converse.chatboxviews.xget(this.model.get('id')), hideChat); - return this.__super__.show.apply(this, arguments); - } + _.each(this.__super__._converse.chatboxviews.xget(this.model.get('id')), hideChat); + this.model.set('hidden', false); + return this.__super__._show.apply(this, arguments); } }, ChatRoomView: { show (focus) { - if (!this.model.get('hidden')) { - _.each(this.__super__._converse.chatboxviews.xget(this.model.get('id')), hideChat); - return this.__super__.show.apply(this, arguments); - } + _.each(this.__super__._converse.chatboxviews.xget(this.model.get('id')), hideChat); + this.model.set('hidden', false); + return this.__super__.show.apply(this, arguments); } - }, - - RosterContactView: { - openChat (ev) { - /* We only have one chat visible at any one - * time. So before opening a chat, we make sure all other - * chats are hidden. - */ - _.each(this.__super__._converse.chatboxviews.xget('controlbox'), hideChat); - this.model.save({'hidden': false}); - return this.__super__.openChat.apply(this, arguments); - }, } } }); diff --git a/src/templates/room_panel.html b/src/templates/room_panel.html index 5aba21219..0f7c677b8 100644 --- a/src/templates/room_panel.html +++ b/src/templates/room_panel.html @@ -1,7 +1,7 @@
- Chatrooms + {{{o.heading_chatrooms}}}