converse-controlbox.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605
  1. // Converse.js (A browser based XMPP chat client)
  2. // http://conversejs.org
  3. //
  4. // Copyright (c) 2012-2017, Jan-Carel Brand <jc@opkode.com>
  5. // Licensed under the Mozilla Public License (MPLv2)
  6. //
  7. /*global define */
  8. (function (root, factory) {
  9. define(["converse-core",
  10. "lodash.fp",
  11. "tpl!converse_brand_heading",
  12. "tpl!controlbox",
  13. "tpl!controlbox_toggle",
  14. "tpl!login_panel",
  15. "converse-chatview",
  16. "converse-rosterview",
  17. "converse-profile"
  18. ], factory);
  19. }(this, function (
  20. converse,
  21. fp,
  22. tpl_brand_heading,
  23. tpl_controlbox,
  24. tpl_controlbox_toggle,
  25. tpl_login_panel
  26. ) {
  27. "use strict";
  28. const CHATBOX_TYPE = 'chatbox';
  29. const { Strophe, Backbone, Promise, _, moment } = converse.env;
  30. const u = converse.env.utils;
  31. const CONNECTION_STATUS_CSS_CLASS = {
  32. 'Error': 'error',
  33. 'Connecting': 'info',
  34. 'Connection failure': 'error',
  35. 'Authenticating': 'info',
  36. 'Authentication failure': 'error',
  37. 'Connected': 'info',
  38. 'Disconnected': 'error',
  39. 'Disconnecting': 'warn',
  40. 'Attached': 'info',
  41. 'Redirect': 'info',
  42. 'Reconnecting': 'warn'
  43. };
  44. const PRETTY_CONNECTION_STATUS = {
  45. 0: 'Error',
  46. 1: 'Connecting',
  47. 2: 'Connection failure',
  48. 3: 'Authenticating',
  49. 4: 'Authentication failure',
  50. 5: 'Connected',
  51. 6: 'Disconnected',
  52. 7: 'Disconnecting',
  53. 8: 'Attached',
  54. 9: 'Redirect',
  55. 10: 'Reconnecting'
  56. };
  57. const REPORTABLE_STATUSES = [
  58. 0, // ERROR'
  59. 1, // CONNECTING
  60. 2, // CONNFAIL
  61. 3, // AUTHENTICATING
  62. 4, // AUTHFAIL
  63. 7, // DISCONNECTING
  64. 10 // RECONNECTING
  65. ];
  66. converse.plugins.add('converse-controlbox', {
  67. /* Plugin dependencies are other plugins which might be
  68. * overridden or relied upon, and therefore need to be loaded before
  69. * this plugin.
  70. *
  71. * If the setting "strict_plugin_dependencies" is set to true,
  72. * an error will be raised if the plugin is not found. By default it's
  73. * false, which means these plugins are only loaded opportunistically.
  74. *
  75. * NB: These plugins need to have already been loaded via require.js.
  76. */
  77. dependencies: ["converse-modal", "converse-chatboxes", "converse-rosterview", "converse-chatview"],
  78. overrides: {
  79. // Overrides mentioned here will be picked up by converse.js's
  80. // plugin architecture they will replace existing methods on the
  81. // relevant objects or classes.
  82. //
  83. // New functions which don't exist yet can also be added.
  84. _tearDown () {
  85. this.__super__._tearDown.apply(this, arguments);
  86. if (this.rosterview) {
  87. // Removes roster groups
  88. this.rosterview.model.off().reset();
  89. this.rosterview.each(function (groupview) {
  90. groupview.removeAll();
  91. groupview.remove();
  92. });
  93. this.rosterview.removeAll().remove();
  94. }
  95. },
  96. clearSession () {
  97. this.__super__.clearSession.apply(this, arguments);
  98. const chatboxes = _.get(this, 'chatboxes', null);
  99. if (!_.isNil(chatboxes)) {
  100. const controlbox = chatboxes.get('controlbox');
  101. if (controlbox &&
  102. controlbox.collection &&
  103. controlbox.collection.browserStorage) {
  104. controlbox.save({'connected': false});
  105. }
  106. }
  107. },
  108. ChatBoxes: {
  109. chatBoxMayBeShown (chatbox) {
  110. return this.__super__.chatBoxMayBeShown.apply(this, arguments) &&
  111. chatbox.get('id') !== 'controlbox';
  112. },
  113. },
  114. ChatBoxViews: {
  115. onChatBoxAdded (item) {
  116. const { _converse } = this.__super__;
  117. if (item.get('box_id') === 'controlbox') {
  118. let view = this.get(item.get('id'));
  119. if (view) {
  120. view.model = item;
  121. view.initialize();
  122. return view;
  123. } else {
  124. view = new _converse.ControlBoxView({model: item});
  125. return this.add(item.get('id'), view);
  126. }
  127. } else {
  128. return this.__super__.onChatBoxAdded.apply(this, arguments);
  129. }
  130. },
  131. closeAllChatBoxes () {
  132. const { _converse } = this.__super__;
  133. this.each(function (view) {
  134. if (view.model.get('id') === 'controlbox' &&
  135. (_converse.disconnection_cause !== _converse.LOGOUT || _converse.show_controlbox_by_default)) {
  136. return;
  137. }
  138. view.close();
  139. });
  140. return this;
  141. },
  142. getChatBoxWidth (view) {
  143. const { _converse } = this.__super__;
  144. const controlbox = this.get('controlbox');
  145. if (view.model.get('id') === 'controlbox') {
  146. /* We return the width of the controlbox or its toggle,
  147. * depending on which is visible.
  148. */
  149. if (!controlbox || !u.isVisible(controlbox.el)) {
  150. return u.getOuterWidth(_converse.controlboxtoggle.el, true);
  151. } else {
  152. return u.getOuterWidth(controlbox.el, true);
  153. }
  154. } else {
  155. return this.__super__.getChatBoxWidth.apply(this, arguments);
  156. }
  157. }
  158. },
  159. ChatBox: {
  160. initialize () {
  161. if (this.get('id') === 'controlbox') {
  162. this.set({'time_opened': moment(0).valueOf()});
  163. } else {
  164. this.__super__.initialize.apply(this, arguments);
  165. }
  166. },
  167. },
  168. ChatBoxView: {
  169. insertIntoDOM () {
  170. const view = this.__super__._converse.chatboxviews.get("controlbox");
  171. if (view) {
  172. view.el.insertAdjacentElement('afterend', this.el)
  173. } else {
  174. this.__super__.insertIntoDOM.apply(this, arguments);
  175. }
  176. return this;
  177. }
  178. }
  179. },
  180. initialize () {
  181. /* The initialize function gets called as soon as the plugin is
  182. * loaded by converse.js's plugin machinery.
  183. */
  184. const { _converse } = this,
  185. { __ } = _converse;
  186. _converse.api.settings.update({
  187. allow_logout: true,
  188. default_domain: undefined,
  189. locked_domain: undefined,
  190. show_controlbox_by_default: false,
  191. sticky_controlbox: false
  192. });
  193. _converse.api.promises.add('controlboxInitialized');
  194. const LABEL_CONTACTS = __('Contacts');
  195. _converse.addControlBox = () =>
  196. _converse.chatboxes.add({
  197. id: 'controlbox',
  198. box_id: 'controlbox',
  199. type: 'controlbox',
  200. closed: !_converse.show_controlbox_by_default
  201. })
  202. _converse.ControlBoxView = _converse.ChatBoxView.extend({
  203. tagName: 'div',
  204. className: 'chatbox',
  205. id: 'controlbox',
  206. events: {
  207. 'click a.close-chatbox-button': 'close'
  208. },
  209. initialize () {
  210. if (_.isUndefined(_converse.controlboxtoggle)) {
  211. _converse.controlboxtoggle = new _converse.ControlBoxToggle();
  212. }
  213. _converse.controlboxtoggle.el.insertAdjacentElement('afterend', this.el);
  214. this.model.on('change:connected', this.onConnected, this);
  215. this.model.on('destroy', this.hide, this);
  216. this.model.on('hide', this.hide, this);
  217. this.model.on('show', this.show, this);
  218. this.model.on('change:closed', this.ensureClosedState, this);
  219. this.render();
  220. if (this.model.get('connected')) {
  221. _converse.api.waitUntil('rosterViewInitialized')
  222. .then(this.insertRoster.bind(this))
  223. .catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
  224. }
  225. _converse.emit('controlboxInitialized', this);
  226. },
  227. render () {
  228. if (this.model.get('connected')) {
  229. if (_.isUndefined(this.model.get('closed'))) {
  230. this.model.set('closed', !_converse.show_controlbox_by_default);
  231. }
  232. }
  233. this.el.innerHTML = tpl_controlbox(_.extend(this.model.toJSON()));
  234. if (!this.model.get('closed')) {
  235. this.show();
  236. } else {
  237. this.hide();
  238. }
  239. if (!_converse.connection.connected ||
  240. !_converse.connection.authenticated ||
  241. _converse.connection.disconnecting) {
  242. this.renderLoginPanel();
  243. } else if (this.model.get('connected') &&
  244. (!this.controlbox_pane || !u.isVisible(this.controlbox_pane.el))) {
  245. this.renderControlBoxPane();
  246. }
  247. return this;
  248. },
  249. onConnected () {
  250. if (this.model.get('connected')) {
  251. this.render();
  252. _converse.api.waitUntil('rosterViewInitialized')
  253. .then(this.insertRoster.bind(this))
  254. .catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
  255. }
  256. },
  257. insertRoster () {
  258. /* Place the rosterview inside the "Contacts" panel. */
  259. this.controlbox_pane.el.insertAdjacentElement(
  260. 'beforeEnd',
  261. _converse.rosterview.el
  262. );
  263. return this;
  264. },
  265. createBrandHeadingHTML () {
  266. return tpl_brand_heading({
  267. 'sticky_controlbox': _converse.sticky_controlbox
  268. });
  269. },
  270. insertBrandHeading () {
  271. const panes_el = this.el.querySelector('.controlbox-panes');
  272. panes_el.insertAdjacentHTML('beforeBegin', this.createBrandHeadingHTML());
  273. },
  274. renderLoginPanel () {
  275. this.el.classList.add("logged-out");
  276. if (_.isNil(this.loginpanel)) {
  277. this.loginpanel = new _converse.LoginPanel({
  278. 'model': new _converse.LoginPanelModel()
  279. });
  280. const panes = this.el.querySelector('.controlbox-panes');
  281. panes.innerHTML = '';
  282. panes.appendChild(this.loginpanel.render().el);
  283. this.insertBrandHeading();
  284. } else {
  285. this.loginpanel.render();
  286. }
  287. return this;
  288. },
  289. renderControlBoxPane () {
  290. /* Renders the "Contacts" panel of the controlbox.
  291. *
  292. * This will only be called after the user has already been
  293. * logged in.
  294. */
  295. if (this.loginpanel) {
  296. this.loginpanel.remove();
  297. delete this.loginpanel;
  298. }
  299. this.el.classList.remove("logged-out");
  300. this.controlbox_pane = new _converse.ControlBoxPane();
  301. this.el.querySelector('.controlbox-panes').insertAdjacentElement(
  302. 'afterBegin',
  303. this.controlbox_pane.el
  304. )
  305. },
  306. close (ev) {
  307. if (ev && ev.preventDefault) { ev.preventDefault(); }
  308. if (_converse.sticky_controlbox) {
  309. return;
  310. }
  311. if (_converse.connection.connected && !_converse.connection.disconnecting) {
  312. this.model.save({'closed': true});
  313. } else {
  314. this.model.trigger('hide');
  315. }
  316. _converse.emit('controlBoxClosed', this);
  317. return this;
  318. },
  319. ensureClosedState () {
  320. if (this.model.get('closed')) {
  321. this.hide();
  322. } else {
  323. this.show();
  324. }
  325. },
  326. hide (callback) {
  327. if (_converse.sticky_controlbox) {
  328. return;
  329. }
  330. u.addClass('hidden', this.el);
  331. _converse.emit('chatBoxClosed', this);
  332. if (!_converse.connection.connected) {
  333. _converse.controlboxtoggle.render();
  334. }
  335. _converse.controlboxtoggle.show(callback);
  336. return this;
  337. },
  338. onControlBoxToggleHidden () {
  339. this.model.set('closed', false);
  340. this.el.classList.remove('hidden');
  341. _converse.emit('controlBoxOpened', this);
  342. },
  343. show () {
  344. _converse.controlboxtoggle.hide(
  345. this.onControlBoxToggleHidden.bind(this)
  346. );
  347. return this;
  348. },
  349. showHelpMessages () {
  350. /* Override showHelpMessages in ChatBoxView, for now do nothing.
  351. *
  352. * Parameters:
  353. * (Array) msgs: Array of messages
  354. */
  355. return;
  356. }
  357. });
  358. _converse.LoginPanelModel = Backbone.Model.extend({
  359. defaults: {
  360. // Passed-by-reference. Fine in this case because there's
  361. // only one such model.
  362. 'errors': [],
  363. }
  364. });
  365. _converse.LoginPanel = Backbone.VDOMView.extend({
  366. tagName: 'div',
  367. id: "converse-login-panel",
  368. className: 'controlbox-pane fade-in',
  369. events: {
  370. 'submit form#converse-login': 'authenticate',
  371. 'change input': 'validate'
  372. },
  373. initialize (cfg) {
  374. this.model.on('change', this.render, this);
  375. this.listenTo(_converse.connfeedback, 'change', this.render);
  376. },
  377. toHTML () {
  378. const connection_status = _converse.connfeedback.get('connection_status');
  379. let feedback_class, pretty_status;
  380. if (_.includes(REPORTABLE_STATUSES, connection_status)) {
  381. pretty_status = PRETTY_CONNECTION_STATUS[connection_status];
  382. feedback_class = CONNECTION_STATUS_CSS_CLASS[pretty_status];
  383. }
  384. return tpl_login_panel(
  385. _.extend(this.model.toJSON(), {
  386. '__': __,
  387. '_converse': _converse,
  388. 'ANONYMOUS': _converse.ANONYMOUS,
  389. 'EXTERNAL': _converse.EXTERNAL,
  390. 'LOGIN': _converse.LOGIN,
  391. 'PREBIND': _converse.PREBIND,
  392. 'auto_login': _converse.auto_login,
  393. 'authentication': _converse.authentication,
  394. 'connection_status': connection_status,
  395. 'conn_feedback_class': feedback_class,
  396. 'conn_feedback_subject': pretty_status,
  397. 'conn_feedback_message': _converse.connfeedback.get('message'),
  398. 'placeholder_username': (_converse.locked_domain || _converse.default_domain) &&
  399. __('Username') || __('user@domain'),
  400. })
  401. );
  402. },
  403. validate () {
  404. const form = this.el.querySelector('form');
  405. const jid_element = form.querySelector('input[name=jid]');
  406. if (jid_element.value &&
  407. !_converse.locked_domain &&
  408. !_converse.default_domain &&
  409. !u.isValidJID(jid_element.value)) {
  410. jid_element.setCustomValidity(__('Please enter a valid XMPP address'));
  411. return false;
  412. }
  413. jid_element.setCustomValidity('');
  414. return true;
  415. },
  416. authenticate (ev) {
  417. /* Authenticate the user based on a form submission event.
  418. */
  419. if (ev && ev.preventDefault) { ev.preventDefault(); }
  420. if (_converse.authentication === _converse.ANONYMOUS) {
  421. this.connect(_converse.jid, null);
  422. return;
  423. }
  424. if (!this.validate()) {
  425. return;
  426. }
  427. let jid = ev.target.querySelector('input[name=jid]').value;
  428. if (_converse.locked_domain) {
  429. jid = Strophe.escapeNode(jid) + '@' + _converse.locked_domain;
  430. } else if (_converse.default_domain && !_.includes(jid, '@')) {
  431. jid = jid + '@' + _converse.default_domain;
  432. }
  433. this.connect(
  434. jid, _.get(ev.target.querySelector('input[name=password]'), 'value')
  435. );
  436. },
  437. connect (jid, password) {
  438. if (jid) {
  439. const resource = Strophe.getResourceFromJid(jid);
  440. if (!resource) {
  441. jid = jid.toLowerCase() + _converse.generateResource();
  442. } else {
  443. jid = Strophe.getBareJidFromJid(jid).toLowerCase()+'/'+resource;
  444. }
  445. }
  446. if (_.includes(["converse/login", "converse/register"],
  447. Backbone.history.getFragment())) {
  448. _converse.router.navigate('', {'replace': true});
  449. }
  450. _converse.connection.reset();
  451. _converse.connection.connect(jid, password, _converse.onConnectStatusChanged);
  452. }
  453. });
  454. _converse.ControlBoxPane = Backbone.NativeView.extend({
  455. tagName: 'div',
  456. className: 'controlbox-pane',
  457. initialize () {
  458. _converse.xmppstatusview = new _converse.XMPPStatusView({
  459. 'model': _converse.xmppstatus
  460. });
  461. this.el.insertAdjacentElement(
  462. 'afterBegin',
  463. _converse.xmppstatusview.render().el
  464. );
  465. }
  466. });
  467. _converse.ControlBoxToggle = Backbone.NativeView.extend({
  468. tagName: 'a',
  469. className: 'toggle-controlbox hidden',
  470. id: 'toggle-controlbox',
  471. events: {
  472. 'click': 'onClick'
  473. },
  474. attributes: {
  475. 'href': "#"
  476. },
  477. initialize () {
  478. _converse.chatboxviews.insertRowColumn(this.render().el);
  479. _converse.api.waitUntil('initialized')
  480. .then(this.render.bind(this))
  481. .catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
  482. },
  483. render () {
  484. // We let the render method of ControlBoxView decide whether
  485. // the ControlBox or the Toggle must be shown. This prevents
  486. // artifacts (i.e. on page load the toggle is shown only to then
  487. // seconds later be hidden in favor of the control box).
  488. this.el.innerHTML = tpl_controlbox_toggle({
  489. 'label_toggle': _converse.connection.connected ? __('Contacts') : __('Toggle chat')
  490. })
  491. return this;
  492. },
  493. hide (callback) {
  494. u.hideElement(this.el);
  495. callback();
  496. },
  497. show (callback) {
  498. u.fadeIn(this.el, callback);
  499. },
  500. showControlBox () {
  501. let controlbox = _converse.chatboxes.get('controlbox');
  502. if (!controlbox) {
  503. controlbox = _converse.addControlBox();
  504. }
  505. if (_converse.connection.connected) {
  506. controlbox.save({closed: false});
  507. } else {
  508. controlbox.trigger('show');
  509. }
  510. },
  511. onClick (e) {
  512. e.preventDefault();
  513. if (u.isVisible(_converse.root.querySelector("#controlbox"))) {
  514. const controlbox = _converse.chatboxes.get('controlbox');
  515. if (_converse.connection.connected) {
  516. controlbox.save({closed: true});
  517. } else {
  518. controlbox.trigger('hide');
  519. }
  520. } else {
  521. this.showControlBox();
  522. }
  523. }
  524. });
  525. Promise.all([
  526. _converse.api.waitUntil('connectionInitialized'),
  527. _converse.api.waitUntil('chatBoxesInitialized')
  528. ]).then(_converse.addControlBox).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
  529. _converse.on('chatBoxesFetched', () => {
  530. const controlbox = _converse.chatboxes.get('controlbox') || _converse.addControlBox();
  531. controlbox.save({connected:true});
  532. });
  533. const disconnect = function () {
  534. /* Upon disconnection, set connected to `false`, so that if
  535. * we reconnect, "onConnected" will be called,
  536. * to fetch the roster again and to send out a presence stanza.
  537. */
  538. const view = _converse.chatboxviews.get('controlbox');
  539. view.model.set({connected:false});
  540. view.renderLoginPanel();
  541. };
  542. _converse.on('disconnected', disconnect);
  543. }
  544. });
  545. }));