muc_messages.js 65 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216
  1. (function (root, factory) {
  2. define([
  3. "jasmine",
  4. "mock",
  5. "test-utils"
  6. ], factory);
  7. } (this, function (jasmine, mock, test_utils) {
  8. "use strict";
  9. const { Promise, Strophe, $msg, $pres, sizzle, stanza_utils } = converse.env;
  10. const u = converse.env.utils;
  11. describe("A Groupchat Message", function () {
  12. describe("an info message", function () {
  13. it("is not rendered as a followup message",
  14. mock.initConverse(
  15. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  16. async function (done, _converse) {
  17. const muc_jid = 'lounge@montague.lit';
  18. await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  19. const view = _converse.api.chatviews.get(muc_jid);
  20. let presence = u.toStanza(`
  21. <presence xmlns="jabber:client" to="${_converse.jid}" from="${muc_jid}/romeo">
  22. <x xmlns="http://jabber.org/protocol/muc#user">
  23. <status code="201"/>
  24. <item role="moderator" affiliation="owner" jid="${_converse.jid}"/>
  25. <status code="110"/>
  26. </x>
  27. </presence>
  28. `);
  29. _converse.connection._dataRecv(test_utils.createRequest(presence));
  30. await u.waitUntil(() => view.el.querySelectorAll('.chat-info').length === 1);
  31. presence = u.toStanza(`
  32. <presence xmlns="jabber:client" to="${_converse.jid}" from="${muc_jid}/romeo1">
  33. <x xmlns="http://jabber.org/protocol/muc#user">
  34. <status code="210"/>
  35. <item role="moderator" affiliation="owner" jid="${_converse.jid}"/>
  36. <status code="110"/>
  37. </x>
  38. </presence>
  39. `);
  40. _converse.connection._dataRecv(test_utils.createRequest(presence));
  41. await u.waitUntil(() => view.el.querySelectorAll('.chat-info').length === 2);
  42. const messages = view.el.querySelectorAll('.chat-info');
  43. expect(u.hasClass('chat-msg--followup', messages[0])).toBe(false);
  44. expect(u.hasClass('chat-msg--followup', messages[1])).toBe(false);
  45. done();
  46. }));
  47. it("is not shown if its a duplicate",
  48. mock.initConverse(
  49. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  50. async function (done, _converse) {
  51. const muc_jid = 'lounge@montague.lit';
  52. await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  53. const view = _converse.api.chatviews.get(muc_jid);
  54. const presence = u.toStanza(`
  55. <presence xmlns="jabber:client" to="${_converse.jid}" from="${muc_jid}/romeo">
  56. <x xmlns="http://jabber.org/protocol/muc#user">
  57. <status code="201"/>
  58. <item role="moderator" affiliation="owner" jid="${_converse.jid}"/>
  59. <status code="110"/>
  60. </x>
  61. </presence>
  62. `);
  63. // XXX: We wait for createInfoMessages to complete, if we don't
  64. // we still get two info messages due to messages
  65. // created from presences not being queued and run
  66. // sequentially (i.e. by waiting for promises to resolve)
  67. // like we do with message stanzas.
  68. spyOn(view.model, 'createInfoMessages').and.callThrough();
  69. _converse.connection._dataRecv(test_utils.createRequest(presence));
  70. await u.waitUntil(() => view.model.createInfoMessages.calls.count());
  71. await u.waitUntil(() => view.el.querySelectorAll('.chat-info').length === 1);
  72. _converse.connection._dataRecv(test_utils.createRequest(presence));
  73. await u.waitUntil(() => view.model.createInfoMessages.calls.count() === 2);
  74. expect(view.el.querySelectorAll('.chat-info').length).toBe(1);
  75. done();
  76. }));
  77. });
  78. it("is rejected if it's an unencapsulated forwarded message",
  79. mock.initConverse(
  80. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  81. async function (done, _converse) {
  82. const muc_jid = 'lounge@montague.lit';
  83. await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  84. const impersonated_jid = `${muc_jid}/alice`;
  85. const received_stanza = u.toStanza(`
  86. <message to='${_converse.jid}' from='${muc_jid}/mallory' type='groupchat' id='${_converse.connection.getUniqueId()}'>
  87. <forwarded xmlns='urn:xmpp:forward:0'>
  88. <delay xmlns='urn:xmpp:delay' stamp='2019-07-10T23:08:25Z'/>
  89. <message from='${impersonated_jid}'
  90. id='0202197'
  91. to='${_converse.bare_jid}'
  92. type='groupchat'
  93. xmlns='jabber:client'>
  94. <body>Yet I should kill thee with much cherishing.</body>
  95. </message>
  96. </forwarded>
  97. </message>
  98. `);
  99. const view = _converse.api.chatviews.get(muc_jid);
  100. spyOn(view.model, 'onMessage').and.callThrough();
  101. await view.model.queueMessage(received_stanza);
  102. spyOn(converse.env.log, 'warn');
  103. _converse.connection._dataRecv(test_utils.createRequest(received_stanza));
  104. await u.waitUntil(() => view.model.onMessage.calls.count());
  105. expect(converse.env.log.warn).toHaveBeenCalledWith(
  106. 'onMessage: Ignoring unencapsulated forwarded groupchat message'
  107. );
  108. expect(view.el.querySelectorAll('.chat-msg').length).toBe(0);
  109. expect(view.model.messages.length).toBe(0);
  110. done();
  111. }));
  112. it("can contain a chat state notification and will still be shown",
  113. mock.initConverse(
  114. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  115. async function (done, _converse) {
  116. const muc_jid = 'lounge@montague.lit';
  117. await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  118. const view = _converse.api.chatviews.get(muc_jid);
  119. if (!view.el.querySelectorAll('.chat-area').length) { view.renderChatArea(); }
  120. const message = 'romeo: Your attention is required';
  121. const nick = mock.chatroom_names[0],
  122. msg = $msg({
  123. from: 'lounge@montague.lit/'+nick,
  124. id: u.getUniqueId(),
  125. to: 'romeo@montague.lit',
  126. type: 'groupchat'
  127. }).c('body').t(message)
  128. .c('active', {'xmlns': "http://jabber.org/protocol/chatstates"})
  129. .tree();
  130. await view.model.queueMessage(msg);
  131. await new Promise(resolve => view.once('messageInserted', resolve));
  132. expect(view.el.querySelector('.chat-msg')).not.toBe(null);
  133. done();
  134. }));
  135. it("is specially marked when you are mentioned in it",
  136. mock.initConverse(
  137. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  138. async function (done, _converse) {
  139. const muc_jid = 'lounge@montague.lit';
  140. await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  141. const view = _converse.api.chatviews.get(muc_jid);
  142. if (!view.el.querySelectorAll('.chat-area').length) { view.renderChatArea(); }
  143. const message = 'romeo: Your attention is required';
  144. const nick = mock.chatroom_names[0],
  145. msg = $msg({
  146. from: 'lounge@montague.lit/'+nick,
  147. id: u.getUniqueId(),
  148. to: 'romeo@montague.lit',
  149. type: 'groupchat'
  150. }).c('body').t(message).tree();
  151. await view.model.queueMessage(msg);
  152. await new Promise(resolve => view.once('messageInserted', resolve));
  153. expect(u.hasClass('mentioned', view.el.querySelector('.chat-msg'))).toBeTruthy();
  154. done();
  155. }));
  156. it("can not be expected to have a unique id attribute",
  157. mock.initConverse(
  158. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  159. async function (done, _converse) {
  160. const muc_jid = 'lounge@montague.lit';
  161. await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  162. const view = _converse.api.chatviews.get(muc_jid);
  163. if (!view.el.querySelectorAll('.chat-area').length) { view.renderChatArea(); }
  164. const id = u.getUniqueId();
  165. let msg = $msg({
  166. from: 'lounge@montague.lit/some1',
  167. id: id,
  168. to: 'romeo@montague.lit',
  169. type: 'groupchat'
  170. }).c('body').t('First message').tree();
  171. await view.model.queueMessage(msg);
  172. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
  173. msg = $msg({
  174. from: 'lounge@montague.lit/some2',
  175. id: id,
  176. to: 'romeo@montague.lit',
  177. type: 'groupchat'
  178. }).c('body').t('Another message').tree();
  179. await view.model.queueMessage(msg);
  180. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 2);
  181. expect(view.model.messages.length).toBe(2);
  182. done();
  183. }));
  184. it("is ignored if it has the same archive-id of an already received one",
  185. mock.initConverse(
  186. ['rosterGroupsFetched'], {},
  187. async function (done, _converse) {
  188. const muc_jid = 'room@muc.example.com';
  189. await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  190. const view = _converse.api.chatviews.get(muc_jid);
  191. spyOn(view.model, 'getDuplicateMessage').and.callThrough();
  192. let stanza = u.toStanza(`
  193. <message xmlns="jabber:client"
  194. from="room@muc.example.com/some1"
  195. to="${_converse.connection.jid}"
  196. type="groupchat">
  197. <body>Typical body text</body>
  198. <stanza-id xmlns="urn:xmpp:sid:0"
  199. id="5f3dbc5e-e1d3-4077-a492-693f3769c7ad"
  200. by="room@muc.example.com"/>
  201. </message>`);
  202. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  203. await u.waitUntil(() => view.model.messages.length === 1);
  204. await u.waitUntil(() => view.model.getDuplicateMessage.calls.count() === 1);
  205. let result = await view.model.getDuplicateMessage.calls.all()[0].returnValue;
  206. expect(result).toBe(undefined);
  207. stanza = u.toStanza(`
  208. <message xmlns="jabber:client"
  209. to="${_converse.connection.jid}"
  210. from="room@muc.example.com">
  211. <result xmlns="urn:xmpp:mam:2" queryid="82d9db27-6cf8-4787-8c2c-5a560263d823" id="5f3dbc5e-e1d3-4077-a492-693f3769c7ad">
  212. <forwarded xmlns="urn:xmpp:forward:0">
  213. <delay xmlns="urn:xmpp:delay" stamp="2018-01-09T06:17:23Z"/>
  214. <message from="room@muc.example.com/some1" type="groupchat">
  215. <body>Typical body text</body>
  216. </message>
  217. </forwarded>
  218. </result>
  219. </message>`);
  220. spyOn(view.model, 'updateMessage');
  221. await view.model.queueMessage(stanza);
  222. await u.waitUntil(() => view.model.getDuplicateMessage.calls.count() === 2);
  223. result = await view.model.getDuplicateMessage.calls.all()[1].returnValue;
  224. expect(result instanceof _converse.Message).toBe(true);
  225. expect(view.model.messages.length).toBe(1);
  226. await u.waitUntil(() => view.model.updateMessage.calls.count());
  227. done();
  228. }));
  229. it("is ignored if it has the same stanza-id of an already received one",
  230. mock.initConverse(
  231. ['rosterGroupsFetched'], {},
  232. async function (done, _converse) {
  233. const muc_jid = 'room@muc.example.com';
  234. await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  235. const view = _converse.api.chatviews.get(muc_jid);
  236. spyOn(view.model, 'getStanzaIdQueryAttrs').and.callThrough();
  237. let stanza = u.toStanza(`
  238. <message xmlns="jabber:client"
  239. from="room@muc.example.com/some1"
  240. to="${_converse.connection.jid}"
  241. type="groupchat">
  242. <body>Typical body text</body>
  243. <stanza-id xmlns="urn:xmpp:sid:0"
  244. id="5f3dbc5e-e1d3-4077-a492-693f3769c7ad"
  245. by="room@muc.example.com"/>
  246. </message>`);
  247. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  248. await u.waitUntil(() => view.model.messages.length === 1);
  249. await u.waitUntil(() => view.model.getStanzaIdQueryAttrs.calls.count() === 1);
  250. let result = await view.model.getStanzaIdQueryAttrs.calls.all()[0].returnValue;
  251. expect(result instanceof Array).toBe(true);
  252. expect(result[0] instanceof Object).toBe(true);
  253. expect(result[0]['stanza_id room@muc.example.com']).toBe("5f3dbc5e-e1d3-4077-a492-693f3769c7ad");
  254. stanza = u.toStanza(`
  255. <message xmlns="jabber:client"
  256. from="room@muc.example.com/some1"
  257. to="${_converse.connection.jid}"
  258. type="groupchat">
  259. <body>Typical body text</body>
  260. <stanza-id xmlns="urn:xmpp:sid:0"
  261. id="5f3dbc5e-e1d3-4077-a492-693f3769c7ad"
  262. by="room@muc.example.com"/>
  263. </message>`);
  264. spyOn(view.model, 'updateMessage');
  265. spyOn(view.model, 'getDuplicateMessage').and.callThrough();
  266. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  267. await u.waitUntil(() => view.model.getDuplicateMessage.calls.count());
  268. result = await view.model.getDuplicateMessage.calls.all()[0].returnValue;
  269. expect(result instanceof _converse.Message).toBe(true);
  270. expect(view.model.messages.length).toBe(1);
  271. await u.waitUntil(() => view.model.updateMessage.calls.count());
  272. done();
  273. }));
  274. it("will be discarded if it's a malicious message meant to look like a carbon copy",
  275. mock.initConverse(
  276. ['rosterGroupsFetched'], {},
  277. async function (done, _converse) {
  278. await test_utils.waitForRoster(_converse, 'current');
  279. await test_utils.openControlBox(_converse);
  280. const muc_jid = 'xsf@muc.xmpp.org';
  281. const sender_jid = `${muc_jid}/romeo`;
  282. const impersonated_jid = `${muc_jid}/i_am_groot`
  283. await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  284. const stanza = $pres({
  285. to: 'romeo@montague.lit/_converse.js-29092160',
  286. from: sender_jid
  287. })
  288. .c('x', {xmlns: Strophe.NS.MUC_USER})
  289. .c('item', {
  290. 'affiliation': 'none',
  291. 'jid': 'newguy@montague.lit/_converse.js-290929789',
  292. 'role': 'participant'
  293. }).tree();
  294. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  295. /*
  296. * <message to="romeo@montague.im/poezio" id="718d40df-3948-4798-a99b-35cc9f03cc4f-641" type="groupchat" from="xsf@muc.xmpp.org/romeo">
  297. * <received xmlns="urn:xmpp:carbons:2">
  298. * <forwarded xmlns="urn:xmpp:forward:0">
  299. * <message xmlns="jabber:client" to="xsf@muc.xmpp.org" type="groupchat" from="xsf@muc.xmpp.org/i_am_groot">
  300. * <body>I am groot.</body>
  301. * </message>
  302. * </forwarded>
  303. * </received>
  304. * </message>
  305. */
  306. const msg = $msg({
  307. 'from': sender_jid,
  308. 'id': _converse.connection.getUniqueId(),
  309. 'to': _converse.connection.jid,
  310. 'type': 'groupchat',
  311. 'xmlns': 'jabber:client'
  312. }).c('received', {'xmlns': 'urn:xmpp:carbons:2'})
  313. .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'})
  314. .c('message', {
  315. 'xmlns': 'jabber:client',
  316. 'from': impersonated_jid,
  317. 'to': muc_jid,
  318. 'type': 'groupchat'
  319. }).c('body').t('I am groot').tree();
  320. const view = _converse.api.chatviews.get(muc_jid);
  321. spyOn(converse.env.log, 'warn');
  322. await view.model.queueMessage(msg);
  323. expect(converse.env.log.warn).toHaveBeenCalledWith(
  324. 'onMessage: Ignoring XEP-0280 "groupchat" message carbon, '+
  325. 'according to the XEP groupchat messages SHOULD NOT be carbon copied'
  326. );
  327. expect(view.el.querySelectorAll('.chat-msg').length).toBe(0);
  328. expect(view.model.messages.length).toBe(0);
  329. done();
  330. }));
  331. it("keeps track of the sender's role and affiliation",
  332. mock.initConverse(
  333. ['rosterGroupsFetched'], {},
  334. async function (done, _converse) {
  335. const muc_jid = 'lounge@montague.lit';
  336. await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  337. const view = _converse.api.chatviews.get(muc_jid);
  338. let msg = $msg({
  339. from: 'lounge@montague.lit/romeo',
  340. id: u.getUniqueId(),
  341. to: 'romeo@montague.lit',
  342. type: 'groupchat'
  343. }).c('body').t('I wrote this message!').tree();
  344. await view.model.queueMessage(msg);
  345. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length);
  346. expect(view.model.messages.last().occupant.get('affiliation')).toBe('owner');
  347. expect(view.model.messages.last().occupant.get('role')).toBe('moderator');
  348. expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
  349. expect(sizzle('.chat-msg', view.el).pop().classList.value.trim()).toBe('message chat-msg groupchat moderator owner');
  350. let presence = $pres({
  351. to:'romeo@montague.lit/orchard',
  352. from:'lounge@montague.lit/romeo',
  353. id: u.getUniqueId()
  354. }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'})
  355. .c('item').attrs({
  356. affiliation: 'member',
  357. jid: 'romeo@montague.lit/orchard',
  358. role: 'participant'
  359. }).up()
  360. .c('status').attrs({code:'110'}).up()
  361. .c('status').attrs({code:'210'}).nodeTree;
  362. _converse.connection._dataRecv(test_utils.createRequest(presence));
  363. msg = $msg({
  364. from: 'lounge@montague.lit/romeo',
  365. id: u.getUniqueId(),
  366. to: 'romeo@montague.lit',
  367. type: 'groupchat'
  368. }).c('body').t('Another message!').tree();
  369. await view.model.queueMessage(msg);
  370. await new Promise(resolve => view.once('messageInserted', resolve));
  371. expect(view.model.messages.last().occupant.get('affiliation')).toBe('member');
  372. expect(view.model.messages.last().occupant.get('role')).toBe('participant');
  373. expect(view.el.querySelectorAll('.chat-msg').length).toBe(2);
  374. expect(sizzle('.chat-msg', view.el).pop().classList.value.trim()).toBe('message chat-msg groupchat participant member');
  375. presence = $pres({
  376. to:'romeo@montague.lit/orchard',
  377. from:'lounge@montague.lit/romeo',
  378. id: u.getUniqueId()
  379. }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'})
  380. .c('item').attrs({
  381. affiliation: 'owner',
  382. jid: 'romeo@montague.lit/orchard',
  383. role: 'moderator'
  384. }).up()
  385. .c('status').attrs({code:'110'}).up()
  386. .c('status').attrs({code:'210'}).nodeTree;
  387. _converse.connection._dataRecv(test_utils.createRequest(presence));
  388. view.model.sendMessage('hello world');
  389. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 3);
  390. const occupant = await u.waitUntil(() => view.model.messages.filter(m => m.get('type') === 'groupchat')[2].occupant);
  391. expect(occupant.get('affiliation')).toBe('owner');
  392. expect(occupant.get('role')).toBe('moderator');
  393. expect(view.el.querySelectorAll('.chat-msg').length).toBe(3);
  394. await u.waitUntil(() => sizzle('.chat-msg', view.el).pop().classList.value.trim() === 'message chat-msg groupchat moderator owner');
  395. const add_events = view.model.occupants._events.add.length;
  396. msg = $msg({
  397. from: 'lounge@montague.lit/some1',
  398. id: u.getUniqueId(),
  399. to: 'romeo@montague.lit',
  400. type: 'groupchat'
  401. }).c('body').t('Message from someone not in the MUC right now').tree();
  402. await view.model.queueMessage(msg);
  403. await new Promise(resolve => view.once('messageInserted', resolve));
  404. expect(view.model.messages.last().occupant).toBeUndefined();
  405. // Check that there's a new "add" event handler, for when the occupant appears.
  406. expect(view.model.occupants._events.add.length).toBe(add_events+1);
  407. // Check that the occupant gets added/removed to the message as it
  408. // gets removed or added.
  409. presence = $pres({
  410. to:'romeo@montague.lit/orchard',
  411. from:'lounge@montague.lit/some1',
  412. id: u.getUniqueId()
  413. }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'})
  414. .c('item').attrs({jid: 'some1@montague.lit/orchard'});
  415. _converse.connection._dataRecv(test_utils.createRequest(presence));
  416. await u.waitUntil(() => view.model.messages.last().occupant);
  417. expect(view.model.messages.last().get('message')).toBe('Message from someone not in the MUC right now');
  418. expect(view.model.messages.last().occupant.get('nick')).toBe('some1');
  419. // Check that the "add" event handler was removed.
  420. expect(view.model.occupants._events.add.length).toBe(add_events);
  421. presence = $pres({
  422. to:'romeo@montague.lit/orchard',
  423. type: 'unavailable',
  424. from:'lounge@montague.lit/some1',
  425. id: u.getUniqueId()
  426. }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'})
  427. .c('item').attrs({jid: 'some1@montague.lit/orchard'});
  428. _converse.connection._dataRecv(test_utils.createRequest(presence));
  429. await u.waitUntil(() => !view.model.messages.last().occupant);
  430. expect(view.model.messages.last().get('message')).toBe('Message from someone not in the MUC right now');
  431. expect(view.model.messages.last().occupant).toBeUndefined();
  432. // Check that there's a new "add" event handler, for when the occupant appears.
  433. expect(view.model.occupants._events.add.length).toBe(add_events+1);
  434. presence = $pres({
  435. to:'romeo@montague.lit/orchard',
  436. from:'lounge@montague.lit/some1',
  437. id: u.getUniqueId()
  438. }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'})
  439. .c('item').attrs({jid: 'some1@montague.lit/orchard'});
  440. _converse.connection._dataRecv(test_utils.createRequest(presence));
  441. await u.waitUntil(() => view.model.messages.last().occupant);
  442. expect(view.model.messages.last().get('message')).toBe('Message from someone not in the MUC right now');
  443. expect(view.model.messages.last().occupant.get('nick')).toBe('some1');
  444. // Check that the "add" event handler was removed.
  445. expect(view.model.occupants._events.add.length).toBe(add_events);
  446. done();
  447. }));
  448. it("keeps track whether you are the sender or not",
  449. mock.initConverse(
  450. ['rosterGroupsFetched'], {},
  451. async function (done, _converse) {
  452. const muc_jid = 'lounge@montague.lit';
  453. await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  454. const view = _converse.api.chatviews.get(muc_jid);
  455. const msg = $msg({
  456. from: 'lounge@montague.lit/romeo',
  457. id: u.getUniqueId(),
  458. to: 'romeo@montague.lit',
  459. type: 'groupchat'
  460. }).c('body').t('I wrote this message!').tree();
  461. await view.model.queueMessage(msg);
  462. expect(view.model.messages.last().get('sender')).toBe('me');
  463. done();
  464. }));
  465. it("can be replaced with a correction",
  466. mock.initConverse(
  467. ['rosterGroupsFetched'], {},
  468. async function (done, _converse) {
  469. const muc_jid = 'lounge@montague.lit';
  470. await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  471. const view = _converse.api.chatviews.get(muc_jid);
  472. const stanza = $pres({
  473. to: 'romeo@montague.lit/_converse.js-29092160',
  474. from: 'coven@chat.shakespeare.lit/newguy'
  475. })
  476. .c('x', {xmlns: Strophe.NS.MUC_USER})
  477. .c('item', {
  478. 'affiliation': 'none',
  479. 'jid': 'newguy@montague.lit/_converse.js-290929789',
  480. 'role': 'participant'
  481. }).tree();
  482. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  483. const msg_id = u.getUniqueId();
  484. await view.model.queueMessage($msg({
  485. 'from': 'lounge@montague.lit/newguy',
  486. 'to': _converse.connection.jid,
  487. 'type': 'groupchat',
  488. 'id': msg_id,
  489. }).c('body').t('But soft, what light through yonder airlock breaks?').tree());
  490. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length);
  491. expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
  492. expect(view.el.querySelector('.chat-msg__text').textContent)
  493. .toBe('But soft, what light through yonder airlock breaks?');
  494. await view.model.queueMessage($msg({
  495. 'from': 'lounge@montague.lit/newguy',
  496. 'to': _converse.connection.jid,
  497. 'type': 'groupchat',
  498. 'id': u.getUniqueId(),
  499. }).c('body').t('But soft, what light through yonder chimney breaks?').up()
  500. .c('replace', {'id': msg_id, 'xmlns': 'urn:xmpp:message-correct:0'}).tree());
  501. await u.waitUntil(() => view.el.querySelector('.chat-msg__text').textContent ===
  502. 'But soft, what light through yonder chimney breaks?', 500);
  503. expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
  504. expect(view.el.querySelectorAll('.chat-msg__content .fa-edit').length).toBe(1);
  505. await view.model.queueMessage($msg({
  506. 'from': 'lounge@montague.lit/newguy',
  507. 'to': _converse.connection.jid,
  508. 'type': 'groupchat',
  509. 'id': u.getUniqueId(),
  510. }).c('body').t('But soft, what light through yonder window breaks?').up()
  511. .c('replace', {'id': msg_id, 'xmlns': 'urn:xmpp:message-correct:0'}).tree());
  512. await u.waitUntil(() => view.el.querySelector('.chat-msg__text').textContent ===
  513. 'But soft, what light through yonder window breaks?', 500);
  514. expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
  515. expect(view.el.querySelectorAll('.chat-msg__content .fa-edit').length).toBe(1);
  516. view.el.querySelector('.chat-msg__content .fa-edit').click();
  517. const modal = view.model.messages.at(0).message_versions_modal;
  518. await u.waitUntil(() => u.isVisible(modal.el), 1000);
  519. const older_msgs = modal.el.querySelectorAll('.older-msg');
  520. expect(older_msgs.length).toBe(2);
  521. expect(older_msgs[0].childNodes[2].textContent).toBe('But soft, what light through yonder airlock breaks?');
  522. expect(older_msgs[0].childNodes[0].nodeName).toBe('TIME');
  523. expect(older_msgs[1].childNodes[0].nodeName).toBe('TIME');
  524. expect(older_msgs[1].childNodes[2].textContent).toBe('But soft, what light through yonder chimney breaks?');
  525. done();
  526. }));
  527. it("can be sent as a correction by using the up arrow",
  528. mock.initConverse(
  529. ['rosterGroupsFetched'], {},
  530. async function (done, _converse) {
  531. const muc_jid = 'lounge@montague.lit';
  532. await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  533. const view = _converse.api.chatviews.get(muc_jid);
  534. const textarea = view.el.querySelector('textarea.chat-textarea');
  535. expect(textarea.value).toBe('');
  536. view.onKeyDown({
  537. target: textarea,
  538. keyCode: 38 // Up arrow
  539. });
  540. expect(textarea.value).toBe('');
  541. textarea.value = 'But soft, what light through yonder airlock breaks?';
  542. view.onKeyDown({
  543. target: textarea,
  544. preventDefault: function preventDefault () {},
  545. keyCode: 13 // Enter
  546. });
  547. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
  548. expect(view.el.querySelector('.chat-msg__text').textContent)
  549. .toBe('But soft, what light through yonder airlock breaks?');
  550. const first_msg = view.model.messages.findWhere({'message': 'But soft, what light through yonder airlock breaks?'});
  551. expect(textarea.value).toBe('');
  552. view.onKeyDown({
  553. target: textarea,
  554. keyCode: 38 // Up arrow
  555. });
  556. await new Promise(resolve => view.model.messages.once('rendered', resolve));
  557. expect(textarea.value).toBe('But soft, what light through yonder airlock breaks?');
  558. expect(view.model.messages.at(0).get('correcting')).toBe(true);
  559. expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
  560. expect(u.hasClass('correcting', view.el.querySelector('.chat-msg'))).toBe(true);
  561. spyOn(_converse.connection, 'send');
  562. textarea.value = 'But soft, what light through yonder window breaks?';
  563. view.onKeyDown({
  564. target: textarea,
  565. preventDefault: function preventDefault () {},
  566. keyCode: 13 // Enter
  567. });
  568. expect(_converse.connection.send).toHaveBeenCalled();
  569. await new Promise(resolve => view.model.messages.once('rendered', resolve));
  570. const msg = _converse.connection.send.calls.all()[0].args[0];
  571. expect(msg.toLocaleString())
  572. .toBe(`<message from="romeo@montague.lit/orchard" id="${msg.nodeTree.getAttribute("id")}" `+
  573. `to="lounge@montague.lit" type="groupchat" `+
  574. `xmlns="jabber:client">`+
  575. `<body>But soft, what light through yonder window breaks?</body>`+
  576. `<active xmlns="http://jabber.org/protocol/chatstates"/>`+
  577. `<replace id="${first_msg.get("msgid")}" xmlns="urn:xmpp:message-correct:0"/>`+
  578. `<origin-id id="${msg.nodeTree.querySelector('origin-id').getAttribute("id")}" xmlns="urn:xmpp:sid:0"/>`+
  579. `</message>`);
  580. expect(view.model.messages.models.length).toBe(1);
  581. const corrected_message = view.model.messages.at(0);
  582. expect(corrected_message.get('msgid')).toBe(first_msg.get('msgid'));
  583. expect(corrected_message.get('correcting')).toBe(false);
  584. const older_versions = corrected_message.get('older_versions');
  585. const keys = Object.keys(older_versions);
  586. expect(keys.length).toBe(1);
  587. expect(older_versions[keys[0]]).toBe('But soft, what light through yonder airlock breaks?');
  588. expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
  589. expect(u.hasClass('correcting', view.el.querySelector('.chat-msg'))).toBe(false);
  590. // Check that messages from other users are skipped
  591. await view.model.queueMessage($msg({
  592. 'from': muc_jid+'/someone-else',
  593. 'id': u.getUniqueId(),
  594. 'to': 'romeo@montague.lit',
  595. 'type': 'groupchat'
  596. }).c('body').t('Hello world').tree());
  597. await new Promise(resolve => view.once('messageInserted', resolve));
  598. expect(view.el.querySelectorAll('.chat-msg').length).toBe(2);
  599. // Test that pressing the down arrow cancels message correction
  600. expect(textarea.value).toBe('');
  601. view.onKeyDown({
  602. target: textarea,
  603. keyCode: 38 // Up arrow
  604. });
  605. expect(textarea.value).toBe('But soft, what light through yonder window breaks?');
  606. expect(view.model.messages.at(0).get('correcting')).toBe(true);
  607. expect(view.el.querySelectorAll('.chat-msg').length).toBe(2);
  608. await u.waitUntil(() => u.hasClass('correcting', view.el.querySelector('.chat-msg')), 500);
  609. expect(textarea.value).toBe('But soft, what light through yonder window breaks?');
  610. view.onKeyDown({
  611. target: textarea,
  612. keyCode: 40 // Down arrow
  613. });
  614. expect(textarea.value).toBe('');
  615. expect(view.model.messages.at(0).get('correcting')).toBe(false);
  616. expect(view.el.querySelectorAll('.chat-msg').length).toBe(2);
  617. await u.waitUntil(() => !u.hasClass('correcting', view.el.querySelector('.chat-msg')), 500);
  618. done();
  619. }));
  620. it("will be shown as received upon MUC reflection",
  621. mock.initConverse(
  622. ['rosterGroupsFetched'], {},
  623. async function (done, _converse) {
  624. await test_utils.waitForRoster(_converse, 'current');
  625. const muc_jid = 'lounge@montague.lit';
  626. await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  627. const view = _converse.api.chatviews.get(muc_jid);
  628. const textarea = view.el.querySelector('textarea.chat-textarea');
  629. textarea.value = 'But soft, what light through yonder airlock breaks?';
  630. view.onKeyDown({
  631. target: textarea,
  632. preventDefault: function preventDefault () {},
  633. keyCode: 13 // Enter
  634. });
  635. await new Promise(resolve => view.once('messageInserted', resolve));
  636. expect(view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length).toBe(0);
  637. const msg_obj = view.model.messages.at(0);
  638. const stanza = u.toStanza(`
  639. <message xmlns="jabber:client"
  640. from="${msg_obj.get('from')}"
  641. to="${_converse.connection.jid}"
  642. type="groupchat">
  643. <body>${msg_obj.get('message')}</body>
  644. <stanza-id xmlns="urn:xmpp:sid:0"
  645. id="5f3dbc5e-e1d3-4077-a492-693f3769c7ad"
  646. by="lounge@montague.lit"/>
  647. <origin-id xmlns="urn:xmpp:sid:0" id="${msg_obj.get('origin_id')}"/>
  648. </message>`);
  649. await view.model.queueMessage(stanza);
  650. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length, 500);
  651. expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0);
  652. expect(view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length).toBe(1);
  653. expect(view.model.messages.length).toBe(1);
  654. const message = view.model.messages.at(0);
  655. expect(message.get('stanza_id lounge@montague.lit')).toBe('5f3dbc5e-e1d3-4077-a492-693f3769c7ad');
  656. expect(message.get('origin_id')).toBe(msg_obj.get('origin_id'));
  657. done();
  658. }));
  659. it("gets updated with its stanza-id upon MUC reflection",
  660. mock.initConverse(
  661. ['rosterGroupsFetched'], {},
  662. async function (done, _converse) {
  663. const muc_jid = 'room@muc.example.com';
  664. await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  665. const view = _converse.api.chatviews.get(muc_jid);
  666. view.model.sendMessage('hello world');
  667. await u.waitUntil(() => view.model.messages.length === 1);
  668. const msg = view.model.messages.at(0);
  669. expect(msg.get('stanza_id')).toBeUndefined();
  670. expect(msg.get('origin_id')).toBe(msg.get('origin_id'));
  671. const stanza = u.toStanza(`
  672. <message xmlns="jabber:client"
  673. from="room@muc.example.com/romeo"
  674. to="${_converse.connection.jid}"
  675. type="groupchat">
  676. <body>Hello world</body>
  677. <stanza-id xmlns="urn:xmpp:sid:0"
  678. id="5f3dbc5e-e1d3-4077-a492-693f3769c7ad"
  679. by="room@muc.example.com"/>
  680. <origin-id xmlns="urn:xmpp:sid:0" id="${msg.get('origin_id')}"/>
  681. </message>`);
  682. spyOn(view.model, 'updateMessage').and.callThrough();
  683. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  684. await u.waitUntil(() => view.model.updateMessage.calls.count() === 1);
  685. expect(view.model.messages.length).toBe(1);
  686. expect(view.model.messages.at(0).get('stanza_id room@muc.example.com')).toBe("5f3dbc5e-e1d3-4077-a492-693f3769c7ad");
  687. expect(view.model.messages.at(0).get('origin_id')).toBe(msg.get('origin_id'));
  688. done();
  689. }));
  690. it("can cause a delivery receipt to be returned",
  691. mock.initConverse(
  692. ['rosterGroupsFetched'], {},
  693. async function (done, _converse) {
  694. await test_utils.waitForRoster(_converse, 'current');
  695. const muc_jid = 'lounge@montague.lit';
  696. await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  697. const view = _converse.api.chatviews.get(muc_jid);
  698. const textarea = view.el.querySelector('textarea.chat-textarea');
  699. textarea.value = 'But soft, what light through yonder airlock breaks?';
  700. view.onKeyDown({
  701. target: textarea,
  702. preventDefault: function preventDefault () {},
  703. keyCode: 13 // Enter
  704. });
  705. await new Promise(resolve => view.once('messageInserted', resolve));
  706. expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
  707. const msg_obj = view.model.messages.at(0);
  708. const stanza = u.toStanza(`
  709. <message xml:lang="en" to="romeo@montague.lit/orchard"
  710. from="lounge@montague.lit/some1" type="groupchat" xmlns="jabber:client">
  711. <received xmlns="urn:xmpp:receipts" id="${msg_obj.get('msgid')}"/>
  712. <origin-id xmlns="urn:xmpp:sid:0" id="CE08D448-5ED8-4B6A-BB5B-07ED9DFE4FF0"/>
  713. </message>`);
  714. spyOn(_converse.api, "trigger").and.callThrough();
  715. spyOn(stanza_utils, "isReceipt").and.callThrough();
  716. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  717. await u.waitUntil(() => stanza_utils.isReceipt.calls.count() === 1);
  718. expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
  719. expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0);
  720. expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object));
  721. done();
  722. }));
  723. it("can cause a chat marker to be returned",
  724. mock.initConverse(
  725. ['rosterGroupsFetched'], {},
  726. async function (done, _converse) {
  727. await test_utils.waitForRoster(_converse, 'current');
  728. const muc_jid = 'lounge@montague.lit';
  729. await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  730. const view = _converse.api.chatviews.get(muc_jid);
  731. const textarea = view.el.querySelector('textarea.chat-textarea');
  732. textarea.value = 'But soft, what light through yonder airlock breaks?';
  733. view.onKeyDown({
  734. target: textarea,
  735. preventDefault: function preventDefault () {},
  736. keyCode: 13 // Enter
  737. });
  738. await new Promise(resolve => view.once('messageInserted', resolve));
  739. expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
  740. expect(view.el.querySelector('.chat-msg .chat-msg__body').textContent.trim())
  741. .toBe("But soft, what light through yonder airlock breaks?");
  742. const msg_obj = view.model.messages.at(0);
  743. let stanza = u.toStanza(`
  744. <message xml:lang="en" to="romeo@montague.lit/orchard"
  745. from="lounge@montague.lit/some1" type="groupchat" xmlns="jabber:client">
  746. <received xmlns="urn:xmpp:chat-markers:0" id="${msg_obj.get('msgid')}"/>
  747. </message>`);
  748. const stanza_utils = converse.env.stanza_utils;
  749. spyOn(stanza_utils, "isChatMarker").and.callThrough();
  750. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  751. await u.waitUntil(() => stanza_utils.isChatMarker.calls.count() === 1);
  752. expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
  753. expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0);
  754. stanza = u.toStanza(`
  755. <message xml:lang="en" to="romeo@montague.lit/orchard"
  756. from="lounge@montague.lit/some1" type="groupchat" xmlns="jabber:client">
  757. <displayed xmlns="urn:xmpp:chat-markers:0" id="${msg_obj.get('msgid')}"/>
  758. </message>`);
  759. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  760. await u.waitUntil(() => stanza_utils.isChatMarker.calls.count() === 2);
  761. expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
  762. expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0);
  763. stanza = u.toStanza(`
  764. <message xml:lang="en" to="romeo@montague.lit/orchard"
  765. from="lounge@montague.lit/some1" type="groupchat" xmlns="jabber:client">
  766. <acknowledged xmlns="urn:xmpp:chat-markers:0" id="${msg_obj.get('msgid')}"/>
  767. </message>`);
  768. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  769. await u.waitUntil(() => stanza_utils.isChatMarker.calls.count() === 3);
  770. expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
  771. expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0);
  772. stanza = u.toStanza(`
  773. <message xml:lang="en" to="romeo@montague.lit/orchard"
  774. from="lounge@montague.lit/some1" type="groupchat" xmlns="jabber:client">
  775. <body>'tis I!</body>
  776. <markable xmlns="urn:xmpp:chat-markers:0"/>
  777. </message>`);
  778. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  779. await u.waitUntil(() => stanza_utils.isChatMarker.calls.count() === 4);
  780. expect(view.el.querySelectorAll('.chat-msg').length).toBe(2);
  781. expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0);
  782. done();
  783. }));
  784. describe("when received", function () {
  785. it("highlights all users mentioned via XEP-0372 references",
  786. mock.initConverse(
  787. ['rosterGroupsFetched'], {},
  788. async function (done, _converse) {
  789. const muc_jid = 'lounge@montague.lit';
  790. await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'tom');
  791. const view = _converse.api.chatviews.get(muc_jid);
  792. ['z3r0', 'mr.robot', 'gibson', 'sw0rdf1sh'].forEach((nick) => {
  793. _converse.connection._dataRecv(test_utils.createRequest(
  794. $pres({
  795. 'to': 'tom@montague.lit/resource',
  796. 'from': `lounge@montague.lit/${nick}`
  797. })
  798. .c('x', {xmlns: Strophe.NS.MUC_USER})
  799. .c('item', {
  800. 'affiliation': 'none',
  801. 'jid': `${nick}@montague.lit/resource`,
  802. 'role': 'participant'
  803. }))
  804. );
  805. });
  806. const msg = $msg({
  807. from: 'lounge@montague.lit/gibson',
  808. id: u.getUniqueId(),
  809. to: 'romeo@montague.lit',
  810. type: 'groupchat'
  811. }).c('body').t('hello z3r0 tom mr.robot, how are you?').up()
  812. .c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'6', 'end':'10', 'type':'mention', 'uri':'xmpp:z3r0@montague.lit'}).up()
  813. .c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'11', 'end':'14', 'type':'mention', 'uri':'xmpp:romeo@montague.lit'}).up()
  814. .c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'15', 'end':'23', 'type':'mention', 'uri':'xmpp:mr.robot@montague.lit'}).nodeTree;
  815. await view.model.queueMessage(msg);
  816. const message = await u.waitUntil(() => view.el.querySelector('.chat-msg__text'));
  817. expect(message.classList.length).toEqual(1);
  818. expect(message.innerHTML).toBe(
  819. 'hello <span class="mention">z3r0</span> '+
  820. '<span class="mention mention--self badge badge-info">tom</span> '+
  821. '<span class="mention">mr.robot</span>, how are you?');
  822. done();
  823. }));
  824. });
  825. describe("in which someone is mentioned", function () {
  826. it("gets parsed for mentions which get turned into references",
  827. mock.initConverse(
  828. ['rosterGroupsFetched'], {},
  829. async function (done, _converse) {
  830. const muc_jid = 'lounge@montague.lit';
  831. await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'tom');
  832. const view = _converse.api.chatviews.get(muc_jid);
  833. ['z3r0', 'mr.robot', 'gibson', 'sw0rdf1sh', 'Link Mauve'].forEach((nick) => {
  834. _converse.connection._dataRecv(test_utils.createRequest(
  835. $pres({
  836. 'to': 'tom@montague.lit/resource',
  837. 'from': `lounge@montague.lit/${nick}`
  838. })
  839. .c('x', {xmlns: Strophe.NS.MUC_USER})
  840. .c('item', {
  841. 'affiliation': 'none',
  842. 'jid': `${nick.replace(/\s/g, '-')}@montague.lit/resource`,
  843. 'role': 'participant'
  844. })));
  845. });
  846. // Also check that nicks from received messages, (but for which
  847. // we don't have occupant objects) can be mentioned.
  848. const stanza = u.toStanza(`
  849. <message xmlns="jabber:client"
  850. from="${muc_jid}/gh0st"
  851. to="${_converse.connection.bare_jid}"
  852. type="groupchat">
  853. <body>Boo!</body>
  854. </message>`);
  855. await view.model.queueMessage(stanza);
  856. // Run a few unit tests for the parseTextForReferences method
  857. let [text, references] = view.model.parseTextForReferences('hello z3r0')
  858. expect(references.length).toBe(0);
  859. expect(text).toBe('hello z3r0');
  860. [text, references] = view.model.parseTextForReferences('hello @z3r0')
  861. expect(references.length).toBe(1);
  862. expect(text).toBe('hello z3r0');
  863. expect(JSON.stringify(references))
  864. .toBe('[{"begin":6,"end":10,"value":"z3r0","type":"mention","uri":"xmpp:z3r0@montague.lit"}]');
  865. [text, references] = view.model.parseTextForReferences('hello @some1 @z3r0 @gibson @mr.robot, how are you?')
  866. expect(text).toBe('hello @some1 z3r0 gibson mr.robot, how are you?');
  867. expect(JSON.stringify(references))
  868. .toBe('[{"begin":13,"end":17,"value":"z3r0","type":"mention","uri":"xmpp:z3r0@montague.lit"},'+
  869. '{"begin":18,"end":24,"value":"gibson","type":"mention","uri":"xmpp:gibson@montague.lit"},'+
  870. '{"begin":25,"end":33,"value":"mr.robot","type":"mention","uri":"xmpp:mr.robot@montague.lit"}]');
  871. [text, references] = view.model.parseTextForReferences('yo @gib')
  872. expect(text).toBe('yo @gib');
  873. expect(references.length).toBe(0);
  874. [text, references] = view.model.parseTextForReferences('yo @gibsonian')
  875. expect(text).toBe('yo @gibsonian');
  876. expect(references.length).toBe(0);
  877. [text, references] = view.model.parseTextForReferences('@gibson')
  878. expect(text).toBe('gibson');
  879. expect(references.length).toBe(1);
  880. expect(JSON.stringify(references))
  881. .toBe('[{"begin":0,"end":6,"value":"gibson","type":"mention","uri":"xmpp:gibson@montague.lit"}]');
  882. [text, references] = view.model.parseTextForReferences('hi @Link Mauve how are you?')
  883. expect(text).toBe('hi Link Mauve how are you?');
  884. expect(references.length).toBe(1);
  885. expect(JSON.stringify(references))
  886. .toBe('[{"begin":3,"end":13,"value":"Link Mauve","type":"mention","uri":"xmpp:Link-Mauve@montague.lit"}]');
  887. [text, references] = view.model.parseTextForReferences('https://example.org/@gibson')
  888. expect(text).toBe('https://example.org/@gibson');
  889. expect(references.length).toBe(0);
  890. expect(JSON.stringify(references))
  891. .toBe('[]');
  892. [text, references] = view.model.parseTextForReferences('mail@gibson.com')
  893. expect(text).toBe('mail@gibson.com');
  894. expect(references.length).toBe(0);
  895. expect(JSON.stringify(references))
  896. .toBe('[]');
  897. [text, references] = view.model.parseTextForReferences(
  898. 'https://linkmauve.fr@Link Mauve/ https://linkmauve.fr/@github/is_back gibson@gibson.com gibson@Link Mauve.fr')
  899. expect(text).toBe(
  900. 'https://linkmauve.fr@Link Mauve/ https://linkmauve.fr/@github/is_back gibson@gibson.com gibson@Link Mauve.fr');
  901. expect(references.length).toBe(0);
  902. expect(JSON.stringify(references))
  903. .toBe('[]');
  904. [text, references] = view.model.parseTextForReferences('@gh0st where are you?')
  905. expect(text).toBe('gh0st where are you?');
  906. expect(references.length).toBe(1);
  907. expect(JSON.stringify(references))
  908. .toBe('[{"begin":0,"end":5,"value":"gh0st","type":"mention","uri":"xmpp:lounge@montague.lit/gh0st"}]');
  909. done();
  910. }));
  911. it("parses for mentions as indicated with an @ preceded by a space or at the start of the text",
  912. mock.initConverse(
  913. ['rosterGroupsFetched'], {},
  914. async function (done, _converse) {
  915. const muc_jid = 'lounge@montague.lit';
  916. await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'tom');
  917. const view = _converse.api.chatviews.get(muc_jid);
  918. ['NotAnAdress', 'darnuria'].forEach((nick) => {
  919. _converse.connection._dataRecv(test_utils.createRequest(
  920. $pres({
  921. 'to': 'tom@montague.lit/resource',
  922. 'from': `lounge@montague.lit/${nick}`
  923. })
  924. .c('x', {xmlns: Strophe.NS.MUC_USER})
  925. .c('item', {
  926. 'affiliation': 'none',
  927. 'jid': `${nick.replace(/\s/g, '-')}@montague.lit/resource`,
  928. 'role': 'participant'
  929. })));
  930. });
  931. // Test that we don't match @nick in email adresses.
  932. let [text, references] = view.model.parseTextForReferences('contact contact@NotAnAdress.eu');
  933. expect(references.length).toBe(0);
  934. expect(text).toBe('contact contact@NotAnAdress.eu');
  935. // Test that we don't match @nick in url
  936. [text, references] = view.model.parseTextForReferences('nice website https://darnuria.eu/@darnuria');
  937. expect(references.length).toBe(0);
  938. expect(text).toBe('nice website https://darnuria.eu/@darnuria');
  939. done();
  940. }));
  941. it("properly encodes the URIs in sent out references",
  942. mock.initConverse(
  943. ['rosterGroupsFetched'], {},
  944. async function (done, _converse) {
  945. const muc_jid = 'lounge@montague.lit';
  946. await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'tom');
  947. const view = _converse.api.roomviews.get(muc_jid);
  948. _converse.connection._dataRecv(test_utils.createRequest(
  949. $pres({
  950. 'to': 'tom@montague.lit/resource',
  951. 'from': `lounge@montague.lit/Link Mauve`
  952. })
  953. .c('x', {xmlns: Strophe.NS.MUC_USER})
  954. .c('item', {
  955. 'affiliation': 'none',
  956. 'role': 'participant'
  957. })));
  958. await u.waitUntil(() => view.model.occupants.length === 2);
  959. const textarea = view.el.querySelector('textarea.chat-textarea');
  960. textarea.value = 'hello @Link Mauve'
  961. const enter_event = {
  962. 'target': textarea,
  963. 'preventDefault': function preventDefault () {},
  964. 'stopPropagation': function stopPropagation () {},
  965. 'keyCode': 13 // Enter
  966. }
  967. spyOn(_converse.connection, 'send');
  968. view.onKeyDown(enter_event);
  969. await new Promise(resolve => view.once('messageInserted', resolve));
  970. const msg = _converse.connection.send.calls.all()[0].args[0];
  971. expect(msg.toLocaleString())
  972. .toBe(`<message from="romeo@montague.lit/orchard" id="${msg.nodeTree.getAttribute("id")}" `+
  973. `to="lounge@montague.lit" type="groupchat" `+
  974. `xmlns="jabber:client">`+
  975. `<body>hello Link Mauve</body>`+
  976. `<active xmlns="http://jabber.org/protocol/chatstates"/>`+
  977. `<reference begin="6" end="16" type="mention" uri="xmpp:lounge@montague.lit/Link%20Mauve" xmlns="urn:xmpp:reference:0"/>`+
  978. `<origin-id id="${msg.nodeTree.querySelector('origin-id').getAttribute("id")}" xmlns="urn:xmpp:sid:0"/>`+
  979. `</message>`);
  980. done();
  981. }));
  982. it("can get corrected and given new references",
  983. mock.initConverse(
  984. ['rosterGroupsFetched'], {},
  985. async function (done, _converse) {
  986. const muc_jid = 'lounge@montague.lit';
  987. await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'tom');
  988. const view = _converse.api.chatviews.get(muc_jid);
  989. ['z3r0', 'mr.robot', 'gibson', 'sw0rdf1sh'].forEach((nick) => {
  990. _converse.connection._dataRecv(test_utils.createRequest(
  991. $pres({
  992. 'to': 'tom@montague.lit/resource',
  993. 'from': `lounge@montague.lit/${nick}`
  994. })
  995. .c('x', {xmlns: Strophe.NS.MUC_USER})
  996. .c('item', {
  997. 'affiliation': 'none',
  998. 'jid': `${nick}@montague.lit/resource`,
  999. 'role': 'participant'
  1000. })));
  1001. });
  1002. await u.waitUntil(() => view.model.occupants.length === 5);
  1003. const textarea = view.el.querySelector('textarea.chat-textarea');
  1004. textarea.value = 'hello @z3r0 @gibson @mr.robot, how are you?'
  1005. const enter_event = {
  1006. 'target': textarea,
  1007. 'preventDefault': function preventDefault () {},
  1008. 'stopPropagation': function stopPropagation () {},
  1009. 'keyCode': 13 // Enter
  1010. }
  1011. spyOn(_converse.connection, 'send');
  1012. view.onKeyDown(enter_event);
  1013. await new Promise(resolve => view.once('messageInserted', resolve));
  1014. const msg = _converse.connection.send.calls.all()[0].args[0];
  1015. expect(msg.toLocaleString())
  1016. .toBe(`<message from="romeo@montague.lit/orchard" id="${msg.nodeTree.getAttribute("id")}" `+
  1017. `to="lounge@montague.lit" type="groupchat" `+
  1018. `xmlns="jabber:client">`+
  1019. `<body>hello z3r0 gibson mr.robot, how are you?</body>`+
  1020. `<active xmlns="http://jabber.org/protocol/chatstates"/>`+
  1021. `<reference begin="6" end="10" type="mention" uri="xmpp:z3r0@montague.lit" xmlns="urn:xmpp:reference:0"/>`+
  1022. `<reference begin="11" end="17" type="mention" uri="xmpp:gibson@montague.lit" xmlns="urn:xmpp:reference:0"/>`+
  1023. `<reference begin="18" end="26" type="mention" uri="xmpp:mr.robot@montague.lit" xmlns="urn:xmpp:reference:0"/>`+
  1024. `<origin-id id="${msg.nodeTree.querySelector('origin-id').getAttribute("id")}" xmlns="urn:xmpp:sid:0"/>`+
  1025. `</message>`);
  1026. const action = view.el.querySelector('.chat-msg .chat-msg__action');
  1027. action.style.opacity = 1;
  1028. action.click();
  1029. expect(textarea.value).toBe('hello @z3r0 @gibson @mr.robot, how are you?');
  1030. expect(view.model.messages.at(0).get('correcting')).toBe(true);
  1031. expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
  1032. await u.waitUntil(() => u.hasClass('correcting', view.el.querySelector('.chat-msg')), 500);
  1033. await u.waitUntil(() => _converse.connection.send.calls.count() === 2);
  1034. textarea.value = 'hello @z3r0 @gibson @sw0rdf1sh, how are you?';
  1035. view.onKeyDown(enter_event);
  1036. await u.waitUntil(() => view.el.querySelector('.chat-msg__text').textContent ===
  1037. 'hello z3r0 gibson sw0rdf1sh, how are you?', 500);
  1038. const correction = _converse.connection.send.calls.all()[2].args[0];
  1039. expect(correction.toLocaleString())
  1040. .toBe(`<message from="romeo@montague.lit/orchard" id="${correction.nodeTree.getAttribute("id")}" `+
  1041. `to="lounge@montague.lit" type="groupchat" `+
  1042. `xmlns="jabber:client">`+
  1043. `<body>hello z3r0 gibson sw0rdf1sh, how are you?</body>`+
  1044. `<active xmlns="http://jabber.org/protocol/chatstates"/>`+
  1045. `<reference begin="6" end="10" type="mention" uri="xmpp:z3r0@montague.lit" xmlns="urn:xmpp:reference:0"/>`+
  1046. `<reference begin="11" end="17" type="mention" uri="xmpp:gibson@montague.lit" xmlns="urn:xmpp:reference:0"/>`+
  1047. `<reference begin="18" end="27" type="mention" uri="xmpp:sw0rdf1sh@montague.lit" xmlns="urn:xmpp:reference:0"/>`+
  1048. `<replace id="${msg.nodeTree.getAttribute("id")}" xmlns="urn:xmpp:message-correct:0"/>`+
  1049. `<origin-id id="${correction.nodeTree.querySelector('origin-id').getAttribute("id")}" xmlns="urn:xmpp:sid:0"/>`+
  1050. `</message>`);
  1051. done();
  1052. }));
  1053. it("includes XEP-0372 references to that person",
  1054. mock.initConverse(
  1055. ['rosterGroupsFetched'], {},
  1056. async function (done, _converse) {
  1057. const muc_jid = 'lounge@montague.lit';
  1058. await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  1059. const view = _converse.api.chatviews.get(muc_jid);
  1060. ['z3r0', 'mr.robot', 'gibson', 'sw0rdf1sh'].forEach((nick) => {
  1061. _converse.connection._dataRecv(test_utils.createRequest(
  1062. $pres({
  1063. 'to': 'tom@montague.lit/resource',
  1064. 'from': `lounge@montague.lit/${nick}`
  1065. })
  1066. .c('x', {xmlns: Strophe.NS.MUC_USER})
  1067. .c('item', {
  1068. 'affiliation': 'none',
  1069. 'jid': `${nick}@montague.lit/resource`,
  1070. 'role': 'participant'
  1071. })));
  1072. });
  1073. await u.waitUntil(() => view.model.occupants.length === 5);
  1074. spyOn(_converse.connection, 'send');
  1075. const textarea = view.el.querySelector('textarea.chat-textarea');
  1076. textarea.value = 'hello @z3r0 @gibson @mr.robot, how are you?'
  1077. const enter_event = {
  1078. 'target': textarea,
  1079. 'preventDefault': function preventDefault () {},
  1080. 'stopPropagation': function stopPropagation () {},
  1081. 'keyCode': 13 // Enter
  1082. }
  1083. view.onKeyDown(enter_event);
  1084. await new Promise(resolve => view.once('messageInserted', resolve));
  1085. const msg = _converse.connection.send.calls.all()[0].args[0];
  1086. expect(msg.toLocaleString())
  1087. .toBe(`<message from="romeo@montague.lit/orchard" id="${msg.nodeTree.getAttribute("id")}" `+
  1088. `to="lounge@montague.lit" type="groupchat" `+
  1089. `xmlns="jabber:client">`+
  1090. `<body>hello z3r0 gibson mr.robot, how are you?</body>`+
  1091. `<active xmlns="http://jabber.org/protocol/chatstates"/>`+
  1092. `<reference begin="6" end="10" type="mention" uri="xmpp:z3r0@montague.lit" xmlns="urn:xmpp:reference:0"/>`+
  1093. `<reference begin="11" end="17" type="mention" uri="xmpp:gibson@montague.lit" xmlns="urn:xmpp:reference:0"/>`+
  1094. `<reference begin="18" end="26" type="mention" uri="xmpp:mr.robot@montague.lit" xmlns="urn:xmpp:reference:0"/>`+
  1095. `<origin-id id="${msg.nodeTree.querySelector('origin-id').getAttribute("id")}" xmlns="urn:xmpp:sid:0"/>`+
  1096. `</message>`);
  1097. done();
  1098. }));
  1099. });
  1100. });
  1101. }));