converse.js 155 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432
  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 (typeof console === "undefined" || typeof console.log === "undefined") {
  11. console = { log: function () {}, error: function () {} };
  12. }
  13. if (typeof define === 'function' && define.amd) {
  14. define("converse", ["converse-dependencies"], function(otr) {
  15. // Use Mustache style syntax for variable interpolation
  16. _.templateSettings = {
  17. evaluate : /\{\[([\s\S]+?)\]\}/g,
  18. interpolate : /\{\{([\s\S]+?)\}\}/g
  19. };
  20. if (typeof otr !== "undefined") {
  21. return factory(jQuery, _, otr.OTR, otr.DSA, console);
  22. } else {
  23. return factory(jQuery, _, undefined, undefined, console);
  24. }
  25. });
  26. } else {
  27. // Browser globals
  28. _.templateSettings = {
  29. evaluate : /\{\[([\s\S]+?)\]\}/g,
  30. interpolate : /\{\{([\s\S]+?)\}\}/g
  31. };
  32. root.converse = factory(jQuery, _, OTR, DSA, console || {log: function(){}});
  33. }
  34. }(this, function ($, _, OTR, DSA, console) {
  35. var converse = {
  36. emit: function(evt, data) {
  37. $(this).trigger(evt, data);
  38. },
  39. once: function(evt, handler) {
  40. $(this).one(evt, handler);
  41. },
  42. on: function(evt, handler) {
  43. $(this).bind(evt, handler);
  44. },
  45. off: function(evt, handler) {
  46. $(this).unbind(evt, handler);
  47. }
  48. };
  49. converse.initialize = function (settings, callback) {
  50. var converse = this;
  51. // Constants
  52. // ---------
  53. var UNENCRYPTED = 0;
  54. var UNVERIFIED= 1;
  55. var VERIFIED= 2;
  56. var FINISHED = 3;
  57. var KEY = {
  58. ENTER: 13
  59. };
  60. var HAS_CSPRNG = ((typeof crypto !== 'undefined') &&
  61. ((typeof crypto.randomBytes === 'function') ||
  62. (typeof crypto.getRandomValues === 'function')
  63. ));
  64. var HAS_CRYPTO = HAS_CSPRNG && (
  65. (typeof CryptoJS !== "undefined") &&
  66. (typeof OTR !== "undefined") &&
  67. (typeof DSA !== "undefined")
  68. );
  69. // Default configuration values
  70. // ----------------------------
  71. this.allow_contact_requests = true;
  72. this.allow_muc = true;
  73. this.allow_otr = true;
  74. this.animate = true;
  75. this.auto_list_rooms = false;
  76. this.auto_subscribe = false;
  77. this.bosh_service_url = undefined; // The BOSH connection manager URL.
  78. this.debug = false;
  79. this.hide_muc_server = false;
  80. this.i18n = locales.en;
  81. this.prebind = false;
  82. this.show_controlbox_by_default = false;
  83. this.show_only_online_users = false;
  84. this.show_emoticons = true;
  85. this.show_toolbar = true;
  86. this.use_vcards = true;
  87. this.xhr_custom_status = false;
  88. this.xhr_custom_status_url = '';
  89. this.xhr_user_search = false;
  90. this.xhr_user_search_url = '';
  91. // Allow only whitelisted configuration attributes to be overwritten
  92. _.extend(this, _.pick(settings, [
  93. 'allow_contact_requests',
  94. 'allow_muc',
  95. 'allow_otr',
  96. 'animate',
  97. 'auto_list_rooms',
  98. 'auto_subscribe',
  99. 'bosh_service_url',
  100. 'connection',
  101. 'debug',
  102. 'fullname',
  103. 'hide_muc_server',
  104. 'i18n',
  105. 'jid',
  106. 'prebind',
  107. 'rid',
  108. 'show_controlbox_by_default',
  109. 'show_emoticons',
  110. 'show_only_online_users',
  111. 'show_toolbar',
  112. 'sid',
  113. 'use_vcards',
  114. 'xhr_custom_status',
  115. 'xhr_custom_status_url',
  116. 'xhr_user_search',
  117. 'xhr_user_search_url'
  118. ]));
  119. // Only allow OTR if we have the capability
  120. this.allow_otr = this.allow_otr && HAS_CRYPTO;
  121. // Translation machinery
  122. // ---------------------
  123. var __ = $.proxy(function (str) {
  124. // Translation factory
  125. if (this.i18n === undefined) {
  126. this.i18n = locales.en;
  127. }
  128. var t = this.i18n.translate(str);
  129. if (arguments.length>1) {
  130. return t.fetch.apply(t, [].slice.call(arguments,1));
  131. } else {
  132. return t.fetch();
  133. }
  134. }, this);
  135. var ___ = function (str) {
  136. /* XXX: This is part of a hack to get gettext to scan strings to be
  137. * translated. Strings we cannot send to the function above because
  138. * they require variable interpolation and we don't yet have the
  139. * variables at scan time.
  140. *
  141. * See actionInfoMessages
  142. */
  143. return str;
  144. };
  145. // Translation aware constants
  146. // ---------------------------
  147. var OTR_CLASS_MAPPING = {};
  148. OTR_CLASS_MAPPING[UNENCRYPTED] = 'unencrypted';
  149. OTR_CLASS_MAPPING[UNVERIFIED] = 'unverified';
  150. OTR_CLASS_MAPPING[VERIFIED] = 'verified';
  151. OTR_CLASS_MAPPING[FINISHED] = 'finished';
  152. var OTR_TRANSLATED_MAPPING = {};
  153. OTR_TRANSLATED_MAPPING[UNENCRYPTED] = __('unencrypted');
  154. OTR_TRANSLATED_MAPPING[UNVERIFIED] = __('unverified');
  155. OTR_TRANSLATED_MAPPING[VERIFIED] = __('verified');
  156. OTR_TRANSLATED_MAPPING[FINISHED] = __('finished');
  157. var STATUSES = {
  158. 'dnd': __('This contact is busy'),
  159. 'online': __('This contact is online'),
  160. 'offline': __('This contact is offline'),
  161. 'unavailable': __('This contact is unavailable'),
  162. 'xa': __('This contact is away for an extended period'),
  163. 'away': __('This contact is away')
  164. };
  165. // Module-level variables
  166. // ----------------------
  167. this.callback = callback || function () {};
  168. this.initial_presence_sent = 0;
  169. this.msg_counter = 0;
  170. // Module-level functions
  171. // ----------------------
  172. this.autoLink = function (text) {
  173. // Convert URLs into hyperlinks
  174. var re = /((http|https|ftp):\/\/[\w?=&.\/\-;#~%\-]+(?![\w\s?&.\/;#~%"=\-]*>))/g;
  175. return text.replace(re, '<a target="_blank" href="$1">$1</a>');
  176. };
  177. this.giveFeedback = function (message, klass) {
  178. $('.conn-feedback').text(message);
  179. $('.conn-feedback').attr('class', 'conn-feedback');
  180. if (klass) {
  181. $('.conn-feedback').addClass(klass);
  182. }
  183. };
  184. this.log = function (txt, level) {
  185. if (this.debug) {
  186. if (level == 'error') {
  187. console.log('ERROR: '+txt);
  188. } else {
  189. console.log(txt);
  190. }
  191. }
  192. };
  193. this.getVCard = function (jid, callback, errback) {
  194. if (!this.use_vcards) {
  195. if (callback) {
  196. callback(jid, jid);
  197. }
  198. return;
  199. }
  200. converse.connection.vcard.get(
  201. $.proxy(function (iq) {
  202. // Successful callback
  203. var $vcard = $(iq).find('vCard');
  204. var fullname = $vcard.find('FN').text(),
  205. img = $vcard.find('BINVAL').text(),
  206. img_type = $vcard.find('TYPE').text(),
  207. url = $vcard.find('URL').text();
  208. if (jid) {
  209. var rosteritem = converse.roster.get(jid);
  210. if (rosteritem) {
  211. fullname = _.isEmpty(fullname)? rosteritem.get('fullname') || jid: fullname;
  212. rosteritem.save({
  213. 'fullname': fullname,
  214. 'image_type': img_type,
  215. 'image': img,
  216. 'url': url,
  217. 'vcard_updated': converse.toISOString(new Date())
  218. });
  219. }
  220. }
  221. if (callback) {
  222. callback(jid, fullname, img, img_type, url);
  223. }
  224. }, this),
  225. jid,
  226. function (iq) {
  227. // Error callback
  228. var rosteritem = converse.roster.get(jid);
  229. if (rosteritem) {
  230. rosteritem.save({
  231. 'vcard_updated': converse.toISOString(new Date())
  232. });
  233. }
  234. if (errback) {
  235. errback(iq);
  236. }
  237. }
  238. );
  239. };
  240. this.onConnect = function (status) {
  241. var $button, $form;
  242. if (status === Strophe.Status.CONNECTED) {
  243. converse.log('Connected');
  244. converse.onConnected();
  245. } else if (status === Strophe.Status.DISCONNECTED) {
  246. $form = $('#converse-login');
  247. $button = $form.find('input[type=submit]');
  248. if ($button) { $button.show().siblings('span').remove(); }
  249. converse.giveFeedback(__('Disconnected'), 'error');
  250. converse.connection.connect(
  251. converse.connection.jid,
  252. converse.connection.pass,
  253. converse.onConnect
  254. );
  255. } else if (status === Strophe.Status.Error) {
  256. $form = $('#converse-login');
  257. $button = $form.find('input[type=submit]');
  258. if ($button) { $button.show().siblings('span').remove(); }
  259. converse.giveFeedback(__('Error'), 'error');
  260. } else if (status === Strophe.Status.CONNECTING) {
  261. converse.giveFeedback(__('Connecting'));
  262. } else if (status === Strophe.Status.CONNFAIL) {
  263. converse.chatboxesview.views.controlbox.trigger('connection-fail');
  264. converse.giveFeedback(__('Connection Failed'), 'error');
  265. } else if (status === Strophe.Status.AUTHENTICATING) {
  266. converse.giveFeedback(__('Authenticating'));
  267. } else if (status === Strophe.Status.AUTHFAIL) {
  268. converse.chatboxesview.views.controlbox.trigger('auth-fail');
  269. converse.giveFeedback(__('Authentication Failed'), 'error');
  270. } else if (status === Strophe.Status.DISCONNECTING) {
  271. converse.giveFeedback(__('Disconnecting'), 'error');
  272. } else if (status === Strophe.Status.ATTACHED) {
  273. converse.log('Attached');
  274. converse.onConnected();
  275. }
  276. };
  277. this.toISOString = function (date) {
  278. var pad;
  279. if (typeof date.toISOString !== 'undefined') {
  280. return date.toISOString();
  281. } else {
  282. // IE <= 8 Doesn't have toISOStringMethod
  283. pad = function (num) {
  284. return (num < 10) ? '0' + num : '' + num;
  285. };
  286. return date.getUTCFullYear() + '-' +
  287. pad(date.getUTCMonth() + 1) + '-' +
  288. pad(date.getUTCDate()) + 'T' +
  289. pad(date.getUTCHours()) + ':' +
  290. pad(date.getUTCMinutes()) + ':' +
  291. pad(date.getUTCSeconds()) + '.000Z';
  292. }
  293. };
  294. this.parseISO8601 = function (datestr) {
  295. /* Parses string formatted as 2013-02-14T11:27:08.268Z to a Date obj.
  296. */
  297.     var numericKeys = [1, 4, 5, 6, 7, 10, 11],
  298. struct = /^\s*(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}\.?\d*)Z\s*$/.exec(datestr),
  299. minutesOffset = 0,
  300. i, k;
  301. for (i = 0; (k = numericKeys[i]); ++i) {
  302. struct[k] = +struct[k] || 0;
  303. }
  304. // allow undefined days and months
  305. struct[2] = (+struct[2] || 1) - 1;
  306. struct[3] = +struct[3] || 1;
  307. if (struct[8] !== 'Z' && struct[9] !== undefined) {
  308. minutesOffset = struct[10] * 60 + struct[11];
  309. if (struct[9] === '+') {
  310. minutesOffset = 0 - minutesOffset;
  311. }
  312. }
  313. return new Date(Date.UTC(struct[1], struct[2], struct[3], struct[4], struct[5] + minutesOffset, struct[6], struct[7]));
  314. };
  315. this.updateMsgCounter = function () {
  316. if (this.msg_counter > 0) {
  317. if (document.title.search(/^Messages \(\d+\) /) == -1) {
  318. document.title = "Messages (" + this.msg_counter + ") " + document.title;
  319. } else {
  320. document.title = document.title.replace(/^Messages \(\d+\) /, "Messages (" + this.msg_counter + ") ");
  321. }
  322. window.blur();
  323. window.focus();
  324. } else if (document.title.search(/^Messages \(\d+\) /) != -1) {
  325. document.title = document.title.replace(/^Messages \(\d+\) /, "");
  326. }
  327. };
  328. this.incrementMsgCounter = function () {
  329. this.msg_counter += 1;
  330. this.updateMsgCounter();
  331. };
  332. this.clearMsgCounter = function () {
  333. this.msg_counter = 0;
  334. this.updateMsgCounter();
  335. };
  336. this.initStatus = function (callback) {
  337. this.xmppstatus = new this.XMPPStatus();
  338. var id = hex_sha1('converse.xmppstatus-'+this.bare_jid);
  339. this.xmppstatus.id = id; // This appears to be necessary for backbone.localStorage
  340. this.xmppstatus.localStorage = new Backbone.LocalStorage(id);
  341. this.xmppstatus.fetch({success: callback, error: callback});
  342. };
  343. this.initRoster = function () {
  344. // Set up the roster
  345. this.roster = new this.RosterItems();
  346. this.roster.localStorage = new Backbone.LocalStorage(
  347. hex_sha1('converse.rosteritems-'+converse.bare_jid));
  348. // Register callbacks that depend on the roster
  349. this.connection.roster.registerCallback(
  350. $.proxy(this.roster.rosterHandler, this.roster),
  351. null, 'presence', null);
  352. this.connection.addHandler(
  353. $.proxy(this.roster.subscribeToSuggestedItems, this.roster),
  354. 'http://jabber.org/protocol/rosterx', 'message', null);
  355. this.connection.addHandler(
  356. $.proxy(function (presence) {
  357. this.presenceHandler(presence);
  358. return true;
  359. }, this.roster), null, 'presence', null);
  360. // No create the view which will fetch roster items from
  361. // localStorage
  362. this.rosterview = new this.RosterView({'model':this.roster});
  363. };
  364. this.onConnected = function () {
  365. if (this.debug) {
  366. this.connection.xmlInput = function (body) { console.log(body); };
  367. this.connection.xmlOutput = function (body) { console.log(body); };
  368. Strophe.log = function (level, msg) { console.log(level+' '+msg); };
  369. Strophe.error = function (msg) { console.log('ERROR: '+msg); };
  370. }
  371. this.bare_jid = Strophe.getBareJidFromJid(this.connection.jid);
  372. this.domain = Strophe.getDomainFromJid(this.connection.jid);
  373. this.features = new this.Features();
  374. this.initStatus($.proxy(function () {
  375. this.initRoster();
  376. this.chatboxes.onConnected();
  377. this.connection.roster.get(function () {});
  378. $(document).click(function() {
  379. if ($('.toggle-otr ul').is(':visible')) {
  380. $('.toggle-otr ul', this).slideUp();
  381. }
  382. if ($('.toggle-smiley ul').is(':visible')) {
  383. $('.toggle-smiley ul', this).slideUp();
  384. }
  385. });
  386. $(window).on("blur focus", $.proxy(function(e) {
  387. if ((this.windowState != e.type) && (e.type == 'focus')) {
  388. converse.clearMsgCounter();
  389. }
  390. this.windowState = e.type;
  391. },this));
  392. this.giveFeedback(__('Online Contacts'));
  393. if (this.callback) {
  394. if (this.connection.service === 'jasmine tests') {
  395. // XXX: Call back with the internal converse object. This
  396. // object should never be exposed to production systems.
  397. // 'jasmine tests' is an invalid http bind service value,
  398. // so we're sure that this is just for tests.
  399. //
  400. // TODO: We might need to consider websockets, which
  401. // probably won't use the 'service' attr. Current
  402. // strophe.js version used by converse.js doesn't support
  403. // websockets.
  404. this.callback(this);
  405. } else {
  406. this.callback();
  407. }
  408. }
  409. }, this));
  410. converse.emit('onReady');
  411. };
  412. // Backbone Models and Views
  413. // -------------------------
  414. this.Message = Backbone.Model.extend();
  415. this.Messages = Backbone.Collection.extend({
  416. model: converse.Message
  417. });
  418. this.ChatBox = Backbone.Model.extend({
  419. initialize: function () {
  420. if (this.get('box_id') !== 'controlbox') {
  421. if (_.contains([UNVERIFIED, VERIFIED], this.get('otr_status'))) {
  422. this.initiateOTR();
  423. }
  424. this.messages = new converse.Messages();
  425. this.messages.localStorage = new Backbone.LocalStorage(
  426. hex_sha1('converse.messages'+this.get('jid')+converse.bare_jid));
  427. this.set({
  428. 'user_id' : Strophe.getNodeFromJid(this.get('jid')),
  429. 'box_id' : hex_sha1(this.get('jid')),
  430. 'otr_status': this.get('otr_status') || UNENCRYPTED
  431. });
  432. }
  433. },
  434. getSession: function () {
  435. // XXX: sessionStorage is not supported in IE < 8. Perhaps a
  436. // user alert is required here...
  437. var saved_key = window.sessionStorage[hex_sha1(this.id+'priv_key')];
  438. var instance_tag = window.sessionStorage[hex_sha1(this.id+'instance_tag')];
  439. var cipher = CryptoJS.lib.PasswordBasedCipher;
  440. var pass = converse.connection.pass;
  441. var pass_check = this.get('pass_check');
  442. var result, key;
  443. if (saved_key && instance_tag && typeof pass_check !== 'undefined') {
  444. var decrypted = cipher.decrypt(CryptoJS.algo.AES, saved_key, pass);
  445. key = DSA.parsePrivate(decrypted.toString(CryptoJS.enc.Latin1));
  446. if (cipher.decrypt(CryptoJS.algo.AES, pass_check, pass).toString(CryptoJS.enc.Latin1) === 'match') {
  447. // Verified that the user's password is still the same
  448. this.trigger('showHelpMessages', [__('Re-establishing encrypted session')]);
  449. return {
  450. 'key': key,
  451. 'instance_tag': instance_tag
  452. };
  453. }
  454. }
  455. // We need to generate a new key and instance tag
  456. result = alert(__('Your browser needs to generate a private key, which will be used in your encrypted chat session. This can take up to 30 seconds during which your browser might freeze and become unresponsive.'));
  457. instance_tag = OTR.makeInstanceTag();
  458. key = new DSA();
  459. // Encrypt the key and set in sessionStorage. Also store
  460. // instance tag
  461. window.sessionStorage[hex_sha1(this.id+'priv_key')] =
  462. cipher.encrypt(CryptoJS.algo.AES, key.packPrivate(), pass).toString();
  463. window.sessionStorage[hex_sha1(this.id+'instance_tag')] = instance_tag;
  464. this.trigger('showHelpMessages', [__('Private key generated.')]);
  465. this.save({'pass_check': cipher.encrypt(CryptoJS.algo.AES, 'match', pass).toString()});
  466. return {
  467. 'key': key,
  468. 'instance_tag': instance_tag
  469. };
  470. },
  471. updateOTRStatus: function (state) {
  472. switch (state) {
  473. case OTR.CONST.STATUS_AKE_SUCCESS:
  474. if (this.otr.msgstate === OTR.CONST.MSGSTATE_ENCRYPTED) {
  475. this.save({'otr_status': UNVERIFIED});
  476. }
  477. break;
  478. case OTR.CONST.STATUS_END_OTR:
  479. if (this.otr.msgstate === OTR.CONST.MSGSTATE_FINISHED) {
  480. this.save({'otr_status': FINISHED});
  481. } else if (this.otr.msgstate === OTR.CONST.MSGSTATE_PLAINTEXT) {
  482. this.save({'otr_status': UNENCRYPTED});
  483. }
  484. break;
  485. }
  486. },
  487. onSMP: function (type, data) {
  488. // Event handler for SMP (Socialist's Millionaire Protocol)
  489. // used by OTR (off-the-record).
  490. switch (type) {
  491. case 'question':
  492. this.otr.smpSecret(prompt(__(
  493. 'Authentication request from %1$s\n\nYour buddy is attempting to verify your identity, by asking you the question below.\n\n%2$s',
  494. [this.get('fullname'), data])));
  495. break;
  496. case 'trust':
  497. if (data === true) {
  498. this.save({'otr_status': VERIFIED});
  499. } else {
  500. this.trigger(
  501. 'showHelpMessages',
  502. [__("Could not verify this user's identify.")],
  503. 'error');
  504. this.save({'otr_status': UNVERIFIED});
  505. }
  506. break;
  507. default:
  508. throw new Error('Unknown type.');
  509. }
  510. },
  511. initiateOTR: function (query_msg) {
  512. // Sets up an OTR object through which we can send and receive
  513. // encrypted messages.
  514. //
  515. // If 'query_msg' is passed in, it means there is an alread incoming
  516. // query message from our buddy. Otherwise, it is us who will
  517. // send the query message to them.
  518. this.save({'otr_status': UNENCRYPTED});
  519. var session = this.getSession();
  520. this.otr = new OTR({
  521. fragment_size: 140,
  522. send_interval: 200,
  523. priv: session.key,
  524. instance_tag: session.instance_tag,
  525. debug: this.debug
  526. });
  527. this.otr.on('status', $.proxy(this.updateOTRStatus, this));
  528. this.otr.on('smp', $.proxy(this.onSMP, this));
  529. this.otr.on('ui', $.proxy(function (msg) {
  530. this.trigger('showReceivedOTRMessage', msg);
  531. }, this));
  532. this.otr.on('io', $.proxy(function (msg) {
  533. this.trigger('sendMessageStanza', msg);
  534. }, this));
  535. this.otr.on('error', $.proxy(function (msg) {
  536. this.trigger('showOTRError', msg);
  537. }, this));
  538. if (query_msg) {
  539. this.otr.receiveMsg(query_msg);
  540. } else {
  541. this.otr.sendQueryMsg();
  542. }
  543. },
  544. endOTR: function () {
  545. if (this.otr) {
  546. this.otr.endOtr();
  547. }
  548. this.save({'otr_status': UNENCRYPTED});
  549. },
  550. createMessage: function (message) {
  551. var $message = $(message),
  552. body = converse.autoLink($message.children('body').text()),
  553. from = Strophe.getBareJidFromJid($message.attr('from')),
  554. composing = $message.find('composing'),
  555. delayed = $message.find('delay').length > 0,
  556. fullname = this.get('fullname'),
  557. stamp, time, sender;
  558. fullname = (_.isEmpty(fullname)? from: fullname).split(' ')[0];
  559. if (!body) {
  560. if (composing.length) {
  561. this.messages.add({
  562. fullname: fullname,
  563. sender: 'them',
  564. delayed: delayed,
  565. time: converse.toISOString(new Date()),
  566. composing: composing.length
  567. });
  568. }
  569. } else {
  570. if (delayed) {
  571. stamp = $message.find('delay').attr('stamp');
  572. time = stamp;
  573. } else {
  574. time = converse.toISOString(new Date());
  575. }
  576. if (from == converse.bare_jid) {
  577. sender = 'me';
  578. } else {
  579. sender = 'them';
  580. }
  581. this.messages.create({
  582. fullname: fullname,
  583. sender: sender,
  584. delayed: delayed,
  585. time: time,
  586. message: body
  587. });
  588. }
  589. },
  590. messageReceived: function (message) {
  591. var $body = $(message).children('body');
  592. var text = ($body.length > 0 ? converse.autoLink($body.text()) : undefined);
  593. if ((!text) || (!converse.allow_otr)) {
  594. return this.createMessage(message);
  595. }
  596. if (_.contains([UNVERIFIED, VERIFIED], this.get('otr_status'))) {
  597. this.otr.receiveMsg(text);
  598. } else {
  599. if (text.match(/^\?OTR/)) {
  600. // They want to initiate OTR
  601. if (!this.otr) {
  602. this.initiateOTR(text);
  603. } else {
  604. this.otr.receiveMsg(text);
  605. }
  606. } else {
  607. // Normal unencrypted message.
  608. this.createMessage(message);
  609. }
  610. }
  611. }
  612. });
  613. this.ChatBoxView = Backbone.View.extend({
  614. length: 200,
  615. tagName: 'div',
  616. className: 'chatbox',
  617. is_chatroom: false, // This is not a multi-user chatroom
  618. events: {
  619. 'click .close-chatbox-button': 'closeChat',
  620. 'keypress textarea.chat-textarea': 'keyPressed',
  621. 'click .toggle-smiley': 'toggleEmoticonMenu',
  622. 'click .toggle-smiley ul li': 'insertEmoticon',
  623. 'click .toggle-otr': 'toggleOTRMenu',
  624. 'click .start-otr': 'startOTRFromToolbar',
  625. 'click .end-otr': 'endOTR',
  626. 'click .auth-otr': 'authOTR'
  627. },
  628. template: _.template(
  629. '<div class="chat-head chat-head-chatbox">' +
  630. '<a class="close-chatbox-button icon-close"></a>' +
  631. '<a href="{{url}}" target="_blank" class="user">' +
  632. '<div class="chat-title"> {{ fullname }} </div>' +
  633. '</a>' +
  634. '<p class="user-custom-message"><p/>' +
  635. '</div>' +
  636. '<div class="chat-content"></div>' +
  637. '<form class="sendXMPPMessage" action="" method="post">' +
  638. '{[ if ('+converse.show_toolbar+') { ]}' +
  639. '<ul class="chat-toolbar no-text-select"></ul>'+
  640. '{[ } ]}' +
  641. '<textarea ' +
  642. 'type="text" ' +
  643. 'class="chat-textarea" ' +
  644. 'placeholder="'+__('Personal message')+'"/>'+
  645. '</form>'
  646. ),
  647. toolbar_template: _.template(
  648. '{[ if (show_emoticons) { ]}' +
  649. '<li class="toggle-smiley icon-happy" title="Insert a smilery">' +
  650. '<ul>' +
  651. '<li><a class="icon-smiley" href="#" data-emoticon=":)"></a></li>'+
  652. '<li><a class="icon-wink" href="#" data-emoticon=";)"></a></li>'+
  653. '<li><a class="icon-grin" href="#" data-emoticon=":D"></a></li>'+
  654. '<li><a class="icon-tongue" href="#" data-emoticon=":P"></a></li>'+
  655. '<li><a class="icon-cool" href="#" data-emoticon="8)"></a></li>'+
  656. '<li><a class="icon-evil" href="#" data-emoticon=">:)"></a></li>'+
  657. '<li><a class="icon-confused" href="#" data-emoticon=":S"></a></li>'+
  658. '<li><a class="icon-wondering" href="#" data-emoticon=":\\"></a></li>'+
  659. '<li><a class="icon-angry" href="#" data-emoticon=">:("></a></li>'+
  660. '<li><a class="icon-sad" href="#" data-emoticon=":("></a></li>'+
  661. '<li><a class="icon-shocked" href="#" data-emoticon=":O"></a></li>'+
  662. '<li><a class="icon-thumbs-up" href="#" data-emoticon="(^.^)b"></a></li>'+
  663. '<li><a class="icon-heart" href="#" data-emoticon="<3"></a></li>'+
  664. '</ul>' +
  665. '</li>' +
  666. '{[ } ]}' +
  667. '{[ if (allow_otr) { ]}' +
  668. '<li class="toggle-otr {{otr_status_class}}" title="{{otr_tooltip}}">'+
  669. '<span class="chat-toolbar-text">{{otr_translated_status}}</span>'+
  670. '{[ if (otr_status == "'+UNENCRYPTED+'") { ]}' +
  671. '<span class="icon-unlocked"></span>'+
  672. '{[ } ]}' +
  673. '{[ if (otr_status == "'+UNVERIFIED+'") { ]}' +
  674. '<span class="icon-lock"></span>'+
  675. '{[ } ]}' +
  676. '{[ if (otr_status == "'+VERIFIED+'") { ]}' +
  677. '<span class="icon-lock"></span>'+
  678. '{[ } ]}' +
  679. '{[ if (otr_status == "'+FINISHED+'") { ]}' +
  680. '<span class="icon-unlocked"></span>'+
  681. '{[ } ]}' +
  682. '<ul>'+
  683. '{[ if (otr_status == "'+UNENCRYPTED+'") { ]}' +
  684. '<li><a class="start-otr" href="#">'+__('Start encrypted conversation')+'</a></li>'+
  685. '{[ } ]}' +
  686. '{[ if (otr_status != "'+UNENCRYPTED+'") { ]}' +
  687. '<li><a class="start-otr" href="#">'+__('Refresh encrypted conversation')+'</a></li>'+
  688. '<li><a class="end-otr" href="#">'+__('End encrypted conversation')+'</a></li>'+
  689. '<li><a class="auth-otr" data-scheme="smp" href="#">'+__('Verify with SMP')+'</a></li>'+
  690. '{[ } ]}' +
  691. '{[ if (otr_status == "'+UNVERIFIED+'") { ]}' +
  692. '<li><a class="auth-otr" data-scheme="fingerprint" href="#">'+__('Verify with fingerprints')+'</a></li>'+
  693. '{[ } ]}' +
  694. '<li><a href="http://www.cypherpunks.ca/otr/help/3.2.0/levels.php" target="_blank">'+__("What\'s this?")+'</a></li>'+
  695. '</ul>'+
  696. '</li>'+
  697. '{[ } ]}'
  698. ),
  699. message_template: _.template(
  700. '<div class="chat-message {{extra_classes}}">' +
  701. '<span class="chat-message-{{sender}}">{{time}} {{username}}:&nbsp;</span>' +
  702. '<span class="chat-message-content">{{message}}</span>' +
  703. '</div>'),
  704. action_template: _.template(
  705. '<div class="chat-message {{extra_classes}}">' +
  706. '<span class="chat-message-{{sender}}">{{time}} **{{username}} </span>' +
  707. '<span class="chat-message-content">{{message}}</span>' +
  708. '</div>'),
  709. new_day_template: _.template(
  710. '<time class="chat-date" datetime="{{isodate}}">{{datestring}}</time>'
  711. ),
  712. initialize: function (){
  713. this.model.messages.on('add', this.onMessageAdded, this);
  714. this.model.on('show', this.show, this);
  715. this.model.on('destroy', this.hide, this);
  716. this.model.on('change', this.onChange, this);
  717. this.model.on('showOTRError', this.showOTRError, this);
  718. this.model.on('buddyStartsOTR', this.buddyStartsOTR, this);
  719. this.model.on('showHelpMessages', this.showHelpMessages, this);
  720. this.model.on('sendMessageStanza', this.sendMessageStanza, this);
  721. this.model.on('showSentOTRMessage', function (text) {
  722. this.showOTRMessage(text, 'me');
  723. }, this);
  724. this.model.on('showReceivedOTRMessage', function (text) {
  725. this.showOTRMessage(text, 'them');
  726. }, this);
  727. this.updateVCard();
  728. this.$el.appendTo(converse.chatboxesview.$el);
  729. this.render().show().model.messages.fetch({add: true});
  730. if (this.model.get('status')) {
  731. this.showStatusMessage(this.model.get('status'));
  732. }
  733. },
  734. render: function () {
  735. this.$el.attr('id', this.model.get('box_id'))
  736. .html(this.template(this.model.toJSON()));
  737. this.renderToolbar().renderAvatar();
  738. return this;
  739. },
  740. showStatusNotification: function (message, replace) {
  741. var $chat_content = this.$el.find('.chat-content');
  742. $chat_content.find('div.chat-event').remove().end()
  743. .append($('<div class="chat-event"></div>').text(message));
  744. this.scrollDown();
  745. },
  746. renderEmoticons: function (text) {
  747. if (converse.show_emoticons) {
  748. text = text.replace(/:\)/g, '<span class="emoticon icon-smiley"></span>');
  749. text = text.replace(/:\-\)/g, '<span class="emoticon icon-smiley"></span>');
  750. text = text.replace(/;\)/g, '<span class="emoticon icon-wink"></span>');
  751. text = text.replace(/;\-\)/g, '<span class="emoticon icon-wink"></span>');
  752. text = text.replace(/:D/g, '<span class="emoticon icon-grin"></span>');
  753. text = text.replace(/:\-D/g, '<span class="emoticon icon-grin"></span>');
  754. text = text.replace(/:P/g, '<span class="emoticon icon-tongue"></span>');
  755. text = text.replace(/:\-P/g, '<span class="emoticon icon-tongue"></span>');
  756. text = text.replace(/:p/g, '<span class="emoticon icon-tongue"></span>');
  757. text = text.replace(/:\-p/g, '<span class="emoticon icon-tongue"></span>');
  758. text = text.replace(/8\)/g, '<span class="emoticon icon-cool"></span>');
  759. text = text.replace(/>:\)/g, '<span class="emoticon icon-evil"></span>');
  760. text = text.replace(/:S/g, '<span class="emoticon icon-confused"></span>');
  761. text = text.replace(/:\\/g, '<span class="emoticon icon-wondering"></span>');
  762. text = text.replace(/:\//g, '<span class="emoticon icon-wondering"></span>');
  763. text = text.replace(/>:\(/g, '<span class="emoticon icon-angry"></span>');
  764. text = text.replace(/:\(/g, '<span class="emoticon icon-sad"></span>');
  765. text = text.replace(/:\-\(/g, '<span class="emoticon icon-sad"></span>');
  766. text = text.replace(/:O/g, '<span class="emoticon icon-shocked"></span>');
  767. text = text.replace(/:\-O/g, '<span class="emoticon icon-shocked"></span>');
  768. text = text.replace(/\=\-O/g, '<span class="emoticon icon-shocked"></span>');
  769. text = text.replace(/\(\^.\^\)b/g, '<span class="emoticon icon-thumbs-up"></span>');
  770. text = text.replace(/<3/g, '<span class="emoticon icon-heart"></span>');
  771. }
  772. return text;
  773. },
  774. showMessage: function ($el, msg_dict) {
  775. var this_date = converse.parseISO8601(msg_dict.time),
  776. text = msg_dict.message,
  777. match = text.match(/^\/(.*?)(?: (.*))?$/),
  778. sender = msg_dict.sender,
  779. template, username;
  780. if ((match) && (match[1] === 'me')) {
  781. text = text.replace(/^\/me/, '');
  782. template = this.action_template;
  783. username = msg_dict.fullname;
  784. } else {
  785. template = this.message_template;
  786. username = sender === 'me' && __('me') || msg_dict.fullname;
  787. }
  788. $el.find('div.chat-event').remove();
  789. $el.append(
  790. template({
  791. 'sender': sender,
  792. 'time': this_date.toTimeString().substring(0,5),
  793. 'message': this.renderEmoticons(text),
  794. 'username': username,
  795. 'extra_classes': msg_dict.delayed && 'delayed' || ''
  796. }));
  797. return this.scrollDown();
  798. },
  799. showOTRMessage: function (text, sender) {
  800. /* "Off-the-record" messages are encrypted and not stored at all,
  801. * so we don't have a backbone converse.Message object to work with.
  802. */
  803. var username = sender === 'me' && sender || this.model.get('fullname');
  804. var $el = this.$el.find('.chat-content');
  805. $el.find('div.chat-event').remove();
  806. $el.append(
  807. this.message_template({
  808. 'sender': sender,
  809. 'time': (new Date()).toTimeString().substring(0,5),
  810. 'message': text,
  811. 'username': username,
  812. 'extra_classes': ''
  813. }));
  814. return this.scrollDown();
  815. },
  816. showHelpMessages: function (msgs, type) {
  817. var $chat_content = this.$el.find('.chat-content'), i,
  818. msgs_length = msgs.length;
  819. for (i=0; i<msgs_length; i++) {
  820. $chat_content.append($('<div class="chat-'+(type||'info')+'">'+msgs[i]+'</div>'));
  821. }
  822. return this.scrollDown();
  823. },
  824. onMessageAdded: function (message) {
  825. var time = message.get('time'),
  826. times = this.model.messages.pluck('time'),
  827. this_date = converse.parseISO8601(time),
  828. $chat_content = this.$el.find('.chat-content'),
  829. previous_message, idx, prev_date, isodate, text, match;
  830. // If this message is on a different day than the one received
  831. // prior, then indicate it on the chatbox.
  832. idx = _.indexOf(times, time)-1;
  833. if (idx >= 0) {
  834. previous_message = this.model.messages.at(idx);
  835. prev_date = converse.parseISO8601(previous_message.get('time'));
  836. isodate = new Date(this_date.getTime());
  837. isodate.setUTCHours(0,0,0,0);
  838. isodate = converse.toISOString(isodate);
  839. if (this.isDifferentDay(prev_date, this_date)) {
  840. $chat_content.append(this.new_day_template({
  841. isodate: isodate,
  842. datestring: this_date.toString().substring(0,15)
  843. }));
  844. }
  845. }
  846. if (message.get('composing')) {
  847. this.showStatusNotification(message.get('fullname')+' '+'is typing');
  848. return;
  849. } else {
  850. this.showMessage($chat_content, _.clone(message.attributes));
  851. }
  852. if ((message.get('sender') != 'me') && (converse.windowState == 'blur')) {
  853. converse.incrementMsgCounter();
  854. }
  855. return this.scrollDown();
  856. },
  857. isDifferentDay: function (prev_date, next_date) {
  858. return (
  859. (next_date.getDate() != prev_date.getDate()) ||
  860. (next_date.getFullYear() != prev_date.getFullYear()) ||
  861. (next_date.getMonth() != prev_date.getMonth()));
  862. },
  863. sendMessageStanza: function (text) {
  864. /*
  865. * Sends the actual XML stanza to the XMPP server.
  866. */
  867. // TODO: Look in ChatPartners to see what resources we have for the recipient.
  868. // if we have one resource, we sent to only that resources, if we have multiple
  869. // we send to the bare jid.
  870. var timestamp = (new Date()).getTime();
  871. var bare_jid = this.model.get('jid');
  872. var message = $msg({from: converse.connection.jid, to: bare_jid, type: 'chat', id: timestamp})
  873. .c('body').t(text).up()
  874. .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'});
  875. // Forward the message, so that other connected resources are also aware of it.
  876. // TODO: Forward the message only to other connected resources (inside the browser)
  877. var forwarded = $msg({to:converse.bare_jid, type:'chat', id:timestamp})
  878. .c('forwarded', {xmlns:'urn:xmpp:forward:0'})
  879. .c('delay', {xmns:'urn:xmpp:delay',stamp:timestamp}).up()
  880. .cnode(message.tree());
  881. converse.connection.send(message);
  882. converse.connection.send(forwarded);
  883. },
  884. sendMessage: function (text) {
  885. var match = text.replace(/^\s*/, "").match(/^\/(.*)\s*$/), msgs;
  886. if (match) {
  887. if (match[1] === "clear") {
  888. this.$el.find('.chat-content').empty();
  889. this.model.messages.reset().localStorage._clear();
  890. return;
  891. }
  892. else if (match[1] === "help") {
  893. msgs = [
  894. '<strong>/help</strong>:'+__('Show this menu')+'',
  895. '<strong>/me</strong>:'+__('Write in the third person')+'',
  896. '<strong>/clear</strong>:'+__('Remove messages')+''
  897. ];
  898. this.showHelpMessages(msgs);
  899. return;
  900. } else if ((converse.allow_otr) || (match[1] === "endotr")) {
  901. return this.endOTR();
  902. } else if ((converse.allow_otr) || (match[1] === "otr")) {
  903. return this.model.initiateOTR();
  904. }
  905. }
  906. if (_.contains([UNVERIFIED, VERIFIED], this.model.get('otr_status'))) {
  907. // Off-the-record encryption is active
  908. this.model.otr.sendMsg(text);
  909. this.model.trigger('showSentOTRMessage', text);
  910. } else {
  911. // We only save unencrypted messages.
  912. var fullname = converse.xmppstatus.get('fullname');
  913. fullname = _.isEmpty(fullname)? converse.bare_jid: fullname;
  914. this.model.messages.create({
  915. fullname: fullname,
  916. sender: 'me',
  917. time: converse.toISOString(new Date()),
  918. message: text
  919. });
  920. this.sendMessageStanza(text);
  921. }
  922. },
  923. keyPressed: function (ev) {
  924. var $textarea = $(ev.target),
  925. message, notify, composing;
  926. if(ev.keyCode == KEY.ENTER) {
  927. ev.preventDefault();
  928. message = $textarea.val();
  929. $textarea.val('').focus();
  930. if (message !== '') {
  931. if (this.model.get('chatroom')) {
  932. this.sendChatRoomMessage(message);
  933. } else {
  934. this.sendMessage(message);
  935. }
  936. converse.emit('onMessageSend', message);
  937. }
  938. this.$el.data('composing', false);
  939. } else if (!this.model.get('chatroom')) {
  940. // composing data is only for single user chat
  941. composing = this.$el.data('composing');
  942. if (!composing) {
  943. if (ev.keyCode != 47) {
  944. // We don't send composing messages if the message
  945. // starts with forward-slash.
  946. notify = $msg({'to':this.model.get('jid'), 'type': 'chat'})
  947. .c('composing', {'xmlns':'http://jabber.org/protocol/chatstates'});
  948. converse.connection.send(notify);
  949. }
  950. this.$el.data('composing', true);
  951. }
  952. }
  953. },
  954. insertEmoticon: function (ev) {
  955. ev.stopPropagation();
  956. this.$el.find('.toggle-smiley ul').slideToggle(200);
  957. var $textbox = this.$el.find('textarea.chat-textarea');
  958. var value = $textbox.val();
  959. var $target = $(ev.target);
  960. $target = $target.is('a') ? $target : $target.children('a');
  961. if (value && (value[value.length-1] !== ' ')) {
  962. value = value + ' ';
  963. }
  964. $textbox.focus().val(value+$target.data('emoticon')+' ');
  965. },
  966. toggleEmoticonMenu: function (ev) {
  967. ev.stopPropagation();
  968. this.$el.find('.toggle-smiley ul').slideToggle(200);
  969. },
  970. toggleOTRMenu: function (ev) {
  971. ev.stopPropagation();
  972. this.$el.find('.toggle-otr ul').slideToggle(200);
  973. },
  974. showOTRError: function (msg) {
  975. if (msg == 'Message cannot be sent at this time.') {
  976. this.showHelpMessages(
  977. [__('Your message could not be sent')], 'error');
  978. } else if (msg == 'Received an unencrypted message.') {
  979. this.showHelpMessages(
  980. [__('We received an unencrypted message')], 'error');
  981. } else if (msg == 'Received an unreadable encrypted message.') {
  982. this.showHelpMessages(
  983. [__('We received an unreadable encrypted message')],
  984. 'error');
  985. } else {
  986. this.showHelpMessages(['Encryption error occured: '+msg], 'error');
  987. }
  988. console.log("OTR ERROR:"+msg);
  989. },
  990. buddyStartsOTR: function (ev) {
  991. this.showHelpMessages([__('This user has requested an encrypted session.')]);
  992. this.model.initiateOTR();
  993. },
  994. startOTRFromToolbar: function (ev) {
  995. $(ev.target).parent().parent().slideUp();
  996. ev.stopPropagation();
  997. this.model.initiateOTR();
  998. },
  999. endOTR: function (ev) {
  1000. if (typeof ev !== "undefined") {
  1001. ev.preventDefault();
  1002. ev.stopPropagation();
  1003. }
  1004. this.model.endOTR();
  1005. },
  1006. authOTR: function (ev) {
  1007. var scheme = $(ev.target).data().scheme;
  1008. var result, question, answer;
  1009. if (scheme === 'fingerprint') {
  1010. result = confirm(__('Here are the fingerprints, please confirm them with %1$s, outside of this chat.\n\nFingerprint for you, %2$s: %3$s\n\nFingerprint for %1$s: %4$s\n\nIf you have confirmed that the fingerprints match, click OK, otherwise click Cancel.', [
  1011. this.model.get('fullname'),
  1012. converse.xmppstatus.get('fullname')||converse.bare_jid,
  1013. this.model.otr.priv.fingerprint(),
  1014. this.model.otr.their_priv_pk.fingerprint()
  1015. ]
  1016. ));
  1017. if (result === true) {
  1018. this.model.save({'otr_status': VERIFIED});
  1019. } else {
  1020. this.model.save({'otr_status': UNVERIFIED});
  1021. }
  1022. } else if (scheme === 'smp') {
  1023. alert(__('You will be prompted to provide a security question and then an answer to that question.\n\nYour buddy will then be prompted the same question and if they type the exact same answer (case sensitive), their identity will have been verified.'));
  1024. question = prompt(__('What is your security question?'));
  1025. if (question) {
  1026. answer = prompt(__('What is the answer to the security question?'));
  1027. this.model.otr.smpSecret(answer, question);
  1028. }
  1029. } else {
  1030. this.showHelpMessages([__('Invalid authentication scheme provided')], 'error');
  1031. }
  1032. },
  1033. onChange: function (item, changed) {
  1034. if (_.has(item.changed, 'chat_status')) {
  1035. var chat_status = item.get('chat_status'),
  1036. fullname = item.get('fullname');
  1037. fullname = _.isEmpty(fullname)? item.get('jid'): fullname;
  1038. if (this.$el.is(':visible')) {
  1039. if (chat_status === 'offline') {
  1040. this.showStatusNotification(fullname+' '+'has gone offline');
  1041. } else if (chat_status === 'away') {
  1042. this.showStatusNotification(fullname+' '+'has gone away');
  1043. } else if ((chat_status === 'dnd')) {
  1044. this.showStatusNotification(fullname+' '+'is busy');
  1045. } else if (chat_status === 'online') {
  1046. this.$el.find('div.chat-event').remove();
  1047. }
  1048. }
  1049. converse.emit('onBuddyStatusChanged', item.attributes, item.get('chat_status'));
  1050. }
  1051. if (_.has(item.changed, 'status')) {
  1052. this.showStatusMessage(item.get('status'));
  1053. converse.emit('onBuddyStatusMessageChanged', item.attributes, item.get('status'));
  1054. }
  1055. if (_.has(item.changed, 'image')) {
  1056. this.renderAvatar();
  1057. }
  1058. if (_.has(item.changed, 'otr_status')) {
  1059. this.renderToolbar().informOTRChange();
  1060. }
  1061. // TODO check for changed fullname as well
  1062. },
  1063. showStatusMessage: function (msg) {
  1064. this.$el.find('p.user-custom-message').text(msg).attr('title', msg);
  1065. },
  1066. closeChat: function () {
  1067. if (converse.connection) {
  1068. this.model.destroy();
  1069. } else {
  1070. this.model.trigger('hide');
  1071. }
  1072. },
  1073. updateVCard: function () {
  1074. var jid = this.model.get('jid'),
  1075. rosteritem = converse.roster.get(jid);
  1076. if ((rosteritem) && (!rosteritem.get('vcard_updated'))) {
  1077. converse.getVCard(
  1078. jid,
  1079. $.proxy(function (jid, fullname, image, image_type, url) {
  1080. this.model.save({
  1081. 'fullname' : fullname || jid,
  1082. 'url': url,
  1083. 'image_type': image_type,
  1084. 'image': image
  1085. });
  1086. }, this),
  1087. $.proxy(function (stanza) {
  1088. converse.log("ChatBoxView.initialize: An error occured while fetching vcard");
  1089. }, this)
  1090. );
  1091. }
  1092. },
  1093. informOTRChange: function () {
  1094. var data = this.model.toJSON();
  1095. var msgs = [];
  1096. if (data.otr_status == UNENCRYPTED) {
  1097. msgs.push(__("Your messages are not encrypted anymore"));
  1098. } else if (data.otr_status == UNVERIFIED){
  1099. msgs.push(__("Your messages are now encrypted but your buddy's identity has not been verified."));
  1100. } else if (data.otr_status == VERIFIED){
  1101. msgs.push(__("Your buddy's identify has been verified."));
  1102. } else if (data.otr_status == FINISHED){
  1103. msgs.push(__("Your buddy has ended encryption on their end, you should do the same."));
  1104. }
  1105. return this.showHelpMessages(msgs);
  1106. },
  1107. renderToolbar: function () {
  1108. if (converse.show_toolbar) {
  1109. var data = this.model.toJSON();
  1110. if (data.otr_status == UNENCRYPTED) {
  1111. data.otr_tooltip = __('Your messages are not encrypted. Click here to enable OTR encryption.');
  1112. } else if (data.otr_status == UNVERIFIED){
  1113. data.otr_tooltip = __('Your messages are encrypted, but your buddy has not been verified.');
  1114. } else if (data.otr_status == VERIFIED){
  1115. data.otr_tooltip = __('Your messages are encrypted and your buddy verified.');
  1116. } else if (data.otr_status == FINISHED){
  1117. data.otr_tooltip = __('Your buddy has closed their end of the private session, you should do the same');
  1118. }
  1119. data.allow_otr = converse.allow_otr && !this.is_chatroom;
  1120. data.show_emoticons = converse.show_emoticons;
  1121. data.otr_translated_status = OTR_TRANSLATED_MAPPING[data.otr_status];
  1122. data.otr_status_class = OTR_CLASS_MAPPING[data.otr_status];
  1123. this.$el.find('.chat-toolbar').html(this.toolbar_template(data));
  1124. }
  1125. return this;
  1126. },
  1127. renderAvatar: function () {
  1128. if (!this.model.get('image')) {
  1129. return;
  1130. }
  1131. var img_src = 'data:'+this.model.get('image_type')+';base64,'+this.model.get('image'),
  1132. canvas = $('<canvas height="33px" width="33px" class="avatar"></canvas>').get(0);
  1133. if (!(canvas.getContext && canvas.getContext('2d'))) {
  1134. return this;
  1135. }
  1136. var ctx = canvas.getContext('2d');
  1137. var img = new Image(); // Create new Image object
  1138. img.onload = function() {
  1139. var ratio = img.width/img.height;
  1140. ctx.drawImage(img, 0,0, 35*ratio, 35);
  1141. };
  1142. img.src = img_src;
  1143. this.$el.find('.chat-title').before(canvas);
  1144. return this;
  1145. },
  1146. focus: function () {
  1147. this.$el.find('.chat-textarea').focus();
  1148. return this;
  1149. },
  1150. hide: function () {
  1151. if (this.$el.is(':visible') && this.$el.css('opacity') == "1") {
  1152. if (converse.animate) {
  1153. this.$el.hide('fast');
  1154. } else {
  1155. this.$el.hide();
  1156. }
  1157. converse.emit('onChatBoxClosed', this);
  1158. }
  1159. },
  1160. show: function () {
  1161. if (this.$el.is(':visible') && this.$el.css('opacity') == "1") {
  1162. converse.emit('onChatBoxFocused', this);
  1163. return this.focus();
  1164. }
  1165. if (converse.animate) {
  1166. this.$el.css({'opacity': 0, 'display': 'inline'}).animate({opacity: '1'}, 200);
  1167. } else {
  1168. this.$el.css({'opacity': 1, 'display': 'inline'});
  1169. }
  1170. if (converse.connection) {
  1171. // Without a connection, we haven't yet initialized
  1172. // localstorage
  1173. this.model.save();
  1174. }
  1175. converse.emit('onChatBoxOpened', this);
  1176. return this;
  1177. },
  1178. scrollDown: function () {
  1179. var $content = this.$el.find('.chat-content');
  1180. $content.scrollTop($content[0].scrollHeight);
  1181. return this;
  1182. }
  1183. });
  1184. this.ContactsPanel = Backbone.View.extend({
  1185. tagName: 'div',
  1186. className: 'oc-chat-content',
  1187. id: 'users',
  1188. events: {
  1189. 'click a.toggle-xmpp-contact-form': 'toggleContactForm',
  1190. 'submit form.add-xmpp-contact': 'addContactFromForm',
  1191. 'submit form.search-xmpp-contact': 'searchContacts',
  1192. 'click a.subscribe-to-user': 'addContactFromList'
  1193. },
  1194. tab_template: _.template('<li><a class="s current" href="#users">'+__('Contacts')+'</a></li>'),
  1195. template: _.template(
  1196. '<form class="set-xmpp-status" action="" method="post">'+
  1197. '<span id="xmpp-status-holder">'+
  1198. '<select id="select-xmpp-status" style="display:none">'+
  1199. '<option value="online">'+__('Online')+'</option>'+
  1200. '<option value="dnd">'+__('Busy')+'</option>'+
  1201. '<option value="away">'+__('Away')+'</option>'+
  1202. '<option value="offline">'+__('Offline')+'</option>'+
  1203. '</select>'+
  1204. '</span>'+
  1205. '</form>'
  1206. ),
  1207. add_contact_dropdown_template: _.template(
  1208. '<dl class="add-converse-contact dropdown">' +
  1209. '<dt id="xmpp-contact-search" class="fancy-dropdown">' +
  1210. '<a class="toggle-xmpp-contact-form" href="#"'+
  1211. 'title="'+__('Click to add new chat contacts')+'">'+
  1212. '<span class="icon-plus"></span>'+__('Add a contact')+'</a>' +
  1213. '</dt>' +
  1214. '<dd class="search-xmpp" style="display:none"><ul></ul></dd>' +
  1215. '</dl>'
  1216. ),
  1217. add_contact_form_template: _.template(
  1218. '<li>'+
  1219. '<form class="add-xmpp-contact">' +
  1220. '<input type="text" name="identifier" class="username" placeholder="'+__('Contact username')+'"/>' +
  1221. '<button type="submit">'+__('Add')+'</button>' +
  1222. '</form>'+
  1223. '<li>'
  1224. ),
  1225. search_contact_template: _.template(
  1226. '<li>'+
  1227. '<form class="search-xmpp-contact">' +
  1228. '<input type="text" name="identifier" class="username" placeholder="'+__('Contact name')+'"/>' +
  1229. '<button type="submit">'+__('Search')+'</button>' +
  1230. '</form>'+
  1231. '<li>'
  1232. ),
  1233. initialize: function (cfg) {
  1234. cfg.$parent.append(this.$el);
  1235. this.$tabs = cfg.$parent.parent().find('#controlbox-tabs');
  1236. },
  1237. render: function () {
  1238. var markup;
  1239. var widgets = this.template();
  1240. this.$tabs.append(this.tab_template());
  1241. if (converse.xhr_user_search) {
  1242. markup = this.search_contact_template();
  1243. } else {
  1244. markup = this.add_contact_form_template();
  1245. }
  1246. if (converse.allow_contact_requests) {
  1247. widgets += this.add_contact_dropdown_template();
  1248. }
  1249. this.$el.html(widgets);
  1250. this.$el.find('.search-xmpp ul').append(markup);
  1251. this.$el.append(converse.rosterview.$el);
  1252. return this;
  1253. },
  1254. toggleContactForm: function (ev) {
  1255. ev.preventDefault();
  1256. this.$el.find('.search-xmpp').toggle('fast', function () {
  1257. if ($(this).is(':visible')) {
  1258. $(this).find('input.username').focus();
  1259. }
  1260. });
  1261. },
  1262. searchContacts: function (ev) {
  1263. ev.preventDefault();
  1264. $.getJSON(xhr_user_search_url+ "?q=" + $(ev.target).find('input.username').val(), function (data) {
  1265. var $ul= $('.search-xmpp ul');
  1266. $ul.find('li.found-user').remove();
  1267. $ul.find('li.chat-info').remove();
  1268. if (!data.length) {
  1269. $ul.append('<li class="chat-info">'+__('No users found')+'</li>');
  1270. }
  1271. $(data).each(function (idx, obj) {
  1272. $ul.append(
  1273. $('<li class="found-user"></li>')
  1274. .append(
  1275. $('<a class="subscribe-to-user" href="#" title="'+__('Click to add as a chat contact')+'"></a>')
  1276. .attr('data-recipient', Strophe.escapeNode(obj.id)+'@'+converse.domain)
  1277. .text(obj.fullname)
  1278. )
  1279. );
  1280. });
  1281. });
  1282. },
  1283. addContactFromForm: function (ev) {
  1284. ev.preventDefault();
  1285. var $input = $(ev.target).find('input');
  1286. var jid = $input.val();
  1287. if (! jid) {
  1288. // this is not a valid JID
  1289. $input.addClass('error');
  1290. return;
  1291. }
  1292. this.addContact(jid);
  1293. $('.search-xmpp').hide();
  1294. },
  1295. addContactFromList: function (ev) {
  1296. ev.preventDefault();
  1297. var $target = $(ev.target),
  1298. jid = $target.attr('data-recipient'),
  1299. name = $target.text();
  1300. this.addContact(jid, name);
  1301. $target.parent().remove();
  1302. $('.search-xmpp').hide();
  1303. },
  1304. addContact: function (jid, name) {
  1305. name = _.isEmpty(name)? jid: name;
  1306. converse.connection.roster.add(jid, name, [], function (iq) {
  1307. converse.connection.roster.subscribe(jid, null, converse.xmppstatus.get('fullname'));
  1308. });
  1309. }
  1310. });
  1311. this.RoomsPanel = Backbone.View.extend({
  1312. tagName: 'div',
  1313. id: 'chatrooms',
  1314. events: {
  1315. 'submit form.add-chatroom': 'createChatRoom',
  1316. 'click input#show-rooms': 'showRooms',
  1317. 'click a.open-room': 'createChatRoom',
  1318. 'click a.room-info': 'showRoomInfo'
  1319. },
  1320. room_template: _.template(
  1321. '<dd class="available-chatroom">'+
  1322. '<a class="open-room" data-room-jid="{{jid}}"'+
  1323. 'title="'+__('Click to open this room')+'" href="#">{{name}}</a>'+
  1324. '<a class="room-info icon-room-info" data-room-jid="{{jid}}"'+
  1325. 'title="'+__('Show more information on this room')+'" href="#">&nbsp;</a>'+
  1326. '</dd>'),
  1327. // FIXME: check markup in mockup
  1328. room_description_template: _.template(
  1329. '<div class="room-info">'+
  1330. '<p class="room-info"><strong>'+__('Description:')+'</strong> {{desc}}</p>' +
  1331. '<p class="room-info"><strong>'+__('Occupants:')+'</strong> {{occ}}</p>' +
  1332. '<p class="room-info"><strong>'+__('Features:')+'</strong> <ul>'+
  1333. '{[ if (passwordprotected) { ]}' +
  1334. '<li class="room-info locked">'+__('Requires authentication')+'</li>' +
  1335. '{[ } ]}' +
  1336. '{[ if (hidden) { ]}' +
  1337. '<li class="room-info">'+__('Hidden')+'</li>' +
  1338. '{[ } ]}' +
  1339. '{[ if (membersonly) { ]}' +
  1340. '<li class="room-info">'+__('Requires an invitation')+'</li>' +
  1341. '{[ } ]}' +
  1342. '{[ if (moderated) { ]}' +
  1343. '<li class="room-info">'+__('Moderated')+'</li>' +
  1344. '{[ } ]}' +
  1345. '{[ if (nonanonymous) { ]}' +
  1346. '<li class="room-info">'+__('Non-anonymous')+'</li>' +
  1347. '{[ } ]}' +
  1348. '{[ if (open) { ]}' +
  1349. '<li class="room-info">'+__('Open room')+'</li>' +
  1350. '{[ } ]}' +
  1351. '{[ if (persistent) { ]}' +
  1352. '<li class="room-info">'+__('Permanent room')+'</li>' +
  1353. '{[ } ]}' +
  1354. '{[ if (publicroom) { ]}' +
  1355. '<li class="room-info">'+__('Public')+'</li>' +
  1356. '{[ } ]}' +
  1357. '{[ if (semianonymous) { ]}' +
  1358. '<li class="room-info">'+__('Semi-anonymous')+'</li>' +
  1359. '{[ } ]}' +
  1360. '{[ if (temporary) { ]}' +
  1361. '<li class="room-info">'+__('Temporary room')+'</li>' +
  1362. '{[ } ]}' +
  1363. '{[ if (unmoderated) { ]}' +
  1364. '<li class="room-info">'+__('Unmoderated')+'</li>' +
  1365. '{[ } ]}' +
  1366. '</p>' +
  1367. '</div>'
  1368. ),
  1369. tab_template: _.template('<li><a class="s" href="#chatrooms">'+__('Rooms')+'</a></li>'),
  1370. template: _.template(
  1371. '<form class="add-chatroom" action="" method="post">'+
  1372. '<input type="text" name="chatroom" class="new-chatroom-name" placeholder="'+__('Room name')+'"/>'+
  1373. '<input type="text" name="nick" class="new-chatroom-nick" placeholder="'+__('Nickname')+'"/>'+
  1374. '<input type="{{ server_input_type }}" name="server" class="new-chatroom-server" placeholder="'+__('Server')+'"/>'+
  1375. '<input type="submit" name="join" value="'+__('Join')+'"/>'+
  1376. '<input type="button" name="show" id="show-rooms" value="'+__('Show rooms')+'"/>'+
  1377. '</form>'+
  1378. '<dl id="available-chatrooms"></dl>'),
  1379. initialize: function (cfg) {
  1380. cfg.$parent.append(
  1381. this.$el.html(
  1382. this.template({
  1383. server_input_type: converse.hide_muc_server && 'hidden' || 'text'
  1384. })
  1385. ).hide());
  1386. this.$tabs = cfg.$parent.parent().find('#controlbox-tabs');
  1387. this.on('update-rooms-list', function (ev) {
  1388. this.updateRoomsList();
  1389. });
  1390. converse.xmppstatus.on("change", $.proxy(function (model) {
  1391. if (!(_.has(model.changed, 'fullname'))) {
  1392. return;
  1393. }
  1394. var $nick = this.$el.find('input.new-chatroom-nick');
  1395. if (! $nick.is(':focus')) {
  1396. $nick.val(model.get('fullname'));
  1397. }
  1398. }, this));
  1399. },
  1400. render: function () {
  1401. this.$tabs.append(this.tab_template());
  1402. return this;
  1403. },
  1404. informNoRoomsFound: function () {
  1405. var $available_chatrooms = this.$el.find('#available-chatrooms');
  1406. // # For translators: %1$s is a variable and will be replaced with the XMPP server name
  1407. $available_chatrooms.html('<dt>'+__('No rooms on %1$s',this.muc_domain)+'</dt>');
  1408. $('input#show-rooms').show().siblings('span.spinner').remove();
  1409. },
  1410. updateRoomsList: function (domain) {
  1411. converse.connection.muc.listRooms(
  1412. this.muc_domain,
  1413. $.proxy(function (iq) { // Success
  1414. var name, jid, i, fragment,
  1415. that = this,
  1416. $available_chatrooms = this.$el.find('#available-chatrooms');
  1417. this.rooms = $(iq).find('query').find('item');
  1418. if (this.rooms.length) {
  1419. // # For translators: %1$s is a variable and will be
  1420. // # replaced with the XMPP server name
  1421. $available_chatrooms.html('<dt>'+__('Rooms on %1$s',this.muc_domain)+'</dt>');
  1422. fragment = document.createDocumentFragment();
  1423. for (i=0; i<this.rooms.length; i++) {
  1424. name = Strophe.unescapeNode($(this.rooms[i]).attr('name')||$(this.rooms[i]).attr('jid'));
  1425. jid = $(this.rooms[i]).attr('jid');
  1426. fragment.appendChild($(this.room_template({
  1427. 'name':name,
  1428. 'jid':jid
  1429. }))[0]);
  1430. }
  1431. $available_chatrooms.append(fragment);
  1432. $('input#show-rooms').show().siblings('span.spinner').remove();
  1433. } else {
  1434. this.informNoRoomsFound();
  1435. }
  1436. return true;
  1437. }, this),
  1438. $.proxy(function (iq) { // Failure
  1439. this.informNoRoomsFound();
  1440. }, this));
  1441. },
  1442. showRooms: function (ev) {
  1443. var $available_chatrooms = this.$el.find('#available-chatrooms');
  1444. var $server = this.$el.find('input.new-chatroom-server');
  1445. var server = $server.val();
  1446. if (!server) {
  1447. $server.addClass('error');
  1448. return;
  1449. }
  1450. this.$el.find('input.new-chatroom-name').removeClass('error');
  1451. $server.removeClass('error');
  1452. $available_chatrooms.empty();
  1453. $('input#show-rooms').hide().after('<span class="spinner"/>');
  1454. this.muc_domain = server;
  1455. this.updateRoomsList();
  1456. },
  1457. showRoomInfo: function (ev) {
  1458. var target = ev.target,
  1459. $dd = $(target).parent('dd'),
  1460. $div = $dd.find('div.room-info');
  1461. if ($div.length) {
  1462. $div.remove();
  1463. } else {
  1464. $dd.find('span.spinner').remove();
  1465. $dd.append('<span class="spinner hor_centered"/>');
  1466. converse.connection.disco.info(
  1467. $(target).attr('data-room-jid'),
  1468. null,
  1469. $.proxy(function (stanza) {
  1470. var $stanza = $(stanza);
  1471. // All MUC features found here: http://xmpp.org/registrar/disco-features.html
  1472. $dd.find('span.spinner').replaceWith(
  1473. this.room_description_template({
  1474. 'desc': $stanza.find('field[var="muc#roominfo_description"] value').text(),
  1475. 'occ': $stanza.find('field[var="muc#roominfo_occupants"] value').text(),
  1476. 'hidden': $stanza.find('feature[var="muc_hidden"]').length,
  1477. 'membersonly': $stanza.find('feature[var="muc_membersonly"]').length,
  1478. 'moderated': $stanza.find('feature[var="muc_moderated"]').length,
  1479. 'nonanonymous': $stanza.find('feature[var="muc_nonanonymous"]').length,
  1480. 'open': $stanza.find('feature[var="muc_open"]').length,
  1481. 'passwordprotected': $stanza.find('feature[var="muc_passwordprotected"]').length,
  1482. 'persistent': $stanza.find('feature[var="muc_persistent"]').length,
  1483. 'publicroom': $stanza.find('feature[var="muc_public"]').length,
  1484. 'semianonymous': $stanza.find('feature[var="muc_semianonymous"]').length,
  1485. 'temporary': $stanza.find('feature[var="muc_temporary"]').length,
  1486. 'unmoderated': $stanza.find('feature[var="muc_unmoderated"]').length
  1487. }));
  1488. }, this));
  1489. }
  1490. },
  1491. createChatRoom: function (ev) {
  1492. ev.preventDefault();
  1493. var name, $name,
  1494. server, $server,
  1495. jid,
  1496. $nick = this.$el.find('input.new-chatroom-nick'),
  1497. nick = $nick.val(),
  1498. chatroom;
  1499. if (!nick) { $nick.addClass('error'); }
  1500. else { $nick.removeClass('error'); }
  1501. if (ev.type === 'click') {
  1502. jid = $(ev.target).attr('data-room-jid');
  1503. } else {
  1504. $name = this.$el.find('input.new-chatroom-name');
  1505. $server= this.$el.find('input.new-chatroom-server');
  1506. server = $server.val();
  1507. name = $name.val().trim().toLowerCase();
  1508. $name.val(''); // Clear the input
  1509. if (name && server) {
  1510. jid = Strophe.escapeNode(name) + '@' + server;
  1511. $name.removeClass('error');
  1512. $server.removeClass('error');
  1513. this.muc_domain = server;
  1514. } else {
  1515. if (!name) { $name.addClass('error'); }
  1516. if (!server) { $server.addClass('error'); }
  1517. return;
  1518. }
  1519. }
  1520. if (!nick) { return; }
  1521. chatroom = converse.chatboxesview.showChatBox({
  1522. 'id': jid,
  1523. 'jid': jid,
  1524. 'name': Strophe.unescapeNode(Strophe.getNodeFromJid(jid)),
  1525. 'nick': nick,
  1526. 'chatroom': true,
  1527. 'box_id' : hex_sha1(jid)
  1528. });
  1529. if (!chatroom.get('connected')) {
  1530. converse.chatboxesview.views[jid].connect(null);
  1531. }
  1532. }
  1533. });
  1534. this.ControlBoxView = converse.ChatBoxView.extend({
  1535. tagName: 'div',
  1536. className: 'chatbox',
  1537. id: 'controlbox',
  1538. events: {
  1539. 'click a.close-chatbox-button': 'closeChat',
  1540. 'click ul#controlbox-tabs li a': 'switchTab'
  1541. },
  1542. initialize: function () {
  1543. this.$el.appendTo(converse.chatboxesview.$el);
  1544. this.model.on('change', $.proxy(function (item, changed) {
  1545. var i;
  1546. if (_.has(item.changed, 'connected')) {
  1547. this.render();
  1548. converse.features.on('add', $.proxy(this.featureAdded, this));
  1549. // Features could have been added before the controlbox was
  1550. // initialized. Currently we're only interested in MUC
  1551. var feature = converse.features.findWhere({'var': 'http://jabber.org/protocol/muc'});
  1552. if (feature) {
  1553. this.featureAdded(feature);
  1554. }
  1555. }
  1556. if (_.has(item.changed, 'visible')) {
  1557. if (item.changed.visible === true) {
  1558. this.show();
  1559. }
  1560. }
  1561. }, this));
  1562. this.model.on('show', this.show, this);
  1563. this.model.on('destroy', this.hide, this);
  1564. this.model.on('hide', this.hide, this);
  1565. if (this.model.get('visible')) {
  1566. this.show();
  1567. }
  1568. },
  1569. featureAdded: function (feature) {
  1570. if ((feature.get('var') == 'http://jabber.org/protocol/muc') && (converse.allow_muc)) {
  1571. this.roomspanel.muc_domain = feature.get('from');
  1572. var $server= this.$el.find('input.new-chatroom-server');
  1573. if (! $server.is(':focus')) {
  1574. $server.val(this.roomspanel.muc_domain);
  1575. }
  1576. if (converse.auto_list_rooms) {
  1577. this.roomspanel.trigger('update-rooms-list');
  1578. }
  1579. }
  1580. },
  1581. template: _.template(
  1582. '<div class="chat-head oc-chat-head">'+
  1583. '<ul id="controlbox-tabs"></ul>'+
  1584. '<a class="close-chatbox-button icon-close"></a>'+
  1585. '</div>'+
  1586. '<div class="controlbox-panes"></div>'
  1587. ),
  1588. switchTab: function (ev) {
  1589. ev.preventDefault();
  1590. var $tab = $(ev.target),
  1591. $sibling = $tab.parent().siblings('li').children('a'),
  1592. $tab_panel = $($tab.attr('href')),
  1593. $sibling_panel = $($sibling.attr('href'));
  1594. $sibling_panel.fadeOut('fast', function () {
  1595. $sibling.removeClass('current');
  1596. $tab.addClass('current');
  1597. $tab_panel.fadeIn('fast', function () {
  1598. });
  1599. });
  1600. },
  1601. showHelpMessages: function (msgs) {
  1602. // Override showHelpMessages in ChatBoxView, for now do nothing.
  1603. return;
  1604. },
  1605. render: function () {
  1606. if ((!converse.prebind) && (!converse.connection)) {
  1607. // Add login panel if the user still has to authenticate
  1608. this.$el.html(this.template(this.model.toJSON()));
  1609. this.loginpanel = new converse.LoginPanel({'$parent': this.$el.find('.controlbox-panes'), 'model': this});
  1610. this.loginpanel.render();
  1611. } else if (!this.contactspanel) {
  1612. this.$el.html(this.template(this.model.toJSON()));
  1613. this.contactspanel = new converse.ContactsPanel({'$parent': this.$el.find('.controlbox-panes')});
  1614. this.contactspanel.render();
  1615. converse.xmppstatusview = new converse.XMPPStatusView({'model': converse.xmppstatus});
  1616. converse.xmppstatusview.render();
  1617. if (converse.allow_muc) {
  1618. this.roomspanel = new converse.RoomsPanel({'$parent': this.$el.find('.controlbox-panes')});
  1619. this.roomspanel.render();
  1620. }
  1621. }
  1622. return this;
  1623. }
  1624. });
  1625. this.ChatRoomView = converse.ChatBoxView.extend({
  1626. length: 300,
  1627. tagName: 'div',
  1628. className: 'chatroom',
  1629. events: {
  1630. 'click a.close-chatbox-button': 'closeChat',
  1631. 'click a.configure-chatroom-button': 'configureChatRoom',
  1632. 'click .toggle-smiley': 'toggleEmoticonMenu',
  1633. 'click .toggle-smiley ul li': 'insertEmoticon',
  1634. 'keypress textarea.chat-textarea': 'keyPressed'
  1635. },
  1636. info_template: _.template('<div class="chat-info">{{message}}</div>'),
  1637. is_chatroom: true,
  1638. sendChatRoomMessage: function (body) {
  1639. var match = body.replace(/^\s*/, "").match(/^\/(.*?)(?: (.*))?$/) || [false],
  1640. $chat_content;
  1641. switch (match[1]) {
  1642. case 'msg':
  1643. // TODO: Private messages
  1644. break;
  1645. case 'clear':
  1646. this.$el.find('.chat-content').empty();
  1647. break;
  1648. case 'topic':
  1649. converse.connection.muc.setTopic(this.model.get('jid'), match[2]);
  1650. break;
  1651. case 'kick':
  1652. converse.connection.muc.kick(this.model.get('jid'), match[2]);
  1653. break;
  1654. case 'ban':
  1655. converse.connection.muc.ban(this.model.get('jid'), match[2]);
  1656. break;
  1657. case 'op':
  1658. converse.connection.muc.op(this.model.get('jid'), match[2]);
  1659. break;
  1660. case 'deop':
  1661. converse.connection.muc.deop(this.model.get('jid'), match[2]);
  1662. break;
  1663. case 'help':
  1664. $chat_content = this.$el.find('.chat-content');
  1665. msgs = [
  1666. '<strong>/help</strong>:'+__('Show this menu')+'',
  1667. '<strong>/me</strong>:'+__('Write in the third person')+'',
  1668. '<strong>/topic</strong>:'+__('Set chatroom topic')+'',
  1669. '<strong>/kick</strong>:'+__('Kick user from chatroom')+'',
  1670. '<strong>/ban</strong>:'+__('Ban user from chatroom')+'',
  1671. '<strong>/clear</strong>:'+__('Remove messages')+''
  1672. ];
  1673. this.showHelpMessages(msgs);
  1674. break;
  1675. default:
  1676. this.last_msgid = converse.connection.muc.groupchat(this.model.get('jid'), body);
  1677. break;
  1678. }
  1679. },
  1680. template: _.template(
  1681. '<div class="chat-head chat-head-chatroom">' +
  1682. '<a class="close-chatbox-button icon-close"></a>' +
  1683. '<a class="configure-chatroom-button icon-wrench" style="display:none"></a>' +
  1684. '<div class="chat-title"> {{ name }} </div>' +
  1685. '<p class="chatroom-topic"><p/>' +
  1686. '</div>' +
  1687. '<div class="chat-body">' +
  1688. '<span class="spinner centered"/>' +
  1689. '</div>'),
  1690. chatarea_template: _.template(
  1691. '<div class="chat-area">' +
  1692. '<div class="chat-content"></div>' +
  1693. '<form class="sendXMPPMessage" action="" method="post">' +
  1694. '{[ if ('+converse.show_toolbar+') { ]}' +
  1695. '<ul class="chat-toolbar no-text-select"></ul>'+
  1696. '{[ } ]}' +
  1697. '<textarea type="text" class="chat-textarea" ' +
  1698. 'placeholder="'+__('Message')+'"/>' +
  1699. '</form>' +
  1700. '</div>' +
  1701. '<div class="participants">' +
  1702. '<ul class="participant-list"></ul>' +
  1703. '</div>'
  1704. ),
  1705. render: function () {
  1706. this.$el.attr('id', this.model.get('box_id'))
  1707. .html(this.template(this.model.toJSON()));
  1708. return this;
  1709. },
  1710. renderChatArea: function () {
  1711. if (!this.$el.find('.chat-area').length) {
  1712. this.$el.find('.chat-body').empty().append(this.chatarea_template());
  1713. this.renderToolbar();
  1714. }
  1715. return this;
  1716. },
  1717. connect: function (password) {
  1718. if (_.has(converse.connection.muc.rooms, this.model.get('jid'))) {
  1719. // If the room exists, it already has event listeners, so we
  1720. // doing add them again.
  1721. converse.connection.muc.join(
  1722. this.model.get('jid'), this.model.get('nick'), null, null, null, password);
  1723. } else {
  1724. converse.connection.muc.join(
  1725. this.model.get('jid'),
  1726. this.model.get('nick'),
  1727. $.proxy(this.onChatRoomMessage, this),
  1728. $.proxy(this.onChatRoomPresence, this),
  1729. $.proxy(this.onChatRoomRoster, this),
  1730. password);
  1731. }
  1732. },
  1733. initialize: function () {
  1734. this.connect(null);
  1735. this.model.messages.on('add', this.onMessageAdded, this);
  1736. this.model.on('destroy', function (model, response, options) {
  1737. this.hide();
  1738. converse.connection.muc.leave(
  1739. this.model.get('jid'),
  1740. this.model.get('nick'),
  1741. $.proxy(this.onLeave, this),
  1742. undefined);
  1743. },
  1744. this);
  1745. this.$el.appendTo(converse.chatboxesview.$el);
  1746. this.render().show().model.messages.fetch({add: true});
  1747. },
  1748. onLeave: function () {
  1749. this.model.set('connected', false);
  1750. },
  1751. form_input_template: _.template('<label>{{label}}<input name="{{name}}" type="{{type}}" value="{{value}}"></label>'),
  1752. select_option_template: _.template('<option value="{{value}}">{{label}}</option>'),
  1753. form_select_template: _.template('<label>{{label}}<select name="{{name}}">{{options}}</select></label>'),
  1754. form_checkbox_template: _.template('<label>{{label}}<input name="{{name}}" type="{{type}}" {{checked}}"></label>'),
  1755. renderConfigurationForm: function (stanza) {
  1756. var $form= this.$el.find('form.chatroom-form'),
  1757. $stanza = $(stanza),
  1758. $fields = $stanza.find('field'),
  1759. title = $stanza.find('title').text(),
  1760. instructions = $stanza.find('instructions').text(),
  1761. i, j, options=[];
  1762. var input_types = {
  1763. 'text-private': 'password',
  1764. 'text-single': 'textline',
  1765. 'boolean': 'checkbox',
  1766. 'hidden': 'hidden',
  1767. 'list-single': 'dropdown'
  1768. };
  1769. $form.find('span.spinner').remove();
  1770. $form.append($('<legend>').text(title));
  1771. if (instructions != title) {
  1772. $form.append($('<p>').text(instructions));
  1773. }
  1774. for (i=0; i<$fields.length; i++) {
  1775. $field = $($fields[i]);
  1776. if ($field.attr('type') == 'list-single') {
  1777. options = [];
  1778. $options = $field.find('option');
  1779. for (j=0; j<$options.length; j++) {
  1780. options.push(this.select_option_template({
  1781. value: $($options[j]).find('value').text(),
  1782. label: $($options[j]).attr('label')
  1783. }));
  1784. }
  1785. $form.append(this.form_select_template({
  1786. name: $field.attr('var'),
  1787. label: $field.attr('label'),
  1788. options: options.join('')
  1789. }));
  1790. } else if ($field.attr('type') == 'boolean') {
  1791. $form.append(this.form_checkbox_template({
  1792. name: $field.attr('var'),
  1793. type: input_types[$field.attr('type')],
  1794. label: $field.attr('label') || '',
  1795. checked: $field.find('value').text() === "1" && 'checked="1"' || ''
  1796. }));
  1797. } else {
  1798. $form.append(this.form_input_template({
  1799. name: $field.attr('var'),
  1800. type: input_types[$field.attr('type')],
  1801. label: $field.attr('label') || '',
  1802. value: $field.find('value').text()
  1803. }));
  1804. }
  1805. }
  1806. $form.append('<input type="submit" value="'+__('Save')+'"/>');
  1807. $form.append('<input type="button" value="'+__('Cancel')+'"/>');
  1808. $form.on('submit', $.proxy(this.saveConfiguration, this));
  1809. $form.find('input[type=button]').on('click', $.proxy(this.cancelConfiguration, this));
  1810. },
  1811. field_template: _.template('<field var="{{name}}"><value>{{value}}</value></field>'),
  1812. saveConfiguration: function (ev) {
  1813. ev.preventDefault();
  1814. var that = this;
  1815. var $inputs = $(ev.target).find(':input:not([type=button]):not([type=submit])'),
  1816. count = $inputs.length,
  1817. configArray = [];
  1818. $inputs.each(function () {
  1819. var $input = $(this), value;
  1820. if ($input.is('[type=checkbox]')) {
  1821. value = $input.is(':checked') && 1 || 0;
  1822. } else {
  1823. value = $input.val();
  1824. }
  1825. var cnode = $(that.field_template({
  1826. name: $input.attr('name'),
  1827. value: value
  1828. }))[0];
  1829. configArray.push(cnode);
  1830. if (!--count) {
  1831. converse.connection.muc.saveConfiguration(
  1832. that.model.get('jid'),
  1833. configArray,
  1834. $.proxy(that.onConfigSaved, that),
  1835. $.proxy(that.onErrorConfigSaved, that)
  1836. );
  1837. }
  1838. });
  1839. this.$el.find('div.chatroom-form-container').hide(
  1840. function () {
  1841. $(this).remove();
  1842. that.$el.find('.chat-area').show();
  1843. that.$el.find('.participants').show();
  1844. });
  1845. },
  1846. onConfigSaved: function (stanza) {
  1847. // XXX
  1848. },
  1849. onErrorConfigSaved: function (stanza) {
  1850. this.showStatusNotification(__("An error occurred while trying to save the form."));
  1851. },
  1852. cancelConfiguration: function (ev) {
  1853. ev.preventDefault();
  1854. var that = this;
  1855. this.$el.find('div.chatroom-form-container').hide(
  1856. function () {
  1857. $(this).remove();
  1858. that.$el.find('.chat-area').show();
  1859. that.$el.find('.participants').show();
  1860. });
  1861. },
  1862. configureChatRoom: function (ev) {
  1863. ev.preventDefault();
  1864. if (this.$el.find('div.chatroom-form-container').length) {
  1865. return;
  1866. }
  1867. this.$el.find('.chat-area').hide();
  1868. this.$el.find('.participants').hide();
  1869. this.$el.find('.chat-body').append(
  1870. $('<div class="chatroom-form-container">'+
  1871. '<form class="chatroom-form">'+
  1872. '<span class="spinner centered"/>'+
  1873. '</form>'+
  1874. '</div>'));
  1875. converse.connection.muc.configure(
  1876. this.model.get('jid'),
  1877. $.proxy(this.renderConfigurationForm, this)
  1878. );
  1879. },
  1880. submitPassword: function (ev) {
  1881. ev.preventDefault();
  1882. var password = this.$el.find('.chatroom-form').find('input[type=password]').val();
  1883. this.$el.find('.chatroom-form-container').replaceWith(
  1884. '<span class="spinner centered"/>');
  1885. this.connect(password);
  1886. },
  1887. renderPasswordForm: function () {
  1888. this.$el.find('span.centered.spinner').remove();
  1889. this.$el.find('.chat-body').append(
  1890. $('<div class="chatroom-form-container">'+
  1891. '<form class="chatroom-form">'+
  1892. '<legend>'+__('This chatroom requires a password')+'</legend>' +
  1893. '<label>'+__('Password: ')+'<input type="password" name="password"/></label>' +
  1894. '<input type="submit" value="'+__('Submit')+'/>' +
  1895. '</form>'+
  1896. '</div>'));
  1897. this.$el.find('.chatroom-form').on('submit', $.proxy(this.submitPassword, this));
  1898. },
  1899. showDisconnectMessage: function (msg) {
  1900. this.$el.find('.chat-area').remove();
  1901. this.$el.find('.participants').remove();
  1902. this.$el.find('span.centered.spinner').remove();
  1903. this.$el.find('.chat-body').append($('<p>'+msg+'</p>'));
  1904. },
  1905. infoMessages: {
  1906. 100: __('This room is not anonymous'),
  1907. 102: __('This room now shows unavailable members'),
  1908. 103: __('This room does not show unavailable members'),
  1909. 104: __('Non-privacy-related room configuration has changed'),
  1910. 170: __('Room logging is now enabled'),
  1911. 171: __('Room logging is now disabled'),
  1912. 172: __('This room is now non-anonymous'),
  1913. 173: __('This room is now semi-anonymous'),
  1914. 174: __('This room is now fully-anonymous'),
  1915. 201: __('A new room has been created'),
  1916. 210: __('Your nickname has been changed')
  1917. },
  1918. actionInfoMessages: {
  1919. /* XXX: Note the triple underscore function and not double
  1920. * underscore.
  1921. *
  1922. * This is a hack. We can't pass the strings to __ because we
  1923. * don't yet know what the variable to interpolate is.
  1924. *
  1925. * Triple underscore will just return the string again, but we
  1926. * can then at least tell gettext to scan for it so that these
  1927. * strings are picked up by the translation machinery.
  1928. */
  1929. 301: ___("<strong>%1$s</strong> has been banned"),
  1930. 307: ___("<strong>%1$s</strong> has been kicked out"),
  1931. 321: ___("<strong>%1$s</strong> has been removed because of an affiliation change"),
  1932. 322: ___("<strong>%1$s</strong> has been removed for not being a member")
  1933. },
  1934. disconnectMessages: {
  1935. 301: __('You have been banned from this room'),
  1936. 307: __('You have been kicked from this room'),
  1937. 321: __("You have been removed from this room because of an affiliation change"),
  1938. 322: __("You have been removed from this room because the room has changed to members-only and you're not a member"),
  1939. 332: __("You have been removed from this room because the MUC (Multi-user chat) service is being shut down.")
  1940. },
  1941. showStatusMessages: function ($el, is_self) {
  1942. /* Check for status codes and communicate their purpose to the user
  1943. * See: http://xmpp.org/registrar/mucstatus.html
  1944. */
  1945. var $chat_content = this.$el.find('.chat-content'),
  1946. $stats = $el.find('status'),
  1947. disconnect_msgs = [],
  1948. info_msgs = [],
  1949. action_msgs = [],
  1950. msgs, i;
  1951. for (i=0; i<$stats.length; i++) {
  1952. var stat = $stats[i].getAttribute('code');
  1953. if (is_self) {
  1954. if (_.contains(_.keys(this.disconnectMessages), stat)) {
  1955. disconnect_msgs.push(this.disconnectMessages[stat]);
  1956. }
  1957. } else {
  1958. if (_.contains(_.keys(this.infoMessages), stat)) {
  1959. info_msgs.push(this.infoMessages[stat]);
  1960. } else if (_.contains(_.keys(this.actionInfoMessages), stat)) {
  1961. action_msgs.push(
  1962. __(this.actionInfoMessages[stat], Strophe.unescapeNode(Strophe.getResourceFromJid($el.attr('from'))))
  1963. );
  1964. }
  1965. }
  1966. }
  1967. if (disconnect_msgs.length > 0) {
  1968. for (i=0; i<disconnect_msgs.length; i++) {
  1969. this.showDisconnectMessage(disconnect_msgs[i]);
  1970. }
  1971. this.model.set('connected', false);
  1972. return;
  1973. }
  1974. this.renderChatArea();
  1975. for (i=0; i<info_msgs.length; i++) {
  1976. $chat_content.append(this.info_template({message: info_msgs[i]}));
  1977. }
  1978. for (i=0; i<action_msgs.length; i++) {
  1979. $chat_content.append(this.info_template({message: action_msgs[i]}));
  1980. }
  1981. return this.scrollDown();
  1982. },
  1983. showErrorMessage: function ($error, room) {
  1984. // We didn't enter the room, so we must remove it from the MUC
  1985. // add-on
  1986. delete converse.connection.muc[room.name];
  1987. if ($error.attr('type') == 'auth') {
  1988. if ($error.find('not-authorized').length) {
  1989. this.renderPasswordForm();
  1990. } else if ($error.find('registration-required').length) {
  1991. this.showDisconnectMessage(__('You are not on the member list of this room'));
  1992. } else if ($error.find('forbidden').length) {
  1993. this.showDisconnectMessage(__('You have been banned from this room'));
  1994. }
  1995. } else if ($error.attr('type') == 'modify') {
  1996. if ($error.find('jid-malformed').length) {
  1997. this.showDisconnectMessage(__('No nickname was specified'));
  1998. }
  1999. } else if ($error.attr('type') == 'cancel') {
  2000. if ($error.find('not-allowed').length) {
  2001. this.showDisconnectMessage(__('You are not allowed to create new rooms'));
  2002. } else if ($error.find('not-acceptable').length) {
  2003. this.showDisconnectMessage(__("Your nickname doesn't conform to this room's policies"));
  2004. } else if ($error.find('conflict').length) {
  2005. this.showDisconnectMessage(__("Your nickname is already taken"));
  2006. } else if ($error.find('item-not-found').length) {
  2007. this.showDisconnectMessage(__("This room does not (yet) exist"));
  2008. } else if ($error.find('service-unavailable').length) {
  2009. this.showDisconnectMessage(__("This room has reached it's maximum number of occupants"));
  2010. }
  2011. }
  2012. },
  2013. onChatRoomPresence: function (presence, room) {
  2014. var nick = room.nick,
  2015. $presence = $(presence),
  2016. from = $presence.attr('from'),
  2017. is_self = ($presence.find("status[code='110']").length) || (from == room.name+'/'+Strophe.escapeNode(nick)),
  2018. $item;
  2019. if ($presence.attr('type') === 'error') {
  2020. this.model.set('connected', false);
  2021. this.showErrorMessage($presence.find('error'), room);
  2022. } else {
  2023. this.model.set('connected', true);
  2024. this.showStatusMessages($presence, is_self);
  2025. if (!this.model.get('connected')) {
  2026. return true;
  2027. }
  2028. if ($presence.find("status[code='201']").length) {
  2029. // This is a new chatroom. We create an instant
  2030. // chatroom, and let the user manually set any
  2031. // configuration setting.
  2032. converse.connection.muc.createInstantRoom(room.name);
  2033. }
  2034. if (is_self) {
  2035. $item = $presence.find('item');
  2036. if ($item.length) {
  2037. if ($item.attr('affiliation') == 'owner') {
  2038. this.$el.find('a.configure-chatroom-button').show();
  2039. }
  2040. }
  2041. if ($presence.find("status[code='210']").length) {
  2042. // check if server changed our nick
  2043. this.model.set({'nick': Strophe.getResourceFromJid(from)});
  2044. }
  2045. }
  2046. }
  2047. return true;
  2048. },
  2049. onChatRoomMessage: function (message) {
  2050. var $message = $(message),
  2051. body = $message.children('body').text(),
  2052. jid = $message.attr('from'),
  2053. $chat_content = this.$el.find('.chat-content'),
  2054. resource = Strophe.getResourceFromJid(jid),
  2055. sender = resource && Strophe.unescapeNode(resource) || '',
  2056. delayed = $message.find('delay').length > 0,
  2057. subject = $message.children('subject').text(),
  2058. match, template, message_datetime, message_date, dates, isodate, stamp;
  2059. if (delayed) {
  2060. stamp = $message.find('delay').attr('stamp');
  2061. message_datetime = converse.parseISO8601(stamp);
  2062. } else {
  2063. message_datetime = new Date();
  2064. }
  2065. // If this message is on a different day than the one received
  2066. // prior, then indicate it on the chatbox.
  2067. dates = $chat_content.find("time").map(function(){return $(this).attr("datetime");}).get();
  2068. message_date = new Date(message_datetime.getTime());
  2069. message_date.setUTCHours(0,0,0,0);
  2070. isodate = converse.toISOString(message_date);
  2071. if (_.indexOf(dates, isodate) == -1) {
  2072. $chat_content.append(this.new_day_template({
  2073. isodate: isodate,
  2074. datestring: message_date.toString().substring(0,15)
  2075. }));
  2076. }
  2077. this.showStatusMessages($message);
  2078. if (subject) {
  2079. this.$el.find('.chatroom-topic').text(subject).attr('title', subject);
  2080. // # For translators: the %1$s and %2$s parts will get replaced by the user and topic text respectively
  2081. // # Example: Topic set by JC Brand to: Hello World!
  2082. $chat_content.append(
  2083. this.info_template({
  2084. 'message': __('Topic set by %1$s to: %2$s', sender, subject)
  2085. }));
  2086. }
  2087. if (!body) { return true; }
  2088. var display_sender = sender === this.model.get('nick') && 'me' || 'room';
  2089. this.showMessage($chat_content, {
  2090. 'message': body,
  2091. 'sender': display_sender,
  2092. 'fullname': sender,
  2093. 'time': converse.toISOString(message_datetime)
  2094. });
  2095. if (display_sender === 'room') {
  2096. // We only emit an event if it's not our own message
  2097. converse.emit('onMessage', message);
  2098. }
  2099. return true;
  2100. },
  2101. occupant_template: _.template(
  2102. '<li class="{{role}}" '+
  2103. '{[ if (role === "moderator") { ]}' +
  2104. 'title="'+__('This user is a moderator')+'"' +
  2105. '{[ } ]}'+
  2106. '{[ if (role === "participant") { ]}' +
  2107. 'title="'+__('This user can send messages in this room')+'"' +
  2108. '{[ } ]}'+
  2109. '{[ if (role === "visitor") { ]}' +
  2110. 'title="'+__('This user can NOT send messages in this room')+'"' +
  2111. '{[ } ]}'+
  2112. '>{{nick}}</li>'
  2113. ),
  2114. onChatRoomRoster: function (roster, room) {
  2115. this.renderChatArea();
  2116. var controlboxview = converse.chatboxesview.views.controlbox,
  2117. roster_size = _.size(roster),
  2118. $participant_list = this.$el.find('.participant-list'),
  2119. participants = [], keys = _.keys(roster), i;
  2120. this.$el.find('.participant-list').empty();
  2121. for (i=0; i<roster_size; i++) {
  2122. participants.push(
  2123. this.occupant_template({
  2124. role: roster[keys[i]].role,
  2125. nick: Strophe.unescapeNode(keys[i])
  2126. }));
  2127. }
  2128. $participant_list.append(participants.join(""));
  2129. return true;
  2130. }
  2131. });
  2132. this.ChatBoxes = Backbone.Collection.extend({
  2133. model: converse.ChatBox,
  2134. onConnected: function () {
  2135. this.localStorage = new Backbone.LocalStorage(
  2136. hex_sha1('converse.chatboxes-'+converse.bare_jid));
  2137. if (!this.get('controlbox')) {
  2138. this.add({
  2139. id: 'controlbox',
  2140. box_id: 'controlbox'
  2141. });
  2142. } else {
  2143. this.get('controlbox').save();
  2144. }
  2145. // This will make sure the Roster is set up
  2146. this.get('controlbox').set({connected:true});
  2147. // Register message handler
  2148. converse.connection.addHandler(
  2149. $.proxy(function (message) {
  2150. this.messageReceived(message);
  2151. return true;
  2152. }, this), null, 'message', 'chat');
  2153. // Get cached chatboxes from localstorage
  2154. this.fetch({
  2155. add: true,
  2156. success: $.proxy(function (collection, resp) {
  2157. if (_.include(_.pluck(resp, 'id'), 'controlbox')) {
  2158. // If the controlbox was saved in localstorage, it must be visible
  2159. this.get('controlbox').set({visible:true}).save();
  2160. }
  2161. }, this)
  2162. });
  2163. },
  2164. messageReceived: function (message) {
  2165. var buddy_jid, $message = $(message),
  2166. message_from = $message.attr('from');
  2167. if (message_from == converse.connection.jid) {
  2168. // FIXME: Forwarded messages should be sent to specific resources,
  2169. // not broadcasted
  2170. return true;
  2171. }
  2172. var $forwarded = $message.children('forwarded');
  2173. if ($forwarded.length) {
  2174. $message = $forwarded.children('message');
  2175. }
  2176. var from = Strophe.getBareJidFromJid(message_from),
  2177. to = Strophe.getBareJidFromJid($message.attr('to')),
  2178. resource, chatbox, roster_item;
  2179. if (from == converse.bare_jid) {
  2180. // I am the sender, so this must be a forwarded message...
  2181. buddy_jid = to;
  2182. resource = Strophe.getResourceFromJid($message.attr('to'));
  2183. } else {
  2184. buddy_jid = from;
  2185. resource = Strophe.getResourceFromJid(message_from);
  2186. }
  2187. chatbox = this.get(buddy_jid);
  2188. roster_item = converse.roster.get(buddy_jid);
  2189. if (roster_item === undefined) {
  2190. // The buddy was likely removed
  2191. converse.log('Could not get roster item for JID '+buddy_jid, 'error');
  2192. return true;
  2193. }
  2194. if (!chatbox) {
  2195. var fullname = roster_item.get('fullname');
  2196. fullname = _.isEmpty(fullname)? buddy_jid: fullname;
  2197. chatbox = this.create({
  2198. 'id': buddy_jid,
  2199. 'jid': buddy_jid,
  2200. 'fullname': fullname,
  2201. 'image_type': roster_item.get('image_type'),
  2202. 'image': roster_item.get('image'),
  2203. 'url': roster_item.get('url')
  2204. });
  2205. }
  2206. chatbox.messageReceived(message);
  2207. converse.roster.addResource(buddy_jid, resource);
  2208. converse.emit('onMessage', message);
  2209. return true;
  2210. }
  2211. });
  2212. this.ChatBoxesView = Backbone.View.extend({
  2213. el: '#conversejs',
  2214. initialize: function () {
  2215. // boxesviewinit
  2216. this.views = {};
  2217. this.model.on("add", function (item) {
  2218. var view = this.views[item.get('id')];
  2219. if (!view) {
  2220. if (item.get('chatroom')) {
  2221. view = new converse.ChatRoomView({'model': item});
  2222. } else if (item.get('box_id') === 'controlbox') {
  2223. view = new converse.ControlBoxView({model: item});
  2224. view.render();
  2225. } else {
  2226. view = new converse.ChatBoxView({model: item});
  2227. }
  2228. this.views[item.get('id')] = view;
  2229. } else {
  2230. delete view.model; // Remove ref to old model to help garbage collection
  2231. view.model = item;
  2232. view.initialize();
  2233. if (item.get('id') !== 'controlbox') {
  2234. // FIXME: Why is it necessary to again append chatboxes?
  2235. view.$el.appendTo(this.$el);
  2236. }
  2237. }
  2238. }, this);
  2239. },
  2240. showChatBox: function (attrs) {
  2241. var chatbox = this.model.get(attrs.jid);
  2242. if (chatbox) {
  2243. chatbox.trigger('show');
  2244. } else {
  2245. chatbox = this.model.create(attrs, {
  2246. 'error': function (model, response) {
  2247. converse.log(response.responseText);
  2248. }
  2249. });
  2250. }
  2251. return chatbox;
  2252. }
  2253. });
  2254. this.RosterItem = Backbone.Model.extend({
  2255. initialize: function (attributes, options) {
  2256. var jid = attributes.jid;
  2257. if (!attributes.fullname) {
  2258. attributes.fullname = jid;
  2259. }
  2260. var attrs = _.extend({
  2261. 'id': jid,
  2262. 'user_id': Strophe.getNodeFromJid(jid),
  2263. 'resources': [],
  2264. 'status': ''
  2265. }, attributes);
  2266. attrs.sorted = false;
  2267. attrs.chat_status = 'offline';
  2268. this.set(attrs);
  2269. }
  2270. });
  2271. this.RosterItemView = Backbone.View.extend({
  2272. tagName: 'dd',
  2273. events: {
  2274. "click .accept-xmpp-request": "acceptRequest",
  2275. "click .decline-xmpp-request": "declineRequest",
  2276. "click .open-chat": "openChat",
  2277. "click .remove-xmpp-contact": "removeContact"
  2278. },
  2279. openChat: function (ev) {
  2280. ev.preventDefault();
  2281. converse.chatboxesview.showChatBox({
  2282. 'id': this.model.get('jid'),
  2283. 'jid': this.model.get('jid'),
  2284. 'fullname': this.model.get('fullname'),
  2285. 'image_type': this.model.get('image_type'),
  2286. 'image': this.model.get('image'),
  2287. 'url': this.model.get('url'),
  2288. 'status': this.model.get('status')
  2289. });
  2290. },
  2291. removeContact: function (ev) {
  2292. ev.preventDefault();
  2293. var result = confirm("Are you sure you want to remove this contact?");
  2294. if (result === true) {
  2295. var bare_jid = this.model.get('jid');
  2296. converse.connection.roster.remove(bare_jid, function (iq) {
  2297. converse.connection.roster.unauthorize(bare_jid);
  2298. converse.rosterview.model.remove(bare_jid);
  2299. });
  2300. }
  2301. },
  2302. acceptRequest: function (ev) {
  2303. var jid = this.model.get('jid');
  2304. converse.connection.roster.authorize(jid);
  2305. converse.connection.roster.add(jid, this.model.get('fullname'), [], function (iq) {
  2306. converse.connection.roster.subscribe(jid, null, converse.xmppstatus.get('fullname'));
  2307. });
  2308. ev.preventDefault();
  2309. },
  2310. declineRequest: function (ev) {
  2311. ev.preventDefault();
  2312. converse.connection.roster.unauthorize(this.model.get('jid'));
  2313. this.model.destroy();
  2314. },
  2315. template: _.template(
  2316. '<a class="open-chat" title="'+__('Click to chat with this contact')+'" href="#">'+
  2317. '<span class="icon-{{ chat_status }}" title="{{ status_desc }}"></span>{{ fullname }}'+
  2318. '</a>' +
  2319. '<a class="remove-xmpp-contact icon-remove" title="'+__('Click to remove this contact')+'" href="#"></a>'),
  2320. pending_template: _.template(
  2321. '<span>{{ fullname }}</span>' +
  2322. '<a class="remove-xmpp-contact icon-remove" title="'+__('Click to remove this contact')+'" href="#"></a>'),
  2323. request_template: _.template('<div>{{ fullname }}</div>' +
  2324. '<button type="button" class="accept-xmpp-request">' +
  2325. 'Accept</button>' +
  2326. '<button type="button" class="decline-xmpp-request">' +
  2327. 'Decline</button>' +
  2328. ''),
  2329. render: function () {
  2330. var item = this.model,
  2331. ask = item.get('ask'),
  2332. requesting = item.get('requesting'),
  2333. subscription = item.get('subscription');
  2334. var classes_to_remove = [
  2335. 'current-xmpp-contact',
  2336. 'pending-xmpp-contact',
  2337. 'requesting-xmpp-contact'
  2338. ].concat(_.keys(STATUSES));
  2339. _.each(classes_to_remove,
  2340. function (cls) {
  2341. if (this.el.className.indexOf(cls) !== -1) {
  2342. this.$el.removeClass(cls);
  2343. }
  2344. }, this);
  2345. this.$el.addClass(item.get('chat_status'));
  2346. if (ask === 'subscribe') {
  2347. this.$el.addClass('pending-xmpp-contact');
  2348. this.$el.html(this.pending_template(item.toJSON()));
  2349. } else if (requesting === true) {
  2350. this.$el.addClass('requesting-xmpp-contact');
  2351. this.$el.html(this.request_template(item.toJSON()));
  2352. converse.controlboxtoggle.showControlBox();
  2353. } else if (subscription === 'both' || subscription === 'to') {
  2354. this.$el.addClass('current-xmpp-contact');
  2355. this.$el.html(this.template(
  2356. _.extend(item.toJSON(), {'status_desc': STATUSES[item.get('chat_status')||'offline']})
  2357. ));
  2358. }
  2359. return this;
  2360. }
  2361. });
  2362. this.RosterItems = Backbone.Collection.extend({
  2363. model: converse.RosterItem,
  2364. comparator : function (rosteritem) {
  2365. var chat_status = rosteritem.get('chat_status'),
  2366. rank = 4;
  2367. switch(chat_status) {
  2368. case 'offline':
  2369. rank = 0;
  2370. break;
  2371. case 'unavailable':
  2372. rank = 1;
  2373. break;
  2374. case 'xa':
  2375. rank = 2;
  2376. break;
  2377. case 'away':
  2378. rank = 3;
  2379. break;
  2380. case 'dnd':
  2381. rank = 4;
  2382. break;
  2383. case 'online':
  2384. rank = 5;
  2385. break;
  2386. }
  2387. return rank;
  2388. },
  2389. subscribeToSuggestedItems: function (msg) {
  2390. $(msg).find('item').each(function () {
  2391. var $this = $(this),
  2392. jid = $this.attr('jid'),
  2393. action = $this.attr('action'),
  2394. fullname = $this.attr('name');
  2395. if (action === 'add') {
  2396. converse.connection.roster.add(jid, fullname, [], function (iq) {
  2397. converse.connection.roster.subscribe(jid, null, converse.xmppstatus.get('fullname'));
  2398. });
  2399. }
  2400. });
  2401. return true;
  2402. },
  2403. isSelf: function (jid) {
  2404. return (Strophe.getBareJidFromJid(jid) === Strophe.getBareJidFromJid(converse.connection.jid));
  2405. },
  2406. addResource: function (bare_jid, resource) {
  2407. var item = this.get(bare_jid),
  2408. resources;
  2409. if (item) {
  2410. resources = item.get('resources');
  2411. if (resources) {
  2412. if (_.indexOf(resources, resource) == -1) {
  2413. resources.push(resource);
  2414. item.set({'resources': resources});
  2415. }
  2416. } else {
  2417. item.set({'resources': [resource]});
  2418. }
  2419. }
  2420. },
  2421. removeResource: function (bare_jid, resource) {
  2422. var item = this.get(bare_jid),
  2423. resources,
  2424. idx;
  2425. if (item) {
  2426. resources = item.get('resources');
  2427. idx = _.indexOf(resources, resource);
  2428. if (idx !== -1) {
  2429. resources.splice(idx, 1);
  2430. item.set({'resources': resources});
  2431. return resources.length;
  2432. }
  2433. }
  2434. return 0;
  2435. },
  2436. subscribeBack: function (jid) {
  2437. var bare_jid = Strophe.getBareJidFromJid(jid);
  2438. if (converse.connection.roster.findItem(bare_jid)) {
  2439. converse.connection.roster.authorize(bare_jid);
  2440. converse.connection.roster.subscribe(jid, null, converse.xmppstatus.get('fullname'));
  2441. } else {
  2442. converse.connection.roster.add(jid, '', [], function (iq) {
  2443. converse.connection.roster.authorize(bare_jid);
  2444. converse.connection.roster.subscribe(jid, null, converse.xmppstatus.get('fullname'));
  2445. });
  2446. }
  2447. },
  2448. unsubscribe: function (jid) {
  2449. /* Upon receiving the presence stanza of type "unsubscribed",
  2450. * the user SHOULD acknowledge receipt of that subscription state
  2451. * notification by sending a presence stanza of type "unsubscribe"
  2452. * this step lets the user's server know that it MUST no longer
  2453. * send notification of the subscription state change to the user.
  2454. */
  2455. converse.xmppstatus.sendPresence('unsubscribe');
  2456. if (converse.connection.roster.findItem(jid)) {
  2457. converse.connection.roster.remove(jid, function (iq) {
  2458. converse.rosterview.model.remove(jid);
  2459. });
  2460. }
  2461. },
  2462. getNumOnlineContacts: function () {
  2463. var count = 0,
  2464. models = this.models,
  2465. models_length = models.length,
  2466. i;
  2467. for (i=0; i<models_length; i++) {
  2468. if (_.indexOf(['offline', 'unavailable'], models[i].get('chat_status')) === -1) {
  2469. count++;
  2470. }
  2471. }
  2472. return count;
  2473. },
  2474. cleanCache: function (items) {
  2475. /* The localstorage cache containing roster contacts might contain
  2476. * some contacts that aren't actually in our roster anymore. We
  2477. * therefore need to remove them now.
  2478. */
  2479. var id, i,
  2480. roster_ids = [];
  2481. for (i=0; i < items.length; ++i) {
  2482. roster_ids.push(items[i].jid);
  2483. }
  2484. for (i=0; i < this.models.length; ++i) {
  2485. id = this.models[i].get('id');
  2486. if (_.indexOf(roster_ids, id) === -1) {
  2487. this.get(id).destroy();
  2488. }
  2489. }
  2490. },
  2491. rosterHandler: function (items) {
  2492. converse.emit('onRoster', items);
  2493. this.cleanCache(items);
  2494. _.each(items, function (item, index, items) {
  2495. if (this.isSelf(item.jid)) { return; }
  2496. var model = this.get(item.jid);
  2497. if (!model) {
  2498. var is_last = (index === (items.length-1)) ? true : false;
  2499. if ((item.subscription === 'none') && (item.ask === null) && !is_last) {
  2500. // We're not interested in zombies
  2501. // (Hack: except if it's the last item, then we still
  2502. // add it so that the roster will be shown).
  2503. return;
  2504. }
  2505. this.create({
  2506. jid: item.jid,
  2507. subscription: item.subscription,
  2508. ask: item.ask,
  2509. fullname: item.name || item.jid,
  2510. is_last: is_last
  2511. });
  2512. } else {
  2513. if ((item.subscription === 'none') && (item.ask === null)) {
  2514. // This user is no longer in our roster
  2515. model.destroy();
  2516. } else if (model.get('subscription') !== item.subscription || model.get('ask') !== item.ask) {
  2517. // only modify model attributes if they are different from the
  2518. // ones that were already set when the rosterItem was added
  2519. model.set({'subscription': item.subscription, 'ask': item.ask, 'requesting': null});
  2520. model.save();
  2521. }
  2522. }
  2523. }, this);
  2524. if (!converse.initial_presence_sent) {
  2525. /* Once we've sent out our initial presence stanza, we'll
  2526. * start receiving presence stanzas from our contacts.
  2527. * We therefore only want to do this after our roster has
  2528. * been set up (otherwise we can't meaningfully process
  2529. * incoming presence stanzas).
  2530. */
  2531. converse.initial_presence_sent = 1;
  2532. converse.xmppstatus.sendPresence();
  2533. }
  2534. },
  2535. handleIncomingSubscription: function (jid) {
  2536. var bare_jid = Strophe.getBareJidFromJid(jid);
  2537. var item = this.get(bare_jid);
  2538. if (!converse.allow_contact_requests) {
  2539. converse.connection.roster.unauthorize(bare_jid);
  2540. return true;
  2541. }
  2542. if (converse.auto_subscribe) {
  2543. if ((!item) || (item.get('subscription') != 'to')) {
  2544. this.subscribeBack(jid);
  2545. } else {
  2546. converse.connection.roster.authorize(bare_jid);
  2547. }
  2548. } else {
  2549. if ((item) && (item.get('subscription') != 'none')) {
  2550. converse.connection.roster.authorize(bare_jid);
  2551. } else {
  2552. if (!this.get(bare_jid)) {
  2553. converse.getVCard(
  2554. bare_jid,
  2555. $.proxy(function (jid, fullname, img, img_type, url) {
  2556. this.add({
  2557. jid: bare_jid,
  2558. subscription: 'none',
  2559. ask: null,
  2560. requesting: true,
  2561. fullname: fullname,
  2562. image: img,
  2563. image_type: img_type,
  2564. url: url,
  2565. vcard_updated: converse.toISOString(new Date()),
  2566. is_last: true
  2567. });
  2568. }, this),
  2569. $.proxy(function (jid, fullname, img, img_type, url) {
  2570. converse.log("Error while retrieving vcard");
  2571. // XXX: Should vcard_updated be set here as
  2572. // well?
  2573. this.add({
  2574. jid: bare_jid,
  2575. subscription: 'none',
  2576. ask: null,
  2577. requesting: true,
  2578. fullname: jid,
  2579. is_last: true
  2580. });
  2581. }, this)
  2582. );
  2583. } else {
  2584. return true;
  2585. }
  2586. }
  2587. }
  2588. return true;
  2589. },
  2590. presenceHandler: function (presence) {
  2591. var $presence = $(presence),
  2592. presence_type = $presence.attr('type');
  2593. if (presence_type === 'error') {
  2594. // TODO
  2595. // error presence stanzas don't necessarily have a 'from' attr.
  2596. return true;
  2597. }
  2598. var jid = $presence.attr('from'),
  2599. bare_jid = Strophe.getBareJidFromJid(jid),
  2600. resource = Strophe.getResourceFromJid(jid),
  2601. $show = $presence.find('show'),
  2602. chat_status = $show.text() || 'online',
  2603. status_message = $presence.find('status'),
  2604. item;
  2605. if (this.isSelf(bare_jid)) {
  2606. if ((converse.connection.jid !== jid)&&(presence_type !== 'unavailable')) {
  2607. // Another resource has changed it's status, we'll update ours as well.
  2608. // FIXME: We should ideally differentiate between converse.js using
  2609. // resources and other resources (i.e Pidgin etc.)
  2610. converse.xmppstatus.save({'status': chat_status});
  2611. }
  2612. return true;
  2613. } else if (($presence.find('x').attr('xmlns') || '').indexOf(Strophe.NS.MUC) === 0) {
  2614. return true; // Ignore MUC
  2615. }
  2616. item = this.get(bare_jid);
  2617. if (item && (status_message.text() != item.get('status'))) {
  2618. item.save({'status': status_message.text()});
  2619. }
  2620. if ((presence_type === 'subscribed') || (presence_type === 'unsubscribe')) {
  2621. return true;
  2622. } else if (presence_type === 'subscribe') {
  2623. return this.handleIncomingSubscription(jid);
  2624. } else if (presence_type === 'unsubscribed') {
  2625. this.unsubscribe(bare_jid);
  2626. } else if (presence_type === 'unavailable') {
  2627. if (this.removeResource(bare_jid, resource) === 0) {
  2628. if (item) {
  2629. item.set({'chat_status': 'offline'});
  2630. }
  2631. }
  2632. } else if (item) {
  2633. // presence_type is undefined
  2634. this.addResource(bare_jid, resource);
  2635. item.set({'chat_status': chat_status});
  2636. }
  2637. return true;
  2638. }
  2639. });
  2640. this.RosterView = Backbone.View.extend({
  2641. tagName: 'dl',
  2642. id: 'converse-roster',
  2643. rosteritemviews: {},
  2644. requesting_contacts_template: _.template(
  2645. '<dt id="xmpp-contact-requests">'+__('Contact requests')+'</dt>'),
  2646. contacts_template: _.template(
  2647. '<dt id="xmpp-contacts">'+__('My contacts')+'</dt>'),
  2648. pending_contacts_template: _.template(
  2649. '<dt id="pending-xmpp-contacts">'+__('Pending contacts')+'</dt>'),
  2650. initialize: function () {
  2651. this.model.on("add", function (item) {
  2652. this.addRosterItemView(item).render(item);
  2653. if (!item.get('vcard_updated')) {
  2654. // This will update the vcard, which triggers a change
  2655. // request which will rerender the roster item.
  2656. converse.getVCard(item.get('jid'));
  2657. }
  2658. }, this);
  2659. this.model.on('change', function (item) {
  2660. if ((_.size(item.changed) === 1) && _.contains(_.keys(item.changed), 'sorted')) {
  2661. return;
  2662. }
  2663. this.updateChatBox(item).render(item);
  2664. }, this);
  2665. this.model.on("remove", function (item) { this.removeRosterItemView(item); }, this);
  2666. this.model.on("destroy", function (item) { this.removeRosterItemView(item); }, this);
  2667. var roster_markup = this.contacts_template();
  2668. if (converse.allow_contact_requests) {
  2669. roster_markup = this.requesting_contacts_template() + roster_markup + this.pending_contacts_template();
  2670. }
  2671. this.$el.hide().html(roster_markup);
  2672. this.model.fetch({add: true}); // Get the cached roster items from localstorage
  2673. },
  2674. updateChatBox: function (item, changed) {
  2675. var chatbox = converse.chatboxes.get(item.get('jid')),
  2676. changes = {};
  2677. if (!chatbox) {
  2678. return this;
  2679. }
  2680. if (_.has(item.changed, 'chat_status')) {
  2681. changes.chat_status = item.get('chat_status');
  2682. }
  2683. if (_.has(item.changed, 'status')) {
  2684. changes.status = item.get('status');
  2685. }
  2686. chatbox.save(changes);
  2687. return this;
  2688. },
  2689. addRosterItemView: function (item) {
  2690. var view = new converse.RosterItemView({model: item});
  2691. this.rosteritemviews[item.id] = view;
  2692. return this;
  2693. },
  2694. removeRosterItemView: function (item) {
  2695. var view = this.rosteritemviews[item.id];
  2696. if (view) {
  2697. view.$el.remove();
  2698. delete this.rosteritemviews[item.id];
  2699. this.render();
  2700. }
  2701. return this;
  2702. },
  2703. renderRosterItem: function (item, view) {
  2704. if ((converse.show_only_online_users) && (item.get('chat_status') !== 'online')) {
  2705. view.$el.remove();
  2706. view.delegateEvents();
  2707. return this;
  2708. }
  2709. if ($.contains(document.documentElement, view.el)) {
  2710. view.render();
  2711. } else {
  2712. this.$el.find('#xmpp-contacts').after(view.render().el);
  2713. }
  2714. },
  2715. render: function (item) {
  2716. var $my_contacts = this.$el.find('#xmpp-contacts'),
  2717. $contact_requests = this.$el.find('#xmpp-contact-requests'),
  2718. $pending_contacts = this.$el.find('#pending-xmpp-contacts'),
  2719. sorted = false,
  2720. $count, changed_presence;
  2721. if (item) {
  2722. var jid = item.id,
  2723. view = this.rosteritemviews[item.id],
  2724. ask = item.get('ask'),
  2725. subscription = item.get('subscription'),
  2726. requesting = item.get('requesting'),
  2727. crit = {order:'asc'};
  2728. if ((ask === 'subscribe') && (subscription == 'none')) {
  2729. $pending_contacts.after(view.render().el);
  2730. $pending_contacts.after($pending_contacts.siblings('dd.pending-xmpp-contact').tsort(crit));
  2731. } else if ((ask === 'subscribe') && (subscription == 'from')) {
  2732. // TODO: We have accepted an incoming subscription
  2733. // request and (automatically) made our own subscription request back.
  2734. // It would be useful to update the roster here to show
  2735. // things are happening... (see docs/DEVELOPER.rst)
  2736. $pending_contacts.after(view.render().el);
  2737. $pending_contacts.after($pending_contacts.siblings('dd.pending-xmpp-contact').tsort(crit));
  2738. } else if (requesting === true) {
  2739. $contact_requests.after(view.render().el);
  2740. $contact_requests.after($contact_requests.siblings('dd.requesting-xmpp-contact').tsort(crit));
  2741. } else if (subscription === 'both' || subscription === 'to') {
  2742. this.renderRosterItem(item, view);
  2743. }
  2744. changed_presence = item.changed.chat_status;
  2745. if (changed_presence) {
  2746. this.sortRoster(changed_presence);
  2747. sorted = true;
  2748. }
  2749. if (item.get('is_last')) {
  2750. if (!sorted) {
  2751. this.sortRoster(item.get('chat_status'));
  2752. }
  2753. if (!this.$el.is(':visible')) {
  2754. // Once all initial roster items have been added, we
  2755. // can show the roster.
  2756. this.$el.show();
  2757. }
  2758. }
  2759. }
  2760. // Hide the headings if there are no contacts under them
  2761. _.each([$my_contacts, $contact_requests, $pending_contacts], function (h) {
  2762. if (h.nextUntil('dt').length) {
  2763. if (!h.is(':visible')) {
  2764. h.show();
  2765. }
  2766. }
  2767. else if (h.is(':visible')) {
  2768. h.hide();
  2769. }
  2770. });
  2771. $count = $('#online-count');
  2772. $count.text('('+this.model.getNumOnlineContacts()+')');
  2773. if (!$count.is(':visible')) {
  2774. $count.show();
  2775. }
  2776. return this;
  2777. },
  2778. sortRoster: function (chat_status) {
  2779. var $my_contacts = this.$el.find('#xmpp-contacts');
  2780. $my_contacts.siblings('dd.current-xmpp-contact.'+chat_status).tsort('a', {order:'asc'});
  2781. $my_contacts.after($my_contacts.siblings('dd.current-xmpp-contact.offline'));
  2782. $my_contacts.after($my_contacts.siblings('dd.current-xmpp-contact.unavailable'));
  2783. $my_contacts.after($my_contacts.siblings('dd.current-xmpp-contact.xa'));
  2784. $my_contacts.after($my_contacts.siblings('dd.current-xmpp-contact.away'));
  2785. $my_contacts.after($my_contacts.siblings('dd.current-xmpp-contact.dnd'));
  2786. $my_contacts.after($my_contacts.siblings('dd.current-xmpp-contact.online'));
  2787. }
  2788. });
  2789. this.XMPPStatus = Backbone.Model.extend({
  2790. initialize: function () {
  2791. this.set({
  2792. 'status' : this.get('status') || 'online'
  2793. });
  2794. this.on('change', $.proxy(function (item) {
  2795. if (this.get('fullname') === undefined) {
  2796. converse.getVCard(
  2797. null, // No 'to' attr when getting one's own vCard
  2798. $.proxy(function (jid, fullname, image, image_type, url) {
  2799. this.save({'fullname': fullname});
  2800. }, this)
  2801. );
  2802. }
  2803. if (_.has(item.changed, 'status')) {
  2804. converse.emit('onStatusChanged', this.get('status'));
  2805. }
  2806. if (_.has(item.changed, 'status_message')) {
  2807. converse.emit('onStatusMessageChanged', this.get('status_message'));
  2808. }
  2809. }, this));
  2810. },
  2811. sendPresence: function (type) {
  2812. if (type === undefined) {
  2813. type = this.get('status') || 'online';
  2814. }
  2815. var status_message = this.get('status_message'),
  2816. presence;
  2817. // Most of these presence types are actually not explicitly sent,
  2818. // but I add all of them here fore reference and future proofing.
  2819. if ((type === 'unavailable') ||
  2820. (type === 'probe') ||
  2821. (type === 'error') ||
  2822. (type === 'unsubscribe') ||
  2823. (type === 'unsubscribed') ||
  2824. (type === 'subscribe') ||
  2825. (type === 'subscribed')) {
  2826. presence = $pres({'type':type});
  2827. } else {
  2828. if (type === 'online') {
  2829. presence = $pres();
  2830. } else {
  2831. presence = $pres().c('show').t(type).up();
  2832. }
  2833. if (status_message) {
  2834. presence.c('status').t(status_message);
  2835. }
  2836. }
  2837. converse.connection.send(presence);
  2838. },
  2839. setStatus: function (value) {
  2840. this.sendPresence(value);
  2841. this.save({'status': value});
  2842. },
  2843. setStatusMessage: function (status_message) {
  2844. converse.connection.send($pres().c('show').t(this.get('status')).up().c('status').t(status_message));
  2845. this.save({'status_message': status_message});
  2846. if (this.xhr_custom_status) {
  2847. $.ajax({
  2848. url: this.xhr_custom_status_url,
  2849. type: 'POST',
  2850. data: {'msg': status_message}
  2851. });
  2852. }
  2853. }
  2854. });
  2855. this.XMPPStatusView = Backbone.View.extend({
  2856. el: "span#xmpp-status-holder",
  2857. events: {
  2858. "click a.choose-xmpp-status": "toggleOptions",
  2859. "click #fancy-xmpp-status-select a.change-xmpp-status-message": "renderStatusChangeForm",
  2860. "submit #set-custom-xmpp-status": "setStatusMessage",
  2861. "click .dropdown dd ul li a": "setStatus"
  2862. },
  2863. toggleOptions: function (ev) {
  2864. ev.preventDefault();
  2865. $(ev.target).parent().parent().siblings('dd').find('ul').toggle('fast');
  2866. },
  2867. change_status_message_template: _.template(
  2868. '<form id="set-custom-xmpp-status">' +
  2869. '<input type="text" class="custom-xmpp-status" {{ status_message }}"'+
  2870. 'placeholder="'+__('Custom status')+'"/>' +
  2871. '<button type="submit">'+__('Save')+'</button>' +
  2872. '</form>'),
  2873. status_template: _.template(
  2874. '<div class="xmpp-status">' +
  2875. '<a class="choose-xmpp-status {{ chat_status }}" data-value="{{status_message}}" href="#" title="'+__('Click to change your chat status')+'">' +
  2876. '<span class="icon-{{ chat_status }}"></span>'+
  2877. '{{ status_message }}' +
  2878. '</a>' +
  2879. '<a class="change-xmpp-status-message icon-pencil" href="#" title="'+__('Click here to write a custom status message')+'"></a>' +
  2880. '</div>'),
  2881. renderStatusChangeForm: function (ev) {
  2882. ev.preventDefault();
  2883. var status_message = this.model.get('status') || 'offline';
  2884. var input = this.change_status_message_template({'status_message': status_message});
  2885. this.$el.find('.xmpp-status').replaceWith(input);
  2886. this.$el.find('.custom-xmpp-status').focus().focus();
  2887. },
  2888. setStatusMessage: function (ev) {
  2889. ev.preventDefault();
  2890. var status_message = $(ev.target).find('input').val();
  2891. if (status_message === "") {
  2892. }
  2893. this.model.setStatusMessage(status_message);
  2894. },
  2895. setStatus: function (ev) {
  2896. ev.preventDefault();
  2897. var $el = $(ev.target),
  2898. value = $el.attr('data-value');
  2899. this.model.setStatus(value);
  2900. this.$el.find(".dropdown dd ul").hide();
  2901. },
  2902. getPrettyStatus: function (stat) {
  2903. if (stat === 'chat') {
  2904. pretty_status = __('online');
  2905. } else if (stat === 'dnd') {
  2906. pretty_status = __('busy');
  2907. } else if (stat === 'xa') {
  2908. pretty_status = __('away for long');
  2909. } else if (stat === 'away') {
  2910. pretty_status = __('away');
  2911. } else {
  2912. pretty_status = __(stat) || __('online'); // XXX: Is 'online' the right default choice here?
  2913. }
  2914. return pretty_status;
  2915. },
  2916. updateStatusUI: function (model) {
  2917. if (!(_.has(model.changed, 'status')) && !(_.has(model.changed, 'status_message'))) {
  2918. return;
  2919. }
  2920. var stat = model.get('status');
  2921. // # For translators: the %1$s part gets replaced with the status
  2922. // # Example, I am online
  2923. var status_message = model.get('status_message') || __("I am %1$s", this.getPrettyStatus(stat));
  2924. this.$el.find('#fancy-xmpp-status-select').html(
  2925. this.status_template({
  2926. 'chat_status': stat,
  2927. 'status_message': status_message
  2928. }));
  2929. },
  2930. choose_template: _.template(
  2931. '<dl id="target" class="dropdown">' +
  2932. '<dt id="fancy-xmpp-status-select" class="fancy-dropdown"></dt>' +
  2933. '<dd><ul class="xmpp-status-menu"></ul></dd>' +
  2934. '</dl>'),
  2935. option_template: _.template(
  2936. '<li>' +
  2937. '<a href="#" class="{{ value }}" data-value="{{ value }}">'+
  2938. '<span class="icon-{{ value }}"></span>'+
  2939. '{{ text }}'+
  2940. '</a>' +
  2941. '</li>'),
  2942. initialize: function () {
  2943. this.model.on("change", this.updateStatusUI, this);
  2944. },
  2945. render: function () {
  2946. // Replace the default dropdown with something nicer
  2947. var $select = this.$el.find('select#select-xmpp-status'),
  2948. chat_status = this.model.get('status') || 'offline',
  2949. options = $('option', $select),
  2950. $options_target,
  2951. options_list = [],
  2952. that = this;
  2953. this.$el.html(this.choose_template());
  2954. this.$el.find('#fancy-xmpp-status-select')
  2955. .html(this.status_template({
  2956. 'status_message': this.model.get('status_message') || __("I am %1$s", this.getPrettyStatus(chat_status)),
  2957. 'chat_status': chat_status
  2958. }));
  2959. // iterate through all the <option> elements and add option values
  2960. options.each(function(){
  2961. options_list.push(that.option_template({'value': $(this).val(),
  2962. 'text': this.text
  2963. }));
  2964. });
  2965. $options_target = this.$el.find("#target dd ul").hide();
  2966. $options_target.append(options_list.join(''));
  2967. $select.remove();
  2968. return this;
  2969. }
  2970. });
  2971. this.Feature = Backbone.Model.extend();
  2972. this.Features = Backbone.Collection.extend({
  2973. /* Service Discovery
  2974. * -----------------
  2975. * This collection stores Feature Models, representing features
  2976. * provided by available XMPP entities (e.g. servers)
  2977. * See XEP-0030 for more details: http://xmpp.org/extensions/xep-0030.html
  2978. * All features are shown here: http://xmpp.org/registrar/disco-features.html
  2979. */
  2980. model: converse.Feature,
  2981. initialize: function () {
  2982. this.localStorage = new Backbone.LocalStorage(
  2983. hex_sha1('converse.features'+converse.bare_jid));
  2984. if (this.localStorage.records.length === 0) {
  2985. // localStorage is empty, so we've likely never queried this
  2986. // domain for features yet
  2987. converse.connection.disco.info(converse.domain, null, $.proxy(this.onInfo, this));
  2988. converse.connection.disco.items(converse.domain, null, $.proxy(this.onItems, this));
  2989. } else {
  2990. this.fetch({add:true});
  2991. }
  2992. },
  2993. onItems: function (stanza) {
  2994. $(stanza).find('query item').each($.proxy(function (idx, item) {
  2995. converse.connection.disco.info(
  2996. $(item).attr('jid'),
  2997. null,
  2998. $.proxy(this.onInfo, this));
  2999. }, this));
  3000. },
  3001. onInfo: function (stanza) {
  3002. var $stanza = $(stanza);
  3003. if (($stanza.find('identity[category=server][type=im]').length === 0) &&
  3004. ($stanza.find('identity[category=conference][type=text]').length === 0)) {
  3005. // This isn't an IM server component
  3006. return;
  3007. }
  3008. $stanza.find('feature').each($.proxy(function (idx, feature) {
  3009. this.create({
  3010. 'var': $(feature).attr('var'),
  3011. 'from': $stanza.attr('from')
  3012. });
  3013. }, this));
  3014. }
  3015. });
  3016. this.LoginPanel = Backbone.View.extend({
  3017. tagName: 'div',
  3018. id: "login-dialog",
  3019. events: {
  3020. 'submit form#converse-login': 'authenticate'
  3021. },
  3022. tab_template: _.template(
  3023. '<li><a class="current" href="#login">'+__('Sign in')+'</a></li>'
  3024. ),
  3025. template: _.template(
  3026. '<form id="converse-login">' +
  3027. '<label>'+__('XMPP/Jabber Username:')+'</label>' +
  3028. '<input type="username" name="jid">' +
  3029. '<label>'+__('Password:')+'</label>' +
  3030. '<input type="password" name="password">' +
  3031. '<input class="login-submit" type="submit" value="'+__('Log In')+'">' +
  3032. '</form">'
  3033. ),
  3034. bosh_url_input: _.template(
  3035. '<label>'+__('BOSH Service URL:')+'</label>' +
  3036. '<input type="text" id="bosh_service_url">'
  3037. ),
  3038. connect: function ($form, jid, password) {
  3039. if ($form) {
  3040. $form.find('input[type=submit]').hide().after('<span class="spinner login-submit"/>');
  3041. }
  3042. converse.connection = new Strophe.Connection(converse.bosh_service_url);
  3043. converse.connection.connect(jid, password, converse.onConnect);
  3044. },
  3045. showConnectButton: function () {
  3046. var $form = this.$el.find('#converse-login');
  3047. var $button = $form.find('input[type=submit]');
  3048. if ($button.length) {
  3049. $button.show().siblings('span').remove();
  3050. }
  3051. },
  3052. initialize: function (cfg) {
  3053. cfg.$parent.html(this.$el.html(this.template()));
  3054. this.$tabs = cfg.$parent.parent().find('#controlbox-tabs');
  3055. this.model.on('connection-fail', function () { this.showConnectButton(); }, this);
  3056. this.model.on('auth-fail', function () { this.showConnectButton(); }, this);
  3057. },
  3058. render: function () {
  3059. this.$tabs.append(this.tab_template());
  3060. this.$el.find('input#jid').focus();
  3061. return this;
  3062. },
  3063. authenticate: function (ev) {
  3064. var $form = $(ev.target),
  3065. $jid_input = $form.find('input[name=jid]'),
  3066. jid = $jid_input.val(),
  3067. $pw_input = $form.find('input[name=password]'),
  3068. password = $pw_input.val(),
  3069. $bsu_input = null,
  3070. errors = false;
  3071. if (! converse.bosh_service_url) {
  3072. $bsu_input = $form.find('input#bosh_service_url');
  3073. converse.bosh_service_url = $bsu_input.val();
  3074. if (! converse.bosh_service_url) {
  3075. errors = true;
  3076. $bsu_input.addClass('error');
  3077. }
  3078. }
  3079. if (! jid) {
  3080. errors = true;
  3081. $jid_input.addClass('error');
  3082. }
  3083. if (! password) {
  3084. errors = true;
  3085. $pw_input.addClass('error');
  3086. }
  3087. if (errors) { return; }
  3088. this.connect($form, jid, password);
  3089. return false;
  3090. },
  3091. remove: function () {
  3092. this.$tabs.empty();
  3093. this.$el.parent().empty();
  3094. }
  3095. });
  3096. this.ControlBoxToggle = Backbone.View.extend({
  3097. tagName: 'a',
  3098. className: 'toggle-online-users',
  3099. id: 'toggle-controlbox',
  3100. events: {
  3101. 'click': 'onClick'
  3102. },
  3103. attributes: {
  3104. 'href': "#"
  3105. },
  3106. template: _.template(
  3107. '<strong class="conn-feedback">Toggle chat</strong>'+
  3108. '<strong style="display: none" id="online-count">(0)</strong>'
  3109. ),
  3110. initialize: function () {
  3111. this.render();
  3112. },
  3113. render: function () {
  3114. $('#conversejs').append(this.$el.html(this.template()));
  3115. return this;
  3116. },
  3117. showControlBox: function () {
  3118. var controlbox = converse.chatboxes.get('controlbox');
  3119. if (!controlbox) {
  3120. converse.chatboxes.add({
  3121. id: 'controlbox',
  3122. box_id: 'controlbox',
  3123. visible: true
  3124. });
  3125. if (converse.connection) {
  3126. converse.chatboxes.get('controlbox').save();
  3127. }
  3128. } else {
  3129. controlbox.trigger('show');
  3130. }
  3131. },
  3132. onClick: function (e) {
  3133. e.preventDefault();
  3134. if ($("div#controlbox").is(':visible')) {
  3135. var controlbox = converse.chatboxes.get('controlbox');
  3136. if (converse.connection) {
  3137. controlbox.destroy();
  3138. } else {
  3139. controlbox.trigger('hide');
  3140. }
  3141. } else {
  3142. this.showControlBox();
  3143. }
  3144. }
  3145. });
  3146. // Initialization
  3147. // --------------
  3148. // This is the end of the initialize method.
  3149. this.chatboxes = new this.ChatBoxes();
  3150. this.chatboxesview = new this.ChatBoxesView({model: this.chatboxes});
  3151. this.controlboxtoggle = new this.ControlBoxToggle();
  3152. if ((this.prebind) && (!this.connection)) {
  3153. if ((!this.jid) || (!this.sid) || (!this.rid) || (!this.bosh_service_url)) {
  3154. this.log('If you set prebind=true, you MUST supply JID, RID and SID values');
  3155. return;
  3156. }
  3157. this.connection = new Strophe.Connection(this.bosh_service_url);
  3158. this.connection.attach(this.jid, this.sid, this.rid, this.onConnect);
  3159. } else if (this.connection) {
  3160. this.onConnected();
  3161. }
  3162. if (this.show_controlbox_by_default) { this.controlboxtoggle.showControlBox(); }
  3163. converse.emit('onInitialized');
  3164. };
  3165. return {
  3166. 'initialize': function (settings, callback) {
  3167. converse.initialize(settings, callback);
  3168. },
  3169. 'once': function(evt, handler) {
  3170. converse.once(evt, handler);
  3171. },
  3172. 'on': function(evt, handler) {
  3173. converse.on(evt, handler);
  3174. },
  3175. 'off': function(evt, handler) {
  3176. converse.off(evt, handler);
  3177. }
  3178. };
  3179. }));