into consideration
- var options = [], j, $options, $values, value, values;
-
- if ($field.attr('type') === 'list-single' || $field.attr('type') === 'list-multi') {
- values = [];
- $values = $field.children('value');
- for (j=0; j<$values.length; j++) {
- values.push($($values[j]).text());
- }
- $options = $field.children('option');
- for (j=0; j<$options.length; j++) {
- value = $($options[j]).find('value').text();
- options.push(tpl_select_option({
- value: value,
- label: $($options[j]).attr('label'),
- selected: _.startsWith(values, value),
- required: $field.find('required').length
- }));
- }
- return tpl_form_select({
- name: $field.attr('var'),
- label: $field.attr('label'),
- options: options.join(''),
- multiple: ($field.attr('type') === 'list-multi'),
- required: $field.find('required').length
- });
- } else if ($field.attr('type') === 'fixed') {
- return $('').text($field.find('value').text());
- } else if ($field.attr('type') === 'jid-multi') {
- return tpl_form_textarea({
- name: $field.attr('var'),
- label: $field.attr('label') || '',
- value: $field.find('value').text(),
- required: $field.find('required').length
- });
- } else if ($field.attr('type') === 'boolean') {
- return tpl_form_checkbox({
- name: $field.attr('var'),
- type: XFORM_TYPE_MAP[$field.attr('type')],
- label: $field.attr('label') || '',
- checked: $field.find('value').text() === "1" && 'checked="1"' || '',
- required: $field.find('required').length
- });
- } else if ($field.attr('type') && $field.attr('var') === 'username') {
- return tpl_form_username({
- domain: ' @'+this.domain,
- name: $field.attr('var'),
- type: XFORM_TYPE_MAP[$field.attr('type')],
- label: $field.attr('label') || '',
- value: $field.find('value').text(),
- required: $field.find('required').length
- });
- } else if ($field.attr('type')) {
- return tpl_form_input({
- name: $field.attr('var'),
- type: XFORM_TYPE_MAP[$field.attr('type')],
- label: $field.attr('label') || '',
- value: $field.find('value').text(),
- required: $field.find('required').length
- });
- } else {
- if ($field.attr('var') === 'ocr') { // Captcha
- return _.reduce(_.map($field.find('uri'),
- $.proxy(function (uri) {
- return tpl_form_captcha({
- label: this.$field.attr('label'),
- name: this.$field.attr('var'),
- data: this.$stanza.find('data[cid="'+uri.textContent.replace(/^cid:/, '')+'"]').text(),
- type: uri.getAttribute('type'),
- required: this.$field.find('required').length
- });
- }, {'$stanza': $stanza, '$field': $field})
- ),
- function (memo, num) { return memo + num; }, ''
- );
- }
- }
- }
- };
-
- utils.detectLocale = function (library_check) {
- /* Determine which locale is supported by the user's system as well
- * as by the relevant library (e.g. converse.js or moment.js).
- *
- * Parameters:
- * (Function) library_check - returns a boolean indicating whether
- * the locale is supported.
- */
- var locale, i;
- if (window.navigator.userLanguage) {
- locale = utils.isLocaleAvailable(window.navigator.userLanguage, library_check);
- }
- if (window.navigator.languages && !locale) {
- for (i=0; i 0 && reqStatus < 500) || req.sends > 5) {
+ var valid_request = reqStatus > 0 && reqStatus < 500;
+ var too_many_retries = req.sends > this._conn.maxRetries;
+ if (valid_request || too_many_retries) {
// remove from internal queue
this._removeRequest(req);
Strophe.debug("request id "+req.id+" should now be removed");
@@ -43570,8 +43773,11 @@ Strophe.Bosh.prototype = {
} else {
Strophe.error("request id "+req.id+"."+req.sends+" error "+reqStatus+" happened");
}
- if (!(reqStatus > 0 && reqStatus < 500) || req.sends > 5) {
+
+ if (!valid_request && !too_many_retries) {
this._throttledRequestHandler();
+ } else if (too_many_retries && !this._conn.connected) {
+ this._conn._changeConnectStatus(Strophe.Status.CONNFAIL, "giving-up");
}
},
@@ -43633,7 +43839,7 @@ Strophe.Bosh.prototype = {
req.xhr.withCredentials = true;
}
} catch (e2) {
- Strophe.error("XHR open failed.");
+ Strophe.error("XHR open failed: " + e2.toString());
if (!this._conn.connected) {
this._conn._changeConnectStatus(
Strophe.Status.CONNFAIL, "bad-service");
@@ -44079,17 +44285,23 @@ Strophe.Websocket.prototype = {
//_connect_cb will check for stream:error and disconnect on error
this._connect_cb(streamStart);
}
- } else if (message.data.indexOf("';
+ if (_.isArray(value)) { ;
+__p += '\n ';
+ _.each(value,function(arrayValue) { ;
+__p += '' +
+__e(arrayValue) +
+'';
+ }); ;
+__p += '\n';
+ } else { ;
+__p += '\n ' +
+__e(value) +
+'\n';
+ } ;
+__p += '\n';
+
+}
+return __p
+};});
+
+
+define('tpl!select_option', ['lodash'], function(_) {return function(obj) {
+obj || (obj = {});
+var __t, __p = '', __e = _.escape, __j = Array.prototype.join;
+function print() { __p += __j.call(arguments, '') }
+with (obj) {
+__p += '\n';
+
+}
+return __p
+};});
+
+
+define('tpl!form_select', ['lodash'], function(_) {return function(obj) {
+obj || (obj = {});
+var __t, __p = '', __e = _.escape, __j = Array.prototype.join;
+function print() { __p += __j.call(arguments, '') }
+with (obj) {
+__p += '\n\n';
+
+}
+return __p
+};});
+
+
+define('tpl!form_textarea', ['lodash'], function(_) {return function(obj) {
+obj || (obj = {});
+var __t, __p = '', __e = _.escape;
+with (obj) {
+__p += '\n\n';
+
+}
+return __p
+};});
+
+
+define('tpl!form_checkbox', ['lodash'], function(_) {return function(obj) {
+obj || (obj = {});
+var __t, __p = '', __e = _.escape;
+with (obj) {
+__p += '\n\n';
+
+}
+return __p
+};});
+
+
+define('tpl!form_username', ['lodash'], function(_) {return function(obj) {
+obj || (obj = {});
+var __t, __p = '', __e = _.escape, __j = Array.prototype.join;
+function print() { __p += __j.call(arguments, '') }
+with (obj) {
+
+ if (label) { ;
+__p += '\n\n';
+ } ;
+__p += '\n\n \n ' +
+__e(domain) +
+'\n
\n';
+
+}
+return __p
+};});
+
+
+define('tpl!form_input', ['lodash'], function(_) {return function(obj) {
+obj || (obj = {});
+var __t, __p = '', __e = _.escape, __j = Array.prototype.join;
+function print() { __p += __j.call(arguments, '') }
+with (obj) {
+
+ if (label) { ;
+__p += '\n\n';
+ } ;
+__p += '\n\n';
+
+}
+return __p
+};});
+
+
+define('tpl!form_captcha', ['lodash'], function(_) {return function(obj) {
+obj || (obj = {});
+var __t, __p = '', __e = _.escape, __j = Array.prototype.join;
+function print() { __p += __j.call(arguments, '') }
+with (obj) {
+
+ if (label) { ;
+__p += '\n\n';
+ } ;
+__p += '\n\n\n\n\n';
+
+}
+return __p
+};});
+
+/*global define, escape, locales, Jed */
+(function (root, factory) {
+ define('utils',[
+ "jquery.noconflict",
+ "sizzle",
+ "jquery.browser",
+ "lodash.noconflict",
+ "locales",
+ "moment_with_locales",
+ "strophe",
+ "tpl!field",
+ "tpl!select_option",
+ "tpl!form_select",
+ "tpl!form_textarea",
+ "tpl!form_checkbox",
+ "tpl!form_username",
+ "tpl!form_input",
+ "tpl!form_captcha"
+ ], factory);
+}(this, function (
+ $, sizzle, dummy, _,
+ locales,
+ moment,
+ Strophe,
+ tpl_field,
+ tpl_select_option,
+ tpl_form_select,
+ tpl_form_textarea,
+ tpl_form_checkbox,
+ tpl_form_username,
+ tpl_form_input,
+ tpl_form_captcha
+ ) {
+ "use strict";
+ locales = locales || {};
+ Strophe = Strophe.Strophe;
+
+ var XFORM_TYPE_MAP = {
+ 'text-private': 'password',
+ 'text-single': 'text',
+ 'fixed': 'label',
+ 'boolean': 'checkbox',
+ 'hidden': 'hidden',
+ 'jid-multi': 'textarea',
+ 'list-single': 'dropdown',
+ 'list-multi': 'dropdown'
+ };
+
+ var afterAnimationEnd = function (el, callback) {
+ el.classList.remove('visible');
+ if (_.isFunction(callback)) {
+ callback();
+ }
+ };
+
+ var unescapeHTML = function (htmlEscapedText) {
+ /* Helper method that replace HTML-escaped symbols with equivalent characters
+ * (e.g. transform occurrences of '&' to '&')
+ *
+ * Parameters:
+ * (String) htmlEscapedText: a String containing the HTML-escaped symbols.
+ */
+ var div = document.createElement('div');
+ div.innerHTML = htmlEscapedText;
+ return div.innerText;
+ }
+
+ var isImage = function (url) {
+ var deferred = new $.Deferred();
+ var img = new Image();
+ var timer = window.setTimeout(function () {
+ deferred.reject();
+ img = null;
+ }, 3000);
+ img.onerror = img.onabort = function () {
+ clearTimeout(timer);
+ deferred.reject();
+ };
+ img.onload = function () {
+ clearTimeout(timer);
+ deferred.resolve(img);
+ };
+ img.src = url;
+ return deferred.promise();
+ };
+
+ $.fn.hasScrollBar = function() {
+ if (!$.contains(document, this.get(0))) {
+ return false;
+ }
+ if(this.parent().height() < this.get(0).scrollHeight) {
+ return true;
+ }
+ return false;
+ };
+
+ var throttledHTML = _.throttle(function (el, html) {
+ el.innerHTML = html;
+ }, 500);
+
+ $.fn.addHyperlinks = function () {
+ if (this.length > 0) {
+ this.each(function (i, obj) {
+ var prot, escaped_url;
+ var x = obj.innerHTML;
+ var list = x.match(/\b(https?:\/\/|www\.|https?:\/\/www\.)[^\s<]{2,200}\b/g );
+ if (list) {
+ for (i=0; i'+ list[i] + '' );
+ }
+ }
+ obj.innerHTML = x;
+ _.forEach(list, function (url) {
+ isImage(unescapeHTML(url)).then(function (img) {
+ img.className = 'chat-image';
+ throttledHTML(obj.querySelector('a'), img.outerHTML);
+ });
+ });
+ });
+ }
+ return this;
+ };
+
+ $.fn.addEmoticons = function (allowed) {
+ if (allowed) {
+ if (this.length > 0) {
+ this.each(function (i, obj) {
+ var text = $(obj).html();
+ text = text.replace(/>:\)/g, '');
+ text = text.replace(/:\)/g, '');
+ text = text.replace(/:\-\)/g, '');
+ text = text.replace(/;\)/g, '');
+ text = text.replace(/;\-\)/g, '');
+ text = text.replace(/:D/g, '');
+ text = text.replace(/:\-D/g, '');
+ text = text.replace(/:P/g, '');
+ text = text.replace(/:\-P/g, '');
+ text = text.replace(/:p/g, '');
+ text = text.replace(/:\-p/g, '');
+ text = text.replace(/8\)/g, '');
+ text = text.replace(/:S/g, '');
+ text = text.replace(/:\\/g, '');
+ text = text.replace(/:\/ /g, '');
+ text = text.replace(/>:\(/g, '');
+ text = text.replace(/:\(/g, '');
+ text = text.replace(/:\-\(/g, '');
+ text = text.replace(/:O/g, '');
+ text = text.replace(/:\-O/g, '');
+ text = text.replace(/\=\-O/g, '');
+ text = text.replace(/\(\^.\^\)b/g, '');
+ text = text.replace(/<3/g, '');
+ $(obj).html(text);
+ });
+ }
+ }
+ return this;
+ };
+
+ var utils = {
+ // Translation machinery
+ // ---------------------
+ __: function (str) {
+ if (!utils.isConverseLocale(this.locale) || this.locale === 'en') {
+ return Jed.sprintf.apply(Jed, arguments);
+ }
+ if (typeof this.jed === "undefined") {
+ this.jed = new Jed(window.JSON.parse(locales[this.locale]));
+ }
+ var t = this.jed.translate(str);
+ if (arguments.length>1) {
+ return t.fetch.apply(t, [].slice.call(arguments,1));
+ } else {
+ return t.fetch();
+ }
+ },
+
+ ___: function (str) {
+ /* XXX: This is part of a hack to get gettext to scan strings to be
+ * translated. Strings we cannot send to the function above because
+ * they require variable interpolation and we don't yet have the
+ * variables at scan time.
+ *
+ * See actionInfoMessages in src/converse-muc.js
+ */
+ return str;
+ },
+
+ isLocaleAvailable: function (locale, available) {
+ /* Check whether the locale or sub locale (e.g. en-US, en) is supported.
+ *
+ * Parameters:
+ * (Function) available - returns a boolean indicating whether the locale is supported
+ */
+ if (available(locale)) {
+ return locale;
+ } else {
+ var sublocale = locale.split("-")[0];
+ if (sublocale !== locale && available(sublocale)) {
+ return sublocale;
+ }
+ }
+ },
+
+ fadeIn: function (el, callback) {
+ if ($.fx.off) {
+ el.classList.remove('hidden');
+ if (_.isFunction(callback)) {
+ callback();
+ }
+ return;
+ }
+ if (_.includes(el.classList, 'hidden')) {
+ /* XXX: This doesn't appear to be working...
+ el.addEventListener("webkitAnimationEnd", _.partial(afterAnimationEnd, el, callback), false);
+ el.addEventListener("animationend", _.partial(afterAnimationEnd, el, callback), false);
+ */
+ setTimeout(_.partial(afterAnimationEnd, el, callback), 351);
+ el.classList.add('visible');
+ el.classList.remove('hidden');
+ } else {
+ afterAnimationEnd(el, callback);
+ }
+ },
+
+ isSameBareJID: function (jid1, jid2) {
+ return Strophe.getBareJidFromJid(jid1).toLowerCase() ===
+ Strophe.getBareJidFromJid(jid2).toLowerCase();
+ },
+
+ isNewMessage: function (message) {
+ /* Given a stanza, determine whether it's a new
+ * message, i.e. not a MAM archived one.
+ */
+ if (message instanceof Element) {
+ return !(sizzle('result[xmlns="'+Strophe.NS.MAM+'"]', message).length);
+ } else {
+ return !message.get('archive_id');
+ }
+ },
+
+ isOTRMessage: function (message) {
+ var body = message.querySelector('body'),
+ text = (!_.isNull(body) ? body.textContent: undefined);
+ return text && !!text.match(/^\?OTR/);
+ },
+
+ isHeadlineMessage: function (message) {
+ var from_jid = message.getAttribute('from');
+ if (message.getAttribute('type') === 'headline') {
+ return true;
+ }
+ if (message.getAttribute('type') !== 'error' &&
+ !_.isNil(from_jid) &&
+ !_.includes(from_jid, '@')) {
+ // Some servers (I'm looking at you Prosody) don't set the message
+ // type to "headline" when sending server messages. For now we
+ // check if an @ signal is included, and if not, we assume it's
+ // a headline message.
+ return true;
+ }
+ return false;
+ },
+
+ merge: function merge (first, second) {
+ /* Merge the second object into the first one.
+ */
+ for (var k in second) {
+ if (_.isObject(first[k])) {
+ merge(first[k], second[k]);
+ } else {
+ first[k] = second[k];
+ }
+ }
+ },
+
+ applyUserSettings: function applyUserSettings (context, settings, user_settings) {
+ /* Configuration settings might be nested objects. We only want to
+ * add settings which are whitelisted.
+ */
+ for (var k in settings) {
+ if (_.isUndefined(user_settings[k])) {
+ continue;
+ }
+ if (_.isObject(settings[k]) && !_.isArray(settings[k])) {
+ applyUserSettings(context[k], settings[k], user_settings[k]);
+ } else {
+ context[k] = user_settings[k];
+ }
+ }
+ },
+
+ refreshWebkit: function () {
+ /* This works around a webkit bug. Refreshes the browser's viewport,
+ * otherwise chatboxes are not moved along when one is closed.
+ */
+ if ($.browser.webkit && window.requestAnimationFrame) {
+ window.requestAnimationFrame(function () {
+ var conversejs = document.getElementById('conversejs');
+ conversejs.style.display = 'none';
+ var tmp = conversejs.offsetHeight; // jshint ignore:line
+ conversejs.style.display = 'block';
+ });
+ }
+ },
+
+ webForm2xForm: function (field) {
+ /* Takes an HTML DOM and turns it into an XForm field.
+ *
+ * Parameters:
+ * (DOMElement) field - the field to convert
+ */
+ var $input = $(field), value;
+ if ($input.is('[type=checkbox]')) {
+ value = $input.is(':checked') && 1 || 0;
+ } else if ($input.is('textarea')) {
+ value = [];
+ var lines = $input.val().split('\n');
+ for( var vk=0; vk into consideration
+ var options = [], j, $options, $values, value, values;
+
+ if ($field.attr('type') === 'list-single' || $field.attr('type') === 'list-multi') {
+ values = [];
+ $values = $field.children('value');
+ for (j=0; j<$values.length; j++) {
+ values.push($($values[j]).text());
+ }
+ $options = $field.children('option');
+ for (j=0; j<$options.length; j++) {
+ value = $($options[j]).find('value').text();
+ options.push(tpl_select_option({
+ value: value,
+ label: $($options[j]).attr('label'),
+ selected: _.startsWith(values, value),
+ required: $field.find('required').length
+ }));
+ }
+ return tpl_form_select({
+ name: $field.attr('var'),
+ label: $field.attr('label'),
+ options: options.join(''),
+ multiple: ($field.attr('type') === 'list-multi'),
+ required: $field.find('required').length
+ });
+ } else if ($field.attr('type') === 'fixed') {
+ return $('').text($field.find('value').text());
+ } else if ($field.attr('type') === 'jid-multi') {
+ return tpl_form_textarea({
+ name: $field.attr('var'),
+ label: $field.attr('label') || '',
+ value: $field.find('value').text(),
+ required: $field.find('required').length
+ });
+ } else if ($field.attr('type') === 'boolean') {
+ return tpl_form_checkbox({
+ name: $field.attr('var'),
+ type: XFORM_TYPE_MAP[$field.attr('type')],
+ label: $field.attr('label') || '',
+ checked: $field.find('value').text() === "1" && 'checked="1"' || '',
+ required: $field.find('required').length
+ });
+ } else if ($field.attr('type') && $field.attr('var') === 'username') {
+ return tpl_form_username({
+ domain: ' @'+this.domain,
+ name: $field.attr('var'),
+ type: XFORM_TYPE_MAP[$field.attr('type')],
+ label: $field.attr('label') || '',
+ value: $field.find('value').text(),
+ required: $field.find('required').length
+ });
+ } else if ($field.attr('type')) {
+ return tpl_form_input({
+ name: $field.attr('var'),
+ type: XFORM_TYPE_MAP[$field.attr('type')],
+ label: $field.attr('label') || '',
+ value: $field.find('value').text(),
+ required: $field.find('required').length
+ });
+ } else {
+ if ($field.attr('var') === 'ocr') { // Captcha
+ return _.reduce(_.map($field.find('uri'),
+ $.proxy(function (uri) {
+ return tpl_form_captcha({
+ label: this.$field.attr('label'),
+ name: this.$field.attr('var'),
+ data: this.$stanza.find('data[cid="'+uri.textContent.replace(/^cid:/, '')+'"]').text(),
+ type: uri.getAttribute('type'),
+ required: this.$field.find('required').length
+ });
+ }, {'$stanza': $stanza, '$field': $field})
+ ),
+ function (memo, num) { return memo + num; }, ''
+ );
+ }
+ }
+ }
+ };
+
+ utils.detectLocale = function (library_check) {
+ /* Determine which locale is supported by the user's system as well
+ * as by the relevant library (e.g. converse.js or moment.js).
+ *
+ * Parameters:
+ * (Function) library_check - returns a boolean indicating whether
+ * the locale is supported.
+ */
+ var locale, i;
+ if (window.navigator.userLanguage) {
+ locale = utils.isLocaleAvailable(window.navigator.userLanguage, library_check);
+ }
+ if (window.navigator.languages && !locale) {
+ for (i=0; i 0) {
- if (document.title.search(/^Messages \(\d+\) /) === -1) {
- document.title = "Messages (" + this.msg_counter + ") " + document.title;
- } else {
- document.title = document.title.replace(/^Messages \(\d+\) /, "Messages (" + this.msg_counter + ") ");
- }
- } else if (document.title.search(/^Messages \(\d+\) /) !== -1) {
- document.title = document.title.replace(/^Messages \(\d+\) /, "");
- }
- };
-
this.incrementMsgCounter = function () {
this.msg_counter += 1;
- this.updateMsgCounter();
+ var unreadMsgCount = this.msg_counter;
+ if (document.title.search(/^Messages \(\d+\) /) === -1) {
+ document.title = "Messages (" + unreadMsgCount + ") " + document.title;
+ } else {
+ document.title = document.title.replace(/^Messages \(\d+\) /, "Messages (" + unreadMsgCount + ") ");
+ }
};
this.clearMsgCounter = function () {
this.msg_counter = 0;
- this.updateMsgCounter();
+ if (document.title.search(/^Messages \(\d+\) /) !== -1) {
+ document.title = document.title.replace(/^Messages \(\d+\) /, "");
+ }
};
this.initStatus = function () {
@@ -47780,7 +48921,9 @@ return Backbone.BrowserStorage;
if (!_.isUndefined(this.roster)) {
this.roster.browserStorage._clear();
}
- this.session.browserStorage._clear();
+ if (!_.isUndefined(this.session) && this.session.browserStorage) {
+ this.session.browserStorage._clear();
+ }
};
this.logOut = function () {
@@ -47818,6 +48961,7 @@ return Backbone.BrowserStorage;
_converse.clearMsgCounter();
}
_converse.windowState = state;
+ _converse.emit('windowStateChanged', {state: state})
};
this.registerGlobalEventHandlers = function () {
@@ -47936,6 +49080,7 @@ return Backbone.BrowserStorage;
}
// First set up chat boxes, before populating the roster, so that
// the controlbox is properly set up and ready for the rosterview.
+ _converse.roster.onConnected();
_converse.chatboxes.onConnected();
_converse.populateRoster();
_converse.registerPresenceHandler();
@@ -47963,6 +49108,7 @@ return Backbone.BrowserStorage;
// by browser.
_converse.connection.flush();
+ _converse.initSession();
_converse.setUserJid();
_converse.enableCarbons();
@@ -47986,6 +49132,17 @@ return Backbone.BrowserStorage;
this.RosterContact = Backbone.Model.extend({
+ defaults: {
+ 'bookmarked': false,
+ 'chat_state': undefined,
+ 'chat_status': 'offline',
+ 'groups': [],
+ 'image': DEFAULT_IMAGE,
+ 'image_type': DEFAULT_IMAGE_TYPE,
+ 'num_unread': 0,
+ 'status': '',
+ },
+
initialize: function (attributes) {
var jid = attributes.jid;
var bare_jid = Strophe.getBareJidFromJid(jid).toLowerCase();
@@ -47995,13 +49152,8 @@ return Backbone.BrowserStorage;
'id': bare_jid,
'jid': bare_jid,
'fullname': bare_jid,
- 'chat_status': 'offline',
'user_id': Strophe.getNodeFromJid(jid),
- 'resources': resource ? {'resource':0} : {},
- 'groups': [],
- 'image_type': DEFAULT_IMAGE_TYPE,
- 'image': DEFAULT_IMAGE,
- 'status': ''
+ 'resources': resource ? {resource :0} : {},
}, attributes));
this.on('destroy', function () { this.removeFromRoster(); }.bind(this));
@@ -48178,6 +49330,45 @@ return Backbone.BrowserStorage;
}
},
+ onConnected: function () {
+ /* Called as soon as the connection has been established
+ * (either after initial login, or after reconnection).
+ *
+ * Use the opportunity to register stanza handlers.
+ */
+ this.registerRosterHandler();
+ this.registerRosterXHandler();
+ },
+
+ registerRosterHandler: function () {
+ /* Register a handler for roster IQ "set" stanzas, which update
+ * roster contacts.
+ */
+ _converse.connection.addHandler(
+ _converse.roster.onRosterPush.bind(_converse.roster),
+ Strophe.NS.ROSTER, 'iq', "set"
+ );
+ },
+
+ registerRosterXHandler: function () {
+ /* Register a handler for RosterX message stanzas, which are
+ * used to suggest roster contacts to a user.
+ */
+ var t = 0;
+ _converse.connection.addHandler(
+ function (msg) {
+ window.setTimeout(
+ function () {
+ _converse.connection.flush();
+ _converse.roster.subscribeToSuggestedItems.bind(_converse.roster)(msg);
+ }, t);
+ t += $(msg).find('item').length*250;
+ return true;
+ },
+ Strophe.NS.ROSTERX, 'message', null
+ );
+ },
+
fetchRosterContacts: function () {
/* Fetches the roster contacts, first by trying the
* sessionStorage cache, and if that's empty, then by querying
@@ -48222,7 +49413,7 @@ return Backbone.BrowserStorage;
},
isSelf: function (jid) {
- return (Strophe.getBareJidFromJid(jid) === Strophe.getBareJidFromJid(_converse.connection.jid));
+ return utils.isSameBareJID(jid, _converse.connection.jid);
},
addAndSubscribe: function (jid, name, groups, message, attributes) {
@@ -48561,6 +49752,14 @@ return Backbone.BrowserStorage;
this.ChatBox = Backbone.Model.extend({
+ defaults: {
+ 'type': 'chatbox',
+ 'bookmarked': false,
+ 'chat_state': undefined,
+ 'num_unread': 0,
+ 'url': ''
+ },
+
initialize: function () {
this.messages = new _converse.Messages();
this.messages.browserStorage = new Backbone.BrowserStorage[_converse.message_storage](
@@ -48569,10 +49768,7 @@ return Backbone.BrowserStorage;
// The chat_state will be set to ACTIVE once the chat box is opened
// and we listen for change:chat_state, so shouldn't set it to ACTIVE here.
'box_id' : b64_sha1(this.get('jid')),
- 'chat_state': undefined,
- 'num_unread': this.get('num_unread') || 0,
'time_opened': this.get('time_opened') || moment().valueOf(),
- 'url': '',
'user_id' : Strophe.getNodeFromJid(this.get('jid'))
});
},
@@ -48627,13 +49823,47 @@ return Backbone.BrowserStorage;
createMessage: function (message, delay, original_stanza) {
return this.messages.create(this.getMessageAttributes.apply(this, arguments));
+ },
+
+ newMessageWillBeHidden: function () {
+ /* Returns a boolean to indicate whether a newly received
+ * message will be visible to the user or not.
+ */
+ return this.get('hidden') ||
+ this.get('minimized') ||
+ this.isScrolledUp() ||
+ _converse.windowState === 'hidden';
+ },
+
+ incrementUnreadMsgCounter: function (stanza) {
+ /* Given a newly received message, update the unread counter if
+ * necessary.
+ */
+ if (_.isNull(stanza.querySelector('body'))) {
+ return; // The message has no text
+ }
+ if (utils.isNewMessage(stanza) && this.newMessageWillBeHidden()) {
+ this.save({'num_unread': this.get('num_unread') + 1});
+ _converse.incrementMsgCounter();
+ }
+ },
+
+ clearUnreadMsgCounter: function() {
+ this.save({'num_unread': 0});
+ },
+
+ isScrolledUp: function () {
+ return this.get('scrolled', true);
}
});
this.ChatBoxes = Backbone.Collection.extend({
- model: _converse.ChatBox,
comparator: 'time_opened',
+ model: function (attrs, options) {
+ return new _converse.ChatBox(attrs, options);
+ },
+
registerMessageHandler: function () {
_converse.connection.addHandler(this.onMessage.bind(this), null, 'message', 'chat');
_converse.connection.addHandler(this.onErrorMessage.bind(this), null, 'message', 'error');
@@ -48673,7 +49903,7 @@ return Backbone.BrowserStorage;
*/
// TODO: we can likely just reuse "onMessage" below
var from_jid = Strophe.getBareJidFromJid(message.getAttribute('from'));
- if (from_jid === _converse.bare_jid) {
+ if (utils.isSameBareJID(from_jid, _converse.bare_jid)) {
return true;
}
// Get chat box, but only create a new one when the message has a body.
@@ -48690,8 +49920,8 @@ return Backbone.BrowserStorage;
* stanzas.
*/
var original_stanza = message,
- contact_jid, forwarded, delay, from_bare_jid,
- from_resource, is_me, msgid,
+ contact_jid, delay, from_bare_jid,
+ from_resource, is_me, msgid, messages,
chatbox, resource,
from_jid = message.getAttribute('from'),
to_jid = message.getAttribute('to'),
@@ -48714,7 +49944,7 @@ return Backbone.BrowserStorage;
);
return true;
}
- forwarded = message.querySelector('forwarded');
+ var forwarded = message.querySelector('forwarded');
if (!_.isNull(forwarded)) {
var forwarded_message = forwarded.querySelector('message');
var forwarded_from = forwarded_message.getAttribute('from');
@@ -48732,7 +49962,6 @@ return Backbone.BrowserStorage;
from_bare_jid = Strophe.getBareJidFromJid(from_jid);
from_resource = Strophe.getResourceFromJid(from_jid);
is_me = from_bare_jid === _converse.bare_jid;
- msgid = message.getAttribute('id');
if (is_me) {
// I am the sender, so this must be a forwarded message...
contact_jid = Strophe.getBareJidFromJid(to_jid);
@@ -48741,19 +49970,55 @@ return Backbone.BrowserStorage;
contact_jid = from_bare_jid;
resource = from_resource;
}
- _converse.emit('message', original_stanza);
// Get chat box, but only create a new one when the message has a body.
chatbox = this.getChatBox(contact_jid, !_.isNull(message.querySelector('body')));
- if (!chatbox) {
- return true;
+ msgid = message.getAttribute('id');
+ if (chatbox) {
+ messages = msgid && chatbox.messages.findWhere({msgid: msgid}) || [];
+ if (_.isEmpty(messages)) {
+ // Only create the message when we're sure it's not a
+ // duplicate
+ chatbox.incrementUnreadMsgCounter(original_stanza);
+ chatbox.createMessage(message, delay, original_stanza);
+ }
}
- if (msgid && chatbox.messages.findWhere({msgid: msgid})) {
- return true; // We already have this message stored.
- }
- chatbox.createMessage(message, delay, original_stanza);
+ _converse.emit('message', {'stanza': original_stanza, 'chatbox': chatbox});
return true;
},
+ createChatBox: function (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.
+ */
+ var bare_jid = Strophe.getBareJidFromJid(jid);
+ var roster_info = {};
+ var roster_item = _converse.roster.get(bare_jid);
+ 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', 'error');
+ return;
+ }
+ return this.create(_.assignIn({
+ 'id': bare_jid,
+ 'jid': bare_jid,
+ 'fullname': jid,
+ 'image_type': DEFAULT_IMAGE_TYPE,
+ 'image': DEFAULT_IMAGE,
+ 'url': '',
+ }, roster_info, attrs || {}));
+ },
+
getChatBox: function (jid, create, attrs) {
/* Returns a chat box or optionally return a newly
* created one if one doesn't exist.
@@ -48764,31 +50029,9 @@ return Backbone.BrowserStorage;
* (Object) attrs - Optional chat box atributes.
*/
jid = jid.toLowerCase();
- var bare_jid = Strophe.getBareJidFromJid(jid);
- var chatbox = this.get(bare_jid);
+ var chatbox = this.get(Strophe.getBareJidFromJid(jid));
if (!chatbox && create) {
- var roster_info = {};
- var roster_item = _converse.roster.get(bare_jid);
- 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', 'error');
- return;
- }
- chatbox = this.create(_.assignIn({
- 'id': bare_jid,
- 'jid': bare_jid,
- 'fullname': jid,
- 'image_type': DEFAULT_IMAGE_TYPE,
- 'image': DEFAULT_IMAGE,
- 'url': '',
- }, roster_info, attrs || {}));
+ chatbox = this.createChatBox(jid, attrs);
}
return chatbox;
}
@@ -49080,39 +50323,92 @@ return Backbone.BrowserStorage;
xhr.send();
};
- this.attemptPreboundSession = function (reconnecting) {
- /* Handle session resumption or initialization when prebind is being used.
- */
- if (!reconnecting && this.keepalive) {
- if (!this.jid) {
- throw new Error("attemptPreboundSession: when using 'keepalive' with 'prebind, "+
- "you must supply the JID of the current user.");
- }
- try {
- return this.connection.restore(this.jid, this.onConnectStatusChanged);
- } catch (e) {
- this.log("Could not restore session for jid: "+this.jid+" Error message: "+e.message);
- this.clearSession(); // If there's a roster, we want to clear it (see #555)
+ this.restoreBOSHSession = function (jid_is_required) {
+ /* Tries to restore a cached BOSH session. */
+ if (!this.jid) {
+ var msg = "restoreBOSHSession: tried to restore a \"keepalive\" session "+
+ "but we don't have the JID for the user!";
+ if (jid_is_required) {
+ throw new Error(msg);
+ } else {
+ _converse.log(msg);
}
}
+ try {
+ this.connection.restore(this.jid, this.onConnectStatusChanged);
+ return true;
+ } catch (e) {
+ this.log(
+ "Could not restore session for jid: "+
+ this.jid+" Error message: "+e.message);
+ this.clearSession(); // If there's a roster, we want to clear it (see #555)
+ return false;
+ }
+ };
- // No keepalive, or session resumption has failed.
- if (!reconnecting && this.jid && this.sid && this.rid) {
- return this.connection.attach(this.jid, this.sid, this.rid, this.onConnectStatusChanged);
- } else if (this.prebind_url) {
+ this.attemptPreboundSession = function (reconnecting) {
+ /* Handle session resumption or initialization when prebind is
+ * being used.
+ */
+ if (!reconnecting) {
+ if (this.keepalive && this.restoreBOSHSession(true)) {
+ return;
+ }
+ // No keepalive, or session resumption has failed.
+ if (this.jid && this.sid && this.rid) {
+ return this.connection.attach(
+ this.jid, this.sid, this.rid,
+ this.onConnectStatusChanged
+ );
+ }
+ }
+ if (this.prebind_url) {
return this.startNewBOSHSession();
} else {
- throw new Error("attemptPreboundSession: If you use prebind and not keepalive, "+
+ throw new Error(
+ "attemptPreboundSession: If you use prebind and not keepalive, "+
"then you MUST supply JID, RID and SID values or a prebind_url.");
}
};
+ this.attemptNonPreboundSession = function (credentials, reconnecting) {
+ /* Handle session resumption or initialization when prebind is not being used.
+ *
+ * Two potential options exist and are handled in this method:
+ * 1. keepalive
+ * 2. auto_login
+ */
+ if (!reconnecting && this.keepalive && this.restoreBOSHSession()) {
+ return;
+ }
+ if (this.auto_login) {
+ if (credentials) {
+ // When credentials are passed in, they override prebinding
+ // or credentials fetching via HTTP
+ this.autoLogin(credentials);
+ } else if (this.credentials_url) {
+ this.fetchLoginCredentials().done(this.autoLogin.bind(this));
+ } else if (!this.jid) {
+ throw new Error(
+ "attemptNonPreboundSession: If you use auto_login, "+
+ "you also need to give either a jid value (and if "+
+ "applicable a password) or you need to pass in a URL "+
+ "from where the username and password can be fetched "+
+ "(via credentials_url)."
+ );
+ } else {
+ this.autoLogin(); // Probably ANONYMOUS login
+ }
+ } else if (reconnecting) {
+ this.autoLogin();
+ }
+ };
+
this.autoLogin = function (credentials) {
if (credentials) {
- // If passed in, then they come from credentials_url, so we
- // set them on the _converse object.
+ // If passed in, the credentials come from credentials_url,
+ // so we set them on the converse object.
this.jid = credentials.jid;
- this.password = credentials.password;
}
if (this.authentication === _converse.ANONYMOUS) {
if (!this.jid) {
@@ -49124,9 +50420,9 @@ return Backbone.BrowserStorage;
this.connection.reset();
this.connection.connect(this.jid.toLowerCase(), null, this.onConnectStatusChanged);
} else if (this.authentication === _converse.LOGIN) {
- var password = _converse.connection.pass || this.password;
+ var password = _.isNil(credentials) ? (_converse.connection.pass || this.password) : credentials.password;
if (!password) {
- if (this.auto_login && !this.password) {
+ if (this.auto_login) {
throw new Error("initConnection: If you use auto_login and "+
"authentication='login' then you also need to provide a password.");
}
@@ -49145,44 +50441,6 @@ return Backbone.BrowserStorage;
}
};
- this.attemptNonPreboundSession = function (credentials, reconnecting) {
- /* Handle session resumption or initialization when prebind is not being used.
- *
- * Two potential options exist and are handled in this method:
- * 1. keepalive
- * 2. auto_login
- */
- if (this.keepalive && !reconnecting) {
- try {
- return this.connection.restore(this.jid, this.onConnectStatusChanged);
- } catch (e) {
- this.log("Could not restore session. Error message: "+e.message);
- this.clearSession(); // If there's a roster, we want to clear it (see #555)
- }
- }
- if (this.auto_login) {
- if (credentials) {
- // When credentials are passed in, they override prebinding
- // or credentials fetching via HTTP
- this.autoLogin(credentials);
- } else if (this.credentials_url) {
- this.fetchLoginCredentials().done(this.autoLogin.bind(this));
- } else if (!this.jid) {
- throw new Error(
- "initConnection: If you use auto_login, you also need"+
- "to give either a jid value (and if applicable a "+
- "password) or you need to pass in a URL from where the "+
- "username and password can be fetched (via credentials_url)."
- );
- } else {
- // Probably ANONYMOUS login
- this.autoLogin();
- }
- } else if (reconnecting) {
- this.autoLogin();
- }
- };
-
this.logIn = function (credentials, reconnecting) {
// We now try to resume or automatically set up a new session.
// Otherwise the user will be shown a login form.
@@ -49225,6 +50483,7 @@ return Backbone.BrowserStorage;
if (this.features) {
this.features.reset();
}
+ this.session.destroy();
window.removeEventListener('click', _converse.onUserActivity);
window.removeEventListener('focus', _converse.onUserActivity);
window.removeEventListener('keypress', _converse.onUserActivity);
@@ -49274,7 +50533,6 @@ return Backbone.BrowserStorage;
}
_converse.initPlugins();
_converse.initChatBoxes();
- _converse.initSession();
_converse.initConnection();
_converse.setUpXMLLogging();
_converse.logIn();
@@ -49487,6 +50745,7 @@ return Backbone.BrowserStorage;
'Backbone': Backbone,
'Strophe': Strophe,
'_': _,
+ 'fp': fp,
'b64_sha1': b64_sha1,
'jQuery': $,
'moment': moment,
@@ -49497,3242 +50756,6 @@ return Backbone.BrowserStorage;
}));
-define('tpl!chatbox', ['lodash'], function(_) {return function(obj) {
-obj || (obj = {});
-var __t, __p = '', __e = _.escape, __j = Array.prototype.join;
-function print() { __p += __j.call(arguments, '') }
-with (obj) {
-__p += '\n
\n
\n
\n
\n
\n
\n
â–¼ ' +
-__e( unread_msgs ) +
-' â–¼
\n ';
- if (show_textarea) { ;
-__p += '\n
\n ';
- } ;
-__p += '\n
\n
\n';
-
-}
-return __p
-};});
-
-
-define('tpl!new_day', ['lodash'], function(_) {return function(obj) {
-obj || (obj = {});
-var __t, __p = '', __e = _.escape;
-with (obj) {
-__p += '\n';
-
-}
-return __p
-};});
-
-
-define('tpl!action', ['lodash'], function(_) {return function(obj) {
-obj || (obj = {});
-var __t, __p = '', __e = _.escape;
-with (obj) {
-__p += '\n';
-
-}
-return __p
-};});
-
-
-define('tpl!message', ['lodash'], function(_) {return function(obj) {
-obj || (obj = {});
-var __t, __p = '', __e = _.escape;
-with (obj) {
-__p += '\n';
-
-}
-return __p
-};});
-
-
-define('tpl!help_message', ['lodash'], function(_) {return function(obj) {
-obj || (obj = {});
-var __t, __p = '', __e = _.escape;
-with (obj) {
-__p += '' +
-__e(message) +
-'
\n';
-
-}
-return __p
-};});
-
-
-define('tpl!toolbar', ['lodash'], function(_) {return function(obj) {
-obj || (obj = {});
-var __t, __p = '', __e = _.escape, __j = Array.prototype.join;
-function print() { __p += __j.call(arguments, '') }
-with (obj) {
-
- if (show_emoticons) { ;
-__p += '\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
\n \n';
- } ;
-__p += '\n';
- if (show_call_button) { ;
-__p += '\n\n';
- } ;
-__p += '\n';
- if (show_clear_button) { ;
-__p += '\n\n';
- } ;
-__p += '\n';
-
-}
-return __p
-};});
-
-
-define('tpl!avatar', ['lodash'], function(_) {return function(obj) {
-obj || (obj = {});
-var __t, __p = '';
-with (obj) {
-__p += '\n';
-
-}
-return __p
-};});
-
-// 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('converse-chatview',[
- "converse-core",
- "tpl!chatbox",
- "tpl!new_day",
- "tpl!action",
- "tpl!message",
- "tpl!help_message",
- "tpl!toolbar",
- "tpl!avatar"
- ], factory);
-}(this, function (
- converse,
- tpl_chatbox,
- tpl_new_day,
- tpl_action,
- tpl_message,
- tpl_help_message,
- tpl_toolbar,
- tpl_avatar
- ) {
- "use strict";
- var $ = converse.env.jQuery,
- $msg = converse.env.$msg,
- Backbone = converse.env.Backbone,
- Strophe = converse.env.Strophe,
- _ = converse.env._,
- moment = converse.env.moment,
- utils = converse.env.utils;
-
- var KEY = {
- ENTER: 13,
- FORWARD_SLASH: 47
- };
-
-
- converse.plugins.add('converse-chatview', {
-
- 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.
-
- ChatBoxViews: {
- onChatBoxAdded: function (item) {
- var _converse = this.__super__._converse;
- var view = this.get(item.get('id'));
- if (!view) {
- view = new _converse.ChatBoxView({model: item});
- this.add(item.get('id'), view);
- return view;
- } else {
- return this.__super__.onChatBoxAdded.apply(this, arguments);
- }
- }
- }
- },
-
-
- initialize: function () {
- /* The initialize function gets called as soon as the plugin is
- * loaded by converse.js's plugin machinery.
- */
- var _converse = this._converse,
- __ = _converse.__;
-
- this.updateSettings({
- chatview_avatar_height: 32,
- chatview_avatar_width: 32,
- show_toolbar: true,
- time_format: 'HH:mm',
- visible_toolbar_buttons: {
- 'emoticons': true,
- 'call': false,
- 'clear': true
- },
- });
-
- _converse.ChatBoxView = Backbone.View.extend({
- length: 200,
- tagName: 'div',
- className: 'chatbox hidden',
- is_chatroom: false, // Leaky abstraction from MUC
-
- events: {
- 'click .close-chatbox-button': 'close',
- 'keypress .chat-textarea': 'keyPressed',
- 'click .send-button': 'onSendButtonClicked',
- 'click .toggle-smiley': 'toggleEmoticonMenu',
- 'click .toggle-smiley ul li': 'insertEmoticon',
- 'click .toggle-clear': 'clearMessages',
- 'click .toggle-call': 'toggleCall',
- 'click .new-msgs-indicator': 'viewUnreadMessages'
- },
-
- initialize: function () {
- this.model.messages.on('add', this.onMessageAdded, this);
- this.model.on('show', this.show, this);
- this.model.on('destroy', this.hide, this);
- // TODO check for changed fullname as well
- this.model.on('change:chat_state', this.sendChatState, this);
- this.model.on('change:chat_status', this.onChatStatusChanged, this);
- this.model.on('change:image', this.renderAvatar, this);
- this.model.on('change:status', this.onStatusChanged, this);
- this.model.on('showHelpMessages', this.showHelpMessages, this);
- this.model.on('sendMessage', this.sendMessage, this);
- this.render().fetchMessages();
- _converse.emit('chatBoxInitialized', this);
- },
-
- render: function () {
- this.$el.attr('id', this.model.get('box_id'))
- .html(tpl_chatbox(
- _.extend(this.model.toJSON(), {
- show_toolbar: _converse.show_toolbar,
- show_textarea: true,
- show_send_button: _converse.show_send_button,
- title: this.model.get('fullname'),
- unread_msgs: __('You have unread messages'),
- info_close: __('Close this chat box'),
- label_personal_message: __('Personal message'),
- label_send: __('Send')
- }
- )
- )
- );
- this.$content = this.$el.find('.chat-content');
- this.renderToolbar().renderAvatar();
- _converse.emit('chatBoxOpened', this);
- utils.refreshWebkit();
- return this.showStatusMessage();
- },
-
- afterMessagesFetched: function () {
- this.insertIntoDOM();
- this.scrollDown();
- // We only start listening for the scroll event after
- // cached messages have been fetched
- this.$content.on('scroll', this.markScrolled.bind(this));
- },
-
- fetchMessages: function () {
- this.model.messages.fetch({
- 'add': true,
- 'success': this.afterMessagesFetched.bind(this),
- 'error': this.afterMessagesFetched.bind(this),
- });
- return this;
- },
-
- insertIntoDOM: function () {
- /* This method gets overridden in src/converse-controlbox.js if
- * the controlbox plugin is active.
- */
- var container = document.querySelector('#conversejs');
- if (this.el.parentNode !== container) {
- container.insertBefore(this.el, container.firstChild);
- }
- return this;
- },
-
- clearStatusNotification: function () {
- this.$content.find('div.chat-event').remove();
- },
-
- showStatusNotification: function (message, keep_old, permanent) {
- if (!keep_old) {
- this.clearStatusNotification();
- }
- var $el = $('').text(message);
- if (!permanent) {
- $el.addClass('chat-event');
- }
- this.$content.append($el);
- this.scrollDown();
- },
-
- addSpinner: function () {
- if (_.isNull(this.el.querySelector('.spinner'))) {
- this.$content.prepend('');
- }
- },
-
- clearSpinner: function () {
- if (this.$content.children(':first').is('span.spinner')) {
- this.$content.children(':first').remove();
- }
- },
-
- insertDayIndicator: function (date, prepend) {
- /* Appends (or prepends if "prepend" is truthy) an indicator
- * into the chat area, showing the day as given by the
- * passed in date.
- *
- * Parameters:
- * (String) date - An ISO8601 date string.
- */
- var day_date = moment(date).startOf('day');
- var insert = prepend ? this.$content.prepend: this.$content.append;
- insert.call(this.$content, tpl_new_day({
- isodate: day_date.format(),
- datestring: day_date.format("dddd MMM Do YYYY")
- }));
- },
-
- insertMessage: function (attrs, prepend) {
- /* Helper method which appends a message (or prepends if the
- * 2nd parameter is set to true) to the end of the chat box's
- * content area.
- *
- * Parameters:
- * (Object) attrs: An object containing the message attributes.
- */
- var that = this;
- var insert = prepend ? this.$content.prepend : this.$content.append;
- _.flow(
- function ($el) {
- insert.call(that.$content, $el);
- return $el;
- },
- this.scrollDown.bind(this)
- )(this.renderMessage(attrs));
- },
-
- showMessage: function (attrs) {
- /* Inserts a chat message into the content area of the chat box.
- * Will also insert a new day indicator if the message is on a
- * different day.
- *
- * The message to show may either be newer than the newest
- * message, or older than the oldest message.
- *
- * Parameters:
- * (Object) attrs: An object containing the message
- * attributes.
- */
- var msg_dates, idx,
- $first_msg = this.$content.find('.chat-message:first'),
- first_msg_date = $first_msg.data('isodate'),
- current_msg_date = moment(attrs.time) || moment,
- last_msg_date = this.$content.find('.chat-message:last').data('isodate');
-
- if (!first_msg_date) {
- // This is the first received message, so we insert a
- // date indicator before it.
- this.insertDayIndicator(current_msg_date);
- this.insertMessage(attrs);
- return;
- }
- if (current_msg_date.isAfter(last_msg_date) ||
- current_msg_date.isSame(last_msg_date)) {
- // The new message is after the last message
- if (current_msg_date.isAfter(last_msg_date, 'day')) {
- // Append a new day indicator
- this.insertDayIndicator(current_msg_date);
- }
- this.insertMessage(attrs);
- return;
- }
- if (current_msg_date.isBefore(first_msg_date) ||
- current_msg_date.isSame(first_msg_date)) {
- // The message is before the first, but on the same day.
- // We need to prepend the message immediately before the
- // first message (so that it'll still be after the day
- // indicator).
- this.insertMessage(attrs, 'prepend');
- if (current_msg_date.isBefore(first_msg_date, 'day')) {
- // This message is also on a different day, so
- // we prepend a day indicator.
- this.insertDayIndicator(current_msg_date, 'prepend');
- }
- return;
- }
- // Find the correct place to position the message
- current_msg_date = current_msg_date.format();
- msg_dates = _.map(this.$content.find('.chat-message'), function (el) {
- return $(el).data('isodate');
- });
- msg_dates.push(current_msg_date);
- msg_dates.sort();
- idx = msg_dates.indexOf(current_msg_date)-1;
- _.flow(
- function ($el) {
- $el.insertAfter(
- this.$content.find('.chat-message[data-isodate="'+msg_dates[idx]+'"]'));
- return $el;
- }.bind(this),
- this.scrollDown.bind(this)
- )(this.renderMessage(attrs));
- },
-
- getExtraMessageTemplateAttributes: function () {
- /* Provides a hook for sending more attributes to the
- * message template.
- *
- * Parameters:
- * (Object) attrs: An object containing message attributes.
- */
- return {};
- },
-
- getExtraMessageClasses: function (attrs) {
- return attrs.delayed && 'delayed' || '';
- },
-
- renderMessage: function (attrs) {
- /* Renders a chat message based on the passed in attributes.
- *
- * Parameters:
- * (Object) attrs: An object containing the message attributes.
- *
- * Returns:
- * The DOM element representing the message.
- */
- var msg_time = moment(attrs.time) || moment,
- text = attrs.message,
- match = text.match(/^\/(.*?)(?: (.*))?$/),
- fullname = this.model.get('fullname') || attrs.fullname,
- template, username;
-
- if ((match) && (match[1] === 'me')) {
- text = text.replace(/^\/me/, '');
- template = tpl_action;
- if (attrs.sender === 'me') {
- fullname = _converse.xmppstatus.get('fullname') || attrs.fullname;
- username = _.isNil(fullname)? _converse.bare_jid: fullname;
- } else {
- username = attrs.fullname;
- }
- } else {
- template = tpl_message;
- username = attrs.sender === 'me' && __('me') || fullname;
- }
- this.$content.find('div.chat-event').remove();
-
- if (text.length > 8000) {
- text = text.substring(0, 10) + '...';
- this.showStatusNotification(
- __("A very large message has been received."+
- "This might be due to an attack meant to degrade the chat performance."+
- "Output has been shortened."),
- true, true);
- }
- var $msg = $(template(
- _.extend(this.getExtraMessageTemplateAttributes(attrs), {
- 'msgid': attrs.msgid,
- 'sender': attrs.sender,
- 'time': msg_time.format(_converse.time_format),
- 'isodate': msg_time.format(),
- 'username': username,
- 'extra_classes': this.getExtraMessageClasses(attrs)
- })
- ));
- $msg.find('.chat-msg-content').first()
- .text(text)
- .addHyperlinks()
- .addEmoticons(_converse.visible_toolbar_buttons.emoticons);
- return $msg;
- },
-
- showHelpMessages: function (msgs, type, spinner) {
- var i, msgs_length = msgs.length;
- for (i=0; i');
- } else if (spinner === false) {
- this.$content.find('span.spinner').remove();
- }
- return this.scrollDown();
- },
-
- handleChatStateMessage: function (message) {
- if (message.get('chat_state') === _converse.COMPOSING) {
- if (message.get('sender') === 'me') {
- this.showStatusNotification(__('Typing from another device'));
- } else {
- this.showStatusNotification(message.get('fullname')+' '+__('is typing'));
- }
- this.clear_status_timeout = window.setTimeout(this.clearStatusNotification.bind(this), 30000);
- } else if (message.get('chat_state') === _converse.PAUSED) {
- if (message.get('sender') === 'me') {
- this.showStatusNotification(__('Stopped typing on the other device'));
- } else {
- this.showStatusNotification(message.get('fullname')+' '+__('has stopped typing'));
- }
- } else if (_.includes([_converse.INACTIVE, _converse.ACTIVE], message.get('chat_state'))) {
- this.$content.find('div.chat-event').remove();
- } else if (message.get('chat_state') === _converse.GONE) {
- this.showStatusNotification(message.get('fullname')+' '+__('has gone away'));
- }
- },
-
- shouldShowOnTextMessage: function () {
- return !this.$el.is(':visible');
- },
-
- updateNewMessageIndicators: function (message) {
- /* We have two indicators of new messages. The unread messages
- * counter, which shows the number of unread messages in
- * the document.title, and the "new messages" indicator in
- * a chat area, if it's scrolled up so that new messages
- * aren't visible.
- *
- * In both cases we ignore MAM messages.
- */
- if (!message.get('archive_id')) {
- if (this.model.get('scrolled', true)) {
- this.$el.find('.new-msgs-indicator').removeClass('hidden');
- }
- if (_converse.windowState === 'hidden' || this.model.get('scrolled', true)) {
- _converse.incrementMsgCounter();
- }
- }
- },
-
- handleTextMessage: function (message) {
- this.showMessage(_.clone(message.attributes));
- if (message.get('sender') !== 'me') {
- this.updateNewMessageIndicators(message);
- } else {
- // We remove the "scrolled" flag so that the chat area
- // gets scrolled down. We always want to scroll down
- // when the user writes a message as opposed to when a
- // message is received.
- this.model.set('scrolled', false);
- }
- if (this.shouldShowOnTextMessage()) {
- this.show();
- } else {
- this.scrollDown();
- }
- },
-
- handleErrorMessage: function (message) {
- var $message = $('[data-msgid='+message.get('msgid')+']');
- if ($message.length) {
- $message.after($('').text(message.get('message')));
- this.scrollDown();
- }
- },
-
- onMessageAdded: function (message) {
- /* Handler that gets called when a new message object is created.
- *
- * Parameters:
- * (Object) message - The message Backbone object that was added.
- */
- if (!_.isUndefined(this.clear_status_timeout)) {
- window.clearTimeout(this.clear_status_timeout);
- delete this.clear_status_timeout;
- }
- if (message.get('type') === 'error') {
- this.handleErrorMessage(message);
- } else if (!message.get('message')) {
- this.handleChatStateMessage(message);
- } else {
- this.handleTextMessage(message);
- }
- },
-
- createMessageStanza: function (message) {
- return $msg({
- from: _converse.connection.jid,
- to: this.model.get('jid'),
- type: 'chat',
- id: message.get('msgid')
- }).c('body').t(message.get('message')).up()
- .c(_converse.ACTIVE, {'xmlns': Strophe.NS.CHATSTATES}).up();
- },
-
- sendMessage: function (message) {
- /* Responsible for sending off a text message.
- *
- * Parameters:
- * (Message) message - The chat message
- */
- // TODO: We might want to send to specfic resources.
- // Especially in the OTR case.
- var messageStanza = this.createMessageStanza(message);
- _converse.connection.send(messageStanza);
- if (_converse.forward_messages) {
- // Forward the message, so that other connected resources are also aware of it.
- _converse.connection.send(
- $msg({ to: _converse.bare_jid, type: 'chat', id: message.get('msgid') })
- .c('forwarded', {xmlns:'urn:xmpp:forward:0'})
- .c('delay', {xmns:'urn:xmpp:delay',stamp:(new Date()).getTime()}).up()
- .cnode(messageStanza.tree())
- );
- }
- },
-
- onMessageSubmitted: function (text) {
- /* This method gets called once the user has typed a message
- * and then pressed enter in a chat box.
- *
- * Parameters:
- * (string) text - The chat message text.
- */
- if (!_converse.connection.authenticated) {
- return this.showHelpMessages(
- ['Sorry, the connection has been lost, '+
- 'and your message could not be sent'],
- 'error'
- );
- }
- var match = text.replace(/^\s*/, "").match(/^\/(.*)\s*$/), msgs;
- if (match) {
- if (match[1] === "clear") {
- return this.clearMessages();
- }
- else if (match[1] === "help") {
- msgs = [
- '/help:'+__('Show this menu')+'',
- '/me:'+__('Write in the third person')+'',
- '/clear:'+__('Remove messages')+''
- ];
- this.showHelpMessages(msgs);
- return;
- }
- }
- var fullname = _converse.xmppstatus.get('fullname');
- fullname = _.isEmpty(fullname)? _converse.bare_jid: fullname;
- var message = this.model.messages.create({
- fullname: fullname,
- sender: 'me',
- time: moment().format(),
- message: text
- });
- this.sendMessage(message);
- },
-
- sendChatState: function () {
- /* 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.
- */
- _converse.connection.send(
- $msg({'to':this.model.get('jid'), 'type': 'chat'})
- .c(this.model.get('chat_state'), {'xmlns': Strophe.NS.CHATSTATES}).up()
- .c('no-store', {'xmlns': Strophe.NS.HINTS}).up()
- .c('no-permanent-store', {'xmlns': Strophe.NS.HINTS})
- );
- },
-
- setChatState: function (state, no_save) {
- /* Mutator for setting the chat state of this chat session.
- * Handles clearing of any chat state notification timeouts and
- * setting new ones if necessary.
- * Timeouts are set when the state being set is COMPOSING or PAUSED.
- * After the timeout, COMPOSING will become PAUSED and PAUSED will become INACTIVE.
- * See XEP-0085 Chat State Notifications.
- *
- * Parameters:
- * (string) state - The chat state (consts ACTIVE, COMPOSING, PAUSED, INACTIVE, GONE)
- * (Boolean) no_save - Just do the cleanup or setup but don't actually save the state.
- */
- if (!_.isUndefined(this.chat_state_timeout)) {
- window.clearTimeout(this.chat_state_timeout);
- delete this.chat_state_timeout;
- }
- if (state === _converse.COMPOSING) {
- this.chat_state_timeout = window.setTimeout(
- this.setChatState.bind(this), _converse.TIMEOUTS.PAUSED, _converse.PAUSED);
- } else if (state === _converse.PAUSED) {
- this.chat_state_timeout = window.setTimeout(
- this.setChatState.bind(this), _converse.TIMEOUTS.INACTIVE, _converse.INACTIVE);
- }
- if (!no_save && this.model.get('chat_state') !== state) {
- this.model.set('chat_state', state);
- }
- return this;
- },
-
- keyPressed: function (ev) {
- /* Event handler for when a key is pressed in a chat box textarea.
- */
- var textarea = ev.target, message;
- if (ev.keyCode === KEY.ENTER) {
- ev.preventDefault();
- message = textarea.value;
- textarea.value = '';
- textarea.focus();
- if (message !== '') {
- this.onMessageSubmitted(message);
- _converse.emit('messageSend', message);
- }
- this.setChatState(_converse.ACTIVE);
- } else {
- // Set chat state to composing if keyCode is not a forward-slash
- // (which would imply an internal command and not a message).
- this.setChatState(_converse.COMPOSING, ev.keyCode === KEY.FORWARD_SLASH);
- }
- },
-
- onSendButtonClicked: function(ev) {
- /* Event handler for when a send button is clicked in a chat box textarea.
- */
- ev.preventDefault();
- var textarea = this.el.querySelector('.chat-textarea'),
- message = textarea.value;
-
- textarea.value = '';
- textarea.focus();
- if (message !== '') {
- this.onMessageSubmitted(message);
- _converse.emit('messageSend', message);
- }
- this.setChatState(_converse.ACTIVE);
- },
-
- clearMessages: function (ev) {
- if (ev && ev.preventDefault) { ev.preventDefault(); }
- var result = confirm(__("Are you sure you want to clear the messages from this chat box?"));
- if (result === true) {
- this.$content.empty();
- this.model.messages.reset();
- this.model.messages.browserStorage._clear();
- }
- return this;
- },
-
- insertIntoTextArea: function (value) {
- var $textbox = this.$el.find('textarea.chat-textarea');
- var existing = $textbox.val();
- if (existing && (existing[existing.length-1] !== ' ')) {
- existing = existing + ' ';
- }
- $textbox.focus().val(existing+value+' ');
- },
-
- insertEmoticon: function (ev) {
- ev.stopPropagation();
- this.$el.find('.toggle-smiley ul').slideToggle(200);
- var $target = $(ev.target);
- $target = $target.is('a') ? $target : $target.children('a');
- this.insertIntoTextArea($target.data('emoticon'));
- },
-
- toggleEmoticonMenu: function (ev) {
- ev.stopPropagation();
- this.$el.find('.toggle-smiley ul').slideToggle(200);
- },
-
- toggleCall: function (ev) {
- ev.stopPropagation();
- _converse.emit('callButtonClicked', {
- connection: _converse.connection,
- model: this.model
- });
- },
-
- onChatStatusChanged: function (item) {
- var chat_status = item.get('chat_status'),
- fullname = item.get('fullname');
- fullname = _.isEmpty(fullname)? item.get('jid'): fullname;
- if (this.$el.is(':visible')) {
- if (chat_status === 'offline') {
- this.showStatusNotification(fullname+' '+__('has gone offline'));
- } else if (chat_status === 'away') {
- this.showStatusNotification(fullname+' '+__('has gone away'));
- } else if ((chat_status === 'dnd')) {
- this.showStatusNotification(fullname+' '+__('is busy'));
- } else if (chat_status === 'online') {
- this.$el.find('div.chat-event').remove();
- }
- }
- },
-
- onStatusChanged: function (item) {
- this.showStatusMessage();
- _converse.emit('contactStatusMessageChanged', {
- 'contact': item.attributes,
- 'message': item.get('status')
- });
- },
-
- showStatusMessage: function (msg) {
- msg = msg || this.model.get('status');
- if (_.isString(msg)) {
- this.$el.find('p.user-custom-message').text(msg).attr('title', msg);
- }
- return this;
- },
-
- close: function (ev) {
- if (ev && ev.preventDefault) { ev.preventDefault(); }
- if (_converse.connection.connected) {
- // Immediately sending the chat state, because the
- // model is going to be destroyed afterwards.
- this.model.set('chat_state', _converse.INACTIVE);
- this.sendChatState();
- }
- try {
- this.model.destroy();
- } catch (e) {
- _converse.log(e);
- }
- this.remove();
- _converse.emit('chatBoxClosed', this);
- return this;
- },
-
- getToolbarOptions: function (options) {
- return _.extend(options || {}, {
- 'label_clear': __('Clear all messages'),
- 'label_insert_smiley': __('Insert a smiley'),
- 'label_start_call': __('Start a call'),
- 'show_call_button': _converse.visible_toolbar_buttons.call,
- 'show_clear_button': _converse.visible_toolbar_buttons.clear,
- 'show_emoticons': _converse.visible_toolbar_buttons.emoticons,
- });
- },
-
- renderToolbar: function (toolbar, options) {
- if (!_converse.show_toolbar) { return; }
- toolbar = toolbar || tpl_toolbar;
- options = _.extend(
- this.model.toJSON(),
- this.getToolbarOptions(options || {})
- );
- this.$el.find('.chat-toolbar').html(toolbar(options));
- return this;
- },
-
- renderAvatar: function () {
- if (!this.model.get('image')) {
- return;
- }
- var width = _converse.chatview_avatar_width;
- var height = _converse.chatview_avatar_height;
- var img_src = 'data:'+this.model.get('image_type')+';base64,'+this.model.get('image'),
- canvas = $(tpl_avatar({
- 'width': width,
- 'height': height
- })).get(0);
-
- if (!(canvas.getContext && canvas.getContext('2d'))) {
- return this;
- }
- var ctx = canvas.getContext('2d');
- var img = new Image(); // Create new Image object
- img.onload = function () {
- var ratio = img.width/img.height;
- if (ratio < 1) {
- ctx.drawImage(img, 0,0, width, height*(1/ratio));
- } else {
- ctx.drawImage(img, 0,0, width, height*ratio);
- }
-
- };
- img.src = img_src;
- this.$el.find('.chat-title').before(canvas);
- return this;
- },
-
- focus: function () {
- this.$el.find('.chat-textarea').focus();
- _converse.emit('chatBoxFocused', this);
- return this;
- },
-
- hide: function () {
- this.el.classList.add('hidden');
- utils.refreshWebkit();
- return this;
- },
-
- afterShown: function (focus) {
- if (_converse.connection.connected) {
- // Without a connection, we haven't yet initialized
- // localstorage
- this.model.save();
- }
- this.setChatState(_converse.ACTIVE);
- this.scrollDown();
- if (focus) {
- this.focus();
- }
- },
-
- _show: function (focus) {
- /* Inner show method that gets debounced */
- if (this.$el.is(':visible') && this.$el.css('opacity') === "1") {
- if (focus) { this.focus(); }
- return;
- }
- utils.fadeIn(this.el, _.bind(this.afterShown, this, focus));
- },
-
- show: function (focus) {
- if (_.isUndefined(this.debouncedShow)) {
- /* We wrap the method in a debouncer and set it on the
- * instance, so that we have it debounced per instance.
- * Debouncing it on the class-level is too broad.
- */
- this.debouncedShow = _.debounce(this._show, 250, {'leading': true});
- }
- this.debouncedShow.apply(this, arguments);
- return this;
- },
-
- hideNewMessagesIndicator: function () {
- var new_msgs_indicator = this.el.querySelector('.new-msgs-indicator');
- if (!_.isNull(new_msgs_indicator)) {
- new_msgs_indicator.classList.add('hidden');
- }
- },
-
- markScrolled: _.debounce(function (ev) {
- /* Called when the chat content is scrolled up or down.
- * We want to record when the user has scrolled away from
- * the bottom, so that we don't automatically scroll away
- * from what the user is reading when new messages are
- * received.
- */
- if (ev && ev.preventDefault) { ev.preventDefault(); }
- if (this.model.get('auto_scrolled')) {
- this.model.set({
- 'scrolled': false,
- 'auto_scrolled': false
- });
- return;
- }
- var is_at_bottom =
- (this.$content.scrollTop() + this.$content.innerHeight()) >=
- this.$content[0].scrollHeight-10;
- if (is_at_bottom) {
- this.hideNewMessagesIndicator();
- this.model.save('scrolled', false);
- } else {
- // We're not at the bottom of the chat area, so we mark
- // that the box is in a scrolled-up state.
- this.model.save('scrolled', true);
- }
- }, 150),
-
- viewUnreadMessages: function () {
- this.model.save('scrolled', false);
- this.scrollDown();
- },
-
- _scrollDown: function () {
- /* Inner method that gets debounced */
- if (this.$content.is(':visible') && !this.model.get('scrolled')) {
- this.$content.scrollTop(this.$content[0].scrollHeight);
- this.hideNewMessagesIndicator();
- this.model.save({'auto_scrolled': true});
- }
- },
-
- scrollDown: function () {
- if (_.isUndefined(this.debouncedScrollDown)) {
- /* We wrap the method in a debouncer and set it on the
- * instance, so that we have it debounced per instance.
- * Debouncing it on the class-level is too broad.
- */
- this.debouncedScrollDown = _.debounce(this._scrollDown, 250);
- }
- this.debouncedScrollDown.apply(this, arguments);
- return this;
- }
- });
- }
- });
-
- return converse;
-}));
-
-
-define('tpl!add_contact_dropdown', ['lodash'], function(_) {return function(obj) {
-obj || (obj = {});
-var __t, __p = '', __e = _.escape;
-with (obj) {
-__p += '\n - \n ' +
-__e(label_add_contact) +
-'\n
\n \n
\n';
-
-}
-return __p
-};});
-
-
-define('tpl!add_contact_form', ['lodash'], function(_) {return function(obj) {
-obj || (obj = {});
-var __t, __p = '', __e = _.escape;
-with (obj) {
-__p += '\n \n\n';
-
-}
-return __p
-};});
-
-
-define('tpl!change_status_message', ['lodash'], function(_) {return function(obj) {
-obj || (obj = {});
-var __t, __p = '', __e = _.escape;
-with (obj) {
-__p += '\n';
-
-}
-return __p
-};});
-
-
-define('tpl!chat_status', ['lodash'], function(_) {return function(obj) {
-obj || (obj = {});
-var __t, __p = '', __e = _.escape;
-with (obj) {
-__p += '\n';
-
-}
-return __p
-};});
-
-
-define('tpl!choose_status', ['lodash'], function(_) {return function(obj) {
-obj || (obj = {});
-var __t, __p = '';
-with (obj) {
-__p += '\n \n \n
\n';
-
-}
-return __p
-};});
-
-
-define('tpl!contacts_panel', ['lodash'], function(_) {return function(obj) {
-obj || (obj = {});
-var __t, __p = '', __e = _.escape, __j = Array.prototype.join;
-function print() { __p += __j.call(arguments, '') }
-with (obj) {
-__p += '\n';
-
-}
-return __p
-};});
-
-
-define('tpl!contacts_tab', ['lodash'], function(_) {return function(obj) {
-obj || (obj = {});
-var __t, __p = '', __e = _.escape, __j = Array.prototype.join;
-function print() { __p += __j.call(arguments, '') }
-with (obj) {
-__p += '\n ' +
-__e(label_contacts) +
-'\n\n';
-
-}
-return __p
-};});
-
-
-define('tpl!controlbox', ['lodash'], function(_) {return function(obj) {
-obj || (obj = {});
-var __t, __p = '', __j = Array.prototype.join;
-function print() { __p += __j.call(arguments, '') }
-with (obj) {
-__p += '\n
\n
\n
\n
\n
\n ';
- if (!sticky_controlbox) { ;
-__p += '\n
\n ';
- } ;
-__p += '\n
\n
\n
\n';
-
-}
-return __p
-};});
-
-
-define('tpl!controlbox_toggle', ['lodash'], function(_) {return function(obj) {
-obj || (obj = {});
-var __t, __p = '', __e = _.escape;
-with (obj) {
-__p += '' +
-__e(label_toggle) +
-'\n';
-
-}
-return __p
-};});
-
-
-define('tpl!login_panel', ['lodash'], function(_) {return function(obj) {
-obj || (obj = {});
-var __t, __p = '', __e = _.escape, __j = Array.prototype.join;
-function print() { __p += __j.call(arguments, '') }
-with (obj) {
-__p += '\n';
-
-}
-return __p
-};});
-
-
-define('tpl!login_tab', ['lodash'], function(_) {return function(obj) {
-obj || (obj = {});
-var __t, __p = '', __e = _.escape;
-with (obj) {
-__p += '' +
-__e(label_sign_in) +
-'\n';
-
-}
-return __p
-};});
-
-
-define('tpl!search_contact', ['lodash'], function(_) {return function(obj) {
-obj || (obj = {});
-var __t, __p = '', __e = _.escape;
-with (obj) {
-__p += '\n \n\n';
-
-}
-return __p
-};});
-
-
-define('tpl!status_option', ['lodash'], function(_) {return function(obj) {
-obj || (obj = {});
-var __t, __p = '', __e = _.escape;
-with (obj) {
-__p += '\n \n \n ' +
-__e( text ) +
-'\n \n\n';
-
-}
-return __p
-};});
-
-
-define('tpl!group_header', ['lodash'], function(_) {return function(obj) {
-obj || (obj = {});
-var __t, __p = '', __e = _.escape;
-with (obj) {
-__p += '' +
-__e(label_group) +
-'\n';
-
-}
-return __p
-};});
-
-
-define('tpl!pending_contact', ['lodash'], function(_) {return function(obj) {
-obj || (obj = {});
-var __t, __p = '', __e = _.escape, __j = Array.prototype.join;
-function print() { __p += __j.call(arguments, '') }
-with (obj) {
-
- if (allow_chat_pending_contacts) { ;
-__p += '\n\n';
- } ;
-__p += '\n' +
-__e(fullname) +
-' \n';
- if (allow_chat_pending_contacts) { ;
-__p += '\n\n';
- } ;
-__p += '\n\n';
-
-}
-return __p
-};});
-
-
-define('tpl!requesting_contact', ['lodash'], function(_) {return function(obj) {
-obj || (obj = {});
-var __t, __p = '', __e = _.escape, __j = Array.prototype.join;
-function print() { __p += __j.call(arguments, '') }
-with (obj) {
-
- if (allow_chat_pending_contacts) { ;
-__p += '\n\n';
- } ;
-__p += '\n' +
-__e(fullname) +
-'\n';
- if (allow_chat_pending_contacts) { ;
-__p += '\n\n';
- } ;
-__p += '\n\n \n \n\n';
-
-}
-return __p
-};});
-
-
-define('tpl!roster', ['lodash'], function(_) {return function(obj) {
-obj || (obj = {});
-var __t, __p = '';
-with (obj) {
-__p += '
\n';
-
-}
-return __p
-};});
-
-
-define('tpl!roster_filter', ['lodash'], function(_) {return function(obj) {
-obj || (obj = {});
-var __t, __p = '', __j = Array.prototype.join;
-function print() { __p += __j.call(arguments, '') }
-with (obj) {
-__p += '\n';
-
-}
-return __p
-};});
-
-
-define('tpl!roster_item', ['lodash'], function(_) {return function(obj) {
-obj || (obj = {});
-var __t, __p = '', __e = _.escape, __j = Array.prototype.join;
-function print() { __p += __j.call(arguments, '') }
-with (obj) {
-__p += '' +
-__e(fullname) +
-'\n';
- if (allow_contact_removal) { ;
-__p += '\n\n';
- } ;
-__p += '\n';
-
-}
-return __p
-};});
-
-// 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('converse-rosterview',["converse-core",
- "tpl!group_header",
- "tpl!pending_contact",
- "tpl!requesting_contact",
- "tpl!roster",
- "tpl!roster_filter",
- "tpl!roster_item"
- ], factory);
-}(this, function (
- converse,
- tpl_group_header,
- tpl_pending_contact,
- tpl_requesting_contact,
- tpl_roster,
- tpl_roster_filter,
- tpl_roster_item) {
- "use strict";
- var $ = converse.env.jQuery,
- Backbone = converse.env.Backbone,
- utils = converse.env.utils,
- Strophe = converse.env.Strophe,
- $iq = converse.env.$iq,
- b64_sha1 = converse.env.b64_sha1,
- _ = converse.env._;
-
-
- converse.plugins.add('converse-rosterview', {
-
- 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.
- afterReconnected: function () {
- this.rosterview.registerRosterXHandler();
- this.__super__.afterReconnected.apply(this, arguments);
- },
-
- _tearDown: function () {
- /* Remove the rosterview when tearing down. It gets created
- * anew when reconnecting or logging in.
- */
- this.__super__._tearDown.apply(this, arguments);
- if (!_.isUndefined(this.rosterview)) {
- this.rosterview.remove();
- }
- },
-
- RosterGroups: {
- comparator: function () {
- // RosterGroupsComparator only gets set later (once i18n is
- // set up), so we need to wrap it in this nameless function.
- var _converse = this.__super__._converse;
- return _converse.RosterGroupsComparator.apply(this, arguments);
- }
- }
- },
-
-
- initialize: function () {
- /* The initialize function gets called as soon as the plugin is
- * loaded by converse.js's plugin machinery.
- */
- var _converse = this._converse,
- __ = _converse.__;
-
- this.updateSettings({
- allow_chat_pending_contacts: true,
- allow_contact_removal: true,
- show_toolbar: true,
- });
-
- var STATUSES = {
- 'dnd': __('This contact is busy'),
- 'online': __('This contact is online'),
- 'offline': __('This contact is offline'),
- 'unavailable': __('This contact is unavailable'),
- 'xa': __('This contact is away for an extended period'),
- 'away': __('This contact is away')
- };
- var LABEL_CONTACTS = __('Contacts');
- var LABEL_GROUPS = __('Groups');
- var HEADER_CURRENT_CONTACTS = __('My contacts');
- var HEADER_PENDING_CONTACTS = __('Pending contacts');
- var HEADER_REQUESTING_CONTACTS = __('Contact requests');
- var HEADER_UNGROUPED = __('Ungrouped');
- var HEADER_WEIGHTS = {};
- HEADER_WEIGHTS[HEADER_REQUESTING_CONTACTS] = 0;
- HEADER_WEIGHTS[HEADER_CURRENT_CONTACTS] = 1;
- HEADER_WEIGHTS[HEADER_UNGROUPED] = 2;
- HEADER_WEIGHTS[HEADER_PENDING_CONTACTS] = 3;
-
- _converse.RosterGroupsComparator = function (a, b) {
- /* Groups are sorted alphabetically, ignoring case.
- * However, Ungrouped, Requesting Contacts and Pending Contacts
- * appear last and in that order.
- */
- a = a.get('name');
- b = b.get('name');
- var special_groups = _.keys(HEADER_WEIGHTS);
- var a_is_special = _.includes(special_groups, a);
- var b_is_special = _.includes(special_groups, b);
- if (!a_is_special && !b_is_special ) {
- return a.toLowerCase() < b.toLowerCase() ? -1 : (a.toLowerCase() > b.toLowerCase() ? 1 : 0);
- } else if (a_is_special && b_is_special) {
- return HEADER_WEIGHTS[a] < HEADER_WEIGHTS[b] ? -1 : (HEADER_WEIGHTS[a] > HEADER_WEIGHTS[b] ? 1 : 0);
- } else if (!a_is_special && b_is_special) {
- return (b === HEADER_REQUESTING_CONTACTS) ? 1 : -1;
- } else if (a_is_special && !b_is_special) {
- return (a === HEADER_REQUESTING_CONTACTS) ? -1 : 1;
- }
- };
-
-
- _converse.RosterFilter = Backbone.Model.extend({
- initialize: function () {
- this.set({
- 'filter_text': '',
- 'filter_type': 'contacts',
- 'chat_state': ''
- });
- },
- });
-
- _converse.RosterFilterView = Backbone.View.extend({
- tagName: 'span',
- events: {
- "keydown .roster-filter": "liveFilter",
- "submit form.roster-filter-form": "submitFilter",
- "click .onX": "clearFilter",
- "mousemove .x": "toggleX",
- "change .filter-type": "changeTypeFilter",
- "change .state-type": "changeChatStateFilter"
- },
-
- initialize: function () {
- this.model.on('change:filter_type', this.render, this);
- this.model.on('change:filter_text', this.renderClearButton, this);
- },
-
- render: function () {
- this.el.innerHTML = tpl_roster_filter(
- _.extend(this.model.toJSON(), {
- placeholder: __('Filter'),
- label_contacts: LABEL_CONTACTS,
- label_groups: LABEL_GROUPS,
- label_state: __('State'),
- label_any: __('Any'),
- label_online: __('Online'),
- label_chatty: __('Chatty'),
- label_busy: __('Busy'),
- label_away: __('Away'),
- label_xa: __('Extended Away'),
- label_offline: __('Offline')
- }));
- this.renderClearButton();
- return this.$el;
- },
-
- renderClearButton: function () {
- var roster_filter = this.el.querySelector('.roster-filter');
- if (_.isNull(roster_filter)) {
- return;
- }
- roster_filter.classList[this.tog(roster_filter.value)]('x');
- },
-
- tog: function (v) {
- return v?'add':'remove';
- },
-
- toggleX: function (ev) {
- if (ev && ev.preventDefault) { ev.preventDefault(); }
- var el = ev.target;
- el.classList[this.tog(el.offsetWidth-18 < ev.clientX-el.getBoundingClientRect().left)]('onX');
- },
-
- changeChatStateFilter: function (ev) {
- if (ev && ev.preventDefault) { ev.preventDefault(); }
- this.model.save({
- 'chat_state': this.el.querySelector('.state-type').value
- });
- },
-
- changeTypeFilter: function (ev) {
- if (ev && ev.preventDefault) { ev.preventDefault(); }
- var type = ev.target.value;
- if (type === 'state') {
- this.model.save({
- 'filter_type': type,
- 'chat_state': this.el.querySelector('.state-type').value
- });
- } else {
- this.model.save({
- 'filter_type': type,
- 'filter_text': this.el.querySelector('.roster-filter').value
- });
- }
- },
-
- liveFilter: _.debounce(function (ev) {
- this.model.save({
- 'filter_type': this.el.querySelector('.filter-type').value,
- 'filter_text': this.el.querySelector('.roster-filter').value
- });
- }, 250),
-
- submitFilter: function (ev) {
- if (ev && ev.preventDefault) { ev.preventDefault(); }
- this.liveFilter();
- this.render();
- },
-
- isActive: function () {
- /* Returns true if the filter is enabled (i.e. if the user
- * has added values to the filter).
- */
- if (this.model.get('filter_type') === 'state' ||
- this.model.get('filter_text')) {
- return true;
- }
- return false;
- },
-
- show: function () {
- if (this.$el.is(':visible')) { return this; }
- this.$el.show();
- return this;
- },
-
- hide: function () {
- if (!this.$el.is(':visible')) { return this; }
- if (this.el.querySelector('.roster-filter').value.length > 0) {
- // Don't hide if user is currently filtering.
- return;
- }
- this.model.save({
- 'filter_text': '',
- 'chat_state': ''
- });
- this.$el.hide();
- return this;
- },
-
- clearFilter: function (ev) {
- if (ev && ev.preventDefault) {
- ev.preventDefault();
- $(ev.target).removeClass('x onX').val('');
- }
- this.model.save({
- 'filter_text': ''
- });
- }
- });
-
- _converse.RosterView = Backbone.Overview.extend({
- tagName: 'div',
- id: 'converse-roster',
-
- initialize: function () {
- this.roster_handler_ref = this.registerRosterHandler();
- this.rosterx_handler_ref = this.registerRosterXHandler();
- _converse.roster.on("add", this.onContactAdd, this);
- _converse.roster.on('change', this.onContactChange, this);
- _converse.roster.on("destroy", this.update, this);
- _converse.roster.on("remove", this.update, this);
- this.model.on("add", this.onGroupAdd, this);
- this.model.on("reset", this.reset, this);
- _converse.on('rosterGroupsFetched', this.positionFetchedGroups, this);
- _converse.on('rosterContactsFetched', this.update, this);
- this.createRosterFilter();
- },
-
- render: function () {
- this.renderRoster();
- this.$el.html(this.filter_view.render());
- if (!_converse.allow_contact_requests) {
- // XXX: if we ever support live editing of config then
- // we'll need to be able to remove this class on the fly.
- this.el.classList.add('no-contact-requests');
- }
- return this;
- },
-
- renderRoster: function () {
- this.$roster = $(tpl_roster());
- this.roster = this.$roster[0];
- },
-
- createRosterFilter: function () {
- // Create a model on which we can store filter properties
- var model = new _converse.RosterFilter();
- model.id = b64_sha1('_converse.rosterfilter'+_converse.bare_jid);
- model.browserStorage = new Backbone.BrowserStorage.local(this.filter.id);
- this.filter_view = new _converse.RosterFilterView({'model': model});
- this.filter_view.model.on('change', this.updateFilter, this);
- this.filter_view.model.fetch();
- },
-
- updateFilter: _.debounce(function () {
- /* Filter the roster again.
- * Called whenever the filter settings have been changed or
- * when contacts have been added, removed or changed.
- *
- * Debounced so that it doesn't get called for every
- * contact fetched from browser storage.
- */
- var type = this.filter_view.model.get('filter_type');
- if (type === 'state') {
- this.filter(this.filter_view.model.get('chat_state'), type);
- } else {
- this.filter(this.filter_view.model.get('filter_text'), type);
- }
- }, 100),
-
- unregisterHandlers: function () {
- _converse.connection.deleteHandler(this.roster_handler_ref);
- delete this.roster_handler_ref;
- _converse.connection.deleteHandler(this.rosterx_handler_ref);
- delete this.rosterx_handler_ref;
- },
-
- update: _.debounce(function () {
- if (_.isNull(this.roster.parentElement)) {
- this.$el.append(this.$roster.show());
- }
- return this.showHideFilter();
- }, _converse.animate ? 100 : 0),
-
- showHideFilter: function () {
- if (!this.$el.is(':visible')) {
- return;
- }
- if (this.$roster.hasScrollBar()) {
- this.filter_view.show();
- } else if (!this.filter_view.isActive()) {
- this.filter_view.hide();
- }
- return this;
- },
-
- filter: function (query, type) {
- // First we make sure the filter is restored to its
- // original state
- _.each(this.getAll(), function (view) {
- if (view.model.contacts.length > 0) {
- view.show().filter('');
- }
- });
- // Now we can filter
- query = query.toLowerCase();
- if (type === 'groups') {
- _.each(this.getAll(), function (view, idx) {
- if (!_.includes(view.model.get('name').toLowerCase(), query.toLowerCase())) {
- view.hide();
- } else if (view.model.contacts.length > 0) {
- view.show();
- }
- });
- } else {
- _.each(this.getAll(), function (view) {
- view.filter(query, type);
- });
- }
- },
-
- reset: function () {
- _converse.roster.reset();
- this.removeAll();
- this.renderRoster();
- this.render().update();
- return this;
- },
-
- registerRosterHandler: function () {
- _converse.connection.addHandler(
- _converse.roster.onRosterPush.bind(_converse.roster),
- Strophe.NS.ROSTER, 'iq', "set"
- );
- },
-
- registerRosterXHandler: function () {
- var t = 0;
- _converse.connection.addHandler(
- function (msg) {
- window.setTimeout(
- function () {
- _converse.connection.flush();
- _converse.roster.subscribeToSuggestedItems.bind(_converse.roster)(msg);
- },
- t
- );
- t += $(msg).find('item').length*250;
- return true;
- },
- Strophe.NS.ROSTERX, 'message', null
- );
- },
-
- onGroupAdd: function (group) {
- var view = new _converse.RosterGroupView({model: group});
- this.add(group.get('name'), view.render());
- this.positionGroup(view);
- },
-
- onContactAdd: function (contact) {
- this.addRosterContact(contact).update();
- this.updateFilter();
- },
-
- onContactChange: function (contact) {
- this.updateChatBox(contact).update();
- if (_.has(contact.changed, 'subscription')) {
- if (contact.changed.subscription === 'from') {
- this.addContactToGroup(contact, HEADER_PENDING_CONTACTS);
- } else if (_.includes(['both', 'to'], contact.get('subscription'))) {
- this.addExistingContact(contact);
- }
- }
- if (_.has(contact.changed, 'ask') && contact.changed.ask === 'subscribe') {
- this.addContactToGroup(contact, HEADER_PENDING_CONTACTS);
- }
- if (_.has(contact.changed, 'subscription') && contact.changed.requesting === 'true') {
- this.addContactToGroup(contact, HEADER_REQUESTING_CONTACTS);
- }
- this.updateFilter();
- },
-
- updateChatBox: function (contact) {
- var chatbox = _converse.chatboxes.get(contact.get('jid')),
- changes = {};
- if (!chatbox) {
- return this;
- }
- if (_.has(contact.changed, 'chat_status')) {
- changes.chat_status = contact.get('chat_status');
- }
- if (_.has(contact.changed, 'status')) {
- changes.status = contact.get('status');
- }
- chatbox.save(changes);
- return this;
- },
-
- positionFetchedGroups: function () {
- /* Instead of throwing an add event for each group
- * fetched, we wait until they're all fetched and then
- * we position them.
- * Works around the problem of positionGroup not
- * working when all groups besides the one being
- * positioned aren't already in inserted into the
- * roster DOM element.
- */
- var that = this;
- this.model.sort();
- this.model.each(function (group, idx) {
- var view = that.get(group.get('name'));
- if (!view) {
- view = new _converse.RosterGroupView({model: group});
- that.add(group.get('name'), view.render());
- }
- if (idx === 0) {
- that.$roster.append(view.$el);
- } else {
- that.appendGroup(view);
- }
- });
- },
-
- positionGroup: function (view) {
- /* Place the group's DOM element in the correct alphabetical
- * position amongst the other groups in the roster.
- */
- var $groups = this.$roster.find('.roster-group'),
- index = $groups.length ? this.model.indexOf(view.model) : 0;
- if (index === 0) {
- this.$roster.prepend(view.$el);
- } else if (index === (this.model.length-1)) {
- this.appendGroup(view);
- } else {
- $($groups.eq(index)).before(view.$el);
- }
- return this;
- },
-
- appendGroup: function (view) {
- /* Add the group at the bottom of the roster
- */
- var $last = this.$roster.find('.roster-group').last();
- var $siblings = $last.siblings('dd');
- if ($siblings.length > 0) {
- $siblings.last().after(view.$el);
- } else {
- $last.after(view.$el);
- }
- return this;
- },
-
- getGroup: function (name) {
- /* Returns the group as specified by name.
- * Creates the group if it doesn't exist.
- */
- var view = this.get(name);
- if (view) {
- return view.model;
- }
- return this.model.create({name: name, id: b64_sha1(name)});
- },
-
- addContactToGroup: function (contact, name) {
- this.getGroup(name).contacts.add(contact);
- },
-
- addExistingContact: function (contact) {
- var groups;
- if (_converse.roster_groups) {
- groups = contact.get('groups');
- if (groups.length === 0) {
- groups = [HEADER_UNGROUPED];
- }
- } else {
- groups = [HEADER_CURRENT_CONTACTS];
- }
- _.each(groups, _.bind(this.addContactToGroup, this, contact));
- },
-
- addRosterContact: function (contact) {
- if (contact.get('subscription') === 'both' || contact.get('subscription') === 'to') {
- this.addExistingContact(contact);
- } else {
- if ((contact.get('ask') === 'subscribe') || (contact.get('subscription') === 'from')) {
- this.addContactToGroup(contact, HEADER_PENDING_CONTACTS);
- } else if (contact.get('requesting') === true) {
- this.addContactToGroup(contact, HEADER_REQUESTING_CONTACTS);
- }
- }
- return this;
- }
- });
-
-
- _converse.RosterContactView = Backbone.View.extend({
- tagName: 'dd',
-
- events: {
- "click .accept-xmpp-request": "acceptRequest",
- "click .decline-xmpp-request": "declineRequest",
- "click .open-chat": "openChat",
- "click .remove-xmpp-contact": "removeContact"
- },
-
- initialize: function () {
- this.model.on("change", this.render, this);
- this.model.on("remove", this.remove, this);
- this.model.on("destroy", this.remove, this);
- this.model.on("open", this.openChat, this);
- },
-
- render: function () {
- var that = this;
- if (!this.mayBeShown()) {
- this.$el.hide();
- return this;
- }
- var item = this.model,
- ask = item.get('ask'),
- chat_status = item.get('chat_status'),
- requesting = item.get('requesting'),
- subscription = item.get('subscription');
-
- var classes_to_remove = [
- 'current-xmpp-contact',
- 'pending-xmpp-contact',
- 'requesting-xmpp-contact'
- ].concat(_.keys(STATUSES));
-
- _.each(classes_to_remove,
- function (cls) {
- if (_.includes(that.el.className, cls)) {
- that.el.classList.remove(cls);
- }
- });
- this.$el.addClass(chat_status).data('status', chat_status);
-
- if ((ask === 'subscribe') || (subscription === 'from')) {
- /* ask === 'subscribe'
- * Means we have asked to subscribe to them.
- *
- * subscription === 'from'
- * They are subscribed to use, but not vice versa.
- * We assume that there is a pending subscription
- * from us to them (otherwise we're in a state not
- * supported by converse.js).
- *
- * So in both cases the user is a "pending" contact.
- */
- this.el.classList.add('pending-xmpp-contact');
- this.$el.html(tpl_pending_contact(
- _.extend(item.toJSON(), {
- 'desc_remove': __('Click to remove this contact'),
- 'allow_chat_pending_contacts': _converse.allow_chat_pending_contacts
- })
- ));
- } else if (requesting === true) {
- this.el.classList.add('requesting-xmpp-contact');
- this.$el.html(tpl_requesting_contact(
- _.extend(item.toJSON(), {
- 'desc_accept': __("Click to accept this contact request"),
- 'desc_decline': __("Click to decline this contact request"),
- 'allow_chat_pending_contacts': _converse.allow_chat_pending_contacts
- })
- ));
- } else if (subscription === 'both' || subscription === 'to') {
- this.el.classList.add('current-xmpp-contact');
- this.$el.removeClass(_.without(['both', 'to'], subscription)[0]).addClass(subscription);
- this.$el.html(tpl_roster_item(
- _.extend(item.toJSON(), {
- 'desc_status': STATUSES[chat_status||'offline'],
- 'desc_chat': __('Click to chat with this contact'),
- 'desc_remove': __('Click to remove this contact'),
- 'title_fullname': __('Name'),
- 'allow_contact_removal': _converse.allow_contact_removal
- })
- ));
- }
- return this;
- },
-
- isGroupCollapsed: function () {
- /* Check whether the group in which this contact appears is
- * collapsed.
- */
- // XXX: this sucks and is fragile.
- // It's because I tried to do the "right thing"
- // and use definition lists to represent roster groups.
- // If roster group items were inside the group elements, we
- // would simplify things by not having to check whether the
- // group is collapsed or not.
- var name = this.$el.prevAll('dt:first').data('group');
- var group = _.head(_converse.rosterview.model.where({'name': name.toString()}));
- if (group.get('state') === _converse.CLOSED) {
- return true;
- }
- return false;
- },
-
- mayBeShown: function () {
- /* Return a boolean indicating whether this contact should
- * generally be visible in the roster.
- *
- * It doesn't check for the more specific case of whether
- * the group it's in is collapsed (see isGroupCollapsed).
- */
- var chatStatus = this.model.get('chat_status');
- if ((_converse.show_only_online_users && chatStatus !== 'online') ||
- (_converse.hide_offline_users && chatStatus === 'offline')) {
- // If pending or requesting, show
- if ((this.model.get('ask') === 'subscribe') ||
- (this.model.get('subscription') === 'from') ||
- (this.model.get('requesting') === true)) {
- return true;
- }
- return false;
- }
- return true;
- },
-
- openChat: function (ev) {
- if (ev && ev.preventDefault) { ev.preventDefault(); }
- return _converse.chatboxviews.showChat(this.model.attributes);
- },
-
- removeContact: function (ev) {
- if (ev && ev.preventDefault) { ev.preventDefault(); }
- if (!_converse.allow_contact_removal) { return; }
- var result = confirm(__("Are you sure you want to remove this contact?"));
- if (result === true) {
- var iq = $iq({type: 'set'})
- .c('query', {xmlns: Strophe.NS.ROSTER})
- .c('item', {jid: this.model.get('jid'), subscription: "remove"});
- _converse.connection.sendIQ(iq,
- function (iq) {
- this.model.destroy();
- this.remove();
- }.bind(this),
- function (err) {
- alert(__("Sorry, there was an error while trying to remove "+name+" as a contact."));
- _converse.log(err);
- }
- );
- }
- },
-
- acceptRequest: function (ev) {
- if (ev && ev.preventDefault) { ev.preventDefault(); }
- _converse.roster.sendContactAddIQ(
- this.model.get('jid'),
- this.model.get('fullname'),
- [],
- function () { this.model.authorize().subscribe(); }.bind(this)
- );
- },
-
- declineRequest: function (ev) {
- if (ev && ev.preventDefault) { ev.preventDefault(); }
- var result = confirm(__("Are you sure you want to decline this contact request?"));
- if (result === true) {
- this.model.unauthorize().destroy();
- }
- return this;
- }
- });
-
-
- _converse.RosterGroupView = Backbone.Overview.extend({
- tagName: 'dt',
- className: 'roster-group',
- events: {
- "click a.group-toggle": "toggle"
- },
-
- initialize: function () {
- this.model.contacts.on("add", this.addContact, this);
- this.model.contacts.on("change:subscription", this.onContactSubscriptionChange, this);
- this.model.contacts.on("change:requesting", this.onContactRequestChange, this);
- this.model.contacts.on("change:chat_status", function (contact) {
- // This might be optimized by instead of first sorting,
- // finding the correct position in positionContact
- this.model.contacts.sort();
- this.positionContact(contact).render();
- }, this);
- this.model.contacts.on("destroy", this.onRemove, this);
- this.model.contacts.on("remove", this.onRemove, this);
- _converse.roster.on('change:groups', this.onContactGroupChange, this);
- },
-
- render: function () {
- this.el.setAttribute('data-group', this.model.get('name'));
- var html = tpl_group_header({
- label_group: this.model.get('name'),
- desc_group_toggle: this.model.get('description'),
- toggle_state: this.model.get('state')
- });
- this.el.innerHTML = html;
- return this;
- },
-
- addContact: function (contact) {
- var view = new _converse.RosterContactView({model: contact});
- this.add(contact.get('id'), view);
- view = this.positionContact(contact).render();
- if (view.mayBeShown()) {
- if (this.model.get('state') === _converse.CLOSED) {
- if (view.$el[0].style.display !== "none") { view.$el.hide(); }
- if (!this.$el.is(':visible')) { this.$el.show(); }
- } else {
- if (this.$el[0].style.display !== "block") { this.show(); }
- }
- }
- },
-
- positionContact: function (contact) {
- /* Place the contact's DOM element in the correct alphabetical
- * position amongst the other contacts in this group.
- */
- var view = this.get(contact.get('id'));
- var index = this.model.contacts.indexOf(contact);
- view.$el.detach();
- if (index === 0) {
- this.$el.after(view.$el);
- } else if (index === (this.model.contacts.length-1)) {
- this.$el.nextUntil('dt').last().after(view.$el);
- } else {
- this.$el.nextUntil('dt').eq(index).before(view.$el);
- }
- return view;
- },
-
- show: function () {
- this.$el.show();
- _.each(this.getAll(), function (view) {
- if (view.mayBeShown() && !view.isGroupCollapsed()) {
- view.$el.show();
- }
- });
- return this;
- },
-
- hide: function () {
- this.$el.nextUntil('dt').addBack().hide();
- },
-
- filter: function (q, type) {
- /* Filter the group's contacts based on the query "q".
- * The query is matched against the contact's full name.
- * If all contacts are filtered out (i.e. hidden), then the
- * group must be filtered out as well.
- */
- var matches;
- if (q.length === 0) {
- if (this.model.get('state') === _converse.OPENED) {
- this.model.contacts.each(function (item) {
- var view = this.get(item.get('id'));
- if (view.mayBeShown() && !view.isGroupCollapsed()) {
- view.$el.show();
- }
- }.bind(this));
- }
- this.showIfNecessary();
- } else {
- q = q.toLowerCase();
- if (type === 'state') {
- if (this.model.get('name') === HEADER_REQUESTING_CONTACTS) {
- // When filtering by chat state, we still want to
- // show requesting contacts, even though they don't
- // have the state in question.
- matches = this.model.contacts.filter(
- function (contact) {
- return utils.contains.not('chat_status', q)(contact) && !contact.get('requesting');
- }
- );
- } else {
- matches = this.model.contacts.filter(
- utils.contains.not('chat_status', q)
- );
- }
- } else {
- matches = this.model.contacts.filter(
- utils.contains.not('fullname', q)
- );
- }
- if (matches.length === this.model.contacts.length) {
- // hide the whole group
- this.hide();
- } else {
- _.each(matches, function (item) {
- this.get(item.get('id')).$el.hide();
- }.bind(this));
- _.each(this.model.contacts.reject(utils.contains.not('fullname', q)), function (item) {
- this.get(item.get('id')).$el.show();
- }.bind(this));
- this.showIfNecessary();
- }
- }
- },
-
- showIfNecessary: function () {
- if (!this.$el.is(':visible') && this.model.contacts.length > 0) {
- this.$el.show();
- }
- },
-
- toggle: function (ev) {
- if (ev && ev.preventDefault) { ev.preventDefault(); }
- var $el = $(ev.target);
- if ($el.hasClass("icon-opened")) {
- this.$el.nextUntil('dt').slideUp();
- this.model.save({state: _converse.CLOSED});
- $el.removeClass("icon-opened").addClass("icon-closed");
- } else {
- $el.removeClass("icon-closed").addClass("icon-opened");
- this.model.save({state: _converse.OPENED});
- this.filter(
- _converse.rosterview.$('.roster-filter').val() || '',
- _converse.rosterview.$('.filter-type').val()
- );
- }
- },
-
- onContactGroupChange: function (contact) {
- var in_this_group = _.includes(contact.get('groups'), this.model.get('name'));
- var cid = contact.get('id');
- var in_this_overview = !this.get(cid);
- if (in_this_group && !in_this_overview) {
- this.model.contacts.remove(cid);
- } else if (!in_this_group && in_this_overview) {
- this.addContact(contact);
- }
- },
-
- onContactSubscriptionChange: function (contact) {
- if ((this.model.get('name') === HEADER_PENDING_CONTACTS) && contact.get('subscription') !== 'from') {
- this.model.contacts.remove(contact.get('id'));
- }
- },
-
- onContactRequestChange: function (contact) {
- if ((this.model.get('name') === HEADER_REQUESTING_CONTACTS) && !contact.get('requesting')) {
- /* We suppress events, otherwise the remove event will
- * also cause the contact's view to be removed from the
- * "Pending Contacts" group.
- */
- this.model.contacts.remove(contact.get('id'), {'silent': true});
- // Since we suppress events, we make sure the view and
- // contact are removed from this group.
- this.get(contact.get('id')).remove();
- this.onRemove(contact);
- }
- },
-
- onRemove: function (contact) {
- this.remove(contact.get('id'));
- if (this.model.contacts.length === 0) {
- this.$el.hide();
- }
- }
- });
-
- /* -------- Event Handlers ----------- */
-
- var initRoster = function () {
- /* Create an instance of RosterView once the RosterGroups
- * collection has been created (in converse-core.js)
- */
- _converse.rosterview = new _converse.RosterView({
- 'model': _converse.rostergroups
- });
- _converse.rosterview.render();
- };
- _converse.on('rosterInitialized', initRoster);
- _converse.on('rosterReadyAfterReconnection', initRoster);
- }
- });
-}));
-
-// 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('converse-controlbox',["converse-core",
- "tpl!add_contact_dropdown",
- "tpl!add_contact_form",
- "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!login_tab",
- "tpl!search_contact",
- "tpl!status_option",
- "converse-chatview",
- "converse-rosterview"
- ], factory);
-}(this, function (
- converse,
- tpl_add_contact_dropdown,
- tpl_add_contact_form,
- 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_login_tab,
- tpl_search_contact,
- tpl_status_option
- ) {
- "use strict";
-
- var USERS_PANEL_ID = 'users';
- // Strophe methods for building stanzas
- var Strophe = converse.env.Strophe,
- Backbone = converse.env.Backbone,
- utils = converse.env.utils;
- // Other necessary globals
- var $ = converse.env.jQuery,
- _ = converse.env._,
- moment = converse.env.moment;
-
-
- 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.
-
- initSession: function () {
- this.controlboxtoggle = new this.ControlBoxToggle();
- this.__super__.initSession.apply(this, arguments);
- },
-
- initConnection: function () {
- this.__super__.initConnection.apply(this, arguments);
- if (this.connection) {
- this.addControlBox();
- }
- },
-
- _tearDown: function () {
- this.__super__._tearDown.apply(this, arguments);
- if (this.rosterview) {
- this.rosterview.unregisterHandlers();
- // Removes roster groups
- this.rosterview.model.off().reset();
- this.rosterview.each(function (groupview) {
- groupview.removeAll();
- groupview.remove();
- });
- this.rosterview.removeAll().remove();
- }
- },
-
- clearSession: function () {
- this.__super__.clearSession.apply(this, arguments);
- if (_.isUndefined(this.connection) && this.connection.connected) {
- this.chatboxes.get('controlbox').save({'connected': false});
- }
- },
-
- ChatBoxes: {
- chatBoxMayBeShown: function (chatbox) {
- return this.__super__.chatBoxMayBeShown.apply(this, arguments) &&
- chatbox.get('id') !== 'controlbox';
- },
-
- onChatBoxesFetched: function (collection, resp) {
- var _converse = this.__super__._converse;
- this.__super__.onChatBoxesFetched.apply(this, arguments);
- if (!_.includes(_.map(collection, 'id'), 'controlbox')) {
- _converse.addControlBox();
- }
- this.get('controlbox').save({connected:true});
- },
- },
-
- ChatBoxViews: {
- onChatBoxAdded: function (item) {
- var _converse = this.__super__._converse;
- if (item.get('box_id') === 'controlbox') {
- var 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: function () {
- var _converse = this.__super__._converse;
- 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: function (view) {
- var _converse = this.__super__._converse;
- var 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: function () {
- if (this.get('id') === 'controlbox') {
- this.set({
- 'time_opened': moment(0).valueOf(),
- 'num_unread': 0
- });
- } else {
- this.__super__.initialize.apply(this, arguments);
- }
- },
- },
-
-
- ChatBoxView: {
- insertIntoDOM: function () {
- var _converse = this.__super__._converse;
- this.$el.insertAfter(_converse.chatboxviews.get("controlbox").$el);
- return this;
- }
- }
- },
-
- initialize: function () {
- /* The initialize function gets called as soon as the plugin is
- * loaded by converse.js's plugin machinery.
- */
- var _converse = this._converse,
- __ = _converse.__;
-
- this.updateSettings({
- allow_logout: true,
- default_domain: undefined,
- show_controlbox_by_default: false,
- sticky_controlbox: false,
- xhr_user_search: false,
- xhr_user_search_url: ''
- });
-
- var LABEL_CONTACTS = __('Contacts');
-
- _converse.addControlBox = function () {
- return _converse.chatboxes.add({
- id: 'controlbox',
- box_id: 'controlbox',
- closed: !_converse.show_controlbox_by_default
- });
- };
-
- _converse.ControlBoxView = _converse.ChatBoxView.extend({
- tagName: 'div',
- className: 'chatbox',
- id: 'controlbox',
- events: {
- 'click a.close-chatbox-button': 'close',
- 'click ul#controlbox-tabs li a': 'switchTab',
- },
-
- initialize: function () {
- 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')) {
- this.insertRoster();
- }
- },
-
- render: function () {
- 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.html(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.contactspanel || !this.contactspanel.$el.is(':visible')) {
- this.renderContactsPanel();
- }
- return this;
- },
-
- onConnected: function () {
- if (this.model.get('connected')) {
- this.render().insertRoster();
- this.model.save();
- }
- },
-
- insertRoster: function () {
- /* Place the rosterview inside the "Contacts" panel.
- */
- this.contactspanel.$el.append(_converse.rosterview.$el);
- return this;
- },
-
- renderLoginPanel: function () {
- this.loginpanel = new _converse.LoginPanel({
- '$parent': this.$el.find('.controlbox-panes'),
- 'model': this
- });
- this.loginpanel.render();
- return this;
- },
-
- renderContactsPanel: function () {
- 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.render();
- _converse.xmppstatusview = new _converse.XMPPStatusView({
- 'model': _converse.xmppstatus
- });
- _converse.xmppstatusview.render();
- },
-
- close: function (ev) {
- if (ev && ev.preventDefault) { ev.preventDefault(); }
- if (_converse.connection.connected && !_converse.connection.disconnecting) {
- this.model.save({'closed': true});
- } else {
- this.model.trigger('hide');
- }
- _converse.emit('controlBoxClosed', this);
- return this;
- },
-
- ensureClosedState: function () {
- if (this.model.get('closed')) {
- this.hide();
- } else {
- this.show();
- }
- },
-
- hide: function (callback) {
- this.$el.addClass('hidden');
- utils.refreshWebkit();
- _converse.emit('chatBoxClosed', this);
- if (!_converse.connection.connected) {
- _converse.controlboxtoggle.render();
- }
- _converse.controlboxtoggle.show(callback);
- return this;
- },
-
- onControlBoxToggleHidden: function () {
- var that = this;
- utils.fadeIn(this.el, function () {
- _converse.controlboxtoggle.updateOnlineCount();
- utils.refreshWebkit();
- that.model.set('closed', false);
- _converse.emit('controlBoxOpened', that);
- });
- },
-
- show: function () {
- _converse.controlboxtoggle.hide(
- this.onControlBoxToggleHidden.bind(this)
- );
- return this;
- },
-
- switchTab: function (ev) {
- // TODO: automatically focus the relevant input
- if (ev && ev.preventDefault) { ev.preventDefault(); }
- var $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: function () {
- /* Override showHelpMessages in ChatBoxView, for now do nothing.
- *
- * Parameters:
- * (Array) msgs: Array of messages
- */
- return;
- }
- });
-
-
- _converse.LoginPanel = Backbone.View.extend({
- tagName: 'div',
- id: "login-dialog",
- className: 'controlbox-pane',
- events: {
- 'submit form#converse-login': 'authenticate'
- },
-
- initialize: function (cfg) {
- cfg.$parent.html(this.$el.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_username': __('XMPP Username:'),
- 'label_password': __('Password:'),
- 'label_anon_login': __('Click here to log in anonymously'),
- 'label_login': __('Log In'),
- 'placeholder_username': (_converse.locked_domain || _converse.default_domain) && __('Username') || __('user@server'),
- 'placeholder_password': __('password')
- })
- ));
- this.$tabs = cfg.$parent.parent().find('#controlbox-tabs');
- },
-
- render: function () {
- this.$tabs.append(tpl_login_tab({label_sign_in: __('Sign in')}));
- this.$el.find('input#jid').focus();
- if (!this.$el.is(':visible')) {
- this.$el.show();
- }
- return this;
- },
-
- authenticate: function (ev) {
- if (ev && ev.preventDefault) { ev.preventDefault(); }
- var $form = $(ev.target);
- if (_converse.authentication === _converse.ANONYMOUS) {
- this.connect($form, _converse.jid, null);
- return;
- }
- var $jid_input = $form.find('input[name=jid]'),
- jid = $jid_input.val(),
- $pw_input = $form.find('input[name=password]'),
- password = $pw_input.val(),
- errors = false;
-
- if (!jid) {
- errors = true;
- $jid_input.addClass('error');
- }
- 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.connect($form, jid, password);
- return false;
- },
-
- connect: function ($form, jid, password) {
- var resource;
- if ($form) {
- $form.find('input[type=submit]').hide().after('');
- }
- if (jid) {
- 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);
- },
-
- remove: function () {
- this.$tabs.empty();
- this.$el.parent().empty();
- }
- });
-
-
- _converse.XMPPStatusView = Backbone.View.extend({
- el: "span#xmpp-status-holder",
-
- events: {
- "click a.choose-xmpp-status": "toggleOptions",
- "click #fancy-xmpp-status-select a.change-xmpp-status-message": "renderStatusChangeForm",
- "submit #set-custom-xmpp-status": "setStatusMessage",
- "click .dropdown dd ul li a": "setStatus"
- },
-
- initialize: function () {
- 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: function () {
- // Replace the default dropdown with something nicer
- var $select = this.$el.find('select#select-xmpp-status'),
- chat_status = this.model.get('status') || 'offline',
- options = $('option', $select),
- $options_target,
- 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