diff --git a/CHANGES.md b/CHANGES.md index 26b5f0406..c0b794d61 100755 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,7 @@ ## 3.3.0 (Unreleased) ### Bugfixes +- #800 Could not register successfully in ejabberd 17.01 - Don't require `auto_login` to be `true` when using the API to log in. - Moment locale wasn't being set to the value passed via the `i18n` option. - Refetch the roster from the server after reconnection. @@ -12,7 +13,9 @@ Otherwise connected contacts might not get your presence updates. ### New Features -- #828 Add routing for the `#converse-login` and `#converse-register` URL +- #314 Add support for opening chat rooms with a URL fragment such as `#converse/room?jid=room@domain` + and private chats with a URL fragment such as `#converse/chat?jid=user@domain` +- #828 Add routing for the `#converse/login` and `#converse/register` URL fragments, which will render the registration and login forms respectively. ### UX/UI changes diff --git a/docs/source/events.rst b/docs/source/events.rst index 1def002bb..1428a28fd 100644 --- a/docs/source/events.rst +++ b/docs/source/events.rst @@ -300,6 +300,22 @@ have to be registered anew. ``_converse.on('reconnected', function () { ... });`` +roomsAutoJoined +--------------- + +Emitted once any rooms that have been configured to be automatically joined, +specified via the _`auto_join_rooms` setting, have been entered. + +``_converse.on('roomsAutoJoined', function () { ... });`` + +Also available as an `ES2015 Promise `_: + +.. code-block:: javascript + + _converse.api.waitUntil('roomsAutoJoined').then(function () { + // Your code here... + }); + roomInviteSent ~~~~~~~~~~~~~~ diff --git a/docs/source/features.rst b/docs/source/features.rst index 97dd51cf3..c453c5ffe 100644 --- a/docs/source/features.rst +++ b/docs/source/features.rst @@ -6,6 +6,16 @@ Features ======== +Open chats via URL +================== + +From version 3.3.0, converse.js now has the ability to open chats (private or +groupchat) based on the URL fragment. + +A room (aka groupchat) can be opened with a URL fragment such as `#converse/room?jid=room@domain` +and a private chat with a URL fragment such as +`#converse/chat?jid=user@domain`. + Off-the-record encryption ========================= diff --git a/src/converse-chatboxes.js b/src/converse-chatboxes.js index 91fe2ec15..62f6650db 100644 --- a/src/converse-chatboxes.js +++ b/src/converse-chatboxes.js @@ -10,7 +10,7 @@ define(["converse-core"], factory); }(this, function (converse) { "use strict"; - const { Backbone, Strophe, b64_sha1, utils, _ } = converse.env; + const { Backbone, Promise, Strophe, b64_sha1, utils, _ } = converse.env; converse.plugins.add('converse-chatboxes', { @@ -55,6 +55,23 @@ 'chatBoxesInitialized' ]); + function openChat (jid) { + if (!utils.isValidJID(jid)) { + return converse.log( + `Invalid JID "${jid}" provided in URL fragment`, + Strophe.LogLevel.WARN + ); + } + Promise.all([ + _converse.api.waitUntil('rosterContactsFetched'), + _converse.api.waitUntil('chatBoxesFetched') + ]).then(() => { + _converse.api.chats.open(jid); + }); + } + _converse.router.route('converse/chat?jid=:jid', openChat); + + _converse.ChatBoxes = Backbone.Collection.extend({ comparator: 'time_opened', @@ -343,9 +360,12 @@ _converse.log("chats.open: You need to provide at least one JID", Strophe.LogLevel.ERROR); return null; } else if (_.isString(jids)) { - return _converse.getViewForChatBox( - _converse.chatboxes.getChatBox(jids, true, attrs).trigger('show') - ); + const chatbox = _converse.chatboxes.getChatBox(jids, true, attrs); + if (_.isNil(chatbox)) { + _converse.log("Could not open chatbox for JID: "+jids); + return; + } + return _converse.getViewForChatBox(chatbox.trigger('show')); } return _.map(jids, (jid) => _converse.getViewForChatBox( diff --git a/src/converse-chatview.js b/src/converse-chatview.js index 32a842dee..8af8f0c37 100644 --- a/src/converse-chatview.js +++ b/src/converse-chatview.js @@ -836,6 +836,9 @@ close (ev) { if (ev && ev.preventDefault) { ev.preventDefault(); } + if (Backbone.history.getFragment() === "converse/chat?jid="+this.model.get('jid')) { + _converse.router.navigate(''); + } if (_converse.connection.connected) { // Immediately sending the chat state, because the // model is going to be destroyed afterwards. diff --git a/src/converse-controlbox.js b/src/converse-controlbox.js index 21ab10327..ce588bf29 100644 --- a/src/converse-controlbox.js +++ b/src/converse-controlbox.js @@ -511,7 +511,7 @@ if (jid_element.value && !_converse.locked_domain && !_converse.default_domain && - _.filter(jid_element.value.split('@')).length < 2) { + !utils.isValidJID(jid_element.value)) { jid_element.setCustomValidity(__('Please enter a valid XMPP address')); return false; } @@ -550,6 +550,10 @@ jid = Strophe.getBareJidFromJid(jid).toLowerCase()+'/'+resource; } } + if (_.includes(["converse/login", "converse/register"], + Backbone.history.getFragment())) { + _converse.router.navigate('', {'replace': true}); + } _converse.connection.reset(); _converse.connection.connect(jid, password, _converse.onConnectStatusChanged); } diff --git a/src/converse-core.js b/src/converse-core.js index 30af1154c..799be571a 100755 --- a/src/converse-core.js +++ b/src/converse-core.js @@ -230,6 +230,9 @@ } }; + _converse.router = new Backbone.Router(); + + _converse.initialize = function (settings, callback) { "use strict"; settings = !_.isUndefined(settings) ? settings : {}; diff --git a/src/converse-muc.js b/src/converse-muc.js index 688504dac..0f1962e2d 100755 --- a/src/converse-muc.js +++ b/src/converse-muc.js @@ -353,9 +353,28 @@ 'toggle_occupants': true }, }); - _converse.api.promises.add('roomsPanelRendered'); + _converse.api.promises.add(['roomsPanelRendered', 'roomsAutoJoined']); - _converse.openChatRoom = function (settings, bring_to_foreground) { + + function openRoom (jid) { + if (!utils.isValidJID(jid)) { + return converse.log( + `Invalid JID "${jid}" provided in URL fragment`, + Strophe.LogLevel.WARN + ); + } + const promises = [_converse.api.waitUntil('roomsAutoJoined')] + if (!_converse.allow_bookmarks) { + promises.push( _converse.api.waitUntil('bookmarksInitialized')); + } + Promise.all(promises).then(() => { + _converse.api.rooms.open(jid); + }); + } + _converse.router.route('converse/room?jid=:jid', openRoom); + + + function openChatRoom (settings, bring_to_foreground) { /* Opens a chat room, making sure that certain attributes * are correct, for example that the "type" is set to * "chatroom". @@ -367,7 +386,7 @@ settings.id = settings.jid; settings.box_id = b64_sha1(settings.jid) return _converse.chatboxviews.showChat(settings, bring_to_foreground); - }; + } _converse.ChatRoom = _converse.ChatBox.extend({ @@ -823,7 +842,11 @@ affiliations = [affiliations]; } return new Promise((resolve, reject) => { - const promises = _.map(affiliations, _.partial(this.requestMemberList, this.model.get('jid'))); + const promises = _.map( + affiliations, + _.partial(this.requestMemberList, this.model.get('jid')) + ); + Promise.all(promises).then( _.flow(this.marshallAffiliationIQs.bind(this), resolve), _.flow(this.marshallAffiliationIQs.bind(this), resolve) @@ -1243,6 +1266,9 @@ * reason for leaving. */ this.hide(); + if (Backbone.history.getFragment() === "converse/room?jid="+this.model.get('jid')) { + _converse.router.navigate(''); + } this.occupantsview.model.reset(); this.occupantsview.model.browserStorage._clear(); if (_converse.connection.connected) { @@ -2637,7 +2663,7 @@ ev.preventDefault(); const data = this.parseRoomDataFromEvent(ev); if (!_.isUndefined(data)) { - _converse.openChatRoom(data); + openChatRoom(data); } }, @@ -2685,7 +2711,7 @@ } } if (result === true) { - const chatroom = _converse.openChatRoom({ + const chatroom = openChatRoom({ 'jid': room_jid, 'password': $x.attr('password') }); @@ -2724,6 +2750,7 @@ Strophe.LogLevel.ERROR); } }); + _converse.emit('roomsAutoJoined'); } _converse.on('chatBoxesFetched', autoJoinRooms); @@ -2775,9 +2802,9 @@ if (_.isUndefined(jids)) { throw new TypeError('rooms.open: You need to provide at least one JID'); } else if (_.isString(jids)) { - return _converse.getChatRoom(jids, attrs, _converse.openChatRoom); + return _converse.getChatRoom(jids, attrs, openChatRoom); } - return _.map(jids, _.partial(_converse.getChatRoom, _, attrs, _converse.openChatRoom)); + return _.map(jids, _.partial(_converse.getChatRoom, _, attrs, openChatRoom)); }, 'get' (jids, attrs, create) { if (_.isString(attrs)) { diff --git a/src/converse-register.js b/src/converse-register.js index 22eaf75f7..f29360d6d 100644 --- a/src/converse-register.js +++ b/src/converse-register.js @@ -59,7 +59,7 @@ // relevant objects or classes. // // New functions which don't exist yet can also be added. - + LoginPanel: { render: function (cfg) { @@ -80,9 +80,6 @@ ControlBoxView: { - events: { - }, - initialize () { this.__super__.initialize.apply(this, arguments); this.model.on('change:active-form', this.showLoginOrRegisterForm.bind(this)) @@ -102,7 +99,6 @@ } }, - renderRegistrationPanel () { const { _converse } = this.__super__; if (_converse.allow_registration) { @@ -149,21 +145,15 @@ providers_link: 'https://xmpp.net/directory.php', // Link to XMPP providers shown on registration page }); - _converse.RegistrationRouter = Backbone.Router.extend({ - initialize () { - this.route('converse-login', _.partial(this.setActiveForm, 'login')); - this.route('converse-register', _.partial(this.setActiveForm, 'register')); - }, - - setActiveForm (value) { - _converse.api.waitUntil('controlboxInitialized').then(() => { - const controlbox = _converse.chatboxes.get('controlbox') - controlbox.set({'active-form': value}); - }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL)); - } - }); - const router = new _converse.RegistrationRouter(); + function setActiveForm (value) { + _converse.api.waitUntil('controlboxInitialized').then(() => { + const controlbox = _converse.chatboxes.get('controlbox') + controlbox.set({'active-form': value}); + }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL)); + } + _converse.router.route('converse/login', _.partial(setActiveForm, 'login')); + _converse.router.route('converse/register', _.partial(setActiveForm, 'register')); _converse.RegisterPanel = Backbone.View.extend({ @@ -425,11 +415,14 @@ ); this.abortRegistration(); } else if (status_code === Strophe.Status.REGISTERED) { - router.navigate(); // Strip the URL fragment _converse.log("Registered successfully."); _converse.connection.reset(); this.showSpinner(); + if (_.includes(["converse/login", "converse/register"], Backbone.history.getFragment())) { + _converse.router.navigate('', {'replace': true}); + } + if (this.fields.password && this.fields.username) { // automatically log the user in _converse.connection.connect( @@ -464,7 +457,7 @@ form.insertAdjacentHTML( 'beforeend', tpl_form_input({ - 'label': key, + 'label': key, 'name': key, 'placeholder': key, 'required': true, diff --git a/src/converse-vcard.js b/src/converse-vcard.js index b2300b312..1df8f47f7 100644 --- a/src/converse-vcard.js +++ b/src/converse-vcard.js @@ -31,7 +31,7 @@ function (iq, jid) { _converse.log( `Error while retrieving vcard for ${jid}`, - Strophe.LogLevel.ERROR + Strophe.LogLevel.WARN ); _converse.createRequestingContactFromVCard(presence, iq, jid); } diff --git a/src/templates/register_link.html b/src/templates/register_link.html index 9606212f4..3a364a0d3 100644 --- a/src/templates/register_link.html +++ b/src/templates/register_link.html @@ -1,4 +1,4 @@

{{{ __("Don't have a chat account?") }}}

-

+

diff --git a/src/templates/register_panel.html b/src/templates/register_panel.html index a74c5e639..ee0877d44 100644 --- a/src/templates/register_panel.html +++ b/src/templates/register_panel.html @@ -17,6 +17,6 @@

{{{ __("Already have a chat account?") }}}

- +

diff --git a/src/utils.js b/src/utils.js index ee9b4b82f..e6f5b2a19 100755 --- a/src/utils.js +++ b/src/utils.js @@ -266,6 +266,10 @@ } }; + u.isValidJID = function (jid) { + return _.filter(jid.split('@')).length === 2 && !jid.startsWith('@') && !jid.endsWith('@'); + }; + u.isSameBareJID = function (jid1, jid2) { return Strophe.getBareJidFromJid(jid1).toLowerCase() === Strophe.getBareJidFromJid(jid2).toLowerCase();