2
0

converse-chatboxes.js 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889
  1. // Converse.js
  2. // http://conversejs.org
  3. //
  4. // Copyright (c) 2012-2018, the Converse.js developers
  5. // Licensed under the Mozilla Public License (MPLv2)
  6. (function (root, factory) {
  7. define([
  8. "converse-core",
  9. "emojione",
  10. "filesize",
  11. "tpl!chatboxes",
  12. "backbone.overview",
  13. "form-utils"
  14. ], factory);
  15. }(this, function (converse, emojione, filesize, tpl_chatboxes) {
  16. "use strict";
  17. const { $msg, Backbone, Promise, Strophe, b64_sha1, moment, sizzle, utils, _ } = converse.env;
  18. const u = converse.env.utils;
  19. converse.plugins.add('converse-chatboxes', {
  20. dependencies: ["converse-roster", "converse-vcard"],
  21. overrides: {
  22. // Overrides mentioned here will be picked up by converse.js's
  23. // plugin architecture they will replace existing methods on the
  24. // relevant objects or classes.
  25. disconnect: function () {
  26. const { _converse } = this.__super__;
  27. _converse.chatboxviews.closeAllChatBoxes();
  28. return this.__super__.disconnect.apply(this, arguments);
  29. },
  30. logOut: function () {
  31. const { _converse } = this.__super__;
  32. _converse.chatboxviews.closeAllChatBoxes();
  33. return this.__super__.logOut.apply(this, arguments);
  34. },
  35. initStatus: function (reconnecting) {
  36. const { _converse } = this.__super__;
  37. if (!reconnecting) {
  38. _converse.chatboxviews.closeAllChatBoxes();
  39. }
  40. return this.__super__.initStatus.apply(this, arguments);
  41. }
  42. },
  43. initialize () {
  44. /* The initialize function gets called as soon as the plugin is
  45. * loaded by converse.js's plugin machinery.
  46. */
  47. const { _converse } = this,
  48. { __ } = _converse;
  49. // Configuration values for this plugin
  50. // ====================================
  51. // Refer to docs/source/configuration.rst for explanations of these
  52. // configuration settings.
  53. _converse.api.settings.update({
  54. auto_join_private_chats: [],
  55. });
  56. _converse.api.promises.add([
  57. 'chatBoxesFetched',
  58. 'chatBoxesInitialized',
  59. 'privateChatsAutoJoined'
  60. ]);
  61. function openChat (jid) {
  62. if (!utils.isValidJID(jid)) {
  63. return _converse.log(
  64. `Invalid JID "${jid}" provided in URL fragment`,
  65. Strophe.LogLevel.WARN
  66. );
  67. }
  68. Promise.all([
  69. _converse.api.waitUntil('rosterContactsFetched'),
  70. _converse.api.waitUntil('chatBoxesFetched')
  71. ]).then(() => {
  72. _converse.api.chats.open(jid);
  73. });
  74. }
  75. _converse.router.route('converse/chat?jid=:jid', openChat);
  76. _converse.Message = Backbone.Model.extend({
  77. defaults () {
  78. return {
  79. 'msgid': _converse.connection.getUniqueId(),
  80. 'time': moment().format()
  81. };
  82. },
  83. initialize () {
  84. this.setVCard();
  85. if (this.get('file')) {
  86. this.on('change:put', this.uploadFile, this);
  87. if (!_.includes([_converse.SUCCESS, _converse.FAILURE], this.get('upload'))) {
  88. this.getRequestSlotURL();
  89. }
  90. }
  91. if (this.isOnlyChatStateNotification()) {
  92. window.setTimeout(this.destroy.bind(this), 20000);
  93. }
  94. },
  95. setVCard () {
  96. if (this.get('type') === 'groupchat') {
  97. const chatbox = this.collection.chatbox,
  98. nick = Strophe.getResourceFromJid(this.get('from'));
  99. if (chatbox.get('nick') === nick) {
  100. this.vcard = _converse.xmppstatus.vcard;
  101. } else {
  102. const occupant = chatbox.occupants.findWhere({'nick': nick});
  103. const jid = (occupant && occupant.get('jid')) ? occupant.get('jid') : this.get('from');
  104. this.vcard = _converse.vcards.findWhere({'jid': jid}) || _converse.vcards.create({'jid': jid});
  105. }
  106. } else {
  107. const jid = this.get('from');
  108. this.vcard = _converse.vcards.findWhere({'jid': jid}) || _converse.vcards.create({'jid': jid});
  109. }
  110. },
  111. isOnlyChatStateNotification () {
  112. return u.isOnlyChatStateNotification(this);
  113. },
  114. getDisplayName () {
  115. if (this.get('type') === 'groupchat') {
  116. return this.get('nick');
  117. } else {
  118. return this.vcard.get('fullname') || this.get('from');
  119. }
  120. },
  121. sendSlotRequestStanza () {
  122. /* Send out an IQ stanza to request a file upload slot.
  123. *
  124. * https://xmpp.org/extensions/xep-0363.html#request
  125. */
  126. const file = this.get('file');
  127. return new Promise((resolve, reject) => {
  128. const iq = converse.env.$iq({
  129. 'from': _converse.jid,
  130. 'to': this.get('slot_request_url'),
  131. 'type': 'get'
  132. }).c('request', {
  133. 'xmlns': Strophe.NS.HTTPUPLOAD,
  134. 'filename': file.name,
  135. 'size': file.size,
  136. 'content-type': file.type
  137. })
  138. _converse.connection.sendIQ(iq, resolve, reject);
  139. });
  140. },
  141. getRequestSlotURL () {
  142. this.sendSlotRequestStanza().then((stanza) => {
  143. const slot = stanza.querySelector('slot');
  144. if (slot) {
  145. this.save({
  146. 'get': slot.querySelector('get').getAttribute('url'),
  147. 'put': slot.querySelector('put').getAttribute('url'),
  148. });
  149. } else {
  150. return this.save({
  151. 'type': 'error',
  152. 'message': __("Sorry, could not determine file upload URL.")
  153. });
  154. }
  155. }).catch((e) => {
  156. _converse.log(e, Strophe.LogLevel.ERROR);
  157. return this.save({
  158. 'type': 'error',
  159. 'message': __("Sorry, could not determine upload URL.")
  160. });
  161. });
  162. },
  163. uploadFile () {
  164. const xhr = new XMLHttpRequest();
  165. xhr.onreadystatechange = () => {
  166. if (xhr.readyState === XMLHttpRequest.DONE) {
  167. _converse.log("Status: " + xhr.status, Strophe.LogLevel.INFO);
  168. if (xhr.status === 200 || xhr.status === 201) {
  169. this.save({
  170. 'upload': _converse.SUCCESS,
  171. 'oob_url': this.get('get'),
  172. 'message': this.get('get')
  173. });
  174. } else {
  175. xhr.onerror();
  176. }
  177. }
  178. };
  179. xhr.upload.addEventListener("progress", (evt) => {
  180. if (evt.lengthComputable) {
  181. this.set('progress', evt.loaded / evt.total);
  182. }
  183. }, false);
  184. xhr.onerror = () => {
  185. let message = __('Sorry, could not succesfully upload your file.');
  186. if (xhr.responseText) {
  187. message += ' ' + __('Your server\'s response: "%1$s"', xhr.responseText)
  188. }
  189. this.save({
  190. 'type': 'error',
  191. 'upload': _converse.FAILURE,
  192. 'message': message
  193. });
  194. };
  195. xhr.open('PUT', this.get('put'), true);
  196. xhr.setRequestHeader("Content-type", 'application/octet-stream');
  197. xhr.send(this.get('file'));
  198. }
  199. });
  200. _converse.Messages = Backbone.Collection.extend({
  201. model: _converse.Message,
  202. comparator: 'time'
  203. });
  204. _converse.ChatBox = _converse.ModelWithVCardAndPresence.extend({
  205. defaults: {
  206. 'bookmarked': false,
  207. 'chat_state': undefined,
  208. 'num_unread': 0,
  209. 'type': 'chatbox',
  210. 'message_type': 'chat',
  211. 'url': ''
  212. },
  213. initialize () {
  214. _converse.ModelWithVCardAndPresence.prototype.initialize.apply(this, arguments);
  215. _converse.api.waitUntil('rosterContactsFetched').then(() => {
  216. this.addRelatedContact(_converse.roster.findWhere({'jid': this.get('jid')}));
  217. });
  218. this.messages = new _converse.Messages();
  219. this.messages.browserStorage = new Backbone.BrowserStorage[_converse.storage](
  220. b64_sha1(`converse.messages${this.get('jid')}${_converse.bare_jid}`));
  221. this.messages.chatbox = this;
  222. this.messages.on('change:upload', (message) => {
  223. if (message.get('upload') === _converse.SUCCESS) {
  224. this.sendMessageStanza(message);
  225. }
  226. });
  227. this.on('change:chat_state', this.sendChatState, this);
  228. this.save({
  229. // The chat_state will be set to ACTIVE once the chat box is opened
  230. // and we listen for change:chat_state, so shouldn't set it to ACTIVE here.
  231. 'box_id' : b64_sha1(this.get('jid')),
  232. 'time_opened': this.get('time_opened') || moment().valueOf(),
  233. 'user_id' : Strophe.getNodeFromJid(this.get('jid'))
  234. });
  235. },
  236. addRelatedContact (contact) {
  237. if (!_.isUndefined(contact)) {
  238. this.contact = contact;
  239. this.trigger('contactAdded', contact);
  240. }
  241. },
  242. getDisplayName () {
  243. return this.vcard.get('fullname') || this.get('jid');
  244. },
  245. createMessageStanza (message) {
  246. /* Given a _converse.Message Backbone.Model, return the XML
  247. * stanza that represents it.
  248. *
  249. * Parameters:
  250. * (Object) message - The Backbone.Model representing the message
  251. */
  252. const stanza = $msg({
  253. 'from': _converse.connection.jid,
  254. 'to': this.get('jid'),
  255. 'type': this.get('message_type'),
  256. 'id': message.get('msgid')
  257. }).c('body').t(message.get('message')).up()
  258. .c(_converse.ACTIVE, {'xmlns': Strophe.NS.CHATSTATES}).up();
  259. if (message.get('is_spoiler')) {
  260. if (message.get('spoiler_hint')) {
  261. stanza.c('spoiler', {'xmlns': Strophe.NS.SPOILER }, message.get('spoiler_hint')).up();
  262. } else {
  263. stanza.c('spoiler', {'xmlns': Strophe.NS.SPOILER }).up();
  264. }
  265. }
  266. if (message.get('file')) {
  267. stanza.c('x', {'xmlns': Strophe.NS.OUTOFBAND}).c('url').t(message.get('message')).up();
  268. }
  269. return stanza;
  270. },
  271. sendMessageStanza (message) {
  272. const messageStanza = this.createMessageStanza(message);
  273. _converse.connection.send(messageStanza);
  274. if (_converse.forward_messages) {
  275. // Forward the message, so that other connected resources are also aware of it.
  276. _converse.connection.send(
  277. $msg({
  278. 'to': _converse.bare_jid,
  279. 'type': this.get('message_type'),
  280. 'id': message.get('msgid')
  281. }).c('forwarded', {'xmlns': Strophe.NS.FORWARD})
  282. .c('delay', {
  283. 'xmns': Strophe.NS.DELAY,
  284. 'stamp': moment().format()
  285. }).up()
  286. .cnode(messageStanza.tree())
  287. );
  288. }
  289. },
  290. getOutgoingMessageAttributes (text, spoiler_hint) {
  291. const fullname = _converse.xmppstatus.get('fullname'),
  292. is_spoiler = this.get('composing_spoiler');
  293. return {
  294. 'fullname': fullname,
  295. 'from': _converse.bare_jid,
  296. 'sender': 'me',
  297. 'time': moment().format(),
  298. 'message': text ? u.httpToGeoUri(emojione.shortnameToUnicode(text), _converse) : undefined,
  299. 'is_spoiler': is_spoiler,
  300. 'spoiler_hint': is_spoiler ? spoiler_hint : undefined
  301. };
  302. },
  303. sendMessage (attrs) {
  304. /* Responsible for sending off a text message.
  305. *
  306. * Parameters:
  307. * (Message) message - The chat message
  308. */
  309. this.sendMessageStanza(this.messages.create(attrs));
  310. },
  311. sendChatState () {
  312. /* Sends a message with the status of the user in this chat session
  313. * as taken from the 'chat_state' attribute of the chat box.
  314. * See XEP-0085 Chat State Notifications.
  315. */
  316. _converse.connection.send(
  317. $msg({'to':this.get('jid'), 'type': 'chat'})
  318. .c(this.get('chat_state'), {'xmlns': Strophe.NS.CHATSTATES}).up()
  319. .c('no-store', {'xmlns': Strophe.NS.HINTS}).up()
  320. .c('no-permanent-store', {'xmlns': Strophe.NS.HINTS})
  321. );
  322. },
  323. sendFiles (files) {
  324. _converse.api.disco.supports(Strophe.NS.HTTPUPLOAD, _converse.domain).then((result) => {
  325. const item = result.pop(),
  326. data = item.dataforms.where({'FORM_TYPE': {'value': Strophe.NS.HTTPUPLOAD, 'type': "hidden"}}).pop(),
  327. max_file_size = window.parseInt(_.get(data, 'attributes.max-file-size.value')),
  328. slot_request_url = _.get(item, 'id');
  329. if (!slot_request_url) {
  330. this.messages.create({
  331. 'message': __("Sorry, looks like file upload is not supported by your server."),
  332. 'type': 'error',
  333. });
  334. return;
  335. }
  336. _.each(files, (file) => {
  337. if (!window.isNaN(max_file_size) && window.parseInt(file.size) > max_file_size) {
  338. return this.messages.create({
  339. 'message': __('The size of your file, %1$s, exceeds the maximum allowed by your server, which is %2$s.',
  340. file.name, filesize(max_file_size)),
  341. 'type': 'error',
  342. });
  343. } else {
  344. this.messages.create(
  345. _.extend(
  346. this.getOutgoingMessageAttributes(), {
  347. 'file': file,
  348. 'progress': 0,
  349. 'slot_request_url': slot_request_url,
  350. 'type': this.get('message_type'),
  351. })
  352. );
  353. }
  354. });
  355. }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
  356. },
  357. getMessageBody (message) {
  358. const type = message.getAttribute('type');
  359. return (type === 'error') ?
  360. _.propertyOf(message.querySelector('error text'))('textContent') :
  361. _.propertyOf(message.querySelector('body'))('textContent');
  362. },
  363. getMessageAttributesFromStanza (message, delay, original_stanza) {
  364. /* Parses a passed in message stanza and returns an object
  365. * of attributes.
  366. *
  367. * Parameters:
  368. * (XMLElement) message - The message stanza
  369. * (XMLElement) delay - The <delay> node from the
  370. * stanza, if there was one.
  371. * (XMLElement) original_stanza - The original stanza,
  372. * that contains the message stanza, if it was
  373. * contained, otherwise it's the message stanza itself.
  374. */
  375. delay = delay || message.querySelector('delay');
  376. const { _converse } = this.__super__,
  377. { __ } = _converse,
  378. spoiler = message.querySelector(`spoiler[xmlns="${Strophe.NS.SPOILER}"]`),
  379. chat_state = message.getElementsByTagName(_converse.COMPOSING).length && _converse.COMPOSING ||
  380. message.getElementsByTagName(_converse.PAUSED).length && _converse.PAUSED ||
  381. message.getElementsByTagName(_converse.INACTIVE).length && _converse.INACTIVE ||
  382. message.getElementsByTagName(_converse.ACTIVE).length && _converse.ACTIVE ||
  383. message.getElementsByTagName(_converse.GONE).length && _converse.GONE;
  384. const attrs = {
  385. 'type': message.getAttribute('type'),
  386. 'chat_state': chat_state,
  387. 'delayed': !_.isNull(delay),
  388. 'message': this.getMessageBody(message) || undefined,
  389. 'msgid': message.getAttribute('id'),
  390. 'time': delay ? delay.getAttribute('stamp') : moment().format(),
  391. 'is_spoiler': !_.isNull(spoiler)
  392. };
  393. if (attrs.type === 'groupchat') {
  394. attrs.from = message.getAttribute('from');
  395. attrs.nick = Strophe.unescapeNode(Strophe.getResourceFromJid(attrs.from));
  396. if (attrs.from === this.get('nick')) {
  397. attrs.sender = 'me';
  398. } else {
  399. attrs.sender = 'them';
  400. }
  401. } else {
  402. attrs.from = Strophe.getBareJidFromJid(message.getAttribute('from'));
  403. if (attrs.from === _converse.bare_jid) {
  404. attrs.sender = 'me';
  405. attrs.fullname = _converse.xmppstatus.get('fullname');
  406. } else {
  407. attrs.sender = 'them';
  408. attrs.fullname = this.get('fullname');
  409. }
  410. }
  411. _.each(sizzle(`x[xmlns="${Strophe.NS.OUTOFBAND}"]`, message), (xform) => {
  412. attrs['oob_url'] = xform.querySelector('url').textContent;
  413. attrs['oob_desc'] = xform.querySelector('url').textContent;
  414. });
  415. if (spoiler) {
  416. attrs.spoiler_hint = spoiler.textContent.length > 0 ? spoiler.textContent : '';
  417. }
  418. return attrs;
  419. },
  420. createMessage (message, delay, original_stanza) {
  421. /* Create a Backbone.Message object inside this chat box
  422. * based on the identified message stanza.
  423. */
  424. const attrs = this.getMessageAttributesFromStanza.apply(this, arguments)
  425. const is_csn = u.isOnlyChatStateNotification(attrs);
  426. if (is_csn && attrs.delayed) {
  427. // No need showing old CSNs
  428. return;
  429. } else if (!is_csn && !attrs.file && !attrs.message && !attrs.oob_url && attrs.type !== 'error') {
  430. // TODO: handle <subject> messages (currently being done by ChatRoom)
  431. return;
  432. } else {
  433. return this.messages.create(attrs);
  434. }
  435. },
  436. newMessageWillBeHidden () {
  437. /* Returns a boolean to indicate whether a newly received
  438. * message will be visible to the user or not.
  439. */
  440. return this.get('hidden') ||
  441. this.get('minimized') ||
  442. this.isScrolledUp() ||
  443. _converse.windowState === 'hidden';
  444. },
  445. incrementUnreadMsgCounter (stanza) {
  446. /* Given a newly received message, update the unread counter if
  447. * necessary.
  448. */
  449. if (_.isNull(stanza.querySelector('body'))) {
  450. return; // The message has no text
  451. }
  452. if (utils.isNewMessage(stanza) && this.newMessageWillBeHidden()) {
  453. this.save({'num_unread': this.get('num_unread') + 1});
  454. _converse.incrementMsgCounter();
  455. }
  456. },
  457. clearUnreadMsgCounter () {
  458. u.safeSave(this, {'num_unread': 0});
  459. },
  460. isScrolledUp () {
  461. return this.get('scrolled', true);
  462. }
  463. });
  464. _converse.ChatBoxes = Backbone.Collection.extend({
  465. comparator: 'time_opened',
  466. model (attrs, options) {
  467. return new _converse.ChatBox(attrs, options);
  468. },
  469. registerMessageHandler () {
  470. _converse.connection.addHandler(
  471. this.onMessage.bind(this), null, 'message', 'chat'
  472. );
  473. _converse.connection.addHandler(
  474. this.onErrorMessage.bind(this), null, 'message', 'error'
  475. );
  476. },
  477. chatBoxMayBeShown (chatbox) {
  478. return true;
  479. },
  480. onChatBoxesFetched (collection) {
  481. /* Show chat boxes upon receiving them from sessionStorage */
  482. collection.each((chatbox) => {
  483. if (this.chatBoxMayBeShown(chatbox)) {
  484. chatbox.trigger('show');
  485. }
  486. });
  487. _converse.emit('chatBoxesFetched');
  488. },
  489. onConnected () {
  490. this.browserStorage = new Backbone.BrowserStorage[_converse.storage](
  491. b64_sha1(`converse.chatboxes-${_converse.bare_jid}`));
  492. this.registerMessageHandler();
  493. this.fetch({
  494. add: true,
  495. success: this.onChatBoxesFetched.bind(this)
  496. });
  497. },
  498. onErrorMessage (message) {
  499. /* Handler method for all incoming error message stanzas
  500. */
  501. // TODO: we can likely just reuse "onMessage" below
  502. const from_jid = Strophe.getBareJidFromJid(message.getAttribute('from'));
  503. if (utils.isSameBareJID(from_jid, _converse.bare_jid)) {
  504. return true;
  505. }
  506. // Get chat box, but only create a new one when the message has a body.
  507. const chatbox = this.getChatBox(from_jid);
  508. if (!chatbox) {
  509. return true;
  510. }
  511. chatbox.createMessage(message, null, message);
  512. return true;
  513. },
  514. onMessage (message) {
  515. /* Handler method for all incoming single-user chat "message"
  516. * stanzas.
  517. *
  518. * Parameters:
  519. * (XMLElement) message - The incoming message stanza
  520. */
  521. let contact_jid, delay, resource,
  522. from_jid = message.getAttribute('from'),
  523. to_jid = message.getAttribute('to');
  524. const original_stanza = message,
  525. to_resource = Strophe.getResourceFromJid(to_jid),
  526. is_carbon = !_.isNull(message.querySelector(`received[xmlns="${Strophe.NS.CARBONS}"]`));
  527. if (_converse.filter_by_resource && (to_resource && to_resource !== _converse.resource)) {
  528. _converse.log(
  529. `onMessage: Ignoring incoming message intended for a different resource: ${to_jid}`,
  530. Strophe.LogLevel.INFO
  531. );
  532. return true;
  533. } else if (utils.isHeadlineMessage(_converse, message)) {
  534. // XXX: Ideally we wouldn't have to check for headline
  535. // messages, but Prosody sends headline messages with the
  536. // wrong type ('chat'), so we need to filter them out here.
  537. _converse.log(
  538. `onMessage: Ignoring incoming headline message sent with type 'chat' from JID: ${from_jid}`,
  539. Strophe.LogLevel.INFO
  540. );
  541. return true;
  542. }
  543. const forwarded = message.querySelector('forwarded');
  544. if (!_.isNull(forwarded)) {
  545. const forwarded_message = forwarded.querySelector('message');
  546. const forwarded_from = forwarded_message.getAttribute('from');
  547. if (is_carbon && Strophe.getBareJidFromJid(forwarded_from) !== from_jid) {
  548. // Prevent message forging via carbons
  549. //
  550. // https://xmpp.org/extensions/xep-0280.html#security
  551. return true;
  552. }
  553. message = forwarded_message;
  554. delay = forwarded.querySelector('delay');
  555. from_jid = message.getAttribute('from');
  556. to_jid = message.getAttribute('to');
  557. }
  558. const from_bare_jid = Strophe.getBareJidFromJid(from_jid),
  559. from_resource = Strophe.getResourceFromJid(from_jid),
  560. is_me = from_bare_jid === _converse.bare_jid;
  561. if (is_me) {
  562. // I am the sender, so this must be a forwarded message...
  563. contact_jid = Strophe.getBareJidFromJid(to_jid);
  564. resource = Strophe.getResourceFromJid(to_jid);
  565. } else {
  566. contact_jid = from_bare_jid;
  567. resource = from_resource;
  568. }
  569. // Get chat box, but only create a new one when the message has a body.
  570. const attrs = {
  571. 'fullname': _.get(_converse.api.contacts.get(contact_jid), 'attributes.fullname')
  572. }
  573. const chatbox = this.getChatBox(contact_jid, attrs, !_.isNull(message.querySelector('body'))),
  574. msgid = message.getAttribute('id');
  575. if (chatbox) {
  576. const messages = msgid && chatbox.messages.findWhere({msgid}) || [];
  577. if (_.isEmpty(messages)) {
  578. // Only create the message when we're sure it's not a
  579. // duplicate
  580. chatbox.incrementUnreadMsgCounter(original_stanza);
  581. chatbox.createMessage(message, delay, original_stanza);
  582. }
  583. }
  584. _converse.emit('message', {'stanza': original_stanza, 'chatbox': chatbox});
  585. return true;
  586. },
  587. getChatBox (jid, attrs={}, create) {
  588. /* Returns a chat box or optionally return a newly
  589. * created one if one doesn't exist.
  590. *
  591. * Parameters:
  592. * (String) jid - The JID of the user whose chat box we want
  593. * (Boolean) create - Should a new chat box be created if none exists?
  594. * (Object) attrs - Optional chat box atributes.
  595. */
  596. if (_.isObject(jid)) {
  597. create = attrs;
  598. attrs = jid;
  599. jid = attrs.jid;
  600. }
  601. jid = Strophe.getBareJidFromJid(jid.toLowerCase());
  602. let chatbox = this.get(Strophe.getBareJidFromJid(jid));
  603. if (!chatbox && create) {
  604. _.extend(attrs, {'jid': jid, 'id': jid});
  605. chatbox = this.create(attrs, {
  606. 'error' (model, response) {
  607. _converse.log(response.responseText);
  608. }
  609. });
  610. }
  611. return chatbox;
  612. }
  613. });
  614. _converse.ChatBoxViews = Backbone.Overview.extend({
  615. _ensureElement () {
  616. /* Override method from backbone.js
  617. * If the #conversejs element doesn't exist, create it.
  618. */
  619. if (!this.el) {
  620. let el = _converse.root.querySelector('#conversejs');
  621. if (_.isNull(el)) {
  622. el = document.createElement('div');
  623. el.setAttribute('id', 'conversejs');
  624. const body = _converse.root.querySelector('body');
  625. if (body) {
  626. body.appendChild(el);
  627. } else {
  628. // Perhaps inside a web component?
  629. _converse.root.appendChild(el);
  630. }
  631. }
  632. if (_.includes(['mobile', 'fullscreen'], _converse.view_mode)) {
  633. el.classList.add('fullscreen');
  634. }
  635. el.innerHTML = '';
  636. this.setElement(el, false);
  637. } else {
  638. this.setElement(_.result(this, 'el'), false);
  639. }
  640. },
  641. initialize () {
  642. this.model.on("add", this.onChatBoxAdded, this);
  643. this.model.on("destroy", this.removeChat, this);
  644. this.el.classList.add(`converse-${_converse.view_mode}`);
  645. this.render();
  646. },
  647. render () {
  648. try {
  649. this.el.innerHTML = tpl_chatboxes();
  650. } catch (e) {
  651. this._ensureElement();
  652. this.el.innerHTML = tpl_chatboxes();
  653. }
  654. this.row_el = this.el.querySelector('.row');
  655. },
  656. insertRowColumn (el) {
  657. /* Add a new DOM element (likely a chat box) into the
  658. * the row managed by this overview.
  659. */
  660. this.row_el.insertAdjacentElement('afterBegin', el);
  661. },
  662. onChatBoxAdded (item) {
  663. // Views aren't created here, since the core code doesn't
  664. // contain any views. Instead, they're created in overrides in
  665. // plugins, such as in converse-chatview.js and converse-muc.js
  666. return this.get(item.get('id'));
  667. },
  668. removeChat (item) {
  669. this.remove(item.get('id'));
  670. },
  671. closeAllChatBoxes () {
  672. /* This method gets overridden in src/converse-controlbox.js if
  673. * the controlbox plugin is active.
  674. */
  675. this.each(function (view) { view.close(); });
  676. return this;
  677. },
  678. chatBoxMayBeShown (chatbox) {
  679. return this.model.chatBoxMayBeShown(chatbox);
  680. }
  681. });
  682. // TODO: move to converse-chatboxviews.js and use there in the API
  683. _converse.getViewForChatBox = function (chatbox) {
  684. if (!chatbox) { return; }
  685. return _converse.chatboxviews.get(chatbox.get('id'));
  686. };
  687. function autoJoinChats () {
  688. /* Automatically join private chats, based on the
  689. * "auto_join_private_chats" configuration setting.
  690. */
  691. _.each(_converse.auto_join_private_chats, function (jid) {
  692. if (_converse.chatboxes.where({'jid': jid}).length) {
  693. return;
  694. }
  695. if (_.isString(jid)) {
  696. _converse.api.chats.open(jid);
  697. } else {
  698. _converse.log(
  699. 'Invalid jid criteria specified for "auto_join_private_chats"',
  700. Strophe.LogLevel.ERROR);
  701. }
  702. });
  703. _converse.emit('privateChatsAutoJoined');
  704. }
  705. /************************ BEGIN Event Handlers ************************/
  706. _converse.on('chatBoxesFetched', autoJoinChats);
  707. _converse.api.waitUntil('rosterContactsFetched').then(() => {
  708. _converse.roster.on('add', (contact) => {
  709. /* When a new contact is added, check if we already have a
  710. * chatbox open for it, and if so attach it to the chatbox.
  711. */
  712. const chatbox = _converse.chatboxes.findWhere({'jid': contact.get('jid')});
  713. if (chatbox) {
  714. chatbox.addRelatedContact(contact);
  715. }
  716. });
  717. });
  718. _converse.on('addClientFeatures', () => {
  719. _converse.api.disco.own.features.add(Strophe.NS.HTTPUPLOAD);
  720. _converse.api.disco.own.features.add(Strophe.NS.OUTOFBAND);
  721. });
  722. _converse.api.listen.on('pluginsInitialized', () => {
  723. _converse.chatboxes = new _converse.ChatBoxes();
  724. _converse.chatboxviews = new _converse.ChatBoxViews({
  725. 'model': _converse.chatboxes
  726. });
  727. _converse.emit('chatBoxesInitialized');
  728. });
  729. _converse.api.listen.on('beforeTearDown', () => {
  730. _converse.chatboxes.remove(); // Don't call off(), events won't get re-registered upon reconnect.
  731. delete _converse.chatboxes.browserStorage;
  732. });
  733. _converse.api.listen.on('statusInitialized', () => _converse.chatboxes.onConnected());
  734. /************************ END Event Handlers ************************/
  735. /************************ BEGIN API ************************/
  736. _.extend(_converse.api, {
  737. 'chats': {
  738. 'create' (jids, attrs) {
  739. if (_.isUndefined(jids)) {
  740. _converse.log(
  741. "chats.create: You need to provide at least one JID",
  742. Strophe.LogLevel.ERROR
  743. );
  744. return null;
  745. }
  746. if (_.isString(jids)) {
  747. if (attrs && !_.get(attrs, 'fullname')) {
  748. attrs.fullname = _.get(_converse.api.contacts.get(jids), 'attributes.fullname');
  749. }
  750. const chatbox = _converse.chatboxes.getChatBox(jids, attrs, true);
  751. if (_.isNil(chatbox)) {
  752. _converse.log("Could not open chatbox for JID: "+jids, Strophe.LogLevel.ERROR);
  753. return;
  754. }
  755. return chatbox;
  756. }
  757. return _.map(jids, (jid) => {
  758. attrs.fullname = _.get(_converse.api.contacts.get(jid), 'attributes.fullname');
  759. return _converse.chatboxes.getChatBox(jid, attrs, true).trigger('show');
  760. });
  761. },
  762. 'open' (jids, attrs) {
  763. if (_.isUndefined(jids)) {
  764. _converse.log("chats.open: You need to provide at least one JID", Strophe.LogLevel.ERROR);
  765. return null;
  766. } else if (_.isString(jids)) {
  767. const chatbox = _converse.api.chats.create(jids, attrs);
  768. chatbox.trigger('show');
  769. return chatbox;
  770. }
  771. return _.map(jids, (jid) => _converse.api.chats.create(jid, attrs).trigger('show'));
  772. },
  773. 'get' (jids) {
  774. if (_.isUndefined(jids)) {
  775. const result = [];
  776. _converse.chatboxes.each(function (chatbox) {
  777. // FIXME: Leaky abstraction from MUC. We need to add a
  778. // base type for chat boxes, and check for that.
  779. if (chatbox.get('type') !== 'chatroom') {
  780. result.push(chatbox);
  781. }
  782. });
  783. return result;
  784. } else if (_.isString(jids)) {
  785. return _converse.chatboxes.getChatBox(jids);
  786. }
  787. return _.map(jids, _.partial(_converse.chatboxes.getChatBox.bind(_converse.chatboxes), _, {}, true));
  788. }
  789. }
  790. });
  791. /************************ END API ************************/
  792. }
  793. });
  794. return converse;
  795. }));