converse-controlbox.js 23 KB

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