misc-functions.js 102 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896
  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. /* ·
  39. ·
  40. · Trigger click on <input type="file"> elements
  41. ·
  42. · @param inputFile: jQuery element to trigger click on
  43. ·
  44. · · · · · · · · · */
  45. function triggerClickOnInputFile(inputFile) {
  46. if(typeof bowser != 'undefined') {
  47. var bowserIntVersion = parseInt(bowser.version,10);
  48. if(typeof bowser.chrome != 'undefined' && bowser.chrome === true && bowserIntVersion < 53
  49. || typeof bowser.opera != 'undefined' && bowser.opera === true && bowserIntVersion < 39
  50. || typeof bowser.safari != 'undefined' && bowser.safari === true && bowserIntVersion < 9) {
  51. var evt = document.createEvent("HTMLEvents");
  52. evt.initEvent("click", true, true);
  53. inputFile[0].dispatchEvent(evt);
  54. console.log('triggering click on on input file element with the old trigger hack for older browsers...');
  55. }
  56. else {
  57. inputFile.trigger('click');
  58. console.log('regular click triggered on input file element');
  59. }
  60. }
  61. else {
  62. inputFile.trigger('click');
  63. console.log('no bowser object found: regular click triggered on input file element');
  64. }
  65. console.log(bowser);
  66. }
  67. /* ·
  68. ·
  69. · Store in localStorage object cache
  70. ·
  71. · @param name: the name of this type of object
  72. · @param unique_id: some unique_id – the key in localStorage will be name-unique_id
  73. · @param object: the object to store
  74. ·
  75. · · · · · · · · · */
  76. function localStorageObjectCache_STORE(name, unique_id, object) {
  77. if(localStorageIsEnabled() === false) {
  78. return false;
  79. }
  80. name = localStorageMaybeAppendIdToKey(name);
  81. if(object === false || object === null || object.length < 1) {
  82. // false or an empty object means we remove this entry
  83. if(typeof localStorage[name + '-' + unique_id] != 'undefined' && localStorage[name + '-' + unique_id] !== null) {
  84. delete localStorage[name + '-' + unique_id];
  85. }
  86. return false;
  87. }
  88. var dataToSave = {};
  89. dataToSave.modified = Date.now();
  90. dataToSave.cdata = LZString.compressToUTF16(JSON.stringify(object));
  91. try {
  92. localStorage.setItem(name + '-' + unique_id, JSON.stringify(dataToSave));
  93. }
  94. catch (e) {
  95. if (e.name == 'QUOTA_EXCEEDED_ERR' || e.name == 'NS_ERROR_DOM_QUOTA_REACHED' || e.name == 'QuotaExceededError' || e.name == 'W3CException_DOM_QUOTA_EXCEEDED_ERR') {
  96. removeOldestLocalStorageEntries(function(){
  97. localStorageObjectCache_STORE(name, unique_id, object);
  98. });
  99. }
  100. else {
  101. console.log('could not store in localStorage, unknown error');
  102. }
  103. }
  104. }
  105. /* ·
  106. ·
  107. · Remove the 100 oldest cached items
  108. ·
  109. · · · · · · · · · */
  110. function removeOldestLocalStorageEntries(callback) {
  111. // grab the expiry and store the modified-key into an object
  112. var modified = Object.keys(localStorage).reduce(function(collection,key){
  113. var currentModified = JSON.parse(localStorage.getItem(key)).modified;
  114. collection[currentModified] = key;
  115. return collection;
  116. },{});
  117. delete modified['undefined']; // we don't want those
  118. // get the modified dates into an array
  119. var modifiedDates = Object.keys(modified);
  120. modifiedDates.sort();
  121. var i = 0;
  122. $.each(modifiedDates,function(k,v){
  123. delete localStorage[modified[v]];
  124. i++;
  125. if(i>=100) {
  126. return false;
  127. }
  128. });
  129. console.log('removed ' + i + ' old localstorage items');
  130. if(i>0) {
  131. callback();
  132. return true;
  133. }
  134. else {
  135. return false;
  136. }
  137. }
  138. /* ·
  139. ·
  140. · Get from localStorage object cache
  141. ·
  142. · @param name: the name of this type of object
  143. · @param unique_id: some unique_id – the key in localStorage will be name-unique_id
  144. ·
  145. · · · · · · · · · */
  146. function localStorageObjectCache_GET(name, unique_id) {
  147. if(localStorageIsEnabled() === false) {
  148. return false;
  149. }
  150. name = localStorageMaybeAppendIdToKey(name);
  151. if(typeof localStorage[name + '-' + unique_id] == 'undefined' || localStorage[name + '-' + unique_id] === null) {
  152. return false;
  153. }
  154. try {
  155. var parsedObject = JSON.parse(localStorage[name + '-' + unique_id]);
  156. }
  157. catch(e) {
  158. return false;
  159. }
  160. if(typeof parsedObject.modified == 'undefined' || parsedObject.modified === null) {
  161. // invalid or old localstorage object found, check the whole localstorage!
  162. checkLocalStorage();
  163. return false;
  164. }
  165. else {
  166. try {
  167. var decompressedAndParsed = JSON.parse(LZString.decompressFromUTF16(parsedObject.cdata));
  168. return decompressedAndParsed;
  169. }
  170. catch(e) {
  171. return false;
  172. }
  173. }
  174. }
  175. // to the following data types we add the logged in user's user id,
  176. // since they contain user specific data (0 for logged out)
  177. // selectedLanguage is handled differently, since we want to be able to
  178. // access the logged out user's data if we're logged in
  179. function localStorageMaybeAppendIdToKey(name) {
  180. if(jQuery.inArray(name, ['browsingHistory', 'conversation', 'queetBoxInput', 'streamState']) !== -1) {
  181. if(window.loggedIn) {
  182. return name + '-' + window.loggedIn.id;
  183. }
  184. else {
  185. return name + '-0' ;
  186. }
  187. }
  188. else {
  189. return name;
  190. }
  191. }
  192. function checkLocalStorage() {
  193. if(localStorageIsEnabled() === false) {
  194. console.log('localstorage disabled');
  195. return false;
  196. }
  197. console.log('checking localStorage for invalid entries');
  198. var dateNow = Date.now()
  199. var corrected = 0;
  200. var deleted = 0;
  201. var compressed = 0;
  202. $.each(localStorage, function(k,entry){
  203. if(typeof entry == 'string') {
  204. // check that entry is valid json
  205. try {
  206. var entryParsed = JSON.parse(entry);
  207. }
  208. catch(e) {
  209. delete localStorage[k];
  210. deleted++;
  211. return true;
  212. }
  213. // check that it is a valid/currently used data type
  214. var validDataTypes = [
  215. 'browsingHistory',
  216. 'conversation',
  217. 'favsAndRequeets',
  218. 'languageData',
  219. 'fullQueetHtml',
  220. 'selectedLanguage',
  221. 'queetBoxInput',
  222. 'streamState',
  223. 'languageErrorMessageDiscarded'
  224. ];
  225. var thisDataType = k.substring(0,k.indexOf('-'));
  226. if($.inArray(thisDataType, validDataTypes) == -1 || k.indexOf('-') == -1) {
  227. delete localStorage[k];
  228. deleted++;
  229. return true;
  230. }
  231. // check that it has a modified entry, if not: add one
  232. if(typeof entryParsed.modified == 'undefined' || entryParsed.modified === null) {
  233. var newEntry = {};
  234. newEntry.modified = dateNow - corrected; // we want as unique dates as possible
  235. newEntry.cdata = entryParsed;
  236. try {
  237. localStorage.setItem(k, JSON.stringify(newEntry));
  238. }
  239. catch (e) {
  240. if (e.name == 'QUOTA_EXCEEDED_ERR' || e.name == 'NS_ERROR_DOM_QUOTA_REACHED' || e.name == 'QuotaExceededError' || e.name == 'W3CException_DOM_QUOTA_EXCEEDED_ERR') {
  241. removeOldestLocalStorageEntries(function(){
  242. localStorage.setItem(k, JSON.stringify(newEntry));
  243. });
  244. }
  245. }
  246. entryParsed = newEntry;
  247. corrected++;
  248. }
  249. // compress uncompressed data
  250. if(typeof entryParsed.data != 'undefined') {
  251. // but not if it's not containing any data (some bug may have saved an empty, false or null value)
  252. if(entryParsed.data === false || entryParsed.data === null || entryParsed.data.length == 0) {
  253. delete localStorage[k];
  254. deleted++;
  255. return true;
  256. }
  257. var dataCompressed = LZString.compressToUTF16(JSON.stringify(entryParsed.data));
  258. var newCompressedEntry = {};
  259. newCompressedEntry.modified = entryParsed.modified;
  260. newCompressedEntry.cdata = dataCompressed;
  261. localStorage.setItem(k, JSON.stringify(newCompressedEntry));
  262. compressed++;
  263. }
  264. }
  265. });
  266. console.log(corrected + ' entries corrected, ' + deleted + ' entries deleted, ' + compressed + ' entries compressed');
  267. }
  268. /* ·
  269. ·
  270. · Checks if localstorage is availible
  271. ·
  272. · We can't just do if(typeof localStorage.selectedLanguage != 'undefined')
  273. · because firefox with cookies disabled then freaks out and stops executing js completely
  274. ·
  275. · · · · · · · · · */
  276. function localStorageIsEnabled() {
  277. var mod = 'test';
  278. try {
  279. localStorage.setItem(mod, mod);
  280. localStorage.removeItem(mod);
  281. return true;
  282. }
  283. catch(e) {
  284. if (e.name == 'QUOTA_EXCEEDED_ERR' || e.name == 'NS_ERROR_DOM_QUOTA_REACHED' || e.name == 'QuotaExceededError' || e.name == 'W3CException_DOM_QUOTA_EXCEEDED_ERR') {
  285. // if localstorage is empty but returns a full error, we assume it's disabled (in an ugly way)
  286. if(localStorage.length === 0) {
  287. return false;
  288. }
  289. var successfulRemoval = removeOldestLocalStorageEntries(function(){
  290. return localStorageIsEnabled();
  291. });
  292. if(successfulRemoval === false) {
  293. return false;
  294. }
  295. }
  296. else {
  297. return false;
  298. }
  299. }
  300. }
  301. /* ·
  302. ·
  303. · Block/unblock user and do necessary stuff
  304. ·
  305. · · · · · · · · · */
  306. function blockUser(arg, callback) {
  307. $('body').click(); // a click somewhere hides any open menus
  308. // arguments is sent as an object, for easier use with a menu's function-row
  309. var userId = arg.userId;
  310. var blockButton_jQueryElement = arg.blockButton_jQueryElement;
  311. display_spinner();
  312. APIBlockOrUnblockUser('block', userId, function(data) {
  313. remove_spinner();
  314. // activate the button, if we were passed one
  315. if(typeof blockButton_jQueryElement != 'undefined') {
  316. blockButton_jQueryElement.removeClass('disabled');
  317. }
  318. if(data && data.statusnet_blocking === true) {
  319. // success
  320. markUserAsBlockedInDOM(userId, data.following);
  321. if(typeof callback == 'function') {
  322. callback(blockButton_jQueryElement);
  323. }
  324. }
  325. else {
  326. // failed!
  327. showErrorMessage(window.sL.failedBlockingUser);
  328. }
  329. });
  330. }
  331. function unblockUser(arg, callback) {
  332. $('body').click(); // a click somewhere hides any open menus
  333. // arguments is sent as an object, for easier use with a menu's function-row
  334. var userId = arg.userId;
  335. var blockButton_jQueryElement = arg.blockButton_jQueryElement;
  336. display_spinner();
  337. APIBlockOrUnblockUser('unblock', userId, function(data) {
  338. remove_spinner();
  339. // activate the button, if we were passed one
  340. if(typeof blockButton_jQueryElement != 'undefined') {
  341. blockButton_jQueryElement.removeClass('disabled');
  342. }
  343. if(data && data.statusnet_blocking === false) {
  344. // success
  345. markUserAsUnblockedInDOM(userId, data.following);
  346. if(typeof callback == 'function') {
  347. callback(blockButton_jQueryElement);
  348. }
  349. }
  350. else {
  351. // failed!
  352. showErrorMessage(window.sL.failedUnblockingUser);
  353. }
  354. });
  355. }
  356. function markUserAsBlockedInDOM(userId, following) {
  357. // display buttons accordingly
  358. $('.qvitter-follow-button[data-follow-user-id="' + userId + '"]').addClass('blocking');
  359. if(following) {
  360. $('.qvitter-follow-button[data-follow-user-id="' + userId + '"]').addClass('following');
  361. }
  362. else {
  363. $('.qvitter-follow-button[data-follow-user-id="' + userId + '"]').removeClass('following');
  364. }
  365. // hide notices from the blocked user
  366. $.each($('.stream-item[data-quitter-id-in-stream][data-user-id="' + userId + '"]'),function(){
  367. $(this).addClass('profile-blocked-by-me');
  368. $(this).children('.queet').attr('data-tooltip',window.sL.thisIsANoticeFromABlockedUser);
  369. });
  370. // add to the window.allBlocking array
  371. if (userIsBlocked(userId) === false) {
  372. window.allBlocking.push(userId);
  373. }
  374. }
  375. function markUserAsUnblockedInDOM(userId, following) {
  376. // display buttons accordingly
  377. $('.qvitter-follow-button[data-follow-user-id="' + userId + '"]').removeClass('blocking');
  378. if(following) {
  379. $('.qvitter-follow-button[data-follow-user-id="' + userId + '"]').addClass('following');
  380. }
  381. else {
  382. $('.qvitter-follow-button[data-follow-user-id="' + userId + '"]').removeClass('following');
  383. }
  384. // hide the user from lists of blocked users
  385. if(window.currentStreamObject.name == 'user blocks' && window.currentStreamObject.nickname == window.loggedIn.screen_name) {
  386. $.each($('.stream-item.user[data-user-id="' + userId + '"]'),function(){
  387. slideUpAndRemoveStreamItem($(this));
  388. });
  389. }
  390. // unhide notices from the blocked user
  391. $.each($('.stream-item[data-quitter-id-in-stream][data-user-id="' + userId + '"]'),function(){
  392. $(this).removeClass('profile-blocked-by-me');
  393. $(this).children('.queet').removeAttr('data-tooltip');
  394. });
  395. // remove from the window.allBlocking array
  396. var existingBlockIndex = window.allBlocking.indexOf(userId);
  397. if (existingBlockIndex > -1) {
  398. window.allBlocking.splice(existingBlockIndex, 1);
  399. }
  400. }
  401. /* ·
  402. ·
  403. · Is this user id blocked?
  404. ·
  405. · · · · · · · · · */
  406. function userIsBlocked(userId) {
  407. var existingBlock = window.allBlocking.indexOf(userId);
  408. if (existingBlock > -1) {
  409. return true;
  410. }
  411. else {
  412. return false;
  413. }
  414. }
  415. /* ·
  416. ·
  417. · Marks all notices from blocked users in an jQuery object as blocked
  418. ·
  419. · · · · · · · · · */
  420. function markAllNoticesFromBlockedUsersAsBlockedInJQueryObject(obj) {
  421. $.each(window.allBlocking,function(){
  422. obj.find('.stream-item[data-user-id="' + this + '"]').addClass('profile-blocked-by-me');
  423. obj.find('.stream-item[data-user-id="' + this + '"]').children('.queet').attr('data-tooltip',window.sL.thisIsANoticeFromABlockedUser);
  424. });
  425. }
  426. /* ·
  427. ·
  428. · Marks all notices from muted users in an jQuery object as muted
  429. ·
  430. · · · · · · · · · */
  431. function markAllNoticesFromMutedUsersAsMutedInJQueryObject(obj) {
  432. $.each(obj.find('.stream-item'),function(){
  433. if(isUserMuted($(this).attr('data-user-id'))) {
  434. $(this).addClass('user-muted');
  435. $(this).children('.queet').attr('data-tooltip',window.sL.thisIsANoticeFromAMutedUser);
  436. }
  437. else {
  438. $(this).children('.queet').removeAttr('data-tooltip');
  439. $(this).removeClass('user-muted');
  440. }
  441. });
  442. }
  443. /* ·
  444. ·
  445. · Marks all profile cards from muted users as muted in DOM
  446. ·
  447. · · · · · · · · · */
  448. function markAllProfileCardsFromMutedUsersAsMutedInDOM() {
  449. $.each($('body').find('.profile-header-inner'),function(){
  450. if(isUserMuted($(this).attr('data-user-id'))) {
  451. $(this).parent('.profile-card').addClass('user-muted');
  452. }
  453. else {
  454. $(this).parent('.profile-card').removeClass('user-muted');
  455. }
  456. });
  457. }
  458. /* ·
  459. ·
  460. · Function invoked after mute and unmute
  461. ·
  462. · · · · · · · · · */
  463. function hideOrShowNoticesAfterMuteOrUnmute() {
  464. markAllNoticesFromMutedUsersAsMutedInJQueryObject($('#feed-body'));
  465. markAllProfileCardsFromMutedUsersAsMutedInDOM();
  466. }
  467. /* ·
  468. ·
  469. · Sandbox/unsandbox user and do necessary stuff
  470. ·
  471. · · · · · · · · · */
  472. function sandboxCreateOrDestroy(arg, callback) {
  473. $('body').click(); // a click somewhere hides any open menus
  474. display_spinner();
  475. APISandboxCreateOrDestroy(arg.createOrDestroy, arg.userId, function(data) {
  476. remove_spinner();
  477. if(!data) {
  478. // failed!
  479. showErrorMessage(window.sL.ERRORfailedSandboxingUser);
  480. }
  481. });
  482. }
  483. /* ·
  484. ·
  485. · Sandbox/unsandbox user and do necessary stuff
  486. ·
  487. · · · · · · · · · */
  488. function silenceCreateOrDestroy(arg, callback) {
  489. $('body').click(); // a click somewhere hides any open menus
  490. display_spinner();
  491. APISilenceCreateOrDestroy(arg.createOrDestroy, arg.userId, function(data) {
  492. remove_spinner();
  493. if(!data) {
  494. // failed!
  495. showErrorMessage(window.sL.ERRORfailedSilencingUser);
  496. }
  497. });
  498. }
  499. /* ·
  500. ·
  501. · Get the logged in user's menu array
  502. ·
  503. · · · · · · · · · */
  504. function loggedInUsersMenuArray() {
  505. return [
  506. {
  507. type: 'function',
  508. functionName: 'goToEditProfile',
  509. label: window.sL.editMyProfile
  510. },
  511. {
  512. type: 'link',
  513. href: window.siteInstanceURL + 'settings/profile',
  514. label: window.sL.settings
  515. },
  516. {
  517. type:'divider'
  518. },
  519. {
  520. type: 'link',
  521. href: window.siteInstanceURL + window.loggedIn.screen_name + '/mutes',
  522. label: window.sL.userMuted
  523. },
  524. {
  525. type: 'link',
  526. href: window.siteInstanceURL + window.loggedIn.screen_name + '/blocks',
  527. label: window.sL.userBlocked
  528. }];
  529. }
  530. /* ·
  531. ·
  532. · Append moderator user actions to menu array
  533. ·
  534. · @param menuArray: array used to build menus in getMenu()
  535. · @param userID: the user id of the user to act on
  536. · @param userScreenName: the screen_name/nickname/username of the user to act on
  537. · @param sandboxed: is the user sandboxed?
  538. · @param silenced: is the user silenced?
  539. ·
  540. · · · · · · · · · */
  541. function appendModeratorUserActionsToMenuArray(menuArray,userID,userScreenName,sandboxed,silenced) {
  542. // not if it's me
  543. if(window.loggedIn.id == userID) {
  544. return menuArray;
  545. }
  546. if(window.loggedIn !== false && window.loggedIn.rights.sandbox === true) {
  547. menuArray.push({type:'divider'});
  548. if(sandboxed === true) {
  549. menuArray.push({
  550. type: 'function',
  551. functionName: 'sandboxCreateOrDestroy',
  552. functionArguments: {
  553. userId: userID,
  554. createOrDestroy: 'destroy'
  555. },
  556. label: window.sL.unSandboxThisUser.replace('{nickname}','@' + userScreenName)
  557. });
  558. }
  559. else {
  560. menuArray.push({
  561. type: 'function',
  562. functionName: 'sandboxCreateOrDestroy',
  563. functionArguments: {
  564. userId: userID,
  565. createOrDestroy: 'create'
  566. },
  567. label: window.sL.sandboxThisUser.replace('{nickname}','@' + userScreenName)
  568. });
  569. }
  570. }
  571. if(window.loggedIn !== false && window.loggedIn.rights.silence === true) {
  572. if(silenced === true) {
  573. menuArray.push({
  574. type: 'function',
  575. functionName: 'silenceCreateOrDestroy',
  576. functionArguments: {
  577. userId: userID,
  578. createOrDestroy: 'destroy'
  579. },
  580. label: window.sL.unSilenceThisUser.replace('{nickname}','@' + userScreenName)
  581. });
  582. }
  583. else {
  584. menuArray.push({
  585. type: 'function',
  586. functionName: 'silenceCreateOrDestroy',
  587. functionArguments: {
  588. userId: userID,
  589. createOrDestroy: 'create'
  590. },
  591. label: window.sL.silenceThisUser.replace('{nickname}','@' + userScreenName)
  592. });
  593. }
  594. }
  595. return menuArray;
  596. }
  597. /* ·
  598. ·
  599. · Updates the times for all queets loaded to DOM
  600. ·
  601. · · · · · · · · · */
  602. function updateAllQueetsTimes() {
  603. $('[data-created-at]').each(function(){
  604. // if the element with the data-created-at doesn't have an a-child, we change the html of the element
  605. if($(this).children('a').length==0){
  606. $(this).html(parseTwitterDate($(this).attr('data-created-at')));
  607. }
  608. // otherwise the change the child's html
  609. else {
  610. $(this).children('a').html(parseTwitterDate($(this).attr('data-created-at')));
  611. }
  612. });
  613. }
  614. /* ·
  615. ·
  616. · Is this a local URL?
  617. ·
  618. · · · · · · · · · */
  619. function isLocalURL(url) {
  620. if(url.substring(0,window.siteInstanceURL.length) == window.siteInstanceURL // site url
  621. || url.substring(0,window.siteAttachmentURLBase.length) == window.siteAttachmentURLBase // attachment url
  622. || url.substring(0,window.siteAvatarURLBase.length) == window.siteAvatarURLBase // avatar url
  623. ) {
  624. return true;
  625. }
  626. return false;
  627. }
  628. /* ·
  629. ·
  630. · Check for hidden items and show the new queets bar if there are any
  631. ·
  632. · · · · · · · · · */
  633. function maybeShowTheNewQueetsBar() {
  634. var newQueetsNum = $('#feed-body').find('.stream-item.hidden:not(.always-hidden):not(.hidden-repeat)').length;
  635. // subtract the number of hidden notices from muted users if this isn't the notifications stream,
  636. // or if this is the notifications stream but the user has opted out of seeing notifications from muted users
  637. var mutedHiddenNum = 0;
  638. if(window.currentStreamObject.name == 'notifications') {
  639. if($('#feed-body').hasClass('hide-notifications-from-muted-users')) {
  640. mutedHiddenNum = $('#feed-body').find('.stream-item.hidden.user-muted:not(.always-hidden):not(.hidden-repeat)').length;
  641. }
  642. }
  643. else {
  644. var mutedHiddenNum = $('#feed-body').find('.stream-item.hidden.user-muted:not(.always-hidden):not(.hidden-repeat)').length;
  645. }
  646. newQueetsNum = newQueetsNum - mutedHiddenNum;
  647. if(newQueetsNum > 0) {
  648. $('#new-queets-bar').parent().removeClass('hidden');
  649. // bar label
  650. if(newQueetsNum == 1) { var q_txt = window.sL.newQueet; }
  651. else { var q_txt = window.sL.newQueets; }
  652. if(window.currentStreamObject.name == 'notifications') {
  653. if(newQueetsNum == 1) { var q_txt = window.sL.newNotification; }
  654. else { var q_txt = window.sL.newNotifications; }
  655. }
  656. $('#new-queets-bar').html(q_txt.replace('{new-notice-count}',newQueetsNum));
  657. }
  658. }
  659. /* ·
  660. ·
  661. · Align tooltips to the hovered element
  662. ·
  663. · · · · · · · · · */
  664. function alignTooltipTohoveredElement(tooltipElement,tooltipCaret,hovered) {
  665. var tooltipWidth = tooltipElement.outerWidth();
  666. var tooltipHeight = tooltipElement.outerHeight();
  667. var windowWidth = $(window).width();
  668. var windowScrollPosY = $(window).scrollTop();
  669. var targetPosX = hovered.offset().left;
  670. var targetPosY = hovered.offset().top;
  671. var targetHeight = hovered.outerHeight();
  672. var targetWidth = hovered.outerWidth();
  673. // too little space on top of element, show tooltip at bottom
  674. if((targetPosY-windowScrollPosY-tooltipHeight-10) < 0) {
  675. var tooltipCaretPosX = targetPosX+targetWidth/2-5;
  676. var tooltipCaretPosY = targetPosY+targetHeight+2;
  677. // caret always directly under element
  678. tooltipCaret.css('left',tooltipCaretPosX + 'px');
  679. tooltipCaret.css('top',tooltipCaretPosY + 'px');
  680. tooltipCaret.addClass('top');
  681. // tooltip itself might bleed over the window edges, and need moving
  682. var tooltipPosX = targetPosX+targetWidth/2-tooltipWidth/2;
  683. var tooltipPosY = targetPosY+targetHeight+7;
  684. if((tooltipPosX+tooltipWidth)>windowWidth) {
  685. tooltipPosX = windowWidth-tooltipWidth-5;
  686. }
  687. if(tooltipPosX < 5) {
  688. tooltipPosX = 5;
  689. }
  690. tooltipElement.css('left',tooltipPosX + 'px');
  691. tooltipElement.css('top',tooltipPosY + 'px');
  692. }
  693. // tooltip at top
  694. else {
  695. var tooltipCaretPosX = targetPosX+targetWidth/2-5;
  696. var tooltipCaretPosY = targetPosY-8;
  697. // caret always directly on top of element
  698. tooltipCaret.css('left',tooltipCaretPosX + 'px');
  699. tooltipCaret.css('top',tooltipCaretPosY + 'px');
  700. tooltipCaret.addClass('bottom');
  701. // tooltip itself might bleed over the window edges, and need moving
  702. var tooltipPosX = targetPosX+targetWidth/2-tooltipWidth/2;
  703. var tooltipPosY = targetPosY-7-tooltipHeight;
  704. if((tooltipPosX+tooltipWidth)>windowWidth) {
  705. tooltipPosX = windowWidth-tooltipWidth-5;
  706. }
  707. if(tooltipPosX < 5) {
  708. tooltipPosX = 5;
  709. }
  710. tooltipElement.css('left',tooltipPosX + 'px');
  711. tooltipElement.css('top',tooltipPosY + 'px');
  712. }
  713. }
  714. /* ·
  715. ·
  716. · Cache the unicode compatible regexps for the syntax highlighting
  717. ·
  718. · · · · · · · · · */
  719. function cacheSyntaxHighlighting() {
  720. window.syntaxHighlightingRegexps = Object();
  721. var allDomains = '(abb|abbott|abogado|ac|academy|accenture|accountant|accountants|active|actor|ad|ads|adult|ae|aero|af|afl|ag|agency|ai|aig|airforce|al|allfinanz|alsace|am|amsterdam|an|android|ao|apartments|aq|aquarelle|ar|archi|army|arpa|as|asia|associates|at|attorney|au|auction|audio|auto|autos|aw|ax|axa|az|ba|band|bank|bar|barclaycard|barclays|bargains|bauhaus|bayern|bb|bbc|bbva|bd|be|beer|berlin|best|bf|bg|bh|bi|bible|bid|bike|bingo|bio|biz|bj|bl|black|blackfriday|bloomberg|blue|bm|bmw|bn|bnpparibas|bo|boats|bond|boo|boutique|bq|br|bridgestone|broker|brother|brussels|bs|bt|budapest|build|builders|business|buzz|bv|bw|by|bz|bzh|ca|cab|cafe|cal|camera|camp|cancerresearch|canon|capetown|capital|caravan|cards|care|career|careers|cars|cartier|casa|cash|casino|cat|catering|cbn|cc|cd|center|ceo|cern|cf|cfa|cfd|cg|ch|channel|chat|cheap|chloe|christmas|chrome|church|ci|cisco|citic|city|ck|cl|claims|cleaning|click|clinic|clothing|club|cm|cn|co|coach|codes|coffee|college|cologne|com|community|company|computer|condos|construction|consulting|contractors|cooking|cool|coop|corsica|country|coupons|courses|cr|credit|creditcard|cricket|crs|cruises|cu|cuisinella|cv|cw|cx|cy|cymru|cyou|cz|dabur|dad|dance|date|dating|datsun|day|dclk|de|deals|degree|delivery|democrat|dental|dentist|desi|design|dev|diamonds|diet|digital|direct|directory|discount|dj|dk|dm|dnp|do|docs|dog|doha|domains|doosan|download|durban|dvag|dz|earth|eat|ec|edu|education|ee|eg|eh|email|emerck|energy|engineer|engineering|enterprises|epson|equipment|er|erni|es|esq|estate|et|eu|eurovision|eus|events|everbank|exchange|expert|exposed|express|fail|faith|fan|fans|farm|fashion|feedback|fi|film|finance|financial|firmdale|fish|fishing|fit|fitness|fj|fk|flights|florist|flowers|flsmidth|fly|fm|fo|foo|football|forex|forsale|foundation|fr|frl|frogans|fund|furniture|futbol|fyi|ga|gal|gallery|garden|gb|gbiz|gd|gdn|ge|gent|gf|gg|ggee|gh|gi|gift|gifts|gives|gl|glass|gle|global|globo|gm|gmail|gmo|gmx|gn|gold|goldpoint|golf|goo|goog|google|gop|gov|gp|gq|gr|graphics|gratis|green|gripe|gs|gt|gu|guge|guide|guitars|guru|gw|gy|hamburg|hangout|haus|healthcare|help|here|hermes|hiphop|hitachi|hiv|hk|hm|hn|hockey|holdings|holiday|homedepot|homes|honda|horse|host|hosting|house|how|hr|ht|hu|ibm|icbc|icu|id|ie|ifm|il|im|immo|immobilien|in|industries|infiniti|info|ing|ink|institute|insure|int|international|investments|io|iq|ir|irish|is|it|iwc|java|jcb|je|jetzt|jewelry|jll|jm|jo|jobs|joburg|jp|juegos|kaufen|kddi|ke|kg|kh|ki|kim|kitchen|kiwi|km|kn|koeln|komatsu|kp|kr|krd|kred|kw|ky|kyoto|kz|la|lacaixa|land|lat|latrobe|lawyer|lb|lc|lds|lease|leclerc|legal|lgbt|li|liaison|lidl|life|lighting|limited|limo|link|lk|loan|loans|lol|london|lotte|lotto|love|lr|ls|lt|ltda|lu|lupin|luxe|luxury|lv|ly|ma|madrid|maif|maison|management|mango|market|marketing|markets|marriott|mba|mc|md|me|media|meet|melbourne|meme|memorial|men|menu|mf|mg|mh|miami|mil|mini|mk|ml|mm|mma|mn|mo|mobi|moda|moe|monash|money|montblanc|mormon|mortgage|moscow|motorcycles|mov|movie|mp|mq|mr|ms|mt|mtn|mtpc|mu|museum|mv|mw|mx|my|mz|na|nadex|nagoya|name|navy|nc|ne|nec|net|network|neustar|new|news|nexus|nf|ng|ngo|nhk|ni|nico|ninja|nissan|nl|no|np|nr|nra|nrw|ntt|nu|nyc|nz|okinawa|om|one|ong|onl|online|ooo|org|organic|osaka|otsuka|ovh|pa|page|panerai|paris|partners|parts|party|pe|pf|pg|ph|pharmacy|philips|photo|photography|photos|physio|piaget|pics|pictet|pictures|pink|pizza|pk|pl|place|plumbing|plus|pm|pn|pohl|poker|porn|post|pr|praxi|press|pro|prod|productions|prof|properties|property|ps|pt|pub|pw|py|qa|qpon|quebec|racing|re|realtor|recipes|red|redstone|rehab|reise|reisen|reit|ren|rent|rentals|repair|report|republican|rest|restaurant|review|reviews|rich|rio|rip|ro|rocks|rodeo|rs|rsvp|ru|ruhr|run|rw|ryukyu|sa|saarland|sale|samsung|sandvik|sandvikcoromant|sap|sarl|saxo|sb|sc|sca|scb|schmidt|scholarships|school|schule|schwarz|science|scot|sd|se|seat|sener|services|sew|sex|sexy|sg|sh|shiksha|shoes|show|shriram|si|singles|site|sj|sk|ski|sky|sl|sm|sn|sncf|so|soccer|social|software|sohu|solar|solutions|sony|soy|space|spiegel|spreadbetting|sr|ss|st|study|style|su|sucks|supplies|supply|support|surf|surgery|suzuki|sv|swiss|sx|sy|sydney|systems|sz|taipei|tatar|tattoo|tax|taxi|tc|td|team|tech|technology|tel|temasek|tennis|tf|tg|th|thd|theater|tickets|tienda|tips|tires|tirol|tj|tk|tl|tm|tn|to|today|tokyo|tools|top|toray|toshiba|tours|town|toys|tp|tr|trade|trading|training|travel|trust|tt|tui|tv|tw|tz|ua|ug|uk|um|university|uno|uol|us|uy|uz|va|vacations|vc|ve|vegas|ventures|versicherung|vet|vg|vi|viajes|video|villas|vision|vlaanderen|vn|vodka|vote|voting|voto|voyage|vu|wales|walter|wang|watch|webcam|website|wed|wedding|weir|wf|whoswho|wien|wiki|williamhill|win|wme|work|works|world|ws|wtc|wtf|xbox|xerox|xin|测试|परीक्षा|佛山|慈善|集团|在线|한국|ভারত|八卦|موقع|বাংলা|公益|公司|移动|我爱你|москва|испытание|қаз|онлайн|сайт|срб|бел|时尚|테스트|淡马锡|орг|삼성|சிங்கப்பூர்|商标|商店|商城|дети|мкд|טעסט|工行|中文网|中信|中国|中國|娱乐|谷歌|భారత్|ලංකා|測試|ભારત|भारत|آزمایشی|பரிட்சை|网店|संगठन|餐厅|网络|укр|香港|δοκιμή|飞利浦|إختبار|台湾|台灣|手机|мон|الجزائر|عمان|ایران|امارات|بازار|پاکستان|الاردن|بھارت|المغرب|السعودية|سودان|عراق|مليسيا|澳門|政府|شبكة|გე|机构|组织机构|健康|ไทย|سورية|рус|рф|تونس|みんな|グーグル|ελ|世界|ਭਾਰਤ|网址|游戏|vermögensberater|vermögensberatung|企业|信息|مصر|قطر|广东|இலங்கை|இந்தியா|հայ|新加坡|فلسطين|テスト|政务|xxx|xyz|yachts|yandex|ye|yodobashi|yoga|yokohama|youtube|yt|za|zip|zm|zone|zuerich|zw|oracle|xn--1qqw23a|xn--30rr7y|xn--3bst00m|xn--3ds443g|xn--3e0b707e|xn--45brj9c|xn--45q11c|xn--4gbrim|xn--55qw42g|xn--55qx5d|xn--6frz82g|xn--6qq986b3xl|xn--80adxhks|xn--80ao21a|xn--80asehdb|xn--80aswg|xn--90a3ac|xn--90ais|xn--9et52u|xn--b4w605ferd|xn--c1avg|xn--cg4bki|xn--clchc0ea0b2g2a9gcd|xn--czr694b|xn--czrs0t|xn--czru2d|xn--d1acj3b|xn--d1alf|xn--estv75g|xn--fiq228c5hs|xn--fiq64b|xn--fiqs8s|xn--fiqz9s|xn--fjq720a|xn--flw351e|xn--fpcrj9c3d|xn--fzc2c9e2c|xn--gecrj9c|xn--h2brj9c|xn--hxt814e|xn--i1b6b1a6a2e|xn--imr513n|xn--io0a7i|xn--j1amh|xn--j6w193g|xn--kcrx77d1x4a|xn--kprw13d|xn--kpry57d|xn--kput3i|xn--l1acc|xn--lgbbat1ad8j|xn--mgb9awbf|xn--mgba3a4f16a|xn--mgbaam7a8h|xn--mgbab2bd|xn--mgbayh7gpa|xn--mgbbh1a71e|xn--mgbc0a9azcg|xn--mgberp4a5d4ar|xn--mgbpl2fh|xn--mgbx4cd0ab|xn--mxtq1m|xn--ngbc5azd|xn--node|xn--nqv7f|xn--nqv7fs00ema|xn--nyqy26a|xn--o3cw4h|xn--ogbpf8fl|xn--p1acf|xn--p1ai|xn--pgbs0dh|xn--q9jyb4c|xn--qcka1pmc|xn--rhqv96g|xn--s9brj9c|xn--ses554g|xn--unup4y|xn--vermgensberater-ctb|xn--vermgensberatung-pwb|xn--vhquv|xn--vuq861b|xn--wgbh1c|xn--wgbl6a|xn--xhq521b|xn--xkc2al3hye2a|xn--xkc2dl3a5ee0h|xn--y9a3aq|xn--yfro4i67o|xn--ygbi2ammx|xn--zfr164b)';
  722. window.syntaxHighlightingRegexps.externalMention = XRegExp.cache('(^|\\s|\\.|<br>|&nbsp;|\\()(@)[a-zA-Z0-9]+(@)[\\p{L}\\p{N}\\-\\.]+(\\.)(' + allDomains + ')($|\\s|\\.|\\,|\\:|\\-|\\<|\\!|\\?|\\&|\\)|\\\')');
  723. window.syntaxHighlightingRegexps.mention = /(^|\s|\.|<br>|&nbsp;|\()(@)[a-zA-Z0-9]+($|\s|\.|\,|\:|\-|\<|\!|\?|\&|\)|\')/;
  724. window.syntaxHighlightingRegexps.tag = XRegExp.cache('(^|\\s|\\.|<br>|&nbsp;|\\()(\\#)[\\p{L}\\p{N}\\-\\.]+($|\\s|\\,|\\:|\\<|\\!|\\?|\\&|\\)|\\\')');
  725. window.syntaxHighlightingRegexps.url = XRegExp.cache('(^|\\s|\\.|<br>|&nbsp;|\\()(http\\:\\/\\/|https\:\\/\\/)([\\p{L}\\p{N}\\-\\.]+)?(\\.)(' + allDomains + ')(\\/[\\p{L}\\p{N}\\%\\!\\*\\\'\\(\\)\\;\\:\\@\\&\\=\\+\\$\\,\\/\\?\\#\\[\\]\\-\\_\\.\\~]+)?(\\/)?($|\\s|\\,|\\:|\\-|\\<|\\!|\\?|\\&|\\)|\\\')');
  726. window.syntaxHighlightingRegexps.urlWithoutProtocol = XRegExp.cache('(^|\\s|\\.|<br>|&nbsp;|\\()[\\p{L}\\p{N}\\-\\.]+(\\.)(' + allDomains + ')(\\/[\\p{L}\\p{N}\\%\\!\\*\\\'\\(\\)\\;\\:\\@\\&\\=\\+\\$\\,\\/\\?\\#\\[\\]\\-\\_\\.\\~]+)?(\\/)?($|\\s|\\.|\\,|\\:|\\-|\\<|\\!|\\?|\\&|\\)|\\\')');
  727. window.syntaxHighlightingRegexps.email = XRegExp.cache('(^|\\s|\\.|<br>|&nbsp;|\\()([a-zA-Z0-9\\!\\#\\$\\%\\&\\\'\\*\\+\\-\\/\\=\\?\\^\\_\\`\\{\\|\\}\\~\\.]+)?(@)[\\p{L}\\p{N}\\-\\.]+(\\.)(' + allDomains + ')($|\\s|\\.|\\,|\\:|\\-|\\<|\\!|\\?|\\&|\\)|\\\')');
  728. cacheSyntaxHighlightingGroups();
  729. }
  730. /* ·
  731. ·
  732. · Cache syntax highlighting for groups
  733. ·
  734. · · · · · · · · · */
  735. function cacheSyntaxHighlightingGroups() {
  736. if(window.groupNicknamesAndLocalAliases.length > 0) {
  737. var allGroupNicknamesAndLocalAliases = '(' + window.groupNicknamesAndLocalAliases.join('|') + ')';
  738. window.syntaxHighlightingRegexps.group = XRegExp.cache('(^|\\s|\\.|<br>|&nbsp;|\\()(\\!)' + allGroupNicknamesAndLocalAliases + '($|\\s|\\.|\\,|\\:|\\-|\\<|\\!|\\?|\\&|\\)|\\\')');
  739. }
  740. }
  741. /* ·
  742. ·
  743. · User array cache (called array because it's an array in php)
  744. ·
  745. · Stored in window.userArrayCache with unique key like instance_url/nickname
  746. · with protocol (http:// or https://) trimmed off, e.g. "quitter.se/hannes2peer"
  747. ·
  748. · · · · · · · · · */
  749. window.userArrayCache = new Object();
  750. window.convertUriToUserArrayCacheKey = new Object();
  751. window.convertStatusnetProfileUrlToUserArrayCacheKey = new Object();
  752. window.convertLocalIdToUserArrayCacheKey = new Object();
  753. function userArrayCacheStore(data) {
  754. if(typeof data == 'undefined') {
  755. return false;
  756. }
  757. // if we are passed a data object with both local and external data, use external data as key
  758. if(typeof data.local != 'undefined'
  759. && typeof data.local.statusnet_profile_url != 'undefined'
  760. && typeof data.external != 'undefined'
  761. && typeof data.external.statusnet_profile_url != 'undefined') {
  762. var instanceUrlWithoutProtocol = guessInstanceUrlWithoutProtocolFromProfileUrlAndNickname(data.external.statusnet_profile_url, data.external.screen_name);
  763. var key = instanceUrlWithoutProtocol + '/' + data.external.screen_name;
  764. var dataToStore = data;
  765. }
  766. // we can also get either local...
  767. else if(typeof data.local != 'undefined' && typeof data.local.statusnet_profile_url != 'undefined' ) {
  768. var instanceUrlWithoutProtocol = guessInstanceUrlWithoutProtocolFromProfileUrlAndNickname(data.local.statusnet_profile_url, data.local.screen_name);
  769. var key = instanceUrlWithoutProtocol + '/' + data.local.screen_name;
  770. data.external = false;
  771. var dataToStore = data;
  772. }
  773. // ...or external...
  774. else if(typeof data.external != 'undefined' && typeof data.external.statusnet_profile_url != 'undefined' ) {
  775. var instanceUrlWithoutProtocol = guessInstanceUrlWithoutProtocolFromProfileUrlAndNickname(data.external.statusnet_profile_url, data.external.screen_name);
  776. var key = instanceUrlWithoutProtocol + '/' + data.external.screen_name;
  777. data.local = false;
  778. var dataToStore = data;
  779. }
  780. // ...or an unspecified data object, in which case we check the avatar urls to see if it's local or external
  781. else if (typeof data.statusnet_profile_url != 'undefined') {
  782. var instanceUrlWithoutProtocol = guessInstanceUrlWithoutProtocolFromProfileUrlAndNickname(data.statusnet_profile_url, data.screen_name);
  783. var key = instanceUrlWithoutProtocol + '/' + data.screen_name;
  784. var localOrExternal = detectLocalOrExternalUserObject(data);
  785. // local
  786. if(localOrExternal == 'local'){
  787. var dataToStore = {local:data,external:false};
  788. }
  789. // external
  790. else {
  791. var dataToStore = {external:data,local:false};
  792. }
  793. }
  794. else {
  795. return false;
  796. }
  797. // store
  798. if(typeof window.userArrayCache[key] == 'undefined') {
  799. window.userArrayCache[key] = dataToStore;
  800. window.userArrayCache[key].modified = Date.now();
  801. // easy conversion between URI and statusnet_profile_url and the key we're using in window.userArrayCache
  802. window.convertLocalIdToUserArrayCacheKey[parseInt(dataToStore.local.id, 10)] = key;
  803. window.convertUriToUserArrayCacheKey[dataToStore.local.ostatus_uri] = key;
  804. window.convertStatusnetProfileUrlToUserArrayCacheKey[dataToStore.local.statusnet_profile_url] = key;
  805. }
  806. else {
  807. if(dataToStore.local) {
  808. // keep old status if newer data doesn't have any
  809. if(typeof dataToStore.local.status == 'undefined' && typeof window.userArrayCache[key].local.status != 'undefined') {
  810. dataToStore.local.status = window.userArrayCache[key].local.status;
  811. }
  812. window.userArrayCache[key].local = dataToStore.local;
  813. // easy conversion between URI and statusnet_profile_url and the key we're using in window.userArrayCache
  814. window.convertLocalIdToUserArrayCacheKey[dataToStore.local.id] = key;
  815. window.convertUriToUserArrayCacheKey[dataToStore.local.ostatus_uri] = key;
  816. window.convertStatusnetProfileUrlToUserArrayCacheKey[dataToStore.local.statusnet_profile_url] = key;
  817. }
  818. if(dataToStore.external) {
  819. window.userArrayCache[key].external = dataToStore.external;
  820. // easy conversion between URI and the key we're using in window.userArrayCache
  821. window.convertUriToUserArrayCacheKey[dataToStore.external.ostatus_uri] = key;
  822. window.convertStatusnetProfileUrlToUserArrayCacheKey[dataToStore.external.statusnet_profile_url] = key;
  823. }
  824. // store the time when this record was modified
  825. if(dataToStore.local || dataToStore.external) {
  826. window.userArrayCache[key].modified = Date.now();
  827. }
  828. }
  829. }
  830. function userArrayCacheGetByLocalNickname(localNickname) {
  831. if(localNickname.substring(0,1) == '@') {
  832. localNickname = localNickname.substring(1);
  833. }
  834. if(typeof window.userArrayCache[window.siteRootDomain + '/' + localNickname] != 'undefined') {
  835. return window.userArrayCache[window.siteRootDomain + '/' + localNickname];
  836. }
  837. else {
  838. return false;
  839. }
  840. }
  841. function userArrayCacheGetByProfileUrlAndNickname(profileUrl, nickname) {
  842. var possibleLocalId = false;
  843. if(nickname.substring(0,1) == '@') {
  844. nickname = nickname.substring(1);
  845. }
  846. if(profileUrl.indexOf(window.siteInstanceURL + 'user/') == 0) {
  847. possibleLocalId = parseInt(profileUrl.substring(window.siteInstanceURL.length+5),10);
  848. }
  849. // the url might match a known profile uri
  850. if(typeof window.convertUriToUserArrayCacheKey[profileUrl] != 'undefined') {
  851. if(typeof window.userArrayCache[window.convertUriToUserArrayCacheKey[profileUrl]] != 'undefined') {
  852. return window.userArrayCache[window.convertUriToUserArrayCacheKey[profileUrl]];
  853. }
  854. }
  855. // or the href attribute might match a known statusnet_profile_url
  856. else if(typeof window.convertStatusnetProfileUrlToUserArrayCacheKey[profileUrl] != 'undefined') {
  857. if(typeof window.userArrayCache[window.convertStatusnetProfileUrlToUserArrayCacheKey[profileUrl]] != 'undefined') {
  858. return window.userArrayCache[window.convertStatusnetProfileUrlToUserArrayCacheKey[profileUrl]];
  859. }
  860. }
  861. // or the local id might match a known id
  862. else if(typeof window.convertLocalIdToUserArrayCacheKey[possibleLocalId] != 'undefined') {
  863. if(typeof window.userArrayCache[window.convertLocalIdToUserArrayCacheKey[possibleLocalId]] != 'undefined') {
  864. return window.userArrayCache[window.convertLocalIdToUserArrayCacheKey[possibleLocalId]];
  865. }
  866. }
  867. // or we try to guess the instance url, and see if we have a match in our cache
  868. else if(typeof window.userArrayCache[guessInstanceUrlWithoutProtocolFromProfileUrlAndNickname(profileUrl, nickname) + '/' + nickname] != 'undefined') {
  869. return window.userArrayCache[guessInstanceUrlWithoutProtocolFromProfileUrlAndNickname(profileUrl, nickname) + '/' + nickname];
  870. }
  871. // we couldn't find any cached user array
  872. else {
  873. return false;
  874. }
  875. }
  876. function userArrayCacheGetUserNicknameById(id) {
  877. var possibleUserURI = window.siteInstanceURL + 'user/' + id;
  878. var key = window.convertUriToUserArrayCacheKey[possibleUserURI];
  879. if(typeof key != 'undefined') {
  880. if(typeof window.userArrayCache[key] != 'undefined') {
  881. return window.userArrayCache[key].local.screen_name;
  882. }
  883. }
  884. return false;
  885. }
  886. /* ·
  887. ·
  888. · Detect if the supplied user object is from the local server or external
  889. ·
  890. · · · · · · · · · */
  891. function detectLocalOrExternalUserObject(userObject) {
  892. if(isLocalURL(userObject.profile_image_url) ) {
  893. return 'local';
  894. } else {
  895. return 'external';
  896. }
  897. }
  898. /* ·
  899. ·
  900. · Guess instance's base installation url without protocol from a profile url
  901. ·
  902. · · · · · · · · · */
  903. function guessInstanceUrlWithoutProtocolFromProfileUrlAndNickname(profileUrl, nickname) {
  904. // remove protocol
  905. var guessedInstanceUrl = removeProtocolFromUrl(profileUrl)
  906. // user/id-style profile urls
  907. if(guessedInstanceUrl.indexOf('/user/') > -1 &&
  908. $.isNumeric(guessedInstanceUrl.substring(guessedInstanceUrl.lastIndexOf('/user/')+6))) {
  909. guessedInstanceUrl = guessedInstanceUrl.substring(0,guessedInstanceUrl.lastIndexOf('/user/'));
  910. }
  911. // nickname-style profile urls
  912. else if(guessedInstanceUrl.substring(guessedInstanceUrl.lastIndexOf('/')+1) == nickname) {
  913. guessedInstanceUrl = guessedInstanceUrl.substring(0,guessedInstanceUrl.lastIndexOf('/'));
  914. }
  915. // remove trailing "index.php" if the instance doesn't use mod_rewrite
  916. if(guessedInstanceUrl.substring(guessedInstanceUrl.lastIndexOf('/')) == '/index.php') {
  917. guessedInstanceUrl = guessedInstanceUrl.substring(0,guessedInstanceUrl.lastIndexOf('/'));
  918. }
  919. // there was a bug once that made some instances have multiple /:s in their url,
  920. // so make sure there's no trailing /:s
  921. while (guessedInstanceUrl.slice(-1) == '/') {
  922. guessedInstanceUrl = guessedInstanceUrl.slice(0,-1);
  923. }
  924. // fix new mastodon style profile urls
  925. if(guessedInstanceUrl.indexOf('/@') > -1) {
  926. guessedInstanceUrl = guessedInstanceUrl.substring(0, guessedInstanceUrl.indexOf('/@'));
  927. }
  928. return guessedInstanceUrl;
  929. }
  930. /* ·
  931. ·
  932. · Remove the protocol (e.g. "http://") from an URL
  933. ·
  934. · · · · · · · · · */
  935. function removeProtocolFromUrl(url) {
  936. if(typeof url == 'undefined'
  937. || url === null
  938. || url === false
  939. || url == '') {
  940. return '';
  941. }
  942. if(url.indexOf('://') == -1) {
  943. return url;
  944. }
  945. return url.substring(url.indexOf('://')+3);
  946. }
  947. /* ·
  948. ·
  949. · Get host from URL
  950. ·
  951. · · · · · · · · · */
  952. function getHost(url) {
  953. var a = document.createElement('a');
  954. a.href = url;
  955. return a.hostname;
  956. }
  957. /* ·
  958. ·
  959. · Is this url a link to my profile?
  960. ·
  961. · · · · · · · · · */
  962. function thisIsALinkToMyProfile(url) {
  963. if(typeof url == 'undefined') {
  964. return false;
  965. }
  966. if(!window.loggedIn) {
  967. return false;
  968. }
  969. if(url.slice(-1) == '/') { // remove trailing '/'
  970. url = url.slice(0,-1);
  971. }
  972. var urlWithoutProtocol = removeProtocolFromUrl(url);
  973. if(removeProtocolFromUrl(window.loggedIn.statusnet_profile_url) == urlWithoutProtocol) {
  974. return true;
  975. }
  976. var userIdUrlWithoutProtocol = removeProtocolFromUrl(window.siteInstanceURL) + 'user/' + window.loggedIn.id;
  977. if(userIdUrlWithoutProtocol == urlWithoutProtocol) {
  978. return true;
  979. }
  980. return false;
  981. }
  982. /* ·
  983. ·
  984. · Iterates recursively through an API response in search for user data to cache
  985. · If we find a "statusnet_profile_url" key we assume the parent is a user array/object
  986. ·
  987. · · · · · · · · · · · · · */
  988. function searchForUserDataToCache(obj) {
  989. for (var property in obj) {
  990. if (obj.hasOwnProperty(property)) {
  991. if (typeof obj[property] == "object") {
  992. searchForUserDataToCache(obj[property]);
  993. }
  994. else if(typeof obj[property] == 'string' && property == 'statusnet_profile_url') {
  995. userArrayCacheStore(obj);
  996. }
  997. }
  998. }
  999. }
  1000. /* ·
  1001. ·
  1002. · Updates user data loaded into the stream with the latest data from the user array cache
  1003. · This function should therefor always be invoked _after_ searchForUserDataToCache()
  1004. ·
  1005. · · · · · · · · · · · · · */
  1006. function updateUserDataInStream() {
  1007. var timeNow = Date.now();
  1008. $.each(window.userArrayCache,function(k,userArray){
  1009. // if the cache record was updated the latest second, we assume this is brand new info that we haven't
  1010. // updated the stream with
  1011. if(typeof userArray.local != 'undefined'
  1012. && userArray.local !== false
  1013. && typeof userArray.modified != 'undefined'
  1014. && (timeNow-userArray.modified)<1000) {
  1015. // add/remove silenced class to stream items and profile cards
  1016. if(userArray.local.is_silenced === true) {
  1017. $('.stream-item[data-user-id=' + userArray.local.id + ']').addClass('silenced');
  1018. $('.profile-card .profile-header-inner[data-user-id=' + userArray.local.id + ']').addClass('silenced');
  1019. $('.user-menu-cog[data-user-id=' + userArray.local.id + ']').addClass('silenced');
  1020. }
  1021. else {
  1022. $('.stream-item[data-user-id=' + userArray.local.id + ']').removeClass('silenced')
  1023. $('.profile-card .profile-header-inner[data-user-id=' + userArray.local.id + ']').removeClass('silenced');
  1024. $('.user-menu-cog[data-user-id=' + userArray.local.id + ']').removeClass('silenced');
  1025. }
  1026. // add/remove sandboxed class to stream items and profile cards
  1027. if(userArray.local.is_sandboxed === true) {
  1028. $('.stream-item[data-user-id=' + userArray.local.id + ']').addClass('sandboxed');
  1029. $('.profile-card .profile-header-inner[data-user-id=' + userArray.local.id + ']').addClass('sandboxed');
  1030. $('.user-menu-cog[data-user-id=' + userArray.local.id + ']').addClass('sandboxed');
  1031. }
  1032. else {
  1033. $('.stream-item[data-user-id=' + userArray.local.id + ']').removeClass('sandboxed')
  1034. $('.profile-card .profile-header-inner[data-user-id=' + userArray.local.id + ']').removeClass('sandboxed');
  1035. $('.user-menu-cog[data-user-id=' + userArray.local.id + ']').removeClass('sandboxed');
  1036. }
  1037. // profile size avatars (notices, users)
  1038. $.each($('img.avatar.profile-size[data-user-id="' + userArray.local.id + '"]'),function(){
  1039. if($(this).attr('src') != userArray.local.profile_image_url_profile_size) {
  1040. $(this).attr('src',userArray.local.profile_image_url_profile_size);
  1041. }
  1042. });
  1043. // standard size avatars (notifications)
  1044. $.each($('img.avatar.standard-size[data-user-id="' + userArray.local.id + '"]'),function(){
  1045. if($(this).attr('src') != userArray.local.profile_image_url) {
  1046. $(this).attr('src',userArray.local.profile_image_url);
  1047. }
  1048. });
  1049. // full names
  1050. $.each($('strong.name[data-user-id="' + userArray.local.id + '"],\
  1051. .fullname[data-user-id="' + userArray.local.id + '"]'),function(){
  1052. if($(this).html() != userArray.local.name) {
  1053. $(this).html(userArray.local.name);
  1054. }
  1055. });
  1056. // user/screen names
  1057. $.each($('.screen-name[data-user-id="' + userArray.local.id + '"]'),function(){
  1058. if($(this).html().substring(1) != userArray.local.screen_name) {
  1059. $(this).html('@' + userArray.local.screen_name);
  1060. }
  1061. });
  1062. // profile urls
  1063. // try to find the last account group with this id, if the statusnet_profile_url seems to
  1064. // be changed we replace it wherever we can find it, even in list urls etc that starts with statusnet_profile_url
  1065. if(userArray.local.is_local === true && $('a.account-group[data-user-id="' + userArray.local.id + '"]').last().attr('href') != userArray.local.statusnet_profile_url) {
  1066. var oldStatusnetProfileURL = $('a.account-group[data-user-id="' + userArray.local.id + '"]').last().attr('href');
  1067. // all links with the exact statusnet_profile_url
  1068. $.each($('[href="' + oldStatusnetProfileURL + '"]'),function(){
  1069. $(this).attr('href',userArray.local.statusnet_profile_url);
  1070. });
  1071. // links starting with statusnet_profile_url
  1072. $.each($('[href*="' + oldStatusnetProfileURL + '/"]'),function(){
  1073. $(this).attr('href',$(this).attr('href').replace(oldStatusnetProfileURL + '/',userArray.local.statusnet_profile_url + '/'));
  1074. });
  1075. }
  1076. // cover photos
  1077. $.each($('.profile-header-inner[data-user-id="' + userArray.local.id + '"]'),function(){
  1078. if($(this).css('background-image') != 'url("' + userArray.local.cover_photo + '")' && userArray.local.cover_photo != false) {
  1079. $(this).css('background-image','url("' + userArray.local.cover_photo + '")');
  1080. }
  1081. });
  1082. // the window.following object might need updating also
  1083. if(typeof window.following != 'undefined' && typeof window.following[userArray.local.id] != 'undefined') {
  1084. if(window.following[userArray.local.id].avatar != userArray.local.profile_image_url) {
  1085. window.following[userArray.local.id].avatar = userArray.local.profile_image_url;
  1086. }
  1087. if(window.following[userArray.local.id].name != userArray.local.name) {
  1088. window.following[userArray.local.id].name = userArray.local.name;
  1089. }
  1090. if(window.following[userArray.local.id].username != userArray.local.screen_name) {
  1091. window.following[userArray.local.id].username = userArray.local.screen_name;
  1092. }
  1093. }
  1094. }
  1095. });
  1096. }
  1097. /* ·
  1098. ·
  1099. · Iterates recursively through an API response in search for updated notice data
  1100. · If we find a "repeated" key we assume the parent is a notice object (chosen arbitrary)
  1101. ·
  1102. · · · · · · · · · · · · · */
  1103. window.knownDeletedNotices = new Object();
  1104. function searchForUpdatedNoticeData(obj) {
  1105. var streamItemsUpdated = false;
  1106. for (var property in obj) {
  1107. if (obj.hasOwnProperty(property)) {
  1108. if (typeof obj[property] == "object") {
  1109. searchForUpdatedNoticeData(obj[property]);
  1110. }
  1111. else if(typeof obj[property] == 'boolean' && property == 'repeated') {
  1112. var streamItemFoundInFeed = $('.stream-item[data-conversation-id][data-quitter-id="' + obj.id + '"]'); // data-conversation-id identifies it as a notice, not a user or something
  1113. // if this is a special qvitter-delete-notice activity notice it means we try to hide
  1114. // the deleted notice from our stream
  1115. // the uri is in the obj.text var, between the double curly brackets
  1116. if(typeof obj.qvitter_delete_notice != 'undefined' && obj.qvitter_delete_notice == true) {
  1117. var uriToHide = obj.text.substring(obj.text.indexOf('{{')+2,obj.text.indexOf('}}'));
  1118. window.knownDeletedNotices[uriToHide] = true;
  1119. var streamItemToHide = $('.stream-item[data-uri="' + uriToHide + '"]');
  1120. slideUpAndRemoveStreamItem(streamItemToHide);
  1121. streamItemsUpdated = true;
  1122. }
  1123. // if this is not a delete notice it means the notice exists and is not deleted,
  1124. // correct any notices that are marked as unrepeated, they might have
  1125. // been marked like that by mistake (i.e. a bug...)
  1126. else if(streamItemFoundInFeed.hasClass('unrepeated')) {
  1127. streamItemFoundInFeed.removeClass('unrepeated always-hidden');
  1128. streamItemsUpdated = true;
  1129. }
  1130. // ordinary notices
  1131. else if(streamItemFoundInFeed.length>0) {
  1132. var queetFoundInFeed = streamItemFoundInFeed.children('.queet');
  1133. var queetID = streamItemFoundInFeed.attr('data-quitter-id');
  1134. // sometimes activity notices don't get the is_activity flag set to true
  1135. // maybe because they were in the process of being saved when
  1136. // we first got them
  1137. if(obj.is_post_verb === false) {
  1138. streamItemFoundInFeed.addClass('activity always-hidden');
  1139. streamItemsUpdated = true;
  1140. }
  1141. // update the avatar row if the queet is expanded and the numbers are not the same
  1142. if(streamItemFoundInFeed.hasClass('expanded')) {
  1143. var oldFavNum = parseInt(queetFoundInFeed.find('.action-fav-num').text(),10);
  1144. var oldRQNum = parseInt(queetFoundInFeed.find('.action-rq-num').text(),10);
  1145. if(oldFavNum != obj.fave_num || oldRQNum != obj.repeat_num) {
  1146. getFavsAndRequeetsForQueet(streamItemFoundInFeed, queetID);
  1147. }
  1148. }
  1149. // attachments might have been added/changed/have had time to be processed
  1150. if(queetFoundInFeed.children('script.attachment-json').text() != JSON.stringify(obj.attachments)) {
  1151. if(queetFoundInFeed.children('script.attachment-json').length == 0) {
  1152. queetFoundInFeed.prepend('<script class="attachment-json" type="application/json">' + JSON.stringify(obj.attachments) + '</script>');
  1153. }
  1154. else {
  1155. queetFoundInFeed.children('script.attachment-json').text(JSON.stringify(obj.attachments));
  1156. }
  1157. var attachmentsHTMLBuild = buildAttachmentHTML(obj.attachments);
  1158. var thumbsIsHidden = false;
  1159. if(queetFoundInFeed.find('.queet-thumbs').hasClass('hide-thumbs')) {
  1160. var thumbsIsHidden = true;
  1161. }
  1162. queetFoundInFeed.find('.queet-thumbs').remove();
  1163. queetFoundInFeed.find('.oembed-data').remove();
  1164. placeQuotedNoticesInQueetText(attachmentsHTMLBuild.quotedNotices,queetFoundInFeed.find('.queet-text'));
  1165. // we might want to hide urls (rendered as attachments) in the queet text
  1166. $.each(queetFoundInFeed.find('.queet-text').find('a'),function(){
  1167. if(attachmentsHTMLBuild.urlsToHide.indexOf($(this).text()) > -1) {
  1168. $(this).removeAttr('style'); // temporary fix
  1169. $(this).addClass('hidden-embedded-link-in-queet-text');
  1170. }
  1171. });
  1172. queetFoundInFeed.find('.queet-text').after(attachmentsHTMLBuild.html);
  1173. if(thumbsIsHidden) {
  1174. queetFoundInFeed.find('.queet-thumbs').addClass('hide-thumbs');
  1175. }
  1176. streamItemsUpdated = true;
  1177. }
  1178. // attentions might have been added to a notice
  1179. if(queetFoundInFeed.children('script.attentions-json').text() != JSON.stringify(obj.attentions)) {
  1180. if(queetFoundInFeed.children('script.attentions-json').length == 0) {
  1181. queetFoundInFeed.prepend('<script class="attentions-json" type="application/json">' + JSON.stringify(obj.attentions) + '</script>');
  1182. }
  1183. else {
  1184. queetFoundInFeed.children('script.attentions-json').text(JSON.stringify(obj.attentions));
  1185. }
  1186. }
  1187. // set favorite data
  1188. queetFoundInFeed.find('.action-fav-num').attr('data-fav-num',obj.fave_num);
  1189. queetFoundInFeed.find('.action-fav-num').html(obj.fave_num);
  1190. if(obj.favorited) {
  1191. streamItemFoundInFeed.addClass('favorited');
  1192. queetFoundInFeed.find('.action-fav-container').children('.with-icn').addClass('done');
  1193. queetFoundInFeed.find('.action-fav-container').find('.icon.sm-fav').attr('data-tooltip',window.sL.favoritedVerb);
  1194. streamItemsUpdated = true;
  1195. }
  1196. else {
  1197. streamItemFoundInFeed.removeClass('favorited');
  1198. queetFoundInFeed.find('.action-fav-container').children('.with-icn').removeClass('done');
  1199. queetFoundInFeed.find('.action-fav-container').find('.icon.sm-fav').attr('data-tooltip',window.sL.favoriteVerb);
  1200. streamItemsUpdated = true;
  1201. }
  1202. // set repeat data
  1203. queetFoundInFeed.find('.action-rq-num').attr('data-rq-num',obj.repeat_num);
  1204. queetFoundInFeed.find('.action-rq-num').html(obj.repeat_num);
  1205. if(obj.repeated) {
  1206. streamItemFoundInFeed.addClass('requeeted');
  1207. queetFoundInFeed.find('.action-rt-container').children('.with-icn').addClass('done');
  1208. queetFoundInFeed.find('.action-rt-container').find('.icon.sm-rt').attr('data-tooltip',window.sL.requeetedVerb);
  1209. streamItemFoundInFeed.attr('data-requeeted-by-me-id',obj.repeated_id);
  1210. streamItemsUpdated = true;
  1211. }
  1212. else {
  1213. streamItemFoundInFeed.removeClass('requeeted');
  1214. queetFoundInFeed.find('.action-rt-container').children('.with-icn').removeClass('done');
  1215. queetFoundInFeed.find('.action-rt-container').find('.icon.sm-rt').attr('data-tooltip',window.sL.requeetVerb);
  1216. streamItemFoundInFeed.removeAttr('data-requeeted-by-me-id');
  1217. streamItemsUpdated = true;
  1218. }
  1219. }
  1220. }
  1221. }
  1222. }
  1223. if(streamItemsUpdated) {
  1224. // TODO, create a queue that runs with setInterval instead, say every 5 s,
  1225. // that way we can run rememberStreamStateInLocalStorage() in the background,
  1226. // and don't slow down stream change etc
  1227. // rememberStreamStateInLocalStorage();
  1228. }
  1229. }
  1230. /* ·
  1231. ·
  1232. · Removes a deleted stream item from the feed gracefully, if not already hidden
  1233. ·
  1234. · · · · · · · · · */
  1235. function slideUpAndRemoveStreamItem(streamItem,callback) {
  1236. if(streamItem.length>0 && !streamItem.hasClass('always-hidden')) {
  1237. streamItem.animate({opacity:'0.2'},1000,'linear',function(){
  1238. $(this).css('height',$(this).height() + 'px');
  1239. $(this).animate({height:'0px'},500,'linear',function(){
  1240. $(this).addClass('deleted always-hidden');
  1241. rememberStreamStateInLocalStorage();
  1242. if(typeof callback == 'function') {
  1243. callback();
  1244. }
  1245. });
  1246. });
  1247. }
  1248. }
  1249. /* ·
  1250. ·
  1251. · Store the current stream's state (html) in localStorage (if we're logged in)
  1252. ·
  1253. · · · · · · · · · */
  1254. function rememberStreamStateInLocalStorage() {
  1255. if(typeof window.currentStreamObject != 'undefined') {
  1256. // don't store expanded content, only store profile card and the top 20 visible stream-items
  1257. var firstTwentyVisibleHTML = '';
  1258. var i = 0;
  1259. $.each($('#feed-body').children('.stream-item'),function(k,streamItem){
  1260. firstTwentyVisibleHTML += $(streamItem).outerHTML();
  1261. if(!$(streamItem).hasClass('always-hidden')) {
  1262. i++;
  1263. }
  1264. if(i>20) {
  1265. return false;
  1266. }
  1267. });
  1268. var feed = $('<div/>').append(firstTwentyVisibleHTML);
  1269. // we add some of these things again when the notices are fetched from the cache
  1270. cleanStreamItemsFromClassesAndConversationElements(feed);
  1271. var feedHtml = feed.html();
  1272. var profileCardHtml = $('#feed').siblings('.profile-card').outerHTML();
  1273. var streamData = {
  1274. card: profileCardHtml,
  1275. feed: feedHtml
  1276. };
  1277. localStorageObjectCache_STORE('streamState',window.currentStreamObject.path, streamData);
  1278. }
  1279. }
  1280. /* ·
  1281. ·
  1282. · Clean stream items from classes and conversation elements,
  1283. · to use e.g. for caching and including in popup footers
  1284. ·
  1285. · @param streamItems: jQuery object with stream items as children
  1286. ·
  1287. · · · · · · · · · */
  1288. function cleanStreamItemsFromClassesAndConversationElements(streamItems) {
  1289. streamItems.children('.stream-item').removeClass('profile-blocked-by-me');
  1290. streamItems.children('.stream-item').children('.queet').removeAttr('data-tooltip'); // can contain tooltip about blocked user
  1291. streamItems.find('.temp-post').remove();
  1292. streamItems.children('.stream-item').removeClass('not-seen');
  1293. streamItems.children('.stream-item').removeClass('hidden-repeat'); // this means we need hide repeats when adding cached notices to feed later
  1294. streamItems.children('.stream-item').removeClass('selected-by-keyboard');
  1295. streamItems.find('.dropdown-menu').remove();
  1296. streamItems.find('.stream-item').removeClass('expanded').removeClass('next-expanded').removeClass('hidden').removeClass('collapsing').addClass('visible');
  1297. streamItems.children('.stream-item').each(function() {
  1298. cleanUpAfterCollapseQueet($(this));
  1299. });
  1300. }
  1301. /* ·
  1302. ·
  1303. · Hide all instances (repeats) of a notice but the first/oldest one
  1304. ·
  1305. · @param streamItems: jQuery object with stream items as children
  1306. ·
  1307. · · · · · · · · · */
  1308. function hideAllButOldestInstanceOfStreamItem(streamItemContainer) {
  1309. streamItemContainer.children('.stream-item').each(function(){
  1310. // if this stream item have siblings _after_ it, with the same id, hide it!
  1311. if($(this).nextAll('.stream-item[data-quitter-id="' + $(this).attr('data-quitter-id') + '"]').length > 0) {
  1312. $(this).addClass('hidden-repeat');
  1313. }
  1314. });
  1315. return streamItemContainer;
  1316. }
  1317. /* ·
  1318. ·
  1319. · Gets the full unshortened HTML for a queet
  1320. ·
  1321. · · · · · · · · · */
  1322. function getFullUnshortenedHtmlForQueet(streamItem, cacheOnly) {
  1323. if(typeof cacheOnly == 'undefined') {
  1324. var cacheOnly = false;
  1325. }
  1326. var queet = streamItem.children('.queet');
  1327. var queetId = streamItem.attr('data-quitter-id');
  1328. var attachmentMore = queet.find('span.attachment.more');
  1329. // only if actually shortened
  1330. if(attachmentMore.length>0
  1331. && queet.children('script.attachment-json').length > 0
  1332. && queet.children('script.attachment-json').text() != 'undefined') {
  1333. // first try localstorage cache
  1334. var cacheData = localStorageObjectCache_GET('fullQueetHtml',queetId);
  1335. if(cacheData) {
  1336. queet.find('.queet-text').html(cacheData);
  1337. queet.outerHTML(detectRTL(queet.outerHTML()));
  1338. }
  1339. // then try static html file attachment, that we should have in the attachment-json script element
  1340. else if(cacheOnly === false){
  1341. var attachmentId = attachmentMore.attr('data-attachment-id');
  1342. $.each(JSON.parse(queet.children('script.attachment-json').text()), function(k,attachment) {
  1343. if(attachment.id == attachmentId) {
  1344. $.get(attachment.url,function(data){
  1345. if(data) {
  1346. // get body and store in localStorage
  1347. var bodyHtml = $('<html/>').html(data).find('body').html();
  1348. localStorageObjectCache_STORE('fullQueetHtml',queetId,bodyHtml);
  1349. queet.find('.queet-text').html($.trim(bodyHtml));
  1350. queet.outerHTML(detectRTL(queet.outerHTML()));
  1351. }
  1352. });
  1353. return false;
  1354. }
  1355. });
  1356. }
  1357. }
  1358. }
  1359. /* ·
  1360. ·
  1361. · Appends a user to the array containing the mentions suggestions to show when typing a notice
  1362. ·
  1363. · · · · · · · · · */
  1364. function appendUserToMentionsSuggestionsArray(user) {
  1365. if(typeof window.following[user.id] == 'undefined') {
  1366. // in the window.following array, we use "false" as url if it's a user from this instance
  1367. if(user.is_local) {
  1368. var url = false;
  1369. }
  1370. else {
  1371. var url = guessInstanceUrlWithoutProtocolFromProfileUrlAndNickname(user.statusnet_profile_url,user.screen_name);
  1372. }
  1373. var userToAdd = {
  1374. avatar: user.profile_image_url,
  1375. id: user.id,
  1376. name: user.name,
  1377. url: url,
  1378. username: user.screen_name
  1379. };
  1380. window.following[user.id] = userToAdd;
  1381. }
  1382. }
  1383. /* ·
  1384. ·
  1385. · Is a profile pref in the qvitter namespace enabled?
  1386. ·
  1387. · · · · · · · · · */
  1388. function isQvitterProfilePrefEnabled(topic) {
  1389. if(typeof window.qvitterProfilePrefs != 'undefined' && typeof window.qvitterProfilePrefs[topic] != 'undefined'
  1390. && window.qvitterProfilePrefs[topic] !== null
  1391. && window.qvitterProfilePrefs[topic] != ''
  1392. && window.qvitterProfilePrefs[topic] !== false
  1393. && window.qvitterProfilePrefs[topic] != 0
  1394. && window.qvitterProfilePrefs[topic] != '0') {
  1395. return true;
  1396. }
  1397. return false;
  1398. }
  1399. /* ·
  1400. ·
  1401. · Is this user muted?
  1402. ·
  1403. · · · · · · · · · */
  1404. function isUserMuted(userID) {
  1405. if(isQvitterProfilePrefEnabled('mute:' + userID)) {
  1406. return true;
  1407. }
  1408. else {
  1409. return false;
  1410. }
  1411. }
  1412. /* ·
  1413. ·
  1414. · Display unread notifications
  1415. ·
  1416. · · · · · · · · · */
  1417. function displayOrHideUnreadNotifications(notifications) {
  1418. var data = $.parseJSON(notifications);
  1419. var totNotif = 0;
  1420. if(data !== null && typeof data != 'undefined') {
  1421. $.each(data,function(k,v){
  1422. totNotif = totNotif + parseInt(v,10);
  1423. });
  1424. }
  1425. if(window.currentStreamObject.name == 'notifications') {
  1426. var hiddenNotifications = $('#feed-body').find('.stream-item.hidden:not(.always-hidden)').length;
  1427. if(hiddenNotifications>0) {
  1428. totNotif = totNotif + hiddenNotifications;
  1429. }
  1430. }
  1431. if(totNotif>0) {
  1432. $('#unseen-notifications').html(totNotif);
  1433. document.title = '(' + totNotif + ') ' + window.siteTitle; // update html page title
  1434. $('#unseen-notifications').show();
  1435. }
  1436. else {
  1437. $('#unseen-notifications').hide();
  1438. document.title = window.siteTitle;
  1439. }
  1440. }
  1441. /* ·
  1442. ·
  1443. · Removes HTML special chars recursively from strings in objects
  1444. · with exceptions: "statusnet_html" found in notices, which we assume
  1445. · gnusocial already stripped from xss, and the "source" which should be
  1446. · html rendered by gnusocial itself and not open for attacks
  1447. ·
  1448. · @param obj: the object to search and replace in
  1449. ·
  1450. · · · · · · · · · · · · · */
  1451. function iterateRecursiveReplaceHtmlSpecialChars(obj) {
  1452. for (var property in obj) {
  1453. if (obj.hasOwnProperty(property)) {
  1454. if (typeof obj[property] == "object") {
  1455. iterateRecursiveReplaceHtmlSpecialChars(obj[property]);
  1456. }
  1457. else if(typeof obj[property] == 'string'
  1458. && property != 'statusnet_html'
  1459. && property != 'oembedHTML' // we trust this to be cleaned server side
  1460. && property != 'source') {
  1461. obj[property] = replaceHtmlSpecialChars(obj[property]);
  1462. }
  1463. }
  1464. }
  1465. return obj;
  1466. }
  1467. function replaceHtmlSpecialChars(text) {
  1468. // don't do anything if the text is undefined
  1469. if(typeof text == 'undefined') {
  1470. return text;
  1471. }
  1472. var map = {
  1473. '&': '&amp;',
  1474. '<': '&lt;',
  1475. '>': '&gt;',
  1476. '"': '&quot;',
  1477. "'": '&#039;'
  1478. };
  1479. return text.replace(/[&<>"']/g, function(m) { return map[m]; });
  1480. }
  1481. /* ·
  1482. ·
  1483. · Checks if register form is valid
  1484. ·
  1485. · @returns true or false
  1486. ·
  1487. · · · · · · · · · */
  1488. function validateRegisterForm(o) {
  1489. var nickname = o.find('#signup-user-nickname-step2');
  1490. var fullname = o.find('#signup-user-name-step2');
  1491. var email = o.find('#signup-user-email-step2');
  1492. var homepage = o.find('#signup-user-homepage-step2');
  1493. var bio = o.find('#signup-user-bio-step2');
  1494. var loc = o.find('#signup-user-location-step2');
  1495. var password1 = o.find('#signup-user-password1-step2');
  1496. var password2 = o.find('#signup-user-password2-step2');
  1497. var passwords = o.find('#signup-user-password1-step2,#signup-user-password2-step2');
  1498. var allFieldsValid = true;
  1499. if(nickname.val().length>1 && /^[a-zA-Z0-9]+$/.test(nickname.val())) {
  1500. nickname.removeClass('invalid'); } else { nickname.addClass('invalid'); if(allFieldsValid)allFieldsValid=false; }
  1501. if(fullname.val().length < 255) {
  1502. fullname.removeClass('invalid'); } else { fullname.addClass('invalid'); if(allFieldsValid)allFieldsValid=false; }
  1503. if(validEmail(email.val())) {
  1504. email.removeClass('invalid'); } else { email.addClass('invalid'); if(allFieldsValid)allFieldsValid=false; }
  1505. if($.trim(homepage.val()).length==0 || /^(ftp|http|https):\/\/[^ "]+$/.test(homepage.val())) {
  1506. homepage.removeClass('invalid'); } else { homepage.addClass('invalid'); if(allFieldsValid)allFieldsValid=false; }
  1507. if(bio.val().length < 140) {
  1508. bio.removeClass('invalid'); } else { bio.addClass('invalid'); if(allFieldsValid)allFieldsValid=false; }
  1509. if(loc.val().length < 255) {
  1510. loc.removeClass('invalid'); } else { loc.addClass('invalid'); if(allFieldsValid)allFieldsValid=false; }
  1511. if(password1.val().length>5 && password2.val().length>5 && password1.val() == password2.val()) {
  1512. passwords.removeClass('invalid'); } else { passwords.addClass('invalid'); if(allFieldsValid)allFieldsValid=false; }
  1513. return allFieldsValid;
  1514. }
  1515. function validEmail(email) {
  1516. if(/^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(email)) {
  1517. return true;
  1518. }
  1519. else {
  1520. return false;
  1521. }
  1522. }
  1523. /* ·
  1524. ·
  1525. · Checks if edit profile form is valid
  1526. ·
  1527. · @returns true or false
  1528. ·
  1529. · · · · · · · · · */
  1530. function validateEditProfileForm(o) {
  1531. var fullname = o.find('input.fullname');
  1532. var homepage = o.find('input.url');
  1533. var bio = o.find('textarea.bio');
  1534. var loc = o.find('input.location');
  1535. var allFieldsValid = true;
  1536. if(fullname.val().length < 255) {
  1537. fullname.removeClass('invalid'); } else { fullname.addClass('invalid'); if(allFieldsValid)allFieldsValid=false; }
  1538. if($.trim(homepage.val()).length==0 || /^(ftp|http|https):\/\/[^ "]+$/.test(homepage.val())) {
  1539. homepage.removeClass('invalid'); } else { homepage.addClass('invalid'); if(allFieldsValid)allFieldsValid=false; }
  1540. if(bio.val().length < 140) {
  1541. bio.removeClass('invalid'); } else { bio.addClass('invalid'); if(allFieldsValid)allFieldsValid=false; }
  1542. if(loc.val().length < 255) {
  1543. loc.removeClass('invalid'); } else { loc.addClass('invalid'); if(allFieldsValid)allFieldsValid=false; }
  1544. return allFieldsValid;
  1545. }
  1546. /* ·
  1547. ·
  1548. · Validate a hex color and add # if missing
  1549. ·
  1550. · @returns hex color with # or false
  1551. ·
  1552. · · · · · · · · · */
  1553. function isValidHexColor(maybeValidHexColor) {
  1554. if(maybeValidHexColor.substring(0,1) != '#') {
  1555. maybeValidHexColor = '#' + maybeValidHexColor;
  1556. }
  1557. var validHexColor = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(maybeValidHexColor);
  1558. if(validHexColor) {
  1559. validHexColor = maybeValidHexColor;
  1560. }
  1561. return validHexColor;
  1562. }
  1563. /**
  1564. * Change profile design
  1565. * @param {Object} obj - user object that should contain one, two or all of backgroundimage, backgroundcolor and linkcolor false or empty string unsets the parameter to default
  1566. */
  1567. function changeDesign(obj) {
  1568. if (!window.currentStreamObject) {
  1569. window.currentStreamObject = {
  1570. name: '',
  1571. nickname: '',
  1572. };
  1573. }
  1574. if (!window.defaultBackgroundColor) {
  1575. window.defaultBackgroundColor = '000';
  1576. }
  1577. if (!window.defaultLinkColor) {
  1578. window.defaultLinkColor = '000';
  1579. }
  1580. // if we're logged out and this is the front page, we use the default design
  1581. if (!window.loggedIn &&
  1582. (window.currentStreamObject.name === 'public timeline' || window.currentStreamObject.name === 'public and external timeline')) {
  1583. obj.backgroundimage = window.fullUrlToThisQvitterApp + window.siteBackground;
  1584. obj.backgroundcolor = window.defaultBackgroundColor;
  1585. obj.linkcolor = window.defaultLinkColor;
  1586. }
  1587. // if no object is defined, abort
  1588. if (!obj) {
  1589. return;
  1590. }
  1591. // remember the design for this stream
  1592. if (typeof window.oldStreamsDesigns[window.currentStreamObject.nickname] === 'undefined') {
  1593. window.oldStreamsDesigns[window.currentStreamObject.nickname] = new Object();
  1594. }
  1595. // change design elements
  1596. if (obj.backgroundimage === false) {
  1597. obj.backgroundimage = '';
  1598. }
  1599. $('body').css('background-image', "url('" + obj.backgroundimage + "')");
  1600. window.oldStreamsDesigns[window.currentStreamObject.nickname].backgroundimage = obj.backgroundimage;
  1601. if (!obj.backgroundcolor) {
  1602. obj.backgroundcolor = window.defaultBackgroundColor;
  1603. }
  1604. changeBackgroundColor(obj.backgroundcolor);
  1605. window.oldStreamsDesigns[window.currentStreamObject.nickname].backgroundcolor = obj.backgroundcolor;
  1606. if (!obj.linkcolor) {
  1607. obj.linkcolor = window.defaultLinkColor;
  1608. }
  1609. changeLinkColor(obj.linkcolor);
  1610. window.oldStreamsDesigns[window.currentStreamObject.nickname].linkcolor = obj.linkcolor;
  1611. }
  1612. // create object to remember designs on page load
  1613. window.oldStreamsDesigns = new Object();
  1614. /* ·
  1615. ·
  1616. · Change background color
  1617. ·
  1618. · @param newLinkColor: hex value with or without #
  1619. ·
  1620. · · · · · · · · · */
  1621. function changeBackgroundColor(newBackgroundColor) {
  1622. // check hex value
  1623. var validHexColor = isValidHexColor(newBackgroundColor);
  1624. if(!validHexColor) {
  1625. console.log('invalid hex value for backgroundcolor: ' + newBackgroundColor);
  1626. return false;
  1627. }
  1628. $('body').css('background-color',validHexColor);
  1629. }
  1630. /* ·
  1631. ·
  1632. · Change link color
  1633. ·
  1634. · @param newLinkColor: hex value with or without #
  1635. ·
  1636. · · · · · · · · · */
  1637. function changeLinkColor(newLinkColor) {
  1638. // check hex value
  1639. var validHexColor = isValidHexColor(newLinkColor);
  1640. if(!validHexColor) {
  1641. console.log('invalid hex value for linkcolor: ' + newLinkColor);
  1642. return false;
  1643. }
  1644. var lighterColor08 = blendRGBColors(hex2rgb(validHexColor),'rgb(255,255,255)',0.8);
  1645. var lighterColor06 = blendRGBColors(hex2rgb(validHexColor),'rgb(255,255,255)',0.6)
  1646. var headStyle = $('#dynamic-styles').children('style');
  1647. var headStyleText = headStyle.text();
  1648. headStyleText = replaceFromStringEndToStringStart(headStyleText,'/*COLORSTART*/','/*COLOREND*/',validHexColor);
  1649. headStyleText = replaceFromStringEndToStringStart(headStyleText,'/*BACKGROUNDCOLORSTART*/','/*BACKGROUNDCOLOREND*/',validHexColor);
  1650. headStyleText = replaceFromStringEndToStringStart(headStyleText,'/*BORDERCOLORSTART*/','/*BORDERCOLOREND*/',validHexColor);
  1651. headStyleText = replaceFromStringEndToStringStart(headStyleText,'/*LIGHTERBACKGROUNDCOLORSTART*/','/*LIGHTERBACKGROUNDCOLOREND*/',lighterColor08);
  1652. headStyleText = replaceFromStringEndToStringStart(headStyleText,'/*LIGHTERBORDERCOLORSTART*/','/*LIGHTERBORDERCOLOREND*/',lighterColor06);
  1653. headStyleText = replaceFromStringEndToStringStart(headStyleText,'/*LIGHTERBORDERBOTTOMCOLORSTART*/','/*LIGHTERBORDERBOTTOMCOLOREND*/',lighterColor08);
  1654. headStyle.text(headStyleText);
  1655. }
  1656. function replaceFromStringEndToStringStart(string,fromStringEnd,toStringStart,withString) {
  1657. return string.substring(0,string.indexOf(fromStringEnd)+fromStringEnd.length) + withString + string.substring(string.indexOf(toStringStart));
  1658. }
  1659. function blendRGBColors(c0, c1, p) {
  1660. var f=c0.split(","),t=c1.split(","),R=parseInt(f[0].slice(4)),G=parseInt(f[1]),B=parseInt(f[2]);
  1661. return "rgb("+(Math.round((parseInt(t[0].slice(4))-R)*p)+R)+","+(Math.round((parseInt(t[1])-G)*p)+G)+","+(Math.round((parseInt(t[2])-B)*p)+B)+")";
  1662. }
  1663. function hex2rgb(hexStr){
  1664. // note: hexStr should be #rrggbb
  1665. var hex = parseInt(hexStr.substring(1), 16);
  1666. var r = (hex & 0xff0000) >> 16;
  1667. var g = (hex & 0x00ff00) >> 8;
  1668. var b = hex & 0x0000ff;
  1669. return 'rgb(' + r + ',' + g + ',' + b + ')';
  1670. }
  1671. /* ·
  1672. ·
  1673. · Right-to-left language detection <o
  1674. · (//
  1675. · @param s: the stream-item to detect rtl in
  1676. ·
  1677. · @return a stream-item that might have rtl-class added
  1678. ·
  1679. · · · · · · · · · */
  1680. function detectRTL(s) {
  1681. var $streamItem = $('<div>').append(s);
  1682. var $queetText = $('<div>').append($streamItem.find('.queet-text').html()); // create an jquery object
  1683. var $a = $queetText.find('a'); $a.remove(); // remove links
  1684. var $vcard = $queetText.find('.vcard'); $vcard.remove(); // remove users, groups
  1685. var $hcard = $queetText.find('.h-card'); $hcard.remove(); // remove users, groups
  1686. var $tag = $queetText.find('.tag'); $tag.remove(); // remove tags
  1687. if($queetText.find('.rtl').length>0) { $queetText.html($queetText.find('.rtl').html()); } // remove rtl container if there is one
  1688. // remove chars we're not interested in
  1689. $queetText.html($queetText.html().replace(/\@/gi,'').replace(/\#/gi,'').replace(/\!/gi,'').replace(/\(/gi,'').replace(/\)/gi,'').replace(/\:D/gi,'').replace(/D\:/gi,'').replace(/\:/gi,'').replace(/\-/gi,'').replace(/\s/gi, ''));
  1690. // count ltr and rtl chars
  1691. var ltrChars = 'A-Za-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02B8\u0300-\u0590\u0800-\u1FFF'+'\u2C00-\uFB1C\uFDFE-\uFE6F\uFEFD-\uFFFF',
  1692. rtlChars = '\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC',
  1693. rtlDirCheck = new RegExp('^[^'+ltrChars+']*['+rtlChars+']'),
  1694. RTLnum = 0,
  1695. LTRnum = 0,
  1696. RTLorLTR = $queetText.html();
  1697. for (var i = 0, len = RTLorLTR.length; i < len; i++) {
  1698. if(rtlDirCheck.test(RTLorLTR[i])) { RTLnum++; }
  1699. else { LTRnum++; }
  1700. }
  1701. // if there are more rtl chars than ltr
  1702. // or if no chars (that we are interested, but body is set to rtl)
  1703. if(RTLnum > LTRnum
  1704. || ($queetText.html().length==0 && $('body').hasClass('rtl'))) {
  1705. $streamItem.children('.stream-item').children('.queet').addClass('rtl');
  1706. }
  1707. else {
  1708. // for ltr languages we move @, ! and # to inside (only because it looks better)
  1709. prependCharIfItDoesntAlreadyExist($streamItem.find('.queet-text').find('.h-card.mention'),'@');
  1710. prependCharIfItDoesntAlreadyExist($streamItem.find('.queet-text').find('.h-card.group'),'!');
  1711. prependCharIfItDoesntAlreadyExist($streamItem.find('.queet-text').find('.vcard .fn.nickname:not(.group)'),'@'); // very old style
  1712. prependCharIfItDoesntAlreadyExist($streamItem.find('.queet-text').find('.vcard .nickname.mention:not(.fn)'),'@'); // old style
  1713. prependCharIfItDoesntAlreadyExist($streamItem.find('.queet-text').find('.vcard .nickname.group'),'!'); // old style
  1714. prependCharIfItDoesntAlreadyExist($streamItem.find('.queet-text').find('a[rel="tag"]'),'#');
  1715. }
  1716. // we remove @, ! and #, they are added as pseudo elements, or have been moved to the inside
  1717. return $streamItem.html().replace(/@<a/gi,'<a').replace(/!<a/gi,'<a').replace(/@<span class="vcard">/gi,'<span class="vcard">').replace(/!<span class="vcard">/gi,'<span class="vcard">').replace(/#<span class="tag">/gi,'<span class="tag">');
  1718. }
  1719. function prependCharIfItDoesntAlreadyExist(jQueryObject,char) {
  1720. if(jQueryObject.text().substring(0,1) != char) {
  1721. jQueryObject.prepend(char);
  1722. }
  1723. }
  1724. /* ·
  1725. ·
  1726. · Takes twitter style dates and converts them
  1727. ·
  1728. · @param tdate: date in the form of e.g. 'Mon Aug 05 16:30:22 +0200 2013'
  1729. ·
  1730. · @return user friendly dates ..M_
  1731. · W
  1732. · Needs global language object window.sL to be populated
  1733. ·
  1734. · · · · · · · · · · · · · */
  1735. function parseTwitterDate(tdate) {
  1736. var month_names = new Array ();
  1737. month_names[month_names.length] = window.sL.shortmonthsJanuary;
  1738. month_names[month_names.length] = window.sL.shortmonthsFebruary
  1739. month_names[month_names.length] = window.sL.shortmonthsMars
  1740. month_names[month_names.length] = window.sL.shortmonthsApril
  1741. month_names[month_names.length] = window.sL.shortmonthsMay
  1742. month_names[month_names.length] = window.sL.shortmonthsJune
  1743. month_names[month_names.length] = window.sL.shortmonthsJuly
  1744. month_names[month_names.length] = window.sL.shortmonthsAugust
  1745. month_names[month_names.length] = window.sL.shortmonthsSeptember
  1746. month_names[month_names.length] = window.sL.shortmonthsOctober
  1747. month_names[month_names.length] = window.sL.shortmonthsNovember
  1748. month_names[month_names.length] = window.sL.shortmonthsDecember
  1749. var system_date = parseDate(tdate);
  1750. var user_date = new Date();
  1751. var diff = Math.floor((user_date - system_date) / 1000);
  1752. if (diff <= 10) {return window.sL.now;}
  1753. if (diff < 60) {return window.sL.shortDateFormatSeconds.replace('{seconds}',Math.round(diff/10)*10);}
  1754. if (diff <= 3540) {return window.sL.shortDateFormatMinutes.replace('{minutes}',Math.round(diff / 60));}
  1755. if (diff <= 86400) {return window.sL.shortDateFormatHours.replace('{hours}',Math.round(diff / 3600));}
  1756. if (diff <= 31536000) {return window.sL.shortDateFormatDate.replace('{day}',system_date.getDate()).replace('{month}',month_names[system_date.getMonth()]);}
  1757. if (diff > 31536000) {return window.sL.shortDateFormatDateAndY.replace('{day}',system_date.getDate()).replace('{month}',month_names[system_date.getMonth()]).replace('{year}',system_date.getFullYear());}
  1758. return system_date;
  1759. }
  1760. function parseTwitterLongDate(tdate) {
  1761. var month_names = new Array ();
  1762. month_names[month_names.length] = window.sL.longmonthsJanuary;
  1763. month_names[month_names.length] = window.sL.longmonthsFebruary
  1764. month_names[month_names.length] = window.sL.longmonthsMars
  1765. month_names[month_names.length] = window.sL.longmonthsApril
  1766. month_names[month_names.length] = window.sL.longmonthsMay
  1767. month_names[month_names.length] = window.sL.longmonthsJune
  1768. month_names[month_names.length] = window.sL.longmonthsJuly
  1769. month_names[month_names.length] = window.sL.longmonthsAugust
  1770. month_names[month_names.length] = window.sL.longmonthsSeptember
  1771. month_names[month_names.length] = window.sL.longmonthsOctober
  1772. month_names[month_names.length] = window.sL.longmonthsNovember
  1773. month_names[month_names.length] = window.sL.longmonthsDecember
  1774. var system_date = parseDate(tdate);
  1775. var hours = system_date.getHours();
  1776. var minutes = ('0'+system_date.getMinutes()).slice(-2);
  1777. var ampm = hours >= 12 ? 'pm' : 'am';
  1778. var time24hours = hours + ':' + minutes;
  1779. var time12hours = hours % 12;
  1780. time12hours = time12hours ? time12hours : 12; // the hour '0' should be '12'
  1781. if(ampm == 'am') { time12hours = window.sL.time12am.replace('{time}',time12hours + ':' + minutes);}
  1782. else { time12hours = window.sL.time12pm.replace('{time}',time12hours + ':' + minutes); }
  1783. return window.sL.longDateFormat.replace('{time24}',time24hours).replace('{hours}',hours).replace('{minutes}',minutes).replace('{time12}',time12hours).replace('{day}',system_date.getDate()).replace('{month}',month_names[system_date.getMonth()]).replace('{year}',system_date.getFullYear());
  1784. }
  1785. function timestampToTwitterDate(timestamp) {
  1786. var a = new Date(timestamp*1000);
  1787. var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
  1788. var days = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
  1789. var day = days[a.getUTCDay()];
  1790. var year = a.getUTCFullYear();
  1791. var month = months[a.getUTCMonth()];
  1792. var date = (a.getUTCDate()<10?'0':'')+a.getUTCDate();
  1793. var hour = (a.getUTCHours()<10?'0':'')+a.getUTCHours();
  1794. var min = (a.getUTCMinutes()<10?'0':'')+a.getUTCMinutes();
  1795. var sec = (a.getUTCSeconds()<10?'0':'')+a.getUTCSeconds();
  1796. return day+' '+month+' '+date+' '+hour+':'+min+':'+sec+' +0000 '+year;
  1797. }
  1798. function parseDate(str) {
  1799. if(typeof str != 'undefined') {
  1800. var v=str.split(' ');
  1801. return new Date(Date.parse(v[1]+" "+v[2]+", "+v[5]+" "+v[3]+" "+v[4]));
  1802. }
  1803. }
  1804. /* ·
  1805. ·
  1806. · If we want to make sure we have empty arrays, not empty objects
  1807. ·
  1808. · · · · · · · · · · */
  1809. function convertEmptyObjectToEmptyArray(data) {
  1810. // empty object? return empty array instead...
  1811. if($.isEmptyObject(data)) {
  1812. return [];
  1813. }
  1814. // leave data unchanged if we don't recognize it
  1815. else {
  1816. return data;
  1817. }
  1818. }
  1819. /* ·
  1820. ·
  1821. · Functions to show and remove the spinner
  1822. ·
  1823. · · · · · · · · · · · · */
  1824. function display_spinner(parent) {
  1825. if($('.loader').length<1) {
  1826. if(typeof parent == 'undefined') {
  1827. $('.global-nav').removeClass('show-logo');
  1828. var parent = 'body';
  1829. }
  1830. $(parent).prepend('\
  1831. <div class="loader">\
  1832. <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"\
  1833. width="40px" height="40px" viewBox="0 0 40 40" enable-background="new 0 0 40 40" xml:space="preserve">\
  1834. <path opacity="0.2" enable-background="new " d="M20.201,8.503c-6.413,0-11.612,5.199-11.612,11.612s5.199,11.611,11.612,11.611\
  1835. c6.412,0,11.611-5.198,11.611-11.611S26.613,8.503,20.201,8.503z M20.201,29.153c-4.992,0-9.039-4.046-9.039-9.038\
  1836. s4.047-9.039,9.039-9.039c4.991,0,9.038,4.047,9.038,9.039S25.192,29.153,20.201,29.153z"/>\
  1837. <path d="M24.717,12.293l1.285-2.227c-1.708-0.988-3.686-1.563-5.801-1.563l0,0v2.573l0,0C21.848,11.076,23.386,11.524,24.717,12.293 z">\
  1838. <animateTransform attributeType="xml"\
  1839. attributeName="transform"\
  1840. type="rotate"\
  1841. from="0 20 20"\
  1842. to="360 20 20"\
  1843. dur="1s"\
  1844. repeatCount="indefinite"/>\
  1845. </path>\
  1846. </svg>\
  1847. </div>\
  1848. ');
  1849. }
  1850. }
  1851. function remove_spinner() {
  1852. $('.loader').remove();
  1853. $('.global-nav').addClass('show-logo');
  1854. }
  1855. /* ·
  1856. ·
  1857. · Converts ...-attachment-links to spans
  1858. ·
  1859. · (Attachments are loaded when queets expand)
  1860. ·
  1861. · · · · · · · · · · · · · · · · · */
  1862. function convertAttachmentMoreHref() {
  1863. $('a.attachment.more').each(function() {
  1864. if(typeof $(this).attr('href') != 'undefined') {
  1865. var attachment_href = $(this).attr('href');
  1866. var attachment_id = attachment_href.substr((~-attachment_href.lastIndexOf("/") >>> 0) + 2);
  1867. if(attachment_id.length>0) {
  1868. $(this).replaceWith($('<span class="attachment more" data-attachment-id="' + attachment_id + '">…</span>'));
  1869. }
  1870. }
  1871. });
  1872. }
  1873. /* ·
  1874. ·
  1875. · Saves the user's bookmarks to the server
  1876. ·
  1877. · · · · · · · · · · · · · */
  1878. function saveAllBookmarks() {
  1879. var i=0;
  1880. var bookmarkContainer = new Object();
  1881. $.each($('#bookmark-container .stream-selection'), function(key,obj) {
  1882. bookmarkContainer[i] = new Object();
  1883. bookmarkContainer[i].dataStreamHref = $(obj).attr('href');
  1884. bookmarkContainer[i].dataStreamHeader = $(obj).text();
  1885. i++;
  1886. });
  1887. postUpdateBookmarks(bookmarkContainer);
  1888. $('#bookmark-container').sortable({delay: 100});
  1889. $('#bookmark-container').disableSelection();
  1890. }
  1891. /* ·
  1892. ·
  1893. · Append all bookmarks to the bookmark container
  1894. ·
  1895. · · · · · · · · · · · · · */
  1896. function appendAllBookmarks(bookmarkContainer) {
  1897. if(typeof bookmarkContainer != 'undefined' && bookmarkContainer) {
  1898. $('#bookmark-container').html('');
  1899. var bookmarkContainerParsed = JSON.parse(bookmarkContainer);
  1900. $.each(bookmarkContainerParsed, function(key,obj) {
  1901. $('#bookmark-container').append('<a class="stream-selection" href="' + obj.dataStreamHref + '">' + obj.dataStreamHeader + '<i class="chev-right" data-tooltip="' + window.sL.tooltipRemoveBookmark + '"></i></a>');
  1902. });
  1903. }
  1904. $('#bookmark-container').sortable({delay: 100});
  1905. $('#bookmark-container').disableSelection();
  1906. }
  1907. /* ·
  1908. ·
  1909. · Change the header of an item in the history container by href attribute
  1910. ·
  1911. · · · · · · · · · · · · · */
  1912. function updateHistoryContainerItemByHref(href, streamHeader) {
  1913. $('#history-container .stream-selection[href="' + href + '"]').html(streamHeader + '<i class="chev-right" data-tooltip="' + window.sL.tooltipBookmarkStream + '"></i>');
  1914. updateHistoryLocalStorage();
  1915. }
  1916. /* ·
  1917. ·
  1918. · Remove items in the history container by href attribute
  1919. ·
  1920. · · · · · · · · · · · · · */
  1921. function removeHistoryContainerItemByHref(href) {
  1922. $('#history-container .stream-selection[href="' + href + '"]').remove();
  1923. updateHistoryLocalStorage();
  1924. }
  1925. /* ·
  1926. ·
  1927. · Updates the browsing history local storage
  1928. ·
  1929. · · · · · · · · · · · · · */
  1930. function updateHistoryLocalStorage() {
  1931. if(localStorageIsEnabled()) {
  1932. var i=0;
  1933. var historyContainer = new Object();
  1934. $.each($('#history-container .stream-selection'), function(key,obj) {
  1935. historyContainer[i] = new Object();
  1936. historyContainer[i].dataStreamHref = $(obj).attr('href');
  1937. historyContainer[i].dataStreamHeader = $(obj).text();
  1938. i++;
  1939. });
  1940. localStorageObjectCache_STORE('browsingHistory', window.loggedIn.screen_name,historyContainer);
  1941. if($('#history-container .stream-selection').length==0) {
  1942. $('#history-container').css('display','none');
  1943. }
  1944. else {
  1945. $('#history-container').css('display','block');
  1946. }
  1947. }
  1948. }
  1949. /* ·
  1950. ·
  1951. · Loads history from local storage to menu
  1952. ·
  1953. · · · · · · · · · · · · · */
  1954. function loadHistoryFromLocalStorage() {
  1955. if(localStorageIsEnabled()) {
  1956. var cacheData = localStorageObjectCache_GET('browsingHistory', window.loggedIn.screen_name);
  1957. if(cacheData) {
  1958. $('#history-container').css('display','block');
  1959. $('#history-container').html('');
  1960. $.each(cacheData, function(key,obj) {
  1961. var streamHeader = replaceHtmlSpecialChars(obj.dataStreamHeader); // because we're pulling the header with jQuery.text() before saving in localstorage, which unescapes our escaped html
  1962. $('#history-container').append('<a class="stream-selection" href="' + obj.dataStreamHref + '">' + streamHeader + '<i class="chev-right" data-tooltip="' + window.sL.tooltipBookmarkStream + '"></i></a>');
  1963. });
  1964. }
  1965. updateHistoryLocalStorage();
  1966. }
  1967. }
  1968. /* ·
  1969. ·
  1970. · Does stream need a ? or a &
  1971. ·
  1972. · · · · · · · · · · · · · */
  1973. function qOrAmp(stream) {
  1974. if(stream.substr(-5) == '.json') {
  1975. return '?';
  1976. }
  1977. else {
  1978. return '&';
  1979. }
  1980. }
  1981. /* ·
  1982. ·
  1983. · Count chars in queet box
  1984. ·
  1985. · @param src: the queetbox's value
  1986. · @param trgt: the counter
  1987. · @param btn: the button
  1988. ·
  1989. · · · · · · · · · · · · · */
  1990. function countCharsInQueetBox(src,trgt,btn) {
  1991. // count linebreaks by converting them to spaces
  1992. var $src_txt = $('<div/>').append(src.html().replace(/<br>/g,' '));
  1993. $src_txt = $('<div/>').append($.trim($src_txt.text().replace(/\n/g,'').replace(/^\s+|\s+$/g, '')));
  1994. var numchars = ($src_txt.text()).length;
  1995. // check for long urls and disable/enable url shorten button if present
  1996. var longurls = 0;
  1997. $.each(src.siblings('.syntax-middle').find('span.url'),function(key,obj){
  1998. if($.trim($(obj).html().replace(/&nbsp;/gi,'').replace(/<br>/gi,'')).length > 20) {
  1999. longurls++;
  2000. }
  2001. });
  2002. if(longurls>0) src.siblings('.queet-toolbar').find('button.shorten').removeClass('disabled');
  2003. else src.siblings('.queet-toolbar').find('button.shorten').addClass('disabled');
  2004. // limited
  2005. if(window.textLimit > 0) {
  2006. trgt.html(window.textLimit - numchars);
  2007. // activate/deactivare button
  2008. if(numchars > 0 && numchars < window.textLimit+1) {
  2009. btn.removeClass('disabled');
  2010. btn.addClass('enabled');
  2011. btn.removeClass('too-long');
  2012. // deactivate button if it's equal to the start text
  2013. var queetBox = btn.closest('.inline-reply-queetbox').children('.queet-box-syntax');
  2014. if(typeof queetBox.attr('data-replies-text') != 'undefined') {
  2015. var $startText = $('<div/>').append(decodeURIComponent(queetBox.attr('data-replies-text')));
  2016. if($.trim($startText.text()) == $.trim($src_txt.text())) {
  2017. btn.removeClass('enabled');
  2018. btn.addClass('disabled');
  2019. }
  2020. }
  2021. }
  2022. else if(numchars > window.textLimit){
  2023. btn.removeClass('enabled');
  2024. btn.addClass('disabled');
  2025. btn.addClass('too-long');
  2026. }
  2027. else {
  2028. btn.removeClass('enabled');
  2029. btn.addClass('disabled');
  2030. btn.removeClass('too-long');
  2031. }
  2032. // counter color
  2033. if((window.textLimit-numchars) < 0) {
  2034. trgt.css('color','#D40D12');
  2035. }
  2036. else {
  2037. trgt.removeAttr('style');
  2038. }
  2039. }
  2040. // unlimited
  2041. else {
  2042. if(numchars > 0) {
  2043. btn.removeClass('disabled');
  2044. btn.addClass('enabled');
  2045. }
  2046. else {
  2047. btn.removeClass('enabled');
  2048. btn.addClass('disabled');
  2049. }
  2050. }
  2051. }
  2052. /* ·
  2053. ·
  2054. · Prefill the queet box with cached text, if there is any in an attribute
  2055. ·
  2056. · @param queetBox: jQuery object for the queet box
  2057. ·
  2058. · · · · · · · · · · · · · */
  2059. function maybePrefillQueetBoxWithCachedText(queetBox) {
  2060. var cachedText = decodeURIComponent(queetBox.attr('data-cached-text'));
  2061. var cachedTextText = $('<div/>').html(cachedText).text();
  2062. if(cachedText != 'undefined' && cachedText != 'false') {
  2063. queetBox.click();
  2064. queetBox.html(cachedText);
  2065. setSelectionRange(queetBox[0], cachedTextText.length, cachedTextText.length);
  2066. queetBox.trigger('input');
  2067. }
  2068. }
  2069. /* ·
  2070. ·
  2071. · Remember my scroll position
  2072. ·
  2073. · @param obj: jQuery object which position we want to remember
  2074. · @param id: id for position to remember
  2075. · @param offset: we might want to offset our remembered scroll, e.g. when stream-item gets margin after expand
  2076. ·
  2077. · · · · · · · · · · · · · */
  2078. function rememberMyScrollPos(obj,id,offset) {
  2079. if(typeof offset == 'undefined') {
  2080. var offset = 0;
  2081. }
  2082. if(typeof window.scrollpositions == 'undefined') { window.scrollpositions = new Object();}
  2083. window.scrollpositions[id] = obj.offset().top - $(window).scrollTop() - offset;
  2084. }
  2085. /* ·
  2086. ·
  2087. · Go back to my scroll po
  2088. ·
  2089. · @param obj: jQuery object to put in the remebered position
  2090. · @param id: id for remembered position
  2091. · @param animate: if we want to animate the scroll
  2092. · @param callback: function to run when animation stops
  2093. ·
  2094. · · · · · · · · · · · · · */
  2095. function backToMyScrollPos(obj,id,animate,callback) {
  2096. var pos = obj.offset().top-window.scrollpositions[id];
  2097. if(animate) {
  2098. if(animate == 'animate' || animate === true) {
  2099. animate = 1000;
  2100. }
  2101. if(typeof callback !== 'undefined'){
  2102. $('html, body').animate({ scrollTop: pos}, animate, 'swing',function(){
  2103. callback();
  2104. });
  2105. }
  2106. else {
  2107. $('html, body').animate({ scrollTop: pos }, animate, 'swing');
  2108. }
  2109. }
  2110. else {
  2111. $('html, body').scrollTop(pos);
  2112. }
  2113. }
  2114. /* ·
  2115. ·
  2116. · Scroll to a stream item
  2117. ·
  2118. · @param streamItem: jQuery object to scroll to
  2119. ·
  2120. · · · · · · · · · · · · · */
  2121. function scrollToQueet(streamItem) {
  2122. var streamItemPos = streamItem.offset().top;
  2123. var windowHeight = $(window).height();
  2124. var streamItemHeight = streamItem.outerHeight();
  2125. // console.log(streamItemHeight);
  2126. // console.log(windowHeight);
  2127. var newScrollPos = Math.round(streamItemPos - windowHeight/2 + streamItemHeight/2);
  2128. $('html, body').scrollTop(newScrollPos);
  2129. }
  2130. /* ·
  2131. ·
  2132. · Clean up user object, remove null etc
  2133. ·
  2134. · · · · · · · · · · · · · */
  2135. function cleanUpUserObject(data) {
  2136. data.name = data.name || '';
  2137. data.profile_image_url = data.profile_image_url || '';
  2138. data.profile_image_url_profile_size = data.profile_image_url_profile_size || '';
  2139. data.profile_image_url_original = data.profile_image_url_original || '';
  2140. data.screen_name = data.screen_name || '';
  2141. data.description = data.description || '';
  2142. data.location = data.location || '';
  2143. data.url = data.url || '';
  2144. data.statusnet_profile_url = data.statusnet_profile_url || '';
  2145. data.statuses_count = data.statuses_count || 0;
  2146. data.followers_count = data.followers_count || 0;
  2147. data.groups_count = data.groups_count || 0;
  2148. data.friends_count = data.friends_count || 0;
  2149. return data;
  2150. }
  2151. /* ·
  2152. ·
  2153. · outerHTML
  2154. ·
  2155. · · · · · · · · · · · · · */
  2156. jQuery.fn.outerHTML = function(s) {
  2157. return s
  2158. ? this.before(s).remove()
  2159. : jQuery("<p>").append(this.eq(0).clone()).html();
  2160. };
  2161. /* ·
  2162. ·
  2163. · Sort divs by attribute descending
  2164. ·
  2165. · · · · · · · · · · · · · */
  2166. jQuery.fn.sortDivsByAttrDesc = function sortDivsByAttrDesc(attr) {
  2167. $("> div", this[0]).sort(dec_sort).appendTo(this[0]);
  2168. function dec_sort(a, b){ return parseInt($(b).attr(attr),10) > parseInt($(a).attr(attr),10) ? 1 : -1; }
  2169. }
  2170. /* ·
  2171. ·
  2172. · Stuff to get and set selection/caret in contenteditables
  2173. ·
  2174. · · · · · · · · · · · · · */
  2175. function getSelectionInElement(element) {
  2176. var caretOffset = Array(0,0);
  2177. var doc = element.ownerDocument || element.document;
  2178. var win = doc.defaultView || doc.parentWindow;
  2179. var sel;
  2180. var range = win.getSelection().getRangeAt(0);
  2181. var preCaretRangeEnd = range.cloneRange();
  2182. preCaretRangeEnd.selectNodeContents(element);
  2183. preCaretRangeEnd.setEnd(range.endContainer, range.endOffset);
  2184. caretOffset[1] = preCaretRangeEnd.toString().length;
  2185. var preCaretRangeStart = range.cloneRange();
  2186. preCaretRangeStart.selectNodeContents(element);
  2187. preCaretRangeStart.setEnd(range.startContainer, range.startOffset);
  2188. caretOffset[0] = preCaretRangeStart.toString().length;
  2189. return caretOffset;
  2190. }
  2191. function getTextNodesIn(node) {
  2192. var textNodes = [];
  2193. if (node.nodeType == 3) {
  2194. textNodes.push(node);
  2195. }
  2196. else {
  2197. var children = node.childNodes;
  2198. for (var i = 0, len = children.length; i < len; ++i) {
  2199. textNodes.push.apply(textNodes, getTextNodesIn(children[i]));
  2200. }
  2201. }
  2202. return textNodes;
  2203. }
  2204. function setSelectionRange(el, start, end) {
  2205. if (document.createRange && window.getSelection) {
  2206. var range = document.createRange();
  2207. range.selectNodeContents(el);
  2208. var textNodes = getTextNodesIn(el);
  2209. var foundStart = false;
  2210. var charCount = 0, endCharCount;
  2211. for (var i = 0, textNode; textNode = textNodes[i++]; ) {
  2212. endCharCount = charCount + textNode.length;
  2213. if(endCharCount == start && endCharCount == end) {
  2214. endCharCount = endCharCount+1;
  2215. }
  2216. if (!foundStart && start >= charCount
  2217. && (start < endCharCount ||
  2218. (start == endCharCount && i < textNodes.length))) {
  2219. range.setStart(textNode, start - charCount);
  2220. foundStart = true;
  2221. }
  2222. if (foundStart && end <= endCharCount) {
  2223. range.setEnd(textNode, end - charCount);
  2224. break;
  2225. }
  2226. charCount = endCharCount;
  2227. }
  2228. var sel = window.getSelection();
  2229. sel.removeAllRanges();
  2230. sel.addRange(range);
  2231. } else if (document.selection && document.body.createTextRange) {
  2232. var textRange = document.body.createTextRange();
  2233. textRange.moveToElementText(el);
  2234. textRange.collapse(true);
  2235. textRange.moveEnd("character", end);
  2236. textRange.moveStart("character", start);
  2237. textRange.select();
  2238. }
  2239. }
  2240. function createRangeFromCharacterIndices(containerEl, start, end) {
  2241. var charIndex = 0, range = document.createRange(), foundStart = false, stop = {};
  2242. range.setStart(containerEl, 0);
  2243. range.collapse(true);
  2244. function traverseTextNodes(node) {
  2245. if (node.nodeType == 3) {
  2246. var nextCharIndex = charIndex + node.length;
  2247. if (!foundStart && start >= charIndex && start <= nextCharIndex) {
  2248. range.setStart(node, start - charIndex);
  2249. foundStart = true;
  2250. }
  2251. if (foundStart && end >= charIndex && end <= nextCharIndex) {
  2252. range.setEnd(node, end - charIndex);
  2253. throw stop;
  2254. }
  2255. charIndex = nextCharIndex;
  2256. } else {
  2257. for (var i = 0, len = node.childNodes.length; i < len; ++i) {
  2258. traverseTextNodes(node.childNodes[i]);
  2259. }
  2260. }
  2261. }
  2262. try {
  2263. traverseTextNodes(containerEl);
  2264. } catch (ex) {
  2265. if (ex == stop) {
  2266. return range;
  2267. } else {
  2268. throw ex;
  2269. }
  2270. }
  2271. }
  2272. function deleteBetweenCharacterIndices(el, from, to) {
  2273. var range = createRangeFromCharacterIndices(el, from, to);
  2274. if(typeof range != 'undefined') {
  2275. range.deleteContents();
  2276. }
  2277. }
  2278. /* ·
  2279. ·
  2280. · Shorten urls in a queet-box
  2281. ·
  2282. · · · · · · · · · · · · · */
  2283. function shortenUrlsInBox(shortenButton) {
  2284. shortenButton.addClass('disabled');
  2285. $.each(shortenButton.parent().parent().siblings('.syntax-middle').find('span.url'),function(key,obj){
  2286. var url = $.trim($(obj).text());
  2287. display_spinner();
  2288. $.ajax({ url: window.urlShortenerAPIURL + '?format=' + window.urlshortenerFormat + '&action=shorturl&signature=' + window.urlShortenerSignature + '&url=' + encodeURIComponent(url), type: "GET", dataType: window.urlshortenerFormat,
  2289. success: function(data) {
  2290. if(typeof data.shorturl != 'undefined') {
  2291. shortenButton.closest('.queet-toolbar').siblings('.upload-image-container').children('img[data-shorturl="' + data.url.url + '"]').attr('data-shorturl',data.shorturl);
  2292. shortenButton.parent().parent().siblings('.queet-box-syntax').html(shortenButton.parent().parent().siblings('.queet-box-syntax').html().replace($('<div/>').text(data.url.url).html(), data.shorturl));
  2293. shortenButton.parent().parent().siblings('.queet-box-syntax').trigger('keyup');
  2294. shortenButton.addClass('disabled'); // make sure the button is disabled right after
  2295. }
  2296. remove_spinner();
  2297. },
  2298. error: function(data) {
  2299. console.log(data);
  2300. remove_spinner();
  2301. }
  2302. });
  2303. });
  2304. }
  2305. /* ·
  2306. ·
  2307. · Youtube ID from Youtube URL
  2308. ·
  2309. · · · · · · · · · · · · · */
  2310. function youTubeIDFromYouTubeURL(url) {
  2311. return url.replace('https://youtube.com/watch?v=','').replace('http://youtube.com/watch?v=','').replace('http://www.youtube.com/watch?v=','').replace('https://www.youtube.com/watch?v=','').replace('http://youtu.be/','').replace('https://youtu.be/','').substr(0,11);
  2312. }
  2313. /* ·
  2314. ·
  2315. · Youtube embed link from youtube url
  2316. ·
  2317. · · · · · · · · · · · · · */
  2318. function youTubeEmbedLinkFromURL(url, autoplay) {
  2319. // get start time hash
  2320. var l = document.createElement("a");
  2321. l.href = url;
  2322. var addStart = '';
  2323. if(l.hash.substring(0,3) == '#t=') {
  2324. addStart = '&start=' + l.hash.substring(3);
  2325. }
  2326. var addAutoplay = '';
  2327. if(typeof autoplay != 'undefined' && autoplay === true) {
  2328. addAutoplay = '&autoplay=1';
  2329. }
  2330. return '//www.youtube.com/embed/' + youTubeIDFromYouTubeURL(url) + '?enablejsapi=1&version=3&playerapiid=ytplayer' + addStart + addAutoplay;
  2331. }
  2332. /* ·
  2333. ·
  2334. · Vimeo ID from Vimeo URL
  2335. ·
  2336. · · · · · · · · · · · · · */
  2337. function vimeoIDFromVimeoURL(url) {
  2338. id = url.replace('http://vimeo.com/','').replace('https://vimeo.com/','');
  2339. if(id.indexOf('#') > -1) {
  2340. id = id.substring(0,id.indexOf('#'));
  2341. }
  2342. return id;
  2343. }
  2344. /* ·
  2345. ·
  2346. · Vimeo embed link from vimeo url
  2347. ·
  2348. · · · · · · · · · · · · · */
  2349. function vimeoEmbedLinkFromURL(url, autoplay) {
  2350. // get start time hash
  2351. var l = document.createElement("a");
  2352. l.href = url;
  2353. var addStart = '';
  2354. if(l.hash.substring(0,3) == '#t=') {
  2355. addStart = l.hash;
  2356. }
  2357. var addAutoplay = '&autoplay=0';
  2358. if(typeof autoplay != 'undefined' && autoplay === true) {
  2359. addAutoplay = '&autoplay=1';
  2360. }
  2361. return 'https://player.vimeo.com/video/' + vimeoIDFromVimeoURL(url) + '?api=1' + addAutoplay + addStart;
  2362. }
  2363. /* ·
  2364. ·
  2365. · CSS class name from URL
  2366. ·
  2367. · · · · · · · · · · · · · */
  2368. function CSSclassNameByHostFromURL(url) {
  2369. var host = getHost(url);
  2370. if(host.indexOf('www.') === 0) {
  2371. host = host.substring(4);
  2372. }
  2373. host = host.toLowerCase().replace(/\./g, "-");
  2374. host = host.replace(/[^a-zA-Z0-9-]+/g, "_");
  2375. if(host == 'youtu-be') {
  2376. host = 'youtube-com';
  2377. }
  2378. return 'host-' + host;
  2379. }
  2380. /* ·
  2381. ·
  2382. · String similarity
  2383. ·
  2384. · @params string1, string2:
  2385. · @returns (int) percent similarity
  2386. ·
  2387. · · · · · · · · · · · · · */
  2388. function stringSimilarity(string1, string2) {
  2389. if(typeof string1 != 'string' || typeof string2 != 'string') {
  2390. return 0;
  2391. }
  2392. // trim and strip html tags
  2393. string1 = $('<div/>').html($.trim(string1)).text();
  2394. string2 = $('<div/>').html($.trim(string2)).text();
  2395. var longestStringLength = string1.length;
  2396. if(string2.length>string1.length) {
  2397. longestStringLength = string2.length;
  2398. }
  2399. var distanceArray = levenshteinenator(string1, string2);
  2400. var distance = distanceArray[distanceArray.length-1][distanceArray[distanceArray.length-1].length-1];
  2401. var percentSimilarity = 100-Math.round(distance/longestStringLength*100);
  2402. return percentSimilarity;
  2403. }
  2404. // from http://andrew.hedges.name/experiments/levenshtein/
  2405. var levenshteinenator = (function () {
  2406. /**
  2407. * @param String a
  2408. * @param String b
  2409. * @return Array
  2410. */
  2411. function levenshteinenator(a, b) {
  2412. var cost;
  2413. var m = a.length;
  2414. var n = b.length;
  2415. // make sure a.length >= b.length to use O(min(n,m)) space, whatever that is
  2416. if (m < n) {
  2417. var c = a; a = b; b = c;
  2418. var o = m; m = n; n = o;
  2419. }
  2420. var r = []; r[0] = [];
  2421. for (var c = 0; c < n + 1; ++c) {
  2422. r[0][c] = c;
  2423. }
  2424. for (var i = 1; i < m + 1; ++i) {
  2425. r[i] = []; r[i][0] = i;
  2426. for ( var j = 1; j < n + 1; ++j ) {
  2427. cost = a.charAt( i - 1 ) === b.charAt( j - 1 ) ? 0 : 1;
  2428. r[i][j] = minimator( r[i-1][j] + 1, r[i][j-1] + 1, r[i-1][j-1] + cost );
  2429. }
  2430. }
  2431. return r;
  2432. }
  2433. /**
  2434. * Return the smallest of the three numbers passed in
  2435. * @param Number x
  2436. * @param Number y
  2437. * @param Number z
  2438. * @return Number
  2439. */
  2440. function minimator(x, y, z) {
  2441. if (x <= y && x <= z) return x;
  2442. if (y <= x && y <= z) return y;
  2443. return z;
  2444. }
  2445. return levenshteinenator;
  2446. }());
  2447. function mock() {
  2448. changeDesign();
  2449. }