muc_messages.js 62 KB

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