').append(stanza).html()
- });
- }, function() {
- jsxc.storage.setUserItem('vcard', bid, {
- state: 'error'
- });
- });
- }
+ session.decline();
+ });
- if (!jsxc.master && key.match(new RegExp('^vcard' + jsxc.storage.SEP)) && e.newValue !== null && !e.newValue.match(/^request:/)) {
- n = JSON.parse(e.newValue);
+ function acceptIncomingStream(session) {
+ jsxc.gui.dialog.close();
- if (typeof n.state !== 'undefined') {
- $(document).trigger('loaded.vcard.jsxc', n);
- }
+ jsxc.gui.showVideoWindow(session.peerID);
- jsxc.storage.removeUserItem('vcard', bid);
+ session.accept();
}
},
/**
- * Save or update buddy data.
- *
- * @memberOf jsxc.storage
- * @param bid
- * @param data
- * @returns {String} Updated or created
+ * Process incoming file offer.
+ *
+ * @param {FileSession} session
*/
- saveBuddy: function(bid, data) {
+ onIncomingFileTransfer: function(session) {
+ jsxc.debug('incoming file transfer from ' + session.peerID);
- if (jsxc.storage.getUserItem('buddy', bid)) {
- jsxc.storage.updateUserItem('buddy', bid, data);
+ var buddylist = jsxc.storage.getUserItem('buddylist') || [];
+ var bid = jsxc.jidToBid(session.peerID);
- return 'updated';
+ if (buddylist.indexOf(bid) > -1) {
+ //Accept file transfers only from contacts
+ session.accept();
+
+ var message = jsxc.gui.window.postMessage({
+ _uid: session.sid + ':msg',
+ bid: bid,
+ direction: jsxc.Message.IN,
+ attachment: {
+ name: session.receiver.metadata.name,
+ type: session.receiver.metadata.type || 'application/octet-stream'
+ }
+ });
+
+ session.receiver.on('progress', function(sent, size) {
+ jsxc.gui.window.updateProgress(message, sent, size);
+ });
}
+ },
- jsxc.storage.setUserItem('buddy', bid, $.extend({
- jid: '',
- name: '',
- status: 0,
- sub: 'none',
- msgstate: 0,
- transferReq: -1,
- trust: false,
- fingerprint: null,
- res: [],
- type: 'chat'
- }, data));
+ /**
+ * Called on incoming call.
+ *
+ * @private
+ * @memberOf jsxc.webrtc
+ * @param {MediaSession} session
+ */
+ onIncomingCall: function(session) {
+ jsxc.debug('incoming call from ' + session.peerID);
- return 'created';
- }
-};
+ var self = jsxc.webrtc;
+ var bid = jsxc.jidToBid(session.peerID);
-/* global MediaStreamTrack, File */
-/* jshint -W020 */
+ session.on('change:connectionState', $.proxy(self.onIceConnectionStateChanged, self));
-/**
- * WebRTC namespace for jsxc.
- *
- * @namespace jsxc.webrtc
- */
-jsxc.webrtc = {
- /** strophe connection */
- conn: null,
+ self.postCallMessage(bid, $.t('Incoming_call'), session.sid);
- /** local video stream */
- localStream: null,
+ // display notification
+ jsxc.notification.notify($.t('Incoming_call'), $.t('from_sender', {
+ sender: bid
+ }));
- /** remote video stream */
- remoteStream: null,
+ // send signal to partner
+ session.ring();
- /** jid of the last caller */
- last_caller: null,
+ jsxc.webrtc.last_caller = session.peerID;
- /** should we auto accept incoming calls? */
- AUTO_ACCEPT: false,
+ if (jsxc.webrtc.AUTO_ACCEPT) {
+ self.acceptIncomingCall(session);
+ return;
+ }
- /** required disco features for video call */
- reqVideoFeatures: ['urn:xmpp:jingle:apps:rtp:video', 'urn:xmpp:jingle:apps:rtp:audio', 'urn:xmpp:jingle:transports:ice-udp:1', 'urn:xmpp:jingle:apps:dtls:0'],
+ var dialog = jsxc.gui.dialog.open(jsxc.gui.template.get('incomingCall', bid), {
+ noClose: true
+ });
- /** required disco features for file transfer */
- reqFileFeatures: ['urn:xmpp:jingle:1', 'urn:xmpp:jingle:apps:file-transfer:3'],
+ dialog.find('.jsxc_accept').click(function() {
+ self.acceptIncomingCall(session);
+ });
- /** bare jid to current jid mapping */
- chatJids: {},
+ dialog.find('.jsxc_reject').click(function() {
+ jsxc.gui.dialog.close();
+ $(document).trigger('reject.call.jsxc');
+
+ session.decline();
+ });
+ },
/**
- * Initialize webrtc plugin.
- *
+ * Called on incoming call.
+ *
* @private
* @memberOf jsxc.webrtc
+ * @param {MediaSession} session
*/
- init: function() {
- var self = jsxc.webrtc;
-
- // shortcut
- self.conn = jsxc.xmpp.conn;
-
- if (!self.conn.jingle) {
- jsxc.error('No jingle plugin found!');
- return;
- }
+ acceptIncomingCall: function(session) {
+ $(document).trigger('accept.call.jsxc');
- var manager = self.conn.jingle.manager;
+ var self = jsxc.webrtc;
- $(document).on('message.jsxc', self.onMessage);
- $(document).on('presence.jsxc', self.onPresence);
+ jsxc.switchEvents({
+ 'mediaready.jingle': function(ev, stream) {
+ self.setStatus('Accept call');
- $(document).on('mediaready.jingle', self.onMediaReady);
- $(document).on('mediafailure.jingle', self.onMediaFailure);
+ self.localStream = stream;
+ self.conn.jingle.localStream = stream;
- manager.on('incoming', $.proxy(self.onIncoming, self));
+ var dialog = jsxc.gui.showVideoWindow(session.peerID);
+ dialog.find('.jsxc_videoContainer').addClass('jsxc_establishing');
- manager.on('terminated', $.proxy(self.onTerminated, self));
- manager.on('ringing', $.proxy(self.onCallRinging, self));
+ session.addStream(stream);
+ session.accept();
+ },
+ 'mediafailure.jingle': function() {
+ session.decline();
+ }
+ });
- manager.on('receivedFile', $.proxy(self.onReceivedFile, self));
+ self.reqUserMedia();
+ },
- manager.on('sentFile', function(sess, metadata) {
- jsxc.debug('sent ' + metadata.hash);
- });
+ /**
+ * 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;
- manager.on('peerStreamAdded', $.proxy(self.onRemoteStreamAdded, self));
- manager.on('peerStreamRemoved', $.proxy(self.onRemoteStreamRemoved, self));
+ if (type === 'MediaSession') {
+ self.onCallTerminated(session, reason);
+ }
+ },
- manager.on('log:*', function(level, msg) {
- jsxc.debug('[JINGLE][' + level + ']', msg);
- });
+ /**
+ * Called if call is terminated.
+ *
+ * @private
+ * @memberOf jsxc.webrtc
+ * @param {BaseSession} session
+ * @param {Object} reason Reason for termination
+ */
+ onCallTerminated: function(session, reason) {
+ var self = jsxc.webrtc;
- if (self.conn.caps) {
- $(document).on('caps.strophe', self.onCaps);
- }
+ self.setStatus('call terminated ' + session.peerID + (reason && reason.condition ? reason.condition : ''));
- var url = jsxc.options.get('RTCPeerConfig').url || jsxc.options.turnCredentialsPath;
- var peerConfig = jsxc.options.get('RTCPeerConfig');
+ var bid = jsxc.jidToBid(session.peerID);
- if (typeof url === 'string' && url.length > 0) {
- self.getTurnCrendentials(url);
- } else {
- if (jsxc.storage.getUserItem('iceValidity')) {
- // old ice validity found. Clean up.
- jsxc.storage.removeUserItem('iceValidity');
-
- // Replace saved servers with the once passed to jsxc
- peerConfig.iceServers = jsxc.options.RTCPeerConfig.iceServers;
- jsxc.options.set('RTCPeerConfig', peerConfig);
+ 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');
}
+ }
- self.conn.jingle.setICEServers(peerConfig.iceServers);
+ // @REVIEW necessary?
+ if ($('.jsxc_remotevideo').length) {
+ $('.jsxc_remotevideo')[0].src = "";
}
- },
- onConnected: function() {
- //Request new credentials after login
- jsxc.storage.removeUserItem('iceValidity');
- },
+ if ($('.jsxc_localvideo').length) {
+ $('.jsxc_localvideo')[0].src = "";
+ }
- onDisconnected: function() {
- var self = jsxc.webrtc;
+ self.conn.jingle.localStream = null;
+ self.localStream = null;
+ self.remoteStream = null;
- $(document).off('message.jsxc', self.onMessage);
- $(document).off('presence.jsxc', self.onPresence);
+ jsxc.gui.closeVideoWindow();
- $(document).off('mediaready.jingle', self.onMediaReady);
- $(document).off('mediafailure.jingle', self.onMediaFailure);
+ // Close incoming call dialog and stop ringing
+ jsxc.gui.dialog.close();
+ $(document).trigger('reject.call.jsxc');
- $(document).off('caps.strophe', self.onCaps);
+ $(document).off('error.jingle');
+
+ 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');
},
/**
- * Checks if cached configuration is valid and if necessary update it.
- *
+ * Called if we receive a remote stream.
+ *
+ * @private
* @memberOf jsxc.webrtc
- * @param {string} [url]
+ * @param {BaseSession} session
+ * @param {Object} stream
*/
- getTurnCrendentials: function(url) {
+ onRemoteStreamAdded: function(session, stream) {
var self = jsxc.webrtc;
- url = url || jsxc.options.get('RTCPeerConfig').url || jsxc.options.turnCredentialsPath;
- var ttl = (jsxc.storage.getUserItem('iceValidity') || 0) - (new Date()).getTime();
+ self.setStatus('Remote stream for session ' + session.sid + ' added.');
- // validity from jsxc < 2.1.0 is invalid
- if (jsxc.storage.getUserItem('iceConfig')) {
- jsxc.storage.removeUserItem('iceConfig');
- ttl = -1;
- }
+ self.remoteStream = stream;
- if (ttl > 0) {
- // credentials valid
+ var isVideoDevice = stream.getVideoTracks().length > 0;
+ var isAudioDevice = stream.getAudioTracks().length > 0;
- self.conn.jingle.setICEServers(jsxc.options.get('RTCPeerConfig').iceServers);
+ self.setStatus(isVideoDevice ? 'Use remote video device.' : 'No remote video device');
+ self.setStatus(isAudioDevice ? 'Use remote audio device.' : 'No remote audio device');
- window.setTimeout(jsxc.webrtc.getTurnCrendentials, ttl + 500);
- return;
- }
+ if ($('.jsxc_remotevideo').length) {
+ self.attachMediaStream($('#jsxc_webrtc .jsxc_remotevideo'), stream);
- $.ajax(url, {
- async: true,
- xhrFields: {
- withCredentials: jsxc.options.get('RTCPeerConfig').withCredentials
- },
- success: function(data) {
- var ttl = data.ttl || 3600;
- var iceServers = data.iceServers;
+ $('#jsxc_webrtc .jsxc_' + (isVideoDevice ? 'remotevideo' : 'noRemoteVideo')).addClass('jsxc_deviceAvailable');
+ }
+ },
- if (!iceServers && data.url) {
- // parse deprecated (v2.1.0) syntax
- jsxc.warn('Received RTCPeer configuration is deprecated. Use now RTCPeerConfig.url.');
+ /**
+ * Attach media stream to element.
+ *
+ * @memberOf jsxc.webrtc
+ * @param element {Element|jQuery}
+ * @param stream {mediastream}
+ */
+ attachMediaStream: function(element, stream) {
+ var el = (element instanceof jQuery) ? element.get(0) : element;
+ el.srcObject = stream;
- iceServers = [{
- urls: data.url
- }];
+ $(element).show();
+ },
- if (data.username) {
- iceServers[0].username = data.username;
- }
+ /**
+ * Called if the remote stream was removed.
+ *
+ * @private
+ * @meberOf jsxc.webrtc
+ * @param {BaseSession} session
+ */
+ onRemoteStreamRemoved: function(session) {
+ this.setStatus('Remote stream for ' + session.jid + ' removed.');
- if (data.credential) {
- iceServers[0].credential = data.credential;
- }
- }
+ //TODO clean up
+ },
- if (iceServers && iceServers.length > 0) {
- // url as parameter is deprecated
- var url = iceServers[0].url && iceServers[0].url.length > 0;
- var urls = iceServers[0].urls && iceServers[0].urls.length > 0;
+ /**
+ * Display information according to the connection state.
+ *
+ * @private
+ * @memberOf jsxc.webrtc
+ * @param {BaseSession} session
+ * @param {String} state
+ */
+ onIceConnectionStateChanged: function(session, state) {
+ var self = jsxc.webrtc;
- if (urls || url) {
- jsxc.debug('ice servers received');
+ jsxc.debug('connection state for ' + session.sid, state);
- var peerConfig = jsxc.options.get('RTCPeerConfig');
- peerConfig.iceServers = iceServers;
- jsxc.options.set('RTCPeerConfig', peerConfig);
+ if (state === 'connected') {
+ $('#jsxc_webrtc .jsxc_deviceAvailable').show();
+ } else if (state === 'failed') {
+ jsxc.gui.window.postMessage({
+ bid: jsxc.jidToBid(session.peerID),
+ direction: jsxc.Message.SYS,
+ msg: $.t('ICE_connection_failure')
+ });
- self.conn.jingle.setICEServers(iceServers);
+ session.end('failed-transport');
- jsxc.storage.setUserItem('iceValidity', (new Date()).getTime() + 1000 * ttl);
- } else {
- jsxc.warn('No valid url found in first ice object.');
- }
- }
- },
- dataType: 'json'
- });
+ $(document).trigger('callterminated.jingle');
+ } else if (state === 'interrupted') {
+ self.setStatus($.t('Connection_interrupted'));
+ }
},
/**
- * Return list of capable resources.
- *
+ * Start a call to the specified jid.
+ *
* @memberOf jsxc.webrtc
- * @param jid
- * @param {(string|string[])} features list of required features
- * @returns {Array}
+ * @param {String} jid full jid
+ * @param {String[]} um requested user media
*/
- getCapableRes: function(jid, features) {
+ startCall: function(jid, um) {
var self = jsxc.webrtc;
- var bid = jsxc.jidToBid(jid);
- var res = Object.keys(jsxc.storage.getUserItem('res', bid) || {}) || [];
- if (!features) {
- return res;
- } else if (typeof features === 'string') {
- features = [features];
+ if (Strophe.getResourceFromJid(jid) === null) {
+ jsxc.debug('We need a full jid');
+ return;
}
- var available = [];
- $.each(res, function(i, r) {
- if (self.conn.caps.hasFeatureByJid(bid + '/' + r, features)) {
- available.push(r);
+ self.last_caller = jid;
+
+ jsxc.switchEvents({
+ 'mediaready.jingle': function(ev, stream) {
+ jsxc.debug('media ready for outgoing call');
+
+ self.initiateOutgoingCall(jid, stream);
+ },
+ 'mediafailure.jingle': function() {
+ jsxc.gui.dialog.close();
}
});
- return available;
+ self.reqUserMedia(um);
},
/**
- * Add "video" button to window menu.
- *
- * @private
- * @memberOf jsxc.webrtc
- * @param event
- * @param win jQuery window object
+ * Start jingle session to jid with stream.
+ *
+ * @param {String} jid
+ * @param {Object} stream
*/
- initWindow: function(event, win) {
+ initiateOutgoingCall: function(jid, stream) {
var self = jsxc.webrtc;
- if (win.hasClass('jsxc_groupchat')) {
- return;
- }
+ self.localStream = stream;
+ self.conn.jingle.localStream = stream;
- jsxc.debug('webrtc.initWindow');
+ var dialog = jsxc.gui.showVideoWindow(jid);
- if (!self.conn) {
- $(document).one('attached.jsxc', function() {
- self.initWindow(null, win);
- });
- return;
- }
+ dialog.find('.jsxc_videoContainer').addClass('jsxc_establishing');
- var div = $('
').addClass('jsxc_video');
- win.find('.jsxc_tools .jsxc_settings').after(div);
+ self.setStatus('Initiate call');
- self.updateIcon(win.data('bid'));
+ // @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);
},
/**
- * Enable or disable "video" icon and assign full jid.
- *
+ * Hang up the current call.
+ *
* @memberOf jsxc.webrtc
- * @param bid CSS conform jid
*/
- updateIcon: function(bid) {
- jsxc.debug('Update icon', bid);
-
- var self = jsxc.webrtc;
-
- if (bid === jsxc.jidToBid(self.conn.jid)) {
- return;
+ hangUp: function(reason, text) {
+ if (jsxc.webrtc.conn.jingle.manager && !$.isEmptyObject(jsxc.webrtc.conn.jingle.manager.peers)) {
+ jsxc.webrtc.conn.jingle.terminate(null, reason, text);
+ } else {
+ jsxc.gui.closeVideoWindow();
}
- var win = jsxc.gui.window.get(bid);
- var jid = win.data('jid');
- var ls = jsxc.storage.getUserItem('buddy', bid);
+ // @TODO check event
+ $(document).trigger('callterminated.jingle');
+ },
- if (typeof jid !== 'string') {
- if (ls && typeof ls.jid === 'string') {
- jid = ls.jid;
- } else {
- jsxc.debug('[webrtc] Could not update icon, because could not find jid for ' + bid);
- return;
- }
+ /**
+ * 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;
}
- var res = Strophe.getResourceFromJid(jid);
+ self.last_caller = jid;
- var el = win.find('.jsxc_video');
+ jsxc.switchEvents({
+ 'mediaready.jingle': function(ev, stream) {
+ self.initiateScreenSharing(jid, stream);
+ },
+ 'mediafailure.jingle': function(ev, err) {
+ jsxc.gui.dialog.close();
- var capableRes = self.getCapableRes(jid, self.reqVideoFeatures);
- var targetRes = res;
+ var browser = self.conn.jingle.RTC.webrtcDetectedBrowser;
- if (targetRes === null) {
- $.each(jsxc.storage.getUserItem('buddy', bid).res || [], function(index, val) {
- if (capableRes.indexOf(val) > -1) {
- targetRes = val;
- return false;
+ 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);
}
- });
+ }
+ });
- jid = jid + '/' + targetRes;
- }
+ self.reqUserMedia(['screen']);
+ },
- el.off('click');
+ /**
+ * 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);
- if (capableRes.indexOf(targetRes) > -1) {
- el.click(function() {
- self.startCall(jid);
- });
+ jsxc.webrtc.localStream = stream;
+ jsxc.webrtc.conn.jingle.localStream = stream;
- el.removeClass('jsxc_disabled');
+ var container = jsxc.gui.showMinimizedVideoWindow();
+ container.addClass('jsxc_establishing');
- el.attr('title', $.t('Start_video_call'));
- } else {
- el.addClass('jsxc_disabled');
+ self.setStatus('Initiate stream');
- el.attr('title', $.t('Video_call_not_possible'));
- }
+ $(document).one('error.jingle', function(e, sid, error) {
+ if (error && error.source !== 'offer') {
+ return;
+ }
- var fileCapableRes = self.getCapableRes(jid, self.reqFileFeatures);
- var resources = Object.keys(jsxc.storage.getUserItem('res', bid) || {}) || [];
+ setTimeout(function() {
+ jsxc.gui.showAlert("Sorry, we couldn't establish a connection. Maybe your buddy is offline.");
+ }, 500);
+ });
- if (fileCapableRes.indexOf(res) > -1 || (res === null && fileCapableRes.length === 1 && resources.length === 1)) {
- win.find('.jsxc_sendFile').removeClass('jsxc_disabled');
+ 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 {
- win.find('.jsxc_sendFile').addClass('jsxc_disabled');
+ constraints = {
+ 'offerToReceiveAudio': false,
+ 'offerToReceiveVideo': false
+ };
}
- },
- /**
- * Check if full jid changed.
- *
- * @private
- * @memberOf jsxc.webrtc
- * @param e
- * @param from full jid
- */
- onMessage: function(e, from) {
- var self = jsxc.webrtc;
- var bid = jsxc.jidToBid(from);
+ var session = self.conn.jingle.initiate(jid, undefined, constraints);
+ session.call = false;
- jsxc.debug('webrtc.onmessage', from);
+ session.on('change:connectionState', $.proxy(self.onIceConnectionStateChanged, self));
+ // @REVIEW also for calls?
+ session.on('accepted', function() {
+ self.onSessionAccepted(session);
+ });
- if (self.chatJids[bid] !== from) {
- self.updateIcon(bid);
- self.chatJids[bid] = from;
- }
+ self.postScreenMessage(bid, $.t('Stream_started'), session.sid);
},
/**
- * Update icon on presence.
- *
- * @memberOf jsxc.webrtc
- * @param ev
- * @param status
- * @private
+ * Session was accepted by other peer.
+ *
+ * @param {BaseSession} session
*/
- onPresence: function(ev, jid, status, presence) {
+ onSessionAccepted: function(session) {
var self = jsxc.webrtc;
- if ($(presence).find('c[xmlns="' + Strophe.NS.CAPS + '"]').length === 0) {
- jsxc.debug('webrtc.onpresence', jid);
+ $('.jsxc_videoContainer').removeClass('jsxc_ringing');
- self.updateIcon(jsxc.jidToBid(jid));
- }
+ self.postScreenMessage(jsxc.jidToBid(session.peerID), $.t('Connection_accepted'), session.sid);
},
/**
- * Display status message to user.
- *
+ * Request media from local user.
+ *
* @memberOf jsxc.webrtc
- * @param txt message
- * @param d duration in ms
*/
- setStatus: function(txt, d) {
- var status = $('.jsxc_webrtc .jsxc_status');
- var duration = (typeof d === 'undefined' || d === null) ? 4000 : d;
-
- jsxc.debug('[Webrtc]', txt);
-
- if (status.html()) {
- // attach old messages
- txt = status.html() + '
' + txt;
+ reqUserMedia: function(um) {
+ if (this.localStream) {
+ $(document).trigger('mediaready.jingle', [this.localStream]);
+ return;
}
- status.html(txt);
-
- status.css({
- 'margin-left': '-' + (status.width() / 2) + 'px',
- opacity: 0,
- display: 'block'
- });
+ um = um || ['video', 'audio'];
- status.stop().animate({
- opacity: 1
+ jsxc.gui.dialog.open(jsxc.gui.template.get('allowMediaAccess'), {
+ noClose: true
});
- clearTimeout(status.data('timeout'));
-
- if (duration === 0) {
- return;
+ 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);
}
- var to = setTimeout(function() {
- status.stop().animate({
- opacity: 0
- }, function() {
- status.html('');
+ function filterUserMedia(devices) {
+ var availableDevices = devices.map(function(device) {
+ return device.kind;
});
- }, duration);
- status.data('timeout', to);
+ um = um.filter(function(el) {
+ return availableDevices.indexOf(el) !== -1 || availableDevices.indexOf(el + 'input') !== -1;
+ });
+
+ if (um.length) {
+ jsxc.webrtc.getUserMedia(um);
+ } else {
+ jsxc.warn('No audio/video device available.');
+ }
+ }
},
/**
- * Update "video" button if we receive cap information.
- *
- * @private
+ * Get user media from local browser.
+ *
* @memberOf jsxc.webrtc
- * @param event
- * @param jid
*/
- onCaps: function(event, jid) {
+ getUserMedia: function(um) {
var self = jsxc.webrtc;
+ var constraints = {};
- if (jsxc.gui.roster.loaded) {
- self.updateIcon(jsxc.jidToBid(jid));
- } else {
- $(document).on('cloaded.roster.jsxc', function() {
- self.updateIcon(jsxc.jidToBid(jid));
- });
+ if (um.indexOf('video') > -1) {
+ constraints.video = true;
+ }
+
+ if (um.indexOf('audio') > -1) {
+ constraints.audio = true;
+ }
+
+ try {
+ self.conn.jingle.getUserMedia(constraints, self.userMediaCallback);
+ } catch (e) {
+ jsxc.error('GUM failed: ', e);
+ $(document).trigger('mediafailure.jingle');
+ }
+ },
+
+ userMediaCallback: function(err, stream) {
+ if (err) {
+ jsxc.warn('Failed to get access to local media. Error ', err);
+ $(document).trigger('mediafailure.jingle', [err]);
+ } else if (stream) {
+ jsxc.debug('onUserMediaSuccess');
+ $(document).trigger('mediaready.jingle', [stream]);
}
},
/**
- * Called if video/audio is ready. Open window and display some messages.
- *
- * @private
+ * Get screen media from local browser.
+ *
* @memberOf jsxc.webrtc
- * @param event
- * @param stream
*/
- onMediaReady: function(event, stream) {
- jsxc.debug('media ready');
-
+ getScreenMedia: function() {
var self = jsxc.webrtc;
- self.localStream = stream;
- self.conn.jingle.localStream = stream;
+ jsxc.debug('get screen media');
+
+ self.conn.jingle.getScreenMedia(self.screenMediaCallback);
+ },
- var dialog = jsxc.gui.showVideoWindow(self.last_caller);
+ screenMediaCallback: function(err, stream) {
+ if (err) {
+ $(document).trigger('mediafailure.jingle', [err]);
- var audioTracks = stream.getAudioTracks();
- var videoTracks = stream.getVideoTracks();
- var i;
+ return;
+ }
- for (i = 0; i < audioTracks.length; i++) {
- self.setStatus((audioTracks.length > 0) ? $.t('Use_local_audio_device') : $.t('No_local_audio_device'));
+ 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;
+ },
- jsxc.debug('using audio device "' + audioTracks[i].label + '"');
+ /**
+ * Make a snapshot from a video stream and display it.
+ *
+ * @memberOf jsxc.webrtc
+ * @param video Video stream
+ */
+ snapshot: function(video) {
+ if (!video) {
+ jsxc.debug('Missing video element');
}
- for (i = 0; i < videoTracks.length; i++) {
- self.setStatus((videoTracks.length > 0) ? $.t('Use_local_video_device') : $.t('No_local_video_device'));
+ $('.jsxc_snapshotbar p').remove();
+
+ var canvas = $('
').css('display', 'none').appendTo('body').attr({
+ width: video.width(),
+ height: video.height()
+ }).get(0);
+ var ctx = canvas.getContext('2d');
- jsxc.debug('using video device "' + videoTracks[i].label + '"');
+ ctx.drawImage(video[0], 0, 0);
+ var img = $('
');
+ var url = null;
- dialog.find('.jsxc_localvideo').show();
+ try {
+ url = canvas.toDataURL('image/jpeg');
+ } catch (err) {
+ jsxc.warn('Error', err);
+ return;
}
- $(document).one('cleanup.dialog.jsxc', $.proxy(self.hangUp, self));
- $(document).trigger('finish.mediaready.jsxc');
+ img[0].src = url;
+ var link = $('
').attr({
+ target: '_blank',
+ href: url
+ });
+ link.append(img);
+ $('.jsxc_snapshotbar').append(link);
+
+ canvas.remove();
},
/**
- * Called if media failes.
- *
- * @private
+ * Send file to full jid via jingle.
+ *
* @memberOf jsxc.webrtc
+ * @param {string} jid full jid
+ * @param {file} file
+ * @return {object} session
*/
- onMediaFailure: function(ev, err) {
+ sendFile: function(jid, file) {
+ jsxc.debug('Send file via webrtc');
+
var self = jsxc.webrtc;
- err = err || {
- name: 'Undefined'
- };
- self.setStatus('media failure');
+ if (!Strophe.getResourceFromJid(jid)) {
+ jsxc.warn('Require full jid to send file via webrtc');
- jsxc.gui.window.postMessage({
- bid: jsxc.jidToBid(jsxc.webrtc.last_caller),
- direction: jsxc.Message.SYS,
- msg: $.t('Media_failure') + ': ' + $.t(err.name) + ' (' + err.name + ').'
+ return;
+ }
+
+ var sess = self.conn.jingle.manager.createFileTransferSession(jid);
+
+ sess.on('change:sessionState', function() {
+ jsxc.debug('Session state', sess.state);
+ });
+ sess.on('change:connectionState', function() {
+ jsxc.debug('Connection state', sess.connectionState);
});
- jsxc.debug('media failure: ' + err.name);
+ sess.start(file);
+
+ return sess;
},
- onIncoming: function(session) {
- var self = jsxc.webrtc;
- var type = (session.constructor) ? session.constructor.name : null;
+ /**
+ * Display received file.
+ *
+ * @memberOf jsxc.webrtc
+ * @param {object} sess
+ * @param {File} file
+ * @param {object} metadata file metadata
+ */
+ onReceivedFile: function(sess, file, metadata) {
+ jsxc.debug('file received', metadata);
- if (type === 'FileTransferSession') {
- self.onIncomingFileTransfer(session);
- } else if (type === 'MediaSession') {
- self.onIncomingCall(session);
+ if (!FileReader) {
+ return;
+ }
+
+ var reader = new FileReader();
+ var type;
+
+ if (!metadata.type) {
+ // 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();
+
+ switch (ext) {
+ case 'jpg':
+ case 'jpeg':
+ case 'png':
+ case 'gif':
+ case 'svg':
+ type = 'image/' + ext.replace(/^jpg$/, 'jpeg');
+ break;
+ case 'mp3':
+ case 'wav':
+ type = 'audio/' + ext;
+ break;
+ case 'pdf':
+ type = 'application/pdf';
+ break;
+ case 'txt':
+ type = 'text/' + ext;
+ break;
+ default:
+ type = 'application/octet-stream';
+ }
+ } else {
+ type = metadata.type;
}
- },
- onIncomingFileTransfer: function(session) {
- jsxc.debug('incoming file transfer from ' + session.peerID);
-
- var buddylist = jsxc.storage.getUserItem('buddylist') || [];
- var bid = jsxc.jidToBid(session.peerID);
-
- if (buddylist.indexOf(bid) > -1) {
- //Accept file transfers only from contacts
- session.accept();
+ reader.onload = function(ev) {
+ // modify element with uid metadata.actualhash
- var message = jsxc.gui.window.postMessage({
- _uid: session.sid + ':msg',
- bid: bid,
+ jsxc.gui.window.postMessage({
+ _uid: sess.sid + ':msg',
+ bid: jsxc.jidToBid(sess.peerID),
direction: jsxc.Message.IN,
attachment: {
- name: session.receiver.metadata.name,
- type: session.receiver.metadata.type || 'application/octet-stream'
+ name: metadata.name,
+ type: type,
+ size: metadata.size,
+ data: ev.target.result
}
});
+ };
- session.receiver.on('progress', function(sent, size) {
- jsxc.gui.window.updateProgress(message, sent, size);
+ if (!file.type) {
+ // file type should be handled in lib
+ file = new File([file], metadata.name, {
+ type: type
});
}
- },
- /**
- * Called on incoming call.
- *
- * @private
- * @memberOf jsxc.webrtc
- * @param event
- * @param sid Session id
- */
- onIncomingCall: function(session) {
- jsxc.debug('incoming call from ' + session.peerID);
+ reader.readAsDataURL(file);
+ }
+};
- var self = jsxc.webrtc;
- var bid = jsxc.jidToBid(session.peerID);
+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
+ });
+};
- session.on('change:connectionState', $.proxy(self.onIceConnectionStateChanged, self));
+jsxc.gui.showMinimizedVideoWindow = function() {
+ var self = jsxc.webrtc;
- jsxc.gui.window.postMessage({
- bid: bid,
- direction: jsxc.Message.SYS,
- msg: $.t('Incoming_call')
- });
+ // needed to trigger complete.dialog.jsxc
+ jsxc.gui.dialog.close();
- // display notification
- jsxc.notification.notify($.t('Incoming_call'), $.t('from_sender', {
- sender: bid
- }));
+ var videoContainer = $('
');
+ videoContainer.addClass('jsxc_videoContainer jsxc_minimized');
+ videoContainer.appendTo('body');
- // send signal to partner
- session.ring();
+ var videoElement = $('
');
+ videoElement.appendTo(videoContainer);
- jsxc.webrtc.last_caller = session.peerID;
+ videoElement[0].muted = true;
+ videoElement[0].volume = 0;
- if (jsxc.webrtc.AUTO_ACCEPT) {
- self.reqUserMedia();
- return;
- }
+ if (self.localStream) {
+ self.attachMediaStream(videoElement, self.localStream);
+ }
- var dialog = jsxc.gui.dialog.open(jsxc.gui.template.get('incomingCall', bid), {
- noClose: true
- });
+ videoContainer.append('
');
+ videoContainer.find('.jsxc_hangUp').click(function() {
+ jsxc.webrtc.hangUp('success');
+ });
+ videoContainer.click(function() {
+ videoContainer.find('.jsxc_controlbar').toggleClass('jsxc_visible');
+ });
- dialog.find('.jsxc_accept').click(function() {
- $(document).trigger('accept.call.jsxc');
+ return videoContainer;
+};
- jsxc.switchEvents({
- 'mediaready.jingle': function(event, stream) {
- self.setStatus('Accept call');
+/**
+ * Display window for video call.
+ *
+ * @memberOf jsxc.gui
+ */
+jsxc.gui.showVideoWindow = function(jid) {
+ var self = jsxc.webrtc;
- session.addStream(stream);
+ // needed to trigger complete.dialog.jsxc
+ jsxc.gui.dialog.close();
- session.accept();
- },
- 'mediafailure.jingle': function() {
- session.decline();
- }
- });
+ $('body').append(jsxc.gui.template.get('videoWindow'));
- self.reqUserMedia();
- });
+ // mute own video element to avoid echoes
+ $('#jsxc_webrtc .jsxc_localvideo')[0].muted = true;
+ $('#jsxc_webrtc .jsxc_localvideo')[0].volume = 0;
- dialog.find('.jsxc_reject').click(function() {
- jsxc.gui.dialog.close();
- $(document).trigger('reject.call.jsxc');
+ var rv = $('#jsxc_webrtc .jsxc_remotevideo');
+ var lv = $('#jsxc_webrtc .jsxc_localvideo');
- session.decline();
- });
- },
+ if (self.localStream) {
+ self.attachMediaStream(lv, self.localStream);
+ }
- onTerminated: function(session, reason) {
- var self = jsxc.webrtc;
- var type = (session.constructor) ? session.constructor.name : null;
+ var w_dialog = $('#jsxc_webrtc').width();
+ var w_remote = rv.width();
- if (type === 'MediaSession') {
- self.onCallTerminated(session, reason);
- }
- },
+ // fit in video
+ if (w_remote > w_dialog) {
+ var scale = w_dialog / w_remote;
+ var new_h = rv.height() * scale;
+ var new_w = w_dialog;
+ var vc = $('#jsxc_webrtc .jsxc_videoContainer');
- /**
- * Called if call is terminated.
- *
- * @private
- * @memberOf jsxc.webrtc
- * @param event
- * @param sid Session id
- * @param reason Reason for termination
- * @param [text] Optional explanation
- */
- onCallTerminated: function(session, reason) {
- this.setStatus('call terminated ' + session.peerID + (reason ? reason.condition : ''));
+ rv.height(new_h);
+ rv.width(new_w);
- var bid = jsxc.jidToBid(session.peerID);
+ vc.height(new_h);
+ vc.width(new_w);
- if (this.localStream) {
- if (typeof this.localStream.stop === 'function') {
- this.localStream.stop();
- } else {
- var tracks = this.localStream.getTracks();
- tracks.forEach(function(track) {
- track.stop();
- });
- }
- }
+ lv.height(lv.height() * scale);
+ lv.width(lv.width() * scale);
+ }
- if ($('.jsxc_videoContainer').length) {
- $('.jsxc_remotevideo')[0].src = "";
- $('.jsxc_localvideo')[0].src = "";
- }
+ if (self.remoteStream) {
+ self.attachMediaStream(rv, self.remoteStream);
- this.conn.jingle.localStream = null;
- this.localStream = null;
- this.remoteStream = null;
+ $('#jsxc_webrtc .jsxc_' + (self.remoteStream.getVideoTracks().length > 0 ? 'remotevideo' : 'noRemoteVideo')).addClass('jsxc_deviceAvailable');
+ }
- jsxc.gui.closeVideoWindow();
+ var win = jsxc.gui.window.open(jsxc.jidToBid(jid));
- $(document).off('error.jingle');
+ win.find('.slimScrollDiv').resizable('disable');
+ jsxc.gui.window.resize(win, {
+ size: {
+ width: $('#jsxc_webrtc .jsxc_chatarea').width(),
+ height: $('#jsxc_webrtc .jsxc_chatarea').height()
+ }
+ }, true);
- jsxc.gui.window.postMessage({
- bid: bid,
- direction: jsxc.Message.SYS,
- msg: ($.t('Call_terminated') + (reason ? (': ' + $.t('jingle_reason_' + reason.condition)) : '') + '.')
- });
- },
+ $('#jsxc_webrtc .jsxc_chatarea ul').append(win.detach());
- /**
- * Remote station is ringing.
- *
- * @private
- * @memberOf jsxc.webrtc
- */
- onCallRinging: function() {
- this.setStatus('ringing...', 0);
- },
+ $('#jsxc_webrtc .jsxc_hangUp').click(function() {
+ jsxc.webrtc.hangUp('success');
+ });
- /**
- * Called if we receive a remote stream.
- *
- * @private
- * @memberOf jsxc.webrtc
- * @param event
- * @param data
- * @param sid Session id
- */
- onRemoteStreamAdded: function(session, stream) {
- this.setStatus('Remote stream for session ' + session.sid + ' added.');
+ $('#jsxc_webrtc .jsxc_fullscreen').click(function() {
- this.remoteStream = stream;
+ if ($.support.fullscreen) {
+ // Reset position of localvideo
+ $(document).one('disabled.fullscreen', function() {
+ lv.removeAttr('style');
+ });
- var isVideoDevice = stream.getVideoTracks().length > 0;
- var isAudioDevice = stream.getAudioTracks().length > 0;
+ $('#jsxc_webrtc .jsxc_videoContainer').fullscreen();
+ }
+ });
- this.setStatus(isVideoDevice ? 'Use remote video device.' : 'No remote video device');
- this.setStatus(isAudioDevice ? 'Use remote audio device.' : 'No remote audio device');
+ $('#jsxc_webrtc .jsxc_videoContainer').click(function() {
+ $('#jsxc_webrtc .jsxc_controlbar').toggleClass('jsxc_visible');
+ });
- if ($('.jsxc_remotevideo').length) {
- this.attachMediaStream($('#jsxc_webrtc .jsxc_remotevideo'), stream);
+ return $('#jsxc_webrtc');
+};
- $('#jsxc_webrtc .jsxc_' + (isVideoDevice ? 'remotevideo' : 'noRemoteVideo')).addClass('jsxc_deviceAvailable');
- }
- },
+jsxc.gui.closeVideoWindow = function() {
+ var win = $('#jsxc_webrtc .jsxc_chatarea > ul > li');
- /**
- * Attach media stream to element.
- *
- * @memberOf jsxc.webrtc
- * @param element {Element|jQuery}
- * @param stream {mediastream}
- */
- attachMediaStream: function(element, stream) {
- var self = jsxc.webrtc;
+ if (win.length > 0) {
+ $('#jsxc_windowList > ul').prepend(win.detach());
+ win.find('.slimScrollDiv').resizable('enable');
+ jsxc.gui.window.resize(win);
+ }
- self.conn.jingle.RTC.attachMediaStream((element instanceof jQuery) ? element.get(0) : element, stream);
- },
+ $('#jsxc_webrtc, .jsxc_videoContainer').remove();
+};
- /**
- * Called if the remote stream was removed.
- *
- * @private
- * @meberOf jsxc.webrtc
- * @param event
- * @param data
- * @param sid Session id
- */
- onRemoteStreamRemoved: function(session) {
- this.setStatus('Remote stream for ' + session.jid + ' removed.');
+$.extend(jsxc.CONST, {
+ KEYCODE_ENTER: 13,
+ KEYCODE_ESC: 27
+});
- //TODO clean up
- },
+$(document).ready(function() {
+ $(document).on('init.window.jsxc', jsxc.webrtc.initWindow);
+ $(document).on('attached.jsxc', jsxc.webrtc.init);
+ $(document).on('disconnected.jsxc', jsxc.webrtc.onDisconnected);
+ $(document).on('connected.jsxc', jsxc.webrtc.onConnected);
+});
+
+/**
+ * Load and save bookmarks according to XEP-0048.
+ *
+ * @namespace jsxc.xmpp.bookmarks
+ */
+jsxc.xmpp.bookmarks = {};
+
+/**
+ * Determines if server is able to store bookmarks.
+ *
+ * @return {boolean} True: Server supports bookmark storage
+ */
+jsxc.xmpp.bookmarks.remote = function() {
+ return jsxc.xmpp.conn.caps && jsxc.xmpp.hasFeatureByJid(jsxc.xmpp.conn.domain, Strophe.NS.PUBSUB + "#publish");
+};
- /**
- * Extracts local and remote ip and display it to the user.
- *
- * @private
- * @memberOf jsxc.webrtc
- * @param event
- * @param sid session id
- * @param sess
- */
- onIceConnectionStateChanged: function(session, state) {
- var self = jsxc.webrtc;
+/**
+ * Load bookmarks from pubsub.
+ *
+ * @memberOf jsxc.xmpp.bookmarks
+ */
+jsxc.xmpp.bookmarks.load = function() {
+ var caps = jsxc.xmpp.conn.caps;
+ var ver = caps._jidVerIndex[jsxc.xmpp.conn.domain];
- jsxc.debug('connection state for ' + session.sid, state);
+ if (!ver || !caps._knownCapabilities[ver]) {
+ // wait until we know server capabilities
+ $(document).on('caps.strophe', function(ev, from) {
+ if (from === jsxc.xmpp.conn.domain) {
+ jsxc.xmpp.bookmarks.load();
- if (state === 'connected') {
+ $(document).off(ev);
+ }
+ });
+ }
- $('#jsxc_webrtc .jsxc_deviceAvailable').show();
- $('#jsxc_webrtc .bubblingG').hide();
+ if (jsxc.xmpp.bookmarks.remote()) {
+ jsxc.xmpp.bookmarks.loadFromRemote();
+ } else {
+ jsxc.xmpp.bookmarks.loadFromLocal();
+ }
+};
- } else if (state === 'failed') {
- jsxc.gui.window.postMessage({
- bid: jsxc.jidToBid(session.peerID),
- direction: jsxc.Message.SYS,
- msg: $.t('ICE_connection_failure')
- });
+/**
+ * Load bookmarks from local storage.
+ *
+ * @private
+ */
+jsxc.xmpp.bookmarks.loadFromLocal = function() {
+ jsxc.debug('Load bookmarks from local storage');
- session.end('failed-transport');
+ var bookmarks = jsxc.storage.getUserItem('bookmarks') || [];
+ var bl = jsxc.storage.getUserItem('buddylist') || [];
- $(document).trigger('callterminated.jingle');
- } else if (state === 'interrupted') {
- self.setStatus($.t('Connection_interrupted'));
- }
- },
+ $.each(bookmarks, function() {
+ var room = this;
+ var roomdata = jsxc.storage.getUserItem('buddy', room) || {};
- /**
- * Start a call to the specified jid.
- *
- * @memberOf jsxc.webrtc
- * @param jid full jid
- * @param um requested user media
- */
- startCall: function(jid, um) {
- var self = this;
+ bl.push(room);
+ jsxc.gui.roster.add(room);
- if (Strophe.getResourceFromJid(jid) === null) {
- jsxc.debug('We need a full jid');
- return;
+ if (roomdata.autojoin) {
+ jsxc.debug('auto join ' + room);
+ jsxc.xmpp.conn.muc.join(room, roomdata.nickname);
}
+ });
- self.last_caller = jid;
-
- jsxc.switchEvents({
- 'finish.mediaready.jsxc': function() {
- self.setStatus('Initiate call');
+ jsxc.storage.setUserItem('buddylist', bl);
+};
- jsxc.gui.window.postMessage({
- bid: jsxc.jidToBid(jid),
- direction: jsxc.Message.SYS,
- msg: $.t('Call_started')
- });
+/**
+ * Load bookmarks from remote storage.
+ *
+ * @private
+ */
+jsxc.xmpp.bookmarks.loadFromRemote = function() {
+ jsxc.debug('Load bookmarks from pubsub');
- $(document).one('error.jingle', function(e, sid, error) {
- if (error && error.source !== 'offer') {
- return;
- }
+ var bookmarks = jsxc.xmpp.conn.bookmarks;
- setTimeout(function() {
- jsxc.gui.showAlert("Sorry, we couldn't establish a connection. Maybe your buddy is offline.");
- }, 500);
- });
+ bookmarks.get(function(stanza) {
+ var bl = jsxc.storage.getUserItem('buddylist');
- var session = self.conn.jingle.initiate(jid);
+ $(stanza).find('conference').each(function() {
+ var conference = $(this);
+ var room = conference.attr('jid');
+ var roomName = conference.attr('name') || room;
+ var autojoin = conference.attr('autojoin') || false;
+ var nickname = conference.find('nick').text();
+ nickname = (nickname.length > 0) ? nickname : Strophe.getNodeFromJid(jsxc.xmpp.conn.jid);
- session.on('change:connectionState', $.proxy(self.onIceConnectionStateChanged, self));
- },
- 'mediafailure.jingle': function() {
- jsxc.gui.dialog.close();
+ if (autojoin === 'true') {
+ autojoin = true;
+ } else if (autojoin === 'false') {
+ autojoin = false;
}
- });
- self.reqUserMedia(um);
- },
-
- /**
- * Hang up the current call.
- *
- * @memberOf jsxc.webrtc
- */
- hangUp: function(reason, text) {
- if (jsxc.webrtc.conn.jingle.manager && !$.isEmptyObject(jsxc.webrtc.conn.jingle.manager.peers)) {
- jsxc.webrtc.conn.jingle.terminate(null, reason, text);
- } else {
- jsxc.gui.closeVideoWindow();
- }
+ var data = jsxc.storage.getUserItem('buddy', room) || {};
- // @TODO check event
- $(document).trigger('callterminated.jingle');
- },
+ data = $.extend(data, {
+ jid: room,
+ name: roomName,
+ sub: 'both',
+ status: 0,
+ type: 'groupchat',
+ state: jsxc.muc.CONST.ROOMSTATE.INIT,
+ subject: null,
+ bookmarked: true,
+ autojoin: autojoin,
+ nickname: nickname
+ });
- /**
- * Request video and audio from local user.
- *
- * @memberOf jsxc.webrtc
- */
- reqUserMedia: function(um) {
- if (this.localStream) {
- $(document).trigger('mediaready.jingle', [this.localStream]);
- return;
- }
+ jsxc.storage.setUserItem('buddy', room, data);
- um = um || ['video', 'audio'];
+ bl.push(room);
+ jsxc.gui.roster.add(room);
- jsxc.gui.dialog.open(jsxc.gui.template.get('allowMediaAccess'), {
- noClose: true
+ if (autojoin) {
+ jsxc.debug('auto join ' + room);
+ jsxc.xmpp.conn.muc.join(room, nickname);
+ }
});
- 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;
- });
+ jsxc.storage.setUserItem('buddylist', bl);
+ }, function(stanza) {
+ var err = jsxc.xmpp.bookmarks.parseErr(stanza);
- um = um.filter(function(el) {
- return availableDevices.indexOf(el) !== -1;
- });
+ if (err.reasons[0] === 'item-not-found') {
+ jsxc.debug('create bookmark node');
- jsxc.webrtc.getUserMedia(um);
+ bookmarks.createBookmarksNode(function() {
+ jsxc.debug('Bookmark node created.');
+ }, function() {
+ jsxc.debug('Could not create bookmark node.');
});
} else {
- jsxc.webrtc.getUserMedia(um);
+ jsxc.debug('[XMPP] Could not create bookmark: ' + err.type, err.reasons);
}
- },
+ });
+};
- getUserMedia: function(um) {
- var self = jsxc.webrtc;
- var constraints = {};
+/**
+ * Parse received error.
+ *
+ * @param {string} stanza
+ * @return {object} err - The parsed error
+ * @return {string} err.type - XMPP error type
+ * @return {array} err.reasons - Array of error reasons
+ */
+jsxc.xmpp.bookmarks.parseErr = function(stanza) {
+ var error = $(stanza).find('error');
+ var type = error.attr('type');
+ var reasons = error.children().map(function() {
+ return $(this).prop('tagName');
+ });
- if (um.indexOf('video') > -1) {
- constraints.video = true;
- }
+ return {
+ type: type,
+ reasons: reasons
+ };
+};
- if (um.indexOf('audio') > -1) {
- constraints.audio = true;
- }
+/**
+ * Deletes the bookmark for the given room and removes it from the roster if soft is false.
+ *
+ * @param {string} room - room jid
+ * @param {boolean} [soft=false] - True: leave room in roster
+ */
+jsxc.xmpp.bookmarks.delete = function(room, soft) {
- try {
- self.conn.jingle.RTC.getUserMedia(constraints,
- function(stream) {
- jsxc.debug('onUserMediaSuccess');
- $(document).trigger('mediaready.jingle', [stream]);
- },
- function(error) {
- jsxc.warn('Failed to get access to local media. Error ', error);
- $(document).trigger('mediafailure.jingle', [error]);
- });
- } catch (e) {
- jsxc.error('GUM failed: ', e);
- $(document).trigger('mediafailure.jingle');
- }
- },
+ if (!soft) {
+ jsxc.gui.roster.purge(room);
+ }
- /**
- * Make a snapshot from a video stream and display it.
- *
- * @memberOf jsxc.webrtc
- * @param video Video stream
- */
- snapshot: function(video) {
- if (!video) {
- jsxc.debug('Missing video element');
+ if (jsxc.xmpp.bookmarks.remote()) {
+ jsxc.xmpp.bookmarks.deleteFromRemote(room, soft);
+ } else {
+ jsxc.xmpp.bookmarks.deleteFromLocal(room, soft);
+ }
+};
+
+/**
+ * Delete bookmark from remote storage.
+ *
+ * @private
+ * @param {string} room - room jid
+ * @param {boolean} [soft=false] - True: leave room in roster
+ */
+jsxc.xmpp.bookmarks.deleteFromRemote = function(room, soft) {
+ var bookmarks = jsxc.xmpp.conn.bookmarks;
+
+ bookmarks.delete(room, function() {
+ jsxc.debug('Bookmark deleted ' + room);
+
+ if (soft) {
+ jsxc.gui.roster.getItem(room).removeClass('jsxc_bookmarked');
+ jsxc.storage.updateUserItem('buddy', room, 'bookmarked', false);
+ jsxc.storage.updateUserItem('buddy', room, 'autojoin', false);
}
+ }, function(stanza) {
+ var err = jsxc.xmpp.bookmarks.parseErr(stanza);
- $('.jsxc_snapshotbar p').remove();
+ jsxc.debug('[XMPP] Could not delete bookmark: ' + err.type, err.reasons);
+ });
+};
- var canvas = $('
').css('display', 'none').appendTo('body').attr({
- width: video.width(),
- height: video.height()
- }).get(0);
- var ctx = canvas.getContext('2d');
+/**
+ * Delete bookmark from local storage.
+ *
+ * @private
+ * @param {string} room - room jid
+ * @param {boolean} [soft=false] - True: leave room in roster
+ */
+jsxc.xmpp.bookmarks.deleteFromLocal = function(room, soft) {
+ var bookmarks = jsxc.storage.getUserItem('bookmarks');
+ var index = bookmarks.indexOf(room);
- ctx.drawImage(video[0], 0, 0);
- var img = $('
');
- var url = null;
+ if (index > -1) {
+ bookmarks.splice(index, 1);
+ }
- try {
- url = canvas.toDataURL('image/jpeg');
- } catch (err) {
- jsxc.warn('Error', err);
- return;
- }
+ jsxc.storage.setUserItem('bookmarks', bookmarks);
- img[0].src = url;
- var link = $('
').attr({
- target: '_blank',
- href: url
- });
- link.append(img);
- $('.jsxc_snapshotbar').append(link);
+ if (soft) {
+ jsxc.gui.roster.getItem(room).removeClass('jsxc_bookmarked');
+ jsxc.storage.updateUserItem('buddy', room, 'bookmarked', false);
+ jsxc.storage.updateUserItem('buddy', room, 'autojoin', false);
+ }
+};
- canvas.remove();
- },
+/**
+ * Adds or overwrites bookmark for given room.
+ *
+ * @param {string} room - room jid
+ * @param {string} alias - room alias
+ * @param {string} nick - preferred user nickname
+ * @param {boolean} autojoin - should we join this room after login?
+ */
+jsxc.xmpp.bookmarks.add = function(room, alias, nick, autojoin) {
+ if (jsxc.xmpp.bookmarks.remote()) {
+ jsxc.xmpp.bookmarks.addToRemote(room, alias, nick, autojoin);
+ } else {
+ jsxc.xmpp.bookmarks.addToLocal(room, alias, nick, autojoin);
+ }
+};
- /**
- * Send file to full jid.
- *
- * @memberOf jsxc.webrtc
- * @param {string} jid full jid
- * @param {file} file
- * @return {object} session
- */
- sendFile: function(jid, file) {
- var self = jsxc.webrtc;
+/**
+ * Adds or overwrites bookmark for given room in remote storage.
+ *
+ * @private
+ * @param {string} room - room jid
+ * @param {string} alias - room alias
+ * @param {string} nick - preferred user nickname
+ * @param {boolean} autojoin - should we join this room after login?
+ */
+jsxc.xmpp.bookmarks.addToRemote = function(room, alias, nick, autojoin) {
+ var bookmarks = jsxc.xmpp.conn.bookmarks;
- var sess = self.conn.jingle.manager.createFileTransferSession(jid);
+ var success = function() {
+ jsxc.debug('New bookmark created', room);
- sess.on('change:sessionState', function() {
- jsxc.debug('Session state', sess.state);
- });
- sess.on('change:connectionState', function() {
- jsxc.debug('Connection state', sess.connectionState);
- });
+ jsxc.gui.roster.getItem(room).addClass('jsxc_bookmarked');
+ jsxc.storage.updateUserItem('buddy', room, 'bookmarked', true);
+ jsxc.storage.updateUserItem('buddy', room, 'autojoin', autojoin);
+ jsxc.storage.updateUserItem('buddy', room, 'nickname', nick);
+ };
+ var error = function() {
+ jsxc.warn('Could not create bookmark', room);
+ };
- sess.start(file);
+ bookmarks.add(room, alias, nick, autojoin, success, error);
+};
- return sess;
- },
+/**
+ * Adds or overwrites bookmark for given room in local storage.
+ *
+ * @private
+ * @param {string} room - room jid
+ * @param {string} alias - room alias
+ * @param {string} nick - preferred user nickname
+ * @param {boolean} autojoin - should we join this room after login?
+ */
+jsxc.xmpp.bookmarks.addToLocal = function(room, alias, nick, autojoin) {
+ jsxc.gui.roster.getItem(room).addClass('jsxc_bookmarked');
+ jsxc.storage.updateUserItem('buddy', room, 'bookmarked', true);
+ jsxc.storage.updateUserItem('buddy', room, 'autojoin', autojoin);
+ jsxc.storage.updateUserItem('buddy', room, 'nickname', nick);
- /**
- * Display received file.
- *
- * @memberOf jsxc.webrtc
- * @param {object} sess
- * @param {File} file
- * @param {object} metadata file metadata
- */
- onReceivedFile: function(sess, file, metadata) {
- jsxc.debug('file received', metadata);
+ var bookmarks = jsxc.storage.getUserItem('bookmarks') || [];
- if (!FileReader) {
- return;
- }
+ if (bookmarks.indexOf(room) < 0) {
+ bookmarks.push(room);
- var reader = new FileReader();
- var type;
+ jsxc.storage.setUserItem('bookmarks', bookmarks);
+ }
+};
- if (!metadata.type) {
- // 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();
+/**
+ * Show dialog to edit bookmark.
+ *
+ * @param {string} room - room jid
+ */
+jsxc.xmpp.bookmarks.showDialog = function(room) {
+ var dialog = jsxc.gui.dialog.open(jsxc.gui.template.get('bookmarkDialog'));
+ var data = jsxc.storage.getUserItem('buddy', room);
- switch (ext) {
- case 'jpg':
- case 'jpeg':
- case 'png':
- case 'gif':
- case 'svg':
- type = 'image/' + ext.replace(/^jpg$/, 'jpeg');
- break;
- case 'mp3':
- case 'wav':
- type = 'audio/' + ext;
- break;
- case 'pdf':
- type = 'application/pdf';
- break;
- case 'txt':
- type = 'text/' + ext;
- break;
- default:
- type = 'application/octet-stream';
- }
+ $('#jsxc_room').val(room);
+ $('#jsxc_nickname').val(data.nickname);
+
+ $('#jsxc_bookmark').change(function() {
+ if ($(this).prop('checked')) {
+ $('#jsxc_nickname').prop('disabled', false);
+ $('#jsxc_autojoin').prop('disabled', false);
+ $('#jsxc_autojoin').parent('.checkbox').removeClass('disabled');
} else {
- type = metadata.type;
+ $('#jsxc_nickname').prop('disabled', true);
+ $('#jsxc_autojoin').prop('disabled', true).prop('checked', false);
+ $('#jsxc_autojoin').parent('.checkbox').addClass('disabled');
}
+ });
- reader.onload = function(ev) {
- // modify element with uid metadata.actualhash
+ $('#jsxc_bookmark').prop('checked', data.bookmarked);
+ $('#jsxc_autojoin').prop('checked', data.autojoin);
- jsxc.gui.window.postMessage({
- _uid: sess.sid + ':msg',
- bid: jsxc.jidToBid(sess.peerID),
- direction: jsxc.Message.IN,
- attachment: {
- name: metadata.name,
- type: type,
- size: metadata.size,
- data: ev.target.result
- }
- });
- };
+ $('#jsxc_bookmark').change();
- if (!file.type) {
- // file type should be handled in lib
- file = new File([file], metadata.name, {
- type: type
- });
+ dialog.find('form').submit(function(ev) {
+ ev.preventDefault();
+
+ var bookmarked = $('#jsxc_bookmark').prop('checked');
+ var autojoin = $('#jsxc_autojoin').prop('checked');
+ var nickname = $('#jsxc_nickname').val();
+
+ if (bookmarked) {
+ jsxc.xmpp.bookmarks.add(room, data.name, nickname, autojoin);
+ } else if (data.bookmarked) {
+ // bookmarked === false
+ jsxc.xmpp.bookmarks.delete(room, true);
}
- reader.readAsDataURL(file);
- }
+ jsxc.gui.dialog.close();
+
+ return false;
+ });
};
/**
- * Display window for video call.
- *
- * @memberOf jsxc.gui
+ * Implements XEP-0085: Chat State Notifications.
+ *
+ * @namespace jsxc.xmpp.chatState
+ * @see {@link http://xmpp.org/extensions/xep-0085.html}
*/
-jsxc.gui.showVideoWindow = function(jid) {
- var self = jsxc.webrtc;
+jsxc.xmpp.chatState = {
+ conn: null,
- // needed to trigger complete.dialog.jsxc
- jsxc.gui.dialog.close();
+ /** Delay between two notification on the message composing */
+ toComposingNotificationDelay: 900,
+};
- $('body').append(jsxc.gui.template.get('videoWindow'));
+jsxc.xmpp.chatState.init = function() {
+ var self = jsxc.xmpp.chatState;
- // mute own video element to avoid echoes
- $('#jsxc_webrtc .jsxc_localvideo')[0].muted = true;
- $('#jsxc_webrtc .jsxc_localvideo')[0].volume = 0;
+ if (!jsxc.xmpp.conn || !jsxc.xmpp.connected) {
+ $(document).on('attached.jsxc', self.init);
- var rv = $('#jsxc_webrtc .jsxc_remotevideo');
- var lv = $('#jsxc_webrtc .jsxc_localvideo');
+ return;
+ }
- lv.draggable({
- containment: "parent"
- });
+ // 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.localStream) {
- self.attachMediaStream(lv, self.localStream);
+ if (self.isDisabled()) {
+ jsxc.debug('chat state notification disabled');
+
+ return;
}
- var w_dialog = $('#jsxc_webrtc').width();
- var w_remote = rv.width();
+ self.conn = jsxc.xmpp.conn;
- // fit in video
- if (w_remote > w_dialog) {
- var scale = w_dialog / w_remote;
- var new_h = rv.height() * scale;
- var new_w = w_dialog;
- var vc = $('#jsxc_webrtc .jsxc_videoContainer');
+ $(document).on('composing.chatstates', jsxc.xmpp.chatState.onComposing);
+ $(document).on('paused.chatstates', jsxc.xmpp.chatState.onPaused);
+ $(document).on('active.chatstates', jsxc.xmpp.chatState.onActive);
+};
- rv.height(new_h);
- rv.width(new_w);
+/**
+ * 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;
- vc.height(new_h);
- vc.width(new_w);
+ if (!data || jsxc.xmpp.chatState.isDisabled()) {
+ return;
+ }
- lv.height(lv.height() * scale);
- lv.width(lv.width() * scale);
+ // ignore own notifications in groupchat
+ if (data.type === 'groupchat' &&
+ Strophe.getResourceFromJid(jid) === Strophe.getNodeFromJid(self.conn.jid)) {
+ return;
}
- if (self.remoteStream) {
- self.attachMediaStream(rv, self.remoteStream);
+ var user = data.type === 'groupchat' ? Strophe.getResourceFromJid(jid) : data.name;
+ var win = jsxc.gui.window.get(bid);
- $('#jsxc_webrtc .jsxc_' + (self.remoteStream.getVideoTracks().length > 0 ? 'remotevideo' : 'noRemoteVideo')).addClass('jsxc_deviceAvailable');
+ if (win.length === 0) {
+ return;
}
- var win = jsxc.gui.window.open(jsxc.jidToBid(jid));
-
- win.find('.slimScrollDiv').resizable('disable');
- jsxc.gui.window.resize(win, {
- size: {
- width: $('#jsxc_webrtc .jsxc_chatarea').width(),
- height: $('#jsxc_webrtc .jsxc_chatarea').height()
- }
- }, true);
+ // add user in array if necessary
+ var usersComposing = win.data('composing') || [];
+ if (usersComposing.indexOf(user) === -1) {
+ usersComposing.push(user);
+ win.data('composing', usersComposing);
+ }
- $('#jsxc_webrtc .jsxc_chatarea ul').append(win.detach());
+ var msg = self._genComposingMsg(data.type, usersComposing);
+ jsxc.xmpp.chatState.setStatus(win, msg);
+};
- $('#jsxc_webrtc .jsxc_hangUp').click(function() {
- jsxc.webrtc.hangUp('success');
- });
+/**
+ * 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;
- $('#jsxc_webrtc .jsxc_fullscreen').click(function() {
+ if (!data || jsxc.xmpp.chatState.isDisabled()) {
+ return;
+ }
- if ($.support.fullscreen) {
- // Reset position of localvideo
- $(document).one('disabled.fullscreen', function() {
- lv.removeAttr('style');
- });
+ var user = data.type === 'groupchat' ? Strophe.getResourceFromJid(jid) : data.name;
+ var win = jsxc.gui.window.get(bid);
- $('#jsxc_webrtc .jsxc_videoContainer').fullscreen();
- }
- });
+ if (win.length === 0) {
+ return;
+ }
- $('#jsxc_webrtc .jsxc_videoContainer').click(function() {
- $('#jsxc_webrtc .jsxc_controlbar').toggleClass('jsxc_visible');
- });
+ var usersComposing = win.data('composing') || [];
- return $('#jsxc_webrtc');
-};
+ if (usersComposing.indexOf(user) >= 0) {
+ // remove user from list
+ usersComposing.splice(usersComposing.indexOf(user), 1);
+ win.data('composing', usersComposing);
+ }
-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);
+ var composingMsg;
+ if (usersComposing.length !== 0) {
+ composingMsg = self._genComposingMsg(data.type, usersComposing);
+ }
- $('#jsxc_webrtc').remove();
+ jsxc.xmpp.chatState.setStatus(win, composingMsg);
};
-$.extend(jsxc.CONST, {
- KEYCODE_ENTER: 13,
- KEYCODE_ESC: 27
-});
-
-$(document).ready(function() {
- $(document).on('init.window.jsxc', jsxc.webrtc.initWindow);
- $(document).on('attached.jsxc', jsxc.webrtc.init);
- $(document).on('disconnected.jsxc', jsxc.webrtc.onDisconnected);
- $(document).on('connected.jsxc', jsxc.webrtc.onConnected);
-});
+/**
+ * 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);
+};
/**
- * Load and save bookmarks according to XEP-0048.
+ * Send composing event.
*
- * @namespace jsxc.xmpp.bookmarks
+ * @memberOf jsxc.xmpp.chatState
+ * @param {String} bid
*/
-jsxc.xmpp.bookmarks = {};
+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);
+};
/**
- * Determines if server is able to store bookmarks.
- *
- * @return {boolean} True: Server supports bookmark storage
+ * Send pause event.
+ *
+ * @memberOf jsxc.xmpp.chatState
+ * @param {String} bid
*/
-jsxc.xmpp.bookmarks.remote = function() {
- return jsxc.xmpp.conn.caps && jsxc.xmpp.hasFeatureByJid(jsxc.xmpp.conn.domain, Strophe.NS.PUBSUB + "#publish");
+jsxc.xmpp.chatState.pauseComposing = function(bid, type) {
+ if (jsxc.xmpp.chatState.isDisabled()) {
+ return;
+ }
+
+ jsxc.xmpp.conn.chatstates.sendPaused(bid, type);
};
/**
- * Load bookmarks from pubsub.
+ * End composing without sending a pause event.
*
- * @memberOf jsxc.xmpp.bookmarks
+ * @memberOf jsxc.xmpp.chatState
+ * @param {String} bid
*/
-jsxc.xmpp.bookmarks.load = function() {
- var caps = jsxc.xmpp.conn.caps;
- var ver = caps._jidVerIndex[jsxc.xmpp.conn.domain];
+jsxc.xmpp.chatState.endComposing = function(bid) {
+ var win = jsxc.gui.window.get(bid);
- if (!ver || !caps._knownCapabilities[ver]) {
- // wait until we know server capabilities
- $(document).on('caps.strophe', function(ev, from) {
- if (from === jsxc.xmpp.conn.domain) {
- jsxc.xmpp.bookmarks.load();
+ if (win.data('composing-timeout')) {
+ clearTimeout(win.data('composing-timeout'));
+ }
+};
- $(document).off(ev);
- }
- });
+/**
+ * Generate composing message.
+ *
+ * @memberOf jsxc.xmpp.chatState
+ * @param {String} the type of the chat ('groupchat' or 'chat')
+ * @param {Array} usersComposing List of users which are currently composing a message
+ */
+jsxc.xmpp.chatState._genComposingMsg = function(chatType, usersComposing) {
+ if (!usersComposing || usersComposing.length === 0) {
+ jsxc.debug('usersComposing array is empty?');
+
+ return '';
+ } else {
+ if (chatType === 'groupchat') {
+ return usersComposing.length > 1 ? usersComposing.join(', ') + $.t('_are_composing') :
+ usersComposing[0] + $.t('_is_composing');
+ }
+ return $.t('_is_composing');
}
+};
- if (jsxc.xmpp.bookmarks.remote()) {
- jsxc.xmpp.bookmarks.loadFromRemote();
+jsxc.xmpp.chatState.setStatus = function(win, msg) {
+ var statusMsgElement = win.find('.jsxc_status-msg');
+
+ statusMsgElement.text(msg || '');
+ statusMsgElement.attr('title', msg || '');
+
+ if (msg) {
+ statusMsgElement.addClass('jsxc_composing');
+ win.addClass('jsxc_status-msg-show');
} else {
- jsxc.xmpp.bookmarks.loadFromLocal();
+ statusMsgElement.removeClass('jsxc_composing');
+ win.removeClass('jsxc_status-msg-show');
}
};
+jsxc.xmpp.chatState.isDisabled = function() {
+ var options = jsxc.options.get('chatState') || {};
+
+ return !options.enable;
+};
+
+$(document).on('attached.jsxc', jsxc.xmpp.chatState.init);
+
/**
- * Load bookmarks from local storage.
+ * Implements Http File Upload (XEP-0363)
*
- * @private
+ * @namespace jsxc.xmpp.httpUpload
+ * @see {@link http://xmpp.org/extensions/xep-0363.html}
*/
-jsxc.xmpp.bookmarks.loadFromLocal = function() {
- jsxc.debug('Load bookmarks from local storage');
-
- var bookmarks = jsxc.storage.getUserItem('bookmarks') || [];
- var bl = jsxc.storage.getUserItem('buddylist') || [];
-
- $.each(bookmarks, function() {
- var room = this;
- var roomdata = jsxc.storage.getUserItem('buddy', room) || {};
+jsxc.xmpp.httpUpload = {
+ conn: null,
- bl.push(room);
- jsxc.gui.roster.add(room);
+ ready: false,
- if (roomdata.autojoin) {
- jsxc.debug('auto join ' + room);
- jsxc.xmpp.conn.muc.join(room, roomdata.nickname);
+ CONST: {
+ NS: {
+ HTTPUPLOAD: 'urn:xmpp:http:upload'
}
- });
-
- jsxc.storage.setUserItem('buddylist', bl);
+ }
};
/**
- * Load bookmarks from remote storage.
- *
- * @private
+ * Set up http file upload.
+ *
+ * @memberOf jsxc.xmpp.httpUpload
+ * @param {Object} o options
*/
-jsxc.xmpp.bookmarks.loadFromRemote = function() {
- jsxc.debug('Load bookmarks from pubsub');
+jsxc.xmpp.httpUpload.init = function(o) {
+ var self = jsxc.xmpp.httpUpload;
+ self.conn = jsxc.xmpp.conn;
- var bookmarks = jsxc.xmpp.conn.bookmarks;
+ var fileTransferOptions = jsxc.options.get('fileTransfer') || {};
+ var options = o || jsxc.options.get('httpUpload');
- bookmarks.get(function(stanza) {
- var bl = jsxc.storage.getUserItem('buddylist');
+ if (!fileTransferOptions.httpUpload.enable) {
+ jsxc.debug('http upload disabled');
- $(stanza).find('conference').each(function() {
- var conference = $(this);
- var room = conference.attr('jid');
- var roomName = conference.attr('name') || room;
- var autojoin = conference.attr('autojoin') || false;
- var nickname = conference.find('nick').text();
- nickname = (nickname.length > 0) ? nickname : Strophe.getNodeFromJid(jsxc.xmpp.conn.jid);
+ jsxc.options.set('httpUpload', false);
- if (autojoin === 'true') {
- autojoin = true;
- } else if (autojoin === 'false') {
- autojoin = false;
- }
+ return;
+ }
- var data = jsxc.storage.getUserItem('buddy', room) || {};
+ if (options && options.server) {
+ self.ready = true;
- data = $.extend(data, {
- jid: room,
- name: roomName,
- sub: 'both',
- status: 0,
- type: 'groupchat',
- state: jsxc.muc.CONST.ROOMSTATE.INIT,
- subject: null,
- bookmarked: true,
- autojoin: autojoin,
- nickname: nickname
- });
+ return;
+ }
- jsxc.storage.setUserItem('buddy', room, data);
+ if (!jsxc.xmpp.conn) {
+ return;
+ }
- bl.push(room);
- jsxc.gui.roster.add(room);
+ var caps = jsxc.xmpp.conn.caps;
+ var domain = jsxc.xmpp.conn.domain;
- if (autojoin) {
- jsxc.debug('auto join ' + room);
- jsxc.xmpp.conn.muc.join(room, nickname);
+ 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);
});
- jsxc.storage.setUserItem('buddylist', bl);
- }, function(stanza) {
- var err = jsxc.xmpp.bookmarks.parseErr(stanza);
+ return;
+ }
- if (err.reasons[0] === 'item-not-found') {
- jsxc.debug('create bookmark node');
+ self.discoverUploadService();
+};
- bookmarks.createBookmarksNode();
- } else {
- jsxc.debug('[XMPP] Could not create bookmark: ' + err.type, err.reasons);
- }
+/**
+ * Discover upload service for http upload.
+ *
+ * @memberOf jsxc.xmpp.httpUpload
+ */
+jsxc.xmpp.httpUpload.discoverUploadService = function() {
+ var self = jsxc.xmpp.httpUpload;
+ var domain = self.conn.domain;
+
+ jsxc.debug('discover http upload service');
+
+ if (jsxc.xmpp.conn.caps.hasFeatureByJid(domain, self.CONST.NS.HTTPUPLOAD)) {
+ self.queryItemForUploadService(domain);
+ }
+
+ self.conn.disco.items(domain, null, function(items) {
+ $(items).find('item').each(function() {
+ var jid = $(this).attr('jid');
+
+ if (self.ready) {
+ // abort, because we already found a service
+ return false;
+ }
+
+ self.queryItemForUploadService(jid);
+ });
});
};
/**
- * Parse received error.
+ * Query item for upload service.
*
- * @param {string} stanza
- * @return {object} err - The parsed error
- * @return {string} err.type - XMPP error type
- * @return {array} err.reasons - Array of error reasons
+ * @param {String} jid
+ * @param {Function} cb Callback on success
+ * @memberOf jsxc.xmpp.httpUpload
*/
-jsxc.xmpp.bookmarks.parseErr = function(stanza) {
- var error = $(stanza).find('error');
- var type = error.attr('type');
- var reasons = error.children().map(function() {
- return $(this).prop('tagName');
- });
+jsxc.xmpp.httpUpload.queryItemForUploadService = function(jid, cb) {
+ var self = jsxc.xmpp.httpUpload;
- return {
- type: type,
- reasons: reasons
- };
+ jsxc.debug('query ' + jid + ' for upload service');
+
+ 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 on ' + jid);
+
+ jsxc.options.set('httpUpload', {
+ server: jid,
+ name: $(info).find('identity').attr('name'),
+ maxSize: parseInt(httpUploadMaxSize.text()) || -1
+ });
+
+ self.ready = true;
+
+ if (typeof cb === 'function') {
+ cb.call(info);
+ }
+ }
+ });
};
/**
- * Deletes the bookmark for the given room and removes it from the roster if soft is false.
+ * Upload file and send link to peer.
*
- * @param {string} room - room jid
- * @param {boolean} [soft=false] - True: leave room in roster
+ * @memberOf jsxc.xmpp.httpUpload
+ * @param {File} file
+ * @param {Message} message Preview message
*/
-jsxc.xmpp.bookmarks.delete = function(room, soft) {
+jsxc.xmpp.httpUpload.sendFile = function(file, message) {
+ jsxc.debug('Send file via http upload');
- if (!soft) {
- jsxc.gui.roster.purge(room);
- }
+ var self = jsxc.xmpp.httpUpload;
- if (jsxc.xmpp.bookmarks.remote()) {
- jsxc.xmpp.bookmarks.deleteFromRemote(room, soft);
- } else {
- jsxc.xmpp.bookmarks.deleteFromLocal(room, soft);
- }
+ // 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) {
+ jsxc.debug('slot received, start upload to ' + data.put);
+
+ self.uploadFile(data.put, file, message, function() {
+ var attachment = message.attachment;
+ var metaString = attachment.type + '|' + attachment.size + '|' + attachment.name;
+ var a = $('
');
+ a.attr('href', data.get);
+
+ attachment.data = data.get;
+
+ if (attachment.thumbnail) {
+ var img = $('');
+ img.attr('alt', 'Preview:' + metaString);
+ img.attr('src', attachment.thumbnail);
+ a.prepend(img);
+ } else {
+ a.text(metaString);
+ }
+
+ message.msg = data.get;
+ message.htmlMsg = $('').append(a).html();
+ message.type = jsxc.Message.HTML;
+ jsxc.gui.window.postMessage(message);
+ });
+ }
+ });
};
/**
- * Delete bookmark from remote storage.
+ * Upload the given file to the given url.
*
- * @private
- * @param {string} room - room jid
- * @param {boolean} [soft=false] - True: leave room in roster
- */
-jsxc.xmpp.bookmarks.deleteFromRemote = function(room, soft) {
- var bookmarks = jsxc.xmpp.conn.bookmarks;
+ * @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');
- bookmarks.delete(room, function() {
- jsxc.debug('Bookmark deleted ' + room);
+ // In case that upload progress is not available, inform user
+ jsxc.gui.window.updateProgress(message, 1, 1);
- if (soft) {
- jsxc.gui.roster.getItem(room).removeClass('jsxc_bookmarked');
- jsxc.storage.updateUserItem('buddy', room, 'bookmarked', false);
- jsxc.storage.updateUserItem('buddy', room, 'autojoin', false);
- }
- }, function(stanza) {
- var err = jsxc.xmpp.bookmarks.parseErr(stanza);
+ if (success_cb) {
+ success_cb();
+ }
+ },
+ error: function() {
+ jsxc.warn('error while uploading file to ' + url);
- jsxc.debug('[XMPP] Could not delete bookmark: ' + err.type, err.reasons);
+ message.error = 'Could not upload file';
+ jsxc.gui.window.postMessage(message);
+ }
});
};
/**
- * Delete bookmark from local storage.
+ * Request upload slot.
*
- * @private
- * @param {string} room - room jid
- * @param {boolean} [soft=false] - True: leave room in roster
- */
-jsxc.xmpp.bookmarks.deleteFromLocal = function(room, soft) {
- var bookmarks = jsxc.storage.getUserItem('bookmarks');
- var index = bookmarks.indexOf(room);
+ * @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 (index > -1) {
- bookmarks.splice(index, 1);
+ 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;
}
- jsxc.storage.setUserItem('bookmarks', bookmarks);
+ 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);
- if (soft) {
- jsxc.gui.roster.getItem(room).removeClass('jsxc_bookmarked');
- jsxc.storage.updateUserItem('buddy', room, 'bookmarked', false);
- jsxc.storage.updateUserItem('buddy', room, 'autojoin', false);
- }
+ self.conn.sendIQ(iq, function(stanza) {
+ self.successfulRequestSlotCB(stanza, cb);
+ }, function(stanza) {
+ self.failedRequestSlotCB(stanza, cb);
+ });
};
/**
- * Adds or overwrites bookmark for given room.
+ * Process successful response to slot request.
*
- * @param {string} room - room jid
- * @param {string} alias - room alias
- * @param {string} nick - preferred user nickname
- * @param {boolean} autojoin - should we join this room after login?
- */
-jsxc.xmpp.bookmarks.add = function(room, alias, nick, autojoin) {
- if (jsxc.xmpp.bookmarks.remote()) {
- jsxc.xmpp.bookmarks.addToRemote(room, alias, nick, autojoin);
+ * @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 {
- jsxc.xmpp.bookmarks.addToLocal(room, alias, nick, autojoin);
+ self.failedRequestSlotCB(stanza, cb);
}
};
/**
- * Adds or overwrites bookmark for given room in remote storage.
+ * Process failed response to slot request.
*
- * @private
- * @param {string} room - room jid
- * @param {string} alias - room alias
- * @param {string} nick - preferred user nickname
- * @param {boolean} autojoin - should we join this room after login?
+ * @memberOf jsxc.xmpp.httpUpload
+ * @param {String} stanza
+ * @param {Function} cb
*/
-jsxc.xmpp.bookmarks.addToRemote = function(room, alias, nick, autojoin) {
- var bookmarks = jsxc.xmpp.conn.bookmarks;
+jsxc.xmpp.httpUpload.failedRequestSlotCB = function(stanza, cb) {
+ if ($(stanza).find('error').length <= 0) {
+ jsxc.warn('response does not contain a slot element');
- var success = function() {
- jsxc.debug('New bookmark created', room);
+ cb();
- jsxc.gui.roster.getItem(room).addClass('jsxc_bookmarked');
- jsxc.storage.updateUserItem('buddy', room, 'bookmarked', true);
- jsxc.storage.updateUserItem('buddy', room, 'autojoin', autojoin);
- jsxc.storage.updateUserItem('buddy', room, 'nickname', nick);
- };
- var error = function() {
- jsxc.warn('Could not create bookmark', room);
+ return;
+ }
+
+ var error = {
+ type: $(stanza).find('error').attr('type') || 'unknown',
+ text: $(stanza).find('error text').text()
};
- bookmarks.add(room, alias, nick, autojoin, success, error);
+ 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('stateUIChange.jsxc', function(ev, state) {
+ if (state === jsxc.CONST.UISTATE.INITIATING) {
+ jsxc.xmpp.httpUpload.init();
+ }
+});
+
/**
- * Adds or overwrites bookmark for given room in local storage.
+ * Implements XEP-0313: Message Archive Management.
*
- * @private
- * @param {string} room - room jid
- * @param {string} alias - room alias
- * @param {string} nick - preferred user nickname
- * @param {boolean} autojoin - should we join this room after login?
+ * @namespace jsxc.xmpp.mam
+ * @see {@link https://xmpp.org/extensions/xep-0313.html}
*/
-jsxc.xmpp.bookmarks.addToLocal = function(room, alias, nick, autojoin) {
- jsxc.gui.roster.getItem(room).addClass('jsxc_bookmarked');
- jsxc.storage.updateUserItem('buddy', room, 'bookmarked', true);
- jsxc.storage.updateUserItem('buddy', room, 'autojoin', autojoin);
- jsxc.storage.updateUserItem('buddy', room, 'nickname', nick);
+jsxc.xmpp.mam = {
+ conn: null
+};
- var bookmarks = jsxc.storage.getUserItem('bookmarks') || [];
+jsxc.xmpp.mam.init = function() {
+ var self = jsxc.xmpp.mam;
- if (bookmarks.indexOf(room) < 0) {
- bookmarks.push(room);
+ self.conn = jsxc.xmpp.conn;
+};
- jsxc.storage.setUserItem('bookmarks', bookmarks);
+jsxc.xmpp.mam.isEnabled = function() {
+ var mamOptions = jsxc.options.get('mam') || {};
+
+ var features = jsxc.storage.getUserItem('features') || [];
+ var hasFeatureMam1 = features.indexOf('urn:xmpp:mam:1') >= 0;
+ var hasFeatureMam2 = features.indexOf('urn:xmpp:mam:2') >= 0;
+
+ if (hasFeatureMam1 && !hasFeatureMam2) {
+ Strophe.addNamespace('MAM', 'urn:xmpp:mam:1');
}
+
+ return (hasFeatureMam1 || hasFeatureMam2) && mamOptions.enable;
};
-/**
- * Show dialog to edit bookmark.
- *
- * @param {string} room - room jid
- */
-jsxc.xmpp.bookmarks.showDialog = function(room) {
- var dialog = jsxc.gui.dialog.open(jsxc.gui.template.get('bookmarkDialog'));
- var data = jsxc.storage.getUserItem('buddy', room);
+jsxc.xmpp.mam.nextMessages = function(bid) {
+ var self = jsxc.xmpp.mam;
+ var buddyData = jsxc.storage.getUserItem('buddy', bid) || {};
+ var lastArchiveUid = buddyData.lastArchiveUid;
+ var queryId = self.conn.getUniqueId();
+ var mamOptions = jsxc.options.get('mam') || {};
+ var history = jsxc.storage.getUserItem('history', bid) || [];
+
+ if (buddyData.archiveExhausted) {
+ jsxc.debug('No more archived messages.');
+ return;
+ }
- $('#jsxc_room').val(room);
- $('#jsxc_nickname').val(data.nickname);
+ var queryOptions = {
+ queryid: queryId,
+ before: lastArchiveUid || '',
+ with: bid,
+ onMessage: function() {
+ var args = Array.from(arguments);
+ args.unshift(bid);
+ self.onMessage.apply(this, args);
+ return true;
+ },
+ onComplete: function() {
+ var args = Array.from(arguments);
+ args.unshift(bid);
+ self.onComplete.apply(this, args);
+ return true;
+ }
+ };
- $('#jsxc_bookmark').change(function() {
- if ($(this).prop('checked')) {
- $('#jsxc_nickname').prop('disabled', false);
- $('#jsxc_autojoin').prop('disabled', false);
- $('#jsxc_autojoin').parent('.checkbox').removeClass('disabled');
- } else {
- $('#jsxc_nickname').prop('disabled', true);
- $('#jsxc_autojoin').prop('disabled', true).prop('checked', false);
- $('#jsxc_autojoin').parent('.checkbox').addClass('disabled');
+ var oldestMessageId = history[history.length - 1];
+
+ if (oldestMessageId && !lastArchiveUid) {
+ var oldestMessage = new jsxc.Message(oldestMessageId);
+ queryOptions.end = (new Date(oldestMessage.stamp)).toISOString();
+ }
+
+ if (mamOptions.max) {
+ queryOptions.max = mamOptions.max;
+ }
+
+ self.conn.mam.query(undefined, queryOptions);
+};
+
+jsxc.xmpp.mam.onMessage = function(bid, stanza) {
+ stanza = $(stanza);
+ var result = stanza.find('result[xmlns="' + Strophe.NS.MAM + '"]');
+ var queryId = result.attr('queryid');
+
+ if (result.length !== 1) {
+ return;
+ }
+
+ var forwarded = result.find('forwarded[xmlns="' + jsxc.CONST.NS.FORWARD + '"]');
+ var message = forwarded.find('message');
+ var messageId = $(message).attr('id');
+
+ if (message.length !== 1) {
+ return;
+ }
+
+ var from = message.attr('from');
+ var to = message.attr('to');
+
+ if (jsxc.jidToBid(from) !== bid && jsxc.jidToBid(to) !== bid) {
+ return;
+ }
+
+ var delay = forwarded.find('delay[xmlns="urn:xmpp:delay"]');
+ var stamp = (delay.length > 0) ? new Date(delay.attr('stamp')) : new Date();
+ stamp = stamp.getTime();
+
+ var body = $(message).find('body:first').text();
+
+ if (!body || body.match(/\?OTR/i)) {
+ return true;
+ }
+
+ var direction = (jsxc.jidToBid(to) === bid) ? jsxc.Message.OUT : jsxc.Message.IN;
+
+ var win = jsxc.gui.window.get(bid);
+ var textarea = win.find('.jsxc_textarea');
+ if (textarea.find('[id="' + messageId + '"]').length === 0) {
+ var pseudoChatElement = $('');
+ pseudoChatElement.attr('id', messageId.replace(/:/g, '-'));
+ pseudoChatElement.attr('data-queryId', queryId);
+
+ var lastMessage = textarea.find('[data-queryId="' + queryId + '"]').last();
+ var history = jsxc.storage.getUserItem('history', bid) || [];
+
+ if (history.indexOf(messageId) < 0) {
+ if (lastMessage.length === 0) {
+ textarea.prepend(pseudoChatElement);
+ history.push(messageId);
+ } else {
+ lastMessage.after(pseudoChatElement);
+ history.splice(history.indexOf(lastMessage.attr('id').replace(/-/g, ':')), 0, messageId);
+ }
}
+
+ jsxc.storage.setUserItem('history', bid, history);
+ }
+
+ jsxc.gui.window.postMessage({
+ _uid: messageId,
+ bid: bid,
+ direction: direction,
+ msg: body,
+ encrypted: false,
+ forwarded: true,
+ stamp: stamp
});
+};
- $('#jsxc_bookmark').prop('checked', data.bookmarked);
- $('#jsxc_autojoin').prop('checked', data.autojoin);
+jsxc.xmpp.mam.onComplete = function(bid, stanza) {
+ stanza = $(stanza);
+ var fin = stanza.find('fin[xmlns="' + Strophe.NS.MAM + '"]');
+ var buddyData = jsxc.storage.getUserItem('buddy', bid) || {};
+ var win = jsxc.gui.window.get(bid);
- $('#jsxc_bookmark').change();
+ buddyData.archiveExhausted = fin.attr('complete') === 'true';
+ buddyData.lastArchiveUid = fin.find('first').text();
- dialog.find('form').submit(function(ev) {
- ev.preventDefault();
+ if (buddyData.archiveExhausted) {
+ win.find('.jsxc_fade').removeClass('jsxc_mam-enable');
+ }
- var bookmarked = $('#jsxc_bookmark').prop('checked');
- var autojoin = $('#jsxc_autojoin').prop('checked');
- var nickname = $('#jsxc_nickname').val();
+ jsxc.storage.setUserItem('buddy', bid, buddyData);
+};
- if (bookmarked) {
- jsxc.xmpp.bookmarks.add(room, data.name, nickname, autojoin);
- } else if (data.bookmarked) {
- // bookmarked === false
- jsxc.xmpp.bookmarks.delete(room, true);
- }
+jsxc.xmpp.mam.initWindow = function(ev, win) {
+ var self = jsxc.xmpp.mam;
- jsxc.gui.dialog.close();
+ if (!jsxc.xmpp.conn && jsxc.master) {
+ $(document).one('attached.jsxc', function() {
+ self.initWindow(null, win);
+ });
+ return;
+ }
- return false;
+ if (!jsxc.master) {
+ return;
+ }
+
+ $(document).on('features.jsxc', function() {
+ jsxc.xmpp.mam.addLoadButton(win);
+ });
+
+ var features = jsxc.storage.getUserItem('features');
+ if (features !== null) {
+ // features.jsxc was already fired
+ jsxc.xmpp.mam.addLoadButton(win);
+ }
+};
+
+jsxc.xmpp.mam.addLoadButton = function(win) {
+ if (!jsxc.xmpp.mam.isEnabled()) {
+ return;
+ }
+
+ var classNameShow = 'jsxc_show';
+ var classNameMamEnable = 'jsxc_mam-enable';
+ var bid = win.attr('data-bid');
+
+ var element = $('
');
+ element.addClass('jsxc_mam-load-more');
+ element.appendTo(win.find('.slimScrollDiv'));
+ element.click(function() {
+ jsxc.xmpp.mam.nextMessages(bid);
+ });
+ element.text($.t('Load_older_messages'));
+
+ win.find('.jsxc_textarea').scroll(function() {
+ var buddyData = jsxc.storage.getUserItem('buddy', bid) || {};
+
+ if (this.scrollTop < 42 && !buddyData.archiveExhausted) {
+ element.addClass(classNameShow);
+ } else {
+ element.removeClass(classNameShow);
+ }
+
+ if (!buddyData.archiveExhausted) {
+ win.find('.jsxc_fade').addClass(classNameMamEnable);
+ }
});
+
+ win.find('.jsxc_textarea').scroll();
};
+$(document).on('attached.jsxc', jsxc.xmpp.mam.init);
+$(document).on('init.window.jsxc', jsxc.xmpp.mam.initWindow);
+
jsxc.gui.template['aboutDialog'] = '
JavaScript XMPP Chat
\n' +
'
\n' +
-' Version: {{version}}\n' +
+' Version: \n' +
'
www.jsxc.org\n' +
'
\n' +
'
\n' +
' Released under the MIT license\n' +
'
\n' +
'
\n' +
-' Real-time chat app for {{app_name}} and more.\n' +
+' Real-time chat app for and more.\n' +
'
Requires an external XMPP server.\n' +
'
\n' +
'
\n' +
@@ -10333,7 +13103,7 @@
'
\n' +
'
\n' +
' Libraries: \n' +
-' strophe.js (multiple), strophe.js/muc (MIT), strophe.js/disco (MIT), strophe.js/caps (MIT), strophe.js/vcard (MIT), strophe.js/bookmarks (MIT), strophe.js/x (MIT), strophe.jinglejs (MIT), Salsa20 (AGPL3), bigint (public domain), cryptojs (code.google.com/p/crypto-js/wiki/license), eventemitter (MIT), otr.js (MPL v2.0), i18next (MIT), Magnific Popup (MIT), favico.js (MIT), emoji one (CC-BY 4.0)\n' +
+' strophe.js (multiple), strophe.js/muc (MIT), strophe.js/disco (MIT), strophe.js/caps (MIT), strophe.js/vcard (MIT), strophe.js/bookmarks (MIT), strophe.js/x (MIT), strophe.js/chatstates (MIT), strophe.js/mam (MIT), strophe.js/rsm (MIT), strophe.jinglejs (MIT), Salsa20 (AGPL3), bigint (public domain), cryptojs (code.google.com/p/crypto-js/wiki/license), eventemitter (MIT), otr.js (MPL v2.0), i18next (MIT), jquery-i18next (MIT), Magnific Popup (MIT), favico.js (MIT), emoji one (CC-BY 4.0)\n' +
'
\n' +
'\n' +
'
\n' +
@@ -10341,7 +13111,7 @@
jsxc.gui.template['alert'] = '
\n' +
'
\n' +
-' {{msg}}\n' +
+' \n' +
'
\n' +
'';
@@ -10350,20 +13120,13 @@
jsxc.gui.template['approveDialog'] = '
\n' +
'
\n' +
-' .\n' +
+' .\n' +
'
\n' +
'\n' +
'
\n' +
'
\n' +
'';
-jsxc.gui.template['authFailDialog'] = '
\n' +
-'
\n' +
-'\n' +
-'
\n' +
-'
\n' +
-'';
-
jsxc.gui.template['authenticationDialog'] = '
Verification
\n' +
'
\n' +
'
\n' +
@@ -10376,22 +13139,22 @@
'
\n' +
'
\n' +
'
\n' +
-'
\n' +
+'
\n' +
'
\n' +
'
\n' +
' \n' +
-'
{{my_priv_fingerprint}}\n' +
+'
\n' +
'
\n' +
'
\n' +
' \n' +
-'
{{bid_priv_fingerprint}}\n' +
+'
\n' +
'
\n' +
'
\n' +
' \n' +
' \n' +
'
\n' +
'
\n' +
-'
\n' +
-'
\n' +
'';
jsxc.gui.template['incomingCall'] = '
\n' +
'
\n' +
-' {{bid_name}}?\n' +
+' ?\n' +
'
\n' +
'\n' +
-'
\n' +
-'
\n' +
+'
\n' +
+'
\n' +
'';
jsxc.gui.template['joinChat'] = '
\n' +
@@ -10588,16 +13364,17 @@
'
\n' +
'
\n' +
-'
\n' +
'
\n' +
'
\n' +
-'