converse.js 172 KB

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