/*· · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · Q V I T T E R · · · · · · \\\\_\ · · \\) \____) · · · · · · @licstart The following is the entire license notice for the · · JavaScript code in this page. · · · · Copyright (C) 2015 Hannes Mannerheim and other contributors · · · · · · This program is free software: you can redistribute it and/or modify · · it under the terms of the GNU Affero General Public License as · · published by the Free Software Foundation, either version 3 of the · · License, or (at your option) any later version. · · · · This program is distributed in the hope that it will be useful, · · but WITHOUT ANY WARRANTY; without even the implied warranty of · · MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the · · GNU Affero General Public License for more details. · · · · You should have received a copy of the GNU Affero General Public License · · along with this program. If not, see . · · · · @licend The above is the entire license notice · · for the JavaScript code in this page. · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · */ /* · · · Trigger click on elements · · @param inputFile: jQuery element to trigger click on · · · · · · · · · · */ function triggerClickOnInputFile(inputFile) { if(typeof bowser != 'undefined') { var bowserIntVersion = parseInt(bowser.version,10); if(typeof bowser.chrome != 'undefined' && bowser.chrome === true && bowserIntVersion < 53 || typeof bowser.opera != 'undefined' && bowser.opera === true && bowserIntVersion < 39 || typeof bowser.safari != 'undefined' && bowser.safari === true && bowserIntVersion < 9) { var evt = document.createEvent("HTMLEvents"); evt.initEvent("click", true, true); inputFile[0].dispatchEvent(evt); console.log('triggering click on on input file element with the old trigger hack for older browsers...'); } else { inputFile.trigger('click'); console.log('regular click triggered on input file element'); } } else { inputFile.trigger('click'); console.log('no bowser object found: regular click triggered on input file element'); } console.log(bowser); } /* · · · Store in localStorage object cache · · @param name: the name of this type of object · @param unique_id: some unique_id – the key in localStorage will be name-unique_id · @param object: the object to store · · · · · · · · · · */ function localStorageObjectCache_STORE(name, unique_id, object) { if(localStorageIsEnabled() === false) { return false; } name = localStorageMaybeAppendIdToKey(name); if(object === false || object === null || object.length < 1) { // false or an empty object means we remove this entry if(typeof localStorage[name + '-' + unique_id] != 'undefined' && localStorage[name + '-' + unique_id] !== null) { delete localStorage[name + '-' + unique_id]; } return false; } var dataToSave = {}; dataToSave.modified = Date.now(); dataToSave.cdata = LZString.compressToUTF16(JSON.stringify(object)); try { localStorage.setItem(name + '-' + unique_id, JSON.stringify(dataToSave)); } catch (e) { if (e.name == 'QUOTA_EXCEEDED_ERR' || e.name == 'NS_ERROR_DOM_QUOTA_REACHED' || e.name == 'QuotaExceededError' || e.name == 'W3CException_DOM_QUOTA_EXCEEDED_ERR') { removeOldestLocalStorageEntries(function(){ localStorageObjectCache_STORE(name, unique_id, object); }); } else { console.log('could not store in localStorage, unknown error'); } } } /* · · · Remove the 100 oldest cached items · · · · · · · · · · */ function removeOldestLocalStorageEntries(callback) { // grab the expiry and store the modified-key into an object var modified = Object.keys(localStorage).reduce(function(collection,key){ var currentModified = JSON.parse(localStorage.getItem(key)).modified; collection[currentModified] = key; return collection; },{}); delete modified['undefined']; // we don't want those // get the modified dates into an array var modifiedDates = Object.keys(modified); modifiedDates.sort(); var i = 0; $.each(modifiedDates,function(k,v){ delete localStorage[modified[v]]; i++; if(i>=100) { return false; } }); console.log('removed ' + i + ' old localstorage items'); if(i>0) { callback(); return true; } else { return false; } } /* · · · Get from localStorage object cache · · @param name: the name of this type of object · @param unique_id: some unique_id – the key in localStorage will be name-unique_id · · · · · · · · · · */ function localStorageObjectCache_GET(name, unique_id) { if(localStorageIsEnabled() === false) { return false; } name = localStorageMaybeAppendIdToKey(name); if(typeof localStorage[name + '-' + unique_id] == 'undefined' || localStorage[name + '-' + unique_id] === null) { return false; } try { var parsedObject = JSON.parse(localStorage[name + '-' + unique_id]); } catch(e) { return false; } if(typeof parsedObject.modified == 'undefined' || parsedObject.modified === null) { // invalid or old localstorage object found, check the whole localstorage! checkLocalStorage(); return false; } else { try { var decompressedAndParsed = JSON.parse(LZString.decompressFromUTF16(parsedObject.cdata)); return decompressedAndParsed; } catch(e) { return false; } } } // to the following data types we add the logged in user's user id, // since they contain user specific data (0 for logged out) // selectedLanguage is handled differently, since we want to be able to // access the logged out user's data if we're logged in function localStorageMaybeAppendIdToKey(name) { if(jQuery.inArray(name, ['browsingHistory', 'conversation', 'queetBoxInput', 'streamState']) !== -1) { if(window.loggedIn) { return name + '-' + window.loggedIn.id; } else { return name + '-0' ; } } else { return name; } } function checkLocalStorage() { if(localStorageIsEnabled() === false) { console.log('localstorage disabled'); return false; } console.log('checking localStorage for invalid entries'); var dateNow = Date.now() var corrected = 0; var deleted = 0; var compressed = 0; $.each(localStorage, function(k,entry){ if(typeof entry == 'string') { // check that entry is valid json try { var entryParsed = JSON.parse(entry); } catch(e) { delete localStorage[k]; deleted++; return true; } // check that it is a valid/currently used data type var validDataTypes = [ 'browsingHistory', 'conversation', 'favsAndRequeets', 'languageData', 'fullQueetHtml', 'selectedLanguage', 'queetBoxInput', 'streamState', 'languageErrorMessageDiscarded' ]; var thisDataType = k.substring(0,k.indexOf('-')); if($.inArray(thisDataType, validDataTypes) == -1 || k.indexOf('-') == -1) { delete localStorage[k]; deleted++; return true; } // check that it has a modified entry, if not: add one if(typeof entryParsed.modified == 'undefined' || entryParsed.modified === null) { var newEntry = {}; newEntry.modified = dateNow - corrected; // we want as unique dates as possible newEntry.cdata = entryParsed; try { localStorage.setItem(k, JSON.stringify(newEntry)); } catch (e) { if (e.name == 'QUOTA_EXCEEDED_ERR' || e.name == 'NS_ERROR_DOM_QUOTA_REACHED' || e.name == 'QuotaExceededError' || e.name == 'W3CException_DOM_QUOTA_EXCEEDED_ERR') { removeOldestLocalStorageEntries(function(){ localStorage.setItem(k, JSON.stringify(newEntry)); }); } } entryParsed = newEntry; corrected++; } // compress uncompressed data if(typeof entryParsed.data != 'undefined') { // but not if it's not containing any data (some bug may have saved an empty, false or null value) if(entryParsed.data === false || entryParsed.data === null || entryParsed.data.length == 0) { delete localStorage[k]; deleted++; return true; } var dataCompressed = LZString.compressToUTF16(JSON.stringify(entryParsed.data)); var newCompressedEntry = {}; newCompressedEntry.modified = entryParsed.modified; newCompressedEntry.cdata = dataCompressed; localStorage.setItem(k, JSON.stringify(newCompressedEntry)); compressed++; } } }); console.log(corrected + ' entries corrected, ' + deleted + ' entries deleted, ' + compressed + ' entries compressed'); } /* · · · Checks if localstorage is availible · · We can't just do if(typeof localStorage.selectedLanguage != 'undefined') · because firefox with cookies disabled then freaks out and stops executing js completely · · · · · · · · · · */ function localStorageIsEnabled() { var mod = 'test'; try { localStorage.setItem(mod, mod); localStorage.removeItem(mod); return true; } catch(e) { if (e.name == 'QUOTA_EXCEEDED_ERR' || e.name == 'NS_ERROR_DOM_QUOTA_REACHED' || e.name == 'QuotaExceededError' || e.name == 'W3CException_DOM_QUOTA_EXCEEDED_ERR') { // if localstorage is empty but returns a full error, we assume it's disabled (in an ugly way) if(localStorage.length === 0) { return false; } var successfulRemoval = removeOldestLocalStorageEntries(function(){ return localStorageIsEnabled(); }); if(successfulRemoval === false) { return false; } } else { return false; } } } /* · · · Block/unblock user and do necessary stuff · · · · · · · · · · */ function blockUser(arg, callback) { $('body').click(); // a click somewhere hides any open menus // arguments is sent as an object, for easier use with a menu's function-row var userId = arg.userId; var blockButton_jQueryElement = arg.blockButton_jQueryElement; display_spinner(); APIBlockOrUnblockUser('block', userId, function(data) { remove_spinner(); // activate the button, if we were passed one if(typeof blockButton_jQueryElement != 'undefined') { blockButton_jQueryElement.removeClass('disabled'); } if(data && data.statusnet_blocking === true) { // success markUserAsBlockedInDOM(userId, data.following); if(typeof callback == 'function') { callback(blockButton_jQueryElement); } } else { // failed! showErrorMessage(window.sL.failedBlockingUser); } }); } function unblockUser(arg, callback) { $('body').click(); // a click somewhere hides any open menus // arguments is sent as an object, for easier use with a menu's function-row var userId = arg.userId; var blockButton_jQueryElement = arg.blockButton_jQueryElement; display_spinner(); APIBlockOrUnblockUser('unblock', userId, function(data) { remove_spinner(); // activate the button, if we were passed one if(typeof blockButton_jQueryElement != 'undefined') { blockButton_jQueryElement.removeClass('disabled'); } if(data && data.statusnet_blocking === false) { // success markUserAsUnblockedInDOM(userId, data.following); if(typeof callback == 'function') { callback(blockButton_jQueryElement); } } else { // failed! showErrorMessage(window.sL.failedUnblockingUser); } }); } function markUserAsBlockedInDOM(userId, following) { // display buttons accordingly $('.qvitter-follow-button[data-follow-user-id="' + userId + '"]').addClass('blocking'); if(following) { $('.qvitter-follow-button[data-follow-user-id="' + userId + '"]').addClass('following'); } else { $('.qvitter-follow-button[data-follow-user-id="' + userId + '"]').removeClass('following'); } // hide notices from the blocked user $.each($('.stream-item[data-quitter-id-in-stream][data-user-id="' + userId + '"]'),function(){ $(this).addClass('profile-blocked-by-me'); $(this).children('.queet').attr('data-tooltip',window.sL.thisIsANoticeFromABlockedUser); }); // add to the window.allBlocking array if (userIsBlocked(userId) === false) { window.allBlocking.push(userId); } } function markUserAsUnblockedInDOM(userId, following) { // display buttons accordingly $('.qvitter-follow-button[data-follow-user-id="' + userId + '"]').removeClass('blocking'); if(following) { $('.qvitter-follow-button[data-follow-user-id="' + userId + '"]').addClass('following'); } else { $('.qvitter-follow-button[data-follow-user-id="' + userId + '"]').removeClass('following'); } // hide the user from lists of blocked users if(window.currentStreamObject.name == 'user blocks' && window.currentStreamObject.nickname == window.loggedIn.screen_name) { $.each($('.stream-item.user[data-user-id="' + userId + '"]'),function(){ slideUpAndRemoveStreamItem($(this)); }); } // unhide notices from the blocked user $.each($('.stream-item[data-quitter-id-in-stream][data-user-id="' + userId + '"]'),function(){ $(this).removeClass('profile-blocked-by-me'); $(this).children('.queet').removeAttr('data-tooltip'); }); // remove from the window.allBlocking array var existingBlockIndex = window.allBlocking.indexOf(userId); if (existingBlockIndex > -1) { window.allBlocking.splice(existingBlockIndex, 1); } } /* · · · Is this user id blocked? · · · · · · · · · · */ function userIsBlocked(userId) { var existingBlock = window.allBlocking.indexOf(userId); if (existingBlock > -1) { return true; } else { return false; } } /* · · · Marks all notices from blocked users in an jQuery object as blocked · · · · · · · · · · */ function markAllNoticesFromBlockedUsersAsBlockedInJQueryObject(obj) { $.each(window.allBlocking,function(){ obj.find('.stream-item[data-user-id="' + this + '"]').addClass('profile-blocked-by-me'); obj.find('.stream-item[data-user-id="' + this + '"]').children('.queet').attr('data-tooltip',window.sL.thisIsANoticeFromABlockedUser); }); } /* · · · Marks all notices from muted users in an jQuery object as muted · · · · · · · · · · */ function markAllNoticesFromMutedUsersAsMutedInJQueryObject(obj) { $.each(obj.find('.stream-item'),function(){ if(isUserMuted($(this).attr('data-user-id'))) { $(this).addClass('user-muted'); $(this).children('.queet').attr('data-tooltip',window.sL.thisIsANoticeFromAMutedUser); } else { $(this).children('.queet').removeAttr('data-tooltip'); $(this).removeClass('user-muted'); } }); } /* · · · Marks all profile cards from muted users as muted in DOM · · · · · · · · · · */ function markAllProfileCardsFromMutedUsersAsMutedInDOM() { $.each($('body').find('.profile-header-inner'),function(){ if(isUserMuted($(this).attr('data-user-id'))) { $(this).parent('.profile-card').addClass('user-muted'); } else { $(this).parent('.profile-card').removeClass('user-muted'); } }); } /* · · · Function invoked after mute and unmute · · · · · · · · · · */ function hideOrShowNoticesAfterMuteOrUnmute() { markAllNoticesFromMutedUsersAsMutedInJQueryObject($('#feed-body')); markAllProfileCardsFromMutedUsersAsMutedInDOM(); } /* · · · Sandbox/unsandbox user and do necessary stuff · · · · · · · · · · */ function sandboxCreateOrDestroy(arg, callback) { $('body').click(); // a click somewhere hides any open menus display_spinner(); APISandboxCreateOrDestroy(arg.createOrDestroy, arg.userId, function(data) { remove_spinner(); if(!data) { // failed! showErrorMessage(window.sL.ERRORfailedSandboxingUser); } }); } /* · · · Sandbox/unsandbox user and do necessary stuff · · · · · · · · · · */ function silenceCreateOrDestroy(arg, callback) { $('body').click(); // a click somewhere hides any open menus display_spinner(); APISilenceCreateOrDestroy(arg.createOrDestroy, arg.userId, function(data) { remove_spinner(); if(!data) { // failed! showErrorMessage(window.sL.ERRORfailedSilencingUser); } }); } /* · · · Get the logged in user's menu array · · · · · · · · · · */ function loggedInUsersMenuArray() { return [ { type: 'function', functionName: 'goToEditProfile', label: window.sL.editMyProfile }, { type: 'link', href: window.siteInstanceURL + 'settings/profile', label: window.sL.settings }, { type:'divider' }, { type: 'link', href: window.siteInstanceURL + window.loggedIn.screen_name + '/mutes', label: window.sL.userMuted }, { type: 'link', href: window.siteInstanceURL + window.loggedIn.screen_name + '/blocks', label: window.sL.userBlocked }]; } /* · · · Append moderator user actions to menu array · · @param menuArray: array used to build menus in getMenu() · @param userID: the user id of the user to act on · @param userScreenName: the screen_name/nickname/username of the user to act on · @param sandboxed: is the user sandboxed? · @param silenced: is the user silenced? · · · · · · · · · · */ function appendModeratorUserActionsToMenuArray(menuArray,userID,userScreenName,sandboxed,silenced) { // not if it's me if(window.loggedIn.id == userID) { return menuArray; } if(window.loggedIn !== false && window.loggedIn.rights.sandbox === true) { menuArray.push({type:'divider'}); if(sandboxed === true) { menuArray.push({ type: 'function', functionName: 'sandboxCreateOrDestroy', functionArguments: { userId: userID, createOrDestroy: 'destroy' }, label: window.sL.unSandboxThisUser.replace('{nickname}','@' + userScreenName) }); } else { menuArray.push({ type: 'function', functionName: 'sandboxCreateOrDestroy', functionArguments: { userId: userID, createOrDestroy: 'create' }, label: window.sL.sandboxThisUser.replace('{nickname}','@' + userScreenName) }); } } if(window.loggedIn !== false && window.loggedIn.rights.silence === true) { if(silenced === true) { menuArray.push({ type: 'function', functionName: 'silenceCreateOrDestroy', functionArguments: { userId: userID, createOrDestroy: 'destroy' }, label: window.sL.unSilenceThisUser.replace('{nickname}','@' + userScreenName) }); } else { menuArray.push({ type: 'function', functionName: 'silenceCreateOrDestroy', functionArguments: { userId: userID, createOrDestroy: 'create' }, label: window.sL.silenceThisUser.replace('{nickname}','@' + userScreenName) }); } } return menuArray; } /* · · · Updates the times for all queets loaded to DOM · · · · · · · · · · */ function updateAllQueetsTimes() { $('[data-created-at]').each(function(){ // if the element with the data-created-at doesn't have an a-child, we change the html of the element if($(this).children('a').length==0){ $(this).html(parseTwitterDate($(this).attr('data-created-at'))); } // otherwise the change the child's html else { $(this).children('a').html(parseTwitterDate($(this).attr('data-created-at'))); } }); } /* · · · Is this a local URL? · · · · · · · · · · */ function isLocalURL(url) { if(url.substring(0,window.siteInstanceURL.length) == window.siteInstanceURL) { return true; } else { if(url.substring(0,window.siteAttachmentURLBase.length) == window.siteAttachmentURLBase) { return true; } else { return false; } } } /* · · · Check for hidden items and show the new queets bar if there are any · · · · · · · · · · */ function maybeShowTheNewQueetsBar() { var newQueetsNum = $('#feed-body').find('.stream-item.hidden:not(.always-hidden):not(.hidden-repeat)').length; // subtract the number of hidden notices from muted users if this isn't the notifications stream, // or if this is the notifications stream but the user has opted out of seeing notifications from muted users var mutedHiddenNum = 0; if(window.currentStreamObject.name == 'notifications') { if($('#feed-body').hasClass('hide-notifications-from-muted-users')) { mutedHiddenNum = $('#feed-body').find('.stream-item.hidden.user-muted:not(.always-hidden):not(.hidden-repeat)').length; } } else { var mutedHiddenNum = $('#feed-body').find('.stream-item.hidden.user-muted:not(.always-hidden):not(.hidden-repeat)').length; } newQueetsNum = newQueetsNum - mutedHiddenNum; if(newQueetsNum > 0) { $('#new-queets-bar').parent().removeClass('hidden'); // bar label if(newQueetsNum == 1) { var q_txt = window.sL.newQueet; } else { var q_txt = window.sL.newQueets; } if(window.currentStreamObject.name == 'notifications') { if(newQueetsNum == 1) { var q_txt = window.sL.newNotification; } else { var q_txt = window.sL.newNotifications; } } $('#new-queets-bar').html(q_txt.replace('{new-notice-count}',newQueetsNum)); } } /* · · · Align tooltips to the hovered element · · · · · · · · · · */ function alignTooltipTohoveredElement(tooltipElement,tooltipCaret,hovered) { var tooltipWidth = tooltipElement.outerWidth(); var tooltipHeight = tooltipElement.outerHeight(); var windowWidth = $(window).width(); var windowScrollPosY = $(window).scrollTop(); var targetPosX = hovered.offset().left; var targetPosY = hovered.offset().top; var targetHeight = hovered.outerHeight(); var targetWidth = hovered.outerWidth(); // too little space on top of element, show tooltip at bottom if((targetPosY-windowScrollPosY-tooltipHeight-10) < 0) { var tooltipCaretPosX = targetPosX+targetWidth/2-5; var tooltipCaretPosY = targetPosY+targetHeight+2; // caret always directly under element tooltipCaret.css('left',tooltipCaretPosX + 'px'); tooltipCaret.css('top',tooltipCaretPosY + 'px'); tooltipCaret.addClass('top'); // tooltip itself might bleed over the window edges, and need moving var tooltipPosX = targetPosX+targetWidth/2-tooltipWidth/2; var tooltipPosY = targetPosY+targetHeight+7; if((tooltipPosX+tooltipWidth)>windowWidth) { tooltipPosX = windowWidth-tooltipWidth-5; } if(tooltipPosX < 5) { tooltipPosX = 5; } tooltipElement.css('left',tooltipPosX + 'px'); tooltipElement.css('top',tooltipPosY + 'px'); } // tooltip at top else { var tooltipCaretPosX = targetPosX+targetWidth/2-5; var tooltipCaretPosY = targetPosY-8; // caret always directly on top of element tooltipCaret.css('left',tooltipCaretPosX + 'px'); tooltipCaret.css('top',tooltipCaretPosY + 'px'); tooltipCaret.addClass('bottom'); // tooltip itself might bleed over the window edges, and need moving var tooltipPosX = targetPosX+targetWidth/2-tooltipWidth/2; var tooltipPosY = targetPosY-7-tooltipHeight; if((tooltipPosX+tooltipWidth)>windowWidth) { tooltipPosX = windowWidth-tooltipWidth-5; } if(tooltipPosX < 5) { tooltipPosX = 5; } tooltipElement.css('left',tooltipPosX + 'px'); tooltipElement.css('top',tooltipPosY + 'px'); } } /* · · · Cache the unicode compatible regexps for the syntax highlighting · · · · · · · · · · */ function cacheSyntaxHighlighting() { window.syntaxHighlightingRegexps = Object(); 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)'; window.syntaxHighlightingRegexps.externalMention = XRegExp.cache('(^|\\s|\\.|
| |\\()(@)[a-zA-Z0-9]+(@)[\\p{L}\\p{N}\\-\\.]+(\\.)(' + allDomains + ')($|\\s|\\.|\\,|\\:|\\-|\\<|\\!|\\?|\\&|\\)|\\\')'); window.syntaxHighlightingRegexps.mention = /(^|\s|\.|
| |\()(@)[a-zA-Z0-9]+($|\s|\.|\,|\:|\-|\<|\!|\?|\&|\)|\')/; window.syntaxHighlightingRegexps.tag = XRegExp.cache('(^|\\s|\\.|
| |\\()(\\#)[\\p{L}\\p{N}\\-\\.]+($|\\s|\\,|\\:|\\<|\\!|\\?|\\&|\\)|\\\')'); window.syntaxHighlightingRegexps.url = XRegExp.cache('(^|\\s|\\.|
| |\\()(http\\:\\/\\/|https\:\\/\\/)([\\p{L}\\p{N}\\-\\.]+)?(\\.)(' + allDomains + ')(\\/[\\p{L}\\p{N}\\%\\!\\*\\\'\\(\\)\\;\\:\\@\\&\\=\\+\\$\\,\\/\\?\\#\\[\\]\\-\\_\\.\\~]+)?(\\/)?($|\\s|\\,|\\:|\\-|\\<|\\!|\\?|\\&|\\)|\\\')'); window.syntaxHighlightingRegexps.urlWithoutProtocol = XRegExp.cache('(^|\\s|\\.|
| |\\()[\\p{L}\\p{N}\\-\\.]+(\\.)(' + allDomains + ')(\\/[\\p{L}\\p{N}\\%\\!\\*\\\'\\(\\)\\;\\:\\@\\&\\=\\+\\$\\,\\/\\?\\#\\[\\]\\-\\_\\.\\~]+)?(\\/)?($|\\s|\\.|\\,|\\:|\\-|\\<|\\!|\\?|\\&|\\)|\\\')'); window.syntaxHighlightingRegexps.email = XRegExp.cache('(^|\\s|\\.|
| |\\()([a-zA-Z0-9\\!\\#\\$\\%\\&\\\'\\*\\+\\-\\/\\=\\?\\^\\_\\`\\{\\|\\}\\~\\.]+)?(@)[\\p{L}\\p{N}\\-\\.]+(\\.)(' + allDomains + ')($|\\s|\\.|\\,|\\:|\\-|\\<|\\!|\\?|\\&|\\)|\\\')'); cacheSyntaxHighlightingGroups(); } /* · · · Cache syntax highlighting for groups · · · · · · · · · · */ function cacheSyntaxHighlightingGroups() { if(window.groupNicknamesAndLocalAliases.length > 0) { var allGroupNicknamesAndLocalAliases = '(' + window.groupNicknamesAndLocalAliases.join('|') + ')'; window.syntaxHighlightingRegexps.group = XRegExp.cache('(^|\\s|\\.|
| |\\()(\\!)' + allGroupNicknamesAndLocalAliases + '($|\\s|\\.|\\,|\\:|\\-|\\<|\\!|\\?|\\&|\\)|\\\')'); } } /* · · · User array cache (called array because it's an array in php) · · Stored in window.userArrayCache with unique key like instance_url/nickname · with protocol (http:// or https://) trimmed off, e.g. "quitter.se/hannes2peer" · · · · · · · · · · */ window.userArrayCache = new Object(); window.convertUriToUserArrayCacheKey = new Object(); window.convertStatusnetProfileUrlToUserArrayCacheKey = new Object(); window.convertLocalIdToUserArrayCacheKey = new Object(); function userArrayCacheStore(data) { if(typeof data == 'undefined') { return false; } // if we are passed a data object with both local and external data, use external data as key if(typeof data.local != 'undefined' && typeof data.local.statusnet_profile_url != 'undefined' && typeof data.external != 'undefined' && typeof data.external.statusnet_profile_url != 'undefined') { var instanceUrlWithoutProtocol = guessInstanceUrlWithoutProtocolFromProfileUrlAndNickname(data.external.statusnet_profile_url, data.external.screen_name); var key = instanceUrlWithoutProtocol + '/' + data.external.screen_name; var dataToStore = data; } // we can also get either local... else if(typeof data.local != 'undefined' && typeof data.local.statusnet_profile_url != 'undefined' ) { var instanceUrlWithoutProtocol = guessInstanceUrlWithoutProtocolFromProfileUrlAndNickname(data.local.statusnet_profile_url, data.local.screen_name); var key = instanceUrlWithoutProtocol + '/' + data.local.screen_name; data.external = false; var dataToStore = data; } // ...or external... else if(typeof data.external != 'undefined' && typeof data.external.statusnet_profile_url != 'undefined' ) { var instanceUrlWithoutProtocol = guessInstanceUrlWithoutProtocolFromProfileUrlAndNickname(data.external.statusnet_profile_url, data.external.screen_name); var key = instanceUrlWithoutProtocol + '/' + data.external.screen_name; data.local = false; var dataToStore = data; } // ...or an unspecified data object, in which case we check the avatar urls to see if it's local or external else if (typeof data.statusnet_profile_url != 'undefined') { var instanceUrlWithoutProtocol = guessInstanceUrlWithoutProtocolFromProfileUrlAndNickname(data.statusnet_profile_url, data.screen_name); var key = instanceUrlWithoutProtocol + '/' + data.screen_name; var localOrExternal = detectLocalOrExternalUserObject(data); // local if(localOrExternal == 'local'){ var dataToStore = {local:data,external:false}; } // external else { var dataToStore = {external:data,local:false}; } } else { return false; } // store if(typeof window.userArrayCache[key] == 'undefined') { window.userArrayCache[key] = dataToStore; window.userArrayCache[key].modified = Date.now(); // easy conversion between URI and statusnet_profile_url and the key we're using in window.userArrayCache window.convertLocalIdToUserArrayCacheKey[parseInt(dataToStore.local.id, 10)] = key; window.convertUriToUserArrayCacheKey[dataToStore.local.ostatus_uri] = key; window.convertStatusnetProfileUrlToUserArrayCacheKey[dataToStore.local.statusnet_profile_url] = key; } else { if(dataToStore.local) { // keep old status if newer data doesn't have any if(typeof dataToStore.local.status == 'undefined' && typeof window.userArrayCache[key].local.status != 'undefined') { dataToStore.local.status = window.userArrayCache[key].local.status; } window.userArrayCache[key].local = dataToStore.local; // easy conversion between URI and statusnet_profile_url and the key we're using in window.userArrayCache window.convertLocalIdToUserArrayCacheKey[dataToStore.local.id] = key; window.convertUriToUserArrayCacheKey[dataToStore.local.ostatus_uri] = key; window.convertStatusnetProfileUrlToUserArrayCacheKey[dataToStore.local.statusnet_profile_url] = key; } if(dataToStore.external) { window.userArrayCache[key].external = dataToStore.external; // easy conversion between URI and the key we're using in window.userArrayCache window.convertUriToUserArrayCacheKey[dataToStore.external.ostatus_uri] = key; window.convertStatusnetProfileUrlToUserArrayCacheKey[dataToStore.external.statusnet_profile_url] = key; } // store the time when this record was modified if(dataToStore.local || dataToStore.external) { window.userArrayCache[key].modified = Date.now(); } } } function userArrayCacheGetByLocalNickname(localNickname) { if(localNickname.substring(0,1) == '@') { localNickname = localNickname.substring(1); } if(typeof window.userArrayCache[window.siteRootDomain + '/' + localNickname] != 'undefined') { return window.userArrayCache[window.siteRootDomain + '/' + localNickname]; } else { return false; } } function userArrayCacheGetByProfileUrlAndNickname(profileUrl, nickname) { var possibleLocalId = false; if(nickname.substring(0,1) == '@') { nickname = nickname.substring(1); } if(profileUrl.indexOf(window.siteInstanceURL + 'user/') == 0) { possibleLocalId = parseInt(profileUrl.substring(window.siteInstanceURL.length+5),10); } // the url might match a known profile uri if(typeof window.convertUriToUserArrayCacheKey[profileUrl] != 'undefined') { if(typeof window.userArrayCache[window.convertUriToUserArrayCacheKey[profileUrl]] != 'undefined') { return window.userArrayCache[window.convertUriToUserArrayCacheKey[profileUrl]]; } } // or the href attribute might match a known statusnet_profile_url else if(typeof window.convertStatusnetProfileUrlToUserArrayCacheKey[profileUrl] != 'undefined') { if(typeof window.userArrayCache[window.convertStatusnetProfileUrlToUserArrayCacheKey[profileUrl]] != 'undefined') { return window.userArrayCache[window.convertStatusnetProfileUrlToUserArrayCacheKey[profileUrl]]; } } // or the local id might match a known id else if(typeof window.convertLocalIdToUserArrayCacheKey[possibleLocalId] != 'undefined') { if(typeof window.userArrayCache[window.convertLocalIdToUserArrayCacheKey[possibleLocalId]] != 'undefined') { return window.userArrayCache[window.convertLocalIdToUserArrayCacheKey[possibleLocalId]]; } } // or we try to guess the instance url, and see if we have a match in our cache else if(typeof window.userArrayCache[guessInstanceUrlWithoutProtocolFromProfileUrlAndNickname(profileUrl, nickname) + '/' + nickname] != 'undefined') { return window.userArrayCache[guessInstanceUrlWithoutProtocolFromProfileUrlAndNickname(profileUrl, nickname) + '/' + nickname]; } // we couldn't find any cached user array else { return false; } } function userArrayCacheGetUserNicknameById(id) { var possibleUserURI = window.siteInstanceURL + 'user/' + id; var key = window.convertUriToUserArrayCacheKey[possibleUserURI]; if(typeof key != 'undefined') { if(typeof window.userArrayCache[key] != 'undefined') { return window.userArrayCache[key].local.screen_name; } } return false; } /* · · · Detect if the supplied user object is from the local server or external · · · · · · · · · · */ function detectLocalOrExternalUserObject(userObject) { var dataProfileImageUrlWithoutProtocol = removeProtocolFromUrl(userObject.profile_image_url); var siteInstanceURLWithoutProtocol = removeProtocolFromUrl(window.siteInstanceURL); if(dataProfileImageUrlWithoutProtocol.substring(0,siteInstanceURLWithoutProtocol.length) == siteInstanceURLWithoutProtocol){ return 'local'; } else { return 'external'; } } /* · · · Guess instance's base installation url without protocol from a profile url · · · · · · · · · · */ function guessInstanceUrlWithoutProtocolFromProfileUrlAndNickname(profileUrl, nickname) { // remove protocol var guessedInstanceUrl = removeProtocolFromUrl(profileUrl) // user/id-style profile urls if(guessedInstanceUrl.indexOf('/user/') > -1 && $.isNumeric(guessedInstanceUrl.substring(guessedInstanceUrl.lastIndexOf('/user/')+6))) { guessedInstanceUrl = guessedInstanceUrl.substring(0,guessedInstanceUrl.lastIndexOf('/user/')); } // nickname-style profile urls else if(guessedInstanceUrl.substring(guessedInstanceUrl.lastIndexOf('/')+1) == nickname) { guessedInstanceUrl = guessedInstanceUrl.substring(0,guessedInstanceUrl.lastIndexOf('/')); } // remove trailing "index.php" if the instance doesn't use mod_rewrite if(guessedInstanceUrl.substring(guessedInstanceUrl.lastIndexOf('/')) == '/index.php') { guessedInstanceUrl = guessedInstanceUrl.substring(0,guessedInstanceUrl.lastIndexOf('/')); } // there was a bug once that made some instances have multiple /:s in their url, // so make sure there's no trailing /:s while (guessedInstanceUrl.slice(-1) == '/') { guessedInstanceUrl = guessedInstanceUrl.slice(0,-1); } // fix new mastodon style profile urls if(guessedInstanceUrl.indexOf('/@') > -1) { guessedInstanceUrl = guessedInstanceUrl.substring(0, guessedInstanceUrl.indexOf('/@')); } return guessedInstanceUrl; } /* · · · Remove the protocol (e.g. "http://") from an URL · · · · · · · · · · */ function removeProtocolFromUrl(url) { if(typeof url == 'undefined' || url === null || url === false || url == '') { return ''; } if(url.indexOf('://') == -1) { return url; } return url.substring(url.indexOf('://')+3); } /* · · · Get host from URL · · · · · · · · · · */ function getHost(url) { var a = document.createElement('a'); a.href = url; return a.hostname; } /* · · · Is this url a link to my profile? · · · · · · · · · · */ function thisIsALinkToMyProfile(url) { if(typeof url == 'undefined') { return false; } if(!window.loggedIn) { return false; } if(url.slice(-1) == '/') { // remove trailing '/' url = url.slice(0,-1); } var urlWithoutProtocol = removeProtocolFromUrl(url); if(removeProtocolFromUrl(window.loggedIn.statusnet_profile_url) == urlWithoutProtocol) { return true; } var userIdUrlWithoutProtocol = removeProtocolFromUrl(window.siteInstanceURL) + 'user/' + window.loggedIn.id; if(userIdUrlWithoutProtocol == urlWithoutProtocol) { return true; } return false; } /* · · · Iterates recursively through an API response in search for user data to cache · If we find a "statusnet_profile_url" key we assume the parent is a user array/object · · · · · · · · · · · · · · */ function searchForUserDataToCache(obj) { for (var property in obj) { if (obj.hasOwnProperty(property)) { if (typeof obj[property] == "object") { searchForUserDataToCache(obj[property]); } else if(typeof obj[property] == 'string' && property == 'statusnet_profile_url') { userArrayCacheStore(obj); } } } } /* · · · Updates user data loaded into the stream with the latest data from the user array cache · This function should therefor always be invoked _after_ searchForUserDataToCache() · · · · · · · · · · · · · · */ function updateUserDataInStream() { var timeNow = Date.now(); $.each(window.userArrayCache,function(k,userArray){ // if the cache record was updated the latest second, we assume this is brand new info that we haven't // updated the stream with if(typeof userArray.local != 'undefined' && userArray.local !== false && typeof userArray.modified != 'undefined' && (timeNow-userArray.modified)<1000) { // add/remove silenced class to stream items and profile cards if(userArray.local.is_silenced === true) { $('.stream-item[data-user-id=' + userArray.local.id + ']').addClass('silenced'); $('.profile-card .profile-header-inner[data-user-id=' + userArray.local.id + ']').addClass('silenced'); $('.user-menu-cog[data-user-id=' + userArray.local.id + ']').addClass('silenced'); } else { $('.stream-item[data-user-id=' + userArray.local.id + ']').removeClass('silenced') $('.profile-card .profile-header-inner[data-user-id=' + userArray.local.id + ']').removeClass('silenced'); $('.user-menu-cog[data-user-id=' + userArray.local.id + ']').removeClass('silenced'); } // add/remove sandboxed class to stream items and profile cards if(userArray.local.is_sandboxed === true) { $('.stream-item[data-user-id=' + userArray.local.id + ']').addClass('sandboxed'); $('.profile-card .profile-header-inner[data-user-id=' + userArray.local.id + ']').addClass('sandboxed'); $('.user-menu-cog[data-user-id=' + userArray.local.id + ']').addClass('sandboxed'); } else { $('.stream-item[data-user-id=' + userArray.local.id + ']').removeClass('sandboxed') $('.profile-card .profile-header-inner[data-user-id=' + userArray.local.id + ']').removeClass('sandboxed'); $('.user-menu-cog[data-user-id=' + userArray.local.id + ']').removeClass('sandboxed'); } // profile size avatars (notices, users) $.each($('img.avatar.profile-size[data-user-id="' + userArray.local.id + '"]'),function(){ if($(this).attr('src') != userArray.local.profile_image_url_profile_size) { $(this).attr('src',userArray.local.profile_image_url_profile_size); } }); // standard size avatars (notifications) $.each($('img.avatar.standard-size[data-user-id="' + userArray.local.id + '"]'),function(){ if($(this).attr('src') != userArray.local.profile_image_url) { $(this).attr('src',userArray.local.profile_image_url); } }); // full names $.each($('strong.name[data-user-id="' + userArray.local.id + '"],\ .fullname[data-user-id="' + userArray.local.id + '"]'),function(){ if($(this).html() != userArray.local.name) { $(this).html(userArray.local.name); } }); // user/screen names $.each($('.screen-name[data-user-id="' + userArray.local.id + '"]'),function(){ if($(this).html().substring(1) != userArray.local.screen_name) { $(this).html('@' + userArray.local.screen_name); } }); // profile urls // try to find the last account group with this id, if the statusnet_profile_url seems to // be changed we replace it wherever we can find it, even in list urls etc that starts with statusnet_profile_url if(userArray.local.is_local === true && $('a.account-group[data-user-id="' + userArray.local.id + '"]').last().attr('href') != userArray.local.statusnet_profile_url) { var oldStatusnetProfileURL = $('a.account-group[data-user-id="' + userArray.local.id + '"]').last().attr('href'); // all links with the exact statusnet_profile_url $.each($('[href="' + oldStatusnetProfileURL + '"]'),function(){ $(this).attr('href',userArray.local.statusnet_profile_url); }); // links starting with statusnet_profile_url $.each($('[href*="' + oldStatusnetProfileURL + '/"]'),function(){ $(this).attr('href',$(this).attr('href').replace(oldStatusnetProfileURL + '/',userArray.local.statusnet_profile_url + '/')); }); } // cover photos $.each($('.profile-header-inner[data-user-id="' + userArray.local.id + '"]'),function(){ if($(this).css('background-image') != 'url("' + userArray.local.cover_photo + '")' && userArray.local.cover_photo != false) { $(this).css('background-image','url("' + userArray.local.cover_photo + '")'); } }); // the window.following object might need updating also if(typeof window.following != 'undefined' && typeof window.following[userArray.local.id] != 'undefined') { if(window.following[userArray.local.id].avatar != userArray.local.profile_image_url) { window.following[userArray.local.id].avatar = userArray.local.profile_image_url; } if(window.following[userArray.local.id].name != userArray.local.name) { window.following[userArray.local.id].name = userArray.local.name; } if(window.following[userArray.local.id].username != userArray.local.screen_name) { window.following[userArray.local.id].username = userArray.local.screen_name; } } } }); } /* · · · Iterates recursively through an API response in search for updated notice data · If we find a "repeated" key we assume the parent is a notice object (chosen arbitrary) · · · · · · · · · · · · · · */ window.knownDeletedNotices = new Object(); function searchForUpdatedNoticeData(obj) { var streamItemsUpdated = false; for (var property in obj) { if (obj.hasOwnProperty(property)) { if (typeof obj[property] == "object") { searchForUpdatedNoticeData(obj[property]); } else if(typeof obj[property] == 'boolean' && property == 'repeated') { 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 // if this is a special qvitter-delete-notice activity notice it means we try to hide // the deleted notice from our stream // the uri is in the obj.text var, between the double curly brackets if(typeof obj.qvitter_delete_notice != 'undefined' && obj.qvitter_delete_notice == true) { var uriToHide = obj.text.substring(obj.text.indexOf('{{')+2,obj.text.indexOf('}}')); window.knownDeletedNotices[uriToHide] = true; var streamItemToHide = $('.stream-item[data-uri="' + uriToHide + '"]'); slideUpAndRemoveStreamItem(streamItemToHide); streamItemsUpdated = true; } // if this is not a delete notice it means the notice exists and is not deleted, // correct any notices that are marked as unrepeated, they might have // been marked like that by mistake (i.e. a bug...) else if(streamItemFoundInFeed.hasClass('unrepeated')) { streamItemFoundInFeed.removeClass('unrepeated always-hidden'); streamItemsUpdated = true; } // ordinary notices else if(streamItemFoundInFeed.length>0) { var queetFoundInFeed = streamItemFoundInFeed.children('.queet'); var queetID = streamItemFoundInFeed.attr('data-quitter-id'); // sometimes activity notices don't get the is_activity flag set to true // maybe because they were in the process of being saved when // we first got them if(obj.is_post_verb === false) { streamItemFoundInFeed.addClass('activity always-hidden'); streamItemsUpdated = true; } // update the avatar row if the queet is expanded and the numbers are not the same if(streamItemFoundInFeed.hasClass('expanded')) { var oldFavNum = parseInt(queetFoundInFeed.find('.action-fav-num').text(),10); var oldRQNum = parseInt(queetFoundInFeed.find('.action-rq-num').text(),10); if(oldFavNum != obj.fave_num || oldRQNum != obj.repeat_num) { getFavsAndRequeetsForQueet(streamItemFoundInFeed, queetID); } } // attachments might have been added/changed/have had time to be processed if(queetFoundInFeed.children('script.attachment-json').text() != JSON.stringify(obj.attachments)) { if(queetFoundInFeed.children('script.attachment-json').length == 0) { queetFoundInFeed.prepend(''); } else { queetFoundInFeed.children('script.attachment-json').text(JSON.stringify(obj.attachments)); } var attachmentsHTMLBuild = buildAttachmentHTML(obj.attachments); var thumbsIsHidden = false; if(queetFoundInFeed.find('.queet-thumbs').hasClass('hide-thumbs')) { var thumbsIsHidden = true; } queetFoundInFeed.find('.queet-thumbs').remove(); queetFoundInFeed.find('.oembed-data').remove(); placeQuotedNoticesInQueetText(attachmentsHTMLBuild.quotedNotices,queetFoundInFeed.find('.queet-text')); // we might want to hide urls (rendered as attachments) in the queet text $.each(queetFoundInFeed.find('.queet-text').find('a'),function(){ if(attachmentsHTMLBuild.urlsToHide.indexOf($(this).text()) > -1) { $(this).removeAttr('style'); // temporary fix $(this).addClass('hidden-embedded-link-in-queet-text'); } }); queetFoundInFeed.find('.queet-text').after(attachmentsHTMLBuild.html); if(thumbsIsHidden) { queetFoundInFeed.find('.queet-thumbs').addClass('hide-thumbs'); } streamItemsUpdated = true; } // attentions might have been added to a notice if(queetFoundInFeed.children('script.attentions-json').text() != JSON.stringify(obj.attentions)) { if(queetFoundInFeed.children('script.attentions-json').length == 0) { queetFoundInFeed.prepend(''); } else { queetFoundInFeed.children('script.attentions-json').text(JSON.stringify(obj.attentions)); } } // set favorite data queetFoundInFeed.find('.action-fav-num').attr('data-fav-num',obj.fave_num); queetFoundInFeed.find('.action-fav-num').html(obj.fave_num); if(obj.favorited) { streamItemFoundInFeed.addClass('favorited'); queetFoundInFeed.find('.action-fav-container').children('.with-icn').addClass('done'); queetFoundInFeed.find('.action-fav-container').find('.icon.sm-fav').attr('data-tooltip',window.sL.favoritedVerb); streamItemsUpdated = true; } else { streamItemFoundInFeed.removeClass('favorited'); queetFoundInFeed.find('.action-fav-container').children('.with-icn').removeClass('done'); queetFoundInFeed.find('.action-fav-container').find('.icon.sm-fav').attr('data-tooltip',window.sL.favoriteVerb); streamItemsUpdated = true; } // set repeat data queetFoundInFeed.find('.action-rq-num').attr('data-rq-num',obj.repeat_num); queetFoundInFeed.find('.action-rq-num').html(obj.repeat_num); if(obj.repeated) { streamItemFoundInFeed.addClass('requeeted'); queetFoundInFeed.find('.action-rt-container').children('.with-icn').addClass('done'); queetFoundInFeed.find('.action-rt-container').find('.icon.sm-rt').attr('data-tooltip',window.sL.requeetedVerb); streamItemFoundInFeed.attr('data-requeeted-by-me-id',obj.repeated_id); streamItemsUpdated = true; } else { streamItemFoundInFeed.removeClass('requeeted'); queetFoundInFeed.find('.action-rt-container').children('.with-icn').removeClass('done'); queetFoundInFeed.find('.action-rt-container').find('.icon.sm-rt').attr('data-tooltip',window.sL.requeetVerb); streamItemFoundInFeed.removeAttr('data-requeeted-by-me-id'); streamItemsUpdated = true; } } } } } if(streamItemsUpdated) { // TODO, create a queue that runs with setInterval instead, say every 5 s, // that way we can run rememberStreamStateInLocalStorage() in the background, // and don't slow down stream change etc // rememberStreamStateInLocalStorage(); } } /* · · · Removes a deleted stream item from the feed gracefully, if not already hidden · · · · · · · · · · */ function slideUpAndRemoveStreamItem(streamItem,callback) { if(streamItem.length>0 && !streamItem.hasClass('always-hidden')) { streamItem.animate({opacity:'0.2'},1000,'linear',function(){ $(this).css('height',$(this).height() + 'px'); $(this).animate({height:'0px'},500,'linear',function(){ $(this).addClass('deleted always-hidden'); rememberStreamStateInLocalStorage(); if(typeof callback == 'function') { callback(); } }); }); } } /* · · · Store the current stream's state (html) in localStorage (if we're logged in) · · · · · · · · · · */ function rememberStreamStateInLocalStorage() { if(typeof window.currentStreamObject != 'undefined') { // don't store expanded content, only store profile card and the top 20 visible stream-items var firstTwentyVisibleHTML = ''; var i = 0; $.each($('#feed-body').children('.stream-item'),function(k,streamItem){ firstTwentyVisibleHTML += $(streamItem).outerHTML(); if(!$(streamItem).hasClass('always-hidden')) { i++; } if(i>20) { return false; } }); var feed = $('
').append(firstTwentyVisibleHTML); // we add some of these things again when the notices are fetched from the cache cleanStreamItemsFromClassesAndConversationElements(feed); var feedHtml = feed.html(); var profileCardHtml = $('#feed').siblings('.profile-card').outerHTML(); var streamData = { card: profileCardHtml, feed: feedHtml }; localStorageObjectCache_STORE('streamState',window.currentStreamObject.path, streamData); } } /* · · · Clean stream items from classes and conversation elements, · to use e.g. for caching and including in popup footers · · @param streamItems: jQuery object with stream items as children · · · · · · · · · · */ function cleanStreamItemsFromClassesAndConversationElements(streamItems) { streamItems.children('.stream-item').removeClass('profile-blocked-by-me'); streamItems.children('.stream-item').children('.queet').removeAttr('data-tooltip'); // can contain tooltip about blocked user streamItems.find('.temp-post').remove(); streamItems.children('.stream-item').removeClass('not-seen'); streamItems.children('.stream-item').removeClass('hidden-repeat'); // this means we need hide repeats when adding cached notices to feed later streamItems.children('.stream-item').removeClass('selected-by-keyboard'); streamItems.find('.dropdown-menu').remove(); streamItems.find('.stream-item').removeClass('expanded').removeClass('next-expanded').removeClass('hidden').removeClass('collapsing').addClass('visible'); streamItems.children('.stream-item').each(function() { cleanUpAfterCollapseQueet($(this)); }); } /* · · · Hide all instances (repeats) of a notice but the first/oldest one · · @param streamItems: jQuery object with stream items as children · · · · · · · · · · */ function hideAllButOldestInstanceOfStreamItem(streamItemContainer) { streamItemContainer.children('.stream-item').each(function(){ // if this stream item have siblings _after_ it, with the same id, hide it! if($(this).nextAll('.stream-item[data-quitter-id="' + $(this).attr('data-quitter-id') + '"]').length > 0) { $(this).addClass('hidden-repeat'); } }); return streamItemContainer; } /* · · · Gets the full unshortened HTML for a queet · · · · · · · · · · */ function getFullUnshortenedHtmlForQueet(streamItem, cacheOnly) { if(typeof cacheOnly == 'undefined') { var cacheOnly = false; } var queet = streamItem.children('.queet'); var queetId = streamItem.attr('data-quitter-id'); var attachmentMore = queet.find('span.attachment.more'); // only if actually shortened if(attachmentMore.length>0 && queet.children('script.attachment-json').length > 0 && queet.children('script.attachment-json').text() != 'undefined') { // first try localstorage cache var cacheData = localStorageObjectCache_GET('fullQueetHtml',queetId); if(cacheData) { queet.find('.queet-text').html(cacheData); queet.outerHTML(detectRTL(queet.outerHTML())); } // then try static html file attachment, that we should have in the attachment-json script element else if(cacheOnly === false){ var attachmentId = attachmentMore.attr('data-attachment-id'); $.each(JSON.parse(queet.children('script.attachment-json').text()), function(k,attachment) { if(attachment.id == attachmentId) { $.get(attachment.url,function(data){ if(data) { // get body and store in localStorage var bodyHtml = $('').html(data).find('body').html(); localStorageObjectCache_STORE('fullQueetHtml',queetId,bodyHtml); queet.find('.queet-text').html($.trim(bodyHtml)); queet.outerHTML(detectRTL(queet.outerHTML())); } }); return false; } }); } } } /* · · · Appends a user to the array containing the mentions suggestions to show when typing a notice · · · · · · · · · · */ function appendUserToMentionsSuggestionsArray(user) { if(typeof window.following[user.id] == 'undefined') { // in the window.following array, we use "false" as url if it's a user from this instance if(user.is_local) { var url = false; } else { var url = guessInstanceUrlWithoutProtocolFromProfileUrlAndNickname(user.statusnet_profile_url,user.screen_name); } var userToAdd = { avatar: user.profile_image_url, id: user.id, name: user.name, url: url, username: user.screen_name }; window.following[user.id] = userToAdd; } } /* · · · Is a profile pref in the qvitter namespace enabled? · · · · · · · · · · */ function isQvitterProfilePrefEnabled(topic) { if(typeof window.qvitterProfilePrefs != 'undefined' && typeof window.qvitterProfilePrefs[topic] != 'undefined' && window.qvitterProfilePrefs[topic] !== null && window.qvitterProfilePrefs[topic] != '' && window.qvitterProfilePrefs[topic] !== false && window.qvitterProfilePrefs[topic] != 0 && window.qvitterProfilePrefs[topic] != '0') { return true; } return false; } /* · · · Is this user muted? · · · · · · · · · · */ function isUserMuted(userID) { if(isQvitterProfilePrefEnabled('mute:' + userID)) { return true; } else { return false; } } /* · · · Display unread notifications · · · · · · · · · · */ function displayOrHideUnreadNotifications(notifications) { var data = $.parseJSON(notifications); var totNotif = 0; if(data !== null && typeof data != 'undefined') { $.each(data,function(k,v){ totNotif = totNotif + parseInt(v,10); }); } if(window.currentStreamObject.name == 'notifications') { var hiddenNotifications = $('#feed-body').find('.stream-item.hidden:not(.always-hidden)').length; if(hiddenNotifications>0) { totNotif = totNotif + hiddenNotifications; } } if(totNotif>0) { $('#unseen-notifications').html(totNotif); document.title = '(' + totNotif + ') ' + window.siteTitle; // update html page title $('#unseen-notifications').show(); } else { $('#unseen-notifications').hide(); document.title = window.siteTitle; } } /* · · · Removes HTML special chars recursively from strings in objects · with exceptions: "statusnet_html" found in notices, which we assume · gnusocial already stripped from xss, and the "source" which should be · html rendered by gnusocial itself and not open for attacks · · @param obj: the object to search and replace in · · · · · · · · · · · · · · */ function iterateRecursiveReplaceHtmlSpecialChars(obj) { for (var property in obj) { if (obj.hasOwnProperty(property)) { if (typeof obj[property] == "object") { iterateRecursiveReplaceHtmlSpecialChars(obj[property]); } else if(typeof obj[property] == 'string' && property != 'statusnet_html' && property != 'oembedHTML' // we trust this to be cleaned server side && property != 'source') { obj[property] = replaceHtmlSpecialChars(obj[property]); } } } return obj; } function replaceHtmlSpecialChars(text) { // don't do anything if the text is undefined if(typeof text == 'undefined') { return text; } var map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; return text.replace(/[&<>"']/g, function(m) { return map[m]; }); } /* · · · Checks if register form is valid · · @returns true or false · · · · · · · · · · */ function validateRegisterForm(o) { var nickname = o.find('#signup-user-nickname-step2'); var fullname = o.find('#signup-user-name-step2'); var email = o.find('#signup-user-email-step2'); var homepage = o.find('#signup-user-homepage-step2'); var bio = o.find('#signup-user-bio-step2'); var loc = o.find('#signup-user-location-step2'); var password1 = o.find('#signup-user-password1-step2'); var password2 = o.find('#signup-user-password2-step2'); var passwords = o.find('#signup-user-password1-step2,#signup-user-password2-step2'); var allFieldsValid = true; if(nickname.val().length>1 && /^[a-zA-Z0-9]+$/.test(nickname.val())) { nickname.removeClass('invalid'); } else { nickname.addClass('invalid'); if(allFieldsValid)allFieldsValid=false; } if(fullname.val().length < 255) { fullname.removeClass('invalid'); } else { fullname.addClass('invalid'); if(allFieldsValid)allFieldsValid=false; } if(validEmail(email.val())) { email.removeClass('invalid'); } else { email.addClass('invalid'); if(allFieldsValid)allFieldsValid=false; } if($.trim(homepage.val()).length==0 || /^(ftp|http|https):\/\/[^ "]+$/.test(homepage.val())) { homepage.removeClass('invalid'); } else { homepage.addClass('invalid'); if(allFieldsValid)allFieldsValid=false; } if(bio.val().length < 140) { bio.removeClass('invalid'); } else { bio.addClass('invalid'); if(allFieldsValid)allFieldsValid=false; } if(loc.val().length < 255) { loc.removeClass('invalid'); } else { loc.addClass('invalid'); if(allFieldsValid)allFieldsValid=false; } if(password1.val().length>5 && password2.val().length>5 && password1.val() == password2.val()) { passwords.removeClass('invalid'); } else { passwords.addClass('invalid'); if(allFieldsValid)allFieldsValid=false; } return allFieldsValid; } function validEmail(email) { 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)) { return true; } else { return false; } } /* · · · Checks if edit profile form is valid · · @returns true or false · · · · · · · · · · */ function validateEditProfileForm(o) { var fullname = o.find('input.fullname'); var homepage = o.find('input.url'); var bio = o.find('textarea.bio'); var loc = o.find('input.location'); var allFieldsValid = true; if(fullname.val().length < 255) { fullname.removeClass('invalid'); } else { fullname.addClass('invalid'); if(allFieldsValid)allFieldsValid=false; } if($.trim(homepage.val()).length==0 || /^(ftp|http|https):\/\/[^ "]+$/.test(homepage.val())) { homepage.removeClass('invalid'); } else { homepage.addClass('invalid'); if(allFieldsValid)allFieldsValid=false; } if(bio.val().length < 140) { bio.removeClass('invalid'); } else { bio.addClass('invalid'); if(allFieldsValid)allFieldsValid=false; } if(loc.val().length < 255) { loc.removeClass('invalid'); } else { loc.addClass('invalid'); if(allFieldsValid)allFieldsValid=false; } return allFieldsValid; } /* · · · Validate a hex color and add # if missing · · @returns hex color with # or false · · · · · · · · · · */ function isValidHexColor(maybeValidHexColor) { if(maybeValidHexColor.substring(0,1) != '#') { maybeValidHexColor = '#' + maybeValidHexColor; } var validHexColor = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(maybeValidHexColor); if(validHexColor) { validHexColor = maybeValidHexColor; } return validHexColor; } /* · · · Change profile design · · @param obj: user object that should contain one, two or all of backgroundimage, backgroundcolor and linkcolor · false or empty string unsets the parameter to default · · · · · · · · · · */ function changeDesign(obj) { // if we're logged out and this is the front page, we use the default design if(!window.loggedIn && (window.currentStreamObject.name == 'public timeline' || window.currentStreamObject.name == 'public and external timeline')) { obj.backgroundimage = window.fullUrlToThisQvitterApp + window.siteBackground; obj.backgroundcolor = window.defaultBackgroundColor; obj.linkcolor = window.defaultLinkColor; } // if no object is defined, abort if(typeof obj == 'undefined') { return false; } // remember the design for this stream if(typeof window.oldStreamsDesigns[window.currentStreamObject.nickname] == 'undefined') { window.oldStreamsDesigns[window.currentStreamObject.nickname] = new Object(); } // change design elements if(typeof obj.backgroundimage != 'undefined') { if(obj.backgroundimage === false || obj.backgroundimage == '') { $('body').css('background-image','url(\'\')'); } else if(obj.backgroundimage.length > 4) { $('body').css('background-image','url(\'' + obj.backgroundimage + '\')'); } window.oldStreamsDesigns[window.currentStreamObject.nickname].backgroundimage = obj.backgroundimage; } if(typeof obj.backgroundcolor != 'undefined') { if(obj.backgroundcolor === false || obj.backgroundcolor == '') { obj.backgroundcolor = window.defaultBackgroundColor; } changeBackgroundColor(obj.backgroundcolor); window.oldStreamsDesigns[window.currentStreamObject.nickname].backgroundcolor = obj.backgroundcolor; } if(typeof obj.linkcolor != 'undefined') { if(obj.linkcolor === false || obj.linkcolor == '') { obj.linkcolor = window.defaultLinkColor; } changeLinkColor(obj.linkcolor); window.oldStreamsDesigns[window.currentStreamObject.nickname].linkcolor = obj.linkcolor; } } // create object to remember designs on page load window.oldStreamsDesigns = new Object(); /* · · · Change background color · · @param newLinkColor: hex value with or without # · · · · · · · · · · */ function changeBackgroundColor(newBackgroundColor) { // check hex value var validHexColor = isValidHexColor(newBackgroundColor); if(!validHexColor) { console.log('invalid hex value for backgroundcolor: ' + newBackgroundColor); return false; } $('body').css('background-color',validHexColor); } /* · · · Change link color · · @param newLinkColor: hex value with or without # · · · · · · · · · · */ function changeLinkColor(newLinkColor) { // check hex value var validHexColor = isValidHexColor(newLinkColor); if(!validHexColor) { console.log('invalid hex value for linkcolor: ' + newLinkColor); return false; } var lighterColor08 = blendRGBColors(hex2rgb(validHexColor),'rgb(255,255,255)',0.8); var lighterColor06 = blendRGBColors(hex2rgb(validHexColor),'rgb(255,255,255)',0.6) var headStyle = $('#dynamic-styles').children('style'); var headStyleText = headStyle.text(); headStyleText = replaceFromStringEndToStringStart(headStyleText,'/*COLORSTART*/','/*COLOREND*/',validHexColor); headStyleText = replaceFromStringEndToStringStart(headStyleText,'/*BACKGROUNDCOLORSTART*/','/*BACKGROUNDCOLOREND*/',validHexColor); headStyleText = replaceFromStringEndToStringStart(headStyleText,'/*BORDERCOLORSTART*/','/*BORDERCOLOREND*/',validHexColor); headStyleText = replaceFromStringEndToStringStart(headStyleText,'/*LIGHTERBACKGROUNDCOLORSTART*/','/*LIGHTERBACKGROUNDCOLOREND*/',lighterColor08); headStyleText = replaceFromStringEndToStringStart(headStyleText,'/*LIGHTERBORDERCOLORSTART*/','/*LIGHTERBORDERCOLOREND*/',lighterColor06); headStyleText = replaceFromStringEndToStringStart(headStyleText,'/*LIGHTERBORDERBOTTOMCOLORSTART*/','/*LIGHTERBORDERBOTTOMCOLOREND*/',lighterColor08); headStyle.text(headStyleText); } function replaceFromStringEndToStringStart(string,fromStringEnd,toStringStart,withString) { return string.substring(0,string.indexOf(fromStringEnd)+fromStringEnd.length) + withString + string.substring(string.indexOf(toStringStart)); } function blendRGBColors(c0, c1, p) { var f=c0.split(","),t=c1.split(","),R=parseInt(f[0].slice(4)),G=parseInt(f[1]),B=parseInt(f[2]); 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)+")"; } function hex2rgb(hexStr){ // note: hexStr should be #rrggbb var hex = parseInt(hexStr.substring(1), 16); var r = (hex & 0xff0000) >> 16; var g = (hex & 0x00ff00) >> 8; var b = hex & 0x0000ff; return 'rgb(' + r + ',' + g + ',' + b + ')'; } /* · · · Right-to-left language detection ').append(s); var $queetText = $('
').append($streamItem.find('.queet-text').html()); // create an jquery object var $a = $queetText.find('a'); $a.remove(); // remove links var $vcard = $queetText.find('.vcard'); $vcard.remove(); // remove users, groups var $hcard = $queetText.find('.h-card'); $hcard.remove(); // remove users, groups var $tag = $queetText.find('.tag'); $tag.remove(); // remove tags if($queetText.find('.rtl').length>0) { $queetText.html($queetText.find('.rtl').html()); } // remove rtl container if there is one // remove chars we're not interested in $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, '')); // count ltr and rtl chars var ltrChars = 'A-Za-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02B8\u0300-\u0590\u0800-\u1FFF'+'\u2C00-\uFB1C\uFDFE-\uFE6F\uFEFD-\uFFFF', rtlChars = '\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC', rtlDirCheck = new RegExp('^[^'+ltrChars+']*['+rtlChars+']'), RTLnum = 0, LTRnum = 0, RTLorLTR = $queetText.html(); for (var i = 0, len = RTLorLTR.length; i < len; i++) { if(rtlDirCheck.test(RTLorLTR[i])) { RTLnum++; } else { LTRnum++; } } // if there are more rtl chars than ltr // or if no chars (that we are interested, but body is set to rtl) if(RTLnum > LTRnum || ($queetText.html().length==0 && $('body').hasClass('rtl'))) { $streamItem.children('.stream-item').children('.queet').addClass('rtl'); } else { // for ltr languages we move @, ! and # to inside (only because it looks better) prependCharIfItDoesntAlreadyExist($streamItem.find('.queet-text').find('.h-card.mention'),'@'); prependCharIfItDoesntAlreadyExist($streamItem.find('.queet-text').find('.h-card.group'),'!'); prependCharIfItDoesntAlreadyExist($streamItem.find('.queet-text').find('.vcard .fn.nickname:not(.group)'),'@'); // very old style prependCharIfItDoesntAlreadyExist($streamItem.find('.queet-text').find('.vcard .nickname.mention:not(.fn)'),'@'); // old style prependCharIfItDoesntAlreadyExist($streamItem.find('.queet-text').find('.vcard .nickname.group'),'!'); // old style prependCharIfItDoesntAlreadyExist($streamItem.find('.queet-text').find('a[rel="tag"]'),'#'); } // we remove @, ! and #, they are added as pseudo elements, or have been moved to the inside return $streamItem.html().replace(/@/gi,'').replace(/!/gi,'').replace(/#/gi,''); } function prependCharIfItDoesntAlreadyExist(jQueryObject,char) { if(jQueryObject.text().substring(0,1) != char) { jQueryObject.prepend(char); } } /* · · · Takes twitter style dates and converts them · · @param tdate: date in the form of e.g. 'Mon Aug 05 16:30:22 +0200 2013' · · @return user friendly dates ..M_ · W · Needs global language object window.sL to be populated · · · · · · · · · · · · · · */ function parseTwitterDate(tdate) { var month_names = new Array (); month_names[month_names.length] = window.sL.shortmonthsJanuary; month_names[month_names.length] = window.sL.shortmonthsFebruary month_names[month_names.length] = window.sL.shortmonthsMars month_names[month_names.length] = window.sL.shortmonthsApril month_names[month_names.length] = window.sL.shortmonthsMay month_names[month_names.length] = window.sL.shortmonthsJune month_names[month_names.length] = window.sL.shortmonthsJuly month_names[month_names.length] = window.sL.shortmonthsAugust month_names[month_names.length] = window.sL.shortmonthsSeptember month_names[month_names.length] = window.sL.shortmonthsOctober month_names[month_names.length] = window.sL.shortmonthsNovember month_names[month_names.length] = window.sL.shortmonthsDecember var system_date = parseDate(tdate); var user_date = new Date(); var diff = Math.floor((user_date - system_date) / 1000); if (diff <= 10) {return window.sL.now;} if (diff < 60) {return window.sL.shortDateFormatSeconds.replace('{seconds}',Math.round(diff/10)*10);} if (diff <= 3540) {return window.sL.shortDateFormatMinutes.replace('{minutes}',Math.round(diff / 60));} if (diff <= 86400) {return window.sL.shortDateFormatHours.replace('{hours}',Math.round(diff / 3600));} if (diff <= 31536000) {return window.sL.shortDateFormatDate.replace('{day}',system_date.getDate()).replace('{month}',month_names[system_date.getMonth()]);} if (diff > 31536000) {return window.sL.shortDateFormatDateAndY.replace('{day}',system_date.getDate()).replace('{month}',month_names[system_date.getMonth()]).replace('{year}',system_date.getFullYear());} return system_date; } function parseTwitterLongDate(tdate) { var month_names = new Array (); month_names[month_names.length] = window.sL.longmonthsJanuary; month_names[month_names.length] = window.sL.longmonthsFebruary month_names[month_names.length] = window.sL.longmonthsMars month_names[month_names.length] = window.sL.longmonthsApril month_names[month_names.length] = window.sL.longmonthsMay month_names[month_names.length] = window.sL.longmonthsJune month_names[month_names.length] = window.sL.longmonthsJuly month_names[month_names.length] = window.sL.longmonthsAugust month_names[month_names.length] = window.sL.longmonthsSeptember month_names[month_names.length] = window.sL.longmonthsOctober month_names[month_names.length] = window.sL.longmonthsNovember month_names[month_names.length] = window.sL.longmonthsDecember var system_date = parseDate(tdate); var hours = system_date.getHours(); var minutes = ('0'+system_date.getMinutes()).slice(-2); var ampm = hours >= 12 ? 'pm' : 'am'; var time24hours = hours + ':' + minutes; var time12hours = hours % 12; time12hours = time12hours ? time12hours : 12; // the hour '0' should be '12' if(ampm == 'am') { time12hours = window.sL.time12am.replace('{time}',time12hours + ':' + minutes);} else { time12hours = window.sL.time12pm.replace('{time}',time12hours + ':' + minutes); } 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()); } function timestampToTwitterDate(timestamp) { var a = new Date(timestamp*1000); var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; var days = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat']; var day = days[a.getUTCDay()]; var year = a.getUTCFullYear(); var month = months[a.getUTCMonth()]; var date = (a.getUTCDate()<10?'0':'')+a.getUTCDate(); var hour = (a.getUTCHours()<10?'0':'')+a.getUTCHours(); var min = (a.getUTCMinutes()<10?'0':'')+a.getUTCMinutes(); var sec = (a.getUTCSeconds()<10?'0':'')+a.getUTCSeconds(); return day+' '+month+' '+date+' '+hour+':'+min+':'+sec+' +0000 '+year; } function parseDate(str) { if(typeof str != 'undefined') { var v=str.split(' '); return new Date(Date.parse(v[1]+" "+v[2]+", "+v[5]+" "+v[3]+" "+v[4])); } } /* · · · If we want to make sure we have empty arrays, not empty objects · · · · · · · · · · · */ function convertEmptyObjectToEmptyArray(data) { // empty object? return empty array instead... if($.isEmptyObject(data)) { return []; } // leave data unchanged if we don't recognize it else { return data; } } /* · · · Functions to show and remove the spinner · · · · · · · · · · · · · */ function display_spinner(parent) { if($('.loader').length<1) { if(typeof parent == 'undefined') { $('.global-nav').removeClass('show-logo'); var parent = 'body'; } $(parent).prepend('\
\ \ \ \ \ \ \
\ '); } } function remove_spinner() { $('.loader').remove(); $('.global-nav').addClass('show-logo'); } /* · · · Converts ...-attachment-links to spans · · (Attachments are loaded when queets expand) · · · · · · · · · · · · · · · · · · */ function convertAttachmentMoreHref() { $('a.attachment.more').each(function() { if(typeof $(this).attr('href') != 'undefined') { var attachment_href = $(this).attr('href'); var attachment_id = attachment_href.substr((~-attachment_href.lastIndexOf("/") >>> 0) + 2); if(attachment_id.length>0) { $(this).replaceWith($('')); } } }); } /* · · · Saves the user's bookmarks to the server · · · · · · · · · · · · · · */ function saveAllBookmarks() { var i=0; var bookmarkContainer = new Object(); $.each($('#bookmark-container .stream-selection'), function(key,obj) { bookmarkContainer[i] = new Object(); bookmarkContainer[i].dataStreamHref = $(obj).attr('href'); bookmarkContainer[i].dataStreamHeader = $(obj).text(); i++; }); postUpdateBookmarks(bookmarkContainer); $('#bookmark-container').sortable({delay: 100}); $('#bookmark-container').disableSelection(); } /* · · · Append all bookmarks to the bookmark container · · · · · · · · · · · · · · */ function appendAllBookmarks(bookmarkContainer) { if(typeof bookmarkContainer != 'undefined' && bookmarkContainer) { $('#bookmark-container').html(''); var bookmarkContainerParsed = JSON.parse(bookmarkContainer); $.each(bookmarkContainerParsed, function(key,obj) { $('#bookmark-container').append('
' + obj.dataStreamHeader + ''); }); } $('#bookmark-container').sortable({delay: 100}); $('#bookmark-container').disableSelection(); } /* · · · Change the header of an item in the history container by href attribute · · · · · · · · · · · · · · */ function updateHistoryContainerItemByHref(href, streamHeader) { $('#history-container .stream-selection[href="' + href + '"]').html(streamHeader + ''); updateHistoryLocalStorage(); } /* · · · Remove items in the history container by href attribute · · · · · · · · · · · · · · */ function removeHistoryContainerItemByHref(href) { $('#history-container .stream-selection[href="' + href + '"]').remove(); updateHistoryLocalStorage(); } /* · · · Updates the browsing history local storage · · · · · · · · · · · · · · */ function updateHistoryLocalStorage() { if(localStorageIsEnabled()) { var i=0; var historyContainer = new Object(); $.each($('#history-container .stream-selection'), function(key,obj) { historyContainer[i] = new Object(); historyContainer[i].dataStreamHref = $(obj).attr('href'); historyContainer[i].dataStreamHeader = $(obj).text(); i++; }); localStorageObjectCache_STORE('browsingHistory', window.loggedIn.screen_name,historyContainer); if($('#history-container .stream-selection').length==0) { $('#history-container').css('display','none'); } else { $('#history-container').css('display','block'); } } } /* · · · Loads history from local storage to menu · · · · · · · · · · · · · · */ function loadHistoryFromLocalStorage() { if(localStorageIsEnabled()) { var cacheData = localStorageObjectCache_GET('browsingHistory', window.loggedIn.screen_name); if(cacheData) { $('#history-container').css('display','block'); $('#history-container').html(''); $.each(cacheData, function(key,obj) { var streamHeader = replaceHtmlSpecialChars(obj.dataStreamHeader); // because we're pulling the header with jQuery.text() before saving in localstorage, which unescapes our escaped html $('#history-container').append('' + streamHeader + ''); }); } updateHistoryLocalStorage(); } } /* · · · Does stream need a ? or a & · · · · · · · · · · · · · · */ function qOrAmp(stream) { if(stream.substr(-5) == '.json') { return '?'; } else { return '&'; } } /* · · · Count chars in queet box · · @param src: the queetbox's value · @param trgt: the counter · @param btn: the button · · · · · · · · · · · · · · */ function countCharsInQueetBox(src,trgt,btn) { // count linebreaks by converting them to spaces var $src_txt = $('
').append(src.html().replace(/
/g,' ')); $src_txt = $('
').append($.trim($src_txt.text().replace(/\n/g,'').replace(/^\s+|\s+$/g, ''))); var numchars = ($src_txt.text()).length; // check for long urls and disable/enable url shorten button if present var longurls = 0; $.each(src.siblings('.syntax-middle').find('span.url'),function(key,obj){ if($.trim($(obj).html().replace(/ /gi,'').replace(/
/gi,'')).length > 20) { longurls++; } }); if(longurls>0) src.siblings('.queet-toolbar').find('button.shorten').removeClass('disabled'); else src.siblings('.queet-toolbar').find('button.shorten').addClass('disabled'); // limited if(window.textLimit > 0) { trgt.html(window.textLimit - numchars); // activate/deactivare button if(numchars > 0 && numchars < window.textLimit+1) { btn.removeClass('disabled'); btn.addClass('enabled'); btn.removeClass('too-long'); // deactivate button if it's equal to the start text var queetBox = btn.closest('.inline-reply-queetbox').children('.queet-box-syntax'); if(typeof queetBox.attr('data-replies-text') != 'undefined') { var $startText = $('
').append(decodeURIComponent(queetBox.attr('data-replies-text'))); if($.trim($startText.text()) == $.trim($src_txt.text())) { btn.removeClass('enabled'); btn.addClass('disabled'); } } } else if(numchars > window.textLimit){ btn.removeClass('enabled'); btn.addClass('disabled'); btn.addClass('too-long'); } else { btn.removeClass('enabled'); btn.addClass('disabled'); btn.removeClass('too-long'); } // counter color if((window.textLimit-numchars) < 0) { trgt.css('color','#D40D12'); } else { trgt.removeAttr('style'); } } // unlimited else { if(numchars > 0) { btn.removeClass('disabled'); btn.addClass('enabled'); } else { btn.removeClass('enabled'); btn.addClass('disabled'); } } } /* · · · Prefill the queet box with cached text, if there is any in an attribute · · @param queetBox: jQuery object for the queet box · · · · · · · · · · · · · · */ function maybePrefillQueetBoxWithCachedText(queetBox) { var cachedText = decodeURIComponent(queetBox.attr('data-cached-text')); var cachedTextText = $('
').html(cachedText).text(); if(cachedText != 'undefined' && cachedText != 'false') { queetBox.click(); queetBox.html(cachedText); setSelectionRange(queetBox[0], cachedTextText.length, cachedTextText.length); queetBox.trigger('input'); } } /* · · · Remember my scroll position · · @param obj: jQuery object which position we want to remember · @param id: id for position to remember · @param offset: we might want to offset our remembered scroll, e.g. when stream-item gets margin after expand · · · · · · · · · · · · · · */ function rememberMyScrollPos(obj,id,offset) { if(typeof offset == 'undefined') { var offset = 0; } if(typeof window.scrollpositions == 'undefined') { window.scrollpositions = new Object();} window.scrollpositions[id] = obj.offset().top - $(window).scrollTop() - offset; } /* · · · Go back to my scroll po · · @param obj: jQuery object to put in the remebered position · @param id: id for remembered position · @param animate: if we want to animate the scroll · @param callback: function to run when animation stops · · · · · · · · · · · · · · */ function backToMyScrollPos(obj,id,animate,callback) { var pos = obj.offset().top-window.scrollpositions[id]; if(animate) { if(animate == 'animate' || animate === true) { animate = 1000; } if(typeof callback !== 'undefined'){ $('html, body').animate({ scrollTop: pos}, animate, 'swing',function(){ callback(); }); } else { $('html, body').animate({ scrollTop: pos }, animate, 'swing'); } } else { $('html, body').scrollTop(pos); } } /* · · · Scroll to a stream item · · @param streamItem: jQuery object to scroll to · · · · · · · · · · · · · · */ function scrollToQueet(streamItem) { var streamItemPos = streamItem.offset().top; var windowHeight = $(window).height(); var streamItemHeight = streamItem.outerHeight(); // console.log(streamItemHeight); // console.log(windowHeight); var newScrollPos = Math.round(streamItemPos - windowHeight/2 + streamItemHeight/2); $('html, body').scrollTop(newScrollPos); } /* · · · Clean up user object, remove null etc · · · · · · · · · · · · · · */ function cleanUpUserObject(data) { data.name = data.name || ''; data.profile_image_url = data.profile_image_url || ''; data.profile_image_url_profile_size = data.profile_image_url_profile_size || ''; data.profile_image_url_original = data.profile_image_url_original || ''; data.screen_name = data.screen_name || ''; data.description = data.description || ''; data.location = data.location || ''; data.url = data.url || ''; data.statusnet_profile_url = data.statusnet_profile_url || ''; data.statuses_count = data.statuses_count || 0; data.followers_count = data.followers_count || 0; data.groups_count = data.groups_count || 0; data.friends_count = data.friends_count || 0; return data; } /* · · · outerHTML · · · · · · · · · · · · · · */ jQuery.fn.outerHTML = function(s) { return s ? this.before(s).remove() : jQuery("

").append(this.eq(0).clone()).html(); }; /* · · · Sort divs by attribute descending · · · · · · · · · · · · · · */ jQuery.fn.sortDivsByAttrDesc = function sortDivsByAttrDesc(attr) { $("> div", this[0]).sort(dec_sort).appendTo(this[0]); function dec_sort(a, b){ return parseInt($(b).attr(attr),10) > parseInt($(a).attr(attr),10) ? 1 : -1; } } /* · · · Stuff to get and set selection/caret in contenteditables · · · · · · · · · · · · · · */ function getSelectionInElement(element) { var caretOffset = Array(0,0); var doc = element.ownerDocument || element.document; var win = doc.defaultView || doc.parentWindow; var sel; var range = win.getSelection().getRangeAt(0); var preCaretRangeEnd = range.cloneRange(); preCaretRangeEnd.selectNodeContents(element); preCaretRangeEnd.setEnd(range.endContainer, range.endOffset); caretOffset[1] = preCaretRangeEnd.toString().length; var preCaretRangeStart = range.cloneRange(); preCaretRangeStart.selectNodeContents(element); preCaretRangeStart.setEnd(range.startContainer, range.startOffset); caretOffset[0] = preCaretRangeStart.toString().length; return caretOffset; } function getTextNodesIn(node) { var textNodes = []; if (node.nodeType == 3) { textNodes.push(node); } else { var children = node.childNodes; for (var i = 0, len = children.length; i < len; ++i) { textNodes.push.apply(textNodes, getTextNodesIn(children[i])); } } return textNodes; } function setSelectionRange(el, start, end) { if (document.createRange && window.getSelection) { var range = document.createRange(); range.selectNodeContents(el); var textNodes = getTextNodesIn(el); var foundStart = false; var charCount = 0, endCharCount; for (var i = 0, textNode; textNode = textNodes[i++]; ) { endCharCount = charCount + textNode.length; if(endCharCount == start && endCharCount == end) { endCharCount = endCharCount+1; } if (!foundStart && start >= charCount && (start < endCharCount || (start == endCharCount && i < textNodes.length))) { range.setStart(textNode, start - charCount); foundStart = true; } if (foundStart && end <= endCharCount) { range.setEnd(textNode, end - charCount); break; } charCount = endCharCount; } var sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range); } else if (document.selection && document.body.createTextRange) { var textRange = document.body.createTextRange(); textRange.moveToElementText(el); textRange.collapse(true); textRange.moveEnd("character", end); textRange.moveStart("character", start); textRange.select(); } } function createRangeFromCharacterIndices(containerEl, start, end) { var charIndex = 0, range = document.createRange(), foundStart = false, stop = {}; range.setStart(containerEl, 0); range.collapse(true); function traverseTextNodes(node) { if (node.nodeType == 3) { var nextCharIndex = charIndex + node.length; if (!foundStart && start >= charIndex && start <= nextCharIndex) { range.setStart(node, start - charIndex); foundStart = true; } if (foundStart && end >= charIndex && end <= nextCharIndex) { range.setEnd(node, end - charIndex); throw stop; } charIndex = nextCharIndex; } else { for (var i = 0, len = node.childNodes.length; i < len; ++i) { traverseTextNodes(node.childNodes[i]); } } } try { traverseTextNodes(containerEl); } catch (ex) { if (ex == stop) { return range; } else { throw ex; } } } function deleteBetweenCharacterIndices(el, from, to) { var range = createRangeFromCharacterIndices(el, from, to); if(typeof range != 'undefined') { range.deleteContents(); } } /* · · · Shorten urls in a queet-box · · · · · · · · · · · · · · */ function shortenUrlsInBox(shortenButton) { shortenButton.addClass('disabled'); $.each(shortenButton.parent().parent().siblings('.syntax-middle').find('span.url'),function(key,obj){ var url = $.trim($(obj).text()); display_spinner(); $.ajax({ url: window.urlShortenerAPIURL + '?format=' + window.urlshortenerFormat + '&action=shorturl&signature=' + window.urlShortenerSignature + '&url=' + encodeURIComponent(url), type: "GET", dataType: window.urlshortenerFormat, success: function(data) { if(typeof data.shorturl != 'undefined') { shortenButton.closest('.queet-toolbar').siblings('.upload-image-container').children('img[data-shorturl="' + data.url.url + '"]').attr('data-shorturl',data.shorturl); shortenButton.parent().parent().siblings('.queet-box-syntax').html(shortenButton.parent().parent().siblings('.queet-box-syntax').html().replace($('

').text(data.url.url).html(), data.shorturl)); shortenButton.parent().parent().siblings('.queet-box-syntax').trigger('keyup'); shortenButton.addClass('disabled'); // make sure the button is disabled right after } remove_spinner(); }, error: function(data) { console.log(data); remove_spinner(); } }); }); } /* · · · Youtube ID from Youtube URL · · · · · · · · · · · · · · */ function youTubeIDFromYouTubeURL(url) { 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); } /* · · · Youtube embed link from youtube url · · · · · · · · · · · · · · */ function youTubeEmbedLinkFromURL(url, autoplay) { // get start time hash var l = document.createElement("a"); l.href = url; var addStart = ''; if(l.hash.substring(0,3) == '#t=') { addStart = '&start=' + l.hash.substring(3); } var addAutoplay = ''; if(typeof autoplay != 'undefined' && autoplay === true) { addAutoplay = '&autoplay=1'; } return '//www.youtube.com/embed/' + youTubeIDFromYouTubeURL(url) + '?enablejsapi=1&version=3&playerapiid=ytplayer' + addStart + addAutoplay; } /* · · · Vimeo ID from Vimeo URL · · · · · · · · · · · · · · */ function vimeoIDFromVimeoURL(url) { id = url.replace('http://vimeo.com/','').replace('https://vimeo.com/',''); if(id.indexOf('#') > -1) { id = id.substring(0,id.indexOf('#')); } return id; } /* · · · Vimeo embed link from vimeo url · · · · · · · · · · · · · · */ function vimeoEmbedLinkFromURL(url, autoplay) { // get start time hash var l = document.createElement("a"); l.href = url; var addStart = ''; if(l.hash.substring(0,3) == '#t=') { addStart = l.hash; } var addAutoplay = '&autoplay=0'; if(typeof autoplay != 'undefined' && autoplay === true) { addAutoplay = '&autoplay=1'; } return 'https://player.vimeo.com/video/' + vimeoIDFromVimeoURL(url) + '?api=1' + addAutoplay + addStart; } /* · · · CSS class name from URL · · · · · · · · · · · · · · */ function CSSclassNameByHostFromURL(url) { var host = getHost(url); if(host.indexOf('www.') === 0) { host = host.substring(4); } host = host.toLowerCase().replace(/\./g, "-"); host = host.replace(/[^a-zA-Z0-9-]+/g, "_"); if(host == 'youtu-be') { host = 'youtube-com'; } return 'host-' + host; } /* · · · String similarity · · @params string1, string2: · @returns (int) percent similarity · · · · · · · · · · · · · · */ function stringSimilarity(string1, string2) { if(typeof string1 != 'string' || typeof string2 != 'string') { return 0; } // trim and strip html tags string1 = $('
').html($.trim(string1)).text(); string2 = $('
').html($.trim(string2)).text(); var longestStringLength = string1.length; if(string2.length>string1.length) { longestStringLength = string2.length; } var distanceArray = levenshteinenator(string1, string2); var distance = distanceArray[distanceArray.length-1][distanceArray[distanceArray.length-1].length-1]; var percentSimilarity = 100-Math.round(distance/longestStringLength*100); return percentSimilarity; } // from http://andrew.hedges.name/experiments/levenshtein/ var levenshteinenator = (function () { /** * @param String a * @param String b * @return Array */ function levenshteinenator(a, b) { var cost; var m = a.length; var n = b.length; // make sure a.length >= b.length to use O(min(n,m)) space, whatever that is if (m < n) { var c = a; a = b; b = c; var o = m; m = n; n = o; } var r = []; r[0] = []; for (var c = 0; c < n + 1; ++c) { r[0][c] = c; } for (var i = 1; i < m + 1; ++i) { r[i] = []; r[i][0] = i; for ( var j = 1; j < n + 1; ++j ) { cost = a.charAt( i - 1 ) === b.charAt( j - 1 ) ? 0 : 1; r[i][j] = minimator( r[i-1][j] + 1, r[i][j-1] + 1, r[i-1][j-1] + cost ); } } return r; } /** * Return the smallest of the three numbers passed in * @param Number x * @param Number y * @param Number z * @return Number */ function minimator(x, y, z) { if (x <= y && x <= z) return x; if (y <= x && y <= z) return y; return z; } return levenshteinenator; }());