// Converse.js (A browser based XMPP chat client) // http://conversejs.org // // Copyright (c) 2012-2017, Jan-Carel Brand // Licensed under the Mozilla Public License (MPLv2) // /*global define */ (function (root, factory) { define(["jquery.noconflict", "converse-core", "lodash.fp", "virtual-dom", "vdom-parser", "tpl!add_contact_dropdown", "tpl!add_contact_form", "tpl!converse_brand_heading", "tpl!change_status_message", "tpl!chat_status", "tpl!choose_status", "tpl!contacts_panel", "tpl!contacts_tab", "tpl!controlbox", "tpl!controlbox_toggle", "tpl!login_panel", "tpl!search_contact", "tpl!status_option", "tpl!spinner", "tpl!login_feedback", "converse-chatview", "converse-rosterview" ], factory); }(this, function ( $, converse, fp, vdom, vdom_parser, tpl_add_contact_dropdown, tpl_add_contact_form, tpl_brand_heading, tpl_change_status_message, tpl_chat_status, tpl_choose_status, tpl_contacts_panel, tpl_contacts_tab, tpl_controlbox, tpl_controlbox_toggle, tpl_login_panel, tpl_search_contact, tpl_status_option, tpl_spinner, tpl_login_feedback ) { "use strict"; const USERS_PANEL_ID = 'users'; const CHATBOX_TYPE = 'chatbox'; const { Strophe, Backbone, Promise, utils, _, moment } = converse.env; converse.plugins.add('converse-controlbox', { overrides: { // Overrides mentioned here will be picked up by converse.js's // plugin architecture they will replace existing methods on the // relevant objects or classes. // // New functions which don't exist yet can also be added. _tearDown () { this.__super__._tearDown.apply(this, arguments); if (this.rosterview) { // Removes roster groups this.rosterview.model.off().reset(); this.rosterview.each(function (groupview) { groupview.removeAll(); groupview.remove(); }); this.rosterview.removeAll().remove(); } }, clearSession () { this.__super__.clearSession.apply(this, arguments); const controlbox = this.chatboxes.get('controlbox'); if (controlbox && controlbox.collection && controlbox.collection.browserStorage) { controlbox.save({'connected': false}); } }, ChatBoxes: { chatBoxMayBeShown (chatbox) { 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: { onChatBoxAdded (item) { const { _converse } = this.__super__; if (item.get('box_id') === 'controlbox') { let view = this.get(item.get('id')); if (view) { view.model = item; view.initialize(); return view; } else { view = new _converse.ControlBoxView({model: item}); return this.add(item.get('id'), view); } } else { return this.__super__.onChatBoxAdded.apply(this, arguments); } }, closeAllChatBoxes () { const { _converse } = this.__super__; this.each(function (view) { if (view.model.get('id') === 'controlbox' && (_converse.disconnection_cause !== _converse.LOGOUT || _converse.show_controlbox_by_default)) { return; } view.close(); }); return this; }, getChatBoxWidth (view) { const { _converse } = this.__super__; const controlbox = this.get('controlbox'); if (view.model.get('id') === 'controlbox') { /* We return the width of the controlbox or its toggle, * depending on which is visible. */ if (!controlbox || !controlbox.$el.is(':visible')) { return _converse.controlboxtoggle.$el.outerWidth(true); } else { return controlbox.$el.outerWidth(true); } } else { return this.__super__.getChatBoxWidth.apply(this, arguments); } } }, ChatBox: { initialize () { if (this.get('id') === 'controlbox') { this.set({'time_opened': moment(0).valueOf()}); } else { this.__super__.initialize.apply(this, arguments); } }, }, ChatBoxView: { insertIntoDOM () { const view = this.__super__._converse.chatboxviews.get("controlbox"); if (view) { view.el.insertAdjacentElement('afterend', this.el) } else { this.__super__.insertIntoDOM.apply(this, arguments); } return this; } } }, initialize () { /* The initialize function gets called as soon as the plugin is * loaded by converse.js's plugin machinery. */ const { _converse } = this, { __ } = _converse; _converse.api.settings.update({ allow_logout: true, default_domain: undefined, locked_domain: undefined, show_controlbox_by_default: false, sticky_controlbox: false, xhr_user_search: false, xhr_user_search_url: '' }); _converse.api.promises.add('controlboxInitialized'); const LABEL_CONTACTS = __('Contacts'); _converse.addControlBox = () => { _converse.chatboxes.add({ id: 'controlbox', box_id: 'controlbox', type: 'controlbox', closed: !_converse.show_controlbox_by_default }) }; _converse.ControlBoxView = _converse.ChatBoxView.extend({ tagName: 'div', className: 'chatbox fade-in', id: 'controlbox', events: { 'click a.close-chatbox-button': 'close', 'click ul#controlbox-tabs li a': 'switchTab', }, initialize () { if (_.isUndefined(_converse.controlboxtoggle)) { _converse.controlboxtoggle = new _converse.ControlBoxToggle(); this.$el.insertAfter(_converse.controlboxtoggle.$el); } this.model.on('change:connected', this.onConnected, this); this.model.on('destroy', this.hide, this); this.model.on('hide', this.hide, this); this.model.on('show', this.show, this); this.model.on('change:closed', this.ensureClosedState, this); this.render(); if (this.model.get('connected')) { _converse.api.waitUntil('rosterViewInitialized') .then(this.insertRoster.bind(this)) .catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL)); } _converse.emit('controlboxInitialized'); }, render () { if (this.model.get('connected')) { if (_.isUndefined(this.model.get('closed'))) { this.model.set('closed', !_converse.show_controlbox_by_default); } } if (!this.model.get('closed')) { this.show(); } else { this.hide(); } this.el.innerHTML = tpl_controlbox( _.extend(this.model.toJSON(), { 'sticky_controlbox': _converse.sticky_controlbox })); if (!_converse.connection.connected || !_converse.connection.authenticated || _converse.connection.disconnecting) { this.renderLoginPanel(); } else if (this.model.get('connected') && (!this.contactspanel || !this.contactspanel.$el.is(':visible'))) { this.renderContactsPanel(); } return this; }, onConnected () { if (this.model.get('connected')) { this.render(); _converse.api.waitUntil('rosterViewInitialized') .then(this.insertRoster.bind(this)) .catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL)); this.model.save(); } }, insertRoster () { /* Place the rosterview inside the "Contacts" panel. */ this.contactspanel.el.insertAdjacentElement( 'beforeEnd', _converse.rosterview.el ); return this; }, createBrandHeadingHTML () { return tpl_brand_heading(); }, insertBrandHeading () { const heading_el = this.el.querySelector('.brand-heading-container'); if (_.isNull(heading_el)) { const el = this.el.querySelector('.controlbox-head'); el.insertAdjacentHTML('beforeend', this.createBrandHeadingHTML()); } else { heading_el.outerHTML = this.createBrandHeadingHTML(); } }, renderLoginPanel () { this.el.classList.add("logged-out"); if (_.isNil(this.loginpanel)) { this.loginpanel = new _converse.LoginPanel({'model': this}); const panes = this.el.querySelector('.controlbox-panes'); panes.innerHTML = ''; panes.appendChild(this.loginpanel.render().el); this.insertBrandHeading(); } else { this.loginpanel.render(); } return this; }, renderContactsPanel () { /* Renders the "Contacts" panel of the controlbox. * * This will only be called after the user has already been * logged in. */ if (this.loginpanel) { this.loginpanel.remove(); delete this.loginpanel; } this.el.classList.remove("logged-out"); if (_.isUndefined(this.model.get('active-panel'))) { this.model.save({'active-panel': USERS_PANEL_ID}); } this.contactspanel = new _converse.ContactsPanel({ '$parent': this.$el.find('.controlbox-panes') }); this.contactspanel.insertIntoDOM(); _converse.xmppstatusview = new _converse.XMPPStatusView({ 'model': _converse.xmppstatus }); _converse.xmppstatusview.render(); }, close (ev) { if (ev && ev.preventDefault) { ev.preventDefault(); } if (_converse.sticky_controlbox) { return; } if (_converse.connection.connected && !_converse.connection.disconnecting) { this.model.save({'closed': true}); } else { this.model.trigger('hide'); } _converse.emit('controlBoxClosed', this); return this; }, ensureClosedState () { if (this.model.get('closed')) { this.hide(); } else { this.show(); } }, hide (callback) { if (_converse.sticky_controlbox) { return; } this.$el.addClass('hidden'); utils.refreshWebkit(); _converse.emit('chatBoxClosed', this); if (!_converse.connection.connected) { _converse.controlboxtoggle.render(); } _converse.controlboxtoggle.show(callback); return this; }, onControlBoxToggleHidden () { _converse.controlboxtoggle.updateOnlineCount(); utils.refreshWebkit(); this.model.set('closed', false); this.el.classList.remove('hidden'); _converse.emit('controlBoxOpened', this); }, show () { _converse.controlboxtoggle.hide( this.onControlBoxToggleHidden.bind(this) ); return this; }, switchTab (ev) { // TODO: automatically focus the relevant input if (ev && ev.preventDefault) { ev.preventDefault(); } const $tab = $(ev.target), $sibling = $tab.parent().siblings('li').children('a'), $tab_panel = $($tab.attr('href')); $($sibling.attr('href')).addClass('hidden'); $sibling.removeClass('current'); $tab.addClass('current'); $tab_panel.removeClass('hidden'); if (!_.isUndefined(_converse.chatboxes.browserStorage)) { this.model.save({'active-panel': $tab.data('id')}); } return this; }, showHelpMessages () { /* Override showHelpMessages in ChatBoxView, for now do nothing. * * Parameters: * (Array) msgs: Array of messages */ return; } }); _converse.LoginPanel = Backbone.View.extend({ tagName: 'div', id: "converse-login-panel", className: 'controlbox-pane fade-in', events: { 'submit form#converse-login': 'authenticate' }, initialize (cfg) { _converse.connfeedback.on('change', this.renderConnectionFeedback, this); }, render () { const html = tpl_login_panel({ '__': __, 'ANONYMOUS': _converse.ANONYMOUS, 'EXTERNAL': _converse.EXTERNAL, 'LOGIN': _converse.LOGIN, 'PREBIND': _converse.PREBIND, 'auto_login': _converse.auto_login, 'authentication': _converse.authentication, 'label_anon_login': __('Click here to log in anonymously'), 'placeholder_username': (_converse.locked_domain || _converse.default_domain) && __('Username') || __('user@domain'), }); const form = this.el.querySelector('form'); if (_.isNull(form)) { this.el.innerHTML = html; } else { const patches = vdom.diff(vdom_parser(form), vdom_parser(html)); vdom.patch(form, patches); } this.renderConnectionFeedback(); return this; }, renderConnectionFeedback () { const feedback_html = tpl_login_feedback({ 'conn_feedback_class': _converse.connfeedback.get('klass'), 'conn_feedback_subject': _converse.connfeedback.get('subject'), 'conn_feedback_message': _converse.connfeedback.get('message'), }); const feedback_el = this.el.querySelector('.conn-feedback'); if (_.isNull(feedback_el)) { this.el.insertAdjacentHTML('afterbegin', feedback_html); } else { feedback_el.outerHTML = feedback_html; } }, showSpinner (only_submit_button=false) { const form = this.el.querySelector('form'); if (only_submit_button) { const button = form.querySelector('input[type=submit]'); button.classList.add('hidden'); button.insertAdjacentHTML('afterend', ''); } else { form.innerHTML = tpl_spinner(); } return this; }, authenticate (ev) { /* Authenticate the user based on a form submission event. */ if (ev && ev.preventDefault) { ev.preventDefault(); } const $form = $(ev.target); if (_converse.authentication === _converse.ANONYMOUS) { this.showSpinner().connect(_converse.jid, null); return; } const $jid_input = $form.find('input[name=jid]'); const $jid_error_msg = $form.find('.invalid-jid-msg'); const $pw_input = $form.find('input[name=password]'); const password = $pw_input.val(); let jid = $jid_input.val(), errors = false; if (!jid || ( !_converse.locked_domain && !_converse.default_domain && _.filter(jid.split('@')).length < 2)) { errors = true; $jid_input.addClass('error'); $jid_error_msg.removeClass('hidden'); } else { $jid_error_msg.addClass('hidden'); } if (!password && _converse.authentication !== _converse.EXTERNAL) { errors = true; $pw_input.addClass('error'); } if (errors) { return; } if (_converse.locked_domain) { jid = Strophe.escapeNode(jid) + '@' + _converse.locked_domain; } else if (_converse.default_domain && !_.includes(jid, '@')) { jid = jid + '@' + _converse.default_domain; } this.showSpinner(true).connect(jid, password); return false; }, connect (jid, password) { if (jid) { const resource = Strophe.getResourceFromJid(jid); if (!resource) { jid = jid.toLowerCase() + _converse.generateResource(); } else { jid = Strophe.getBareJidFromJid(jid).toLowerCase()+'/'+resource; } } _converse.connection.reset(); _converse.connection.connect(jid, password, _converse.onConnectStatusChanged); } }); _converse.XMPPStatusView = Backbone.View.extend({ el: "form#set-xmpp-status", events: { "click a.choose-xmpp-status": "toggleOptions", "click #fancy-xmpp-status-select a.change-xmpp-status-message": "renderStatusChangeForm", "submit": "setStatusMessage", "click .dropdown dd ul li a": "setStatus" }, initialize () { this.model.on("change:status", this.updateStatusUI, this); this.model.on("change:status_message", this.updateStatusUI, this); this.model.on("update-status-ui", this.updateStatusUI, this); }, render () { // Replace the default dropdown with something nicer const $select = this.$el.find('select#select-xmpp-status'); const chat_status = this.model.get('status') || 'offline'; const options = $('option', $select); const options_list = []; this.$el.html(tpl_choose_status()); this.$el.find('#fancy-xmpp-status-select') .html(tpl_chat_status({ 'status_message': this.model.get('status_message') || __("I am %1$s", this.getPrettyStatus(chat_status)), 'chat_status': chat_status, 'desc_custom_status': __('Click here to write a custom status message'), 'desc_change_status': __('Click to change your chat status') })); // iterate through all the