converse.js 305 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608460946104611461246134614461546164617461846194620462146224623462446254626462746284629463046314632463346344635463646374638463946404641464246434644464546464647464846494650465146524653465446554656465746584659466046614662466346644665466646674668466946704671467246734674467546764677467846794680468146824683468446854686468746884689469046914692469346944695469646974698469947004701470247034704470547064707470847094710471147124713471447154716471747184719472047214722472347244725472647274728472947304731473247334734473547364737473847394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767476847694770477147724773477447754776477747784779478047814782478347844785478647874788478947904791479247934794479547964797479847994800480148024803480448054806480748084809481048114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845484648474848484948504851485248534854485548564857485848594860486148624863486448654866486748684869487048714872487348744875487648774878487948804881488248834884488548864887488848894890489148924893489448954896489748984899490049014902490349044905490649074908490949104911491249134914491549164917491849194920492149224923492449254926492749284929493049314932493349344935493649374938493949404941494249434944494549464947494849494950495149524953495449554956495749584959496049614962496349644965496649674968496949704971497249734974497549764977497849794980498149824983498449854986498749884989499049914992499349944995499649974998499950005001500250035004500550065007500850095010501150125013501450155016501750185019502050215022502350245025502650275028502950305031503250335034503550365037503850395040504150425043504450455046504750485049505050515052505350545055505650575058505950605061506250635064506550665067506850695070507150725073507450755076507750785079508050815082508350845085508650875088508950905091509250935094509550965097509850995100510151025103510451055106510751085109511051115112511351145115511651175118511951205121512251235124512551265127512851295130513151325133513451355136513751385139514051415142514351445145514651475148514951505151515251535154515551565157515851595160516151625163516451655166516751685169517051715172517351745175517651775178517951805181518251835184518551865187518851895190519151925193519451955196519751985199520052015202520352045205520652075208520952105211521252135214521552165217521852195220522152225223522452255226522752285229523052315232523352345235523652375238523952405241524252435244524552465247524852495250525152525253525452555256525752585259526052615262526352645265526652675268526952705271527252735274527552765277527852795280528152825283528452855286528752885289529052915292529352945295529652975298529953005301530253035304530553065307530853095310531153125313531453155316531753185319532053215322532353245325532653275328532953305331533253335334533553365337533853395340534153425343534453455346534753485349535053515352535353545355535653575358535953605361536253635364536553665367536853695370537153725373537453755376537753785379538053815382538353845385538653875388538953905391539253935394539553965397539853995400540154025403540454055406540754085409541054115412541354145415541654175418541954205421542254235424542554265427542854295430543154325433543454355436543754385439544054415442544354445445544654475448544954505451545254535454545554565457545854595460546154625463546454655466546754685469547054715472547354745475547654775478547954805481548254835484548554865487548854895490549154925493549454955496549754985499550055015502550355045505550655075508550955105511551255135514551555165517551855195520552155225523552455255526552755285529553055315532553355345535553655375538553955405541554255435544554555465547554855495550555155525553555455555556555755585559556055615562556355645565556655675568556955705571557255735574557555765577557855795580558155825583558455855586558755885589559055915592559355945595559655975598559956005601560256035604560556065607560856095610561156125613561456155616561756185619562056215622562356245625562656275628562956305631563256335634563556365637563856395640564156425643564456455646564756485649565056515652565356545655565656575658565956605661566256635664566556665667566856695670567156725673567456755676567756785679568056815682568356845685568656875688568956905691569256935694569556965697569856995700570157025703570457055706570757085709571057115712571357145715571657175718571957205721572257235724572557265727572857295730573157325733573457355736573757385739574057415742574357445745574657475748574957505751575257535754575557565757575857595760576157625763576457655766576757685769577057715772577357745775577657775778577957805781578257835784578557865787578857895790579157925793579457955796579757985799580058015802580358045805580658075808580958105811581258135814581558165817581858195820582158225823582458255826582758285829583058315832583358345835583658375838583958405841584258435844584558465847584858495850585158525853585458555856585758585859586058615862586358645865586658675868586958705871587258735874587558765877587858795880588158825883588458855886588758885889589058915892589358945895589658975898589959005901590259035904590559065907590859095910591159125913591459155916591759185919592059215922592359245925592659275928592959305931593259335934593559365937593859395940594159425943594459455946594759485949595059515952595359545955595659575958595959605961596259635964596559665967596859695970597159725973597459755976597759785979598059815982598359845985598659875988598959905991599259935994599559965997599859996000600160026003600460056006600760086009601060116012601360146015601660176018601960206021602260236024602560266027602860296030603160326033603460356036603760386039604060416042604360446045604660476048604960506051605260536054605560566057605860596060606160626063606460656066606760686069607060716072607360746075607660776078607960806081608260836084608560866087608860896090609160926093609460956096609760986099610061016102610361046105610661076108610961106111611261136114611561166117611861196120612161226123612461256126612761286129613061316132613361346135613661376138613961406141614261436144614561466147614861496150615161526153615461556156615761586159616061616162616361646165616661676168616961706171617261736174617561766177617861796180618161826183618461856186618761886189619061916192619361946195619661976198619962006201620262036204620562066207620862096210621162126213621462156216621762186219622062216222622362246225622662276228622962306231623262336234623562366237623862396240624162426243624462456246624762486249625062516252625362546255625662576258625962606261626262636264626562666267626862696270627162726273627462756276627762786279628062816282628362846285628662876288628962906291629262936294629562966297629862996300630163026303630463056306630763086309631063116312631363146315631663176318631963206321632263236324632563266327632863296330633163326333633463356336633763386339634063416342634363446345634663476348634963506351635263536354635563566357635863596360636163626363636463656366636763686369637063716372637363746375637663776378637963806381638263836384638563866387638863896390639163926393639463956396639763986399640064016402640364046405640664076408640964106411641264136414641564166417641864196420642164226423642464256426642764286429643064316432643364346435643664376438643964406441644264436444644564466447644864496450645164526453645464556456645764586459646064616462646364646465646664676468646964706471647264736474647564766477647864796480648164826483648464856486648764886489649064916492649364946495649664976498649965006501650265036504650565066507650865096510651165126513651465156516651765186519652065216522652365246525652665276528652965306531653265336534653565366537653865396540654165426543654465456546654765486549655065516552655365546555655665576558655965606561656265636564656565666567656865696570657165726573657465756576657765786579658065816582658365846585658665876588658965906591659265936594659565966597659865996600660166026603660466056606660766086609661066116612661366146615661666176618661966206621662266236624662566266627662866296630663166326633663466356636663766386639664066416642664366446645664666476648664966506651665266536654665566566657665866596660666166626663666466656666666766686669667066716672667366746675667666776678667966806681668266836684668566866687668866896690669166926693669466956696669766986699670067016702670367046705670667076708670967106711671267136714671567166717
  1. // Converse.js (A browser based XMPP chat client)
  2. // http://conversejs.org
  3. //
  4. // Copyright (c) 2012-2015, Jan-Carel Brand <jc@opkode.com>
  5. // Licensed under the Mozilla Public License (MPLv2)
  6. //
  7. /*global Backbone, CryptoJS, crypto, define, window, jQuery, setTimeout, clearTimeout, document, templates, _,
  8. $iq, $msg, $pres, $build, DSA, OTR, Strophe, moment, utils, b64_sha1, locales */
  9. (function (root, factory) {
  10. if (typeof define === 'function' && define.amd) {
  11. // AMD module loading
  12. // ------------------
  13. // When using require.js, two modules are loaded as dependencies.
  14. //
  15. // * **converse-dependencies**: A list of dependencies on which converse.js
  16. // depends. The path to this module is in main.js and the module itself can
  17. //
  18. // * **converse-templates**: The HTML templates used by converse.js.
  19. //
  20. // The dependencies are then split up and passed into the factory function, which
  21. // contains and instantiates converse.js.
  22. define("converse",
  23. ["converse-dependencies", "converse-templates"],
  24. function (dependencies, templates) {
  25. return factory(
  26. templates,
  27. dependencies.jQuery,
  28. dependencies.$iq,
  29. dependencies.$msg,
  30. dependencies.$pres,
  31. dependencies.$build,
  32. dependencies.otr ? dependencies.otr.DSA : undefined,
  33. dependencies.otr ? dependencies.otr.OTR : undefined,
  34. dependencies.Strophe,
  35. dependencies.underscore,
  36. dependencies.moment,
  37. dependencies.utils,
  38. dependencies.SHA1.b64_sha1
  39. );
  40. }
  41. );
  42. } else {
  43. // When not using a module loader
  44. // -------------------------------
  45. // In this case, the dependencies need to be available already as
  46. // global variables, and should be loaded separately via *script* tags.
  47. // See the file **non_amd.html** for an example of this usecase.
  48. root.converse = factory(templates, jQuery, $iq, $msg, $pres, $build, DSA, OTR, Strophe, _, moment, utils, b64_sha1);
  49. }
  50. }(this, function (templates, $, $iq, $msg, $pres, $build, DSA, OTR, Strophe, _, moment, utils, b64_sha1) {
  51. /* "use strict";
  52. * Cannot use this due to Safari bug.
  53. * See https://github.com/jcbrand/converse.js/issues/196
  54. */
  55. // Use Mustache style syntax for variable interpolation
  56. /* Configuration of underscore templates (this config is distinct to the
  57. * config of requirejs-tpl in main.js). This one is for normal inline templates.
  58. */
  59. _.templateSettings = {
  60. evaluate : /\{\[([\s\S]+?)\]\}/g,
  61. interpolate : /\{\{([\s\S]+?)\}\}/g
  62. };
  63. var contains = function (attr, query) {
  64. return function (item) {
  65. if (typeof attr === 'object') {
  66. var value = false;
  67. _.each(attr, function (a) {
  68. value = value || item.get(a).toLowerCase().indexOf(query.toLowerCase()) !== -1;
  69. });
  70. return value;
  71. } else if (typeof attr === 'string') {
  72. return item.get(attr).toLowerCase().indexOf(query.toLowerCase()) !== -1;
  73. } else {
  74. throw new TypeError('contains: wrong attribute type. Must be string or array.');
  75. }
  76. };
  77. };
  78. contains.not = function (attr, query) {
  79. return function (item) {
  80. return !(contains(attr, query)(item));
  81. };
  82. };
  83. var converse = {
  84. plugins: {},
  85. templates: templates,
  86. emit: function (evt, data) {
  87. $(this).trigger(evt, data);
  88. },
  89. once: function (evt, handler) {
  90. $(this).one(evt, handler);
  91. },
  92. on: function (evt, handler) {
  93. $(this).bind(evt, handler);
  94. },
  95. off: function (evt, handler) {
  96. $(this).unbind(evt, handler);
  97. },
  98. refreshWebkit: function () {
  99. /* This works around a webkit bug. Refresh the browser's viewport,
  100. * otherwise chatboxes are not moved along when one is closed.
  101. */
  102. if ($.browser.webkit) {
  103. var conversejs = document.getElementById('conversejs');
  104. conversejs.style.display = 'none';
  105. conversejs.offsetHeight = conversejs.offsetHeight;
  106. conversejs.style.display = 'block';
  107. }
  108. }
  109. };
  110. // Global constants
  111. // XEP-0059 Result Set Management
  112. var RSM_ATTRIBUTES = ['max', 'first', 'last', 'after', 'before', 'index', 'count'];
  113. // XEP-0313 Message Archive Management
  114. var MAM_ATTRIBUTES = ['with', 'start', 'end'];
  115. var STATUS_WEIGHTS = {
  116. 'offline': 6,
  117. 'unavailable': 5,
  118. 'xa': 4,
  119. 'away': 3,
  120. 'dnd': 2,
  121. 'chat': 1, // We currently don't differentiate between "chat" and "online"
  122. 'online': 1
  123. };
  124. converse.initialize = function (settings, callback) {
  125. "use strict";
  126. var converse = this;
  127. var unloadevent;
  128. if ('onpagehide' in window) {
  129. // Pagehide gets thrown in more cases than unload. Specifically it
  130. // gets thrown when the page is cached and not just
  131. // closed/destroyed. It's the only viable event on mobile Safari.
  132. // https://www.webkit.org/blog/516/webkit-page-cache-ii-the-unload-event/
  133. unloadevent = 'pagehide';
  134. } else if ('onbeforeunload' in window) {
  135. unloadevent = 'beforeunload';
  136. } else if ('onunload' in window) {
  137. unloadevent = 'unload';
  138. }
  139. // Logging
  140. Strophe.log = function (level, msg) { converse.log(level+' '+msg, level); };
  141. Strophe.error = function (msg) { converse.log(msg, 'error'); };
  142. // Add Strophe Namespaces
  143. Strophe.addNamespace('CARBONS', 'urn:xmpp:carbons:2');
  144. Strophe.addNamespace('CHATSTATES', 'http://jabber.org/protocol/chatstates');
  145. Strophe.addNamespace('CSI', 'urn:xmpp:csi:0');
  146. Strophe.addNamespace('MAM', 'urn:xmpp:mam:0');
  147. Strophe.addNamespace('MUC_ADMIN', Strophe.NS.MUC + "#admin");
  148. Strophe.addNamespace('MUC_OWNER', Strophe.NS.MUC + "#owner");
  149. Strophe.addNamespace('MUC_REGISTER', "jabber:iq:register");
  150. Strophe.addNamespace('MUC_ROOMCONF', Strophe.NS.MUC + "#roomconfig");
  151. Strophe.addNamespace('MUC_USER', Strophe.NS.MUC + "#user");
  152. Strophe.addNamespace('REGISTER', 'jabber:iq:register');
  153. Strophe.addNamespace('ROSTERX', 'http://jabber.org/protocol/rosterx');
  154. Strophe.addNamespace('RSM', 'http://jabber.org/protocol/rsm');
  155. Strophe.addNamespace('XFORM', 'jabber:x:data');
  156. // Add Strophe Statuses
  157. var i = 0;
  158. Object.keys(Strophe.Status).forEach(function (key) {
  159. i = Math.max(i, Strophe.Status[key]);
  160. });
  161. Strophe.Status.REGIFAIL = i + 1;
  162. Strophe.Status.REGISTERED = i + 2;
  163. Strophe.Status.CONFLICT = i + 3;
  164. Strophe.Status.NOTACCEPTABLE = i + 5;
  165. // Constants
  166. // ---------
  167. var LOGIN = "login";
  168. var ANONYMOUS = "anonymous";
  169. var PREBIND = "prebind";
  170. var UNENCRYPTED = 0;
  171. var UNVERIFIED= 1;
  172. var VERIFIED= 2;
  173. var FINISHED = 3;
  174. var KEY = {
  175. ENTER: 13,
  176. FORWARD_SLASH: 47
  177. };
  178. var PRETTY_CONNECTION_STATUS = {
  179. 0: 'ERROR',
  180. 1: 'CONNECTING',
  181. 2: 'CONNFAIL',
  182. 3: 'AUTHENTICATING',
  183. 4: 'AUTHFAIL',
  184. 5: 'CONNECTED',
  185. 6: 'DISCONNECTED',
  186. 7: 'DISCONNECTING',
  187. 8: 'ATTACHED',
  188. 9: 'REDIRECT'
  189. };
  190. // XEP-0085 Chat states
  191. // http://xmpp.org/extensions/xep-0085.html
  192. var INACTIVE = 'inactive';
  193. var ACTIVE = 'active';
  194. var COMPOSING = 'composing';
  195. var PAUSED = 'paused';
  196. var GONE = 'gone';
  197. this.TIMEOUTS = { // Set as module attr so that we can override in tests.
  198. 'PAUSED': 20000,
  199. 'INACTIVE': 90000
  200. };
  201. var HAS_CSPRNG = ((typeof crypto !== 'undefined') &&
  202. ((typeof crypto.randomBytes === 'function') ||
  203. (typeof crypto.getRandomValues === 'function')
  204. ));
  205. var HAS_CRYPTO = HAS_CSPRNG && (
  206. (typeof CryptoJS !== "undefined") &&
  207. (typeof OTR !== "undefined") &&
  208. (typeof DSA !== "undefined")
  209. );
  210. var OPENED = 'opened';
  211. var CLOSED = 'closed';
  212. // Detect support for the user's locale
  213. // ------------------------------------
  214. this.isConverseLocale = function (locale) { return typeof locales[locale] !== "undefined"; };
  215. this.isMomentLocale = function (locale) { return moment.locale() !== moment.locale(locale); };
  216. this.isLocaleAvailable = function (locale, available) {
  217. /* Check whether the locale or sub locale (e.g. en-US, en) is supported.
  218. *
  219. * Parameters:
  220. * (Function) available - returns a boolean indicating whether the locale is supported
  221. */
  222. if (available(locale)) {
  223. return locale;
  224. } else {
  225. var sublocale = locale.split("-")[0];
  226. if (sublocale !== locale && available(sublocale)) {
  227. return sublocale;
  228. }
  229. }
  230. };
  231. this.detectLocale = function (library_check) {
  232. /* Determine which locale is supported by the user's system as well
  233. * as by the relevant library (e.g. converse.js or moment.js).
  234. *
  235. * Parameters:
  236. * (Function) library_check - returns a boolean indicating whether the locale is supported
  237. */
  238. var locale, i;
  239. if (window.navigator.userLanguage) {
  240. locale = this.isLocaleAvailable(window.navigator.userLanguage, library_check);
  241. }
  242. if (window.navigator.languages && !locale) {
  243. for (i=0; i<window.navigator.languages.length && !locale; i++) {
  244. locale = this.isLocaleAvailable(window.navigator.languages[i], library_check);
  245. }
  246. }
  247. if (window.navigator.browserLanguage && !locale) {
  248. locale = this.isLocaleAvailable(window.navigator.browserLanguage, library_check);
  249. }
  250. if (window.navigator.language && !locale) {
  251. locale = this.isLocaleAvailable(window.navigator.language, library_check);
  252. }
  253. if (window.navigator.systemLanguage && !locale) {
  254. locale = this.isLocaleAvailable(window.navigator.systemLanguage, library_check);
  255. }
  256. return locale || 'en';
  257. };
  258. if (!moment.locale) { //moment.lang is deprecated after 2.8.1, use moment.locale instead
  259. moment.locale = moment.lang;
  260. }
  261. moment.locale(this.detectLocale(this.isMomentLocale));
  262. this.i18n = settings.i18n ? settings.i18n : locales[this.detectLocale(this.isConverseLocale)];
  263. // Translation machinery
  264. // ---------------------
  265. var __ = utils.__.bind(this);
  266. var ___ = utils.___;
  267. // Default configuration values
  268. // ----------------------------
  269. this.default_settings = {
  270. allow_chat_pending_contacts: false,
  271. allow_contact_removal: true,
  272. allow_contact_requests: true,
  273. allow_dragresize: true,
  274. allow_logout: true,
  275. allow_muc: true,
  276. allow_otr: true,
  277. allow_registration: true,
  278. animate: true,
  279. archived_messages_page_size: '20',
  280. authentication: 'login', // Available values are "login", "prebind", "anonymous".
  281. auto_away: 0, // Seconds after which user status is set to 'away'
  282. auto_join_on_invite: false, // Auto-join chatroom on invite
  283. auto_list_rooms: false,
  284. auto_login: false, // Currently only used in connection with anonymous login
  285. auto_reconnect: false,
  286. auto_subscribe: false,
  287. auto_xa: 0, // Seconds after which user status is set to 'xa'
  288. bosh_service_url: undefined, // The BOSH connection manager URL.
  289. cache_otr_key: false,
  290. csi_waiting_time: 0, // Support for XEP-0352. Seconds before client is considered idle and CSI is sent out.
  291. debug: false,
  292. default_domain: undefined,
  293. domain_placeholder: __(" e.g. conversejs.org"), // Placeholder text shown in the domain input on the registration form
  294. expose_rid_and_sid: false,
  295. forward_messages: false,
  296. hide_muc_server: false,
  297. hide_offline_users: false,
  298. include_offline_state: false,
  299. jid: undefined,
  300. keepalive: false,
  301. locked_domain: undefined,
  302. message_archiving: 'never', // Supported values are 'always', 'never', 'roster' (See https://xmpp.org/extensions/xep-0313.html#prefs )
  303. message_carbons: false, // Support for XEP-280
  304. muc_history_max_stanzas: undefined, // Takes an integer, limits the amount of messages to fetch from chat room's history
  305. no_trimming: false, // Set to true for phantomjs tests (where browser apparently has no width)
  306. password: undefined,
  307. ping_interval: 180, //in seconds
  308. play_sounds: false,
  309. prebind: false, // XXX: Deprecated, use "authentication" instead.
  310. prebind_url: null,
  311. providers_link: 'https://xmpp.net/directory.php', // Link to XMPP providers shown on registration page
  312. rid: undefined,
  313. roster_groups: false,
  314. show_controlbox_by_default: false,
  315. show_only_online_users: false,
  316. show_toolbar: true,
  317. sid: undefined,
  318. sounds_path: '/sounds/',
  319. storage: 'session',
  320. use_otr_by_default: false,
  321. use_vcards: true,
  322. visible_toolbar_buttons: {
  323. 'emoticons': true,
  324. 'call': false,
  325. 'clear': true,
  326. 'toggle_occupants': true
  327. },
  328. websocket_url: undefined,
  329. xhr_custom_status: false,
  330. xhr_custom_status_url: '',
  331. xhr_user_search: false,
  332. xhr_user_search_url: ''
  333. };
  334. _.extend(this, this.default_settings);
  335. // Allow only whitelisted configuration attributes to be overwritten
  336. _.extend(this, _.pick(settings, Object.keys(this.default_settings)));
  337. // BBB
  338. if (this.prebind === true) { this.authentication = PREBIND; }
  339. if (this.authentication === ANONYMOUS) {
  340. if (!this.jid) {
  341. throw("Config Error: you need to provide the server's domain via the " +
  342. "'jid' option when using anonymous authentication.");
  343. }
  344. }
  345. if (settings.visible_toolbar_buttons) {
  346. _.extend(
  347. this.visible_toolbar_buttons,
  348. _.pick(settings.visible_toolbar_buttons, [
  349. 'emoticons', 'call', 'clear', 'toggle_occupants'
  350. ]
  351. ));
  352. }
  353. $.fx.off = !this.animate;
  354. // Only allow OTR if we have the capability
  355. this.allow_otr = this.allow_otr && HAS_CRYPTO;
  356. // Only use OTR by default if allow OTR is enabled to begin with
  357. this.use_otr_by_default = this.use_otr_by_default && this.allow_otr;
  358. // Translation aware constants
  359. // ---------------------------
  360. var OTR_CLASS_MAPPING = {};
  361. OTR_CLASS_MAPPING[UNENCRYPTED] = 'unencrypted';
  362. OTR_CLASS_MAPPING[UNVERIFIED] = 'unverified';
  363. OTR_CLASS_MAPPING[VERIFIED] = 'verified';
  364. OTR_CLASS_MAPPING[FINISHED] = 'finished';
  365. var OTR_TRANSLATED_MAPPING = {};
  366. OTR_TRANSLATED_MAPPING[UNENCRYPTED] = __('unencrypted');
  367. OTR_TRANSLATED_MAPPING[UNVERIFIED] = __('unverified');
  368. OTR_TRANSLATED_MAPPING[VERIFIED] = __('verified');
  369. OTR_TRANSLATED_MAPPING[FINISHED] = __('finished');
  370. var STATUSES = {
  371. 'dnd': __('This contact is busy'),
  372. 'online': __('This contact is online'),
  373. 'offline': __('This contact is offline'),
  374. 'unavailable': __('This contact is unavailable'),
  375. 'xa': __('This contact is away for an extended period'),
  376. 'away': __('This contact is away')
  377. };
  378. var DESC_GROUP_TOGGLE = __('Click to hide these contacts');
  379. var HEADER_CURRENT_CONTACTS = __('My contacts');
  380. var HEADER_PENDING_CONTACTS = __('Pending contacts');
  381. var HEADER_REQUESTING_CONTACTS = __('Contact requests');
  382. var HEADER_UNGROUPED = __('Ungrouped');
  383. var LABEL_CONTACTS = __('Contacts');
  384. var LABEL_GROUPS = __('Groups');
  385. var HEADER_WEIGHTS = {};
  386. HEADER_WEIGHTS[HEADER_CURRENT_CONTACTS] = 0;
  387. HEADER_WEIGHTS[HEADER_UNGROUPED] = 1;
  388. HEADER_WEIGHTS[HEADER_REQUESTING_CONTACTS] = 2;
  389. HEADER_WEIGHTS[HEADER_PENDING_CONTACTS] = 3;
  390. // Module-level variables
  391. // ----------------------
  392. this.callback = callback || function () {};
  393. /* When reloading the page:
  394. * For new sessions, we need to send out a presence stanza to notify
  395. * the server/network that we're online.
  396. * When re-attaching to an existing session (e.g. via the keepalive
  397. * option), we don't need to again send out a presence stanza, because
  398. * it's as if "we never left" (see onConnectStatusChanged).
  399. * https://github.com/jcbrand/converse.js/issues/521
  400. */
  401. this.send_initial_presence = true;
  402. this.msg_counter = 0;
  403. this.reconnectTimeout = undefined;
  404. // Module-level functions
  405. // ----------------------
  406. this.sendCSI = function (stat) {
  407. /* Send out a Chat Status Notification (XEP-0352) */
  408. if (converse.features[Strophe.NS.CSI] || true) {
  409. converse.connection.send($build(stat, {xmlns: Strophe.NS.CSI}));
  410. this.inactive = (stat === INACTIVE) ? true : false;
  411. }
  412. };
  413. this.onUserActivity = function () {
  414. /* Resets counters and flags relating to CSI and auto_away/auto_xa */
  415. if (this.idle_seconds > 0) {
  416. this.idle_seconds = 0;
  417. }
  418. if (!converse.connection.authenticated) {
  419. // We can't send out any stanzas when there's no authenticated connection.
  420. // This can happen when the connection reconnects.
  421. return;
  422. }
  423. if (this.inactive) {
  424. this.sendCSI(ACTIVE);
  425. }
  426. if (this.auto_changed_status === true) {
  427. this.auto_changed_status = false;
  428. this.xmppstatus.setStatus('online');
  429. }
  430. };
  431. this.onEverySecond = function () {
  432. /* An interval handler running every second.
  433. * Used for CSI and the auto_away and auto_xa
  434. * features.
  435. */
  436. if (!converse.connection.authenticated) {
  437. // We can't send out any stanzas when there's no authenticated connection.
  438. // This can happen when the connection reconnects.
  439. return;
  440. }
  441. var stat = this.xmppstatus.getStatus();
  442. this.idle_seconds++;
  443. if (this.csi_waiting_time > 0 && this.idle_seconds > this.csi_waiting_time && !this.inactive) {
  444. this.sendCSI(INACTIVE);
  445. }
  446. if (this.auto_away > 0 && this.idle_seconds > this.auto_away && stat !== 'away' && stat !== 'xa') {
  447. this.auto_changed_status = true;
  448. this.xmppstatus.setStatus('away');
  449. } else if (this.auto_xa > 0 && this.idle_seconds > this.auto_xa && stat !== 'xa') {
  450. this.auto_changed_status = true;
  451. this.xmppstatus.setStatus('xa');
  452. }
  453. };
  454. this.registerIntervalHandler = function () {
  455. /* Set an interval of one second and register a handler for it.
  456. * Required for the auto_away, auto_xa and csi_waiting_time features.
  457. */
  458. if (this.auto_away < 1 && this.auto_xa < 1 && this.csi_waiting_time < 1) {
  459. // Waiting time of less then one second means features aren't used.
  460. return;
  461. }
  462. this.idle_seconds = 0;
  463. this.auto_changed_status = false; // Was the user's status changed by converse.js?
  464. $(window).on('click mousemove keypress focus'+unloadevent , this.onUserActivity.bind(this));
  465. window.setInterval(this.onEverySecond.bind(this), 1000);
  466. };
  467. this.playNotification = function () {
  468. var audio;
  469. if (converse.play_sounds && typeof Audio !== "undefined") {
  470. audio = new Audio(converse.sounds_path+"msg_received.ogg");
  471. if (audio.canPlayType('/audio/ogg')) {
  472. audio.play();
  473. } else {
  474. audio = new Audio(converse.sounds_path+"msg_received.mp3");
  475. audio.play();
  476. }
  477. }
  478. };
  479. this.giveFeedback = function (message, klass) {
  480. $('.conn-feedback').each(function (idx, el) {
  481. var $el = $(el);
  482. $el.addClass('conn-feedback').text(message);
  483. if (klass) {
  484. $el.addClass(klass);
  485. } else {
  486. $el.removeClass('error');
  487. }
  488. });
  489. };
  490. this.log = function (txt, level) {
  491. var logger;
  492. if (typeof console === "undefined" || typeof console.log === "undefined") {
  493. logger = { log: function () {}, error: function () {} };
  494. } else {
  495. logger = console;
  496. }
  497. if (this.debug) {
  498. if (level === 'error') {
  499. logger.log('ERROR: '+txt);
  500. } else {
  501. logger.log(txt);
  502. }
  503. }
  504. };
  505. this.rejectPresenceSubscription = function (jid, message) {
  506. /* Reject or cancel another user's subscription to our presence updates.
  507. * Parameters:
  508. * (String) jid - The Jabber ID of the user whose subscription
  509. * is being canceled.
  510. * (String) message - An optional message to the user
  511. */
  512. var pres = $pres({to: jid, type: "unsubscribed"});
  513. if (message && message !== "") { pres.c("status").t(message); }
  514. converse.connection.send(pres);
  515. };
  516. this.getVCard = function (jid, callback, errback) {
  517. /* Request the VCard of another user.
  518. *
  519. * Parameters:
  520. * (String) jid - The Jabber ID of the user whose VCard is being requested.
  521. * (Function) callback - A function to call once the VCard is returned
  522. * (Function) errback - A function to call if an error occured
  523. * while trying to fetch the VCard.
  524. */
  525. if (!this.use_vcards) {
  526. if (callback) { callback(jid, jid); }
  527. return;
  528. }
  529. converse.connection.vcard.get(
  530. function (iq) { // Successful callback
  531. var $vcard = $(iq).find('vCard');
  532. var fullname = $vcard.find('FN').text(),
  533. img = $vcard.find('BINVAL').text(),
  534. img_type = $vcard.find('TYPE').text(),
  535. url = $vcard.find('URL').text();
  536. if (jid) {
  537. var contact = converse.roster.get(jid);
  538. if (contact) {
  539. fullname = _.isEmpty(fullname)? contact.get('fullname') || jid: fullname;
  540. contact.save({
  541. 'fullname': fullname,
  542. 'image_type': img_type,
  543. 'image': img,
  544. 'url': url,
  545. 'vcard_updated': moment().format()
  546. });
  547. }
  548. }
  549. if (callback) { callback(iq, jid, fullname, img, img_type, url); }
  550. }.bind(this),
  551. jid,
  552. function (iq) { // Error callback
  553. var contact = converse.roster.get(jid);
  554. if (contact) {
  555. contact.save({ 'vcard_updated': moment().format() });
  556. }
  557. if (errback) { errback(iq, jid); }
  558. }
  559. );
  560. };
  561. this.reconnect = function (condition) {
  562. converse.log('Attempting to reconnect in 5 seconds');
  563. converse.giveFeedback(__('Attempting to reconnect in 5 seconds'), 'error');
  564. clearTimeout(converse.reconnectTimeout);
  565. converse.reconnectTimeout = setTimeout(function () {
  566. if (converse.authentication !== "prebind") {
  567. this.connection.connect(
  568. this.connection.jid,
  569. this.connection.pass,
  570. function (status, condition) {
  571. this.onConnectStatusChanged(status, condition, true);
  572. }.bind(this),
  573. this.connection.wait,
  574. this.connection.hold,
  575. this.connection.route
  576. );
  577. } else if (converse.prebind_url) {
  578. this.clearSession();
  579. this._tearDown();
  580. this.startNewBOSHSession();
  581. }
  582. }.bind(this), 5000);
  583. };
  584. this.renderLoginPanel = function () {
  585. converse._tearDown();
  586. var view = converse.chatboxviews.get('controlbox');
  587. view.model.set({connected:false});
  588. view.renderLoginPanel();
  589. };
  590. this.onConnectStatusChanged = function (status, condition, reconnect) {
  591. converse.log("Status changed to: "+PRETTY_CONNECTION_STATUS[status]);
  592. if (status === Strophe.Status.CONNECTED || status === Strophe.Status.ATTACHED) {
  593. // By default we always want to send out an initial presence stanza.
  594. converse.send_initial_presence = true;
  595. delete converse.disconnection_cause;
  596. if (!!converse.reconnectTimeout) {
  597. clearTimeout(converse.reconnectTimeout);
  598. delete converse.reconnectTimeout;
  599. }
  600. if ((typeof reconnect !== 'undefined') && (reconnect)) {
  601. converse.log(status === Strophe.Status.CONNECTED ? 'Reconnected' : 'Reattached');
  602. converse.onReconnected();
  603. } else {
  604. converse.log(status === Strophe.Status.CONNECTED ? 'Connected' : 'Attached');
  605. if (converse.connection.restored) {
  606. converse.send_initial_presence = false; // No need to send an initial presence stanza when
  607. // we're restoring an existing session.
  608. }
  609. converse.onConnected();
  610. }
  611. } else if (status === Strophe.Status.DISCONNECTED) {
  612. if (converse.disconnection_cause === Strophe.Status.CONNFAIL && converse.auto_reconnect) {
  613. converse.reconnect(condition);
  614. } else {
  615. converse.renderLoginPanel();
  616. }
  617. } else if (status === Strophe.Status.ERROR) {
  618. converse.giveFeedback(__('Error'), 'error');
  619. } else if (status === Strophe.Status.CONNECTING) {
  620. converse.giveFeedback(__('Connecting'));
  621. } else if (status === Strophe.Status.AUTHENTICATING) {
  622. converse.giveFeedback(__('Authenticating'));
  623. } else if (status === Strophe.Status.AUTHFAIL) {
  624. converse.giveFeedback(__('Authentication Failed'), 'error');
  625. converse.connection.disconnect(__('Authentication Failed'));
  626. converse.disconnection_cause = Strophe.Status.AUTHFAIL;
  627. } else if (status === Strophe.Status.CONNFAIL) {
  628. converse.disconnection_cause = Strophe.Status.CONNFAIL;
  629. } else if (status === Strophe.Status.DISCONNECTING) {
  630. // FIXME: what about prebind?
  631. if (!converse.connection.connected) {
  632. converse.renderLoginPanel();
  633. }
  634. if (condition) {
  635. converse.giveFeedback(condition, 'error');
  636. }
  637. }
  638. };
  639. this.applyDragResistance = function (value, default_value) {
  640. /* This method applies some resistance around the
  641. * default_value. If value is close enough to
  642. * default_value, then default_value is returned instead.
  643. */
  644. if (typeof value === 'undefined') {
  645. return undefined;
  646. } else if (typeof default_value === 'undefined') {
  647. return value;
  648. }
  649. var resistance = 10;
  650. if ((value !== default_value) &&
  651. (Math.abs(value- default_value) < resistance)) {
  652. return default_value;
  653. }
  654. return value;
  655. };
  656. this.updateMsgCounter = function () {
  657. if (this.msg_counter > 0) {
  658. if (document.title.search(/^Messages \(\d+\) /) === -1) {
  659. document.title = "Messages (" + this.msg_counter + ") " + document.title;
  660. } else {
  661. document.title = document.title.replace(/^Messages \(\d+\) /, "Messages (" + this.msg_counter + ") ");
  662. }
  663. window.blur();
  664. window.focus();
  665. } else if (document.title.search(/^Messages \(\d+\) /) !== -1) {
  666. document.title = document.title.replace(/^Messages \(\d+\) /, "");
  667. }
  668. };
  669. this.incrementMsgCounter = function () {
  670. this.msg_counter += 1;
  671. this.updateMsgCounter();
  672. };
  673. this.clearMsgCounter = function () {
  674. this.msg_counter = 0;
  675. this.updateMsgCounter();
  676. };
  677. this.initStatus = function (callback) {
  678. this.xmppstatus = new this.XMPPStatus();
  679. var id = b64_sha1('converse.xmppstatus-'+converse.bare_jid);
  680. this.xmppstatus.id = id; // Appears to be necessary for backbone.browserStorage
  681. this.xmppstatus.browserStorage = new Backbone.BrowserStorage[converse.storage](id);
  682. this.xmppstatus.fetch({success: callback, error: callback});
  683. };
  684. this.initSession = function () {
  685. this.session = new this.Session();
  686. var id = b64_sha1('converse.bosh-session');
  687. this.session.id = id; // Appears to be necessary for backbone.browserStorage
  688. this.session.browserStorage = new Backbone.BrowserStorage[converse.storage](id);
  689. this.session.fetch();
  690. };
  691. this.clearSession = function () {
  692. if (this.roster) {
  693. this.roster.browserStorage._clear();
  694. }
  695. this.session.browserStorage._clear();
  696. if (converse.connection.connected) {
  697. converse.chatboxes.get('controlbox').save({'connected': false});
  698. }
  699. };
  700. this.logOut = function () {
  701. converse.chatboxviews.closeAllChatBoxes(false);
  702. converse.clearSession();
  703. converse.connection.disconnect();
  704. };
  705. this.registerGlobalEventHandlers = function () {
  706. $(document).click(function () {
  707. if ($('.toggle-otr ul').is(':visible')) {
  708. $('.toggle-otr ul', this).slideUp();
  709. }
  710. if ($('.toggle-smiley ul').is(':visible')) {
  711. $('.toggle-smiley ul', this).slideUp();
  712. }
  713. });
  714. $(document).on('mousemove', function (ev) {
  715. if (!this.resizing || !this.allow_dragresize) { return true; }
  716. ev.preventDefault();
  717. this.resizing.chatbox.resizeChatBox(ev);
  718. }.bind(this));
  719. $(document).on('mouseup', function (ev) {
  720. if (!this.resizing || !this.allow_dragresize) { return true; }
  721. ev.preventDefault();
  722. var height = this.applyDragResistance(
  723. this.resizing.chatbox.height,
  724. this.resizing.chatbox.model.get('default_height')
  725. );
  726. var width = this.applyDragResistance(
  727. this.resizing.chatbox.width,
  728. this.resizing.chatbox.model.get('default_width')
  729. );
  730. if (this.connection.connected) {
  731. this.resizing.chatbox.model.save({'height': height});
  732. this.resizing.chatbox.model.save({'width': width});
  733. } else {
  734. this.resizing.chatbox.model.set({'height': height});
  735. this.resizing.chatbox.model.set({'width': width});
  736. }
  737. this.resizing = null;
  738. }.bind(this));
  739. $(window).on("blur focus", function (ev) {
  740. if ((this.windowState !== ev.type) && (ev.type === 'focus')) {
  741. converse.clearMsgCounter();
  742. }
  743. this.windowState = ev.type;
  744. }.bind(this));
  745. $(window).on("resize", _.debounce(function (ev) {
  746. this.chatboxviews.trimChats();
  747. }.bind(this), 200));
  748. };
  749. this.ping = function (jid, success, error, timeout) {
  750. // XXX: We could first check here if the server advertised that it supports PING.
  751. // However, some servers don't advertise while still keeping the
  752. // connection option due to pings.
  753. //
  754. // var feature = converse.features.findWhere({'var': Strophe.NS.PING});
  755. converse.lastStanzaDate = new Date();
  756. if (typeof jid === 'undefined' || jid === null) {
  757. jid = Strophe.getDomainFromJid(converse.bare_jid);
  758. }
  759. if (typeof timeout === 'undefined' ) { timeout = null; }
  760. if (typeof success === 'undefined' ) { success = null; }
  761. if (typeof error === 'undefined' ) { error = null; }
  762. if (converse.connection) {
  763. converse.connection.ping.ping(jid, success, error, timeout);
  764. return true;
  765. }
  766. return false;
  767. };
  768. this.pong = function (ping) {
  769. converse.lastStanzaDate = new Date();
  770. converse.connection.ping.pong(ping);
  771. return true;
  772. };
  773. this.registerPongHandler = function () {
  774. converse.connection.disco.addFeature(Strophe.NS.PING);
  775. converse.connection.ping.addPingHandler(this.pong);
  776. };
  777. this.registerPingHandler = function () {
  778. this.registerPongHandler();
  779. if (this.ping_interval > 0) {
  780. this.connection.addHandler(function () {
  781. /* Handler on each stanza, saves the received date
  782. * in order to ping only when needed.
  783. */
  784. this.lastStanzaDate = new Date();
  785. return true;
  786. }.bind(converse));
  787. this.connection.addTimedHandler(1000, function () {
  788. var now = new Date();
  789. if (!this.lastStanzaDate) {
  790. this.lastStanzaDate = now;
  791. }
  792. if ((now - this.lastStanzaDate)/1000 > this.ping_interval) {
  793. return this.ping();
  794. }
  795. return true;
  796. }.bind(converse));
  797. }
  798. };
  799. this.onReconnected = function () {
  800. // We need to re-register all the event handlers on the newly
  801. // created connection.
  802. this.initStatus(function () {
  803. this.registerPingHandler();
  804. this.rosterview.registerRosterXHandler();
  805. this.rosterview.registerPresenceHandler();
  806. this.chatboxes.registerMessageHandler();
  807. this.xmppstatus.sendPresence();
  808. this.giveFeedback(__('Contacts'));
  809. }.bind(this));
  810. };
  811. this.enableCarbons = function () {
  812. /* Ask the XMPP server to enable Message Carbons
  813. * See XEP-0280 https://xmpp.org/extensions/xep-0280.html#enabling
  814. */
  815. if (!this.message_carbons || this.session.get('carbons_enabled')) {
  816. return;
  817. }
  818. var carbons_iq = new Strophe.Builder('iq', {
  819. from: this.connection.jid,
  820. id: 'enablecarbons',
  821. type: 'set'
  822. })
  823. .c('enable', {xmlns: Strophe.NS.CARBONS});
  824. this.connection.addHandler(function (iq) {
  825. if ($(iq).find('error').length > 0) {
  826. converse.log('ERROR: An error occured while trying to enable message carbons.');
  827. } else {
  828. this.session.save({carbons_enabled: true});
  829. converse.log('Message carbons have been enabled.');
  830. }
  831. }.bind(this), null, "iq", null, "enablecarbons");
  832. this.connection.send(carbons_iq);
  833. };
  834. this.onConnected = function () {
  835. // When reconnecting, there might be some open chat boxes. We don't
  836. // know whether these boxes are of the same account or not, so we
  837. // close them now.
  838. this.chatboxviews.closeAllChatBoxes();
  839. this.jid = this.connection.jid;
  840. this.bare_jid = Strophe.getBareJidFromJid(this.connection.jid);
  841. this.resource = Strophe.getResourceFromJid(this.connection.jid);
  842. this.domain = Strophe.getDomainFromJid(this.connection.jid);
  843. this.minimized_chats = new converse.MinimizedChats({model: this.chatboxes});
  844. this.features = new this.Features();
  845. this.enableCarbons();
  846. this.initStatus(function () {
  847. this.registerPingHandler();
  848. this.registerIntervalHandler();
  849. this.chatboxes.onConnected();
  850. this.giveFeedback(__('Contacts'));
  851. if (this.callback) {
  852. if (this.connection.service === 'jasmine tests') {
  853. // XXX: Call back with the internal converse object. This
  854. // object should never be exposed to production systems.
  855. // 'jasmine tests' is an invalid http bind service value,
  856. // so we're sure that this is just for tests.
  857. this.callback(this);
  858. } else {
  859. this.callback();
  860. }
  861. }
  862. }.bind(this));
  863. converse.emit('ready');
  864. };
  865. // Backbone Models and Views
  866. // -------------------------
  867. this.OTR = Backbone.Model.extend({
  868. // A model for managing OTR settings.
  869. getSessionPassphrase: function () {
  870. if (converse.authentication === 'prebind') {
  871. var key = b64_sha1(converse.connection.jid),
  872. pass = window.sessionStorage[key];
  873. if (typeof pass === 'undefined') {
  874. pass = Math.floor(Math.random()*4294967295).toString();
  875. window.sessionStorage[key] = pass;
  876. }
  877. return pass;
  878. } else {
  879. return converse.connection.pass;
  880. }
  881. },
  882. generatePrivateKey: function (instance_tag) {
  883. var key = new DSA();
  884. var jid = converse.connection.jid;
  885. if (converse.cache_otr_key) {
  886. var cipher = CryptoJS.lib.PasswordBasedCipher;
  887. var pass = this.getSessionPassphrase();
  888. if (typeof pass !== "undefined") {
  889. // Encrypt the key and set in sessionStorage. Also store instance tag.
  890. window.sessionStorage[b64_sha1(jid+'priv_key')] =
  891. cipher.encrypt(CryptoJS.algo.AES, key.packPrivate(), pass).toString();
  892. window.sessionStorage[b64_sha1(jid+'instance_tag')] = instance_tag;
  893. window.sessionStorage[b64_sha1(jid+'pass_check')] =
  894. cipher.encrypt(CryptoJS.algo.AES, 'match', pass).toString();
  895. }
  896. }
  897. return key;
  898. }
  899. });
  900. this.Message = Backbone.Model.extend({
  901. idAttribute: 'msgid',
  902. defaults: function(){
  903. return {
  904. msgid: converse.connection.getUniqueId()
  905. };
  906. }
  907. });
  908. this.Messages = Backbone.Collection.extend({
  909. model: converse.Message,
  910. comparator: 'time'
  911. });
  912. this.ChatBox = Backbone.Model.extend({
  913. initialize: function () {
  914. var height = this.get('height'),
  915. width = this.get('width'),
  916. settings = {
  917. 'height': converse.applyDragResistance(height, this.get('default_height')),
  918. 'width': converse.applyDragResistance(width, this.get('default_width')),
  919. 'num_unread': this.get('num_unread') || 0
  920. };
  921. if (this.get('box_id') !== 'controlbox') {
  922. this.messages = new converse.Messages();
  923. this.messages.browserStorage = new Backbone.BrowserStorage[converse.storage](
  924. b64_sha1('converse.messages'+this.get('jid')+converse.bare_jid));
  925. this.save(_.extend(settings, {
  926. // The chat_state will be set to ACTIVE once the chat box is opened
  927. // and we listen for change:chat_state, so shouldn't set it to ACTIVE here.
  928. 'chat_state': undefined,
  929. 'box_id' : b64_sha1(this.get('jid')),
  930. 'minimized': this.get('minimized') || false,
  931. 'otr_status': this.get('otr_status') || UNENCRYPTED,
  932. 'time_minimized': this.get('time_minimized') || moment(),
  933. 'time_opened': this.get('time_opened') || moment().valueOf(),
  934. 'url': '',
  935. 'user_id' : Strophe.getNodeFromJid(this.get('jid'))
  936. }));
  937. } else {
  938. this.set(_.extend(settings, { 'time_opened': moment(0).valueOf() }));
  939. }
  940. },
  941. maximize: function () {
  942. this.save({
  943. 'minimized': false,
  944. 'time_opened': moment().valueOf()
  945. });
  946. },
  947. minimize: function () {
  948. this.save({
  949. 'minimized': true,
  950. 'time_minimized': moment().format()
  951. });
  952. },
  953. getSession: function (callback) {
  954. var cipher = CryptoJS.lib.PasswordBasedCipher;
  955. var pass, instance_tag, saved_key, pass_check;
  956. if (converse.cache_otr_key) {
  957. pass = converse.otr.getSessionPassphrase();
  958. if (typeof pass !== "undefined") {
  959. instance_tag = window.sessionStorage[b64_sha1(this.id+'instance_tag')];
  960. saved_key = window.sessionStorage[b64_sha1(this.id+'priv_key')];
  961. pass_check = window.sessionStorage[b64_sha1(this.connection.jid+'pass_check')];
  962. if (saved_key && instance_tag && typeof pass_check !== 'undefined') {
  963. var decrypted = cipher.decrypt(CryptoJS.algo.AES, saved_key, pass);
  964. var key = DSA.parsePrivate(decrypted.toString(CryptoJS.enc.Latin1));
  965. if (cipher.decrypt(CryptoJS.algo.AES, pass_check, pass).toString(CryptoJS.enc.Latin1) === 'match') {
  966. // Verified that the passphrase is still the same
  967. this.trigger('showHelpMessages', [__('Re-establishing encrypted session')]);
  968. callback({
  969. 'key': key,
  970. 'instance_tag': instance_tag
  971. });
  972. return; // Our work is done here
  973. }
  974. }
  975. }
  976. }
  977. // We need to generate a new key and instance tag
  978. this.trigger('showHelpMessages', [
  979. __('Generating private key.'),
  980. __('Your browser might become unresponsive.')],
  981. null,
  982. true // show spinner
  983. );
  984. setTimeout(function () {
  985. var instance_tag = OTR.makeInstanceTag();
  986. callback({
  987. 'key': converse.otr.generatePrivateKey.call(this, instance_tag),
  988. 'instance_tag': instance_tag
  989. });
  990. }, 500);
  991. },
  992. updateOTRStatus: function (state) {
  993. switch (state) {
  994. case OTR.CONST.STATUS_AKE_SUCCESS:
  995. if (this.otr.msgstate === OTR.CONST.MSGSTATE_ENCRYPTED) {
  996. this.save({'otr_status': UNVERIFIED});
  997. }
  998. break;
  999. case OTR.CONST.STATUS_END_OTR:
  1000. if (this.otr.msgstate === OTR.CONST.MSGSTATE_FINISHED) {
  1001. this.save({'otr_status': FINISHED});
  1002. } else if (this.otr.msgstate === OTR.CONST.MSGSTATE_PLAINTEXT) {
  1003. this.save({'otr_status': UNENCRYPTED});
  1004. }
  1005. break;
  1006. }
  1007. },
  1008. onSMP: function (type, data) {
  1009. // Event handler for SMP (Socialist's Millionaire Protocol)
  1010. // used by OTR (off-the-record).
  1011. switch (type) {
  1012. case 'question':
  1013. this.otr.smpSecret(prompt(__(
  1014. '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',
  1015. [this.get('fullname'), data])));
  1016. break;
  1017. case 'trust':
  1018. if (data === true) {
  1019. this.save({'otr_status': VERIFIED});
  1020. } else {
  1021. this.trigger(
  1022. 'showHelpMessages',
  1023. [__("Could not verify this user's identify.")],
  1024. 'error');
  1025. this.save({'otr_status': UNVERIFIED});
  1026. }
  1027. break;
  1028. default:
  1029. throw new TypeError('ChatBox.onSMP: Unknown type for SMP');
  1030. }
  1031. },
  1032. initiateOTR: function (query_msg) {
  1033. // Sets up an OTR object through which we can send and receive
  1034. // encrypted messages.
  1035. //
  1036. // If 'query_msg' is passed in, it means there is an alread incoming
  1037. // query message from our contact. Otherwise, it is us who will
  1038. // send the query message to them.
  1039. this.save({'otr_status': UNENCRYPTED});
  1040. this.getSession(function (session) {
  1041. this.otr = new OTR({
  1042. fragment_size: 140,
  1043. send_interval: 200,
  1044. priv: session.key,
  1045. instance_tag: session.instance_tag,
  1046. debug: this.debug
  1047. });
  1048. this.otr.on('status', this.updateOTRStatus.bind(this));
  1049. this.otr.on('smp', this.onSMP.bind(this));
  1050. this.otr.on('ui', function (msg) {
  1051. this.trigger('showReceivedOTRMessage', msg);
  1052. }.bind(this));
  1053. this.otr.on('io', function (msg) {
  1054. this.trigger('sendMessage', new converse.Message({ message: msg }));
  1055. }.bind(this));
  1056. this.otr.on('error', function (msg) {
  1057. this.trigger('showOTRError', msg);
  1058. }.bind(this));
  1059. this.trigger('showHelpMessages', [__('Exchanging private key with contact.')]);
  1060. if (query_msg) {
  1061. this.otr.receiveMsg(query_msg);
  1062. } else {
  1063. this.otr.sendQueryMsg();
  1064. }
  1065. }.bind(this));
  1066. },
  1067. endOTR: function () {
  1068. if (this.otr) {
  1069. this.otr.endOtr();
  1070. }
  1071. this.save({'otr_status': UNENCRYPTED});
  1072. },
  1073. createMessage: function ($message, $delay, archive_id) {
  1074. $delay = $delay || $message.find('delay');
  1075. var body = $message.children('body').text(),
  1076. delayed = $delay.length > 0,
  1077. fullname = this.get('fullname'),
  1078. is_groupchat = $message.attr('type') === 'groupchat',
  1079. msgid = $message.attr('id'),
  1080. chat_state = $message.find(COMPOSING).length && COMPOSING ||
  1081. $message.find(PAUSED).length && PAUSED ||
  1082. $message.find(INACTIVE).length && INACTIVE ||
  1083. $message.find(ACTIVE).length && ACTIVE ||
  1084. $message.find(GONE).length && GONE,
  1085. stamp, time, sender, from;
  1086. if (is_groupchat) {
  1087. from = Strophe.unescapeNode(Strophe.getResourceFromJid($message.attr('from')));
  1088. } else {
  1089. from = Strophe.getBareJidFromJid($message.attr('from'));
  1090. }
  1091. fullname = (_.isEmpty(fullname) ? from: fullname).split(' ')[0];
  1092. if (delayed) {
  1093. stamp = $delay.attr('stamp');
  1094. time = stamp;
  1095. } else {
  1096. time = moment().format();
  1097. }
  1098. if ((is_groupchat && from === this.get('nick')) || (!is_groupchat && from === converse.bare_jid)) {
  1099. sender = 'me';
  1100. } else {
  1101. sender = 'them';
  1102. }
  1103. this.messages.create({
  1104. chat_state: chat_state,
  1105. delayed: delayed,
  1106. fullname: fullname,
  1107. message: body || undefined,
  1108. msgid: msgid,
  1109. sender: sender,
  1110. time: time,
  1111. archive_id: archive_id
  1112. });
  1113. },
  1114. receiveMessage: function ($message, $delay, archive_id) {
  1115. var $body = $message.children('body');
  1116. var text = ($body.length > 0 ? $body.text() : undefined);
  1117. if ((!text) || (!converse.allow_otr)) {
  1118. return this.createMessage($message, $delay, archive_id);
  1119. }
  1120. if (text.match(/^\?OTRv23?/)) {
  1121. this.initiateOTR(text);
  1122. } else {
  1123. if (_.contains([UNVERIFIED, VERIFIED], this.get('otr_status'))) {
  1124. this.otr.receiveMsg(text);
  1125. } else {
  1126. if (text.match(/^\?OTR/)) {
  1127. if (!this.otr) {
  1128. this.initiateOTR(text);
  1129. } else {
  1130. this.otr.receiveMsg(text);
  1131. }
  1132. } else {
  1133. // Normal unencrypted message.
  1134. this.createMessage($message, $delay, archive_id);
  1135. }
  1136. }
  1137. }
  1138. }
  1139. });
  1140. this.ChatBoxView = Backbone.View.extend({
  1141. length: 200,
  1142. tagName: 'div',
  1143. className: 'chatbox',
  1144. is_chatroom: false, // This is not a multi-user chatroom
  1145. events: {
  1146. 'click .close-chatbox-button': 'close',
  1147. 'click .toggle-chatbox-button': 'minimize',
  1148. 'keypress textarea.chat-textarea': 'keyPressed',
  1149. 'click .toggle-smiley': 'toggleEmoticonMenu',
  1150. 'click .toggle-smiley ul li': 'insertEmoticon',
  1151. 'click .toggle-clear': 'clearMessages',
  1152. 'click .toggle-otr': 'toggleOTRMenu',
  1153. 'click .start-otr': 'startOTRFromToolbar',
  1154. 'click .end-otr': 'endOTR',
  1155. 'click .auth-otr': 'authOTR',
  1156. 'click .toggle-call': 'toggleCall',
  1157. 'mousedown .dragresize-top': 'onStartVerticalResize',
  1158. 'mousedown .dragresize-left': 'onStartHorizontalResize',
  1159. 'mousedown .dragresize-topleft': 'onStartDiagonalResize'
  1160. },
  1161. initialize: function () {
  1162. $(window).on('resize', _.debounce(this.setDimensions.bind(this), 100));
  1163. this.model.messages.on('add', this.onMessageAdded, this);
  1164. this.model.on('show', this.show, this);
  1165. this.model.on('destroy', this.hide, this);
  1166. // TODO check for changed fullname as well
  1167. this.model.on('change:chat_state', this.sendChatState, this);
  1168. this.model.on('change:chat_status', this.onChatStatusChanged, this);
  1169. this.model.on('change:image', this.renderAvatar, this);
  1170. this.model.on('change:otr_status', this.onOTRStatusChanged, this);
  1171. this.model.on('change:minimized', this.onMinimizedChanged, this);
  1172. this.model.on('change:status', this.onStatusChanged, this);
  1173. this.model.on('showOTRError', this.showOTRError, this);
  1174. this.model.on('showHelpMessages', this.showHelpMessages, this);
  1175. this.model.on('sendMessage', this.sendMessage, this);
  1176. this.model.on('showSentOTRMessage', function (text) {
  1177. this.showMessage({'message': text, 'sender': 'me'});
  1178. }, this);
  1179. this.model.on('showReceivedOTRMessage', function (text) {
  1180. this.showMessage({'message': text, 'sender': 'them'});
  1181. }, this);
  1182. this.updateVCard().render().fetchMessages().insertIntoPage().hide();
  1183. if ((_.contains([UNVERIFIED, VERIFIED], this.model.get('otr_status'))) || converse.use_otr_by_default) {
  1184. this.model.initiateOTR();
  1185. }
  1186. },
  1187. render: function () {
  1188. this.$el.attr('id', this.model.get('box_id'))
  1189. .html(converse.templates.chatbox(
  1190. _.extend(this.model.toJSON(), {
  1191. show_toolbar: converse.show_toolbar,
  1192. info_close: __('Close this chat box'),
  1193. info_minimize: __('Minimize this chat box'),
  1194. info_view: __('View more information on this person'),
  1195. label_personal_message: __('Personal message')
  1196. }
  1197. )
  1198. )
  1199. );
  1200. this.setWidth();
  1201. this.$content = this.$el.find('.chat-content');
  1202. this.renderToolbar().renderAvatar();
  1203. this.$content.on('scroll', _.debounce(this.onScroll.bind(this), 100));
  1204. converse.emit('chatBoxOpened', this);
  1205. setTimeout(converse.refreshWebkit, 50);
  1206. return this.showStatusMessage();
  1207. },
  1208. setWidth: function () {
  1209. // If a custom width is applied (due to drag-resizing),
  1210. // then we need to set the width of the .chatbox element as well.
  1211. if (this.model.get('width')) {
  1212. this.$el.css('width', this.model.get('width'));
  1213. }
  1214. },
  1215. onScroll: function (ev) {
  1216. if ($(ev.target).scrollTop() === 0 && this.model.messages.length) {
  1217. this.fetchArchivedMessages({
  1218. 'before': this.model.messages.at(0).get('archive_id'),
  1219. 'with': this.model.get('jid'),
  1220. 'max': converse.archived_messages_page_size
  1221. });
  1222. }
  1223. },
  1224. fetchMessages: function () {
  1225. /* Responsible for fetching previously sent messages, first
  1226. * from session storage, and then once that's done by calling
  1227. * fetchArchivedMessages, which fetches from the XMPP server if
  1228. * applicable.
  1229. */
  1230. this.model.messages.fetch({
  1231. 'add': true,
  1232. 'success': function () {
  1233. if (!converse.features.findWhere({'var': Strophe.NS.MAM})) {
  1234. return;
  1235. }
  1236. if (this.model.messages.length < converse.archived_messages_page_size) {
  1237. this.fetchArchivedMessages({
  1238. 'before': '', // Page backwards from the most recent message
  1239. 'with': this.model.get('jid'),
  1240. 'max': converse.archived_messages_page_size
  1241. });
  1242. }
  1243. }.bind(this)
  1244. });
  1245. return this;
  1246. },
  1247. fetchArchivedMessages: function (options) {
  1248. /* Fetch archived chat messages from the XMPP server.
  1249. *
  1250. * Then, upon receiving them, call onMessage on the chat box,
  1251. * so that they are displayed inside it.
  1252. */
  1253. if (!converse.features.findWhere({'var': Strophe.NS.MAM})) {
  1254. converse.log("Attempted to fetch archived messages but this user's server doesn't support XEP-0313");
  1255. return;
  1256. }
  1257. this.addSpinner();
  1258. API.archive.query(_.extend(options, {'groupchat': this.is_chatroom}),
  1259. function (messages) {
  1260. this.clearSpinner();
  1261. if (messages.length) {
  1262. if (this.is_chatroom) {
  1263. _.map(messages, this.onChatRoomMessage.bind(this));
  1264. } else {
  1265. _.map(messages, converse.chatboxes.onMessage.bind(converse.chatboxes));
  1266. }
  1267. }
  1268. }.bind(this),
  1269. function () {
  1270. this.clearSpinner();
  1271. converse.log("Error while trying to fetch archived messages", "error");
  1272. }.bind(this)
  1273. );
  1274. },
  1275. insertIntoPage: function () {
  1276. this.$el.insertAfter(converse.chatboxviews.get("controlbox").$el);
  1277. return this;
  1278. },
  1279. adjustToViewport: function () {
  1280. /* Event handler called when viewport gets resized. We remove
  1281. * custom width/height from chat boxes.
  1282. */
  1283. var viewport_width = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
  1284. var viewport_height = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
  1285. if (viewport_width <= 480) {
  1286. this.model.set('height', undefined);
  1287. this.model.set('width', undefined);
  1288. } else if (viewport_width <= this.model.get('width')) {
  1289. this.model.set('width', undefined);
  1290. } else if (viewport_height <= this.model.get('height')) {
  1291. this.model.set('height', undefined);
  1292. }
  1293. },
  1294. initDragResize: function () {
  1295. /* Determine and store the default box size.
  1296. * We need this information for the drag-resizing feature.
  1297. */
  1298. var $flyout = this.$el.find('.box-flyout');
  1299. if (typeof this.model.get('height') === 'undefined') {
  1300. var height = $flyout.height();
  1301. var width = $flyout.width();
  1302. this.model.set('height', height);
  1303. this.model.set('default_height', height);
  1304. this.model.set('width', width);
  1305. this.model.set('default_width', width);
  1306. }
  1307. var min_width = $flyout.css('min-width');
  1308. var min_height = $flyout.css('min-height');
  1309. this.model.set('min_width', min_width.endsWith('px') ? Number(min_width.replace(/px$/, '')) :0);
  1310. this.model.set('min_height', min_height.endsWith('px') ? Number(min_height.replace(/px$/, '')) :0);
  1311. // Initialize last known mouse position
  1312. this.prev_pageY = 0;
  1313. this.prev_pageX = 0;
  1314. if (converse.connection.connected) {
  1315. this.height = this.model.get('height');
  1316. this.width = this.model.get('width');
  1317. }
  1318. return this;
  1319. },
  1320. setDimensions: function () {
  1321. // Make sure the chat box has the right height and width.
  1322. this.adjustToViewport();
  1323. this.setChatBoxHeight(this.model.get('height'));
  1324. this.setChatBoxWidth(this.model.get('width'));
  1325. },
  1326. clearStatusNotification: function () {
  1327. this.$content.find('div.chat-event').remove();
  1328. },
  1329. showStatusNotification: function (message, keep_old) {
  1330. if (!keep_old) {
  1331. this.clearStatusNotification();
  1332. }
  1333. var was_at_bottom = this.$content.scrollTop() + this.$content.innerHeight() >= this.$content[0].scrollHeight;
  1334. this.$content.append($('<div class="chat-info chat-event"></div>').text(message));
  1335. if (was_at_bottom) {
  1336. this.scrollDown();
  1337. }
  1338. },
  1339. clearChatRoomMessages: function (ev) {
  1340. if (typeof ev !== "undefined") { ev.stopPropagation(); }
  1341. var result = confirm(__("Are you sure you want to clear the messages from this room?"));
  1342. if (result === true) {
  1343. this.$content.empty();
  1344. }
  1345. return this;
  1346. },
  1347. addSpinner: function () {
  1348. if (!this.$content.first().hasClass('spinner')) {
  1349. this.$content.prepend('<span class="spinner"/>');
  1350. }
  1351. },
  1352. clearSpinner: function () {
  1353. if (this.$content.children(':first').is('span.spinner')) {
  1354. this.$content.children(':first').remove();
  1355. }
  1356. },
  1357. prependDayIndicator: function (date) {
  1358. /* Prepends an indicator into the chat area, showing the day as
  1359. * given by the passed in date.
  1360. *
  1361. * Parameters:
  1362. * (String) date - An ISO8601 date string.
  1363. */
  1364. var day_date = moment(date).startOf('day');
  1365. this.$content.prepend(converse.templates.new_day({
  1366. isodate: day_date.format(),
  1367. datestring: day_date.format("dddd MMM Do YYYY")
  1368. }));
  1369. },
  1370. appendMessage: function (attrs) {
  1371. /* Helper method which appends a message to the end of the chat
  1372. * box's content area.
  1373. *
  1374. * Parameters:
  1375. * (Object) attrs: An object containing the message attributes.
  1376. */
  1377. _.compose(
  1378. _.debounce(this.scrollDown.bind(this), 50),
  1379. this.$content.append.bind(this.$content)
  1380. )(this.renderMessage(attrs));
  1381. },
  1382. showMessage: function (attrs) {
  1383. /* Inserts a chat message into the content area of the chat box.
  1384. * Will also insert a new day indicator if the message is on a
  1385. * different day.
  1386. *
  1387. * The message to show may either be newer than the newest
  1388. * message, or older than the oldest message.
  1389. *
  1390. * Parameters:
  1391. * (Object) attrs: An object containing the message attributes.
  1392. */
  1393. var $first_msg = this.$content.children('.chat-message:first'),
  1394. first_msg_date = $first_msg.data('isodate'),
  1395. last_msg_date, current_msg_date, day_date, $msgs, msg_dates, idx;
  1396. if (!first_msg_date) {
  1397. this.appendMessage(attrs);
  1398. return;
  1399. }
  1400. current_msg_date = moment(attrs.time) || moment;
  1401. last_msg_date = this.$content.children('.chat-message:last').data('isodate');
  1402. if (typeof last_msg_date !== "undefined" && (current_msg_date.isAfter(last_msg_date) || current_msg_date.isSame(last_msg_date))) {
  1403. // The new message is after the last message
  1404. if (current_msg_date.isAfter(last_msg_date, 'day')) {
  1405. // Append a new day indicator
  1406. day_date = moment(current_msg_date).startOf('day');
  1407. this.$content.append(converse.templates.new_day({
  1408. isodate: current_msg_date.format(),
  1409. datestring: current_msg_date.format("dddd MMM Do YYYY")
  1410. }));
  1411. }
  1412. this.appendMessage(attrs);
  1413. return;
  1414. }
  1415. if (typeof first_msg_date !== "undefined" &&
  1416. (current_msg_date.isBefore(first_msg_date) ||
  1417. (current_msg_date.isSame(first_msg_date) && !current_msg_date.isSame(last_msg_date)))) {
  1418. // The new message is before the first message
  1419. if ($first_msg.prev().length === 0) {
  1420. // There's no day indicator before the first message, so we prepend one.
  1421. this.prependDayIndicator(first_msg_date);
  1422. }
  1423. if (current_msg_date.isBefore(first_msg_date, 'day')) {
  1424. _.compose(
  1425. this.scrollDownMessageHeight.bind(this),
  1426. function ($el) {
  1427. this.$content.prepend($el);
  1428. return $el;
  1429. }.bind(this)
  1430. )(this.renderMessage(attrs));
  1431. // This message is on a different day, so we add a day indicator.
  1432. this.prependDayIndicator(current_msg_date);
  1433. } else {
  1434. // The message is before the first, but on the same day.
  1435. // We need to prepend the message immediately before the
  1436. // first message (so that it'll still be after the day indicator).
  1437. _.compose(
  1438. this.scrollDownMessageHeight.bind(this),
  1439. function ($el) {
  1440. $el.insertBefore($first_msg);
  1441. return $el;
  1442. }
  1443. )(this.renderMessage(attrs));
  1444. }
  1445. } else {
  1446. // We need to find the correct place to position the message
  1447. current_msg_date = current_msg_date.format();
  1448. $msgs = this.$content.children('.chat-message');
  1449. msg_dates = _.map($msgs, function (el) {
  1450. return $(el).data('isodate');
  1451. });
  1452. msg_dates.push(current_msg_date);
  1453. msg_dates.sort();
  1454. idx = msg_dates.indexOf(current_msg_date)-1;
  1455. _.compose(
  1456. this.scrollDownMessageHeight.bind(this),
  1457. function ($el) {
  1458. $el.insertAfter(this.$content.find('.chat-message[data-isodate="'+msg_dates[idx]+'"]'));
  1459. return $el;
  1460. }.bind(this)
  1461. )(this.renderMessage(attrs));
  1462. }
  1463. },
  1464. renderMessage: function (attrs) {
  1465. /* Renders a chat message based on the passed in attributes.
  1466. *
  1467. * Parameters:
  1468. * (Object) attrs: An object containing the message attributes.
  1469. *
  1470. * Returns:
  1471. * The DOM element representing the message.
  1472. */
  1473. var msg_time = moment(attrs.time) || moment,
  1474. text = attrs.message,
  1475. match = text.match(/^\/(.*?)(?: (.*))?$/),
  1476. fullname = this.model.get('fullname') || attrs.fullname,
  1477. extra_classes = attrs.delayed && 'delayed' || '',
  1478. template, username;
  1479. if ((match) && (match[1] === 'me')) {
  1480. text = text.replace(/^\/me/, '');
  1481. template = converse.templates.action;
  1482. username = fullname;
  1483. } else {
  1484. template = converse.templates.message;
  1485. username = attrs.sender === 'me' && __('me') || fullname;
  1486. }
  1487. this.$content.find('div.chat-event').remove();
  1488. if (this.is_chatroom && attrs.sender === 'them' && (new RegExp("\\b"+this.model.get('nick')+"\\b")).test(text)) {
  1489. // Add special class to mark groupchat messages in which we
  1490. // are mentioned.
  1491. extra_classes += ' mentioned';
  1492. }
  1493. return $(template({
  1494. msgid: attrs.msgid,
  1495. 'sender': attrs.sender,
  1496. 'time': msg_time.format('hh:mm'),
  1497. 'isodate': msg_time.format(),
  1498. 'username': username,
  1499. 'message': '',
  1500. 'extra_classes': extra_classes
  1501. })).children('.chat-msg-content').first().text(text)
  1502. .addHyperlinks()
  1503. .addEmoticons(converse.visible_toolbar_buttons.emoticons).parent();
  1504. },
  1505. showHelpMessages: function (msgs, type, spinner) {
  1506. var i, msgs_length = msgs.length;
  1507. for (i=0; i<msgs_length; i++) {
  1508. this.$content.append($('<div class="chat-'+(type||'info')+'">'+msgs[i]+'</div>'));
  1509. }
  1510. if (spinner === true) {
  1511. this.$content.append('<span class="spinner"/>');
  1512. } else if (spinner === false) {
  1513. this.$content.find('span.spinner').remove();
  1514. }
  1515. return this.scrollDown();
  1516. },
  1517. onMessageAdded: function (message) {
  1518. /* Handler that gets called when a new message object is created.
  1519. *
  1520. * Parameters:
  1521. * (Object) message - The message Backbone object that was added.
  1522. */
  1523. if (typeof this.clear_status_timeout !== 'undefined') {
  1524. clearTimeout(this.clear_status_timeout);
  1525. delete this.clear_status_timeout;
  1526. }
  1527. if (!message.get('message')) {
  1528. if (message.get('chat_state') === COMPOSING) {
  1529. this.showStatusNotification(message.get('fullname')+' '+__('is typing'));
  1530. this.clear_status_timeout = setTimeout(this.clearStatusNotification.bind(this), 10000);
  1531. return;
  1532. } else if (message.get('chat_state') === PAUSED) {
  1533. this.showStatusNotification(message.get('fullname')+' '+__('has stopped typing'));
  1534. return;
  1535. } else if (_.contains([INACTIVE, ACTIVE], message.get('chat_state'))) {
  1536. this.$content.find('div.chat-event').remove();
  1537. return;
  1538. } else if (message.get('chat_state') === GONE) {
  1539. this.showStatusNotification(message.get('fullname')+' '+__('has gone away'));
  1540. return;
  1541. }
  1542. } else {
  1543. this.showMessage(_.clone(message.attributes));
  1544. }
  1545. if ((message.get('sender') !== 'me') && (converse.windowState === 'blur')) {
  1546. converse.incrementMsgCounter();
  1547. }
  1548. if (!this.model.get('minimized') && !this.$el.is(':visible')) {
  1549. _.debounce(this.show.bind(this), 100)();
  1550. }
  1551. },
  1552. sendMessage: function (message) {
  1553. /* Responsible for sending off a text message.
  1554. *
  1555. * Parameters:
  1556. * (Message) message - The chat message
  1557. */
  1558. // TODO: We might want to send to specfic resources. Especially in the OTR case.
  1559. var bare_jid = this.model.get('jid');
  1560. var messageStanza = $msg({from: converse.connection.jid, to: bare_jid, type: 'chat', id: message.get('msgid')})
  1561. .c('body').t(message.get('message')).up()
  1562. .c(ACTIVE, {'xmlns': Strophe.NS.CHATSTATES}).up();
  1563. if (this.model.get('otr_status') !== UNENCRYPTED) {
  1564. // OTR messages aren't carbon copied
  1565. messageStanza.c('private', {'xmlns': Strophe.NS.CARBONS});
  1566. }
  1567. converse.connection.send(messageStanza);
  1568. if (converse.forward_messages) {
  1569. // Forward the message, so that other connected resources are also aware of it.
  1570. converse.connection.send(
  1571. $msg({ to: converse.bare_jid, type: 'chat', id: message.get('msgid') })
  1572. .c('forwarded', {xmlns:'urn:xmpp:forward:0'})
  1573. .c('delay', {xmns:'urn:xmpp:delay',stamp:(new Date()).getTime()}).up()
  1574. .cnode(messageStanza.tree())
  1575. );
  1576. }
  1577. },
  1578. onMessageSubmitted: function (text) {
  1579. /* This method gets called once the user has typed a message
  1580. * and then pressed enter in a chat box.
  1581. *
  1582. * Parameters:
  1583. * (string) text - The chat message text.
  1584. */
  1585. if (!converse.connection.authenticated) {
  1586. return this.showHelpMessages(['Sorry, the connection has been lost, and your message could not be sent'], 'error');
  1587. }
  1588. var match = text.replace(/^\s*/, "").match(/^\/(.*)\s*$/), msgs;
  1589. if (match) {
  1590. if (match[1] === "clear") {
  1591. return this.clearMessages();
  1592. }
  1593. else if (match[1] === "help") {
  1594. msgs = [
  1595. '<strong>/help</strong>:'+__('Show this menu')+'',
  1596. '<strong>/me</strong>:'+__('Write in the third person')+'',
  1597. '<strong>/clear</strong>:'+__('Remove messages')+''
  1598. ];
  1599. this.showHelpMessages(msgs);
  1600. return;
  1601. } else if ((converse.allow_otr) && (match[1] === "endotr")) {
  1602. return this.endOTR();
  1603. } else if ((converse.allow_otr) && (match[1] === "otr")) {
  1604. return this.model.initiateOTR();
  1605. }
  1606. }
  1607. if (_.contains([UNVERIFIED, VERIFIED], this.model.get('otr_status'))) {
  1608. // Off-the-record encryption is active
  1609. this.model.otr.sendMsg(text);
  1610. this.model.trigger('showSentOTRMessage', text);
  1611. } else {
  1612. // We only save unencrypted messages.
  1613. var fullname = converse.xmppstatus.get('fullname');
  1614. fullname = _.isEmpty(fullname)? converse.bare_jid: fullname;
  1615. var message = this.model.messages.create({
  1616. fullname: fullname,
  1617. sender: 'me',
  1618. time: moment().format(),
  1619. message: text
  1620. });
  1621. this.sendMessage(message);
  1622. }
  1623. },
  1624. sendChatState: function () {
  1625. /* Sends a message with the status of the user in this chat session
  1626. * as taken from the 'chat_state' attribute of the chat box.
  1627. * See XEP-0085 Chat State Notifications.
  1628. */
  1629. converse.connection.send(
  1630. $msg({'to':this.model.get('jid'), 'type': 'chat'})
  1631. .c(this.model.get('chat_state'), {'xmlns': Strophe.NS.CHATSTATES})
  1632. );
  1633. },
  1634. setChatState: function (state, no_save) {
  1635. /* Mutator for setting the chat state of this chat session.
  1636. * Handles clearing of any chat state notification timeouts and
  1637. * setting new ones if necessary.
  1638. * Timeouts are set when the state being set is COMPOSING or PAUSED.
  1639. * After the timeout, COMPOSING will become PAUSED and PAUSED will become INACTIVE.
  1640. * See XEP-0085 Chat State Notifications.
  1641. *
  1642. * Parameters:
  1643. * (string) state - The chat state (consts ACTIVE, COMPOSING, PAUSED, INACTIVE, GONE)
  1644. * (Boolean) no_save - Just do the cleanup or setup but don't actually save the state.
  1645. */
  1646. if (typeof this.chat_state_timeout !== 'undefined') {
  1647. clearTimeout(this.chat_state_timeout);
  1648. delete this.chat_state_timeout;
  1649. }
  1650. if (state === COMPOSING) {
  1651. this.chat_state_timeout = setTimeout(
  1652. this.setChatState.bind(this), converse.TIMEOUTS.PAUSED, PAUSED);
  1653. } else if (state === PAUSED) {
  1654. this.chat_state_timeout = setTimeout(
  1655. this.setChatState.bind(this), converse.TIMEOUTS.INACTIVE, INACTIVE);
  1656. }
  1657. if (!no_save && this.model.get('chat_state') !== state) {
  1658. this.model.set('chat_state', state);
  1659. }
  1660. return this;
  1661. },
  1662. keyPressed: function (ev) {
  1663. /* Event handler for when a key is pressed in a chat box textarea.
  1664. */
  1665. var $textarea = $(ev.target), message;
  1666. if (ev.keyCode === KEY.ENTER) {
  1667. ev.preventDefault();
  1668. message = $textarea.val();
  1669. $textarea.val('').focus();
  1670. if (message !== '') {
  1671. if (this.model.get('chatroom')) {
  1672. this.onChatRoomMessageSubmitted(message);
  1673. } else {
  1674. this.onMessageSubmitted(message);
  1675. }
  1676. converse.emit('messageSend', message);
  1677. }
  1678. this.setChatState(ACTIVE);
  1679. } else if (!this.model.get('chatroom')) { // chat state data is currently only for single user chat
  1680. // Set chat state to composing if keyCode is not a forward-slash
  1681. // (which would imply an internal command and not a message).
  1682. this.setChatState(COMPOSING, ev.keyCode === KEY.FORWARD_SLASH);
  1683. }
  1684. },
  1685. onStartVerticalResize: function (ev) {
  1686. if (!converse.allow_dragresize) { return true; }
  1687. // Record element attributes for mouseMove().
  1688. this.height = this.$el.children('.box-flyout').height();
  1689. converse.resizing = {
  1690. 'chatbox': this,
  1691. 'direction': 'top'
  1692. };
  1693. this.prev_pageY = ev.pageY;
  1694. },
  1695. onStartHorizontalResize: function (ev) {
  1696. if (!converse.allow_dragresize) { return true; }
  1697. this.width = this.$el.children('.box-flyout').width();
  1698. converse.resizing = {
  1699. 'chatbox': this,
  1700. 'direction': 'left'
  1701. };
  1702. this.prev_pageX = ev.pageX;
  1703. },
  1704. onStartDiagonalResize: function (ev) {
  1705. this.onStartHorizontalResize(ev);
  1706. this.onStartVerticalResize(ev);
  1707. converse.resizing.direction = 'topleft';
  1708. },
  1709. setChatBoxHeight: function (height) {
  1710. if (!this.model.get('minimized')) {
  1711. if (height) {
  1712. height = converse.applyDragResistance(height, this.model.get('default_height'))+'px';
  1713. } else {
  1714. height = "";
  1715. }
  1716. this.$el.children('.box-flyout')[0].style.height = height;
  1717. }
  1718. },
  1719. setChatBoxWidth: function (width) {
  1720. if (!this.model.get('minimized')) {
  1721. if (width) {
  1722. width = converse.applyDragResistance(width, this.model.get('default_width'))+'px';
  1723. } else {
  1724. width = "";
  1725. }
  1726. this.$el[0].style.width = width;
  1727. this.$el.children('.box-flyout')[0].style.width = width;
  1728. }
  1729. },
  1730. resizeChatBox: function (ev) {
  1731. var diff;
  1732. if (converse.resizing.direction.indexOf('top') === 0) {
  1733. diff = ev.pageY - this.prev_pageY;
  1734. if (diff) {
  1735. this.height = ((this.height-diff) > (this.model.get('min_height') || 0)) ? (this.height-diff) : this.model.get('min_height');
  1736. this.prev_pageY = ev.pageY;
  1737. this.setChatBoxHeight(this.height);
  1738. }
  1739. }
  1740. if (converse.resizing.direction.indexOf('left') !== -1) {
  1741. diff = this.prev_pageX - ev.pageX;
  1742. if (diff) {
  1743. this.width = ((this.width+diff) > (this.model.get('min_width') || 0)) ? (this.width+diff) : this.model.get('min_width');
  1744. this.prev_pageX = ev.pageX;
  1745. this.setChatBoxWidth(this.width);
  1746. }
  1747. }
  1748. },
  1749. clearMessages: function (ev) {
  1750. if (ev && ev.preventDefault) { ev.preventDefault(); }
  1751. var result = confirm(__("Are you sure you want to clear the messages from this chat box?"));
  1752. if (result === true) {
  1753. this.$content.empty();
  1754. this.model.messages.reset();
  1755. this.model.messages.browserStorage._clear();
  1756. }
  1757. return this;
  1758. },
  1759. insertEmoticon: function (ev) {
  1760. ev.stopPropagation();
  1761. this.$el.find('.toggle-smiley ul').slideToggle(200);
  1762. var $textbox = this.$el.find('textarea.chat-textarea');
  1763. var value = $textbox.val();
  1764. var $target = $(ev.target);
  1765. $target = $target.is('a') ? $target : $target.children('a');
  1766. if (value && (value[value.length-1] !== ' ')) {
  1767. value = value + ' ';
  1768. }
  1769. $textbox.focus().val(value+$target.data('emoticon')+' ');
  1770. },
  1771. toggleEmoticonMenu: function (ev) {
  1772. ev.stopPropagation();
  1773. this.$el.find('.toggle-smiley ul').slideToggle(200);
  1774. },
  1775. toggleOTRMenu: function (ev) {
  1776. ev.stopPropagation();
  1777. this.$el.find('.toggle-otr ul').slideToggle(200);
  1778. },
  1779. showOTRError: function (msg) {
  1780. if (msg === 'Message cannot be sent at this time.') {
  1781. this.showHelpMessages(
  1782. [__('Your message could not be sent')], 'error');
  1783. } else if (msg === 'Received an unencrypted message.') {
  1784. this.showHelpMessages(
  1785. [__('We received an unencrypted message')], 'error');
  1786. } else if (msg === 'Received an unreadable encrypted message.') {
  1787. this.showHelpMessages(
  1788. [__('We received an unreadable encrypted message')],
  1789. 'error');
  1790. } else {
  1791. this.showHelpMessages(['Encryption error occured: '+msg], 'error');
  1792. }
  1793. converse.log("OTR ERROR:"+msg);
  1794. },
  1795. startOTRFromToolbar: function (ev) {
  1796. $(ev.target).parent().parent().slideUp();
  1797. ev.stopPropagation();
  1798. this.model.initiateOTR();
  1799. },
  1800. endOTR: function (ev) {
  1801. if (typeof ev !== "undefined") {
  1802. ev.preventDefault();
  1803. ev.stopPropagation();
  1804. }
  1805. this.model.endOTR();
  1806. },
  1807. authOTR: function (ev) {
  1808. var scheme = $(ev.target).data().scheme;
  1809. var result, question, answer;
  1810. if (scheme === 'fingerprint') {
  1811. 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.', [
  1812. this.model.get('fullname'),
  1813. converse.xmppstatus.get('fullname')||converse.bare_jid,
  1814. this.model.otr.priv.fingerprint(),
  1815. this.model.otr.their_priv_pk.fingerprint()
  1816. ]
  1817. ));
  1818. if (result === true) {
  1819. this.model.save({'otr_status': VERIFIED});
  1820. } else {
  1821. this.model.save({'otr_status': UNVERIFIED});
  1822. }
  1823. } else if (scheme === 'smp') {
  1824. 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.'));
  1825. question = prompt(__('What is your security question?'));
  1826. if (question) {
  1827. answer = prompt(__('What is the answer to the security question?'));
  1828. this.model.otr.smpSecret(answer, question);
  1829. }
  1830. } else {
  1831. this.showHelpMessages([__('Invalid authentication scheme provided')], 'error');
  1832. }
  1833. },
  1834. toggleCall: function (ev) {
  1835. ev.stopPropagation();
  1836. converse.emit('callButtonClicked', {
  1837. connection: converse.connection,
  1838. model: this.model
  1839. });
  1840. },
  1841. onChatStatusChanged: function (item) {
  1842. var chat_status = item.get('chat_status'),
  1843. fullname = item.get('fullname');
  1844. fullname = _.isEmpty(fullname)? item.get('jid'): fullname;
  1845. if (this.$el.is(':visible')) {
  1846. if (chat_status === 'offline') {
  1847. this.showStatusNotification(fullname+' '+__('has gone offline'));
  1848. } else if (chat_status === 'away') {
  1849. this.showStatusNotification(fullname+' '+__('has gone away'));
  1850. } else if ((chat_status === 'dnd')) {
  1851. this.showStatusNotification(fullname+' '+__('is busy'));
  1852. } else if (chat_status === 'online') {
  1853. this.$el.find('div.chat-event').remove();
  1854. }
  1855. }
  1856. converse.emit('contactStatusChanged', item.attributes, item.get('chat_status'));
  1857. },
  1858. onStatusChanged: function (item) {
  1859. this.showStatusMessage();
  1860. converse.emit('contactStatusMessageChanged', item.attributes, item.get('status'));
  1861. },
  1862. onOTRStatusChanged: function () {
  1863. this.renderToolbar().informOTRChange();
  1864. },
  1865. onMinimizedChanged: function (item) {
  1866. if (item.get('minimized')) {
  1867. this.hide();
  1868. } else {
  1869. this.maximize();
  1870. }
  1871. },
  1872. showStatusMessage: function (msg) {
  1873. msg = msg || this.model.get('status');
  1874. if (typeof msg === "string") {
  1875. this.$el.find('p.user-custom-message').text(msg).attr('title', msg);
  1876. }
  1877. return this;
  1878. },
  1879. close: function (ev) {
  1880. if (ev && ev.preventDefault) { ev.preventDefault(); }
  1881. if (converse.connection.connected) {
  1882. this.model.destroy();
  1883. this.setChatState(INACTIVE);
  1884. } else {
  1885. this.hide();
  1886. }
  1887. converse.emit('chatBoxClosed', this);
  1888. return this;
  1889. },
  1890. maximize: function () {
  1891. var chatboxviews = converse.chatboxviews;
  1892. // Restores a minimized chat box
  1893. this.$el.insertAfter(chatboxviews.get("controlbox").$el).show('fast', function () {
  1894. /* Now that the chat box is visible, we can call trimChats
  1895. * to make space available if need be.
  1896. */
  1897. chatboxviews.trimChats(this);
  1898. converse.refreshWebkit();
  1899. this.setChatState(ACTIVE).focus();
  1900. converse.emit('chatBoxMaximized', this);
  1901. }.bind(this));
  1902. },
  1903. minimize: function (ev) {
  1904. if (ev && ev.preventDefault) { ev.preventDefault(); }
  1905. // Minimizes a chat box
  1906. this.setChatState(INACTIVE).model.minimize();
  1907. this.$el.hide('fast', converse.refreshwebkit);
  1908. converse.emit('chatBoxMinimized', this);
  1909. },
  1910. updateVCard: function () {
  1911. if (!this.use_vcards) { return this; }
  1912. var jid = this.model.get('jid'),
  1913. contact = converse.roster.get(jid);
  1914. if ((contact) && (!contact.get('vcard_updated'))) {
  1915. converse.getVCard(
  1916. jid,
  1917. function (iq, jid, fullname, image, image_type, url) {
  1918. this.model.save({
  1919. 'fullname' : fullname || jid,
  1920. 'url': url,
  1921. 'image_type': image_type,
  1922. 'image': image
  1923. });
  1924. }.bind(this),
  1925. function () {
  1926. converse.log("ChatBoxView.initialize: An error occured while fetching vcard");
  1927. }
  1928. );
  1929. }
  1930. return this;
  1931. },
  1932. informOTRChange: function () {
  1933. var data = this.model.toJSON();
  1934. var msgs = [];
  1935. if (data.otr_status === UNENCRYPTED) {
  1936. msgs.push(__("Your messages are not encrypted anymore"));
  1937. } else if (data.otr_status === UNVERIFIED) {
  1938. msgs.push(__("Your messages are now encrypted but your contact's identity has not been verified."));
  1939. } else if (data.otr_status === VERIFIED) {
  1940. msgs.push(__("Your contact's identify has been verified."));
  1941. } else if (data.otr_status === FINISHED) {
  1942. msgs.push(__("Your contact has ended encryption on their end, you should do the same."));
  1943. }
  1944. return this.showHelpMessages(msgs, 'info', false);
  1945. },
  1946. renderToolbar: function () {
  1947. if (converse.show_toolbar) {
  1948. var data = this.model.toJSON();
  1949. if (data.otr_status === UNENCRYPTED) {
  1950. data.otr_tooltip = __('Your messages are not encrypted. Click here to enable OTR encryption.');
  1951. } else if (data.otr_status === UNVERIFIED) {
  1952. data.otr_tooltip = __('Your messages are encrypted, but your contact has not been verified.');
  1953. } else if (data.otr_status === VERIFIED) {
  1954. data.otr_tooltip = __('Your messages are encrypted and your contact verified.');
  1955. } else if (data.otr_status === FINISHED) {
  1956. data.otr_tooltip = __('Your contact has closed their end of the private session, you should do the same');
  1957. }
  1958. this.$el.find('.chat-toolbar').html(
  1959. converse.templates.toolbar(
  1960. _.extend(data, {
  1961. FINISHED: FINISHED,
  1962. UNENCRYPTED: UNENCRYPTED,
  1963. UNVERIFIED: UNVERIFIED,
  1964. VERIFIED: VERIFIED,
  1965. allow_otr: converse.allow_otr && !this.is_chatroom,
  1966. label_clear: __('Clear all messages'),
  1967. label_end_encrypted_conversation: __('End encrypted conversation'),
  1968. label_insert_smiley: __('Insert a smiley'),
  1969. label_hide_occupants: __('Hide the list of occupants'),
  1970. label_refresh_encrypted_conversation: __('Refresh encrypted conversation'),
  1971. label_start_call: __('Start a call'),
  1972. label_start_encrypted_conversation: __('Start encrypted conversation'),
  1973. label_verify_with_fingerprints: __('Verify with fingerprints'),
  1974. label_verify_with_smp: __('Verify with SMP'),
  1975. label_whats_this: __("What\'s this?"),
  1976. otr_status_class: OTR_CLASS_MAPPING[data.otr_status],
  1977. otr_translated_status: OTR_TRANSLATED_MAPPING[data.otr_status],
  1978. show_call_button: converse.visible_toolbar_buttons.call,
  1979. show_clear_button: converse.visible_toolbar_buttons.clear,
  1980. show_emoticons: converse.visible_toolbar_buttons.emoticons,
  1981. show_occupants_toggle: this.is_chatroom && converse.visible_toolbar_buttons.toggle_occupants
  1982. })
  1983. )
  1984. );
  1985. }
  1986. return this;
  1987. },
  1988. renderAvatar: function () {
  1989. if (!this.model.get('image')) {
  1990. return;
  1991. }
  1992. var img_src = 'data:'+this.model.get('image_type')+';base64,'+this.model.get('image'),
  1993. canvas = $('<canvas height="32px" width="32px" class="avatar"></canvas>').get(0);
  1994. if (!(canvas.getContext && canvas.getContext('2d'))) {
  1995. return this;
  1996. }
  1997. var ctx = canvas.getContext('2d');
  1998. var img = new Image(); // Create new Image object
  1999. img.onload = function () {
  2000. var ratio = img.width/img.height;
  2001. if (ratio < 1) {
  2002. ctx.drawImage(img, 0,0, 32, 32*(1/ratio));
  2003. } else {
  2004. ctx.drawImage(img, 0,0, 32, 32*ratio);
  2005. }
  2006. };
  2007. img.src = img_src;
  2008. this.$el.find('.chat-title').before(canvas);
  2009. return this;
  2010. },
  2011. focus: function () {
  2012. this.$el.find('.chat-textarea').focus();
  2013. converse.emit('chatBoxFocused', this);
  2014. return this;
  2015. },
  2016. hide: function () {
  2017. if (this.$el.is(':visible') && this.$el.css('opacity') === "1") {
  2018. this.$el.hide();
  2019. converse.refreshWebkit();
  2020. }
  2021. return this;
  2022. },
  2023. show: function (callback) {
  2024. if (this.$el.is(':visible') && this.$el.css('opacity') === "1") {
  2025. return this.focus();
  2026. }
  2027. this.initDragResize().setDimensions();
  2028. this.$el.fadeIn(function () {
  2029. if (typeof callback === "function") {
  2030. callback.apply(this, arguments);
  2031. }
  2032. if (converse.connection.connected) {
  2033. // Without a connection, we haven't yet initialized
  2034. // localstorage
  2035. this.model.save();
  2036. }
  2037. this.setChatState(ACTIVE);
  2038. this.scrollDown().focus();
  2039. }.bind(this));
  2040. return this;
  2041. },
  2042. scrollDownMessageHeight: function ($message) {
  2043. if (this.$content.is(':visible')) {
  2044. this.$content.scrollTop(this.$content.scrollTop() + $message[0].scrollHeight);
  2045. }
  2046. return this;
  2047. },
  2048. scrollDown: function () {
  2049. if (this.$content.is(':visible')) {
  2050. this.$content.scrollTop(this.$content[0].scrollHeight);
  2051. }
  2052. return this;
  2053. }
  2054. });
  2055. this.ContactsPanel = Backbone.View.extend({
  2056. tagName: 'div',
  2057. className: 'controlbox-pane',
  2058. id: 'users',
  2059. events: {
  2060. 'click a.toggle-xmpp-contact-form': 'toggleContactForm',
  2061. 'submit form.add-xmpp-contact': 'addContactFromForm',
  2062. 'submit form.search-xmpp-contact': 'searchContacts',
  2063. 'click a.subscribe-to-user': 'addContactFromList'
  2064. },
  2065. initialize: function (cfg) {
  2066. cfg.$parent.append(this.$el);
  2067. this.$tabs = cfg.$parent.parent().find('#controlbox-tabs');
  2068. },
  2069. render: function () {
  2070. var markup;
  2071. var widgets = converse.templates.contacts_panel({
  2072. label_online: __('Online'),
  2073. label_busy: __('Busy'),
  2074. label_away: __('Away'),
  2075. label_offline: __('Offline'),
  2076. label_logout: __('Log out'),
  2077. include_offline_state: converse.include_offline_state,
  2078. allow_logout: converse.allow_logout
  2079. });
  2080. this.$tabs.append(converse.templates.contacts_tab({label_contacts: LABEL_CONTACTS}));
  2081. if (converse.xhr_user_search) {
  2082. markup = converse.templates.search_contact({
  2083. label_contact_name: __('Contact name'),
  2084. label_search: __('Search')
  2085. });
  2086. } else {
  2087. markup = converse.templates.add_contact_form({
  2088. label_contact_username: __('e.g. user@example.com'),
  2089. label_add: __('Add')
  2090. });
  2091. }
  2092. if (converse.allow_contact_requests) {
  2093. widgets += converse.templates.add_contact_dropdown({
  2094. label_click_to_chat: __('Click to add new chat contacts'),
  2095. label_add_contact: __('Add a contact')
  2096. });
  2097. }
  2098. this.$el.html(widgets);
  2099. this.$el.find('.search-xmpp ul').append(markup);
  2100. return this;
  2101. },
  2102. toggleContactForm: function (ev) {
  2103. ev.preventDefault();
  2104. this.$el.find('.search-xmpp').toggle('fast', function () {
  2105. if ($(this).is(':visible')) {
  2106. $(this).find('input.username').focus();
  2107. }
  2108. });
  2109. },
  2110. searchContacts: function (ev) {
  2111. ev.preventDefault();
  2112. $.getJSON(converse.xhr_user_search_url+ "?q=" + $(ev.target).find('input.username').val(), function (data) {
  2113. var $ul= $('.search-xmpp ul');
  2114. $ul.find('li.found-user').remove();
  2115. $ul.find('li.chat-info').remove();
  2116. if (!data.length) {
  2117. $ul.append('<li class="chat-info">'+__('No users found')+'</li>');
  2118. }
  2119. $(data).each(function (idx, obj) {
  2120. $ul.append(
  2121. $('<li class="found-user"></li>')
  2122. .append(
  2123. $('<a class="subscribe-to-user" href="#" title="'+__('Click to add as a chat contact')+'"></a>')
  2124. .attr('data-recipient', Strophe.getNodeFromJid(obj.id)+"@"+Strophe.getDomainFromJid(obj.id))
  2125. .text(obj.fullname)
  2126. )
  2127. );
  2128. });
  2129. });
  2130. },
  2131. addContactFromForm: function (ev) {
  2132. ev.preventDefault();
  2133. var $input = $(ev.target).find('input');
  2134. var jid = $input.val();
  2135. if (! jid) {
  2136. // this is not a valid JID
  2137. $input.addClass('error');
  2138. return;
  2139. }
  2140. converse.roster.addAndSubscribe(jid);
  2141. $('.search-xmpp').hide();
  2142. },
  2143. addContactFromList: function (ev) {
  2144. ev.preventDefault();
  2145. var $target = $(ev.target),
  2146. jid = $target.attr('data-recipient'),
  2147. name = $target.text();
  2148. converse.roster.addAndSubscribe(jid, name);
  2149. $target.parent().remove();
  2150. $('.search-xmpp').hide();
  2151. }
  2152. });
  2153. this.RoomsPanel = Backbone.View.extend({
  2154. tagName: 'div',
  2155. className: 'controlbox-pane',
  2156. id: 'chatrooms',
  2157. events: {
  2158. 'submit form.add-chatroom': 'createChatRoom',
  2159. 'click input#show-rooms': 'showRooms',
  2160. 'click a.open-room': 'createChatRoom',
  2161. 'click a.room-info': 'showRoomInfo',
  2162. 'change input[name=server]': 'setDomain',
  2163. 'change input[name=nick]': 'setNick'
  2164. },
  2165. initialize: function (cfg) {
  2166. this.$parent = cfg.$parent;
  2167. this.model.on('change:muc_domain', this.onDomainChange, this);
  2168. this.model.on('change:nick', this.onNickChange, this);
  2169. },
  2170. render: function () {
  2171. this.$parent.append(
  2172. this.$el.html(
  2173. converse.templates.room_panel({
  2174. 'server_input_type': converse.hide_muc_server && 'hidden' || 'text',
  2175. 'server_label_global_attr': converse.hide_muc_server && ' hidden' || '',
  2176. 'label_room_name': __('Room name'),
  2177. 'label_nickname': __('Nickname'),
  2178. 'label_server': __('Server'),
  2179. 'label_join': __('Join Room'),
  2180. 'label_show_rooms': __('Show rooms')
  2181. })
  2182. ).hide());
  2183. this.$tabs = this.$parent.parent().find('#controlbox-tabs');
  2184. this.$tabs.append(converse.templates.chatrooms_tab({label_rooms: __('Rooms')}));
  2185. return this;
  2186. },
  2187. onDomainChange: function (model) {
  2188. var $server = this.$el.find('input.new-chatroom-server');
  2189. $server.val(model.get('muc_domain'));
  2190. if (converse.auto_list_rooms) {
  2191. this.updateRoomsList();
  2192. }
  2193. },
  2194. onNickChange: function (model) {
  2195. var $nick = this.$el.find('input.new-chatroom-nick');
  2196. $nick.val(model.get('nick'));
  2197. },
  2198. informNoRoomsFound: function () {
  2199. var $available_chatrooms = this.$el.find('#available-chatrooms');
  2200. // For translators: %1$s is a variable and will be replaced with the XMPP server name
  2201. $available_chatrooms.html('<dt>'+__('No rooms on %1$s',this.model.get('muc_domain'))+'</dt>');
  2202. $('input#show-rooms').show().siblings('span.spinner').remove();
  2203. },
  2204. onRoomsFound: function (iq) {
  2205. /* Handle the IQ stanza returned from the server, containing
  2206. * all its public rooms.
  2207. */
  2208. var name, jid, i, fragment,
  2209. $available_chatrooms = this.$el.find('#available-chatrooms');
  2210. this.rooms = $(iq).find('query').find('item');
  2211. if (this.rooms.length) {
  2212. // For translators: %1$s is a variable and will be
  2213. // replaced with the XMPP server name
  2214. $available_chatrooms.html('<dt>'+__('Rooms on %1$s',this.model.get('muc_domain'))+'</dt>');
  2215. fragment = document.createDocumentFragment();
  2216. for (i=0; i<this.rooms.length; i++) {
  2217. name = Strophe.unescapeNode($(this.rooms[i]).attr('name')||$(this.rooms[i]).attr('jid'));
  2218. jid = $(this.rooms[i]).attr('jid');
  2219. fragment.appendChild($(
  2220. converse.templates.room_item({
  2221. 'name':name,
  2222. 'jid':jid,
  2223. 'open_title': __('Click to open this room'),
  2224. 'info_title': __('Show more information on this room')
  2225. })
  2226. )[0]);
  2227. }
  2228. $available_chatrooms.append(fragment);
  2229. $('input#show-rooms').show().siblings('span.spinner').remove();
  2230. } else {
  2231. this.informNoRoomsFound();
  2232. }
  2233. return true;
  2234. },
  2235. updateRoomsList: function () {
  2236. /* Send and IQ stanza to the server asking for all rooms
  2237. */
  2238. converse.connection.sendIQ(
  2239. $iq({
  2240. to: this.model.get('muc_domain'),
  2241. from: converse.connection.jid,
  2242. type: "get"
  2243. }).c("query", {xmlns: Strophe.NS.DISCO_ITEMS}),
  2244. this.onRoomsFound.bind(this),
  2245. this.informNoRoomsFound.bind(this)
  2246. );
  2247. },
  2248. showRooms: function () {
  2249. var $available_chatrooms = this.$el.find('#available-chatrooms');
  2250. var $server = this.$el.find('input.new-chatroom-server');
  2251. var server = $server.val();
  2252. if (!server) {
  2253. $server.addClass('error');
  2254. return;
  2255. }
  2256. this.$el.find('input.new-chatroom-name').removeClass('error');
  2257. $server.removeClass('error');
  2258. $available_chatrooms.empty();
  2259. $('input#show-rooms').hide().after('<span class="spinner"/>');
  2260. this.model.save({muc_domain: server});
  2261. this.updateRoomsList();
  2262. },
  2263. showRoomInfo: function (ev) {
  2264. var target = ev.target,
  2265. $dd = $(target).parent('dd'),
  2266. $div = $dd.find('div.room-info');
  2267. if ($div.length) {
  2268. $div.remove();
  2269. } else {
  2270. $dd.find('span.spinner').remove();
  2271. $dd.append('<span class="spinner hor_centered"/>');
  2272. converse.connection.disco.info(
  2273. $(target).attr('data-room-jid'),
  2274. null,
  2275. function (stanza) {
  2276. var $stanza = $(stanza);
  2277. // All MUC features found here: http://xmpp.org/registrar/disco-features.html
  2278. $dd.find('span.spinner').replaceWith(
  2279. converse.templates.room_description({
  2280. 'desc': $stanza.find('field[var="muc#roominfo_description"] value').text(),
  2281. 'occ': $stanza.find('field[var="muc#roominfo_occupants"] value').text(),
  2282. 'hidden': $stanza.find('feature[var="muc_hidden"]').length,
  2283. 'membersonly': $stanza.find('feature[var="muc_membersonly"]').length,
  2284. 'moderated': $stanza.find('feature[var="muc_moderated"]').length,
  2285. 'nonanonymous': $stanza.find('feature[var="muc_nonanonymous"]').length,
  2286. 'open': $stanza.find('feature[var="muc_open"]').length,
  2287. 'passwordprotected': $stanza.find('feature[var="muc_passwordprotected"]').length,
  2288. 'persistent': $stanza.find('feature[var="muc_persistent"]').length,
  2289. 'publicroom': $stanza.find('feature[var="muc_public"]').length,
  2290. 'semianonymous': $stanza.find('feature[var="muc_semianonymous"]').length,
  2291. 'temporary': $stanza.find('feature[var="muc_temporary"]').length,
  2292. 'unmoderated': $stanza.find('feature[var="muc_unmoderated"]').length,
  2293. 'label_desc': __('Description:'),
  2294. 'label_occ': __('Occupants:'),
  2295. 'label_features': __('Features:'),
  2296. 'label_requires_auth': __('Requires authentication'),
  2297. 'label_hidden': __('Hidden'),
  2298. 'label_requires_invite': __('Requires an invitation'),
  2299. 'label_moderated': __('Moderated'),
  2300. 'label_non_anon': __('Non-anonymous'),
  2301. 'label_open_room': __('Open room'),
  2302. 'label_permanent_room': __('Permanent room'),
  2303. 'label_public': __('Public'),
  2304. 'label_semi_anon': __('Semi-anonymous'),
  2305. 'label_temp_room': __('Temporary room'),
  2306. 'label_unmoderated': __('Unmoderated')
  2307. }));
  2308. }.bind(this));
  2309. }
  2310. },
  2311. createChatRoom: function (ev) {
  2312. ev.preventDefault();
  2313. var name, $name,
  2314. server, $server,
  2315. jid,
  2316. $nick = this.$el.find('input.new-chatroom-nick'),
  2317. nick = $nick.val(),
  2318. chatroom;
  2319. if (!nick) { $nick.addClass('error'); }
  2320. else { $nick.removeClass('error'); }
  2321. if (ev.type === 'click') {
  2322. name = $(ev.target).text();
  2323. jid = $(ev.target).attr('data-room-jid');
  2324. } else {
  2325. $name = this.$el.find('input.new-chatroom-name');
  2326. $server= this.$el.find('input.new-chatroom-server');
  2327. server = $server.val();
  2328. name = $name.val().trim();
  2329. $name.val(''); // Clear the input
  2330. if (name && server) {
  2331. jid = Strophe.escapeNode(name.toLowerCase()) + '@' + server.toLowerCase();
  2332. $name.removeClass('error');
  2333. $server.removeClass('error');
  2334. this.model.save({muc_domain: server});
  2335. } else {
  2336. if (!name) { $name.addClass('error'); }
  2337. if (!server) { $server.addClass('error'); }
  2338. return;
  2339. }
  2340. }
  2341. if (!nick) { return; }
  2342. chatroom = converse.chatboxviews.showChat({
  2343. 'id': jid,
  2344. 'jid': jid,
  2345. 'name': name || Strophe.unescapeNode(Strophe.getNodeFromJid(jid)),
  2346. 'nick': nick,
  2347. 'chatroom': true,
  2348. 'box_id' : b64_sha1(jid)
  2349. });
  2350. },
  2351. setDomain: function (ev) {
  2352. this.model.save({muc_domain: ev.target.value});
  2353. },
  2354. setNick: function (ev) {
  2355. this.model.save({nick: ev.target.value});
  2356. }
  2357. });
  2358. this.ControlBoxView = converse.ChatBoxView.extend({
  2359. tagName: 'div',
  2360. className: 'chatbox',
  2361. id: 'controlbox',
  2362. events: {
  2363. 'click a.close-chatbox-button': 'close',
  2364. 'click ul#controlbox-tabs li a': 'switchTab',
  2365. 'mousedown .dragresize-top': 'onStartVerticalResize',
  2366. 'mousedown .dragresize-left': 'onStartHorizontalResize',
  2367. 'mousedown .dragresize-topleft': 'onStartDiagonalResize'
  2368. },
  2369. initialize: function () {
  2370. this.$el.insertAfter(converse.controlboxtoggle.$el);
  2371. $(window).on('resize', _.debounce(this.setDimensions.bind(this), 100));
  2372. this.model.on('change:connected', this.onConnected, this);
  2373. this.model.on('destroy', this.hide, this);
  2374. this.model.on('hide', this.hide, this);
  2375. this.model.on('show', this.show, this);
  2376. this.model.on('change:closed', this.ensureClosedState, this);
  2377. this.render();
  2378. if (this.model.get('connected')) {
  2379. this.initRoster();
  2380. }
  2381. if (typeof this.model.get('closed')==='undefined') {
  2382. this.model.set('closed', !converse.show_controlbox_by_default);
  2383. }
  2384. if (!this.model.get('closed')) {
  2385. this.show();
  2386. } else {
  2387. this.hide();
  2388. }
  2389. },
  2390. render: function () {
  2391. if (!converse.connection.connected || !converse.connection.authenticated || converse.connection.disconnecting) {
  2392. // TODO: we might need to take prebinding into consideration here.
  2393. this.renderLoginPanel();
  2394. } else if (!this.contactspanel || !this.contactspanel.$el.is(':visible')) {
  2395. this.renderContactsPanel();
  2396. }
  2397. return this;
  2398. },
  2399. giveFeedback: function (message, klass) {
  2400. var $el = this.$('.conn-feedback');
  2401. $el.addClass('conn-feedback').text(message);
  2402. if (klass) {
  2403. $el.addClass(klass);
  2404. }
  2405. },
  2406. onConnected: function () {
  2407. if (this.model.get('connected')) {
  2408. this.render().initRoster();
  2409. converse.features.off('add', this.featureAdded, this);
  2410. converse.features.on('add', this.featureAdded, this);
  2411. // Features could have been added before the controlbox was
  2412. // initialized. Currently we're only interested in MUC
  2413. var feature = converse.features.findWhere({'var': Strophe.NS.MUC});
  2414. if (feature) {
  2415. this.featureAdded(feature);
  2416. }
  2417. }
  2418. },
  2419. initRoster: function () {
  2420. /* We initialize the roster, which will appear inside the
  2421. * Contacts Panel.
  2422. */
  2423. converse.roster = new converse.RosterContacts();
  2424. converse.roster.browserStorage = new Backbone.BrowserStorage[converse.storage](
  2425. b64_sha1('converse.contacts-'+converse.bare_jid));
  2426. var rostergroups = new converse.RosterGroups();
  2427. rostergroups.browserStorage = new Backbone.BrowserStorage[converse.storage](
  2428. b64_sha1('converse.roster.groups'+converse.bare_jid));
  2429. converse.rosterview = new converse.RosterView({model: rostergroups});
  2430. this.contactspanel.$el.append(converse.rosterview.$el);
  2431. converse.rosterview.render().fetch().update();
  2432. return this;
  2433. },
  2434. renderLoginPanel: function () {
  2435. var $feedback = this.$('.conn-feedback'); // we want to still show any existing feedback.
  2436. this.$el.html(converse.templates.controlbox(this.model.toJSON()));
  2437. var cfg = {'$parent': this.$el.find('.controlbox-panes'), 'model': this};
  2438. if (!this.loginpanel) {
  2439. this.loginpanel = new converse.LoginPanel(cfg);
  2440. if (converse.allow_registration) {
  2441. this.registerpanel = new converse.RegisterPanel(cfg);
  2442. }
  2443. } else {
  2444. this.loginpanel.delegateEvents().initialize(cfg);
  2445. if (converse.allow_registration) {
  2446. this.registerpanel.delegateEvents().initialize(cfg);
  2447. }
  2448. }
  2449. this.loginpanel.render();
  2450. if (converse.allow_registration) {
  2451. this.registerpanel.render().$el.hide();
  2452. }
  2453. this.initDragResize().setDimensions();
  2454. if ($feedback.length && $feedback.text() !== __('Connecting')) {
  2455. this.$('.conn-feedback').replaceWith($feedback);
  2456. }
  2457. return this;
  2458. },
  2459. renderContactsPanel: function () {
  2460. this.$el.html(converse.templates.controlbox(this.model.toJSON()));
  2461. this.contactspanel = new converse.ContactsPanel({'$parent': this.$el.find('.controlbox-panes')});
  2462. this.contactspanel.render();
  2463. converse.xmppstatusview = new converse.XMPPStatusView({'model': converse.xmppstatus});
  2464. converse.xmppstatusview.render();
  2465. if (converse.allow_muc) {
  2466. this.roomspanel = new converse.RoomsPanel({
  2467. '$parent': this.$el.find('.controlbox-panes'),
  2468. 'model': new (Backbone.Model.extend({
  2469. id: b64_sha1('converse.roomspanel'+converse.bare_jid), // Required by sessionStorage
  2470. browserStorage: new Backbone.BrowserStorage[converse.storage](
  2471. b64_sha1('converse.roomspanel'+converse.bare_jid))
  2472. }))()
  2473. });
  2474. this.roomspanel.render().model.fetch();
  2475. if (!this.roomspanel.model.get('nick')) {
  2476. this.roomspanel.model.save({nick: Strophe.getNodeFromJid(converse.bare_jid)});
  2477. }
  2478. }
  2479. this.initDragResize().setDimensions();
  2480. },
  2481. close: function (ev) {
  2482. if (ev && ev.preventDefault) { ev.preventDefault(); }
  2483. if (converse.connection.connected) {
  2484. this.model.save({'closed': true});
  2485. } else {
  2486. this.model.trigger('hide');
  2487. }
  2488. converse.emit('controlBoxClosed', this);
  2489. return this;
  2490. },
  2491. ensureClosedState: function () {
  2492. if (this.model.get('closed')) {
  2493. this.hide();
  2494. } else {
  2495. this.show();
  2496. }
  2497. },
  2498. hide: function (callback) {
  2499. this.$el.hide('fast', function () {
  2500. converse.refreshWebkit();
  2501. converse.emit('chatBoxClosed', this);
  2502. converse.controlboxtoggle.show(function () {
  2503. if (typeof callback === "function") {
  2504. callback();
  2505. }
  2506. });
  2507. });
  2508. return this;
  2509. },
  2510. show: function () {
  2511. converse.controlboxtoggle.hide(function () {
  2512. this.$el.show('fast', function () {
  2513. if (converse.rosterview) {
  2514. converse.rosterview.update();
  2515. }
  2516. converse.refreshWebkit();
  2517. }.bind(this));
  2518. converse.emit('controlBoxOpened', this);
  2519. }.bind(this));
  2520. return this;
  2521. },
  2522. featureAdded: function (feature) {
  2523. if ((feature.get('var') === Strophe.NS.MUC) && (converse.allow_muc)) {
  2524. this.roomspanel.model.save({muc_domain: feature.get('from')});
  2525. var $server= this.$el.find('input.new-chatroom-server');
  2526. if (! $server.is(':focus')) {
  2527. $server.val(this.roomspanel.model.get('muc_domain'));
  2528. }
  2529. }
  2530. },
  2531. switchTab: function (ev) {
  2532. // TODO: automatically focus the relevant input
  2533. if (ev && ev.preventDefault) { ev.preventDefault(); }
  2534. var $tab = $(ev.target),
  2535. $sibling = $tab.parent().siblings('li').children('a'),
  2536. $tab_panel = $($tab.attr('href'));
  2537. $($sibling.attr('href')).hide();
  2538. $sibling.removeClass('current');
  2539. $tab.addClass('current');
  2540. $tab_panel.show();
  2541. return this;
  2542. },
  2543. showHelpMessages: function (msgs) {
  2544. // Override showHelpMessages in ChatBoxView, for now do nothing.
  2545. return;
  2546. }
  2547. });
  2548. this.ChatRoomOccupant = Backbone.Model;
  2549. this.ChatRoomOccupantView = Backbone.View.extend({
  2550. tagName: 'li',
  2551. initialize: function () {
  2552. this.model.on('add', this.render, this);
  2553. this.model.on('change', this.render, this);
  2554. this.model.on('destroy', this.destroy, this);
  2555. },
  2556. render: function () {
  2557. var $new = converse.templates.occupant(
  2558. _.extend(
  2559. this.model.toJSON(), {
  2560. 'desc_moderator': __('This user is a moderator'),
  2561. 'desc_occupant': __('This user can send messages in this room'),
  2562. 'desc_visitor': __('This user can NOT send messages in this room')
  2563. })
  2564. );
  2565. this.$el.replaceWith($new);
  2566. this.setElement($new, true);
  2567. return this;
  2568. },
  2569. destroy: function () {
  2570. this.$el.remove();
  2571. }
  2572. });
  2573. this.ChatRoomOccupants = Backbone.Collection.extend({
  2574. model: converse.ChatRoomOccupant
  2575. });
  2576. this.ChatRoomOccupantsView = Backbone.Overview.extend({
  2577. tagName: 'div',
  2578. className: 'occupants',
  2579. initialize: function () {
  2580. this.model.on("add", this.onOccupantAdded, this);
  2581. },
  2582. render: function () {
  2583. this.$el.html(
  2584. converse.templates.chatroom_sidebar({
  2585. 'label_invitation': __('Invite...'),
  2586. 'label_occupants': __('Occupants')
  2587. })
  2588. );
  2589. return this.initInviteWidget();
  2590. },
  2591. onOccupantAdded: function (item) {
  2592. var view = this.get(item.get('id'));
  2593. if (!view) {
  2594. view = this.add(item.get('id'), new converse.ChatRoomOccupantView({model: item}));
  2595. } else {
  2596. delete view.model; // Remove ref to old model to help garbage collection
  2597. view.model = item;
  2598. view.initialize();
  2599. }
  2600. this.$('.occupant-list').append(view.render().$el);
  2601. },
  2602. parsePresence: function (pres) {
  2603. var id = Strophe.getResourceFromJid(pres.getAttribute("from"));
  2604. var data = {
  2605. id: id,
  2606. nick: id,
  2607. type: pres.getAttribute("type"),
  2608. states: []
  2609. };
  2610. _.each(pres.childNodes, function (child) {
  2611. switch (child.nodeName) {
  2612. case "status":
  2613. data.status = child.textContent || null;
  2614. break;
  2615. case "show":
  2616. data.show = child.textContent || null;
  2617. break;
  2618. case "x":
  2619. if (child.getAttribute("xmlns") === Strophe.NS.MUC_USER) {
  2620. _.each(child.childNodes, function (item) {
  2621. switch (item.nodeName) {
  2622. case "item":
  2623. data.affiliation = item.getAttribute("affiliation");
  2624. data.role = item.getAttribute("role");
  2625. data.jid = item.getAttribute("jid");
  2626. data.nick = item.getAttribute("nick") || data.nick;
  2627. break;
  2628. case "status":
  2629. if (item.getAttribute("code")) {
  2630. data.states.push(item.getAttribute("code"));
  2631. }
  2632. }
  2633. });
  2634. }
  2635. }
  2636. });
  2637. return data;
  2638. },
  2639. updateOccupantsOnPresence: function (pres) {
  2640. var occupant;
  2641. var data = this.parsePresence(pres);
  2642. switch (data.type) {
  2643. case 'error':
  2644. return true;
  2645. case 'unavailable':
  2646. occupant = this.model.get(data.id);
  2647. if (occupant) { occupant.destroy(); }
  2648. break;
  2649. default:
  2650. occupant = this.model.get(data.id);
  2651. if (occupant) {
  2652. occupant.save(data);
  2653. } else {
  2654. this.model.create(data);
  2655. }
  2656. }
  2657. },
  2658. initInviteWidget: function () {
  2659. var $el = this.$('input.invited-contact');
  2660. $el.typeahead({
  2661. minLength: 1,
  2662. highlight: true
  2663. }, {
  2664. name: 'contacts-dataset',
  2665. source: function (q, cb) {
  2666. var results = [];
  2667. _.each(converse.roster.filter(contains(['fullname', 'jid'], q)), function (n) {
  2668. results.push({value: n.get('fullname'), jid: n.get('jid')});
  2669. });
  2670. cb(results);
  2671. },
  2672. templates: {
  2673. suggestion: _.template('<p data-jid="{{jid}}">{{value}}</p>')
  2674. }
  2675. });
  2676. $el.on('typeahead:selected', function (ev, suggestion, dname) {
  2677. var reason = prompt(
  2678. __(___('You are about to invite %1$s to the chat room "%2$s". '), suggestion.value, this.model.get('id')) +
  2679. __("You may optionally include a message, explaining the reason for the invitation.")
  2680. );
  2681. if (reason !== null) {
  2682. this.chatroomview.directInvite(suggestion.jid, reason);
  2683. }
  2684. $(ev.target).typeahead('val', '');
  2685. }.bind(this));
  2686. return this;
  2687. }
  2688. });
  2689. this.ChatRoomView = converse.ChatBoxView.extend({
  2690. length: 300,
  2691. tagName: 'div',
  2692. className: 'chatbox chatroom',
  2693. events: {
  2694. 'click .close-chatbox-button': 'close',
  2695. 'click .toggle-chatbox-button': 'minimize',
  2696. 'click .configure-chatroom-button': 'configureChatRoom',
  2697. 'click .toggle-smiley': 'toggleEmoticonMenu',
  2698. 'click .toggle-smiley ul li': 'insertEmoticon',
  2699. 'click .toggle-clear': 'clearChatRoomMessages',
  2700. 'click .toggle-call': 'toggleCall',
  2701. 'click .toggle-occupants a': 'toggleOccupants',
  2702. 'keypress textarea.chat-textarea': 'keyPressed',
  2703. 'mousedown .dragresize-top': 'onStartVerticalResize',
  2704. 'mousedown .dragresize-left': 'onStartHorizontalResize',
  2705. 'mousedown .dragresize-topleft': 'onStartDiagonalResize'
  2706. },
  2707. is_chatroom: true,
  2708. initialize: function () {
  2709. $(window).on('resize', _.debounce(this.setDimensions.bind(this), 100));
  2710. this.model.messages.on('add', this.onMessageAdded, this);
  2711. this.model.on('change:minimized', function (item) {
  2712. if (item.get('minimized')) {
  2713. this.hide();
  2714. } else {
  2715. this.maximize();
  2716. }
  2717. }, this);
  2718. this.model.on('destroy', function () {
  2719. this.hide().leave();
  2720. }, this);
  2721. this.occupantsview = new converse.ChatRoomOccupantsView({
  2722. model: new converse.ChatRoomOccupants({nick: this.model.get('nick')})
  2723. });
  2724. var id = b64_sha1('converse.occupants'+converse.bare_jid+this.model.get('id')+this.model.get('nick'));
  2725. this.occupantsview.model.browserStorage = new Backbone.BrowserStorage[converse.storage](id);
  2726. this.occupantsview.chatroomview = this;
  2727. this.render().$el.hide();
  2728. this.occupantsview.model.fetch({add:true});
  2729. this.join(null, {'maxstanzas': converse.muc_history_max_stanzas});
  2730. this.fetchMessages();
  2731. converse.emit('chatRoomOpened', this);
  2732. this.$el.insertAfter(converse.chatboxviews.get("controlbox").$el);
  2733. if (this.model.get('minimized')) {
  2734. this.hide();
  2735. } else {
  2736. this.show();
  2737. }
  2738. },
  2739. render: function () {
  2740. this.$el.attr('id', this.model.get('box_id'))
  2741. .html(converse.templates.chatroom(this.model.toJSON()));
  2742. this.renderChatArea();
  2743. this.$content.on('scroll', _.debounce(this.onScroll.bind(this), 100));
  2744. this.setWidth();
  2745. setTimeout(converse.refreshWebkit, 50);
  2746. return this;
  2747. },
  2748. renderChatArea: function () {
  2749. if (!this.$('.chat-area').length) {
  2750. this.$('.chatroom-body').empty()
  2751. .append(
  2752. converse.templates.chatarea({
  2753. 'show_toolbar': converse.show_toolbar,
  2754. 'label_message': __('Message')
  2755. }))
  2756. .append(this.occupantsview.render().$el);
  2757. this.renderToolbar();
  2758. this.$content = this.$el.find('.chat-content');
  2759. }
  2760. this.toggleOccupants(null, true);
  2761. return this;
  2762. },
  2763. toggleOccupants: function (ev, preserve_state) {
  2764. if (ev) {
  2765. ev.preventDefault();
  2766. ev.stopPropagation();
  2767. }
  2768. if (preserve_state) {
  2769. // Bit of a hack, to make sure that the sidebar's state doesn't change
  2770. this.model.set({hidden_occupants: !this.model.get('hidden_occupants')});
  2771. }
  2772. var $el = this.$('.icon-hide-users');
  2773. if (!this.model.get('hidden_occupants')) {
  2774. this.model.save({hidden_occupants: true});
  2775. $el.removeClass('icon-hide-users').addClass('icon-show-users');
  2776. this.$('.occupants').addClass('hidden');
  2777. this.$('.chat-area').addClass('full');
  2778. this.scrollDown();
  2779. } else {
  2780. this.model.save({hidden_occupants: false});
  2781. $el.removeClass('icon-show-users').addClass('icon-hide-users');
  2782. this.$('.chat-area').removeClass('full');
  2783. this.$('div.occupants').removeClass('hidden');
  2784. this.scrollDown();
  2785. }
  2786. },
  2787. directInvite: function (receiver, reason) {
  2788. var attrs = {
  2789. xmlns: 'jabber:x:conference',
  2790. jid: this.model.get('jid')
  2791. };
  2792. if (reason !== null) { attrs.reason = reason; }
  2793. if (this.model.get('password')) { attrs.password = this.model.get('password'); }
  2794. var invitation = $msg({
  2795. from: converse.connection.jid,
  2796. to: receiver,
  2797. id: converse.connection.getUniqueId()
  2798. }).c('x', attrs);
  2799. converse.connection.send(invitation);
  2800. converse.emit('roomInviteSent', this, receiver, reason);
  2801. },
  2802. onCommandError: function (stanza) {
  2803. this.showStatusNotification(__("Error: could not execute the command"), true);
  2804. },
  2805. sendChatRoomMessage: function (text) {
  2806. var msgid = converse.connection.getUniqueId();
  2807. var msg = $msg({
  2808. to: this.model.get('jid'),
  2809. from: converse.connection.jid,
  2810. type: 'groupchat',
  2811. id: msgid
  2812. }).c("body").t(text).up()
  2813. .c("x", {xmlns: "jabber:x:event"}).c("composing");
  2814. converse.connection.send(msg);
  2815. var fullname = converse.xmppstatus.get('fullname');
  2816. this.model.messages.create({
  2817. fullname: _.isEmpty(fullname)? converse.bare_jid: fullname,
  2818. sender: 'me',
  2819. time: moment().format(),
  2820. message: text,
  2821. msgid: msgid
  2822. });
  2823. },
  2824. setAffiliation: function(room, jid, affiliation, reason, onSuccess, onError) {
  2825. var item = $build("item", {jid: jid, affiliation: affiliation});
  2826. var iq = $iq({to: room, type: "set"}).c("query", {xmlns: Strophe.NS.MUC_ADMIN}).cnode(item.node);
  2827. if (reason !== null) { iq.c("reason", reason); }
  2828. return converse.connection.sendIQ(iq.tree(), onSuccess, onError);
  2829. },
  2830. modifyRole: function(room, nick, role, reason, onSuccess, onError) {
  2831. var item = $build("item", {nick: nick, role: role});
  2832. var iq = $iq({to: room, type: "set"}).c("query", {xmlns: Strophe.NS.MUC_ADMIN}).cnode(item.node);
  2833. if (reason !== null) { iq.c("reason", reason); }
  2834. return converse.connection.sendIQ(iq.tree(), onSuccess, onError);
  2835. },
  2836. member: function(room, jid, reason, handler_cb, error_cb) {
  2837. return this.setAffiliation(room, jid, 'member', reason, handler_cb, error_cb);
  2838. },
  2839. revoke: function(room, jid, reason, handler_cb, error_cb) {
  2840. return this.setAffiliation(room, jid, 'none', reason, handler_cb, error_cb);
  2841. },
  2842. owner: function(room, jid, reason, handler_cb, error_cb) {
  2843. return this.setAffiliation(room, jid, 'owner', reason, handler_cb, error_cb);
  2844. },
  2845. admin: function(room, jid, reason, handler_cb, error_cb) {
  2846. return this.setAffiliation(room, jid, 'admin', reason, handler_cb, error_cb);
  2847. },
  2848. validateRoleChangeCommand: function (command, args) {
  2849. /* Check that a command to change a chat room user's role or
  2850. * affiliation has anough arguments.
  2851. */
  2852. // TODO check if first argument is valid
  2853. if (args.length < 1 || args.length > 2) {
  2854. this.showStatusNotification(
  2855. __("Error: the \""+command+"\" command takes two arguments, the user's nickname and optionally a reason."),
  2856. true
  2857. );
  2858. return false;
  2859. }
  2860. return true;
  2861. },
  2862. onChatRoomMessageSubmitted: function (text) {
  2863. /* Gets called when the user presses enter to send off a
  2864. * message in a chat room.
  2865. *
  2866. * Parameters:
  2867. * (String) text - The message text.
  2868. */
  2869. var match = text.replace(/^\s*/, "").match(/^\/(.*?)(?: (.*))?$/) || [false, '', ''],
  2870. args = match[2] && match[2].splitOnce(' ') || [];
  2871. switch (match[1]) {
  2872. case 'admin':
  2873. if (!this.validateRoleChangeCommand(match[1], args)) { break; }
  2874. this.setAffiliation(
  2875. this.model.get('jid'), args[0], 'admin', args[1],
  2876. undefined, this.onCommandError.bind(this));
  2877. break;
  2878. case 'ban':
  2879. if (!this.validateRoleChangeCommand(match[1], args)) { break; }
  2880. this.setAffiliation(
  2881. this.model.get('jid'), args[0], 'outcast', args[1],
  2882. undefined, this.onCommandError.bind(this));
  2883. break;
  2884. case 'clear':
  2885. this.clearChatRoomMessages();
  2886. break;
  2887. case 'deop':
  2888. if (!this.validateRoleChangeCommand(match[1], args)) { break; }
  2889. this.modifyRole(
  2890. this.model.get('jid'), args[0], 'occupant', args[1],
  2891. undefined, this.onCommandError.bind(this));
  2892. break;
  2893. case 'help':
  2894. this.showHelpMessages([
  2895. '<strong>/admin</strong>: ' +__("Change user's affiliation to admin"),
  2896. '<strong>/ban</strong>: ' +__('Ban user from room'),
  2897. '<strong>/clear</strong>: ' +__('Remove messages'),
  2898. '<strong>/deop</strong>: ' +__('Change user role to occupant'),
  2899. '<strong>/help</strong>: ' +__('Show this menu'),
  2900. '<strong>/kick</strong>: ' +__('Kick user from room'),
  2901. '<strong>/me</strong>: ' +__('Write in 3rd person'),
  2902. '<strong>/member</strong>: '+__('Grant membership to a user'),
  2903. '<strong>/mute</strong>: ' +__("Remove user's ability to post messages"),
  2904. '<strong>/nick</strong>: ' +__('Change your nickname'),
  2905. '<strong>/op</strong>: ' +__('Grant moderator role to user'),
  2906. '<strong>/owner</strong>: ' +__('Grant ownership of this room'),
  2907. '<strong>/revoke</strong>: '+__("Revoke user's membership"),
  2908. '<strong>/topic</strong>: ' +__('Set room topic'),
  2909. '<strong>/voice</strong>: ' +__('Allow muted user to post messages')
  2910. ]);
  2911. break;
  2912. case 'kick':
  2913. if (!this.validateRoleChangeCommand(match[1], args)) { break; }
  2914. this.modifyRole(
  2915. this.model.get('jid'), args[0], 'none', args[1],
  2916. undefined, this.onCommandError.bind(this));
  2917. break;
  2918. case 'mute':
  2919. if (!this.validateRoleChangeCommand(match[1], args)) { break; }
  2920. this.modifyRole(
  2921. this.model.get('jid'), args[0], 'visitor', args[1],
  2922. undefined, this.onCommandError.bind(this));
  2923. break;
  2924. case 'member':
  2925. if (!this.validateRoleChangeCommand(match[1], args)) { break; }
  2926. this.setAffiliation(
  2927. this.model.get('jid'), args[0], 'member', args[1],
  2928. undefined, this.onCommandError.bind(this));
  2929. break;
  2930. case 'nick':
  2931. converse.connection.send($pres({
  2932. from: converse.connection.jid,
  2933. to: this.getRoomJIDAndNick(match[2]),
  2934. id: converse.connection.getUniqueId()
  2935. }).tree());
  2936. break;
  2937. case 'owner':
  2938. if (!this.validateRoleChangeCommand(match[1], args)) { break; }
  2939. this.setAffiliation(
  2940. this.model.get('jid'), args[0], 'owner', args[1],
  2941. undefined, this.onCommandError.bind(this));
  2942. break;
  2943. case 'op':
  2944. if (!this.validateRoleChangeCommand(match[1], args)) { break; }
  2945. this.modifyRole(
  2946. this.model.get('jid'), args[0], 'moderator', args[1],
  2947. undefined, this.onCommandError.bind(this));
  2948. break;
  2949. case 'revoke':
  2950. if (!this.validateRoleChangeCommand(match[1], args)) { break; }
  2951. this.setAffiliation(
  2952. this.model.get('jid'), args[0], 'none', args[1],
  2953. undefined, this.onCommandError.bind(this));
  2954. break;
  2955. case 'topic':
  2956. converse.connection.send(
  2957. $msg({
  2958. to: this.model.get('jid'),
  2959. from: converse.connection.jid,
  2960. type: "groupchat"
  2961. }).c("subject", {xmlns: "jabber:client"}).t(match[2]).tree()
  2962. );
  2963. break;
  2964. case 'voice':
  2965. if (!this.validateRoleChangeCommand(match[1], args)) { break; }
  2966. this.modifyRole(
  2967. this.model.get('jid'), args[0], 'occupant', args[1],
  2968. undefined, this.onCommandError.bind(this));
  2969. break;
  2970. default:
  2971. this.sendChatRoomMessage(text);
  2972. break;
  2973. }
  2974. },
  2975. handleMUCStanza: function (stanza) {
  2976. var xmlns, xquery, i;
  2977. var from = stanza.getAttribute('from');
  2978. var is_mam = $(stanza).find('[xmlns="'+Strophe.NS.MAM+'"]').length > 0;
  2979. if (!from || (this.model.get('id') !== from.split("/")[0]) || is_mam) {
  2980. return true;
  2981. }
  2982. if (stanza.nodeName === "message") {
  2983. _.compose(this.onChatRoomMessage.bind(this), this.showStatusMessages.bind(this))(stanza);
  2984. } else if (stanza.nodeName === "presence") {
  2985. xquery = stanza.getElementsByTagName("x");
  2986. if (xquery.length > 0) {
  2987. for (i = 0; i < xquery.length; i++) {
  2988. xmlns = xquery[i].getAttribute("xmlns");
  2989. if (xmlns && xmlns.match(Strophe.NS.MUC)) {
  2990. this.onChatRoomPresence(stanza);
  2991. break;
  2992. }
  2993. }
  2994. }
  2995. }
  2996. return true;
  2997. },
  2998. getRoomJIDAndNick: function (nick) {
  2999. nick = nick || this.model.get('nick');
  3000. var room = this.model.get('jid');
  3001. var node = Strophe.getNodeFromJid(room);
  3002. var domain = Strophe.getDomainFromJid(room);
  3003. return node + "@" + domain + (nick !== null ? "/" + nick : "");
  3004. },
  3005. join: function (password, history_attrs, extended_presence) {
  3006. var stanza = $pres({
  3007. from: converse.connection.jid,
  3008. to: this.getRoomJIDAndNick()
  3009. }).c("x", {
  3010. xmlns: Strophe.NS.MUC
  3011. });
  3012. if (typeof history_attrs === "object" && Object.keys(history_attrs).length) {
  3013. stanza = stanza.c("history", history_attrs).up();
  3014. }
  3015. if (password) {
  3016. stanza.cnode(Strophe.xmlElement("password", [], password));
  3017. }
  3018. if (typeof extended_presence !== "undefined" && extended_presence !== null) {
  3019. stanza.up.cnode(extended_presence);
  3020. }
  3021. if (!this.handler) {
  3022. this.handler = converse.connection.addHandler(this.handleMUCStanza.bind(this));
  3023. }
  3024. this.model.set('connection_status', Strophe.Status.CONNECTING);
  3025. return converse.connection.send(stanza);
  3026. },
  3027. leave: function(exit_msg) {
  3028. var presenceid = converse.connection.getUniqueId();
  3029. var presence = $pres({
  3030. type: "unavailable",
  3031. id: presenceid,
  3032. from: converse.connection.jid,
  3033. to: this.getRoomJIDAndNick()
  3034. });
  3035. if (exit_msg !== null) {
  3036. presence.c("status", exit_msg);
  3037. }
  3038. converse.connection.addHandler(
  3039. function () { this.model.set('connection_status', Strophe.Status.DISCONNECTED); }.bind(this),
  3040. null, "presence", null, presenceid);
  3041. converse.connection.send(presence);
  3042. },
  3043. renderConfigurationForm: function (stanza) {
  3044. var $form = this.$el.find('form.chatroom-form'),
  3045. $fieldset = $form.children('fieldset:first'),
  3046. $stanza = $(stanza),
  3047. $fields = $stanza.find('field'),
  3048. title = $stanza.find('title').text(),
  3049. instructions = $stanza.find('instructions').text();
  3050. $fieldset.find('span.spinner').remove();
  3051. $fieldset.append($('<legend>').text(title));
  3052. if (instructions && instructions !== title) {
  3053. $fieldset.append($('<p class="instructions">').text(instructions));
  3054. }
  3055. _.each($fields, function (field) {
  3056. $fieldset.append(utils.xForm2webForm($(field), $stanza));
  3057. });
  3058. $form.append('<fieldset></fieldset>');
  3059. $fieldset = $form.children('fieldset:last');
  3060. $fieldset.append('<input type="submit" class="pure-button button-primary" value="'+__('Save')+'"/>');
  3061. $fieldset.append('<input type="button" class="pure-button button-cancel" value="'+__('Cancel')+'"/>');
  3062. $fieldset.find('input[type=button]').on('click', this.cancelConfiguration.bind(this));
  3063. $form.on('submit', this.saveConfiguration.bind(this));
  3064. },
  3065. sendConfiguration: function(config, onSuccess, onError) {
  3066. // Send an IQ stanza with the room configuration.
  3067. var iq = $iq({to: this.model.get('jid'), type: "set"})
  3068. .c("query", {xmlns: Strophe.NS.MUC_OWNER})
  3069. .c("x", {xmlns: Strophe.NS.XFORM, type: "submit"});
  3070. _.each(config, function (node) { iq.cnode(node).up(); });
  3071. return converse.connection.sendIQ(iq.tree(), onSuccess, onError);
  3072. },
  3073. saveConfiguration: function (ev) {
  3074. ev.preventDefault();
  3075. var that = this;
  3076. var $inputs = $(ev.target).find(':input:not([type=button]):not([type=submit])'),
  3077. count = $inputs.length,
  3078. configArray = [];
  3079. $inputs.each(function () {
  3080. configArray.push(utils.webForm2xForm(this));
  3081. if (!--count) {
  3082. that.sendConfiguration(
  3083. configArray,
  3084. that.onConfigSaved.bind(that),
  3085. that.onErrorConfigSaved.bind(that)
  3086. );
  3087. }
  3088. });
  3089. this.$el.find('div.chatroom-form-container').hide(
  3090. function () {
  3091. $(this).remove();
  3092. that.$el.find('.chat-area').removeClass('hidden');
  3093. that.$el.find('.occupants').removeClass('hidden');
  3094. });
  3095. },
  3096. onConfigSaved: function (stanza) {
  3097. // TODO: provide feedback
  3098. },
  3099. onErrorConfigSaved: function (stanza) {
  3100. this.showStatusNotification(__("An error occurred while trying to save the form."));
  3101. },
  3102. cancelConfiguration: function (ev) {
  3103. ev.preventDefault();
  3104. var that = this;
  3105. this.$el.find('div.chatroom-form-container').hide(
  3106. function () {
  3107. $(this).remove();
  3108. that.$el.find('.chat-area').removeClass('hidden');
  3109. that.$el.find('.occupants').removeClass('hidden');
  3110. });
  3111. },
  3112. configureChatRoom: function (ev) {
  3113. ev.preventDefault();
  3114. if (this.$el.find('div.chatroom-form-container').length) {
  3115. return;
  3116. }
  3117. this.$('.chatroom-body').children().addClass('hidden');
  3118. this.$('.chatroom-body').append(converse.templates.chatroom_form());
  3119. converse.connection.sendIQ(
  3120. $iq({
  3121. to: this.model.get('jid'),
  3122. type: "get"
  3123. }).c("query", {xmlns: Strophe.NS.MUC_OWNER}).tree(),
  3124. this.renderConfigurationForm.bind(this)
  3125. );
  3126. },
  3127. submitPassword: function (ev) {
  3128. ev.preventDefault();
  3129. var password = this.$el.find('.chatroom-form').find('input[type=password]').val();
  3130. this.$el.find('.chatroom-form-container').replaceWith('<span class="spinner centered"/>');
  3131. this.join(password);
  3132. },
  3133. renderPasswordForm: function () {
  3134. this.$('.chatroom-body').children().hide();
  3135. this.$('span.centered.spinner').remove();
  3136. this.$('.chatroom-body').append(
  3137. converse.templates.chatroom_password_form({
  3138. heading: __('This chatroom requires a password'),
  3139. label_password: __('Password: '),
  3140. label_submit: __('Submit')
  3141. }));
  3142. this.$('.chatroom-form').on('submit', this.submitPassword.bind(this));
  3143. },
  3144. showDisconnectMessage: function (msg) {
  3145. this.$('.chat-area').hide();
  3146. this.$('.occupants').hide();
  3147. this.$('span.centered.spinner').remove();
  3148. this.$('.chatroom-body').append($('<p>'+msg+'</p>'));
  3149. },
  3150. /* http://xmpp.org/extensions/xep-0045.html
  3151. * ----------------------------------------
  3152. * 100 message Entering a room Inform user that any occupant is allowed to see the user's full JID
  3153. * 101 message (out of band) Affiliation change Inform user that his or her affiliation changed while not in the room
  3154. * 102 message Configuration change Inform occupants that room now shows unavailable members
  3155. * 103 message Configuration change Inform occupants that room now does not show unavailable members
  3156. * 104 message Configuration change Inform occupants that a non-privacy-related room configuration change has occurred
  3157. * 110 presence Any room presence Inform user that presence refers to one of its own room occupants
  3158. * 170 message or initial presence Configuration change Inform occupants that room logging is now enabled
  3159. * 171 message Configuration change Inform occupants that room logging is now disabled
  3160. * 172 message Configuration change Inform occupants that the room is now non-anonymous
  3161. * 173 message Configuration change Inform occupants that the room is now semi-anonymous
  3162. * 174 message Configuration change Inform occupants that the room is now fully-anonymous
  3163. * 201 presence Entering a room Inform user that a new room has been created
  3164. * 210 presence Entering a room Inform user that the service has assigned or modified the occupant's roomnick
  3165. * 301 presence Removal from room Inform user that he or she has been banned from the room
  3166. * 303 presence Exiting a room Inform all occupants of new room nickname
  3167. * 307 presence Removal from room Inform user that he or she has been kicked from the room
  3168. * 321 presence Removal from room Inform user that he or she is being removed from the room because of an affiliation change
  3169. * 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
  3170. * 332 presence Removal from room Inform user that he or she is being removed from the room because of a system shutdown
  3171. */
  3172. infoMessages: {
  3173. 100: __('This room is not anonymous'),
  3174. 102: __('This room now shows unavailable members'),
  3175. 103: __('This room does not show unavailable members'),
  3176. 104: __('Non-privacy-related room configuration has changed'),
  3177. 170: __('Room logging is now enabled'),
  3178. 171: __('Room logging is now disabled'),
  3179. 172: __('This room is now non-anonymous'),
  3180. 173: __('This room is now semi-anonymous'),
  3181. 174: __('This room is now fully-anonymous'),
  3182. 201: __('A new room has been created')
  3183. },
  3184. disconnectMessages: {
  3185. 301: __('You have been banned from this room'),
  3186. 307: __('You have been kicked from this room'),
  3187. 321: __("You have been removed from this room because of an affiliation change"),
  3188. 322: __("You have been removed from this room because the room has changed to members-only and you're not a member"),
  3189. 332: __("You have been removed from this room because the MUC (Multi-user chat) service is being shut down.")
  3190. },
  3191. actionInfoMessages: {
  3192. /* XXX: Note the triple underscore function and not double
  3193. * underscore.
  3194. *
  3195. * This is a hack. We can't pass the strings to __ because we
  3196. * don't yet know what the variable to interpolate is.
  3197. *
  3198. * Triple underscore will just return the string again, but we
  3199. * can then at least tell gettext to scan for it so that these
  3200. * strings are picked up by the translation machinery.
  3201. */
  3202. 301: ___("<strong>%1$s</strong> has been banned"),
  3203. 303: ___("<strong>%1$s</strong>'s nickname has changed"),
  3204. 307: ___("<strong>%1$s</strong> has been kicked out"),
  3205. 321: ___("<strong>%1$s</strong> has been removed because of an affiliation change"),
  3206. 322: ___("<strong>%1$s</strong> has been removed for not being a member")
  3207. },
  3208. newNicknameMessages: {
  3209. 210: ___('Your nickname has been automatically changed to: <strong>%1$s</strong>'),
  3210. 303: ___('Your nickname has been changed to: <strong>%1$s</strong>')
  3211. },
  3212. showStatusMessages: function (el, is_self) {
  3213. /* Check for status codes and communicate their purpose to the user.
  3214. * Allow user to configure chat room if they are the owner.
  3215. * See: http://xmpp.org/registrar/mucstatus.html
  3216. */
  3217. var $el = $(el),
  3218. disconnect_msgs = [],
  3219. msgs = [],
  3220. reasons = [];
  3221. $el.find('x[xmlns="'+Strophe.NS.MUC_USER+'"]').each(function (idx, x) {
  3222. var $item = $(x).find('item');
  3223. if (Strophe.getBareJidFromJid($item.attr('jid')) === converse.bare_jid && $item.attr('affiliation') === 'owner') {
  3224. this.$el.find('a.configure-chatroom-button').show();
  3225. }
  3226. $(x).find('item reason').each(function (idx, reason) {
  3227. if ($(reason).text()) {
  3228. reasons.push($(reason).text());
  3229. }
  3230. });
  3231. $(x).find('status').each(function (idx, stat) {
  3232. var code = stat.getAttribute('code');
  3233. var from_nick = Strophe.unescapeNode(Strophe.getResourceFromJid($el.attr('from')));
  3234. if (is_self && code === "210") {
  3235. msgs.push(__(this.newNicknameMessages[code], from_nick));
  3236. } else if (is_self && code === "303") {
  3237. msgs.push(__(this.newNicknameMessages[code], $item.attr('nick')));
  3238. } else if (is_self && _.contains(_.keys(this.disconnectMessages), code)) {
  3239. disconnect_msgs.push(this.disconnectMessages[code]);
  3240. } else if (!is_self && _.contains(_.keys(this.actionInfoMessages), code)) {
  3241. msgs.push(__(this.actionInfoMessages[code], from_nick));
  3242. } else if (_.contains(_.keys(this.infoMessages), code)) {
  3243. msgs.push(this.infoMessages[code]);
  3244. } else if (code !== '110') {
  3245. if ($(stat).text()) {
  3246. msgs.push($(stat).text()); // Sometimes the status contains human readable text and not a code.
  3247. }
  3248. }
  3249. }.bind(this));
  3250. }.bind(this));
  3251. if (disconnect_msgs.length > 0) {
  3252. for (i=0; i<disconnect_msgs.length; i++) {
  3253. this.showDisconnectMessage(disconnect_msgs[i]);
  3254. }
  3255. for (i=0; i<reasons.length; i++) {
  3256. this.showDisconnectMessage(__('The reason given is: "'+reasons[i]+'"'), true);
  3257. }
  3258. this.model.set('connection_status', Strophe.Status.DISCONNECTED);
  3259. return;
  3260. }
  3261. for (i=0; i<msgs.length; i++) {
  3262. this.$content.append(converse.templates.info({message: msgs[i]}));
  3263. }
  3264. for (i=0; i<reasons.length; i++) {
  3265. this.showStatusNotification(__('The reason given is: "'+reasons[i]+'"'), true);
  3266. }
  3267. this.scrollDown();
  3268. return el;
  3269. },
  3270. showErrorMessage: function ($error) {
  3271. // We didn't enter the room, so we must remove it from the MUC
  3272. // add-on
  3273. if ($error.attr('type') === 'auth') {
  3274. if ($error.find('not-authorized').length) {
  3275. this.renderPasswordForm();
  3276. } else if ($error.find('registration-required').length) {
  3277. this.showDisconnectMessage(__('You are not on the member list of this room'));
  3278. } else if ($error.find('forbidden').length) {
  3279. this.showDisconnectMessage(__('You have been banned from this room'));
  3280. }
  3281. } else if ($error.attr('type') === 'modify') {
  3282. if ($error.find('jid-malformed').length) {
  3283. this.showDisconnectMessage(__('No nickname was specified'));
  3284. }
  3285. } else if ($error.attr('type') === 'cancel') {
  3286. if ($error.find('not-allowed').length) {
  3287. this.showDisconnectMessage(__('You are not allowed to create new rooms'));
  3288. } else if ($error.find('not-acceptable').length) {
  3289. this.showDisconnectMessage(__("Your nickname doesn't conform to this room's policies"));
  3290. } else if ($error.find('conflict').length) {
  3291. this.showDisconnectMessage(__("Your nickname is already taken"));
  3292. // TODO: give user the option of choosing a different nickname
  3293. } else if ($error.find('item-not-found').length) {
  3294. this.showDisconnectMessage(__("This room does not (yet) exist"));
  3295. } else if ($error.find('service-unavailable').length) {
  3296. this.showDisconnectMessage(__("This room has reached it's maximum number of occupants"));
  3297. }
  3298. }
  3299. },
  3300. onChatRoomPresence: function (pres) {
  3301. var $presence = $(pres), is_self;
  3302. var nick = this.model.get('nick');
  3303. if ($presence.attr('type') === 'error') {
  3304. this.model.set('connection_status', Strophe.Status.DISCONNECTED);
  3305. this.showErrorMessage($presence.find('error'));
  3306. } else {
  3307. is_self = ($presence.find("status[code='110']").length) ||
  3308. ($presence.attr('from') === this.model.get('id')+'/'+Strophe.escapeNode(nick));
  3309. if (this.model.get('connection_status') !== Strophe.Status.CONNECTED) {
  3310. this.model.set('connection_status', Strophe.Status.CONNECTED);
  3311. }
  3312. this.showStatusMessages(pres, is_self);
  3313. }
  3314. this.occupantsview.updateOccupantsOnPresence(pres);
  3315. },
  3316. onChatRoomMessage: function (message) {
  3317. var $message = $(message),
  3318. archive_id = $message.find('result[xmlns="'+Strophe.NS.MAM+'"]').attr('id'),
  3319. delayed = $message.find('delay').length > 0,
  3320. $forwarded = $message.find('forwarded'),
  3321. $delay;
  3322. if ($forwarded.length) {
  3323. $message = $forwarded.children('message');
  3324. $delay = $forwarded.children('delay');
  3325. delayed = $delay.length > 0;
  3326. }
  3327. var body = $message.children('body').text(),
  3328. jid = $message.attr('from'),
  3329. msgid = $message.attr('id'),
  3330. resource = Strophe.getResourceFromJid(jid),
  3331. sender = resource && Strophe.unescapeNode(resource) || '',
  3332. subject = $message.children('subject').text();
  3333. if (msgid && this.model.messages.findWhere({msgid: msgid})) {
  3334. return true; // We already have this message stored.
  3335. }
  3336. if (subject) {
  3337. this.$el.find('.chatroom-topic').text(subject).attr('title', subject);
  3338. // For translators: the %1$s and %2$s parts will get replaced by the user and topic text respectively
  3339. // Example: Topic set by JC Brand to: Hello World!
  3340. this.$content.append(
  3341. converse.templates.info({
  3342. 'message': __('Topic set by %1$s to: %2$s', sender, subject)
  3343. }));
  3344. }
  3345. if (sender === '') {
  3346. return true;
  3347. }
  3348. this.model.createMessage($message, $delay, archive_id);
  3349. if (!delayed && sender !== this.model.get('nick') && (new RegExp("\\b"+this.model.get('nick')+"\\b")).test(body)) {
  3350. converse.playNotification();
  3351. }
  3352. if (sender !== this.model.get('nick')) {
  3353. // We only emit an event if it's not our own message
  3354. converse.emit('message', message);
  3355. }
  3356. return true;
  3357. }
  3358. });
  3359. this.ChatBoxes = Backbone.Collection.extend({
  3360. model: converse.ChatBox,
  3361. comparator: 'time_opened',
  3362. registerMessageHandler: function () {
  3363. converse.connection.addHandler(
  3364. function (message) {
  3365. this.onMessage(message);
  3366. return true;
  3367. }.bind(this), null, 'message', 'chat');
  3368. converse.connection.addHandler(
  3369. function (message) {
  3370. this.onInvite(message);
  3371. return true;
  3372. }.bind(this), 'jabber:x:conference', 'message');
  3373. },
  3374. onConnected: function () {
  3375. this.browserStorage = new Backbone.BrowserStorage[converse.storage](
  3376. b64_sha1('converse.chatboxes-'+converse.bare_jid));
  3377. this.registerMessageHandler();
  3378. this.fetch({
  3379. add: true,
  3380. success: function (collection, resp) {
  3381. collection.each(function (chatbox) {
  3382. if (chatbox.get('id') !== 'controlbox' && !chatbox.get('minimized')) {
  3383. chatbox.trigger('show');
  3384. }
  3385. });
  3386. if (!_.include(_.pluck(resp, 'id'), 'controlbox')) {
  3387. this.add({
  3388. id: 'controlbox',
  3389. box_id: 'controlbox'
  3390. });
  3391. }
  3392. this.get('controlbox').save({connected:true});
  3393. }.bind(this)
  3394. });
  3395. },
  3396. isOnlyChatStateNotification: function ($msg) {
  3397. // See XEP-0085 Chat State Notification
  3398. return (
  3399. $msg.find('body').length === 0 && (
  3400. $msg.find(ACTIVE).length !== 0 ||
  3401. $msg.find(COMPOSING).length !== 0 ||
  3402. $msg.find(INACTIVE).length !== 0 ||
  3403. $msg.find(PAUSED).length !== 0 ||
  3404. $msg.find(GONE).length !== 0
  3405. )
  3406. );
  3407. },
  3408. onInvite: function (message) {
  3409. var $message = $(message),
  3410. $x = $message.children('x[xmlns="jabber:x:conference"]'),
  3411. from = Strophe.getBareJidFromJid($message.attr('from')),
  3412. room_jid = $x.attr('jid'),
  3413. reason = $x.attr('reason'),
  3414. contact = converse.roster.get(from),
  3415. result;
  3416. if (converse.auto_join_on_invite) {
  3417. result = true;
  3418. } else {
  3419. contact = contact? contact.get('fullname'): Strophe.getNodeFromJid(from); // Invite request might come from someone not your roster list
  3420. if (!reason) {
  3421. result = confirm(
  3422. __(___("%1$s has invited you to join a chat room: %2$s"), contact, room_jid)
  3423. );
  3424. } else {
  3425. result = confirm(
  3426. __(___('%1$s has invited you to join a chat room: %2$s, and left the following reason: "%3$s"'), contact, room_jid, reason)
  3427. );
  3428. }
  3429. }
  3430. if (result === true) {
  3431. var chatroom = converse.chatboxviews.showChat({
  3432. 'id': room_jid,
  3433. 'jid': room_jid,
  3434. 'name': Strophe.unescapeNode(Strophe.getNodeFromJid(room_jid)),
  3435. 'nick': Strophe.unescapeNode(Strophe.getNodeFromJid(converse.connection.jid)),
  3436. 'chatroom': true,
  3437. 'box_id' : b64_sha1(room_jid),
  3438. 'password': $x.attr('password')
  3439. });
  3440. if (!_.contains(
  3441. [Strophe.Status.CONNECTING, Strophe.Status.CONNECTED],
  3442. chatroom.get('connection_status'))
  3443. ) {
  3444. converse.chatboxviews.get(room_jid).join(null);
  3445. }
  3446. }
  3447. },
  3448. onMessage: function (message) {
  3449. /* Handler method for all incoming single-user chat "message" stanzas.
  3450. */
  3451. var $message = $(message),
  3452. contact_jid, $forwarded, $delay, from_bare_jid, from_resource, is_me, msgid,
  3453. chatbox, resource,
  3454. from_jid = $message.attr('from'),
  3455. to_jid = $message.attr('to'),
  3456. to_resource = Strophe.getResourceFromJid(to_jid),
  3457. archive_id = $message.find('result[xmlns="'+Strophe.NS.MAM+'"]').attr('id');
  3458. if (to_resource && to_resource !== converse.resource) {
  3459. converse.log('Ignore incoming message intended for a different resource: '+to_jid, 'info');
  3460. return true;
  3461. }
  3462. if (from_jid === converse.connection.jid) {
  3463. // FIXME: Forwarded messages should be sent to specific resources, not broadcasted
  3464. converse.log("Ignore incoming message sent from this client's JID: "+from_jid, 'info');
  3465. return true;
  3466. }
  3467. $forwarded = $message.find('forwarded');
  3468. if ($forwarded.length) {
  3469. $message = $forwarded.children('message');
  3470. $delay = $forwarded.children('delay');
  3471. from_jid = $message.attr('from');
  3472. to_jid = $message.attr('to');
  3473. }
  3474. from_bare_jid = Strophe.getBareJidFromJid(from_jid);
  3475. from_resource = Strophe.getResourceFromJid(from_jid);
  3476. is_me = from_bare_jid === converse.bare_jid;
  3477. msgid = $message.attr('id');
  3478. if (is_me) {
  3479. // I am the sender, so this must be a forwarded message...
  3480. contact_jid = Strophe.getBareJidFromJid(to_jid);
  3481. resource = Strophe.getResourceFromJid(to_jid);
  3482. } else {
  3483. contact_jid = from_bare_jid;
  3484. resource = from_resource;
  3485. }
  3486. // Get chat box, but only create a new one when the message has a body.
  3487. chatbox = this.getChatBox(contact_jid, $message.find('body').length > 0);
  3488. if (!chatbox) {
  3489. return true;
  3490. }
  3491. if (msgid && chatbox.messages.findWhere({msgid: msgid})) {
  3492. return true; // We already have this message stored.
  3493. }
  3494. if (!this.isOnlyChatStateNotification($message) && !is_me && !$forwarded.length) {
  3495. converse.playNotification();
  3496. }
  3497. chatbox.receiveMessage($message, $delay, archive_id);
  3498. converse.roster.addResource(contact_jid, resource);
  3499. converse.emit('message', message);
  3500. return true;
  3501. },
  3502. getChatBox: function (jid, create) {
  3503. /* Returns a chat box or optionally return a newly
  3504. * created one if one doesn't exist.
  3505. *
  3506. * Parameters:
  3507. * (String) jid - The JID of the user whose chat box we want
  3508. * (Boolean) create - Should a new chat box be created if none exists?
  3509. */
  3510. jid = jid.toLowerCase();
  3511. var bare_jid = Strophe.getBareJidFromJid(jid);
  3512. var chatbox = this.get(bare_jid);
  3513. if (!chatbox && create) {
  3514. var roster_item = converse.roster.get(bare_jid);
  3515. if (roster_item === undefined) {
  3516. converse.log('Could not get roster item for JID '+bare_jid, 'error');
  3517. return;
  3518. }
  3519. chatbox = this.create({
  3520. 'id': bare_jid,
  3521. 'jid': bare_jid,
  3522. 'fullname': _.isEmpty(roster_item.get('fullname'))? jid: roster_item.get('fullname'),
  3523. 'image_type': roster_item.get('image_type'),
  3524. 'image': roster_item.get('image'),
  3525. 'url': roster_item.get('url')
  3526. });
  3527. }
  3528. return chatbox;
  3529. }
  3530. });
  3531. this.ChatBoxViews = Backbone.Overview.extend({
  3532. initialize: function () {
  3533. this.model.on("add", this.onChatBoxAdded, this);
  3534. this.model.on("change:minimized", function (item) {
  3535. if (item.get('minimized') === true) {
  3536. /* When a chat is minimized in trimChats, trimChats needs to be
  3537. * called again (in case the minimized chats toggle is newly shown).
  3538. */
  3539. this.trimChats();
  3540. } else {
  3541. this.trimChats(this.get(item.get('id')));
  3542. }
  3543. }, this);
  3544. },
  3545. _ensureElement: function () {
  3546. /* Override method from backbone.js
  3547. * If the #conversejs element doesn't exist, create it.
  3548. */
  3549. if (!this.el) {
  3550. var $el = $('#conversejs');
  3551. if (!$el.length) {
  3552. $el = $('<div id="conversejs">');
  3553. $('body').append($el);
  3554. }
  3555. $el.html(converse.templates.chats_panel());
  3556. this.setElement($el, false);
  3557. } else {
  3558. this.setElement(_.result(this, 'el'), false);
  3559. }
  3560. },
  3561. onChatBoxAdded: function (item) {
  3562. var view = this.get(item.get('id'));
  3563. if (!view) {
  3564. if (item.get('chatroom')) {
  3565. view = new converse.ChatRoomView({'model': item});
  3566. } else if (item.get('box_id') === 'controlbox') {
  3567. view = new converse.ControlBoxView({model: item});
  3568. } else {
  3569. view = new converse.ChatBoxView({model: item});
  3570. }
  3571. this.add(item.get('id'), view);
  3572. } else {
  3573. delete view.model; // Remove ref to old model to help garbage collection
  3574. view.model = item;
  3575. view.initialize();
  3576. }
  3577. this.trimChats(view);
  3578. },
  3579. trimChats: function (newchat) {
  3580. /* This method is called when a newly created chat box will
  3581. * be shown.
  3582. *
  3583. * It checks whether there is enough space on the page to show
  3584. * another chat box. Otherwise it minimize the oldest chat box
  3585. * to create space.
  3586. */
  3587. if (converse.no_trimming || (this.model.length <= 1)) {
  3588. return;
  3589. }
  3590. var oldest_chat,
  3591. controlbox_width = 0,
  3592. $minimized = converse.minimized_chats.$el,
  3593. minimized_width = _.contains(this.model.pluck('minimized'), true) ? $minimized.outerWidth(true) : 0,
  3594. boxes_width = newchat ? newchat.$el.outerWidth(true) : 0,
  3595. new_id = newchat ? newchat.model.get('id') : null,
  3596. controlbox = this.get('controlbox');
  3597. if (!controlbox || !controlbox.$el.is(':visible')) {
  3598. controlbox_width = converse.controlboxtoggle.$el.outerWidth(true);
  3599. } else {
  3600. controlbox_width = controlbox.$el.outerWidth(true);
  3601. }
  3602. _.each(this.getAll(), function (view) {
  3603. var id = view.model.get('id');
  3604. if ((id !== 'controlbox') && (id !== new_id) && (!view.model.get('minimized')) && view.$el.is(':visible')) {
  3605. boxes_width += view.$el.outerWidth(true);
  3606. }
  3607. });
  3608. if ((minimized_width + boxes_width + controlbox_width) > $('body').outerWidth(true)) {
  3609. oldest_chat = this.getOldestMaximizedChat();
  3610. if (oldest_chat && oldest_chat.get('id') !== new_id) {
  3611. oldest_chat.minimize();
  3612. }
  3613. }
  3614. },
  3615. getOldestMaximizedChat: function () {
  3616. // Get oldest view (which is not controlbox)
  3617. var i = 0;
  3618. var model = this.model.sort().at(i);
  3619. while (model.get('id') === 'controlbox' || model.get('minimized') === true) {
  3620. i++;
  3621. model = this.model.at(i);
  3622. if (!model) {
  3623. return null;
  3624. }
  3625. }
  3626. return model;
  3627. },
  3628. closeAllChatBoxes: function (include_controlbox) {
  3629. // TODO: once Backbone.Overview has been refactored, we should
  3630. // be able to call .each on the views themselves.
  3631. var ids = [];
  3632. this.model.each(function (model) {
  3633. var id = model.get('id');
  3634. if (include_controlbox || id !== 'controlbox') {
  3635. ids.push(id);
  3636. }
  3637. });
  3638. ids.forEach(function(id) {
  3639. var chatbox = this.get(id);
  3640. if (chatbox) { chatbox.close(); }
  3641. }, this);
  3642. return this;
  3643. },
  3644. showChat: function (attrs) {
  3645. /* Find the chat box and show it. If it doesn't exist, create it.
  3646. */
  3647. var chatbox = this.model.get(attrs.jid);
  3648. if (!chatbox) {
  3649. chatbox = this.model.create(attrs, {
  3650. 'error': function (model, response) {
  3651. converse.log(response.responseText);
  3652. }
  3653. });
  3654. }
  3655. if (chatbox.get('minimized')) {
  3656. chatbox.maximize();
  3657. } else {
  3658. chatbox.trigger('show');
  3659. }
  3660. return chatbox;
  3661. }
  3662. });
  3663. this.MinimizedChatBoxView = Backbone.View.extend({
  3664. tagName: 'div',
  3665. className: 'chat-head',
  3666. events: {
  3667. 'click .close-chatbox-button': 'close',
  3668. 'click .restore-chat': 'restore'
  3669. },
  3670. initialize: function () {
  3671. this.model.messages.on('add', function (m) {
  3672. if (m.get('message')) {
  3673. this.updateUnreadMessagesCounter();
  3674. }
  3675. }, this);
  3676. this.model.on('change:minimized', this.clearUnreadMessagesCounter, this);
  3677. this.model.on('showReceivedOTRMessage', this.updateUnreadMessagesCounter, this);
  3678. this.model.on('showSentOTRMessage', this.updateUnreadMessagesCounter, this);
  3679. },
  3680. render: function () {
  3681. var data = _.extend(
  3682. this.model.toJSON(),
  3683. { 'tooltip': __('Click to restore this chat') }
  3684. );
  3685. if (this.model.get('chatroom')) {
  3686. data.title = this.model.get('name');
  3687. this.$el.addClass('chat-head-chatroom');
  3688. } else {
  3689. data.title = this.model.get('fullname');
  3690. this.$el.addClass('chat-head-chatbox');
  3691. }
  3692. return this.$el.html(converse.templates.trimmed_chat(data));
  3693. },
  3694. clearUnreadMessagesCounter: function () {
  3695. this.model.set({'num_unread': 0});
  3696. this.render();
  3697. },
  3698. updateUnreadMessagesCounter: function () {
  3699. this.model.set({'num_unread': this.model.get('num_unread') + 1});
  3700. this.render();
  3701. },
  3702. close: function (ev) {
  3703. if (ev && ev.preventDefault) { ev.preventDefault(); }
  3704. this.remove();
  3705. this.model.destroy();
  3706. converse.emit('chatBoxClosed', this);
  3707. return this;
  3708. },
  3709. restore: _.debounce(function (ev) {
  3710. if (ev && ev.preventDefault) { ev.preventDefault(); }
  3711. this.model.messages.off('add',null,this);
  3712. this.remove();
  3713. this.model.maximize();
  3714. }, 200, true)
  3715. });
  3716. this.MinimizedChats = Backbone.Overview.extend({
  3717. el: "#minimized-chats",
  3718. events: {
  3719. "click #toggle-minimized-chats": "toggle"
  3720. },
  3721. initialize: function () {
  3722. this.initToggle();
  3723. this.model.on("add", this.onChanged, this);
  3724. this.model.on("destroy", this.removeChat, this);
  3725. this.model.on("change:minimized", this.onChanged, this);
  3726. this.model.on('change:num_unread', this.updateUnreadMessagesCounter, this);
  3727. },
  3728. tearDown: function () {
  3729. this.model.off("add", this.onChanged);
  3730. this.model.off("destroy", this.removeChat);
  3731. this.model.off("change:minimized", this.onChanged);
  3732. this.model.off('change:num_unread', this.updateUnreadMessagesCounter);
  3733. return this;
  3734. },
  3735. initToggle: function () {
  3736. this.toggleview = new converse.MinimizedChatsToggleView({
  3737. model: new converse.MinimizedChatsToggle()
  3738. });
  3739. var id = b64_sha1('converse.minchatstoggle'+converse.bare_jid);
  3740. this.toggleview.model.id = id; // Appears to be necessary for backbone.browserStorage
  3741. this.toggleview.model.browserStorage = new Backbone.BrowserStorage[converse.storage](id);
  3742. this.toggleview.model.fetch();
  3743. },
  3744. render: function () {
  3745. if (this.keys().length === 0) {
  3746. this.$el.hide('fast');
  3747. } else if (this.keys().length === 1) {
  3748. this.$el.show('fast');
  3749. }
  3750. return this.$el;
  3751. },
  3752. toggle: function (ev) {
  3753. if (ev && ev.preventDefault) { ev.preventDefault(); }
  3754. this.toggleview.model.save({'collapsed': !this.toggleview.model.get('collapsed')});
  3755. this.$('.minimized-chats-flyout').toggle();
  3756. },
  3757. onChanged: function (item) {
  3758. if (item.get('id') !== 'controlbox' && item.get('minimized')) {
  3759. this.addChat(item);
  3760. } else if (this.get(item.get('id'))) {
  3761. this.removeChat(item);
  3762. }
  3763. },
  3764. addChat: function (item) {
  3765. var existing = this.get(item.get('id'));
  3766. if (existing && existing.$el.parent().length !== 0) {
  3767. return;
  3768. }
  3769. var view = new converse.MinimizedChatBoxView({model: item});
  3770. this.$('.minimized-chats-flyout').append(view.render());
  3771. this.add(item.get('id'), view);
  3772. this.toggleview.model.set({'num_minimized': this.keys().length});
  3773. this.render();
  3774. },
  3775. removeChat: function (item) {
  3776. this.remove(item.get('id'));
  3777. this.toggleview.model.set({'num_minimized': this.keys().length});
  3778. this.render();
  3779. },
  3780. updateUnreadMessagesCounter: function () {
  3781. var ls = this.model.pluck('num_unread'),
  3782. count = 0, i;
  3783. for (i=0; i<ls.length; i++) { count += ls[i]; }
  3784. this.toggleview.model.set({'num_unread': count});
  3785. this.render();
  3786. }
  3787. });
  3788. this.MinimizedChatsToggle = Backbone.Model.extend({
  3789. initialize: function () {
  3790. this.set({
  3791. 'collapsed': this.get('collapsed') || false,
  3792. 'num_minimized': this.get('num_minimized') || 0,
  3793. 'num_unread': this.get('num_unread') || 0
  3794. });
  3795. }
  3796. });
  3797. this.MinimizedChatsToggleView = Backbone.View.extend({
  3798. el: '#toggle-minimized-chats',
  3799. initialize: function () {
  3800. this.model.on('change:num_minimized', this.render, this);
  3801. this.model.on('change:num_unread', this.render, this);
  3802. this.$flyout = this.$el.siblings('.minimized-chats-flyout');
  3803. },
  3804. render: function () {
  3805. this.$el.html(converse.templates.toggle_chats(
  3806. _.extend(this.model.toJSON(), {
  3807. 'Minimized': __('Minimized')
  3808. })
  3809. ));
  3810. if (this.model.get('collapsed')) {
  3811. this.$flyout.hide();
  3812. } else {
  3813. this.$flyout.show();
  3814. }
  3815. return this.$el;
  3816. }
  3817. });
  3818. this.RosterContact = Backbone.Model.extend({
  3819. initialize: function (attributes, options) {
  3820. var jid = attributes.jid;
  3821. var bare_jid = Strophe.getBareJidFromJid(jid);
  3822. var resource = Strophe.getResourceFromJid(jid);
  3823. attributes.jid = bare_jid;
  3824. this.set(_.extend({
  3825. 'id': bare_jid,
  3826. 'jid': bare_jid,
  3827. 'fullname': bare_jid,
  3828. 'chat_status': 'offline',
  3829. 'user_id': Strophe.getNodeFromJid(jid),
  3830. 'resources': resource ? [resource] : [],
  3831. 'groups': [],
  3832. 'image_type': 'image/png',
  3833. 'image': "iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAIAAABt+uBvAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gwHCy455JBsggAABkJJREFUeNrtnM1PE1sUwHvvTD8otWLHST/Gimi1CEgr6M6FEWuIBo2pujDVsNDEP8GN/4MbN7oxrlipG2OCgZgYlxAbkRYw1KqkIDRCSkM7nXvvW8x7vjyNeQ9m7p1p3z1LQk/v/Dhz7vkEXL161cHl9wI5Ag6IA+KAOCAOiAPigDggLhwQB2S+iNZ+PcYY/SWEEP2HAAAIoSAIoihCCP+ngDDGtVotGAz29/cfOXJEUZSOjg6n06lp2sbGRqlUWlhYyGazS0tLbrdbEASrzgksyeYJId3d3el0uqenRxRFAAAA4KdfIIRgjD9+/Pj8+fOpqSndslofEIQwHA6Pjo4mEon//qmFhYXHjx8vLi4ihBgDEnp7e9l8E0Jo165dQ0NDd+/eDYVC2/qsJElDQ0OEkKWlpa2tLZamxAhQo9EIBoOjo6MXL17csZLe3l5FUT59+lQul5l5JRaAVFWNRqN37tw5ceKEQVWRSOTw4cOFQuHbt2+iKLYCIISQLMu3b99OJpOmKAwEAgcPHszn8+vr6wzsiG6UQQhxuVyXLl0aGBgwUW0sFstkMl6v90fo1KyAMMYDAwPnzp0zXfPg4GAqlWo0Gk0MiBAiy/L58+edTqf5Aa4onj59OhaLYYybFRCEMBaL0fNxBw4cSCQStN0QRUBut3t4eJjq6U+dOiVJElVPRBFQIBDo6+ujCqirqyscDlONGykC2lYyYSR6pBoQQapHZwAoHo/TuARYAOrs7GQASFEUqn6aIiBJkhgA6ujooFpUo6iaTa7koFwnaoWadLNe81tbWwzoaJrWrICWl5cZAFpbW6OabVAEtLi4yABQsVjUNK0pAWWzWQaAcrlcswKanZ1VVZUqHYRQEwOq1Wpv3ryhCmh6erpcLjdrNl+v1ycnJ+l5UELI27dvv3//3qxxEADgy5cvExMT9Mznw4cPtFtAdAPFarU6Pj5eKpVM17yxsfHy5cvV1VXazXu62gVBKBQKT58+rdVqJqrFGL948eLdu3dU8/g/H4FBUaJYLAqC0NPTY9brMD4+PjY25mDSracOCABACJmZmXE6nUePHjWu8NWrV48ePSKEsGlAs7Agfd5nenq6Wq0mk0kjDzY2NvbkyRMIIbP2PLvhBUEQ8vl8NpuNx+M+n29bzhVjvLKycv/+/YmJCcazQuwA6YzW1tYmJyf1SY+2trZ/rRk1Go1SqfT69esHDx4UCgVmNaa/zZ/9ABUhRFXVYDB48uTJeDweiUQkSfL7/T9MA2NcqVTK5fLy8vL8/PzU1FSxWHS5XJaM4wGr9sUwxqqqer3eUCgkSZJuUBBCfTRvc3OzXC6vrKxUKhWn02nhCJ5lM4oQQo/HgxD6+vXr58+fHf8sDOp+HQDg8XgclorFU676dKLlo6yWRdItIBwQB8QBcUCtfosRQjRNQwhhjPUC4w46WXryBSHU1zgEQWBz99EFhDGu1+t+v//48ePxeFxRlD179ng8nh0Efgiher2+vr6ur3HMzMysrq7uTJVdACGEurq6Ll++nEgkPB7Pj9jPoDHqOxyqqubz+WfPnuVyuV9XPeyeagAAAoHArVu3BgcHab8CuVzu4cOHpVKJUnfA5GweY+xyuc6cOXPv3r1IJMLAR8iyPDw8XK/Xi8Wiqqqmm5KZgBBC7e3tN27cuHbtGuPVpf7+/lAoNDs7W61WzfVKpgHSSzw3b95MpVKW3MfRaDQSiczNzVUqFRMZmQOIEOL1eq9fv3727FlL1t50URRFluX5+flqtWpWEGAOIFEUU6nUlStXLKSjy759+xwOx9zcnKZpphzGHMzhcDiTydgk9r1w4YIp7RPTAAmCkMlk2FeLf/tIEKbTab/fbwtAhJBoNGrutpNx6e7uPnTokC1eMU3T0um0DZPMkZER6wERQnw+n/FFSxpy7Nix3bt3WwwIIcRgIWnHkkwmjecfRgGx7DtuV/r6+iwGhDHev3+/bQF1dnYaH6E2CkiWZdsC2rt3r8WAHA5HW1ubbQGZcjajgOwTH/4qNko1Wlg4IA6IA+KAOKBWBUQIsfNojyliKIoRRfH9+/dut9umf3wzpoUNNQ4BAJubmwz+ic+OxefzWWlBhJD29nbug7iT5sIBcUAcEAfEAXFAHBAHxOVn+QMrmWpuPZx12gAAAABJRU5ErkJggg==",
  3834. 'status': ''
  3835. }, attributes));
  3836. this.on('destroy', function () { this.removeFromRoster(); }.bind(this));
  3837. },
  3838. subscribe: function (message) {
  3839. /* Send a presence subscription request to this roster contact
  3840. *
  3841. * Parameters:
  3842. * (String) message - An optional message to explain the
  3843. * reason for the subscription request.
  3844. */
  3845. this.save('ask', "subscribe"); // ask === 'subscribe' Means we have ask to subscribe to them.
  3846. var pres = $pres({to: this.get('jid'), type: "subscribe"});
  3847. if (message && message !== "") {
  3848. pres.c("status").t(message).up();
  3849. }
  3850. var nick = converse.xmppstatus.get('fullname');
  3851. if (nick && nick !== "") {
  3852. pres.c('nick', {'xmlns': Strophe.NS.NICK}).t(nick).up();
  3853. }
  3854. converse.connection.send(pres);
  3855. return this;
  3856. },
  3857. ackSubscribe: function () {
  3858. /* Upon receiving the presence stanza of type "subscribed",
  3859. * the user SHOULD acknowledge receipt of that subscription
  3860. * state notification by sending a presence stanza of type
  3861. * "subscribe" to the contact
  3862. */
  3863. converse.connection.send($pres({
  3864. 'type': 'subscribe',
  3865. 'to': this.get('jid')
  3866. }));
  3867. },
  3868. ackUnsubscribe: function (jid) {
  3869. /* Upon receiving the presence stanza of type "unsubscribed",
  3870. * the user SHOULD acknowledge receipt of that subscription state
  3871. * notification by sending a presence stanza of type "unsubscribe"
  3872. * this step lets the user's server know that it MUST no longer
  3873. * send notification of the subscription state change to the user.
  3874. * Parameters:
  3875. * (String) jid - The Jabber ID of the user who is unsubscribing
  3876. */
  3877. converse.connection.send($pres({'type': 'unsubscribe', 'to': this.get('jid')}));
  3878. this.destroy(); // Will cause removeFromRoster to be called.
  3879. },
  3880. unauthorize: function (message) {
  3881. /* Unauthorize this contact's presence subscription
  3882. * Parameters:
  3883. * (String) message - Optional message to send to the person being unauthorized
  3884. */
  3885. converse.rejectPresenceSubscription(this.get('jid'), message);
  3886. return this;
  3887. },
  3888. authorize: function (message) {
  3889. /* Authorize presence subscription
  3890. * Parameters:
  3891. * (String) message - Optional message to send to the person being authorized
  3892. */
  3893. var pres = $pres({to: this.get('jid'), type: "subscribed"});
  3894. if (message && message !== "") {
  3895. pres.c("status").t(message);
  3896. }
  3897. converse.connection.send(pres);
  3898. return this;
  3899. },
  3900. removeResource: function (resource) {
  3901. var resources = this.get('resources'), idx;
  3902. if (resource) {
  3903. idx = _.indexOf(resources, resource);
  3904. if (idx !== -1) {
  3905. resources.splice(idx, 1);
  3906. this.save({'resources': resources});
  3907. }
  3908. }
  3909. else {
  3910. // if there is no resource (resource is null), it probably
  3911. // means that the user is now completely offline. To make sure
  3912. // that there isn't any "ghost" resources left, we empty the array
  3913. this.save({'resources': []});
  3914. return 0;
  3915. }
  3916. return resources.length;
  3917. },
  3918. removeFromRoster: function (callback) {
  3919. /* Instruct the XMPP server to remove this contact from our roster
  3920. * Parameters:
  3921. * (Function) callback
  3922. */
  3923. var iq = $iq({type: 'set'})
  3924. .c('query', {xmlns: Strophe.NS.ROSTER})
  3925. .c('item', {jid: this.get('jid'), subscription: "remove"});
  3926. converse.connection.sendIQ(iq, callback, callback);
  3927. return this;
  3928. },
  3929. showInRoster: function () {
  3930. var chatStatus = this.get('chat_status');
  3931. if ((converse.show_only_online_users && chatStatus !== 'online') || (converse.hide_offline_users && chatStatus === 'offline')) {
  3932. // If pending or requesting, show
  3933. if ((this.get('ask') === 'subscribe') ||
  3934. (this.get('subscription') === 'from') ||
  3935. (this.get('requesting') === true)) {
  3936. return true;
  3937. }
  3938. return false;
  3939. }
  3940. return true;
  3941. }
  3942. });
  3943. this.RosterContactView = Backbone.View.extend({
  3944. tagName: 'dd',
  3945. events: {
  3946. "click .accept-xmpp-request": "acceptRequest",
  3947. "click .decline-xmpp-request": "declineRequest",
  3948. "click .open-chat": "openChat",
  3949. "click .remove-xmpp-contact": "removeContact"
  3950. },
  3951. initialize: function () {
  3952. this.model.on("change", this.render, this);
  3953. this.model.on("remove", this.remove, this);
  3954. this.model.on("destroy", this.remove, this);
  3955. this.model.on("open", this.openChat, this);
  3956. },
  3957. render: function () {
  3958. if (!this.model.showInRoster()) {
  3959. this.$el.hide();
  3960. return this;
  3961. } else if (this.$el[0].style.display === "none") {
  3962. this.$el.show();
  3963. }
  3964. var item = this.model,
  3965. ask = item.get('ask'),
  3966. chat_status = item.get('chat_status'),
  3967. requesting = item.get('requesting'),
  3968. subscription = item.get('subscription');
  3969. var classes_to_remove = [
  3970. 'current-xmpp-contact',
  3971. 'pending-xmpp-contact',
  3972. 'requesting-xmpp-contact'
  3973. ].concat(_.keys(STATUSES));
  3974. _.each(classes_to_remove,
  3975. function (cls) {
  3976. if (this.el.className.indexOf(cls) !== -1) {
  3977. this.$el.removeClass(cls);
  3978. }
  3979. }, this);
  3980. this.$el.addClass(chat_status).data('status', chat_status);
  3981. if ((ask === 'subscribe') || (subscription === 'from')) {
  3982. /* ask === 'subscribe'
  3983. * Means we have asked to subscribe to them.
  3984. *
  3985. * subscription === 'from'
  3986. * They are subscribed to use, but not vice versa.
  3987. * We assume that there is a pending subscription
  3988. * from us to them (otherwise we're in a state not
  3989. * supported by converse.js).
  3990. *
  3991. * So in both cases the user is a "pending" contact.
  3992. */
  3993. this.$el.addClass('pending-xmpp-contact');
  3994. this.$el.html(converse.templates.pending_contact(
  3995. _.extend(item.toJSON(), {
  3996. 'desc_remove': __('Click to remove this contact'),
  3997. 'allow_chat_pending_contacts': converse.allow_chat_pending_contacts
  3998. })
  3999. ));
  4000. } else if (requesting === true) {
  4001. this.$el.addClass('requesting-xmpp-contact');
  4002. this.$el.html(converse.templates.requesting_contact(
  4003. _.extend(item.toJSON(), {
  4004. 'desc_accept': __("Click to accept this contact request"),
  4005. 'desc_decline': __("Click to decline this contact request"),
  4006. 'allow_chat_pending_contacts': converse.allow_chat_pending_contacts
  4007. })
  4008. ));
  4009. converse.controlboxtoggle.showControlBox();
  4010. } else if (subscription === 'both' || subscription === 'to') {
  4011. this.$el.addClass('current-xmpp-contact');
  4012. this.$el.removeClass(_.without(['both', 'to'], subscription)[0]).addClass(subscription);
  4013. this.$el.html(converse.templates.roster_item(
  4014. _.extend(item.toJSON(), {
  4015. 'desc_status': STATUSES[chat_status||'offline'],
  4016. 'desc_chat': __('Click to chat with this contact'),
  4017. 'desc_remove': __('Click to remove this contact'),
  4018. 'title_fullname': __('Name'),
  4019. 'allow_contact_removal': converse.allow_contact_removal
  4020. })
  4021. ));
  4022. }
  4023. return this;
  4024. },
  4025. openChat: function (ev) {
  4026. if (ev && ev.preventDefault) { ev.preventDefault(); }
  4027. return converse.chatboxviews.showChat(this.model.attributes);
  4028. },
  4029. removeContact: function (ev) {
  4030. if (ev && ev.preventDefault) { ev.preventDefault(); }
  4031. if (!converse.allow_contact_removal) { return; }
  4032. var result = confirm(__("Are you sure you want to remove this contact?"));
  4033. if (result === true) {
  4034. var iq = $iq({type: 'set'})
  4035. .c('query', {xmlns: Strophe.NS.ROSTER})
  4036. .c('item', {jid: this.model.get('jid'), subscription: "remove"});
  4037. converse.connection.sendIQ(iq,
  4038. function (iq) {
  4039. this.model.destroy();
  4040. this.remove();
  4041. }.bind(this),
  4042. function (err) {
  4043. alert(__("Sorry, there was an error while trying to remove "+name+" as a contact."));
  4044. converse.log(err);
  4045. }
  4046. );
  4047. }
  4048. },
  4049. acceptRequest: function (ev) {
  4050. if (ev && ev.preventDefault) { ev.preventDefault(); }
  4051. converse.roster.sendContactAddIQ(
  4052. this.model.get('jid'),
  4053. this.model.get('fullname'),
  4054. [],
  4055. function () { this.model.authorize().subscribe(); }.bind(this)
  4056. );
  4057. },
  4058. declineRequest: function (ev) {
  4059. if (ev && ev.preventDefault) { ev.preventDefault(); }
  4060. var result = confirm(__("Are you sure you want to decline this contact request?"));
  4061. if (result === true) {
  4062. this.model.unauthorize().destroy();
  4063. }
  4064. return this;
  4065. }
  4066. });
  4067. this.RosterContacts = Backbone.Collection.extend({
  4068. model: converse.RosterContact,
  4069. comparator: function (contact1, contact2) {
  4070. var name1, name2;
  4071. var status1 = contact1.get('chat_status') || 'offline';
  4072. var status2 = contact2.get('chat_status') || 'offline';
  4073. if (STATUS_WEIGHTS[status1] === STATUS_WEIGHTS[status2]) {
  4074. name1 = contact1.get('fullname').toLowerCase();
  4075. name2 = contact2.get('fullname').toLowerCase();
  4076. return name1 < name2 ? -1 : (name1 > name2? 1 : 0);
  4077. } else {
  4078. return STATUS_WEIGHTS[status1] < STATUS_WEIGHTS[status2] ? -1 : 1;
  4079. }
  4080. },
  4081. subscribeToSuggestedItems: function (msg) {
  4082. $(msg).find('item').each(function (i, items) {
  4083. if (this.getAttribute('action') === 'add') {
  4084. converse.roster.addAndSubscribe(
  4085. this.getAttribute('jid'), null, converse.xmppstatus.get('fullname'));
  4086. }
  4087. });
  4088. return true;
  4089. },
  4090. isSelf: function (jid) {
  4091. return (Strophe.getBareJidFromJid(jid) === Strophe.getBareJidFromJid(converse.connection.jid));
  4092. },
  4093. addAndSubscribe: function (jid, name, groups, message, attributes) {
  4094. /* Add a roster contact and then once we have confirmation from
  4095. * the XMPP server we subscribe to that contact's presence updates.
  4096. * Parameters:
  4097. * (String) jid - The Jabber ID of the user being added and subscribed to.
  4098. * (String) name - The name of that user
  4099. * (Array of Strings) groups - Any roster groups the user might belong to
  4100. * (String) message - An optional message to explain the
  4101. * reason for the subscription request.
  4102. * (Object) attributes - Any additional attributes to be stored on the user's model.
  4103. */
  4104. this.addContact(jid, name, groups, attributes).done(function (contact) {
  4105. if (contact instanceof converse.RosterContact) {
  4106. contact.subscribe(message);
  4107. }
  4108. });
  4109. },
  4110. sendContactAddIQ: function (jid, name, groups, callback, errback) {
  4111. /* Send an IQ stanza to the XMPP server to add a new roster contact.
  4112. * Parameters:
  4113. * (String) jid - The Jabber ID of the user being added
  4114. * (String) name - The name of that user
  4115. * (Array of Strings) groups - Any roster groups the user might belong to
  4116. * (Function) callback - A function to call once the VCard is returned
  4117. * (Function) errback - A function to call if an error occured
  4118. */
  4119. name = _.isEmpty(name)? jid: name;
  4120. var iq = $iq({type: 'set'})
  4121. .c('query', {xmlns: Strophe.NS.ROSTER})
  4122. .c('item', { jid: jid, name: name });
  4123. _.map(groups, function (group) { iq.c('group').t(group).up(); });
  4124. converse.connection.sendIQ(iq, callback, errback);
  4125. },
  4126. addContact: function (jid, name, groups, attributes) {
  4127. /* Adds a RosterContact instance to converse.roster and
  4128. * registers the contact on the XMPP server.
  4129. * Returns a promise which is resolved once the XMPP server has
  4130. * responded.
  4131. * Parameters:
  4132. * (String) jid - The Jabber ID of the user being added and subscribed to.
  4133. * (String) name - The name of that user
  4134. * (Array of Strings) groups - Any roster groups the user might belong to
  4135. * (Object) attributes - Any additional attributes to be stored on the user's model.
  4136. */
  4137. var deferred = new $.Deferred();
  4138. groups = groups || [];
  4139. name = _.isEmpty(name)? jid: name;
  4140. this.sendContactAddIQ(jid, name, groups,
  4141. function (iq) {
  4142. var contact = this.create(_.extend({
  4143. ask: undefined,
  4144. fullname: name,
  4145. groups: groups,
  4146. jid: jid,
  4147. requesting: false,
  4148. subscription: 'none'
  4149. }, attributes), {sort: false});
  4150. deferred.resolve(contact);
  4151. }.bind(this),
  4152. function (err) {
  4153. alert(__("Sorry, there was an error while trying to add "+name+" as a contact."));
  4154. converse.log(err);
  4155. deferred.resolve(err);
  4156. }
  4157. );
  4158. return deferred.promise();
  4159. },
  4160. addResource: function (bare_jid, resource) {
  4161. var item = this.get(bare_jid),
  4162. resources;
  4163. if (item) {
  4164. resources = item.get('resources');
  4165. if (resources) {
  4166. if (_.indexOf(resources, resource) === -1) {
  4167. resources.push(resource);
  4168. item.set({'resources': resources});
  4169. }
  4170. } else {
  4171. item.set({'resources': [resource]});
  4172. }
  4173. }
  4174. },
  4175. subscribeBack: function (bare_jid) {
  4176. var contact = this.get(bare_jid);
  4177. if (contact instanceof converse.RosterContact) {
  4178. contact.authorize().subscribe();
  4179. } else {
  4180. // Can happen when a subscription is retried or roster was deleted
  4181. this.addContact(bare_jid, '', [], { 'subscription': 'from' }).done(function (contact) {
  4182. if (contact instanceof converse.RosterContact) {
  4183. contact.authorize().subscribe();
  4184. }
  4185. });
  4186. }
  4187. },
  4188. getNumOnlineContacts: function () {
  4189. var count = 0,
  4190. ignored = ['offline', 'unavailable'],
  4191. models = this.models,
  4192. models_length = models.length,
  4193. i;
  4194. if (converse.show_only_online_users) {
  4195. ignored = _.union(ignored, ['dnd', 'xa', 'away']);
  4196. }
  4197. for (i=0; i<models_length; i++) {
  4198. if (_.indexOf(ignored, models[i].get('chat_status')) === -1) {
  4199. count++;
  4200. }
  4201. }
  4202. return count;
  4203. },
  4204. onRosterPush: function (iq) {
  4205. /* Handle roster updates from the XMPP server.
  4206. * See: https://xmpp.org/rfcs/rfc6121.html#roster-syntax-actions-push
  4207. *
  4208. * Parameters:
  4209. * (XMLElement) IQ - The IQ stanza received from the XMPP server.
  4210. */
  4211. var id = iq.getAttribute('id');
  4212. var from = iq.getAttribute('from');
  4213. if (from && from !== "" && Strophe.getBareJidFromJid(from) !== converse.bare_jid) {
  4214. // Receiving client MUST ignore stanza unless it has no from or from = user's bare JID.
  4215. // XXX: Some naughty servers apparently send from a full
  4216. // JID so we need to explicitly compare bare jids here.
  4217. // https://github.com/jcbrand/converse.js/issues/493
  4218. converse.connection.send(
  4219. $iq({type: 'error', id: id, from: converse.connection.jid})
  4220. .c('error', {'type': 'cancel'})
  4221. .c('service-unavailable', {'xmlns': Strophe.NS.ROSTER })
  4222. );
  4223. return true;
  4224. }
  4225. converse.connection.send($iq({type: 'result', id: id, from: converse.connection.jid}));
  4226. $(iq).children('query').find('item').each(function (idx, item) {
  4227. this.updateContact(item);
  4228. }.bind(this));
  4229. converse.emit('rosterPush', iq);
  4230. return true;
  4231. },
  4232. fetchFromServer: function (callback) {
  4233. /* Get the roster from the XMPP server */
  4234. var iq = $iq({type: 'get', 'id': converse.connection.getUniqueId('roster')})
  4235. .c('query', {xmlns: Strophe.NS.ROSTER});
  4236. return converse.connection.sendIQ(iq, function () {
  4237. this.onReceivedFromServer.apply(this, arguments);
  4238. callback.apply(this, arguments);
  4239. }.bind(this));
  4240. },
  4241. onReceivedFromServer: function (iq) {
  4242. /* An IQ stanza containing the roster has been received from
  4243. * the XMPP server.
  4244. */
  4245. converse.emit('roster', iq);
  4246. $(iq).children('query').find('item').each(function (idx, item) {
  4247. this.updateContact(item);
  4248. }.bind(this));
  4249. },
  4250. updateContact: function (item) {
  4251. /* Update or create RosterContact models based on items
  4252. * received in the IQ from the server.
  4253. */
  4254. var jid = item.getAttribute('jid');
  4255. if (this.isSelf(jid)) { return; }
  4256. var groups = [],
  4257. contact = this.get(jid),
  4258. ask = item.getAttribute("ask"),
  4259. subscription = item.getAttribute("subscription");
  4260. $.map(item.getElementsByTagName('group'), function (group) {
  4261. groups.push(Strophe.getText(group));
  4262. });
  4263. if (!contact) {
  4264. if ((subscription === "none" && ask === null) || (subscription === "remove")) {
  4265. return; // We're lazy when adding contacts.
  4266. }
  4267. this.create({
  4268. ask: ask,
  4269. fullname: item.getAttribute("name") || jid,
  4270. groups: groups,
  4271. jid: jid,
  4272. subscription: subscription
  4273. }, {sort: false});
  4274. } else {
  4275. if (subscription === "remove") {
  4276. return contact.destroy(); // will trigger removeFromRoster
  4277. }
  4278. // We only find out about requesting contacts via the
  4279. // presence handler, so if we receive a contact
  4280. // here, we know they aren't requesting anymore.
  4281. // see docs/DEVELOPER.rst
  4282. contact.save({
  4283. subscription: subscription,
  4284. ask: ask,
  4285. requesting: null,
  4286. groups: groups
  4287. });
  4288. }
  4289. },
  4290. createContactFromVCard: function (iq, jid, fullname, img, img_type, url) {
  4291. var bare_jid = Strophe.getBareJidFromJid(jid);
  4292. this.create({
  4293. jid: bare_jid,
  4294. subscription: 'none',
  4295. ask: null,
  4296. requesting: true,
  4297. fullname: fullname || bare_jid,
  4298. image: img,
  4299. image_type: img_type,
  4300. url: url,
  4301. vcard_updated: moment().format()
  4302. });
  4303. },
  4304. handleIncomingSubscription: function (jid) {
  4305. var bare_jid = Strophe.getBareJidFromJid(jid);
  4306. var contact = this.get(bare_jid);
  4307. if (!converse.allow_contact_requests) {
  4308. converse.rejectPresenceSubscription(jid, __("This client does not allow presence subscriptions"));
  4309. }
  4310. if (converse.auto_subscribe) {
  4311. if ((!contact) || (contact.get('subscription') !== 'to')) {
  4312. this.subscribeBack(bare_jid);
  4313. } else {
  4314. contact.authorize();
  4315. }
  4316. } else {
  4317. if (contact) {
  4318. if (contact.get('subscription') !== 'none') {
  4319. contact.authorize();
  4320. } else if (contact.get('ask') === "subscribe") {
  4321. contact.authorize();
  4322. }
  4323. } else if (!contact) {
  4324. converse.getVCard(
  4325. bare_jid, this.createContactFromVCard.bind(this),
  4326. function (iq, jid) {
  4327. converse.log("Error while retrieving vcard for "+jid);
  4328. this.createContactFromVCard.call(this, iq, jid);
  4329. }.bind(this)
  4330. );
  4331. }
  4332. }
  4333. },
  4334. presenceHandler: function (presence) {
  4335. var $presence = $(presence),
  4336. presence_type = presence.getAttribute('type');
  4337. if (presence_type === 'error') { return true; }
  4338. var jid = presence.getAttribute('from'),
  4339. bare_jid = Strophe.getBareJidFromJid(jid),
  4340. resource = Strophe.getResourceFromJid(jid),
  4341. chat_status = $presence.find('show').text() || 'online',
  4342. status_message = $presence.find('status'),
  4343. contact = this.get(bare_jid);
  4344. if (this.isSelf(bare_jid)) {
  4345. if ((converse.connection.jid !== jid)&&(presence_type !== 'unavailable')) {
  4346. // Another resource has changed its status, we'll update ours as well.
  4347. converse.xmppstatus.save({'status': chat_status});
  4348. if (status_message.length) { converse.xmppstatus.save({'status_message': status_message.text()}); }
  4349. }
  4350. return;
  4351. } else if (($presence.find('x').attr('xmlns') || '').indexOf(Strophe.NS.MUC) === 0) {
  4352. return; // Ignore MUC
  4353. }
  4354. if (contact && (status_message.text() !== contact.get('status'))) {
  4355. contact.save({'status': status_message.text()});
  4356. }
  4357. if (presence_type === 'subscribed' && contact) {
  4358. contact.ackSubscribe();
  4359. } else if (presence_type === 'unsubscribed' && contact) {
  4360. contact.ackUnsubscribe();
  4361. } else if (presence_type === 'unsubscribe') {
  4362. return;
  4363. } else if (presence_type === 'subscribe') {
  4364. this.handleIncomingSubscription(jid);
  4365. } else if (presence_type === 'unavailable' && contact) {
  4366. // Only set the user to offline if there aren't any
  4367. // other resources still available.
  4368. if (contact.removeResource(resource) === 0) {
  4369. contact.save({'chat_status': "offline"});
  4370. }
  4371. } else if (contact) { // presence_type is undefined
  4372. this.addResource(bare_jid, resource);
  4373. contact.save({'chat_status': chat_status});
  4374. }
  4375. }
  4376. });
  4377. this.RosterGroup = Backbone.Model.extend({
  4378. initialize: function (attributes, options) {
  4379. this.set(_.extend({
  4380. description: DESC_GROUP_TOGGLE,
  4381. state: OPENED
  4382. }, attributes));
  4383. // Collection of contacts belonging to this group.
  4384. this.contacts = new converse.RosterContacts();
  4385. }
  4386. });
  4387. this.RosterGroupView = Backbone.Overview.extend({
  4388. tagName: 'dt',
  4389. className: 'roster-group',
  4390. events: {
  4391. "click a.group-toggle": "toggle"
  4392. },
  4393. initialize: function () {
  4394. this.model.contacts.on("add", this.addContact, this);
  4395. this.model.contacts.on("change:subscription", this.onContactSubscriptionChange, this);
  4396. this.model.contacts.on("change:requesting", this.onContactRequestChange, this);
  4397. this.model.contacts.on("change:chat_status", function (contact) {
  4398. // This might be optimized by instead of first sorting,
  4399. // finding the correct position in positionContact
  4400. this.model.contacts.sort();
  4401. this.positionContact(contact).render();
  4402. }, this);
  4403. this.model.contacts.on("destroy", this.onRemove, this);
  4404. this.model.contacts.on("remove", this.onRemove, this);
  4405. converse.roster.on('change:groups', this.onContactGroupChange, this);
  4406. },
  4407. render: function () {
  4408. this.$el.attr('data-group', this.model.get('name'));
  4409. this.$el.html(
  4410. $(converse.templates.group_header({
  4411. label_group: this.model.get('name'),
  4412. desc_group_toggle: this.model.get('description'),
  4413. toggle_state: this.model.get('state')
  4414. }))
  4415. );
  4416. return this;
  4417. },
  4418. addContact: function (contact) {
  4419. var view = new converse.RosterContactView({model: contact});
  4420. this.add(contact.get('id'), view);
  4421. view = this.positionContact(contact).render();
  4422. if (contact.showInRoster()) {
  4423. if (this.model.get('state') === CLOSED) {
  4424. if (view.$el[0].style.display !== "none") { view.$el.hide(); }
  4425. if (!this.$el.is(':visible')) { this.$el.show(); }
  4426. } else {
  4427. if (this.$el[0].style.display !== "block") { this.show(); }
  4428. }
  4429. }
  4430. },
  4431. positionContact: function (contact) {
  4432. /* Place the contact's DOM element in the correct alphabetical
  4433. * position amongst the other contacts in this group.
  4434. */
  4435. var view = this.get(contact.get('id'));
  4436. var index = this.model.contacts.indexOf(contact);
  4437. view.$el.detach();
  4438. if (index === 0) {
  4439. this.$el.after(view.$el);
  4440. } else if (index === (this.model.contacts.length-1)) {
  4441. this.$el.nextUntil('dt').last().after(view.$el);
  4442. } else {
  4443. this.$el.nextUntil('dt').eq(index).before(view.$el);
  4444. }
  4445. return view;
  4446. },
  4447. show: function () {
  4448. this.$el.show();
  4449. _.each(this.getAll(), function (contactView) {
  4450. if (contactView.model.showInRoster()) {
  4451. contactView.$el.show();
  4452. }
  4453. });
  4454. },
  4455. hide: function () {
  4456. this.$el.nextUntil('dt').addBack().hide();
  4457. },
  4458. filter: function (q) {
  4459. /* Filter the group's contacts based on the query "q".
  4460. * The query is matched against the contact's full name.
  4461. * If all contacts are filtered out (i.e. hidden), then the
  4462. * group must be filtered out as well.
  4463. */
  4464. var matches;
  4465. if (q.length === 0) {
  4466. if (this.model.get('state') === OPENED) {
  4467. this.model.contacts.each(function (item) {
  4468. if (item.showInRoster()) {
  4469. this.get(item.get('id')).$el.show();
  4470. }
  4471. }.bind(this));
  4472. }
  4473. this.showIfNecessary();
  4474. } else {
  4475. q = q.toLowerCase();
  4476. matches = this.model.contacts.filter(contains.not('fullname', q));
  4477. if (matches.length === this.model.contacts.length) { // hide the whole group
  4478. this.hide();
  4479. } else {
  4480. _.each(matches, function (item) {
  4481. this.get(item.get('id')).$el.hide();
  4482. }.bind(this));
  4483. _.each(this.model.contacts.reject(contains.not('fullname', q)), function (item) {
  4484. this.get(item.get('id')).$el.show();
  4485. }.bind(this));
  4486. this.showIfNecessary();
  4487. }
  4488. }
  4489. },
  4490. showIfNecessary: function () {
  4491. if (!this.$el.is(':visible') && this.model.contacts.length > 0) {
  4492. this.$el.show();
  4493. }
  4494. },
  4495. toggle: function (ev) {
  4496. if (ev && ev.preventDefault) { ev.preventDefault(); }
  4497. var $el = $(ev.target);
  4498. if ($el.hasClass("icon-opened")) {
  4499. this.$el.nextUntil('dt').slideUp();
  4500. this.model.save({state: CLOSED});
  4501. $el.removeClass("icon-opened").addClass("icon-closed");
  4502. } else {
  4503. $el.removeClass("icon-closed").addClass("icon-opened");
  4504. this.model.save({state: OPENED});
  4505. this.filter(
  4506. converse.rosterview.$('.roster-filter').val(),
  4507. converse.rosterview.$('.filter-type').val()
  4508. );
  4509. }
  4510. },
  4511. onContactGroupChange: function (contact) {
  4512. var in_this_group = _.contains(contact.get('groups'), this.model.get('name'));
  4513. var cid = contact.get('id');
  4514. var in_this_overview = !this.get(cid);
  4515. if (in_this_group && !in_this_overview) {
  4516. this.model.contacts.remove(cid);
  4517. } else if (!in_this_group && in_this_overview) {
  4518. this.addContact(contact);
  4519. }
  4520. },
  4521. onContactSubscriptionChange: function (contact) {
  4522. if ((this.model.get('name') === HEADER_PENDING_CONTACTS) && contact.get('subscription') !== 'from') {
  4523. this.model.contacts.remove(contact.get('id'));
  4524. }
  4525. },
  4526. onContactRequestChange: function (contact) {
  4527. if ((this.model.get('name') === HEADER_REQUESTING_CONTACTS) && !contact.get('requesting')) {
  4528. this.model.contacts.remove(contact.get('id'));
  4529. }
  4530. },
  4531. onRemove: function (contact) {
  4532. this.remove(contact.get('id'));
  4533. if (this.model.contacts.length === 0) {
  4534. this.$el.hide();
  4535. }
  4536. }
  4537. });
  4538. this.RosterGroups = Backbone.Collection.extend({
  4539. model: converse.RosterGroup,
  4540. comparator: function (a, b) {
  4541. /* Groups are sorted alphabetically, ignoring case.
  4542. * However, Ungrouped, Requesting Contacts and Pending Contacts
  4543. * appear last and in that order. */
  4544. a = a.get('name');
  4545. b = b.get('name');
  4546. var special_groups = _.keys(HEADER_WEIGHTS);
  4547. var a_is_special = _.contains(special_groups, a);
  4548. var b_is_special = _.contains(special_groups, b);
  4549. if (!a_is_special && !b_is_special ) {
  4550. return a.toLowerCase() < b.toLowerCase() ? -1 : (a.toLowerCase() > b.toLowerCase() ? 1 : 0);
  4551. } else if (a_is_special && b_is_special) {
  4552. return HEADER_WEIGHTS[a] < HEADER_WEIGHTS[b] ? -1 : (HEADER_WEIGHTS[a] > HEADER_WEIGHTS[b] ? 1 : 0);
  4553. } else if (!a_is_special && b_is_special) {
  4554. return (b === HEADER_CURRENT_CONTACTS) ? 1 : -1;
  4555. } else if (a_is_special && !b_is_special) {
  4556. return (a === HEADER_CURRENT_CONTACTS) ? -1 : 1;
  4557. }
  4558. }
  4559. });
  4560. this.RosterView = Backbone.Overview.extend({
  4561. tagName: 'div',
  4562. id: 'converse-roster',
  4563. events: {
  4564. "keydown .roster-filter": "liveFilter",
  4565. "click .onX": "clearFilter",
  4566. "mousemove .x": "togglePointer",
  4567. "change .filter-type": "changeFilterType"
  4568. },
  4569. initialize: function () {
  4570. this.roster_handler_ref = this.registerRosterHandler();
  4571. this.rosterx_handler_ref = this.registerRosterXHandler();
  4572. this.presence_ref = this.registerPresenceHandler();
  4573. converse.roster.on("add", this.onContactAdd, this);
  4574. converse.roster.on('change', this.onContactChange, this);
  4575. converse.roster.on("destroy", this.update, this);
  4576. converse.roster.on("remove", this.update, this);
  4577. this.model.on("add", this.onGroupAdd, this);
  4578. this.model.on("reset", this.reset, this);
  4579. this.$roster = $('<dl class="roster-contacts" style="display: none;"></dl>');
  4580. },
  4581. unregisterHandlers: function () {
  4582. converse.connection.deleteHandler(this.roster_handler_ref);
  4583. delete this.roster_handler_ref;
  4584. converse.connection.deleteHandler(this.rosterx_handler_ref);
  4585. delete this.rosterx_handler_ref;
  4586. converse.connection.deleteHandler(this.presence_ref);
  4587. delete this.presence_ref;
  4588. },
  4589. update: _.debounce(function () {
  4590. var $count = $('#online-count');
  4591. $count.text('('+converse.roster.getNumOnlineContacts()+')');
  4592. if (!$count.is(':visible')) {
  4593. $count.show();
  4594. }
  4595. if (this.$roster.parent().length === 0) {
  4596. this.$el.append(this.$roster.show());
  4597. }
  4598. return this.showHideFilter();
  4599. }, converse.animate ? 100 : 0),
  4600. render: function () {
  4601. this.$el.html(converse.templates.roster({
  4602. placeholder: __('Type to filter'),
  4603. label_contacts: LABEL_CONTACTS,
  4604. label_groups: LABEL_GROUPS
  4605. }));
  4606. if (!converse.allow_contact_requests) {
  4607. // XXX: if we ever support live editing of config then
  4608. // we'll need to be able to remove this class on the fly.
  4609. this.$el.addClass('no-contact-requests');
  4610. }
  4611. return this;
  4612. },
  4613. fetch: function () {
  4614. this.model.fetch({
  4615. silent: true, // We use the success handler to handle groups that were added,
  4616. // we need to first have all groups before positionFetchedGroups
  4617. // will work properly.
  4618. success: function (collection, resp, options) {
  4619. if (collection.length !== 0) {
  4620. this.positionFetchedGroups(collection, resp, options);
  4621. }
  4622. converse.roster.fetch({
  4623. add: true,
  4624. success: function (collection) {
  4625. if (collection.length === 0) {
  4626. /* We don't have any roster contacts stored in sessionStorage,
  4627. * so lets fetch the roster from the XMPP server. We pass in
  4628. * 'sendPresence' as callback method, because after initially
  4629. * fetching the roster we are ready to receive presence
  4630. * updates from our contacts.
  4631. */
  4632. converse.roster.fetchFromServer(function () {
  4633. converse.xmppstatus.sendPresence();
  4634. });
  4635. } else if (converse.send_initial_presence) {
  4636. /* We're not going to fetch the roster again because we have
  4637. * it already cached in sessionStorage, but we still need to
  4638. * send out a presence stanza because this is a new session.
  4639. * See: https://github.com/jcbrand/converse.js/issues/536
  4640. */
  4641. converse.xmppstatus.sendPresence();
  4642. }
  4643. }
  4644. });
  4645. }.bind(this)
  4646. });
  4647. return this;
  4648. },
  4649. changeFilterType: function (ev) {
  4650. if (ev && ev.preventDefault) { ev.preventDefault(); }
  4651. this.clearFilter();
  4652. this.filter(
  4653. this.$('.roster-filter').val(),
  4654. ev.target.value
  4655. );
  4656. },
  4657. tog: function (v) {
  4658. return v?'addClass':'removeClass';
  4659. },
  4660. togglePointer: function (ev) {
  4661. if (ev && ev.preventDefault) { ev.preventDefault(); }
  4662. var el = ev.target;
  4663. $(el)[this.tog(el.offsetWidth-18 < ev.clientX-el.getBoundingClientRect().left)]('onX');
  4664. },
  4665. filter: function (query, type) {
  4666. query = query.toLowerCase();
  4667. if (type === 'groups') {
  4668. _.each(this.getAll(), function (view, idx) {
  4669. if (view.model.get('name').toLowerCase().indexOf(query.toLowerCase()) === -1) {
  4670. view.hide();
  4671. } else if (view.model.contacts.length > 0) {
  4672. view.show();
  4673. }
  4674. });
  4675. } else {
  4676. _.each(this.getAll(), function (view) {
  4677. view.filter(query, type);
  4678. });
  4679. }
  4680. },
  4681. liveFilter: _.debounce(function (ev) {
  4682. if (ev && ev.preventDefault) { ev.preventDefault(); }
  4683. var $filter = this.$('.roster-filter');
  4684. var q = $filter.val();
  4685. var t = this.$('.filter-type').val();
  4686. $filter[this.tog(q)]('x');
  4687. this.filter(q, t);
  4688. }, 300),
  4689. clearFilter: function (ev) {
  4690. if (ev && ev.preventDefault) {
  4691. ev.preventDefault();
  4692. $(ev.target).removeClass('x onX').val('');
  4693. }
  4694. this.filter('');
  4695. },
  4696. showHideFilter: function () {
  4697. if (!this.$el.is(':visible')) {
  4698. return;
  4699. }
  4700. var $filter = this.$('.roster-filter');
  4701. var $type = this.$('.filter-type');
  4702. var visible = $filter.is(':visible');
  4703. if (visible && $filter.val().length > 0) {
  4704. // Don't hide if user is currently filtering.
  4705. return;
  4706. }
  4707. if (this.$roster.hasScrollBar()) {
  4708. if (!visible) {
  4709. $filter.show();
  4710. $type.show();
  4711. }
  4712. } else {
  4713. $filter.hide();
  4714. $type.hide();
  4715. }
  4716. return this;
  4717. },
  4718. reset: function () {
  4719. converse.roster.reset();
  4720. this.removeAll();
  4721. this.$roster = $('<dl class="roster-contacts" style="display: none;"></dl>');
  4722. this.render().update();
  4723. return this;
  4724. },
  4725. registerRosterHandler: function () {
  4726. converse.connection.addHandler(
  4727. converse.roster.onRosterPush.bind(converse.roster),
  4728. Strophe.NS.ROSTER, 'iq', "set"
  4729. );
  4730. },
  4731. registerRosterXHandler: function () {
  4732. var t = 0;
  4733. converse.connection.addHandler(
  4734. function (msg) {
  4735. window.setTimeout(
  4736. function () {
  4737. converse.connection.flush();
  4738. converse.roster.subscribeToSuggestedItems.bind(converse.roster)(msg);
  4739. },
  4740. t
  4741. );
  4742. t += $(msg).find('item').length*250;
  4743. return true;
  4744. },
  4745. Strophe.NS.ROSTERX, 'message', null
  4746. );
  4747. },
  4748. registerPresenceHandler: function () {
  4749. converse.connection.addHandler(
  4750. function (presence) {
  4751. converse.roster.presenceHandler(presence);
  4752. return true;
  4753. }.bind(this), null, 'presence', null);
  4754. },
  4755. onGroupAdd: function (group) {
  4756. var view = new converse.RosterGroupView({model: group});
  4757. this.add(group.get('name'), view.render());
  4758. this.positionGroup(view);
  4759. },
  4760. onContactAdd: function (contact) {
  4761. this.addRosterContact(contact).update();
  4762. if (!contact.get('vcard_updated')) {
  4763. // This will update the vcard, which triggers a change
  4764. // request which will rerender the roster contact.
  4765. converse.getVCard(contact.get('jid'));
  4766. }
  4767. },
  4768. onContactChange: function (contact) {
  4769. this.updateChatBox(contact).update();
  4770. if (_.has(contact.changed, 'subscription')) {
  4771. if (contact.changed.subscription === 'from') {
  4772. this.addContactToGroup(contact, HEADER_PENDING_CONTACTS);
  4773. } else if (_.contains(['both', 'to'], contact.get('subscription'))) {
  4774. this.addExistingContact(contact);
  4775. }
  4776. }
  4777. if (_.has(contact.changed, 'ask') && contact.changed.ask === 'subscribe') {
  4778. this.addContactToGroup(contact, HEADER_PENDING_CONTACTS);
  4779. }
  4780. if (_.has(contact.changed, 'subscription') && contact.changed.requesting === 'true') {
  4781. this.addContactToGroup(contact, HEADER_REQUESTING_CONTACTS);
  4782. }
  4783. this.liveFilter();
  4784. },
  4785. updateChatBox: function (contact) {
  4786. var chatbox = converse.chatboxes.get(contact.get('jid')),
  4787. changes = {};
  4788. if (!chatbox) {
  4789. return this;
  4790. }
  4791. if (_.has(contact.changed, 'chat_status')) {
  4792. changes.chat_status = contact.get('chat_status');
  4793. }
  4794. if (_.has(contact.changed, 'status')) {
  4795. changes.status = contact.get('status');
  4796. }
  4797. chatbox.save(changes);
  4798. return this;
  4799. },
  4800. positionFetchedGroups: function (model, resp, options) {
  4801. /* Instead of throwing an add event for each group
  4802. * fetched, we wait until they're all fetched and then
  4803. * we position them.
  4804. * Works around the problem of positionGroup not
  4805. * working when all groups besides the one being
  4806. * positioned aren't already in inserted into the
  4807. * roster DOM element.
  4808. */
  4809. model.sort();
  4810. model.each(function (group, idx) {
  4811. var view = this.get(group.get('name'));
  4812. if (!view) {
  4813. view = new converse.RosterGroupView({model: group});
  4814. this.add(group.get('name'), view.render());
  4815. }
  4816. if (idx === 0) {
  4817. this.$roster.append(view.$el);
  4818. } else {
  4819. this.appendGroup(view);
  4820. }
  4821. }.bind(this));
  4822. },
  4823. positionGroup: function (view) {
  4824. /* Place the group's DOM element in the correct alphabetical
  4825. * position amongst the other groups in the roster.
  4826. */
  4827. var $groups = this.$roster.find('.roster-group'),
  4828. index = $groups.length ? this.model.indexOf(view.model) : 0;
  4829. if (index === 0) {
  4830. this.$roster.prepend(view.$el);
  4831. } else if (index === (this.model.length-1)) {
  4832. this.appendGroup(view);
  4833. } else {
  4834. $($groups.eq(index)).before(view.$el);
  4835. }
  4836. return this;
  4837. },
  4838. appendGroup: function (view) {
  4839. /* Add the group at the bottom of the roster
  4840. */
  4841. var $last = this.$roster.find('.roster-group').last();
  4842. var $siblings = $last.siblings('dd');
  4843. if ($siblings.length > 0) {
  4844. $siblings.last().after(view.$el);
  4845. } else {
  4846. $last.after(view.$el);
  4847. }
  4848. return this;
  4849. },
  4850. getGroup: function (name) {
  4851. /* Returns the group as specified by name.
  4852. * Creates the group if it doesn't exist.
  4853. */
  4854. var view = this.get(name);
  4855. if (view) {
  4856. return view.model;
  4857. }
  4858. return this.model.create({name: name, id: b64_sha1(name)});
  4859. },
  4860. addContactToGroup: function (contact, name) {
  4861. this.getGroup(name).contacts.add(contact);
  4862. },
  4863. addExistingContact: function (contact) {
  4864. var groups;
  4865. if (converse.roster_groups) {
  4866. groups = contact.get('groups');
  4867. if (groups.length === 0) {
  4868. groups = [HEADER_UNGROUPED];
  4869. }
  4870. } else {
  4871. groups = [HEADER_CURRENT_CONTACTS];
  4872. }
  4873. _.each(groups, _.bind(this.addContactToGroup, this, contact));
  4874. },
  4875. addRosterContact: function (contact) {
  4876. if (contact.get('subscription') === 'both' || contact.get('subscription') === 'to') {
  4877. this.addExistingContact(contact);
  4878. } else {
  4879. if ((contact.get('ask') === 'subscribe') || (contact.get('subscription') === 'from')) {
  4880. this.addContactToGroup(contact, HEADER_PENDING_CONTACTS);
  4881. } else if (contact.get('requesting') === true) {
  4882. this.addContactToGroup(contact, HEADER_REQUESTING_CONTACTS);
  4883. }
  4884. }
  4885. return this;
  4886. }
  4887. });
  4888. this.XMPPStatus = Backbone.Model.extend({
  4889. initialize: function () {
  4890. this.set({
  4891. 'status' : this.getStatus()
  4892. });
  4893. this.on('change', function (item) {
  4894. if (this.get('fullname') === undefined) {
  4895. converse.getVCard(
  4896. null, // No 'to' attr when getting one's own vCard
  4897. function (iq, jid, fullname, image, image_type, url) {
  4898. this.save({'fullname': fullname});
  4899. }.bind(this)
  4900. );
  4901. }
  4902. if (_.has(item.changed, 'status')) {
  4903. converse.emit('statusChanged', this.get('status'));
  4904. }
  4905. if (_.has(item.changed, 'status_message')) {
  4906. converse.emit('statusMessageChanged', this.get('status_message'));
  4907. }
  4908. }.bind(this));
  4909. },
  4910. constructPresence: function (type, status_message) {
  4911. if (typeof type === 'undefined') {
  4912. type = this.get('status') || 'online';
  4913. }
  4914. if (typeof status_message === 'undefined') {
  4915. status_message = this.get('status_message');
  4916. }
  4917. var presence;
  4918. // Most of these presence types are actually not explicitly sent,
  4919. // but I add all of them here fore reference and future proofing.
  4920. if ((type === 'unavailable') ||
  4921. (type === 'probe') ||
  4922. (type === 'error') ||
  4923. (type === 'unsubscribe') ||
  4924. (type === 'unsubscribed') ||
  4925. (type === 'subscribe') ||
  4926. (type === 'subscribed')) {
  4927. presence = $pres({'type': type});
  4928. } else if (type === 'offline') {
  4929. presence = $pres({'type': 'unavailable'});
  4930. if (status_message) {
  4931. presence.c('show').t(type);
  4932. }
  4933. } else {
  4934. if (type === 'online') {
  4935. presence = $pres();
  4936. } else {
  4937. presence = $pres().c('show').t(type).up();
  4938. }
  4939. if (status_message) {
  4940. presence.c('status').t(status_message);
  4941. }
  4942. }
  4943. return presence;
  4944. },
  4945. sendPresence: function (type, status_message) {
  4946. converse.connection.send(this.constructPresence(type, status_message));
  4947. },
  4948. setStatus: function (value) {
  4949. this.sendPresence(value);
  4950. this.save({'status': value});
  4951. },
  4952. getStatus: function () {
  4953. return this.get('status') || 'online';
  4954. },
  4955. setStatusMessage: function (status_message) {
  4956. this.sendPresence(this.getStatus(), status_message);
  4957. var prev_status = this.get('status_message');
  4958. this.save({'status_message': status_message});
  4959. if (this.xhr_custom_status) {
  4960. $.ajax({
  4961. url: this.xhr_custom_status_url,
  4962. type: 'POST',
  4963. data: {'msg': status_message}
  4964. });
  4965. }
  4966. if (prev_status === status_message) {
  4967. this.trigger("update-status-ui", this);
  4968. }
  4969. }
  4970. });
  4971. this.XMPPStatusView = Backbone.View.extend({
  4972. el: "span#xmpp-status-holder",
  4973. events: {
  4974. "click a.choose-xmpp-status": "toggleOptions",
  4975. "click #fancy-xmpp-status-select a.change-xmpp-status-message": "renderStatusChangeForm",
  4976. "submit #set-custom-xmpp-status": "setStatusMessage",
  4977. "click .dropdown dd ul li a": "setStatus"
  4978. },
  4979. initialize: function () {
  4980. this.model.on("change:status", this.updateStatusUI, this);
  4981. this.model.on("change:status_message", this.updateStatusUI, this);
  4982. this.model.on("update-status-ui", this.updateStatusUI, this);
  4983. },
  4984. render: function () {
  4985. // Replace the default dropdown with something nicer
  4986. var $select = this.$el.find('select#select-xmpp-status'),
  4987. chat_status = this.model.get('status') || 'offline',
  4988. options = $('option', $select),
  4989. $options_target,
  4990. options_list = [];
  4991. this.$el.html(converse.templates.choose_status());
  4992. this.$el.find('#fancy-xmpp-status-select')
  4993. .html(converse.templates.chat_status({
  4994. 'status_message': this.model.get('status_message') || __("I am %1$s", this.getPrettyStatus(chat_status)),
  4995. 'chat_status': chat_status,
  4996. 'desc_custom_status': __('Click here to write a custom status message'),
  4997. 'desc_change_status': __('Click to change your chat status')
  4998. }));
  4999. // iterate through all the <option> elements and add option values
  5000. options.each(function () {
  5001. options_list.push(converse.templates.status_option({
  5002. 'value': $(this).val(),
  5003. 'text': this.text
  5004. }));
  5005. });
  5006. $options_target = this.$el.find("#target dd ul").hide();
  5007. $options_target.append(options_list.join(''));
  5008. $select.remove();
  5009. return this;
  5010. },
  5011. toggleOptions: function (ev) {
  5012. ev.preventDefault();
  5013. $(ev.target).parent().parent().siblings('dd').find('ul').toggle('fast');
  5014. },
  5015. renderStatusChangeForm: function (ev) {
  5016. ev.preventDefault();
  5017. var status_message = this.model.get('status') || 'offline';
  5018. var input = converse.templates.change_status_message({
  5019. 'status_message': status_message,
  5020. 'label_custom_status': __('Custom status'),
  5021. 'label_save': __('Save')
  5022. });
  5023. var $xmppstatus = this.$el.find('.xmpp-status');
  5024. $xmppstatus.parent().addClass('no-border');
  5025. $xmppstatus.replaceWith(input);
  5026. this.$el.find('.custom-xmpp-status').focus().focus();
  5027. },
  5028. setStatusMessage: function (ev) {
  5029. ev.preventDefault();
  5030. this.model.setStatusMessage($(ev.target).find('input').val());
  5031. },
  5032. setStatus: function (ev) {
  5033. ev.preventDefault();
  5034. var $el = $(ev.target),
  5035. value = $el.attr('data-value');
  5036. if (value === 'logout') {
  5037. this.$el.find(".dropdown dd ul").hide();
  5038. converse.logOut();
  5039. } else {
  5040. this.model.setStatus(value);
  5041. this.$el.find(".dropdown dd ul").hide();
  5042. }
  5043. },
  5044. getPrettyStatus: function (stat) {
  5045. if (stat === 'chat') {
  5046. return __('online');
  5047. } else if (stat === 'dnd') {
  5048. return __('busy');
  5049. } else if (stat === 'xa') {
  5050. return __('away for long');
  5051. } else if (stat === 'away') {
  5052. return __('away');
  5053. } else if (stat === 'offline') {
  5054. return __('offline');
  5055. } else {
  5056. return __(stat) || __('online');
  5057. }
  5058. },
  5059. updateStatusUI: function (model) {
  5060. var stat = model.get('status');
  5061. // For translators: the %1$s part gets replaced with the status
  5062. // Example, I am online
  5063. var status_message = model.get('status_message') || __("I am %1$s", this.getPrettyStatus(stat));
  5064. this.$el.find('#fancy-xmpp-status-select').removeClass('no-border').html(
  5065. converse.templates.chat_status({
  5066. 'chat_status': stat,
  5067. 'status_message': status_message,
  5068. 'desc_custom_status': __('Click here to write a custom status message'),
  5069. 'desc_change_status': __('Click to change your chat status')
  5070. }));
  5071. }
  5072. });
  5073. this.Session = Backbone.Model; // General session settings to be saved to sessionStorage.
  5074. this.Feature = Backbone.Model;
  5075. this.Features = Backbone.Collection.extend({
  5076. /* Service Discovery
  5077. * -----------------
  5078. * This collection stores Feature Models, representing features
  5079. * provided by available XMPP entities (e.g. servers)
  5080. * See XEP-0030 for more details: http://xmpp.org/extensions/xep-0030.html
  5081. * All features are shown here: http://xmpp.org/registrar/disco-features.html
  5082. */
  5083. model: converse.Feature,
  5084. initialize: function () {
  5085. this.addClientIdentities().addClientFeatures();
  5086. this.browserStorage = new Backbone.BrowserStorage[converse.storage](
  5087. b64_sha1('converse.features'+converse.bare_jid));
  5088. this.on('add', this.onFeatureAdded, this);
  5089. if (this.browserStorage.records.length === 0) {
  5090. // browserStorage is empty, so we've likely never queried this
  5091. // domain for features yet
  5092. converse.connection.disco.info(converse.domain, null, this.onInfo.bind(this));
  5093. converse.connection.disco.items(converse.domain, null, this.onItems.bind(this));
  5094. } else {
  5095. this.fetch({add:true});
  5096. }
  5097. },
  5098. onFeatureAdded: function (feature) {
  5099. var prefs = feature.get('preferences') || {};
  5100. converse.emit('serviceDiscovered', feature);
  5101. if (feature.get('var') === Strophe.NS.MAM && prefs['default'] !== converse.message_archiving) {
  5102. // Ask the server for archiving preferences
  5103. converse.connection.sendIQ(
  5104. $iq({'type': 'get'}).c('prefs', {'xmlns': Strophe.NS.MAM}),
  5105. _.bind(this.onMAMPreferences, this, feature),
  5106. _.bind(this.onMAMError, this, feature)
  5107. );
  5108. }
  5109. },
  5110. onMAMPreferences: function (feature, iq) {
  5111. /* Handle returned IQ stanza containing Message Archive
  5112. * Management (XEP-0313) preferences.
  5113. *
  5114. * XXX: For now we only handle the global default preference.
  5115. * The XEP also provides for per-JID preferences, which is
  5116. * currently not supported in converse.js.
  5117. *
  5118. * Per JID preferences will be set in chat boxes, so it'll
  5119. * probbaly be handled elsewhere in any case.
  5120. */
  5121. var $prefs = $(iq).find('prefs[xmlns="'+Strophe.NS.MAM+'"]');
  5122. var default_pref = $prefs.attr('default');
  5123. var stanza;
  5124. if (default_pref !== converse.message_archiving) {
  5125. stanza = $iq({'type': 'set'}).c('prefs', {'xmlns':Strophe.NS.MAM, 'default':converse.message_archiving});
  5126. $prefs.children().each(function (idx, child) {
  5127. stanza.cnode(child).up();
  5128. });
  5129. converse.connection.sendIQ(stanza, _.bind(function (feature, iq) {
  5130. // XXX: Strictly speaking, the server should respond with the updated prefs
  5131. // (see example 18: https://xmpp.org/extensions/xep-0313.html#config)
  5132. // but Prosody doesn't do this, so we don't rely on it.
  5133. feature.save({'preferences': {'default':converse.message_archiving}});
  5134. }, this, feature),
  5135. _.bind(this.onMAMError, this, feature)
  5136. );
  5137. } else {
  5138. feature.save({'preferences': {'default':converse.message_archiving}});
  5139. }
  5140. },
  5141. onMAMError: function (iq) {
  5142. if ($(iq).find('feature-not-implemented').length) {
  5143. converse.log("Message Archive Management (XEP-0313) not supported by this browser");
  5144. } else {
  5145. converse.log("An error occured while trying to set archiving preferences.");
  5146. converse.log(iq);
  5147. }
  5148. },
  5149. addClientIdentities: function () {
  5150. /* See http://xmpp.org/registrar/disco-categories.html
  5151. */
  5152. converse.connection.disco.addIdentity('client', 'web', 'Converse.js');
  5153. return this;
  5154. },
  5155. addClientFeatures: function () {
  5156. /* The strophe.disco.js plugin keeps a list of features which
  5157. * it will advertise to any #info queries made to it.
  5158. *
  5159. * See: http://xmpp.org/extensions/xep-0030.html#info
  5160. */
  5161. converse.connection.disco.addFeature('jabber:x:conference');
  5162. converse.connection.disco.addFeature(Strophe.NS.BOSH);
  5163. converse.connection.disco.addFeature(Strophe.NS.CHATSTATES);
  5164. converse.connection.disco.addFeature(Strophe.NS.DISCO_INFO);
  5165. converse.connection.disco.addFeature(Strophe.NS.MAM);
  5166. converse.connection.disco.addFeature(Strophe.NS.ROSTERX); // Limited support
  5167. if (converse.use_vcards) {
  5168. converse.connection.disco.addFeature(Strophe.NS.VCARD);
  5169. }
  5170. if (converse.allow_muc) {
  5171. converse.connection.disco.addFeature(Strophe.NS.MUC);
  5172. }
  5173. if (converse.message_carbons) {
  5174. converse.connection.disco.addFeature(Strophe.NS.CARBONS);
  5175. }
  5176. return this;
  5177. },
  5178. onItems: function (stanza) {
  5179. $(stanza).find('query item').each(function (idx, item) {
  5180. converse.connection.disco.info(
  5181. $(item).attr('jid'),
  5182. null,
  5183. this.onInfo.bind(this));
  5184. }.bind(this));
  5185. },
  5186. onInfo: function (stanza) {
  5187. var $stanza = $(stanza);
  5188. if (($stanza.find('identity[category=server][type=im]').length === 0) &&
  5189. ($stanza.find('identity[category=conference][type=text]').length === 0)) {
  5190. // This isn't an IM server component
  5191. return;
  5192. }
  5193. $stanza.find('feature').each(function (idx, feature) {
  5194. var namespace = $(feature).attr('var');
  5195. this[namespace] = true;
  5196. this.create({
  5197. 'var': namespace,
  5198. 'from': $stanza.attr('from')
  5199. });
  5200. }.bind(this));
  5201. }
  5202. });
  5203. this.RegisterPanel = Backbone.View.extend({
  5204. tagName: 'div',
  5205. id: "register",
  5206. className: 'controlbox-pane',
  5207. events: {
  5208. 'submit form#converse-register': 'onProviderChosen'
  5209. },
  5210. initialize: function (cfg) {
  5211. this.reset();
  5212. this.$parent = cfg.$parent;
  5213. this.$tabs = cfg.$parent.parent().find('#controlbox-tabs');
  5214. this.registerHooks();
  5215. },
  5216. render: function () {
  5217. this.$parent.append(this.$el.html(
  5218. converse.templates.register_panel({
  5219. 'label_domain': __("Your XMPP provider's domain name:"),
  5220. 'label_register': __('Fetch registration form'),
  5221. 'help_providers': __('Tip: A list of public XMPP providers is available'),
  5222. 'help_providers_link': __('here'),
  5223. 'href_providers': converse.providers_link,
  5224. 'domain_placeholder': converse.domain_placeholder
  5225. })
  5226. ));
  5227. this.$tabs.append(converse.templates.register_tab({label_register: __('Register')}));
  5228. return this;
  5229. },
  5230. registerHooks: function () {
  5231. /* Hook into Strophe's _connect_cb, so that we can send an IQ
  5232. * requesting the registration fields.
  5233. */
  5234. var conn = converse.connection;
  5235. var connect_cb = conn._connect_cb.bind(conn);
  5236. conn._connect_cb = function (req, callback, raw) {
  5237. if (!this._registering) {
  5238. connect_cb(req, callback, raw);
  5239. } else {
  5240. if (this.getRegistrationFields(req, callback, raw)) {
  5241. this._registering = false;
  5242. }
  5243. }
  5244. }.bind(this);
  5245. },
  5246. getRegistrationFields: function (req, _callback, raw) {
  5247. /* Send an IQ stanza to the XMPP server asking for the
  5248. * registration fields.
  5249. * Parameters:
  5250. * (Strophe.Request) req - The current request
  5251. * (Function) callback
  5252. */
  5253. converse.log("sendQueryStanza was called");
  5254. var conn = converse.connection;
  5255. conn.connected = true;
  5256. var body = conn._proto._reqToData(req);
  5257. if (!body) { return; }
  5258. if (conn._proto._connect_cb(body) === Strophe.Status.CONNFAIL) {
  5259. return false;
  5260. }
  5261. var register = body.getElementsByTagName("register");
  5262. var mechanisms = body.getElementsByTagName("mechanism");
  5263. if (register.length === 0 && mechanisms.length === 0) {
  5264. conn._proto._no_auth_received(_callback);
  5265. return false;
  5266. }
  5267. if (register.length === 0) {
  5268. conn._changeConnectStatus(
  5269. Strophe.Status.REGIFAIL,
  5270. __('Sorry, the given provider does not support in band account registration. Please try with a different provider.')
  5271. );
  5272. return true;
  5273. }
  5274. // Send an IQ stanza to get all required data fields
  5275. conn._addSysHandler(this.onRegistrationFields.bind(this), null, "iq", null, null);
  5276. conn.send($iq({type: "get"}).c("query", {xmlns: Strophe.NS.REGISTER}).tree());
  5277. return true;
  5278. },
  5279. onRegistrationFields: function (stanza) {
  5280. /* Handler for Registration Fields Request.
  5281. *
  5282. * Parameters:
  5283. * (XMLElement) elem - The query stanza.
  5284. */
  5285. if (stanza.getElementsByTagName("query").length !== 1) {
  5286. converse.connection._changeConnectStatus(Strophe.Status.REGIFAIL, "unknown");
  5287. return false;
  5288. }
  5289. this.setFields(stanza);
  5290. this.renderRegistrationForm(stanza);
  5291. return false;
  5292. },
  5293. reset: function (settings) {
  5294. var defaults = {
  5295. fields: {},
  5296. urls: [],
  5297. title: "",
  5298. instructions: "",
  5299. registered: false,
  5300. _registering: false,
  5301. domain: null,
  5302. form_type: null
  5303. };
  5304. _.extend(this, defaults);
  5305. if (settings) {
  5306. _.extend(this, _.pick(settings, Object.keys(defaults)));
  5307. }
  5308. },
  5309. onProviderChosen: function (ev) {
  5310. /* Callback method that gets called when the user has chosen an
  5311. * XMPP provider.
  5312. *
  5313. * Parameters:
  5314. * (Submit Event) ev - Form submission event.
  5315. */
  5316. if (ev && ev.preventDefault) { ev.preventDefault(); }
  5317. var $form = $(ev.target),
  5318. $domain_input = $form.find('input[name=domain]'),
  5319. domain = $domain_input.val();
  5320. if (!domain) {
  5321. $domain_input.addClass('error');
  5322. return;
  5323. }
  5324. $form.find('input[type=submit]').hide()
  5325. .after(converse.templates.registration_request({
  5326. cancel: __('Cancel'),
  5327. info_message: __('Requesting a registration form from the XMPP server')
  5328. }));
  5329. $form.find('button.cancel').on('click', this.cancelRegistration.bind(this));
  5330. this.reset({
  5331. domain: Strophe.getDomainFromJid(domain),
  5332. _registering: true
  5333. });
  5334. converse.connection.connect(this.domain, "", this.onRegistering.bind(this));
  5335. return false;
  5336. },
  5337. giveFeedback: function (message, klass) {
  5338. this.$('.reg-feedback').attr('class', 'reg-feedback').text(message);
  5339. if (klass) {
  5340. $('.reg-feedback').addClass(klass);
  5341. }
  5342. },
  5343. onRegistering: function (status, error) {
  5344. var that;
  5345. converse.log('onRegistering');
  5346. if (_.contains([
  5347. Strophe.Status.DISCONNECTED,
  5348. Strophe.Status.CONNFAIL,
  5349. Strophe.Status.REGIFAIL,
  5350. Strophe.Status.NOTACCEPTABLE,
  5351. Strophe.Status.CONFLICT
  5352. ], status)) {
  5353. converse.log('Problem during registration: Strophe.Status is: '+status);
  5354. this.cancelRegistration();
  5355. if (error) {
  5356. this.giveFeedback(error, 'error');
  5357. } else {
  5358. this.giveFeedback(__(
  5359. 'Something went wrong while establishing a connection with "%1$s". Are you sure it exists?',
  5360. this.domain
  5361. ), 'error');
  5362. }
  5363. } else if (status === Strophe.Status.REGISTERED) {
  5364. converse.log("Registered successfully.");
  5365. converse.connection.reset();
  5366. that = this;
  5367. this.$('form').hide(function () {
  5368. $(this).replaceWith('<span class="spinner centered"/>');
  5369. if (that.fields.password && that.fields.username) {
  5370. // automatically log the user in
  5371. converse.connection.connect(
  5372. that.fields.username.toLowerCase()+'@'+that.domain.toLowerCase(),
  5373. that.fields.password,
  5374. converse.onConnectStatusChanged
  5375. );
  5376. converse.chatboxviews.get('controlbox')
  5377. .switchTab({target: that.$tabs.find('.current')})
  5378. .giveFeedback(__('Now logging you in'));
  5379. } else {
  5380. converse.chatboxviews.get('controlbox')
  5381. .renderLoginPanel()
  5382. .giveFeedback(__('Registered successfully'));
  5383. }
  5384. that.reset();
  5385. });
  5386. }
  5387. },
  5388. renderRegistrationForm: function (stanza) {
  5389. /* Renders the registration form based on the XForm fields
  5390. * received from the XMPP server.
  5391. *
  5392. * Parameters:
  5393. * (XMLElement) stanza - The IQ stanza received from the XMPP server.
  5394. */
  5395. var $form= this.$('form'),
  5396. $stanza = $(stanza),
  5397. $fields, $input;
  5398. $form.empty().append(converse.templates.registration_form({
  5399. 'domain': this.domain,
  5400. 'title': this.title,
  5401. 'instructions': this.instructions
  5402. }));
  5403. if (this.form_type === 'xform') {
  5404. $fields = $stanza.find('field');
  5405. _.each($fields, function (field) {
  5406. $form.append(utils.xForm2webForm.bind(this, $(field), $stanza));
  5407. }.bind(this));
  5408. } else {
  5409. // Show fields
  5410. _.each(Object.keys(this.fields), function (key) {
  5411. if (key === "username") {
  5412. $input = templates.form_username({
  5413. domain: ' @'+this.domain,
  5414. name: key,
  5415. type: "text",
  5416. label: key,
  5417. value: '',
  5418. required: 1
  5419. });
  5420. } else {
  5421. $form.append('<label>'+key+'</label>');
  5422. $input = $('<input placeholder="'+key+'" name="'+key+'"></input>');
  5423. if (key === 'password' || key === 'email') {
  5424. $input.attr('type', key);
  5425. }
  5426. }
  5427. $form.append($input);
  5428. }.bind(this));
  5429. // Show urls
  5430. _.each(this.urls, function (url) {
  5431. $form.append($('<a target="blank"></a>').attr('href', url).text(url));
  5432. }.bind(this));
  5433. }
  5434. if (this.fields) {
  5435. $form.append('<input type="submit" class="pure-button button-primary" value="'+__('Register')+'"/>');
  5436. $form.on('submit', this.submitRegistrationForm.bind(this));
  5437. $form.append('<input type="button" class="pure-button button-cancel" value="'+__('Cancel')+'"/>');
  5438. $form.find('input[type=button]').on('click', this.cancelRegistration.bind(this));
  5439. } else {
  5440. $form.append('<input type="button" class="submit" value="'+__('Return')+'"/>');
  5441. $form.find('input[type=button]').on('click', this.cancelRegistration.bind(this));
  5442. }
  5443. },
  5444. reportErrors: function (stanza) {
  5445. /* Report back to the user any error messages received from the
  5446. * XMPP server after attempted registration.
  5447. *
  5448. * Parameters:
  5449. * (XMLElement) stanza - The IQ stanza received from the
  5450. * XMPP server.
  5451. */
  5452. var $form= this.$('form'), flash;
  5453. var $errmsgs = $(stanza).find('error text');
  5454. var $flash = $form.find('.form-errors');
  5455. if (!$flash.length) {
  5456. flash = '<legend class="form-errors"></legend>';
  5457. if ($form.find('p.instructions').length) {
  5458. $form.find('p.instructions').append(flash);
  5459. } else {
  5460. $form.prepend(flash);
  5461. }
  5462. $flash = $form.find('.form-errors');
  5463. } else {
  5464. $flash.empty();
  5465. }
  5466. $errmsgs.each(function (idx, txt) {
  5467. $flash.append($('<p>').text($(txt).text()));
  5468. });
  5469. if (!$errmsgs.length) {
  5470. $flash.append($('<p>').text(
  5471. __('The provider rejected your registration attempt. '+
  5472. 'Please check the values you entered for correctness.')));
  5473. }
  5474. $flash.show();
  5475. },
  5476. cancelRegistration: function (ev) {
  5477. /* Handler, when the user cancels the registration form.
  5478. */
  5479. if (ev && ev.preventDefault) { ev.preventDefault(); }
  5480. converse.connection.reset();
  5481. this.render();
  5482. },
  5483. submitRegistrationForm : function (ev) {
  5484. /* Handler, when the user submits the registration form.
  5485. * Provides form error feedback or starts the registration
  5486. * process.
  5487. *
  5488. * Parameters:
  5489. * (Event) ev - the submit event.
  5490. */
  5491. if (ev && ev.preventDefault) { ev.preventDefault(); }
  5492. var $empty_inputs = this.$('input.required:emptyVal');
  5493. if ($empty_inputs.length) {
  5494. $empty_inputs.addClass('error');
  5495. return;
  5496. }
  5497. var $inputs = $(ev.target).find(':input:not([type=button]):not([type=submit])'),
  5498. iq = $iq({type: "set"}).c("query", {xmlns:Strophe.NS.REGISTER});
  5499. if (this.form_type === 'xform') {
  5500. iq.c("x", {xmlns: Strophe.NS.XFORM, type: 'submit'});
  5501. $inputs.each(function () {
  5502. iq.cnode(utils.webForm2xForm(this)).up();
  5503. });
  5504. } else {
  5505. $inputs.each(function () {
  5506. var $input = $(this);
  5507. iq.c($input.attr('name'), {}, $input.val());
  5508. });
  5509. }
  5510. converse.connection._addSysHandler(this._onRegisterIQ.bind(this), null, "iq", null, null);
  5511. converse.connection.send(iq);
  5512. this.setFields(iq.tree());
  5513. },
  5514. setFields: function (stanza) {
  5515. /* Stores the values that will be sent to the XMPP server
  5516. * during attempted registration.
  5517. *
  5518. * Parameters:
  5519. * (XMLElement) stanza - the IQ stanza that will be sent to the XMPP server.
  5520. */
  5521. var $query = $(stanza).find('query'), $xform;
  5522. if ($query.length > 0) {
  5523. $xform = $query.find('x[xmlns="'+Strophe.NS.XFORM+'"]');
  5524. if ($xform.length > 0) {
  5525. this._setFieldsFromXForm($xform);
  5526. } else {
  5527. this._setFieldsFromLegacy($query);
  5528. }
  5529. }
  5530. },
  5531. _setFieldsFromLegacy: function ($query) {
  5532. $query.children().each(function (idx, field) {
  5533. var $field = $(field);
  5534. if (field.tagName.toLowerCase() === 'instructions') {
  5535. this.instructions = Strophe.getText(field);
  5536. return;
  5537. } else if (field.tagName.toLowerCase() === 'x') {
  5538. if ($field.attr('xmlns') === 'jabber:x:oob') {
  5539. $field.find('url').each(function (idx, url) {
  5540. this.urls.push($(url).text());
  5541. }.bind(this));
  5542. }
  5543. return;
  5544. }
  5545. this.fields[field.tagName.toLowerCase()] = Strophe.getText(field);
  5546. }.bind(this));
  5547. this.form_type = 'legacy';
  5548. },
  5549. _setFieldsFromXForm: function ($xform) {
  5550. this.title = $xform.find('title').text();
  5551. this.instructions = $xform.find('instructions').text();
  5552. $xform.find('field').each(function (idx, field) {
  5553. var _var = field.getAttribute('var');
  5554. if (_var) {
  5555. this.fields[_var.toLowerCase()] = $(field).children('value').text();
  5556. } else {
  5557. // TODO: other option seems to be type="fixed"
  5558. converse.log("WARNING: Found field we couldn't parse");
  5559. }
  5560. }.bind(this));
  5561. this.form_type = 'xform';
  5562. },
  5563. _onRegisterIQ: function (stanza) {
  5564. /* Callback method that gets called when a return IQ stanza
  5565. * is received from the XMPP server, after attempting to
  5566. * register a new user.
  5567. *
  5568. * Parameters:
  5569. * (XMLElement) stanza - The IQ stanza.
  5570. */
  5571. var error = null,
  5572. query = stanza.getElementsByTagName("query");
  5573. if (query.length > 0) {
  5574. query = query[0];
  5575. }
  5576. if (stanza.getAttribute("type") === "error") {
  5577. converse.log("Registration failed.");
  5578. error = stanza.getElementsByTagName("error");
  5579. if (error.length !== 1) {
  5580. converse.connection._changeConnectStatus(Strophe.Status.REGIFAIL, "unknown");
  5581. return false;
  5582. }
  5583. error = error[0].firstChild.tagName.toLowerCase();
  5584. if (error === 'conflict') {
  5585. converse.connection._changeConnectStatus(Strophe.Status.CONFLICT, error);
  5586. } else if (error === 'not-acceptable') {
  5587. converse.connection._changeConnectStatus(Strophe.Status.NOTACCEPTABLE, error);
  5588. } else {
  5589. converse.connection._changeConnectStatus(Strophe.Status.REGIFAIL, error);
  5590. }
  5591. this.reportErrors(stanza);
  5592. } else {
  5593. converse.connection._changeConnectStatus(Strophe.Status.REGISTERED, null);
  5594. }
  5595. return false;
  5596. },
  5597. remove: function () {
  5598. this.$tabs.empty();
  5599. this.$el.parent().empty();
  5600. }
  5601. });
  5602. this.LoginPanel = Backbone.View.extend({
  5603. tagName: 'div',
  5604. id: "login-dialog",
  5605. className: 'controlbox-pane',
  5606. events: {
  5607. 'submit form#converse-login': 'authenticate'
  5608. },
  5609. initialize: function (cfg) {
  5610. cfg.$parent.html(this.$el.html(
  5611. converse.templates.login_panel({
  5612. 'LOGIN': LOGIN,
  5613. 'ANONYMOUS': ANONYMOUS,
  5614. 'PREBIND': PREBIND,
  5615. 'auto_login': converse.auto_login,
  5616. 'authentication': converse.authentication,
  5617. 'label_username': __('XMPP Username:'),
  5618. 'label_password': __('Password:'),
  5619. 'label_anon_login': __('Click here to log in anonymously'),
  5620. 'label_login': __('Log In'),
  5621. 'placeholder_username': (converse.locked_domain || converse.default_domain) && __('Username') || __('user@server'),
  5622. 'placeholder_password': __('password')
  5623. })
  5624. ));
  5625. this.$tabs = cfg.$parent.parent().find('#controlbox-tabs');
  5626. },
  5627. render: function () {
  5628. this.$tabs.append(converse.templates.login_tab({label_sign_in: __('Sign in')}));
  5629. this.$el.find('input#jid').focus();
  5630. if (!this.$el.is(':visible')) {
  5631. this.$el.show();
  5632. }
  5633. return this;
  5634. },
  5635. authenticate: function (ev) {
  5636. if (ev && ev.preventDefault) { ev.preventDefault(); }
  5637. var $form = $(ev.target);
  5638. if (converse.authentication === ANONYMOUS) {
  5639. this.connect($form, converse.jid, null);
  5640. return;
  5641. }
  5642. var $jid_input = $form.find('input[name=jid]'),
  5643. jid = $jid_input.val(),
  5644. $pw_input = $form.find('input[name=password]'),
  5645. password = $pw_input.val(),
  5646. errors = false;
  5647. if (! jid) {
  5648. errors = true;
  5649. $jid_input.addClass('error');
  5650. }
  5651. if (! password) {
  5652. errors = true;
  5653. $pw_input.addClass('error');
  5654. }
  5655. if (errors) { return; }
  5656. if (converse.locked_domain) {
  5657. jid = Strophe.escapeNode(jid) + '@' + converse.locked_domain;
  5658. } else if (converse.default_domain && jid.indexOf('@') === -1) {
  5659. jid = jid + '@' + converse.default_domain;
  5660. }
  5661. this.connect($form, jid, password);
  5662. return false;
  5663. },
  5664. connect: function ($form, jid, password) {
  5665. var resource;
  5666. if ($form) {
  5667. $form.find('input[type=submit]').hide().after('<span class="spinner login-submit"/>');
  5668. }
  5669. if (jid) {
  5670. resource = Strophe.getResourceFromJid(jid);
  5671. if (!resource) {
  5672. jid = jid.toLowerCase() + '/converse.js-' + Math.floor(Math.random()*139749825).toString();
  5673. } else {
  5674. jid = Strophe.getBareJidFromJid(jid).toLowerCase()+'/'+Strophe.getResourceFromJid(jid);
  5675. }
  5676. }
  5677. converse.connection.connect(jid, password, converse.onConnectStatusChanged);
  5678. },
  5679. remove: function () {
  5680. this.$tabs.empty();
  5681. this.$el.parent().empty();
  5682. }
  5683. });
  5684. this.ControlBoxToggle = Backbone.View.extend({
  5685. tagName: 'a',
  5686. className: 'toggle-controlbox',
  5687. id: 'toggle-controlbox',
  5688. events: {
  5689. 'click': 'onClick'
  5690. },
  5691. attributes: {
  5692. 'href': "#"
  5693. },
  5694. initialize: function () {
  5695. this.render();
  5696. },
  5697. render: function () {
  5698. $('#conversejs').prepend(this.$el.html(
  5699. converse.templates.controlbox_toggle({
  5700. 'label_toggle': __('Toggle chat')
  5701. })
  5702. ));
  5703. // We let the render method of ControlBoxView decide whether
  5704. // the ControlBox or the Toggle must be shown. This prevents
  5705. // artifacts (i.e. on page load the toggle is shown only to then
  5706. // seconds later be hidden in favor of the control box).
  5707. this.$el.hide();
  5708. return this;
  5709. },
  5710. hide: function (callback) {
  5711. this.$el.fadeOut('fast', callback);
  5712. },
  5713. show: function (callback) {
  5714. this.$el.show('fast', callback);
  5715. },
  5716. showControlBox: function () {
  5717. var controlbox = converse.chatboxes.get('controlbox');
  5718. if (!controlbox) {
  5719. controlbox = converse.addControlBox();
  5720. }
  5721. if (converse.connection.connected) {
  5722. controlbox.save({closed: false});
  5723. } else {
  5724. controlbox.trigger('show');
  5725. }
  5726. },
  5727. onClick: function (e) {
  5728. e.preventDefault();
  5729. if ($("div#controlbox").is(':visible')) {
  5730. var controlbox = converse.chatboxes.get('controlbox');
  5731. if (converse.connection.connected) {
  5732. controlbox.save({closed: true});
  5733. } else {
  5734. controlbox.trigger('hide');
  5735. }
  5736. } else {
  5737. this.showControlBox();
  5738. }
  5739. }
  5740. });
  5741. this.addControlBox = function () {
  5742. return this.chatboxes.add({
  5743. id: 'controlbox',
  5744. box_id: 'controlbox',
  5745. closed: !this.show_controlbox_by_default
  5746. });
  5747. };
  5748. this.setUpXMLLogging = function () {
  5749. if (this.debug) {
  5750. this.connection.xmlInput = function (body) { converse.log(body); };
  5751. this.connection.xmlOutput = function (body) { converse.log(body); };
  5752. }
  5753. };
  5754. this.startNewBOSHSession = function () {
  5755. $.ajax({
  5756. url: this.prebind_url,
  5757. type: 'GET',
  5758. success: function (response) {
  5759. this.connection.attach(
  5760. response.jid,
  5761. response.sid,
  5762. response.rid,
  5763. this.onConnectStatusChanged
  5764. );
  5765. }.bind(this),
  5766. error: function (response) {
  5767. delete this.connection;
  5768. this.emit('noResumeableSession');
  5769. }.bind(this)
  5770. });
  5771. };
  5772. this.attemptPreboundSession = function (tokens) {
  5773. /* Handle session resumption or initialization when prebind is being used.
  5774. */
  5775. if (this.keepalive) {
  5776. if (!this.jid) {
  5777. throw new Error("initConnection: when using 'keepalive' with 'prebind, you must supply the JID of the current user.");
  5778. }
  5779. try {
  5780. return this.connection.restore(this.jid, this.onConnectStatusChanged);
  5781. } catch (e) {
  5782. this.log("Could not restore session for jid: "+this.jid+" Error message: "+e.message);
  5783. this.clearSession(); // If there's a roster, we want to clear it (see #555)
  5784. }
  5785. } else { // Not keepalive
  5786. if (this.jid && this.sid && this.rid) {
  5787. return this.connection.attach(this.jid, this.sid, this.rid, this.onConnectStatusChanged);
  5788. } else {
  5789. throw new Error("initConnection: If you use prebind and not keepalive, "+
  5790. "then you MUST supply JID, RID and SID values");
  5791. }
  5792. }
  5793. // We haven't been able to attach yet. Let's see if there
  5794. // is a prebind_url, otherwise there's nothing with which
  5795. // we can attach.
  5796. if (this.prebind_url) {
  5797. this.startNewBOSHSession();
  5798. } else {
  5799. delete this.connection;
  5800. this.emit('noResumeableSession');
  5801. }
  5802. };
  5803. this.attemptNonPreboundSession = function () {
  5804. /* Handle session resumption or initialization when prebind is not being used.
  5805. *
  5806. * Two potential options exist and are handled in this method:
  5807. * 1. keepalive
  5808. * 2. auto_login
  5809. */
  5810. if (this.keepalive) {
  5811. try {
  5812. return this.connection.restore(undefined, this.onConnectStatusChanged);
  5813. } catch (e) {
  5814. this.log("Could not restore session. Error message: "+e.message);
  5815. this.clearSession(); // If there's a roster, we want to clear it (see #555)
  5816. }
  5817. }
  5818. if (this.auto_login) {
  5819. if (!this.jid) {
  5820. throw new Error("initConnection: If you use auto_login, you also need to provide a jid value");
  5821. }
  5822. if (this.authentication === ANONYMOUS) {
  5823. this.connection.connect(this.jid.toLowerCase(), null, this.onConnectStatusChanged);
  5824. } else if (this.authentication === LOGIN) {
  5825. if (!this.password) {
  5826. throw new Error("initConnection: If you use auto_login and "+
  5827. "authentication='login' then you also need to provide a password.");
  5828. }
  5829. this.jid = Strophe.getBareJidFromJid(this.jid).toLowerCase()+'/'+Strophe.getResourceFromJid(this.jid);
  5830. this.connection.connect(this.jid, this.password, this.onConnectStatusChanged);
  5831. }
  5832. }
  5833. };
  5834. this.initConnection = function () {
  5835. if (this.connection && this.connection.connected) {
  5836. this.setUpXMLLogging();
  5837. this.onConnected();
  5838. } else {
  5839. if (!this.bosh_service_url && ! this.websocket_url) {
  5840. throw new Error("initConnection: you must supply a value for either the bosh_service_url or websocket_url or both.");
  5841. }
  5842. if (('WebSocket' in window || 'MozWebSocket' in window) && this.websocket_url) {
  5843. this.connection = new Strophe.Connection(this.websocket_url);
  5844. } else if (this.bosh_service_url) {
  5845. this.connection = new Strophe.Connection(this.bosh_service_url, {'keepalive': this.keepalive});
  5846. } else {
  5847. throw new Error("initConnection: this browser does not support websockets and bosh_service_url wasn't specified.");
  5848. }
  5849. this.setUpXMLLogging();
  5850. // We now try to resume or automatically set up a new session.
  5851. // Otherwise the user will be shown a login form.
  5852. if (this.authentication === PREBIND) {
  5853. this.attemptPreboundSession();
  5854. } else {
  5855. this.attemptNonPreboundSession();
  5856. }
  5857. }
  5858. };
  5859. this._tearDown = function () {
  5860. /* Remove those views which are only allowed with a valid
  5861. * connection.
  5862. */
  5863. if (this.roster) {
  5864. this.roster.off().reset(); // Removes roster contacts
  5865. }
  5866. if (this.rosterview) {
  5867. this.rosterview.unregisterHandlers();
  5868. this.rosterview.model.off().reset(); // Removes roster groups
  5869. this.rosterview.undelegateEvents().remove();
  5870. }
  5871. this.chatboxes.remove(); // Don't call off(), events won't get re-registered upon reconnect.
  5872. if (this.features) {
  5873. this.features.reset();
  5874. }
  5875. if (this.minimized_chats) {
  5876. this.minimized_chats.undelegateEvents().model.reset();
  5877. this.minimized_chats.removeAll(); // Remove sub-views
  5878. this.minimized_chats.tearDown().remove(); // Remove overview
  5879. delete this.minimized_chats;
  5880. }
  5881. return this;
  5882. };
  5883. this._initialize = function () {
  5884. this.chatboxes = new this.ChatBoxes();
  5885. this.chatboxviews = new this.ChatBoxViews({model: this.chatboxes});
  5886. this.controlboxtoggle = new this.ControlBoxToggle();
  5887. this.otr = new this.OTR();
  5888. this.initSession();
  5889. this.initConnection();
  5890. if (this.connection) {
  5891. this.addControlBox();
  5892. }
  5893. return this;
  5894. };
  5895. this._overrideAttribute = function (key, plugin) {
  5896. // See converse.plugins.override
  5897. var value = plugin.overrides[key];
  5898. if (typeof value === "function") {
  5899. if (typeof plugin._super === "undefined") {
  5900. plugin._super = {'converse': converse};
  5901. }
  5902. plugin._super[key] = converse[key].bind(converse);
  5903. converse[key] = value.bind(plugin);
  5904. } else {
  5905. converse[key] = value;
  5906. }
  5907. };
  5908. this._extendObject = function (obj, attributes) {
  5909. // See converse.plugins.extend
  5910. if (!obj.prototype._super) {
  5911. obj.prototype._super = {'converse': converse};
  5912. }
  5913. _.each(attributes, function (value, key) {
  5914. if (key === 'events') {
  5915. obj.prototype[key] = _.extend(value, obj.prototype[key]);
  5916. } else {
  5917. if (typeof value === 'function') {
  5918. obj.prototype._super[key] = obj.prototype[key];
  5919. }
  5920. obj.prototype[key] = value;
  5921. }
  5922. });
  5923. };
  5924. this._initializePlugins = function () {
  5925. _.each(this.plugins, function (plugin) {
  5926. plugin.converse = converse;
  5927. _.each(Object.keys(plugin.overrides), function (key) {
  5928. /* We automatically override all methods and Backbone views and
  5929. * models that are in the "overrides" namespace.
  5930. */
  5931. var override = plugin.overrides[key];
  5932. if (typeof override === "object") {
  5933. this._extendObject(converse[key], override);
  5934. } else {
  5935. this._overrideAttribute(key, plugin);
  5936. }
  5937. }.bind(this));
  5938. if (typeof plugin.initialize === "function") {
  5939. plugin.initialize.bind(plugin)(this);
  5940. }
  5941. }.bind(this));
  5942. };
  5943. // Initialization
  5944. // --------------
  5945. // This is the end of the initialize method.
  5946. if (settings.connection) {
  5947. this.connection = settings.connection;
  5948. }
  5949. this._initializePlugins();
  5950. this._initialize();
  5951. this.registerGlobalEventHandlers();
  5952. converse.emit('initialized');
  5953. };
  5954. var wrappedChatBox = function (chatbox) {
  5955. if (!chatbox) { return; }
  5956. var view = converse.chatboxviews.get(chatbox.get('jid'));
  5957. return {
  5958. 'close': view.close.bind(view),
  5959. 'endOTR': chatbox.endOTR.bind(chatbox),
  5960. 'focus': view.focus.bind(view),
  5961. 'get': chatbox.get.bind(chatbox),
  5962. 'initiateOTR': chatbox.initiateOTR.bind(chatbox),
  5963. 'is_chatroom': view.is_chatroom,
  5964. 'maximize': chatbox.maximize.bind(chatbox),
  5965. 'minimize': chatbox.minimize.bind(chatbox),
  5966. 'open': view.show.bind(view),
  5967. 'set': chatbox.set.bind(chatbox)
  5968. };
  5969. };
  5970. var API = {
  5971. 'initialize': function (settings, callback) {
  5972. converse.initialize(settings, callback);
  5973. },
  5974. 'disconnect': function () {
  5975. converse.connection.disconnect();
  5976. },
  5977. 'account': {
  5978. // XXX: Deprecated, will be removed with next non-minor release
  5979. 'logout': function () {
  5980. converse.logOut();
  5981. }
  5982. },
  5983. 'user': {
  5984. 'logout': function () {
  5985. converse.logOut();
  5986. },
  5987. 'status': {
  5988. 'get': function () {
  5989. return converse.xmppstatus.get('status');
  5990. },
  5991. 'set': function (value, message) {
  5992. var data = {'status': value};
  5993. if (!_.contains(_.keys(STATUS_WEIGHTS), value)) {
  5994. throw new Error('Invalid availability value. See https://xmpp.org/rfcs/rfc3921.html#rfc.section.2.2.2.1');
  5995. }
  5996. if (typeof message === "string") {
  5997. data.status_message = message;
  5998. }
  5999. converse.xmppstatus.save(data);
  6000. },
  6001. 'message': {
  6002. 'get': function () {
  6003. return converse.xmppstatus.get('status_message');
  6004. },
  6005. 'set': function (stat) {
  6006. converse.xmppstatus.save({'status_message': stat});
  6007. }
  6008. }
  6009. },
  6010. },
  6011. 'settings': {
  6012. 'get': function (key) {
  6013. if (_.contains(Object.keys(converse.default_settings), key)) {
  6014. return converse[key];
  6015. }
  6016. },
  6017. 'set': function (key, val) {
  6018. var o = {};
  6019. if (typeof key === "object") {
  6020. _.extend(converse, _.pick(key, Object.keys(converse.default_settings)));
  6021. } else if (typeof key === "string") {
  6022. o[key] = val;
  6023. _.extend(converse, _.pick(o, Object.keys(converse.default_settings)));
  6024. }
  6025. }
  6026. },
  6027. 'contacts': {
  6028. 'get': function (jids) {
  6029. var _transform = function (jid) {
  6030. var contact = converse.roster.get(Strophe.getBareJidFromJid(jid));
  6031. if (contact) {
  6032. return contact.attributes;
  6033. }
  6034. return null;
  6035. };
  6036. if (typeof jids === "undefined") {
  6037. jids = converse.roster.pluck('jid');
  6038. } else if (typeof jids === "string") {
  6039. return _transform(jids);
  6040. }
  6041. return _.map(jids, _transform);
  6042. },
  6043. 'add': function (jid, name) {
  6044. if (typeof jid !== "string" || jid.indexOf('@') < 0) {
  6045. throw new TypeError('contacts.add: invalid jid');
  6046. }
  6047. converse.roster.addAndSubscribe(jid, _.isEmpty(name)? jid: name);
  6048. }
  6049. },
  6050. 'chats': {
  6051. 'open': function (jids) {
  6052. var chatbox;
  6053. if (typeof jids === "undefined") {
  6054. converse.log("chats.open: You need to provide at least one JID", "error");
  6055. return null;
  6056. } else if (typeof jids === "string") {
  6057. chatbox = wrappedChatBox(converse.chatboxes.getChatBox(jids, true));
  6058. chatbox.open();
  6059. return chatbox;
  6060. }
  6061. return _.map(jids, function (jid) {
  6062. chatbox = wrappedChatBox(converse.chatboxes.getChatBox(jid, true));
  6063. chatbox.open();
  6064. return chatbox;
  6065. });
  6066. },
  6067. 'get': function (jids) {
  6068. if (typeof jids === "undefined") {
  6069. converse.log("chats.get: You need to provide at least one JID", "error");
  6070. return null;
  6071. } else if (typeof jids === "string") {
  6072. return wrappedChatBox(converse.chatboxes.getChatBox(jids, true));
  6073. }
  6074. return _.map(jids, _.partial(_.compose(wrappedChatBox, converse.chatboxes.getChatBox.bind(converse.chatboxes)), _, true));
  6075. }
  6076. },
  6077. 'archive': {
  6078. 'query': function (options, callback, errback) {
  6079. /* Do a MAM (XEP-0313) query for archived messages.
  6080. *
  6081. * Parameters:
  6082. * (Object) options - Query parameters, either MAM-specific or also for Result Set Management.
  6083. * (Function) callback - A function to call whenever we receive query-relevant stanza.
  6084. * (Function) errback - A function to call when an error stanza is received.
  6085. *
  6086. * The options parameter can also be an instance of
  6087. * Strophe.RSM to enable easy querying between results pages.
  6088. *
  6089. * The callback function may be called multiple times, first
  6090. * for the initial IQ result and then for each message
  6091. * returned. The last time the callback is called, a
  6092. * Strophe.RSM object is returned on which "next" or "previous"
  6093. * can be called before passing it in again to this method, to
  6094. * get the next or previous page in the result set.
  6095. */
  6096. var date, messages = [];
  6097. if (typeof options === "function") {
  6098. callback = options;
  6099. errback = callback;
  6100. }
  6101. if (!converse.features.findWhere({'var': Strophe.NS.MAM})) {
  6102. throw new Error('This server does not support XEP-0313, Message Archive Management');
  6103. }
  6104. var queryid = converse.connection.getUniqueId();
  6105. var attrs = {'type':'set'};
  6106. if (typeof options !== "undefined" && options.groupchat) {
  6107. if (!options['with']) {
  6108. throw new Error('You need to specify a "with" value containing the chat room JID, when querying groupchat messages.');
  6109. }
  6110. attrs.to = options['with'];
  6111. }
  6112. var stanza = $iq(attrs).c('query', {'xmlns':Strophe.NS.MAM, 'queryid':queryid});
  6113. if (typeof options !== "undefined") {
  6114. stanza.c('x', {'xmlns':Strophe.NS.XFORM, 'type': 'submit'})
  6115. .c('field', {'var':'FORM_TYPE', 'type': 'hidden'})
  6116. .c('value').t(Strophe.NS.MAM).up().up();
  6117. if (options['with'] && !options.groupchat) {
  6118. stanza.c('field', {'var':'with'}).c('value').t(options['with']).up().up();
  6119. }
  6120. _.each(['start', 'end'], function (t) {
  6121. if (options[t]) {
  6122. date = moment(options[t]);
  6123. if (date.isValid()) {
  6124. stanza.c('field', {'var':t}).c('value').t(date.format()).up().up();
  6125. } else {
  6126. throw new TypeError('archive.query: invalid date provided for: '+t);
  6127. }
  6128. }
  6129. });
  6130. stanza.up();
  6131. if (options instanceof Strophe.RSM) {
  6132. stanza.cnode(options.toXML());
  6133. } else if (_.intersection(RSM_ATTRIBUTES, _.keys(options)).length) {
  6134. stanza.cnode(new Strophe.RSM(options).toXML());
  6135. }
  6136. }
  6137. converse.connection.addHandler(function (message) {
  6138. var $msg = $(message), $fin, rsm;
  6139. if (typeof callback === "function") {
  6140. $fin = $msg.find('fin[xmlns="'+Strophe.NS.MAM+'"]');
  6141. if ($fin.length) {
  6142. rsm = new Strophe.RSM({xml: $fin.find('set')[0]});
  6143. _.extend(rsm, _.pick(options, ['max']));
  6144. _.extend(rsm, _.pick(options, MAM_ATTRIBUTES));
  6145. callback(messages, rsm);
  6146. return false; // We've received all messages, decommission this handler
  6147. } else if (queryid === $msg.find('result').attr('queryid')) {
  6148. messages.push(message);
  6149. }
  6150. return true;
  6151. } else {
  6152. return false; // There's no callback, so no use in continuing this handler.
  6153. }
  6154. }, Strophe.NS.MAM);
  6155. converse.connection.sendIQ(stanza, null, errback);
  6156. }
  6157. },
  6158. 'rooms': {
  6159. 'open': function (jids, nick) {
  6160. if (!nick) {
  6161. nick = Strophe.getNodeFromJid(converse.bare_jid);
  6162. }
  6163. if (typeof nick !== "string") {
  6164. throw new TypeError('rooms.open: invalid nick, must be string');
  6165. }
  6166. var _transform = function (jid) {
  6167. jid = jid.toLowerCase();
  6168. var chatroom = converse.chatboxes.get(jid);
  6169. converse.log('jid');
  6170. if (!chatroom) {
  6171. chatroom = converse.chatboxviews.showChat({
  6172. 'id': jid,
  6173. 'jid': jid,
  6174. 'name': Strophe.unescapeNode(Strophe.getNodeFromJid(jid)),
  6175. 'nick': nick,
  6176. 'chatroom': true,
  6177. 'box_id' : b64_sha1(jid)
  6178. });
  6179. }
  6180. return wrappedChatBox(converse.chatboxes.getChatBox(jid, true));
  6181. };
  6182. if (typeof jids === "undefined") {
  6183. throw new TypeError('rooms.open: You need to provide at least one JID');
  6184. } else if (typeof jids === "string") {
  6185. return _transform(jids);
  6186. }
  6187. return _.map(jids, _transform);
  6188. },
  6189. 'get': function (jids) {
  6190. if (typeof jids === "undefined") {
  6191. throw new TypeError("rooms.get: You need to provide at least one JID");
  6192. } else if (typeof jids === "string") {
  6193. return wrappedChatBox(converse.chatboxes.getChatBox(jids, true));
  6194. }
  6195. return _.map(jids, _.partial(wrappedChatBox, _.bind(converse.chatboxes.getChatBox, converse.chatboxes, _, true)));
  6196. }
  6197. },
  6198. 'tokens': {
  6199. 'get': function (id) {
  6200. if (!converse.expose_rid_and_sid || typeof converse.connection === "undefined") {
  6201. return null;
  6202. }
  6203. if (id.toLowerCase() === 'rid') {
  6204. return converse.connection.rid || converse.connection._proto.rid;
  6205. } else if (id.toLowerCase() === 'sid') {
  6206. return converse.connection.sid || converse.connection._proto.sid;
  6207. }
  6208. }
  6209. },
  6210. 'listen': {
  6211. 'once': function (evt, handler) {
  6212. converse.once(evt, handler);
  6213. },
  6214. 'on': function (evt, handler) {
  6215. converse.on(evt, handler);
  6216. },
  6217. 'not': function (evt, handler) {
  6218. converse.off(evt, handler);
  6219. },
  6220. },
  6221. 'send': function (stanza) {
  6222. converse.connection.send(stanza);
  6223. },
  6224. 'ping': function (jid) {
  6225. converse.ping(jid);
  6226. },
  6227. 'plugins': {
  6228. 'add': function (name, plugin) {
  6229. converse.plugins[name] = plugin;
  6230. },
  6231. 'remove': function (name) {
  6232. delete converse.plugins[name];
  6233. },
  6234. 'override': function (name, value) {
  6235. /* Helper method for overriding methods and attributes directly on the
  6236. * converse object. For Backbone objects, use instead the 'extend'
  6237. * method.
  6238. *
  6239. * If a method is overridden, then the original method will still be
  6240. * available via the _super attribute.
  6241. *
  6242. * name: The attribute being overridden.
  6243. * value: The value of the attribute being overridden.
  6244. */
  6245. converse._overrideAttribute(name, value);
  6246. },
  6247. 'extend': function (obj, attributes) {
  6248. /* Helper method for overriding or extending Converse's Backbone Views or Models
  6249. *
  6250. * When a method is overriden, the original will still be available
  6251. * on the _super attribute of the object being overridden.
  6252. *
  6253. * obj: The Backbone View or Model
  6254. * attributes: A hash of attributes, such as you would pass to Backbone.Model.extend or Backbone.View.extend
  6255. */
  6256. converse._extendObject(obj, attributes);
  6257. }
  6258. },
  6259. 'env': {
  6260. '$build': $build,
  6261. '$iq': $iq,
  6262. '$msg': $msg,
  6263. '$pres': $pres,
  6264. 'Strophe': Strophe,
  6265. '_': _,
  6266. 'b64_sha1': b64_sha1,
  6267. 'jQuery': $,
  6268. 'moment': moment
  6269. }
  6270. };
  6271. return API;
  6272. }));