converse-muc-views.js 101 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046
  1. // Converse.js
  2. // http://conversejs.org
  3. //
  4. // Copyright (c) 2013-2018, the Converse.js developers
  5. // Licensed under the Mozilla Public License (MPLv2)
  6. (function (root, factory) {
  7. define([
  8. "converse-core",
  9. "formdata-polyfill",
  10. "utils/muc",
  11. "xss",
  12. "templates/add_chatroom_modal.html",
  13. "templates/chatarea.html",
  14. "templates/chatroom.html",
  15. "templates/chatroom_details_modal.html",
  16. "templates/chatroom_disconnect.html",
  17. "templates/chatroom_features.html",
  18. "templates/chatroom_form.html",
  19. "templates/chatroom_head.html",
  20. "templates/chatroom_invite.html",
  21. "templates/chatroom_nickname_form.html",
  22. "templates/chatroom_password_form.html",
  23. "templates/chatroom_sidebar.html",
  24. "templates/info.html",
  25. "templates/list_chatrooms_modal.html",
  26. "templates/occupant.html",
  27. "templates/room_description.html",
  28. "templates/room_item.html",
  29. "templates/room_panel.html",
  30. "templates/rooms_results.html",
  31. "templates/spinner.html",
  32. "awesomplete",
  33. "converse-modal"
  34. ], factory);
  35. }(this, function (
  36. converse,
  37. _FormData,
  38. muc_utils,
  39. xss,
  40. tpl_add_chatroom_modal,
  41. tpl_chatarea,
  42. tpl_chatroom,
  43. tpl_chatroom_details_modal,
  44. tpl_chatroom_disconnect,
  45. tpl_chatroom_features,
  46. tpl_chatroom_form,
  47. tpl_chatroom_head,
  48. tpl_chatroom_invite,
  49. tpl_chatroom_nickname_form,
  50. tpl_chatroom_password_form,
  51. tpl_chatroom_sidebar,
  52. tpl_info,
  53. tpl_list_chatrooms_modal,
  54. tpl_occupant,
  55. tpl_room_description,
  56. tpl_room_item,
  57. tpl_room_panel,
  58. tpl_rooms_results,
  59. tpl_spinner,
  60. Awesomplete
  61. ) {
  62. "use strict";
  63. const { Backbone, Promise, Strophe, b64_sha1, moment, f, sizzle, _, $build, $iq, $msg, $pres } = converse.env;
  64. const u = converse.env.utils;
  65. const ROOM_FEATURES_MAP = {
  66. 'passwordprotected': 'unsecured',
  67. 'unsecured': 'passwordprotected',
  68. 'hidden': 'publicroom',
  69. 'publicroom': 'hidden',
  70. 'membersonly': 'open',
  71. 'open': 'membersonly',
  72. 'persistent': 'temporary',
  73. 'temporary': 'persistent',
  74. 'nonanonymous': 'semianonymous',
  75. 'semianonymous': 'nonanonymous',
  76. 'moderated': 'unmoderated',
  77. 'unmoderated': 'moderated'
  78. };
  79. converse.plugins.add('converse-muc-views', {
  80. /* Dependencies are other plugins which might be
  81. * overridden or relied upon, and therefore need to be loaded before
  82. * this plugin. They are "optional" because they might not be
  83. * available, in which case any overrides applicable to them will be
  84. * ignored.
  85. *
  86. * NB: These plugins need to have already been loaded via require.js.
  87. *
  88. * It's possible to make these dependencies "non-optional".
  89. * If the setting "strict_plugin_dependencies" is set to true,
  90. * an error will be raised if the plugin is not found.
  91. */
  92. dependencies: ["converse-autocomplete", "converse-modal", "converse-controlbox", "converse-chatview"],
  93. overrides: {
  94. ControlBoxView: {
  95. renderRoomsPanel () {
  96. const { _converse } = this.__super__;
  97. this.roomspanel = new _converse.RoomsPanel({
  98. 'model': new (_converse.RoomsPanelModel.extend({
  99. 'id': b64_sha1(`converse.roomspanel${_converse.bare_jid}`), // Required by sessionStorage
  100. 'browserStorage': new Backbone.BrowserStorage[_converse.config.get('storage')](
  101. b64_sha1(`converse.roomspanel${_converse.bare_jid}`))
  102. }))()
  103. });
  104. this.roomspanel.model.fetch();
  105. this.el.querySelector('.controlbox-pane').insertAdjacentElement(
  106. 'beforeEnd', this.roomspanel.render().el);
  107. if (!this.roomspanel.model.get('nick')) {
  108. this.roomspanel.model.save({
  109. nick: _converse.xmppstatus.vcard.get('nickname') || Strophe.getNodeFromJid(_converse.bare_jid)
  110. });
  111. }
  112. _converse.emit('roomsPanelRendered');
  113. },
  114. renderControlBoxPane () {
  115. const { _converse } = this.__super__;
  116. this.__super__.renderControlBoxPane.apply(this, arguments);
  117. if (_converse.allow_muc) {
  118. this.renderRoomsPanel();
  119. }
  120. },
  121. }
  122. },
  123. initialize () {
  124. const { _converse } = this,
  125. { __ } = _converse;
  126. _converse.api.promises.add(['roomsPanelRendered']);
  127. // Configuration values for this plugin
  128. // ====================================
  129. // Refer to docs/source/configuration.rst for explanations of these
  130. // configuration settings.
  131. _converse.api.settings.update({
  132. 'auto_list_rooms': false,
  133. 'hide_muc_server': false, // TODO: no longer implemented...
  134. 'muc_disable_moderator_commands': false,
  135. 'visible_toolbar_buttons': {
  136. 'toggle_occupants': true
  137. }
  138. });
  139. function ___ (str) {
  140. /* This is part of a hack to get gettext to scan strings to be
  141. * translated. Strings we cannot send to the function above because
  142. * they require variable interpolation and we don't yet have the
  143. * variables at scan time.
  144. *
  145. * See actionInfoMessages further below.
  146. */
  147. return str;
  148. }
  149. /* http://xmpp.org/extensions/xep-0045.html
  150. * ----------------------------------------
  151. * 100 message Entering a groupchat Inform user that any occupant is allowed to see the user's full JID
  152. * 101 message (out of band) Affiliation change Inform user that his or her affiliation changed while not in the groupchat
  153. * 102 message Configuration change Inform occupants that groupchat now shows unavailable members
  154. * 103 message Configuration change Inform occupants that groupchat now does not show unavailable members
  155. * 104 message Configuration change Inform occupants that a non-privacy-related groupchat configuration change has occurred
  156. * 110 presence Any groupchat presence Inform user that presence refers to one of its own groupchat occupants
  157. * 170 message or initial presence Configuration change Inform occupants that groupchat logging is now enabled
  158. * 171 message Configuration change Inform occupants that groupchat logging is now disabled
  159. * 172 message Configuration change Inform occupants that the groupchat is now non-anonymous
  160. * 173 message Configuration change Inform occupants that the groupchat is now semi-anonymous
  161. * 174 message Configuration change Inform occupants that the groupchat is now fully-anonymous
  162. * 201 presence Entering a groupchat Inform user that a new groupchat has been created
  163. * 210 presence Entering a groupchat Inform user that the service has assigned or modified the occupant's roomnick
  164. * 301 presence Removal from groupchat Inform user that he or she has been banned from the groupchat
  165. * 303 presence Exiting a groupchat Inform all occupants of new groupchat nickname
  166. * 307 presence Removal from groupchat Inform user that he or she has been kicked from the groupchat
  167. * 321 presence Removal from groupchat Inform user that he or she is being removed from the groupchat because of an affiliation change
  168. * 322 presence Removal from groupchat Inform user that he or she is being removed from the groupchat because the groupchat has been changed to members-only and the user is not a member
  169. * 332 presence Removal from groupchat Inform user that he or she is being removed from the groupchat because of a system shutdown
  170. */
  171. _converse.muc = {
  172. info_messages: {
  173. 100: __('This groupchat is not anonymous'),
  174. 102: __('This groupchat now shows unavailable members'),
  175. 103: __('This groupchat does not show unavailable members'),
  176. 104: __('The groupchat configuration has changed'),
  177. 170: __('groupchat logging is now enabled'),
  178. 171: __('groupchat logging is now disabled'),
  179. 172: __('This groupchat is now no longer anonymous'),
  180. 173: __('This groupchat is now semi-anonymous'),
  181. 174: __('This groupchat is now fully-anonymous'),
  182. 201: __('A new groupchat has been created')
  183. },
  184. disconnect_messages: {
  185. 301: __('You have been banned from this groupchat'),
  186. 307: __('You have been kicked from this groupchat'),
  187. 321: __("You have been removed from this groupchat because of an affiliation change"),
  188. 322: __("You have been removed from this groupchat because the groupchat has changed to members-only and you're not a member"),
  189. 332: __("You have been removed from this groupchat because the service hosting it is being shut down")
  190. },
  191. action_info_messages: {
  192. /* XXX: Note the triple underscore function and not double
  193. * underscore.
  194. *
  195. * This is a hack. We can't pass the strings to __ because we
  196. * don't yet know what the variable to interpolate is.
  197. *
  198. * Triple underscore will just return the string again, but we
  199. * can then at least tell gettext to scan for it so that these
  200. * strings are picked up by the translation machinery.
  201. */
  202. 301: ___("%1$s has been banned"),
  203. 303: ___("%1$s's nickname has changed"),
  204. 307: ___("%1$s has been kicked out"),
  205. 321: ___("%1$s has been removed because of an affiliation change"),
  206. 322: ___("%1$s has been removed for not being a member")
  207. },
  208. new_nickname_messages: {
  209. 210: ___('Your nickname has been automatically set to %1$s'),
  210. 303: ___('Your nickname has been changed to %1$s')
  211. }
  212. };
  213. function insertRoomInfo (el, stanza) {
  214. /* Insert groupchat info (based on returned #disco IQ stanza)
  215. *
  216. * Parameters:
  217. * (HTMLElement) el: The HTML DOM element that should
  218. * contain the info.
  219. * (XMLElement) stanza: The IQ stanza containing the groupchat
  220. * info.
  221. */
  222. // All MUC features found here: http://xmpp.org/registrar/disco-features.html
  223. el.querySelector('span.spinner').remove();
  224. el.querySelector('a.room-info').classList.add('selected');
  225. el.insertAdjacentHTML(
  226. 'beforeEnd',
  227. tpl_room_description({
  228. 'jid': stanza.getAttribute('from'),
  229. 'desc': _.get(_.head(sizzle('field[var="muc#roominfo_description"] value', stanza)), 'textContent'),
  230. 'occ': _.get(_.head(sizzle('field[var="muc#roominfo_occupants"] value', stanza)), 'textContent'),
  231. 'hidden': sizzle('feature[var="muc_hidden"]', stanza).length,
  232. 'membersonly': sizzle('feature[var="muc_membersonly"]', stanza).length,
  233. 'moderated': sizzle('feature[var="muc_moderated"]', stanza).length,
  234. 'nonanonymous': sizzle('feature[var="muc_nonanonymous"]', stanza).length,
  235. 'open': sizzle('feature[var="muc_open"]', stanza).length,
  236. 'passwordprotected': sizzle('feature[var="muc_passwordprotected"]', stanza).length,
  237. 'persistent': sizzle('feature[var="muc_persistent"]', stanza).length,
  238. 'publicroom': sizzle('feature[var="muc_publicroom"]', stanza).length,
  239. 'semianonymous': sizzle('feature[var="muc_semianonymous"]', stanza).length,
  240. 'temporary': sizzle('feature[var="muc_temporary"]', stanza).length,
  241. 'unmoderated': sizzle('feature[var="muc_unmoderated"]', stanza).length,
  242. 'label_desc': __('Description:'),
  243. 'label_jid': __('Groupchat Address (JID):'),
  244. 'label_occ': __('Participants:'),
  245. 'label_features': __('Features:'),
  246. 'label_requires_auth': __('Requires authentication'),
  247. 'label_hidden': __('Hidden'),
  248. 'label_requires_invite': __('Requires an invitation'),
  249. 'label_moderated': __('Moderated'),
  250. 'label_non_anon': __('Non-anonymous'),
  251. 'label_open_room': __('Open'),
  252. 'label_permanent_room': __('Permanent'),
  253. 'label_public': __('Public'),
  254. 'label_semi_anon': __('Semi-anonymous'),
  255. 'label_temp_room': __('Temporary'),
  256. 'label_unmoderated': __('Unmoderated')
  257. }));
  258. }
  259. function toggleRoomInfo (ev) {
  260. /* Show/hide extra information about a groupchat in a listing. */
  261. const parent_el = u.ancestor(ev.target, '.room-item'),
  262. div_el = parent_el.querySelector('div.room-info');
  263. if (div_el) {
  264. u.slideIn(div_el).then(u.removeElement)
  265. parent_el.querySelector('a.room-info').classList.remove('selected');
  266. } else {
  267. parent_el.insertAdjacentHTML('beforeend', tpl_spinner());
  268. _converse.api.disco.info(ev.target.getAttribute('data-room-jid'), null)
  269. .then((stanza) => insertRoomInfo(parent_el, stanza))
  270. .catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
  271. }
  272. }
  273. _converse.ListChatRoomsModal = _converse.BootstrapModal.extend({
  274. events: {
  275. 'submit form': 'showRooms',
  276. 'click a.room-info': 'toggleRoomInfo',
  277. 'change input[name=nick]': 'setNick',
  278. 'change input[name=server]': 'setDomain',
  279. 'click .open-room': 'openRoom'
  280. },
  281. initialize () {
  282. _converse.BootstrapModal.prototype.initialize.apply(this, arguments);
  283. this.model.on('change:muc_domain', this.onDomainChange, this);
  284. },
  285. toHTML () {
  286. return tpl_list_chatrooms_modal(_.extend(this.model.toJSON(), {
  287. 'heading_list_chatrooms': __('Query for Groupchats'),
  288. 'label_server_address': __('Server address'),
  289. 'label_query': __('Show groupchats'),
  290. 'server_placeholder': __('conference.example.org')
  291. }));
  292. },
  293. afterRender () {
  294. this.el.addEventListener('shown.bs.modal', () => {
  295. this.el.querySelector('input[name="server"]').focus();
  296. }, false);
  297. },
  298. openRoom (ev) {
  299. ev.preventDefault();
  300. const jid = ev.target.getAttribute('data-room-jid');
  301. const name = ev.target.getAttribute('data-room-name');
  302. this.modal.hide();
  303. _converse.api.rooms.open(jid, {'name': name});
  304. },
  305. toggleRoomInfo (ev) {
  306. ev.preventDefault();
  307. toggleRoomInfo(ev);
  308. },
  309. onDomainChange (model) {
  310. if (_converse.auto_list_rooms) {
  311. this.updateRoomsList();
  312. }
  313. },
  314. roomStanzaItemToHTMLElement (groupchat) {
  315. const name = Strophe.unescapeNode(groupchat.getAttribute('name') || groupchat.getAttribute('jid'));
  316. const div = document.createElement('div');
  317. div.innerHTML = tpl_room_item({
  318. 'name': Strophe.xmlunescape(name),
  319. 'jid': groupchat.getAttribute('jid'),
  320. 'open_title': __('Click to open this groupchat'),
  321. 'info_title': __('Show more information on this groupchat')
  322. });
  323. return div.firstElementChild;
  324. },
  325. removeSpinner () {
  326. _.each(this.el.querySelectorAll('span.spinner'),
  327. (el) => el.parentNode.removeChild(el)
  328. );
  329. },
  330. informNoRoomsFound () {
  331. const chatrooms_el = this.el.querySelector('.available-chatrooms');
  332. chatrooms_el.innerHTML = tpl_rooms_results({
  333. 'feedback_text': __('No groupchats found')
  334. });
  335. const input_el = this.el.querySelector('input[name="server"]');
  336. input_el.classList.remove('hidden')
  337. this.removeSpinner();
  338. },
  339. onRoomsFound (iq) {
  340. /* Handle the IQ stanza returned from the server, containing
  341. * all its public groupchats.
  342. */
  343. const available_chatrooms = this.el.querySelector('.available-chatrooms');
  344. this.rooms = iq.querySelectorAll('query item');
  345. if (this.rooms.length) {
  346. // For translators: %1$s is a variable and will be
  347. // replaced with the XMPP server name
  348. available_chatrooms.innerHTML = tpl_rooms_results({
  349. 'feedback_text': __('Groupchats found:')
  350. });
  351. const fragment = document.createDocumentFragment();
  352. const children = _.reject(_.map(this.rooms, this.roomStanzaItemToHTMLElement), _.isNil)
  353. _.each(children, (child) => fragment.appendChild(child));
  354. available_chatrooms.appendChild(fragment);
  355. this.removeSpinner();
  356. } else {
  357. this.informNoRoomsFound();
  358. }
  359. return true;
  360. },
  361. updateRoomsList () {
  362. /* Send an IQ stanza to the server asking for all groupchats
  363. */
  364. _converse.connection.sendIQ(
  365. $iq({
  366. 'to': this.model.get('muc_domain'),
  367. 'from': _converse.connection.jid,
  368. 'type': "get"
  369. }).c("query", {xmlns: Strophe.NS.DISCO_ITEMS}),
  370. this.onRoomsFound.bind(this),
  371. this.informNoRoomsFound.bind(this),
  372. 5000
  373. );
  374. },
  375. showRooms (ev) {
  376. ev.preventDefault();
  377. const data = new FormData(ev.target);
  378. this.model.save('muc_domain', Strophe.getDomainFromJid(data.get('server')));
  379. this.updateRoomsList();
  380. },
  381. setDomain (ev) {
  382. this.model.save('muc_domain', Strophe.getDomainFromJid(ev.target.value));
  383. },
  384. setNick (ev) {
  385. this.model.save({nick: ev.target.value});
  386. }
  387. });
  388. _converse.AddChatRoomModal = _converse.BootstrapModal.extend({
  389. events: {
  390. 'submit form.add-chatroom': 'openChatRoom'
  391. },
  392. toHTML () {
  393. return tpl_add_chatroom_modal(_.extend(this.model.toJSON(), {
  394. 'heading_new_chatroom': __('Enter a new Groupchat'),
  395. 'label_room_address': __('Groupchat address'),
  396. 'label_nickname': __('Optional nickname'),
  397. 'chatroom_placeholder': __('name@conference.example.org'),
  398. 'label_join': __('Join'),
  399. }));
  400. },
  401. afterRender () {
  402. this.el.addEventListener('shown.bs.modal', () => {
  403. this.el.querySelector('input[name="chatroom"]').focus();
  404. }, false);
  405. },
  406. parseRoomDataFromEvent (form) {
  407. const data = new FormData(form);
  408. const jid = data.get('chatroom');
  409. this.model.save('muc_domain', Strophe.getDomainFromJid(jid));
  410. return {
  411. 'jid': jid,
  412. 'nick': data.get('nickname')
  413. }
  414. },
  415. openChatRoom (ev) {
  416. ev.preventDefault();
  417. const data = this.parseRoomDataFromEvent(ev.target);
  418. if (data.nick === "") {
  419. // Make sure defaults apply if no nick is provided.
  420. data.nick = undefined;
  421. }
  422. _converse.api.rooms.open(data.jid, data);
  423. this.modal.hide();
  424. ev.target.reset();
  425. }
  426. });
  427. _converse.RoomDetailsModal = _converse.BootstrapModal.extend({
  428. initialize () {
  429. _converse.BootstrapModal.prototype.initialize.apply(this, arguments);
  430. this.model.on('change', this.render, this);
  431. this.model.occupants.on('change', this.render, this);
  432. },
  433. toHTML () {
  434. return tpl_chatroom_details_modal(_.extend(
  435. this.model.toJSON(), {
  436. '_': _,
  437. '__': __,
  438. 'topic': u.addHyperlinks(xss.filterXSS(_.get(this.model.get('subject'), 'text'), {'whiteList': {}})),
  439. 'display_name': __('Groupchat info for %1$s', this.model.getDisplayName()),
  440. 'num_occupants': this.model.occupants.length
  441. })
  442. );
  443. }
  444. });
  445. _converse.ChatRoomView = _converse.ChatBoxView.extend({
  446. /* Backbone.NativeView which renders a groupchat, based upon the view
  447. * for normal one-on-one chat boxes.
  448. */
  449. length: 300,
  450. tagName: 'div',
  451. className: 'chatbox chatroom hidden',
  452. is_chatroom: true,
  453. events: {
  454. 'change input.fileupload': 'onFileSelection',
  455. 'click .chat-msg__action-edit': 'onMessageEditButtonClicked',
  456. 'click .chatbox-navback': 'showControlBox',
  457. 'click .close-chatbox-button': 'close',
  458. 'click .configure-chatroom-button': 'getAndRenderConfigurationForm',
  459. 'click .show-room-details-modal': 'showRoomDetailsModal',
  460. 'click .hide-occupants': 'hideOccupants',
  461. 'click .new-msgs-indicator': 'viewUnreadMessages',
  462. 'click .occupant-nick': 'onOccupantClicked',
  463. 'click .send-button': 'onFormSubmitted',
  464. 'click .toggle-call': 'toggleCall',
  465. 'click .toggle-occupants': 'toggleOccupants',
  466. 'click .toggle-smiley ul.emoji-picker li': 'insertEmoji',
  467. 'click .toggle-smiley': 'toggleEmojiMenu',
  468. 'click .upload-file': 'toggleFileUpload',
  469. 'keydown .chat-textarea': 'keyPressed',
  470. 'keyup .chat-textarea': 'keyUp',
  471. 'input .chat-textarea': 'inputChanged'
  472. },
  473. initialize () {
  474. this.initDebounced();
  475. this.model.messages.on('add', this.onMessageAdded, this);
  476. this.model.messages.on('rendered', this.scrollDown, this);
  477. this.model.on('change:affiliation', this.renderHeading, this);
  478. this.model.on('change:connection_status', this.afterConnected, this);
  479. this.model.on('change:name', this.renderHeading, this);
  480. this.model.on('change:subject', this.renderHeading, this);
  481. this.model.on('change:subject', this.setChatRoomSubject, this);
  482. this.model.on('configurationNeeded', this.getAndRenderConfigurationForm, this);
  483. this.model.on('destroy', this.hide, this);
  484. this.model.on('show', this.show, this);
  485. this.model.occupants.on('add', this.showJoinNotification, this);
  486. this.model.occupants.on('remove', this.showLeaveNotification, this);
  487. this.model.occupants.on('change:show', this.showJoinOrLeaveNotification, this);
  488. this.model.occupants.on('change:role', this.informOfOccupantsRoleChange, this);
  489. this.model.occupants.on('change:affiliation', this.informOfOccupantsAffiliationChange, this);
  490. this.createEmojiPicker();
  491. this.createOccupantsView();
  492. this.render().insertIntoDOM();
  493. this.registerHandlers();
  494. if (this.model.get('connection_status') !== converse.ROOMSTATUS.ENTERED) {
  495. const handler = () => {
  496. if (!u.isPersistableModel(this.model)) {
  497. // Happens during tests, nothing to do if this
  498. // is a hanging chatbox (i.e. not in the collection anymore).
  499. return;
  500. }
  501. this.populateAndJoin();
  502. _converse.emit('chatRoomOpened', this);
  503. }
  504. this.model.getRoomFeatures().then(handler, handler);
  505. } else {
  506. this.fetchMessages();
  507. _converse.emit('chatRoomOpened', this);
  508. }
  509. },
  510. render () {
  511. this.el.setAttribute('id', this.model.get('box_id'));
  512. this.el.innerHTML = tpl_chatroom();
  513. this.renderHeading();
  514. this.renderChatArea();
  515. this.renderMessageForm();
  516. this.initAutoComplete();
  517. if (this.model.get('connection_status') !== converse.ROOMSTATUS.ENTERED) {
  518. this.showSpinner();
  519. }
  520. return this;
  521. },
  522. renderHeading () {
  523. /* Render the heading UI of the groupchat. */
  524. this.el.querySelector('.chat-head-chatroom').innerHTML = this.generateHeadingHTML();
  525. },
  526. renderChatArea () {
  527. /* Render the UI container in which groupchat messages will appear.
  528. */
  529. if (_.isNull(this.el.querySelector('.chat-area'))) {
  530. const container_el = this.el.querySelector('.chatroom-body');
  531. container_el.insertAdjacentHTML('beforeend', tpl_chatarea({
  532. 'show_send_button': _converse.show_send_button
  533. }));
  534. container_el.insertAdjacentElement('beforeend', this.occupantsview.el);
  535. this.content = this.el.querySelector('.chat-content');
  536. this.toggleOccupants(null, true);
  537. }
  538. return this;
  539. },
  540. initAutoComplete () {
  541. this.auto_complete = new _converse.AutoComplete(this.el, {
  542. 'auto_first': true,
  543. 'auto_evaluate': false,
  544. 'min_chars': 1,
  545. 'match_current_word': true,
  546. 'match_on_tab': true,
  547. 'list': () => this.model.occupants.map(o => ({'label': o.getDisplayName(), 'value': `@${o.getDisplayName()}`})),
  548. 'filter': _converse.FILTER_STARTSWITH,
  549. 'trigger_on_at': true
  550. });
  551. this.auto_complete.on('suggestion-box-selectcomplete', () => (this.auto_completing = false));
  552. },
  553. keyPressed (ev) {
  554. if (this.auto_complete.keyPressed(ev)) {
  555. return;
  556. }
  557. return _converse.ChatBoxView.prototype.keyPressed.apply(this, arguments);
  558. },
  559. keyUp (ev) {
  560. this.auto_complete.evaluate(ev);
  561. },
  562. showRoomDetailsModal (ev) {
  563. ev.preventDefault();
  564. if (_.isUndefined(this.model.room_details_modal)) {
  565. this.model.room_details_modal = new _converse.RoomDetailsModal({'model': this.model});
  566. }
  567. this.model.room_details_modal.show(ev);
  568. },
  569. showChatStateNotification (message) {
  570. if (message.get('sender') === 'me') {
  571. return;
  572. }
  573. return _converse.ChatBoxView.prototype.showChatStateNotification.apply(this, arguments);
  574. },
  575. createOccupantsView () {
  576. /* Create the ChatRoomOccupantsView Backbone.NativeView
  577. */
  578. this.model.occupants.chatroomview = this;
  579. this.occupantsview = new _converse.ChatRoomOccupantsView({'model': this.model.occupants});
  580. return this;
  581. },
  582. informOfOccupantsAffiliationChange(occupant, changed) {
  583. const previous_affiliation = occupant._previousAttributes.affiliation,
  584. current_affiliation = occupant.get('affiliation');
  585. if (previous_affiliation === 'admin') {
  586. this.showChatEvent(__("%1$s is no longer an admin of this groupchat", occupant.get('nick')))
  587. } else if (previous_affiliation === 'owner') {
  588. this.showChatEvent(__("%1$s is no longer an owner of this groupchat", occupant.get('nick')))
  589. } else if (previous_affiliation === 'outcast') {
  590. this.showChatEvent(__("%1$s is no longer banned from this groupchat", occupant.get('nick')))
  591. }
  592. if (current_affiliation === 'none' && previous_affiliation === 'member') {
  593. this.showChatEvent(__("%1$s is no longer a permanent member of this groupchat", occupant.get('nick')))
  594. } if (current_affiliation === 'member') {
  595. this.showChatEvent(__("%1$s is now a permanent member of this groupchat", occupant.get('nick')))
  596. } else if (current_affiliation === 'outcast') {
  597. this.showChatEvent(__("%1$s has been banned from this groupchat", occupant.get('nick')))
  598. } else if (current_affiliation === 'admin' || current_affiliation == 'owner') {
  599. this.showChatEvent(__(`%1$s is now an ${current_affiliation} of this groupchat`, occupant.get('nick')))
  600. }
  601. },
  602. informOfOccupantsRoleChange (occupant, changed) {
  603. const previous_role = occupant._previousAttributes.role;
  604. if (previous_role === 'moderator') {
  605. this.showChatEvent(__("%1$s is no longer a moderator", occupant.get('nick')))
  606. }
  607. if (previous_role === 'visitor') {
  608. this.showChatEvent(__("%1$s has been given a voice again", occupant.get('nick')))
  609. }
  610. if (occupant.get('role') === 'visitor') {
  611. this.showChatEvent(__("%1$s has been muted", occupant.get('nick')))
  612. }
  613. if (occupant.get('role') === 'moderator') {
  614. this.showChatEvent(__("%1$s is now a moderator", occupant.get('nick')))
  615. }
  616. },
  617. generateHeadingHTML () {
  618. /* Returns the heading HTML to be rendered.
  619. */
  620. return tpl_chatroom_head(
  621. _.extend(this.model.toJSON(), {
  622. 'Strophe': Strophe,
  623. 'info_close': __('Close and leave this groupchat'),
  624. 'info_configure': __('Configure this groupchat'),
  625. 'info_details': __('Show more details about this groupchat'),
  626. 'description': u.addHyperlinks(xss.filterXSS(_.get(this.model.get('subject'), 'text'), {'whiteList': {}})),
  627. }));
  628. },
  629. afterShown () {
  630. /* Override from converse-chatview, specifically to avoid
  631. * the 'active' chat state from being sent out prematurely.
  632. *
  633. * This is instead done in `afterConnected` below.
  634. */
  635. if (u.isPersistableModel(this.model)) {
  636. this.model.clearUnreadMsgCounter();
  637. this.model.save();
  638. }
  639. this.occupantsview.setOccupantsHeight();
  640. this.scrollDown();
  641. this.renderEmojiPicker();
  642. },
  643. show () {
  644. if (u.isVisible(this.el)) {
  645. this.focus();
  646. return;
  647. }
  648. // Override from converse-chatview in order to not use
  649. // "fadeIn", which causes flashing.
  650. u.showElement(this.el);
  651. this.afterShown();
  652. },
  653. afterConnected () {
  654. if (this.model.get('connection_status') === converse.ROOMSTATUS.ENTERED) {
  655. this.hideSpinner();
  656. this.setChatState(_converse.ACTIVE);
  657. this.scrollDown();
  658. this.focus();
  659. }
  660. },
  661. getToolbarOptions () {
  662. return _.extend(
  663. _converse.ChatBoxView.prototype.getToolbarOptions.apply(this, arguments),
  664. {
  665. 'label_hide_occupants': __('Hide the list of participants'),
  666. 'show_occupants_toggle': this.is_chatroom && _converse.visible_toolbar_buttons.toggle_occupants
  667. }
  668. );
  669. },
  670. close (ev) {
  671. /* Close this chat box, which implies leaving the groupchat as
  672. * well.
  673. */
  674. this.hide();
  675. if (Backbone.history.getFragment() === "converse/room?jid="+this.model.get('jid')) {
  676. _converse.router.navigate('');
  677. }
  678. this.model.leave();
  679. _converse.ChatBoxView.prototype.close.apply(this, arguments);
  680. },
  681. setOccupantsVisibility () {
  682. const icon_el = this.el.querySelector('.toggle-occupants');
  683. if (this.model.get('hidden_occupants')) {
  684. u.removeClass('fa-angle-double-right', icon_el);
  685. u.addClass('fa-angle-double-left', icon_el);
  686. u.addClass('full', this.el.querySelector('.chat-area'));
  687. u.hideElement(this.el.querySelector('.occupants'));
  688. } else {
  689. u.addClass('fa-angle-double-right', icon_el);
  690. u.removeClass('fa-angle-double-left', icon_el);
  691. u.removeClass('full', this.el.querySelector('.chat-area'));
  692. u.removeClass('hidden', this.el.querySelector('.occupants'));
  693. }
  694. this.occupantsview.setOccupantsHeight();
  695. },
  696. hideOccupants (ev, preserve_state) {
  697. /* Show or hide the right sidebar containing the chat
  698. * occupants (and the invite widget).
  699. */
  700. if (ev) {
  701. ev.preventDefault();
  702. ev.stopPropagation();
  703. }
  704. this.model.save({'hidden_occupants': true});
  705. this.setOccupantsVisibility();
  706. this.scrollDown();
  707. },
  708. toggleOccupants (ev, preserve_state) {
  709. /* Show or hide the right sidebar containing the chat
  710. * occupants (and the invite widget).
  711. */
  712. if (ev) {
  713. ev.preventDefault();
  714. ev.stopPropagation();
  715. }
  716. if (!preserve_state) {
  717. this.model.set({'hidden_occupants': !this.model.get('hidden_occupants')});
  718. }
  719. this.setOccupantsVisibility();
  720. this.scrollDown();
  721. },
  722. onOccupantClicked (ev) {
  723. /* When an occupant is clicked, insert their nickname into
  724. * the chat textarea input.
  725. */
  726. this.insertIntoTextArea(ev.target.textContent);
  727. },
  728. handleChatStateNotification (message) {
  729. /* Override the method on the ChatBoxView base class to
  730. * ignore <gone/> notifications in groupchats.
  731. *
  732. * As laid out in the business rules in XEP-0085
  733. * http://xmpp.org/extensions/xep-0085.html#bizrules-groupchat
  734. */
  735. if (message.get('fullname') === this.model.get('nick')) {
  736. // Don't know about other servers, but OpenFire sends
  737. // back to you your own chat state notifications.
  738. // We ignore them here...
  739. return;
  740. }
  741. if (message.get('chat_state') !== _converse.GONE) {
  742. _converse.ChatBoxView.prototype.handleChatStateNotification.apply(this, arguments);
  743. }
  744. },
  745. modifyRole (groupchat, nick, role, reason, onSuccess, onError) {
  746. const item = $build("item", {nick, role});
  747. const iq = $iq({to: groupchat, type: "set"}).c("query", {xmlns: Strophe.NS.MUC_ADMIN}).cnode(item.node);
  748. if (reason !== null) { iq.c("reason", reason); }
  749. return _converse.connection.sendIQ(iq, onSuccess, onError);
  750. },
  751. verifyRoles (roles) {
  752. const me = this.model.occupants.findWhere({'jid': _converse.bare_jid});
  753. if (!_.includes(roles, me.get('role'))) {
  754. this.showErrorMessage(__(`Forbidden: you do not have the necessary role in order to do that.`))
  755. return false;
  756. }
  757. return true;
  758. },
  759. verifyAffiliations (affiliations) {
  760. const me = this.model.occupants.findWhere({'jid': _converse.bare_jid});
  761. if (!_.includes(affiliations, me.get('affiliation'))) {
  762. this.showErrorMessage(__(`Forbidden: you do not have the necessary affiliation in order to do that.`))
  763. return false;
  764. }
  765. return true;
  766. },
  767. validateRoleChangeCommand (command, args) {
  768. /* Check that a command to change a groupchat user's role or
  769. * affiliation has anough arguments.
  770. */
  771. if (args.length < 1 || args.length > 2) {
  772. this.showErrorMessage(
  773. __('Error: the "%1$s" command takes two arguments, the user\'s nickname and optionally a reason.', command)
  774. );
  775. return false;
  776. }
  777. if (!this.model.occupants.findWhere({'nick': args[0]}) && !this.model.occupants.findWhere({'jid': args[0]})) {
  778. this.showErrorMessage(__('Error: couldn\'t find a groupchat participant "%1$s"', args[0]));
  779. return false;
  780. }
  781. return true;
  782. },
  783. onCommandError (err) {
  784. _converse.log(err, Strophe.LogLevel.FATAL);
  785. this.showErrorMessage(__("Sorry, an error happened while running the command. Check your browser's developer console for details."));
  786. },
  787. parseMessageForCommands (text) {
  788. if (_converse.ChatBoxView.prototype.parseMessageForCommands.apply(this, arguments)) {
  789. return true;
  790. }
  791. if (_converse.muc_disable_moderator_commands) {
  792. return false;
  793. }
  794. const match = text.replace(/^\s*/, "").match(/^\/(.*?)(?: (.*))?$/) || [false, '', ''],
  795. args = match[2] && match[2].splitOnce(' ').filter(s => s) || [],
  796. command = match[1].toLowerCase();
  797. switch (command) {
  798. case 'admin':
  799. if (!this.verifyAffiliations(['owner']) || !this.validateRoleChangeCommand(command, args)) {
  800. break;
  801. }
  802. this.model.setAffiliation('admin', [{
  803. 'jid': args[0],
  804. 'reason': args[1]
  805. }]).then(
  806. () => this.model.occupants.fetchMembers(),
  807. (err) => this.onCommandError(err)
  808. );
  809. break;
  810. case 'ban':
  811. if (!this.verifyAffiliations(['owner', 'admin']) || !this.validateRoleChangeCommand(command, args)) {
  812. break;
  813. }
  814. this.model.setAffiliation('outcast', [{
  815. 'jid': args[0],
  816. 'reason': args[1]
  817. }]).then(
  818. () => this.model.occupants.fetchMembers(),
  819. (err) => this.onCommandError(err)
  820. );
  821. break;
  822. case 'deop':
  823. if (!this.verifyAffiliations(['admin', 'owner']) || !this.validateRoleChangeCommand(command, args)) {
  824. break;
  825. }
  826. this.modifyRole(
  827. this.model.get('jid'), args[0], 'participant', args[1],
  828. undefined, this.onCommandError.bind(this));
  829. break;
  830. case 'help':
  831. this.showHelpMessages([
  832. `<strong>/admin</strong>: ${__("Change user's affiliation to admin")}`,
  833. `<strong>/ban</strong>: ${__('Ban user from groupchat')}`,
  834. `<strong>/clear</strong>: ${__('Remove messages')}`,
  835. `<strong>/deop</strong>: ${__('Change user role to participant')}`,
  836. `<strong>/help</strong>: ${__('Show this menu')}`,
  837. `<strong>/kick</strong>: ${__('Kick user from groupchat')}`,
  838. `<strong>/me</strong>: ${__('Write in 3rd person')}`,
  839. `<strong>/member</strong>: ${__('Grant membership to a user')}`,
  840. `<strong>/mute</strong>: ${__("Remove user's ability to post messages")}`,
  841. `<strong>/nick</strong>: ${__('Change your nickname')}`,
  842. `<strong>/op</strong>: ${__('Grant moderator role to user')}`,
  843. `<strong>/owner</strong>: ${__('Grant ownership of this groupchat')}`,
  844. `<strong>/register</strong>: ${__("Register a nickname for this room")}`,
  845. `<strong>/revoke</strong>: ${__("Revoke user's membership")}`,
  846. `<strong>/subject</strong>: ${__('Set groupchat subject')}`,
  847. `<strong>/topic</strong>: ${__('Set groupchat subject (alias for /subject)')}`,
  848. `<strong>/voice</strong>: ${__('Allow muted user to post messages')}`
  849. ]);
  850. break;
  851. case 'kick':
  852. if (!this.verifyRoles(['moderator']) || !this.validateRoleChangeCommand(command, args)) {
  853. break;
  854. }
  855. this.modifyRole(
  856. this.model.get('jid'), args[0], 'none', args[1],
  857. undefined, this.onCommandError.bind(this));
  858. break;
  859. case 'mute':
  860. if (!this.verifyRoles(['moderator']) || !this.validateRoleChangeCommand(command, args)) {
  861. break;
  862. }
  863. this.modifyRole(
  864. this.model.get('jid'), args[0], 'visitor', args[1],
  865. undefined, this.onCommandError.bind(this));
  866. break;
  867. case 'member': {
  868. if (!this.verifyAffiliations(['admin', 'owner']) || !this.validateRoleChangeCommand(command, args)) {
  869. break;
  870. }
  871. const occupant = this.model.occupants.findWhere({'nick': args[0]}) ||
  872. this.model.occupants.findWhere({'jid': args[0]}),
  873. attrs = {
  874. 'jid': occupant.get('jid'),
  875. 'reason': args[1]
  876. };
  877. if (_converse.auto_register_muc_nickname) {
  878. attrs['nick'] = occupant.get('nick');
  879. }
  880. this.model.setAffiliation('member', [attrs])
  881. .then(() => this.model.occupants.fetchMembers())
  882. .catch(err => this.onCommandError(err));
  883. break;
  884. } case 'nick':
  885. if (!this.verifyRoles(['visitor', 'participant', 'moderator'])) {
  886. break;
  887. }
  888. _converse.connection.send($pres({
  889. from: _converse.connection.jid,
  890. to: this.model.getRoomJIDAndNick(match[2]),
  891. id: _converse.connection.getUniqueId()
  892. }).tree());
  893. break;
  894. case 'owner':
  895. if (!this.verifyAffiliations(['owner']) || !this.validateRoleChangeCommand(command, args)) {
  896. break;
  897. }
  898. this.model.setAffiliation('owner', [{
  899. 'jid': args[0],
  900. 'reason': args[1]
  901. }]).then(
  902. () => this.model.occupants.fetchMembers(),
  903. (err) => this.onCommandError(err)
  904. );
  905. break;
  906. case 'op':
  907. if (!this.verifyAffiliations(['admin', 'owner']) || !this.validateRoleChangeCommand(command, args)) {
  908. break;
  909. }
  910. this.modifyRole(
  911. this.model.get('jid'), args[0], 'moderator', args[1],
  912. undefined, this.onCommandError.bind(this));
  913. break;
  914. case 'register':
  915. if (args.length > 1) {
  916. this.showErrorMessage(__(`Error: invalid number of arguments`))
  917. } else {
  918. this.model.registerNickname().then(err_msg => {
  919. if (err_msg) this.showErrorMessage(err_msg)
  920. });
  921. }
  922. break;
  923. case 'revoke':
  924. if (!this.verifyAffiliations(['admin', 'owner']) || !this.validateRoleChangeCommand(command, args)) {
  925. break;
  926. }
  927. this.model.setAffiliation('none', [{
  928. 'jid': args[0],
  929. 'reason': args[1]
  930. }]).then(
  931. () => this.model.occupants.fetchMembers(),
  932. (err) => this.onCommandError(err)
  933. );
  934. break;
  935. case 'topic':
  936. case 'subject':
  937. // TODO: should be done via API call to _converse.api.rooms
  938. _converse.connection.send(
  939. $msg({
  940. to: this.model.get('jid'),
  941. from: _converse.connection.jid,
  942. type: "groupchat"
  943. }).c("subject", {xmlns: "jabber:client"}).t(match[2] || "").tree()
  944. );
  945. break;
  946. case 'voice':
  947. if (!this.verifyRoles(['moderator']) || !this.validateRoleChangeCommand(command, args)) {
  948. break;
  949. }
  950. this.modifyRole(
  951. this.model.get('jid'), args[0], 'participant', args[1],
  952. undefined, this.onCommandError.bind(this));
  953. break;
  954. default:
  955. return false;
  956. }
  957. return true;
  958. },
  959. registerHandlers () {
  960. /* Register presence and message handlers for this chat
  961. * groupchat
  962. */
  963. // XXX: Ideally this can be refactored out so that we don't
  964. // need to do stanza processing inside the views in this
  965. // module. See the comment in "onPresence" for more info.
  966. this.model.addHandler('presence', 'ChatRoomView.onPresence', this.onPresence.bind(this));
  967. // XXX instead of having a method showStatusMessages, we could instead
  968. // create message models in converse-muc.js and then give them views in this module.
  969. this.model.addHandler('message', 'ChatRoomView.showStatusMessages', this.showStatusMessages.bind(this));
  970. },
  971. onPresence (pres) {
  972. /* Handles all MUC presence stanzas.
  973. *
  974. * Parameters:
  975. * (XMLElement) pres: The stanza
  976. */
  977. // XXX: Current thinking is that excessive stanza
  978. // processing inside a view is a "code smell".
  979. // Instead stanza processing should happen inside the
  980. // models/collections.
  981. if (pres.getAttribute('type') === 'error') {
  982. this.showErrorMessageFromPresence(pres);
  983. } else {
  984. // Instead of doing it this way, we could perhaps rather
  985. // create StatusMessage objects inside the messages
  986. // Collection and then simply render those. Then stanza
  987. // processing is done on the model and rendering in the
  988. // view(s).
  989. this.showStatusMessages(pres);
  990. }
  991. },
  992. populateAndJoin () {
  993. this.model.occupants.fetchMembers();
  994. this.join();
  995. this.fetchMessages();
  996. },
  997. join (nick, password) {
  998. /* Join the groupchat.
  999. *
  1000. * Parameters:
  1001. * (String) nick: The user's nickname
  1002. * (String) password: Optional password, if required by
  1003. * the groupchat.
  1004. */
  1005. if (!nick && !this.model.get('nick')) {
  1006. this.checkForReservedNick();
  1007. return this;
  1008. }
  1009. this.model.join(nick, password);
  1010. return this;
  1011. },
  1012. renderConfigurationForm (stanza) {
  1013. /* Renders a form given an IQ stanza containing the current
  1014. * groupchat configuration.
  1015. *
  1016. * Returns a promise which resolves once the user has
  1017. * either submitted the form, or canceled it.
  1018. *
  1019. * Parameters:
  1020. * (XMLElement) stanza: The IQ stanza containing the groupchat
  1021. * config.
  1022. */
  1023. const container_el = this.el.querySelector('.chatroom-body');
  1024. _.each(container_el.querySelectorAll('.chatroom-form-container'), u.removeElement);
  1025. _.each(container_el.children, u.hideElement);
  1026. container_el.insertAdjacentHTML('beforeend', tpl_chatroom_form());
  1027. const form_el = container_el.querySelector('form.chatroom-form'),
  1028. fieldset_el = form_el.querySelector('fieldset'),
  1029. fields = stanza.querySelectorAll('field'),
  1030. title = _.get(stanza.querySelector('title'), 'textContent'),
  1031. instructions = _.get(stanza.querySelector('instructions'), 'textContent');
  1032. u.removeElement(fieldset_el.querySelector('span.spinner'));
  1033. fieldset_el.insertAdjacentHTML('beforeend', `<legend>${title}</legend>`);
  1034. if (instructions && instructions !== title) {
  1035. fieldset_el.insertAdjacentHTML('beforeend', `<p class="form-help">${instructions}</p>`);
  1036. }
  1037. _.each(fields, function (field) {
  1038. fieldset_el.insertAdjacentHTML('beforeend', u.xForm2webForm(field, stanza));
  1039. });
  1040. // Render save/cancel buttons
  1041. const last_fieldset_el = document.createElement('fieldset');
  1042. last_fieldset_el.insertAdjacentHTML(
  1043. 'beforeend',
  1044. `<input type="submit" class="btn btn-primary" value="${__('Save')}"/>`);
  1045. last_fieldset_el.insertAdjacentHTML(
  1046. 'beforeend',
  1047. `<input type="button" class="btn btn-secondary" value="${__('Cancel')}"/>`);
  1048. form_el.insertAdjacentElement('beforeend', last_fieldset_el);
  1049. last_fieldset_el.querySelector('input[type=button]').addEventListener('click', (ev) => {
  1050. ev.preventDefault();
  1051. this.closeForm();
  1052. });
  1053. form_el.addEventListener('submit',
  1054. (ev) => {
  1055. ev.preventDefault();
  1056. this.model.saveConfiguration(ev.target)
  1057. .then(() => this.model.refreshRoomFeatures());
  1058. this.closeForm();
  1059. },
  1060. false
  1061. );
  1062. },
  1063. closeForm () {
  1064. /* Remove the configuration form without submitting and
  1065. * return to the chat view.
  1066. */
  1067. u.removeElement(this.el.querySelector('.chatroom-form-container'));
  1068. this.renderAfterTransition();
  1069. },
  1070. getAndRenderConfigurationForm (ev) {
  1071. /* Start the process of configuring a groupchat, either by
  1072. * rendering a configuration form, or by auto-configuring
  1073. * based on the "roomconfig" data stored on the
  1074. * Backbone.Model.
  1075. *
  1076. * Stores the new configuration on the Backbone.Model once
  1077. * completed.
  1078. *
  1079. * Paremeters:
  1080. * (Event) ev: DOM event that might be passed in if this
  1081. * method is called due to a user action. In this
  1082. * case, auto-configure won't happen, regardless of
  1083. * the settings.
  1084. */
  1085. this.showSpinner();
  1086. this.model.fetchRoomConfiguration()
  1087. .then(this.renderConfigurationForm.bind(this))
  1088. .catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
  1089. },
  1090. submitNickname (ev) {
  1091. /* Get the nickname value from the form and then join the
  1092. * groupchat with it.
  1093. */
  1094. ev.preventDefault();
  1095. const nick_el = ev.target.nick;
  1096. const nick = nick_el.value;
  1097. if (!nick) {
  1098. nick_el.classList.add('error');
  1099. return;
  1100. }
  1101. else {
  1102. nick_el.classList.remove('error');
  1103. }
  1104. this.el.querySelector('.chatroom-form-container').outerHTML = tpl_spinner();
  1105. this.join(nick);
  1106. },
  1107. checkForReservedNick () {
  1108. /* User service-discovery to ask the XMPP server whether
  1109. * this user has a reserved nickname for this groupchat.
  1110. * If so, we'll use that, otherwise we render the nickname form.
  1111. */
  1112. this.showSpinner();
  1113. this.model.checkForReservedNick()
  1114. .then(this.onReservedNickFound.bind(this))
  1115. .catch(this.onReservedNickNotFound.bind(this));
  1116. },
  1117. onReservedNickFound (iq) {
  1118. if (this.model.get('nick')) {
  1119. this.join();
  1120. } else {
  1121. this.onReservedNickNotFound();
  1122. }
  1123. },
  1124. onReservedNickNotFound (message) {
  1125. const nick = this.model.getDefaultNick();
  1126. if (nick) {
  1127. this.join(nick);
  1128. } else {
  1129. this.renderNicknameForm(message);
  1130. }
  1131. },
  1132. onNicknameClash (presence) {
  1133. /* When the nickname is already taken, we either render a
  1134. * form for the user to choose a new nickname, or we
  1135. * try to make the nickname unique by adding an integer to
  1136. * it. So john will become john-2, and then john-3 and so on.
  1137. *
  1138. * Which option is take depends on the value of
  1139. * muc_nickname_from_jid.
  1140. */
  1141. if (_converse.muc_nickname_from_jid) {
  1142. const nick = presence.getAttribute('from').split('/')[1];
  1143. if (nick === this.model.getDefaultNick()) {
  1144. this.join(nick + '-2');
  1145. } else {
  1146. const del= nick.lastIndexOf("-");
  1147. const num = nick.substring(del+1, nick.length);
  1148. this.join(nick.substring(0, del+1) + String(Number(num)+1));
  1149. }
  1150. } else {
  1151. this.renderNicknameForm(
  1152. __("The nickname you chose is reserved or "+
  1153. "currently in use, please choose a different one.")
  1154. );
  1155. }
  1156. },
  1157. hideChatRoomContents () {
  1158. const container_el = this.el.querySelector('.chatroom-body');
  1159. if (!_.isNull(container_el)) {
  1160. _.each(container_el.children, (child) => { child.classList.add('hidden'); });
  1161. }
  1162. },
  1163. renderNicknameForm (message) {
  1164. /* Render a form which allows the user to choose their
  1165. * nickname.
  1166. */
  1167. this.hideChatRoomContents();
  1168. _.each(this.el.querySelectorAll('span.centered.spinner'), u.removeElement);
  1169. if (!_.isString(message)) {
  1170. message = '';
  1171. }
  1172. const container_el = this.el.querySelector('.chatroom-body');
  1173. container_el.insertAdjacentHTML(
  1174. 'beforeend',
  1175. tpl_chatroom_nickname_form({
  1176. heading: __('Please choose your nickname'),
  1177. label_nickname: __('Nickname'),
  1178. label_join: __('Enter groupchat'),
  1179. validation_message: message
  1180. }));
  1181. this.model.save('connection_status', converse.ROOMSTATUS.NICKNAME_REQUIRED);
  1182. const form_el = this.el.querySelector('.chatroom-form');
  1183. form_el.addEventListener('submit', this.submitNickname.bind(this), false);
  1184. },
  1185. submitPassword (ev) {
  1186. ev.preventDefault();
  1187. const password = this.el.querySelector('.chatroom-form input[type=password]').value;
  1188. this.showSpinner();
  1189. this.join(this.model.get('nick'), password);
  1190. },
  1191. renderPasswordForm () {
  1192. const container_el = this.el.querySelector('.chatroom-body');
  1193. _.each(container_el.children, u.hideElement);
  1194. _.each(this.el.querySelectorAll('.spinner'), u.removeElement);
  1195. _.each(this.el.querySelectorAll('.chatroom-form-container'), u.removeElement);
  1196. container_el.insertAdjacentHTML('beforeend',
  1197. tpl_chatroom_password_form({
  1198. 'heading': __('This groupchat requires a password'),
  1199. 'label_password': __('Password: '),
  1200. 'label_submit': __('Submit')
  1201. }));
  1202. this.model.save('connection_status', converse.ROOMSTATUS.PASSWORD_REQUIRED);
  1203. this.el.querySelector('.chatroom-form')
  1204. .addEventListener('submit', ev => this.submitPassword(ev), false);
  1205. },
  1206. showDisconnectMessages (msgs) {
  1207. if (_.isString(msgs)) {
  1208. msgs = [msgs];
  1209. }
  1210. u.hideElement(this.el.querySelector('.chat-area'));
  1211. u.hideElement(this.el.querySelector('.occupants'));
  1212. _.each(this.el.querySelectorAll('.spinner'), u.removeElement);
  1213. const container = this.el.querySelector('.disconnect-container');
  1214. container.innerHTML = tpl_chatroom_disconnect({
  1215. '_': _,
  1216. 'disconnect_messages': msgs
  1217. })
  1218. u.showElement(container);
  1219. },
  1220. getMessageFromStatus (stat, stanza, is_self) {
  1221. /* Parameters:
  1222. * (XMLElement) stat: A <status> element.
  1223. * (Boolean) is_self: Whether the element refers to the
  1224. * current user.
  1225. * (XMLElement) stanza: The original stanza received.
  1226. */
  1227. const code = stat.getAttribute('code');
  1228. if (code === '110' || (code === '100' && !is_self)) { return; }
  1229. if (code in _converse.muc.info_messages) {
  1230. return _converse.muc.info_messages[code];
  1231. }
  1232. let nick;
  1233. if (!is_self) {
  1234. if (code in _converse.muc.action_info_messages) {
  1235. nick = Strophe.getResourceFromJid(stanza.getAttribute('from'));
  1236. return __(_converse.muc.action_info_messages[code], nick);
  1237. }
  1238. } else if (code in _converse.muc.new_nickname_messages) {
  1239. if (is_self && code === "210") {
  1240. nick = Strophe.getResourceFromJid(stanza.getAttribute('from'));
  1241. } else if (is_self && code === "303") {
  1242. nick = stanza.querySelector('x item').getAttribute('nick');
  1243. }
  1244. return __(_converse.muc.new_nickname_messages[code], nick);
  1245. }
  1246. return;
  1247. },
  1248. parseXUserElement (x, stanza, is_self) {
  1249. /* Parse the passed-in <x xmlns='http://jabber.org/protocol/muc#user'>
  1250. * element and construct a map containing relevant
  1251. * information.
  1252. */
  1253. // 1. Get notification messages based on the <status> elements.
  1254. const statuses = x.querySelectorAll('status');
  1255. const mapper = _.partial(this.getMessageFromStatus, _, stanza, is_self);
  1256. const notification = {};
  1257. const messages = _.reject(_.map(statuses, mapper), _.isUndefined);
  1258. if (messages.length) {
  1259. notification.messages = messages;
  1260. }
  1261. // 2. Get disconnection messages based on the <status> elements
  1262. const codes = _.invokeMap(statuses, Element.prototype.getAttribute, 'code');
  1263. const disconnection_codes = _.intersection(codes, _.keys(_converse.muc.disconnect_messages));
  1264. const disconnected = is_self && disconnection_codes.length > 0;
  1265. if (disconnected) {
  1266. notification.disconnected = true;
  1267. notification.disconnection_message = _converse.muc.disconnect_messages[disconnection_codes[0]];
  1268. }
  1269. // 3. Find the reason and actor from the <item> element
  1270. const item = x.querySelector('item');
  1271. // By using querySelector above, we assume here there is
  1272. // one <item> per <x xmlns='http://jabber.org/protocol/muc#user'>
  1273. // element. This appears to be a safe assumption, since
  1274. // each <x/> element pertains to a single user.
  1275. if (!_.isNull(item)) {
  1276. const reason = item.querySelector('reason');
  1277. if (reason) {
  1278. notification.reason = reason ? reason.textContent : undefined;
  1279. }
  1280. const actor = item.querySelector('actor');
  1281. if (actor) {
  1282. notification.actor = actor ? actor.getAttribute('nick') : undefined;
  1283. }
  1284. }
  1285. return notification;
  1286. },
  1287. showNotificationsforUser (notification) {
  1288. /* Given the notification object generated by
  1289. * parseXUserElement, display any relevant messages and
  1290. * information to the user.
  1291. */
  1292. if (notification.disconnected) {
  1293. const messages = [];
  1294. messages.push(notification.disconnection_message);
  1295. if (notification.actor) {
  1296. messages.push(__('This action was done by %1$s.', notification.actor));
  1297. }
  1298. if (notification.reason) {
  1299. messages.push(__('The reason given is: "%1$s".', notification.reason));
  1300. }
  1301. this.showDisconnectMessages(messages);
  1302. this.model.save('connection_status', converse.ROOMSTATUS.DISCONNECTED);
  1303. return;
  1304. }
  1305. _.each(notification.messages, (message) => {
  1306. this.content.insertAdjacentHTML(
  1307. 'beforeend',
  1308. tpl_info({
  1309. 'data': '',
  1310. 'isodate': moment().format(),
  1311. 'extra_classes': 'chat-event',
  1312. 'message': message
  1313. }));
  1314. });
  1315. if (notification.reason) {
  1316. this.showChatEvent(__('The reason given is: "%1$s".', notification.reason));
  1317. }
  1318. if (_.get(notification.messages, 'length')) {
  1319. this.scrollDown();
  1320. }
  1321. },
  1322. showJoinOrLeaveNotification (occupant) {
  1323. if (!occupant.isMember() || _.includes(occupant.get('states'), '303')) {
  1324. return;
  1325. }
  1326. if (occupant.get('show') === 'offline') {
  1327. this.showLeaveNotification(occupant);
  1328. } else if (occupant.get('show') === 'online') {
  1329. this.showJoinNotification(occupant);
  1330. }
  1331. },
  1332. showJoinNotification (occupant) {
  1333. if (this.model.get('connection_status') !== converse.ROOMSTATUS.ENTERED) {
  1334. return;
  1335. }
  1336. const nick = occupant.get('nick'),
  1337. stat = occupant.get('status'),
  1338. last_el = this.content.lastElementChild;
  1339. if (_.includes(_.get(last_el, 'classList', []), 'chat-info') &&
  1340. _.get(last_el, 'dataset', {}).leave === `"${nick}"`) {
  1341. last_el.outerHTML =
  1342. tpl_info({
  1343. 'data': `data-leavejoin="${nick}"`,
  1344. 'isodate': moment().format(),
  1345. 'extra_classes': 'chat-event',
  1346. 'message': __('%1$s has left and re-entered the groupchat', nick)
  1347. });
  1348. const el = this.content.lastElementChild;
  1349. setTimeout(() => u.addClass('fade-out', el), 5000);
  1350. setTimeout(() => el.parentElement && el.parentElement.removeChild(el), 5250);
  1351. } else {
  1352. let message;
  1353. if (_.isNil(stat)) {
  1354. message = __('%1$s has entered the groupchat', nick);
  1355. } else {
  1356. message = __('%1$s has entered the groupchat. "%2$s"', nick, stat);
  1357. }
  1358. const data = {
  1359. 'data': `data-join="${nick}"`,
  1360. 'isodate': moment().format(),
  1361. 'extra_classes': 'chat-event',
  1362. 'message': message
  1363. };
  1364. if (_.includes(_.get(last_el, 'classList', []), 'chat-info') &&
  1365. _.get(last_el, 'dataset', {}).joinleave === `"${nick}"`) {
  1366. last_el.outerHTML = tpl_info(data);
  1367. } else {
  1368. const el = u.stringToElement(tpl_info(data));
  1369. this.content.insertAdjacentElement('beforeend', el);
  1370. this.insertDayIndicator(el);
  1371. }
  1372. }
  1373. this.scrollDown();
  1374. },
  1375. showLeaveNotification (occupant) {
  1376. if (_.includes(occupant.get('states'), '303') || _.includes(occupant.get('states'), '307')) {
  1377. return;
  1378. }
  1379. const nick = occupant.get('nick'),
  1380. stat = occupant.get('status'),
  1381. last_el = this.content.lastElementChild;
  1382. if (last_el &&
  1383. _.includes(_.get(last_el, 'classList', []), 'chat-info') &&
  1384. moment(last_el.getAttribute('data-isodate')).isSame(new Date(), "day") &&
  1385. _.get(last_el, 'dataset', {}).join === `"${nick}"`) {
  1386. let message;
  1387. if (_.isNil(stat)) {
  1388. message = __('%1$s has entered and left the groupchat', nick);
  1389. } else {
  1390. message = __('%1$s has entered and left the groupchat. "%2$s"', nick, stat);
  1391. }
  1392. last_el.outerHTML =
  1393. tpl_info({
  1394. 'data': `data-joinleave="${nick}"`,
  1395. 'isodate': moment().format(),
  1396. 'extra_classes': 'chat-event',
  1397. 'message': message
  1398. });
  1399. const el = this.content.lastElementChild;
  1400. setTimeout(() => u.addClass('fade-out', el), 5000);
  1401. setTimeout(() => el.parentElement && el.parentElement.removeChild(el), 5250);
  1402. } else {
  1403. let message;
  1404. if (_.isNil(stat)) {
  1405. message = __('%1$s has left the groupchat', nick);
  1406. } else {
  1407. message = __('%1$s has left the groupchat. "%2$s"', nick, stat);
  1408. }
  1409. const data = {
  1410. 'message': message,
  1411. 'isodate': moment().format(),
  1412. 'extra_classes': 'chat-event',
  1413. 'data': `data-leave="${nick}"`
  1414. }
  1415. if (last_el &&
  1416. _.includes(_.get(last_el, 'classList', []), 'chat-info') &&
  1417. _.get(last_el, 'dataset', {}).leavejoin === `"${nick}"`) {
  1418. last_el.outerHTML = tpl_info(data);
  1419. } else {
  1420. const el = u.stringToElement(tpl_info(data));
  1421. this.content.insertAdjacentElement('beforeend', el);
  1422. this.insertDayIndicator(el);
  1423. }
  1424. }
  1425. this.scrollDown();
  1426. },
  1427. showStatusMessages (stanza) {
  1428. /* Check for status codes and communicate their purpose to the user.
  1429. * See: http://xmpp.org/registrar/mucstatus.html
  1430. *
  1431. * Parameters:
  1432. * (XMLElement) stanza: The message or presence stanza
  1433. * containing the status codes.
  1434. */
  1435. const elements = sizzle(`x[xmlns="${Strophe.NS.MUC_USER}"]`, stanza);
  1436. const is_self = stanza.querySelectorAll("status[code='110']").length;
  1437. const iteratee = _.partial(this.parseXUserElement.bind(this), _, stanza, is_self);
  1438. const notifications = _.reject(_.map(elements, iteratee), _.isEmpty);
  1439. _.each(notifications, this.showNotificationsforUser.bind(this));
  1440. },
  1441. showErrorMessageFromPresence (presence) {
  1442. // We didn't enter the groupchat, so we must remove it from the MUC add-on
  1443. const error = presence.querySelector('error');
  1444. if (error.getAttribute('type') === 'auth') {
  1445. if (!_.isNull(error.querySelector('not-authorized'))) {
  1446. this.renderPasswordForm();
  1447. } else if (!_.isNull(error.querySelector('registration-required'))) {
  1448. this.showDisconnectMessages(__('You are not on the member list of this groupchat.'));
  1449. } else if (!_.isNull(error.querySelector('forbidden'))) {
  1450. this.showDisconnectMessages(__('You have been banned from this groupchat.'));
  1451. }
  1452. } else if (error.getAttribute('type') === 'modify') {
  1453. if (!_.isNull(error.querySelector('jid-malformed'))) {
  1454. this.showDisconnectMessages(__('No nickname was specified.'));
  1455. }
  1456. } else if (error.getAttribute('type') === 'cancel') {
  1457. if (!_.isNull(error.querySelector('not-allowed'))) {
  1458. this.showDisconnectMessages(__('You are not allowed to create new groupchats.'));
  1459. } else if (!_.isNull(error.querySelector('not-acceptable'))) {
  1460. this.showDisconnectMessages(__("Your nickname doesn't conform to this groupchat's policies."));
  1461. } else if (!_.isNull(error.querySelector('conflict'))) {
  1462. this.onNicknameClash(presence);
  1463. } else if (!_.isNull(error.querySelector('item-not-found'))) {
  1464. this.showDisconnectMessages(__("This groupchat does not (yet) exist."));
  1465. } else if (!_.isNull(error.querySelector('service-unavailable'))) {
  1466. this.showDisconnectMessages(__("This groupchat has reached its maximum number of participants."));
  1467. } else if (!_.isNull(error.querySelector('remote-server-not-found'))) {
  1468. const messages = [__("Remote server not found")];
  1469. const reason = _.get(error.querySelector('text'), 'textContent');
  1470. if (reason) {
  1471. messages.push(__('The explanation given is: "%1$s".', reason));
  1472. }
  1473. this.showDisconnectMessages(messages);
  1474. }
  1475. }
  1476. },
  1477. renderAfterTransition () {
  1478. /* Rerender the groupchat after some kind of transition. For
  1479. * example after the spinner has been removed or after a
  1480. * form has been submitted and removed.
  1481. */
  1482. if (this.model.get('connection_status') == converse.ROOMSTATUS.NICKNAME_REQUIRED) {
  1483. this.renderNicknameForm();
  1484. } else if (this.model.get('connection_status') == converse.ROOMSTATUS.PASSWORD_REQUIRED) {
  1485. this.renderPasswordForm();
  1486. } else {
  1487. this.el.querySelector('.chat-area').classList.remove('hidden');
  1488. this.setOccupantsVisibility();
  1489. this.scrollDown();
  1490. }
  1491. },
  1492. showSpinner () {
  1493. u.removeElement(this.el.querySelector('.spinner'));
  1494. const container_el = this.el.querySelector('.chatroom-body');
  1495. const children = Array.prototype.slice.call(container_el.children, 0);
  1496. container_el.insertAdjacentHTML('afterbegin', tpl_spinner());
  1497. _.each(children, u.hideElement);
  1498. },
  1499. hideSpinner () {
  1500. /* Check if the spinner is being shown and if so, hide it.
  1501. * Also make sure then that the chat area and occupants
  1502. * list are both visible.
  1503. */
  1504. const spinner = this.el.querySelector('.spinner');
  1505. if (!_.isNull(spinner)) {
  1506. u.removeElement(spinner);
  1507. this.renderAfterTransition();
  1508. }
  1509. return this;
  1510. },
  1511. setChatRoomSubject () {
  1512. // For translators: the %1$s and %2$s parts will get
  1513. // replaced by the user and topic text respectively
  1514. // Example: Topic set by JC Brand to: Hello World!
  1515. const subject = this.model.get('subject'),
  1516. message = subject.text ? __('Topic set by %1$s', subject.author) :
  1517. __('Topic cleared by %1$s', subject.author),
  1518. date = moment().format();
  1519. this.content.insertAdjacentHTML(
  1520. 'beforeend',
  1521. tpl_info({
  1522. 'data': '',
  1523. 'isodate': date,
  1524. 'extra_classes': 'chat-event',
  1525. 'message': message
  1526. }));
  1527. if (subject.text) {
  1528. this.content.insertAdjacentHTML(
  1529. 'beforeend',
  1530. tpl_info({
  1531. 'data': '',
  1532. 'isodate': date,
  1533. 'extra_classes': 'chat-topic',
  1534. 'message': u.addHyperlinks(xss.filterXSS(_.get(this.model.get('subject'), 'text'), {'whiteList': {}})),
  1535. 'render_message': true
  1536. }));
  1537. }
  1538. this.scrollDown();
  1539. }
  1540. });
  1541. _converse.RoomsPanel = Backbone.NativeView.extend({
  1542. /* Backbone.NativeView which renders MUC section of the control box.
  1543. */
  1544. tagName: 'div',
  1545. className: 'controlbox-section',
  1546. id: 'chatrooms',
  1547. events: {
  1548. 'click a.chatbox-btn.show-add-muc-modal': 'showAddRoomModal',
  1549. 'click a.chatbox-btn.show-list-muc-modal': 'showListRoomsModal'
  1550. },
  1551. render () {
  1552. this.el.innerHTML = tpl_room_panel({
  1553. 'heading_chatrooms': __('Groupchats'),
  1554. 'title_new_room': __('Add a new groupchat'),
  1555. 'title_list_rooms': __('Query for groupchats')
  1556. });
  1557. return this;
  1558. },
  1559. showAddRoomModal (ev) {
  1560. if (_.isUndefined(this.add_room_modal)) {
  1561. this.add_room_modal = new _converse.AddChatRoomModal({'model': this.model});
  1562. }
  1563. this.add_room_modal.show(ev);
  1564. },
  1565. showListRoomsModal(ev) {
  1566. if (_.isUndefined(this.list_rooms_modal)) {
  1567. this.list_rooms_modal = new _converse.ListChatRoomsModal({'model': this.model});
  1568. }
  1569. this.list_rooms_modal.show(ev);
  1570. }
  1571. });
  1572. _converse.ChatRoomOccupantView = Backbone.VDOMView.extend({
  1573. tagName: 'li',
  1574. initialize () {
  1575. this.model.on('change', this.render, this);
  1576. },
  1577. toHTML () {
  1578. const show = this.model.get('show');
  1579. return tpl_occupant(
  1580. _.extend(
  1581. { '_': _, // XXX Normally this should already be included,
  1582. // but with the current webpack build,
  1583. // we only get a subset of the _ methods.
  1584. 'jid': '',
  1585. 'show': show,
  1586. 'hint_show': _converse.PRETTY_CHAT_STATUS[show],
  1587. 'hint_occupant': __('Click to mention %1$s in your message.', this.model.get('nick')),
  1588. 'desc_moderator': __('This user is a moderator.'),
  1589. 'desc_participant': __('This user can send messages in this groupchat.'),
  1590. 'desc_visitor': __('This user can NOT send messages in this groupchat.'),
  1591. 'label_moderator': __('Moderator'),
  1592. 'label_visitor': __('Visitor'),
  1593. 'label_owner': __('Owner'),
  1594. 'label_member': __('Member'),
  1595. 'label_admin': __('Admin')
  1596. }, this.model.toJSON())
  1597. );
  1598. },
  1599. destroy () {
  1600. this.el.parentElement.removeChild(this.el);
  1601. }
  1602. });
  1603. _converse.ChatRoomOccupantsView = Backbone.OrderedListView.extend({
  1604. tagName: 'div',
  1605. className: 'occupants col-md-3 col-4',
  1606. listItems: 'model',
  1607. sortEvent: 'change:role',
  1608. listSelector: '.occupant-list',
  1609. ItemView: _converse.ChatRoomOccupantView,
  1610. initialize () {
  1611. Backbone.OrderedListView.prototype.initialize.apply(this, arguments);
  1612. this.chatroomview = this.model.chatroomview;
  1613. this.chatroomview.model.on('change:open', this.renderInviteWidget, this);
  1614. this.chatroomview.model.on('change:affiliation', this.renderInviteWidget, this);
  1615. this.chatroomview.model.on('change:hidden', this.onFeatureChanged, this);
  1616. this.chatroomview.model.on('change:mam_enabled', this.onFeatureChanged, this);
  1617. this.chatroomview.model.on('change:membersonly', this.onFeatureChanged, this);
  1618. this.chatroomview.model.on('change:moderated', this.onFeatureChanged, this);
  1619. this.chatroomview.model.on('change:nonanonymous', this.onFeatureChanged, this);
  1620. this.chatroomview.model.on('change:open', this.onFeatureChanged, this);
  1621. this.chatroomview.model.on('change:passwordprotected', this.onFeatureChanged, this);
  1622. this.chatroomview.model.on('change:persistent', this.onFeatureChanged, this);
  1623. this.chatroomview.model.on('change:publicroom', this.onFeatureChanged, this);
  1624. this.chatroomview.model.on('change:semianonymous', this.onFeatureChanged, this);
  1625. this.chatroomview.model.on('change:temporary', this.onFeatureChanged, this);
  1626. this.chatroomview.model.on('change:unmoderated', this.onFeatureChanged, this);
  1627. this.chatroomview.model.on('change:unsecured', this.onFeatureChanged, this);
  1628. this.render();
  1629. this.model.fetch({
  1630. 'add': true,
  1631. 'silent': true,
  1632. 'success': this.sortAndPositionAllItems.bind(this)
  1633. });
  1634. },
  1635. render () {
  1636. this.el.innerHTML = tpl_chatroom_sidebar(
  1637. _.extend(this.chatroomview.model.toJSON(), {
  1638. 'allow_muc_invitations': _converse.allow_muc_invitations,
  1639. 'label_occupants': __('Participants')
  1640. })
  1641. );
  1642. if (_converse.allow_muc_invitations) {
  1643. _converse.api.waitUntil('rosterContactsFetched').then(
  1644. this.renderInviteWidget.bind(this)
  1645. );
  1646. }
  1647. return this.renderRoomFeatures();
  1648. },
  1649. renderInviteWidget () {
  1650. const form = this.el.querySelector('form.room-invite');
  1651. if (this.shouldInviteWidgetBeShown()) {
  1652. if (_.isNull(form)) {
  1653. const heading = this.el.querySelector('.occupants-heading');
  1654. heading.insertAdjacentHTML(
  1655. 'afterend',
  1656. tpl_chatroom_invite({
  1657. 'error_message': null,
  1658. 'label_invitation': __('Invite'),
  1659. })
  1660. );
  1661. this.initInviteWidget();
  1662. }
  1663. } else if (!_.isNull(form)) {
  1664. form.remove();
  1665. }
  1666. return this;
  1667. },
  1668. renderRoomFeatures () {
  1669. const picks = _.pick(this.chatroomview.model.attributes, converse.ROOM_FEATURES),
  1670. iteratee = (a, v) => a || v,
  1671. el = this.el.querySelector('.chatroom-features');
  1672. el.innerHTML = tpl_chatroom_features(
  1673. _.extend(this.chatroomview.model.toJSON(), {
  1674. '__': __,
  1675. 'has_features': _.reduce(_.values(picks), iteratee)
  1676. }));
  1677. this.setOccupantsHeight();
  1678. return this;
  1679. },
  1680. onFeatureChanged (model) {
  1681. /* When a feature has been changed, it's logical opposite
  1682. * must be set to the opposite value.
  1683. *
  1684. * So for example, if "temporary" was set to "false", then
  1685. * "persistent" will be set to "true" in this method.
  1686. *
  1687. * Additionally a debounced render method is called to make
  1688. * sure the features widget gets updated.
  1689. */
  1690. if (_.isUndefined(this.debouncedRenderRoomFeatures)) {
  1691. this.debouncedRenderRoomFeatures = _.debounce(
  1692. this.renderRoomFeatures, 100, {'leading': false}
  1693. );
  1694. }
  1695. const changed_features = {};
  1696. _.each(_.keys(model.changed), function (k) {
  1697. if (!_.isNil(ROOM_FEATURES_MAP[k])) {
  1698. changed_features[ROOM_FEATURES_MAP[k]] = !model.changed[k];
  1699. }
  1700. });
  1701. this.chatroomview.model.save(changed_features, {'silent': true});
  1702. this.debouncedRenderRoomFeatures();
  1703. },
  1704. setOccupantsHeight () {
  1705. const el = this.el.querySelector('.chatroom-features');
  1706. this.el.querySelector('.occupant-list').style.cssText =
  1707. `height: calc(100% - ${el.offsetHeight}px - 5em);`;
  1708. },
  1709. promptForInvite (suggestion) {
  1710. const reason = prompt(
  1711. __('You are about to invite %1$s to the groupchat "%2$s". '+
  1712. 'You may optionally include a message, explaining the reason for the invitation.',
  1713. suggestion.text.label, this.model.get('id'))
  1714. );
  1715. if (reason !== null) {
  1716. this.chatroomview.model.directInvite(suggestion.text.value, reason);
  1717. }
  1718. const form = suggestion.target.form,
  1719. error = form.querySelector('.pure-form-message.error');
  1720. if (!_.isNull(error)) {
  1721. error.parentNode.removeChild(error);
  1722. }
  1723. suggestion.target.value = '';
  1724. },
  1725. inviteFormSubmitted (evt) {
  1726. evt.preventDefault();
  1727. const el = evt.target.querySelector('input.invited-contact'),
  1728. jid = el.value;
  1729. if (!jid || _.compact(jid.split('@')).length < 2) {
  1730. evt.target.outerHTML = tpl_chatroom_invite({
  1731. 'error_message': __('Please enter a valid XMPP username'),
  1732. 'label_invitation': __('Invite'),
  1733. });
  1734. this.initInviteWidget();
  1735. return;
  1736. }
  1737. this.promptForInvite({
  1738. 'target': el,
  1739. 'text': {
  1740. 'label': jid,
  1741. 'value': jid
  1742. }});
  1743. },
  1744. shouldInviteWidgetBeShown () {
  1745. return _converse.allow_muc_invitations &&
  1746. (this.chatroomview.model.get('open') ||
  1747. this.chatroomview.model.get('affiliation') === "owner"
  1748. );
  1749. },
  1750. initInviteWidget () {
  1751. const form = this.el.querySelector('form.room-invite');
  1752. if (_.isNull(form)) {
  1753. return;
  1754. }
  1755. form.addEventListener('submit', this.inviteFormSubmitted.bind(this), false);
  1756. const el = this.el.querySelector('input.invited-contact');
  1757. const list = _converse.roster.map(function (item) {
  1758. const label = item.get('fullname') || item.get('jid');
  1759. return {'label': label, 'value':item.get('jid')};
  1760. });
  1761. const awesomplete = new Awesomplete(el, {
  1762. 'minChars': 1,
  1763. 'list': list
  1764. });
  1765. el.addEventListener('awesomplete-selectcomplete',
  1766. this.promptForInvite.bind(this));
  1767. }
  1768. });
  1769. function setMUCDomain (domain, controlboxview) {
  1770. _converse.muc_domain = domain;
  1771. controlboxview.roomspanel.model.save('muc_domain', Strophe.getDomainFromJid(domain));
  1772. }
  1773. function setMUCDomainFromDisco (controlboxview) {
  1774. /* Check whether service discovery for the user's domain
  1775. * returned MUC information and use that to automatically
  1776. * set the MUC domain in the "Add groupchat" modal.
  1777. */
  1778. function featureAdded (feature) {
  1779. if (!feature) { return; }
  1780. if (feature.get('var') === Strophe.NS.MUC) {
  1781. feature.entity.getIdentity('conference', 'text').then(identity => {
  1782. if (identity) {
  1783. setMUCDomain(feature.get('from'), controlboxview);
  1784. }
  1785. });
  1786. }
  1787. }
  1788. _converse.api.waitUntil('discoInitialized').then(() => {
  1789. _converse.api.listen.on('serviceDiscovered', featureAdded);
  1790. // Features could have been added before the controlbox was
  1791. // initialized. We're only interested in MUC
  1792. _converse.disco_entities.each(entity => featureAdded(entity.features.findWhere({'var': Strophe.NS.MUC })));
  1793. }).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
  1794. }
  1795. function fetchAndSetMUCDomain (controlboxview) {
  1796. if (controlboxview.model.get('connected')) {
  1797. if (!controlboxview.roomspanel.model.get('muc_domain')) {
  1798. if (_.isUndefined(_converse.muc_domain)) {
  1799. setMUCDomainFromDisco(controlboxview);
  1800. } else {
  1801. setMUCDomain(_converse.muc_domain, controlboxview);
  1802. }
  1803. }
  1804. }
  1805. }
  1806. /************************ BEGIN Event Handlers ************************/
  1807. _converse.on('chatBoxViewsInitialized', () => {
  1808. const that = _converse.chatboxviews;
  1809. _converse.chatboxes.on('add', item => {
  1810. if (!that.get(item.get('id')) && item.get('type') === _converse.CHATROOMS_TYPE) {
  1811. return that.add(item.get('id'), new _converse.ChatRoomView({'model': item}));
  1812. }
  1813. });
  1814. });
  1815. _converse.on('controlboxInitialized', (view) => {
  1816. if (!_converse.allow_muc) {
  1817. return;
  1818. }
  1819. fetchAndSetMUCDomain(view);
  1820. view.model.on('change:connected', _.partial(fetchAndSetMUCDomain, view));
  1821. });
  1822. function reconnectToChatRooms () {
  1823. /* Upon a reconnection event from converse, join again
  1824. * all the open groupchats.
  1825. */
  1826. _converse.chatboxviews.each(function (view) {
  1827. if (view.model.get('type') === _converse.CHATROOMS_TYPE) {
  1828. view.model.save('connection_status', converse.ROOMSTATUS.DISCONNECTED);
  1829. view.model.registerHandlers();
  1830. view.populateAndJoin();
  1831. }
  1832. });
  1833. }
  1834. _converse.on('reconnected', reconnectToChatRooms);
  1835. /************************ END Event Handlers ************************/
  1836. /************************ BEGIN API ************************/
  1837. _.extend(_converse.api, {
  1838. /**
  1839. * The "roomviews" namespace groups methods relevant to chatroom
  1840. * (aka groupchats) views.
  1841. *
  1842. * @namespace _converse.api.roomviews
  1843. * @memberOf _converse.api
  1844. */
  1845. 'roomviews': {
  1846. /**
  1847. * Lets you close open chatrooms.
  1848. *
  1849. * You can call this method without any arguments to close
  1850. * all open chatrooms, or you can specify a single JID or
  1851. * an array of JIDs.
  1852. *
  1853. * @method _converse.api.roomviews.close
  1854. * @param {(String[]|String)} jids The JID or array of JIDs of the chatroom(s)
  1855. */
  1856. 'close' (jids) {
  1857. if (_.isUndefined(jids)) {
  1858. _converse.chatboxviews.each(function (view) {
  1859. if (view.is_chatroom && view.model) {
  1860. view.close();
  1861. }
  1862. });
  1863. } else if (_.isString(jids)) {
  1864. const view = _converse.chatboxviews.get(jids);
  1865. if (view) { view.close(); }
  1866. } else {
  1867. _.each(jids, function (jid) {
  1868. const view = _converse.chatboxviews.get(jid);
  1869. if (view) { view.close(); }
  1870. });
  1871. }
  1872. }
  1873. }
  1874. });
  1875. }
  1876. });
  1877. }));