converse.js 121 KB

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