muc_messages.js 65 KB

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