converse.js 234 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608460946104611461246134614461546164617461846194620462146224623462446254626462746284629463046314632463346344635463646374638463946404641464246434644464546464647464846494650465146524653465446554656465746584659466046614662466346644665466646674668466946704671467246734674467546764677467846794680468146824683468446854686468746884689469046914692469346944695469646974698469947004701470247034704470547064707470847094710471147124713471447154716471747184719472047214722472347244725472647274728472947304731473247334734473547364737473847394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767476847694770477147724773477447754776477747784779478047814782478347844785478647874788478947904791479247934794479547964797479847994800480148024803480448054806480748084809481048114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845484648474848484948504851485248534854485548564857485848594860486148624863486448654866486748684869487048714872487348744875487648774878487948804881488248834884488548864887488848894890489148924893489448954896489748984899490049014902490349044905490649074908490949104911491249134914491549164917491849194920492149224923492449254926492749284929493049314932493349344935493649374938493949404941494249434944494549464947494849494950495149524953495449554956495749584959496049614962496349644965496649674968496949704971497249734974497549764977497849794980498149824983498449854986498749884989499049914992499349944995499649974998499950005001500250035004500550065007500850095010501150125013501450155016501750185019502050215022502350245025502650275028502950305031503250335034503550365037503850395040504150425043504450455046504750485049505050515052505350545055505650575058505950605061506250635064506550665067506850695070507150725073507450755076507750785079508050815082508350845085508650875088508950905091509250935094509550965097509850995100510151025103510451055106510751085109511051115112511351145115511651175118511951205121512251235124512551265127512851295130513151325133513451355136513751385139514051415142514351445145514651475148514951505151515251535154515551565157515851595160516151625163516451655166516751685169517051715172517351745175517651775178517951805181518251835184518551865187518851895190519151925193519451955196519751985199520052015202520352045205520652075208520952105211521252135214521552165217521852195220522152225223522452255226522752285229523052315232523352345235523652375238523952405241524252435244524552465247524852495250525152525253525452555256525752585259526052615262526352645265526652675268526952705271527252735274527552765277527852795280528152825283528452855286528752885289
  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. if (typeof otr !== "undefined") {
  16. return factory(
  17. dependencies.jQuery,
  18. _,
  19. otr.OTR,
  20. otr.DSA,
  21. templates,
  22. dependencies.moment,
  23. dependencies.utils
  24. );
  25. } else {
  26. return factory(
  27. dependencies.jQuery,
  28. _,
  29. undefined,
  30. undefined,
  31. templates,
  32. dependencies.moment,
  33. dependencies.utils
  34. );
  35. }
  36. }
  37. );
  38. } else {
  39. root.converse = factory(jQuery, _, OTR, DSA, JST, moment, utils);
  40. }
  41. }(this, function ($, _, OTR, DSA, templates, moment, utils) {
  42. // "use strict";
  43. // Cannot use this due to Safari bug.
  44. // See https://github.com/jcbrand/converse.js/issues/196
  45. if (typeof console === "undefined" || typeof console.log === "undefined") {
  46. console = { log: function () {}, error: function () {} };
  47. }
  48. // Configuration of underscore templates (this config is distict to the
  49. // config of requirejs-tpl in main.js). This one is for normal inline
  50. // templates.
  51. // Use Mustache style syntax for variable interpolation
  52. _.templateSettings = {
  53. evaluate : /\{\[([\s\S]+?)\]\}/g,
  54. interpolate : /\{\{([\s\S]+?)\}\}/g
  55. };
  56. var contains = function (attr, query) {
  57. return function (item) {
  58. if (typeof attr === 'object') {
  59. var value = false;
  60. _.each(attr, function (a) {
  61. value = value || item.get(a).toLowerCase().indexOf(query.toLowerCase()) !== -1;
  62. });
  63. return value;
  64. } else if (typeof attr === 'string') {
  65. return item.get(attr).toLowerCase().indexOf(query.toLowerCase()) !== -1;
  66. } else {
  67. throw new Error('Wrong attribute type. Must be string or array.');
  68. }
  69. };
  70. };
  71. contains.not = function (attr, query) {
  72. return function (item) {
  73. return !(contains(attr, query)(item));
  74. };
  75. };
  76. // XXX: these can perhaps be moved to src/polyfills.js
  77. String.prototype.splitOnce = function (delimiter) {
  78. var components = this.split(delimiter);
  79. return [components.shift(), components.join(delimiter)];
  80. };
  81. $.fn.addEmoticons = function () {
  82. if (converse.visible_toolbar_buttons.emoticons) {
  83. if (this.length > 0) {
  84. this.each(function (i, obj) {
  85. var text = $(obj).html();
  86. text = text.replace(/&gt;:\)/g, '<span class="emoticon icon-evil"></span>');
  87. text = text.replace(/:\)/g, '<span class="emoticon icon-smiley"></span>');
  88. text = text.replace(/:\-\)/g, '<span class="emoticon icon-smiley"></span>');
  89. text = text.replace(/;\)/g, '<span class="emoticon icon-wink"></span>');
  90. text = text.replace(/;\-\)/g, '<span class="emoticon icon-wink"></span>');
  91. text = text.replace(/:D/g, '<span class="emoticon icon-grin"></span>');
  92. text = text.replace(/:\-D/g, '<span class="emoticon icon-grin"></span>');
  93. text = text.replace(/:P/g, '<span class="emoticon icon-tongue"></span>');
  94. text = text.replace(/:\-P/g, '<span class="emoticon icon-tongue"></span>');
  95. text = text.replace(/:p/g, '<span class="emoticon icon-tongue"></span>');
  96. text = text.replace(/:\-p/g, '<span class="emoticon icon-tongue"></span>');
  97. text = text.replace(/8\)/g, '<span class="emoticon icon-cool"></span>');
  98. text = text.replace(/:S/g, '<span class="emoticon icon-confused"></span>');
  99. text = text.replace(/:\\/g, '<span class="emoticon icon-wondering"></span>');
  100. text = text.replace(/:\/ /g, '<span class="emoticon icon-wondering"></span>');
  101. text = text.replace(/&gt;:\(/g, '<span class="emoticon icon-angry"></span>');
  102. text = text.replace(/:\(/g, '<span class="emoticon icon-sad"></span>');
  103. text = text.replace(/:\-\(/g, '<span class="emoticon icon-sad"></span>');
  104. text = text.replace(/:O/g, '<span class="emoticon icon-shocked"></span>');
  105. text = text.replace(/:\-O/g, '<span class="emoticon icon-shocked"></span>');
  106. text = text.replace(/\=\-O/g, '<span class="emoticon icon-shocked"></span>');
  107. text = text.replace(/\(\^.\^\)b/g, '<span class="emoticon icon-thumbs-up"></span>');
  108. text = text.replace(/&lt;3/g, '<span class="emoticon icon-heart"></span>');
  109. $(obj).html(text);
  110. });
  111. }
  112. }
  113. return this;
  114. };
  115. var playNotification = function () {
  116. var audio;
  117. if (converse.play_sounds && typeof Audio !== "undefined"){
  118. audio = new Audio("sounds/msg_received.ogg");
  119. if (audio.canPlayType('/audio/ogg')) {
  120. audio.play();
  121. } else {
  122. audio = new Audio("/sounds/msg_received.mp3");
  123. audio.play();
  124. }
  125. }
  126. };
  127. var converse = {
  128. plugins: {},
  129. templates: templates,
  130. emit: function (evt, data) {
  131. $(this).trigger(evt, data);
  132. },
  133. once: function (evt, handler) {
  134. $(this).one(evt, handler);
  135. },
  136. on: function (evt, handler) {
  137. $(this).bind(evt, handler);
  138. },
  139. off: function (evt, handler) {
  140. $(this).unbind(evt, handler);
  141. },
  142. refreshWebkit: function () {
  143. /* This works around a webkit bug. Refresh the browser's viewport,
  144. * otherwise chatboxes are not moved along when one is closed.
  145. */
  146. if ($.browser.webkit) {
  147. var conversejs = document.getElementById('conversejs');
  148. conversejs.style.display = 'none';
  149. conversejs.offsetHeight = conversejs.offsetHeight;
  150. conversejs.style.display = 'block';
  151. }
  152. }
  153. };
  154. converse.initialize = function (settings, callback) {
  155. var converse = this;
  156. // Logging
  157. Strophe.log = function (level, msg) { console.log(level+' '+msg); };
  158. Strophe.error = function (msg) {
  159. console.log('ERROR: '+msg);
  160. };
  161. // Add Strophe Namespaces
  162. Strophe.addNamespace('REGISTER', 'jabber:iq:register');
  163. Strophe.addNamespace('XFORM', 'jabber:x:data');
  164. // Add Strophe Statuses
  165. var i = 0;
  166. Object.keys(Strophe.Status).forEach(function (key) {
  167. i = Math.max(i, Strophe.Status[key]);
  168. });
  169. Strophe.Status.REGIFAIL = i + 1;
  170. Strophe.Status.REGISTERED = i + 2;
  171. Strophe.Status.CONFLICT = i + 3;
  172. Strophe.Status.NOTACCEPTABLE = i + 5;
  173. // Constants
  174. // ---------
  175. var UNENCRYPTED = 0;
  176. var UNVERIFIED= 1;
  177. var VERIFIED= 2;
  178. var FINISHED = 3;
  179. var KEY = {
  180. ENTER: 13
  181. };
  182. var STATUS_WEIGHTS = {
  183. 'offline': 6,
  184. 'unavailable': 5,
  185. 'xa': 4,
  186. 'away': 3,
  187. 'dnd': 2,
  188. 'online': 1
  189. };
  190. var INACTIVE = 'inactive';
  191. var ACTIVE = 'active';
  192. var COMPOSING = 'composing';
  193. var PAUSED = 'paused';
  194. var GONE = 'gone';
  195. var HAS_CSPRNG = ((typeof crypto !== 'undefined') &&
  196. ((typeof crypto.randomBytes === 'function') ||
  197. (typeof crypto.getRandomValues === 'function')
  198. ));
  199. var HAS_CRYPTO = HAS_CSPRNG && (
  200. (typeof CryptoJS !== "undefined") &&
  201. (typeof OTR !== "undefined") &&
  202. (typeof DSA !== "undefined")
  203. );
  204. var OPENED = 'opened';
  205. var CLOSED = 'closed';
  206. // Default configuration values
  207. // ----------------------------
  208. var default_settings = {
  209. allow_contact_requests: true,
  210. allow_dragresize: true,
  211. allow_logout: true,
  212. allow_muc: true,
  213. allow_otr: true,
  214. allow_registration: true,
  215. animate: true,
  216. auto_list_rooms: false,
  217. auto_reconnect: false,
  218. auto_subscribe: false,
  219. bosh_service_url: undefined, // The BOSH connection manager URL.
  220. cache_otr_key: false,
  221. debug: false,
  222. domain_placeholder: " e.g. conversejs.org", // Placeholder text shown in the domain input on the registration form
  223. default_box_height: 400, // The default height, in pixels, for the control box, chat boxes and chatrooms.
  224. expose_rid_and_sid: false,
  225. forward_messages: false,
  226. hide_muc_server: false,
  227. hide_offline_users: false,
  228. i18n: locales.en,
  229. jid: undefined,
  230. keepalive: false,
  231. message_carbons: false,
  232. no_trimming: false, // Set to true for phantomjs tests (where browser apparently has no width)
  233. play_sounds: false,
  234. prebind: false,
  235. providers_link: 'https://xmpp.net/directory.php', // Link to XMPP providers shown on registration page
  236. rid: undefined,
  237. roster_groups: false,
  238. show_controlbox_by_default: false,
  239. show_only_online_users: false,
  240. show_toolbar: true,
  241. sid: undefined,
  242. storage: 'session',
  243. use_otr_by_default: false,
  244. use_vcards: true,
  245. visible_toolbar_buttons: {
  246. 'emoticons': true,
  247. 'call': false,
  248. 'clear': true,
  249. 'toggle_participants': true
  250. },
  251. xhr_custom_status: false,
  252. xhr_custom_status_url: '',
  253. xhr_user_search: false,
  254. xhr_user_search_url: ''
  255. };
  256. _.extend(this, default_settings);
  257. // Allow only whitelisted configuration attributes to be overwritten
  258. _.extend(this, _.pick(settings, Object.keys(default_settings)));
  259. if (settings.visible_toolbar_buttons) {
  260. _.extend(
  261. this.visible_toolbar_buttons,
  262. _.pick(settings.visible_toolbar_buttons, [
  263. 'emoticons', 'call', 'clear', 'toggle_participants'
  264. ]
  265. ));
  266. }
  267. $.fx.off = !this.animate;
  268. // Only allow OTR if we have the capability
  269. this.allow_otr = this.allow_otr && HAS_CRYPTO;
  270. // Only use OTR by default if allow OTR is enabled to begin with
  271. this.use_otr_by_default = this.use_otr_by_default && this.allow_otr;
  272. // Translation machinery
  273. // ---------------------
  274. var __ = $.proxy(utils.__, this);
  275. var ___ = utils.___;
  276. // Translation aware constants
  277. // ---------------------------
  278. var OTR_CLASS_MAPPING = {};
  279. OTR_CLASS_MAPPING[UNENCRYPTED] = 'unencrypted';
  280. OTR_CLASS_MAPPING[UNVERIFIED] = 'unverified';
  281. OTR_CLASS_MAPPING[VERIFIED] = 'verified';
  282. OTR_CLASS_MAPPING[FINISHED] = 'finished';
  283. var OTR_TRANSLATED_MAPPING = {};
  284. OTR_TRANSLATED_MAPPING[UNENCRYPTED] = __('unencrypted');
  285. OTR_TRANSLATED_MAPPING[UNVERIFIED] = __('unverified');
  286. OTR_TRANSLATED_MAPPING[VERIFIED] = __('verified');
  287. OTR_TRANSLATED_MAPPING[FINISHED] = __('finished');
  288. var STATUSES = {
  289. 'dnd': __('This contact is busy'),
  290. 'online': __('This contact is online'),
  291. 'offline': __('This contact is offline'),
  292. 'unavailable': __('This contact is unavailable'),
  293. 'xa': __('This contact is away for an extended period'),
  294. 'away': __('This contact is away')
  295. };
  296. var DESC_GROUP_TOGGLE = __('Click to hide these contacts');
  297. var HEADER_CURRENT_CONTACTS = __('My contacts');
  298. var HEADER_PENDING_CONTACTS = __('Pending contacts');
  299. var HEADER_REQUESTING_CONTACTS = __('Contact requests');
  300. var HEADER_UNGROUPED = __('Ungrouped');
  301. var LABEL_CONTACTS = __('Contacts');
  302. var LABEL_GROUPS = __('Groups');
  303. var HEADER_WEIGHTS = {};
  304. HEADER_WEIGHTS[HEADER_CURRENT_CONTACTS] = 0;
  305. HEADER_WEIGHTS[HEADER_UNGROUPED] = 1;
  306. HEADER_WEIGHTS[HEADER_REQUESTING_CONTACTS] = 2;
  307. HEADER_WEIGHTS[HEADER_PENDING_CONTACTS] = 3;
  308. // Module-level variables
  309. // ----------------------
  310. this.callback = callback || function () {};
  311. this.initial_presence_sent = 0;
  312. this.msg_counter = 0;
  313. // Module-level functions
  314. // ----------------------
  315. this.giveFeedback = function (message, klass) {
  316. $('.conn-feedback').each(function (idx, el) {
  317. var $el = $(el);
  318. $el.addClass('conn-feedback').text(message);
  319. if (klass) {
  320. $el.addClass(klass);
  321. } else {
  322. $el.removeClass('error');
  323. }
  324. });
  325. };
  326. this.log = function (txt, level) {
  327. if (this.debug) {
  328. if (level == 'error') {
  329. console.log('ERROR: '+txt);
  330. } else {
  331. console.log(txt);
  332. }
  333. }
  334. };
  335. this.getVCard = function (jid, callback, errback) {
  336. if (!this.use_vcards) {
  337. if (callback) {
  338. callback(jid, jid);
  339. }
  340. return;
  341. }
  342. converse.connection.vcard.get(
  343. $.proxy(function (iq) {
  344. // Successful callback
  345. var $vcard = $(iq).find('vCard');
  346. var fullname = $vcard.find('FN').text(),
  347. img = $vcard.find('BINVAL').text(),
  348. img_type = $vcard.find('TYPE').text(),
  349. url = $vcard.find('URL').text();
  350. if (jid) {
  351. var contact = converse.roster.get(jid);
  352. if (contact) {
  353. fullname = _.isEmpty(fullname)? contact.get('fullname') || jid: fullname;
  354. contact.save({
  355. 'fullname': fullname,
  356. 'image_type': img_type,
  357. 'image': img,
  358. 'url': url,
  359. 'vcard_updated': moment().format()
  360. });
  361. }
  362. }
  363. if (callback) {
  364. callback(jid, fullname, img, img_type, url);
  365. }
  366. }, this),
  367. jid,
  368. function (iq) {
  369. // Error callback
  370. var contact = converse.roster.get(jid);
  371. if (contact) {
  372. contact.save({
  373. 'vcard_updated': moment().format()
  374. });
  375. }
  376. if (errback) {
  377. errback(jid, iq);
  378. }
  379. }
  380. );
  381. };
  382. this.reconnect = function () {
  383. converse.giveFeedback(__('Reconnecting'), 'error');
  384. converse.emit('reconnect');
  385. if (!converse.prebind) {
  386. this.connection.connect(
  387. this.connection.jid,
  388. this.connection.pass,
  389. function (status, condition) {
  390. converse.onConnect(status, condition, true);
  391. },
  392. this.connection.wait,
  393. this.connection.hold,
  394. this.connection.route
  395. );
  396. }
  397. };
  398. this.renderLoginPanel = function () {
  399. converse._tearDown();
  400. var view = converse.chatboxviews.get('controlbox');
  401. view.model.set({connected:false});
  402. view.renderLoginPanel();
  403. };
  404. this.onConnect = function (status, condition, reconnect) {
  405. if ((status === Strophe.Status.CONNECTED) ||
  406. (status === Strophe.Status.ATTACHED)) {
  407. if ((typeof reconnect !== 'undefined') && (reconnect)) {
  408. converse.log(status === Strophe.Status.CONNECTED ? 'Reconnected' : 'Reattached');
  409. converse.onReconnected();
  410. } else {
  411. converse.log(status === Strophe.Status.CONNECTED ? 'Connected' : 'Attached');
  412. converse.onConnected();
  413. }
  414. } else if (status === Strophe.Status.DISCONNECTED) {
  415. if (converse.auto_reconnect) {
  416. converse.reconnect();
  417. } else {
  418. converse.renderLoginPanel();
  419. }
  420. } else if (status === Strophe.Status.Error) {
  421. converse.giveFeedback(__('Error'), 'error');
  422. } else if (status === Strophe.Status.CONNECTING) {
  423. converse.giveFeedback(__('Connecting'));
  424. } else if (status === Strophe.Status.AUTHENTICATING) {
  425. converse.giveFeedback(__('Authenticating'));
  426. } else if (status === Strophe.Status.AUTHFAIL) {
  427. converse.giveFeedback(__('Authentication Failed'), 'error');
  428. converse.connection.disconnect(__('Authentication Failed'));
  429. } else if (status === Strophe.Status.DISCONNECTING) {
  430. if (!converse.connection.connected) {
  431. converse.renderLoginPanel();
  432. }
  433. if (condition) {
  434. converse.giveFeedback(condition, 'error');
  435. }
  436. }
  437. };
  438. this.applyHeightResistance = function (height) {
  439. /* This method applies some resistance/gravity around the
  440. * "default_box_height". If "height" is close enough to
  441. * default_box_height, then that is returned instead.
  442. */
  443. if (typeof height === 'undefined') {
  444. return converse.default_box_height;
  445. }
  446. var resistance = 10;
  447. if ((height !== converse.default_box_height) &&
  448. (Math.abs(height - converse.default_box_height) < resistance)) {
  449. return converse.default_box_height;
  450. }
  451. return height;
  452. };
  453. this.updateMsgCounter = function () {
  454. if (this.msg_counter > 0) {
  455. if (document.title.search(/^Messages \(\d+\) /) == -1) {
  456. document.title = "Messages (" + this.msg_counter + ") " + document.title;
  457. } else {
  458. document.title = document.title.replace(/^Messages \(\d+\) /, "Messages (" + this.msg_counter + ") ");
  459. }
  460. window.blur();
  461. window.focus();
  462. } else if (document.title.search(/^Messages \(\d+\) /) != -1) {
  463. document.title = document.title.replace(/^Messages \(\d+\) /, "");
  464. }
  465. };
  466. this.incrementMsgCounter = function () {
  467. this.msg_counter += 1;
  468. this.updateMsgCounter();
  469. };
  470. this.clearMsgCounter = function () {
  471. this.msg_counter = 0;
  472. this.updateMsgCounter();
  473. };
  474. this.initStatus = function (callback) {
  475. this.xmppstatus = new this.XMPPStatus();
  476. var id = b64_sha1('converse.xmppstatus-'+converse.bare_jid);
  477. this.xmppstatus.id = id; // Appears to be necessary for backbone.browserStorage
  478. this.xmppstatus.browserStorage = new Backbone.BrowserStorage[converse.storage](id);
  479. this.xmppstatus.fetch({success: callback, error: callback});
  480. };
  481. this.initSession = function () {
  482. this.session = new this.BOSHSession();
  483. var id = b64_sha1('converse.bosh-session');
  484. this.session.id = id; // Appears to be necessary for backbone.browserStorage
  485. this.session.browserStorage = new Backbone.BrowserStorage[converse.storage](id);
  486. this.session.fetch();
  487. $(window).on('beforeunload', $.proxy(function () {
  488. if (converse.connection.authenticated) {
  489. this.setSession();
  490. } else {
  491. this.clearSession();
  492. }
  493. }, this));
  494. };
  495. this.clearSession = function () {
  496. this.roster.browserStorage._clear();
  497. this.session.browserStorage._clear();
  498. // XXX: this should perhaps go into the beforeunload handler
  499. converse.chatboxes.get('controlbox').save({'connected': false});
  500. };
  501. this.setSession = function () {
  502. if (this.keepalive) {
  503. this.session.save({
  504. jid: this.connection.jid,
  505. rid: this.connection._proto.rid,
  506. sid: this.connection._proto.sid
  507. });
  508. }
  509. };
  510. this.logOut = function () {
  511. converse.chatboxviews.closeAllChatBoxes(false);
  512. converse.clearSession();
  513. converse.connection.disconnect();
  514. };
  515. this.registerGlobalEventHandlers = function () {
  516. $(document).click(function () {
  517. if ($('.toggle-otr ul').is(':visible')) {
  518. $('.toggle-otr ul', this).slideUp();
  519. }
  520. if ($('.toggle-smiley ul').is(':visible')) {
  521. $('.toggle-smiley ul', this).slideUp();
  522. }
  523. });
  524. $(document).on('mousemove', $.proxy(function (ev) {
  525. if (!this.resized_chatbox || !this.allow_dragresize) { return true; }
  526. ev.preventDefault();
  527. this.resized_chatbox.resizeChatBox(ev);
  528. }, this));
  529. $(document).on('mouseup', $.proxy(function (ev) {
  530. if (!this.resized_chatbox || !this.allow_dragresize) { return true; }
  531. ev.preventDefault();
  532. var height = this.applyHeightResistance(this.resized_chatbox.height);
  533. if (this.connection.connected) {
  534. this.resized_chatbox.model.save({'height': height});
  535. } else {
  536. this.resized_chatbox.model.set({'height': height});
  537. }
  538. this.resized_chatbox = null;
  539. }, this));
  540. $(window).on("blur focus", $.proxy(function (ev) {
  541. if ((this.windowState != ev.type) && (ev.type == 'focus')) {
  542. converse.clearMsgCounter();
  543. }
  544. this.windowState = ev.type;
  545. },this));
  546. $(window).on("resize", _.debounce($.proxy(function (ev) {
  547. this.chatboxviews.trimChats();
  548. },this), 200));
  549. };
  550. this.onReconnected = function () {
  551. // We need to re-register all the event handlers on the newly
  552. // created connection.
  553. this.initStatus($.proxy(function () {
  554. this.registerRosterXHandler();
  555. this.registerPresenceHandler();
  556. this.chatboxes.registerMessageHandler();
  557. converse.xmppstatus.sendPresence();
  558. this.giveFeedback(__('Online Contacts'));
  559. }, this));
  560. };
  561. this.enableCarbons = function () {
  562. /* Ask the XMPP server to enable Message Carbons
  563. * See XEP-0280 https://xmpp.org/extensions/xep-0280.html#enabling
  564. */
  565. if (!this.message_carbons) {
  566. return;
  567. }
  568. var carbons_iq = new Strophe.Builder('iq', {
  569. from: this.connection.jid,
  570. id: 'enablecarbons',
  571. type: 'set'
  572. })
  573. .c('enable', {xmlns: 'urn:xmpp:carbons:2'});
  574. this.connection.send(carbons_iq);
  575. this.connection.addHandler(function (iq) {
  576. if ($(iq).find('error').length > 0) {
  577. converse.log('ERROR: An error occured while trying to enable message carbons.');
  578. } else {
  579. converse.log('Message carbons appear to have been enabled.');
  580. }
  581. }, null, "iq", null, "enablecarbons");
  582. };
  583. this.onConnected = function () {
  584. // When reconnecting, there might be some open chat boxes. We don't
  585. // know whether these boxes are of the same account or not, so we
  586. // close them now.
  587. this.chatboxviews.closeAllChatBoxes();
  588. this.setSession();
  589. this.jid = this.connection.jid;
  590. this.bare_jid = Strophe.getBareJidFromJid(this.connection.jid);
  591. this.domain = Strophe.getDomainFromJid(this.connection.jid);
  592. this.minimized_chats = new converse.MinimizedChats({model: this.chatboxes});
  593. this.features = new this.Features();
  594. this.enableCarbons();
  595. this.initStatus($.proxy(function () {
  596. this.chatboxes.onConnected();
  597. this.giveFeedback(__('Online Contacts'));
  598. if (this.callback) {
  599. if (this.connection.service === 'jasmine tests') {
  600. // XXX: Call back with the internal converse object. This
  601. // object should never be exposed to production systems.
  602. // 'jasmine tests' is an invalid http bind service value,
  603. // so we're sure that this is just for tests.
  604. //
  605. // TODO: We might need to consider websockets, which
  606. // probably won't use the 'service' attr. Current
  607. // strophe.js version used by converse.js doesn't support
  608. // websockets.
  609. this.callback(this);
  610. } else {
  611. this.callback();
  612. }
  613. }
  614. }, this));
  615. converse.emit('ready');
  616. };
  617. // Backbone Models and Views
  618. // -------------------------
  619. this.OTR = Backbone.Model.extend({
  620. // A model for managing OTR settings.
  621. getSessionPassphrase: function () {
  622. if (converse.prebind) {
  623. var key = b64_sha1(converse.connection.jid),
  624. pass = window.sessionStorage[key];
  625. if (typeof pass === 'undefined') {
  626. pass = Math.floor(Math.random()*4294967295).toString();
  627. window.sessionStorage[key] = pass;
  628. }
  629. return pass;
  630. } else {
  631. return converse.connection.pass;
  632. }
  633. },
  634. generatePrivateKey: function () {
  635. var key = new DSA();
  636. var jid = converse.connection.jid;
  637. if (converse.cache_otr_key) {
  638. var cipher = CryptoJS.lib.PasswordBasedCipher;
  639. var pass = this.getSessionPassphrase();
  640. if (typeof pass !== "undefined") {
  641. // Encrypt the key and set in sessionStorage. Also store instance tag.
  642. window.sessionStorage[b64_sha1(jid+'priv_key')] =
  643. cipher.encrypt(CryptoJS.algo.AES, key.packPrivate(), pass).toString();
  644. window.sessionStorage[b64_sha1(jid+'instance_tag')] = instance_tag;
  645. window.sessionStorage[b64_sha1(jid+'pass_check')] =
  646. cipher.encrypt(CryptoJS.algo.AES, 'match', pass).toString();
  647. }
  648. }
  649. return key;
  650. }
  651. });
  652. this.Message = Backbone.Model;
  653. this.Messages = Backbone.Collection.extend({
  654. model: converse.Message
  655. });
  656. this.ChatBox = Backbone.Model.extend({
  657. initialize: function () {
  658. var height = converse.applyHeightResistance(this.get('height'));
  659. if (this.get('box_id') !== 'controlbox') {
  660. this.messages = new converse.Messages();
  661. this.messages.browserStorage = new Backbone.BrowserStorage[converse.storage](
  662. b64_sha1('converse.messages'+this.get('jid')+converse.bare_jid));
  663. this.save({
  664. 'box_id' : b64_sha1(this.get('jid')),
  665. 'height': height,
  666. 'minimized': this.get('minimized') || false,
  667. 'otr_status': this.get('otr_status') || UNENCRYPTED,
  668. 'time_minimized': this.get('time_minimized') || moment(),
  669. 'time_opened': this.get('time_opened') || moment().valueOf(),
  670. 'user_id' : Strophe.getNodeFromJid(this.get('jid')),
  671. 'num_unread': this.get('num_unread') || 0,
  672. 'url': ''
  673. });
  674. } else {
  675. this.set({
  676. 'height': height,
  677. 'time_opened': moment(0).valueOf(),
  678. 'num_unread': this.get('num_unread') || 0
  679. });
  680. }
  681. },
  682. maximize: function () {
  683. this.save({
  684. 'minimized': false,
  685. 'time_opened': moment().valueOf()
  686. });
  687. },
  688. minimize: function () {
  689. this.save({
  690. 'minimized': true,
  691. 'time_minimized': moment().format()
  692. });
  693. },
  694. getSession: function (callback) {
  695. var cipher = CryptoJS.lib.PasswordBasedCipher;
  696. var result, pass, instance_tag, saved_key, pass_check;
  697. if (converse.cache_otr_key) {
  698. pass = converse.otr.getSessionPassphrase();
  699. if (typeof pass !== "undefined") {
  700. instance_tag = window.sessionStorage[b64_sha1(this.id+'instance_tag')];
  701. saved_key = window.sessionStorage[b64_sha1(this.id+'priv_key')];
  702. pass_check = window.sessionStorage[b64_sha1(this.connection.jid+'pass_check')];
  703. if (saved_key && instance_tag && typeof pass_check !== 'undefined') {
  704. var decrypted = cipher.decrypt(CryptoJS.algo.AES, saved_key, pass);
  705. var key = DSA.parsePrivate(decrypted.toString(CryptoJS.enc.Latin1));
  706. if (cipher.decrypt(CryptoJS.algo.AES, pass_check, pass).toString(CryptoJS.enc.Latin1) === 'match') {
  707. // Verified that the passphrase is still the same
  708. this.trigger('showHelpMessages', [__('Re-establishing encrypted session')]);
  709. callback({
  710. 'key': key,
  711. 'instance_tag': instance_tag
  712. });
  713. return; // Our work is done here
  714. }
  715. }
  716. }
  717. }
  718. // We need to generate a new key and instance tag
  719. this.trigger('showHelpMessages', [
  720. __('Generating private key.'),
  721. __('Your browser might become unresponsive.')],
  722. null,
  723. true // show spinner
  724. );
  725. setTimeout(function () {
  726. callback({
  727. 'key': converse.otr.generatePrivateKey.apply(this),
  728. 'instance_tag': OTR.makeInstanceTag()
  729. });
  730. }, 500);
  731. },
  732. updateOTRStatus: function (state) {
  733. switch (state) {
  734. case OTR.CONST.STATUS_AKE_SUCCESS:
  735. if (this.otr.msgstate === OTR.CONST.MSGSTATE_ENCRYPTED) {
  736. this.save({'otr_status': UNVERIFIED});
  737. }
  738. break;
  739. case OTR.CONST.STATUS_END_OTR:
  740. if (this.otr.msgstate === OTR.CONST.MSGSTATE_FINISHED) {
  741. this.save({'otr_status': FINISHED});
  742. } else if (this.otr.msgstate === OTR.CONST.MSGSTATE_PLAINTEXT) {
  743. this.save({'otr_status': UNENCRYPTED});
  744. }
  745. break;
  746. }
  747. },
  748. onSMP: function (type, data) {
  749. // Event handler for SMP (Socialist's Millionaire Protocol)
  750. // used by OTR (off-the-record).
  751. switch (type) {
  752. case 'question':
  753. this.otr.smpSecret(prompt(__(
  754. 'Authentication request from %1$s\n\nYour chat contact is attempting to verify your identity, by asking you the question below.\n\n%2$s',
  755. [this.get('fullname'), data])));
  756. break;
  757. case 'trust':
  758. if (data === true) {
  759. this.save({'otr_status': VERIFIED});
  760. } else {
  761. this.trigger(
  762. 'showHelpMessages',
  763. [__("Could not verify this user's identify.")],
  764. 'error');
  765. this.save({'otr_status': UNVERIFIED});
  766. }
  767. break;
  768. default:
  769. throw new Error('Unknown type.');
  770. }
  771. },
  772. initiateOTR: function (query_msg) {
  773. // Sets up an OTR object through which we can send and receive
  774. // encrypted messages.
  775. //
  776. // If 'query_msg' is passed in, it means there is an alread incoming
  777. // query message from our contact. Otherwise, it is us who will
  778. // send the query message to them.
  779. this.save({'otr_status': UNENCRYPTED});
  780. var session = this.getSession($.proxy(function (session) {
  781. this.otr = new OTR({
  782. fragment_size: 140,
  783. send_interval: 200,
  784. priv: session.key,
  785. instance_tag: session.instance_tag,
  786. debug: this.debug
  787. });
  788. this.otr.on('status', $.proxy(this.updateOTRStatus, this));
  789. this.otr.on('smp', $.proxy(this.onSMP, this));
  790. this.otr.on('ui', $.proxy(function (msg) {
  791. this.trigger('showReceivedOTRMessage', msg);
  792. }, this));
  793. this.otr.on('io', $.proxy(function (msg) {
  794. this.trigger('sendMessageStanza', msg);
  795. }, this));
  796. this.otr.on('error', $.proxy(function (msg) {
  797. this.trigger('showOTRError', msg);
  798. }, this));
  799. this.trigger('showHelpMessages', [__('Exchanging private key with contact.')]);
  800. if (query_msg) {
  801. this.otr.receiveMsg(query_msg);
  802. } else {
  803. this.otr.sendQueryMsg();
  804. }
  805. }, this));
  806. },
  807. endOTR: function () {
  808. if (this.otr) {
  809. this.otr.endOtr();
  810. }
  811. this.save({'otr_status': UNENCRYPTED});
  812. },
  813. createMessage: function ($message) {
  814. var body = $message.children('body').text(),
  815. composing = $message.find('composing'),
  816. paused = $message.find('paused'),
  817. delayed = $message.find('delay').length > 0,
  818. fullname = this.get('fullname'),
  819. is_groupchat = $message.attr('type') === 'groupchat',
  820. msgid = $message.attr('id'),
  821. stamp, time, sender, from;
  822. if (is_groupchat) {
  823. from = Strophe.unescapeNode(Strophe.getResourceFromJid($message.attr('from')));
  824. } else {
  825. from = Strophe.getBareJidFromJid($message.attr('from'));
  826. }
  827. fullname = (_.isEmpty(fullname)? from: fullname).split(' ')[0];
  828. if (!body) {
  829. if (composing.length || paused.length) {
  830. // FIXME: use one attribute for chat states (e.g.
  831. // chatstate) instead of saving 'paused' and
  832. // 'composing' separately.
  833. this.messages.add({
  834. fullname: fullname,
  835. sender: 'them',
  836. delayed: delayed,
  837. time: moment().format(),
  838. composing: composing.length,
  839. paused: paused.length
  840. });
  841. }
  842. } else {
  843. if (delayed) {
  844. stamp = $message.find('delay').attr('stamp');
  845. time = stamp;
  846. } else {
  847. time = moment().format();
  848. }
  849. if ((is_groupchat && from === this.get('nick')) || (!is_groupchat && from == converse.bare_jid)) {
  850. sender = 'me';
  851. } else {
  852. sender = 'them';
  853. }
  854. this.messages.create({
  855. fullname: fullname,
  856. sender: sender,
  857. delayed: delayed,
  858. time: time,
  859. message: body,
  860. msgid: msgid
  861. });
  862. }
  863. },
  864. receiveMessage: function ($message) {
  865. var $body = $message.children('body');
  866. var text = ($body.length > 0 ? $body.text() : undefined);
  867. if ((!text) || (!converse.allow_otr)) {
  868. return this.createMessage($message);
  869. }
  870. if (text.match(/^\?OTRv23?/)) {
  871. this.initiateOTR(text);
  872. } else {
  873. if (_.contains([UNVERIFIED, VERIFIED], this.get('otr_status'))) {
  874. this.otr.receiveMsg(text);
  875. } else {
  876. if (text.match(/^\?OTR/)) {
  877. if (!this.otr) {
  878. this.initiateOTR(text);
  879. } else {
  880. this.otr.receiveMsg(text);
  881. }
  882. } else {
  883. // Normal unencrypted message.
  884. this.createMessage($message);
  885. }
  886. }
  887. }
  888. }
  889. });
  890. this.ChatBoxView = Backbone.View.extend({
  891. length: 200,
  892. tagName: 'div',
  893. className: 'chatbox',
  894. is_chatroom: false, // This is not a multi-user chatroom
  895. events: {
  896. 'click .close-chatbox-button': 'close',
  897. 'click .toggle-chatbox-button': 'minimize',
  898. 'keypress textarea.chat-textarea': 'keyPressed',
  899. 'click .toggle-smiley': 'toggleEmoticonMenu',
  900. 'click .toggle-smiley ul li': 'insertEmoticon',
  901. 'click .toggle-clear': 'clearMessages',
  902. 'click .toggle-otr': 'toggleOTRMenu',
  903. 'click .start-otr': 'startOTRFromToolbar',
  904. 'click .end-otr': 'endOTR',
  905. 'click .auth-otr': 'authOTR',
  906. 'click .toggle-call': 'toggleCall',
  907. 'mousedown .dragresize-tm': 'onDragResizeStart'
  908. },
  909. initialize: function (){
  910. this.model.messages.on('add', this.onMessageAdded, this);
  911. this.model.on('show', this.show, this);
  912. this.model.on('destroy', this.hide, this);
  913. this.model.on('change', this.onChange, this);
  914. this.model.on('showOTRError', this.showOTRError, this);
  915. // XXX: doesn't look like this event is being used?
  916. this.model.on('buddyStartsOTR', this.buddyStartsOTR, this);
  917. this.model.on('showHelpMessages', this.showHelpMessages, this);
  918. this.model.on('sendMessageStanza', this.sendMessageStanza, this);
  919. this.model.on('showSentOTRMessage', function (text) {
  920. this.showMessage({'message': text, 'sender': 'me'});
  921. }, this);
  922. this.model.on('showReceivedOTRMessage', function (text) {
  923. this.showMessage({'message': text, 'sender': 'them'});
  924. }, this);
  925. this.updateVCard();
  926. this.$el.insertAfter(converse.chatboxviews.get("controlbox").$el);
  927. this.render().model.messages.fetch({add: true});
  928. if (this.model.get('minimized')) {
  929. this.hide();
  930. } else {
  931. this.show();
  932. }
  933. if ((_.contains([UNVERIFIED, VERIFIED], this.model.get('otr_status'))) || converse.use_otr_by_default) {
  934. this.model.initiateOTR();
  935. }
  936. },
  937. render: function () {
  938. this.$el.attr('id', this.model.get('box_id'))
  939. .html(converse.templates.chatbox(
  940. _.extend(this.model.toJSON(), {
  941. show_toolbar: converse.show_toolbar,
  942. label_personal_message: __('Personal message')
  943. }
  944. )
  945. )
  946. );
  947. this.renderToolbar().renderAvatar();
  948. converse.emit('chatBoxOpened', this);
  949. setTimeout(function () {
  950. converse.refreshWebkit();
  951. }, 50);
  952. return this.showStatusMessage();
  953. },
  954. initDragResize: function () {
  955. this.prev_pageY = 0; // To store last known mouse position
  956. if (converse.connection.connected) {
  957. this.height = this.model.get('height');
  958. }
  959. return this;
  960. },
  961. showStatusNotification: function (message, keep_old) {
  962. var $chat_content = this.$el.find('.chat-content');
  963. if (!keep_old) {
  964. $chat_content.find('div.chat-event').remove();
  965. }
  966. $chat_content.append($('<div class="chat-event"></div>').text(message));
  967. this.scrollDown();
  968. },
  969. clearChatRoomMessages: function (ev) {
  970. if (typeof ev !== "undefined") { ev.stopPropagation(); }
  971. var result = confirm(__("Are you sure you want to clear the messages from this room?"));
  972. if (result === true) {
  973. this.$el.find('.chat-content').empty();
  974. }
  975. return this;
  976. },
  977. showMessage: function (msg_dict) {
  978. var $content = this.$el.find('.chat-content'),
  979. msg_time = moment(msg_dict.time) || moment,
  980. text = msg_dict.message,
  981. match = text.match(/^\/(.*?)(?: (.*))?$/),
  982. fullname = this.model.get('fullname') || msg_dict.fullname,
  983. extra_classes = msg_dict.delayed && 'delayed' || '',
  984. template, username;
  985. if ((match) && (match[1] === 'me')) {
  986. text = text.replace(/^\/me/, '');
  987. template = converse.templates.action;
  988. username = fullname;
  989. } else {
  990. template = converse.templates.message;
  991. username = msg_dict.sender === 'me' && __('me') || fullname;
  992. }
  993. $content.find('div.chat-event').remove();
  994. if (this.is_chatroom && msg_dict.sender == 'them' && (new RegExp("\\b"+this.model.get('nick')+"\\b")).test(text)) {
  995. // Add special class to mark groupchat messages in which we
  996. // are mentioned.
  997. extra_classes += ' mentioned';
  998. }
  999. var message = template({
  1000. 'sender': msg_dict.sender,
  1001. 'time': msg_time.format('hh:mm'),
  1002. 'username': username,
  1003. 'message': '',
  1004. 'extra_classes': extra_classes
  1005. });
  1006. $content.append($(message).children('.chat-message-content').first().text(text).addHyperlinks().addEmoticons().parent());
  1007. this.scrollDown();
  1008. },
  1009. showHelpMessages: function (msgs, type, spinner) {
  1010. var $chat_content = this.$el.find('.chat-content'), i,
  1011. msgs_length = msgs.length;
  1012. for (i=0; i<msgs_length; i++) {
  1013. $chat_content.append($('<div class="chat-'+(type||'info')+'">'+msgs[i]+'</div>'));
  1014. }
  1015. if (spinner === true) {
  1016. $chat_content.append('<span class="spinner"/>');
  1017. } else if (spinner === false) {
  1018. $chat_content.find('span.spinner').remove();
  1019. }
  1020. return this.scrollDown();
  1021. },
  1022. onMessageAdded: function (message) {
  1023. var time = message.get('time'),
  1024. times = this.model.messages.pluck('time'),
  1025. previous_message, idx, this_date, prev_date, text, match;
  1026. // If this message is on a different day than the one received
  1027. // prior, then indicate it on the chatbox.
  1028. idx = _.indexOf(times, time)-1;
  1029. if (idx >= 0) {
  1030. previous_message = this.model.messages.at(idx);
  1031. prev_date = moment(previous_message.get('time'));
  1032. if (prev_date.isBefore(time, 'day')) {
  1033. this_date = moment(time);
  1034. this.$el.find('.chat-content').append(converse.templates.new_day({
  1035. isodate: this_date.format("YYYY-MM-DD"),
  1036. datestring: this_date.format("dddd MMM Do YYYY")
  1037. }));
  1038. }
  1039. }
  1040. if (message.get(COMPOSING)) {
  1041. this.showStatusNotification(message.get('fullname')+' '+__('is typing'));
  1042. return;
  1043. } else if (message.get(PAUSED)) {
  1044. this.showStatusNotification(message.get('fullname')+' '+__('has stopped typing'));
  1045. return;
  1046. } else {
  1047. this.showMessage(_.clone(message.attributes));
  1048. }
  1049. if ((message.get('sender') != 'me') && (converse.windowState == 'blur')) {
  1050. converse.incrementMsgCounter();
  1051. }
  1052. return this.scrollDown();
  1053. },
  1054. sendMessageStanza: function (text) {
  1055. /*
  1056. * Sends the actual XML stanza to the XMPP server.
  1057. */
  1058. // TODO: Look in ChatPartners to see what resources we have for the recipient.
  1059. // if we have one resource, we sent to only that resources, if we have multiple
  1060. // we send to the bare jid.
  1061. var timestamp = (new Date()).getTime();
  1062. var bare_jid = this.model.get('jid');
  1063. var message = $msg({from: converse.connection.jid, to: bare_jid, type: 'chat', id: timestamp})
  1064. .c('body').t(text).up()
  1065. .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'});
  1066. converse.connection.send(message);
  1067. if (converse.forward_messages) {
  1068. // Forward the message, so that other connected resources are also aware of it.
  1069. var forwarded = $msg({to:converse.bare_jid, type:'chat', id:timestamp})
  1070. .c('forwarded', {xmlns:'urn:xmpp:forward:0'})
  1071. .c('delay', {xmns:'urn:xmpp:delay',stamp:timestamp}).up()
  1072. .cnode(message.tree());
  1073. converse.connection.send(forwarded);
  1074. }
  1075. },
  1076. sendMessage: function (text) {
  1077. var match = text.replace(/^\s*/, "").match(/^\/(.*)\s*$/), msgs;
  1078. if (match) {
  1079. if (match[1] === "clear") {
  1080. return this.clearMessages();
  1081. }
  1082. else if (match[1] === "help") {
  1083. msgs = [
  1084. '<strong>/help</strong>:'+__('Show this menu')+'',
  1085. '<strong>/me</strong>:'+__('Write in the third person')+'',
  1086. '<strong>/clear</strong>:'+__('Remove messages')+''
  1087. ];
  1088. this.showHelpMessages(msgs);
  1089. return;
  1090. } else if ((converse.allow_otr) && (match[1] === "endotr")) {
  1091. return this.endOTR();
  1092. } else if ((converse.allow_otr) && (match[1] === "otr")) {
  1093. return this.model.initiateOTR();
  1094. }
  1095. }
  1096. if (_.contains([UNVERIFIED, VERIFIED], this.model.get('otr_status'))) {
  1097. // Off-the-record encryption is active
  1098. this.model.otr.sendMsg(text);
  1099. this.model.trigger('showSentOTRMessage', text);
  1100. } else {
  1101. // We only save unencrypted messages.
  1102. var fullname = converse.xmppstatus.get('fullname');
  1103. fullname = _.isEmpty(fullname)? converse.bare_jid: fullname;
  1104. this.model.messages.create({
  1105. fullname: fullname,
  1106. sender: 'me',
  1107. time: moment().format(),
  1108. message: text
  1109. });
  1110. this.sendMessageStanza(text);
  1111. }
  1112. },
  1113. keyPressed: function (ev) {
  1114. var $textarea = $(ev.target),
  1115. message, notify, composing;
  1116. if(ev.keyCode == KEY.ENTER) {
  1117. ev.preventDefault();
  1118. message = $textarea.val();
  1119. $textarea.val('').focus();
  1120. if (message !== '') {
  1121. if (this.model.get('chatroom')) {
  1122. this.sendChatRoomMessage(message);
  1123. } else {
  1124. this.sendMessage(message);
  1125. }
  1126. converse.emit('messageSend', message);
  1127. }
  1128. this.$el.data('composing', false);
  1129. } else if (!this.model.get('chatroom')) {
  1130. // composing data is only for single user chat
  1131. composing = this.$el.data('composing');
  1132. if (!composing) {
  1133. if (ev.keyCode != 47) {
  1134. // We don't send composing messages if the message
  1135. // starts with forward-slash.
  1136. notify = $msg({'to':this.model.get('jid'), 'type': 'chat'})
  1137. .c('composing', {'xmlns':'http://jabber.org/protocol/chatstates'});
  1138. converse.connection.send(notify);
  1139. }
  1140. this.$el.data('composing', true);
  1141. }
  1142. }
  1143. },
  1144. onDragResizeStart: function (ev) {
  1145. if (!converse.allow_dragresize) { return true; }
  1146. // Record element attributes for mouseMove().
  1147. this.height = this.$el.children('.box-flyout').height();
  1148. converse.resized_chatbox = this;
  1149. this.prev_pageY = ev.pageY;
  1150. },
  1151. setChatBoxHeight: function (height) {
  1152. if (!this.model.get('minimized')) {
  1153. this.$el.children('.box-flyout')[0].style.height = converse.applyHeightResistance(height)+'px';
  1154. }
  1155. },
  1156. resizeChatBox: function (ev) {
  1157. var diff = ev.pageY - this.prev_pageY;
  1158. if (!diff) { return; }
  1159. this.height -= diff;
  1160. this.prev_pageY = ev.pageY;
  1161. this.setChatBoxHeight(this.height);
  1162. },
  1163. clearMessages: function (ev) {
  1164. if (ev && ev.preventDefault) { ev.preventDefault(); }
  1165. var result = confirm(__("Are you sure you want to clear the messages from this chat box?"));
  1166. if (result === true) {
  1167. this.$el.find('.chat-content').empty();
  1168. this.model.messages.reset();
  1169. this.model.messages.browserStorage._clear();
  1170. }
  1171. return this;
  1172. },
  1173. insertEmoticon: function (ev) {
  1174. ev.stopPropagation();
  1175. this.$el.find('.toggle-smiley ul').slideToggle(200);
  1176. var $textbox = this.$el.find('textarea.chat-textarea');
  1177. var value = $textbox.val();
  1178. var $target = $(ev.target);
  1179. $target = $target.is('a') ? $target : $target.children('a');
  1180. if (value && (value[value.length-1] !== ' ')) {
  1181. value = value + ' ';
  1182. }
  1183. $textbox.focus().val(value+$target.data('emoticon')+' ');
  1184. },
  1185. toggleEmoticonMenu: function (ev) {
  1186. ev.stopPropagation();
  1187. this.$el.find('.toggle-smiley ul').slideToggle(200);
  1188. },
  1189. toggleOTRMenu: function (ev) {
  1190. ev.stopPropagation();
  1191. this.$el.find('.toggle-otr ul').slideToggle(200);
  1192. },
  1193. showOTRError: function (msg) {
  1194. if (msg == 'Message cannot be sent at this time.') {
  1195. this.showHelpMessages(
  1196. [__('Your message could not be sent')], 'error');
  1197. } else if (msg == 'Received an unencrypted message.') {
  1198. this.showHelpMessages(
  1199. [__('We received an unencrypted message')], 'error');
  1200. } else if (msg == 'Received an unreadable encrypted message.') {
  1201. this.showHelpMessages(
  1202. [__('We received an unreadable encrypted message')],
  1203. 'error');
  1204. } else {
  1205. this.showHelpMessages(['Encryption error occured: '+msg], 'error');
  1206. }
  1207. console.log("OTR ERROR:"+msg);
  1208. },
  1209. buddyStartsOTR: function (ev) {
  1210. this.showHelpMessages([__('This user has requested an encrypted session.')]);
  1211. this.model.initiateOTR();
  1212. },
  1213. startOTRFromToolbar: function (ev) {
  1214. $(ev.target).parent().parent().slideUp();
  1215. ev.stopPropagation();
  1216. this.model.initiateOTR();
  1217. },
  1218. endOTR: function (ev) {
  1219. if (typeof ev !== "undefined") {
  1220. ev.preventDefault();
  1221. ev.stopPropagation();
  1222. }
  1223. this.model.endOTR();
  1224. },
  1225. authOTR: function (ev) {
  1226. var scheme = $(ev.target).data().scheme;
  1227. var result, question, answer;
  1228. if (scheme === 'fingerprint') {
  1229. 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.', [
  1230. this.model.get('fullname'),
  1231. converse.xmppstatus.get('fullname')||converse.bare_jid,
  1232. this.model.otr.priv.fingerprint(),
  1233. this.model.otr.their_priv_pk.fingerprint()
  1234. ]
  1235. ));
  1236. if (result === true) {
  1237. this.model.save({'otr_status': VERIFIED});
  1238. } else {
  1239. this.model.save({'otr_status': UNVERIFIED});
  1240. }
  1241. } else if (scheme === 'smp') {
  1242. alert(__('You will be prompted to provide a security question and then an answer to that question.\n\nYour contact will then be prompted the same question and if they type the exact same answer (case sensitive), their identity will be verified.'));
  1243. question = prompt(__('What is your security question?'));
  1244. if (question) {
  1245. answer = prompt(__('What is the answer to the security question?'));
  1246. this.model.otr.smpSecret(answer, question);
  1247. }
  1248. } else {
  1249. this.showHelpMessages([__('Invalid authentication scheme provided')], 'error');
  1250. }
  1251. },
  1252. toggleCall: function (ev) {
  1253. ev.stopPropagation();
  1254. converse.emit('callButtonClicked', {
  1255. connection: converse.connection,
  1256. model: this.model
  1257. });
  1258. },
  1259. onChange: function (item, changed) {
  1260. if (_.has(item.changed, 'chat_status')) {
  1261. var chat_status = item.get('chat_status'),
  1262. fullname = item.get('fullname');
  1263. fullname = _.isEmpty(fullname)? item.get('jid'): fullname;
  1264. if (this.$el.is(':visible')) {
  1265. if (chat_status === 'offline') {
  1266. this.showStatusNotification(fullname+' '+'has gone offline');
  1267. } else if (chat_status === 'away') {
  1268. this.showStatusNotification(fullname+' '+'has gone away');
  1269. } else if ((chat_status === 'dnd')) {
  1270. this.showStatusNotification(fullname+' '+'is busy');
  1271. } else if (chat_status === 'online') {
  1272. this.$el.find('div.chat-event').remove();
  1273. }
  1274. }
  1275. converse.emit('contactStatusChanged', item.attributes, item.get('chat_status'));
  1276. // TODO: DEPRECATED AND SHOULD BE REMOVED IN 0.9.0
  1277. converse.emit('buddyStatusChanged', item.attributes, item.get('chat_status'));
  1278. }
  1279. if (_.has(item.changed, 'status')) {
  1280. this.showStatusMessage();
  1281. converse.emit('contactStatusMessageChanged', item.attributes, item.get('status'));
  1282. // TODO: DEPRECATED AND SHOULD BE REMOVED IN 0.9.0
  1283. converse.emit('buddyStatusMessageChanged', item.attributes, item.get('status'));
  1284. }
  1285. if (_.has(item.changed, 'image')) {
  1286. this.renderAvatar();
  1287. }
  1288. if (_.has(item.changed, 'otr_status')) {
  1289. this.renderToolbar().informOTRChange();
  1290. }
  1291. if (_.has(item.changed, 'minimized')) {
  1292. if (item.get('minimized')) {
  1293. this.hide();
  1294. } else {
  1295. this.maximize();
  1296. }
  1297. }
  1298. // TODO check for changed fullname as well
  1299. },
  1300. showStatusMessage: function (msg) {
  1301. msg = msg || this.model.get('status');
  1302. if (typeof msg === "string") {
  1303. this.$el.find('p.user-custom-message').text(msg).attr('title', msg);
  1304. }
  1305. return this;
  1306. },
  1307. close: function (ev) {
  1308. if (ev && ev.preventDefault) { ev.preventDefault(); }
  1309. if (converse.connection.connected) {
  1310. this.model.destroy();
  1311. } else {
  1312. this.model.trigger('hide');
  1313. }
  1314. converse.emit('chatBoxClosed', this);
  1315. return this;
  1316. },
  1317. maximize: function () {
  1318. // Restores a minimized chat box
  1319. this.$el.insertAfter(converse.chatboxviews.get("controlbox").$el).show('fast', $.proxy(function () {
  1320. converse.refreshWebkit();
  1321. this.focus();
  1322. converse.emit('chatBoxMaximized', this);
  1323. }, this));
  1324. },
  1325. minimize: function (ev) {
  1326. if (ev && ev.preventDefault) { ev.preventDefault(); }
  1327. // Minimizes a chat box
  1328. this.model.minimize();
  1329. this.$el.hide('fast', converse.refreshwebkit);
  1330. converse.emit('chatBoxMinimized', this);
  1331. },
  1332. updateVCard: function () {
  1333. var jid = this.model.get('jid'),
  1334. contact = converse.roster.get(jid);
  1335. if ((contact) && (!contact.get('vcard_updated'))) {
  1336. converse.getVCard(
  1337. jid,
  1338. $.proxy(function (jid, fullname, image, image_type, url) {
  1339. this.model.save({
  1340. 'fullname' : fullname || jid,
  1341. 'url': url,
  1342. 'image_type': image_type,
  1343. 'image': image
  1344. });
  1345. }, this),
  1346. $.proxy(function (stanza) {
  1347. converse.log("ChatBoxView.initialize: An error occured while fetching vcard");
  1348. }, this)
  1349. );
  1350. }
  1351. },
  1352. informOTRChange: function () {
  1353. var data = this.model.toJSON();
  1354. var msgs = [];
  1355. if (data.otr_status == UNENCRYPTED) {
  1356. msgs.push(__("Your messages are not encrypted anymore"));
  1357. } else if (data.otr_status == UNVERIFIED){
  1358. msgs.push(__("Your messages are now encrypted but your contact's identity has not been verified."));
  1359. } else if (data.otr_status == VERIFIED){
  1360. msgs.push(__("Your contact's identify has been verified."));
  1361. } else if (data.otr_status == FINISHED){
  1362. msgs.push(__("Your contact has ended encryption on their end, you should do the same."));
  1363. }
  1364. return this.showHelpMessages(msgs, 'info', false);
  1365. },
  1366. renderToolbar: function () {
  1367. if (converse.show_toolbar) {
  1368. var data = this.model.toJSON();
  1369. if (data.otr_status == UNENCRYPTED) {
  1370. data.otr_tooltip = __('Your messages are not encrypted. Click here to enable OTR encryption.');
  1371. } else if (data.otr_status == UNVERIFIED){
  1372. data.otr_tooltip = __('Your messages are encrypted, but your contact has not been verified.');
  1373. } else if (data.otr_status == VERIFIED){
  1374. data.otr_tooltip = __('Your messages are encrypted and your contact verified.');
  1375. } else if (data.otr_status == FINISHED){
  1376. data.otr_tooltip = __('Your contact has closed their end of the private session, you should do the same');
  1377. }
  1378. this.$el.find('.chat-toolbar').html(
  1379. converse.templates.toolbar(
  1380. _.extend(data, {
  1381. FINISHED: FINISHED,
  1382. UNENCRYPTED: UNENCRYPTED,
  1383. UNVERIFIED: UNVERIFIED,
  1384. VERIFIED: VERIFIED,
  1385. allow_otr: converse.allow_otr && !this.is_chatroom,
  1386. label_clear: __('Clear all messages'),
  1387. label_end_encrypted_conversation: __('End encrypted conversation'),
  1388. label_hide_participants: __('Hide the list of participants'),
  1389. label_refresh_encrypted_conversation: __('Refresh encrypted conversation'),
  1390. label_start_call: __('Start a call'),
  1391. label_start_encrypted_conversation: __('Start encrypted conversation'),
  1392. label_verify_with_fingerprints: __('Verify with fingerprints'),
  1393. label_verify_with_smp: __('Verify with SMP'),
  1394. label_whats_this: __("What\'s this?"),
  1395. otr_status_class: OTR_CLASS_MAPPING[data.otr_status],
  1396. otr_translated_status: OTR_TRANSLATED_MAPPING[data.otr_status],
  1397. show_call_button: converse.visible_toolbar_buttons.call,
  1398. show_clear_button: converse.visible_toolbar_buttons.clear,
  1399. show_emoticons: converse.visible_toolbar_buttons.emoticons,
  1400. show_participants_toggle: this.is_chatroom && converse.visible_toolbar_buttons.toggle_participants
  1401. })
  1402. )
  1403. );
  1404. }
  1405. return this;
  1406. },
  1407. renderAvatar: function () {
  1408. if (!this.model.get('image')) {
  1409. return;
  1410. }
  1411. var img_src = 'data:'+this.model.get('image_type')+';base64,'+this.model.get('image'),
  1412. canvas = $('<canvas height="31px" width="31px" class="avatar"></canvas>').get(0);
  1413. if (!(canvas.getContext && canvas.getContext('2d'))) {
  1414. return this;
  1415. }
  1416. var ctx = canvas.getContext('2d');
  1417. var img = new Image(); // Create new Image object
  1418. img.onload = function () {
  1419. var ratio = img.width/img.height;
  1420. ctx.drawImage(img, 0,0, 35*ratio, 35);
  1421. };
  1422. img.src = img_src;
  1423. this.$el.find('.chat-title').before(canvas);
  1424. return this;
  1425. },
  1426. focus: function () {
  1427. this.$el.find('.chat-textarea').focus();
  1428. converse.emit('chatBoxFocused', this);
  1429. return this;
  1430. },
  1431. hide: function () {
  1432. if (this.$el.is(':visible') && this.$el.css('opacity') == "1") {
  1433. this.$el.hide();
  1434. converse.refreshWebkit();
  1435. }
  1436. return this;
  1437. },
  1438. show: function (callback) {
  1439. if (this.$el.is(':visible') && this.$el.css('opacity') == "1") {
  1440. return this.focus();
  1441. }
  1442. this.$el.fadeIn(callback);
  1443. if (converse.connection.connected) {
  1444. // Without a connection, we haven't yet initialized
  1445. // localstorage
  1446. this.model.save();
  1447. this.initDragResize();
  1448. }
  1449. return this;
  1450. },
  1451. scrollDown: function () {
  1452. var $content = this.$('.chat-content');
  1453. if ($content.is(':visible')) {
  1454. $content.scrollTop($content[0].scrollHeight);
  1455. }
  1456. return this;
  1457. }
  1458. });
  1459. this.ContactsPanel = Backbone.View.extend({
  1460. tagName: 'div',
  1461. className: 'controlbox-pane',
  1462. id: 'users',
  1463. events: {
  1464. 'click a.toggle-xmpp-contact-form': 'toggleContactForm',
  1465. 'submit form.add-xmpp-contact': 'addContactFromForm',
  1466. 'submit form.search-xmpp-contact': 'searchContacts',
  1467. 'click a.subscribe-to-user': 'addContactFromList'
  1468. },
  1469. initialize: function (cfg) {
  1470. cfg.$parent.append(this.$el);
  1471. this.$tabs = cfg.$parent.parent().find('#controlbox-tabs');
  1472. },
  1473. render: function () {
  1474. var markup;
  1475. var widgets = converse.templates.contacts_panel({
  1476. label_online: __('Online'),
  1477. label_busy: __('Busy'),
  1478. label_away: __('Away'),
  1479. label_offline: __('Offline'),
  1480. label_logout: __('Log out'),
  1481. allow_logout: converse.allow_logout
  1482. });
  1483. this.$tabs.append(converse.templates.contacts_tab({label_contacts: LABEL_CONTACTS}));
  1484. if (converse.xhr_user_search) {
  1485. markup = converse.templates.search_contact({
  1486. label_contact_name: __('Contact name'),
  1487. label_search: __('Search')
  1488. });
  1489. } else {
  1490. markup = converse.templates.add_contact_form({
  1491. label_contact_username: __('Contact username'),
  1492. label_add: __('Add')
  1493. });
  1494. }
  1495. if (converse.allow_contact_requests) {
  1496. widgets += converse.templates.add_contact_dropdown({
  1497. label_click_to_chat: __('Click to add new chat contacts'),
  1498. label_add_contact: __('Add a contact')
  1499. });
  1500. }
  1501. this.$el.html(widgets);
  1502. this.$el.find('.search-xmpp ul').append(markup);
  1503. return this;
  1504. },
  1505. toggleContactForm: function (ev) {
  1506. ev.preventDefault();
  1507. this.$el.find('.search-xmpp').toggle('fast', function () {
  1508. if ($(this).is(':visible')) {
  1509. $(this).find('input.username').focus();
  1510. }
  1511. });
  1512. },
  1513. searchContacts: function (ev) {
  1514. ev.preventDefault();
  1515. $.getJSON(converse.xhr_user_search_url+ "?q=" + $(ev.target).find('input.username').val(), function (data) {
  1516. var $ul= $('.search-xmpp ul');
  1517. $ul.find('li.found-user').remove();
  1518. $ul.find('li.chat-info').remove();
  1519. if (!data.length) {
  1520. $ul.append('<li class="chat-info">'+__('No users found')+'</li>');
  1521. }
  1522. $(data).each(function (idx, obj) {
  1523. $ul.append(
  1524. $('<li class="found-user"></li>')
  1525. .append(
  1526. $('<a class="subscribe-to-user" href="#" title="'+__('Click to add as a chat contact')+'"></a>')
  1527. .attr('data-recipient', Strophe.escapeNode(obj.id)+'@'+converse.domain)
  1528. .text(obj.fullname)
  1529. )
  1530. );
  1531. });
  1532. });
  1533. },
  1534. addContactFromForm: function (ev) {
  1535. ev.preventDefault();
  1536. var $input = $(ev.target).find('input');
  1537. var jid = $input.val();
  1538. if (! jid) {
  1539. // this is not a valid JID
  1540. $input.addClass('error');
  1541. return;
  1542. }
  1543. this.addContact(jid);
  1544. $('.search-xmpp').hide();
  1545. },
  1546. addContactFromList: function (ev) {
  1547. ev.preventDefault();
  1548. var $target = $(ev.target),
  1549. jid = $target.attr('data-recipient'),
  1550. name = $target.text();
  1551. this.addContact(jid, name);
  1552. $target.parent().remove();
  1553. $('.search-xmpp').hide();
  1554. },
  1555. addContact: function (jid, name) {
  1556. name = _.isEmpty(name)? jid: name;
  1557. converse.connection.roster.add(jid, name, [], function (iq) {
  1558. converse.connection.roster.subscribe(jid, null, converse.xmppstatus.get('fullname'));
  1559. });
  1560. }
  1561. });
  1562. this.RoomsPanel = Backbone.View.extend({
  1563. tagName: 'div',
  1564. className: 'controlbox-pane',
  1565. id: 'chatrooms',
  1566. events: {
  1567. 'submit form.add-chatroom': 'createChatRoom',
  1568. 'click input#show-rooms': 'showRooms',
  1569. 'click a.open-room': 'createChatRoom',
  1570. 'click a.room-info': 'showRoomInfo',
  1571. 'change input[name=server]': 'setDomain',
  1572. 'change input[name=nick]': 'setNick'
  1573. },
  1574. initialize: function (cfg) {
  1575. this.$parent = cfg.$parent;
  1576. this.model.on('change:muc_domain', this.onDomainChange, this);
  1577. this.model.on('change:nick', this.onNickChange, this);
  1578. },
  1579. render: function () {
  1580. this.$parent.append(
  1581. this.$el.html(
  1582. converse.templates.room_panel({
  1583. 'server_input_type': converse.hide_muc_server && 'hidden' || 'text',
  1584. 'label_room_name': __('Room name'),
  1585. 'label_nickname': __('Nickname'),
  1586. 'label_server': __('Server'),
  1587. 'label_join': __('Join'),
  1588. 'label_show_rooms': __('Show rooms')
  1589. })
  1590. ).hide());
  1591. this.$tabs = this.$parent.parent().find('#controlbox-tabs');
  1592. this.$tabs.append(converse.templates.chatrooms_tab({label_rooms: __('Rooms')}));
  1593. return this;
  1594. },
  1595. onDomainChange: function (model) {
  1596. var $server = this.$el.find('input.new-chatroom-server');
  1597. $server.val(model.get('muc_domain'));
  1598. if (converse.auto_list_rooms) {
  1599. this.updateRoomsList();
  1600. }
  1601. },
  1602. onNickChange: function (model) {
  1603. var $nick = this.$el.find('input.new-chatroom-nick');
  1604. $nick.val(model.get('nick'));
  1605. },
  1606. informNoRoomsFound: function () {
  1607. var $available_chatrooms = this.$el.find('#available-chatrooms');
  1608. // # For translators: %1$s is a variable and will be replaced with the XMPP server name
  1609. $available_chatrooms.html('<dt>'+__('No rooms on %1$s',this.model.get('muc_domain'))+'</dt>');
  1610. $('input#show-rooms').show().siblings('span.spinner').remove();
  1611. },
  1612. updateRoomsList: function () {
  1613. converse.connection.muc.listRooms(
  1614. this.model.get('muc_domain'),
  1615. $.proxy(function (iq) { // Success
  1616. var name, jid, i, fragment,
  1617. that = this,
  1618. $available_chatrooms = this.$el.find('#available-chatrooms');
  1619. this.rooms = $(iq).find('query').find('item');
  1620. if (this.rooms.length) {
  1621. // # For translators: %1$s is a variable and will be
  1622. // # replaced with the XMPP server name
  1623. $available_chatrooms.html('<dt>'+__('Rooms on %1$s',this.model.get('muc_domain'))+'</dt>');
  1624. fragment = document.createDocumentFragment();
  1625. for (i=0; i<this.rooms.length; i++) {
  1626. name = Strophe.unescapeNode($(this.rooms[i]).attr('name')||$(this.rooms[i]).attr('jid'));
  1627. jid = $(this.rooms[i]).attr('jid');
  1628. fragment.appendChild($(
  1629. converse.templates.room_item({
  1630. 'name':name,
  1631. 'jid':jid,
  1632. 'open_title': __('Click to open this room'),
  1633. 'info_title': __('Show more information on this room')
  1634. })
  1635. )[0]);
  1636. }
  1637. $available_chatrooms.append(fragment);
  1638. $('input#show-rooms').show().siblings('span.spinner').remove();
  1639. } else {
  1640. this.informNoRoomsFound();
  1641. }
  1642. return true;
  1643. }, this),
  1644. $.proxy(function (iq) { // Failure
  1645. this.informNoRoomsFound();
  1646. }, this));
  1647. },
  1648. showRooms: function (ev) {
  1649. var $available_chatrooms = this.$el.find('#available-chatrooms');
  1650. var $server = this.$el.find('input.new-chatroom-server');
  1651. var server = $server.val();
  1652. if (!server) {
  1653. $server.addClass('error');
  1654. return;
  1655. }
  1656. this.$el.find('input.new-chatroom-name').removeClass('error');
  1657. $server.removeClass('error');
  1658. $available_chatrooms.empty();
  1659. $('input#show-rooms').hide().after('<span class="spinner"/>');
  1660. this.model.save({muc_domain: server});
  1661. this.updateRoomsList();
  1662. },
  1663. showRoomInfo: function (ev) {
  1664. var target = ev.target,
  1665. $dd = $(target).parent('dd'),
  1666. $div = $dd.find('div.room-info');
  1667. if ($div.length) {
  1668. $div.remove();
  1669. } else {
  1670. $dd.find('span.spinner').remove();
  1671. $dd.append('<span class="spinner hor_centered"/>');
  1672. converse.connection.disco.info(
  1673. $(target).attr('data-room-jid'),
  1674. null,
  1675. $.proxy(function (stanza) {
  1676. var $stanza = $(stanza);
  1677. // All MUC features found here: http://xmpp.org/registrar/disco-features.html
  1678. $dd.find('span.spinner').replaceWith(
  1679. converse.templates.room_description({
  1680. 'desc': $stanza.find('field[var="muc#roominfo_description"] value').text(),
  1681. 'occ': $stanza.find('field[var="muc#roominfo_occupants"] value').text(),
  1682. 'hidden': $stanza.find('feature[var="muc_hidden"]').length,
  1683. 'membersonly': $stanza.find('feature[var="muc_membersonly"]').length,
  1684. 'moderated': $stanza.find('feature[var="muc_moderated"]').length,
  1685. 'nonanonymous': $stanza.find('feature[var="muc_nonanonymous"]').length,
  1686. 'open': $stanza.find('feature[var="muc_open"]').length,
  1687. 'passwordprotected': $stanza.find('feature[var="muc_passwordprotected"]').length,
  1688. 'persistent': $stanza.find('feature[var="muc_persistent"]').length,
  1689. 'publicroom': $stanza.find('feature[var="muc_public"]').length,
  1690. 'semianonymous': $stanza.find('feature[var="muc_semianonymous"]').length,
  1691. 'temporary': $stanza.find('feature[var="muc_temporary"]').length,
  1692. 'unmoderated': $stanza.find('feature[var="muc_unmoderated"]').length,
  1693. 'label_desc': __('Description:'),
  1694. 'label_occ': __('Occupants:'),
  1695. 'label_features': __('Features:'),
  1696. 'label_requires_auth': __('Requires authentication'),
  1697. 'label_hidden': __('Hidden'),
  1698. 'label_requires_invite': __('Requires an invitation'),
  1699. 'label_moderated': __('Moderated'),
  1700. 'label_non_anon': __('Non-anonymous'),
  1701. 'label_open_room': __('Open room'),
  1702. 'label_permanent_room': __('Permanent room'),
  1703. 'label_public': __('Public'),
  1704. 'label_semi_anon': _('Semi-anonymous'),
  1705. 'label_temp_room': _('Temporary room'),
  1706. 'label_unmoderated': __('Unmoderated')
  1707. }));
  1708. }, this));
  1709. }
  1710. },
  1711. createChatRoom: function (ev) {
  1712. ev.preventDefault();
  1713. var name, $name,
  1714. server, $server,
  1715. jid,
  1716. $nick = this.$el.find('input.new-chatroom-nick'),
  1717. nick = $nick.val(),
  1718. chatroom;
  1719. if (!nick) { $nick.addClass('error'); }
  1720. else { $nick.removeClass('error'); }
  1721. if (ev.type === 'click') {
  1722. jid = $(ev.target).attr('data-room-jid');
  1723. } else {
  1724. $name = this.$el.find('input.new-chatroom-name');
  1725. $server= this.$el.find('input.new-chatroom-server');
  1726. server = $server.val();
  1727. name = $name.val().trim().toLowerCase();
  1728. $name.val(''); // Clear the input
  1729. if (name && server) {
  1730. jid = Strophe.escapeNode(name) + '@' + server;
  1731. $name.removeClass('error');
  1732. $server.removeClass('error');
  1733. this.model.save({muc_domain: server});
  1734. } else {
  1735. if (!name) { $name.addClass('error'); }
  1736. if (!server) { $server.addClass('error'); }
  1737. return;
  1738. }
  1739. }
  1740. if (!nick) { return; }
  1741. chatroom = converse.chatboxviews.showChat({
  1742. 'id': jid,
  1743. 'jid': jid,
  1744. 'name': Strophe.unescapeNode(Strophe.getNodeFromJid(jid)),
  1745. 'nick': nick,
  1746. 'chatroom': true,
  1747. 'box_id' : b64_sha1(jid)
  1748. });
  1749. },
  1750. setDomain: function (ev) {
  1751. this.model.save({muc_domain: ev.target.value});
  1752. },
  1753. setNick: function (ev) {
  1754. this.model.save({nick: ev.target.value});
  1755. }
  1756. });
  1757. this.ControlBoxView = converse.ChatBoxView.extend({
  1758. tagName: 'div',
  1759. className: 'chatbox',
  1760. id: 'controlbox',
  1761. events: {
  1762. 'click a.close-chatbox-button': 'close',
  1763. 'click ul#controlbox-tabs li a': 'switchTab',
  1764. 'mousedown .dragresize-tm': 'onDragResizeStart'
  1765. },
  1766. initialize: function () {
  1767. this.$el.insertAfter(converse.controlboxtoggle.$el);
  1768. this.model.on('change:connected', this.onConnected, this);
  1769. this.model.on('destroy', this.hide, this);
  1770. this.model.on('hide', this.hide, this);
  1771. this.model.on('show', this.show, this);
  1772. this.model.on('change:closed', this.ensureClosedState, this);
  1773. this.render();
  1774. if (this.model.get('connected')) {
  1775. this.initRoster();
  1776. }
  1777. if (!this.model.get('closed')) {
  1778. this.show();
  1779. } else {
  1780. this.hide();
  1781. }
  1782. },
  1783. giveFeedback: function (message, klass) {
  1784. var $el = this.$('.conn-feedback');
  1785. $el.addClass('conn-feedback').text(message);
  1786. if (klass) {
  1787. $el.addClass(klass);
  1788. }
  1789. },
  1790. onConnected: function () {
  1791. if (this.model.get('connected')) {
  1792. this.render().initRoster();
  1793. converse.features.off('add', this.featureAdded, this);
  1794. converse.features.on('add', this.featureAdded, this);
  1795. // Features could have been added before the controlbox was
  1796. // initialized. Currently we're only interested in MUC
  1797. var feature = converse.features.findWhere({'var': 'http://jabber.org/protocol/muc'});
  1798. if (feature) {
  1799. this.featureAdded(feature);
  1800. }
  1801. }
  1802. },
  1803. initRoster: function () {
  1804. /* We initialize the roster, which will appear inside the
  1805. * Contacts Panel.
  1806. */
  1807. converse.roster = new converse.RosterContacts();
  1808. converse.roster.browserStorage = new Backbone.BrowserStorage[converse.storage](
  1809. b64_sha1('converse.contacts-'+converse.bare_jid));
  1810. var rostergroups = new converse.RosterGroups();
  1811. rostergroups.browserStorage = new Backbone.BrowserStorage[converse.storage](
  1812. b64_sha1('converse.roster.groups'+converse.bare_jid));
  1813. converse.rosterview = new converse.RosterView({model: rostergroups});
  1814. this.contactspanel.$el.append(converse.rosterview.$el);
  1815. converse.rosterview.render().fetch().update();
  1816. return this;
  1817. },
  1818. render: function () {
  1819. if (!converse.connection.connected || !converse.connection.authenticated || converse.connection.disconnecting) {
  1820. // TODO: we might need to take prebinding into consideration here.
  1821. this.renderLoginPanel();
  1822. } else if (!this.contactspanel || !this.contactspanel.$el.is(':visible')) {
  1823. this.renderContactsPanel();
  1824. }
  1825. return this;
  1826. },
  1827. renderLoginPanel: function () {
  1828. var $feedback = this.$('.conn-feedback'); // we want to still show any existing feedback.
  1829. this.$el.html(converse.templates.controlbox(this.model.toJSON()));
  1830. var cfg = {'$parent': this.$el.find('.controlbox-panes'), 'model': this};
  1831. if (!this.loginpanel) {
  1832. this.loginpanel = new converse.LoginPanel(cfg);
  1833. if (converse.allow_registration) {
  1834. this.registerpanel = new converse.RegisterPanel(cfg);
  1835. }
  1836. } else {
  1837. this.loginpanel.delegateEvents().initialize(cfg);
  1838. if (converse.allow_registration) {
  1839. this.registerpanel.delegateEvents().initialize(cfg);
  1840. }
  1841. }
  1842. this.loginpanel.render();
  1843. if (converse.allow_registration) {
  1844. this.registerpanel.render().$el.hide();
  1845. }
  1846. this.initDragResize();
  1847. if ($feedback.length) {
  1848. this.$('.conn-feedback').replaceWith($feedback);
  1849. }
  1850. return this;
  1851. },
  1852. renderContactsPanel: function () {
  1853. var model;
  1854. this.$el.html(converse.templates.controlbox(this.model.toJSON()));
  1855. this.contactspanel = new converse.ContactsPanel({'$parent': this.$el.find('.controlbox-panes')});
  1856. this.contactspanel.render();
  1857. converse.xmppstatusview = new converse.XMPPStatusView({'model': converse.xmppstatus});
  1858. converse.xmppstatusview.render();
  1859. if (converse.allow_muc) {
  1860. this.roomspanel = new converse.RoomsPanel({
  1861. '$parent': this.$el.find('.controlbox-panes'),
  1862. 'model': new (Backbone.Model.extend({
  1863. id: b64_sha1('converse.roomspanel'+converse.bare_jid), // Required by sessionStorage
  1864. browserStorage: new Backbone.BrowserStorage[converse.storage](
  1865. b64_sha1('converse.roomspanel'+converse.bare_jid))
  1866. }))()
  1867. });
  1868. this.roomspanel.render().model.fetch();
  1869. if (!this.roomspanel.model.get('nick')) {
  1870. this.roomspanel.model.save({nick: Strophe.getNodeFromJid(converse.bare_jid)});
  1871. }
  1872. }
  1873. this.initDragResize();
  1874. },
  1875. close: function (ev) {
  1876. if (ev && ev.preventDefault) { ev.preventDefault(); }
  1877. if (converse.connection.connected) {
  1878. this.model.save({'closed': true});
  1879. } else {
  1880. this.model.trigger('hide');
  1881. }
  1882. converse.emit('controlBoxClosed', this);
  1883. return this;
  1884. },
  1885. ensureClosedState: function () {
  1886. if (this.model.get('closed')) {
  1887. this.hide();
  1888. } else {
  1889. this.show();
  1890. }
  1891. },
  1892. hide: function (callback) {
  1893. this.$el.hide('fast', function () {
  1894. converse.refreshWebkit();
  1895. converse.emit('chatBoxClosed', this);
  1896. converse.controlboxtoggle.show(function () {
  1897. if (typeof callback === "function") {
  1898. callback();
  1899. }
  1900. });
  1901. });
  1902. return this;
  1903. },
  1904. show: function () {
  1905. converse.controlboxtoggle.hide($.proxy(function () {
  1906. this.$el.show('fast', function () {
  1907. if (converse.rosterview) {
  1908. converse.rosterview.update();
  1909. }
  1910. converse.refreshWebkit();
  1911. }.bind(this));
  1912. converse.emit('controlBoxOpened', this);
  1913. }, this));
  1914. return this;
  1915. },
  1916. featureAdded: function (feature) {
  1917. if ((feature.get('var') == 'http://jabber.org/protocol/muc') && (converse.allow_muc)) {
  1918. this.roomspanel.model.save({muc_domain: feature.get('from')});
  1919. var $server= this.$el.find('input.new-chatroom-server');
  1920. if (! $server.is(':focus')) {
  1921. $server.val(this.roomspanel.model.get('muc_domain'));
  1922. }
  1923. }
  1924. },
  1925. switchTab: function (ev) {
  1926. // TODO: automatically focus the relevant input
  1927. if (ev && ev.preventDefault) { ev.preventDefault(); }
  1928. var $tab = $(ev.target),
  1929. $sibling = $tab.parent().siblings('li').children('a'),
  1930. $tab_panel = $($tab.attr('href'));
  1931. $($sibling.attr('href')).hide();
  1932. $sibling.removeClass('current');
  1933. $tab.addClass('current');
  1934. $tab_panel.show();
  1935. return this;
  1936. },
  1937. showHelpMessages: function (msgs) {
  1938. // Override showHelpMessages in ChatBoxView, for now do nothing.
  1939. return;
  1940. }
  1941. });
  1942. this.ChatRoomOccupant = Backbone.Model;
  1943. this.ChatRoomOccupantView = Backbone.View.extend({
  1944. tagName: 'li',
  1945. initialize: function () {
  1946. this.model.on('change', this.render, this);
  1947. this.model.on('destroy', this.destroy, this);
  1948. },
  1949. render: function () {
  1950. var $new = converse.templates.occupant(
  1951. _.extend(
  1952. this.model.toJSON(), {
  1953. 'desc_moderator': __('This user is a moderator'),
  1954. 'desc_participant': __('This user can send messages in this room'),
  1955. 'desc_visitor': __('This user can NOT send messages in this room')
  1956. })
  1957. );
  1958. this.$el.replaceWith($new);
  1959. this.setElement($new, true);
  1960. return this;
  1961. },
  1962. destroy: function () {
  1963. this.$el.remove();
  1964. }
  1965. });
  1966. this.ChatRoomOccupants = Backbone.Collection.extend({
  1967. model: converse.ChatRoomOccupant,
  1968. initialize: function (options) {
  1969. this.browserStorage = new Backbone.BrowserStorage[converse.storage](
  1970. b64_sha1('converse.occupants'+converse.bare_jid+options.nick));
  1971. }
  1972. });
  1973. this.ChatRoomOccupantsView = Backbone.Overview.extend({
  1974. tagName: 'div',
  1975. className: 'participants',
  1976. initialize: function () {
  1977. this.model.on("add", this.onOccupantAdded, this);
  1978. },
  1979. render: function () {
  1980. this.$el.html(
  1981. converse.templates.chatroom_sidebar({
  1982. 'label_invitation': __('Invite...'),
  1983. 'label_occupants': __('Occupants')
  1984. })
  1985. );
  1986. return this.initInviteWidget();
  1987. },
  1988. onOccupantAdded: function (item) {
  1989. var view = this.get(item.get('id'));
  1990. if (!view) {
  1991. view = this.add(item.get('id'), new converse.ChatRoomOccupantView({model: item}));
  1992. } else {
  1993. delete view.model; // Remove ref to old model to help garbage collection
  1994. view.model = item;
  1995. view.initialize();
  1996. }
  1997. this.$('.participant-list').append(view.render().$el);
  1998. },
  1999. onChatRoomRoster: function (roster, room) {
  2000. var roster_size = _.size(roster),
  2001. $participant_list = this.$('.participant-list'),
  2002. participants = [],
  2003. keys = _.keys(roster),
  2004. occupant, attrs, i, nick;
  2005. for (i=0; i<roster_size; i++) {
  2006. nick = Strophe.unescapeNode(keys[i]);
  2007. attrs = {
  2008. 'id': nick,
  2009. 'role': roster[keys[i]].role,
  2010. 'nick': nick
  2011. };
  2012. occupant = this.model.get(nick);
  2013. if (occupant) {
  2014. occupant.save(attrs);
  2015. } else {
  2016. this.model.create(attrs);
  2017. }
  2018. }
  2019. _.each(_.difference(this.model.pluck('id'), keys), function (id) {
  2020. this.model.get(id).destroy();
  2021. }, this);
  2022. return true;
  2023. },
  2024. initInviteWidget: function () {
  2025. var $el = this.$('input.invited-contact');
  2026. $el.typeahead({
  2027. minLength: 1,
  2028. highlight: true
  2029. }, {
  2030. name: 'contacts-dataset',
  2031. source: function (q, cb) {
  2032. var results = [];
  2033. _.each(converse.roster.filter(contains(['fullname', 'jid'], q)), function (n) {
  2034. results.push({value: n.get('fullname'), jid: n.get('jid')});
  2035. });
  2036. cb(results);
  2037. },
  2038. templates: {
  2039. suggestion: _.template('<p data-jid="{{jid}}">{{value}}</p>')
  2040. }
  2041. });
  2042. $el.on('typeahead:selected', $.proxy(function (ev, suggestion, dname) {
  2043. var reason = prompt(
  2044. __(___('You are about to invite %1$s to the chat room "%2$s". '), suggestion.value, this.model.get('id')) +
  2045. __("You may optionally include a message, explaining the reason for the invitation.")
  2046. );
  2047. if (reason !== null) {
  2048. converse.connection.muc.rooms[this.chatroomview.model.get('id')].directInvite(suggestion.jid, reason);
  2049. converse.emit('roomInviteSent', this, suggestion.jid, reason);
  2050. }
  2051. $(ev.target).typeahead('val', '');
  2052. }, this));
  2053. return this;
  2054. }
  2055. });
  2056. this.ChatRoomView = converse.ChatBoxView.extend({
  2057. length: 300,
  2058. tagName: 'div',
  2059. className: 'chatroom',
  2060. events: {
  2061. 'click .close-chatbox-button': 'close',
  2062. 'click .toggle-chatbox-button': 'minimize',
  2063. 'click .configure-chatroom-button': 'configureChatRoom',
  2064. 'click .toggle-smiley': 'toggleEmoticonMenu',
  2065. 'click .toggle-smiley ul li': 'insertEmoticon',
  2066. 'click .toggle-clear': 'clearChatRoomMessages',
  2067. 'click .toggle-participants a': 'toggleOccupants',
  2068. 'keypress textarea.chat-textarea': 'keyPressed',
  2069. 'mousedown .dragresize-tm': 'onDragResizeStart'
  2070. },
  2071. is_chatroom: true,
  2072. initialize: function () {
  2073. this.model.messages.on('add', this.onMessageAdded, this);
  2074. this.model.on('change:minimized', function (item) {
  2075. if (item.get('minimized')) {
  2076. this.hide();
  2077. } else {
  2078. this.maximize();
  2079. }
  2080. }, this);
  2081. this.model.on('destroy', function (model, response, options) {
  2082. this.hide();
  2083. converse.connection.muc.leave(
  2084. this.model.get('jid'),
  2085. this.model.get('nick'),
  2086. $.proxy(this.onLeave, this),
  2087. undefined);
  2088. },
  2089. this);
  2090. this.occupantsview = new converse.ChatRoomOccupantsView({
  2091. model: new converse.ChatRoomOccupants({nick: this.model.get('nick')})
  2092. });
  2093. this.occupantsview.chatroomview = this;
  2094. this.render();
  2095. this.occupantsview.model.fetch({add:true});
  2096. this.connect(null);
  2097. converse.emit('chatRoomOpened', this);
  2098. this.$el.insertAfter(converse.chatboxviews.get("controlbox").$el);
  2099. this.model.messages.fetch({add: true});
  2100. if (this.model.get('minimized')) {
  2101. this.hide();
  2102. } else {
  2103. this.show();
  2104. }
  2105. },
  2106. render: function () {
  2107. this.$el.attr('id', this.model.get('box_id'))
  2108. .html(converse.templates.chatroom(this.model.toJSON()));
  2109. this.renderChatArea();
  2110. setTimeout(function () {
  2111. converse.refreshWebkit();
  2112. }, 50);
  2113. return this;
  2114. },
  2115. renderChatArea: function () {
  2116. if (!this.$('.chat-area').length) {
  2117. this.$('.chat-body').empty()
  2118. .append(
  2119. converse.templates.chatarea({
  2120. 'show_toolbar': converse.show_toolbar,
  2121. 'label_message': __('Message')
  2122. }))
  2123. .append(this.occupantsview.render().$el);
  2124. this.renderToolbar();
  2125. }
  2126. // XXX: This is a bit of a hack, to make sure that the
  2127. // sidebar's state is remembered.
  2128. this.model.set({hidden_occupants: !this.model.get('hidden_occupants')});
  2129. this.toggleOccupants();
  2130. return this;
  2131. },
  2132. toggleOccupants: function (ev) {
  2133. if (ev) {
  2134. ev.preventDefault();
  2135. ev.stopPropagation();
  2136. }
  2137. var $el = this.$('.icon-hide-users');
  2138. if (!this.model.get('hidden_occupants')) {
  2139. this.model.save({hidden_occupants: true});
  2140. $el.removeClass('icon-hide-users').addClass('icon-show-users');
  2141. this.$('form.sendXMPPMessage, .chat-area').animate({width: '100%'});
  2142. this.$('div.participants').animate({width: 0}, $.proxy(function () {
  2143. this.scrollDown();
  2144. }, this));
  2145. } else {
  2146. this.model.save({hidden_occupants: false});
  2147. $el.removeClass('icon-show-users').addClass('icon-hide-users');
  2148. this.$('.chat-area, form.sendXMPPMessage').css({width: ''});
  2149. this.$('div.participants').show().animate({width: 'auto'}, $.proxy(function () {
  2150. this.scrollDown();
  2151. }, this));
  2152. }
  2153. },
  2154. onCommandError: function (stanza) {
  2155. this.showStatusNotification(__("Error: could not execute the command"), true);
  2156. },
  2157. createChatRoomMessage: function (text) {
  2158. var fullname = converse.xmppstatus.get('fullname');
  2159. this.model.messages.create({
  2160. fullname: _.isEmpty(fullname)? converse.bare_jid: fullname,
  2161. sender: 'me',
  2162. time: moment().format(),
  2163. message: text,
  2164. msgid: converse.connection.muc.groupchat(this.model.get('jid'), text, undefined, String((new Date()).getTime()))
  2165. });
  2166. },
  2167. sendChatRoomMessage: function (text) {
  2168. var match = text.replace(/^\s*/, "").match(/^\/(.*?)(?: (.*))?$/) || [false], args;
  2169. switch (match[1]) {
  2170. case 'ban':
  2171. args = match[2].splitOnce(' ');
  2172. converse.connection.muc.ban(this.model.get('jid'), args[0], args[1], undefined, $.proxy(this.onCommandError, this));
  2173. break;
  2174. case 'clear':
  2175. this.clearChatRoomMessages();
  2176. break;
  2177. case 'deop':
  2178. args = match[2].splitOnce(' ');
  2179. converse.connection.muc.deop(this.model.get('jid'), args[0], args[1], undefined, $.proxy(this.onCommandError, this));
  2180. break;
  2181. case 'help':
  2182. this.showHelpMessages([
  2183. '<strong>/ban</strong>: ' +__('Ban user from room'),
  2184. '<strong>/clear</strong>: ' +__('Remove messages'),
  2185. '<strong>/help</strong>: ' +__('Show this menu'),
  2186. '<strong>/kick</strong>: ' +__('Kick user from room'),
  2187. '<strong>/me</strong>: ' +__('Write in 3rd person'),
  2188. '<strong>/mute</strong>: ' +__("Remove user's ability to post messages"),
  2189. '<strong>/nick</strong>: ' +__('Change your nickname'),
  2190. '<strong>/topic</strong>: ' +__('Set room topic'),
  2191. '<strong>/voice</strong>: ' +__('Allow muted user to post messages')
  2192. ]);
  2193. break;
  2194. case 'kick':
  2195. args = match[2].splitOnce(' ');
  2196. converse.connection.muc.kick(this.model.get('jid'), args[0], args[1], undefined, $.proxy(this.onCommandError, this));
  2197. break;
  2198. case 'mute':
  2199. args = match[2].splitOnce(' ');
  2200. converse.connection.muc.mute(this.model.get('jid'), args[0], args[1], undefined, $.proxy(this.onCommandError, this));
  2201. break;
  2202. case 'nick':
  2203. converse.connection.muc.changeNick(this.model.get('jid'), match[2]);
  2204. break;
  2205. case 'op':
  2206. args = match[2].splitOnce(' ');
  2207. converse.connection.muc.op(this.model.get('jid'), args[0], args[1], undefined, $.proxy(this.onCommandError, this));
  2208. break;
  2209. case 'topic':
  2210. converse.connection.muc.setTopic(this.model.get('jid'), match[2]);
  2211. break;
  2212. case 'voice':
  2213. args = match[2].splitOnce(' ');
  2214. converse.connection.muc.voice(this.model.get('jid'), args[0], args[1], undefined, $.proxy(this.onCommandError, this));
  2215. break;
  2216. default:
  2217. this.createChatRoomMessage(text);
  2218. break;
  2219. }
  2220. },
  2221. connect: function (password) {
  2222. if (_.has(converse.connection.muc.rooms, this.model.get('jid'))) {
  2223. // If the room exists, it already has event listeners, so we
  2224. // don't add them again.
  2225. converse.connection.muc.join(
  2226. this.model.get('jid'), this.model.get('nick'), null, null, null, password);
  2227. } else {
  2228. converse.connection.muc.join(
  2229. this.model.get('jid'),
  2230. this.model.get('nick'),
  2231. $.proxy(this.onChatRoomMessage, this),
  2232. $.proxy(this.onChatRoomPresence, this),
  2233. $.proxy(this.onChatRoomRoster, this),
  2234. password);
  2235. }
  2236. },
  2237. onLeave: function () {
  2238. this.model.set('connected', false);
  2239. },
  2240. renderConfigurationForm: function (stanza) {
  2241. var $form= this.$el.find('form.chatroom-form'),
  2242. $stanza = $(stanza),
  2243. $fields = $stanza.find('field'),
  2244. title = $stanza.find('title').text(),
  2245. instructions = $stanza.find('instructions').text();
  2246. $form.find('span.spinner').remove();
  2247. $form.append($('<legend>').text(title));
  2248. if (instructions && instructions != title) {
  2249. $form.append($('<p class="instructions">').text(instructions));
  2250. }
  2251. _.each($fields, function (field) {
  2252. $form.append(utils.xForm2webForm($(field), $stanza));
  2253. });
  2254. $form.append('<input type="submit" class="save-submit" value="'+__('Save')+'"/>');
  2255. $form.append('<input type="button" class="cancel-submit" value="'+__('Cancel')+'"/>');
  2256. $form.on('submit', $.proxy(this.saveConfiguration, this));
  2257. $form.find('input[type=button]').on('click', $.proxy(this.cancelConfiguration, this));
  2258. },
  2259. saveConfiguration: function (ev) {
  2260. ev.preventDefault();
  2261. var that = this;
  2262. var $inputs = $(ev.target).find(':input:not([type=button]):not([type=submit])'),
  2263. count = $inputs.length,
  2264. configArray = [];
  2265. $inputs.each(function () {
  2266. configArray.push(utils.webForm2xForm(this));
  2267. if (!--count) {
  2268. converse.connection.muc.saveConfiguration(
  2269. that.model.get('jid'),
  2270. configArray,
  2271. $.proxy(that.onConfigSaved, that),
  2272. $.proxy(that.onErrorConfigSaved, that)
  2273. );
  2274. }
  2275. });
  2276. this.$el.find('div.chatroom-form-container').hide(
  2277. function () {
  2278. $(this).remove();
  2279. that.$el.find('.chat-area').show();
  2280. that.$el.find('.participants').show();
  2281. });
  2282. },
  2283. onConfigSaved: function (stanza) {
  2284. // TODO: provide feedback
  2285. },
  2286. onErrorConfigSaved: function (stanza) {
  2287. this.showStatusNotification(__("An error occurred while trying to save the form."));
  2288. },
  2289. cancelConfiguration: function (ev) {
  2290. ev.preventDefault();
  2291. var that = this;
  2292. this.$el.find('div.chatroom-form-container').hide(
  2293. function () {
  2294. $(this).remove();
  2295. that.$el.find('.chat-area').show();
  2296. that.$el.find('.participants').show();
  2297. });
  2298. },
  2299. configureChatRoom: function (ev) {
  2300. ev.preventDefault();
  2301. if (this.$el.find('div.chatroom-form-container').length) {
  2302. return;
  2303. }
  2304. this.$('.chat-body').children().hide();
  2305. this.$('.chat-body').append(
  2306. $('<div class="chatroom-form-container">'+
  2307. '<form class="chatroom-form">'+
  2308. '<span class="spinner centered"/>'+
  2309. '</form>'+
  2310. '</div>'));
  2311. converse.connection.muc.configure(
  2312. this.model.get('jid'),
  2313. $.proxy(this.renderConfigurationForm, this)
  2314. );
  2315. },
  2316. submitPassword: function (ev) {
  2317. ev.preventDefault();
  2318. var password = this.$el.find('.chatroom-form').find('input[type=password]').val();
  2319. this.$el.find('.chatroom-form-container').replaceWith('<span class="spinner centered"/>');
  2320. this.connect(password);
  2321. },
  2322. renderPasswordForm: function () {
  2323. this.$('.chat-body').children().hide();
  2324. this.$('span.centered.spinner').remove();
  2325. this.$('.chat-body').append(
  2326. converse.templates.chatroom_password_form({
  2327. heading: __('This chatroom requires a password'),
  2328. label_password: __('Password: '),
  2329. label_submit: __('Submit')
  2330. }));
  2331. this.$('.chatroom-form').on('submit', $.proxy(this.submitPassword, this));
  2332. },
  2333. showDisconnectMessage: function (msg) {
  2334. this.$('.chat-area').hide();
  2335. this.$('.participants').hide();
  2336. this.$('span.centered.spinner').remove();
  2337. this.$('.chat-body').append($('<p>'+msg+'</p>'));
  2338. },
  2339. /* http://xmpp.org/extensions/xep-0045.html
  2340. * ----------------------------------------
  2341. * 100 message Entering a room Inform user that any occupant is allowed to see the user's full JID
  2342. * 101 message (out of band) Affiliation change Inform user that his or her affiliation changed while not in the room
  2343. * 102 message Configuration change Inform occupants that room now shows unavailable members
  2344. * 103 message Configuration change Inform occupants that room now does not show unavailable members
  2345. * 104 message Configuration change Inform occupants that a non-privacy-related room configuration change has occurred
  2346. * 110 presence Any room presence Inform user that presence refers to one of its own room occupants
  2347. * 170 message or initial presence Configuration change Inform occupants that room logging is now enabled
  2348. * 171 message Configuration change Inform occupants that room logging is now disabled
  2349. * 172 message Configuration change Inform occupants that the room is now non-anonymous
  2350. * 173 message Configuration change Inform occupants that the room is now semi-anonymous
  2351. * 174 message Configuration change Inform occupants that the room is now fully-anonymous
  2352. * 201 presence Entering a room Inform user that a new room has been created
  2353. * 210 presence Entering a room Inform user that the service has assigned or modified the occupant's roomnick
  2354. * 301 presence Removal from room Inform user that he or she has been banned from the room
  2355. * 303 presence Exiting a room Inform all occupants of new room nickname
  2356. * 307 presence Removal from room Inform user that he or she has been kicked from the room
  2357. * 321 presence Removal from room Inform user that he or she is being removed from the room because of an affiliation change
  2358. * 322 presence Removal from room Inform user that he or she is being removed from the room because the room has been changed to members-only and the user is not a member
  2359. * 332 presence Removal from room Inform user that he or she is being removed from the room because of a system shutdown
  2360. */
  2361. infoMessages: {
  2362. 100: __('This room is not anonymous'),
  2363. 102: __('This room now shows unavailable members'),
  2364. 103: __('This room does not show unavailable members'),
  2365. 104: __('Non-privacy-related room configuration has changed'),
  2366. 170: __('Room logging is now enabled'),
  2367. 171: __('Room logging is now disabled'),
  2368. 172: __('This room is now non-anonymous'),
  2369. 173: __('This room is now semi-anonymous'),
  2370. 174: __('This room is now fully-anonymous'),
  2371. 201: __('A new room has been created')
  2372. },
  2373. disconnectMessages: {
  2374. 301: __('You have been banned from this room'),
  2375. 307: __('You have been kicked from this room'),
  2376. 321: __("You have been removed from this room because of an affiliation change"),
  2377. 322: __("You have been removed from this room because the room has changed to members-only and you're not a member"),
  2378. 332: __("You have been removed from this room because the MUC (Multi-user chat) service is being shut down.")
  2379. },
  2380. actionInfoMessages: {
  2381. /* XXX: Note the triple underscore function and not double
  2382. * underscore.
  2383. *
  2384. * This is a hack. We can't pass the strings to __ because we
  2385. * don't yet know what the variable to interpolate is.
  2386. *
  2387. * Triple underscore will just return the string again, but we
  2388. * can then at least tell gettext to scan for it so that these
  2389. * strings are picked up by the translation machinery.
  2390. */
  2391. 301: ___("<strong>%1$s</strong> has been banned"),
  2392. 303: ___("<strong>%1$s</strong>'s nickname has changed"),
  2393. 307: ___("<strong>%1$s</strong> has been kicked out"),
  2394. 321: ___("<strong>%1$s</strong> has been removed because of an affiliation change"),
  2395. 322: ___("<strong>%1$s</strong> has been removed for not being a member")
  2396. },
  2397. newNicknameMessages: {
  2398. 210: ___('Your nickname has been automatically changed to: <strong>%1$s</strong>'),
  2399. 303: ___('Your nickname has been changed to: <strong>%1$s</strong>')
  2400. },
  2401. showStatusMessages: function ($el, is_self) {
  2402. /* Check for status codes and communicate their purpose to the user.
  2403. * Allow user to configure chat room if they are the owner.
  2404. * See: http://xmpp.org/registrar/mucstatus.html
  2405. */
  2406. var $chat_content,
  2407. disconnect_msgs = [],
  2408. msgs = [],
  2409. reasons = [];
  2410. $el.find('x[xmlns="'+Strophe.NS.MUC_USER+'"]').each($.proxy(function (idx, x) {
  2411. var $item = $(x).find('item');
  2412. if (Strophe.getBareJidFromJid($item.attr('jid')) === converse.bare_jid && $item.attr('affiliation') === 'owner') {
  2413. this.$el.find('a.configure-chatroom-button').show();
  2414. }
  2415. $(x).find('item reason').each(function (idx, reason) {
  2416. if ($(reason).text()) {
  2417. reasons.push($(reason).text());
  2418. }
  2419. });
  2420. $(x).find('status').each($.proxy(function (idx, stat) {
  2421. var code = stat.getAttribute('code');
  2422. if (is_self && _.contains(_.keys(this.newNicknameMessages), code)) {
  2423. this.model.save({'nick': Strophe.getResourceFromJid($el.attr('from'))});
  2424. msgs.push(__(this.newNicknameMessages[code], $item.attr('nick')));
  2425. } else if (is_self && _.contains(_.keys(this.disconnectMessages), code)) {
  2426. disconnect_msgs.push(this.disconnectMessages[code]);
  2427. } else if (!is_self && _.contains(_.keys(this.actionInfoMessages), code)) {
  2428. msgs.push(
  2429. __(this.actionInfoMessages[code], Strophe.unescapeNode(Strophe.getResourceFromJid($el.attr('from'))))
  2430. );
  2431. } else if (_.contains(_.keys(this.infoMessages), code)) {
  2432. msgs.push(this.infoMessages[code]);
  2433. } else if (code !== '110') {
  2434. if ($(stat).text()) {
  2435. msgs.push($(stat).text()); // Sometimes the status contains human readable text and not a code.
  2436. }
  2437. }
  2438. }, this));
  2439. }, this));
  2440. if (disconnect_msgs.length > 0) {
  2441. for (i=0; i<disconnect_msgs.length; i++) {
  2442. this.showDisconnectMessage(disconnect_msgs[i]);
  2443. }
  2444. for (i=0; i<reasons.length; i++) {
  2445. this.showDisconnectMessage(__('The reason given is: "'+reasons[i]+'"'), true);
  2446. }
  2447. this.model.set('connected', false);
  2448. return;
  2449. }
  2450. $chat_content = this.$el.find('.chat-content');
  2451. for (i=0; i<msgs.length; i++) {
  2452. $chat_content.append(converse.templates.info({message: msgs[i]}));
  2453. }
  2454. for (i=0; i<reasons.length; i++) {
  2455. this.showStatusNotification(__('The reason given is: "'+reasons[i]+'"'), true);
  2456. }
  2457. return this.scrollDown();
  2458. },
  2459. showErrorMessage: function ($error, room) {
  2460. // We didn't enter the room, so we must remove it from the MUC
  2461. // add-on
  2462. delete converse.connection.muc[room.name];
  2463. if ($error.attr('type') == 'auth') {
  2464. if ($error.find('not-authorized').length) {
  2465. this.renderPasswordForm();
  2466. } else if ($error.find('registration-required').length) {
  2467. this.showDisconnectMessage(__('You are not on the member list of this room'));
  2468. } else if ($error.find('forbidden').length) {
  2469. this.showDisconnectMessage(__('You have been banned from this room'));
  2470. }
  2471. } else if ($error.attr('type') == 'modify') {
  2472. if ($error.find('jid-malformed').length) {
  2473. this.showDisconnectMessage(__('No nickname was specified'));
  2474. }
  2475. } else if ($error.attr('type') == 'cancel') {
  2476. if ($error.find('not-allowed').length) {
  2477. this.showDisconnectMessage(__('You are not allowed to create new rooms'));
  2478. } else if ($error.find('not-acceptable').length) {
  2479. this.showDisconnectMessage(__("Your nickname doesn't conform to this room's policies"));
  2480. } else if ($error.find('conflict').length) {
  2481. // TODO: give user the option of choosing a different
  2482. // nickname
  2483. this.showDisconnectMessage(__("Your nickname is already taken"));
  2484. } else if ($error.find('item-not-found').length) {
  2485. this.showDisconnectMessage(__("This room does not (yet) exist"));
  2486. } else if ($error.find('service-unavailable').length) {
  2487. this.showDisconnectMessage(__("This room has reached it's maximum number of occupants"));
  2488. }
  2489. }
  2490. },
  2491. onChatRoomPresence: function (presence, room) {
  2492. var $presence = $(presence), is_self;
  2493. if ($presence.attr('type') === 'error') {
  2494. this.model.set('connected', false);
  2495. this.showErrorMessage($presence.find('error'), room);
  2496. } else {
  2497. is_self = ($presence.find("status[code='110']").length) || ($presence.attr('from') == room.name+'/'+Strophe.escapeNode(room.nick));
  2498. if (!this.model.get('conneced')) {
  2499. this.model.set('connected', true);
  2500. this.$('span.centered.spinner').remove();
  2501. this.$el.find('.chat-body').children().show();
  2502. }
  2503. this.showStatusMessages($presence, is_self);
  2504. }
  2505. return true;
  2506. },
  2507. onChatRoomMessage: function (message) {
  2508. var $message = $(message),
  2509. body = $message.children('body').text(),
  2510. jid = $message.attr('from'),
  2511. msgid = $message.attr('id'),
  2512. resource = Strophe.getResourceFromJid(jid),
  2513. sender = resource && Strophe.unescapeNode(resource) || '',
  2514. delayed = $message.find('delay').length > 0,
  2515. subject = $message.children('subject').text();
  2516. if (msgid && this.model.messages.findWhere({msgid: msgid})) {
  2517. return true; // We already have this message stored.
  2518. }
  2519. this.showStatusMessages($message);
  2520. if (subject) {
  2521. this.$el.find('.chatroom-topic').text(subject).attr('title', subject);
  2522. // # For translators: the %1$s and %2$s parts will get replaced by the user and topic text respectively
  2523. // # Example: Topic set by JC Brand to: Hello World!
  2524. this.$el.find('.chat-content').append(
  2525. converse.templates.info({
  2526. 'message': __('Topic set by %1$s to: %2$s', sender, subject)
  2527. }));
  2528. }
  2529. if (sender === '') {
  2530. return true;
  2531. }
  2532. this.model.createMessage($message);
  2533. if (!delayed && sender !== this.model.get('nick') && (new RegExp("\\b"+this.model.get('nick')+"\\b")).test(body)) {
  2534. playNotification();
  2535. }
  2536. if (sender !== this.model.get('nick')) {
  2537. // We only emit an event if it's not our own message
  2538. converse.emit('message', message);
  2539. }
  2540. return true;
  2541. },
  2542. onChatRoomRoster: function (roster, room) {
  2543. return this.occupantsview.onChatRoomRoster(roster, room);
  2544. }
  2545. });
  2546. this.ChatBoxes = Backbone.Collection.extend({
  2547. model: converse.ChatBox,
  2548. comparator: 'time_opened',
  2549. registerMessageHandler: function () {
  2550. converse.connection.addHandler(
  2551. $.proxy(function (message) {
  2552. this.onMessage(message);
  2553. return true;
  2554. }, this), null, 'message', 'chat');
  2555. converse.connection.addHandler(
  2556. $.proxy(function (message) {
  2557. this.onInvite(message);
  2558. return true;
  2559. }, this), 'jabber:x:conference', 'message');
  2560. },
  2561. onConnected: function () {
  2562. this.browserStorage = new Backbone.BrowserStorage[converse.storage](
  2563. b64_sha1('converse.chatboxes-'+converse.bare_jid));
  2564. this.registerMessageHandler();
  2565. this.fetch({
  2566. add: true,
  2567. success: $.proxy(function (collection, resp) {
  2568. if (!_.include(_.pluck(resp, 'id'), 'controlbox')) {
  2569. this.add({
  2570. id: 'controlbox',
  2571. box_id: 'controlbox'
  2572. });
  2573. }
  2574. this.get('controlbox').save({connected:true});
  2575. }, this)
  2576. });
  2577. },
  2578. isOnlyChatStateNotification: function ($msg) {
  2579. // See XEP-0085 Chat State Notification
  2580. return (
  2581. $msg.find('body').length === 0 && (
  2582. $msg.find(ACTIVE).length !== 0 ||
  2583. $msg.find(COMPOSING).length !== 0 ||
  2584. $msg.find(INACTIVE).length !== 0 ||
  2585. $msg.find(PAUSED).length !== 0 ||
  2586. $msg.find(GONE).length !== 0
  2587. )
  2588. );
  2589. },
  2590. onInvite: function (message) {
  2591. var $message = $(message),
  2592. $x = $message.children('x[xmlns="jabber:x:conference"]'),
  2593. from = Strophe.getBareJidFromJid($message.attr('from')),
  2594. room_jid = $x.attr('jid'),
  2595. reason = $x.attr('reason'),
  2596. contact = converse.roster.get(from),
  2597. result;
  2598. if (!reason) {
  2599. result = confirm(
  2600. __(___("%1$s has invited you to join a chat room: %2$s"), contact.get('fullname'), room_jid)
  2601. );
  2602. } else {
  2603. result = confirm(
  2604. __(___('%1$s has invited you to join a chat room: %2$s, and left the following reason: "%3$s"'),
  2605. contact.get('fullname'), room_jid, reason)
  2606. );
  2607. }
  2608. if (result === true) {
  2609. var chatroom = converse.chatboxviews.showChat({
  2610. 'id': room_jid,
  2611. 'jid': room_jid,
  2612. 'name': Strophe.unescapeNode(Strophe.getNodeFromJid(room_jid)),
  2613. 'nick': Strophe.unescapeNode(Strophe.getNodeFromJid(converse.connection.jid)),
  2614. 'chatroom': true,
  2615. 'box_id' : b64_sha1(room_jid),
  2616. 'password': $x.attr('password')
  2617. });
  2618. if (!chatroom.get('connected')) {
  2619. converse.chatboxviews.get(room_jid).connect(null);
  2620. }
  2621. }
  2622. },
  2623. onMessage: function (message) {
  2624. var $message = $(message);
  2625. var contact_jid, $forwarded, $received, $sent,
  2626. msgid = $message.attr('id'),
  2627. chatbox, resource, roster_item,
  2628. message_from = $message.attr('from');
  2629. if (message_from === converse.connection.jid) {
  2630. // FIXME: Forwarded messages should be sent to specific resources,
  2631. // not broadcasted
  2632. return true;
  2633. }
  2634. $forwarded = $message.children('forwarded');
  2635. $received = $message.children('received[xmlns="urn:xmpp:carbons:2"]');
  2636. $sent = $message.children('sent[xmlns="urn:xmpp:carbons:2"]');
  2637. if ($forwarded.length) {
  2638. $message = $forwarded.children('message');
  2639. } else if ($received.length) {
  2640. $message = $received.children('forwarded').children('message');
  2641. message_from = $message.attr('from');
  2642. } else if ($sent.length) {
  2643. $message = $sent.children('forwarded').children('message');
  2644. message_from = $message.attr('from');
  2645. }
  2646. var from = Strophe.getBareJidFromJid(message_from),
  2647. to = Strophe.getBareJidFromJid($message.attr('to'));
  2648. if (from == converse.bare_jid) {
  2649. // I am the sender, so this must be a forwarded message...
  2650. contact_jid = to;
  2651. resource = Strophe.getResourceFromJid($message.attr('to'));
  2652. } else {
  2653. contact_jid = from; // XXX: Should we add toLowerCase here? See ticket #234
  2654. resource = Strophe.getResourceFromJid(message_from);
  2655. }
  2656. roster_item = converse.roster.get(contact_jid);
  2657. if (roster_item === undefined) {
  2658. // The contact was likely removed
  2659. converse.log('Could not get roster item for JID '+contact_jid, 'error');
  2660. return true;
  2661. }
  2662. chatbox = this.get(contact_jid);
  2663. if (!chatbox) {
  2664. var fullname = roster_item.get('fullname');
  2665. fullname = _.isEmpty(fullname)? contact_jid: fullname;
  2666. chatbox = this.create({
  2667. 'id': contact_jid,
  2668. 'jid': contact_jid,
  2669. 'fullname': fullname,
  2670. 'image_type': roster_item.get('image_type'),
  2671. 'image': roster_item.get('image'),
  2672. 'url': roster_item.get('url')
  2673. });
  2674. }
  2675. if (msgid && chatbox.messages.findWhere({msgid: msgid})) {
  2676. // FIXME: There's still a bug here..
  2677. // If a duplicate message is received just after the chat
  2678. // box was closed, then it'll open again (due to it being
  2679. // created here above), with now new messages.
  2680. // The solution is mostly likely to not let chat boxes show
  2681. // automatically when they are created, but to require
  2682. // "show" to be called explicitly.
  2683. return true; // We already have this message stored.
  2684. }
  2685. if (!this.isOnlyChatStateNotification($message) && from !== converse.bare_jid) {
  2686. playNotification();
  2687. }
  2688. chatbox.receiveMessage($message);
  2689. converse.roster.addResource(contact_jid, resource);
  2690. converse.emit('message', message);
  2691. return true;
  2692. }
  2693. });
  2694. this.ChatBoxViews = Backbone.Overview.extend({
  2695. initialize: function () {
  2696. this.model.on("add", this.onChatBoxAdded, this);
  2697. this.model.on("change:minimized", function (item) {
  2698. if (item.get('minimized') === false) {
  2699. this.trimChats(this.get(item.get('id')));
  2700. } else {
  2701. this.trimChats();
  2702. }
  2703. }, this);
  2704. },
  2705. _ensureElement: function () {
  2706. /* Override method from backbone.js
  2707. * If the #conversejs element doesn't exist, create it.
  2708. */
  2709. if (!this.el) {
  2710. var $el = $('#conversejs');
  2711. if (!$el.length) {
  2712. $el = $('<div id="conversejs">');
  2713. $('body').append($el);
  2714. }
  2715. $el.html(converse.templates.chats_panel());
  2716. this.setElement($el, false);
  2717. } else {
  2718. this.setElement(_.result(this, 'el'), false);
  2719. }
  2720. },
  2721. onChatBoxAdded: function (item) {
  2722. var view = this.get(item.get('id'));
  2723. if (!view) {
  2724. if (item.get('chatroom')) {
  2725. view = new converse.ChatRoomView({'model': item});
  2726. } else if (item.get('box_id') === 'controlbox') {
  2727. view = new converse.ControlBoxView({model: item});
  2728. } else {
  2729. view = new converse.ChatBoxView({model: item});
  2730. }
  2731. this.add(item.get('id'), view);
  2732. } else {
  2733. delete view.model; // Remove ref to old model to help garbage collection
  2734. view.model = item;
  2735. view.initialize();
  2736. }
  2737. this.trimChats(view);
  2738. },
  2739. trimChats: function (newchat) {
  2740. /* This method is called when a newly created chat box will
  2741. * be shown.
  2742. *
  2743. * It checks whether there is enough space on the page to show
  2744. * another chat box. Otherwise it minimize the oldest chat box
  2745. * to create space.
  2746. */
  2747. if (converse.no_trimming || (this.model.length <= 1)) {
  2748. return;
  2749. }
  2750. var oldest_chat,
  2751. controlbox_width = 0,
  2752. $minimized = converse.minimized_chats.$el,
  2753. minimized_width = _.contains(this.model.pluck('minimized'), true) ? $minimized.outerWidth(true) : 0,
  2754. boxes_width = newchat ? newchat.$el.outerWidth(true) : 0,
  2755. new_id = newchat ? newchat.model.get('id') : null,
  2756. controlbox = this.get('controlbox');
  2757. if (!controlbox || !controlbox.$el.is(':visible')) {
  2758. controlbox_width = converse.controlboxtoggle.$el.outerWidth(true);
  2759. } else {
  2760. controlbox_width = controlbox.$el.outerWidth(true);
  2761. }
  2762. _.each(this.getAll(), function (view) {
  2763. var id = view.model.get('id');
  2764. if ((id !== 'controlbox') && (id !== new_id) && (!view.model.get('minimized')) && view.$el.is(':visible')) {
  2765. boxes_width += view.$el.outerWidth(true);
  2766. }
  2767. });
  2768. if ((minimized_width + boxes_width + controlbox_width) > this.$el.outerWidth(true)) {
  2769. oldest_chat = this.getOldestMaximizedChat();
  2770. if (oldest_chat) {
  2771. oldest_chat.minimize();
  2772. }
  2773. }
  2774. },
  2775. getOldestMaximizedChat: function () {
  2776. // Get oldest view (which is not controlbox)
  2777. var i = 0;
  2778. var model = this.model.sort().at(i);
  2779. while (model.get('id') === 'controlbox' || model.get('minimized') === true) {
  2780. i++;
  2781. model = this.model.at(i);
  2782. if (!model) {
  2783. return null;
  2784. }
  2785. }
  2786. return model;
  2787. },
  2788. closeAllChatBoxes: function (include_controlbox) {
  2789. var i, chatbox;
  2790. // TODO: once Backbone.Overview has been refactored, we should
  2791. // be able to call .each on the views themselves.
  2792. this.model.each($.proxy(function (model) {
  2793. var id = model.get('id');
  2794. if (include_controlbox || id !== 'controlbox') {
  2795. if (this.get(id)) { // Should always resolve, but shit happens
  2796. this.get(id).close();
  2797. }
  2798. }
  2799. }, this));
  2800. return this;
  2801. },
  2802. showChat: function (attrs) {
  2803. /* Find the chat box and show it.
  2804. * If it doesn't exist, create it.
  2805. */
  2806. var chatbox = this.model.get(attrs.jid);
  2807. if (!chatbox) {
  2808. chatbox = this.model.create(attrs, {
  2809. 'error': function (model, response) {
  2810. converse.log(response.responseText);
  2811. }
  2812. });
  2813. }
  2814. if (chatbox.get('minimized')) {
  2815. chatbox.maximize();
  2816. } else {
  2817. chatbox.trigger('show');
  2818. }
  2819. return chatbox;
  2820. }
  2821. });
  2822. this.MinimizedChatBoxView = Backbone.View.extend({
  2823. tagName: 'div',
  2824. className: 'chat-head',
  2825. events: {
  2826. 'click .close-chatbox-button': 'close',
  2827. 'click .restore-chat': 'restore'
  2828. },
  2829. initialize: function () {
  2830. this.model.messages.on('add', function (m) {
  2831. if (!(m.get('composing') || m.get('paused'))) {
  2832. this.updateUnreadMessagesCounter();
  2833. }
  2834. }, this);
  2835. this.model.on('change:minimized', this.clearUnreadMessagesCounter, this);
  2836. this.model.on('showReceivedOTRMessage', this.updateUnreadMessagesCounter, this);
  2837. this.model.on('showSentOTRMessage', this.updateUnreadMessagesCounter, this);
  2838. },
  2839. render: function () {
  2840. var data = _.extend(
  2841. this.model.toJSON(),
  2842. { 'tooltip': __('Click to restore this chat') }
  2843. );
  2844. if (this.model.get('chatroom')) {
  2845. data.title = this.model.get('name');
  2846. this.$el.addClass('chat-head-chatroom');
  2847. } else {
  2848. data.title = this.model.get('fullname');
  2849. this.$el.addClass('chat-head-chatbox');
  2850. }
  2851. return this.$el.html(converse.templates.trimmed_chat(data));
  2852. },
  2853. clearUnreadMessagesCounter: function () {
  2854. this.model.set({'num_unread': 0});
  2855. this.render();
  2856. },
  2857. updateUnreadMessagesCounter: function () {
  2858. this.model.set({'num_unread': this.model.get('num_unread') + 1});
  2859. this.render();
  2860. },
  2861. close: function (ev) {
  2862. if (ev && ev.preventDefault) { ev.preventDefault(); }
  2863. this.remove();
  2864. this.model.destroy();
  2865. converse.emit('chatBoxClosed', this);
  2866. return this;
  2867. },
  2868. restore: _.debounce(function (ev) {
  2869. if (ev && ev.preventDefault) {
  2870. ev.preventDefault();
  2871. }
  2872. this.model.messages.off('add',null,this);
  2873. this.remove();
  2874. this.model.maximize();
  2875. }, 200, true)
  2876. });
  2877. this.MinimizedChats = Backbone.Overview.extend({
  2878. el: "#minimized-chats",
  2879. events: {
  2880. "click #toggle-minimized-chats": "toggle"
  2881. },
  2882. initialize: function () {
  2883. this.initToggle();
  2884. this.model.on("add", this.onChanged, this);
  2885. this.model.on("destroy", this.removeChat, this);
  2886. this.model.on("change:minimized", this.onChanged, this);
  2887. this.model.on('change:num_unread', this.updateUnreadMessagesCounter, this);
  2888. },
  2889. tearDown: function () {
  2890. this.model.off("add", this.onChanged);
  2891. this.model.off("destroy", this.removeChat);
  2892. this.model.off("change:minimized", this.onChanged);
  2893. this.model.off('change:num_unread', this.updateUnreadMessagesCounter);
  2894. return this;
  2895. },
  2896. initToggle: function () {
  2897. this.toggleview = new converse.MinimizedChatsToggleView({
  2898. model: new converse.MinimizedChatsToggle()
  2899. });
  2900. var id = b64_sha1('converse.minchatstoggle'+converse.bare_jid);
  2901. this.toggleview.model.id = id; // Appears to be necessary for backbone.browserStorage
  2902. this.toggleview.model.browserStorage = new Backbone.BrowserStorage[converse.storage](id);
  2903. this.toggleview.model.fetch();
  2904. },
  2905. render: function () {
  2906. if (this.keys().length === 0) {
  2907. this.$el.hide('fast');
  2908. } else if (this.keys().length === 1) {
  2909. this.$el.show('fast');
  2910. }
  2911. return this.$el;
  2912. },
  2913. toggle: function (ev) {
  2914. if (ev && ev.preventDefault) { ev.preventDefault(); }
  2915. this.toggleview.model.save({'collapsed': !this.toggleview.model.get('collapsed')});
  2916. this.$('.minimized-chats-flyout').toggle();
  2917. },
  2918. onChanged: function (item) {
  2919. if (item.get('id') !== 'controlbox' && item.get('minimized')) {
  2920. this.addChat(item);
  2921. } else if (this.get(item.get('id'))) {
  2922. this.removeChat(item);
  2923. }
  2924. },
  2925. addChat: function (item) {
  2926. var existing = this.get(item.get('id'));
  2927. if (existing && existing.$el.parent().length !== 0) {
  2928. return;
  2929. }
  2930. var view = new converse.MinimizedChatBoxView({model: item});
  2931. this.$('.minimized-chats-flyout').append(view.render());
  2932. this.add(item.get('id'), view);
  2933. this.toggleview.model.set({'num_minimized': this.keys().length});
  2934. this.render();
  2935. },
  2936. removeChat: function (item) {
  2937. this.remove(item.get('id'));
  2938. this.toggleview.model.set({'num_minimized': this.keys().length});
  2939. this.render();
  2940. },
  2941. updateUnreadMessagesCounter: function () {
  2942. var ls = this.model.pluck('num_unread'),
  2943. count = 0, i;
  2944. for (i=0; i<ls.length; i++) { count += ls[i]; }
  2945. this.toggleview.model.set({'num_unread': count});
  2946. this.render();
  2947. }
  2948. });
  2949. this.MinimizedChatsToggle = Backbone.Model.extend({
  2950. initialize: function () {
  2951. this.set({
  2952. 'collapsed': this.get('collapsed') || false,
  2953. 'num_minimized': this.get('num_minimized') || 0,
  2954. 'num_unread': this.get('num_unread') || 0
  2955. });
  2956. }
  2957. });
  2958. this.MinimizedChatsToggleView = Backbone.View.extend({
  2959. el: '#toggle-minimized-chats',
  2960. initialize: function () {
  2961. this.model.on('change:num_minimized', this.render, this);
  2962. this.model.on('change:num_unread', this.render, this);
  2963. this.$flyout = this.$el.siblings('.minimized-chats-flyout');
  2964. },
  2965. render: function () {
  2966. this.$el.html(converse.templates.toggle_chats(
  2967. _.extend(this.model.toJSON(), {
  2968. 'Minimized': __('Minimized')
  2969. })
  2970. ));
  2971. if (this.model.get('collapsed')) {
  2972. this.$flyout.hide();
  2973. } else {
  2974. this.$flyout.show();
  2975. }
  2976. return this.$el;
  2977. }
  2978. });
  2979. this.RosterContact = Backbone.Model.extend({
  2980. initialize: function (attributes, options) {
  2981. var jid = attributes.jid;
  2982. var attrs = _.extend({
  2983. 'id': jid,
  2984. 'fullname': jid,
  2985. 'chat_status': 'offline',
  2986. 'user_id': Strophe.getNodeFromJid(jid),
  2987. 'resources': [],
  2988. 'groups': [],
  2989. 'status': ''
  2990. }, attributes);
  2991. this.set(attrs);
  2992. },
  2993. showInRoster: function () {
  2994. var chatStatus = this.get('chat_status');
  2995. if ((converse.show_only_online_users && chatStatus !== 'online') || (converse.hide_offline_users && chatStatus === 'offline')) {
  2996. // If pending or requesting, show
  2997. if ((this.get('ask') === 'subscribe') || (this.get('subscription') === 'from') || (this.get('requesting') === true)) {
  2998. return true;
  2999. }
  3000. return false;
  3001. }
  3002. return true;
  3003. }
  3004. });
  3005. this.RosterContactView = Backbone.View.extend({
  3006. tagName: 'dd',
  3007. events: {
  3008. "click .accept-xmpp-request": "acceptRequest",
  3009. "click .decline-xmpp-request": "declineRequest",
  3010. "click .open-chat": "openChat",
  3011. "click .remove-xmpp-contact": "removeContact"
  3012. },
  3013. initialize: function () {
  3014. this.model.on("change", this.render, this);
  3015. this.model.on("remove", this.remove, this);
  3016. this.model.on("destroy", this.remove, this);
  3017. this.model.on("open", this.openChat, this);
  3018. },
  3019. render: function () {
  3020. if (!this.model.showInRoster()) {
  3021. this.$el.hide();
  3022. return this;
  3023. } else if (this.$el[0].style.display === "none") {
  3024. this.$el.show();
  3025. }
  3026. var item = this.model,
  3027. ask = item.get('ask'),
  3028. chat_status = item.get('chat_status'),
  3029. requesting = item.get('requesting'),
  3030. subscription = item.get('subscription');
  3031. var classes_to_remove = [
  3032. 'current-xmpp-contact',
  3033. 'pending-xmpp-contact',
  3034. 'requesting-xmpp-contact'
  3035. ].concat(_.keys(STATUSES));
  3036. _.each(classes_to_remove,
  3037. function (cls) {
  3038. if (this.el.className.indexOf(cls) !== -1) {
  3039. this.$el.removeClass(cls);
  3040. }
  3041. }, this);
  3042. this.$el.addClass(chat_status).data('status', chat_status);
  3043. if ((ask === 'subscribe') || (subscription === 'from')) {
  3044. /* ask === 'subscribe'
  3045. * Means we have asked to subscribe to them.
  3046. *
  3047. * subscription === 'from'
  3048. * They are subscribed to use, but not vice versa.
  3049. * We assume that there is a pending subscription
  3050. * from us to them (otherwise we're in a state not
  3051. * supported by converse.js).
  3052. *
  3053. * So in both cases the user is a "pending" contact.
  3054. */
  3055. this.$el.addClass('pending-xmpp-contact');
  3056. this.$el.html(converse.templates.pending_contact(
  3057. _.extend(item.toJSON(), {
  3058. 'desc_remove': __('Click to remove this contact')
  3059. })
  3060. ));
  3061. } else if (requesting === true) {
  3062. this.$el.addClass('requesting-xmpp-contact');
  3063. this.$el.html(converse.templates.requesting_contact(
  3064. _.extend(item.toJSON(), {
  3065. 'desc_accept': __("Click to accept this contact request"),
  3066. 'desc_decline': __("Click to decline this contact request")
  3067. })
  3068. ));
  3069. converse.controlboxtoggle.showControlBox();
  3070. } else if (subscription === 'both' || subscription === 'to') {
  3071. this.$el.addClass('current-xmpp-contact');
  3072. this.$el.html(converse.templates.roster_item(
  3073. _.extend(item.toJSON(), {
  3074. 'desc_status': STATUSES[chat_status||'offline'],
  3075. 'desc_chat': __('Click to chat with this contact'),
  3076. 'desc_remove': __('Click to remove this contact')
  3077. })
  3078. ));
  3079. }
  3080. return this;
  3081. },
  3082. openChat: function (ev) {
  3083. if (ev && ev.preventDefault) { ev.preventDefault(); }
  3084. // XXX: Can this.model.attributes be used here, instead of
  3085. // manually specifying all attributes?
  3086. return converse.chatboxviews.showChat({
  3087. 'id': this.model.get('jid'),
  3088. 'jid': this.model.get('jid'),
  3089. 'fullname': this.model.get('fullname'),
  3090. 'image_type': this.model.get('image_type'),
  3091. 'image': this.model.get('image'),
  3092. 'url': this.model.get('url'),
  3093. 'status': this.model.get('status')
  3094. });
  3095. },
  3096. removeContact: function (ev) {
  3097. if (ev && ev.preventDefault) { ev.preventDefault(); }
  3098. var result = confirm(__("Are you sure you want to remove this contact?"));
  3099. if (result === true) {
  3100. var bare_jid = this.model.get('jid');
  3101. converse.connection.roster.remove(bare_jid, $.proxy(function (iq) {
  3102. converse.connection.roster.unauthorize(bare_jid);
  3103. converse.rosterview.model.remove(bare_jid);
  3104. this.model.destroy();
  3105. this.remove();
  3106. }, this));
  3107. }
  3108. },
  3109. acceptRequest: function (ev) {
  3110. if (ev && ev.preventDefault) { ev.preventDefault(); }
  3111. var jid = this.model.get('jid');
  3112. converse.connection.roster.authorize(jid);
  3113. converse.connection.roster.add(jid, this.model.get('fullname'), [], function (iq) {
  3114. converse.connection.roster.subscribe(jid, null, converse.xmppstatus.get('fullname'));
  3115. });
  3116. },
  3117. declineRequest: function (ev) {
  3118. if (ev && ev.preventDefault) { ev.preventDefault(); }
  3119. var result = confirm(__("Are you sure you want to decline this contact request?"));
  3120. if (result === true) {
  3121. converse.connection.roster.unauthorize(this.model.get('jid'));
  3122. this.model.destroy();
  3123. }
  3124. return this;
  3125. }
  3126. });
  3127. this.RosterContacts = Backbone.Collection.extend({
  3128. model: converse.RosterContact,
  3129. comparator: function (contact1, contact2) {
  3130. var name1, name2;
  3131. var status1 = contact1.get('chat_status') || 'offline';
  3132. var status2 = contact2.get('chat_status') || 'offline';
  3133. if (STATUS_WEIGHTS[status1] === STATUS_WEIGHTS[status2]) {
  3134. name1 = contact1.get('fullname').toLowerCase();
  3135. name2 = contact2.get('fullname').toLowerCase();
  3136. return name1 < name2 ? -1 : (name1 > name2? 1 : 0);
  3137. } else {
  3138. return STATUS_WEIGHTS[status1] < STATUS_WEIGHTS[status2] ? -1 : 1;
  3139. }
  3140. },
  3141. subscribeToSuggestedItems: function (msg) {
  3142. $(msg).find('item').each(function (i, items) {
  3143. var $this = $(this),
  3144. jid = $this.attr('jid'),
  3145. action = $this.attr('action'),
  3146. fullname = $this.attr('name');
  3147. if (action === 'add') {
  3148. converse.connection.roster.subscribe(jid, null, converse.xmppstatus.get('fullname'));
  3149. }
  3150. });
  3151. return true;
  3152. },
  3153. isSelf: function (jid) {
  3154. return (Strophe.getBareJidFromJid(jid) === Strophe.getBareJidFromJid(converse.connection.jid));
  3155. },
  3156. addResource: function (bare_jid, resource) {
  3157. var item = this.get(bare_jid),
  3158. resources;
  3159. if (item) {
  3160. resources = item.get('resources');
  3161. if (resources) {
  3162. if (_.indexOf(resources, resource) == -1) {
  3163. resources.push(resource);
  3164. item.set({'resources': resources});
  3165. }
  3166. } else {
  3167. item.set({'resources': [resource]});
  3168. }
  3169. }
  3170. },
  3171. removeResource: function (bare_jid, resource) {
  3172. var item = this.get(bare_jid),
  3173. resources,
  3174. idx;
  3175. if (item) {
  3176. resources = item.get('resources');
  3177. idx = _.indexOf(resources, resource);
  3178. if (idx !== -1) {
  3179. resources.splice(idx, 1);
  3180. item.save({'resources': resources});
  3181. return resources.length;
  3182. }
  3183. }
  3184. return 0;
  3185. },
  3186. subscribeBack: function (jid) {
  3187. var bare_jid = Strophe.getBareJidFromJid(jid);
  3188. if (converse.connection.roster.findItem(bare_jid)) {
  3189. converse.connection.roster.authorize(bare_jid);
  3190. converse.connection.roster.subscribe(jid, null, converse.xmppstatus.get('fullname'));
  3191. } else {
  3192. converse.connection.roster.add(jid, '', [], function (iq) {
  3193. converse.connection.roster.authorize(bare_jid);
  3194. converse.connection.roster.subscribe(jid, null, converse.xmppstatus.get('fullname'));
  3195. });
  3196. }
  3197. },
  3198. unsubscribe: function (jid) {
  3199. /* Upon receiving the presence stanza of type "unsubscribed",
  3200. * the user SHOULD acknowledge receipt of that subscription state
  3201. * notification by sending a presence stanza of type "unsubscribe"
  3202. * this step lets the user's server know that it MUST no longer
  3203. * send notification of the subscription state change to the user.
  3204. */
  3205. converse.xmppstatus.sendPresence('unsubscribe');
  3206. if (converse.connection.roster.findItem(jid)) {
  3207. converse.connection.roster.remove(jid, function (iq) {
  3208. converse.rosterview.model.remove(jid);
  3209. });
  3210. }
  3211. },
  3212. getNumOnlineContacts: function () {
  3213. var count = 0,
  3214. ignored = ['offline', 'unavailable'],
  3215. models = this.models,
  3216. models_length = models.length,
  3217. i;
  3218. if (converse.show_only_online_users) {
  3219. ignored = _.union(ignored, ['dnd', 'xa', 'away']);
  3220. }
  3221. for (i=0; i<models_length; i++) {
  3222. if (_.indexOf(ignored, models[i].get('chat_status')) === -1) {
  3223. count++;
  3224. }
  3225. }
  3226. return count;
  3227. },
  3228. clearCache: function (items) {
  3229. /* The localstorage cache containing roster contacts might contain
  3230. * some contacts that aren't actually in our roster anymore. We
  3231. * therefore need to remove them now.
  3232. */
  3233. var id, i, contact;
  3234. for (i=0; i < this.models.length; ++i) {
  3235. id = this.models[i].get('id');
  3236. if (_.indexOf(_.pluck(items, 'jid'), id) === -1) {
  3237. contact = this.get(id);
  3238. if (contact && !contact.get('requesting')) {
  3239. contact.destroy();
  3240. }
  3241. }
  3242. }
  3243. },
  3244. // TODO: see if we can only use 2nd item par
  3245. rosterHandler: function (items, item) {
  3246. converse.emit('roster', items);
  3247. this.clearCache(items);
  3248. var new_items = item ? [item] : items;
  3249. _.each(new_items, function (item, index, items) {
  3250. if (this.isSelf(item.jid)) { return; }
  3251. var model = this.get(item.jid);
  3252. if (!model) {
  3253. var is_last = (index === (items.length-1)) ? true : false;
  3254. if ((item.subscription === 'none') && (item.ask === null) && !is_last) {
  3255. // We're not interested in zombies
  3256. // (Hack: except if it's the last item, then we still
  3257. // add it so that the roster will be shown).
  3258. return;
  3259. }
  3260. this.create({
  3261. ask: item.ask,
  3262. fullname: item.name || item.jid,
  3263. groups: item.groups,
  3264. jid: item.jid,
  3265. subscription: item.subscription
  3266. }, {sort: false});
  3267. } else {
  3268. if ((item.subscription === 'none') && (item.ask === null)) {
  3269. // This user is no longer in our roster
  3270. model.destroy();
  3271. } else {
  3272. // We only find out about requesting contacts via the
  3273. // presence handler, so if we receive a contact
  3274. // here, we know they aren't requesting anymore.
  3275. // see docs/DEVELOPER.rst
  3276. model.save({
  3277. subscription: item.subscription,
  3278. ask: item.ask,
  3279. requesting: null,
  3280. groups: item.groups
  3281. });
  3282. }
  3283. }
  3284. }, this);
  3285. if (!converse.initial_presence_sent) {
  3286. /* Once we've sent out our initial presence stanza, we'll
  3287. * start receiving presence stanzas from our contacts.
  3288. * We therefore only want to do this after our roster has
  3289. * been set up (otherwise we can't meaningfully process
  3290. * incoming presence stanzas).
  3291. */
  3292. converse.initial_presence_sent = 1;
  3293. converse.xmppstatus.sendPresence();
  3294. }
  3295. },
  3296. handleIncomingSubscription: function (jid) {
  3297. var bare_jid = Strophe.getBareJidFromJid(jid);
  3298. var item = this.get(bare_jid);
  3299. if (!converse.allow_contact_requests) {
  3300. converse.connection.roster.unauthorize(bare_jid);
  3301. return true;
  3302. }
  3303. if (converse.auto_subscribe) {
  3304. if ((!item) || (item.get('subscription') != 'to')) {
  3305. this.subscribeBack(jid);
  3306. } else {
  3307. converse.connection.roster.authorize(bare_jid);
  3308. }
  3309. } else {
  3310. if ((item) && (item.get('subscription') != 'none')) {
  3311. converse.connection.roster.authorize(bare_jid);
  3312. } else {
  3313. if (!this.get(bare_jid)) {
  3314. converse.getVCard(
  3315. bare_jid,
  3316. $.proxy(function (jid, fullname, img, img_type, url) {
  3317. this.create({
  3318. jid: bare_jid,
  3319. subscription: 'none',
  3320. ask: null,
  3321. requesting: true,
  3322. fullname: fullname || jid,
  3323. image: img,
  3324. image_type: img_type,
  3325. url: url,
  3326. vcard_updated: moment().format()
  3327. });
  3328. }, this),
  3329. $.proxy(function (jid, iq) {
  3330. converse.log("Error while retrieving vcard");
  3331. this.create({
  3332. jid: bare_jid,
  3333. subscription: 'none',
  3334. ask: null,
  3335. requesting: true,
  3336. fullname: bare_jid,
  3337. vcard_updated: moment().format()
  3338. });
  3339. }, this)
  3340. );
  3341. } else {
  3342. return true;
  3343. }
  3344. }
  3345. }
  3346. return true;
  3347. },
  3348. presenceHandler: function (presence) {
  3349. var $presence = $(presence),
  3350. presence_type = $presence.attr('type');
  3351. if (presence_type === 'error') {
  3352. return true;
  3353. }
  3354. var jid = $presence.attr('from'),
  3355. bare_jid = Strophe.getBareJidFromJid(jid),
  3356. resource = Strophe.getResourceFromJid(jid),
  3357. $show = $presence.find('show'),
  3358. chat_status = $show.text() || 'online',
  3359. status_message = $presence.find('status'),
  3360. contact;
  3361. if (this.isSelf(bare_jid)) {
  3362. if ((converse.connection.jid !== jid)&&(presence_type !== 'unavailable')) {
  3363. // Another resource has changed it's status, we'll update ours as well.
  3364. converse.xmppstatus.save({'status': chat_status});
  3365. }
  3366. return true;
  3367. } else if (($presence.find('x').attr('xmlns') || '').indexOf(Strophe.NS.MUC) === 0) {
  3368. return true; // Ignore MUC
  3369. }
  3370. contact = this.get(bare_jid);
  3371. if (contact && (status_message.text() != contact.get('status'))) {
  3372. contact.save({'status': status_message.text()});
  3373. }
  3374. if ((presence_type === 'subscribed') || (presence_type === 'unsubscribe')) {
  3375. return true;
  3376. } else if (presence_type === 'subscribe') {
  3377. return this.handleIncomingSubscription(jid);
  3378. } else if (presence_type === 'unsubscribed') {
  3379. this.unsubscribe(bare_jid);
  3380. } else if (presence_type === 'unavailable') {
  3381. if (this.removeResource(bare_jid, resource) === 0) {
  3382. chat_status = "offline";
  3383. }
  3384. if (contact && chat_status) {
  3385. contact.save({'chat_status': chat_status});
  3386. }
  3387. } else if (contact) {
  3388. // presence_type is undefined
  3389. this.addResource(bare_jid, resource);
  3390. contact.save({'chat_status': chat_status});
  3391. }
  3392. return true;
  3393. }
  3394. });
  3395. this.RosterGroup = Backbone.Model.extend({
  3396. initialize: function (attributes, options) {
  3397. this.set(_.extend({
  3398. description: DESC_GROUP_TOGGLE,
  3399. state: OPENED
  3400. }, attributes));
  3401. // Collection of contacts belonging to this group.
  3402. this.contacts = new converse.RosterContacts();
  3403. }
  3404. });
  3405. this.RosterGroupView = Backbone.Overview.extend({
  3406. tagName: 'dt',
  3407. className: 'roster-group',
  3408. events: {
  3409. "click a.group-toggle": "toggle"
  3410. },
  3411. initialize: function () {
  3412. this.model.contacts.on("add", this.addContact, this);
  3413. this.model.contacts.on("change:subscription", this.onContactSubscriptionChange, this);
  3414. this.model.contacts.on("change:requesting", this.onContactRequestChange, this);
  3415. this.model.contacts.on("change:chat_status", function (contact) {
  3416. // This might be optimized by instead of first sorting,
  3417. // finding the correct position in positionContact
  3418. this.model.contacts.sort();
  3419. this.positionContact(contact).render();
  3420. }, this);
  3421. this.model.contacts.on("destroy", this.onRemove, this);
  3422. this.model.contacts.on("remove", this.onRemove, this);
  3423. converse.roster.on('change:groups', this.onContactGroupChange, this);
  3424. },
  3425. render: function () {
  3426. this.$el.attr('data-group', this.model.get('name'));
  3427. this.$el.html(
  3428. $(converse.templates.group_header({
  3429. label_group: this.model.get('name'),
  3430. desc_group_toggle: this.model.get('description'),
  3431. toggle_state: this.model.get('state')
  3432. }))
  3433. );
  3434. return this;
  3435. },
  3436. addContact: function (contact) {
  3437. var view = new converse.RosterContactView({model: contact});
  3438. this.add(contact.get('id'), view);
  3439. view = this.positionContact(contact).render();
  3440. if (contact.showInRoster()) {
  3441. if (this.model.get('state') === CLOSED) {
  3442. if (view.$el[0].style.display !== "none") { view.$el.hide(); }
  3443. if (this.$el[0].style.display === "none") { this.$el.show(); }
  3444. } else {
  3445. if (this.$el[0].style.display !== "block") { this.show(); }
  3446. }
  3447. }
  3448. },
  3449. positionContact: function (contact) {
  3450. /* Place the contact's DOM element in the correct alphabetical
  3451. * position amongst the other contacts in this group.
  3452. */
  3453. var view = this.get(contact.get('id'));
  3454. var index = this.model.contacts.indexOf(contact);
  3455. view.$el.detach();
  3456. if (index === 0) {
  3457. this.$el.after(view.$el);
  3458. } else if (index == (this.model.contacts.length-1)) {
  3459. this.$el.nextUntil('dt').last().after(view.$el);
  3460. } else {
  3461. this.$el.nextUntil('dt').eq(index).before(view.$el);
  3462. }
  3463. return view;
  3464. },
  3465. show: function () {
  3466. // FIXME: There's a bug here, if show_only_online_users is true
  3467. // Possible solution, get the group, call _.each and check
  3468. // showInRoster
  3469. this.$el.nextUntil('dt').addBack().show();
  3470. },
  3471. hide: function () {
  3472. this.$el.nextUntil('dt').addBack().hide();
  3473. },
  3474. filter: function (q) {
  3475. /* Filter the group's contacts based on the query "q".
  3476. * The query is matched against the contact's full name.
  3477. * If all contacts are filtered out (i.e. hidden), then the
  3478. * group must be filtered out as well.
  3479. */
  3480. var matches, rejects;
  3481. if (q.length === 0) {
  3482. if (this.model.get('state') === OPENED) {
  3483. this.model.contacts.each($.proxy(function (item) {
  3484. if (item.showInRoster()) {
  3485. this.get(item.get('id')).$el.show();
  3486. }
  3487. }, this));
  3488. }
  3489. this.showIfNecessary();
  3490. } else {
  3491. q = q.toLowerCase();
  3492. matches = this.model.contacts.filter(contains.not('fullname', q));
  3493. if (matches.length === this.model.contacts.length) { // hide the whole group
  3494. this.hide();
  3495. } else {
  3496. _.each(matches, $.proxy(function (item) {
  3497. this.get(item.get('id')).$el.hide();
  3498. }, this));
  3499. _.each(this.model.contacts.reject(contains.not('fullname', q)), $.proxy(function (item) {
  3500. this.get(item.get('id')).$el.show();
  3501. }, this));
  3502. this.showIfNecessary();
  3503. }
  3504. }
  3505. },
  3506. showIfNecessary: function () {
  3507. if (!this.$el.is(':visible') && this.model.contacts.length > 0) {
  3508. this.$el.show();
  3509. }
  3510. },
  3511. toggle: function (ev) {
  3512. if (ev && ev.preventDefault) { ev.preventDefault(); }
  3513. var $el = $(ev.target);
  3514. if ($el.hasClass("icon-opened")) {
  3515. this.$el.nextUntil('dt').slideUp();
  3516. this.model.save({state: CLOSED});
  3517. $el.removeClass("icon-opened").addClass("icon-closed");
  3518. } else {
  3519. $el.removeClass("icon-closed").addClass("icon-opened");
  3520. this.model.save({state: OPENED});
  3521. this.filter(
  3522. converse.rosterview.$('.roster-filter').val(),
  3523. converse.rosterview.$('.filter-type').val()
  3524. );
  3525. }
  3526. },
  3527. onContactGroupChange: function (contact) {
  3528. var in_this_group = _.contains(contact.get('groups'), this.model.get('name'));
  3529. var cid = contact.get('id');
  3530. var in_this_overview = !this.get(cid);
  3531. if (in_this_group && !in_this_overview) {
  3532. this.model.contacts.remove(cid);
  3533. } else if (!in_this_group && in_this_overview) {
  3534. this.addContact(contact);
  3535. }
  3536. },
  3537. onContactSubscriptionChange: function (contact) {
  3538. if ((this.model.get('name') === HEADER_PENDING_CONTACTS) && contact.get('subscription') !== 'from') {
  3539. this.model.contacts.remove(contact.get('id'));
  3540. }
  3541. },
  3542. onContactRequestChange: function (contact) {
  3543. if ((this.model.get('name') === HEADER_REQUESTING_CONTACTS) && !contact.get('requesting')) {
  3544. this.model.contacts.remove(contact.get('id'));
  3545. }
  3546. },
  3547. onRemove: function (contact) {
  3548. this.remove(contact.get('id'));
  3549. if (this.model.contacts.length === 0) {
  3550. this.$el.hide();
  3551. }
  3552. }
  3553. });
  3554. this.RosterGroups = Backbone.Collection.extend({
  3555. model: converse.RosterGroup,
  3556. comparator: function (a, b) {
  3557. /* Groups are sorted alphabetically, ignoring case.
  3558. * However, Ungrouped, Requesting Contacts and Pending Contacts
  3559. * appear last and in that order. */
  3560. a = a.get('name');
  3561. b = b.get('name');
  3562. var special_groups = _.keys(HEADER_WEIGHTS);
  3563. var a_is_special = _.contains(special_groups, a);
  3564. var b_is_special = _.contains(special_groups, b);
  3565. if (!a_is_special && !b_is_special ) {
  3566. return a.toLowerCase() < b.toLowerCase() ? -1 : (a.toLowerCase() > b.toLowerCase() ? 1 : 0);
  3567. } else if (a_is_special && b_is_special) {
  3568. return HEADER_WEIGHTS[a] < HEADER_WEIGHTS[b] ? -1 : (HEADER_WEIGHTS[a] > HEADER_WEIGHTS[b] ? 1 : 0);
  3569. } else if (!a_is_special && b_is_special) {
  3570. return (b === HEADER_CURRENT_CONTACTS) ? 1 : -1;
  3571. } else if (a_is_special && !b_is_special) {
  3572. return (a === HEADER_CURRENT_CONTACTS) ? -1 : 1;
  3573. }
  3574. }
  3575. });
  3576. this.RosterView = Backbone.Overview.extend({
  3577. tagName: 'div',
  3578. id: 'converse-roster',
  3579. events: {
  3580. "keydown .roster-filter": "liveFilter",
  3581. "click .onX": "clearFilter",
  3582. "mousemove .x": "togglePointer",
  3583. "change .filter-type": "changeFilterType"
  3584. },
  3585. initialize: function () {
  3586. this.registerRosterHandler();
  3587. this.registerRosterXHandler();
  3588. this.registerPresenceHandler();
  3589. converse.roster.on("add", this.onContactAdd, this);
  3590. converse.roster.on('change', this.onContactChange, this);
  3591. converse.roster.on("destroy", this.update, this);
  3592. converse.roster.on("remove", this.update, this);
  3593. this.model.on("add", this.onGroupAdd, this);
  3594. this.model.on("reset", this.reset, this);
  3595. this.$roster = $('<dl class="roster-contacts" style="display: none;"></dl>');
  3596. },
  3597. update: _.debounce(function () {
  3598. var $count = $('#online-count');
  3599. $count.text('('+converse.roster.getNumOnlineContacts()+')');
  3600. if (!$count.is(':visible')) {
  3601. $count.show();
  3602. }
  3603. if (this.$roster.parent().length === 0) {
  3604. this.$el.append(this.$roster.show());
  3605. }
  3606. return this.showHideFilter();
  3607. }, converse.animate ? 100 : 0),
  3608. render: function () {
  3609. this.$el.html(converse.templates.roster({
  3610. placeholder: __('Type to filter'),
  3611. label_contacts: LABEL_CONTACTS,
  3612. label_groups: LABEL_GROUPS
  3613. }));
  3614. return this;
  3615. },
  3616. fetch: function () {
  3617. this.model.fetch({
  3618. silent: true, // We use the success handler to handle groups that were added,
  3619. // we need to first have all groups before positionFetchedGroups
  3620. // will work properly.
  3621. success: $.proxy(function (collection, resp, options) {
  3622. if (collection.length !== 0) {
  3623. this.positionFetchedGroups(collection, resp, options);
  3624. }
  3625. converse.roster.fetch({
  3626. add: true,
  3627. success: function (collection) {
  3628. // XXX: Bit of a hack.
  3629. // strophe.roster expects .get to be called for
  3630. // every page load so that its "items" attr
  3631. // gets populated.
  3632. // This is very inefficient for large rosters,
  3633. // and we already have the roster cached in
  3634. // sessionStorage.
  3635. // Therefore we manually populate the "items"
  3636. // attr.
  3637. // Ideally we should eventually replace
  3638. // strophe.roster with something better.
  3639. if (collection.length > 0) {
  3640. collection.each(function (item) {
  3641. converse.connection.roster.items.push({
  3642. name : item.get('fullname'),
  3643. jid : item.get('jid'),
  3644. subscription : item.get('subscription'),
  3645. ask : item.get('ask'),
  3646. groups : item.get('groups'),
  3647. resources : item.get('resources')
  3648. });
  3649. });
  3650. converse.initial_presence_sent = 1;
  3651. converse.xmppstatus.sendPresence();
  3652. } else {
  3653. converse.connection.roster.get();
  3654. }
  3655. }
  3656. });
  3657. }, this)
  3658. });
  3659. return this;
  3660. },
  3661. changeFilterType: function (ev) {
  3662. if (ev && ev.preventDefault) { ev.preventDefault(); }
  3663. this.clearFilter();
  3664. this.filter(
  3665. this.$('.roster-filter').val(),
  3666. ev.target.value
  3667. );
  3668. },
  3669. tog: function (v) {
  3670. return v?'addClass':'removeClass';
  3671. },
  3672. togglePointer: function (ev) {
  3673. if (ev && ev.preventDefault) { ev.preventDefault(); }
  3674. var el = ev.target;
  3675. $(el)[this.tog(el.offsetWidth-18 < ev.clientX-el.getBoundingClientRect().left)]('onX');
  3676. },
  3677. filter: function (query, type) {
  3678. var matches;
  3679. query = query.toLowerCase();
  3680. if (type === 'groups') {
  3681. _.each(this.getAll(), function (view, idx) {
  3682. if (view.model.get('name').toLowerCase().indexOf(query.toLowerCase()) === -1) {
  3683. view.hide();
  3684. } else if (view.model.contacts.length > 0) {
  3685. view.show();
  3686. }
  3687. });
  3688. } else {
  3689. _.each(this.getAll(), function (view) {
  3690. view.filter(query, type);
  3691. });
  3692. }
  3693. },
  3694. liveFilter: _.debounce(function (ev) {
  3695. if (ev && ev.preventDefault) { ev.preventDefault(); }
  3696. var $filter = this.$('.roster-filter');
  3697. var q = $filter.val();
  3698. var t = this.$('.filter-type').val();
  3699. $filter[this.tog(q)]('x');
  3700. this.filter(q, t);
  3701. }, 300),
  3702. clearFilter: function (ev) {
  3703. if (ev && ev.preventDefault) {
  3704. ev.preventDefault();
  3705. $(ev.target).removeClass('x onX').val('');
  3706. }
  3707. this.filter('');
  3708. },
  3709. showHideFilter: function () {
  3710. if (!this.$el.is(':visible')) {
  3711. return;
  3712. }
  3713. var $filter = this.$('.roster-filter');
  3714. var $type = this.$('.filter-type');
  3715. var visible = $filter.is(':visible');
  3716. if (visible && $filter.val().length > 0) {
  3717. // Don't hide if user is currently filtering.
  3718. return;
  3719. }
  3720. if (this.$roster.hasScrollBar()) {
  3721. if (!visible) {
  3722. $filter.show();
  3723. $type.show();
  3724. }
  3725. } else {
  3726. $filter.hide();
  3727. $type.hide();
  3728. }
  3729. return this;
  3730. },
  3731. reset: function () {
  3732. converse.roster.reset();
  3733. this.removeAll();
  3734. this.$roster = $('<dl class="roster-contacts" style="display: none;"></dl>');
  3735. this.render().update();
  3736. return this;
  3737. },
  3738. registerRosterHandler: function () {
  3739. // Register handlers that depend on the roster
  3740. converse.connection.roster.registerCallback(
  3741. $.proxy(converse.roster.rosterHandler, converse.roster)
  3742. );
  3743. },
  3744. registerRosterXHandler: function () {
  3745. var t = 0;
  3746. converse.connection.addHandler(
  3747. function (msg) {
  3748. window.setTimeout(
  3749. function () {
  3750. converse.connection.flush();
  3751. $.proxy(converse.roster.subscribeToSuggestedItems, converse.roster)(msg);
  3752. },
  3753. t
  3754. );
  3755. t += $(msg).find('item').length*250;
  3756. return true;
  3757. },
  3758. 'http://jabber.org/protocol/rosterx', 'message', null);
  3759. },
  3760. registerPresenceHandler: function () {
  3761. converse.connection.addHandler(
  3762. $.proxy(function (presence) {
  3763. converse.roster.presenceHandler(presence);
  3764. return true;
  3765. }, this), null, 'presence', null);
  3766. },
  3767. onGroupAdd: function (group) {
  3768. var view = new converse.RosterGroupView({model: group});
  3769. this.add(group.get('name'), view.render());
  3770. this.positionGroup(view);
  3771. },
  3772. onContactAdd: function (contact) {
  3773. this.addRosterContact(contact).update();
  3774. if (!contact.get('vcard_updated')) {
  3775. // This will update the vcard, which triggers a change
  3776. // request which will rerender the roster contact.
  3777. converse.getVCard(contact.get('jid'));
  3778. }
  3779. },
  3780. onContactChange: function (contact) {
  3781. this.updateChatBox(contact).update();
  3782. if (_.has(contact.changed, 'subscription')) {
  3783. if (contact.changed.subscription == 'from') {
  3784. this.addContactToGroup(contact, HEADER_PENDING_CONTACTS);
  3785. } else if (contact.get('subscription') === 'both') {
  3786. this.addExistingContact(contact);
  3787. }
  3788. }
  3789. if (_.has(contact.changed, 'ask') && contact.changed.ask == 'subscribe') {
  3790. this.addContactToGroup(contact, HEADER_PENDING_CONTACTS);
  3791. }
  3792. if (_.has(contact.changed, 'subscription') && contact.changed.requesting == 'true') {
  3793. this.addContactToGroup(contact, HEADER_REQUESTING_CONTACTS);
  3794. }
  3795. this.liveFilter();
  3796. },
  3797. updateChatBox: function (contact) {
  3798. var chatbox = converse.chatboxes.get(contact.get('jid')),
  3799. changes = {};
  3800. if (!chatbox) {
  3801. return this;
  3802. }
  3803. if (_.has(contact.changed, 'chat_status')) {
  3804. changes.chat_status = contact.get('chat_status');
  3805. }
  3806. if (_.has(contact.changed, 'status')) {
  3807. changes.status = contact.get('status');
  3808. }
  3809. chatbox.save(changes);
  3810. return this;
  3811. },
  3812. positionFetchedGroups: function (model, resp, options) {
  3813. /* Instead of throwing an add event for each group
  3814. * fetched, we wait until they're all fetched and then
  3815. * we position them.
  3816. * Works around the problem of positionGroup not
  3817. * working when all groups besides the one being
  3818. * positioned aren't already in inserted into the
  3819. * roster DOM element.
  3820. */
  3821. model.sort();
  3822. model.each($.proxy(function (group, idx) {
  3823. var view = this.get(group.get('name'));
  3824. if (!view) {
  3825. view = new converse.RosterGroupView({model: group});
  3826. this.add(group.get('name'), view.render());
  3827. }
  3828. if (idx === 0) {
  3829. this.$roster.append(view.$el);
  3830. } else {
  3831. this.appendGroup(view);
  3832. }
  3833. }, this));
  3834. },
  3835. positionGroup: function (view) {
  3836. /* Place the group's DOM element in the correct alphabetical
  3837. * position amongst the other groups in the roster.
  3838. */
  3839. var $groups = this.$roster.find('.roster-group'),
  3840. index = $groups.length ? this.model.indexOf(view.model) : 0;
  3841. if (index === 0) {
  3842. this.$roster.prepend(view.$el);
  3843. } else if (index == (this.model.length-1)) {
  3844. this.appendGroup(view);
  3845. } else {
  3846. $($groups.eq(index)).before(view.$el);
  3847. }
  3848. return this;
  3849. },
  3850. appendGroup: function (view) {
  3851. /* Add the group at the bottom of the roster
  3852. */
  3853. var $last = this.$roster.find('.roster-group').last();
  3854. var $siblings = $last.siblings('dd');
  3855. if ($siblings.length > 0) {
  3856. $siblings.last().after(view.$el);
  3857. } else {
  3858. $last.after(view.$el);
  3859. }
  3860. return this;
  3861. },
  3862. getGroup: function (name) {
  3863. /* Returns the group as specified by name.
  3864. * Creates the group if it doesn't exist.
  3865. */
  3866. var view = this.get(name);
  3867. if (view) {
  3868. return view.model;
  3869. }
  3870. return this.model.create({name: name, id: b64_sha1(name)});
  3871. },
  3872. addContactToGroup: function (contact, name) {
  3873. this.getGroup(name).contacts.add(contact);
  3874. },
  3875. addExistingContact: function (contact) {
  3876. var groups;
  3877. if (converse.roster_groups) {
  3878. groups = contact.get('groups');
  3879. if (groups.length === 0) {
  3880. groups = [HEADER_UNGROUPED];
  3881. }
  3882. } else {
  3883. groups = [HEADER_CURRENT_CONTACTS];
  3884. }
  3885. _.each(groups, $.proxy(function (name) {
  3886. this.addContactToGroup(contact, name);
  3887. }, this));
  3888. },
  3889. addRosterContact: function (contact) {
  3890. if (contact.get('subscription') === 'both' || contact.get('subscription') === 'to') {
  3891. this.addExistingContact(contact);
  3892. } else {
  3893. if ((contact.get('ask') === 'subscribe') || (contact.get('subscription') === 'from')) {
  3894. this.addContactToGroup(contact, HEADER_PENDING_CONTACTS);
  3895. } else if (contact.get('requesting') === true) {
  3896. this.addContactToGroup(contact, HEADER_REQUESTING_CONTACTS);
  3897. }
  3898. }
  3899. return this;
  3900. }
  3901. });
  3902. this.XMPPStatus = Backbone.Model.extend({
  3903. initialize: function () {
  3904. this.set({
  3905. 'status' : this.get('status') || 'online'
  3906. });
  3907. this.on('change', $.proxy(function (item) {
  3908. if (this.get('fullname') === undefined) {
  3909. converse.getVCard(
  3910. null, // No 'to' attr when getting one's own vCard
  3911. $.proxy(function (jid, fullname, image, image_type, url) {
  3912. this.save({'fullname': fullname});
  3913. }, this)
  3914. );
  3915. }
  3916. if (_.has(item.changed, 'status')) {
  3917. converse.emit('statusChanged', this.get('status'));
  3918. }
  3919. if (_.has(item.changed, 'status_message')) {
  3920. converse.emit('statusMessageChanged', this.get('status_message'));
  3921. }
  3922. }, this));
  3923. },
  3924. sendPresence: function (type) {
  3925. if (type === undefined) {
  3926. type = this.get('status') || 'online';
  3927. }
  3928. var status_message = this.get('status_message'),
  3929. presence;
  3930. // Most of these presence types are actually not explicitly sent,
  3931. // but I add all of them here fore reference and future proofing.
  3932. if ((type === 'unavailable') ||
  3933. (type === 'probe') ||
  3934. (type === 'error') ||
  3935. (type === 'unsubscribe') ||
  3936. (type === 'unsubscribed') ||
  3937. (type === 'subscribe') ||
  3938. (type === 'subscribed')) {
  3939. presence = $pres({'type': type});
  3940. } else if (type === 'offline') {
  3941. presence = $pres({'type': 'unavailable'});
  3942. if (status_message) {
  3943. presence.c('show').t(type);
  3944. }
  3945. } else {
  3946. if (type === 'online') {
  3947. presence = $pres();
  3948. } else {
  3949. presence = $pres().c('show').t(type).up();
  3950. }
  3951. if (status_message) {
  3952. presence.c('status').t(status_message);
  3953. }
  3954. }
  3955. converse.connection.send(presence);
  3956. },
  3957. setStatus: function (value) {
  3958. this.sendPresence(value);
  3959. this.save({'status': value});
  3960. },
  3961. setStatusMessage: function (status_message) {
  3962. converse.connection.send($pres().c('show').t(this.get('status')).up().c('status').t(status_message));
  3963. this.save({'status_message': status_message});
  3964. if (this.xhr_custom_status) {
  3965. $.ajax({
  3966. url: this.xhr_custom_status_url,
  3967. type: 'POST',
  3968. data: {'msg': status_message}
  3969. });
  3970. }
  3971. }
  3972. });
  3973. this.XMPPStatusView = Backbone.View.extend({
  3974. el: "span#xmpp-status-holder",
  3975. events: {
  3976. "click a.choose-xmpp-status": "toggleOptions",
  3977. "click #fancy-xmpp-status-select a.change-xmpp-status-message": "renderStatusChangeForm",
  3978. "submit #set-custom-xmpp-status": "setStatusMessage",
  3979. "click .dropdown dd ul li a": "setStatus"
  3980. },
  3981. initialize: function () {
  3982. this.model.on("change", this.updateStatusUI, this);
  3983. },
  3984. render: function () {
  3985. // Replace the default dropdown with something nicer
  3986. var $select = this.$el.find('select#select-xmpp-status'),
  3987. chat_status = this.model.get('status') || 'offline',
  3988. options = $('option', $select),
  3989. $options_target,
  3990. options_list = [],
  3991. that = this;
  3992. this.$el.html(converse.templates.choose_status());
  3993. this.$el.find('#fancy-xmpp-status-select')
  3994. .html(converse.templates.chat_status({
  3995. 'status_message': this.model.get('status_message') || __("I am %1$s", this.getPrettyStatus(chat_status)),
  3996. 'chat_status': chat_status,
  3997. 'desc_custom_status': __('Click here to write a custom status message'),
  3998. 'desc_change_status': __('Click to change your chat status')
  3999. }));
  4000. // iterate through all the <option> elements and add option values
  4001. options.each(function (){
  4002. options_list.push(converse.templates.status_option({
  4003. 'value': $(this).val(),
  4004. 'text': this.text
  4005. }));
  4006. });
  4007. $options_target = this.$el.find("#target dd ul").hide();
  4008. $options_target.append(options_list.join(''));
  4009. $select.remove();
  4010. return this;
  4011. },
  4012. toggleOptions: function (ev) {
  4013. ev.preventDefault();
  4014. $(ev.target).parent().parent().siblings('dd').find('ul').toggle('fast');
  4015. },
  4016. renderStatusChangeForm: function (ev) {
  4017. ev.preventDefault();
  4018. var status_message = this.model.get('status') || 'offline';
  4019. var input = converse.templates.change_status_message({
  4020. 'status_message': status_message,
  4021. 'label_custom_status': __('Custom status'),
  4022. 'label_save': __('Save')
  4023. });
  4024. this.$el.find('.xmpp-status').replaceWith(input);
  4025. this.$el.find('.custom-xmpp-status').focus().focus();
  4026. },
  4027. setStatusMessage: function (ev) {
  4028. ev.preventDefault();
  4029. var status_message = $(ev.target).find('input').val();
  4030. this.model.setStatusMessage(status_message);
  4031. },
  4032. setStatus: function (ev) {
  4033. ev.preventDefault();
  4034. var $el = $(ev.target),
  4035. value = $el.attr('data-value');
  4036. if (value === 'logout') {
  4037. this.$el.find(".dropdown dd ul").hide();
  4038. converse.logOut();
  4039. } else {
  4040. this.model.setStatus(value);
  4041. this.$el.find(".dropdown dd ul").hide();
  4042. }
  4043. },
  4044. getPrettyStatus: function (stat) {
  4045. var pretty_status;
  4046. if (stat === 'chat') {
  4047. pretty_status = __('online');
  4048. } else if (stat === 'dnd') {
  4049. pretty_status = __('busy');
  4050. } else if (stat === 'xa') {
  4051. pretty_status = __('away for long');
  4052. } else if (stat === 'away') {
  4053. pretty_status = __('away');
  4054. } else {
  4055. pretty_status = __(stat) || __('online');
  4056. }
  4057. return pretty_status;
  4058. },
  4059. updateStatusUI: function (model) {
  4060. if (!(_.has(model.changed, 'status')) && !(_.has(model.changed, 'status_message'))) {
  4061. return;
  4062. }
  4063. var stat = model.get('status');
  4064. // # For translators: the %1$s part gets replaced with the status
  4065. // # Example, I am online
  4066. var status_message = model.get('status_message') || __("I am %1$s", this.getPrettyStatus(stat));
  4067. this.$el.find('#fancy-xmpp-status-select').html(
  4068. converse.templates.chat_status({
  4069. 'chat_status': stat,
  4070. 'status_message': status_message,
  4071. 'desc_custom_status': __('Click here to write a custom status message'),
  4072. 'desc_change_status': __('Click to change your chat status')
  4073. }));
  4074. }
  4075. });
  4076. this.BOSHSession = Backbone.Model;
  4077. this.Feature = Backbone.Model;
  4078. this.Features = Backbone.Collection.extend({
  4079. /* Service Discovery
  4080. * -----------------
  4081. * This collection stores Feature Models, representing features
  4082. * provided by available XMPP entities (e.g. servers)
  4083. * See XEP-0030 for more details: http://xmpp.org/extensions/xep-0030.html
  4084. * All features are shown here: http://xmpp.org/registrar/disco-features.html
  4085. */
  4086. model: converse.Feature,
  4087. initialize: function () {
  4088. this.addClientIdentities().addClientFeatures();
  4089. this.browserStorage = new Backbone.BrowserStorage[converse.storage](
  4090. b64_sha1('converse.features'+converse.bare_jid));
  4091. if (this.browserStorage.records.length === 0) {
  4092. // browserStorage is empty, so we've likely never queried this
  4093. // domain for features yet
  4094. converse.connection.disco.info(converse.domain, null, $.proxy(this.onInfo, this));
  4095. converse.connection.disco.items(converse.domain, null, $.proxy(this.onItems, this));
  4096. } else {
  4097. this.fetch({add:true});
  4098. }
  4099. },
  4100. addClientIdentities: function () {
  4101. /* See http://xmpp.org/registrar/disco-categories.html
  4102. */
  4103. converse.connection.disco.addIdentity('client', 'web', 'Converse.js');
  4104. return this;
  4105. },
  4106. addClientFeatures: function () {
  4107. /* The strophe.disco.js plugin keeps a list of features which
  4108. * it will advertise to any #info queries made to it.
  4109. *
  4110. * See: http://xmpp.org/extensions/xep-0030.html#info
  4111. *
  4112. * TODO: these features need to be added in the relevant
  4113. * feature-providing Models, not here
  4114. */
  4115. converse.connection.disco.addFeature('http://jabber.org/protocol/chatstates'); // Limited support
  4116. converse.connection.disco.addFeature('http://jabber.org/protocol/rosterx'); // Limited support
  4117. converse.connection.disco.addFeature('jabber:x:conference');
  4118. converse.connection.disco.addFeature('urn:xmpp:carbons:2');
  4119. converse.connection.disco.addFeature(Strophe.NS.VCARD);
  4120. converse.connection.disco.addFeature(Strophe.NS.BOSH);
  4121. converse.connection.disco.addFeature(Strophe.NS.DISCO_INFO);
  4122. converse.connection.disco.addFeature(Strophe.NS.MUC);
  4123. return this;
  4124. },
  4125. onItems: function (stanza) {
  4126. $(stanza).find('query item').each($.proxy(function (idx, item) {
  4127. converse.connection.disco.info(
  4128. $(item).attr('jid'),
  4129. null,
  4130. $.proxy(this.onInfo, this));
  4131. }, this));
  4132. },
  4133. onInfo: function (stanza) {
  4134. var $stanza = $(stanza);
  4135. if (($stanza.find('identity[category=server][type=im]').length === 0) &&
  4136. ($stanza.find('identity[category=conference][type=text]').length === 0)) {
  4137. // This isn't an IM server component
  4138. return;
  4139. }
  4140. $stanza.find('feature').each($.proxy(function (idx, feature) {
  4141. this.create({
  4142. 'var': $(feature).attr('var'),
  4143. 'from': $stanza.attr('from')
  4144. });
  4145. }, this));
  4146. }
  4147. });
  4148. this.RegisterPanel = Backbone.View.extend({
  4149. tagName: 'div',
  4150. id: "register",
  4151. className: 'controlbox-pane',
  4152. events: {
  4153. 'submit form#converse-register': 'onProviderChosen'
  4154. },
  4155. initialize: function (cfg) {
  4156. this.reset();
  4157. this.$parent = cfg.$parent;
  4158. this.$tabs = cfg.$parent.parent().find('#controlbox-tabs');
  4159. this.registerHooks();
  4160. },
  4161. render: function () {
  4162. this.$parent.append(this.$el.html(
  4163. converse.templates.register_panel({
  4164. 'label_domain': __("Your XMPP provider's domain name:"),
  4165. 'label_register': __('Fetch registration form'),
  4166. 'help_providers': __('Tip: A list of public XMPP providers is available'),
  4167. 'help_providers_link': __('here'),
  4168. 'href_providers': converse.providers_link,
  4169. 'domain_placeholder': converse.domain_placeholder
  4170. })
  4171. ));
  4172. this.$tabs.append(converse.templates.register_tab({label_register: __('Register')}));
  4173. return this;
  4174. },
  4175. registerHooks: function () {
  4176. /* Hook into Strophe's _connect_cb, so that we can send an IQ
  4177. * requesting the registration fields.
  4178. */
  4179. var conn = converse.connection;
  4180. var connect_cb = conn._connect_cb.bind(conn);
  4181. conn._connect_cb = $.proxy(function (req, callback, raw) {
  4182. if (!this._registering) {
  4183. connect_cb(req, callback, raw);
  4184. } else {
  4185. if (this.getRegistrationFields(req, callback, raw)) {
  4186. this._registering = false;
  4187. }
  4188. }
  4189. }, this);
  4190. },
  4191. getRegistrationFields: function (req, _callback, raw) {
  4192. /* Send an IQ stanza to the XMPP server asking for the
  4193. * registration fields.
  4194. *
  4195. * Parameters:
  4196. * (Strophe.Request) req - The current request
  4197. * (Function) callback
  4198. */
  4199. converse.log("sendQueryStanza was called");
  4200. var conn = converse.connection;
  4201. conn.connected = true;
  4202. var body = conn._proto._reqToData(req);
  4203. if (!body) { return; }
  4204. if (conn._proto._connect_cb(body) === Strophe.Status.CONNFAIL) {
  4205. return false;
  4206. }
  4207. var register = body.getElementsByTagName("register");
  4208. var mechanisms = body.getElementsByTagName("mechanism");
  4209. if (register.length === 0 && mechanisms.length === 0) {
  4210. conn._proto._no_auth_received(_callback);
  4211. return false;
  4212. }
  4213. if (register.length === 0) {
  4214. conn._changeConnectStatus(
  4215. Strophe.Status.REGIFAIL,
  4216. __('Sorry, the given provider does not support in band account registration. Please try with a different provider.')
  4217. );
  4218. return true;
  4219. }
  4220. // Send an IQ stanza to get all required data fields
  4221. conn._addSysHandler(this.onRegistrationFields.bind(this), null, "iq", null, null);
  4222. conn.send($iq({type: "get"}).c("query", {xmlns: Strophe.NS.REGISTER}).tree());
  4223. return true;
  4224. },
  4225. onRegistrationFields: function (stanza) {
  4226. /* Handler for Registration Fields Request.
  4227. *
  4228. * Parameters:
  4229. * (XMLElement) elem - The query stanza.
  4230. */
  4231. if (stanza.getElementsByTagName("query").length !== 1) {
  4232. converse.connection._changeConnectStatus(Strophe.Status.REGIFAIL, "unknown");
  4233. return false;
  4234. }
  4235. this.setFields(stanza);
  4236. this.renderRegistrationForm(stanza);
  4237. return false;
  4238. },
  4239. reset: function (settings) {
  4240. var defaults = {
  4241. fields: {},
  4242. urls: [],
  4243. title: "",
  4244. instructions: "",
  4245. registered: false,
  4246. _registering: false,
  4247. domain: null,
  4248. form_type: null
  4249. };
  4250. _.extend(this, defaults);
  4251. if (settings) {
  4252. _.extend(this, _.pick(settings, Object.keys(defaults)));
  4253. }
  4254. },
  4255. onProviderChosen: function (ev) {
  4256. /* Callback method that gets called when the user has chosen an
  4257. * XMPP provider.
  4258. *
  4259. * Parameters:
  4260. * (Submit Event) ev - Form submission event.
  4261. */
  4262. if (ev && ev.preventDefault) { ev.preventDefault(); }
  4263. var $form = $(ev.target),
  4264. $domain_input = $form.find('input[name=domain]'),
  4265. domain = $domain_input.val(),
  4266. errors = false;
  4267. if (!domain) {
  4268. $domain_input.addClass('error');
  4269. return;
  4270. }
  4271. $form.find('input[type=submit]').hide()
  4272. .after(converse.templates.registration_request({
  4273. cancel: __('Cancel'),
  4274. info_message: __('Requesting a registration form from the XMPP server')
  4275. }));
  4276. $form.find('button.cancel').on('click', $.proxy(this.cancelRegistration, this));
  4277. this.reset({
  4278. domain: Strophe.getDomainFromJid(domain),
  4279. _registering: true
  4280. });
  4281. converse.connection.connect(this.domain, "", $.proxy(this.onRegistering, this));
  4282. return false;
  4283. },
  4284. giveFeedback: function (message, klass) {
  4285. this.$('.reg-feedback').attr('class', 'reg-feedback').text(message);
  4286. if (klass) {
  4287. $('.reg-feedback').addClass(klass);
  4288. }
  4289. },
  4290. onRegistering: function (status, error) {
  4291. var that;
  4292. console.log('onRegistering');
  4293. if (_.contains([
  4294. Strophe.Status.DISCONNECTED,
  4295. Strophe.Status.CONNFAIL,
  4296. Strophe.Status.REGIFAIL,
  4297. Strophe.Status.NOTACCEPTABLE,
  4298. Strophe.Status.CONFLICT
  4299. ], status)) {
  4300. converse.log('Problem during registration: Strophe.Status is: '+status);
  4301. this.cancelRegistration();
  4302. if (error) {
  4303. this.giveFeedback(error, 'error');
  4304. } else {
  4305. this.giveFeedback(__(
  4306. 'Something went wrong while establishing a connection with "%1$s". Are you sure it exists?',
  4307. this.domain
  4308. ), 'error');
  4309. }
  4310. } else if (status == Strophe.Status.REGISTERED) {
  4311. converse.log("Registered successfully.");
  4312. converse.connection.reset();
  4313. that = this;
  4314. this.$('form').hide(function () {
  4315. $(this).replaceWith('<span class="spinner centered"/>');
  4316. if (that.fields.password && that.fields.username) {
  4317. // automatically log the user in
  4318. converse.connection.connect(
  4319. that.fields.username+'@'+that.domain,
  4320. that.fields.password,
  4321. converse.onConnect
  4322. );
  4323. converse.chatboxviews.get('controlbox')
  4324. .switchTab({target: that.$tabs.find('.current')})
  4325. .giveFeedback(__('Now logging you in'));
  4326. } else {
  4327. converse.chatboxviews.get('controlbox')
  4328. .renderLoginPanel()
  4329. .giveFeedback(__('Registered successfully'));
  4330. }
  4331. that.reset();
  4332. });
  4333. }
  4334. },
  4335. renderRegistrationForm: function (stanza) {
  4336. /* Renders the registration form based on the XForm fields
  4337. * received from the XMPP server.
  4338. *
  4339. * Parameters:
  4340. * (XMLElement) stanza - The IQ stanza received from the XMPP server.
  4341. */
  4342. var $form= this.$('form'),
  4343. $stanza = $(stanza),
  4344. $fields;
  4345. $form.empty().append(converse.templates.registration_form({
  4346. 'domain': this.domain,
  4347. 'title': this.title,
  4348. 'instructions': this.instructions
  4349. }));
  4350. if (this.form_type == 'xform') {
  4351. $fields = $stanza.find('field');
  4352. _.each($fields, $.proxy(function (field) {
  4353. $form.append(utils.xForm2webForm.bind(this, $(field), $stanza));
  4354. }, this));
  4355. } else {
  4356. // Show fields
  4357. _.each(Object.keys(this.fields), $.proxy(function (key) {
  4358. $form.append('<label>'+key+'</label>');
  4359. var $input = $('<input placeholder="'+key+'" name="'+key+'"></input>');
  4360. if (key === 'password' || key === 'email') {
  4361. $input.attr('type', key);
  4362. }
  4363. $form.append($input);
  4364. }, this));
  4365. // Show urls
  4366. _.each(this.urls, $.proxy(function (url) {
  4367. $form.append($('<a target="blank"></a>').attr('href', url).text(url));
  4368. }, this));
  4369. }
  4370. if (this.fields) {
  4371. $form.append('<input type="submit" class="save-submit" value="'+__('Register')+'"/>');
  4372. $form.on('submit', $.proxy(this.submitRegistrationForm, this));
  4373. $form.append('<input type="button" class="cancel-submit" value="'+__('Cancel')+'"/>');
  4374. $form.find('input[type=button]').on('click', $.proxy(this.cancelRegistration, this));
  4375. } else {
  4376. $form.append('<input type="button" class="submit" value="'+__('Return')+'"/>');
  4377. $form.find('input[type=button]').on('click', $.proxy(this.cancelRegistration, this));
  4378. }
  4379. },
  4380. reportErrors: function (stanza) {
  4381. /* Report back to the user any error messages received from the
  4382. * XMPP server after attempted registration.
  4383. *
  4384. * Parameters:
  4385. * (XMLElement) stanza - The IQ stanza received from the
  4386. * XMPP server.
  4387. */
  4388. var $form= this.$('form'), flash;
  4389. var $errmsgs = $(stanza).find('error text');
  4390. var $flash = $form.find('.form-errors');
  4391. if (!$flash.length) {
  4392. flash = '<legend class="form-errors"></legend>';
  4393. if ($form.find('p.instructions').length) {
  4394. $form.find('p.instructions').append(flash);
  4395. } else {
  4396. $form.prepend(flash);
  4397. }
  4398. $flash = $form.find('.form-errors');
  4399. } else {
  4400. $flash.empty();
  4401. }
  4402. $errmsgs.each(function (idx, txt) {
  4403. $flash.append($('<p>').text($(txt).text()));
  4404. });
  4405. if (!$errmsgs.length) {
  4406. $flash.append($('<p>').text(
  4407. __('The provider rejected your registration attempt. '+
  4408. 'Please check the values you entered for correctness.')));
  4409. }
  4410. $flash.show();
  4411. },
  4412. cancelRegistration: function (ev) {
  4413. /* Handler, when the user cancels the registration form.
  4414. */
  4415. if (ev && ev.preventDefault) { ev.preventDefault(); }
  4416. converse.connection.reset();
  4417. this.render();
  4418. },
  4419. submitRegistrationForm : function (ev) {
  4420. /* Handler, when the user submits the registration form.
  4421. * Provides form error feedback or starts the registration
  4422. * process.
  4423. *
  4424. * Parameters:
  4425. * (Event) ev - the submit event.
  4426. */
  4427. if (ev && ev.preventDefault) { ev.preventDefault(); }
  4428. var $empty_inputs = this.$('input.required:emptyVal');
  4429. if ($empty_inputs.length) {
  4430. $empty_inputs.addClass('error');
  4431. return;
  4432. }
  4433. var $inputs = $(ev.target).find(':input:not([type=button]):not([type=submit])'),
  4434. iq = $iq({type: "set"})
  4435. .c("query", {xmlns:Strophe.NS.REGISTER})
  4436. .c("x", {xmlns: Strophe.NS.XFORM, type: 'submit'});
  4437. $inputs.each(function () {
  4438. iq.cnode(utils.webForm2xForm(this)).up();
  4439. });
  4440. converse.connection._addSysHandler(this._onRegisterIQ.bind(this), null, "iq", null, null);
  4441. converse.connection.send(iq);
  4442. this.setFields(iq.tree());
  4443. },
  4444. setFields: function (stanza) {
  4445. /* Stores the values that will be sent to the XMPP server
  4446. * during attempted registration.
  4447. *
  4448. * Parameters:
  4449. * (XMLElement) stanza - the IQ stanza that will be sent to the XMPP server.
  4450. */
  4451. var $query = $(stanza).find('query'), $xform;
  4452. if ($query.length > 0) {
  4453. $xform = $query.find('x[xmlns="'+Strophe.NS.XFORM+'"]');
  4454. if ($xform.length > 0) {
  4455. this._setFieldsFromXForm($xform);
  4456. } else {
  4457. this._setFieldsFromLegacy($query);
  4458. }
  4459. }
  4460. },
  4461. _setFieldsFromLegacy: function ($query) {
  4462. $query.children().each($.proxy(function (idx, field) {
  4463. var $field = $(field);
  4464. if (field.tagName.toLowerCase() === 'instructions') {
  4465. this.instructions = Strophe.getText(field);
  4466. return;
  4467. } else if (field.tagName.toLowerCase() === 'x') {
  4468. if ($field.attr('xmlns') === 'jabber:x:oob') {
  4469. $field.find('url').each($.proxy(function (idx, url) {
  4470. this.urls.push($(url).text());
  4471. }, this));
  4472. }
  4473. return;
  4474. }
  4475. this.fields[field.tagName.toLowerCase()] = Strophe.getText(field);
  4476. }, this));
  4477. this.form_type = 'legacy';
  4478. },
  4479. _setFieldsFromXForm: function ($xform) {
  4480. this.title = $xform.find('title').text();
  4481. this.instructions = $xform.find('instructions').text();
  4482. $xform.find('field').each($.proxy(function (idx, field) {
  4483. var _var = field.getAttribute('var');
  4484. if (_var) {
  4485. this.fields[_var.toLowerCase()] = $(field).children('value').text();
  4486. } else {
  4487. // TODO: other option seems to be type="fixed"
  4488. console.log("WARNING: Found field we couldn't parse");
  4489. }
  4490. }, this));
  4491. this.form_type = 'xform';
  4492. },
  4493. _onRegisterIQ: function (stanza) {
  4494. /* Callback method that gets called when a return IQ stanza
  4495. * is received from the XMPP server, after attempting to
  4496. * register a new user.
  4497. *
  4498. * Parameters:
  4499. * (XMLElement) stanza - The IQ stanza.
  4500. */
  4501. var i, field, error = null, that,
  4502. query = stanza.getElementsByTagName("query");
  4503. if (query.length > 0) {
  4504. query = query[0];
  4505. }
  4506. if (stanza.getAttribute("type") === "error") {
  4507. converse.log("Registration failed.");
  4508. error = stanza.getElementsByTagName("error");
  4509. if (error.length !== 1) {
  4510. converse.connection._changeConnectStatus(Strophe.Status.REGIFAIL, "unknown");
  4511. return false;
  4512. }
  4513. error = error[0].firstChild.tagName.toLowerCase();
  4514. if (error === 'conflict') {
  4515. converse.connection._changeConnectStatus(Strophe.Status.CONFLICT, error);
  4516. } else if (error === 'not-acceptable') {
  4517. converse.connection._changeConnectStatus(Strophe.Status.NOTACCEPTABLE, error);
  4518. } else {
  4519. converse.connection._changeConnectStatus(Strophe.Status.REGIFAIL, error);
  4520. }
  4521. this.reportErrors(stanza);
  4522. } else {
  4523. converse.connection._changeConnectStatus(Strophe.Status.REGISTERED, null);
  4524. }
  4525. return false;
  4526. },
  4527. remove: function () {
  4528. this.$tabs.empty();
  4529. this.$el.parent().empty();
  4530. }
  4531. });
  4532. this.LoginPanel = Backbone.View.extend({
  4533. tagName: 'div',
  4534. id: "login-dialog",
  4535. className: 'controlbox-pane',
  4536. events: {
  4537. 'submit form#converse-login': 'authenticate'
  4538. },
  4539. initialize: function (cfg) {
  4540. cfg.$parent.html(this.$el.html(
  4541. converse.templates.login_panel({
  4542. 'label_username': __('XMPP Username:'),
  4543. 'label_password': __('Password:'),
  4544. 'label_login': __('Log In')
  4545. })
  4546. ));
  4547. this.$tabs = cfg.$parent.parent().find('#controlbox-tabs');
  4548. },
  4549. render: function () {
  4550. this.$tabs.append(converse.templates.login_tab({label_sign_in: __('Sign in')}));
  4551. this.$el.find('input#jid').focus();
  4552. if (!this.$el.is(':visible')) {
  4553. this.$el.show();
  4554. }
  4555. return this;
  4556. },
  4557. authenticate: function (ev) {
  4558. if (ev && ev.preventDefault) { ev.preventDefault(); }
  4559. var $form = $(ev.target),
  4560. $jid_input = $form.find('input[name=jid]'),
  4561. jid = $jid_input.val(),
  4562. $pw_input = $form.find('input[name=password]'),
  4563. password = $pw_input.val(),
  4564. $bsu_input = null,
  4565. errors = false;
  4566. if (! converse.bosh_service_url) {
  4567. $bsu_input = $form.find('input#bosh_service_url');
  4568. converse.bosh_service_url = $bsu_input.val();
  4569. if (! converse.bosh_service_url) {
  4570. errors = true;
  4571. $bsu_input.addClass('error');
  4572. }
  4573. }
  4574. if (! jid) {
  4575. errors = true;
  4576. $jid_input.addClass('error');
  4577. }
  4578. if (! password) {
  4579. errors = true;
  4580. $pw_input.addClass('error');
  4581. }
  4582. if (errors) { return; }
  4583. this.connect($form, jid, password);
  4584. return false;
  4585. },
  4586. connect: function ($form, jid, password) {
  4587. if ($form) {
  4588. $form.find('input[type=submit]').hide().after('<span class="spinner login-submit"/>');
  4589. }
  4590. var resource = Strophe.getResourceFromJid(jid);
  4591. if (!resource) {
  4592. jid += '/converse.js-' + Math.floor(Math.random()*139749825).toString();
  4593. }
  4594. converse.connection.connect(jid, password, converse.onConnect);
  4595. },
  4596. remove: function () {
  4597. this.$tabs.empty();
  4598. this.$el.parent().empty();
  4599. }
  4600. });
  4601. this.ControlBoxToggle = Backbone.View.extend({
  4602. tagName: 'a',
  4603. className: 'toggle-controlbox',
  4604. id: 'toggle-controlbox',
  4605. events: {
  4606. 'click': 'onClick'
  4607. },
  4608. attributes: {
  4609. 'href': "#"
  4610. },
  4611. initialize: function () {
  4612. this.render();
  4613. },
  4614. render: function () {
  4615. $('#conversejs').prepend(this.$el.html(
  4616. converse.templates.controlbox_toggle({
  4617. 'label_toggle': __('Toggle chat')
  4618. })
  4619. ));
  4620. // We let the render method of ControlBoxView decide whether
  4621. // the ControlBox or the Toggle must be shown. This prevents
  4622. // artifacts (i.e. on page load the toggle is shown only to then
  4623. // seconds later be hidden in favor of the control box).
  4624. this.$el.hide();
  4625. return this;
  4626. },
  4627. hide: function (callback) {
  4628. this.$el.fadeOut('fast', callback);
  4629. },
  4630. show: function (callback) {
  4631. this.$el.show('fast', callback);
  4632. },
  4633. showControlBox: function () {
  4634. var controlbox = converse.chatboxes.get('controlbox');
  4635. if (!controlbox) {
  4636. controlbox = converse.addControlBox();
  4637. }
  4638. if (converse.connection.connected) {
  4639. controlbox.save({closed: false});
  4640. } else {
  4641. controlbox.trigger('show');
  4642. }
  4643. },
  4644. onClick: function (e) {
  4645. e.preventDefault();
  4646. if ($("div#controlbox").is(':visible')) {
  4647. var controlbox = converse.chatboxes.get('controlbox');
  4648. if (converse.connection.connected) {
  4649. controlbox.save({closed: true});
  4650. } else {
  4651. controlbox.trigger('hide');
  4652. }
  4653. } else {
  4654. this.showControlBox();
  4655. }
  4656. }
  4657. });
  4658. this.addControlBox = function () {
  4659. return this.chatboxes.add({
  4660. id: 'controlbox',
  4661. box_id: 'controlbox',
  4662. height: this.default_box_height,
  4663. closed: !this.show_controlbox_by_default
  4664. });
  4665. };
  4666. this.setUpXMLLogging = function () {
  4667. if (this.debug) {
  4668. this.connection.xmlInput = function (body) { console.log(body); };
  4669. this.connection.xmlOutput = function (body) { console.log(body); };
  4670. }
  4671. };
  4672. this.initConnection = function () {
  4673. var rid, sid, jid;
  4674. if (this.connection && this.connection.connected) {
  4675. this.setUpXMLLogging();
  4676. this.onConnected();
  4677. } else {
  4678. // XXX: it's not yet clear what the order of preference should
  4679. // be between RID and SID received via the initialize method or
  4680. // those received from sessionStorage.
  4681. //
  4682. // What do you we if we receive values from both avenues?
  4683. //
  4684. // Also, what do we do when the keepalive session values are
  4685. // expired? Do we try to fall back?
  4686. if (!this.bosh_service_url) {
  4687. throw("Error: you must supply a value for the bosh_service_url");
  4688. }
  4689. this.connection = new Strophe.Connection(this.bosh_service_url);
  4690. this.setUpXMLLogging();
  4691. if (this.prebind) {
  4692. if (this.jid && this.sid && this.rid) {
  4693. this.connection.attach(this.jid, this.sid, this.rid, this.onConnect);
  4694. }
  4695. if (!this.keepalive) {
  4696. throw("If you use prebind and don't use keepalive, "+
  4697. "then you MUST supply JID, RID and SID values");
  4698. }
  4699. }
  4700. if (this.keepalive) {
  4701. rid = this.session.get('rid');
  4702. sid = this.session.get('sid');
  4703. jid = this.session.get('jid');
  4704. if (rid && jid && sid) {
  4705. this.session.save({rid: rid}); // The RID needs to be increased with each request.
  4706. this.connection.attach(jid, sid, rid, this.onConnect);
  4707. } else if (this.prebind) {
  4708. delete this.connection;
  4709. this.emit('noResumeableSession');
  4710. }
  4711. }
  4712. }
  4713. };
  4714. this._tearDown = function () {
  4715. /* Remove those views which are only allowed with a valid
  4716. * connection.
  4717. */
  4718. this.initial_presence_sent = false;
  4719. if (this.roster) {
  4720. this.roster.off().reset(); // Removes roster contacts
  4721. }
  4722. this.connection.roster._callbacks = []; // Remove all Roster handlers (e.g. rosterHandler)
  4723. if (this.rosterview) {
  4724. this.rosterview.model.off().reset(); // Removes roster groups
  4725. this.rosterview.undelegateEvents().remove();
  4726. }
  4727. this.chatboxes.remove(); // Don't call off(), events won't get re-registered upon reconnect.
  4728. if (this.features) {
  4729. this.features.reset();
  4730. }
  4731. if (this.minimized_chats) {
  4732. this.minimized_chats.undelegateEvents().model.reset();
  4733. this.minimized_chats.removeAll(); // Remove sub-views
  4734. this.minimized_chats.tearDown().remove(); // Remove overview
  4735. delete this.minimized_chats;
  4736. }
  4737. return this;
  4738. };
  4739. this._initialize = function () {
  4740. this.chatboxes = new this.ChatBoxes();
  4741. this.chatboxviews = new this.ChatBoxViews({model: this.chatboxes});
  4742. this.controlboxtoggle = new this.ControlBoxToggle();
  4743. this.otr = new this.OTR();
  4744. this.initSession();
  4745. this.initConnection();
  4746. if (this.connection) {
  4747. this.addControlBox();
  4748. }
  4749. return this;
  4750. };
  4751. this._initializePlugins = function () {
  4752. _.each(this.plugins, $.proxy(function (plugin) {
  4753. $.proxy(plugin, this)(this);
  4754. }, this));
  4755. };
  4756. // Initialization
  4757. // --------------
  4758. // This is the end of the initialize method.
  4759. if (settings.connection) {
  4760. this.connection = settings.connection;
  4761. }
  4762. this._initializePlugins();
  4763. this._initialize();
  4764. this.registerGlobalEventHandlers();
  4765. converse.emit('initialized');
  4766. };
  4767. var wrappedChatBox = function (chatbox) {
  4768. return {
  4769. 'endOTR': $.proxy(chatbox.endOTR, chatbox),
  4770. 'get': $.proxy(chatbox.get, chatbox),
  4771. 'initiateOTR': $.proxy(chatbox.initiateOTR, chatbox),
  4772. 'maximize': $.proxy(chatbox.maximize, chatbox),
  4773. 'minimize': $.proxy(chatbox.minimize, chatbox),
  4774. 'set': $.proxy(chatbox.set, chatbox),
  4775. 'open': chatbox.trigger.bind(chatbox, 'show')
  4776. };
  4777. };
  4778. return {
  4779. 'initialize': function (settings, callback) {
  4780. converse.initialize(settings, callback);
  4781. },
  4782. 'contacts': {
  4783. 'get': function (jids) {
  4784. var _transform = function (jid) {
  4785. var contact = converse.roster.get(Strophe.getBareJidFromJid(jid));
  4786. if (contact) {
  4787. return contact.attributes;
  4788. }
  4789. return null;
  4790. };
  4791. if (typeof jids === "string") {
  4792. return _transform(jids);
  4793. }
  4794. return _.map(jids, _transform);
  4795. }
  4796. },
  4797. 'chats': {
  4798. 'get': function (jids) {
  4799. var _transform = function (jid) {
  4800. var chatbox = converse.chatboxes.get(jid);
  4801. if (!chatbox) {
  4802. var roster_item = converse.roster.get(jid);
  4803. if (roster_item === undefined) {
  4804. converse.log('Could not get roster item for JID '+jid, 'error');
  4805. return null;
  4806. }
  4807. chatbox = converse.chatboxes.create({
  4808. 'id': jid,
  4809. 'jid': jid,
  4810. 'fullname': _.isEmpty(roster_item.get('fullname'))? jid: roster_item.get('fullname'),
  4811. 'image_type': roster_item.get('image_type'),
  4812. 'image': roster_item.get('image'),
  4813. 'url': roster_item.get('url')
  4814. });
  4815. }
  4816. return wrappedChatBox(chatbox);
  4817. };
  4818. if (typeof jids === "string") {
  4819. return _transform(jids);
  4820. }
  4821. return _.map(jids, _transform);
  4822. }
  4823. },
  4824. 'tokens': {
  4825. 'get': function (id) {
  4826. if (!converse.expose_rid_and_sid || typeof converse.connection === "undefined") {
  4827. return null;
  4828. }
  4829. if (id.toLowerCase() === 'rid') {
  4830. return converse.connection.rid || converse.connection._proto.rid;
  4831. } else if (id.toLowerCase() === 'sid') {
  4832. return converse.connection.sid || converse.connection._proto.sid;
  4833. }
  4834. }
  4835. },
  4836. 'listen': {
  4837. 'once': function (evt, handler) {
  4838. converse.once(evt, handler);
  4839. },
  4840. 'on': function (evt, handler) {
  4841. converse.on(evt, handler);
  4842. },
  4843. 'not': function (evt, handler) {
  4844. converse.off(evt, handler);
  4845. },
  4846. },
  4847. 'plugins': {
  4848. 'add': function (name, callback) {
  4849. converse.plugins[name] = callback;
  4850. },
  4851. 'remove': function (name) {
  4852. delete converse.plugins[name];
  4853. },
  4854. 'extend': function (obj, attributes) {
  4855. /* Helper method for overriding or extending Converse's Backbone Views or Models
  4856. *
  4857. * When a method is overriden, the original will still be available
  4858. * on the _super attribute of the object being overridden.
  4859. *
  4860. * obj: The Backbone View or Model
  4861. * attributes: A hash of attributes, such as you would pass to Backbone.Model.extend or Backbone.View.extend
  4862. */
  4863. if (!obj.prototype._super) {
  4864. obj.prototype._super = {};
  4865. }
  4866. _.each(attributes, function (value, key) {
  4867. if (key === 'events') {
  4868. obj.prototype[key] = _.extend(value, obj.prototype[key]);
  4869. } else {
  4870. if (typeof key === 'function') {
  4871. obj.prototype._super[key] = obj.prototype[key];
  4872. }
  4873. obj.prototype[key] = value;
  4874. }
  4875. });
  4876. }
  4877. },
  4878. 'env': {
  4879. 'jQuery': $,
  4880. 'Strophe': Strophe,
  4881. '_': _
  4882. },
  4883. // Deprecated API methods
  4884. 'getBuddy': function (jid) {
  4885. converse.log('WARNING: the "getBuddy" API method has been deprecated. Please use "contacts.get" instead');
  4886. return this.contacts.get(jid);
  4887. },
  4888. 'getChatBox': function (jid) {
  4889. converse.log('WARNING: the "getChatBox" API method has been deprecated. Please use "chats.get" instead');
  4890. return this.chats.get(jid);
  4891. },
  4892. 'openChatBox': function (jid) {
  4893. converse.log('WARNING: the "openChatBox" API method has been deprecated. Please use "chats.get(jid).open()" instead');
  4894. var chat = this.chats.get(jid);
  4895. if (chat) { chat.open(); }
  4896. return chat;
  4897. },
  4898. 'getRID': function () {
  4899. converse.log('WARNING: the "getRID" API method has been deprecated. Please use "tokens.get(\'rid\')" instead');
  4900. return this.tokens.get('rid');
  4901. },
  4902. 'getSID': function () {
  4903. converse.log('WARNING: the "getSID" API method has been deprecated. Please use "tokens.get(\'sid\')" instead');
  4904. return this.tokens.get('sid');
  4905. },
  4906. 'once': function (evt, handler) {
  4907. converse.log('WARNING: the "one" API method has been deprecated. Please use "listen.once" instead');
  4908. return this.listen.once(evt, handler);
  4909. },
  4910. 'on': function (evt, handler) {
  4911. converse.log('WARNING: the "on" API method has been deprecated. Please use "listen.on" instead');
  4912. return this.listen.on(evt, handler);
  4913. },
  4914. 'off': function (evt, handler) {
  4915. converse.log('WARNING: the "off" API method has been deprecated. Please use "listen.not" instead');
  4916. return this.listen.not(evt, handler);
  4917. }
  4918. };
  4919. }));