2
0

converse-minimize.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  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, window, document */
  8. (function (root, factory) {
  9. define(["converse-core",
  10. "tpl!chatbox_minimize",
  11. "tpl!toggle_chats",
  12. "tpl!trimmed_chat",
  13. "tpl!chats_panel",
  14. "converse-chatview"
  15. ], factory);
  16. }(this, function (
  17. converse,
  18. tpl_chatbox_minimize,
  19. tpl_toggle_chats,
  20. tpl_trimmed_chat,
  21. tpl_chats_panel
  22. ) {
  23. "use strict";
  24. const { _ , Backbone, Promise, Strophe, b64_sha1, moment } = converse.env;
  25. const u = converse.env.utils;
  26. converse.plugins.add('converse-minimize', {
  27. /* Optional dependencies are other plugins which might be
  28. * overridden or relied upon, and therefore need to be loaded before
  29. * this plugin. They are called "optional" because they might not be
  30. * available, in which case any overrides applicable to them will be
  31. * ignored.
  32. *
  33. * It's possible however to make optional dependencies non-optional.
  34. * If the setting "strict_plugin_dependencies" is set to true,
  35. * an error will be raised if the plugin is not found.
  36. *
  37. * NB: These plugins need to have already been loaded via require.js.
  38. */
  39. dependencies: ["converse-chatview", "converse-controlbox", "converse-muc", "converse-muc-views", "converse-headline"],
  40. enabled (_converse) {
  41. return _converse.view_mode == 'overlayed';
  42. },
  43. overrides: {
  44. // Overrides mentioned here will be picked up by converse.js's
  45. // plugin architecture they will replace existing methods on the
  46. // relevant objects or classes.
  47. //
  48. // New functions which don't exist yet can also be added.
  49. registerGlobalEventHandlers () {
  50. const { _converse } = this.__super__;
  51. window.addEventListener("resize", _.debounce(function (ev) {
  52. if (_converse.connection.connected) {
  53. _converse.chatboxviews.trimChats();
  54. }
  55. }, 200));
  56. return this.__super__.registerGlobalEventHandlers.apply(this, arguments);
  57. },
  58. ChatBox: {
  59. initialize () {
  60. this.__super__.initialize.apply(this, arguments);
  61. this.on('show', this.maximize, this);
  62. if (this.get('id') === 'controlbox') {
  63. return;
  64. }
  65. this.save({
  66. 'minimized': this.get('minimized') || false,
  67. 'time_minimized': this.get('time_minimized') || moment(),
  68. });
  69. },
  70. maximize () {
  71. u.safeSave(this, {
  72. 'minimized': false,
  73. 'time_opened': moment().valueOf()
  74. });
  75. },
  76. minimize () {
  77. u.safeSave(this, {
  78. 'minimized': true,
  79. 'time_minimized': moment().format()
  80. });
  81. },
  82. },
  83. ChatBoxView: {
  84. events: {
  85. 'click .toggle-chatbox-button': 'minimize',
  86. },
  87. initialize () {
  88. this.model.on('change:minimized', this.onMinimizedChanged, this);
  89. return this.__super__.initialize.apply(this, arguments);
  90. },
  91. _show () {
  92. const { _converse } = this.__super__;
  93. if (!this.model.get('minimized')) {
  94. this.__super__._show.apply(this, arguments);
  95. _converse.chatboxviews.trimChats(this);
  96. } else {
  97. this.minimize();
  98. }
  99. },
  100. isNewMessageHidden () {
  101. return this.model.get('minimized') ||
  102. this.__super__.isNewMessageHidden.apply(this, arguments);
  103. },
  104. shouldShowOnTextMessage () {
  105. return !this.model.get('minimized') &&
  106. this.__super__.shouldShowOnTextMessage.apply(this, arguments);
  107. },
  108. setChatBoxHeight (height) {
  109. if (!this.model.get('minimized')) {
  110. return this.__super__.setChatBoxHeight.apply(this, arguments);
  111. }
  112. },
  113. setChatBoxWidth (width) {
  114. if (!this.model.get('minimized')) {
  115. return this.__super__.setChatBoxWidth.apply(this, arguments);
  116. }
  117. },
  118. onMinimizedChanged (item) {
  119. if (item.get('minimized')) {
  120. this.minimize();
  121. } else {
  122. this.maximize();
  123. }
  124. },
  125. maximize () {
  126. // Restores a minimized chat box
  127. const { _converse } = this.__super__;
  128. this.insertIntoDOM();
  129. if (!this.model.isScrolledUp()) {
  130. this.model.clearUnreadMsgCounter();
  131. }
  132. this.show();
  133. this.__super__._converse.emit('chatBoxMaximized', this);
  134. return this;
  135. },
  136. minimize (ev) {
  137. const { _converse } = this.__super__;
  138. if (ev && ev.preventDefault) { ev.preventDefault(); }
  139. // save the scroll position to restore it on maximize
  140. if (this.model.collection && this.model.collection.browserStorage) {
  141. this.model.save({'scroll': this.content.scrollTop});
  142. } else {
  143. this.model.set({'scroll': this.content.scrollTop});
  144. }
  145. this.setChatState(_converse.INACTIVE).model.minimize();
  146. this.hide();
  147. _converse.emit('chatBoxMinimized', this);
  148. },
  149. },
  150. ChatBoxHeading: {
  151. render () {
  152. const { _converse } = this.__super__,
  153. { __ } = _converse;
  154. const result = this.__super__.render.apply(this, arguments);
  155. const new_html = tpl_chatbox_minimize(
  156. {info_minimize: __('Minimize this chat box')}
  157. );
  158. const el = this.el.querySelector('.toggle-chatbox-button');
  159. if (el) {
  160. el.outerHTML = new_html;
  161. } else {
  162. const button = this.el.querySelector('.close-chatbox-button');
  163. button.insertAdjacentHTML('afterEnd', new_html);
  164. }
  165. }
  166. },
  167. ChatRoomView: {
  168. events: {
  169. 'click .toggle-chatbox-button': 'minimize',
  170. },
  171. initialize () {
  172. this.model.on('change:minimized', function (item) {
  173. if (item.get('minimized')) {
  174. this.hide();
  175. } else {
  176. this.maximize();
  177. }
  178. }, this);
  179. const result = this.__super__.initialize.apply(this, arguments);
  180. if (this.model.get('minimized')) {
  181. this.hide();
  182. }
  183. return result;
  184. },
  185. generateHeadingHTML () {
  186. const { _converse } = this.__super__,
  187. { __ } = _converse;
  188. const html = this.__super__.generateHeadingHTML.apply(this, arguments);
  189. const div = document.createElement('div');
  190. div.innerHTML = html;
  191. const button = div.querySelector('.close-chatbox-button');
  192. button.insertAdjacentHTML('afterend',
  193. tpl_chatbox_minimize({
  194. 'info_minimize': __('Minimize this chat box')
  195. })
  196. );
  197. return div.innerHTML;
  198. }
  199. },
  200. ChatBoxes: {
  201. chatBoxMayBeShown (chatbox) {
  202. return this.__super__.chatBoxMayBeShown.apply(this, arguments) &&
  203. !chatbox.get('minimized');
  204. },
  205. },
  206. ChatBoxViews: {
  207. getChatBoxWidth (view) {
  208. if (!view.model.get('minimized') && u.isVisible(view.el)) {
  209. return u.getOuterWidth(view.el, true);
  210. }
  211. return 0;
  212. },
  213. getShownChats () {
  214. return this.filter((view) =>
  215. // The controlbox can take a while to close,
  216. // so we need to check its state. That's why we checked
  217. // the 'closed' state.
  218. !view.model.get('minimized') &&
  219. !view.model.get('closed') &&
  220. u.isVisible(view.el)
  221. );
  222. },
  223. trimChats (newchat) {
  224. /* This method is called when a newly created chat box will
  225. * be shown.
  226. *
  227. * It checks whether there is enough space on the page to show
  228. * another chat box. Otherwise it minimizes the oldest chat box
  229. * to create space.
  230. */
  231. const { _converse } = this.__super__,
  232. shown_chats = this.getShownChats(),
  233. body_width = u.getOuterWidth(document.querySelector('body'), true);
  234. if (_converse.no_trimming || shown_chats.length <= 1) {
  235. return;
  236. }
  237. if (this.getChatBoxWidth(shown_chats[0]) === body_width) {
  238. // If the chats shown are the same width as the body,
  239. // then we're in responsive mode and the chats are
  240. // fullscreen. In this case we don't trim.
  241. return;
  242. }
  243. _converse.api.waitUntil('minimizedChatsInitialized').then(() => {
  244. const minimized_el = _.get(_converse.minimized_chats, 'el'),
  245. new_id = newchat ? newchat.model.get('id') : null;
  246. if (minimized_el) {
  247. const minimized_width = _.includes(this.model.pluck('minimized'), true) ?
  248. u.getOuterWidth(minimized_el, true) : 0;
  249. const boxes_width = _.reduce(
  250. this.xget(new_id),
  251. (memo, view) => memo + this.getChatBoxWidth(view),
  252. newchat ? u.getOuterWidth(newchat.el, true) : 0
  253. );
  254. if ((minimized_width + boxes_width) > body_width) {
  255. const oldest_chat = this.getOldestMaximizedChat([new_id]);
  256. if (oldest_chat) {
  257. // We hide the chat immediately, because waiting
  258. // for the event to fire (and letting the
  259. // ChatBoxView hide it then) causes race
  260. // conditions.
  261. const view = this.get(oldest_chat.get('id'));
  262. if (view) {
  263. view.hide();
  264. }
  265. oldest_chat.minimize();
  266. }
  267. }
  268. }
  269. }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
  270. },
  271. getOldestMaximizedChat (exclude_ids) {
  272. // Get oldest view (if its id is not excluded)
  273. exclude_ids.push('controlbox');
  274. let i = 0;
  275. let model = this.model.sort().at(i);
  276. while (_.includes(exclude_ids, model.get('id')) ||
  277. model.get('minimized') === true) {
  278. i++;
  279. model = this.model.at(i);
  280. if (!model) {
  281. return null;
  282. }
  283. }
  284. return model;
  285. }
  286. }
  287. },
  288. initialize () {
  289. /* The initialize function gets called as soon as the plugin is
  290. * loaded by Converse.js's plugin machinery.
  291. */
  292. const { _converse } = this,
  293. { __ } = _converse;
  294. // Add new HTML templates.
  295. _converse.templates.chatbox_minimize = tpl_chatbox_minimize;
  296. _converse.templates.toggle_chats = tpl_toggle_chats;
  297. _converse.templates.trimmed_chat = tpl_trimmed_chat;
  298. _converse.templates.chats_panel = tpl_chats_panel;
  299. _converse.api.settings.update({
  300. no_trimming: false, // Set to true for phantomjs tests (where browser apparently has no width)
  301. });
  302. _converse.api.promises.add('minimizedChatsInitialized');
  303. _converse.MinimizedChatBoxView = Backbone.NativeView.extend({
  304. tagName: 'div',
  305. className: 'chat-head row no-gutters',
  306. events: {
  307. 'click .close-chatbox-button': 'close',
  308. 'click .restore-chat': 'restore'
  309. },
  310. initialize () {
  311. this.model.on('change:num_unread', this.render, this);
  312. },
  313. render () {
  314. const data = _.extend(
  315. this.model.toJSON(),
  316. { 'tooltip': __('Click to restore this chat') }
  317. );
  318. if (this.model.get('type') === 'chatroom') {
  319. data.title = this.model.get('name');
  320. u.addClass('chat-head-chatroom', this.el);
  321. } else {
  322. data.title = this.model.get('fullname');
  323. u.addClass('chat-head-chatbox', this.el);
  324. }
  325. this.el.innerHTML = tpl_trimmed_chat(data);
  326. return this.el;
  327. },
  328. close (ev) {
  329. if (ev && ev.preventDefault) { ev.preventDefault(); }
  330. this.remove();
  331. const view = _converse.chatboxviews.get(this.model.get('id'));
  332. if (view) {
  333. // This will call model.destroy(), removing it from the
  334. // collection and will also emit 'chatBoxClosed'
  335. view.close();
  336. } else {
  337. this.model.destroy();
  338. _converse.emit('chatBoxClosed', this);
  339. }
  340. return this;
  341. },
  342. restore: _.debounce(function (ev) {
  343. if (ev && ev.preventDefault) { ev.preventDefault(); }
  344. this.model.off('change:num_unread', null, this);
  345. this.remove();
  346. this.model.maximize();
  347. }, 200, {'leading': true})
  348. });
  349. _converse.MinimizedChats = Backbone.Overview.extend({
  350. tagName: 'div',
  351. id: "minimized-chats",
  352. className: 'hidden',
  353. events: {
  354. "click #toggle-minimized-chats": "toggle"
  355. },
  356. initialize () {
  357. this.render();
  358. this.initToggle();
  359. this.addMultipleChats(this.model.where({'minimized': true}));
  360. this.model.on("add", this.onChanged, this);
  361. this.model.on("destroy", this.removeChat, this);
  362. this.model.on("change:minimized", this.onChanged, this);
  363. this.model.on('change:num_unread', this.updateUnreadMessagesCounter, this);
  364. },
  365. render () {
  366. if (!this.el.parentElement) {
  367. this.el.innerHTML = tpl_chats_panel();
  368. _converse.chatboxviews.insertRowColumn(this.el);
  369. }
  370. if (this.keys().length === 0) {
  371. this.el.classList.add('hidden');
  372. } else if (this.keys().length > 0 && !u.isVisible(this.el)) {
  373. this.el.classList.remove('hidden');
  374. _converse.chatboxviews.trimChats();
  375. }
  376. return this.el;
  377. },
  378. tearDown () {
  379. this.model.off("add", this.onChanged);
  380. this.model.off("destroy", this.removeChat);
  381. this.model.off("change:minimized", this.onChanged);
  382. this.model.off('change:num_unread', this.updateUnreadMessagesCounter);
  383. return this;
  384. },
  385. initToggle () {
  386. this.toggleview = new _converse.MinimizedChatsToggleView({
  387. model: new _converse.MinimizedChatsToggle()
  388. });
  389. const id = b64_sha1(`converse.minchatstoggle${_converse.bare_jid}`);
  390. this.toggleview.model.id = id; // Appears to be necessary for backbone.browserStorage
  391. this.toggleview.model.browserStorage = new Backbone.BrowserStorage[_converse.storage](id);
  392. this.toggleview.model.fetch();
  393. },
  394. toggle (ev) {
  395. if (ev && ev.preventDefault) { ev.preventDefault(); }
  396. this.toggleview.model.save({'collapsed': !this.toggleview.model.get('collapsed')});
  397. u.slideToggleElement(this.el.querySelector('.minimized-chats-flyout'), 200);
  398. },
  399. onChanged (item) {
  400. if (item.get('id') === 'controlbox') {
  401. // The ControlBox has it's own minimize toggle
  402. return;
  403. }
  404. if (item.get('minimized')) {
  405. this.addChat(item);
  406. } else if (this.get(item.get('id'))) {
  407. this.removeChat(item);
  408. }
  409. },
  410. addChatView (item) {
  411. const existing = this.get(item.get('id'));
  412. if (existing && existing.el.parentNode) {
  413. return;
  414. }
  415. const view = new _converse.MinimizedChatBoxView({model: item});
  416. this.el.querySelector('.minimized-chats-flyout').insertAdjacentElement('beforeEnd', view.render());
  417. this.add(item.get('id'), view);
  418. },
  419. addMultipleChats (items) {
  420. _.each(items, this.addChatView.bind(this));
  421. this.toggleview.model.set({'num_minimized': this.keys().length});
  422. this.render();
  423. },
  424. addChat (item) {
  425. this.addChatView(item);
  426. this.toggleview.model.set({'num_minimized': this.keys().length});
  427. this.render();
  428. },
  429. removeChat (item) {
  430. this.remove(item.get('id'));
  431. this.toggleview.model.set({'num_minimized': this.keys().length});
  432. this.render();
  433. },
  434. updateUnreadMessagesCounter () {
  435. const ls = this.model.pluck('num_unread');
  436. let count = 0, i;
  437. for (i=0; i<ls.length; i++) { count += ls[i]; }
  438. this.toggleview.model.save({'num_unread': count});
  439. this.render();
  440. }
  441. });
  442. _converse.MinimizedChatsToggle = Backbone.Model.extend({
  443. defaults: {
  444. 'collapsed': false,
  445. 'num_minimized': 0,
  446. 'num_unread': 0
  447. }
  448. });
  449. _converse.MinimizedChatsToggleView = Backbone.NativeView.extend({
  450. el: '#toggle-minimized-chats',
  451. initialize () {
  452. this.model.on('change:num_minimized', this.render, this);
  453. this.model.on('change:num_unread', this.render, this);
  454. this.flyout = this.el.parentElement.querySelector('.minimized-chats-flyout');
  455. },
  456. render () {
  457. this.el.innerHTML = tpl_toggle_chats(
  458. _.extend(this.model.toJSON(), {
  459. 'Minimized': __('Minimized')
  460. })
  461. );
  462. if (this.model.get('collapsed')) {
  463. u.hideElement(this.flyout);
  464. } else {
  465. u.showElement(this.flyout);
  466. }
  467. return this.el;
  468. }
  469. });
  470. Promise.all([
  471. _converse.api.waitUntil('connectionInitialized'),
  472. _converse.api.waitUntil('chatBoxesInitialized')
  473. ]).then(() => {
  474. _converse.minimized_chats = new _converse.MinimizedChats({
  475. model: _converse.chatboxes
  476. });
  477. _converse.emit('minimizedChatsInitialized');
  478. }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
  479. _converse.on('controlBoxOpened', function (chatbox) {
  480. // Wrapped in anon method because at scan time, chatboxviews
  481. // attr not set yet.
  482. if (_converse.connection.connected) {
  483. _converse.chatboxviews.trimChats(chatbox);
  484. }
  485. });
  486. }
  487. });
  488. }));