qvitter.js 154 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321
  1. /*· · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · ·
  2. · ·
  3. · ·
  4. · Q V I T T E R ·
  5. · ·
  6. · ·
  7. · <o) ·
  8. · /_//// ·
  9. · (____/ ·
  10. · (o< ·
  11. · o> \\\\_\ ·
  12. · \\) \____) ·
  13. · ·
  14. · ·
  15. · @licstart The following is the entire license notice for the ·
  16. · JavaScript code in this page. ·
  17. · ·
  18. · Copyright (C) 2015 Hannes Mannerheim and other contributors ·
  19. · ·
  20. · ·
  21. · This program is free software: you can redistribute it and/or modify ·
  22. · it under the terms of the GNU Affero General Public License as ·
  23. · published by the Free Software Foundation, either version 3 of the ·
  24. · License, or (at your option) any later version. ·
  25. · ·
  26. · This program is distributed in the hope that it will be useful, ·
  27. · but WITHOUT ANY WARRANTY; without even the implied warranty of ·
  28. · MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ·
  29. · GNU Affero General Public License for more details. ·
  30. · ·
  31. · You should have received a copy of the GNU Affero General Public License ·
  32. · along with this program. If not, see <http://www.gnu.org/licenses/>. ·
  33. · ·
  34. · @licend The above is the entire license notice ·
  35. · for the JavaScript code in this page. ·
  36. · ·
  37. · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · */
  38. // plugins can add translatons to this object
  39. window.pluginTranslations = [];
  40. // object to keep old states of streams in, to speed up stream change
  41. window.oldStreams = new Object();
  42. // check our localStorage and make sure it's correct
  43. checkLocalStorage();
  44. // don't let users inject html/scripts into their own user data... not that it matters, it is only displayed to themselves, but just to be 200% safe
  45. window.loggedIn = iterateRecursiveReplaceHtmlSpecialChars(window.loggedIn);
  46. // hack to supress basic auth popup, e.g. if the user has to tabs open and
  47. // log out in one of them. but microsoft browsers and chrome 59+ doesn't support this
  48. if(typeof bowser != 'undefined') {
  49. var bowserIntVersion = parseInt(bowser.version,10);
  50. if(typeof bowser.msie == 'undefined'
  51. && typeof bowser.msedge == 'undefined'
  52. && !(typeof bowser.chrome != 'undefined' && bowser.chrome === true && bowserIntVersion <= 59)) {
  53. window.apiRoot = window.apiRoot.replace('://','://x:x@');
  54. }
  55. }
  56. /* ·
  57. ·
  58. · Update stream on back button
  59. ·
  60. · · · · · · · · · · · · · */
  61. window.onpopstate = function(event) {
  62. if(event && event.state) {
  63. display_spinner();
  64. setNewCurrentStream(pathToStreamRouter(event.state.strm),false,false,function(){
  65. remove_spinner();
  66. });
  67. }
  68. }
  69. /* ·
  70. ·
  71. · Discard error messages
  72. ·
  73. · · · · · · · · · · · · · */
  74. $('body').on('click','.discard-error-message',function(){
  75. // don't nag on people
  76. if($(this).parent().hasClass('language-error-message')) {
  77. localStorageObjectCache_STORE('languageErrorMessageDiscarded',$(this).parent().attr('data-language-name'), true);
  78. }
  79. $(this).addClass('clicked');
  80. $(this).closest('.error-message, .language-error-message').slideUp(100,function(){
  81. $(this).remove();
  82. });
  83. });
  84. /* ·
  85. ·
  86. · welcome text expand and collapse
  87. ·
  88. · · · · · · · · · · · · · */
  89. $('body').on('click','.show-full-welcome-text, .front-welcome-text:not(.expanded) sup',function(){
  90. $('.front-welcome-text').toggleClass('expanded');
  91. if($('.front-welcome-text').hasClass('expanded')) {
  92. var welcomeTextInnerObjectsHeightSum = $('.front-welcome-text > p').outerHeight() + $('.front-welcome-text > h1').outerHeight() + 50;
  93. $('.front-welcome-text').css('height', welcomeTextInnerObjectsHeightSum + 'px')
  94. }
  95. else {
  96. $('.front-welcome-text').css('height', '180px');
  97. $('.front-welcome-text').css('overflow', 'hidden');
  98. var scrollTo = $(window).scrollTop() - ($('.front-welcome-text').outerHeight()-200);
  99. if(scrollTo < 0) { scrollTo = 0;}
  100. $('html, body').animate({ scrollTop: scrollTo}, 300, 'linear');
  101. }
  102. });
  103. $('body').on('click','.welcome-text-register-link',function(){
  104. var scrollTo = $('#user-container').offset().top;
  105. $('html, body').animate({ scrollTop: scrollTo}, 300, 'linear');
  106. });
  107. /* ·
  108. ·
  109. · Check for tooltips to display
  110. ·
  111. · · · · · · · · · · · · · */
  112. $('body').on({
  113. mouseover: function (e) {
  114. removeAllTooltips();
  115. // convert title to tooltip
  116. if($(e.target).is('[title]')) {
  117. var titleAttribute = replaceHtmlSpecialChars($(e.target).attr('title')); // can contain malicious code
  118. $(e.target).attr('data-tooltip',titleAttribute);
  119. $(e.target).removeAttr('title');
  120. }
  121. // regular tooltips
  122. if($(e.target).is('[data-tooltip]')) {
  123. var tooltipClass = '';
  124. tooltip_data = $(e.target).attr('data-tooltip');
  125. // if embedded content is hidden, we show it in tooltips
  126. if($('#feed-body').hasClass('embedded-content-hidden-by-user')
  127. && !$(e.target).is('.oembed-item')
  128. && $(e.target).closest('.queet').length > 0
  129. && $(e.target).closest('.queet').find('.oembed-item[href="' + $(e.target).attr('href') + '"]').length > 0) {
  130. tooltip_data = $(e.target).closest('.queet').find('.oembed-item[href="' + $(e.target).attr('href') + '"]').html();
  131. tooltipClass = 'oembed';
  132. }
  133. else if($('#feed-body').hasClass('embedded-content-hidden-by-user')
  134. && !$(e.target).is('.attachment-thumb')
  135. && $(e.target).closest('.queet').length > 0
  136. && $(e.target).text().indexOf('/attachment/') > -1) {
  137. // local attachments has /attachment/-url in its href attribute
  138. if($(e.target).closest('.queet').find('.thumb-container[data-local-attachment-url="' + $(e.target).attr('href') + '"]').length>0) {
  139. tooltip_data = $(e.target).closest('.queet').find('.thumb-container[data-local-attachment-url="' + $(e.target).attr('href') + '"]').outerHTML();
  140. tooltipClass = 'thumb';
  141. }
  142. // remote attachments are identified by full url
  143. else if($(e.target).closest('.queet').find('.thumb-container[href="' + $(e.target).attr('data-tooltip') + '"]').length>0) {
  144. tooltip_data = $(e.target).closest('.queet').find('.thumb-container[href="' + $(e.target).attr('data-tooltip') + '"]').outerHTML();
  145. tooltipClass = 'thumb';
  146. }
  147. // sometimes the attachment link in the queet text does not give us any clue to
  148. // which attachment it is referring to. but if it is the only link and there is
  149. // exactly one attachment, we can safely assume that the link is referring to
  150. // that attachment
  151. else if($(e.target).closest('.queet').find('.thumb-container').length == 1
  152. && $(e.target).closest('.queet-text').find('a.attachment').length == 1) {
  153. tooltip_data = $(e.target).closest('.queet').find('.thumb-container').outerHTML();
  154. tooltipClass = 'thumb';
  155. }
  156. }
  157. else if($('#feed-body').hasClass('quotes-hidden-by-user')
  158. && !$(e.target).is('.quote-link-container')
  159. && $(e.target).is('[data-quote-url]')
  160. && $(e.target).closest('.queet-text').find('.quote-link-container[data-quote-url="' + $(e.target).attr('data-quote-url') + '"]').length > 0) {
  161. tooltip_data = $(e.target).closest('.queet-text').find('.quote-link-container[data-quote-url="' + $(e.target).attr('data-quote-url') + '"]').html();
  162. tooltipClass = 'quote';
  163. }
  164. var tooltipElement = $('<div class="tooltip ' + tooltipClass + '" lang="' + window.selectedLanguage + '">' + tooltip_data + '</div>');
  165. var tooltipCaret = $('<div class="tooltip-caret"></div>');
  166. $('body').prepend(tooltipElement);
  167. $('body').prepend(tooltipCaret);
  168. // align tooltip to the hovered element
  169. alignTooltipTohoveredElement(tooltipElement,tooltipCaret,$(e.target));
  170. // fade in
  171. tooltipElement.css('opacity','1');
  172. tooltipCaret.css('opacity','1');
  173. }
  174. },
  175. mouseleave: function (e) {
  176. removeAllTooltips();
  177. }
  178. });
  179. // tooltips should be removed very easily, e.g. when any of these events happen
  180. $('body').on("touchstart scroll click dblclick mousedown mouseup submit keydown keypress keyup", function(e){
  181. removeAllTooltips();
  182. });
  183. // removes all tooltips
  184. function removeAllTooltips() {
  185. $('.tooltip,.tooltip-caret').remove();
  186. }
  187. /* ·
  188. ·
  189. · Check for profile hovercards to display
  190. ·
  191. · · · · · · · · · · · · · */
  192. window.userArrayLastRetrieved = new Object();
  193. $('body').on('mouseover',function (e) {
  194. // no hovercards on these elements
  195. if($(e.target).is('#user-queets') || $(e.target).closest('a').is('#user-queets')
  196. || $(e.target).is('.tweet-stats') || $(e.target).closest('a').is('.tweet-stats')) {
  197. return true;
  198. }
  199. var timeNow = new Date().getTime();
  200. removeAllhoverCards(e,timeNow);
  201. var hoverCardData = false;
  202. var userArray = false;
  203. var hrefAttr = false;
  204. var possibleNickname = false;
  205. // closest a-element with a href
  206. if($(e.target).is('[href]')) {
  207. var targetElement = $(e.target);
  208. }
  209. else if($(e.target).closest('a').length>0) {
  210. if($(e.target).closest('a').is('[href]')) {
  211. var targetElement = $(e.target).closest('a');
  212. }
  213. else {
  214. var targetElement = $(e.target);
  215. }
  216. }
  217. else {
  218. var targetElement = $(e.target);
  219. }
  220. // get href-attribute
  221. if(targetElement.is('[href]')) {
  222. hrefAttr = targetElement.attr('href');
  223. }
  224. else {
  225. return true;
  226. }
  227. // no hover card if the element has the no-hover-card class
  228. if(targetElement.hasClass('no-hover-card')) {
  229. return true;
  230. }
  231. // no hovercard for anchor links
  232. if(hrefAttr.substring(0,1) == '#') {
  233. return true;
  234. }
  235. // guess what element close by could be a nickname
  236. if($(e.target).is('[href]')) {
  237. possibleNickname = $(e.target).text();
  238. }
  239. else if($(e.target).closest('a').length>0 && $(e.target).closest('a').is('[href]')) {
  240. if($(e.target).siblings('.screen-name').length>0) { // the screen name can be in a sibling if we're lucky
  241. possibleNickname = $(e.target).siblings('.screen-name').text();
  242. }
  243. else {
  244. possibleNickname = $(e.target).text();
  245. }
  246. }
  247. // see if we have it in cache, otherwise query server
  248. getUserArrayData(hrefAttr, possibleNickname, timeNow, targetElement, function(userArray, timeOut){
  249. // bad data
  250. if(typeof userArray.local == 'undefined') {
  251. return;
  252. }
  253. // build card from either the local or external data, depending on what we got
  254. if (userArray.local !== null && userArray.local.is_local == true) {
  255. var profileCard = buildProfileCard(userArray.local);
  256. }
  257. else if(userArray.local !== null && userArray.local.is_local == false && (typeof userArray.external == 'undefined' || userArray.external === null || userArray.external === false)) {
  258. var profileCard = buildProfileCard(userArray.local);
  259. }
  260. else if ((userArray.local === null || userArray.local === false || userArray.local.is_local == false) && typeof userArray.external != 'undefined' && userArray.external !== false && userArray.external !== null) {
  261. var profileCard = buildExternalProfileCard(userArray);
  262. }
  263. else {
  264. console.log('could not build profile card...');
  265. return;
  266. }
  267. var hoverCardElement = $('<div id="hover-card-' + timeNow + '" class="hover-card" data-card-created="' + timeNow + '">' + profileCard.profileCardHtml + '</div>');
  268. var hoverCardCaret = $('<div id="hover-card-caret-' + timeNow + '" class="hover-card-caret"></div>');
  269. targetElement.attr('data-awaiting-hover-card',timeNow);
  270. // let user hover for 600ms before showing the card
  271. setTimeout(function(){
  272. // make sure user is still hovering the same link and that that the link awaits the same hover card
  273. // (user can have flickered on and off the link triggering two or more hover cards to in setTimeout delay)
  274. if(targetElement.is(":hover") && parseInt(targetElement.attr('data-awaiting-hover-card'),10) == timeNow) {
  275. if($('.hover-card').length == 0) { // no card if there already is one open
  276. $('body').prepend(hoverCardElement);
  277. $('body').prepend(hoverCardCaret);
  278. targetElement.attr('data-hover-card-active',timeNow);
  279. // if the user array has not been retrieved from the server for the last 60 seconds,
  280. // we query it for the lastest data
  281. if((typeof window.userArrayLastRetrieved[hrefAttr] == 'undefined') || (timeNow - window.userArrayLastRetrieved[hrefAttr]) > 60000) {
  282. window.userArrayLastRetrieved[hrefAttr] = timeNow;
  283. // local users
  284. if(userArray.local && userArray.local.is_local === true) {
  285. getFromAPI('users/show.json?id=' + userArray.local.screen_name, function(data){
  286. if(data) {
  287. var newProfileCard = buildProfileCard(data);
  288. hoverCardElement.html(newProfileCard.profileCardHtml);
  289. alignTooltipTohoveredElement(hoverCardElement,hoverCardCaret,targetElement);
  290. }
  291. });
  292. }
  293. // external users
  294. else if(!userArray.local || userArray.local.is_local === false) {
  295. getFromAPI('qvitter/external_user_show.json?profileurl=' + encodeURIComponent(hrefAttr),function(data){
  296. if(data && data.external !== null) {
  297. var newProfileCard = buildExternalProfileCard(data);
  298. hoverCardElement.html(newProfileCard.profileCardHtml);
  299. alignTooltipTohoveredElement(hoverCardElement,hoverCardCaret,targetElement);
  300. }
  301. });
  302. }
  303. }
  304. // hide tooltips
  305. $('.tooltip,.tooltip-caret').remove();
  306. // align hover card to the hovered element
  307. alignTooltipTohoveredElement(hoverCardElement,hoverCardCaret,targetElement);
  308. // fade in
  309. hoverCardElement.css('opacity','1');
  310. hoverCardCaret.css('opacity','1');
  311. }
  312. }
  313. },timeOut);
  314. });
  315. });
  316. // get user array from cache (or from server)
  317. function getUserArrayData(maybeProfileUrl,maybeNickname,timeNow,targetElement,callback) {
  318. if(maybeProfileUrl && maybeNickname) {
  319. userArray = userArrayCacheGetByProfileUrlAndNickname(maybeProfileUrl, maybeNickname);
  320. // no cached user array found, query server if this seems to be a profile url
  321. if(!userArray) {
  322. var streamObject = URLtoStreamRouter(maybeProfileUrl);
  323. // pathToStreamRouter failed finding a local stream for this path, maybe it's a remote profile?
  324. if(streamObject === false) {
  325. // we don't want to query the server every time we just pass an a-element with the cursor, so if the user
  326. // hovers the element for, say, 200ms we ask the server if the link could be a remote profile
  327. setTimeout(function(){
  328. if(targetElement.is(":hover")) {
  329. // don't try this if we already tried it less than a minute ago
  330. if((typeof window.userArrayLastRetrieved[maybeProfileUrl] == 'undefined') || (timeNow - window.userArrayLastRetrieved[maybeProfileUrl]) > 60000) {
  331. window.userArrayLastRetrieved[maybeProfileUrl] = timeNow;
  332. getFromAPI('qvitter/external_user_show.json?profileurl=' + encodeURIComponent(maybeProfileUrl),function(data){
  333. if(data) {
  334. // we want hover cards to appear _at least_ 600ms after hover (see below)
  335. var timeAfterServerQuery = new Date().getTime();
  336. var queryTime = timeAfterServerQuery-timeNow;
  337. if(queryTime<600) {
  338. var timeOut = 600-queryTime;
  339. }
  340. else {
  341. var timeOut = 0;
  342. }
  343. callback(data,timeOut);
  344. }
  345. });
  346. }
  347. }
  348. },200);
  349. }
  350. // likely an uncached local profile
  351. else if(streamObject && (streamObject.name == 'profile' || streamObject.name == 'profile by id')) {
  352. var nicknameOrId = streamObject.nickname;
  353. if(!nicknameOrId) {
  354. nicknameOrId = streamObject.id;
  355. }
  356. // don't query too often for the same user
  357. if(typeof window.userArrayLastRetrieved[maybeProfileUrl] == 'undefined' || (timeNow - window.userArrayLastRetrieved[maybeProfileUrl]) > 60000) {
  358. window.userArrayLastRetrieved[maybeProfileUrl] = timeNow;
  359. // query server and cache user data (done automatically in getFromAPI)
  360. getFromAPI('users/show.json?id=' + nicknameOrId, function(data){
  361. if(data) {
  362. userArray = {local:data};
  363. // we want hover cards to appear _at least_ 600ms after hover
  364. // we could just set the timeout to 0 and let the card appear
  365. // whenever it's loaded, but this will not feel good if we're
  366. // on a crazy fast server. so we calculate the diff time and makes
  367. // sure the total delay is at least 600ms
  368. var timeAfterServerQuery = new Date().getTime();
  369. var queryTime = timeAfterServerQuery-timeNow;
  370. if(queryTime<600) {
  371. var timeOut = 600-queryTime;
  372. }
  373. else {
  374. var timeOut = 0;
  375. }
  376. // continue to display the hover card
  377. callback(userArray,timeOut);
  378. }
  379. });
  380. }
  381. }
  382. }
  383. // from cache
  384. else {
  385. // continue to display the hover card
  386. // 600ms before cards appear feels pretty good
  387. // but this can be tweaked if cards appear to fast/slow
  388. callback(userArray,600);
  389. }
  390. }
  391. }
  392. // hover cards should be removed very easily, e.g. when any of these events happen
  393. $('body').on("mouseleave touchstart scroll click dblclick mousedown mouseup submit keydown keypress keyup", function(e){
  394. var timeNow = new Date().getTime();
  395. removeAllhoverCards(e,timeNow);
  396. });
  397. // removes all hover cards
  398. function removeAllhoverCards(event,priorTo) {
  399. // don't remove hovercards until after 100ms, so user have time to move the cursor to it (which gives it the dont-remove-card class)
  400. setTimeout(function(){
  401. $.each($('.hover-card'),function(){
  402. // don't remove card if it was created after removeAllhoverCards() was called
  403. if($(this).data('card-created') < priorTo) {
  404. // don't remove it if we're hovering it right now!
  405. if(!$(this).hasClass('dont-remove-card')) {
  406. $('[data-hover-card-active="' + $(this).data('card-created') + '"]').removeAttr('data-hover-card-active');
  407. $('#hover-card-caret-' + $(this).data('card-created')).remove();
  408. $(this).remove();
  409. }
  410. }
  411. });
  412. },100);
  413. }
  414. // if we're hovering a hover card, give it a class, so we don't remove it
  415. $('body').on('mouseover','.hover-card', function(e) {
  416. $(this).addClass('dont-remove-card');
  417. });
  418. $('body').on('mouseleave','.hover-card', function(e) {
  419. $(this).removeClass('dont-remove-card');
  420. });
  421. /* ·
  422. ·
  423. · find someone tool
  424. ·
  425. · · · · · · · · · · · · · */
  426. $('#find-someone input').keyup(function(e){
  427. var thisFindSomeoneInput = $(this);
  428. if(e.keyCode==13 && !thisFindSomeoneInput.hasClass('submitted')) {
  429. thisFindSomeoneInput.addClass('submitted');
  430. thisFindSomeoneInput.attr('disabled','disabled');
  431. var val = $.trim(thisFindSomeoneInput.val());
  432. // if this is a simple text input, we assume it is a local user
  433. if(val.length>1 && /^(@)?[a-zA-Z0-9]+$/.test(val)) {
  434. if(val.indexOf('@') == 0) {
  435. val = val.replace('@','');
  436. }
  437. setNewCurrentStream(pathToStreamRouter(val),true,false,function(){
  438. foundSomeone(thisFindSomeoneInput);
  439. });
  440. }
  441. // urls might be a remote user
  442. else if(val.length==0 || /^(ftp|http|https):\/\/[^ "]+$/.test(val)) {
  443. getFromAPI('qvitter/external_user_show.json?profileurl=' + encodeURIComponent(val),function(data){
  444. if(data && data.local !== null) {
  445. setNewCurrentStream(pathToStreamRouter('user/' + data.local.id),true,false,function(){
  446. foundSomeone(thisFindSomeoneInput);
  447. });
  448. }
  449. else {
  450. cantFindSomeone(thisFindSomeoneInput);
  451. }
  452. });
  453. }
  454. // @user@instance.domain style syntax
  455. else if(val.length==0 || /^(@)?[a-zA-Z0-9]+@[a-zA-Z0-9\-]+(\.)(.*)+$/.test(val)) {
  456. if(val.indexOf('@') == 0) {
  457. val = val.substring(1)
  458. }
  459. var username = val.substring(0, val.indexOf('@'));
  460. var domain = val.substring(val.indexOf('@')+1);
  461. var urlToTry = 'https://' + domain + '/' + username;
  462. var secondUrlToTry = 'http://' + domain + '/' + username;
  463. var thirdUrlToTry = 'https://' + domain + '/@' + username; // mastodon
  464. getFromAPI('qvitter/external_user_show.json?profileurl=' + encodeURIComponent(urlToTry),function(data){
  465. if(data && data.local !== null) {
  466. setNewCurrentStream(pathToStreamRouter('user/' + data.local.id),true,false,function(){
  467. foundSomeone(thisFindSomeoneInput);
  468. });
  469. }
  470. else {
  471. getFromAPI('qvitter/external_user_show.json?profileurl=' + encodeURIComponent(secondUrlToTry),function(data){
  472. if(data && data.local !== null) {
  473. setNewCurrentStream(pathToStreamRouter('user/' + data.local.id),true,false,function(){
  474. foundSomeone(thisFindSomeoneInput);
  475. });
  476. }
  477. else {
  478. getFromAPI('qvitter/external_user_show.json?profileurl=' + encodeURIComponent(thirdUrlToTry),function(data){
  479. if(data && data.local !== null) {
  480. setNewCurrentStream(pathToStreamRouter('user/' + data.local.id),true,false,function(){
  481. foundSomeone(thisFindSomeoneInput);
  482. });
  483. }
  484. else {
  485. cantFindSomeone(thisFindSomeoneInput);
  486. }
  487. });
  488. }
  489. });
  490. }
  491. });
  492. }
  493. else {
  494. cantFindSomeone(thisFindSomeoneInput);
  495. }
  496. }
  497. });
  498. function cantFindSomeone(thisFindSomeoneInput) {
  499. thisFindSomeoneInput.css('background-color','pink');
  500. thisFindSomeoneInput.effect('shake',{distance:5,times:3,duration:700},function(){
  501. thisFindSomeoneInput.animate({backgroundColor:'#fff'},1000);
  502. thisFindSomeoneInput.removeAttr('disabled');
  503. thisFindSomeoneInput.removeClass('submitted');
  504. thisFindSomeoneInput.focus();
  505. });
  506. }
  507. function foundSomeone(thisFindSomeoneInput) {
  508. thisFindSomeoneInput.removeAttr('disabled');
  509. thisFindSomeoneInput.val('');
  510. thisFindSomeoneInput.blur();
  511. thisFindSomeoneInput.removeClass('submitted');
  512. }
  513. /* ·
  514. ·
  515. · fix login and register box to top when they reach top
  516. ·
  517. · · · · · · · · · · · · · */
  518. $(window).scroll(function(e){
  519. if($('#page-container > .profile-card').length > 0) {
  520. var feedOrProfileCardOffsetTop = $('#page-container > .profile-card').offset().top;
  521. }
  522. else {
  523. var feedOrProfileCardOffsetTop = $('#feed').offset().top;
  524. }
  525. if ($(this).scrollTop() > (feedOrProfileCardOffsetTop-55) && $('#login-content').css('position') != 'fixed'){
  526. var loginAndSignUpBoxesHeight = $('#login-content').outerHeight() + $('.front-signup').not('#popup-signup').outerHeight();
  527. $('#login-register-container').css({'position': 'fixed', 'top': '55px'});
  528. }
  529. else if ($(this).scrollTop() < (feedOrProfileCardOffsetTop-55) && $('#login-content').css('position') != 'absolute'){
  530. $('#login-register-container').css({'position': 'relative', 'top': 'auto'});
  531. }
  532. });
  533. /* ·
  534. ·
  535. · Tooltip to show what federated means
  536. ·
  537. · · · · · · · · · · · · · */
  538. $('body').on('mouseenter','#federated-tooltip',function(){
  539. $('#what-is-federation').fadeIn(100);
  540. });
  541. $('body').on('mouseleave','#what-is-federation',function(){
  542. $('#what-is-federation').fadeOut(100);
  543. });
  544. /* ·
  545. ·
  546. · Scroll to top when clicking top bar
  547. ·
  548. · · · · · · · · · · · · · */
  549. $('body').on('click','.global-nav',function(e) {
  550. if($(e.target).hasClass('global-nav')) {
  551. $(window).scrollTop(0);
  552. }
  553. });
  554. /* ·
  555. ·
  556. · Register
  557. ·
  558. · · · · · · · · · · · · · */
  559. if(!window.registrationsClosed) {
  560. $('.front-signup input, .front-signup button').removeAttr('disabled'); // clear this onload
  561. $('#signup-btn-step1').click(function(){
  562. $(document).trigger('onClickStep1Register'); // hook
  563. display_spinner();
  564. $('.front-signup input, .front-signup button').addClass('disabled');
  565. $('.front-signup input, .front-signup button').attr('disabled','disabled');
  566. // 7 s timeout to annoy human spammers
  567. setTimeout(function(){
  568. remove_spinner();
  569. popUpAction('popup-register',window.sL.signUp,'<div id="popup-signup" class="front-signup">' +
  570. '<div class="signup-input-container"><div id="atsign">@</div><input placeholder="' + window.sL.registerNickname + '" type="text" autocomplete="off" class="text-input" id="signup-user-nickname-step2"><div class="fieldhelp">a-z0-9</div></div>' +
  571. '<div class="signup-input-container"><input placeholder="' + window.sL.signUpFullName + '" type="text" autocomplete="off" class="text-input" id="signup-user-name-step2" value="' + $('#signup-user-name').val() + '"></div>' +
  572. '<div class="signup-input-container"><input placeholder="' + window.sL.signUpEmail + '" type="text" autocomplete="off" id="signup-user-email-step2" value="' + $('#signup-user-email').val() + '"></div>' +
  573. '<div class="signup-input-container"><input placeholder="' + window.sL.registerHomepage + '" type="text" autocomplete="off" class="text-input" id="signup-user-homepage-step2"></div>' +
  574. '<div class="signup-input-container"><input placeholder="' + window.sL.registerBio + '" type="text" autocomplete="off" class="text-input" id="signup-user-bio-step2"></div>' +
  575. '<div class="signup-input-container"><input placeholder="' + window.sL.registerLocation + '" type="text" autocomplete="off" class="text-input" id="signup-user-location-step2"></div>' +
  576. '<div class="signup-input-container"><input placeholder="' + window.sL.loginPassword + '" type="password" class="text-input" id="signup-user-password1-step2" value="' + $('#signup-user-password').val() + '"><div class="fieldhelp">>5</div></div>' +
  577. '<div class="signup-input-container"><input placeholder="' + window.sL.registerRepeatPassword + '" type="password" class="text-input" id="signup-user-password2-step2"></div>' +
  578. '<div id="signup-terms-header">' + window.sL.showTerms.replace(/{site-title}/g,window.siteTitle) + '</div><div id="signup-terms-container"></div>' +
  579. '<button id="signup-btn-step2" class="signup-btn disabled" type="submit">' + window.sL.signUpButtonText + '</button>' +
  580. '</div>',false);
  581. // ask api if nickname is ok, if no typing for 1 s
  582. $('#signup-user-nickname-step2').on('keyup',function(){
  583. clearTimeout(window.checkNicknameTimeout);
  584. var thisInputElement = $(this);
  585. var thisValue = $(this).val();
  586. if(thisValue.length>1 && /^[a-zA-Z0-9]+$/.test(thisValue)) {
  587. thisInputElement.addClass('nickname-taken');
  588. if($('.spinner-wrap').length==0) {
  589. thisInputElement.after('<div class="spinner-wrap"><div class="spinner"><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i></div></div>');
  590. }
  591. window.checkNicknameTimeout = setTimeout(function(){
  592. $.get(window.apiRoot + 'check_nickname.json?nickname=' + encodeURIComponent(thisValue),function(data){
  593. $('.spinner-wrap').remove();
  594. if(data==0) {
  595. $('#signup-user-password2-step2').trigger('keyup'); // revalidates
  596. }
  597. else {
  598. thisInputElement.removeClass('nickname-taken');
  599. $('#signup-user-password2-step2').trigger('keyup');
  600. }
  601. });
  602. },1000);
  603. }
  604. else {
  605. $('.spinner-wrap').remove();
  606. }
  607. });
  608. // ask api if email is in use, if no typing for 1 s
  609. $('#signup-user-email-step2').on('keyup',function(){
  610. clearTimeout(window.checkEmailTimeout);
  611. var thisInputElement = $(this);
  612. var thisValue = $(this).val();
  613. if(thisValue.length>1 && validEmail(thisValue)) {
  614. thisInputElement.addClass('email-in-use');
  615. if($('.spinner-wrap').length==0) {
  616. thisInputElement.after('<div class="spinner-wrap"><div class="spinner"><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i></div></div>');
  617. }
  618. window.checkEmailTimeout = setTimeout(function(){
  619. $.get(window.apiRoot + 'qvitter/check_email.json?email=' + encodeURIComponent(thisValue),function(data){
  620. $('.spinner-wrap').remove();
  621. if(data==1) {
  622. $('#signup-user-password2-step2').trigger('keyup'); // revalidates
  623. thisInputElement.after('<div class="fieldhelp email-in-use">' + window.sL.emailAlreadyInUse + '</div>');
  624. }
  625. else {
  626. thisInputElement.removeClass('email-in-use');
  627. thisInputElement.siblings('.fieldhelp.email-in-use').remove();
  628. $('#signup-user-password2-step2').trigger('keyup');
  629. }
  630. });
  631. },1000);
  632. }
  633. else {
  634. $('.spinner-wrap').remove();
  635. }
  636. });
  637. // validate on keyup / paste / blur
  638. $('#popup-register input').on('keyup paste blur',function(){
  639. setTimeout(function () { // defer validation as after paste the content is not immediately available
  640. if(validateRegisterForm($('#popup-register'))
  641. && !$('#signup-user-nickname-step2').hasClass('nickname-taken')
  642. && !$('#signup-user-email-step2').hasClass('email-in-use')) {
  643. $('#signup-btn-step2').removeClass('disabled');
  644. }
  645. else {
  646. $('#signup-btn-step2').addClass('disabled');
  647. }
  648. }, 0);
  649. });
  650. $('#popup-register input').trigger('keyup');
  651. // submit on enter
  652. $('input#signup-user-name-step2,input#signup-user-email-step2,input#signup-user-password1-step2, input#signup-user-password2-step2').keyup(function(e){
  653. if(e.keyCode==13) {
  654. $('#signup-btn-step2').trigger('click');
  655. }
  656. });
  657. $('#signup-btn-step2').click(function(){
  658. if(!$(this).hasClass('disabled')) {
  659. $('#popup-register input,#popup-register button').addClass('disabled');
  660. display_spinner();
  661. $.ajax({ url: window.apiRoot + 'account/register.json',
  662. type: "POST",
  663. data: {
  664. nickname: $('#signup-user-nickname-step2').val(),
  665. email: $('#signup-user-email-step2').val(),
  666. fullname: $('#signup-user-name-step2').val(),
  667. homepage: $('#signup-user-homepage-step2').val(),
  668. bio: $('#signup-user-bio-step2').val(),
  669. location: $('#signup-user-location-step2').val(),
  670. password: $('#signup-user-password1-step2').val(),
  671. confirm: $('#signup-user-password2-step2').val(),
  672. cBS: window.cBS,
  673. cBSm: window.cBSm,
  674. username: 'none',
  675. },
  676. dataType:"json",
  677. error: function(data){
  678. if(typeof data.responseJSON != 'undefined' && typeof data.responseJSON.error != 'undefined') {
  679. remove_spinner();
  680. $('#popup-register input,#popup-register button').removeClass('disabled');
  681. $('#signup-user-password2-step2').trigger('keyup'); // revalidate
  682. showErrorMessage(data.responseJSON.error,$('#popup-register .modal-header'));
  683. }
  684. },
  685. success: function(data) {
  686. remove_spinner();
  687. if(typeof data.error == 'undefined') {
  688. $('input#nickname').val($('#signup-user-nickname-step2').val());
  689. $('input#password').val($('#signup-user-password1-step2').val());
  690. $('input#rememberme').prop('checked', true);
  691. $('#submit-login').trigger('click');
  692. $('#popup-register').remove();
  693. }
  694. else {
  695. alert('Try again! ' + data.error);
  696. $('#popup-register input,#popup-register button').removeClass('disabled');
  697. }
  698. }
  699. });
  700. }
  701. });
  702. // reactivate register form on popup close
  703. $('#popup-register').on('remove',function(){
  704. $('.front-signup input, .front-signup button').removeAttr('disabled');
  705. $('.front-signup input, .front-signup button').removeClass('disabled');
  706. });
  707. },7000);
  708. });
  709. // submit on enter
  710. $('input#signup-user-name,input#signup-user-email,input#signup-user-password').keyup(function(e){
  711. if(e.keyCode==13) {
  712. $('#signup-btn-step1').trigger('click');
  713. }
  714. });
  715. }
  716. /* ·
  717. ·
  718. · Show/hide Terms of Use
  719. ·
  720. · · · · · · · · · · · · · */
  721. $('body').on('click','#signup-terms-header',function(){
  722. if($('#signup-terms-container').text().length > 0) {
  723. $('#signup-terms-container').html('');
  724. }
  725. else {
  726. if(window.customTermsOfUse) {
  727. $('#signup-terms-container').html(window.customTermsOfUse);
  728. }
  729. else {
  730. getDoc('terms',function(termsHtml){
  731. $('#signup-terms-container').html(termsHtml);
  732. });
  733. }
  734. }
  735. });
  736. /* ·
  737. ·
  738. · set language, autologin or show welcome screen
  739. ·
  740. · · · · · · · · · · · · · */
  741. $('#submit-login').removeAttr('disabled'); // might be remebered by browser...
  742. $(window).load(function() {
  743. // set language, from local storage, else browser language
  744. var browserLang = navigator.language || navigator.userLanguage;
  745. // use english if browser has no language set
  746. if(typeof browserLang == 'undefined') {
  747. var browserLang = 'en-GB';
  748. }
  749. // browsers report e.g. sv-SE but we want it in the format "sv" or "sv_se"
  750. browserLang = browserLang.replace('-','_').toLowerCase();
  751. // if we're logged out, we see if there's a saved language in localstorage
  752. if(window.loggedIn === false) {
  753. var cacheDataLoggedOut = localStorageObjectCache_GET('selectedLanguage','logged_out');
  754. if(cacheDataLoggedOut) {
  755. window.selectedLanguage = cacheDataLoggedOut;
  756. }
  757. else {
  758. window.selectedLanguage = browserLang;
  759. }
  760. }
  761. // if we're logged in, we check
  762. // 1) check if the logged in user has selected a language
  763. // 2) check if there a selected language for logged_out users (i.e. the user selected the language before logging in)
  764. // 3) go with the browser language
  765. else {
  766. var cacheDataLoggedOut = localStorageObjectCache_GET('selectedLanguage','logged_out');
  767. var cacheDataLoggedIn = localStorageObjectCache_GET('selectedLanguage',window.loggedIn.id);
  768. if(cacheDataLoggedIn) {
  769. window.selectedLanguage = cacheDataLoggedIn;
  770. }
  771. else if(cacheDataLoggedOut) {
  772. window.selectedLanguage = cacheDataLoggedOut;
  773. }
  774. else {
  775. window.selectedLanguage = browserLang;
  776. }
  777. }
  778. // check that the language is available,
  779. if(typeof window.availableLanguages[window.selectedLanguage] == 'undefined') {
  780. var similarLanguageFound = false;
  781. // if not there might be a base language, e.g. "sv" instead of "sv_se"
  782. if(window.selectedLanguage.indexOf('_') > -1
  783. && typeof window.availableLanguages[window.selectedLanguage.substring(0,window.selectedLanguage.indexOf('_'))] != 'undefined') {
  784. window.selectedLanguage = window.selectedLanguage.substring(0,window.selectedLanguage.indexOf('_'));
  785. similarLanguageFound = true;
  786. }
  787. else {
  788. // there's also a chance there no base language, but a similar country specific language that we can use (rather than english)
  789. if(window.selectedLanguage.indexOf('_') > -1) {
  790. var baseLan = window.selectedLanguage.indexOf('_');
  791. }
  792. else {
  793. var baseLan = window.selectedLanguage;
  794. }
  795. $.each(window.availableLanguages, function(lanCode,lanData){
  796. if(lanCode.substring(0,lanCode.indexOf('_')) == baseLan) {
  797. window.selectedLanguage = lanCode;
  798. similarLanguageFound = true;
  799. return false;
  800. }
  801. });
  802. }
  803. // if we can't find a similar language, go with english
  804. if(similarLanguageFound === false) {
  805. window.selectedLanguage = 'en';
  806. }
  807. }
  808. // english is always available
  809. if(window.selectedLanguage == 'en') {
  810. proceedToSetLanguageAndLogin(window.englishLanguageData);
  811. }
  812. else {
  813. // if we already have this version of this language in localstorage, we
  814. // use that cached version. we do this because $.ajax doesn't respect caching, it seems
  815. var cacheData = localStorageObjectCache_GET('languageData',window.availableLanguages[window.selectedLanguage]);
  816. if(cacheData) {
  817. proceedToSetLanguageAndLogin(cacheData);
  818. }
  819. else {
  820. $.ajax({
  821. dataType: "json",
  822. url: window.fullUrlToThisQvitterApp + 'locale/' + window.availableLanguages[window.selectedLanguage],
  823. error: function(data){console.log(data)},
  824. success: function(data) {
  825. // store this response in localstorage
  826. localStorageObjectCache_STORE('languageData',window.availableLanguages[window.selectedLanguage], data);
  827. proceedToSetLanguageAndLogin(data);
  828. }
  829. });
  830. }
  831. }
  832. });
  833. // proceed to set language and login
  834. function proceedToSetLanguageAndLogin(data){
  835. window.sL = data;
  836. // plugins might have added translations
  837. $.each(window.pluginTranslations,function(k,pluginTranslation) {
  838. if(typeof pluginTranslation[window.selectedLanguage] != 'undefined') {
  839. $.extend(window.sL,pluginTranslation[window.selectedLanguage]);
  840. }
  841. else if(typeof pluginTranslation['en'] != 'undefined') {
  842. $.extend(window.sL,pluginTranslation['en']);
  843. }
  844. });
  845. // plugins might want to wait and do stuff until after language is set
  846. $(document).trigger('qvitterAfterLanguageIsSet');
  847. // if this is a RTL-language, add rtl class to body
  848. if(window.sL.directionality == 'rtl') {
  849. $('body').addClass('rtl');
  850. }
  851. window.siteTitle = $('head title').html(); // remember this for later use
  852. // replace placeholders in translation
  853. $.each(window.sL,function(k,v){
  854. window.sL[k] = v.replace(/{site-title}/g,window.siteTitle);
  855. });
  856. // suggest user to help translate if their browsers language does't exist
  857. if(typeof window.availableLanguages[window.usersLanguageCode] == 'undefined' && !localStorageObjectCache_GET('languageErrorMessageDiscarded',window.usersLanguageNameInEnglish)) { // but don't nag
  858. $('#page-container').prepend('<div class="language-error-message" data-language-name="' + window.usersLanguageNameInEnglish + '">' + window.siteTitle + ' is not availible in your language (' + replaceHtmlSpecialChars(window.usersLanguageNameInEnglish) + '). Visit <a href="https://git.gnu.io/h2p/Qvitter/tree/master/locale">Qvitter\'s repository homepage</a> if you want to help us to translate the interface. <span class="discard-error-message"></span></div>');
  859. }
  860. // if selected language is too similar to english, we display a message telling people to help with the translation
  861. if(window.sL.languageName != 'English') {
  862. var numberOfStringsSameAsEnglish = 0;
  863. var totalStrings = 0;
  864. $.each(window.sL,function(k,v){
  865. if(v == window.englishLanguageData[k]) {
  866. numberOfStringsSameAsEnglish++;
  867. }
  868. totalStrings++;
  869. });
  870. numberOfStringsSameAsEnglish = Math.max(0, numberOfStringsSameAsEnglish-20); totalStrings = Math.max(0, totalStrings-20); // ~20 strings, e.g. shortened months, is often same in many languages
  871. var percentTranslated = parseInt((1-(numberOfStringsSameAsEnglish/totalStrings))*100,10);
  872. if(percentTranslated < 95) {
  873. if(!localStorageObjectCache_GET('languageErrorMessageDiscarded',window.sL.languageName)) { // but don't nag
  874. $('#page-container').prepend('<div class="language-error-message" data-language-name="' + window.sL.languageName + '">' + window.sL.onlyPartlyTranslated.replace('{percent}',percentTranslated).replace('{language-name}',window.sL.languageName) + '<span class="discard-error-message"></span></div>');
  875. }
  876. }
  877. }
  878. // set some static strings
  879. if(window.customWelcomeText !== false && typeof window.customWelcomeText[window.selectedLanguage] != 'undefined') {
  880. $('.front-welcome-text').html(window.customWelcomeText[window.selectedLanguage]);
  881. // collapse long welcome texts and add expand button
  882. if($('.front-welcome-text').outerHeight()>250) {
  883. $('.front-welcome-text').css('height','240px');
  884. $('.front-welcome-text').css('overflow', 'hidden');
  885. $('.front-welcome-text').append('<div class="show-full-welcome-text"></div>');
  886. }
  887. }
  888. else {
  889. $('.front-welcome-text').html('<h1>' + window.sL.welcomeHeading + '</h1>');
  890. if(window.enableWelcomeText) {
  891. $('.front-welcome-text').append(window.sL.welcomeText);
  892. }
  893. }
  894. $('#nickname').attr('placeholder',window.sL.loginUsername);
  895. $('#password').attr('placeholder',window.sL.loginPassword);
  896. $('button#submit-login').html(window.sL.loginSignIn);
  897. $('#rememberme_label').html(window.sL.loginRememberMe);
  898. $('#forgot-password').html(window.sL.loginForgotPassword);
  899. $('.front-signup h2').html('<strong>' + window.sL.newToQuitter + '</strong> ' + window.sL.signUp);
  900. $('#signup-user-name').attr('placeholder',window.sL.signUpFullName);
  901. $('#signup-user-email').attr('placeholder',window.sL.signUpEmail);
  902. $('#signup-user-password').attr('placeholder',window.sL.loginPassword);
  903. $('.front-signup button.signup-btn').html(window.sL.signUpButtonText);
  904. $('#user-queets .label').html(window.sL.notices);
  905. $('#user-following .label').html(window.sL.following);
  906. $('#user-followers .label').html(window.sL.followers);
  907. $('#user-groups .label').html(window.sL.groups);
  908. $('#queet-box').html(window.sL.compose);
  909. $('#queet-box').attr('data-start-text',encodeURIComponent(window.sL.compose));
  910. $('#user-footer .queet-button button').html(window.sL.queetVerb);
  911. $('#stream-header').html(window.sL.queetsNounPlural);
  912. $('#logout').html(window.sL.logout);
  913. $('#settings').html(window.sL.settings);
  914. $('#other-servers-link').html(window.sL.otherServers);
  915. $('.language-dropdown .dropdown-toggle small').html(window.sL.languageSelected);
  916. $('.language-dropdown .current-language').html(window.sL.languageName);
  917. $('.stream-selection.friends-timeline').prepend(window.sL.timeline);
  918. $('.stream-selection.mentions').prepend(window.sL.mentions);
  919. $('.stream-selection.notifications').prepend(window.sL.notifications);
  920. $('.stream-selection.favorites').prepend(window.sL.favoritesNoun);
  921. $('.stream-selection.public-timeline').prepend(window.sL.publicTimeline);
  922. $('.stream-selection.public-and-external-timeline').prepend(window.sL.publicAndExtTimeline)
  923. $('#search-query').attr('placeholder',window.sL.searchVerb);
  924. $('#blocking-link').html(window.sL.userBlocks);
  925. $('#faq-link').html(window.sL.FAQ);
  926. $('#tou-link').html(window.sL.showTerms);
  927. $('#add-edit-language-link').html(window.sL.addEditLanguageLink);
  928. $('#shortcuts-link').html(window.sL.keyboardShortcuts);
  929. $('#invite-link').html(window.sL.inviteAFriend);
  930. $('#classic-link').html(window.sL.classicInterface);
  931. $('#edit-profile-header-link').html(window.sL.editMyProfile);
  932. $('#mini-logged-in-user-cog-wheel').attr('data-tooltip',window.sL.profileSettings);
  933. $('#accessibility-toggle-link').html(window.sL.accessibilityToggleLink);
  934. $('#settingslink .nav-session').attr('data-tooltip',window.sL.profileAndSettings);
  935. $('#top-compose').attr('data-tooltip',window.sL.compose);
  936. $('button.upload-image').attr('data-tooltip',window.sL.tooltipAttachFile);
  937. $('button.shorten').attr('data-tooltip',window.sL.tooltipShortenUrls);
  938. $('.reload-stream').attr('data-tooltip',window.sL.tooltipReloadStream);
  939. $('#clear-history').html(window.sL.clearHistory);
  940. $('#user-screen-name, #user-avatar, #user-name').attr('data-tooltip', window.sL.viewMyProfilePage);
  941. $('#top-menu-profile-link-view-profile').html(window.sL.viewMyProfilePage);
  942. $('#find-someone input').attr('placeholder',window.sL.findSomeone);
  943. $('#find-someone input').attr('data-tooltip',window.sL.findSomeoneTooltip);
  944. // show site body now
  945. $('#user-container').css('display','block');
  946. $('#feed').css('display','block');
  947. // logged in or not?
  948. if(window.loggedIn) {
  949. proceedLoggedIn();
  950. }
  951. else {
  952. proceedLoggedOut();
  953. }
  954. }
  955. /* ·
  956. ·
  957. · Stuff we do on load specifically for logged out users
  958. ·
  959. · · · · · · · · · · · · · */
  960. function proceedLoggedOut() {
  961. display_spinner();
  962. setNewCurrentStream(getStreamFromUrl(),true,false,function(){
  963. // $('input#nickname').focus(); --> maybe not a good idea on mobile?
  964. $('#page-container').css('opacity','1');
  965. });
  966. }
  967. /* ·
  968. ·
  969. · Stuff we do on load specifically for logged in users
  970. ·
  971. · · · · · · · · · · · · · */
  972. function proceedLoggedIn() {
  973. display_spinner();
  974. // get everyone we follow, block and our memberships and stor in global objects
  975. getAllFollowsMembershipsAndBlocks(function(){
  976. // do this now not to stall slow computers, also we know of group memberships to highlight now
  977. cacheSyntaxHighlighting();
  978. // we might have cached text for the queet box
  979. // (we need to get the mentions suggestions and cache the syntax highlighting before doing this)
  980. $('#queet-box').attr('data-cached-text',encodeURIComponent(localStorageObjectCache_GET('queetBoxInput','queet-box')));
  981. maybePrefillQueetBoxWithCachedText($('#queet-box'));
  982. });
  983. // load history
  984. loadHistoryFromLocalStorage();
  985. // show bookmarks
  986. appendAllBookmarks(window.qvitterProfilePrefs.bookmarks);
  987. // set stream
  988. setNewCurrentStream(getStreamFromUrl(),true,false,function(){
  989. $('.language-dropdown').css('display','none');
  990. $('#page-container').css('opacity','1');
  991. $('#search').fadeIn('slow');
  992. $('#settingslink .dropdown-toggle').fadeIn('slow');
  993. $('#top-compose').fadeIn('slow');
  994. $('input#nickname').blur();
  995. });
  996. }
  997. /* ·
  998. ·
  999. · Shake the login box, i.e. when indicating an error
  1000. ·
  1001. · · · · · · · · · · · · · */
  1002. function shakeLoginBox() {
  1003. $('input#nickname').css('background-color','pink');
  1004. $('input#password').css('background-color','pink');
  1005. $('#login-content').effect('shake',{distance:5,times:2},function(){
  1006. $('input#nickname').animate({backgroundColor:'#fff'},1000);
  1007. $('input#password').animate({backgroundColor:'#fff'},1000);
  1008. });
  1009. }
  1010. /* ·
  1011. ·
  1012. · In the login form, we want to check the remember-me-checkbox when its label is clicked
  1013. ·
  1014. · · · · · · · · · · · · · */
  1015. $('#rememberme_label').click(function(){
  1016. if($('#rememberme').prop('checked')) {
  1017. $('#rememberme').prop('checked', false);
  1018. }
  1019. else {
  1020. $('#rememberme').prop('checked', true);
  1021. }
  1022. });
  1023. $('#rememberme_label').disableSelection();
  1024. /* ·
  1025. ·
  1026. · Submit login form
  1027. ·
  1028. · · · · · · · · · · · · · */
  1029. $('#form_login').submit(function(e) {
  1030. if(typeof window.loginCheckSuccessful == 'undefined') {
  1031. e.preventDefault();
  1032. checkLogin($('input#nickname').val(),$('input#password').val(),function(data){
  1033. window.loginCheckSuccessful = true;
  1034. $('#form_login').submit();
  1035. });
  1036. }
  1037. });
  1038. /* ·
  1039. ·
  1040. · Logout
  1041. ·
  1042. · · · · · · · · · · · · · */
  1043. $('#logout').click(function(){
  1044. window.location.href =window.siteInstanceURL + 'main/logout';
  1045. });
  1046. /* ·
  1047. ·
  1048. · FAQ
  1049. ·
  1050. · · · · · · · · · · · · · */
  1051. $('#faq-link').click(function(){
  1052. popUpAction('popup-faq', window.siteTitle + ' ' + window.sL.FAQ,'<div id="faq-container"></div>',false);
  1053. getDoc('faq',function(faqHtml){
  1054. $('#faq-container').html(faqHtml);
  1055. });
  1056. });
  1057. /* ·
  1058. ·
  1059. · Terms
  1060. ·
  1061. · · · · · · · · · · · · · */
  1062. $('#tou-link,.tou-link').click(function(){
  1063. popUpAction('popup-terms', window.sL.showTerms,'<div id="terms-container"></div>',false);
  1064. if(window.customTermsOfUse) {
  1065. $('#terms-container').html(window.customTermsOfUse);
  1066. }
  1067. else {
  1068. getDoc('terms',function(termsHtml){
  1069. $('#terms-container').html(termsHtml);
  1070. });
  1071. }
  1072. });
  1073. /* ·
  1074. ·
  1075. · Classic Link, toggle setting in api and redirect to /all
  1076. ·
  1077. · · · · · · · · · · · · · */
  1078. $('#classic-link, #accessibility-toggle-link').click(function(){
  1079. getFromAPI('qvitter/toggle_qvitter.json',function(data){
  1080. if(data.success === true) {
  1081. window.location.href = window.siteInstanceURL + window.loggedIn.screen_name + '/all';
  1082. }
  1083. });
  1084. });
  1085. /* ·
  1086. ·
  1087. · Handling the language dropdown selection
  1088. ·
  1089. · · · · · · · · · · · · · */
  1090. $('.dropdown').click(function(){$(this).toggleClass('dropped')});
  1091. $('.dropdown').disableSelection();
  1092. $(document).bind('click', function (e) {
  1093. if(!$(e.target).is('#logo') && !$(e.target).is('#settingslink') && !$(e.target).is('.nav-session') && !$(e.target).is('.dropdown-toggle') && !$(e.target).is('.dropdown-toggle small') && !$(e.target).is('.dropdown-toggle span') && !$(e.target).is('.dropdown-toggle b')) {
  1094. $('.dropdown').removeClass('dropped');
  1095. $('.quitter-settings.dropdown-menu').removeClass('dropped');
  1096. }
  1097. });
  1098. $('.language-link').click(function(){
  1099. if(window.loggedIn === false) {
  1100. var selectedForUser = 'logged_out';
  1101. }
  1102. else {
  1103. var selectedForUser = window.loggedIn.id;
  1104. }
  1105. localStorageObjectCache_STORE('selectedLanguage',selectedForUser, $(this).attr('data-lang-code'));
  1106. location.reload(); // reload
  1107. });
  1108. /* ·
  1109. ·
  1110. · Show the logo menu dropdown on click
  1111. ·
  1112. · · · · · · · · · · · · · */
  1113. $('#settingslink').click(function(){
  1114. removeAllTooltips();
  1115. if(!$('.quitter-settings').hasClass('dropped')) { $('.quitter-settings').addClass('dropped'); }
  1116. else { $('.quitter-settings').removeClass('dropped'); }
  1117. });
  1118. /* ·
  1119. ·
  1120. · Show/hide the user menu dropdown on click
  1121. ·
  1122. · · · · · · · · · · · · · */
  1123. $('body').on('click','.user-menu-cog',function(e){
  1124. if(!$(e.target).is($(this))) {
  1125. // don't show/hide when clicking inside the menu
  1126. }
  1127. // hide
  1128. else if($(this).hasClass('dropped')) {
  1129. $(this).removeClass('dropped');
  1130. $(this).children('.dropdown-menu').remove();
  1131. }
  1132. // show
  1133. else {
  1134. $(this).addClass('dropped');
  1135. var userID = $(this).attr('data-user-id');
  1136. var userScreenName = $(this).attr('data-screen-name');
  1137. var silenced = $(this).hasClass('silenced');
  1138. var sandboxed = $(this).hasClass('sandboxed');
  1139. // menu
  1140. var menuArray = [];
  1141. // settings etc if it's me
  1142. if(userID == window.loggedIn.id) {
  1143. menuArray = loggedInUsersMenuArray();
  1144. }
  1145. // block etc if it not me
  1146. else {
  1147. if(userIsBlocked(userID)) {
  1148. menuArray.push({
  1149. type: 'function',
  1150. functionName: 'unblockUser',
  1151. functionArguments: {
  1152. userId: userID
  1153. },
  1154. label: window.sL.unblockUser.replace('{username}','@' + userScreenName)
  1155. });
  1156. }
  1157. else {
  1158. menuArray.push({
  1159. type: 'function',
  1160. functionName: 'blockUser',
  1161. functionArguments: {
  1162. userId: userID
  1163. },
  1164. label: window.sL.blockUser.replace('{username}','@' + userScreenName)
  1165. });
  1166. }
  1167. // mute profile pref
  1168. menuArray.push({
  1169. type: 'profile-prefs-toggle',
  1170. namespace: 'qvitter',
  1171. topic: 'mute:' + userID,
  1172. label: window.sL.muteUser,
  1173. enabledLabel: window.sL.unmuteUser,
  1174. tickDisabled: true,
  1175. callback: 'hideOrShowNoticesAfterMuteOrUnmute'
  1176. });
  1177. // moderator actions
  1178. menuArray = appendModeratorUserActionsToMenuArray(menuArray,userID,userScreenName,sandboxed,silenced);
  1179. }
  1180. var menu = $(getMenu(menuArray)).appendTo(this);
  1181. alignMenuToParent(menu,$(this));
  1182. }
  1183. });
  1184. // hide the stream menu when clicking outside it
  1185. $('body').on('click',function(e){
  1186. if(!$(e.target).is('.user-menu-cog') && $('.user-menu-cog').hasClass('dropped') && !$(e.target).closest('.user-menu-cog').length>0) {
  1187. $('.user-menu-cog').children('.dropdown-menu').remove();
  1188. $('.user-menu-cog').removeClass('dropped');
  1189. }
  1190. });
  1191. /* ·
  1192. ·
  1193. · Show/hide the stream menu dropdown on click
  1194. ·
  1195. · · · · · · · · · · · · · */
  1196. $('body').on('click','#stream-menu-cog',function(e){
  1197. if(!$(e.target).is('#stream-menu-cog') && $(e.target).closest('#stream-menu-cog').length>0) {
  1198. // don't show/hide when clicking inside the menu
  1199. }
  1200. // hide
  1201. else if($(this).hasClass('dropped')) {
  1202. $(this).removeClass('dropped');
  1203. $(this).children('.dropdown-menu').remove();
  1204. }
  1205. // show
  1206. else {
  1207. $(this).addClass('dropped');
  1208. var menu = $(streamObjectGetMenu(window.currentStreamObject)).appendTo(this);
  1209. alignMenuToParent(menu,$(this));
  1210. }
  1211. });
  1212. // hide the stream menu when clicking outside it
  1213. $('body').on('click',function(e){
  1214. if(!$(e.target).is('#stream-menu-cog') && $('#stream-menu-cog').hasClass('dropped') && !$(e.target).closest('#stream-menu-cog').length>0) {
  1215. $('#stream-menu-cog').children('.dropdown-menu').remove();
  1216. $('#stream-menu-cog').removeClass('dropped');
  1217. }
  1218. });
  1219. /* ·
  1220. ·
  1221. · Open a queet menu when clicking ellipsis button
  1222. ·
  1223. · · · · · · · · · · · · · */
  1224. $('body').on('click','.sm-ellipsis',function(e){
  1225. if(!$(e.target).is('.sm-ellipsis') && $(e.target).closest('.sm-ellipsis').length>0) {
  1226. // don't show/hide when clicking inside the menu
  1227. }
  1228. // hide
  1229. else if($(this).hasClass('dropped')) {
  1230. $(this).removeClass('dropped');
  1231. $(this).children('.dropdown-menu').remove();
  1232. }
  1233. // show
  1234. else {
  1235. $(this).addClass('dropped');
  1236. var closestStreamItem = $(this).closest('.queet').parent('.stream-item');
  1237. var streamItemUsername = closestStreamItem.attr('data-user-screen-name');
  1238. var streamItemUserID = closestStreamItem.attr('data-user-id');
  1239. var streamItemID = closestStreamItem.attr('data-quitter-id');
  1240. var streamItemUserSandboxed = closestStreamItem.hasClass('sandboxed');
  1241. var streamItemUserSilenced = closestStreamItem.hasClass('silenced');
  1242. // menu
  1243. var menuArray = [];
  1244. // delete my notice, or others notices for mods with rights
  1245. if(streamItemUserID == window.loggedIn.id || window.loggedIn.rights.delete_others_notice === true) {
  1246. menuArray.push({
  1247. type: 'function',
  1248. functionName: 'deleteQueet',
  1249. functionArguments: {
  1250. streamItemID: streamItemID
  1251. },
  1252. label: window.sL.deleteVerb
  1253. });
  1254. }
  1255. // block/unblock if it's not me
  1256. if(streamItemUserID != window.loggedIn.id) {
  1257. if(userIsBlocked(streamItemUserID)) {
  1258. menuArray.push({
  1259. type: 'function',
  1260. functionName: 'unblockUser',
  1261. functionArguments: {
  1262. userId: streamItemUserID
  1263. },
  1264. label: window.sL.unblockUser.replace('{username}',streamItemUsername)
  1265. });
  1266. }
  1267. else {
  1268. menuArray.push({
  1269. type: 'function',
  1270. functionName: 'blockUser',
  1271. functionArguments: {
  1272. userId: streamItemUserID
  1273. },
  1274. label: window.sL.blockUser.replace('{username}',streamItemUsername)
  1275. });
  1276. }
  1277. // mute profile pref
  1278. menuArray.push({
  1279. type: 'profile-prefs-toggle',
  1280. namespace: 'qvitter',
  1281. topic: 'mute:' + streamItemUserID,
  1282. label: window.sL.muteUser,
  1283. enabledLabel: window.sL.unmuteUser,
  1284. tickDisabled: true,
  1285. callback: 'hideOrShowNoticesAfterMuteOrUnmute'
  1286. });
  1287. }
  1288. // moderator actions
  1289. menuArray = appendModeratorUserActionsToMenuArray(menuArray,streamItemUserID,streamItemUsername,streamItemUserSandboxed,streamItemUserSilenced);
  1290. // add menu to DOM and align it
  1291. var menu = $(getMenu(menuArray)).appendTo(this);
  1292. alignMenuToParent(menu,$(this));
  1293. }
  1294. });
  1295. // remove the ellipsis menu when clicking outside it
  1296. $('body').on('click',function(e){
  1297. if(!$(e.target).is('.sm-ellipsis') && $('.sm-ellipsis.dropped').length>0 && !$(e.target).closest('.sm-ellipsis').length>0) {
  1298. $('.sm-ellipsis').children('.dropdown-menu').remove();
  1299. $('.sm-ellipsis').removeClass('dropped');
  1300. }
  1301. });
  1302. /* ·
  1303. ·
  1304. · When clicking a function row in a stream menu – invoke the function
  1305. ·
  1306. · · · · · · · · · · · · · */
  1307. $('body').on('click','.row-type-function',function(e){
  1308. var thisFunctionRow = $(this);
  1309. // don't invoke the function again if it's not finished last time
  1310. if(thisFunctionRow.hasClass('clicked')) {
  1311. return true;
  1312. }
  1313. thisFunctionRow.addClass('clicked');
  1314. var functionName = $(this).attr('data-function-name');
  1315. if(typeof $(this).attr('data-function-arguments') == 'undefined' || $(this).attr('data-function-arguments') == 'undefined') {
  1316. var functionArguments = false;
  1317. }
  1318. else {
  1319. var functionArguments = JSON.parse($(this).attr('data-function-arguments'));
  1320. }
  1321. window[functionName](functionArguments, function(success){
  1322. if(success) {
  1323. thisFunctionRow.removeClass('clicked');
  1324. }
  1325. });
  1326. });
  1327. /* ·
  1328. ·
  1329. · When toggeling a a profile pref in a dropdown menu
  1330. ·
  1331. · · · · · · · · · · · · · */
  1332. $('body').on('click','.row-type-profile-prefs-toggle',function(e){
  1333. var thisToggle = $(this);
  1334. // wait for last toggle to finish before toggeling again
  1335. if(thisToggle.hasClass('clicked')) {
  1336. return true;
  1337. }
  1338. if(thisToggle.attr('data-profile-pref-state') == 'disabled') {
  1339. var prefDataToSet = '1';
  1340. }
  1341. else if(thisToggle.attr('data-profile-pref-state') == 'enabled') {
  1342. var prefDataToSet = '0';
  1343. }
  1344. else { // invalid
  1345. return true;
  1346. }
  1347. thisToggle.addClass('clicked');
  1348. var prefNamespace = thisToggle.attr('data-profile-prefs-namespace');
  1349. var prefTopic = thisToggle.attr('data-profile-prefs-topic');
  1350. var prefLabel = thisToggle.attr('data-profile-prefs-label');
  1351. var prefEnabledLabel = thisToggle.attr('data-profile-prefs-enabled-label');
  1352. // only prefs in the 'qvitter' namespace allowed
  1353. if(prefNamespace != 'qvitter') {
  1354. return true;
  1355. }
  1356. // save pref to server
  1357. postSetProfilePref(prefNamespace,prefTopic,prefDataToSet,function(data){
  1358. if(data === false) { // error
  1359. showErrorMessage(window.sL.ERRORfailedSavingYourSetting + ' (' + prefTopic + ')');
  1360. }
  1361. else { // success
  1362. thisToggle.removeClass('clicked');
  1363. if(thisToggle.attr('data-profile-pref-state') == 'disabled') {
  1364. thisToggle.removeClass('disabled');
  1365. thisToggle.addClass('enabled');
  1366. thisToggle.attr('data-profile-pref-state','enabled');
  1367. if(prefEnabledLabel != 'undefined') {
  1368. thisToggle.html(prefEnabledLabel);
  1369. }
  1370. window.qvitterProfilePrefs[prefTopic] = '1';
  1371. }
  1372. else if(thisToggle.attr('data-profile-pref-state') == 'enabled') {
  1373. thisToggle.removeClass('enabled');
  1374. thisToggle.addClass('disabled');
  1375. thisToggle.attr('data-profile-pref-state','disabled');
  1376. if(prefEnabledLabel != 'undefined') {
  1377. thisToggle.html(prefLabel);
  1378. }
  1379. window.qvitterProfilePrefs[prefTopic] = '0';
  1380. }
  1381. // run callback
  1382. if(typeof thisToggle.attr('data-profile-pref-callback') != 'undefined'
  1383. && thisToggle.attr('data-profile-pref-state') != 'undefined'
  1384. && typeof window[thisToggle.attr('data-profile-pref-callback')] == 'function') {
  1385. window[thisToggle.attr('data-profile-pref-callback')]();
  1386. }
  1387. }
  1388. });
  1389. });
  1390. /* ·
  1391. ·
  1392. · Mark all notifications as seen
  1393. ·
  1394. · · · · · · · · · · · · · */
  1395. function markAllNotificationsAsSeen(arg,callback) {
  1396. display_spinner();
  1397. getFromAPI('qvitter/mark_all_notifications_as_seen.json',function(data){
  1398. if(data === false) {
  1399. showErrorMessage(window.sL.ERRORfailedMarkingAllNotificationsAsRead);
  1400. callback(true);
  1401. }
  1402. else {
  1403. helloAPI(function(){
  1404. $('.stream-item').removeClass('not-seen');
  1405. $('#new-queets-bar').trigger('click'); // show any hidden notifications (this will also remove the dropdown menu)
  1406. remove_spinner();
  1407. callback(true);
  1408. });
  1409. }
  1410. });
  1411. }
  1412. /* ·
  1413. ·
  1414. · Show or hide embedded content in timeline?
  1415. ·
  1416. · · · · · · · · · · · · · */
  1417. function showOrHideEmbeddedContentInTimelineFromProfilePref() {
  1418. if(typeof window.qvitterProfilePrefs['hide_embedded_in_timeline:' + window.currentStreamObject.path] != 'undefined') {
  1419. var showHide = window.qvitterProfilePrefs['hide_embedded_in_timeline:' + window.currentStreamObject.path];
  1420. if(parseInt(showHide,10) == 1) {
  1421. $('#feed-body').addClass('embedded-content-hidden-by-user');
  1422. return;
  1423. }
  1424. }
  1425. $('#feed-body').removeClass('embedded-content-hidden-by-user');
  1426. }
  1427. /* ·
  1428. ·
  1429. · Show or hide quotes in timeline?
  1430. ·
  1431. · · · · · · · · · · · · · */
  1432. function showOrHideQuotesInTimelineFromProfilePref() {
  1433. if(typeof window.qvitterProfilePrefs['hide_quotes_in_timeline:' + window.currentStreamObject.path] != 'undefined') {
  1434. var showHide = window.qvitterProfilePrefs['hide_quotes_in_timeline:' + window.currentStreamObject.path];
  1435. if(parseInt(showHide,10) == 1) {
  1436. $('#feed-body').addClass('quotes-hidden-by-user');
  1437. return;
  1438. }
  1439. }
  1440. $('#feed-body').removeClass('quotes-hidden-by-user');
  1441. }
  1442. /* ·
  1443. ·
  1444. · Show or hide notices from muted users in notifications?
  1445. ·
  1446. · · · · · · · · · · · · · */
  1447. function showOrHideNoticesFromMutedUsersInNotifications() {
  1448. if(typeof window.qvitterProfilePrefs['hide_notifications_from_muted_users'] != 'undefined') {
  1449. var showHide = window.qvitterProfilePrefs['hide_notifications_from_muted_users'];
  1450. if(parseInt(showHide,10) == 1) {
  1451. $('#feed-body').addClass('hide-notifications-from-muted-users');
  1452. return;
  1453. }
  1454. }
  1455. $('#feed-body').removeClass('hide-notifications-from-muted-users')
  1456. }
  1457. /* ·
  1458. ·
  1459. · When clicking an external follow button
  1460. ·
  1461. · · · · · · · · · · · · · */
  1462. $('body').on('click','.external-follow-button',function(event){
  1463. popUpAction('popup-external-follow', window.sL.userExternalFollow + ' ' + $('.profile-card-inner .screen-name').html(),'<form method="post" action="' + window.siteInstanceURL + 'main/ostatus"><input type="hidden" id="nickname" name="nickname" value="' + $('.profile-card-inner .screen-name').html().substring(1) + '"><input type="text" id="profile" name="profile" placeholder="' + window.sL.userExternalFollowHelp + '" /></form>','<div class="right"><button class="close">' + window.sL.cancelVerb + '</button><button class="primary">' + window.sL.userExternalFollow + '</button></div>');
  1464. $('#popup-external-follow form input#profile').focus();
  1465. $('#popup-external-follow button.primary').click(function(){
  1466. $('#popup-external-follow form').submit();
  1467. });
  1468. });
  1469. /* ·
  1470. ·
  1471. · When clicking an external join button
  1472. ·
  1473. · · · · · · · · · · · · · */
  1474. $('body').on('click','.external-member-button',function(event){
  1475. popUpAction('popup-external-join', window.sL.joinExternalGroup + ' ' + $('.profile-card-inner .screen-name').html(),'<form method="post" action="' + window.siteInstanceURL + 'main/ostatus"><input type="hidden" id="group" name="group" value="' + $('.profile-card-inner .screen-name').html().substring(1) + '"><input type="text" id="profile" name="profile" placeholder="' + window.sL.userExternalFollowHelp + '" /></form>','<div class="right"><button class="close">' + window.sL.cancelVerb + '</button><button class="primary">' + window.sL.userExternalFollow + '</button></div>');
  1476. $('#popup-external-join form input#profile').focus();
  1477. $('#popup-external-join button.primary').click(function(){
  1478. $('#popup-external-join form').submit();
  1479. });
  1480. });
  1481. /* ·
  1482. ·
  1483. · When clicking a follow/block button
  1484. ·
  1485. · · · · · · · · · · · · · */
  1486. $('body').on('click','.qvitter-follow-button',function(event){
  1487. if($(this).hasClass('disabled')) {
  1488. return true;
  1489. }
  1490. $(this).addClass('disabled');
  1491. var user_id = $(this).attr('data-follow-user-id');
  1492. // if we have a local id, it's straightforward, but we could be handling an unfollow
  1493. if(typeof user_id != 'undefined') {
  1494. // unblock?
  1495. if($(this).hasClass('blocking')) {
  1496. unblockUser({userId: user_id, blockButton_jQueryElement: $(this)});
  1497. return true;
  1498. }
  1499. // follow or unfollow?
  1500. if($(this).hasClass('following')) {
  1501. var followOrUnfollow = 'unfollow';
  1502. }
  1503. else {
  1504. var followOrUnfollow = 'follow';
  1505. }
  1506. // post to api
  1507. display_spinner();
  1508. APIFollowOrUnfollowUser(followOrUnfollow,user_id, this, function(data,this_element) {
  1509. remove_spinner();
  1510. $(this_element).removeClass('disabled');
  1511. if(data) {
  1512. if(data.following) {
  1513. $(this_element).addClass('following');
  1514. $('#user-following strong').html(parseInt($('#user-following strong').html(),10)+1);
  1515. appendUserToMentionsSuggestionsArray(data);
  1516. }
  1517. else {
  1518. $(this_element).removeClass('following');
  1519. $('#user-following strong').html(parseInt($('#user-following strong').html(),10)-1);
  1520. }
  1521. }
  1522. });
  1523. return true;
  1524. }
  1525. // if there's no local user id, we have to take a detour
  1526. $.ajax({ url: window.siteInstanceURL + 'main/ostatussub',
  1527. type: "POST",
  1528. data: {
  1529. token: window.commonSessionToken,
  1530. profile: $(this).attr('data-follow-user'),
  1531. submit: 'Confirm'
  1532. },
  1533. error: function(data){ console.log('error'); console.log(data); },
  1534. success: function(data) {
  1535. // reload page on success
  1536. // since ostatussub doesn't have an api, there's no good way to get the local user id here,
  1537. // and change the button to an unsubscribe button.
  1538. window.location.replace(window.siteInstanceURL + window.loggedIn.screen_name + '/subscriptions');
  1539. }
  1540. });
  1541. });
  1542. /* ·
  1543. ·
  1544. · When clicking a join group button
  1545. ·
  1546. · · · · · · · · · · · · · */
  1547. $('body').on('click','.member-button',function(event){
  1548. if(!$(this).hasClass('disabled')) {
  1549. $(this).addClass('disabled');
  1550. // get group id
  1551. var group_id = $(this).attr('data-group-id');
  1552. // join or leave?
  1553. if($(this).hasClass('member')) {
  1554. var joinOrLeave = 'leave';
  1555. }
  1556. else {
  1557. var joinOrLeave = 'join';
  1558. }
  1559. // post to api
  1560. display_spinner();
  1561. APIJoinOrLeaveGroup(joinOrLeave,group_id, this, function(data,this_element) {
  1562. remove_spinner();
  1563. $(this_element).removeClass('disabled');
  1564. if(data) {
  1565. if(data.member) {
  1566. $(this_element).addClass('member');
  1567. $('.profile-card .member-stats strong').html(parseInt($('.profile-card .member-stats strong').html(),10)+1);
  1568. $('#user-groups strong').html(parseInt($('#user-groups strong').html(),10)+1);
  1569. // add group to mention suggestion array
  1570. if(data.homepage_logo === null) {
  1571. data.homepage_logo = window.defaultAvatarStreamSize;
  1572. }
  1573. var groupmembershipObject = { avatar:data.homepage_logo, id:data.id, name:data.fullname, url:data.url, username:data.nickname };
  1574. window.groupMemberships.push(groupmembershipObject);
  1575. }
  1576. else if(data.member === false) {
  1577. $(this_element).removeClass('member');
  1578. $('.profile-card .member-stats strong').html(parseInt($('.profile-card .member-stats strong').html(),10)-1);
  1579. $('#user-groups strong').html(parseInt($('#user-groups strong').html(),10)-1);
  1580. // remove group from mention suggestion array, if it's there
  1581. var groupToRemove = false;
  1582. $.each(window.groupMemberships,function(k,v) {
  1583. if(v.id == data.id) {
  1584. groupToRemove = k;
  1585. }
  1586. });
  1587. if(groupToRemove) {
  1588. console.log('remove at' + groupToRemove);
  1589. window.groupMemberships.splice(groupToRemove,1);
  1590. }
  1591. }
  1592. }
  1593. });
  1594. }
  1595. });
  1596. /* ·
  1597. ·
  1598. · Go to profile page when clicking the small user header in left column
  1599. ·
  1600. · · · · · · · · · · · · · */
  1601. $('#user-header').on('click',function(e){
  1602. // not if we're clicking the mini-logged-in-user-cog-wheel
  1603. if($(e.target).is('#mini-logged-in-user-cog-wheel')) {
  1604. return;
  1605. }
  1606. setNewCurrentStream(pathToStreamRouter(window.loggedIn.screen_name),true,false);
  1607. });
  1608. /* ·
  1609. ·
  1610. · Searching
  1611. ·
  1612. · · · · · · · · · · · · · */
  1613. $('#search-query').on('keyup',function(e) { if(e.keyCode==13) { showSearchStream(); }}); // on enter in input field
  1614. $('button.icon.nav-search').on('click',function(e) { showSearchStream();}); // on click on magnifying glass
  1615. function showSearchStream() {
  1616. var path = 'search/notice?q=' + encodeURIComponent(replaceHtmlSpecialChars($('#search-query').val()));
  1617. setNewCurrentStream(pathToStreamRouter(path),true,false);
  1618. }
  1619. /* ·
  1620. · <o
  1621. · Hijack all links and look for local users, tags, searches and groups. (//
  1622. ·
  1623. · If found, select that stream and prevent links default behaviour
  1624. ·
  1625. · · · · · · · · · · · · · */
  1626. $('body').on('click','a', function(e) {
  1627. // don't hijack if metakeys are pressed!
  1628. if(e.ctrlKey || e.altKey || e.shiftKey || e.metaKey) {
  1629. return;
  1630. }
  1631. // don't hijack if this is an anchor link in the faq
  1632. if($(this).closest('.modal-container').attr('id') == 'popup-faq' && $(this).attr('href').indexOf('#') > -1) {
  1633. return;
  1634. }
  1635. // don't hijack and don't follow link if this is the bookmark stream icon, prevent link but don't set a new currentstream
  1636. if($(e.target).is('i.chev-right')) {
  1637. e.preventDefault();
  1638. return;
  1639. }
  1640. // don't hijack links with donthijack attribute
  1641. if(!!$(this).attr('donthijack') || $(this).attr('donthijack') == '') {
  1642. return;
  1643. }
  1644. // all links opens in new tab
  1645. $(this).attr('target','_blank');
  1646. // only proceed if we really have a href attribute
  1647. if(typeof $(this).attr('href') == 'undefined') {
  1648. return;
  1649. }
  1650. // profile picture
  1651. if ($(this).hasClass('profile-picture')) {
  1652. e.preventDefault();
  1653. if($(this).closest('.modal-container').attr('id') != 'edit-profile-popup') { // no popup if we're editing our profile
  1654. popUpAction('popup-profile-picture', $('.profile-card-inner .screen-name').html(),'<img style="width:100%;display:block;" src="' + $(this).attr('href') + '" />',false);
  1655. $('.hover-card,.hover-card-caret').remove();
  1656. }
  1657. }
  1658. // hijack link if we find a matching link that qvitter can handle
  1659. else {
  1660. var hrefAttr = $(this).attr('href');
  1661. // this might be a remote profile that we want to reroute to a local instance/user/id url, let's check our cache
  1662. if(typeof window.convertStatusnetProfileUrlToUserArrayCacheKey[hrefAttr] != 'undefined') {
  1663. if(typeof window.userArrayCache[window.convertStatusnetProfileUrlToUserArrayCacheKey[hrefAttr]] != 'undefined') {
  1664. var cachedUserArray = window.userArrayCache[window.convertStatusnetProfileUrlToUserArrayCacheKey[hrefAttr]];
  1665. if(cachedUserArray.local.is_local === false) {
  1666. hrefAttr = window.siteInstanceURL + 'user/' + cachedUserArray.local.id;
  1667. }
  1668. }
  1669. }
  1670. else if(typeof window.convertUriToUserArrayCacheKey[hrefAttr] != 'undefined') {
  1671. if(typeof window.userArrayCache[window.convertUriToUserArrayCacheKey[hrefAttr]] != 'undefined') {
  1672. var cachedUserArray = window.userArrayCache[window.convertUriToUserArrayCacheKey[hrefAttr]];
  1673. if(cachedUserArray.local.is_local === false) {
  1674. hrefAttr = window.siteInstanceURL + 'user/' + cachedUserArray.local.id;
  1675. }
  1676. }
  1677. }
  1678. var streamObject = URLtoStreamRouter(hrefAttr);
  1679. if(streamObject && streamObject.stream) {
  1680. e.preventDefault();
  1681. // if this is a user/{id} type link but we know the nickname already
  1682. if(streamObject.name == 'profile by id' && streamObject.nickname !== false) {
  1683. setNewCurrentStream(pathToStreamRouter(streamObject.nickname),true,streamObject.id);
  1684. }
  1685. // same with group/{id}/id links
  1686. else if(streamObject.name == 'group notice stream by id') {
  1687. // we might be member of the group and thereby already know its nickname
  1688. if (typeof window.groupMemberships != 'undefined' && typeof window.groupMemberships[streamObject.id] != 'undefined') {
  1689. setNewCurrentStream(pathToStreamRouter('group/' + window.groupMemberships[streamObject.id].username),true,streamObject.id);
  1690. }
  1691. // if the text() of the clicked element looks like a group nickname, use that (but send id to setNewCurrentStream() in case this is bad data)
  1692. else if(/^![a-zA-Z0-9]+$/.test($(e.target).text()) || /^[a-zA-Z0-9]+$/.test($(e.target).text())) {
  1693. var nickname = $(e.target).text();
  1694. if(nickname.indexOf('!') == 0) {
  1695. nickname = nickname.substring(1); // remove any starting !
  1696. }
  1697. setNewCurrentStream(pathToStreamRouter('group/' + nickname),true,streamObject.id);
  1698. }
  1699. // if we can't figure out or guess a nickname, query the server for it
  1700. else {
  1701. getNicknameByGroupIdFromAPI(streamObject.id,function(nickname) {
  1702. if(nickname) {
  1703. setNewCurrentStream(pathToStreamRouter('group/' + nickname),true,false);
  1704. }
  1705. else {
  1706. alert('group not found');
  1707. }
  1708. });
  1709. }
  1710. }
  1711. // all other links that qvitter can handle
  1712. else {
  1713. setNewCurrentStream(streamObject,true,false);
  1714. }
  1715. }
  1716. }
  1717. });
  1718. /* ·
  1719. ·
  1720. · When user clicks the bookmark icon to bookmark
  1721. ·
  1722. · · · · · · · · · · · · · */
  1723. $('body').on('click','#history-container .chev-right',function(event){
  1724. var thisStreamLink = $(this).parent('.stream-selection');
  1725. thisStreamLink.slideUp(300,function(){
  1726. $('#bookmark-container').append(thisStreamLink.outerHTML());
  1727. $('#bookmark-container').children().last().children('.chev-right').attr('data-tooltip',window.sL.tooltipRemoveBookmark);
  1728. $('#bookmark-container').children().last().fadeIn(300,function(){
  1729. thisStreamLink.remove();
  1730. updateHistoryLocalStorage();
  1731. saveAllBookmarks();
  1732. });
  1733. });
  1734. });
  1735. /* ·
  1736. ·
  1737. · When user clicks the x to remove a bookmark
  1738. ·
  1739. · · · · · · · · · · · · · */
  1740. $('body').on('click','#bookmark-container .chev-right',function(event){
  1741. $(this).parent('.stream-selection').remove();
  1742. saveAllBookmarks();
  1743. });
  1744. /* ·
  1745. ·
  1746. · When sorting the bookmark menu
  1747. ·
  1748. · · · · · · · · · · · · · */
  1749. $('#bookmark-container').on("sortupdate", function() {
  1750. saveAllBookmarks();
  1751. });
  1752. /* ·
  1753. ·
  1754. · When clearing the browsing history
  1755. ·
  1756. · · · · · · · · · · · · · */
  1757. $('#clear-history').on('click', function() {
  1758. $('#history-container').empty();
  1759. updateHistoryLocalStorage();
  1760. });
  1761. /* ·
  1762. ·
  1763. · Load more from the current stream when scroll is 1000px from bottom
  1764. ·
  1765. · The search API is crap and don't do max_id and last_id, so we have to do pages there...
  1766. ·
  1767. · · · · · · · · · · · · · */
  1768. $(window).scroll(function() {
  1769. if($(window).scrollTop() + $(window).height() > $(document).height() - 1000) {
  1770. // not if we're already loading or if no stream is set yet
  1771. if(!$('body').hasClass('loading-older') && typeof window.currentStreamObject != "undefined" && $('#feed-body').attr('data-end-reached') != 'true') {
  1772. $('body').addClass('loading-older');
  1773. // remove loading class in 10 seconds, i.e. try again if failed to load within 10 s
  1774. if(window.currentStreamObject.name != 'search') {
  1775. setTimeout(function(){$('body').removeClass('loading-older');},10000);
  1776. }
  1777. var lastStreamItemId = $('#feed-body').children('.stream-item').last().attr('id');
  1778. // if this is a stream that uses 'page' for paging, i.e. search or users lists,
  1779. // we need page and rpp vars (page number is stored in an attribute in feed-body)
  1780. if(window.currentStreamObject.maxIdOrPage == 'page') {
  1781. if(typeof $('#feed-body').attr('data-search-page-number') != 'undefined') {
  1782. var searchPage = parseInt($('#feed-body').attr('data-search-page-number'),10);
  1783. }
  1784. else {
  1785. var searchPage=2;
  1786. }
  1787. var nextPage = searchPage+1;
  1788. var getVars = qOrAmp(window.currentStreamObject.stream) + 'rpp=20&page=' + searchPage; // search uses 'rpp' var and others 'count' for paging, though we can add rrp to others aswell without any problem
  1789. }
  1790. // normal streams
  1791. else {
  1792. var getVars = qOrAmp(window.currentStreamObject.stream) + 'max_id=' + ($('#feed-body').children('.stream-item').last().attr('data-quitter-id-in-stream'));
  1793. }
  1794. display_spinner('#footer-spinner-container');
  1795. getFromAPI(window.currentStreamObject.stream + getVars,function(data){
  1796. // if data returned an empty array, we have probably reached the bottom
  1797. if(data.length == 0) {
  1798. $('#feed-body').attr('data-end-reached',true);
  1799. }
  1800. else if(data) {
  1801. addToFeed(data, lastStreamItemId,'visible');
  1802. $('body').removeClass('loading-older');
  1803. // if this is search our group users lists, we remember page number (if we got any users)
  1804. if(window.currentStreamObject.maxIdOrPage == 'page') {
  1805. $('#feed-body').attr('data-search-page-number',nextPage);
  1806. }
  1807. }
  1808. remove_spinner();
  1809. });
  1810. }
  1811. }
  1812. });
  1813. /* ·
  1814. ·
  1815. · Updates all queets' times/dates
  1816. ·
  1817. · · · · · · · · · · · · · */
  1818. var updateTimesInterval=self.setInterval(function(){
  1819. updateAllQueetsTimes();
  1820. },10000);
  1821. /* ·
  1822. ·
  1823. · Check for new queets
  1824. ·
  1825. · · · · · · · · · · · · · */
  1826. var checkForNewQueetsInterval=window.setInterval(function(){checkForNewQueets()},window.timeBetweenPolling);
  1827. function checkForNewQueets() {
  1828. // no new requests if requests are very slow, e.g. searches
  1829. if(!$('body').hasClass('loading-newer')) {
  1830. $('body').addClass('loading-newer');
  1831. // only if logged in and only for notice or notification streams
  1832. if(window.loggedIn && (window.currentStreamObject.type == 'notices' || window.currentStreamObject.type == 'notifications')) {
  1833. var lastId = $('#feed-body').children('.stream-item').not('.temp-post').not('.posted-from-form').attr('data-quitter-id-in-stream');
  1834. var addThisStream = window.currentStreamObject.stream;
  1835. getFromAPI(addThisStream + qOrAmp(window.currentStreamObject.stream) + 'since_id=' + lastId,function(data){
  1836. $('body').removeClass('loading-newer');
  1837. if(data) {
  1838. if(addThisStream == window.currentStreamObject.stream) {
  1839. addToFeed(data, false, 'hidden');
  1840. // say hello to the api if this is notifications stream, to
  1841. // get correct unread notifcation count
  1842. if(window.currentStreamObject.name == 'notifications') {
  1843. helloAPI();
  1844. }
  1845. // if we have hidden items, show new-queets-bar
  1846. maybeShowTheNewQueetsBar()
  1847. }
  1848. }
  1849. });
  1850. }
  1851. }
  1852. }
  1853. /* ·
  1854. ·
  1855. · Show hidden queets when user clicks on new-queets-bar
  1856. ·
  1857. · · · · · · · · · · · · · */
  1858. $('body').on('click','#new-queets-bar',function(){
  1859. if(window.currentStreamObject.name == 'notifications') {
  1860. document.title = window.siteTitle;
  1861. }
  1862. var hiddenStreamItems = $('.stream-item.hidden');
  1863. hiddenStreamItems.css('opacity','0')
  1864. hiddenStreamItems.animate({opacity:'1'}, 200);
  1865. hiddenStreamItems.addClass('visible');
  1866. hiddenStreamItems.removeClass('hidden');
  1867. $('#new-queets-bar').parent().addClass('hidden');
  1868. // say hello to the api if this is notifications stream, to
  1869. // get correct unread notifcation count
  1870. if(window.currentStreamObject.name == 'notifications') {
  1871. helloAPI();
  1872. }
  1873. });
  1874. /* ·
  1875. ·
  1876. · Expand and de-expand queets when clicking anywhere but on a few element types
  1877. ·
  1878. · · · · · · · · · · · · · */
  1879. $('body').on('click','.queet',function (event) {
  1880. if(typeof $(this).attr('href') == 'undefined'
  1881. && $(event.target).closest('a').length == 0
  1882. && !$(event.target).is('\
  1883. a,\
  1884. video,\
  1885. .cm-mention,\
  1886. .cm-tag,\
  1887. .cm-group,\
  1888. .cm-url,\
  1889. pre,\
  1890. img,\
  1891. .name,\
  1892. .queet-box,\
  1893. .syntax-two,\
  1894. button,\
  1895. .show-full-conversation,\
  1896. span.mention,\
  1897. .action-reply-container a span,\
  1898. .action-reply-container a b,\
  1899. .action-rt-container a span,\
  1900. .action-rt-container a b,\
  1901. .action-del-container a span,\
  1902. .action-del-container a b,\
  1903. .action-fav-container a span,\
  1904. .action-fav-container a b,\
  1905. .action-ellipsis-container a span,\
  1906. .action-ellipsis-container a b,\
  1907. span.group,\
  1908. .longdate,\
  1909. .screen-name,\
  1910. .discard-error-message')
  1911. && !$(this).parent('.stream-item').hasClass('user')) { // not if user stream
  1912. expand_queet($(this).parent());
  1913. }
  1914. });
  1915. /* ·
  1916. ·
  1917. · Image popups
  1918. ·
  1919. · · · · · · · · · · · · · */
  1920. $('body').on('click','.stream-item .queet img.attachment-thumb',function (event) {
  1921. event.preventDefault();
  1922. // don't do anything if we are in the middle of collapsing
  1923. if($(this).closest('.stream-item').hasClass('collapsing')) {
  1924. // no action
  1925. }
  1926. // if the stream item isn't expanded, expand that
  1927. else if(!$(this).closest('.stream-item').hasClass('expanded')) {
  1928. expand_queet($(this).closest('.stream-item'));
  1929. }
  1930. // otherwise we open the popup
  1931. else {
  1932. var thisAttachmentThumbSrc = $(this).attr('src');
  1933. var parentStreamItem = $(this).closest('.stream-item');
  1934. var $parentStreamItemClone = $('<div/>').append(parentStreamItem.outerHTML());
  1935. var $queetThumbsClone = $('<div/>').append($parentStreamItemClone.children('.stream-item').children('.queet').find('.queet-thumbs').outerHTML());
  1936. // cleaned version of the stream item to show in the footer
  1937. cleanStreamItemsFromClassesAndConversationElements($parentStreamItemClone);
  1938. $parentStreamItemClone.find('.context,.stream-item-footer').remove();
  1939. var parentStreamItemHTMLWithoutFooter = $parentStreamItemClone.outerHTML();
  1940. $thumbToDisplay = $queetThumbsClone.find('img.attachment-thumb[src="' + thisAttachmentThumbSrc + '"]');
  1941. $thumbToDisplay.parent().addClass('display-this-thumb');
  1942. // "play" all animated gifs and add youtube iframes to all youtube and vimeo videos
  1943. $.each($queetThumbsClone.find('img.attachment-thumb'),function(){
  1944. if($(this).attr('data-mime-type') == 'image/gif'
  1945. && $(this).parent().hasClass('play-button')) {
  1946. $(this).attr('src',$(this).attr('data-full-image-url'));
  1947. $(this).parent('.thumb-container').css('background-image','url(\'' + $(this).attr('data-full-image-url') + '\')');
  1948. }
  1949. else if($(this).parent().hasClass('host-youtube-com')){
  1950. $(this).parent().prepend('<iframe width="510" height="315" src="' + youTubeEmbedLinkFromURL($(this).attr('data-full-image-url'), $(this).parent().hasClass('display-this-thumb')) + '" frameborder="0" allowscriptaccess="always" allowfullscreen></iframe>');
  1951. }
  1952. else if($(this).parent().hasClass('host-vimeo-com')){
  1953. $(this).parent().prepend('<iframe src="' + vimeoEmbedLinkFromURL($(this).attr('data-full-image-url'), $(this).parent().hasClass('display-this-thumb')) + '" width="510" height="315" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>');
  1954. }
  1955. });
  1956. // navigation buttons
  1957. var imgNum = parentStreamItem.children('.queet').find('.attachment-thumb').length;
  1958. if(imgNum > 1) {
  1959. $queetThumbsClone.find('.queet-thumbs').before('<div class="prev-thumb"></div>');
  1960. $queetThumbsClone.find('.queet-thumbs').after('<div class="next-thumb"></div>');
  1961. }
  1962. if(parentStreamItem.hasClass('expanded')) {
  1963. var calculatedDimensions = calculatePopUpAndImageDimensions($thumbToDisplay);
  1964. var $thisImgInQueetThumbsClone = $queetThumbsClone.find('img[src="' + $thumbToDisplay.attr('src') + '"]');
  1965. // set dimensions
  1966. $thisImgInQueetThumbsClone.width(calculatedDimensions.displayImgWidth);
  1967. $thisImgInQueetThumbsClone.parent('.thumb-container').width(calculatedDimensions.displayImgWidth);
  1968. $thisImgInQueetThumbsClone.parent('.thumb-container').children('iframe').attr('width',calculatedDimensions.displayImgWidth);
  1969. $thisImgInQueetThumbsClone.parent('.thumb-container').children('iframe').attr('height',calculatedDimensions.displayImgHeight);
  1970. // open popup
  1971. popUpAction('queet-thumb-popup', '', '' + $queetThumbsClone.outerHTML() + '', parentStreamItemHTMLWithoutFooter, calculatedDimensions.popUpWidth);
  1972. disableOrEnableNavigationButtonsInImagePopup($('#queet-thumb-popup'));
  1973. // for some reason vimeo autoplays when we have a #t=x start time, so stop any vimeo videos running in the background (delay so it has time to load)
  1974. setTimeout(function(){
  1975. $.each($('#queet-thumb-popup').find('.thumb-container.host-vimeo-com:not(.display-this-thumb)').children('iframe'),function(){
  1976. console.log($(this).attr('src'));
  1977. this.contentWindow.postMessage('{"method": "pause"}', '*');
  1978. });
  1979. },1000);
  1980. }
  1981. }
  1982. });
  1983. // popups can be max 900px wide, and should not be higher than the window, so we need to do some calculating
  1984. function calculatePopUpAndImageDimensions(img) {
  1985. var img_src = img.attr('src');
  1986. // trick to get width and height, we can't go with what gnusocial tells us, because
  1987. // gnusocial doesn't (always?) report width and height after proper orientation
  1988. $('body').prepend('<div id="img-dimension-check" style="opacity:0;"><img src="' + img_src + '" /></div>');
  1989. var imgWidth = $('#img-dimension-check img').width();
  1990. var imgHeight = $('#img-dimension-check img').height();
  1991. $('#img-dimension-check').remove();
  1992. // e.g. svg's may not have dimensions set, in that case we just make them small
  1993. if(typeof imgWidth == 'undefined' && typeof imgHeight == 'undefined') {
  1994. return {popUpWidth: 540, displayImgWidth: 540};
  1995. }
  1996. var thisImgWidth = parseInt(imgWidth,10);
  1997. var thisImgHeight = parseInt(imgHeight,10);
  1998. var maxImageHeight = $(window).height() - 120; // 120 being a little more than a short queet in the footer
  1999. if(thisImgWidth < 540) {
  2000. var displayImgWidth = thisImgWidth;
  2001. var popUpWidth = 540;
  2002. if(thisImgHeight > maxImageHeight) {
  2003. displayImgWidth = Math.round(maxImageHeight/thisImgHeight*displayImgWidth);
  2004. }
  2005. }
  2006. else if(thisImgWidth < 900) {
  2007. var displayImgWidth = thisImgWidth;
  2008. if(thisImgHeight > maxImageHeight) {
  2009. displayImgWidth = Math.round(maxImageHeight/thisImgHeight*displayImgWidth);
  2010. if(displayImgWidth < 540) {
  2011. var popUpWidth = 540;
  2012. }
  2013. else {
  2014. var popUpWidth = displayImgWidth;
  2015. }
  2016. }
  2017. else {
  2018. var popUpWidth = displayImgWidth;
  2019. }
  2020. }
  2021. else {
  2022. var displayImgWidth = 900;
  2023. var displayImgHeight = 900/thisImgWidth*thisImgHeight;
  2024. if(displayImgHeight > maxImageHeight) {
  2025. displayImgWidth = Math.round(maxImageHeight*displayImgWidth/displayImgHeight);
  2026. if(displayImgWidth < 540) {
  2027. var popUpWidth = 540;
  2028. }
  2029. else if(displayImgWidth < 900) {
  2030. var popUpWidth = displayImgWidth;
  2031. }
  2032. else {
  2033. var popUpWidth = 900;
  2034. }
  2035. }
  2036. else {
  2037. var popUpWidth = 900;
  2038. }
  2039. }
  2040. // vimeo can't be too small, or the design freaks out
  2041. if(img.parent('.thumb-container').hasClass('host-vimeo-com') && displayImgWidth < 540) {
  2042. displayImgWidth = 540;
  2043. displayImgHeight = 305;
  2044. }
  2045. return {popUpWidth: popUpWidth, displayImgWidth: displayImgWidth, displayImgHeight: displayImgHeight };
  2046. }
  2047. // switch to next image when clicking the image in the popup
  2048. $('body').on('click','#queet-thumb-popup .attachment-thumb',function (event) {
  2049. event.preventDefault();
  2050. var nextImage = $(this).parent().next().children('.attachment-thumb');
  2051. if(nextImage.length>0) {
  2052. // start and stop youtube videos, if any
  2053. $.each($(this).parent('.host-youtube-com').children('iframe'),function(){
  2054. this.contentWindow.postMessage('{"event":"command","func":"' + 'stopVideo' + '","args":""}', '*');
  2055. });
  2056. $.each(nextImage.parent('.host-youtube-com').children('iframe'),function(){
  2057. this.contentWindow.postMessage('{"event":"command","func":"' + 'playVideo' + '","args":""}', '*');
  2058. });
  2059. // start stop vimeo
  2060. $.each($(this).parent('.host-vimeo-com').children('iframe'),function(){
  2061. this.contentWindow.postMessage('{"method": "pause"}', '*');
  2062. });
  2063. $.each(nextImage.parent('.host-vimeo-com').children('iframe'),function(){
  2064. this.contentWindow.postMessage('{"method": "play"}', '*');
  2065. });
  2066. // set dimensions of next image and the popup
  2067. var calculatedDimensions = calculatePopUpAndImageDimensions(nextImage);
  2068. nextImage.width(calculatedDimensions.displayImgWidth);
  2069. nextImage.parent('.thumb-container').width(calculatedDimensions.displayImgWidth);
  2070. nextImage.parent('.thumb-container').children('iframe').attr('width',calculatedDimensions.displayImgWidth);
  2071. nextImage.parent('.thumb-container').children('iframe').attr('height',calculatedDimensions.displayImgHeight);
  2072. $('#queet-thumb-popup .modal-draggable').width(calculatedDimensions.popUpWidth);
  2073. // switch image
  2074. $(this).parent().removeClass('display-this-thumb');
  2075. $(this).parent().next().addClass('display-this-thumb');
  2076. disableOrEnableNavigationButtonsInImagePopup($('#queet-thumb-popup'));
  2077. centerPopUp($('#queet-thumb-popup .modal-draggable'));
  2078. }
  2079. });
  2080. // navigation buttons in image popup
  2081. $('body').on('click','#queet-thumb-popup .next-thumb',function (event) {
  2082. $(this).parent().find('.display-this-thumb').children('img').trigger('click');
  2083. });
  2084. $('body').on('click','#queet-thumb-popup .prev-thumb',function (event) {
  2085. var prevImage = $(this).parent().find('.display-this-thumb').prev().children('img');
  2086. if(prevImage.length>0) {
  2087. // start and stop youtube videos, if any
  2088. $.each($(this).parent().find('.display-this-thumb.host-youtube-com').children('iframe'),function(){
  2089. this.contentWindow.postMessage('{"event":"command","func":"' + 'stopVideo' + '","args":""}', '*');
  2090. });
  2091. $.each(prevImage.parent('.host-youtube-com').children('iframe'),function(){
  2092. this.contentWindow.postMessage('{"event":"command","func":"' + 'playVideo' + '","args":""}', '*');
  2093. });
  2094. // start stop vimeo
  2095. $.each($(this).parent().find('.display-this-thumb.host-vimeo-com').children('iframe'),function(){
  2096. this.contentWindow.postMessage('{"method": "pause"}', '*');
  2097. });
  2098. $.each(prevImage.parent('.host-vimeo-com').children('iframe'),function(){
  2099. this.contentWindow.postMessage('{"method": "play"}', '*');
  2100. });
  2101. // set dimensions of next image and the popup
  2102. var calculatedDimensions = calculatePopUpAndImageDimensions(prevImage);
  2103. prevImage.width(calculatedDimensions.displayImgWidth);
  2104. prevImage.parent('.thumb-container').width(calculatedDimensions.displayImgWidth);
  2105. prevImage.parent('.thumb-container').children('iframe').attr('width',calculatedDimensions.displayImgWidth);
  2106. prevImage.parent('.thumb-container').children('iframe').attr('height',calculatedDimensions.displayImgHeight);
  2107. $('#queet-thumb-popup .modal-draggable').width(calculatedDimensions.popUpWidth);
  2108. // switch image
  2109. $(this).parent().find('.display-this-thumb').removeClass('display-this-thumb');
  2110. prevImage.parent().addClass('display-this-thumb');
  2111. disableOrEnableNavigationButtonsInImagePopup($('#queet-thumb-popup'));
  2112. centerPopUp($('#queet-thumb-popup .modal-draggable'));
  2113. }
  2114. });
  2115. function disableOrEnableNavigationButtonsInImagePopup(popUp) {
  2116. if(popUp.find('.display-this-thumb').prev().length < 1) {
  2117. popUp.find('.prev-thumb').addClass('disabled');
  2118. }
  2119. else {
  2120. popUp.find('.prev-thumb').removeClass('disabled');
  2121. }
  2122. if(popUp.find('.display-this-thumb').next().length < 1) {
  2123. popUp.find('.next-thumb').addClass('disabled');
  2124. }
  2125. else {
  2126. popUp.find('.next-thumb').removeClass('disabled');
  2127. }
  2128. }
  2129. /* ·
  2130. ·
  2131. · Collapse all open conversations and the welcome text on esc or when clicking the margin
  2132. ·
  2133. · · · · · · · · · · · · · */
  2134. $('body').click(function(event){
  2135. if($(event.target).is('body') || $(event.target).is('#page-container')) {
  2136. $('.front-welcome-text.expanded > .show-full-welcome-text').trigger('click');
  2137. $.each($('.stream-item.expanded'),function(){
  2138. expand_queet($(this), false);
  2139. });
  2140. }
  2141. });
  2142. $(document).keyup(function(e){
  2143. if(e.keyCode==27) { // esc
  2144. $('.front-welcome-text.expanded > .show-full-welcome-text').trigger('click');
  2145. $.each($('.stream-item.expanded'),function(){
  2146. expand_queet($(this), false);
  2147. });
  2148. }
  2149. });
  2150. /* ·
  2151. ·
  2152. · When clicking the delete-button
  2153. ·
  2154. · · · · · · · · · · · · · */
  2155. function deleteQueet(arg) {
  2156. var this_stream_item = $('.stream-item[data-quitter-id="' + arg.streamItemID + '"]');
  2157. // don't do anything if this is a queet being posted
  2158. if(this_stream_item.hasClass('temp-post')) {
  2159. return;
  2160. }
  2161. var this_qid = this_stream_item.attr('data-quitter-id');
  2162. var $queetHtml = $(this_stream_item.outerHTML());
  2163. $queetHtml.children('.stream-item.conversation').remove();
  2164. $queetHtml.find('.context,.stream-item-footer,.inline-reply-queetbox,.expanded-content').remove();
  2165. var queetHtmlWithoutFooterAndConversation = $queetHtml.outerHTML();
  2166. popUpAction('popup-delete-' + this_qid, window.sL.deleteConfirmation,queetHtmlWithoutFooterAndConversation,'<div class="right"><button class="close">' + window.sL.cancelVerb + '</button><button class="primary">' + window.sL.deleteVerb + '</button></div>');
  2167. $('#popup-delete-' + this_qid + ' button.primary').on('click',function(){
  2168. display_spinner();
  2169. $('.modal-container').remove();
  2170. // delete
  2171. postActionToAPI('statuses/destroy/' + this_qid + '.json', function(data) {
  2172. if(data) {
  2173. remove_spinner();
  2174. window.knownDeletedNotices[$('.stream-item[data-quitter-id="' + this_qid + '"]').attr('data-uri')] = true;
  2175. slideUpAndRemoveStreamItem($('.stream-item[data-quitter-id="' + this_qid + '"]'));
  2176. }
  2177. else {
  2178. remove_spinner();
  2179. }
  2180. });
  2181. });
  2182. }
  2183. /* ·
  2184. ·
  2185. · When clicking the requeet-button
  2186. ·
  2187. · · · · · · · · · · · · · */
  2188. $('body').on('click','.action-rt-container .icon:not(.is-mine)',function(){
  2189. var this_stream_item = $(this).closest('.stream-item');
  2190. var this_queet = this_stream_item.children('.queet');
  2191. var this_action = $(this).closest('li');
  2192. // requeet
  2193. if(!this_action.children('.with-icn').hasClass('done')) {
  2194. // update the repeat count immediately
  2195. var newRqNum = parseInt(this_queet.find('.action-rq-num').html(),10)+1;
  2196. this_queet.find('.action-rq-num').html(newRqNum);
  2197. this_queet.find('.action-rq-num').attr('data-rq-num',newRqNum);
  2198. this_action.children('.with-icn').addClass('done');
  2199. this_stream_item.addClass('requeeted');
  2200. // requeet animation
  2201. this_action.children('.with-icn').children('.sm-rt').addClass('rotate');
  2202. // remove the fav and rq cache for this queet, to avoid number flickering
  2203. localStorageObjectCache_STORE('favsAndRequeets',this_stream_item.attr('data-quitter-id'), false);
  2204. // post requeet
  2205. postActionToAPI('statuses/retweet/' + this_stream_item.attr('data-quitter-id') + '.json', function(data) {
  2206. if(data) {
  2207. // success
  2208. this_stream_item.attr('data-requeeted-by-me-id',data.id);
  2209. getFavsAndRequeetsForQueet(this_stream_item, this_stream_item.attr('data-quitter-id'));
  2210. // mark all instances of this notice as repeated
  2211. $('.stream-item[data-quitter-id="' + data.retweeted_status.id + '"]').addClass('requeeted');
  2212. $('.stream-item[data-quitter-id="' + data.retweeted_status.id + '"]').attr('data-requeeted-by-me-id',data.id);
  2213. $('.stream-item[data-quitter-id="' + data.retweeted_status.id + '"]').children('.queet').find('.action-rt-container').children('.with-icn').addClass('done');
  2214. }
  2215. else {
  2216. // error
  2217. this_action.children('.with-icn').removeClass('done');
  2218. this_action.find('.with-icn b').html(window.sL.requeetVerb);
  2219. this_stream_item.removeClass('requeeted');
  2220. }
  2221. });
  2222. }
  2223. // un-requeet
  2224. else if(this_action.children('.with-icn').hasClass('done')) {
  2225. display_spinner();
  2226. var myRequeetID = this_stream_item.attr('data-requeeted-by-me-id');
  2227. // display button as unrepeated
  2228. this_action.children('.with-icn').removeClass('done');
  2229. this_action.find('.with-icn b').html(window.sL.requeetVerb);
  2230. this_stream_item.removeClass('requeeted');
  2231. // post unrequeet
  2232. postActionToAPI('statuses/destroy/' + myRequeetID + '.json', function(data) {
  2233. if(data) {
  2234. // remove my repeat-notice from the feed, if it's there
  2235. slideUpAndRemoveStreamItem($('.stream-item[data-quitter-id-in-stream="' + myRequeetID + '"]'));
  2236. // mark all instances of this notice as non-repeated
  2237. $('.stream-item[data-quitter-id="' + this_stream_item.attr('data-quitter-id') + '"]').removeClass('requeeted');
  2238. $('.stream-item[data-quitter-id="' + this_stream_item.attr('data-quitter-id') + '"]').removeAttr('data-requeeted-by-me-id');
  2239. $('.stream-item[data-quitter-id="' + this_stream_item.attr('data-quitter-id') + '"]').children('.queet').find('.action-rt-container').children('.with-icn').removeClass('done');
  2240. getFavsAndRequeetsForQueet(this_stream_item, this_stream_item.attr('data-quitter-id'));
  2241. remove_spinner();
  2242. }
  2243. else {
  2244. remove_spinner();
  2245. this_action.children('.with-icn').addClass('done');
  2246. this_action.find('.with-icn b').html(window.sL.requeetedVerb);
  2247. this_stream_item.addClass('requeeted');
  2248. }
  2249. });
  2250. }
  2251. });
  2252. /* ·
  2253. ·
  2254. · When clicking the fav-button
  2255. ·
  2256. · · · · · · · · · · · · · */
  2257. $('body').on('click','.action-fav-container',function(){
  2258. var this_stream_item = $(this).closest('.stream-item');
  2259. var this_queet = this_stream_item.children('.queet');
  2260. // don't do anything if this is a queet being posted
  2261. if(this_stream_item.hasClass('temp-post')) {
  2262. return;
  2263. }
  2264. var this_action = $(this);
  2265. // fav
  2266. if(!this_action.children('.with-icn').hasClass('done')) {
  2267. // update the fav count immediately
  2268. var newFavNum = parseInt(this_queet.find('.action-fav-num').html(),10)+1;
  2269. this_queet.find('.action-fav-num').html(newFavNum);
  2270. this_queet.find('.action-fav-num').attr('data-fav-num',newFavNum);
  2271. this_action.children('.with-icn').addClass('done');
  2272. this_stream_item.addClass('favorited');
  2273. // fav animation
  2274. this_action.children('.with-icn').children('.sm-fav').addClass('pulse');
  2275. // remove the fav and rq cache for this queet, to avoid number flickering
  2276. localStorageObjectCache_STORE('favsAndRequeets',this_stream_item.attr('data-quitter-id'), false);
  2277. // post fav
  2278. postActionToAPI('favorites/create/' + this_stream_item.attr('data-quitter-id') + '.json', function(data) {
  2279. if(data) {
  2280. // success
  2281. getFavsAndRequeetsForQueet(this_stream_item, this_stream_item.attr('data-quitter-id'));
  2282. // mark all instances of this notice as favorited
  2283. $('.stream-item[data-quitter-id="' + this_stream_item.attr('data-quitter-id') + '"]').addClass('favorited');
  2284. $('.stream-item[data-quitter-id="' + this_stream_item.attr('data-quitter-id') + '"]').children('.queet').find('.action-fav-container').children('.with-icn').addClass('done');
  2285. }
  2286. else {
  2287. // error
  2288. this_action.children('.with-icn').removeClass('done');
  2289. this_action.find('.with-icn b').html(window.sL.favoriteVerb);
  2290. this_stream_item.removeClass('favorited');
  2291. }
  2292. });
  2293. }
  2294. // unfav
  2295. else {
  2296. // update the fav count immediately
  2297. var newFavNum = Math.max(0, parseInt(this_queet.find('.action-fav-num').html(),10)-1);
  2298. this_queet.find('.action-fav-num').html(newFavNum);
  2299. this_queet.find('.action-fav-num').attr('data-fav-num',newFavNum);
  2300. this_action.children('.with-icn').removeClass('done');
  2301. this_action.find('.with-icn b').html(window.sL.favoriteVerb);
  2302. this_stream_item.removeClass('favorited');
  2303. // remove the fav and rq cache for this queet, to avoid number flickering
  2304. localStorageObjectCache_STORE('favsAndRequeets',this_stream_item.attr('data-quitter-id'), false);
  2305. // post unfav
  2306. postActionToAPI('favorites/destroy/' + this_stream_item.attr('data-quitter-id') + '.json', function(data) {
  2307. if(data) {
  2308. // success
  2309. getFavsAndRequeetsForQueet(this_stream_item, this_stream_item.attr('data-quitter-id'));
  2310. // mark all instances of this notice as non-favorited
  2311. $('.stream-item[data-quitter-id="' + this_stream_item.attr('data-quitter-id') + '"]').removeClass('favorited');
  2312. $('.stream-item[data-quitter-id="' + this_stream_item.attr('data-quitter-id') + '"]').children('.queet').find('.action-fav-container').children('.with-icn').removeClass('done');
  2313. }
  2314. else {
  2315. // error
  2316. this_action.children('.with-icn').addClass('done');
  2317. this_action.find('.with-icn b').html(window.sL.favoritedVerb);
  2318. this_stream_item.addClass('favorited');
  2319. }
  2320. });
  2321. }
  2322. });
  2323. /* ·
  2324. ·
  2325. · When clicking the reply-button
  2326. ·
  2327. · · · · · · · · · · · · · */
  2328. $('body').on('click','.action-reply-container',function(){
  2329. var this_stream_item = $(this).closest('.stream-item');
  2330. // don't do anything if this is a queet being posted
  2331. if(this_stream_item.hasClass('temp-post')) {
  2332. return;
  2333. }
  2334. var this_stream_item_id = this_stream_item.attr('data-quitter-id');
  2335. this_stream_item.addClass('replying-to');
  2336. // grabbing the stream-item and view it in the popup, stripped of conversation footer, reply box and other sruff
  2337. var streamItemHTML = $('<div/>').html(this_stream_item.outerHTML());
  2338. cleanStreamItemsFromClassesAndConversationElements(streamItemHTML);
  2339. streamItemHTML.find('.context,.stream-item-footer').remove();
  2340. var streamItemHTMLWithoutFooter = streamItemHTML.outerHTML();
  2341. popUpAction('popup-reply-' + this_stream_item_id, window.sL.replyTo + ' ' + this_stream_item.children('.queet').find('.screen-name').html(),replyFormHtml(this_stream_item,this_stream_item_id),streamItemHTMLWithoutFooter);
  2342. $('#popup-reply-' + this_stream_item_id).find('.modal-body').find('.queet-box').trigger('click'); // expand
  2343. // fix the width of the queet box, otherwise the syntax highlighting break
  2344. var queetBox = $('#popup-reply-' + this_stream_item_id).find('.modal-body').find('.inline-reply-queetbox');
  2345. var queetBoxWidth = queetBox.width()-20;
  2346. queetBox.children('.queet-box-syntax, .syntax-middle, .syntax-two').width(queetBoxWidth);
  2347. maybePrefillQueetBoxWithCachedText(queetBox.children('.queet-box'));
  2348. });
  2349. /* ·
  2350. ·
  2351. · When clicking the compose button
  2352. ·
  2353. · · · · · · · · · · · · · */
  2354. $('body').on('click','#top-compose',function(){
  2355. popUpAction('popup-compose', window.sL.compose,queetBoxPopUpHtml(),false);
  2356. var queetBoxWidth = $('#popup-compose').find('.inline-reply-queetbox').width()-20;
  2357. $('#popup-compose').find('.queet-box-syntax').width(queetBoxWidth);
  2358. $('#popup-compose').find('.syntax-middle').width(queetBoxWidth);
  2359. $('#popup-compose').find('.syntax-two').width(queetBoxWidth);
  2360. $('#popup-compose').find('.queet-box').trigger('click');
  2361. maybePrefillQueetBoxWithCachedText($('#popup-compose').find('.queet-box'));
  2362. });
  2363. /* ·
  2364. ·
  2365. · Close popups
  2366. ·
  2367. · · · · · · · · · · · · · */
  2368. $('body').on('click','.modal-container button.close',function(){
  2369. $('.stream-item').removeClass('replying-to');
  2370. $('.modal-container').remove();
  2371. });
  2372. $('body').on('click','.modal-close',function(){
  2373. $('.stream-item').removeClass('replying-to');
  2374. $('.modal-container').remove();
  2375. });
  2376. $('body').on('click','.modal-container',function(e){
  2377. if($(e.target).is('.modal-container')) {
  2378. $('.stream-item').removeClass('replying-to');
  2379. $('.modal-container').remove();
  2380. abortEditProfile();
  2381. }
  2382. });
  2383. $(document).keyup(function(e){
  2384. if(e.keyCode==27) {
  2385. $('.stream-item').removeClass('replying-to');
  2386. $('.modal-container').remove();
  2387. $('*').blur();
  2388. abortEditProfile();
  2389. }
  2390. });
  2391. /* ·
  2392. ·
  2393. · Post queets, inline and popup replies
  2394. ·
  2395. · · · · · · · · · · · · · */
  2396. $('body').on('click', '.queet-toolbar button',function () {
  2397. if($(this).hasClass('enabled')) {
  2398. // set temp post id
  2399. if($('.temp-post').length == 0) {
  2400. var tempPostId = 'stream-item-temp-post-i';
  2401. }
  2402. else {
  2403. var tempPostId = $('.temp-post').attr('id') + 'i';
  2404. }
  2405. var queetBox = $(this).parent().parent().siblings('.queet-box');
  2406. var queetBoxID = queetBox.attr('id');
  2407. // jquery's .text() function is not consistent in converting <br>:s to \n:s,
  2408. // so we do this detour to make sure line breaks are preserved.
  2409. // In firefox (and maybe some other browsers), queetBox.html() may have <div>s
  2410. // and may or may not have <br> in them.
  2411. // To deal with this, remove any <br>s right before </div> and then add ones.
  2412. queetBox.html(queetBox.html().replace(/(<br>)*<\/div>/g, '<br></div>').replace(/({|})/g, '!$1').replace(/<br>/g, '{{{lb}}}'));
  2413. var queetText = $.trim(queetBox.text().replace(/^\s+|\s+$/g, '').replace(/\n/g, ''));
  2414. queetText = queetText.replace(/{{{lb}}}/g, "\n").replace(/!({|})/g, '$1');
  2415. var queetTempText = replaceHtmlSpecialChars(queetText.replace(/\n/g,'<br>')); // no xss
  2416. queetTempText = queetTempText.replace(/&lt;br&gt;/g,'<br>'); // but preserve line breaks
  2417. var queetHtml = '<div id="' + tempPostId + '" class="stream-item conversation temp-post" style="opacity:1"><div class="queet"><span class="dogear"></span><div class="queet-content"><div class="stream-item-header"><a class="account-group"><img class="avatar" src="' + $('#user-avatar').attr('src') + '" /><strong class="name">' + $('#user-name').html() + '</strong> <span class="screen-name">@' + $('#user-screen-name').html() + '</span></a><small class="created-at"> ' + window.sL.posting + '</small></div><div class="queet-text">' + queetTempText + '</div><div class="stream-item-footer"><ul class="queet-actions"><li class="action-reply-container"><a class="with-icn"><span class="icon sm-reply" title="' + window.sL.replyVerb + '"></span></a></li><li class="action-del-container"><a class="with-icn"><span class="icon sm-trash" title="' + window.sL.deleteVerb + '"></span></a></li></i></li><li class="action-fav-container"><a class="with-icn"><span class="icon sm-fav" title="' + window.sL.favoriteVerb + '"></span></a></li></ul></div></div></div></div>';
  2418. queetHtml = detectRTL(queetHtml);
  2419. // popup reply
  2420. if($('.modal-container').find('.toolbar-reply button').length>0){
  2421. var in_reply_to_status_id = $('.modal-container').attr('id').substring(12);
  2422. }
  2423. // if this is a inline reply
  2424. else if(queetBox.parent().hasClass('inline-reply-queetbox')) {
  2425. var in_reply_to_status_id = queetBox.closest('.stream-item').attr('data-quitter-id');
  2426. }
  2427. // not a reply
  2428. else {
  2429. var in_reply_to_status_id = false;
  2430. }
  2431. // remove any popups
  2432. $('.modal-container').remove();
  2433. // try to find a queet to add the temp queet to:
  2434. var tempQueetInsertedInConversation = false;
  2435. // if the queet is in conversation, add it to parent's conversation
  2436. if($('.stream-item.replying-to').length > 0 && $('.stream-item.replying-to').hasClass('conversation')) {
  2437. var insertedTempQueet = $(queetHtml).appendTo($('.stream-item.replying-to').parent());
  2438. findAndMarkLastVisibleInConversation($('.stream-item.replying-to').parent());
  2439. insertedTempQueet.parent().children('.view-more-container-bottom').remove(); // remove any view-more-container-bottom:s, they only cause trouble at this point
  2440. tempQueetInsertedInConversation = true;
  2441. }
  2442. // if the queet is expanded, add it to its conversation
  2443. else if($('.stream-item.replying-to').length > 0 && $('.stream-item.replying-to').hasClass('expanded')) {
  2444. var insertedTempQueet = $(queetHtml).appendTo($('.stream-item.replying-to'));
  2445. findAndMarkLastVisibleInConversation($('.stream-item.replying-to'));
  2446. insertedTempQueet.parent().children('.view-more-container-bottom').remove(); // remove any view-more-container-bottom:s, they only cause trouble at this point
  2447. tempQueetInsertedInConversation = true;
  2448. }
  2449. // maybe the replying-to class is missing but we still have a suiting place to add it
  2450. else if($('.stream-item.expanded[data-quitter-id="' + in_reply_to_status_id + '"]').length > 0) {
  2451. var insertedTempQueet = $(queetHtml).appendTo($('.stream-item.expanded[data-quitter-id="' + in_reply_to_status_id + '"]'));
  2452. findAndMarkLastVisibleInConversation($('.stream-item.expanded[data-quitter-id="' + in_reply_to_status_id + '"]'));
  2453. insertedTempQueet.parent().children('.view-more-container-bottom').remove(); // remove any view-more-container-bottom:s, they only cause trouble at this point
  2454. tempQueetInsertedInConversation = true;
  2455. }
  2456. // if we can't find a proper place, add it to top and remove conversation class
  2457. // if this is either 1) our home/all feed, 2) our user timeline or 3) whole site or 4) whole network
  2458. else if(window.currentStreamObject.name == 'friends timeline'
  2459. || window.currentStreamObject.name == 'my profile'
  2460. || window.currentStreamObject.name == 'public timeline'
  2461. || window.currentStreamObject.name == 'public and external timeline') {
  2462. var insertedTempQueet = $(queetHtml).prependTo('#feed-body');
  2463. insertedTempQueet.removeClass('conversation');
  2464. }
  2465. // don't add it to the current stream, open a popup instead, without conversation class
  2466. else {
  2467. popUpAction('popup-sending','','',false);
  2468. var insertedTempQueet = $(queetHtml).prependTo($('#popup-sending').find('.modal-body'));
  2469. insertedTempQueet.removeClass('conversation');
  2470. }
  2471. // maybe post queet in groups
  2472. var postToGroups = '';
  2473. var postToGropsArray = new Array();
  2474. $.each(queetBox.siblings('.post-to-group'),function(){
  2475. postToGropsArray.push($(this).data('group-id'));
  2476. });
  2477. if(postToGropsArray.length > 0) {
  2478. postToGroups = postToGropsArray.join(':');
  2479. }
  2480. // remove any post-to-group-divs
  2481. queetBox.siblings('.post-to-group').remove();
  2482. // remove any replying-to classes
  2483. $('.stream-item').removeClass('replying-to');
  2484. // null reply box
  2485. collapseQueetBox(queetBox);
  2486. // check for new queets (one second from) NOW
  2487. setTimeout('checkForNewQueets()', 1000);
  2488. // post queet
  2489. postQueetToAPI(queetText, in_reply_to_status_id, postToGroups, function(data){ if(data) {
  2490. var queetHtml = buildQueetHtml(data, data.id, 'visible posted-from-form', false, tempQueetInsertedInConversation);
  2491. // while we were waiting for our posted queet to arrive here, it may have already
  2492. // arrived in the automatic update of the feed, so if it's already there, we
  2493. // replace it (but not if the temp queet is inserted in a conversation of course, or if
  2494. // the user has had time to expand it)
  2495. var alredyArrived = $('#feed-body > .stream-item[data-quitter-id-in-stream=' + data.id + ']');
  2496. if(alredyArrived.length > 0 && tempQueetInsertedInConversation === false) {
  2497. if(!alredyArrived.hasClass('expanded')) {
  2498. alredyArrived.replaceWith(queetHtml);
  2499. }
  2500. }
  2501. else {
  2502. var newInsertedQueet = $(queetHtml).insertBefore(insertedTempQueet);
  2503. findAndMarkLastVisibleInConversation(insertedTempQueet.parent());
  2504. // make ranting easier, move the reply-form to this newly created notice
  2505. // if we have not started writing in it, or if it's missing
  2506. // only if this is an expanded conversation
  2507. // and only if we're ranting, i.e. no replies the queetbox
  2508. var parentQueetBox = insertedTempQueet.parent().find('.inline-reply-queetbox');
  2509. if(parentQueetBox.length == 0
  2510. || parentQueetBox.children('.syntax-middle').css('display') == 'none') {
  2511. if(insertedTempQueet.parent().hasClass('expanded') || insertedTempQueet.parent().hasClass('conversation')) {
  2512. if(parentQueetBox.children('.queet-box').attr('data-replies-text') == '') {
  2513. insertedTempQueet.parent().find('.inline-reply-queetbox').remove();
  2514. newInsertedQueet.children('.queet').append(replyFormHtml(newInsertedQueet,newInsertedQueet.attr('data-quitter-id')));
  2515. }
  2516. }
  2517. }
  2518. }
  2519. // remove temp queet
  2520. insertedTempQueet.remove();
  2521. // clear queetbox input cache
  2522. localStorageObjectCache_STORE('queetBoxInput',queetBox.attr('id'),false);
  2523. // queet count
  2524. $('#user-queets strong').html(parseInt($('#user-queets strong').html(),10)+1);
  2525. // fadeout any posting-popups
  2526. setTimeout(function(){
  2527. $('#popup-sending').fadeOut(1000, function(){
  2528. $('#popup-sending').remove();
  2529. });
  2530. },100);
  2531. }});
  2532. }
  2533. });
  2534. /* ·
  2535. ·
  2536. · Count chars in queet box on keyup, also check for any attachments to show/hide
  2537. ·
  2538. · · · · · · · · · · · · · */
  2539. $('body').on('keyup input paste','.queet-box-syntax',function () {
  2540. countCharsInQueetBox($(this),$(this).siblings('.queet-toolbar').find('.queet-counter'),$(this).siblings('.queet-toolbar').find('.queet-button button'));
  2541. var attachments = $(this).siblings('.upload-image-container');
  2542. $.each(attachments,function(k,attachment){
  2543. var attachmentShorturl = $(attachment).children('img').attr('data-shorturl');
  2544. if($(attachment).siblings('.queet-box-syntax').text().indexOf(attachmentShorturl) > -1) {
  2545. $(attachment).show();
  2546. }
  2547. else {
  2548. $(attachment).hide();
  2549. }
  2550. });
  2551. });
  2552. /* ·
  2553. ·
  2554. · Middle button expands queet box
  2555. ·
  2556. · · · · · · · · · · · · · */
  2557. $('body').on('mousedown','.queet-box-syntax',function (e) {
  2558. if( e.which == 2 ) {
  2559. e.preventDefault();
  2560. $(this).trigger('click');
  2561. }
  2562. });
  2563. /* ·
  2564. ·
  2565. · Shorten URL's
  2566. ·
  2567. · · · · · · · · · · · · · */
  2568. $('body').on('click','button.shorten',function () {
  2569. shortenUrlsInBox($(this));
  2570. });
  2571. /* ·
  2572. ·
  2573. · Reload current stream
  2574. ·
  2575. · · · · · · · · · · · · · */
  2576. $('body').on('click','.reload-stream',function () {
  2577. reloadCurrentStream();
  2578. });
  2579. // can be used a callback too, e.g. from profile pref toggles
  2580. function reloadCurrentStream() {
  2581. setNewCurrentStream(URLtoStreamRouter(window.location.href),false,false,false);
  2582. }
  2583. /* ·
  2584. ·
  2585. · Reload current stream and clear cache
  2586. ·
  2587. · · · · · · · · · · · · · */
  2588. function reloadCurrentStreamAndClearCache() {
  2589. $('#feed-body').empty();
  2590. rememberStreamStateInLocalStorage();
  2591. // reload
  2592. reloadCurrentStream();
  2593. }
  2594. /* ·
  2595. ·
  2596. · Expand/collapse queet box on click and blur
  2597. ·
  2598. · · · · · · · · · · · · · */
  2599. $('body').on('click contextmenu','.queet-box-syntax',function () {
  2600. if($(this).html() == decodeURIComponent($(this).attr('data-start-text'))) {
  2601. $(this).attr('contenteditable','true');
  2602. $(this).focus();
  2603. $(this).siblings('.syntax-middle').html('&nbsp;');
  2604. $(this).siblings('.syntax-two').html('&nbsp;');
  2605. $(this).siblings('.queet-toolbar').css('display','block');
  2606. $(this).siblings('.syntax-middle').css('display','block');
  2607. $(this).siblings('.mentions-suggestions').css('display','block');
  2608. $(this).siblings('.syntax-two').css('display','block');
  2609. $(this).siblings('.queet-toolbar').find('.queet-button button').addClass('disabled');
  2610. countCharsInQueetBox($(this),$(this).siblings('.queet-toolbar .queet-counter'),$(this).siblings('.queet-toolbar button'));
  2611. $(this)[0].addEventListener("paste", stripHtmlFromPaste);
  2612. if(typeof $(this).attr('data-replies-text') != 'undefined') {
  2613. $(this).html(decodeURIComponent($(this).attr('data-replies-text')));
  2614. var repliesLen = decodeURIComponent($(this).attr('data-replies-text')).replace('&nbsp;',' ').length;
  2615. setSelectionRange($(this)[0], repliesLen, repliesLen);
  2616. }
  2617. else {
  2618. $(this).html('');
  2619. }
  2620. $(this).trigger('input');
  2621. $(this).closest('.stream-item').addClass('replying-to');
  2622. }
  2623. });
  2624. $('body').on('mousedown','.syntax-two',function () {
  2625. $(this).addClass('clicked');
  2626. });
  2627. $('body').on('blur','.queet-box-syntax',function (e) {
  2628. // empty the mention suggestions on blur, timeout because we want to capture clicks in .mentions-suggestions
  2629. setTimeout(function(){
  2630. $(this).siblings('.mentions-suggestions').empty();
  2631. },10);
  2632. // don't collapse if a toolbar button has been clicked
  2633. var clickedToolbarButtons = $(this).siblings('.queet-toolbar').find('button.clicked');
  2634. if(clickedToolbarButtons.length>0) {
  2635. clickedToolbarButtons.removeClass('clicked');
  2636. return true;
  2637. }
  2638. // don't collapse if an error message discard button has been clicked
  2639. if($(this).siblings('.error-message').children('.discard-error-message').length>0) {
  2640. return true;
  2641. }
  2642. // don't collapse if we're clicking around inside queet-box
  2643. var syntaxTwoBox = $(this).siblings('.syntax-two');
  2644. if(syntaxTwoBox.hasClass('clicked')) {
  2645. syntaxTwoBox.removeClass('clicked');
  2646. return true;
  2647. }
  2648. // don't collapse if we're in a modal
  2649. if($(this).parent().parent().hasClass('modal-body')) {
  2650. return true;
  2651. }
  2652. // collapse if nothing is change
  2653. if($(this).attr('data-replies-text') != 'undefined') {
  2654. var $startText = $('<div/>').append(decodeURIComponent($(this).attr('data-replies-text')));
  2655. if($.trim($startText.text()) == $.trim($(this).text()) || $(this).html().length == 0 || $(this).html() == '<br>' || $(this).html() == '<br />' || $(this).html() == '&nbsp;' || $(this).html() == '&nbsp;<br>') {
  2656. collapseQueetBox($(this));
  2657. }
  2658. }
  2659. // collapse if empty
  2660. else if($(this).html().length == 0 || $(this).html() == '<br>' || $(this).html() == '<br />' || $(this).html() == '&nbsp;' || $(this).html() == '&nbsp;<br>') {
  2661. collapseQueetBox($(this));
  2662. }
  2663. });
  2664. function collapseQueetBox(qB) {
  2665. qB.closest('.stream-item').removeClass('replying-to');
  2666. qB.siblings('.upload-image-container').remove();
  2667. qB.siblings('.syntax-middle').css('display','none');
  2668. qB.siblings('.syntax-two').css('display','none');
  2669. qB.siblings('.mentions-suggestions').css('display','none');
  2670. qB.attr('contenteditable','false');
  2671. qB.html(decodeURIComponent(qB.attr('data-start-text')));
  2672. qB.siblings('.queet-toolbar').find('button').removeClass('enabled');
  2673. qB.siblings('.queet-toolbar').css('display','none');
  2674. qB.removeAttr('style');
  2675. qB[0].removeEventListener("paste", stripHtmlFromPaste);
  2676. }
  2677. /* ·
  2678. ·
  2679. · Syntax highlighting in queetbox
  2680. ·
  2681. · · · · · · · · · · · · · */
  2682. // transfer focus and position/selection to background div
  2683. $('body').on('mouseup', 'div.syntax-two', function(e){
  2684. // don't transfer rightclicks, instead wait for oninput and transfer after
  2685. // this makes spell checker work
  2686. if( e.which == 3 ) {
  2687. $(this)[0].oninput = function() {
  2688. $(this).siblings('div.queet-box-syntax').html($(this).html());
  2689. $(this).trigger('mouseup'); // transfer focus
  2690. }
  2691. }
  2692. else {
  2693. $(this).removeClass('clicked');
  2694. var caretPos = getSelectionInElement($(this)[0]);
  2695. var thisQueetBox = $(this).siblings('div.queet-box-syntax');
  2696. thisQueetBox.focus();
  2697. setSelectionRange(thisQueetBox[0], caretPos[0], caretPos[1]);
  2698. // fixes problem with caret not showing after delete, unfocus and refocus
  2699. if(thisQueetBox.html() == '<br>') {
  2700. thisQueetBox.html(' ');
  2701. }
  2702. }
  2703. });
  2704. // strip html from paste
  2705. function stripHtmlFromPaste(e) {
  2706. e.preventDefault();
  2707. var text = replaceHtmlSpecialChars(e.clipboardData.getData("text/plain"));
  2708. text = text.replace(/\n/g,'<br>').replace(/\t/g, '&nbsp;&nbsp;&nbsp;&nbsp;'); // keep line-breaks and tabs
  2709. document.execCommand("insertHTML", false, text);
  2710. }
  2711. // sync divs
  2712. $('body').on('keyup paste input', 'div.queet-box-syntax', function() {
  2713. var currentVal = $(this).html();
  2714. currentVal = currentVal.replace(/<br>$/, '').replace(/&nbsp;$/, '').replace(/ $/, ''); // fix
  2715. $(this).siblings('.syntax-two').html(currentVal);
  2716. // If user click post area before load finished (top gnu logo), this is undefined.
  2717. if (!window.syntaxHighlightingRegexps) {
  2718. window.syntaxHighlightingRegexps = Object();
  2719. }
  2720. // loop through the regexps and highlight
  2721. $.each(window.syntaxHighlightingRegexps,function(k,v){
  2722. var i = 0;
  2723. while(currentVal.match(v) && i < 100) { // 100 matches is enough, we don't want to get caught in an infinite loop here
  2724. var currentMatch = currentVal.match(v);
  2725. // too small match, probably a single ! or something, just replace that with its html code
  2726. if($.trim(currentMatch[0]).length < 2) {
  2727. currentVal = currentVal.replace(currentMatch[0],currentMatch[0].replace('#','&#35;').replace('@','&#64;').replace('.','&#046;').replace('!','&#33;'));
  2728. }
  2729. // long enough match, create a mention span
  2730. else {
  2731. // don't include ending char, if any of these (but tags can contain and end with . and -)
  2732. if(currentMatch[0].slice(-1) == '<'
  2733. || currentMatch[0].slice(-1) == '&'
  2734. || currentMatch[0].slice(-1) == '?'
  2735. || currentMatch[0].slice(-1) == '!'
  2736. || currentMatch[0].slice(-1) == ' '
  2737. || (currentMatch[0].slice(-1) == '-' && k != 'tag')
  2738. || currentMatch[0].slice(-1) == ':'
  2739. || (currentMatch[0].slice(-1) == '.' && k != 'tag')
  2740. || currentMatch[0].slice(-1) == ','
  2741. || currentMatch[0].slice(-1) == ')'
  2742. || currentMatch[0].slice(-1) == '\'') {
  2743. currentMatch[0] = currentMatch[0].slice(0,-1);
  2744. }
  2745. // don't include these start strings
  2746. if(currentMatch[0].substring(0,1) == ' '
  2747. || currentMatch[0].substring(0,1) == '(') {
  2748. currentMatch[0] = currentMatch[0].substring(1);
  2749. }
  2750. else if(currentMatch[0].substring(0,6) == '&nbsp;') {
  2751. currentMatch[0] = currentMatch[0].substring(6);
  2752. }
  2753. currentVal = currentVal.replace(currentMatch[0],'<span class="' + k + '">' + currentMatch[0].replace('#','&#35;').replace('@','&#64;').replace('.','&#046;').replace('!','&#33;') + '</span>')
  2754. }
  2755. i++;
  2756. }
  2757. });
  2758. // safari fix
  2759. if(typeof bowser.safari != 'undefined') {
  2760. currentVal = currentVal.replace(/&nbsp;<span/g,' <span');
  2761. }
  2762. $(this).siblings('.syntax-middle').html(currentVal);
  2763. });
  2764. /* ·
  2765. ·
  2766. · Auto suggest mentions in queet-box
  2767. ·
  2768. · · · · · · · · · · · · · */
  2769. // navigate in mentions with mouse
  2770. $('body').on('mouseenter', '.mentions-suggestions > div', function(){
  2771. $('.mentions-suggestions > div').removeClass('selected');
  2772. $(this).addClass('selected');
  2773. }).on('mouseleave', '.mentions-suggestions > div', function(){
  2774. $(this).removeClass('selected');
  2775. });
  2776. $('body').on('click', '.mentions-suggestions > div', function(){
  2777. $(this).parent().siblings('.queet-box-syntax').focus();
  2778. $(this).siblings().removeClass('selected');
  2779. $(this).addClass('selected');
  2780. useSelectedMention($(this).parent().siblings('.queet-box-syntax'));
  2781. });
  2782. // navigate in mentions with keyboard
  2783. $('body').on('keydown', '.queet-box-syntax', function(e) {
  2784. if($(this).siblings('.mentions-suggestions').children('div').length > 0) {
  2785. // enter or tab
  2786. if (!e.ctrlKey && (e.keyCode == '13' || e.keyCode == '9')) {
  2787. e.preventDefault();
  2788. useSelectedMention($(this));
  2789. }
  2790. // downkey
  2791. else if (e.keyCode == '40') {
  2792. e.preventDefault();
  2793. if($(this).siblings('.mentions-suggestions').children('div.selected').length > 0) {
  2794. var selected = $(this).siblings('.mentions-suggestions').children('div.selected');
  2795. selected.removeClass('selected');
  2796. selected.next().addClass('selected');
  2797. }
  2798. else {
  2799. $(this).siblings('.mentions-suggestions').children('div').first().addClass('selected');
  2800. }
  2801. }
  2802. // upkey
  2803. else if (e.keyCode == '38') {
  2804. e.preventDefault();
  2805. if($(this).siblings('.mentions-suggestions').children('div.selected').length > 0) {
  2806. var selected = $(this).siblings('.mentions-suggestions').children('div.selected');
  2807. selected.removeClass('selected');
  2808. selected.prev().addClass('selected');
  2809. }
  2810. else {
  2811. $(this).siblings('.mentions-suggestions').children('div').last().addClass('selected');
  2812. }
  2813. }
  2814. }
  2815. });
  2816. function useSelectedMention(queetBox){
  2817. // use selected
  2818. if(queetBox.siblings('.mentions-suggestions').children('div.selected').length > 0) {
  2819. var selectedSuggestion = queetBox.siblings('.mentions-suggestions').children('div.selected');
  2820. }
  2821. // if none selected, take top suggestion
  2822. else {
  2823. var selectedSuggestion = queetBox.siblings('.mentions-suggestions').children('div').first();
  2824. }
  2825. var username = selectedSuggestion.children('span').html();
  2826. var name = selectedSuggestion.children('strong').html();
  2827. // if this is a group, we remember its id, the user might be member of multiple groups with the same username
  2828. if(selectedSuggestion.hasClass('group-suggestion')) {
  2829. var groupId = selectedSuggestion.data('group-id');
  2830. if(queetBox.siblings('.post-to-group[data-group-id="' + groupId + '"]').length < 1) {
  2831. if(queetBox.siblings('.post-to-group').length>0) {
  2832. var addAfter = queetBox.siblings('.post-to-group').last();
  2833. }
  2834. else {
  2835. var addAfter = queetBox;
  2836. }
  2837. addAfter.after('<div class="post-to-group" data-group-username="' + username + '" data-group-id="' + groupId + '">' + name + '</div>');
  2838. }
  2839. }
  2840. // replace the halfwritten username with the one we want
  2841. deleteBetweenCharacterIndices(queetBox[0], window.lastMention.mentionPos+1, window.lastMention.cursorPos);
  2842. var range = createRangeFromCharacterIndices(queetBox[0], window.lastMention.mentionPos+1, window.lastMention.mentionPos+1);
  2843. range.insertNode(document.createTextNode(username + '\u00a0')); // non-breaking-space, to prevent collapse
  2844. // put caret after
  2845. setSelectionRange(queetBox[0], window.lastMention.mentionPos+username.length+2, window.lastMention.mentionPos+username.length+2);
  2846. queetBox.siblings('.mentions-suggestions').empty();
  2847. queetBox.trigger('input'); // avoid some flickering
  2848. }
  2849. // check for removed group mentions
  2850. $('body').on('keyup', 'div.queet-box-syntax', function(e) {
  2851. var groupMentions = $(this).siblings('.post-to-group');
  2852. var queetBoxGroups = $(this).siblings('.syntax-middle').find('.group');
  2853. var queetBoxGroupsString = '';
  2854. $.each(queetBoxGroups,function(){
  2855. queetBoxGroupsString = queetBoxGroupsString + $(this).html() + ':';
  2856. });
  2857. $.each(groupMentions,function(){
  2858. if(queetBoxGroupsString.indexOf('!' + $(this).data('group-username') + ':') == -1) {
  2859. $(this).remove();
  2860. }
  2861. });
  2862. });
  2863. window.lastMention = new Object();
  2864. // check for user mentions
  2865. $('body').on('keyup', 'div.queet-box-syntax', function(e) { checkMentions(e, true);});
  2866. // check for group mentions
  2867. $('body').on('keyup', 'div.queet-box-syntax', function(e) { checkMentions(e, false);});
  2868. /**
  2869. * check for user/group mentions
  2870. *
  2871. * @param {Object} e: Event object
  2872. * @param {boolean} isUser
  2873. */
  2874. function checkMentions(e, isUser) {
  2875. var mark = '!';
  2876. var prefix = 'group';
  2877. if (isUser) {
  2878. mark = '@';
  2879. prefix = 'user';
  2880. }
  2881. // var queetBox = $(this);
  2882. var queetBox = $('body div.queet-box-syntax');
  2883. var cursorPosArray = getSelectionInElement(queetBox[0]);
  2884. var cursorPos = cursorPosArray[0];
  2885. if(e.keyCode != '40' && e.keyCode != '38' && e.keyCode != '13' && e.keyCode != '9') {
  2886. var contents = queetBox.text().substring(0,cursorPos);
  2887. var mentionPos = contents.lastIndexOf(mark);
  2888. var check_contents = contents.substring(mentionPos - 1, cursorPos);
  2889. var regex = new RegExp('(^|\s|\.|\n)(' + mark + ')[a-zA-Z0-9]+');
  2890. var match = check_contents.match(regex);
  2891. if (contents.indexOf(mark) >= 0 && match) {
  2892. if(contents.lastIndexOf(mark) > 1) {
  2893. match[0] = match[0].substring(1,match[0].length);
  2894. }
  2895. if((contents.lastIndexOf(mark)+match[0].length) == cursorPos) {
  2896. queetBox.siblings('.mentions-suggestions').children('.' + prefix + '-suggestion').remove();
  2897. queetBox.siblings('.mentions-suggestions').css('top',(queetBox.height()+20) + 'px');
  2898. var term = match[0].substring(match[0].lastIndexOf(mark)+1, match[0].length).toLowerCase();
  2899. window.lastMention.mentionPos = mentionPos;
  2900. window.lastMention.cursorPos = cursorPos;
  2901. // see if any user/group we're following matches
  2902. var suggestionsToShow = [];
  2903. var suggestionsUsernameCount = {};
  2904. if (isUser) {
  2905. suggestionsUsernameCount[window.loggedIn.screen_name] = 1; // any suggestions with the same screen name as mine will get their server url added
  2906. }
  2907. var targets = isUser ? (window.following) : (window.groupMemberships);
  2908. $.each(targets, function(){
  2909. var userregex = new RegExp(term);
  2910. if(this.username.toLowerCase().match(userregex) || this.name.toLowerCase().match(userregex)) {
  2911. var suggestion = {avatar:this.avatar, name:this.name, username:this.username,url:this.url};
  2912. if (!isUser) {
  2913. suggestion.id = this.id;
  2914. }
  2915. suggestionsToShow.push(suggestion);
  2916. // count the usernames to see if we need to show the server for any of them
  2917. if(typeof suggestionsUsernameCount[this.username] != 'undefined') {
  2918. suggestionsUsernameCount[this.username] = suggestionsUsernameCount[this.username] + 1;
  2919. }
  2920. else {
  2921. suggestionsUsernameCount[this.username] = 1;
  2922. }
  2923. }
  2924. });
  2925. // show matches
  2926. $.each(suggestionsToShow,function(){
  2927. var serverHtml = '';
  2928. if(suggestionsUsernameCount[this.username]>1 && this.url !== false) {
  2929. serverHtml = isUser ? ('@' + this.url) : (this.url + '/group/');
  2930. }
  2931. if (isUser) {
  2932. queetBox.siblings('.mentions-suggestions').append('<div class="user-suggestion" title="@' + this.username + serverHtml + '"><img height="24" width="24" src="' + this.avatar + '" /><strong>' + this.name + '</strong> @<span>' + this.username + serverHtml + '</span></div>')
  2933. }
  2934. else {
  2935. queetBox.siblings('.mentions-suggestions').append('<div class="group-suggestion" title="' + serverHtml + this.username + '" data-group-id="' + this.id + '"><img height="24" width="24" src="' + this.avatar + '" /><strong>' + this.name + '</strong> !<span>' + this.username + '</span></div>')
  2936. }
  2937. });
  2938. }
  2939. else {
  2940. queetBox.siblings('.mentions-suggestions').children('.' + prefix + '-suggestion').remove();
  2941. }
  2942. }
  2943. else {
  2944. queetBox.siblings('.mentions-suggestions').children('.' + prefix + '-suggestion').remove();
  2945. }
  2946. }
  2947. }
  2948. /* ·
  2949. ·
  2950. · Any click empties the mentions-suggestions
  2951. ·
  2952. · · · · · · · · · · · · · */
  2953. $(document).click(function() {
  2954. $('.mentions-suggestions').empty();
  2955. });
  2956. /* ·
  2957. ·
  2958. · Store unposted queets in cache, if the user accidentally reloads the page or something
  2959. ·
  2960. · · · · · · · · · · · · · */
  2961. $('body').on('keyup', 'div.queet-box-syntax', function(e) {
  2962. var thisId = $(this).attr('id');
  2963. var thisText = $.trim($(this).text());
  2964. // keep in global var to avoid doing all these operations every keystroke
  2965. if(typeof window.queetBoxCurrentlyActive == 'undefined'
  2966. || window.queetBoxCurrentlyActive.id != thisId) {
  2967. window.queetBoxCurrentlyActive = {
  2968. id: thisId,
  2969. startText: $.trim($('<div/>').append(decodeURIComponent($(this).attr('data-start-text'))).text()),
  2970. repliesText: $.trim($('<div/>').append(decodeURIComponent($(this).attr('data-replies-text'))).text())
  2971. };
  2972. }
  2973. // remove from cache if empty, or same as default text
  2974. if(thisText == ''
  2975. || thisText == window.sL.compose
  2976. || thisText == window.queetBoxCurrentlyActive.startText
  2977. || thisText == window.queetBoxCurrentlyActive.repliesText) {
  2978. localStorageObjectCache_STORE('queetBoxInput',thisId,false);
  2979. }
  2980. else {
  2981. localStorageObjectCache_STORE('queetBoxInput',thisId,$(this).html());
  2982. }
  2983. });
  2984. /* ·
  2985. ·
  2986. · Keyboard shortcuts
  2987. ·
  2988. · · · · · · · · · · · · · */
  2989. // menu
  2990. $('#shortcuts-link').click(function(){
  2991. // not if disabled
  2992. if($(this).hasClass('disabled')) {
  2993. return true;
  2994. }
  2995. popUpAction('popup-shortcuts', window.sL.keyboardShortcuts,'<div id="shortcuts-container"></div>',false);
  2996. getDoc('shortcuts',function(html){
  2997. $('#shortcuts-container').html(html);
  2998. centerPopUp($('#popup-shortcuts').find('.modal-draggable'));
  2999. });
  3000. });
  3001. // send queet on ctrl+enter or ⌘+enter (mac)
  3002. $('body').on('keydown','.queet-box-syntax',function (e) {
  3003. // do nothing if shortcuts are disabled
  3004. if(window.disableKeyboardShortcuts === true) {
  3005. return true;
  3006. }
  3007. if((e.ctrlKey && e.which == 13)
  3008. || (e.metaKey && e.which == 13)) {
  3009. e.preventDefault();
  3010. var pressThisButton = $(this).siblings('.queet-toolbar').children('.queet-button').children('button');
  3011. pressThisButton.click();
  3012. $(this).blur();
  3013. }
  3014. });
  3015. $('body').keyup(function (e) {
  3016. // do nothing if shortcuts are disabled
  3017. if(window.disableKeyboardShortcuts === true) {
  3018. return true;
  3019. }
  3020. // only if queetbox is blurred, and we're not typing in any input, and we're logged in
  3021. if($('.queet-box-syntax[contenteditable="true"]').length == 0
  3022. && $(":focus").length == 0
  3023. && window.loggedIn !== false) {
  3024. // shortcuts documentation on '?'
  3025. if(e.shiftKey && (e.which == 171 || e.which == 191)) {
  3026. $('#shortcuts-link').click();
  3027. }
  3028. // queet box popup on 'n'
  3029. else if(e.which == 78) { // n
  3030. e.preventDefault();
  3031. var pressThis = $('#top-compose')
  3032. pressThis.click();
  3033. }
  3034. // select first queet on first selection, 'j' or 'k'
  3035. else if ((e.which == 74 || e.which == 75) && $('.stream-item.selected-by-keyboard').length == 0) {
  3036. $('#feed-body').children('.stream-item.visible').first().addClass('selected-by-keyboard');
  3037. }
  3038. // only if we have a selected queet
  3039. else if($('.stream-item.selected-by-keyboard').length == 1) {
  3040. var selectedQueet = $('#feed-body').children('.stream-item.selected-by-keyboard');
  3041. // next queet on 'j'
  3042. if(e.which == 74) {
  3043. selectedQueet.removeClass('selected-by-keyboard');
  3044. var next = selectedQueet.nextAll('.visible').not('.always-hidden').first();
  3045. next.addClass('selected-by-keyboard');
  3046. scrollToQueet(next);
  3047. }
  3048. // prev queet on 'k'
  3049. else if(e.which == 75) {
  3050. selectedQueet.removeClass('selected-by-keyboard');
  3051. var prev = selectedQueet.prevAll('.visible').not('.always-hidden').first();
  3052. prev.addClass('selected-by-keyboard');
  3053. scrollToQueet(prev);
  3054. }
  3055. // fav queet on 'f'
  3056. else if(e.which == 70) {
  3057. selectedQueet.children('.queet').find('.icon.sm-fav').click();
  3058. }
  3059. // rq queet on 't'
  3060. else if(e.which == 84) {
  3061. selectedQueet.children('.queet').find('.icon.sm-rt:not(.is-mine)').click();
  3062. }
  3063. // expand/collapse queet on enter
  3064. else if(e.which == 13) {
  3065. selectedQueet.children('.queet').click();
  3066. }
  3067. // reply to queet on 'r'
  3068. else if(e.which == 82) {
  3069. if(selectedQueet.hasClass('expanded')) {
  3070. selectedQueet.find('.queet-box-syntax').click();
  3071. }
  3072. else {
  3073. selectedQueet.children('.queet').find('.icon.sm-reply').click();
  3074. }
  3075. }
  3076. }
  3077. }
  3078. });
  3079. /* ·
  3080. ·
  3081. · When clicking show more links, walk upwards or downwards
  3082. ·
  3083. · · · · · · · · · · · · · */
  3084. $('body').on('click','.view-more-container-bottom', function(){
  3085. var thisParentStreamItem = $(this).parent('.stream-item');
  3086. findReplyToStatusAndShow(thisParentStreamItem, thisParentStreamItem.attr('data-quitter-id'),$(this).attr('data-replies-after'));
  3087. $(this).remove();
  3088. findAndMarkLastVisibleInConversation(thisParentStreamItem);
  3089. });
  3090. $('body').on('click','.view-more-container-top', function(){
  3091. var this_qid = $(this).closest('.stream-item:not(.conversation)').attr('data-quitter-id');
  3092. var queet = $(this).siblings('.queet');
  3093. var thisParentStreamItem = $(this).parent('.stream-item');
  3094. rememberMyScrollPos(queet,'moretop' + this_qid);
  3095. findInReplyToStatusAndShow(thisParentStreamItem, thisParentStreamItem.attr('data-quitter-id'),$(this).attr('data-trace-from'),false,true);
  3096. $(this).remove();
  3097. backToMyScrollPos(queet,'moretop' + this_qid,false);
  3098. // remove the "show full conversation" link if nothing more to show
  3099. if(thisParentStreamItem.find('.hidden-conversation').length == 0) {
  3100. thisParentStreamItem.children('.queet').find('.show-full-conversation').remove();
  3101. }
  3102. findAndMarkLastVisibleInConversation(thisParentStreamItem);
  3103. });
  3104. /* ·
  3105. ·
  3106. · When clicking "show full conversation", show all hidden queets in conversation
  3107. ·
  3108. · · · · · · · · · · · · · */
  3109. $('body').on('click','.show-full-conversation',function(){
  3110. var this_q = $(this).closest('.queet');
  3111. var thisStreamItem = this_q.parent();
  3112. var this_qid = thisStreamItem.attr('data-quitter-id');
  3113. rememberMyScrollPos(this_q,this_qid);
  3114. thisStreamItem.find('.view-more-container-top').remove();
  3115. thisStreamItem.find('.view-more-container-bottom').remove();
  3116. $.each(thisStreamItem.find('.hidden-conversation'),function(key,obj){
  3117. $(obj).removeClass('hidden-conversation');
  3118. $(obj).animate({opacity:'1'},400,function(){
  3119. $(obj).css('background-color','pink').animate({backgroundColor:'#F6F6F6'},1000);
  3120. });
  3121. });
  3122. $(this).remove();
  3123. backToMyScrollPos(this_q,this_qid,false);
  3124. findAndMarkLastVisibleInConversation(thisStreamItem);
  3125. });
  3126. /* ·
  3127. ·
  3128. · Edit profile
  3129. ·
  3130. · · · · · · · · · · · · · */
  3131. $('body').on('click','#page-container > .profile-card .edit-profile-button',function(){
  3132. if(!$(this).hasClass('disabled')) {
  3133. $(this).addClass('disabled');
  3134. $('html').scrollTop(0);
  3135. $('html').addClass('fixed');
  3136. $('body').prepend('<div id="edit-profile-popup" class="modal-container"></div>');
  3137. display_spinner();
  3138. getFromAPI('users/show/' + window.loggedIn.screen_name + '.json', function(data){
  3139. remove_spinner();
  3140. if(data){
  3141. data = cleanUpUserObject(data);
  3142. // use avatar if no cover photo
  3143. var coverPhotoHtml = '';
  3144. if(data.cover_photo !== false) {
  3145. coverPhotoHtml = 'background-image:url(\'' + data.cover_photo + '\')';
  3146. }
  3147. $('.hover-card,.hover-card-caret').remove();
  3148. $('#edit-profile-popup').prepend('\
  3149. <div class="edit-profile-container">\
  3150. <div class="upload-background-image"></div>\
  3151. <input type="file" name="background-image-input" id="background-image-input" />\
  3152. <div class="profile-card">\
  3153. <div class="profile-header-inner" style="' + coverPhotoHtml + '">\
  3154. <input type="file" name="cover-photo-input" id="cover-photo-input" />\
  3155. <div class="close-edit-profile-window"></div>\
  3156. <div class="upload-cover-photo"></div>\
  3157. <input type="file" name="avatar-input" id="avatar-input" />\
  3158. <div class="upload-avatar"></div>\
  3159. <div class="profile-header-inner-overlay"></div>\
  3160. <a class="profile-picture" href="' + data.profile_image_url_original + '"><img src="' + data.profile_image_url_profile_size + '" /></a>\
  3161. <div class="profile-card-inner">\
  3162. <input class="fullname" id="edit-profile-fullname" placeholder="' + window.sL.signUpFullName + '" data-start-value="' + data.name + '" value="' + data.name + '" />\
  3163. <h2 class="username"><span class="screen-name">@' + data.screen_name + '</span><span class="follow-status"></span></h2>\
  3164. <div class="bio-container">\
  3165. <textarea class="bio" id="edit-profile-bio" data-start-value="' + data.description + '" placeholder="' + window.sL.registerBio + '">' + data.description + '</textarea>\
  3166. </div>\
  3167. <p class="location-and-url">\
  3168. <input class="location" id="edit-profile-location" placeholder="' + window.sL.registerLocation + '" data-start-value="' + data.location + '" value="' + data.location + '" />\
  3169. <span class="divider"> · </span>\
  3170. <input class="url" id="edit-profile-url" placeholder="' + window.sL.registerHomepage + '" data-start-value="' + data.url + '" value="' + data.url + '" />\
  3171. </p>\
  3172. </div>\
  3173. </div>\
  3174. <div class="profile-banner-footer">\
  3175. <div class="color-selection">\
  3176. <label for="link-color-selection">' + window.sL.linkColor + '</label>\
  3177. <input id="link-color-selection" type="text" value="#' + window.loggedIn.linkcolor + '" />\
  3178. </div>\
  3179. <div class="color-selection">\
  3180. <label for="link-color-selection">' + window.sL.backgroundColor + '</label>\
  3181. <input id="background-color-selection" type="text" value="#' + window.loggedIn.backgroundcolor + '" />\
  3182. </div>\
  3183. <div class="user-actions">\
  3184. <button type="button" class="abort-edit-profile-button"><span class="button-text edit-profile-text">' + window.sL.cancelVerb + '</span>\
  3185. <button type="button" class="save-profile-button"><span class="button-text edit-profile-text">' + window.sL.saveChanges + '</span>\
  3186. <button type="button" class="crop-and-save-button"><span class="button-text edit-profile-text">' + window.sL.cropAndSave + '</span>\
  3187. </div>\
  3188. <div class="clearfix"></div>\
  3189. </div>\
  3190. </div>\
  3191. </div>');
  3192. $('#edit-profile-popup .profile-card').css('top',$('#page-container .profile-card').offset().top-53 + 'px'); // position exactly over
  3193. // save colors on change
  3194. $('#link-color-selection').minicolors({
  3195. change: function(hex) {
  3196. // pause for 500ms before saving and displaying color changes
  3197. window.changeToLinkColor = hex;
  3198. setTimeout(function(){
  3199. if(hex == window.changeToLinkColor) {
  3200. changeDesign({linkcolor:hex});
  3201. postNewLinkColor(hex.substring(1));
  3202. window.loggedIn.linkcolor = hex.substring(1);
  3203. }
  3204. },500);
  3205. }
  3206. });
  3207. $('#background-color-selection').minicolors({
  3208. change: function(hex) {
  3209. // pause for 500ms before saving and displaying color changes
  3210. window.changeToBackgroundColor = hex;
  3211. setTimeout(function(){
  3212. if(hex == window.changeToBackgroundColor) {
  3213. changeDesign({backgroundcolor:hex});
  3214. postNewBackgroundColor(hex.substring(1));
  3215. window.loggedIn.backgroundcolor = hex.substring(1);
  3216. }
  3217. },500);
  3218. }
  3219. });
  3220. // also on keyup in input (minicolors 'change' event does not do this, apparently)
  3221. $('#link-color-selection').on('keyup',function(){
  3222. keyupSetLinkColor($(this).val());
  3223. });
  3224. $('#background-color-selection').on('keyup',function(){
  3225. keyupSetBGColor($(this).val());
  3226. });
  3227. // check if profile info is change and show/hide buttons
  3228. $('input.fullname,textarea.bio,input.location,input.url').on('keyup paste input',function(){
  3229. showHideSaveProfileButtons();
  3230. });
  3231. }
  3232. else {
  3233. abortEditProfile();
  3234. }
  3235. });
  3236. }
  3237. });
  3238. // function to see if anything in profile is changed and show/hide buttons accordingly
  3239. function showHideSaveProfileButtons() {
  3240. if($('input.fullname').val() != $('input.fullname').attr('data-start-value')
  3241. || $('textarea.bio').val() != $('textarea.bio').attr('data-start-value')
  3242. || $('input.location').val() != $('input.location').attr('data-start-value')
  3243. || $('input.url').val() != $('input.url').attr('data-start-value')) {
  3244. $('.abort-edit-profile-button, .save-profile-button').show();
  3245. }
  3246. else {
  3247. $('.abort-edit-profile-button, .save-profile-button').hide();
  3248. }
  3249. }
  3250. // idle function for linkcolor selection by keyboard input
  3251. var keyupLinkColorTimer;
  3252. function keyupSetLinkColor(hex) {
  3253. clearTimeout(keyupLinkColorTimer);
  3254. keyupLinkColorTimer = setTimeout(function () {
  3255. $('#link-color-selection').minicolors('value',hex);
  3256. changeLinkColor($('#link-color-selection').val());
  3257. postNewLinkColor($('#link-color-selection').val().substring(1));
  3258. }, 500);
  3259. }
  3260. // idle function for bgcolor selection by keyboard input
  3261. var keyupBGColorTimer;
  3262. function keyupSetBGColor(hex) {
  3263. clearTimeout(keyupBGColorTimer);
  3264. keyupBGColorTimer = setTimeout(function () {
  3265. $('#background-color-selection').minicolors('value',hex);
  3266. $('body').css('background-color',$('#background-color-selection').val());
  3267. postNewBackgroundColor($('#background-color-selection').val().substring(1));
  3268. }, 500);
  3269. }
  3270. // cancel
  3271. $('body').on('click','.close-edit-profile-window',function(){
  3272. abortEditProfile();
  3273. });
  3274. $('body').on('click','.abort-edit-profile-button',function(){
  3275. // if this is the avatar or cover photo
  3276. if($('#edit-profile-popup .jwc_frame').length>0) {
  3277. cleanUpAfterCropping();
  3278. }
  3279. // if profile info
  3280. else {
  3281. abortEditProfile();
  3282. }
  3283. });
  3284. function abortEditProfile() {
  3285. $('#edit-profile-popup').remove();
  3286. $('.edit-profile-button').removeClass('disabled');
  3287. $('html').removeClass('fixed');
  3288. }
  3289. // validate
  3290. $('body').on('keyup paste input', '#edit-profile-popup input,#edit-profile-popup textarea', function() {
  3291. if(validateEditProfileForm($('#edit-profile-popup'))){
  3292. $('.save-profile-button').removeAttr('disabled');
  3293. $('.save-profile-button').removeClass('disabled');
  3294. }
  3295. else {
  3296. $('.save-profile-button').attr('disabled','disabled');
  3297. $('.save-profile-button').addClass('disabled');
  3298. }
  3299. });
  3300. // submit cover photo or avatar
  3301. $('body').on('click','.crop-and-save-button',function(){
  3302. if($('.crop-and-save-button').attr('disabled') != 'disabled') {
  3303. $('.crop-and-save-button').attr('disabled','disabled');
  3304. $('.crop-and-save-button').addClass('disabled');
  3305. display_spinner();
  3306. // if this is the cover photo
  3307. if($('#edit-profile-popup .jwc_frame.cover-photo-to-crop').length>0) {
  3308. var coverImgFormData = new FormData();
  3309. coverImgFormData.append('banner', $('#cover-photo-input')[0].files[0]);
  3310. coverImgFormData.append('height', window.jwc.result.cropH);
  3311. coverImgFormData.append('width', window.jwc.result.cropW);
  3312. coverImgFormData.append('offset_left', window.jwc.result.cropX);
  3313. coverImgFormData.append('offset_top', window.jwc.result.cropY);
  3314. $.ajax({
  3315. url: window.apiRoot + 'account/update_profile_banner.json',
  3316. type: "POST",
  3317. data: coverImgFormData,
  3318. processData: false,
  3319. contentType: false,
  3320. cache: false,
  3321. dataType: "json",
  3322. error: function(data){
  3323. console.log('error saving profile banner'); console.log(data);
  3324. $('.crop-and-save-button').removeAttr('disabled');
  3325. $('.crop-and-save-button').removeClass('disabled');
  3326. cleanUpAfterCropping();
  3327. remove_spinner();
  3328. },
  3329. success: function(data) {
  3330. remove_spinner();
  3331. if(typeof data.error == 'undefined') {
  3332. $('.crop-and-save-button').removeAttr('disabled');
  3333. $('.crop-and-save-button').removeClass('disabled');
  3334. cleanUpAfterCropping();
  3335. $('.profile-header-inner').css('background-image','url(' + data.url + ')');
  3336. $('#user-header').css('background-image','url(' + data.url + ')');
  3337. }
  3338. else {
  3339. alert('Try again! ' + data.error);
  3340. $('.crop-and-save-button').removeAttr('disabled');
  3341. $('.crop-and-save-button').removeClass('disabled');
  3342. }
  3343. }
  3344. });
  3345. }
  3346. // if this is the avatar
  3347. else if($('#edit-profile-popup .jwc_frame.avatar-to-crop').length>0) {
  3348. $.ajax({ url: window.apiRoot + 'qvitter/update_avatar.json',
  3349. type: "POST",
  3350. data: {
  3351. cropH: window.jwc.result.cropH,
  3352. cropW: window.jwc.result.cropW,
  3353. cropX: window.jwc.result.cropX,
  3354. cropY: window.jwc.result.cropY,
  3355. img: $('#avatar-to-crop').attr('src')
  3356. },
  3357. dataType:"json",
  3358. error: function(data){ console.log('error'); console.log(data); },
  3359. success: function(data) {
  3360. remove_spinner();
  3361. if(typeof data.error == 'undefined') {
  3362. $('.crop-and-save-button').removeAttr('disabled');
  3363. $('.crop-and-save-button').removeClass('disabled');
  3364. cleanUpAfterCropping();
  3365. $('.profile-picture').attr('href',data.profile_image_url_original);
  3366. $('.profile-picture img, #user-avatar').attr('src',data.profile_image_url_profile_size);
  3367. $('#settingslink .nav-session').css('background-image','url(\'' + data.profile_image_url_profile_size + '\')');
  3368. $('.account-group .name[data-user-id="' + window.loggedIn.id + '"]').siblings('.avatar').attr('src',data.profile_image_url_profile_size);
  3369. }
  3370. else {
  3371. alert('Try again! ' + data.error);
  3372. $('.crop-and-save-button').removeAttr('disabled');
  3373. $('.crop-and-save-button').removeClass('disabled');
  3374. }
  3375. }
  3376. });
  3377. }
  3378. // if this is the background-image
  3379. else if($('#edit-profile-popup .jwc_frame.background-to-crop').length>0) {
  3380. $.ajax({ url: window.apiRoot + 'qvitter/update_background_image.json',
  3381. type: "POST",
  3382. data: {
  3383. cropH: window.jwc.result.cropH,
  3384. cropW: window.jwc.result.cropW,
  3385. cropX: window.jwc.result.cropX,
  3386. cropY: window.jwc.result.cropY,
  3387. img: $('#background-to-crop').attr('src')
  3388. },
  3389. dataType:"json",
  3390. error: function(data){ console.log('error'); console.log(data); },
  3391. success: function(data) {
  3392. remove_spinner();
  3393. if(typeof data.error == 'undefined') {
  3394. $('.crop-and-save-button').removeAttr('disabled');
  3395. $('.crop-and-save-button').removeClass('disabled');
  3396. cleanUpAfterCropping();
  3397. changeDesign({backgroundimage:data.url});
  3398. window.loggedIn.background_image = data.url;
  3399. }
  3400. else {
  3401. alert('Try again! ' + data.error);
  3402. $('.crop-and-save-button').removeAttr('disabled');
  3403. $('.crop-and-save-button').removeClass('disabled');
  3404. }
  3405. }
  3406. });
  3407. }
  3408. }
  3409. });
  3410. // submit new profile info
  3411. $('body').on('click','.save-profile-button',function(){
  3412. if($('.save-profile-button').attr('disabled') != 'disabled') {
  3413. $('.save-profile-button').attr('disabled','disabled');
  3414. $('.save-profile-button').addClass('disabled');
  3415. display_spinner();
  3416. if(validateEditProfileForm($('#edit-profile-popup'))) {
  3417. $.ajax({ url: window.apiRoot + 'account/update_profile.json',
  3418. type: "POST",
  3419. data: {
  3420. name: $('#edit-profile-popup input.fullname').val(),
  3421. url: $('#edit-profile-popup input.url').val(),
  3422. location: $('#edit-profile-popup input.location').val(),
  3423. description: $('#edit-profile-popup textarea.bio').val(),
  3424. },
  3425. dataType:"json",
  3426. error: function(data){ console.log('error'); console.log(data); },
  3427. success: function(data) {
  3428. remove_spinner();
  3429. if(typeof data.error == 'undefined') {
  3430. location.reload(); // reload, hopefully the new profile is saved
  3431. }
  3432. else {
  3433. alert('Try again! ' + data.error);
  3434. $('.save-profile-button').removeAttr('disabled');
  3435. $('.save-profile-button').removeClass('disabled');
  3436. }
  3437. }
  3438. });
  3439. }
  3440. }
  3441. });
  3442. // cover photo, avatar and background image select and crop
  3443. $('body').on('click','.upload-cover-photo, .upload-avatar, .upload-background-image',function(){
  3444. var coverOrAvatar = $(this).attr('class');
  3445. if(coverOrAvatar == 'upload-cover-photo') {
  3446. var inputId = 'cover-photo-input'
  3447. }
  3448. else if(coverOrAvatar == 'upload-avatar') {
  3449. var inputId = 'avatar-input'
  3450. }
  3451. else if(coverOrAvatar == 'upload-background-image') {
  3452. var inputId = 'background-image-input'
  3453. }
  3454. $('#' + inputId).click(function(){ $(this).one('change',function(e){ // trick to make the change event only fire once when selecting a file
  3455. coverPhotoAndAvatarSelectAndCrop(e, coverOrAvatar);
  3456. })});
  3457. // trigger click
  3458. triggerClickOnInputFile($('#' + inputId));
  3459. });
  3460. // load image from file input
  3461. function coverPhotoAndAvatarSelectAndCrop(e, coverOrAvatar) {
  3462. if(coverOrAvatar == 'upload-cover-photo') {
  3463. var targetWidth = 588;
  3464. var targetHeight = 260;
  3465. var cropId = 'cover-photo-to-crop';
  3466. }
  3467. else if(coverOrAvatar == 'upload-avatar') {
  3468. var targetWidth = 220;
  3469. var targetHeight = 220;
  3470. var maxWidth = 1040;
  3471. var minWidth = 1040;
  3472. var cropId = 'avatar-to-crop';
  3473. }
  3474. else if(coverOrAvatar == 'upload-background-image') {
  3475. var targetWidth = $(window).width();
  3476. var targetHeight = $(window).height()-46;
  3477. var maxWidth = 3000;
  3478. var minWidth = 3000;
  3479. var cropId = 'background-to-crop';
  3480. }
  3481. // get orientation
  3482. loadImage.parseMetaData(e.target.files[0], function (data) {
  3483. if (data.exif) {
  3484. var orientation = data.exif.get('Orientation');
  3485. }
  3486. else {
  3487. var orientation = 1;
  3488. }
  3489. display_spinner();
  3490. // clean up
  3491. cleanUpAfterCropping();
  3492. // create image
  3493. loadImage(e.target.files[0],
  3494. function (img) {
  3495. if(typeof img.target == 'undefined') {
  3496. var appendedImg = $('#edit-profile-popup .profile-card').prepend('<img id="' + cropId +'" src="' + img.toDataURL('image/jpeg') + '" />');
  3497. // enable cropping
  3498. $('#' + cropId).jWindowCrop({
  3499. targetWidth:targetWidth,
  3500. targetHeight:targetHeight,
  3501. onChange: function(result) {
  3502. remove_spinner();
  3503. }
  3504. });
  3505. // align centered, fade out background
  3506. $('#' + cropId).parent().addClass(cropId);
  3507. $('#' + cropId).parent().css('position','absolute')
  3508. $('#' + cropId).parent().css('left','50%')
  3509. $('#' + cropId).parent().css('margin-left','-' + (targetWidth/2) + 'px')
  3510. $('#' + cropId).parent().siblings('.profile-header-inner').children('div,input,a').css('display','none');
  3511. // replace the hardcoded "click to drag" string
  3512. $('#' + cropId).siblings('.jwc_controls').children('span').html(window.sL.clickToDrag);
  3513. window.jwc = $('#' + cropId).getjWindowCrop();
  3514. $('.save-profile-button').hide();
  3515. $('.abort-edit-profile-button, .crop-and-save-button').show();
  3516. }
  3517. else {
  3518. remove_spinner();
  3519. $('.queet-box-loading-cover').remove();
  3520. alert('could not read image');
  3521. }
  3522. },
  3523. { maxWidth: maxWidth,
  3524. minWidth: minWidth,
  3525. canvas: true,
  3526. orientation: orientation } // Options
  3527. );
  3528. });
  3529. }
  3530. function cleanUpAfterCropping(){
  3531. $('.jwc_frame').siblings('.profile-header-inner').children('div,input,a').css('display','block');
  3532. if(typeof window.jwc != 'undefined') {
  3533. window.jwc.destroy();
  3534. }
  3535. $('.jwc_frame').remove();
  3536. $('#cover-photo-to-crop').remove();
  3537. $('#avatar-to-crop').remove();
  3538. $('#background-to-crop').remove();
  3539. $('input:file').unbind('click');
  3540. $('.crop-and-save-button').removeClass('disabled');
  3541. $('.crop-and-save-button').removeAttr('disabled');
  3542. $('.crop-and-save-button, .abort-edit-profile-button').hide();
  3543. showHideSaveProfileButtons();
  3544. }
  3545. /* ·
  3546. ·
  3547. · Upload attachment
  3548. ·
  3549. · · · · · · · · · · · · · */
  3550. $('body').on('mousedown','.upload-image',function () {
  3551. // remember caret position
  3552. var caretPos = getSelectionInElement($(this).closest('.queet-toolbar').siblings('.queet-box-syntax')[0]);
  3553. $(this).attr('data-caret-pos',caretPos);
  3554. // prevent queet-box collapse
  3555. $(this).addClass('clicked');
  3556. });
  3557. $('body').on('click','.upload-image',function () {
  3558. var thisUploadButton = $(this);
  3559. $('#upload-image-input').one('click',function(){ // trick to make the change event only fire once when selecting a file
  3560. $(this).unbind('change');
  3561. $(this).one('change',function(e){
  3562. uploadAttachment(e, thisUploadButton);
  3563. })
  3564. });
  3565. // trigger click
  3566. triggerClickOnInputFile($('#upload-image-input'));
  3567. });
  3568. function uploadAttachment(e, thisUploadButton) {
  3569. // loader cover stuff
  3570. thisUploadButton.closest('.queet-toolbar').parent().append('<div class="queet-box-loading-cover"></div>');
  3571. thisUploadButton.closest('.queet-toolbar').siblings('.queet-box-loading-cover').width(thisUploadButton.closest('.queet-toolbar').parent().outerWidth());
  3572. display_spinner(thisUploadButton.closest('.queet-toolbar').siblings('.queet-box-loading-cover')[0]);
  3573. thisUploadButton.closest('.queet-toolbar').siblings('.queet-box-loading-cover').find('.loader').css('top', (thisUploadButton.closest('.queet-toolbar').parent().outerHeight()/2-20) + 'px');
  3574. var uploadButton = thisUploadButton.closest('.queet-toolbar').find('.upload-image');
  3575. var queetBox = thisUploadButton.closest('.queet-toolbar').siblings('.queet-box-syntax');
  3576. var caretPos = uploadButton.attr('data-caret-pos').split(',');
  3577. var imgFormData = new FormData();
  3578. imgFormData.append('media', $('#upload-image-input')[0].files[0]);
  3579. // upload
  3580. $.ajax({ url: window.apiRoot + 'statusnet/media/upload',
  3581. type: "POST",
  3582. data: imgFormData,
  3583. contentType: false,
  3584. processData: false,
  3585. dataType: "xml",
  3586. error: function(data, textStatus, errorThrown){
  3587. showErrorMessage(window.sL.ERRORattachmentUploadFailed, queetBox.siblings('.syntax-two'));
  3588. $('.queet-box-loading-cover').remove();
  3589. queetBox.focus();
  3590. },
  3591. success: function(data) {
  3592. var rsp = $(data).find('rsp');
  3593. if (rsp.attr('stat') == 'ok') {
  3594. // maybe add thumbnail below queet box
  3595. if($(data).find('atom\\:link,link').length>0) {
  3596. var mimeType = $(data).find('atom\\:link,link').attr('type');
  3597. if(mimeType.indexOf('image/') == 0) {
  3598. var imgUrl = $(data).find('atom\\:link,link').attr('href');
  3599. thisUploadButton.closest('.queet-toolbar').before('<span class="upload-image-container"><img class="to-upload" src="' + imgUrl + '" /></span>');
  3600. }
  3601. }
  3602. var mediaurl = rsp.find('mediaurl').text();
  3603. $('img.to-upload').attr('data-shorturl', mediaurl);
  3604. $('img.to-upload').addClass('uploaded');
  3605. $('img.to-upload').removeClass('to-upload');
  3606. // insert shorturl in queet box
  3607. deleteBetweenCharacterIndices(queetBox[0], caretPos[0], caretPos[1]);
  3608. var range = createRangeFromCharacterIndices(queetBox[0], caretPos[0], caretPos[0]);
  3609. if(typeof range == 'undefined') {
  3610. // if queetbox is empty no range is returned, and inserting will fail,
  3611. // so we insert a space and try to get range again...
  3612. queetBox.html('&nbsp;');
  3613. range = createRangeFromCharacterIndices(queetBox[0], caretPos[0], caretPos[0]);
  3614. }
  3615. range.insertNode(document.createTextNode(' ' + mediaurl + ' '));
  3616. // put caret after
  3617. queetBox.focus();
  3618. var putCaretAt = parseInt(caretPos[0],10)+mediaurl.length+2;
  3619. setSelectionRange(queetBox[0], putCaretAt, putCaretAt);
  3620. queetBox.trigger('input'); // avoid some flickering
  3621. setTimeout(function(){ queetBox.trigger('input');},1); // make sure chars are counted and shorten-button activated
  3622. $('.queet-box-loading-cover').remove();
  3623. }
  3624. else {
  3625. alert('Try again! ' + rsp.find('err').attr('msg'));
  3626. $('.save-profile-button').removeAttr('disabled');
  3627. $('.save-profile-button').removeClass('disabled');
  3628. $('img.to-upload').parent().remove();
  3629. $('.queet-box-loading-cover').remove();
  3630. }
  3631. }
  3632. });
  3633. }
  3634. /* ·
  3635. ·
  3636. · Small edit profile button on hover cards goes to edit profile
  3637. ·
  3638. · · · · · · · · · · · · · */
  3639. $('body').on('click','.hover-card .edit-profile-button',function(){
  3640. goToEditProfile();
  3641. });
  3642. /* ·
  3643. ·
  3644. · User menu when clicking the mini cog wheel in the logged in mini card
  3645. ·
  3646. · · · · · · · · · · · · · */
  3647. $('body').on('click','#mini-logged-in-user-cog-wheel:not(.dropped)',function(){
  3648. var menu = $(getMenu(loggedInUsersMenuArray())).appendTo(this);
  3649. alignMenuToParent(menu,$(this));
  3650. $(this).addClass('dropped');
  3651. });
  3652. // hide when clicking it again
  3653. $('body').on('click','#mini-logged-in-user-cog-wheel.dropped',function(e){
  3654. if($(e.target).is('#mini-logged-in-user-cog-wheel')) {
  3655. $('#mini-logged-in-user-cog-wheel').children('.dropdown-menu').remove();
  3656. $('#mini-logged-in-user-cog-wheel').removeClass('dropped');
  3657. }
  3658. });
  3659. // hide the menu when clicking outside it
  3660. $('body').on('click',function(e){
  3661. if($('#mini-logged-in-user-cog-wheel').hasClass('dropped') && !$(e.target).closest('#mini-logged-in-user-cog-wheel').length>0) {
  3662. $('#mini-logged-in-user-cog-wheel').children('.dropdown-menu').remove();
  3663. $('#mini-logged-in-user-cog-wheel').removeClass('dropped');
  3664. }
  3665. });
  3666. /* ·
  3667. ·
  3668. · Goes to edit profile
  3669. ·
  3670. · · · · · · · · · · · · · */
  3671. function goToEditProfile(arg, callback) {
  3672. if(window.currentStreamObject.name == 'my profile') {
  3673. $('#page-container > .profile-card .edit-profile-button').trigger('click');
  3674. if(typeof callback == 'function') {
  3675. callback(true);
  3676. }
  3677. }
  3678. else {
  3679. setNewCurrentStream(pathToStreamRouter(window.loggedIn.screen_name), true, false, function(){
  3680. $('#page-container > .profile-card .edit-profile-button').trigger('click');
  3681. if(typeof callback == 'function') {
  3682. callback(true);
  3683. }
  3684. });
  3685. }
  3686. }