2
0

muc_messages.js 56 KB

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