muc_messages.js 64 KB

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