converse.js 121 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668
  1. /*!
  2. * Converse.js (Web-based XMPP instant messaging client)
  3. * http://conversejs.org
  4. *
  5. * Copyright (c) 2012, Jan-Carel Brand <jc@opkode.com>
  6. * Dual licensed under the MIT and GPL Licenses
  7. */
  8. // AMD/global registrations
  9. (function (root, factory) {
  10. if (console===undefined || console.log===undefined) {
  11. console = { log: function () {}, error: function () {} };
  12. }
  13. if (typeof define === 'function' && define.amd) {
  14. define("converse", [
  15. "locales",
  16. "localstorage",
  17. "tinysort",
  18. "sjcl",
  19. "strophe",
  20. "strophe.muc",
  21. "strophe.roster",
  22. "strophe.vcard",
  23. "strophe.disco"
  24. ], function() {
  25. // Use Mustache style syntax for variable interpolation
  26. _.templateSettings = {
  27. evaluate : /\{\[([\s\S]+?)\]\}/g,
  28. interpolate : /\{\{([\s\S]+?)\}\}/g
  29. };
  30. return factory(jQuery, _, console);
  31. }
  32. );
  33. } else {
  34. // Browser globals
  35. _.templateSettings = {
  36. evaluate : /\{\[([\s\S]+?)\]\}/g,
  37. interpolate : /\{\{([\s\S]+?)\}\}/g
  38. };
  39. root.converse = factory(jQuery, _, console || {log: function(){}});
  40. }
  41. }(this, function ($, _, console) {
  42. var converse = {};
  43. converse.initialize = function (settings) {
  44. // Default values
  45. this.animate = true;
  46. this.auto_list_rooms = false;
  47. this.auto_subscribe = false;
  48. this.bosh_service_url = ''; // The BOSH connection manager URL. Required if you are not prebinding.
  49. this.hide_muc_server = false;
  50. this.i18n = locales.en;
  51. this.prebind = false;
  52. this.show_controlbox_by_default = false;
  53. this.xhr_user_search = false;
  54. _.extend(this, settings);
  55. var __ = function (str) {
  56. var t = converse.i18n.translate(str);
  57. if (arguments.length>1) {
  58. return t.fetch.apply(t, [].slice.call(arguments,1));
  59. } else {
  60. return t.fetch();
  61. }
  62. };
  63. this.msg_counter = 0;
  64. this.autoLink = function (text) {
  65. // Convert URLs into hyperlinks
  66. var re = /((http|https|ftp):\/\/[\w?=&.\/\-;#~%\-]+(?![\w\s?&.\/;#~%"=\-]*>))/g;
  67. return text.replace(re, '<a target="_blank" href="$1">$1</a>');
  68. };
  69. this.toISOString = function (date) {
  70. var pad;
  71. if (typeof date.toISOString !== 'undefined') {
  72. return date.toISOString();
  73. } else {
  74. // IE <= 8 Doesn't have toISOStringMethod
  75. pad = function (num) {
  76. return (num < 10) ? '0' + num : '' + num;
  77. };
  78. return date.getUTCFullYear() + '-' +
  79. pad(date.getUTCMonth() + 1) + '-' +
  80. pad(date.getUTCDate()) + 'T' +
  81. pad(date.getUTCHours()) + ':' +
  82. pad(date.getUTCMinutes()) + ':' +
  83. pad(date.getUTCSeconds()) + '.000Z';
  84. }
  85. };
  86. this.parseISO8601 = function (datestr) {
  87. /* Parses string formatted as 2013-02-14T11:27:08.268Z to a Date obj.
  88. */
  89.     var numericKeys = [1, 4, 5, 6, 7, 10, 11],
  90. struct = /^\s*(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}\.?\d*)Z\s*$/.exec(datestr),
  91. minutesOffset = 0,
  92. i, k;
  93. for (i = 0; (k = numericKeys[i]); ++i) {
  94. struct[k] = +struct[k] || 0;
  95. }
  96. // allow undefined days and months
  97. struct[2] = (+struct[2] || 1) - 1;
  98. struct[3] = +struct[3] || 1;
  99. if (struct[8] !== 'Z' && struct[9] !== undefined) {
  100. minutesOffset = struct[10] * 60 + struct[11];
  101. if (struct[9] === '+') {
  102. minutesOffset = 0 - minutesOffset;
  103. }
  104. }
  105. return new Date(Date.UTC(struct[1], struct[2], struct[3], struct[4], struct[5] + minutesOffset, struct[6], struct[7]));
  106. };
  107. this.updateMsgCounter = function () {
  108. if (this.msg_counter > 0) {
  109. if (document.title.search(/^Messages \(\d+\) /) == -1) {
  110. document.title = "Messages (" + this.msg_counter + ") " + document.title;
  111. } else {
  112. document.title = document.title.replace(/^Messages \(\d+\) /, "Messages (" + this.msg_counter + ") ");
  113. }
  114. window.blur();
  115. window.focus();
  116. } else if (document.title.search(/^Messages \(\d+\) /) != -1) {
  117. document.title = document.title.replace(/^Messages \(\d+\) /, "");
  118. }
  119. };
  120. this.incrementMsgCounter = function () {
  121. this.msg_counter += 1;
  122. this.updateMsgCounter();
  123. };
  124. this.clearMsgCounter = function () {
  125. this.msg_counter = 0;
  126. this.updateMsgCounter();
  127. };
  128. this.collections = {
  129. /* FIXME: XEP-0136 specifies 'urn:xmpp:archive' but the mod_archive_odbc
  130. * add-on for ejabberd wants the URL below. This might break for other
  131. * Jabber servers.
  132. */
  133. 'URI': 'http://www.xmpp.org/extensions/xep-0136.html#ns'
  134. };
  135. this.collections.getLastCollection = function (jid, callback) {
  136. var bare_jid = Strophe.getBareJidFromJid(jid),
  137. iq = $iq({'type':'get'})
  138. .c('list', {'xmlns': this.URI,
  139. 'with': bare_jid
  140. })
  141. .c('set', {'xmlns': 'http://jabber.org/protocol/rsm'})
  142. .c('before').up()
  143. .c('max')
  144. .t('1');
  145. converse.connection.sendIQ(iq,
  146. callback,
  147. function () {
  148. console.log('Error while retrieving collections');
  149. });
  150. };
  151. this.collections.getLastMessages = function (jid, callback) {
  152. var that = this;
  153. this.getLastCollection(jid, function (result) {
  154. // Retrieve the last page of a collection (max 30 elements).
  155. var $collection = $(result).find('chat'),
  156. jid = $collection.attr('with'),
  157. start = $collection.attr('start'),
  158. iq = $iq({'type':'get'})
  159. .c('retrieve', {'start': start,
  160. 'xmlns': that.URI,
  161. 'with': jid
  162. })
  163. .c('set', {'xmlns': 'http://jabber.org/protocol/rsm'})
  164. .c('max')
  165. .t('30');
  166. converse.connection.sendIQ(iq, callback);
  167. });
  168. };
  169. this.Message = Backbone.Model.extend();
  170. this.Messages = Backbone.Collection.extend({
  171. model: converse.Message
  172. });
  173. this.ChatBox = Backbone.Model.extend({
  174. initialize: function () {
  175. if (this.get('box_id') !== 'controlbox') {
  176. this.messages = new converse.Messages();
  177. this.messages.localStorage = new Backbone.LocalStorage(
  178. hex_sha1('converse.messages'+this.get('jid')));
  179. this.set({
  180. 'user_id' : Strophe.getNodeFromJid(this.get('jid')),
  181. 'box_id' : hex_sha1(this.get('jid')),
  182. 'fullname' : this.get('fullname'),
  183. 'url': this.get('url'),
  184. 'image_type': this.get('image_type'),
  185. 'image': this.get('image')
  186. });
  187. }
  188. },
  189. messageReceived: function (message) {
  190. var $message = $(message),
  191. body = converse.autoLink($message.children('body').text()),
  192. from = Strophe.getBareJidFromJid($message.attr('from')),
  193. composing = $message.find('composing'),
  194. delayed = $message.find('delay').length > 0,
  195. fullname = (this.get('fullname')||'').split(' ')[0],
  196. stamp, time, sender;
  197. if (!body) {
  198. if (composing.length) {
  199. this.messages.add({
  200. fullname: fullname,
  201. sender: 'them',
  202. delayed: delayed,
  203. time: converse.toISOString(new Date()),
  204. composing: composing.length
  205. });
  206. }
  207. } else {
  208. if (delayed) {
  209. stamp = $message.find('delay').attr('stamp');
  210. time = stamp;
  211. } else {
  212. time = converse.toISOString(new Date());
  213. }
  214. if (from == converse.bare_jid) {
  215. sender = 'me';
  216. } else {
  217. sender = 'them';
  218. }
  219. this.messages.create({
  220. fullname: fullname,
  221. sender: sender,
  222. delayed: delayed,
  223. time: time,
  224. message: body
  225. });
  226. }
  227. }
  228. });
  229. this.ChatBoxView = Backbone.View.extend({
  230. length: 200,
  231. tagName: 'div',
  232. className: 'chatbox',
  233. events: {
  234. 'click .close-chatbox-button': 'closeChat',
  235. 'keypress textarea.chat-textarea': 'keyPressed'
  236. },
  237. message_template: _.template(
  238. '<div class="chat-message {{extra_classes}}">' +
  239. '<span class="chat-message-{{sender}}">{{time}} {{username}}:&nbsp;</span>' +
  240. '<span class="chat-message-content">{{message}}</span>' +
  241. '</div>'),
  242. action_template: _.template(
  243. '<div class="chat-message {{extra_classes}}">' +
  244. '<span class="chat-message-{{sender}}">{{time}} **{{username}} </span>' +
  245. '<span class="chat-message-content">{{message}}</span>' +
  246. '</div>'),
  247. new_day_template: _.template(
  248. '<time class="chat-date" datetime="{{isodate}}">{{datestring}}</time>'
  249. ),
  250. appendMessage: function ($el, msg_dict) {
  251. var this_date = converse.parseISO8601(msg_dict.time),
  252. text = msg_dict.message,
  253. match = text.match(/^\/(.*?)(?: (.*))?$/),
  254. sender = msg_dict.sender,
  255. template, username;
  256. if ((match) && (match[1] === 'me')) {
  257. text = text.replace(/^\/me/, '');
  258. template = this.action_template;
  259. username = msg_dict.fullname;
  260. } else {
  261. template = this.message_template;
  262. username = sender === 'me' && sender || msg_dict.fullname;
  263. }
  264. $el.find('div.chat-event').remove();
  265. $el.append(
  266. template({
  267. 'sender': sender,
  268. 'time': this_date.toTimeString().substring(0,5),
  269. 'message': text,
  270. 'username': username,
  271. 'extra_classes': msg_dict.delayed && 'delayed' || ''
  272. }));
  273. },
  274. insertStatusNotification: function (message, replace) {
  275. var $chat_content = this.$el.find('.chat-content');
  276. $chat_content.find('div.chat-event').remove().end()
  277. .append($('<div class="chat-event"></div>').text(message));
  278. this.scrollDown();
  279. },
  280. showMessage: function (message) {
  281. var time = message.get('time'),
  282. times = this.model.messages.pluck('time'),
  283. this_date = converse.parseISO8601(time),
  284. $chat_content = this.$el.find('.chat-content'),
  285. previous_message, idx, prev_date, isodate, text, match;
  286. // If this message is on a different day than the one received
  287. // prior, then indicate it on the chatbox.
  288. idx = _.indexOf(times, time)-1;
  289. if (idx >= 0) {
  290. previous_message = this.model.messages.at(idx);
  291. prev_date = converse.parseISO8601(previous_message.get('time'));
  292. isodate = new Date(this_date.getTime());
  293. isodate.setUTCHours(0,0,0,0);
  294. isodate = converse.toISOString(isodate);
  295. if (this.isDifferentDay(prev_date, this_date)) {
  296. $chat_content.append(this.new_day_template({
  297. isodate: isodate,
  298. datestring: this_date.toString().substring(0,15)
  299. }));
  300. }
  301. }
  302. if (message.get('composing')) {
  303. this.insertStatusNotification(message.get('fullname')+' '+'is typing');
  304. return;
  305. } else {
  306. this.appendMessage($chat_content, _.clone(message.attributes));
  307. }
  308. if ((message.get('sender') != 'me') && (converse.windowState == 'blur')) {
  309. converse.incrementMsgCounter();
  310. }
  311. this.scrollDown();
  312. },
  313. isDifferentDay: function (prev_date, next_date) {
  314. return (
  315. (next_date.getDate() != prev_date.getDate()) ||
  316. (next_date.getFullYear() != prev_date.getFullYear()) ||
  317. (next_date.getMonth() != prev_date.getMonth()));
  318. },
  319. addHelpMessages: function (msgs) {
  320. var $chat_content = this.$el.find('.chat-content'), i,
  321. msgs_length = msgs.length;
  322. for (i=0; i<msgs_length; i++) {
  323. $chat_content.append($('<div class="chat-info">'+msgs[i]+'</div>'));
  324. }
  325. this.scrollDown();
  326. },
  327. sendMessage: function (text) {
  328. // TODO: Look in ChatPartners to see what resources we have for the recipient.
  329. // if we have one resource, we sent to only that resources, if we have multiple
  330. // we send to the bare jid.
  331. var timestamp = (new Date()).getTime(),
  332. bare_jid = this.model.get('jid'),
  333. match = text.replace(/^\s*/, "").match(/^\/(.*)\s*$/),
  334. msgs;
  335. if (match) {
  336. if (match[1] === "clear") {
  337. this.$el.find('.chat-content').empty();
  338. this.model.messages.reset();
  339. return;
  340. }
  341. else if (match[1] === "help") {
  342. msgs = [
  343. '<strong>/help</strong>:'+__('Show this menu')+'',
  344. '<strong>/me</strong>:'+__('Write in the third person')+'',
  345. '<strong>/clear</strong>:'+__('Remove messages')+''
  346. ];
  347. this.addHelpMessages(msgs);
  348. return;
  349. }
  350. }
  351. var message = $msg({from: converse.bare_jid, to: bare_jid, type: 'chat', id: timestamp})
  352. .c('body').t(text).up()
  353. .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'});
  354. // Forward the message, so that other connected resources are also aware of it.
  355. // TODO: Forward the message only to other connected resources (inside the browser)
  356. var forwarded = $msg({to:converse.bare_jid, type:'chat', id:timestamp})
  357. .c('forwarded', {xmlns:'urn:xmpp:forward:0'})
  358. .c('delay', {xmns:'urn:xmpp:delay',stamp:timestamp}).up()
  359. .cnode(message.tree());
  360. converse.connection.send(message);
  361. converse.connection.send(forwarded);
  362. // Add the new message
  363. this.model.messages.create({
  364. fullname: converse.xmppstatus.get('fullname')||converse.bare_jid,
  365. sender: 'me',
  366. time: converse.toISOString(new Date()),
  367. message: text
  368. });
  369. },
  370. keyPressed: function (ev) {
  371. var $textarea = $(ev.target),
  372. message, notify, composing;
  373. if(ev.keyCode == 13) {
  374. ev.preventDefault();
  375. message = $textarea.val();
  376. $textarea.val('').focus();
  377. if (message !== '') {
  378. if (this.model.get('chatroom')) {
  379. this.sendChatRoomMessage(message);
  380. } else {
  381. this.sendMessage(message);
  382. }
  383. }
  384. this.$el.data('composing', false);
  385. } else if (!this.model.get('chatroom')) {
  386. // composing data is only for single user chat
  387. composing = this.$el.data('composing');
  388. if (!composing) {
  389. if (ev.keyCode != 47) {
  390. // We don't send composing messages if the message
  391. // starts with forward-slash.
  392. notify = $msg({'to':this.model.get('jid'), 'type': 'chat'})
  393. .c('composing', {'xmlns':'http://jabber.org/protocol/chatstates'});
  394. converse.connection.send(notify);
  395. }
  396. this.$el.data('composing', true);
  397. }
  398. }
  399. },
  400. onChange: function (item, changed) {
  401. if (_.has(item.changed, 'chat_status')) {
  402. var chat_status = item.get('chat_status'),
  403. fullname = item.get('fullname');
  404. if (this.$el.is(':visible')) {
  405. if (chat_status === 'offline') {
  406. this.insertStatusNotification(fullname+' '+'has gone offline');
  407. } else if (chat_status === 'away') {
  408. this.insertStatusNotification(fullname+' '+'has gone away');
  409. } else if ((chat_status === 'dnd')) {
  410. this.insertStatusNotification(fullname+' '+'is busy');
  411. } else if (chat_status === 'online') {
  412. this.$el.find('div.chat-event').remove();
  413. }
  414. }
  415. } if (_.has(item.changed, 'status')) {
  416. this.showStatusMessage(item.get('status'));
  417. } if (_.has(item.changed, 'image')) {
  418. this.renderAvatar();
  419. }
  420. // TODO check for changed fullname as well
  421. },
  422. showStatusMessage: function (msg) {
  423. this.$el.find('p.user-custom-message').text(msg).attr('title', msg);
  424. },
  425. closeChat: function () {
  426. if (converse.connection) {
  427. this.model.destroy();
  428. } else {
  429. this.model.trigger('hide');
  430. }
  431. },
  432. updateVCard: function () {
  433. var jid = this.model.get('jid'),
  434. rosteritem = converse.roster.get(jid);
  435. if ((rosteritem)&&(!rosteritem.get('vcard_updated'))) {
  436. converse.getVCard(
  437. jid,
  438. $.proxy(function (jid, fullname, image, image_type, url) {
  439. this.model.save({
  440. 'fullname' : fullname || jid,
  441. 'url': url,
  442. 'image_type': image_type,
  443. 'image': image,
  444. 'vcard_updated': converse.toISOString(new Date())
  445. });
  446. }, this),
  447. $.proxy(function (stanza) {
  448. console.log("ChatBoxView.initialize: An error occured while fetching vcard");
  449. }, this)
  450. );
  451. }
  452. },
  453. initialize: function (){
  454. this.model.messages.on('add', this.showMessage, this);
  455. this.model.on('show', this.show, this);
  456. this.model.on('destroy', this.hide, this);
  457. this.model.on('change', this.onChange, this);
  458. this.updateVCard();
  459. this.$el.appendTo(converse.chatboxesview.$el);
  460. this.render().show().model.messages.fetch({add: true});
  461. if (this.model.get('status')) {
  462. this.showStatusMessage(this.model.get('status'));
  463. }
  464. },
  465. template: _.template(
  466. '<div class="chat-head chat-head-chatbox">' +
  467. '<a class="close-chatbox-button">X</a>' +
  468. '<a href="{{url}}" target="_blank" class="user">' +
  469. '<div class="chat-title"> {{ fullname }} </div>' +
  470. '</a>' +
  471. '<p class="user-custom-message"><p/>' +
  472. '</div>' +
  473. '<div class="chat-content"></div>' +
  474. '<form class="sendXMPPMessage" action="" method="post">' +
  475. '<textarea ' +
  476. 'type="text" ' +
  477. 'class="chat-textarea" ' +
  478. 'placeholder="'+__('Personal message')+'"/>'+
  479. '</form>'),
  480. renderAvatar: function () {
  481. if (!this.model.get('image')) {
  482. return;
  483. }
  484. var img_src = 'data:'+this.model.get('image_type')+';base64,'+this.model.get('image'),
  485. canvas = $('<canvas height="35px" width="35px" class="avatar"></canvas>'),
  486. ctx = canvas.get(0).getContext('2d'),
  487. img = new Image(); // Create new Image object
  488. img.onload = function() {
  489. var ratio = img.width/img.height;
  490. ctx.drawImage(img, 0,0, 35*ratio, 35);
  491. };
  492. img.src = img_src;
  493. this.$el.find('.chat-title').before(canvas);
  494. },
  495. render: function () {
  496. this.$el.attr('id', this.model.get('box_id'))
  497. .html(this.template(this.model.toJSON()));
  498. this.renderAvatar();
  499. return this;
  500. },
  501. focus: function () {
  502. this.$el.find('.chat-textarea').focus();
  503. return this;
  504. },
  505. hide: function () {
  506. if (converse.animate) {
  507. this.$el.hide('fast');
  508. } else {
  509. this.$el.hide();
  510. }
  511. },
  512. show: function () {
  513. if (this.$el.is(':visible') && this.$el.css('opacity') == "1") {
  514. return this.focus();
  515. }
  516. if (converse.animate) {
  517. this.$el.css({'opacity': 0, 'display': 'inline'}).animate({opacity: '1'}, 200);
  518. } else {
  519. this.$el.css({'opacity': 1, 'display': 'inline'});
  520. }
  521. if (converse.connection) {
  522. // Without a connection, we haven't yet initialized
  523. // localstorage
  524. this.model.save();
  525. }
  526. return this;
  527. },
  528. scrollDown: function () {
  529. var $content = this.$el.find('.chat-content');
  530. $content.scrollTop($content[0].scrollHeight);
  531. return this;
  532. }
  533. });
  534. this.ContactsPanel = Backbone.View.extend({
  535. tagName: 'div',
  536. className: 'oc-chat-content',
  537. id: 'users',
  538. events: {
  539. 'click a.toggle-xmpp-contact-form': 'toggleContactForm',
  540. 'submit form.add-xmpp-contact': 'addContactFromForm',
  541. 'submit form.search-xmpp-contact': 'searchContacts',
  542. 'click a.subscribe-to-user': 'addContactFromList'
  543. },
  544. tab_template: _.template('<li><a class="s current" href="#users">'+__('Contacts')+'</a></li>'),
  545. template: _.template(
  546. '<form class="set-xmpp-status" action="" method="post">'+
  547. '<span id="xmpp-status-holder">'+
  548. '<select id="select-xmpp-status" style="display:none">'+
  549. '<option value="online">'+__('Online')+'</option>'+
  550. '<option value="dnd">'+__('Busy')+'</option>'+
  551. '<option value="away">'+__('Away')+'</option>'+
  552. '<option value="offline">'+__('Offline')+'</option>'+
  553. '</select>'+
  554. '</span>'+
  555. '</form>'+
  556. '<dl class="add-converse-contact dropdown">' +
  557. '<dt id="xmpp-contact-search" class="fancy-dropdown">' +
  558. '<a class="toggle-xmpp-contact-form" href="#"'+
  559. 'title="'+__('Click to add new chat contacts')+'">'+__('Add a contact')+'</a>' +
  560. '</dt>' +
  561. '<dd class="search-xmpp" style="display:none"><ul></ul></dd>' +
  562. '</dl>'
  563. ),
  564. add_contact_template: _.template(
  565. '<li>'+
  566. '<form class="add-xmpp-contact">' +
  567. '<input type="text" name="identifier" class="username" placeholder="'+__('Contact username')+'"/>' +
  568. '<button type="submit">'+__('Add')+'</button>' +
  569. '</form>'+
  570. '<li>'
  571. ),
  572. search_contact_template: _.template(
  573. '<li>'+
  574. '<form class="search-xmpp-contact">' +
  575. '<input type="text" name="identifier" class="username" placeholder="'+__('Contact name')+'"/>' +
  576. '<button type="submit">'+__('Search')+'</button>' +
  577. '</form>'+
  578. '<li>'
  579. ),
  580. initialize: function (cfg) {
  581. cfg.$parent.append(this.$el);
  582. this.$tabs = cfg.$parent.parent().find('#controlbox-tabs');
  583. },
  584. render: function () {
  585. var markup;
  586. this.$tabs.append(this.tab_template());
  587. if (converse.xhr_user_search) {
  588. markup = this.search_contact_template();
  589. } else {
  590. markup = this.add_contact_template();
  591. }
  592. this.$el.html(this.template());
  593. this.$el.find('.search-xmpp ul').append(markup);
  594. this.$el.append(converse.rosterview.$el);
  595. return this;
  596. },
  597. toggleContactForm: function (ev) {
  598. ev.preventDefault();
  599. this.$el.find('.search-xmpp').toggle('fast', function () {
  600. if ($(this).is(':visible')) {
  601. $(this).find('input.username').focus();
  602. }
  603. });
  604. },
  605. searchContacts: function (ev) {
  606. ev.preventDefault();
  607. $.getJSON(portal_url + "/search-users?q=" + $(ev.target).find('input.username').val(), function (data) {
  608. var $ul= $('.search-xmpp ul');
  609. $ul.find('li.found-user').remove();
  610. $ul.find('li.chat-info').remove();
  611. if (!data.length) {
  612. $ul.append('<li class="chat-info">'+__('No users found')+'</li>');
  613. }
  614. $(data).each(function (idx, obj) {
  615. $ul.append(
  616. $('<li class="found-user"></li>')
  617. .append(
  618. $('<a class="subscribe-to-user" href="#" title="'+__('Click to add as a chat contact')+'"></a>')
  619. .attr('data-recipient', Strophe.escapeNode(obj.id)+'@'+converse.domain)
  620. .text(obj.fullname)
  621. )
  622. );
  623. });
  624. });
  625. },
  626. addContactFromForm: function (ev) {
  627. ev.preventDefault();
  628. var $input = $(ev.target).find('input');
  629. var jid = $input.val();
  630. if (! jid) {
  631. // this is not a valid JID
  632. $input.addClass('error');
  633. return;
  634. }
  635. converse.getVCard(
  636. jid,
  637. $.proxy(function (jid, fullname, image, image_type, url) {
  638. this.addContact(jid, fullname);
  639. }, this),
  640. $.proxy(function (stanza) {
  641. console.log("An error occured while fetching vcard");
  642. if ($(stanza).find('error').attr('code') == '503') {
  643. // If we get service-unavailable, we continue to create
  644. // the user
  645. var jid = $(stanza).attr('from');
  646. this.addContact(jid, jid);
  647. }
  648. }, this));
  649. $('.search-xmpp').hide();
  650. },
  651. addContactFromList: function (ev) {
  652. ev.preventDefault();
  653. var $target = $(ev.target),
  654. jid = $target.attr('data-recipient'),
  655. name = $target.text();
  656. this.addContact(jid, name);
  657. $target.parent().remove();
  658. $('.search-xmpp').hide();
  659. },
  660. addContact: function (jid, name) {
  661. converse.connection.roster.add(jid, name, [], function (iq) {
  662. converse.connection.roster.subscribe(jid, null, converse.xmppstatus.get('fullname'));
  663. });
  664. }
  665. });
  666. this.RoomsPanel = Backbone.View.extend({
  667. tagName: 'div',
  668. id: 'chatrooms',
  669. events: {
  670. 'submit form.add-chatroom': 'createChatRoom',
  671. 'click input#show-rooms': 'showRooms',
  672. 'click a.open-room': 'createChatRoom',
  673. 'click a.room-info': 'showRoomInfo'
  674. },
  675. room_template: _.template(
  676. '<dd class="available-chatroom">'+
  677. '<a class="open-room" data-room-jid="{{jid}}"'+
  678. 'title="'+__('Click to open this room')+'" href="#">{{name}}</a>'+
  679. '<a class="room-info" data-room-jid="{{jid}}"'+
  680. 'title="'+__('Show more information on this room')+'" href="#">&nbsp;</a>'+
  681. '</dd>'),
  682. room_description_template: _.template(
  683. '<div class="room-info">'+
  684. '<p class="room-info"><strong>'+__('Description:')+'</strong> {{desc}}</p>' +
  685. '<p class="room-info"><strong>'+__('Occupants:')+'</strong> {{occ}}</p>' +
  686. '<p class="room-info"><strong>'+__('Features:')+'</strong> <ul>'+
  687. '{[ if (passwordprotected) { ]}' +
  688. '<li class="room-info locked">'+__('Requires authentication')+'</li>' +
  689. '{[ } ]}' +
  690. '{[ if (hidden) { ]}' +
  691. '<li class="room-info">'+__('Hidden')+'</li>' +
  692. '{[ } ]}' +
  693. '{[ if (membersonly) { ]}' +
  694. '<li class="room-info">'+__('Requires an invitation')+'</li>' +
  695. '{[ } ]}' +
  696. '{[ if (moderated) { ]}' +
  697. '<li class="room-info">'+__('Moderated')+'</li>' +
  698. '{[ } ]}' +
  699. '{[ if (nonanonymous) { ]}' +
  700. '<li class="room-info">'+__('Non-anonymous')+'</li>' +
  701. '{[ } ]}' +
  702. '{[ if (open) { ]}' +
  703. '<li class="room-info">'+__('Open room')+'</li>' +
  704. '{[ } ]}' +
  705. '{[ if (persistent) { ]}' +
  706. '<li class="room-info">'+__('Permanent room')+'</li>' +
  707. '{[ } ]}' +
  708. '{[ if (publicroom) { ]}' +
  709. '<li class="room-info">'+__('Public')+'</li>' +
  710. '{[ } ]}' +
  711. '{[ if (semianonymous) { ]}' +
  712. '<li class="room-info">'+__('Semi-anonymous')+'</li>' +
  713. '{[ } ]}' +
  714. '{[ if (temporary) { ]}' +
  715. '<li class="room-info">'+__('Temporary room')+'</li>' +
  716. '{[ } ]}' +
  717. '{[ if (unmoderated) { ]}' +
  718. '<li class="room-info">'+__('Unmoderated')+'</li>' +
  719. '{[ } ]}' +
  720. '</p>' +
  721. '</div>'
  722. ),
  723. tab_template: _.template('<li><a class="s" href="#chatrooms">'+__('Rooms')+'</a></li>'),
  724. template: _.template(
  725. '<form class="add-chatroom" action="" method="post">'+
  726. '<input type="text" name="chatroom" class="new-chatroom-name" placeholder="'+__('Room name')+'"/>'+
  727. '<input type="text" name="nick" class="new-chatroom-nick" placeholder="'+__('Nickname')+'"/>'+
  728. '<input type="{{ server_input_type }}" name="server" class="new-chatroom-server" placeholder="'+__('Server')+'"/>'+
  729. '<input type="submit" name="join" value="'+__('Join')+'"/>'+
  730. '<input type="button" name="show" id="show-rooms" value="'+__('Show rooms')+'"/>'+
  731. '</form>'+
  732. '<dl id="available-chatrooms"></dl>'),
  733. initialize: function (cfg) {
  734. cfg.$parent.append(
  735. this.$el.html(
  736. this.template({
  737. server_input_type: converse.hide_muc_server && 'hidden' || 'text'
  738. })
  739. ).hide());
  740. this.$tabs = cfg.$parent.parent().find('#controlbox-tabs');
  741. this.on('update-rooms-list', function (ev) {
  742. this.updateRoomsList();
  743. });
  744. converse.xmppstatus.on("change", $.proxy(function (model) {
  745. if (!(_.has(model.changed, 'fullname'))) {
  746. return;
  747. }
  748. var $nick = this.$el.find('input.new-chatroom-nick');
  749. if (! $nick.is(':focus')) {
  750. $nick.val(model.get('fullname'));
  751. }
  752. }, this));
  753. },
  754. render: function () {
  755. this.$tabs.append(this.tab_template());
  756. return this;
  757. },
  758. informNoRoomsFound: function () {
  759. var $available_chatrooms = this.$el.find('#available-chatrooms');
  760. // # For translators: %1$s is a variable and will be replaced with the XMPP server name
  761. $available_chatrooms.html('<dt>'+__('No rooms on %1$s',this.muc_domain)+'</dt>');
  762. $('input#show-rooms').show().siblings('span.spinner').remove();
  763. },
  764. updateRoomsList: function (domain) {
  765. converse.connection.muc.listRooms(
  766. this.muc_domain,
  767. $.proxy(function (iq) { // Success
  768. var name, jid, i, fragment,
  769. that = this,
  770. $available_chatrooms = this.$el.find('#available-chatrooms');
  771. this.rooms = $(iq).find('query').find('item');
  772. if (this.rooms.length) {
  773. // # For translators: %1$s is a variable and will be
  774. // # replaced with the XMPP server name
  775. $available_chatrooms.html('<dt>'+__('Rooms on %1$s',this.muc_domain)+'</dt>');
  776. fragment = document.createDocumentFragment();
  777. for (i=0; i<this.rooms.length; i++) {
  778. name = Strophe.unescapeNode($(this.rooms[i]).attr('name')||$(this.rooms[i]).attr('jid'));
  779. jid = $(this.rooms[i]).attr('jid');
  780. fragment.appendChild($(this.room_template({
  781. 'name':name,
  782. 'jid':jid
  783. }))[0]);
  784. }
  785. $available_chatrooms.append(fragment);
  786. $('input#show-rooms').show().siblings('span.spinner').remove();
  787. } else {
  788. this.informNoRoomsFound();
  789. }
  790. return true;
  791. }, this),
  792. $.proxy(function (iq) { // Failure
  793. this.informNoRoomsFound();
  794. }, this));
  795. },
  796. showRooms: function (ev) {
  797. var $available_chatrooms = this.$el.find('#available-chatrooms');
  798. var $server = this.$el.find('input.new-chatroom-server');
  799. var server = $server.val();
  800. if (!server) {
  801. $server.addClass('error');
  802. return;
  803. }
  804. this.$el.find('input.new-chatroom-name').removeClass('error');
  805. $server.removeClass('error');
  806. $available_chatrooms.empty();
  807. $('input#show-rooms').hide().after('<span class="spinner"/>');
  808. this.muc_domain = server;
  809. this.updateRoomsList();
  810. },
  811. showRoomInfo: function (ev) {
  812. var target = ev.target,
  813. $dd = $(target).parent('dd'),
  814. $div = $dd.find('div.room-info');
  815. if ($div.length) {
  816. $div.remove();
  817. } else {
  818. $dd.find('span.spinner').remove();
  819. $dd.append('<span class="spinner hor_centered"/>');
  820. converse.connection.disco.info(
  821. $(target).attr('data-room-jid'),
  822. null,
  823. $.proxy(function (stanza) {
  824. var $stanza = $(stanza);
  825. // All MUC features found here: http://xmpp.org/registrar/disco-features.html
  826. $dd.find('span.spinner').replaceWith(
  827. this.room_description_template({
  828. 'desc': $stanza.find('field[var="muc#roominfo_description"] value').text(),
  829. 'occ': $stanza.find('field[var="muc#roominfo_occupants"] value').text(),
  830. 'hidden': $stanza.find('feature[var="muc_hidden"]').length,
  831. 'membersonly': $stanza.find('feature[var="muc_membersonly"]').length,
  832. 'moderated': $stanza.find('feature[var="muc_moderated"]').length,
  833. 'nonanonymous': $stanza.find('feature[var="muc_nonanonymous"]').length,
  834. 'open': $stanza.find('feature[var="muc_open"]').length,
  835. 'passwordprotected': $stanza.find('feature[var="muc_passwordprotected"]').length,
  836. 'persistent': $stanza.find('feature[var="muc_persistent"]').length,
  837. 'publicroom': $stanza.find('feature[var="muc_public"]').length,
  838. 'semianonymous': $stanza.find('feature[var="muc_semianonymous"]').length,
  839. 'temporary': $stanza.find('feature[var="muc_temporary"]').length,
  840. 'unmoderated': $stanza.find('feature[var="muc_unmoderated"]').length
  841. }));
  842. }, this));
  843. }
  844. },
  845. createChatRoom: function (ev) {
  846. ev.preventDefault();
  847. var name, $name,
  848. server, $server,
  849. jid,
  850. $nick = this.$el.find('input.new-chatroom-nick'),
  851. nick = $nick.val(),
  852. chatroom;
  853. if (!nick) { $nick.addClass('error'); }
  854. else { $nick.removeClass('error'); }
  855. if (ev.type === 'click') {
  856. jid = $(ev.target).attr('data-room-jid');
  857. } else {
  858. $name = this.$el.find('input.new-chatroom-name');
  859. $server= this.$el.find('input.new-chatroom-server');
  860. server = $server.val();
  861. name = $name.val().trim().toLowerCase();
  862. $name.val(''); // Clear the input
  863. if (name && server) {
  864. jid = Strophe.escapeNode(name) + '@' + server;
  865. $name.removeClass('error');
  866. $server.removeClass('error');
  867. this.muc_domain = server;
  868. } else {
  869. if (!name) { $name.addClass('error'); }
  870. if (!server) { $server.addClass('error'); }
  871. return;
  872. }
  873. }
  874. if (!nick) { return; }
  875. chatroom = converse.chatboxes.createChatBox({
  876. 'id': jid,
  877. 'jid': jid,
  878. 'name': Strophe.unescapeNode(Strophe.getNodeFromJid(jid)),
  879. 'nick': nick,
  880. 'chatroom': true,
  881. 'box_id' : hex_sha1(jid)
  882. });
  883. if (!chatroom.get('connected')) {
  884. converse.chatboxesview.views[jid].connect(null);
  885. }
  886. }
  887. });
  888. this.ControlBoxView = converse.ChatBoxView.extend({
  889. tagName: 'div',
  890. className: 'chatbox',
  891. id: 'controlbox',
  892. events: {
  893. 'click a.close-chatbox-button': 'closeChat',
  894. 'click ul#controlbox-tabs li a': 'switchTab'
  895. },
  896. initialize: function () {
  897. this.$el.appendTo(converse.chatboxesview.$el);
  898. this.model.on('change', $.proxy(function (item, changed) {
  899. var i;
  900. if (_.has(item.changed, 'connected')) {
  901. this.render();
  902. converse.features.on('add', $.proxy(this.featureAdded, this));
  903. // Features could have been added before the controlbox was
  904. // initialized. Currently we're only interested in MUC
  905. var feature = converse.features.findWhere({'var': 'http://jabber.org/protocol/muc'});
  906. if (feature) {
  907. this.featureAdded(feature);
  908. }
  909. }
  910. if (_.has(item.changed, 'visible')) {
  911. if (item.changed.visible === true) {
  912. this.show();
  913. }
  914. }
  915. }, this));
  916. this.model.on('show', this.show, this);
  917. this.model.on('destroy', this.hide, this);
  918. this.model.on('hide', this.hide, this);
  919. if (this.model.get('visible')) {
  920. this.show();
  921. }
  922. },
  923. featureAdded: function (feature) {
  924. if (feature.get('var') == 'http://jabber.org/protocol/muc') {
  925. this.roomspanel.muc_domain = feature.get('from');
  926. var $server= this.$el.find('input.new-chatroom-server');
  927. if (! $server.is(':focus')) {
  928. $server.val(this.roomspanel.muc_domain);
  929. }
  930. if (converse.auto_list_rooms) {
  931. this.roomspanel.trigger('update-rooms-list');
  932. }
  933. }
  934. },
  935. template: _.template(
  936. '<div class="chat-head oc-chat-head">'+
  937. '<ul id="controlbox-tabs"></ul>'+
  938. '<a class="close-chatbox-button">X</a>'+
  939. '</div>'+
  940. '<div id="controlbox-panes"></div>'
  941. ),
  942. switchTab: function (ev) {
  943. ev.preventDefault();
  944. var $tab = $(ev.target),
  945. $sibling = $tab.parent().siblings('li').children('a'),
  946. $tab_panel = $($tab.attr('href')),
  947. $sibling_panel = $($sibling.attr('href'));
  948. $sibling_panel.fadeOut('fast', function () {
  949. $sibling.removeClass('current');
  950. $tab.addClass('current');
  951. $tab_panel.fadeIn('fast', function () {
  952. });
  953. });
  954. },
  955. addHelpMessages: function (msgs) {
  956. // Override addHelpMessages in ChatBoxView, for now do nothing.
  957. return;
  958. },
  959. render: function () {
  960. if ((!converse.prebind) && (!converse.connection)) {
  961. // Add login panel if the user still has to authenticate
  962. this.$el.html(this.template(this.model.toJSON()));
  963. this.loginpanel = new converse.LoginPanel({'$parent': this.$el.find('#controlbox-panes')});
  964. this.loginpanel.render();
  965. } else if (!this.contactspanel) {
  966. this.$el.html(this.template(this.model.toJSON()));
  967. this.contactspanel = new converse.ContactsPanel({'$parent': this.$el.find('#controlbox-panes')});
  968. this.contactspanel.render();
  969. converse.xmppstatusview = new converse.XMPPStatusView({'model': converse.xmppstatus});
  970. converse.xmppstatusview.render();
  971. this.roomspanel = new converse.RoomsPanel({'$parent': this.$el.find('#controlbox-panes')});
  972. this.roomspanel.render();
  973. }
  974. return this;
  975. }
  976. });
  977. this.ChatRoomView = converse.ChatBoxView.extend({
  978. length: 300,
  979. tagName: 'div',
  980. className: 'chatroom',
  981. events: {
  982. 'click a.close-chatbox-button': 'closeChat',
  983. 'click a.configure-chatroom-button': 'configureChatRoom',
  984. 'keypress textarea.chat-textarea': 'keyPressed'
  985. },
  986. info_template: _.template('<div class="chat-info">{{message}}</div>'),
  987. sendChatRoomMessage: function (body) {
  988. var match = body.replace(/^\s*/, "").match(/^\/(.*?)(?: (.*))?$/) || [false],
  989. $chat_content;
  990. switch (match[1]) {
  991. case 'msg':
  992. // TODO: Private messages
  993. break;
  994. case 'clear':
  995. this.$el.find('.chat-content').empty();
  996. break;
  997. case 'topic':
  998. converse.connection.muc.setTopic(this.model.get('jid'), match[2]);
  999. break;
  1000. case 'kick':
  1001. converse.connection.muc.kick(this.model.get('jid'), match[2]);
  1002. break;
  1003. case 'ban':
  1004. converse.connection.muc.ban(this.model.get('jid'), match[2]);
  1005. break;
  1006. case 'op':
  1007. converse.connection.muc.op(this.model.get('jid'), match[2]);
  1008. break;
  1009. case 'deop':
  1010. converse.connection.muc.deop(this.model.get('jid'), match[2]);
  1011. break;
  1012. case 'help':
  1013. $chat_content = this.$el.find('.chat-content');
  1014. msgs = [
  1015. '<strong>/help</strong>:'+__('Show this menu')+'',
  1016. '<strong>/me</strong>:'+__('Write in the third person')+'',
  1017. '<strong>/topic</strong>:'+__('Set chatroom topic')+'',
  1018. '<strong>/kick</strong>:'+__('Kick user from chatroom')+'',
  1019. '<strong>/ban</strong>:'+__('Ban user from chatroom')+'',
  1020. '<strong>/clear</strong>:'+__('Remove messages')+''
  1021. ];
  1022. this.addHelpMessages(msgs);
  1023. break;
  1024. default:
  1025. this.last_msgid = converse.connection.muc.groupchat(this.model.get('jid'), body);
  1026. break;
  1027. }
  1028. },
  1029. template: _.template(
  1030. '<div class="chat-head chat-head-chatroom">' +
  1031. '<a class="close-chatbox-button">X</a>' +
  1032. '<a class="configure-chatroom-button" style="display:none">&nbsp;</a>' +
  1033. '<div class="chat-title"> {{ name }} </div>' +
  1034. '<p class="chatroom-topic"><p/>' +
  1035. '</div>' +
  1036. '<div class="chat-body">' +
  1037. '<span class="spinner centered"/>' +
  1038. '</div>'),
  1039. chatarea_template: _.template(
  1040. '<div class="chat-area">' +
  1041. '<div class="chat-content"></div>' +
  1042. '<form class="sendXMPPMessage" action="" method="post">' +
  1043. '<textarea type="text" class="chat-textarea" ' +
  1044. 'placeholder="'+__('Message')+'"/>' +
  1045. '</form>' +
  1046. '</div>' +
  1047. '<div class="participants">' +
  1048. '<ul class="participant-list"></ul>' +
  1049. '</div>'
  1050. ),
  1051. render: function () {
  1052. this.$el.attr('id', this.model.get('box_id'))
  1053. .html(this.template(this.model.toJSON()));
  1054. return this;
  1055. },
  1056. renderChatArea: function () {
  1057. if (!this.$el.find('.chat-area').length) {
  1058. this.$el.find('.chat-body').empty().append(this.chatarea_template());
  1059. }
  1060. return this;
  1061. },
  1062. connect: function (password) {
  1063. if (_.has(converse.connection.muc.rooms, this.model.get('jid'))) {
  1064. // If the room exists, it already has event listeners, so we
  1065. // doing add them again.
  1066. converse.connection.muc.join(
  1067. this.model.get('jid'), this.model.get('nick'), null, null, null, password);
  1068. } else {
  1069. converse.connection.muc.join(
  1070. this.model.get('jid'),
  1071. this.model.get('nick'),
  1072. $.proxy(this.onChatRoomMessage, this),
  1073. $.proxy(this.onChatRoomPresence, this),
  1074. $.proxy(this.onChatRoomRoster, this),
  1075. password);
  1076. }
  1077. },
  1078. initialize: function () {
  1079. this.connect(null);
  1080. this.model.messages.on('add', this.showMessage, this);
  1081. this.model.on('destroy', function (model, response, options) {
  1082. this.$el.hide('fast');
  1083. converse.connection.muc.leave(
  1084. this.model.get('jid'),
  1085. this.model.get('nick'),
  1086. $.proxy(this.onLeave, this),
  1087. undefined);
  1088. },
  1089. this);
  1090. this.$el.appendTo(converse.chatboxesview.$el);
  1091. this.render().show().model.messages.fetch({add: true});
  1092. },
  1093. onLeave: function () {
  1094. this.model.set('connected', false);
  1095. },
  1096. form_input_template: _.template('<label>{{label}}<input name="{{name}}" type="{{type}}" value="{{value}}"></label>'),
  1097. select_option_template: _.template('<option value="{{value}}">{{label}}</option>'),
  1098. form_select_template: _.template('<label>{{label}}<select name="{{name}}">{{options}}</select></label>'),
  1099. form_checkbox_template: _.template('<label>{{label}}<input name="{{name}}" type="{{type}}" {{checked}}"></label>'),
  1100. renderConfigurationForm: function (stanza) {
  1101. var $form= this.$el.find('form.chatroom-form'),
  1102. $stanza = $(stanza),
  1103. $fields = $stanza.find('field'),
  1104. title = $stanza.find('title').text(),
  1105. instructions = $stanza.find('instructions').text(),
  1106. i, j, options=[];
  1107. var input_types = {
  1108. 'text-private': 'password',
  1109. 'text-single': 'textline',
  1110. 'boolean': 'checkbox',
  1111. 'hidden': 'hidden',
  1112. 'list-single': 'dropdown'
  1113. };
  1114. $form.find('span.spinner').remove();
  1115. $form.append($('<legend>').text(title));
  1116. if (instructions != title) {
  1117. $form.append($('<p>').text(instructions));
  1118. }
  1119. for (i=0; i<$fields.length; i++) {
  1120. $field = $($fields[i]);
  1121. if ($field.attr('type') == 'list-single') {
  1122. options = [];
  1123. $options = $field.find('option');
  1124. for (j=0; j<$options.length; j++) {
  1125. options.push(this.select_option_template({
  1126. value: $($options[j]).find('value').text(),
  1127. label: $($options[j]).attr('label')
  1128. }));
  1129. }
  1130. $form.append(this.form_select_template({
  1131. name: $field.attr('var'),
  1132. label: $field.attr('label'),
  1133. options: options.join('')
  1134. }));
  1135. } else if ($field.attr('type') == 'boolean') {
  1136. $form.append(this.form_checkbox_template({
  1137. name: $field.attr('var'),
  1138. type: input_types[$field.attr('type')],
  1139. label: $field.attr('label') || '',
  1140. checked: $field.find('value').text() === "1" && 'checked="1"' || ''
  1141. }));
  1142. } else {
  1143. $form.append(this.form_input_template({
  1144. name: $field.attr('var'),
  1145. type: input_types[$field.attr('type')],
  1146. label: $field.attr('label') || '',
  1147. value: $field.find('value').text()
  1148. }));
  1149. }
  1150. }
  1151. $form.append('<input type="submit" value="'+__('Save')+'"/>');
  1152. $form.append('<input type="button" value="'+__('Cancel')+'"/>');
  1153. $form.on('submit', $.proxy(this.saveConfiguration, this));
  1154. $form.find('input[type=button]').on('click', $.proxy(this.cancelConfiguration, this));
  1155. },
  1156. field_template: _.template('<field var="{{name}}"><value>{{value}}</value></field>'),
  1157. saveConfiguration: function (ev) {
  1158. ev.preventDefault();
  1159. var that = this;
  1160. var $inputs = $(ev.target).find(':input:not([type=button]):not([type=submit])'),
  1161. count = $inputs.length,
  1162. configArray = [];
  1163. $inputs.each(function () {
  1164. var $input = $(this), value;
  1165. if ($input.is('[type=checkbox]')) {
  1166. value = $input.is(':checked') && 1 || 0;
  1167. } else {
  1168. value = $input.val();
  1169. }
  1170. var cnode = $(that.field_template({
  1171. name: $input.attr('name'),
  1172. value: value
  1173. }))[0];
  1174. configArray.push(cnode);
  1175. if (!--count) {
  1176. converse.connection.muc.saveConfiguration(
  1177. that.model.get('jid'),
  1178. configArray,
  1179. $.proxy(that.onConfigSaved, that),
  1180. $.proxy(that.onErrorConfigSaved, that)
  1181. );
  1182. }
  1183. });
  1184. this.$el.find('div.chatroom-form-container').hide(
  1185. function () {
  1186. $(this).remove();
  1187. that.$el.find('.chat-area').show();
  1188. that.$el.find('.participants').show();
  1189. });
  1190. },
  1191. onConfigSaved: function (stanza) {
  1192. // XXX
  1193. },
  1194. onErrorConfigSaved: function (stanza) {
  1195. this.insertStatusNotification(__("An error occurred while trying to save the form."));
  1196. },
  1197. cancelConfiguration: function (ev) {
  1198. ev.preventDefault();
  1199. var that = this;
  1200. this.$el.find('div.chatroom-form-container').hide(
  1201. function () {
  1202. $(this).remove();
  1203. that.$el.find('.chat-area').show();
  1204. that.$el.find('.participants').show();
  1205. });
  1206. },
  1207. configureChatRoom: function (ev) {
  1208. ev.preventDefault();
  1209. if (this.$el.find('div.chatroom-form-container').length) {
  1210. return;
  1211. }
  1212. this.$el.find('.chat-area').hide();
  1213. this.$el.find('.participants').hide();
  1214. this.$el.find('.chat-body').append(
  1215. $('<div class="chatroom-form-container">'+
  1216. '<form class="chatroom-form">'+
  1217. '<span class="spinner centered"/>'+
  1218. '</form>'+
  1219. '</div>'));
  1220. converse.connection.muc.configure(
  1221. this.model.get('jid'),
  1222. $.proxy(this.renderConfigurationForm, this)
  1223. );
  1224. },
  1225. submitPassword: function (ev) {
  1226. ev.preventDefault();
  1227. var password = this.$el.find('.chatroom-form').find('input[type=password]').val();
  1228. this.$el.find('.chatroom-form-container').replaceWith(
  1229. '<span class="spinner centered"/>');
  1230. this.connect(password);
  1231. },
  1232. renderPasswordForm: function () {
  1233. this.$el.find('span.centered.spinner').remove();
  1234. this.$el.find('.chat-body').append(
  1235. $('<div class="chatroom-form-container">'+
  1236. '<form class="chatroom-form">'+
  1237. '<legend>'+__('This chatroom requires a password')+'</legend>' +
  1238. '<label>'+__('Password: ')+'<input type="password" name="password"/></label>' +
  1239. '<input type="submit" value="'+__('Submit')+'/>' +
  1240. '</form>'+
  1241. '</div>'));
  1242. this.$el.find('.chatroom-form').on('submit', $.proxy(this.submitPassword, this));
  1243. },
  1244. showDisconnectMessage: function (msg) {
  1245. this.$el.find('.chat-area').remove();
  1246. this.$el.find('.participants').remove();
  1247. this.$el.find('span.centered.spinner').remove();
  1248. this.$el.find('.chat-body').append($('<p>'+msg+'</p>'));
  1249. },
  1250. infoMessages: {
  1251. 100: __('This room is not anonymous'),
  1252. 102: __('This room now shows unavailable members'),
  1253. 103: __('This room does not show unavailable members'),
  1254. 104: __('Non-privacy-related room configuration has changed'),
  1255. 170: __('Room logging is now enabled'),
  1256. 171: __('Room logging is now disabled'),
  1257. 172: __('This room is now non-anonymous'),
  1258. 173: __('This room is now semi-anonymous'),
  1259. 174: __('This room is now fully-anonymous'),
  1260. 201: __('A new room has been created'),
  1261. 210: __('Your nickname has been changed')
  1262. },
  1263. actionInfoMessages: {
  1264. // # For translations: %1$s will be replaced with the user's nickname
  1265. // # Don't translate "strong"
  1266. // # Example: <strong>jcbrand</strong> has been banned
  1267. 301: converse.i18n.translate('<strong>%1$s</strong> has been banned'),
  1268. // # For translations: %1$s will be replaced with the user's nickname
  1269. // # Don't translate "strong"
  1270. // # Example: <strong>jcbrand</strong> has been kicked out
  1271. 307: converse.i18n.translate('<strong>%1$s</strong> has been kicked out'),
  1272. // # For translations: %1$s will be replaced with the user's nickname
  1273. // # Don't translate "strong"
  1274. // # Example: <strong>jcbrand</strong> has been removed because of an affiliasion change
  1275. 321: converse.i18n.translate("<strong>%1$s</strong> has been removed because of an affiliation change"),
  1276. // # For translations: %1$s will be replaced with the user's nickname
  1277. // # Don't translate "strong"
  1278. // # Example: <strong>jcbrand</strong> has been removed for not being a member
  1279. 322: converse.i18n.translate("<strong>%1$s</strong> has been removed for not being a member")
  1280. },
  1281. disconnectMessages: {
  1282. 301: __('You have been banned from this room'),
  1283. 307: __('You have been kicked from this room'),
  1284. 321: __("You have been removed from this room because of an affiliation change"),
  1285. 322: __("You have been removed from this room because the room has changed to members-only and you're not a member"),
  1286. 332: __("You have been removed from this room because the MUC (Multi-user chat) service is being shut down.")
  1287. },
  1288. showStatusMessages: function ($el, is_self) {
  1289. /* Check for status codes and communicate their purpose to the user
  1290. * See: http://xmpp.org/registrar/mucstatus.html
  1291. */
  1292. var $chat_content = this.$el.find('.chat-content'),
  1293. $stats = $el.find('status'),
  1294. disconnect_msgs = [],
  1295. info_msgs = [],
  1296. action_msgs = [],
  1297. msgs, i;
  1298. for (i=0; i<$stats.length; i++) {
  1299. var stat = $stats[i].getAttribute('code');
  1300. if (is_self) {
  1301. if (_.contains(_.keys(this.disconnectMessages), stat)) {
  1302. disconnect_msgs.push(this.disconnectMessages[stat]);
  1303. }
  1304. } else {
  1305. if (_.contains(_.keys(this.infoMessages), stat)) {
  1306. info_msgs.push(this.infoMessages[stat]);
  1307. } else if (_.contains(_.keys(this.actionInfoMessages), stat)) {
  1308. action_msgs.push(
  1309. this.actionInfoMessages[stat].fetch(
  1310. Strophe.unescapeNode(Strophe.getResourceFromJid($el.attr('from')))
  1311. ));
  1312. }
  1313. }
  1314. }
  1315. if (disconnect_msgs.length > 0) {
  1316. for (i=0; i<disconnect_msgs.length; i++) {
  1317. this.showDisconnectMessage(disconnect_msgs[i]);
  1318. }
  1319. this.model.set('connected', false);
  1320. return;
  1321. }
  1322. this.renderChatArea();
  1323. for (i=0; i<info_msgs.length; i++) {
  1324. $chat_content.append(this.info_template({message: info_msgs[i]}));
  1325. }
  1326. for (i=0; i<action_msgs.length; i++) {
  1327. $chat_content.append(this.info_template({message: action_msgs[i]}));
  1328. }
  1329. this.scrollDown();
  1330. },
  1331. showErrorMessage: function ($error, room) {
  1332. var $chat_content = this.$el.find('.chat-content');
  1333. // We didn't enter the room, so we must remove it from the MUC
  1334. // add-on
  1335. converse.connection.muc.removeRoom(room.name);
  1336. if ($error.attr('type') == 'auth') {
  1337. if ($error.find('not-authorized').length) {
  1338. this.renderPasswordForm();
  1339. } else if ($error.find('registration-required').length) {
  1340. this.showDisconnectMessage(__('You are not on the member list of this room'));
  1341. } else if ($error.find('forbidden').length) {
  1342. this.showDisconnectMessage(__('You have been banned from this room'));
  1343. }
  1344. } else if ($error.attr('type') == 'modify') {
  1345. if ($error.find('jid-malformed').length) {
  1346. this.showDisconnectMessage(__('No nickname was specified'));
  1347. }
  1348. } else if ($error.attr('type') == 'cancel') {
  1349. if ($error.find('not-allowed').length) {
  1350. this.showDisconnectMessage(__('You are not allowed to create new rooms'));
  1351. } else if ($error.find('not-acceptable').length) {
  1352. this.showDisconnectMessage(__("Your nickname doesn't conform to this room's policies"));
  1353. } else if ($error.find('conflict').length) {
  1354. this.showDisconnectMessage(__("Your nickname is already taken"));
  1355. } else if ($error.find('item-not-found').length) {
  1356. this.showDisconnectMessage(__("This room does not (yet) exist"));
  1357. } else if ($error.find('service-unavailable').length) {
  1358. this.showDisconnectMessage(__("This room has reached it's maximum number of occupants"));
  1359. }
  1360. }
  1361. },
  1362. onChatRoomPresence: function (presence, room) {
  1363. var nick = room.nick,
  1364. $presence = $(presence),
  1365. from = $presence.attr('from'),
  1366. is_self = ($presence.find("status[code='110']").length) || (from == room.name+'/'+Strophe.escapeNode(nick)),
  1367. $item;
  1368. if ($presence.attr('type') === 'error') {
  1369. this.model.set('connected', false);
  1370. this.showErrorMessage($presence.find('error'), room);
  1371. } else {
  1372. this.model.set('connected', true);
  1373. this.showStatusMessages($presence, is_self);
  1374. if (!this.model.get('connected')) {
  1375. return true;
  1376. }
  1377. if ($presence.find("status[code='201']").length) {
  1378. // This is a new chatroom. We create an instant
  1379. // chatroom, and let the user manually set any
  1380. // configuration setting.
  1381. converse.connection.muc.createInstantRoom(room.name);
  1382. }
  1383. if (is_self) {
  1384. $item = $presence.find('item');
  1385. if ($item.length) {
  1386. if ($item.attr('affiliation') == 'owner') {
  1387. this.$el.find('a.configure-chatroom-button').show();
  1388. }
  1389. }
  1390. if ($presence.find("status[code='210']").length) {
  1391. // check if server changed our nick
  1392. this.model.set({'nick': Strophe.getResourceFromJid(from)});
  1393. }
  1394. }
  1395. }
  1396. return true;
  1397. },
  1398. onChatRoomMessage: function (message) {
  1399. var $message = $(message),
  1400. body = $message.children('body').text(),
  1401. jid = $message.attr('from'),
  1402. $chat_content = this.$el.find('.chat-content'),
  1403. resource = Strophe.getResourceFromJid(jid),
  1404. sender = resource && Strophe.unescapeNode(resource) || '',
  1405. delayed = $message.find('delay').length > 0,
  1406. subject = $message.children('subject').text(),
  1407. match, template, message_datetime, message_date, dates, isodate, stamp;
  1408. if (delayed) {
  1409. stamp = $message.find('delay').attr('stamp');
  1410. message_datetime = converse.parseISO8601(stamp);
  1411. } else {
  1412. message_datetime = new Date();
  1413. }
  1414. // If this message is on a different day than the one received
  1415. // prior, then indicate it on the chatbox.
  1416. dates = $chat_content.find("time").map(function(){return $(this).attr("datetime");}).get();
  1417. message_date = new Date(message_datetime.getTime());
  1418. message_date.setUTCHours(0,0,0,0);
  1419. isodate = converse.toISOString(message_date);
  1420. if (_.indexOf(dates, isodate) == -1) {
  1421. $chat_content.append(this.new_day_template({
  1422. isodate: isodate,
  1423. datestring: message_date.toString().substring(0,15)
  1424. }));
  1425. }
  1426. this.showStatusMessages($message);
  1427. if (subject) {
  1428. this.$el.find('.chatroom-topic').text(subject).attr('title', subject);
  1429. // # For translators: the %1$s and %2$s parts will get replaced by the user and topic text respectively
  1430. // # Example: Topic set by JC Brand to: Hello World!
  1431. $chat_content.append(this.info_template({'message': __('Topic set by %1$s to: %2$s', sender, subject)}));
  1432. }
  1433. if (!body) { return true; }
  1434. this.appendMessage($chat_content,
  1435. {'message': body,
  1436. 'sender': sender === this.model.get('nick') && 'me' || 'room',
  1437. 'fullname': sender,
  1438. 'time': converse.toISOString(message_datetime)
  1439. });
  1440. this.scrollDown();
  1441. return true;
  1442. },
  1443. occupant_template: _.template(
  1444. '<li class="{{role}}" '+
  1445. '{[ if (role === "moderator") { ]}' +
  1446. 'title="'+__('This user is a moderator')+'"' +
  1447. '{[ } ]}'+
  1448. '{[ if (role === "participant") { ]}' +
  1449. 'title="'+__('This user can send messages in this room')+'"' +
  1450. '{[ } ]}'+
  1451. '{[ if (role === "visitor") { ]}' +
  1452. 'title="'+__('This user can NOT send messages in this room')+'"' +
  1453. '{[ } ]}'+
  1454. '>{{nick}}</li>'
  1455. ),
  1456. onChatRoomRoster: function (roster, room) {
  1457. this.renderChatArea();
  1458. var controlboxview = converse.chatboxesview.views.controlbox,
  1459. roster_size = _.size(roster),
  1460. $participant_list = this.$el.find('.participant-list'),
  1461. participants = [], keys = _.keys(roster), i;
  1462. this.$el.find('.participant-list').empty();
  1463. for (i=0; i<roster_size; i++) {
  1464. participants.push(
  1465. this.occupant_template({
  1466. role: roster[keys[i]].role,
  1467. nick: Strophe.unescapeNode(keys[i])
  1468. }));
  1469. }
  1470. $participant_list.append(participants.join(""));
  1471. return true;
  1472. }
  1473. });
  1474. this.ChatBoxes = Backbone.Collection.extend({
  1475. model: converse.ChatBox,
  1476. onConnected: function () {
  1477. this.localStorage = new Backbone.LocalStorage(
  1478. hex_sha1('converse.chatboxes-'+converse.bare_jid));
  1479. if (!this.get('controlbox')) {
  1480. this.add({
  1481. id: 'controlbox',
  1482. box_id: 'controlbox'
  1483. });
  1484. } else {
  1485. this.get('controlbox').save();
  1486. }
  1487. // This will make sure the Roster is set up
  1488. this.get('controlbox').set({connected:true});
  1489. // Get cached chatboxes from localstorage
  1490. this.fetch({
  1491. add: true,
  1492. success: $.proxy(function (collection, resp) {
  1493. if (_.include(_.pluck(resp, 'id'), 'controlbox')) {
  1494. // If the controlbox was saved in localstorage, it must be visible
  1495. this.get('controlbox').set({visible:true}).save();
  1496. }
  1497. }, this)
  1498. });
  1499. },
  1500. createChatBox: function (attrs) {
  1501. var chatbox = this.get(attrs.jid);
  1502. if (chatbox) {
  1503. chatbox.trigger('show');
  1504. } else {
  1505. chatbox = this.create(attrs);
  1506. }
  1507. return chatbox;
  1508. },
  1509. messageReceived: function (message) {
  1510. var partner_jid, $message = $(message),
  1511. message_from = $message.attr('from');
  1512. if (message_from == converse.connection.jid) {
  1513. // FIXME: Forwarded messages should be sent to specific resources,
  1514. // not broadcasted
  1515. return true;
  1516. }
  1517. var $forwarded = $message.children('forwarded');
  1518. if ($forwarded.length) {
  1519. $message = $forwarded.children('message');
  1520. }
  1521. var from = Strophe.getBareJidFromJid(message_from),
  1522. to = Strophe.getBareJidFromJid($message.attr('to')),
  1523. resource, chatbox, roster_item;
  1524. if (from == converse.bare_jid) {
  1525. // I am the sender, so this must be a forwarded message...
  1526. partner_jid = to;
  1527. resource = Strophe.getResourceFromJid($message.attr('to'));
  1528. } else {
  1529. partner_jid = from;
  1530. resource = Strophe.getResourceFromJid(message_from);
  1531. }
  1532. chatbox = this.get(partner_jid);
  1533. roster_item = converse.roster.get(partner_jid);
  1534. if (!chatbox) {
  1535. chatbox = this.create({
  1536. 'id': partner_jid,
  1537. 'jid': partner_jid,
  1538. 'fullname': roster_item.get('fullname') || jid,
  1539. 'image_type': roster_item.get('image_type'),
  1540. 'image': roster_item.get('image'),
  1541. 'url': roster_item.get('url')
  1542. });
  1543. }
  1544. chatbox.messageReceived(message);
  1545. converse.roster.addResource(partner_jid, resource);
  1546. return true;
  1547. }
  1548. });
  1549. this.ChatBoxesView = Backbone.View.extend({
  1550. el: '#collective-xmpp-chat-data',
  1551. initialize: function () {
  1552. // boxesviewinit
  1553. this.views = {};
  1554. this.model.on("add", function (item) {
  1555. var view = this.views[item.get('id')];
  1556. if (!view) {
  1557. if (item.get('chatroom')) {
  1558. view = new converse.ChatRoomView({'model': item});
  1559. } else if (item.get('box_id') === 'controlbox') {
  1560. view = new converse.ControlBoxView({model: item});
  1561. view.render();
  1562. } else {
  1563. view = new converse.ChatBoxView({model: item});
  1564. }
  1565. this.views[item.get('id')] = view;
  1566. } else {
  1567. delete view.model; // Remove ref to old model to help garbage collection
  1568. view.model = item;
  1569. view.initialize();
  1570. if (item.get('id') !== 'controlbox') {
  1571. // FIXME: Why is it necessary to again append chatboxes?
  1572. view.$el.appendTo(this.$el);
  1573. }
  1574. }
  1575. }, this);
  1576. }
  1577. });
  1578. this.RosterItem = Backbone.Model.extend({
  1579. initialize: function (attributes, options) {
  1580. var jid = attributes.jid;
  1581. if (!attributes.fullname) {
  1582. attributes.fullname = jid;
  1583. }
  1584. var attrs = _.extend({
  1585. 'id': jid,
  1586. 'user_id': Strophe.getNodeFromJid(jid),
  1587. 'resources': [],
  1588. 'status': ''
  1589. }, attributes);
  1590. attrs.sorted = false;
  1591. attrs.chat_status = 'offline';
  1592. this.set(attrs);
  1593. }
  1594. });
  1595. this.RosterItemView = Backbone.View.extend({
  1596. tagName: 'dd',
  1597. events: {
  1598. "click .accept-xmpp-request": "acceptRequest",
  1599. "click .decline-xmpp-request": "declineRequest",
  1600. "click .open-chat": "openChat",
  1601. "click .remove-xmpp-contact": "removeContact"
  1602. },
  1603. openChat: function (ev) {
  1604. ev.preventDefault();
  1605. converse.chatboxes.createChatBox({
  1606. 'id': this.model.get('jid'),
  1607. 'jid': this.model.get('jid'),
  1608. 'fullname': this.model.get('fullname'),
  1609. 'image_type': this.model.get('image_type'),
  1610. 'image': this.model.get('image'),
  1611. 'url': this.model.get('url'),
  1612. 'status': this.model.get('status')
  1613. });
  1614. },
  1615. removeContact: function (ev) {
  1616. ev.preventDefault();
  1617. var result = confirm("Are you sure you want to remove this contact?");
  1618. if (result === true) {
  1619. var bare_jid = this.model.get('jid');
  1620. converse.connection.roster.remove(bare_jid, function (iq) {
  1621. converse.connection.roster.unauthorize(bare_jid);
  1622. converse.rosterview.model.remove(bare_jid);
  1623. });
  1624. }
  1625. },
  1626. acceptRequest: function (ev) {
  1627. var jid = this.model.get('jid');
  1628. converse.connection.roster.authorize(jid);
  1629. converse.connection.roster.add(jid, this.model.get('fullname'), [], function (iq) {
  1630. converse.connection.roster.subscribe(jid, null, converse.xmppstatus.get('fullname'));
  1631. });
  1632. ev.preventDefault();
  1633. },
  1634. declineRequest: function (ev) {
  1635. ev.preventDefault();
  1636. converse.connection.roster.unauthorize(this.model.get('jid'));
  1637. this.model.destroy();
  1638. },
  1639. template: _.template(
  1640. '<a class="open-chat" title="'+__('Click to chat with this contact')+'" href="#">{{ fullname }}</a>' +
  1641. '<a class="remove-xmpp-contact" title="'+__('Click to remove this contact')+'" href="#"></a>'),
  1642. pending_template: _.template(
  1643. '<span>{{ fullname }}</span>' +
  1644. '<a class="remove-xmpp-contact" title="'+__('Click to remove this contact')+'" href="#"></a>'),
  1645. request_template: _.template('<div>{{ fullname }}</div>' +
  1646. '<button type="button" class="accept-xmpp-request">' +
  1647. 'Accept</button>' +
  1648. '<button type="button" class="decline-xmpp-request">' +
  1649. 'Decline</button>' +
  1650. ''),
  1651. render: function () {
  1652. var item = this.model,
  1653. ask = item.get('ask'),
  1654. subscription = item.get('subscription');
  1655. this.$el.addClass(item.get('chat_status'));
  1656. if (ask === 'subscribe') {
  1657. this.$el.addClass('pending-xmpp-contact');
  1658. this.$el.html(this.pending_template(item.toJSON()));
  1659. } else if (ask === 'request') {
  1660. this.$el.addClass('requesting-xmpp-contact');
  1661. this.$el.html(this.request_template(item.toJSON()));
  1662. converse.showControlBox();
  1663. } else if (subscription === 'both' || subscription === 'to') {
  1664. this.$el.addClass('current-xmpp-contact');
  1665. this.$el.html(this.template(item.toJSON()));
  1666. }
  1667. return this;
  1668. },
  1669. initialize: function () {
  1670. this.options.model.on('change', function (item, changed) {
  1671. if (_.has(item.changed, 'chat_status')) {
  1672. this.$el.attr('class', item.changed.chat_status);
  1673. }
  1674. }, this);
  1675. }
  1676. });
  1677. this.getVCard = function (jid, callback, errback) {
  1678. converse.connection.vcard.get($.proxy(function (iq) {
  1679. $vcard = $(iq).find('vCard');
  1680. var fullname = $vcard.find('FN').text(),
  1681. img = $vcard.find('BINVAL').text(),
  1682. img_type = $vcard.find('TYPE').text(),
  1683. url = $vcard.find('URL').text();
  1684. var rosteritem = converse.roster.get(jid);
  1685. if (rosteritem) {
  1686. rosteritem.save({
  1687. 'fullname': fullname || jid,
  1688. 'image_type': img_type,
  1689. 'image': img,
  1690. 'url': url,
  1691. 'vcard_updated': converse.toISOString(new Date())
  1692. });
  1693. }
  1694. callback(jid, fullname, img, img_type, url);
  1695. }, this), jid, errback);
  1696. };
  1697. this.RosterItems = Backbone.Collection.extend({
  1698. model: converse.RosterItem,
  1699. comparator : function (rosteritem) {
  1700. var chat_status = rosteritem.get('chat_status'),
  1701. rank = 4;
  1702. switch(chat_status) {
  1703. case 'offline':
  1704. rank = 0;
  1705. break;
  1706. case 'unavailable':
  1707. rank = 1;
  1708. break;
  1709. case 'xa':
  1710. rank = 2;
  1711. break;
  1712. case 'away':
  1713. rank = 3;
  1714. break;
  1715. case 'dnd':
  1716. rank = 4;
  1717. break;
  1718. case 'online':
  1719. rank = 5;
  1720. break;
  1721. }
  1722. return rank;
  1723. },
  1724. subscribeToSuggestedItems: function (msg) {
  1725. $(msg).find('item').each(function () {
  1726. var $this = $(this),
  1727. jid = $this.attr('jid'),
  1728. action = $this.attr('action'),
  1729. fullname = $this.attr('name');
  1730. if (action === 'add') {
  1731. converse.connection.roster.add(jid, fullname, [], function (iq) {
  1732. converse.connection.roster.subscribe(jid, null, converse.xmppstatus.get('fullname'));
  1733. });
  1734. }
  1735. });
  1736. return true;
  1737. },
  1738. isSelf: function (jid) {
  1739. return (Strophe.getBareJidFromJid(jid) === Strophe.getBareJidFromJid(converse.connection.jid));
  1740. },
  1741. getItem: function (id) {
  1742. return Backbone.Collection.prototype.get.call(this, id);
  1743. },
  1744. addResource: function (bare_jid, resource) {
  1745. var item = this.getItem(bare_jid),
  1746. resources;
  1747. if (item) {
  1748. resources = item.get('resources');
  1749. if (resources) {
  1750. if (_.indexOf(resources, resource) == -1) {
  1751. resources.push(resource);
  1752. item.set({'resources': resources});
  1753. }
  1754. } else {
  1755. item.set({'resources': [resource]});
  1756. }
  1757. }
  1758. },
  1759. removeResource: function (bare_jid, resource) {
  1760. var item = this.getItem(bare_jid),
  1761. resources,
  1762. idx;
  1763. if (item) {
  1764. resources = item.get('resources');
  1765. idx = _.indexOf(resources, resource);
  1766. if (idx !== -1) {
  1767. resources.splice(idx, 1);
  1768. item.set({'resources': resources});
  1769. return resources.length;
  1770. }
  1771. }
  1772. return 0;
  1773. },
  1774. subscribeBack: function (jid) {
  1775. var bare_jid = Strophe.getBareJidFromJid(jid);
  1776. if (converse.connection.roster.findItem(bare_jid)) {
  1777. converse.connection.roster.authorize(bare_jid);
  1778. converse.connection.roster.subscribe(jid, null, converse.xmppstatus.get('fullname'));
  1779. } else {
  1780. converse.connection.roster.add(jid, '', [], function (iq) {
  1781. converse.connection.roster.authorize(bare_jid);
  1782. converse.connection.roster.subscribe(jid, null, converse.xmppstatus.get('fullname'));
  1783. });
  1784. }
  1785. },
  1786. unsubscribe: function (jid) {
  1787. /* Upon receiving the presence stanza of type "unsubscribed",
  1788. * the user SHOULD acknowledge receipt of that subscription state
  1789. * notification by sending a presence stanza of type "unsubscribe"
  1790. * this step lets the user's server know that it MUST no longer
  1791. * send notification of the subscription state change to the user.
  1792. */
  1793. converse.xmppstatus.sendPresence('unsubscribe');
  1794. if (converse.connection.roster.findItem(jid)) {
  1795. converse.connection.roster.remove(jid, function (iq) {
  1796. converse.rosterview.model.remove(jid);
  1797. });
  1798. }
  1799. },
  1800. getNumOnlineContacts: function () {
  1801. var count = 0,
  1802. models = this.models,
  1803. models_length = models.length,
  1804. i;
  1805. for (i=0; i<models_length; i++) {
  1806. if (_.indexOf(['offline', 'unavailable'], models[i].get('chat_status')) === -1) {
  1807. count++;
  1808. }
  1809. }
  1810. return count;
  1811. },
  1812. cleanCache: function (items) {
  1813. /* The localstorage cache containing roster contacts might contain
  1814. * some contacts that aren't actually in our roster anymore. We
  1815. * therefore need to remove them now.
  1816. */
  1817. var id, i,
  1818. roster_ids = [];
  1819. for (i=0; i < items.length; ++i) {
  1820. roster_ids.push(items[i].jid);
  1821. }
  1822. for (i=0; i < this.models.length; ++i) {
  1823. id = this.models[i].get('id');
  1824. if (_.indexOf(roster_ids, id) === -1) {
  1825. this.getItem(id).destroy();
  1826. }
  1827. }
  1828. },
  1829. rosterHandler: function (items) {
  1830. this.cleanCache(items);
  1831. _.each(items, function (item, index, items) {
  1832. if (this.isSelf(item.jid)) { return; }
  1833. var model = this.getItem(item.jid);
  1834. if (!model) {
  1835. is_last = false;
  1836. if (index === (items.length-1)) { is_last = true; }
  1837. this.create({
  1838. jid: item.jid,
  1839. subscription: item.subscription,
  1840. ask: item.ask,
  1841. fullname: item.name || item.jid,
  1842. is_last: is_last
  1843. });
  1844. } else {
  1845. if ((item.subscription === 'none') && (item.ask === null)) {
  1846. // This user is no longer in our roster
  1847. model.destroy();
  1848. } else if (model.get('subscription') !== item.subscription || model.get('ask') !== item.ask) {
  1849. // only modify model attributes if they are different from the
  1850. // ones that were already set when the rosterItem was added
  1851. model.set({'subscription': item.subscription, 'ask': item.ask});
  1852. model.save();
  1853. }
  1854. }
  1855. }, this);
  1856. },
  1857. presenceHandler: function (presence) {
  1858. var $presence = $(presence),
  1859. presence_type = $presence.attr('type');
  1860. if (presence_type === 'error') {
  1861. // TODO
  1862. // error presence stanzas don't necessarily have a 'from' attr.
  1863. return true;
  1864. }
  1865. var jid = $presence.attr('from'),
  1866. bare_jid = Strophe.getBareJidFromJid(jid),
  1867. resource = Strophe.getResourceFromJid(jid),
  1868. $show = $presence.find('show'),
  1869. chat_status = $show.text() || 'online',
  1870. status_message = $presence.find('status'),
  1871. item;
  1872. if (this.isSelf(bare_jid)) {
  1873. if ((converse.connection.jid !== jid)&&(presence_type !== 'unavailable')) {
  1874. // Another resource has changed it's status, we'll update ours as well.
  1875. // FIXME: We should ideally differentiate between converse.js using
  1876. // resources and other resources (i.e Pidgin etc.)
  1877. converse.xmppstatus.save({'status': chat_status});
  1878. }
  1879. return true;
  1880. } else if (($presence.find('x').attr('xmlns') || '').indexOf(Strophe.NS.MUC) === 0) {
  1881. return true; // Ignore MUC
  1882. }
  1883. item = this.getItem(bare_jid);
  1884. if (item && (status_message.text() != item.get('status'))) {
  1885. item.save({'status': status_message.text()});
  1886. }
  1887. if ((presence_type === 'subscribed') || (presence_type === 'unsubscribe')) {
  1888. return true;
  1889. } else if (presence_type === 'subscribe') {
  1890. if (converse.auto_subscribe) {
  1891. if ((!item) || (item.get('subscription') != 'to')) {
  1892. this.subscribeBack(jid);
  1893. } else {
  1894. converse.connection.roster.authorize(bare_jid);
  1895. }
  1896. } else {
  1897. if ((item) && (item.get('subscription') != 'none')) {
  1898. converse.connection.roster.authorize(bare_jid);
  1899. } else {
  1900. converse.getVCard(
  1901. bare_jid,
  1902. $.proxy(function (jid, fullname, img, img_type, url) {
  1903. this.add({
  1904. jid: bare_jid,
  1905. subscription: 'none',
  1906. ask: 'request',
  1907. fullname: fullname,
  1908. image: img,
  1909. image_type: img_type,
  1910. url: url,
  1911. is_last: true
  1912. });
  1913. }, this),
  1914. $.proxy(function (jid, fullname, img, img_type, url) {
  1915. console.log("Error while retrieving vcard");
  1916. this.add({jid: bare_jid, subscription: 'none', ask: 'request', fullname: jid, is_last: true});
  1917. }, this)
  1918. );
  1919. }
  1920. }
  1921. } else if (presence_type === 'unsubscribed') {
  1922. this.unsubscribe(bare_jid);
  1923. } else if (presence_type === 'unavailable') {
  1924. if (this.removeResource(bare_jid, resource) === 0) {
  1925. if (item) {
  1926. item.set({'chat_status': 'offline'});
  1927. }
  1928. }
  1929. } else if (item) {
  1930. // presence_type is undefined
  1931. this.addResource(bare_jid, resource);
  1932. item.set({'chat_status': chat_status});
  1933. }
  1934. return true;
  1935. }
  1936. });
  1937. this.RosterView = Backbone.View.extend({
  1938. tagName: 'dl',
  1939. id: 'converse-roster',
  1940. rosteritemviews: {},
  1941. removeRosterItem: function (item) {
  1942. var view = this.rosteritemviews[item.id];
  1943. if (view) {
  1944. view.$el.remove();
  1945. delete this.rosteritemviews[item.id];
  1946. this.render();
  1947. }
  1948. },
  1949. initialize: function () {
  1950. this.model.on("add", function (item) {
  1951. var view = new converse.RosterItemView({model: item});
  1952. this.rosteritemviews[item.id] = view;
  1953. this.render(item);
  1954. }, this);
  1955. this.model.on('change', function (item, changed) {
  1956. if ((_.size(item.changed) === 1) && _.contains(_.keys(item.changed), 'sorted')) {
  1957. return;
  1958. }
  1959. this.updateChatBox(item, changed);
  1960. this.render(item);
  1961. }, this);
  1962. this.model.on("remove", function (item) { this.removeRosterItem(item); }, this);
  1963. this.model.on("destroy", function (item) { this.removeRosterItem(item); }, this);
  1964. this.$el.hide().html(this.template());
  1965. this.model.fetch({
  1966. add: true,
  1967. success: function (model, resp, options) {
  1968. if (resp.length === 0) {
  1969. // The presence stanza is sent out once all
  1970. // roster contacts have been added and rendered.
  1971. // See RosterView's render method.
  1972. //
  1973. // If there aren't any roster contacts, we still
  1974. // want to send a presence stanza, so we do it here.
  1975. converse.xmppstatus.sendPresence();
  1976. }
  1977. },
  1978. }); // Get the cached roster items from localstorage
  1979. },
  1980. updateChatBox: function (item, changed) {
  1981. var chatbox = converse.chatboxes.get(item.get('jid')),
  1982. changes = {};
  1983. if (!chatbox) { return; }
  1984. if (_.has(item.changed, 'chat_status')) {
  1985. changes.chat_status = item.get('chat_status');
  1986. }
  1987. if (_.has(item.changed, 'status')) {
  1988. changes.status = item.get('status');
  1989. }
  1990. chatbox.save(changes);
  1991. },
  1992. template: _.template('<dt id="xmpp-contact-requests">'+__('Contact requests')+'</dt>' +
  1993. '<dt id="xmpp-contacts">'+__('My contacts')+'</dt>' +
  1994. '<dt id="pending-xmpp-contacts">'+__('Pending contacts')+'</dt>'),
  1995. render: function (item) {
  1996. var $my_contacts = this.$el.find('#xmpp-contacts'),
  1997. $contact_requests = this.$el.find('#xmpp-contact-requests'),
  1998. $pending_contacts = this.$el.find('#pending-xmpp-contacts'),
  1999. $count, presence_change;
  2000. if (item) {
  2001. var jid = item.id,
  2002. view = this.rosteritemviews[item.id],
  2003. ask = item.get('ask'),
  2004. subscription = item.get('subscription'),
  2005. crit = {order:'asc'};
  2006. if (ask === 'subscribe') {
  2007. $pending_contacts.after(view.render().el);
  2008. $pending_contacts.after($pending_contacts.siblings('dd.pending-xmpp-contact').tsort(crit));
  2009. } else if (ask === 'request') {
  2010. $contact_requests.after(view.render().el);
  2011. $contact_requests.after($contact_requests.siblings('dd.requesting-xmpp-contact').tsort(crit));
  2012. } else if (subscription === 'both' || subscription === 'to') {
  2013. if (!item.get('sorted')) {
  2014. // this attribute will be true only after all of the elements have been added on the page
  2015. // at this point all offline
  2016. $my_contacts.after(view.render().el);
  2017. }
  2018. else {
  2019. // just by calling render will be enough to change the icon of the existing item without
  2020. // having to reinsert it and the sort will come from the presence change
  2021. view.render();
  2022. }
  2023. }
  2024. presence_change = view.model.changed.chat_status;
  2025. if (presence_change) {
  2026. // resort all items only if the model has changed it's chat_status as this render
  2027. // is also triggered when the resource is changed which always comes before the presence change
  2028. // therefore we avoid resorting when the change doesn't affect the position of the item
  2029. $my_contacts.after($my_contacts.siblings('dd.current-xmpp-contact.offline').tsort('a', crit));
  2030. $my_contacts.after($my_contacts.siblings('dd.current-xmpp-contact.unavailable').tsort('a', crit));
  2031. $my_contacts.after($my_contacts.siblings('dd.current-xmpp-contact.away').tsort('a', crit));
  2032. $my_contacts.after($my_contacts.siblings('dd.current-xmpp-contact.dnd').tsort('a', crit));
  2033. $my_contacts.after($my_contacts.siblings('dd.current-xmpp-contact.online').tsort('a', crit));
  2034. }
  2035. if (item.get('is_last') && !item.get('sorted')) {
  2036. // this will be true after all of the roster items have been added with the default
  2037. // options where all of the items are offline and now we can show the rosterView
  2038. item.set('sorted', true);
  2039. this.initialSort();
  2040. this.$el.show();
  2041. converse.xmppstatus.sendPresence();
  2042. }
  2043. }
  2044. // Hide the headings if there are no contacts under them
  2045. _.each([$my_contacts, $contact_requests, $pending_contacts], function (h) {
  2046. if (h.nextUntil('dt').length) {
  2047. h.show();
  2048. }
  2049. else {
  2050. h.hide();
  2051. }
  2052. });
  2053. $count = $('#online-count');
  2054. $count.text('('+this.model.getNumOnlineContacts()+')');
  2055. if (!$count.is(':visible')) {
  2056. $count.show();
  2057. }
  2058. return this;
  2059. },
  2060. initialSort: function () {
  2061. var $my_contacts = this.$el.find('#xmpp-contacts'),
  2062. crit = {order:'asc'};
  2063. $my_contacts.after($my_contacts.siblings('dd.current-xmpp-contact.offline').tsort('a', crit));
  2064. $my_contacts.after($my_contacts.siblings('dd.current-xmpp-contact.unavailable').tsort('a', crit));
  2065. }
  2066. });
  2067. this.XMPPStatus = Backbone.Model.extend({
  2068. initialize: function () {
  2069. this.set({
  2070. 'status' : this.get('status') || 'online',
  2071. });
  2072. this.on('change', $.proxy(function () {
  2073. if (this.get('fullname') === undefined) {
  2074. converse.getVCard(
  2075. null, // No 'to' attr when getting one's own vCard
  2076. $.proxy(function (jid, fullname, image, image_type, url) {
  2077. this.save({'fullname': fullname});
  2078. }, this)
  2079. );
  2080. }
  2081. }, this));
  2082. },
  2083. sendPresence: function (type) {
  2084. if (type === undefined) {
  2085. type = this.get('status') || 'online';
  2086. }
  2087. var status_message = this.get('status_message'),
  2088. presence;
  2089. // Most of these presence types are actually not explicitly sent,
  2090. // but I add all of them here fore reference and future proofing.
  2091. if ((type === 'unavailable') ||
  2092. (type === 'probe') ||
  2093. (type === 'error') ||
  2094. (type === 'unsubscribe') ||
  2095. (type === 'unsubscribed') ||
  2096. (type === 'subscribe') ||
  2097. (type === 'subscribed')) {
  2098. presence = $pres({'type':type});
  2099. } else {
  2100. if (type === 'online') {
  2101. presence = $pres();
  2102. } else {
  2103. presence = $pres().c('show').t(type).up();
  2104. }
  2105. if (status_message) {
  2106. presence.c('status').t(status_message);
  2107. }
  2108. }
  2109. converse.connection.send(presence);
  2110. },
  2111. setStatus: function (value) {
  2112. this.sendPresence(value);
  2113. this.save({'status': value});
  2114. },
  2115. setStatusMessage: function (status_message) {
  2116. converse.connection.send($pres().c('show').t(this.get('status')).up().c('status').t(status_message));
  2117. this.save({'status_message': status_message});
  2118. }
  2119. });
  2120. this.XMPPStatusView = Backbone.View.extend({
  2121. el: "span#xmpp-status-holder",
  2122. events: {
  2123. "click a.choose-xmpp-status": "toggleOptions",
  2124. "click #fancy-xmpp-status-select a.change-xmpp-status-message": "renderStatusChangeForm",
  2125. "submit #set-custom-xmpp-status": "setStatusMessage",
  2126. "click .dropdown dd ul li a": "setStatus"
  2127. },
  2128. toggleOptions: function (ev) {
  2129. ev.preventDefault();
  2130. $(ev.target).parent().parent().siblings('dd').find('ul').toggle('fast');
  2131. },
  2132. change_status_message_template: _.template(
  2133. '<form id="set-custom-xmpp-status">' +
  2134. '<input type="text" class="custom-xmpp-status" {{ status_message }}"'+
  2135. 'placeholder="'+__('Custom status')+'"/>' +
  2136. '<button type="submit">'+__('Save')+'</button>' +
  2137. '</form>'),
  2138. status_template: _.template(
  2139. '<div class="xmpp-status">' +
  2140. '<a class="choose-xmpp-status {{ chat_status }}" data-value="{{status_message}}" href="#" title="'+__('Click to change your chat status')+'">' +
  2141. '{{ status_message }}' +
  2142. '</a>' +
  2143. '<a class="change-xmpp-status-message" href="#" title="'+__('Click here to write a custom status message')+'"></a>' +
  2144. '</div>'),
  2145. renderStatusChangeForm: function (ev) {
  2146. ev.preventDefault();
  2147. var status_message = this.model.get('status') || 'offline';
  2148. var input = this.change_status_message_template({'status_message': status_message});
  2149. this.$el.find('.xmpp-status').replaceWith(input);
  2150. this.$el.find('.custom-xmpp-status').focus().focus();
  2151. },
  2152. setStatusMessage: function (ev) {
  2153. ev.preventDefault();
  2154. var status_message = $(ev.target).find('input').val();
  2155. if (status_message === "") {
  2156. }
  2157. this.model.setStatusMessage(status_message);
  2158. },
  2159. setStatus: function (ev) {
  2160. ev.preventDefault();
  2161. var $el = $(ev.target),
  2162. value = $el.attr('data-value');
  2163. this.model.setStatus(value);
  2164. this.$el.find(".dropdown dd ul").hide();
  2165. },
  2166. getPrettyStatus: function (stat) {
  2167. if (stat === 'chat') {
  2168. pretty_status = __('online');
  2169. } else if (stat === 'dnd') {
  2170. pretty_status = __('busy');
  2171. } else if (stat === 'xa') {
  2172. pretty_status = __('away for long');
  2173. } else if (stat === 'away') {
  2174. pretty_status = __('away');
  2175. } else {
  2176. pretty_status = __(stat) || __('online'); // XXX: Is 'online' the right default choice here?
  2177. }
  2178. return pretty_status;
  2179. },
  2180. updateStatusUI: function (model) {
  2181. if (!(_.has(model.changed, 'status')) && !(_.has(model.changed, 'status_message'))) {
  2182. return;
  2183. }
  2184. var stat = model.get('status');
  2185. // # For translators: the %1$s part gets replaced with the status
  2186. // # Example, I am online
  2187. var status_message = model.get('status_message') || __("I am %1$s", this.getPrettyStatus(stat));
  2188. this.$el.find('#fancy-xmpp-status-select').html(
  2189. this.status_template({
  2190. 'chat_status': stat,
  2191. 'status_message': status_message
  2192. }));
  2193. },
  2194. choose_template: _.template(
  2195. '<dl id="target" class="dropdown">' +
  2196. '<dt id="fancy-xmpp-status-select" class="fancy-dropdown"></dt>' +
  2197. '<dd><ul></ul></dd>' +
  2198. '</dl>'),
  2199. option_template: _.template(
  2200. '<li>' +
  2201. '<a href="#" class="{{ value }}" data-value="{{ value }}">{{ text }}</a>' +
  2202. '</li>'),
  2203. initialize: function () {
  2204. this.model.on("change", this.updateStatusUI, this);
  2205. },
  2206. render: function () {
  2207. // Replace the default dropdown with something nicer
  2208. var $select = this.$el.find('select#select-xmpp-status'),
  2209. chat_status = this.model.get('status') || 'offline',
  2210. options = $('option', $select),
  2211. $options_target,
  2212. options_list = [],
  2213. that = this;
  2214. this.$el.html(this.choose_template());
  2215. this.$el.find('#fancy-xmpp-status-select')
  2216. .html(this.status_template({
  2217. 'status_message': this.model.get('status_message') || __("I am %1$s", this.getPrettyStatus(chat_status)),
  2218. 'chat_status': chat_status
  2219. }));
  2220. // iterate through all the <option> elements and add option values
  2221. options.each(function(){
  2222. options_list.push(that.option_template({'value': $(this).val(),
  2223. 'text': this.text
  2224. }));
  2225. });
  2226. $options_target = this.$el.find("#target dd ul").hide();
  2227. $options_target.append(options_list.join(''));
  2228. $select.remove();
  2229. return this;
  2230. }
  2231. });
  2232. this.Feature = Backbone.Model.extend();
  2233. this.Features = Backbone.Collection.extend({
  2234. /* Service Discovery
  2235. * -----------------
  2236. * This collection stores Feature Models, representing features
  2237. * provided by available XMPP entities (e.g. servers)
  2238. * See XEP-0030 for more details: http://xmpp.org/extensions/xep-0030.html
  2239. * All features are shown here: http://xmpp.org/registrar/disco-features.html
  2240. */
  2241. model: converse.Feature,
  2242. initialize: function () {
  2243. this.localStorage = new Backbone.LocalStorage(
  2244. hex_sha1('converse.features'+converse.bare_jid));
  2245. if (this.localStorage.records.length === 0) {
  2246. // localStorage is empty, so we've likely never queried this
  2247. // domain for features yet
  2248. converse.connection.disco.info(converse.domain, null, $.proxy(this.onInfo, this));
  2249. converse.connection.disco.items(converse.domain, null, $.proxy(this.onItems, this));
  2250. } else {
  2251. this.fetch({add:true});
  2252. }
  2253. },
  2254. onItems: function (stanza) {
  2255. $(stanza).find('query item').each($.proxy(function (idx, item) {
  2256. converse.connection.disco.info(
  2257. $(item).attr('jid'),
  2258. null,
  2259. $.proxy(this.onInfo, this));
  2260. }, this));
  2261. },
  2262. onInfo: function (stanza) {
  2263. var $stanza = $(stanza);
  2264. if (($stanza.find('identity[category=server][type=im]').length === 0) &&
  2265. ($stanza.find('identity[category=conference][type=text]').length === 0)) {
  2266. // This isn't an IM server component
  2267. return;
  2268. }
  2269. $stanza.find('feature').each($.proxy(function (idx, feature) {
  2270. this.create({
  2271. 'var': $(feature).attr('var'),
  2272. 'from': $stanza.attr('from')
  2273. });
  2274. }, this));
  2275. }
  2276. });
  2277. this.LoginPanel = Backbone.View.extend({
  2278. tagName: 'div',
  2279. id: "login-dialog",
  2280. events: {
  2281. 'submit form#converse-login': 'authenticate'
  2282. },
  2283. tab_template: _.template(
  2284. '<li><a class="current" href="#login">'+__('Sign in')+'</a></li>'),
  2285. template: _.template(
  2286. '<form id="converse-login">' +
  2287. '<label>'+__('XMPP/Jabber Username:')+'</label>' +
  2288. '<input type="text" id="jid">' +
  2289. '<label>'+__('Password:')+'</label>' +
  2290. '<input type="password" id="password">' +
  2291. '<input class="login-submit" type="submit" value="'+__('Log In')+'">' +
  2292. '</form">'),
  2293. bosh_url_input: _.template(
  2294. '<label>'+__('BOSH Service URL:')+'</label>' +
  2295. '<input type="text" id="bosh_service_url">'),
  2296. connect: function ($form, jid, password) {
  2297. var button = null,
  2298. connection = new Strophe.Connection(converse.bosh_service_url);
  2299. if ($form) {
  2300. $button = $form.find('input[type=submit]');
  2301. $button.hide().after('<span class="spinner login-submit"/>');
  2302. }
  2303. connection.connect(jid, password, $.proxy(function (status, message) {
  2304. if (status === Strophe.Status.CONNECTED) {
  2305. console.log(__('Connected'));
  2306. converse.onConnected(connection);
  2307. } else if (status === Strophe.Status.DISCONNECTED) {
  2308. if ($button) { $button.show().siblings('span').remove(); }
  2309. converse.giveFeedback(__('Disconnected'), 'error');
  2310. this.connect(null, connection.jid, connection.pass);
  2311. } else if (status === Strophe.Status.Error) {
  2312. if ($button) { $button.show().siblings('span').remove(); }
  2313. converse.giveFeedback(__('Error'), 'error');
  2314. } else if (status === Strophe.Status.CONNECTING) {
  2315. converse.giveFeedback(__('Connecting'));
  2316. } else if (status === Strophe.Status.CONNFAIL) {
  2317. if ($button) { $button.show().siblings('span').remove(); }
  2318. converse.giveFeedback(__('Connection Failed'), 'error');
  2319. } else if (status === Strophe.Status.AUTHENTICATING) {
  2320. converse.giveFeedback(__('Authenticating'));
  2321. } else if (status === Strophe.Status.AUTHFAIL) {
  2322. if ($button) { $button.show().siblings('span').remove(); }
  2323. converse.giveFeedback(__('Authentication Failed'), 'error');
  2324. } else if (status === Strophe.Status.DISCONNECTING) {
  2325. converse.giveFeedback(__('Disconnecting'), 'error');
  2326. } else if (status === Strophe.Status.ATTACHED) {
  2327. console.log(__('Attached'));
  2328. }
  2329. }, this));
  2330. },
  2331. initialize: function (cfg) {
  2332. cfg.$parent.append(this.$el.html(this.template()));
  2333. this.$tabs = cfg.$parent.parent().find('#controlbox-tabs');
  2334. },
  2335. render: function () {
  2336. this.$tabs.append(this.tab_template());
  2337. this.$el.find('input#jid').focus();
  2338. return this;
  2339. },
  2340. authenticate: function (ev) {
  2341. ev.preventDefault();
  2342. var $form = $(ev.target),
  2343. $jid_input = $form.find('input#jid'),
  2344. jid = $jid_input.val(),
  2345. $pw_input = $form.find('input#password'),
  2346. password = $pw_input.val(),
  2347. $bsu_input = null,
  2348. errors = false;
  2349. if (! converse.bosh_service_url) {
  2350. $bsu_input = $form.find('input#bosh_service_url');
  2351. converse.bosh_service_url = $bsu_input.val();
  2352. if (! converse.bosh_service_url) {
  2353. errors = true;
  2354. $bsu_input.addClass('error');
  2355. }
  2356. }
  2357. if (! jid) {
  2358. errors = true;
  2359. $jid_input.addClass('error');
  2360. }
  2361. if (! password) {
  2362. errors = true;
  2363. $pw_input.addClass('error');
  2364. }
  2365. if (errors) { return; }
  2366. this.connect($form, jid, password);
  2367. },
  2368. remove: function () {
  2369. this.$tabs.empty();
  2370. this.$el.parent().empty();
  2371. }
  2372. });
  2373. this.showControlBox = function () {
  2374. var controlbox = this.chatboxes.get('controlbox');
  2375. if (!controlbox) {
  2376. this.chatboxes.add({
  2377. id: 'controlbox',
  2378. box_id: 'controlbox',
  2379. visible: true
  2380. });
  2381. if (this.connection) {
  2382. this.chatboxes.get('controlbox').save();
  2383. }
  2384. } else {
  2385. controlbox.trigger('show');
  2386. }
  2387. };
  2388. this.toggleControlBox = function () {
  2389. if ($("div#controlbox").is(':visible')) {
  2390. var controlbox = this.chatboxes.get('controlbox');
  2391. if (this.connection) {
  2392. controlbox.destroy();
  2393. } else {
  2394. controlbox.trigger('hide');
  2395. }
  2396. } else {
  2397. this.showControlBox();
  2398. }
  2399. };
  2400. this.giveFeedback = function (message, klass) {
  2401. $('.conn-feedback').text(message);
  2402. $('.conn-feedback').attr('class', 'conn-feedback');
  2403. if (klass) {
  2404. $('.conn-feedback').addClass(klass);
  2405. }
  2406. };
  2407. this.initStatus = function (callback) {
  2408. this.xmppstatus = new this.XMPPStatus();
  2409. var id = hex_sha1('converse.xmppstatus-'+this.bare_jid);
  2410. this.xmppstatus.id = id; // This appears to be necessary for backbone.localStorage
  2411. this.xmppstatus.localStorage = new Backbone.LocalStorage(id);
  2412. this.xmppstatus.fetch({success: callback, error: callback});
  2413. };
  2414. this.initRoster = function () {
  2415. // Set up the roster
  2416. this.roster = new this.RosterItems();
  2417. this.roster.localStorage = new Backbone.LocalStorage(
  2418. hex_sha1('converse.rosteritems-'+this.bare_jid));
  2419. this.connection.roster.registerCallback(
  2420. $.proxy(this.roster.rosterHandler, this.roster),
  2421. null, 'presence', null);
  2422. this.rosterview = new this.RosterView({'model':this.roster});
  2423. }
  2424. this.onConnected = function (connection, callback) {
  2425. this.connection = connection;
  2426. this.connection.xmlInput = function (body) { console.log(body); };
  2427. this.connection.xmlOutput = function (body) { console.log(body); };
  2428. this.bare_jid = Strophe.getBareJidFromJid(this.connection.jid);
  2429. this.domain = Strophe.getDomainFromJid(this.connection.jid);
  2430. this.features = new this.Features();
  2431. this.initStatus($.proxy(function () {
  2432. this.initRoster();
  2433. this.chatboxes.onConnected();
  2434. this.connection.addHandler(
  2435. $.proxy(this.roster.subscribeToSuggestedItems, this.roster),
  2436. 'http://jabber.org/protocol/rosterx', 'message', null);
  2437. this.connection.roster.get($.proxy(function (a) {
  2438. this.connection.addHandler(
  2439. $.proxy(function (presence) {
  2440. this.presenceHandler(presence);
  2441. return true;
  2442. }, this.roster), null, 'presence', null);
  2443. this.connection.addHandler(
  2444. $.proxy(function (message) {
  2445. this.chatboxes.messageReceived(message);
  2446. return true;
  2447. }, this), null, 'message', 'chat');
  2448. }, this));
  2449. $(window).on("blur focus", $.proxy(function(e) {
  2450. if ((this.windowState != e.type) && (e.type == 'focus')) {
  2451. converse.clearMsgCounter();
  2452. }
  2453. this.windowState = e.type;
  2454. },this));
  2455. this.giveFeedback(__('Online Contacts'));
  2456. if (callback) {
  2457. callback();
  2458. }
  2459. }, this));
  2460. };
  2461. // This is the end of the initialize method.
  2462. this.chatboxes = new this.ChatBoxes();
  2463. this.chatboxesview = new this.ChatBoxesView({model: this.chatboxes});
  2464. $('.toggle-online-users').bind(
  2465. 'click',
  2466. $.proxy(function (e) {
  2467. e.preventDefault(); this.toggleControlBox();
  2468. }, this)
  2469. );
  2470. if (this.show_controlbox_by_default) {
  2471. this.toggleControlBox();
  2472. }
  2473. };
  2474. return converse;
  2475. }));