Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/jsxc.nextcloud.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'build/js/jsxc/jsxc.js')
-rw-r--r--build/js/jsxc/jsxc.js2902
1 files changed, 2200 insertions, 702 deletions
diff --git a/build/js/jsxc/jsxc.js b/build/js/jsxc/jsxc.js
index d1b72cd..935bc12 100644
--- a/build/js/jsxc/jsxc.js
+++ b/build/js/jsxc/jsxc.js
@@ -1,13 +1,13 @@
/*!
- * jsxc v3.0.1 - 2016-10-28
+ * jsxc v3.1.0-beta - 2017-01-23
*
- * Copyright (c) 2016 Klaus Herberth <klaus@jsxc.org> <br>
+ * Copyright (c) 2017 Klaus Herberth <klaus@jsxc.org> <br>
* Released under the MIT license
*
* Please see http://www.jsxc.org/
*
* @author Klaus Herberth <klaus@jsxc.org>
- * @version 3.0.1
+ * @version 3.1.0-beta
* @license MIT
*/
@@ -25,7 +25,7 @@ var jsxc = null, RTC = null, RTCPeerconnection = null;
*/
jsxc = {
/** Version of jsxc */
- version: '3.0.1',
+ version: '3.1.0-beta',
/** True if i'm the master */
master: false,
@@ -78,6 +78,12 @@ jsxc = {
/** My bar id */
bid: null,
+ /** Current state */
+ currentState: null,
+
+ /** Current UI state */
+ currentUIState: null,
+
/** Some constants */
CONST: {
NOTIFICATION_DEFAULT: 'default',
@@ -98,7 +104,20 @@ jsxc = {
FORWARD: 'urn:xmpp:forward:0'
},
HIDDEN: 'hidden',
- SHOWN: 'shown'
+ SHOWN: 'shown',
+ STATE: {
+ INITIATING: 0,
+ PREVCONFOUND: 1,
+ SUSPEND: 2,
+ TRYTOINTERCEPT: 3,
+ INTERCEPTED: 4,
+ ESTABLISHING: 5,
+ READY: 6
+ },
+ UISTATE: {
+ INITIATING: 0,
+ READY: 1
+ }
},
/**
@@ -210,6 +229,7 @@ jsxc = {
* @param {object} options See {@link jsxc.options}
*/
init: function(options) {
+ jsxc.changeState(jsxc.CONST.STATE.INITIATING);
if (options && options.loginForm && typeof options.loginForm.attachIfFound === 'boolean' && !options.loginForm.ifFound) {
// translate deprated option attachIfFound found to new ifFound
@@ -270,15 +290,22 @@ jsxc = {
lang = jsxc.options.defaultLang;
}
- // initialize i18n translator
- $.i18n.init({
+ // initialize i18next translator
+ window.i18next.init({
lng: lang,
fallbackLng: 'en',
- resStore: I18next,
- // use localStorage and set expiration to a day
- useLocalStorage: true,
- localStorageExpirationTime: 60 * 60 * 24 * 1000,
- debug: jsxc.storage.getItem('debug') === true
+ resources: I18next,
+ debug: jsxc.storage.getItem('debug') === true,
+ interpolation: {
+ prefix: '__',
+ suffix: '__'
+ }
+ }, function() {
+ window.jqueryI18next.init(window.i18next, $, {
+ tName: 't',
+ i18nName: 'i18next',
+ handleName: 'localize',
+ });
});
if (jsxc.storage.getItem('debug') === true) {
@@ -303,6 +330,7 @@ jsxc = {
// Looking for a login form
if (!jsxc.isLoginForm()) {
+ jsxc.changeState(jsxc.CONST.STATE.SUSPEND);
if (jsxc.options.displayRosterMinimized()) {
// Show minimized roster
@@ -314,6 +342,8 @@ jsxc = {
return;
}
+ jsxc.changeState(jsxc.CONST.STATE.TRYTOINTERCEPT);
+
if (typeof jsxc.options.formFound === 'function') {
jsxc.options.formFound.call();
}
@@ -335,7 +365,9 @@ jsxc = {
form.off('submit');
// Add jsxc login action to form
- form.submit(function() {
+ form.submit(function(ev) {
+ ev.preventDefault();
+
jsxc.prepareLogin(function(settings) {
if (settings !== false) {
// settings.xmpp.onlogin is deprecated since v2.1.0
@@ -356,9 +388,12 @@ jsxc = {
return false;
});
+ jsxc.changeState(jsxc.CONST.STATE.INTERCEPTED);
+
} else if (!jsxc.isLoginForm() || (jsxc.options.loginForm && jsxc.options.loginForm.ifFound === 'attach')) {
// Restore old connection
+ jsxc.changeState(jsxc.CONST.STATE.PREVCONFOUND);
if (typeof jsxc.storage.getItem('alive') === 'undefined') {
jsxc.onMaster();
@@ -566,14 +601,10 @@ jsxc = {
jsxc.gui.init();
$('#jsxc_roster').removeClass('jsxc_noConnection');
- jsxc.restoreRoster();
- jsxc.restoreWindows();
- jsxc.restoreCompleted = true;
-
jsxc.registerLogout();
jsxc.gui.updateAvatar($('#jsxc_roster > .jsxc_bottom'), jsxc.jidToBid(jsxc.storage.getItem('jid')), 'own');
- $(document).trigger('restoreCompleted.jsxc');
+ jsxc.gui.restore();
},
/**
@@ -787,8 +818,12 @@ jsxc = {
if (form.find('#submit').length > 0) {
form.find('#submit').click();
- } else {
+ } else if (form.get(0) && typeof form.get(0).submit === 'function') {
form.submit();
+ } else if (form.find('[type="submit"]').length > 0) {
+ form.find('[type="submit"]').click();
+ } else {
+ jsxc.warn('Could not submit login form.');
}
},
@@ -928,12 +963,28 @@ jsxc = {
isExtraSmallDevice: function() {
return $(window).width() < 500;
+ },
+
+ changeState: function(state) {
+ jsxc.currentState = state;
+
+ jsxc.debug('State changed to ' + Object.keys(jsxc.CONST.STATE)[state]);
+
+ $(document).trigger('stateChange.jsxc', state);
+ },
+
+ changeUIState: function(state) {
+ jsxc.currentUIState = state;
+
+ jsxc.debug('UI State changed to ' + Object.keys(jsxc.CONST.UISTATE)[state]);
+
+ $(document).trigger('stateUIChange.jsxc', state);
}
};
/**
* Handle XMPP stuff.
- *
+ *
* @namespace jsxc.xmpp
*/
jsxc.xmpp = {
@@ -941,14 +992,14 @@ jsxc.xmpp = {
/**
* Create new connection or attach to old
- *
+ *
* @name login
* @memberOf jsxc.xmpp
* @private
*/
/**
* Create new connection with given parameters.
- *
+ *
* @name login^2
* @param {string} jid
* @param {string} password
@@ -957,7 +1008,7 @@ jsxc.xmpp = {
*/
/**
* Attach connection with given parameters.
- *
+ *
* @name login^3
* @param {string} jid
* @param {string} sid
@@ -1074,6 +1125,8 @@ jsxc.xmpp = {
jsxc.xmpp.conn.caps.node = 'http://jsxc.org/';
}
+ jsxc.changeState(jsxc.CONST.STATE.ESTABLISHING);
+
if (sid && rid) {
jsxc.debug('Try to attach');
jsxc.debug('SID: ' + sid);
@@ -1109,7 +1162,7 @@ jsxc.xmpp = {
/**
* Logs user out of his xmpp session and does some clean up.
- *
+ *
* @param {boolean} complete If set to false, roster will not be removed
* @returns {Boolean}
*/
@@ -1176,7 +1229,7 @@ jsxc.xmpp = {
/**
* Triggered if connection is established
- *
+ *
* @private
*/
connected: function() {
@@ -1204,12 +1257,13 @@ jsxc.xmpp = {
jsxc.xmpp.conn.resume();
jsxc.onMaster();
+ jsxc.changeState(jsxc.CONST.STATE.READY);
$(document).trigger('attached.jsxc');
},
/**
* Triggered if connection is attached
- *
+ *
* @private
*/
attached: function() {
@@ -1217,7 +1271,8 @@ jsxc.xmpp = {
$('#jsxc_roster').removeClass('jsxc_noConnection');
jsxc.xmpp.conn.addHandler(jsxc.xmpp.onRosterChanged, 'jabber:iq:roster', 'iq', 'set');
- jsxc.xmpp.conn.addHandler(jsxc.xmpp.onMessage, null, 'message', 'chat');
+ jsxc.xmpp.conn.addHandler(jsxc.xmpp.onChatMessage, null, 'message', 'chat');
+ jsxc.xmpp.conn.addHandler(jsxc.xmpp.onHeadlineMessage, null, 'message', 'headline');
jsxc.xmpp.conn.addHandler(jsxc.xmpp.onReceived, null, 'message');
jsxc.xmpp.conn.addHandler(jsxc.xmpp.onPresence, null, 'presence');
@@ -1279,17 +1334,15 @@ jsxc.xmpp = {
jsxc.xmpp.sendPres();
if (!jsxc.restoreCompleted) {
- jsxc.restoreRoster();
- jsxc.restoreWindows();
- jsxc.restoreCompleted = true;
-
- $(document).trigger('restoreCompleted.jsxc');
+ jsxc.gui.restore();
}
}
jsxc.xmpp.saveSessionParameter();
jsxc.masterActions();
+
+ jsxc.changeState(jsxc.CONST.STATE.READY);
},
saveSessionParameter: function() {
@@ -1350,7 +1403,7 @@ jsxc.xmpp = {
/**
* Triggered if lost connection
- *
+ *
* @private
*/
disconnected: function() {
@@ -1375,6 +1428,7 @@ jsxc.xmpp = {
if (jsxc.triggeredFromElement) {
$(document).trigger('toggle.roster.jsxc', ['hidden', 0]);
+ jsxc.gui.roster.ready = false;
$('#jsxc_roster').remove();
// REVIEW: logoutElement without href attribute?
@@ -1389,11 +1443,13 @@ jsxc.xmpp = {
jsxc.role_allocation = false;
jsxc.master = false;
jsxc.storage.removeItem('alive');
+
+ jsxc.changeState(jsxc.CONST.STATE.SUSPEND);
},
/**
* Triggered on connection fault
- *
+ *
* @param {String} condition information why we lost the connection
* @private
*/
@@ -1407,7 +1463,7 @@ jsxc.xmpp = {
/**
* Triggered on auth fail.
- *
+ *
* @private
*/
onAuthFail: function() {
@@ -1429,7 +1485,7 @@ jsxc.xmpp = {
/**
* Triggered on initial roster load
- *
+ *
* @param {dom} iq
* @private
*/
@@ -1477,11 +1533,12 @@ jsxc.xmpp = {
jsxc.gui.roster.loaded = true;
jsxc.debug('Roster loaded');
$(document).trigger('cloaded.roster.jsxc');
+ jsxc.changeUIState(jsxc.CONST.UISTATE.READY);
},
/**
* Triggerd on roster changes
- *
+ *
* @param {dom} iq
* @returns {Boolean} True to preserve handler
* @private
@@ -1556,19 +1613,19 @@ jsxc.xmpp = {
/**
* Triggered on incoming presence stanzas
- *
+ *
* @param {dom} presence
* @private
*/
onPresence: function(presence) {
/*
* <presence xmlns='jabber:client' type='unavailable' from='' to=''/>
- *
+ *
* <presence xmlns='jabber:client' from='' to=''> <priority>5</priority>
* <c xmlns='http://jabber.org/protocol/caps'
* node='http://psi-im.org/caps' ver='caps-b75d8d2b25' ext='ca cs
* ep-notify-2 html'/> </presence>
- *
+ *
* <presence xmlns='jabber:client' from='' to=''> <show>chat</show>
* <status></status> <priority>5</priority> <c
* xmlns='http://jabber.org/protocol/caps' node='http://psi-im.org/caps'
@@ -1619,7 +1676,11 @@ jsxc.xmpp = {
jid: jid,
approve: -1
});
- jsxc.notice.add($.t('Friendship_request'), $.t('from') + ' ' + jid, 'gui.showApproveDialog', [jid]);
+ jsxc.notice.add({
+ msg: $.t('Friendship_request'),
+ description: $.t('from') + ' ' + jid,
+ type: 'contact'
+ }, 'gui.showApproveDialog', [jid]);
return true;
} else if (ptype === 'unavailable' || ptype === 'unsubscribed') {
@@ -1635,7 +1696,7 @@ jsxc.xmpp = {
if (status === 0) {
delete res[r];
- } else {
+ } else if (r) {
res[r] = status;
}
@@ -1690,7 +1751,7 @@ jsxc.xmpp = {
jsxc.storage.setUserItem('buddy', bid, data);
jsxc.storage.setUserItem('res', bid, res);
- jsxc.debug('Presence (' + from + '): ' + status);
+ jsxc.debug('Presence (' + from + '): ' + jsxc.CONST.STATUS[status]);
jsxc.gui.update(bid);
jsxc.gui.roster.reorder(bid);
@@ -1703,13 +1764,12 @@ jsxc.xmpp = {
/**
* Triggered on incoming message stanzas
- *
+ *
* @param {dom} presence
* @returns {Boolean}
* @private
*/
- onMessage: function(stanza) {
-
+ onChatMessage: function(stanza) {
var forwarded = $(stanza).find('forwarded[xmlns="' + jsxc.CONST.NS.FORWARD + '"]');
var message, carbon;
@@ -1732,6 +1792,7 @@ jsxc.xmpp = {
}
var body = $(message).find('body:first').text();
+ var htmlBody = $(message).find('body[xmlns="' + Strophe.NS.XHTML + '"]');
if (!body || (body.match(/\?OTR/i) && forwarded)) {
return true;
@@ -1781,7 +1842,10 @@ jsxc.xmpp = {
var chat = jsxc.storage.getUserItem('chat', bid) || [];
if (chat.length === 0) {
- jsxc.notice.add($.t('Unknown_sender'), $.t('You_received_a_message_from_an_unknown_sender') + ' (' + bid + ').', 'gui.showUnknownSender', [bid]);
+ jsxc.notice.add({
+ msg: $.t('Unknown_sender'),
+ description: $.t('You_received_a_message_from_an_unknown_sender') + ' (' + bid + ').'
+ }, 'gui.showUnknownSender', [bid]);
}
var msg = jsxc.removeHTML(body);
@@ -1819,10 +1883,40 @@ jsxc.xmpp = {
}));
}
- if (jsxc.otr.objects.hasOwnProperty(bid)) {
+ var attachment;
+ if (htmlBody.length === 1) {
+ var httpUploadElement = htmlBody.find('a[data-type][data-name][data-size]');
+
+ if (httpUploadElement.length === 1) {
+ attachment = {
+ type: httpUploadElement.attr('data-type'),
+ name: httpUploadElement.attr('data-name'),
+ size: httpUploadElement.attr('data-size'),
+ };
+
+ if (httpUploadElement.attr('data-thumbnail') && httpUploadElement.attr('data-thumbnail').match(/^\s*data:[a-z]+\/[a-z0-9-+.*]+;base64,[a-z0-9=+/]+$/i)) {
+ attachment.thumbnail = httpUploadElement.attr('data-thumbnail');
+ }
+
+ if (httpUploadElement.attr('href') && httpUploadElement.attr('href').match(/^https:\/\//)) {
+ attachment.data = httpUploadElement.attr('href');
+ body = null;
+ }
+
+ if (!attachment.type.match(/^[a-z]+\/[a-z0-9-+.*]+$/i) || !attachment.name.match(/^[\s\w.,-]+$/i) || !attachment.size.match(/^\d+$/i)) {
+ attachment = undefined;
+
+ jsxc.warn('Invalid file type, name or size.');
+ }
+ }
+ }
+
+ if (jsxc.otr.objects.hasOwnProperty(bid) && body) {
+ // @TODO check for file upload url after decryption
jsxc.otr.objects[bid].receiveMsg(body, {
stamp: stamp,
- forwarded: forwarded
+ forwarded: forwarded,
+ attachment: attachment
});
} else {
jsxc.gui.window.postMessage({
@@ -1831,7 +1925,8 @@ jsxc.xmpp = {
msg: body,
encrypted: false,
forwarded: forwarded,
- stamp: stamp
+ stamp: stamp,
+ attachment: attachment
});
}
@@ -1840,8 +1935,40 @@ jsxc.xmpp = {
},
/**
+ * Process message stanzas of type headline.
+ *
+ * @param {String} stanza Message stanza of type headline
+ * @return {Boolean}
+ */
+ onHeadlineMessage: function(stanza) {
+ stanza = $(stanza);
+
+ var from = stanza.attr('from');
+ var domain = Strophe.getDomainFromJid(from);
+
+ if (domain !== from) {
+ if (!jsxc.storage.getUserItem('buddy', jsxc.jidToBid(from))) {
+ return true;
+ }
+ } else if (domain !== Strophe.getDomainFromJid(jsxc.xmpp.conn.jid)) {
+ return true;
+ }
+
+ var subject = stanza.find('subject:first').text() || $.t('Notification');
+ var body = stanza.find('body:first').text();
+
+ jsxc.notice.add({
+ msg: subject,
+ description: body,
+ type: (domain === from) ? 'announcement' : null
+ }, 'gui.showNotification', [subject, body, from]);
+
+ return true;
+ },
+
+ /**
* Triggerd if the rid changed
- *
+ *
* @param {integer} rid next valid request id
* @private
*/
@@ -1851,7 +1978,7 @@ jsxc.xmpp = {
/**
* response to friendship request
- *
+ *
* @param {string} from jid from original friendship req
* @param {boolean} approve
*/
@@ -1872,7 +1999,7 @@ jsxc.xmpp = {
/**
* Add buddy to my friends
- *
+ *
* @param {string} username jid
* @param {string} alias
*/
@@ -1908,7 +2035,7 @@ jsxc.xmpp = {
/**
* Remove buddy from my friends
- *
+ *
* @param {type} jid
*/
removeBuddy: function(jid) {
@@ -1943,39 +2070,63 @@ jsxc.xmpp = {
/**
* Public function to send message.
- *
+ *
* @memberOf jsxc.xmpp
* @param bid css jid of user
* @param msg message
* @param uid unique id
*/
- sendMessage: function(bid, msg, uid) {
- if (jsxc.otr.objects.hasOwnProperty(bid)) {
- jsxc.otr.objects[bid].sendMsg(msg, uid);
+ sendMessage: function(message) {
+ var bid = message.bid;
+ var msg = message.htmlMsg;
+
+ var mucRoomNames = (jsxc.xmpp.conn.muc && jsxc.xmpp.conn.muc.roomNames) ? jsxc.xmpp.conn.muc.roomNames : [];
+ var isMucBid = mucRoomNames.indexOf(bid) >= 0;
+
+ if (jsxc.otr.objects.hasOwnProperty(bid) && !isMucBid) {
+ jsxc.otr.objects[bid].sendMsg(msg, message);
} else {
- jsxc.xmpp._sendMessage(jsxc.gui.window.get(bid).data('jid'), msg, uid);
+ jsxc.xmpp._sendMessage(jsxc.gui.window.get(bid).data('jid'), msg, message);
}
},
/**
* Create message stanza and send it.
- *
+ *
* @memberOf jsxc.xmpp
* @param jid Jabber id
* @param msg Message
* @param uid unique id
* @private
*/
- _sendMessage: function(jid, msg, uid) {
+ _sendMessage: function(jid, msg, message) {
+ // @TODO put jid into message object
var data = jsxc.storage.getUserItem('buddy', jsxc.jidToBid(jid)) || {};
var isBar = (Strophe.getBareJidFromJid(jid) === jid);
var type = data.type || 'chat';
+ message = message || {};
var xmlMsg = $msg({
to: jid,
type: type,
- id: uid
- }).c('body').t(msg);
+ id: message._uid
+ });
+
+ if (message.type === jsxc.Message.HTML) {
+ xmlMsg.c("html", {
+ xmlns: Strophe.NS.XHTML_IM
+ });
+
+ // Omit StropheJS XEP-0071 limitations
+ var body = Strophe.xmlElement("body", {
+ xmlns: Strophe.NS.XHTML
+ });
+ body.innerHTML = msg;
+
+ xmlMsg.node.appendChild(body);
+ } else {
+ xmlMsg.c('body').t(msg);
+ }
if (jsxc.xmpp.carbons.enabled && msg.match(/^\?OTR/)) {
xmlMsg.up().c("private", {
@@ -1990,12 +2141,19 @@ jsxc.xmpp = {
});
}
+ if (jsxc.xmpp.conn.chatstates && !jsxc.xmpp.chatState.isDisabled()) {
+ // send active event (XEP-0085)
+ xmlMsg.up().c('active', {
+ xmlns: Strophe.NS.CHATSTATES
+ });
+ }
+
jsxc.xmpp.conn.send(xmlMsg);
},
/**
* This function loads a vcard.
- *
+ *
* @memberOf jsxc.xmpp
* @param bid
* @param cb
@@ -2019,7 +2177,7 @@ jsxc.xmpp = {
/**
* Retrieves capabilities.
- *
+ *
* @memberOf jsxc.xmpp
* @param jid
* @returns List of known capabilities
@@ -2041,7 +2199,7 @@ jsxc.xmpp = {
/**
* Test if jid has given features
- *
+ *
* @param {string} jid Jabber id
* @param {string[]} feature Single feature or list of features
* @param {Function} cb Called with the result as first param.
@@ -2093,7 +2251,7 @@ jsxc.xmpp = {
/**
* Handle carbons (XEP-0280);
- *
+ *
* @namespace jsxc.xmpp.carbons
*/
jsxc.xmpp.carbons = {
@@ -2101,7 +2259,7 @@ jsxc.xmpp.carbons = {
/**
* Enable carbons.
- *
+ *
* @memberOf jsxc.xmpp.carbons
* @param cb callback
*/
@@ -2127,7 +2285,7 @@ jsxc.xmpp.carbons = {
/**
* Disable carbons.
- *
+ *
* @memberOf jsxc.xmpp.carbons
* @param cb callback
*/
@@ -2153,7 +2311,7 @@ jsxc.xmpp.carbons = {
/**
* Enable/Disable carbons depending on options key.
- *
+ *
* @memberOf jsxc.xmpp.carbons
* @param err error message
*/
@@ -2171,266 +2329,254 @@ jsxc.xmpp.carbons = {
};
/**
- * Load message object with given uid.
- *
- * @class Message
- * @memberOf jsxc
- * @param {string} uid Unified identifier from message object
+ * @namespace jsxc.fileTransfer
+ * @type {Object}
*/
+jsxc.fileTransfer = {};
+
/**
- * Create new message object.
+ * Make bytes more human readable.
*
- * @class Message
- * @memberOf jsxc
- * @param {object} args New message properties
- * @param {string} args.bid
- * @param {direction} args.direction
- * @param {string} args.msg
- * @param {boolean} args.encrypted
- * @param {boolean} args.forwarded
- * @param {boolean} args.sender
- * @param {integer} args.stamp
- * @param {object} args.attachment Attached data
- * @param {string} args.attachment.name File name
- * @param {string} args.attachment.size File size
- * @param {string} args.attachment.type File type
- * @param {string} args.attachment.data File data
+ * @memberOf jsxc.fileTransfer
+ * @param {Integer} byte
+ * @return {String}
*/
+jsxc.fileTransfer.formatByte = function(byte) {
+ var s = ['', 'KB', 'MB', 'GB', 'TB'];
+ var i;
-jsxc.Message = function() {
-
- /** @member {string} */
- this._uid = null;
-
- /** @member {boolean} */
- this._received = false;
-
- /** @member {boolean} */
- this.encrypted = false;
-
- /** @member {boolean} */
- this.forwarded = false;
-
- /** @member {integer} */
- this.stamp = new Date().getTime();
-
- if (typeof arguments[0] === 'string' && arguments[0].length > 0 && arguments.length === 1) {
- this._uid = arguments[0];
-
- this.load(this._uid);
- } else if (typeof arguments[0] === 'object' && arguments[0] !== null) {
- $.extend(this, arguments[0]);
+ for (i = 1; i < s.length; i++) {
+ if (byte < 1024) {
+ break;
+ }
+ byte /= 1024;
}
- if (!this._uid) {
- this._uid = new Date().getTime() + ':msg';
- }
+ return (Math.round(byte * 10) / 10) + s[i - 1];
};
/**
- * Load message properties.
+ * Start file transfer dialog.
*
- * @memberof jsxc.Message
- * @param {string} uid
+ * @memberOf jsxc.fileTransfer
+ * @param {String} jid
*/
-jsxc.Message.prototype.load = function(uid) {
- var data = jsxc.storage.getUserItem('msg', uid);
+jsxc.fileTransfer.startGuiAction = function(jid) {
+ var bid = jsxc.jidToBid(jid);
+ var res = Strophe.getResourceFromJid(jid);
- if (!data) {
- jsxc.debug('Could not load message with uid ' + uid);
+ if (!res && !jsxc.xmpp.httpUpload.ready) {
+ jsxc.fileTransfer.selectResource(bid, jsxc.fileTransfer.startGuiAction);
+
+ return;
}
- $.extend(this, data);
+ jsxc.fileTransfer.showFileSelection(jid);
};
/**
- * Save message properties and create thumbnail.
+ * Show select dialog for file transfer capable resources.
*
- * @memberOf jsxc.Message
- * @return {Message} this object
+ * @memberOf jsxc.fileTransfer
+ * @param {String} bid
+ * @param {Function} success_cb Called if user selects resource
+ * @param {Function} error_cb Called if no resource was found or selected
*/
-jsxc.Message.prototype.save = function() {
- var history;
-
- if (this.bid) {
- history = jsxc.storage.getUserItem('history', this.bid) || [];
+jsxc.fileTransfer.selectResource = function(bid, success_cb, error_cb) {
+ var win = jsxc.gui.window.get(bid);
+ var jid = win.data('jid');
+ var res = Strophe.getResourceFromJid(jid);
+
+ var fileCapableRes = jsxc.webrtc.getCapableRes(jid, jsxc.webrtc.reqFileFeatures);
+ var resources = Object.keys(jsxc.storage.getUserItem('res', bid)) || [];
+
+ if (res === null && resources.length === 1 && fileCapableRes.length === 1) {
+ // only one resource is available and this resource is also capable to receive files
+ res = fileCapableRes[0];
+ jid = bid + '/' + res;
+
+ success_cb(jid);
+ } else if (fileCapableRes.indexOf(res) >= 0) {
+ // currently used resource is capable to receive files
+ success_cb(bid + '/' + res);
+ } else if (fileCapableRes.indexOf(res) < 0) {
+ // show selection dialog
+ jsxc.gui.window.selectResource(bid, $.t('Your_contact_uses_multiple_clients_'), function(data) {
+ if (data.status === 'unavailable') {
+ jsxc.gui.window.hideOverlay(bid);
- if (history.indexOf(this._uid) < 0) {
- if (history.length > jsxc.options.get('numberOfMsg')) {
- jsxc.Message.delete(history.pop());
+ if (typeof error_cb === 'function') {
+ error_cb();
+ }
+ } else if (data.status === 'selected') {
+ success_cb(bid + '/' + data.result);
}
- } else {
- history = null;
- }
+ }, fileCapableRes);
}
+};
- if (Image && this.attachment && this.attachment.type.match(/^image\//i) && this.attachment.data) {
- var sHeight, sWidth, sx, sy;
- var dHeight = 100,
- dWidth = 100;
- var canvas = $("<canvas>").get(0);
+/**
+ * Show file selector.
+ *
+ * @memberOf jsxc.fileTransfer
+ * @param {String} jid
+ */
+jsxc.fileTransfer.showFileSelection = function(jid) {
+ var bid = jsxc.jidToBid(jid);
+ var msg = $('<div><div><label><input type="file" name="files" /><label></div></div>');
+ msg.addClass('jsxc_chatmessage');
- canvas.width = dWidth;
- canvas.height = dHeight;
+ jsxc.gui.window.showOverlay(bid, msg, true);
- var ctx = canvas.getContext("2d");
- var img = new Image();
+ // open file selection for user
+ msg.find('label').click();
- img.src = this.attachment.data;
+ msg.find('[type="file"]').change(function(ev) {
+ var file = ev.target.files[0]; // FileList object
- if (img.height > img.width) {
- sHeight = img.width;
- sWidth = img.width;
- sx = 0;
- sy = (img.height - img.width) / 2;
- } else {
- sHeight = img.height;
- sWidth = img.height;
- sx = (img.width - img.height) / 2;
- sy = 0;
+ if (!file) {
+ return;
}
- ctx.drawImage(img, sx, sy, sWidth, sHeight, 0, 0, dWidth, dHeight);
+ jsxc.fileTransfer.fileSelected(jid, msg, file);
+ });
+};
- this.attachment.thumbnail = canvas.toDataURL();
+/**
+ * Callback for file selector.
+ *
+ * @memberOf jsxc.fileTransfer
+ * @param {String} jid
+ * @param {jQuery} msg jQuery object of temporary file message
+ * @param {File} file selected file
+ */
+jsxc.fileTransfer.fileSelected = function(jid, msg, file) {
+ var bid = jsxc.jidToBid(jid);
- if (this.direction === 'out') {
- // save storage
- this.attachment.data = null;
- }
- }
+ if (file.transportMethod !== 'webrtc' && jsxc.xmpp.httpUpload.ready && file.size > jsxc.options.get('httpUpload').maxSize) {
+ jsxc.debug('File too large for http upload.');
- var data;
+ file.transportMethod = 'webrtc';
- if (this.attachment && this.attachment.size > jsxc.options.maxStorableSize && this.direction === 'in') {
- jsxc.debug('Attachment to large to store');
+ jsxc.fileTransfer.selectResource(bid, function(jid) {
+ jsxc.fileTransfer.fileSelected(jid, msg, file);
+ }, function() {
+ var maxSize = jsxc.fileTransfer.formatByte(jsxc.options.get('httpUpload').maxSize);
+ var fileSize = jsxc.fileTransfer.formatByte(file.size);
- data = this.attachment.data;
- this.attachment.data = null;
- this.attachment.persistent = false;
+ jsxc.gui.window.postMessage({
+ bid: bid,
+ direction: jsxc.Message.SYS,
+ msg: $.t('File_too_large') + ' (' + fileSize + ' > ' + maxSize + ')'
+ });
- //TODO inform user
+ jsxc.gui.window.hideOverlay(bid);
+ });
+
+ return;
+ } else if (!jsxc.xmpp.httpUpload.ready && Strophe.getResourceFromJid(jid)) {
+ // http upload not available
+ file.transportMethod = 'webrtc';
}
- jsxc.storage.setUserItem('msg', this._uid, this);
+ var attachment = $('<div>');
+ attachment.addClass('jsxc_attachment');
+ attachment.addClass('jsxc_' + file.type.replace(/\//, '-'));
+ attachment.addClass('jsxc_' + file.type.replace(/^([^/]+)\/.*/, '$1'));
- if (history) {
- history.unshift(this._uid);
+ msg.empty().append(attachment);
- jsxc.storage.setUserItem('history', this.bid, history);
- }
+ if (FileReader && file.type.match(/^image\//)) {
+ // show image preview
+ var img = $('<img alt="preview">').attr('title', file.name);
+ img.attr('src', jsxc.options.get('root') + '/img/loading.gif');
+ img.appendTo(attachment);
- if (data && this.attachment) {
- this.attachment.data = data;
+ var reader = new FileReader();
+
+ reader.onload = function() {
+ img.attr('src', reader.result);
+ };
+
+ reader.readAsDataURL(file);
+ } else {
+ attachment.text(file.name + ' (' + file.size + ' byte)');
}
- return this;
-};
+ $('<button>').addClass('jsxc_btn jsxc_btn-primary').text($.t('Send')).click(function() {
+ // user confirmed file transfer
+ jsxc.gui.window.hideOverlay(bid);
+ msg.remove();
-/**
- * Remove object from storage.
- *
- * @memberOf jsxc.Message
- */
-jsxc.Message.prototype.delete = function() {
- jsxc.Message.delete(this._uid);
-};
+ var message = jsxc.gui.window.postMessage({
+ bid: bid,
+ direction: 'out',
+ attachment: {
+ name: file.name,
+ size: file.size,
+ type: file.type,
+ data: (file.type.match(/^image\//)) ? img.attr('src') : null
+ }
+ });
-/**
- * Returns object as jquery object.
- *
- * @memberOf jsxc.Message
- * @return {jQuery} Representation in DOM
- */
-jsxc.Message.prototype.getDOM = function() {
- return jsxc.Message.getDOM(this._uid);
-};
+ if (file.transportMethod === 'webrtc') {
+ var sess = jsxc.webrtc.sendFile(jid, file);
-/**
- * Mark message as received.
- *
- * @memberOf jsxc.Message
- */
-jsxc.Message.prototype.received = function() {
- this._received = true;
- this.save();
+ sess.sender.on('progress', function(sent, size) {
+ jsxc.gui.window.updateProgress(message, sent, size);
- this.getDOM().addClass('jsxc_received');
-};
+ if (sent === size) {
+ message.received();
+ }
+ });
+ } else {
+ // progress is updated in xmpp.httpUpload.uploadFile
+ jsxc.xmpp.httpUpload.sendFile(file, message);
+ }
+ }).appendTo(msg);
-/**
- * Returns true if the message was already received.
- *
- * @memberOf jsxc.Message
- * @return {boolean} true means received
- */
-jsxc.Message.prototype.isReceived = function() {
- return this._received;
+ $('<button>').addClass('jsxc_btn jsxc_btn-default').text($.t('Abort')).click(function() {
+ // user aborted file transfer
+ jsxc.gui.window.hideOverlay(bid);
+ }).appendTo(msg);
};
/**
- * Remove message with uid.
+ * Enable/disable icons for file transfer.
*
- * @memberOf jsxc.Message
- * @static
- * @param {string} uid message uid
+ * @memberOf jsxc.fileTransfer
+ * @param {String} bid
*/
-jsxc.Message.delete = function(uid) {
- var data = jsxc.storage.getUserItem('msg', uid);
+jsxc.fileTransfer.updateIcons = function(bid) {
+ var win = jsxc.gui.window.get(bid);
- if (data) {
- jsxc.storage.removeUserItem('msg', uid);
+ if (!win || win.length === 0 || !jsxc.xmpp.conn) {
+ return;
+ }
- if (data.bid) {
- var history = jsxc.storage.getUserItem('history', data.bid) || [];
+ jsxc.debug('Update file transfer icons for ' + bid);
- history = $.grep(history, function(el) {
- return el !== uid;
- });
+ if (jsxc.xmpp.httpUpload.ready) {
+ win.find('.jsxc_sendFile').removeClass('jsxc_disabled');
- jsxc.storage.setUserItem('history', data.bid);
- }
+ return;
}
-};
-
-/**
- * Returns message object as jquery object.
- *
- * @memberOf jsxc.Message
- * @static
- * @param {string} uid message uid
- * @return {jQuery} jQuery representation in DOM
- */
-jsxc.Message.getDOM = function(uid) {
- return $('#' + uid.replace(/:/g, '-'));
-};
-
-/**
- * Message direction can be incoming, outgoing or system.
- *
- * @typedef {(jsxc.Message.IN|jsxc.Message.OUT|jsxc.Message.SYS)} direction
- */
-/**
- * @constant
- * @type {string}
- * @default
- */
-jsxc.Message.IN = 'in';
+ var jid = win.data('jid');
+ var res = Strophe.getResourceFromJid(jid);
+ var fileCapableRes = jsxc.webrtc.getCapableRes(bid, jsxc.webrtc.reqFileFeatures);
+ var resources = Object.keys(jsxc.storage.getUserItem('res', bid) || {}) || [];
-/**
- * @constant
- * @type {string}
- * @default
- */
-jsxc.Message.OUT = 'out';
+ if (fileCapableRes.indexOf(res) > -1 || (res === null && fileCapableRes.length === 1 && resources.length === 1)) {
+ win.find('.jsxc_sendFile').removeClass('jsxc_disabled');
+ } else {
+ win.find('.jsxc_sendFile').addClass('jsxc_disabled');
+ }
+};
-/**
- * @constant
- * @type {string}
- * @default
- */
-jsxc.Message.SYS = 'sys';
+$(document).on('update.gui.jsxc', function(ev, bid) {
+ jsxc.fileTransfer.updateIcons(bid);
+});
/* global Favico, emojione*/
/**
@@ -2477,7 +2623,8 @@ jsxc.gui = {
':jabber:': ['jabber'],
':xmpp:': ['xmpp'],
':jsxc:': ['jsxc'],
- ':owncloud:': ['owncloud']
+ ':owncloud:': ['owncloud'],
+ ':nextcloud:': ['nextcloud']
},
'emojione': emojione.emojioneList
},
@@ -2535,6 +2682,8 @@ jsxc.gui = {
return;
}
+ jsxc.changeUIState(jsxc.CONST.UISTATE.INITIATING);
+
jsxc.gui.regShortNames = new RegExp(emojione.regShortNames.source + '|(' + Object.keys(jsxc.gui.emoticonList.core).join('|') + ')', 'gi');
$('body').append($(jsxc.gui.template.get('windowList')));
@@ -2670,6 +2819,8 @@ jsxc.gui = {
ri.find('.jsxc_name').attr('title', info);
jsxc.gui.updateAvatar(ri.add(we.find('.jsxc_bar')), data.jid, data.avatar);
+
+ $(document).trigger('update.gui.jsxc', [bid]);
},
/**
@@ -2860,9 +3011,15 @@ jsxc.gui = {
* Creates and show loginbox
*/
showLoginBox: function() {
- // Set focus to password field
- $(document).on("complete.dialog.jsxc", function() {
- $('#jsxc_password').focus();
+ // Set focus to username or password field
+ $(document).one("complete.dialog.jsxc", function() {
+ setTimeout(function() {
+ if ($("#jsxc_username").val().length === 0) {
+ $("#jsxc_username").focus();
+ } else {
+ $('#jsxc_password').focus();
+ }
+ }, 50);
});
jsxc.gui.dialog.open(jsxc.gui.template.get('loginBox'));
@@ -3096,6 +3253,7 @@ jsxc.gui = {
if (val !== '') {
jsxc.options.getUsers.call(this, val, function(list) {
+ $('#jsxc_userlist').empty();
$.each(list || {}, function(uid, displayname) {
var option = $('<option>');
option.attr('data-username', uid);
@@ -3618,6 +3776,26 @@ jsxc.gui = {
},
/**
+ * Show notification dialog.
+ *
+ * @param {String} subject
+ * @param {String} body
+ * @param {String} from
+ */
+ showNotification: function(subject, body, from) {
+ var dialog = jsxc.gui.dialog.open(jsxc.gui.template.get('notification'));
+
+ dialog.find('h3').text(subject);
+ dialog.find('.jsxc_msg').text(body);
+
+ if (from) {
+ dialog.find('.jsxc_meta').text($.t('from') + ' ' + from);
+ } else {
+ dialog.find('.jsxc_meta').hide();
+ }
+ },
+
+ /**
* Change own presence to pres.
*
* @memberOf jsxc.gui
@@ -3880,7 +4058,7 @@ jsxc.gui = {
filename = jsxc.gui.emoticonList.core[shortname][jsxc.gui.emoticonList.core[shortname].length - 1].replace(/^:([^:]+):$/, '$1');
src = jsxc.options.root + '/img/emotions/' + filename + '.svg';
} else if (jsxc.gui.emoticonList.emojione[shortname]) {
- filename = jsxc.gui.emoticonList.emojione[shortname][jsxc.gui.emoticonList.emojione[shortname].length - 1];
+ filename = jsxc.gui.emoticonList.emojione[shortname].fname;
src = jsxc.options.root + '/lib/emojione/assets/svg/' + filename + '.svg';
}
@@ -3893,7 +4071,22 @@ jsxc.gui = {
return div.prop('outerHTML');
});
+ var obj = $('<div>' + str + '</div>');
+ if (obj.find('.jsxc_emoticon').length === 1 && obj.text().replace(/ /, '').length === 0 && obj.find('*').length === 1) {
+ obj.find('.jsxc_emoticon').addClass('jsxc_large');
+ str = obj.prop('outerHTML');
+ }
+
return str;
+ },
+
+ restore: function() {
+ jsxc.restoreRoster();
+ jsxc.restoreWindows();
+ jsxc.restoreCompleted = true;
+
+ $(document).trigger('restoreCompleted.jsxc');
+ jsxc.changeUIState(jsxc.CONST.UISTATE.READY);
}
};
@@ -4470,6 +4663,9 @@ jsxc.gui.window = {
};
win.find('.jsxc_more').click(expandClick);
+ win.find('.jsxc_menu').click(function() {
+ $('body').click();
+ });
win.find('.jsxc_verification').click(function() {
jsxc.gui.showVerification(bid);
@@ -4505,11 +4701,19 @@ jsxc.gui.window = {
return false;
});
+ var textinputBlurTimeout;
win.find('.jsxc_textinput').keyup(function(ev) {
var body = $(this).val();
- if (ev.which === 13) {
+ // I'm composing a message
+ if (ev.which !== 13) {
+ jsxc.xmpp.chatState.startComposing(bid);
+ }
+
+ if (ev.which === 13 && !ev.shiftKey) {
body = '';
+
+ jsxc.xmpp.chatState.endComposing(bid);
}
jsxc.storage.updateUserItem('window', bid, 'text', body);
@@ -4518,7 +4722,8 @@ jsxc.gui.window = {
jsxc.gui.window.close(bid);
}
}).keypress(function(ev) {
- if (ev.which !== 13 || !$(this).val()) {
+ if (ev.which !== 13 || ev.shiftKey || !$(this).val()) {
+ resizeTextarea.call(this);
return;
}
@@ -4528,16 +4733,40 @@ jsxc.gui.window = {
msg: $(this).val()
});
- $(this).val('');
+ $(this).css('height', '').val('');
+
+ ev.preventDefault();
}).focus(function() {
+ if (textinputBlurTimeout) {
+ clearTimeout(textinputBlurTimeout);
+ }
+
// remove unread flag
jsxc.gui.readMsg(bid);
+
+ resizeTextarea.call(this);
+ }).blur(function() {
+ var self = $(this);
+
+ textinputBlurTimeout = setTimeout(function() {
+ self.css('height', '');
+ }, 1200);
}).mouseenter(function() {
$('#jsxc_windowList').data('isOver', true);
}).mouseleave(function() {
$('#jsxc_windowList').data('isOver', false);
});
+ function resizeTextarea() {
+ if (!$(this).data('originalHeight')) {
+ $(this).data('originalHeight', $(this).outerHeight());
+ }
+ // compensate rounding error
+ if ($(this).outerHeight() < (this.scrollHeight - 1) && $(this).val()) {
+ $(this).height($(this).data('originalHeight') * 1.5);
+ }
+ }
+
win.find('.jsxc_textarea').click(function() {
// check if user clicks element or selects text
if (typeof getSelection === 'function' && !getSelection().toString()) {
@@ -4597,8 +4826,8 @@ jsxc.gui.window = {
li.append(jsxc.gui.shortnameToImage(':' + val[1] + ':'));
li.find('div').attr('title', ins);
li.click(function() {
- win.find('input').val(win.find('input').val() + ins);
- win.find('input').focus();
+ win.find('.jsxc_textinput').val(win.find('.jsxc_textinput').val() + ins);
+ win.find('.jsxc_textinput').focus();
});
win.find('.jsxc_emoticons ul').prepend(li);
});
@@ -4951,7 +5180,10 @@ jsxc.gui.window = {
}
var data = jsxc.storage.getUserItem('buddy', message.bid);
- var html_msg = message.msg;
+
+ if (!message.htmlMsg && message.msg) {
+ message.htmlMsg = message.msg;
+ }
// remove html tags and reencode html tags
message.msg = jsxc.removeHTML(message.msg);
@@ -4969,7 +5201,7 @@ jsxc.gui.window = {
message.msg = $.t('unencrypted_message_received') + ' ' + message.msg;
}
- message.encrypted = message.encrypted || data.msgstate === OTR.CONST.MSGSTATE_ENCRYPTED;
+ message.encrypted = (typeof message.encrypted === 'boolean') ? message.encrypted : data.msgstate === OTR.CONST.MSGSTATE_ENCRYPTED;
try {
message.save();
@@ -4985,11 +5217,11 @@ jsxc.gui.window = {
if (message.direction === 'in' && !jsxc.gui.window.get(message.bid).find('.jsxc_textinput').is(":focus")) {
jsxc.gui.unreadMsg(message.bid);
- $(document).trigger('postmessagein.jsxc', [message.bid, html_msg]);
+ $(document).trigger('postmessagein.jsxc', [message.bid, message.htmlMsg]);
}
- if (message.direction === jsxc.Message.OUT && jsxc.master && message.forwarded !== true && html_msg) {
- jsxc.xmpp.sendMessage(message.bid, html_msg, message._uid);
+ if (message.direction === jsxc.Message.OUT && jsxc.master && message.forwarded !== true && message.htmlMsg) {
+ jsxc.xmpp.sendMessage(message);
}
jsxc.gui.window._postMessage(message);
@@ -5060,6 +5292,12 @@ jsxc.gui.window = {
// replace line breaks
msg = msg.replace(/(\r\n|\r|\n)/g, '<br />');
+ // replace /me command (XEP-0245)
+ var bidData = jsxc.storage.getUserItem('buddy', bid) || {};
+ if (direction === 'in') {
+ msg = msg.replace(/^\/me /, '<i title="/me">' + jsxc.removeHTML(bidData.name || bid) + '</i> ');
+ }
+
var msgDiv = $("<div>"),
msgTsDiv = $("<div>");
msgDiv.addClass('jsxc_chatmessage jsxc_' + direction);
@@ -5070,16 +5308,30 @@ jsxc.gui.window = {
if (message.isReceived() || false) {
msgDiv.addClass('jsxc_received');
+ } else {
+ msgDiv.removeClass('jsxc_received');
}
if (message.forwarded) {
msgDiv.addClass('jsxc_forwarded');
+ } else {
+ msgDiv.removeClass('jsxc_forwarded');
}
if (message.encrypted) {
msgDiv.addClass('jsxc_encrypted');
+ } else {
+ msgDiv.removeClass('jsxc_encrypted');
+ }
+
+ if (message.error) {
+ msgDiv.addClass('jsxc_error');
+ } else {
+ msgDiv.removeClass('jsxc_error');
}
+ msgDiv.attr('title', message.error);
+
if (message.attachment && message.attachment.name) {
var attachment = $('<div>');
attachment.addClass('jsxc_attachment');
@@ -5104,6 +5356,10 @@ jsxc.gui.window = {
attachment = $('<a>').append(attachment);
attachment.attr('href', message.attachment.data);
attachment.attr('download', message.attachment.name);
+
+ if (message.attachment.data === message.msg) {
+ msgDiv.find('div').first().empty();
+ }
}
msgDiv.find('div').first().append(attachment);
@@ -5268,8 +5524,6 @@ jsxc.gui.window = {
if (sent === size) {
span.remove();
-
- message.received();
}
},
@@ -5370,99 +5624,7 @@ jsxc.gui.window = {
},
sendFile: function(jid) {
- var bid = jsxc.jidToBid(jid);
- var win = jsxc.gui.window.get(bid);
- var res = Strophe.getResourceFromJid(jid);
-
- if (!res) {
- jid = win.data('jid');
- res = Strophe.getResourceFromJid(jid);
-
- var fileCapableRes = jsxc.webrtc.getCapableRes(jid, jsxc.webrtc.reqFileFeatures);
- var resources = Object.keys(jsxc.storage.getUserItem('res', bid)) || [];
-
- if (res === null && resources.length === 1 && fileCapableRes.length === 1) {
- res = fileCapableRes[0];
- jid = bid + '/' + res;
- } else if (fileCapableRes.indexOf(res) < 0) {
- jsxc.gui.window.selectResource(bid, $.t('Your_contact_uses_multiple_clients_'), function(data) {
- if (data.status === 'unavailable') {
- jsxc.gui.window.hideOverlay(bid);
- } else if (data.status === 'selected') {
- jsxc.gui.window.sendFile(bid + '/' + data.result);
- }
- }, fileCapableRes);
-
- return;
- }
- }
-
- var msg = $('<div><div><label><input type="file" name="files" /><label></div></div>');
- msg.addClass('jsxc_chatmessage');
-
- jsxc.gui.window.showOverlay(bid, msg, true);
-
- msg.find('label').click();
-
- msg.find('[type="file"]').change(function(ev) {
- var file = ev.target.files[0]; // FileList object
-
- if (!file) {
- return;
- }
-
- var attachment = $('<div>');
- attachment.addClass('jsxc_attachment');
- attachment.addClass('jsxc_' + file.type.replace(/\//, '-'));
- attachment.addClass('jsxc_' + file.type.replace(/^([^/]+)\/.*/, '$1'));
-
- msg.empty().append(attachment);
-
- if (FileReader && file.type.match(/^image\//)) {
- var img = $('<img alt="preview">').attr('title', file.name);
- img.attr('src', jsxc.options.get('root') + '/img/loading.gif');
- img.appendTo(attachment);
-
- var reader = new FileReader();
-
- reader.onload = function() {
- img.attr('src', reader.result);
- };
-
- reader.readAsDataURL(file);
- } else {
- attachment.text(file.name + ' (' + file.size + ' byte)');
- }
-
- $('<button>').addClass('jsxc_btn jsxc_btn-primary').text($.t('Send')).click(function() {
- var sess = jsxc.webrtc.sendFile(jid, file);
-
- jsxc.gui.window.hideOverlay(bid);
-
- var message = jsxc.gui.window.postMessage({
- _uid: sess.sid + ':msg',
- bid: bid,
- direction: 'out',
- attachment: {
- name: file.name,
- size: file.size,
- type: file.type,
- data: (file.type.match(/^image\//)) ? img.attr('src') : null
- }
- });
-
- sess.sender.on('progress', function(sent, size) {
- jsxc.gui.window.updateProgress(message, sent, size);
- });
-
- msg.remove();
-
- }).appendTo(msg);
-
- $('<button>').addClass('jsxc_btn jsxc_btn-default').text($.t('Abort')).click(function() {
- jsxc.gui.window.hideOverlay(bid);
- }).appendTo(msg);
- });
+ jsxc.fileTransfer.startGuiAction(jid);
}
};
@@ -5496,7 +5658,7 @@ jsxc.gui.template.get = function(name, bid, msg) {
$.extend(ph, {
bid_priv_fingerprint: (data && data.fingerprint) ? data.fingerprint.replace(/(.{8})/g, '$1 ') : $.t('not_available'),
bid_jid: bid,
- bid_name: (data && data.name) ? data.name : bid
+ bid_name: (data && data.name) ? jsxc.escapeHTML(data.name) : bid
});
}
@@ -5526,7 +5688,7 @@ jsxc.gui.template.get = function(name, bid, msg) {
}
});
- ret.i18n();
+ ret.localize(ph);
return ret;
}
@@ -5536,6 +5698,274 @@ jsxc.gui.template.get = function(name, bid, msg) {
};
/**
+ * Load message object with given uid.
+ *
+ * @class Message
+ * @memberOf jsxc
+ * @param {string} uid Unified identifier from message object
+ */
+/**
+ * Create new message object.
+ *
+ * @class Message
+ * @memberOf jsxc
+ * @param {object} args New message properties
+ * @param {string} args.bid
+ * @param {direction} args.direction
+ * @param {string} args.msg
+ * @param {boolean} args.encrypted
+ * @param {boolean} args.forwarded
+ * @param {boolean} args.sender
+ * @param {integer} args.stamp
+ * @param {object} args.attachment Attached data
+ * @param {string} args.attachment.name File name
+ * @param {string} args.attachment.size File size
+ * @param {string} args.attachment.type File type
+ * @param {string} args.attachment.data File data
+ */
+
+jsxc.Message = function() {
+
+ /** @member {string} */
+ this._uid = null;
+
+ /** @member {boolean} */
+ this._received = false;
+
+ /** @member {boolean} */
+ this.encrypted = null;
+
+ /** @member {boolean} */
+ this.forwarded = false;
+
+ /** @member {integer} */
+ this.stamp = new Date().getTime();
+
+ this.type = jsxc.Message.PLAIN;
+
+ if (typeof arguments[0] === 'string' && arguments[0].length > 0 && arguments.length === 1) {
+ this._uid = arguments[0];
+
+ this.load(this._uid);
+ } else if (typeof arguments[0] === 'object' && arguments[0] !== null) {
+ $.extend(this, arguments[0]);
+ }
+
+ if (!this._uid) {
+ this._uid = new Date().getTime() + ':msg';
+ }
+};
+
+/**
+ * Load message properties.
+ *
+ * @memberof jsxc.Message
+ * @param {string} uid
+ */
+jsxc.Message.prototype.load = function(uid) {
+ var data = jsxc.storage.getUserItem('msg', uid);
+
+ if (!data) {
+ jsxc.debug('Could not load message with uid ' + uid);
+ }
+
+ $.extend(this, data);
+};
+
+/**
+ * Save message properties and create thumbnail.
+ *
+ * @memberOf jsxc.Message
+ * @return {Message} this object
+ */
+jsxc.Message.prototype.save = function() {
+ var history;
+
+ if (this.bid) {
+ history = jsxc.storage.getUserItem('history', this.bid) || [];
+
+ if (history.indexOf(this._uid) < 0) {
+ if (history.length > jsxc.options.get('numberOfMsg')) {
+ jsxc.Message.delete(history.pop());
+ }
+ } else {
+ history = null;
+ }
+ }
+
+ if (Image && this.attachment && this.attachment.type.match(/^image\//i) && this.attachment.data && !this.attachment.thumbnail) {
+ var sHeight, sWidth, sx, sy;
+ var dHeight = 100,
+ dWidth = 100;
+ var canvas = $("<canvas>").get(0);
+
+ canvas.width = dWidth;
+ canvas.height = dHeight;
+
+ var ctx = canvas.getContext("2d");
+ var img = new Image();
+
+ img.src = this.attachment.data;
+
+ if (img.height > img.width) {
+ sHeight = img.width;
+ sWidth = img.width;
+ sx = 0;
+ sy = (img.height - img.width) / 2;
+ } else {
+ sHeight = img.height;
+ sWidth = img.height;
+ sx = (img.width - img.height) / 2;
+ sy = 0;
+ }
+
+ ctx.drawImage(img, sx, sy, sWidth, sHeight, 0, 0, dWidth, dHeight);
+
+ this.attachment.thumbnail = canvas.toDataURL();
+
+ if (this.direction === 'out') {
+ // save storage
+ this.attachment.data = null;
+ }
+ }
+
+ var data;
+
+ if (this.attachment && this.attachment.size > jsxc.options.maxStorableSize && this.direction === 'in') {
+ jsxc.debug('Attachment to large to store');
+
+ data = this.attachment.data;
+ this.attachment.data = null;
+ this.attachment.persistent = false;
+
+ //TODO inform user
+ }
+
+ jsxc.storage.setUserItem('msg', this._uid, this);
+
+ if (history) {
+ history.unshift(this._uid);
+
+ jsxc.storage.setUserItem('history', this.bid, history);
+ }
+
+ if (data && this.attachment) {
+ this.attachment.data = data;
+ }
+
+ return this;
+};
+
+/**
+ * Remove object from storage.
+ *
+ * @memberOf jsxc.Message
+ */
+jsxc.Message.prototype.delete = function() {
+ jsxc.Message.delete(this._uid);
+};
+
+/**
+ * Returns object as jquery object.
+ *
+ * @memberOf jsxc.Message
+ * @return {jQuery} Representation in DOM
+ */
+jsxc.Message.prototype.getDOM = function() {
+ return jsxc.Message.getDOM(this._uid);
+};
+
+/**
+ * Mark message as received.
+ *
+ * @memberOf jsxc.Message
+ */
+jsxc.Message.prototype.received = function() {
+ this._received = true;
+ this.save();
+
+ this.getDOM().addClass('jsxc_received');
+};
+
+/**
+ * Returns true if the message was already received.
+ *
+ * @memberOf jsxc.Message
+ * @return {boolean} true means received
+ */
+jsxc.Message.prototype.isReceived = function() {
+ return this._received;
+};
+
+/**
+ * Remove message with uid.
+ *
+ * @memberOf jsxc.Message
+ * @static
+ * @param {string} uid message uid
+ */
+jsxc.Message.delete = function(uid) {
+ var data = jsxc.storage.getUserItem('msg', uid);
+
+ if (data) {
+ jsxc.storage.removeUserItem('msg', uid);
+
+ if (data.bid) {
+ var history = jsxc.storage.getUserItem('history', data.bid) || [];
+
+ history = $.grep(history, function(el) {
+ return el !== uid;
+ });
+
+ jsxc.storage.setUserItem('history', data.bid, history);
+ }
+ }
+};
+
+/**
+ * Returns message object as jquery object.
+ *
+ * @memberOf jsxc.Message
+ * @static
+ * @param {string} uid message uid
+ * @return {jQuery} jQuery representation in DOM
+ */
+jsxc.Message.getDOM = function(uid) {
+ return $('#' + uid.replace(/:/g, '-'));
+};
+
+/**
+ * Message direction can be incoming, outgoing or system.
+ *
+ * @typedef {(jsxc.Message.IN|jsxc.Message.OUT|jsxc.Message.SYS)} direction
+ */
+
+/**
+ * @constant
+ * @type {string}
+ * @default
+ */
+jsxc.Message.IN = 'in';
+
+/**
+ * @constant
+ * @type {string}
+ * @default
+ */
+jsxc.Message.OUT = 'out';
+
+/**
+ * @constant
+ * @type {string}
+ * @default
+ */
+jsxc.Message.SYS = 'sys';
+
+jsxc.Message.HTML = 'html';
+
+jsxc.Message.PLAIN = 'plain';
+
+/**
* Implements Multi-User Chat (XEP-0045).
*
* @namespace jsxc.muc
@@ -5626,6 +6056,10 @@ jsxc.muc = {
$(document).one('ready.roster.jsxc', jsxc.muc.initMenu);
}
+ // remove maybe previously attached handlers
+ $(document).off('presence.jsxc', jsxc.muc.onPresence);
+ $(document).off('error.presence.jsxc', jsxc.muc.onPresenceError);
+
$(document).on('presence.jsxc', jsxc.muc.onPresence);
$(document).on('error.presence.jsxc', jsxc.muc.onPresenceError);
@@ -5658,6 +6092,8 @@ jsxc.muc = {
var self = jsxc.muc;
var dialog = jsxc.gui.dialog.open(jsxc.gui.template.get('joinChat'));
+ // @TODO split this monster function apart
+
// hide second step button
dialog.find('.jsxc_join').hide();
@@ -5672,7 +6108,27 @@ jsxc.muc = {
}
// display conference server
+ var serverInputTimeout;
dialog.find('#jsxc_server').val(jsxc.options.get('muc').server);
+ dialog.find('#jsxc_server').on('input', function() {
+ var self = $(this);
+
+ if (serverInputTimeout) {
+ clearTimeout(serverInputTimeout);
+ dialog.find('.jsxc_inputinfo.jsxc_room').hide();
+ }
+
+ dialog.find('.jsxc_inputinfo.jsxc_server').hide().text('');
+ dialog.find('#jsxc_server').removeClass('jsxc_invalid');
+
+ if (self.val() && self.val().match(/^[.-0-9a-zA-Z]+$/i)) {
+ dialog.find('.jsxc_inputinfo.jsxc_room').show().addClass('jsxc_waiting');
+
+ serverInputTimeout = setTimeout(function() {
+ loadRoomList(self.val());
+ }, 1800);
+ }
+ }).trigger('input');
// handle error response
var error_handler = function(event, condition, room) {
@@ -5720,7 +6176,7 @@ jsxc.muc = {
delete self.conn.muc.rooms[room];
}
- dialog.find('.jsxc_warning').text(msg);
+ $('<p>').addClass('jsxc_warning').text(msg).appendTo(dialog.find('.jsxc_msg'));
};
$(document).on('error.muc.jsxc', error_handler);
@@ -5729,42 +6185,6 @@ jsxc.muc = {
$(document).off('error.muc.jsxc', error_handler);
});
- // load room list
- self.conn.muc.listRooms(jsxc.options.get('muc').server, function(stanza) {
- // workaround: chrome does not display dropdown arrow for dynamically filled datalists
- $('#jsxc_roomlist option:last').remove();
-
- $(stanza).find('item').each(function() {
- var r = $('<option>');
- var rjid = $(this).attr('jid').toLowerCase();
- var rnode = Strophe.getNodeFromJid(rjid);
- var rname = $(this).attr('name') || rnode;
-
- r.text(rname);
- r.attr('data-jid', rjid);
- r.attr('value', rnode);
-
- $('#jsxc_roomlist select').append(r);
- });
-
- var set = $(stanza).find('set[xmlns="http://jabber.org/protocol/rsm"]');
-
- if (set.length > 0) {
- var count = set.find('count').text() || '?';
-
- dialog.find('.jsxc_inputinfo').removeClass('jsxc_waiting').text($.t('Could_load_only', {
- count: count
- }));
- } else {
- dialog.find('.jsxc_inputinfo').hide();
- }
- }, function() {
- jsxc.warn('Could not load rooms');
-
- // room autocompletion is a comfort feature, so it is not necessary to inform the user
- dialog.find('.jsxc_inputinfo').hide();
- });
-
dialog.find('#jsxc_nickname').attr('placeholder', Strophe.getNodeFromJid(self.conn.jid));
dialog.find('#jsxc_bookmark').change(function() {
@@ -5782,7 +6202,7 @@ jsxc.muc = {
var room = ($('#jsxc_room').val()) ? jsxc.jidToBid($('#jsxc_room').val()) : null;
var nickname = $('#jsxc_nickname').val() || Strophe.getNodeFromJid(self.conn.jid);
- var password = $('#jsxc_password').val() || null;
+ var server = dialog.find('#jsxc_server').val();
if (!room || !room.match(/^[^"&\'\/:<>@\s]+$/i)) {
$('#jsxc_room').addClass('jsxc_invalid').keyup(function() {
@@ -5793,8 +6213,12 @@ jsxc.muc = {
return false;
}
+ if (dialog.find('#jsxc_server').hasClass('jsxc_invalid')) {
+ return false;
+ }
+
if (!room.match(/@(.*)$/)) {
- room += '@' + jsxc.options.get('muc').server;
+ room += '@' + server;
}
if (jsxc.xmpp.conn.muc.roomNames.indexOf(room) < 0) {
@@ -5816,6 +6240,7 @@ jsxc.muc = {
var bookmark = $("#jsxc_bookmark").prop("checked");
var autojoin = $('#jsxc_autojoin').prop('checked');
+ var password = $('#jsxc_password').val() || null;
// clean up
jsxc.gui.window.clear(room);
@@ -5838,12 +6263,18 @@ jsxc.muc = {
$(stanza).find('feature').each(function() {
var feature = $(this).attr('var');
- if (feature !== '' && i18n.exists(feature)) {
+ if (feature !== '' && i18next.exists(feature)) {
var tr = $('<tr>');
$('<td>').text($.t(feature + '.keyword')).appendTo(tr);
$('<td>').text($.t(feature + '.description')).appendTo(tr);
tr.appendTo(table);
}
+
+ if (feature === 'muc_passwordprotected') {
+ dialog.find('#jsxc_password').parents('.form-group').removeClass('jsxc_hidden');
+ dialog.find('#jsxc_password').attr('required', 'required');
+ dialog.find('#jsxc_password').addClass('jsxc_invalid');
+ }
});
dialog.find('.jsxc_msg').append(table);
@@ -5861,7 +6292,7 @@ jsxc.muc = {
discoReceived();
});
} else {
- dialog.find('.jsxc_warning').text($.t('You_already_joined_this_room'));
+ $('<p>').addClass('jsxc_warning').text($.t('You_already_joined_this_room')).appendTo(dialog.find('.jsxc_msg'));
}
return false;
@@ -5872,12 +6303,15 @@ jsxc.muc = {
if (ev.which !== 13) {
// reset messages and room information
- dialog.find('.jsxc_warning').empty();
+ dialog.find('.jsxc_warning').remove();
- if (dialog.find('.jsxc_continue').is(":hidden")) {
+ if (dialog.find('.jsxc_continue').is(":hidden") && $(this).attr('id') !== 'jsxc_password') {
dialog.find('.jsxc_continue').show();
dialog.find('.jsxc_join').hide().off('click');
dialog.find('.jsxc_msg').empty();
+ dialog.find('#jsxc_password').parents('.form-group').addClass('jsxc_hidden');
+ dialog.find('#jsxc_password').attr('required', '');
+ dialog.find('#jsxc_password').removeClass('jsxc_invalid');
jsxc.gui.dialog.resize();
}
@@ -5890,6 +6324,58 @@ jsxc.muc = {
dialog.find('.jsxc_join').click();
}
});
+
+ function loadRoomList(server) {
+ if (!server) {
+ dialog.find('.jsxc_inputinfo').hide();
+
+ return;
+ }
+
+ // load room list
+ self.conn.muc.listRooms(server, function(stanza) {
+ // workaround: chrome does not display dropdown arrow for dynamically filled datalists
+ $('#jsxc_roomlist option:last').remove();
+
+ $(stanza).find('item').each(function() {
+ var r = $('<option>');
+ var rjid = $(this).attr('jid').toLowerCase();
+ var rnode = Strophe.getNodeFromJid(rjid);
+ var rname = $(this).attr('name') || rnode;
+
+ r.text(rname);
+ r.attr('data-jid', rjid);
+ r.attr('value', rnode);
+
+ $('#jsxc_roomlist select').append(r);
+ });
+
+ var set = $(stanza).find('set[xmlns="http://jabber.org/protocol/rsm"]');
+
+ if (set.length > 0) {
+ var count = set.find('count').text() || '?';
+
+ dialog.find('.jsxc_inputinfo').show().removeClass('jsxc_waiting').text($.t('Could_load_only', {
+ count: count
+ }));
+ } else {
+ dialog.find('.jsxc_inputinfo').hide();
+ }
+ }, function(stanza) {
+ var errTextMsg = $(stanza).find('error text').text() || null;
+ jsxc.warn('Could not load rooms', errTextMsg);
+
+ if (errTextMsg) {
+ dialog.find('.jsxc_inputinfo.jsxc_server').show().text(errTextMsg);
+ }
+
+ if ($(stanza).find('error remote-server-not-found')) {
+ dialog.find('#jsxc_server').addClass('jsxc_invalid');
+ }
+
+ dialog.find('.jsxc_inputinfo.jsxc_room').hide();
+ });
+ }
},
/**
@@ -5928,8 +6414,8 @@ jsxc.muc = {
var form = dialog.find('form');
// work around Strophe.x behaviour
- form.find('[type="checkbox"]').change(function(){
- $(this).val(this.checked ? 1 : 0);
+ form.find('[type="checkbox"]').change(function() {
+ $(this).val(this.checked ? 1 : 0);
});
var submit = $('<button>');
@@ -7147,7 +7633,7 @@ jsxc.muc = {
$(document).on('init.window.jsxc', jsxc.muc.initWindow);
$(document).on('add.roster.jsxc', jsxc.muc.onAddRoster);
-$(document).one('attached.jsxc', function() {
+$(document).on('attached.jsxc', function() {
jsxc.muc.init();
});
@@ -7158,7 +7644,7 @@ $(document).one('connected.jsxc', function() {
/**
* This namespace handle the notice system.
- *
+ *
* @namspace jsxc.notice
* @memberOf jsxc
*/
@@ -7168,7 +7654,7 @@ jsxc.notice = {
/**
* Loads the saved notices.
- *
+ *
* @memberOf jsxc.notice
*/
load: function() {
@@ -7184,25 +7670,28 @@ jsxc.notice = {
if (saved.hasOwnProperty(key)) {
var val = saved[key];
- jsxc.notice.add(val.msg, val.description, val.fnName, val.fnParams, key);
+ jsxc.notice.add(val, val.fnName, val.fnParams, key);
}
}
},
/**
* Add a new notice to the stack;
- *
+ *
* @memberOf jsxc.notice
- * @param msg Header message
- * @param description Notice description
- * @param fnName Function name to be called if you open the notice
+ * @param {Object} data
+ * @param {String} data.msg Header message
+ * @param {String} data.description Notice description
+ * @param {String} fnName Function name to be called if you open the notice
* @param fnParams Array of params for function
- * @param id Notice id
+ * @param {String} id Notice id
*/
- add: function(msg, description, fnName, fnParams, id) {
+ add: function(data, fnName, fnParams, id) {
var nid = id || Date.now();
var list = $('#jsxc_notice ul');
var notice = $('<li/>');
+ var msg = data.msg;
+ var description = data.description;
notice.click(function() {
jsxc.notice.remove(nid);
@@ -7212,6 +7701,10 @@ jsxc.notice = {
return false;
});
+ if (data.type) {
+ notice.addClass('jsxc_' + data.type + 'icon');
+ }
+
notice.text(msg);
notice.attr('title', description || '');
notice.attr('data-nid', nid);
@@ -7219,11 +7712,13 @@ jsxc.notice = {
$('#jsxc_notice > span').text(++jsxc.notice._num);
+ var saved = jsxc.storage.getUserItem('notices') || {};
+
if (!id) {
- var saved = jsxc.storage.getUserItem('notices') || {};
saved[nid] = {
msg: msg,
description: description,
+ type: data.type,
fnName: fnName,
fnParams: fnParams
};
@@ -7231,11 +7726,23 @@ jsxc.notice = {
jsxc.notification.notify(msg, description || '', null, true, jsxc.CONST.SOUNDS.NOTICE);
}
+
+ if (Object.keys(saved).length > 3 && list.find('.jsxc_closeAll').length === 0) {
+ // add close all button
+ var closeAll = $('<li>');
+ closeAll.addClass('jsxc_closeAll jsxc_deleteicon jsxc_warning');
+ closeAll.text($.t('Close_all'));
+ closeAll.prependTo(list);
+ closeAll.click(jsxc.notice.removeAll);
+ } else if (Object.keys(saved).length <= 3 && list.find('.jsxc_closeAll').length !== 0) {
+ // remove close all button
+ list.find('.jsxc_closeAll').remove();
+ }
},
/**
* Removes notice from stack
- *
+ *
* @memberOf jsxc.notice
* @param nid The notice id
*/
@@ -7245,14 +7752,30 @@ jsxc.notice = {
el.remove();
$('#jsxc_notice > span').text(--jsxc.notice._num || '');
- var s = jsxc.storage.getUserItem('notices');
+ var s = jsxc.storage.getUserItem('notices') || {};
delete s[nid];
jsxc.storage.setUserItem('notices', s);
+
+ if (Object.keys(s).length <= 3 && $('#jsxc_notice .jsxc_closeAll').length !== 0) {
+ // remove close all button
+ $('#jsxc_notice .jsxc_closeAll').remove();
+ }
+ },
+
+ /**
+ * Remove all notices.
+ */
+ removeAll: function() {
+ jsxc.notice._num = 0;
+ jsxc.storage.setUserItem('notices', {});
+
+ $('#jsxc_notice ul').empty();
+ $('#jsxc_notice > span').text('');
},
/**
* Check if there is already a notice for the given function name.
- *
+ *
* @memberOf jsxc.notice
* @param {string} fnName Function name
* @returns {boolean} True if there is >0 functions with the given name
@@ -7275,7 +7798,7 @@ jsxc.notice = {
/**
* This namespace handles the Notification API.
- *
+ *
* @namespace jsxc.notification
*/
jsxc.notification = {
@@ -7285,7 +7808,7 @@ jsxc.notification = {
/**
* Register notification on incoming messages.
- *
+ *
* @memberOf jsxc.notification
*/
init: function() {
@@ -7314,7 +7837,7 @@ jsxc.notification = {
/**
* Shows a pop up notification and optional play sound.
- *
+ *
* @param title Title
* @param msg Message
* @param d Duration
@@ -7383,7 +7906,7 @@ jsxc.notification = {
/**
* Checks if browser has support for notifications and add on chrome to the
* default api.
- *
+ *
* @returns {Boolean} True if the browser has support.
*/
hasSupport: function() {
@@ -7438,7 +7961,10 @@ jsxc.notification = {
$(document).one('postmessagein.jsxc', function() {
setTimeout(function() {
- jsxc.notice.add($.t('Notifications') + '?', $.t('Should_we_notify_you_'), 'gui.showRequestNotification');
+ jsxc.notice.add({
+ msg: $.t('Notifications') + '?',
+ description: $.t('Should_we_notify_you_')
+ }, 'gui.showRequestNotification');
}, 1000);
});
},
@@ -7462,7 +7988,7 @@ jsxc.notification = {
/**
* Check permission.
- *
+ *
* @returns {Boolean} True if we have the permission
*/
hasPermission: function() {
@@ -7471,7 +7997,7 @@ jsxc.notification = {
/**
* Plays the given file.
- *
+ *
* @memberOf jsxc.notification
* @param {string} soundFile File relative to the sound directory
* @param {boolean} loop True for loop
@@ -7505,7 +8031,7 @@ jsxc.notification = {
/**
* Stop/remove current sound.
- *
+ *
* @memberOf jsxc.notification
*/
stopSound: function() {
@@ -7519,7 +8045,7 @@ jsxc.notification = {
/**
* Mute sound.
- *
+ *
* @memberOf jsxc.notification
* @param {boolean} external True if triggered from external tab. Default:
* false.
@@ -7534,7 +8060,7 @@ jsxc.notification = {
/**
* Unmute sound.
- *
+ *
* @memberOf jsxc.notification
* @param {boolean} external True if triggered from external tab. Default:
* false.
@@ -7550,7 +8076,7 @@ jsxc.notification = {
/**
* Set some options for the chat.
- *
+ *
* @namespace jsxc.options
*/
jsxc.options = {
@@ -7569,7 +8095,7 @@ jsxc.options = {
enable: true,
ERROR_START_AKE: false,
debug: false,
- SEND_WHITESPACE_TAG: true,
+ SEND_WHITESPACE_TAG: false,
WHITESPACE_START_AKE: true
},
@@ -7610,9 +8136,9 @@ jsxc.options = {
},
/**
- * This function is called if a login form was found, but before any
+ * This function is called if a login form was found, but before any
* modification is done to it.
- *
+ *
* @memberOf jsxc.options
* @function
*/
@@ -7638,7 +8164,7 @@ jsxc.options = {
},
/**
- * Action after login was called: dialog [String] Show wait dialog, false [boolean] |
+ * Action after login was called: dialog [String] Show wait dialog, false [boolean] |
* quiet [String] Do nothing
*/
onConnecting: 'dialog',
@@ -7657,25 +8183,25 @@ jsxc.options = {
/**
* True: Attach connection even is login form was found.
- *
+ *
* @type {Boolean}
* @deprecated since 3.0.0. Use now loginForm.ifFound (true => attach, false => pause)
*/
attachIfFound: true,
/**
- * Describes what we should do if login form was found:
+ * Describes what we should do if login form was found:
* - Attach connection
* - Force new connection with loginForm.jid and loginForm.passed
* - Pause connection and do nothing
- *
+ *
* @type {(attach|force|pause)}
*/
ifFound: 'attach',
/**
- * True: Display roster minimized after first login. Afterwards the last
- * roster state will be used.
+ * True: Display roster minimized after first login. Afterwards the last
+ * roster state will be used.
*/
startMinimized: false
},
@@ -7720,7 +8246,7 @@ jsxc.options = {
/**
* If no avatar is found, this function is called.
- *
+ *
* @param jid Jid of that user.
* @this {jQuery} Elements to update with probable .jsxc_avatar elements
*/
@@ -7736,7 +8262,7 @@ jsxc.options = {
/**
* Returns permanent saved settings and overwrite default jsxc.options.
- *
+ *
* @memberOf jsxc.options
* @function
* @param username {string} username
@@ -7747,7 +8273,7 @@ jsxc.options = {
/**
* Call this function to save user settings permanent.
- *
+ *
* @memberOf jsxc.options
* @param data Holds all data as key/value
* @param cb Called with true on success, false otherwise
@@ -7758,19 +8284,19 @@ jsxc.options = {
carbons: {
/** Enable carbon copies? */
- enable: false
+ enable: true
},
/**
* Processes user list.
- *
+ *
* @callback getUsers-cb
* @param {object} list List of users, key: username, value: alias
*/
/**
* Returns a list of usernames and aliases
- *
+ *
* @function getUsers
* @memberOf jsxc.options
* @param {string} search Search token (start with)
@@ -7828,7 +8354,32 @@ jsxc.options = {
}
},
- maxStorableSize: 1000000
+ /** Maximal storage size for attachments received via data channels (webrtc). */
+ maxStorableSize: 1000000,
+
+ /** Options for file transfer. */
+ fileTransfer: {
+ httpUpload: {
+ enable: true
+ },
+ // @TODO add option to enable/disable data channels
+ },
+
+ /** Default option for chat state notifications */
+ chatState: {
+ enable: true
+ },
+
+ /**
+ * Download urls to screen media extensions.
+ *
+ * @type {Object}
+ * @see example extensions {@link https://github.com/otalk/getScreenMedia}
+ */
+ screenMediaExtension: {
+ firefox: '',
+ chrome: ''
+ }
};
/**
@@ -7841,7 +8392,7 @@ jsxc.otr = {
dsaFallback: null,
/**
* Handler for otr receive event
- *
+ *
* @memberOf jsxc.otr
* @param {Object} d
* @param {string} d.bid
@@ -7873,28 +8424,29 @@ jsxc.otr = {
msg: d.msg,
encrypted: d.encrypted,
forwarded: d.forwarded,
- stamp: d.stamp
+ stamp: d.stamp,
+ attachment: d.attachment
});
}
},
/**
* Handler for otr send event
- *
+ *
* @param {string} jid
* @param {string} msg message to be send
*/
- sendMessage: function(jid, msg, uid) {
+ sendMessage: function(jid, msg, message) {
if (jsxc.otr.objects[jsxc.jidToBid(jid)].msgstate !== 0) {
jsxc.otr.backup(jsxc.jidToBid(jid));
}
- jsxc.xmpp._sendMessage(jid, msg, uid);
+ jsxc.xmpp._sendMessage(jid, msg, message);
},
/**
* Create new otr instance
- *
+ *
* @param {type} bid
* @returns {undefined}
*/
@@ -8045,17 +8597,18 @@ jsxc.otr = {
msg: msg,
encrypted: encrypted === true,
stamp: meta.stamp,
- forwarded: meta.forwarded
+ forwarded: meta.forwarded,
+ attachment: meta.attachment
});
});
// Send message
- jsxc.otr.objects[bid].on('io', function(msg, uid) {
+ jsxc.otr.objects[bid].on('io', function(msg, message) {
var jid = jsxc.gui.window.get(bid).data('jid') || jsxc.otr.objects[bid].jid;
jsxc.otr.objects[bid].jid = jid;
- jsxc.otr.sendMessage(jid, msg, uid);
+ jsxc.otr.sendMessage(jid, msg, message);
});
jsxc.otr.objects[bid].on('error', function(err) {
@@ -8076,7 +8629,7 @@ jsxc.otr = {
/**
* show verification dialog with related part (secret or question)
- *
+ *
* @param {type} bid
* @param {string} [data]
* @returns {undefined}
@@ -8108,7 +8661,7 @@ jsxc.otr = {
/**
* Send verification request to buddy
- *
+ *
* @param {string} bid
* @param {string} sec secret
* @param {string} [quest] question
@@ -8122,7 +8675,7 @@ jsxc.otr = {
/**
* Toggle encryption state
- *
+ *
* @param {type} bid
* @returns {undefined}
*/
@@ -8140,7 +8693,7 @@ jsxc.otr = {
/**
* Send request to encrypt the session
- *
+ *
* @param {type} bid
* @returns {undefined}
*/
@@ -8156,7 +8709,7 @@ jsxc.otr = {
/**
* Abort encryptet session
- *
+ *
* @param {type} bid
* @param cb callback
* @returns {undefined}
@@ -8176,7 +8729,7 @@ jsxc.otr = {
/**
* Backups otr session
- *
+ *
* @param {string} bid
*/
backup: function(bid) {
@@ -8208,7 +8761,7 @@ jsxc.otr = {
/**
* Restore old otr session
- *
+ *
* @param {string} bid
*/
restore: function(bid) {
@@ -8243,7 +8796,7 @@ jsxc.otr = {
/**
* Create or load DSA key
- *
+ *
* @returns {unresolved}
*/
createDSA: function() {
@@ -8341,7 +8894,7 @@ jsxc.otr = {
/**
* Ending of DSA key generation.
- *
+ *
* @param {DSA} dsa DSA object
*/
DSAready: function(dsa) {
@@ -8386,7 +8939,7 @@ jsxc.storage = {
var self = jsxc.storage;
if (uk && !jsxc.bid) {
- console.trace('Unable to create user prefix');
+ jsxc.warn('Unable to create user prefix');
}
return self.PREFIX + self.SEP + ((uk && jsxc.bid) ? jsxc.bid + self.SEP : '');
@@ -9015,20 +9568,31 @@ jsxc.tab = {
});
},
- /*jshint -W098 */
- execMaster: function(cmd, params) {
+ /**
+ * Execute command in master tab.
+ *
+ * @param {String} cmd Command
+ * @param {String[]} params List of parameters
+ */
+ execMaster: function() {
var args = Array.prototype.slice.call(arguments);
args.unshift(jsxc.tab.CONST.MASTER);
jsxc.tab.exec.apply(this, args);
},
- execSlave: function(cmd, params) {
- var args = Array.prototype.slice.call(arguments);
- args.unshift(jsxc.tab.CONST.SLAVE);
- jsxc.tab.exec.apply(this, args);
- }
- /*jshint +W098 */
+ /**
+ * Execute command in all slave tabs.
+ *
+ * @param {String} cmd Command
+ * @param {String[]} params List of parameters
+ */
+ execSlave: function() {
+ var args = Array.prototype.slice.call(arguments);
+ args.unshift(jsxc.tab.CONST.SLAVE);
+
+ jsxc.tab.exec.apply(this, args);
+ }
};
/* global MediaStreamTrack, File */
@@ -9036,7 +9600,7 @@ jsxc.tab = {
/**
* WebRTC namespace for jsxc.
- *
+ *
* @namespace jsxc.webrtc
*/
jsxc.webrtc = {
@@ -9066,7 +9630,7 @@ jsxc.webrtc = {
/**
* Initialize webrtc plugin.
- *
+ *
* @private
* @memberOf jsxc.webrtc
*/
@@ -9086,20 +9650,20 @@ jsxc.webrtc = {
$(document).on('message.jsxc', self.onMessage);
$(document).on('presence.jsxc', self.onPresence);
- $(document).on('mediaready.jingle', self.onMediaReady);
$(document).on('mediafailure.jingle', self.onMediaFailure);
manager.on('incoming', $.proxy(self.onIncoming, self));
+ // @REVIEW those events could be session based
manager.on('terminated', $.proxy(self.onTerminated, self));
manager.on('ringing', $.proxy(self.onCallRinging, self));
manager.on('receivedFile', $.proxy(self.onReceivedFile, self));
-
manager.on('sentFile', function(sess, metadata) {
jsxc.debug('sent ' + metadata.hash);
});
+ // @REVIEW those events could be session based
manager.on('peerStreamAdded', $.proxy(self.onRemoteStreamAdded, self));
manager.on('peerStreamRemoved', $.proxy(self.onRemoteStreamRemoved, self));
@@ -9141,7 +9705,6 @@ jsxc.webrtc = {
$(document).off('message.jsxc', self.onMessage);
$(document).off('presence.jsxc', self.onPresence);
- $(document).off('mediaready.jingle', self.onMediaReady);
$(document).off('mediafailure.jingle', self.onMediaFailure);
$(document).off('caps.strophe', self.onCaps);
@@ -9149,7 +9712,7 @@ jsxc.webrtc = {
/**
* Checks if cached configuration is valid and if necessary update it.
- *
+ *
* @memberOf jsxc.webrtc
* @param {string} [url]
*/
@@ -9226,7 +9789,7 @@ jsxc.webrtc = {
/**
* Return list of capable resources.
- *
+ *
* @memberOf jsxc.webrtc
* @param jid
* @param {(string|string[])} features list of required features
@@ -9255,7 +9818,7 @@ jsxc.webrtc = {
/**
* Add "video" button to window menu.
- *
+ *
* @private
* @memberOf jsxc.webrtc
* @param event
@@ -9277,15 +9840,27 @@ jsxc.webrtc = {
return;
}
+ // Add video call icon
var div = $('<div>').addClass('jsxc_video');
win.find('.jsxc_tools .jsxc_settings').after(div);
+ var screenMediaExtension = jsxc.options.get('screenMediaExtension') || {};
+ var browser = self.conn.jingle.RTC.webrtcDetectedBrowser;
+ if (screenMediaExtension[browser] || jsxc.storage.getItem('debug')) {
+ // Add screen sharing button if extension is available or we are in debug mode
+ var a = $('<a>');
+ a.text($.t('Share_screen'));
+ a.addClass('jsxc_shareScreen jsxc_video');
+ a.attr('href', '#');
+ win.find('.jsxc_settings .jsxc_menu li:last').after($('<li>').append(a));
+ }
+
self.updateIcon(win.data('bid'));
},
/**
* Enable or disable "video" icon and assign full jid.
- *
+ *
* @memberOf jsxc.webrtc
* @param bid CSS conform jid
*/
@@ -9333,7 +9908,11 @@ jsxc.webrtc = {
if (capableRes.indexOf(targetRes) > -1) {
el.click(function() {
- self.startCall(jid);
+ if ($(this).hasClass('jsxc_shareScreen')) {
+ self.startScreenSharing(jid);
+ } else {
+ self.startCall(jid);
+ }
});
el.removeClass('jsxc_disabled');
@@ -9344,20 +9923,11 @@ jsxc.webrtc = {
el.attr('title', $.t('Video_call_not_possible'));
}
-
- var fileCapableRes = self.getCapableRes(jid, self.reqFileFeatures);
- var resources = Object.keys(jsxc.storage.getUserItem('res', bid) || {}) || [];
-
- if (fileCapableRes.indexOf(res) > -1 || (res === null && fileCapableRes.length === 1 && resources.length === 1)) {
- win.find('.jsxc_sendFile').removeClass('jsxc_disabled');
- } else {
- win.find('.jsxc_sendFile').addClass('jsxc_disabled');
- }
},
/**
* Check if full jid changed.
- *
+ *
* @private
* @memberOf jsxc.webrtc
* @param e
@@ -9377,7 +9947,7 @@ jsxc.webrtc = {
/**
* Update icon on presence.
- *
+ *
* @memberOf jsxc.webrtc
* @param ev
* @param status
@@ -9395,7 +9965,7 @@ jsxc.webrtc = {
/**
* Display status message to user.
- *
+ *
* @memberOf jsxc.webrtc
* @param txt message
* @param d duration in ms
@@ -9442,7 +10012,7 @@ jsxc.webrtc = {
/**
* Update "video" button if we receive cap information.
- *
+ *
* @private
* @memberOf jsxc.webrtc
* @param event
@@ -9461,67 +10031,47 @@ jsxc.webrtc = {
},
/**
- * Called if video/audio is ready. Open window and display some messages.
- *
- * @private
- * @memberOf jsxc.webrtc
- * @param event
- * @param stream
- */
- onMediaReady: function(event, stream) {
- jsxc.debug('media ready');
-
- var self = jsxc.webrtc;
-
- self.localStream = stream;
- self.conn.jingle.localStream = stream;
-
- var dialog = jsxc.gui.showVideoWindow(self.last_caller);
-
- var audioTracks = stream.getAudioTracks();
- var videoTracks = stream.getVideoTracks();
- var i;
-
- for (i = 0; i < audioTracks.length; i++) {
- self.setStatus((audioTracks.length > 0) ? $.t('Use_local_audio_device') : $.t('No_local_audio_device'));
-
- jsxc.debug('using audio device "' + audioTracks[i].label + '"');
- }
-
- for (i = 0; i < videoTracks.length; i++) {
- self.setStatus((videoTracks.length > 0) ? $.t('Use_local_video_device') : $.t('No_local_video_device'));
-
- jsxc.debug('using video device "' + videoTracks[i].label + '"');
-
- dialog.find('.jsxc_localvideo').show();
- }
-
- $(document).trigger('finish.mediaready.jsxc');
- },
-
- /**
* Called if media failes.
- *
+ *
* @private
* @memberOf jsxc.webrtc
*/
onMediaFailure: function(ev, err) {
var self = jsxc.webrtc;
- err = err || {
- name: 'Undefined'
- };
+ var msg;
+ err = err || {};
self.setStatus('media failure');
+ switch (err.name) {
+ case 'NotAllowedError':
+ case 'PERMISSION_DENIED':
+ msg = $.t('PermissionDeniedError');
+ break;
+ case 'HTTPS_REQUIRED':
+ case 'EXTENSION_UNAVAILABLE':
+ msg = $.t(err.name);
+ break;
+ default:
+ msg = $.t(err.name) !== err.name ? $.t(err.name) : $.t('UNKNOWN_ERROR');
+ }
+
jsxc.gui.window.postMessage({
bid: jsxc.jidToBid(jsxc.webrtc.last_caller),
direction: jsxc.Message.SYS,
- msg: $.t('Media_failure') + ': ' + $.t(err.name) + ' (' + err.name + ').'
+ msg: $.t('Media_failure') + ': ' + msg + ' (' + err.name + ').'
});
+ jsxc.gui.dialog.close();
+
jsxc.debug('media failure: ' + err.name);
},
+ /**
+ * Process incoming jingle offer.
+ *
+ * @param {BaseSession} session
+ */
onIncoming: function(session) {
var self = jsxc.webrtc;
var type = (session.constructor) ? session.constructor.name : null;
@@ -9529,10 +10079,88 @@ jsxc.webrtc = {
if (type === 'FileTransferSession') {
self.onIncomingFileTransfer(session);
} else if (type === 'MediaSession') {
- self.onIncomingCall(session);
+ var reqMedia = false;
+
+ $.each(session.pc.remoteDescription.contents, function() {
+ if (this.senders === 'both') {
+ reqMedia = true;
+ }
+ });
+
+ session.call = reqMedia;
+
+ if (reqMedia) {
+ self.onIncomingCall(session);
+ } else {
+ self.onIncomingStream(session);
+ }
+ } else {
+ jsxc.warn('Unknown session type.');
}
},
+ /**
+ * Process incoming stream offer.
+ *
+ * @param {MediaSession} session
+ */
+ onIncomingStream: function(session) {
+ jsxc.debug('incoming stream from ' + session.peerID);
+
+ var self = jsxc.webrtc;
+ var bid = jsxc.jidToBid(session.peerID);
+
+ session.on('change:connectionState', $.proxy(self.onIceConnectionStateChanged, self));
+
+ self.postScreenMessage(bid, $.t('Incoming_stream'), session.sid);
+
+ // display notification
+ jsxc.notification.notify($.t('Incoming_stream'), $.t('from_sender', {
+ sender: bid
+ }));
+
+ // send signal to partner
+ session.ring();
+
+ jsxc.webrtc.last_caller = session.peerID;
+
+ if (jsxc.webrtc.AUTO_ACCEPT) {
+ acceptIncomingStream(session);
+
+ return;
+ }
+
+ var dialog = jsxc.gui.dialog.open(jsxc.gui.template.get('incomingCall', bid), {
+ noClose: true
+ });
+
+ dialog.find('.jsxc_accept').click(function() {
+ $(document).trigger('accept.call.jsxc');
+
+ acceptIncomingStream(session);
+ });
+
+ dialog.find('.jsxc_reject').click(function() {
+ jsxc.gui.dialog.close();
+ $(document).trigger('reject.call.jsxc');
+
+ session.decline();
+ });
+
+ function acceptIncomingStream(session) {
+ jsxc.gui.dialog.close();
+
+ jsxc.gui.showVideoWindow(session.peerID);
+
+ session.accept();
+ }
+ },
+
+ /**
+ * Process incoming file offer.
+ *
+ * @param {FileSession} session
+ */
onIncomingFileTransfer: function(session) {
jsxc.debug('incoming file transfer from ' + session.peerID);
@@ -9561,11 +10189,10 @@ jsxc.webrtc = {
/**
* Called on incoming call.
- *
+ *
* @private
* @memberOf jsxc.webrtc
- * @param event
- * @param sid Session id
+ * @param {MediaSession} session
*/
onIncomingCall: function(session) {
jsxc.debug('incoming call from ' + session.peerID);
@@ -9575,11 +10202,7 @@ jsxc.webrtc = {
session.on('change:connectionState', $.proxy(self.onIceConnectionStateChanged, self));
- jsxc.gui.window.postMessage({
- bid: bid,
- direction: jsxc.Message.SYS,
- msg: $.t('Incoming_call')
- });
+ self.postCallMessage(bid, $.t('Incoming_call'), session.sid);
// display notification
jsxc.notification.notify($.t('Incoming_call'), $.t('from_sender', {
@@ -9591,27 +10214,8 @@ jsxc.webrtc = {
jsxc.webrtc.last_caller = session.peerID;
- function acceptCall() {
- $(document).trigger('accept.call.jsxc');
-
- jsxc.switchEvents({
- 'mediaready.jingle': function(event, stream) {
- self.setStatus('Accept call');
-
- session.addStream(stream);
-
- session.accept();
- },
- 'mediafailure.jingle': function() {
- session.decline();
- }
- });
-
- self.reqUserMedia();
- }
-
if (jsxc.webrtc.AUTO_ACCEPT) {
- acceptCall();
+ self.acceptIncomingCall(session);
return;
}
@@ -9619,7 +10223,9 @@ jsxc.webrtc = {
noClose: true
});
- dialog.find('.jsxc_accept').click(acceptCall);
+ dialog.find('.jsxc_accept').click(function() {
+ self.acceptIncomingCall(session);
+ });
dialog.find('.jsxc_reject').click(function() {
jsxc.gui.dialog.close();
@@ -9629,6 +10235,45 @@ jsxc.webrtc = {
});
},
+ /**
+ * Called on incoming call.
+ *
+ * @private
+ * @memberOf jsxc.webrtc
+ * @param {MediaSession} session
+ */
+ acceptIncomingCall: function(session) {
+ $(document).trigger('accept.call.jsxc');
+
+ var self = jsxc.webrtc;
+
+ jsxc.switchEvents({
+ 'mediaready.jingle': function(ev, stream) {
+ self.setStatus('Accept call');
+
+ self.localStream = stream;
+ self.conn.jingle.localStream = stream;
+
+ var dialog = jsxc.gui.showVideoWindow(session.peerID);
+ dialog.find('.jsxc_videoContainer').addClass('jsxc_establishing');
+
+ session.addStream(stream);
+ session.accept();
+ },
+ 'mediafailure.jingle': function() {
+ session.decline();
+ }
+ });
+
+ self.reqUserMedia();
+ },
+
+ /**
+ * Process jingle termination event.
+ *
+ * @param {BaseSession} session
+ * @param {Object} reason Reason for termination
+ */
onTerminated: function(session, reason) {
var self = jsxc.webrtc;
var type = (session.constructor) ? session.constructor.name : null;
@@ -9640,82 +10285,99 @@ jsxc.webrtc = {
/**
* Called if call is terminated.
- *
+ *
* @private
* @memberOf jsxc.webrtc
- * @param event
- * @param sid Session id
- * @param reason Reason for termination
- * @param [text] Optional explanation
+ * @param {BaseSession} session
+ * @param {Object} reason Reason for termination
*/
onCallTerminated: function(session, reason) {
- this.setStatus('call terminated ' + session.peerID + (reason && reason.condition ? reason.condition : ''));
+ var self = jsxc.webrtc;
+
+ self.setStatus('call terminated ' + session.peerID + (reason && reason.condition ? reason.condition : ''));
var bid = jsxc.jidToBid(session.peerID);
- if (this.localStream) {
- if (typeof this.localStream.stop === 'function') {
- this.localStream.stop();
- } else {
- var tracks = this.localStream.getTracks();
+ if (self.localStream) {
+ // stop local stream
+ if (typeof self.localStream.getTracks === 'function') {
+ var tracks = self.localStream.getTracks();
tracks.forEach(function(track) {
track.stop();
});
+ } else if (typeof self.localStream.stop === 'function') {
+ self.localStream.stop();
+ } else {
+ jsxc.warn('Could not stop local stream');
}
}
- if ($('.jsxc_videoContainer').length) {
+ // @REVIEW necessary?
+ if ($('.jsxc_remotevideo').length) {
$('.jsxc_remotevideo')[0].src = "";
+ }
+
+ if ($('.jsxc_localvideo').length) {
$('.jsxc_localvideo')[0].src = "";
}
- this.conn.jingle.localStream = null;
- this.localStream = null;
- this.remoteStream = null;
+ self.conn.jingle.localStream = null;
+ self.localStream = null;
+ self.remoteStream = null;
jsxc.gui.closeVideoWindow();
+ // Close incoming call dialog and stop ringing
+ jsxc.gui.dialog.close();
+ $(document).trigger('reject.call.jsxc');
+
$(document).off('error.jingle');
- jsxc.gui.window.postMessage({
- bid: bid,
- direction: jsxc.Message.SYS,
- msg: ($.t('Call_terminated') + (reason && reason.condition ? (': ' + $.t('jingle_reason_' + reason.condition)) : '') + '.')
- });
+ var msg = (reason && reason.condition ? (': ' + $.t('jingle_reason_' + reason.condition)) : '') + '.';
+ if (session.call) {
+ msg = $.t('Call_terminated') + msg;
+ jsxc.webrtc.postCallMessage(bid, msg, session.sid);
+ } else {
+ msg = $.t('Stream_terminated') + msg;
+ jsxc.webrtc.postScreenMessage(bid, msg, session.sid);
+ }
},
/**
* Remote station is ringing.
- *
+ *
* @private
* @memberOf jsxc.webrtc
*/
onCallRinging: function() {
this.setStatus('ringing...', 0);
+
+ $('.jsxc_videoContainer').removeClass('jsxc_establishing').addClass('jsxc_ringing');
},
/**
* Called if we receive a remote stream.
- *
+ *
* @private
* @memberOf jsxc.webrtc
- * @param event
- * @param data
- * @param sid Session id
+ * @param {BaseSession} session
+ * @param {Object} stream
*/
onRemoteStreamAdded: function(session, stream) {
- this.setStatus('Remote stream for session ' + session.sid + ' added.');
+ var self = jsxc.webrtc;
+
+ self.setStatus('Remote stream for session ' + session.sid + ' added.');
- this.remoteStream = stream;
+ self.remoteStream = stream;
var isVideoDevice = stream.getVideoTracks().length > 0;
var isAudioDevice = stream.getAudioTracks().length > 0;
- this.setStatus(isVideoDevice ? 'Use remote video device.' : 'No remote video device');
- this.setStatus(isAudioDevice ? 'Use remote audio device.' : 'No remote audio device');
+ self.setStatus(isVideoDevice ? 'Use remote video device.' : 'No remote video device');
+ self.setStatus(isAudioDevice ? 'Use remote audio device.' : 'No remote audio device');
if ($('.jsxc_remotevideo').length) {
- this.attachMediaStream($('#jsxc_webrtc .jsxc_remotevideo'), stream);
+ self.attachMediaStream($('#jsxc_webrtc .jsxc_remotevideo'), stream);
$('#jsxc_webrtc .jsxc_' + (isVideoDevice ? 'remotevideo' : 'noRemoteVideo')).addClass('jsxc_deviceAvailable');
}
@@ -9723,7 +10385,7 @@ jsxc.webrtc = {
/**
* Attach media stream to element.
- *
+ *
* @memberOf jsxc.webrtc
* @param element {Element|jQuery}
* @param stream {mediastream}
@@ -9732,16 +10394,16 @@ jsxc.webrtc = {
var self = jsxc.webrtc;
self.conn.jingle.RTC.attachMediaStream((element instanceof jQuery) ? element.get(0) : element, stream);
+
+ $(element).show();
},
/**
* Called if the remote stream was removed.
- *
+ *
* @private
* @meberOf jsxc.webrtc
- * @param event
- * @param data
- * @param sid Session id
+ * @param {BaseSession} session
*/
onRemoteStreamRemoved: function(session) {
this.setStatus('Remote stream for ' + session.jid + ' removed.');
@@ -9750,13 +10412,12 @@ jsxc.webrtc = {
},
/**
- * Extracts local and remote ip and display it to the user.
- *
+ * Display information according to the connection state.
+ *
* @private
* @memberOf jsxc.webrtc
- * @param event
- * @param sid session id
- * @param sess
+ * @param {BaseSession} session
+ * @param {String} state
*/
onIceConnectionStateChanged: function(session, state) {
var self = jsxc.webrtc;
@@ -9764,10 +10425,7 @@ jsxc.webrtc = {
jsxc.debug('connection state for ' + session.sid, state);
if (state === 'connected') {
-
$('#jsxc_webrtc .jsxc_deviceAvailable').show();
- $('#jsxc_webrtc .bubblingG').hide();
-
} else if (state === 'failed') {
jsxc.gui.window.postMessage({
bid: jsxc.jidToBid(session.peerID),
@@ -9785,13 +10443,13 @@ jsxc.webrtc = {
/**
* Start a call to the specified jid.
- *
+ *
* @memberOf jsxc.webrtc
- * @param jid full jid
- * @param um requested user media
+ * @param {String} jid full jid
+ * @param {String[]} um requested user media
*/
startCall: function(jid, um) {
- var self = this;
+ var self = jsxc.webrtc;
if (Strophe.getResourceFromJid(jid) === null) {
jsxc.debug('We need a full jid');
@@ -9801,28 +10459,10 @@ jsxc.webrtc = {
self.last_caller = jid;
jsxc.switchEvents({
- 'finish.mediaready.jsxc': function() {
- self.setStatus('Initiate call');
+ 'mediaready.jingle': function(ev, stream) {
+ jsxc.debug('media ready for outgoing call');
- jsxc.gui.window.postMessage({
- bid: jsxc.jidToBid(jid),
- direction: jsxc.Message.SYS,
- msg: $.t('Call_started')
- });
-
- $(document).one('error.jingle', function(e, sid, error) {
- if (error && error.source !== 'offer') {
- return;
- }
-
- setTimeout(function() {
- jsxc.gui.showAlert("Sorry, we couldn't establish a connection. Maybe your buddy is offline.");
- }, 500);
- });
-
- var session = self.conn.jingle.initiate(jid);
-
- session.on('change:connectionState', $.proxy(self.onIceConnectionStateChanged, self));
+ self.initiateOutgoingCall(jid, stream);
},
'mediafailure.jingle': function() {
jsxc.gui.dialog.close();
@@ -9833,8 +10473,47 @@ jsxc.webrtc = {
},
/**
+ * Start jingle session to jid with stream.
+ *
+ * @param {String} jid
+ * @param {Object} stream
+ */
+ initiateOutgoingCall: function(jid, stream) {
+ var self = jsxc.webrtc;
+
+ self.localStream = stream;
+ self.conn.jingle.localStream = stream;
+
+ var dialog = jsxc.gui.showVideoWindow(jid);
+
+ dialog.find('.jsxc_videoContainer').addClass('jsxc_establishing');
+
+ self.setStatus('Initiate call');
+
+ // @REVIEW session based?
+ $(document).one('error.jingle', function(ev, sid, error) {
+ if (error && error.source !== 'offer') {
+ return;
+ }
+
+ setTimeout(function() {
+ jsxc.gui.showAlert("Sorry, we couldn't establish a connection. Maybe your buddy is offline.");
+ }, 500);
+ });
+
+ var session = self.conn.jingle.initiate(jid);
+
+ // flag session as call
+ session.call = true;
+
+ session.on('change:connectionState', $.proxy(self.onIceConnectionStateChanged, self));
+
+ self.postCallMessage(jsxc.jidToBid(jid), $.t('Call_started'), session.sid);
+ },
+
+ /**
* Hang up the current call.
- *
+ *
* @memberOf jsxc.webrtc
*/
hangUp: function(reason, text) {
@@ -9849,8 +10528,121 @@ jsxc.webrtc = {
},
/**
- * Request video and audio from local user.
- *
+ * Start outgoing screen sharing session.
+ *
+ * @param {String} jid
+ */
+ startScreenSharing: function(jid) {
+ var self = this;
+
+ if (Strophe.getResourceFromJid(jid) === null) {
+ jsxc.debug('We need a full jid');
+ return;
+ }
+
+ self.last_caller = jid;
+
+ jsxc.switchEvents({
+ 'mediaready.jingle': function(ev, stream) {
+ self.initiateScreenSharing(jid, stream);
+ },
+ 'mediafailure.jingle': function(ev, err) {
+ jsxc.gui.dialog.close();
+
+ var browser = self.conn.jingle.RTC.webrtcDetectedBrowser;
+
+ var screenMediaExtension = jsxc.options.get('screenMediaExtension') || {};
+ if (screenMediaExtension[browser] &&
+ (err.name === 'EXTENSION_UNAVAILABLE' || (err.name === 'NotAllowedError' && browser === 'firefox'))) {
+ // post download link after explanation
+ setTimeout(function() {
+ jsxc.gui.window.postMessage({
+ bid: jsxc.jidToBid(jid),
+ direction: jsxc.Message.SYS,
+ msg: $.t('Install_extension') + screenMediaExtension[browser]
+ });
+ }, 500);
+ }
+ }
+ });
+
+ self.reqUserMedia(['screen']);
+ },
+
+ /**
+ * Initiate outgoing (one-way) jingle session to jid with stream.
+ *
+ * @param {String} jid
+ * @param {Object} stream
+ */
+ initiateScreenSharing: function(jid, stream) {
+ var self = jsxc.webrtc;
+ var bid = jsxc.jidToBid(jid);
+
+ jsxc.webrtc.localStream = stream;
+ jsxc.webrtc.conn.jingle.localStream = stream;
+
+ var container = jsxc.gui.showMinimizedVideoWindow();
+ container.addClass('jsxc_establishing');
+
+ self.setStatus('Initiate stream');
+
+ $(document).one('error.jingle', function(e, sid, error) {
+ if (error && error.source !== 'offer') {
+ return;
+ }
+
+ setTimeout(function() {
+ jsxc.gui.showAlert("Sorry, we couldn't establish a connection. Maybe your buddy is offline.");
+ }, 500);
+ });
+
+ var browser = self.conn.jingle.RTC.webrtcDetectedBrowser;
+ var browserVersion = self.conn.jingle.RTC.webrtcDetectedVersion;
+ var constraints;
+
+ if ((browserVersion < 33 && browser === 'firefox') || browser === 'chrome') {
+ constraints = {
+ mandatory: {
+ 'OfferToReceiveAudio': false,
+ 'OfferToReceiveVideo': false
+ }
+ };
+ } else {
+ constraints = {
+ 'offerToReceiveAudio': false,
+ 'offerToReceiveVideo': false
+ };
+ }
+
+ var session = self.conn.jingle.initiate(jid, undefined, constraints);
+ session.call = false;
+
+ session.on('change:connectionState', $.proxy(self.onIceConnectionStateChanged, self));
+ // @REVIEW also for calls?
+ session.on('accepted', function() {
+ self.onSessionAccepted(session);
+ });
+
+ self.postScreenMessage(bid, $.t('Stream_started'), session.sid);
+ },
+
+ /**
+ * Session was accepted by other peer.
+ *
+ * @param {BaseSession} session
+ */
+ onSessionAccepted: function(session) {
+ var self = jsxc.webrtc;
+
+ $('.jsxc_videoContainer').removeClass('jsxc_ringing');
+
+ self.postScreenMessage(jsxc.jidToBid(session.peerID), $.t('Connection_accepted'), session.sid);
+ },
+
+ /**
+ * Request media from local user.
+ *
* @memberOf jsxc.webrtc
*/
reqUserMedia: function(um) {
@@ -9864,26 +10656,45 @@ jsxc.webrtc = {
jsxc.gui.dialog.open(jsxc.gui.template.get('allowMediaAccess'), {
noClose: true
});
- this.setStatus('please allow access to microphone and camera');
- if (typeof MediaStreamTrack !== 'undefined' && typeof MediaStreamTrack.getSources !== 'undefined') {
- MediaStreamTrack.getSources(function(sourceInfo) {
- var availableDevices = sourceInfo.map(function(el) {
-
- return el.kind;
+ if (um.indexOf('screen') >= 0) {
+ jsxc.webrtc.getScreenMedia();
+ } else if (typeof navigator !== 'undefined' && typeof navigator.mediaDevices !== 'undefined' &&
+ typeof navigator.mediaDevices.enumerateDevices !== 'undefined') {
+ navigator.mediaDevices.enumerateDevices()
+ .then(filterUserMedia)
+ .catch(function(err) {
+ jsxc.warn(err.name + ": " + err.message);
});
+ } else if (typeof MediaStreamTrack !== 'undefined' && typeof MediaStreamTrack.getSources !== 'undefined') {
+ // @deprecated in chrome since v56
+ MediaStreamTrack.getSources(filterUserMedia);
+ } else {
+ jsxc.webrtc.getUserMedia(um);
+ }
- um = um.filter(function(el) {
- return availableDevices.indexOf(el) !== -1;
- });
+ function filterUserMedia(devices) {
+ var availableDevices = devices.map(function(device) {
+ return device.kind;
+ });
- jsxc.webrtc.getUserMedia(um);
+ um = um.filter(function(el) {
+ return availableDevices.indexOf(el) !== -1 || availableDevices.indexOf(el + 'input') !== -1;
});
- } else {
- jsxc.webrtc.getUserMedia(um);
+
+ if (um.length) {
+ jsxc.webrtc.getUserMedia(um);
+ } else {
+ jsxc.warn('No audio/video device available.');
+ }
}
},
+ /**
+ * Get user media from local browser.
+ *
+ * @memberOf jsxc.webrtc
+ */
getUserMedia: function(um) {
var self = jsxc.webrtc;
var constraints = {};
@@ -9913,8 +10724,50 @@ jsxc.webrtc = {
},
/**
+ * Get screen media from local browser.
+ *
+ * @memberOf jsxc.webrtc
+ */
+ getScreenMedia: function() {
+ var self = jsxc.webrtc;
+
+ jsxc.debug('get screen media');
+
+ self.conn.jingle.getScreenMedia(self.screenMediaCallback);
+ },
+
+ screenMediaCallback: function(err, stream) {
+ if (err) {
+ $(document).trigger('mediafailure.jingle', [err]);
+
+ return;
+ }
+
+ if (stream) {
+ jsxc.debug('onScreenMediaSuccess');
+ $(document).trigger('mediaready.jingle', [stream]);
+ }
+ },
+
+ screenMediaAvailable: function() {
+ var self = jsxc.webrtc;
+ var browser = self.conn.jingle.RTC.webrtcDetectedBrowser;
+
+ // test if chrome extension for this domain is available
+ var chrome = !!sessionStorage.getScreenMediaJSExtensionId && browser === 'chrome';
+
+ // the ff extension from {@link https://github.com/otalk/getScreenMedia}
+ // does not provide any possibility to determine if it is installed or not.
+ // Starting with Firefox 52 {@link https://www.mozilla.org/en-US/firefox/52.0a2/auroranotes/}
+ // no extension is needed anyway.
+ var firefox = browser === 'firefox';
+
+ return chrome || firefox;
+ },
+
+ /**
* Make a snapshot from a video stream and display it.
- *
+ *
* @memberOf jsxc.webrtc
* @param video Video stream
*/
@@ -9954,7 +10807,7 @@ jsxc.webrtc = {
},
/**
- * Send file to full jid.
+ * Send file to full jid via jingle.
*
* @memberOf jsxc.webrtc
* @param {string} jid full jid
@@ -9962,8 +10815,16 @@ jsxc.webrtc = {
* @return {object} session
*/
sendFile: function(jid, file) {
+ jsxc.debug('Send file via webrtc');
+
var self = jsxc.webrtc;
+ if (!Strophe.getResourceFromJid(jid)) {
+ jsxc.warn('Require full jid to send file via webrtc');
+
+ return;
+ }
+
var sess = self.conn.jingle.manager.createFileTransferSession(jid);
sess.on('change:sessionState', function() {
@@ -9997,7 +10858,7 @@ jsxc.webrtc = {
var type;
if (!metadata.type) {
- // detect file type via file extension, because XEP-0234 v0.14
+ // detect file type via file extension, because XEP-0234 v0.14
// does not send any type
var ext = metadata.name.replace(/.+\.([a-z0-9]+)$/i, '$1').toLowerCase();
@@ -10053,9 +10914,60 @@ jsxc.webrtc = {
}
};
+jsxc.webrtc.postCallMessage = function(bid, msg, uid) {
+ jsxc.gui.window.postMessage({
+ _uid: uid,
+ bid: bid,
+ direction: jsxc.Message.SYS,
+ msg: ':telephone_receiver: ' + msg
+ });
+};
+jsxc.webrtc.postScreenMessage = function(bid, msg, uid) {
+ jsxc.gui.window.postMessage({
+ _uid: uid,
+ bid: bid,
+ direction: jsxc.Message.SYS,
+ msg: ':computer: ' + msg
+ });
+};
+
+jsxc.gui.showMinimizedVideoWindow = function() {
+ var self = jsxc.webrtc;
+
+ // needed to trigger complete.dialog.jsxc
+ jsxc.gui.dialog.close();
+
+ var videoContainer = $('<div/>');
+ videoContainer.addClass('jsxc_videoContainer jsxc_minimized');
+ videoContainer.appendTo('body');
+ videoContainer.draggable({
+ containment: "parent"
+ });
+
+ var videoElement = $('<video class="jsxc_localvideo" autoplay=""></video>');
+ videoElement.appendTo(videoContainer);
+
+ videoElement[0].muted = true;
+ videoElement[0].volume = 0;
+
+ if (self.localStream) {
+ self.attachMediaStream(videoElement, self.localStream);
+ }
+
+ videoContainer.append('<div class="jsxc_controlbar"><div><div class="jsxc_hangUp jsxc_videoControl"></div></div></div></div>');
+ videoContainer.find('.jsxc_hangUp').click(function() {
+ jsxc.webrtc.hangUp('success');
+ });
+ videoContainer.click(function() {
+ videoContainer.find('.jsxc_controlbar').toggleClass('jsxc_visible');
+ });
+
+ return videoContainer;
+};
+
/**
* Display window for video call.
- *
+ *
* @memberOf jsxc.gui
*/
jsxc.gui.showVideoWindow = function(jid) {
@@ -10144,11 +11056,14 @@ jsxc.gui.showVideoWindow = function(jid) {
jsxc.gui.closeVideoWindow = function() {
var win = $('#jsxc_webrtc .jsxc_chatarea > ul > li');
- $('#jsxc_windowList > ul').prepend(win.detach());
- win.find('.slimScrollDiv').resizable('enable');
- jsxc.gui.window.resize(win);
- $('#jsxc_webrtc').remove();
+ if (win.length > 0) {
+ $('#jsxc_windowList > ul').prepend(win.detach());
+ win.find('.slimScrollDiv').resizable('enable');
+ jsxc.gui.window.resize(win);
+ }
+
+ $('#jsxc_webrtc, .jsxc_videoContainer').remove();
};
$.extend(jsxc.CONST, {
@@ -10505,6 +11420,564 @@ jsxc.xmpp.bookmarks.showDialog = function(room) {
});
};
+/**
+ * Implements XEP-0085: Chat State Notifications.
+ *
+ * @namespace jsxc.xmpp.chatState
+ * @see {@link http://xmpp.org/extensions/xep-0085.html}
+ */
+jsxc.xmpp.chatState = {
+ conn: null,
+
+ /** Delay between two notification on the message composing */
+ toComposingNotificationDelay: 900,
+};
+
+jsxc.xmpp.chatState.init = function() {
+ var self = jsxc.xmpp.chatState;
+
+ if (!jsxc.xmpp.conn || !jsxc.xmpp.connected) {
+ $(document).on('attached.jsxc', self.init);
+
+ return;
+ }
+
+ // prevent double execution after reconnect
+ $(document).off('composing.chatstates', jsxc.xmpp.chatState.onComposing);
+ $(document).off('paused.chatstates', jsxc.xmpp.chatState.onPaused);
+ $(document).off('active.chatstates', jsxc.xmpp.chatState.onActive);
+
+ if (self.isDisabled()) {
+ jsxc.debug('chat state notification disabled');
+
+ return;
+ }
+
+ self.conn = jsxc.xmpp.conn;
+
+ $(document).on('composing.chatstates', jsxc.xmpp.chatState.onComposing);
+ $(document).on('paused.chatstates', jsxc.xmpp.chatState.onPaused);
+ $(document).on('active.chatstates', jsxc.xmpp.chatState.onActive);
+};
+
+/**
+ * Composing event received. Display message.
+ *
+ * @memberOf jsxc.xmpp.chatState
+ * @param {Event} ev
+ * @param {String} jid
+ */
+jsxc.xmpp.chatState.onComposing = function(ev, jid) {
+ var self = jsxc.xmpp.chatState;
+ var bid = jsxc.jidToBid(jid);
+ var data = jsxc.storage.getUserItem('buddy', bid) || null;
+
+ if (!data || jsxc.xmpp.chatState.isDisabled()) {
+ return;
+ }
+
+ // ignore own notifications in groupchat
+ if (data.type === 'groupchat' &&
+ Strophe.getResourceFromJid(jid) === Strophe.getNodeFromJid(self.conn.jid)) {
+ return;
+ }
+
+ var user = data.type === 'groupchat' ? Strophe.getResourceFromJid(jid) : data.name;
+ var win = jsxc.gui.window.get(bid);
+
+ if (win.length === 0) {
+ return;
+ }
+
+ clearTimeout(win.data('composing-timeout'));
+
+ // add user in array if necessary
+ var usersComposing = win.data('composing') || [];
+ if (usersComposing.indexOf(user) === -1) {
+ usersComposing.push(user);
+ win.data('composing', usersComposing);
+ }
+
+ var textarea = win.find('.jsxc_textarea');
+ var composingNotif = textarea.find('.jsxc_composing');
+
+ if (composingNotif.length < 1) {
+ // notification not present, add it
+ composingNotif = $('<div>').addClass('jsxc_composing')
+ .addClass('jsxc_chatmessage')
+ .addClass('jsxc_sys')
+ .appendTo(textarea);
+ }
+
+ var msg = self._genComposingMsg(usersComposing);
+ composingNotif.text(msg);
+
+ // scroll to bottom
+ jsxc.gui.window.scrollDown(bid);
+
+ // show message
+ composingNotif.addClass('jsxc_fadein');
+};
+
+/**
+ * Pause event receive. Remove or update composing message.
+ *
+ * @memberOf jsxc.xmpp.chatState
+ * @param {Event} ev
+ * @param {String} jid
+ */
+jsxc.xmpp.chatState.onPaused = function(ev, jid) {
+ var self = jsxc.xmpp.chatState;
+ var bid = jsxc.jidToBid(jid);
+ var data = jsxc.storage.getUserItem('buddy', bid) || null;
+
+ if (!data || jsxc.xmpp.chatState.isDisabled()) {
+ return;
+ }
+
+ var user = data.type === 'groupchat' ? Strophe.getResourceFromJid(jid) : data.name;
+ var win = jsxc.gui.window.get(bid);
+
+ if (win.length === 0) {
+ return;
+ }
+
+ var el = win.find('.jsxc_composing');
+ var usersComposing = win.data('composing') || [];
+
+ if (usersComposing.indexOf(user) >= 0) {
+ // remove user from list
+ usersComposing.splice(usersComposing.indexOf(user), 1);
+ win.data('composing', usersComposing);
+ }
+
+ if (usersComposing.length === 0) {
+ var durationValue = el.css('transition-duration') || '0s';
+ var duration = parseFloat(durationValue) || 0;
+
+ if (durationValue.match(/[^m]s$/)) {
+ duration *= 1000;
+ }
+
+ el.removeClass('jsxc_fadein');
+
+ var to = setTimeout(function() {
+ el.remove();
+ }, duration);
+
+ win.data('composing-timeout', to);
+ } else {
+ // update message
+ el.text(self._genComposingMsg(usersComposing));
+ }
+};
+
+/**
+ * Active event received.
+ *
+ * @memberOf jsxc.xmpp.chatState
+ * @param {Event} ev
+ * @param {String} jid
+ */
+jsxc.xmpp.chatState.onActive = function(ev, jid) {
+ jsxc.xmpp.chatState.onPaused(ev, jid);
+};
+
+/**
+ * Send composing event.
+ *
+ * @memberOf jsxc.xmpp.chatState
+ * @param {String} bid
+ */
+jsxc.xmpp.chatState.startComposing = function(bid) {
+ var self = jsxc.xmpp.chatState;
+
+ if (!jsxc.xmpp.conn || !jsxc.xmpp.conn.chatstates || jsxc.xmpp.chatState.isDisabled()) {
+ return;
+ }
+
+ var win = jsxc.gui.window.get(bid);
+ var timeout = win.data('composing-timeout');
+ var type = win.hasClass('jsxc_groupchat') ? 'groupchat' : 'chat';
+
+ if (timeout) {
+ // @REVIEW page reload?
+ clearTimeout(timeout);
+ } else {
+ jsxc.xmpp.conn.chatstates.sendComposing(bid, type);
+ }
+
+ timeout = setTimeout(function() {
+ self.pauseComposing(bid, type);
+
+ win.data('composing-timeout', null);
+ }, self.toComposingNotificationDelay);
+
+ win.data('composing-timeout', timeout);
+};
+
+/**
+ * Send pause event.
+ *
+ * @memberOf jsxc.xmpp.chatState
+ * @param {String} bid
+ */
+jsxc.xmpp.chatState.pauseComposing = function(bid, type) {
+ if (jsxc.xmpp.chatState.isDisabled()) {
+ return;
+ }
+
+ jsxc.xmpp.conn.chatstates.sendPaused(bid, type);
+};
+
+/**
+ * End composing without sending a pause event.
+ *
+ * @memberOf jsxc.xmpp.chatState
+ * @param {String} bid
+ */
+jsxc.xmpp.chatState.endComposing = function(bid) {
+ var win = jsxc.gui.window.get(bid);
+
+ if (win.data('composing-timeout')) {
+ clearTimeout(win.data('composing-timeout'));
+ }
+};
+
+/**
+ * Generate composing message.
+ *
+ * @memberOf jsxc.xmpp.chatState
+ * @param {Array} usersComposing List of users which are currently composing a message
+ */
+jsxc.xmpp.chatState._genComposingMsg = function(usersComposing) {
+ if (!usersComposing || usersComposing.length === 0) {
+ jsxc.debug('usersComposing array is empty?');
+
+ return '';
+ } else {
+ return usersComposing.length > 1 ? usersComposing.join(', ') + $.t('_are_composing') :
+ usersComposing[0] + $.t('_is_composing');
+ }
+};
+
+jsxc.xmpp.chatState.isDisabled = function() {
+ var options = jsxc.options.get('chatState') || {};
+
+ return !options.enable;
+};
+
+$(document).on('attached.jsxc', jsxc.xmpp.chatState.init);
+
+/**
+ * Implements Http File Upload (XEP-0363)
+ *
+ * @namespace jsxc.xmpp.httpUpload
+ * @see {@link http://xmpp.org/extensions/xep-0363.html}
+ */
+jsxc.xmpp.httpUpload = {
+ conn: null,
+
+ ready: false,
+
+ CONST: {
+ NS: {
+ HTTPUPLOAD: 'urn:xmpp:http:upload'
+ }
+ }
+};
+
+/**
+ * Set up http file upload.
+ *
+ * @memberOf jsxc.xmpp.httpUpload
+ * @param {Object} o options
+ */
+jsxc.xmpp.httpUpload.init = function(o) {
+ var self = jsxc.xmpp.httpUpload;
+ self.conn = jsxc.xmpp.conn;
+
+ var fileTransferOptions = jsxc.options.get('fileTransfer') || {};
+ var options = o || jsxc.options.get('httpUpload');
+
+ if (!fileTransferOptions.httpUpload.enable) {
+ jsxc.debug('http upload disabled');
+
+ jsxc.options.set('httpUpload', false);
+
+ return;
+ }
+
+ if (options && options.server) {
+ self.ready = true;
+
+ return;
+ }
+
+ var caps = jsxc.xmpp.conn.caps;
+ var domain = jsxc.xmpp.conn.domain;
+
+ if (!caps || !domain || typeof caps._knownCapabilities[caps._jidVerIndex[domain]] === 'undefined') {
+ jsxc.debug('Waiting for server capabilities');
+
+ $(document).on('caps.strophe', function onCaps(ev, from) {
+
+ if (from !== domain) {
+ return;
+ }
+
+ self.init();
+
+ $(document).off('caps.strophe', onCaps);
+ });
+
+ return;
+ }
+
+ if (caps.hasFeatureByJid(domain, self.CONST.NS.HTTPUPLOAD)) {
+ self.discoverUploadService();
+ } else {
+ jsxc.debug(domain + ' does not support http upload');
+ }
+};
+
+/**
+ * Discover upload service for http upload.
+ *
+ * @memberOf jsxc.xmpp.httpUpload
+ */
+jsxc.xmpp.httpUpload.discoverUploadService = function() {
+ var self = jsxc.xmpp.httpUpload;
+
+ jsxc.debug('discover http upload service');
+
+ self.conn.disco.items(self.conn.domain, null, function(items) {
+ $(items).find('item').each(function() {
+ var jid = $(this).attr('jid');
+ var discovered = false;
+
+ self.conn.disco.info(jid, null, function(info) {
+ var httpUploadFeature = $(info).find('feature[var="' + self.CONST.NS.HTTPUPLOAD + '"]');
+ var httpUploadMaxSize = $(info).find('field[var="max-file-size"]');
+
+ if (httpUploadFeature.length > 0) {
+ jsxc.debug('http upload service found', jid);
+
+ jsxc.options.set('httpUpload', {
+ server: jid,
+ name: $(info).find('identity').attr('name'),
+ maxSize: parseInt(httpUploadMaxSize.text())
+ });
+
+ discovered = true;
+ self.ready = true;
+ }
+ });
+
+ return !discovered;
+ });
+ });
+};
+
+/**
+ * Upload file and send link to peer.
+ *
+ * @memberOf jsxc.xmpp.httpUpload
+ * @param {File} file
+ * @param {Message} message Preview message
+ */
+jsxc.xmpp.httpUpload.sendFile = function(file, message) {
+ jsxc.debug('Send file via http upload');
+
+ var self = jsxc.xmpp.httpUpload;
+
+ // even if the link is encrypted the file isn't
+ message.encrypted = false;
+
+ self.requestSlot(file, function(data) {
+ if (!data) {
+ // general error
+ jsxc.warn('Unknown error occured. Please check the debug log.');
+ } else if (data.error) {
+ // specific error
+ jsxc.warn('The xmpp server responded with an error of the type "' + data.error.type + '"');
+
+ message.getDOM().remove();
+
+ jsxc.gui.window.postMessage({
+ bid: message.bid,
+ direction: jsxc.Message.SYS,
+ msg: data.error.text
+ });
+
+ message.delete();
+ } else if (data.get && data.put) {
+ // slot received, start upload
+ self.uploadFile(data.put, file, message, function() {
+ var a = $('<a>');
+ a.attr('href', data.get);
+ a.attr('data-name', message.attachment.name);
+ a.attr('data-type', message.attachment.type);
+ a.attr('data-size', message.attachment.size);
+
+ if (message.attachment.thumbnail) {
+ a.attr('data-thumbnail', message.attachment.thumbnail);
+ }
+
+ a.text(data.get);
+ message.attachment.data = data.get;
+
+ message.msg = $('<span>').append(a).html();
+ message.type = jsxc.Message.HTML;
+ jsxc.gui.window.postMessage(message);
+ });
+ }
+ });
+};
+
+/**
+ * Upload the given file to the given url.
+ *
+ * @memberOf jsxc.xmpp.httpUpload
+ * @param {String} url upload url
+ * @param {File} file
+ * @param {Message} message preview message
+ * @param {Function} success_cb callback on successful transition
+ */
+jsxc.xmpp.httpUpload.uploadFile = function(url, file, message, success_cb) {
+ $.ajax({
+ url: url,
+ type: 'PUT',
+ contentType: 'application/octet-stream',
+ data: file,
+ processData: false,
+ xhr: function() {
+ var xhr = $.ajaxSettings.xhr();
+
+ // track upload progress
+ xhr.upload.onprogress = function(ev) {
+ if (ev.lengthComputable) {
+ jsxc.gui.window.updateProgress(message, ev.loaded, ev.total);
+ }
+ };
+ return xhr;
+ },
+ success: function() {
+ jsxc.debug('file successful uploaded');
+
+ // In case that upload progress is not available, inform user
+ jsxc.gui.window.updateProgress(message, 1, 1);
+
+ if (success_cb) {
+ success_cb();
+ }
+ },
+ error: function() {
+ jsxc.warn('error while uploading file to ' + url);
+
+ message.error = 'Could not upload file';
+ jsxc.gui.window.postMessage(message);
+ }
+ });
+};
+
+/**
+ * Request upload slot.
+ *
+ * @memberOf jsxc.xmpp.httpUpload
+ * @param {File} file
+ * @param {Function} cb Callback after finished request
+ */
+jsxc.xmpp.httpUpload.requestSlot = function(file, cb) {
+ var self = jsxc.xmpp.httpUpload;
+ var options = jsxc.options.get('httpUpload');
+
+ if (!options || !options.server) {
+ jsxc.warn('could not request upload slot, because I am not aware of a server or http upload is disabled');
+
+ return;
+ }
+
+ var iq = $iq({
+ to: options.server,
+ type: 'get'
+ }).c('request', {
+ xmlns: self.CONST.NS.HTTPUPLOAD
+ }).c('filename').t(file.name)
+ .up()
+ .c('size').t(file.size);
+
+ self.conn.sendIQ(iq, function(stanza) {
+ self.successfulRequestSlotCB(stanza, cb);
+ }, function(stanza) {
+ self.failedRequestSlotCB(stanza, cb);
+ });
+};
+
+/**
+ * Process successful response to slot request.
+ *
+ * @memberOf jsxc.xmpp.httpUpload
+ * @param {String} stanza
+ * @param {Function} cb
+ */
+jsxc.xmpp.httpUpload.successfulRequestSlotCB = function(stanza, cb) {
+ var self = jsxc.xmpp.httpUpload;
+ var slot = $(stanza).find('slot[xmlns="' + self.CONST.NS.HTTPUPLOAD + '"]');
+
+ if (slot.length > 0) {
+ var put = slot.find('put').text();
+ var get = slot.find('get').text();
+
+ cb({
+ put: put,
+ get: get
+ });
+ } else {
+ self.failedRequestSlotCB(stanza, cb);
+ }
+};
+
+/**
+ * Process failed response to slot request.
+ *
+ * @memberOf jsxc.xmpp.httpUpload
+ * @param {String} stanza
+ * @param {Function} cb
+ */
+jsxc.xmpp.httpUpload.failedRequestSlotCB = function(stanza, cb) {
+ if ($(stanza).find('error').length <= 0) {
+ jsxc.warn('response does not contain a slot element');
+
+ cb();
+
+ return;
+ }
+
+ var error = {
+ type: $(stanza).find('error').attr('type') || 'unknown',
+ text: $(stanza).find('error text').text()
+ };
+
+ if ($(stanza).find('error not-acceptable')) {
+ error.reason = 'not-acceptable';
+ } else if ($(stanza).find('error resource-constraint')) {
+ error.reason = 'resource-constraint';
+ } else if ($(stanza).find('error not-allowed')) {
+ error.reason = 'not-allowed';
+ }
+
+ cb({
+ error: error
+ });
+};
+
+$(document).on('stateChange.jsxc', function(ev, state) {
+ if (state === jsxc.CONST.STATE.READY) {
+ jsxc.xmpp.httpUpload.init();
+ }
+});
+
jsxc.gui.template['aboutDialog'] = '<h3>JavaScript XMPP Chat</h3>\n' +
@@ -10526,7 +11999,7 @@ jsxc.gui.template['aboutDialog'] = '<h3>JavaScript XMPP Chat</h3>\n' +
'</p>\n' +
'<p class="jsxc_libraries">\n' +
' <b>Libraries: </b>\n' +
-' <a href="http://strophe.im/strophejs/">strophe.js</a> (multiple), <a href="https://github.com/strophe/strophejs-plugins">strophe.js/muc</a> (MIT), <a href="https://github.com/strophe/strophejs-plugins">strophe.js/disco</a> (MIT), <a href="https://github.com/strophe/strophejs-plugins">strophe.js/caps</a> (MIT), <a href="https://github.com/strophe/strophejs-plugins">strophe.js/vcard</a> (MIT), <a href="https://github.com/strophe/strophejs-plugins/tree/master/bookmarks">strophe.js/bookmarks</a> (MIT), <a href="https://github.com/strophe/strophejs-plugins/tree/master/dataforms">strophe.js/x</a> (MIT), <a href="https://github.com/sualko/strophe.jinglejs">strophe.jinglejs</a> (MIT), <a href="https://github.com/neoatlantis/node-salsa20">Salsa20</a> (AGPL3), <a href="www.leemon.com">bigint</a> (public domain), <a href="code.google.com/p/crypto-js">cryptojs</a> (code.google.com/p/crypto-js/wiki/license), <a href="http://git.io/ee">eventemitter</a> (MIT), <a href="https://arlolra.github.io/otr/">otr.js</a> (MPL v2.0), <a href="http://i18next.com/">i18next</a> (MIT), <a href="http://dimsemenov.com/plugins/magnific-popup/">Magnific Popup</a> (MIT), <a href="https://github.com/ejci/favico.js">favico.js</a> (MIT), <a href="http://emojione.com">emoji one</a> (CC-BY 4.0)\n' +
+' <a href="http://strophe.im/strophejs/">strophe.js</a> (multiple), <a href="https://github.com/strophe/strophejs-plugins">strophe.js/muc</a> (MIT), <a href="https://github.com/strophe/strophejs-plugins">strophe.js/disco</a> (MIT), <a href="https://github.com/strophe/strophejs-plugins">strophe.js/caps</a> (MIT), <a href="https://github.com/strophe/strophejs-plugins">strophe.js/vcard</a> (MIT), <a href="https://github.com/strophe/strophejs-plugins/tree/master/bookmarks">strophe.js/bookmarks</a> (MIT), <a href="https://github.com/strophe/strophejs-plugins/tree/master/dataforms">strophe.js/x</a> (MIT), <a href="https://github.com/strophe/strophejs-plugins/tree/master/chatstates">strophe.js/chatstates</a> (MIT), <a href="https://github.com/sualko/strophe.jinglejs">strophe.jinglejs</a> (MIT), <a href="https://github.com/neoatlantis/node-salsa20">Salsa20</a> (AGPL3), <a href="www.leemon.com">bigint</a> (public domain), <a href="code.google.com/p/crypto-js">cryptojs</a> (code.google.com/p/crypto-js/wiki/license), <a href="http://git.io/ee">eventemitter</a> (MIT), <a href="https://arlolra.github.io/otr/">otr.js</a> (MPL v2.0), <a href="http://i18next.com/">i18next</a> (MIT), <a href="http://i18next.com/">jquery-i18next</a> (MIT), <a href="http://dimsemenov.com/plugins/magnific-popup/">Magnific Popup</a> (MIT), <a href="https://github.com/ejci/favico.js">favico.js</a> (MIT), <a href="http://emojione.com">emoji one</a> (CC-BY 4.0)\n' +
'</p>\n' +
'\n' +
'<button class="btn btn-default pull-right jsxc_debuglog">Show debug log</button>\n' +
@@ -10550,13 +12023,6 @@ jsxc.gui.template['approveDialog'] = '<h3 data-i18n="Subscription_request"></h3>
'<button class="btn btn-default jsxc_deny pull-right" data-i18n="Deny"></button>\n' +
'';
-jsxc.gui.template['authFailDialog'] = '<h3 data-i18n="Login_failed"></h3>\n' +
-'<p data-i18n="Sorry_we_cant_authentikate_"></p>\n' +
-'\n' +
-'<button class="btn btn-primary jsxc_retry pull-right" data-i18n="Continue_without_chat"></button>\n' +
-'<button class="btn btn-default jsxc_cancel pull-right" data-i18n="Retry"></button>\n' +
-'';
-
jsxc.gui.template['authenticationDialog'] = '<h3>Verification</h3>\n' +
'<p data-i18n="Authenticating_a_buddy_helps_"></p>\n' +
'<div>\n' +
@@ -10622,6 +12088,13 @@ jsxc.gui.template['authenticationDialog'] = '<h3>Verification</h3>\n' +
'</div>\n' +
'';
+jsxc.gui.template['authFailDialog'] = '<h3 data-i18n="Login_failed"></h3>\n' +
+'<p data-i18n="Sorry_we_cant_authentikate_"></p>\n' +
+'\n' +
+'<button class="btn btn-primary jsxc_retry pull-right" data-i18n="Continue_without_chat"></button>\n' +
+'<button class="btn btn-default jsxc_cancel pull-right" data-i18n="Retry"></button>\n' +
+'';
+
jsxc.gui.template['bookmarkDialog'] = '<h3 data-i18n="Edit_bookmark"></h3>\n' +
'<form class="form-horizontal">\n' +
' <div class="form-group">\n' +
@@ -10716,7 +12189,7 @@ jsxc.gui.template['chatWindow'] = '<li class="jsxc_windowItem">\n' +
' </div>\n' +
' </div>\n' +
' <div class="jsxc_transfer jsxc_otr jsxc_disabled" />\n' +
-' <input type="text" class="jsxc_textinput" data-i18n="[placeholder]Message" />\n' +
+' <textarea class="jsxc_textinput" data-i18n="[placeholder]Message"></textarea>\n' +
' </div>\n' +
' </div>\n' +
'</li>\n' +
@@ -10724,8 +12197,8 @@ jsxc.gui.template['chatWindow'] = '<li class="jsxc_windowItem">\n' +
jsxc.gui.template['confirmDialog'] = '<p data-var="msg"></p>\n' +
'\n' +
-'<button class="btn btn-primary jsxc_confirm pull-right" data-i18n="Confirm"></button>\n' +
-'<button class="btn btn-default jsxc_dismiss jsxc_close pull-right" data-i18n="Dismiss"></button>\n' +
+'<button class="jsxc_btn jsxc_btn-primary jsxc_confirm pull-right" data-i18n="Confirm"></button>\n' +
+'<button class="jsxc_btn jsxc_btn-default jsxc_dismiss jsxc_close pull-right" data-i18n="Dismiss"></button>\n' +
'';
jsxc.gui.template['contactDialog'] = '<h3 data-i18n="Add_buddy"></h3>\n' +
@@ -10771,8 +12244,8 @@ jsxc.gui.template['incomingCall'] = '<h3 data-i18n="Incoming_call"></h3>\n' +
' <span data-i18n="Do_you_want_to_accept_the_call_from"></span> <span data-var="bid_name" />?\n' +
'</p>\n' +
'\n' +
-'<button class="btn btn-primary jsxc_accept pull-right" data-i18n="Accept"></button>\n' +
-'<button class="btn btn-default jsxc_reject pull-right" data-i18n="Reject"></button>\n' +
+'<button class="jsxc_btn jsxc_btn-primary jsxc_accept pull-right" data-i18n="Accept"></button>\n' +
+'<button class="jsxc_btn jsxc_btn-default jsxc_reject pull-right" data-i18n="Reject"></button>\n' +
'';
jsxc.gui.template['joinChat'] = '<h3 data-i18n="Join_chat"></h3>\n' +
@@ -10781,16 +12254,17 @@ jsxc.gui.template['joinChat'] = '<h3 data-i18n="Join_chat"></h3>\n' +
' <div class="form-group">\n' +
' <label class="col-sm-4 control-label" for="jsxc_server" data-i18n="Server"></label>\n' +
' <div class="col-sm-8">\n' +
-' <input type="text" name="server" id="jsxc_server" class="form-control" required="required" readonly="readonly" />\n' +
+' <input type="text" name="server" id="jsxc_server" class="form-control" required="required" pattern="^[.-0-9a-zA-Z]+" />\n' +
+' <p class="jsxc_inputinfo jsxc_server jsxc_hidden"></p>\n' +
' </div>\n' +
' </div>\n' +
' <div class="form-group">\n' +
' <label class="col-sm-4 control-label" for="jsxc_room" data-i18n="Room"></label>\n' +
' <div class="col-sm-8">\n' +
' <input type="text" name="room" id="jsxc_room" class="form-control" autocomplete="off" list="jsxc_roomlist" required="required" pattern="^[^\\x22&\'\\/:<>@\\s]+" />\n' +
+' <p class="jsxc_inputinfo jsxc_room" data-i18n="Rooms_are_loaded"></p>\n' +
' </div>\n' +
' </div>\n' +
-' <p class="jsxc_inputinfo jsxc_waiting jsxc_room" data-i18n="Rooms_are_loaded"></p>\n' +
' <datalist id="jsxc_roomlist">\n' +
' <p>\n' +
' <label for="jsxc_roomlist_select"></label>\n' +
@@ -10806,7 +12280,7 @@ jsxc.gui.template['joinChat'] = '<h3 data-i18n="Join_chat"></h3>\n' +
' <input type="text" name="nickname" id="jsxc_nickname" class="form-control" />\n' +
' </div>\n' +
' </div>\n' +
-' <div class="form-group">\n' +
+' <div class="form-group jsxc_hidden">\n' +
' <label class="col-sm-4 control-label" for="jsxc_password" data-i18n="Password"></label>\n' +
' <div class="col-sm-8">\n' +
' <input type="text" name="password" id="jsxc_password" class="form-control" />\n' +
@@ -10833,7 +12307,6 @@ jsxc.gui.template['joinChat'] = '<h3 data-i18n="Join_chat"></h3>\n' +
' <div class="jsxc_msg"></div>\n' +
' <div class="form-group">\n' +
' <div class="col-sm-offset-4 col-sm-8">\n' +
-' <span class="jsxc_warning"></span>\n' +
' <button class="btn btn-default jsxc_close" data-i18n="Close"></button>\n' +
' <button class="btn btn-primary jsxc_continue" data-i18n="Continue"></button>\n' +
' <button class="btn btn-success jsxc_join" data-i18n="Join"></button>\n' +
@@ -10866,6 +12339,13 @@ jsxc.gui.template['loginBox'] = '<h3 data-i18n="Login"></h3>\n' +
'</form>\n' +
'';
+jsxc.gui.template['notification'] = '<h3></h3>\n' +
+'\n' +
+'<p class="jsxc_msg"></p>\n' +
+'\n' +
+'<p class="jsxc_meta"></p>\n' +
+'';
+
jsxc.gui.template['pleaseAccept'] = '<p data-i18n="Please_accept_"></p>\n' +
'';
@@ -11063,6 +12543,27 @@ jsxc.gui.template['settings'] = '<form class="form-horizontal col-sm-6">\n' +
' </div>\n' +
' </fieldset>\n' +
'</form>\n' +
+'\n' +
+'<form class="form-horizontal col-sm-6" data-onsubmit="xmpp.chatState.init">\n' +
+' <fieldset class="jsxc_fieldsetCarbons jsxc_fieldset">\n' +
+' <h3 data-i18n="Chat_state_notifications"></h3>\n' +
+' <p data-i18n="setting-explanation-chat-state"></p>\n' +
+' <div class="form-group">\n' +
+' <div class="col-sm-12">\n' +
+' <div class="checkbox">\n' +
+' <label>\n' +
+' <input type="checkbox" id="chatState-enable"><span data-i18n="Enable"></span>\n' +
+' </label>\n' +
+' </div>\n' +
+' </div>\n' +
+' </div>\n' +
+' <div class="form-group">\n' +
+' <div class="col-sm-12">\n' +
+' <button class="btn btn-primary jsxc_continue" type="submit" data-i18n="Save"></button>\n' +
+' </div>\n' +
+' </div>\n' +
+' </fieldset>\n' +
+'</form>\n' +
'';
jsxc.gui.template['vCard'] = '<h3>\n' +
@@ -11082,9 +12583,6 @@ jsxc.gui.template['videoWindow'] = '<div id="jsxc_webrtc">\n' +
' <video class="jsxc_localvideo" autoplay></video>\n' +
' <video class="jsxc_remotevideo" autoplay></video>\n' +
' <div class="jsxc_status"></div>\n' +
-' <div class="bubblingG">\n' +
-' <span id="bubblingG_1"> </span> <span id="bubblingG_2"> </span> <span id="bubblingG_3"> </span>\n' +
-' </div>\n' +
' <div class="jsxc_noRemoteVideo">\n' +
' <div>\n' +
' <div></div>\n' +