NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Tsu Helper // @namespace tsu-helper // @description Tsu script that adds a bunch of tweaks to make Tsu more user friendly. // @include http://*tsu.co* // @include https://*tsu.co* // @version 2.6 // @copyright 2014-2015 Armando Lüscher // @author Armando Lüscher // @oujs:author noplanman // @grant none // @homepageURL https://j.mp/tsu-helper // @supportURL https://j.mp/tsu-helper-issues // ==/UserScript== /** * For changelog see https://j.mp/tsu-helper-changelog */ /** * How nice of you to visit! I've tried to make this code as clean as possible with lots of * comments for everybody to learn from. * * Because that is what this life is about, to learn from each other and to grow together! * * If you have any questions, ideas, feature requests, (pretty much anything) about it, just ask :-) * * Simply visit the GitHub page here: https://j.mp/tsu-helper-issues and choose "New Issue". * I will then get back to you as soon as I can ;-) */ // Make sure we have jQuery loaded. if ( ! ( 'jQuery' in window ) ) { return false; } // Run everything as soon as the DOM is set up. jQuery( document ).ready(function( $ ) { // Display Debug options? (for public). var publicDebug = false; /** * Base64 library, just decoder: http://www.webtoolkit.info/javascript-base64.html * @param {string} e Base64 string to decode. */ function base64_decode(e){var t='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';var n='';var r,i,s;var o,u,a,f;var l=0;e=e.replace(/[^A-Za-z0-9\+\/\=]/g,'');while(l<e.length){o=t.indexOf(e.charAt(l++));u=t.indexOf(e.charAt(l++));a=t.indexOf(e.charAt(l++));f=t.indexOf(e.charAt(l++));r=o<<2|u>>4;i=(u&15)<<4|a>>2;s=(a&3)<<6|f;n=n+String.fromCharCode(r);if(a!=64){n=n+String.fromCharCode(i);}if(f!=64){n=n+String.fromCharCode(s);}}return n;} // Check if a string starts with a certain string. 'function'!=typeof String.prototype.startsWith&&(String.prototype.startsWith=function(t){return this.slice(0,t.length)==t;}); // Check if a string ends with a certain string. 'function'!=typeof String.prototype.endsWith&&(String.prototype.endsWith=function(t){return this.slice(-t.length)==t;}); // Check if a string contains a certain string. 'function'!=typeof String.prototype.contains&&(String.prototype.contains=function(t){return this.indexOf(t)>=0;}); // Add stringify to jQuery (https://gist.github.com/chicagoworks/754454). jQuery.extend({stringify:function(r){if('JSON'in window)return JSON.stringify(r);var n=typeof r;if('object'!=n||null===r)return'string'==n&&(r='"'+r+'"'),String(r);var t,i,e=[],o=r&&r.constructor==Array;for(t in r)i=r[t],n=typeof i,r.hasOwnProperty(t)&&('string'==n?i='"'+i+'"':'object'==n&&null!==i&&(i=jQuery.stringify(i)),e.push((o?'':'"'+t+'":')+String(i)));return(o?'[':'{')+String(e)+(o?']':'}');}}); // Serialize form data to save settings (http://stackoverflow.com/questions/1184624/convert-form-data-to-js-object-with-jquery). $.fn.serializeObject=function(){var i={},e=this.serializeArray();return $.each(e,function(){void 0!==i[this.name]?(i[this.name].push||(i[this.name]=[i[this.name]]),i[this.name].push(this.value||"")):i[this.name]=this.value||"";}),i;}; /** * Like WP, return "selected" attribute text. * @param {string} val1 Value to compare. * @param {string} val2 Value to compare with. * @return {string} "Selected" text or nothing. */ function selected( val1, val2 ) { return ( val1 === val2 ) ? ' selected="selected"' : ''; } /** * Like WP, return "checked" attribute text. * @param {string} val1 Value to compare. * @param {string} val2 Value to compare with. * @return {string} "Checked" text or nothing. */ function checked( val1, val2 ) { // Compare to "true" by default. val2 = ( undefined === val2 ) ? true : val2; return ( val1 === val2 ) ? ' checked="checked"' : ''; } /** * All settings related methods and variables. */ var Settings = { // All available settings with default values. settingsDefault : { debugLevel : 'disabled', // Debugging level. (disabled,[l]og,[i]nfo,[w]arning,[e]rror) hideAds : false, // Hide all ads. quickMention : true, // Add quick mention links. emphasizeNRP : true, // Emphasize nested replies parents. checkSocial : true, // Check the social network sharing. checkMaxHM : true, // Check for maximum hashtags and mentions. notifReloaded : 10 // How many items to display on the Notifications popup (0=disabled) }, // Init with default settings on "load". settings : {}, // Name used for the settings cookie. cookieName : 'tsu-helper-settings', /** * Set default settings. */ setDefaults : function( $form ) { Settings.populateForm( $form, true ); }, /** * Load settings from cookie. */ load : function() { // Init with defaults and add all loaded settings. $.extend( true, Settings.settings, Settings.settingsDefault ); var savedJSON = $.cookie( Settings.cookieName ); if ( savedJSON ) { $.extend( Settings.settings, $.parseJSON( savedJSON ) ); } return Settings.settings; }, /** * Populate the passed form with the current settings. * @param {jQuery} $form The form to be populated. * @param {boolean} defaults Load the default values? */ populateForm : function( $form, defaults ) { if ( $form ) { for ( var setting in Settings.settings ) { if ( Settings.settings.hasOwnProperty( setting ) ) { var $input = $( '[name=' + setting + ']', $form ); var val = ( defaults ) ? Settings.settingsDefault[ setting ] : Settings.settings[ setting ]; if ( 'checkbox' === $input.attr( 'type' ) ) { $input.prop( 'checked', val ); } else { $input.val( val ); } } } } }, /** * Save settings to cookie. */ save : function( $form ) { // First save? if ( undefined === $.cookie( Settings.cookieName ) && ! confirm( 'Settings will be saved in a cookie. Ok?' ) ) { return false; } // If a form is passed, use those values. if ( $form ) { // Default to false and then get form settings. // This is necessary for checkbox inputs as they are assigned by checked state, not value. Settings.settings.hideAds = false; Settings.settings.quickMention = false; Settings.settings.emphasizeNRP = false; Settings.settings.checkSocial = false; Settings.settings.checkMaxHM = false; $.extend( Settings.settings, $form.serializeObject() ); } $.cookie( Settings.cookieName, $.stringify( Settings.settings ), { expires: 999, path: '/' } ); return true; } }; // Load settings from cookie. var settings = Settings.load(); /** * All updater and version related variables. */ var Updater = { // The local version. localVersion : 2.6, // The remote version (loaded in the "check" method). remoteVersion : null, // URL where to get the newest script. scriptURL : 'https://openuserjs.org/install/noplanman/Tsu_Helper.user.js', // Version details. versionAPIURL : 'https://api.github.com/repos/noplanman/tsu-helper/contents/VERSION', // Get the remote version on GitHub. init : function() { try { var response = $.ajax({ type: 'GET', url: Updater.versionAPIURL, async: false }).fail(function() { doLog( 'Couldn\'t get remote version number for Tsu Helper.', 'w' ); }).responseJSON; // Set the remote version. Updater.remoteVersion = parseFloat( base64_decode( response.content ) ); doLog( 'Versions: Local (' + Updater.localVersion + '), Remote (' + Updater.remoteVersion + ')', 'i' ); } catch( e ) { doLog( 'Couldn\'t get remote version number for Tsu Helper.', 'w' ); } }, /** * Is there a newer version available? * @return {Boolean} If there is a newer version available. */ hasUpdate : function() { return ( Updater.remoteVersion > Updater.localVersion ); } }; // Initialise the updater to fetch the remote version. Updater.init(); /** * TSU constants. */ var TSUConst = { // Define the maximum number of hashtags and mentions allowed. maxHashtags : 10, maxMentions : 10, // Texts for all possible notifications. kindsTexts : { friend_request_accepted : 'Friend Requests accepted', new_comment_on_post : 'Comments on your Posts', new_comment_on_post_you_commented_on : 'Comments on other Posts', new_follower : 'New Followers', new_like_on_post : 'Likes on your Posts', new_like_on_comment : 'Likes on your Comments', new_post_on_your_wall : 'Posts on your Wall', someone_mentioned_you_in_a_post : 'Mentioned in a Post or Comment', someone_shared_your_post : 'Shares of your Posts', donation_received : 'Donations received', someone_joined_your_network : 'Users who joined your Network' } }; /** * Page related things. */ var Page = { // The current page. current : '', /** * Get the current page to know which queries to load and observe and * also for special cases of how the Friends and Followers details are loaded. */ init : function() { doLog( 'Getting current page.', 'i' ); if ( $( 'body.newsfeed' ).length ) { Page.current = 'home'; // Home feed. } else if ( $( 'body.notifications.show' ).length || $( 'body.notifications.index' ).length ) { Page.current = 'notifications'; // Show notifications. } else if ( $( 'body.search_hashtag' ).length ) { Page.current = 'hashtag'; // Hashtag page. } else if ( $( 'body.profile.diary' ).length ) { Page.current = 'diary'; // Diary. } else if ( $( 'body.show_post' ).length ) { Page.current = 'post'; // Single post. } else if ( $( 'body.dashboard' ).length ) { Page.current = 'analytics'; // Analytics. Observer.queryToObserve = ''; // No observer necessary! } else if ( $( 'body.messages' ).length ) { Page.current = 'messages'; // Messages. Observer.queryToLoad = '.messages_content .message_box'; Observer.queryToObserve = '.messages_content'; } // Group queries to load. if ( Page.is( 'has-posts' ) ) { queryToLoad = '.comment'; // Add userlinks to query. Observer.queryToLoadFF = '.card .card_sub .info'; } Observer.queryToLoad += ',' + Observer.queryToLoadFF; doLog( 'Current page: ' + Page.current, 'i' ); }, /** * Check if the passed page is the current one. * @param {string} pages Comma seperated list of pages. * @return {boolean} If the current page is in the list. */ is : function( pages ) { // To make things easier, allow shortcuts. pages = pages.replace( /has-userlinks/g, 'has-posts messages' ); pages = pages.replace( /has-posts/g, 'home hashtag diary post' ); // Make an array. pages = pages.split( ' ' ); // Is the current page in the passed page list? for ( var i = pages.length - 1; i >= 0; i-- ) { if ( Page.current === pages[i] ) { return true; } } return false; } }; /** * Quick Mention links. */ var QM = { // The currently active textarea to insert the @mentions. $activeReplyTextArea : null, /** * Add text to the passed textarea input field. * @param {jQuery} $textArea jQuery object of the textarea input field. * @param {string} text Text to add. */ addTextToTextArea : function( $textArea, text ) { if ( $textArea ) { var textAreaText = $textArea.val(); var caretPos1 = $textArea[0].selectionStart; var caretPos2 = $textArea[0].selectionEnd; $textArea.val( textAreaText.substring( 0, caretPos1 ) + text + textAreaText.substring( caretPos2 ) ); $textArea[0].selectionStart = $textArea[0].selectionEnd = caretPos1 + text.length; $textArea.focus(); } }, /** * Add the @mention links to the replies. */ load : function() { // Make sure the setting is enabled and we're on the right page. if ( ! settings.quickMention || ! Page.is( 'has-posts' ) ) { return; } doLog( 'Adding Quick Mention links.', 'i' ); // Process all reply links to autofocus the reply textarea input field. $( '.load_more_post_comment_replies' ).not( '.th-qm-reply-processed' ).each(function() { var $replyLink = $( this ); $replyLink.click(function() { var $postComment = $replyLink.closest( '.post_comment' ); var $replyContainer = $postComment.siblings( '.comment_reply_container' ); var $textArea = $replyContainer.children( '.post_write_comment' ).find( '#comment_text' ); // This gets called before the "official" click, so the logic is inversed! // And delay everything a bit too, as it gets lazy-loaded. if ( $replyContainer.is( ':visible' ) ) { setTimeout(function() { // Only set the active textarea null if it's this one. if ( $textArea[0] === QM.$activeReplyTextArea[0] ) { QM.$activeReplyTextArea = null; // Hide all @ links. $( '.th-qm-reply' ).hide(); } }, 100); } else { setTimeout(function() { $postComment.find( '.th-qm-reply' ).show(); $textArea.focus(); }, 100); } }); $replyLink.addClass( 'th-qm-reply-processed' ); }); // Process all comment / reply textarea input fields to set themselves as active on focus. $( '.post_write_comment #comment_text' ).not( '.th-qm-textarea-processed' ).each(function() { $( this ).focusin( function() { QM.$activeReplyTextArea = $( this ); $( '.th-qm-active-input' ).removeClass( 'th-qm-active-input' ); QM.$activeReplyTextArea.closest( '.expandingText_parent' ).addClass( 'th-qm-active-input' ); }); $( this ).addClass( 'th-qm-textarea-processed' ); }); // Link for all comments. $( '.post_comment_header' ).not( '.th-qm-added' ).each(function() { var $head = $( this ); var $commentArea = $head.closest( '.post_comment' ); // Get just the last part of the href, the username. var hrefBits = $head.find( 'a' ).attr( 'href' ).split( '/' ); var atUsername = '@' + hrefBits[ hrefBits.length - 1 ] + ' '; var $mentionLink = $( '<a/>', { class : 'th-qm-reply', html : '@ +', title : 'Add ' + atUsername + 'to current reply.', click : function() { QM.addTextToTextArea( QM.$activeReplyTextArea, atUsername ); } }) .hide(); // Start hidden, as it will appear with the mouse over event. // Show / hide link on hover / blur if there is an active reply input selected. $commentArea.hover( function() { if ( QM.$activeReplyTextArea && QM.$activeReplyTextArea.length ) { $mentionLink.show(); } }, function() { $mentionLink.hide(); } ); $head.addClass( 'th-qm-added' ); // Position the @ link. var $profilePic = $head.find( '.post_profile_picture' ); var offset = $profilePic.position(); $mentionLink.offset({ top: offset.top + $profilePic.height(), left: offset.left }); $head.append( $mentionLink ); }); // Link for all textareas. $( '.post_write_comment' ).not( '.th-qm-added' ).each(function() { var $commentArea = $( this ); var $commentInput = $commentArea.find( '#comment_text' ); var $head = null; var linkElement = null; var isReply = $commentArea.hasClass( 'reply' ); // Is this a nested comment? Then use the previous reply as the username. if ( isReply ) { $head = $commentArea.closest( '.comment' ).find( '.post_comment .post_comment_header' ); linkElement = 'a'; } else { // Get the current post to determine the username. var $post = $commentArea.closest( '.post' ); // Defaults as if we have a shared post. $head = $post.find( '.share_header' ); linkElement = '.evac_user a'; // If it's not a share, get the post header. if ( 0 === $head.length ) { $head = $post.find( '.post_header' ); linkElement = '.post_header_pp a'; } } // Get just the last part of the href, the username. var hrefBits = $head.find( linkElement ).attr( 'href' ).split( '/' ); var atUsername = '@' + hrefBits[ hrefBits.length - 1 ] + ' '; var $mentionLink = $( '<a/>', { class : 'th-qm-comment', html : '@ >', title : 'Add ' + atUsername + 'to this ' + ( ( isReply ) ? 'reply.' : 'comment.' ), click : function() { QM.addTextToTextArea( $commentInput, atUsername ); } }) .hide(); // Start hidden, as it will appear with the mouse over event. // Show / hide link on hover / blur. $commentArea.hover( function() { $mentionLink.show(); }, function() { $mentionLink.hide(); } ); $commentArea.addClass( 'th-qm-added' ); $commentArea.find( '.post_profile_picture' ).parent().after( $mentionLink ); }); } }; /** * The MutationObserver to detect page changes. */ var Observer = { // The mutation observer object. observer : null, // The elements that we are observing. queryToObserve : 'body', // The query of objects that trigger the observer. queryToLoad : '', // The query of userlinks to look for. queryToLoadFF : '', /** * Start observing for DOM changes. */ init : function() { // Check if we can use the MutationObserver. if ( 'MutationObserver' in window ) { // Are we observing anything on this page? if ( '' === Observer.queryToObserve ) { return; } var toObserve = document.querySelector( Observer.queryToObserve ); if ( toObserve ) { doLog( 'Started Observer.', 'i' ); Observer.observer = new MutationObserver( function( mutations ) { function itemsInArray( needles, haystack ) { for ( var i = needles.length - 1; i >= 0; i-- ) { if ( $.inArray( needles[ i ], haystack ) > -1 ) { return true; } } return false; } // Helper to determine if added or removed nodes have a specific class. function mutationNodesHaveClass( mutation, classes ) { classes = classes.split( ' ' ); // Added nodes. for ( var ma = mutation.addedNodes.length - 1; ma >= 0; ma-- ) { var addedNode = mutation.addedNodes[ ma ]; // In case the node has no className (e.g. textnode), just ignore it. if ( 'className' in addedNode && 'string' === typeof addedNode.className && itemsInArray( addedNode.className.split( ' ' ), classes ) ) { return true; } } // Removed nodes. for ( var mr = mutation.removedNodes.length - 1; mr >= 0; mr-- ) { var removedNode = mutation.removedNodes[ mr ]; // In case the node has no className (e.g. textnode), just ignore it. if ( 'className' in removedNode && 'string' === typeof removedNode.className && itemsInArray( removedNode.className.split( ' ' ), classes ) ) { return true; } } return false; } doLog( mutations.length + ' DOM changes.' ); doLog( mutations ); // Only react to changes we're interested in. for ( var m = mutations.length - 1; m >= 0; m-- ) { var $hoverCard = $( '.tooltipster-user-profile' ); // Are we on a hover card? if ( $hoverCard.length && mutationNodesHaveClass( mutations[ m ], 'tooltipster-user-profile' ) ) { FFC.loadUserHoverCard( $hoverCard.find( '.card .card_sub .info' ) ); } // Run all functions responding to DOM updates. // When loading a card, only if it's not a hover card, as those get loaded above. if ( mutationNodesHaveClass( mutations[ m ], 'post comment message_content_feed message_box' ) || ( mutationNodesHaveClass( mutations[ m ], 'card' ) && $hoverCard.length === 0 ) ) { FFC.loadAll(); QM.load(); emphasizeNestedRepliesParents(); tweakMessagesPage(); } } }); // Observe child and subtree changes. Observer.observer.observe( toObserve, { childList: true, subtree: true }); } } else { // If we have no MutationObserver, use "waitForKeyElements" function. // Instead of using queryToObserve, we wait for the ones that need to be loaded, queryToLoad. $.getScript( 'https://gist.github.com/raw/2625891/waitForKeyElements.js', function() { doLog( 'Started Observer (waitForKeyElements).', 'i' ); // !! Specifically check for the correct page to prevent overhead !! if ( Page.is( 'has-userlinks' ) ) { waitForKeyElements( Observer.queryToLoad, FFC.loadAll() ); } if ( Page.is( 'has-posts' ) ) { waitForKeyElements( Observer.queryToLoad, QM.load ); waitForKeyElements( Observer.queryToLoad, emphasizeNestedRepliesParents ); } if ( Page.is( 'messages' ) ) { waitForKeyElements( Observer.queryToLoad, tweakMessagesPage ); } }); } } }; /** * Post related things. */ var Posting = { // Are we busy waiting for the popup to appear? waitingForPopup : false, /** * Initialise. */ init : function() { // Remind to post to FB and Twitter in case forgotten to click checkbox. $( '#create_post_form' ).submit(function( event ) { return Posting.postFormSubmit( $( this ), event ); }); // Set focus to message entry field on page load. if ( Page.is( 'home diary' ) ) { $( '#text' ).focus(); } // When using the "Create" or "Message" buttons, wait for the post input form. $( 'body' ).on( 'click', '.create_post_popup, .message_pop_up', function() { if ( ! Posting.waitingForPopup ) { if ( $( this ).hasClass( 'create_post_popup' ) ) { Posting.waitForPopup( 'post' ); } else if ( $( this ).hasClass( 'message_pop_up' ) ) { Posting.waitForPopup( 'message' ); } } }); // Auto-focus title entry field when adding title. $( 'body' ).on( 'click', '.create_post .options .add_title', function() { var $postForm = $( this ).closest( '#create_post_form' ); var $postTitle = $postForm.find( '#title' ); // Focus title or text field, depending if the title is being added or removed. if ( $postTitle.is( ':visible' ) ) { setTimeout( function() { $postForm.find( '#text' ).focus(); }, 50 ); } else { setTimeout( function() { $postTitle.focus(); }, 50 ); } }); // Auto-focus message entry field when adding/removing image. $( 'body' ).on( 'click', '.create_post .options .filebutton, .cancel_icon_createpost', function() { var $postText = $( this ).closest( '#create_post_form' ).find( '#text' ); setTimeout( function() { $postText.focus(); }, 50 ); }); /** * Open post by double clicking header (only on pages with posts). */ if ( ! Page.is( 'has-posts' ) ) { return; } $( 'body' ).on( 'dblclick', '.post_header_name, .share_header', function( event ) { var $post = $( this ).closest( '.post' ); var isShare = $post.find( '.share_header' ).length; var isOriginal = ! $( this ).hasClass( 'share_header' ); $post.find( '#post_link_dropdown a' ).each(function() { var linkText = $( this ).text().trim().toLowerCase(); if ( ( ! isShare && 'open' === linkText ) || ( ! isOriginal && 'open' === linkText ) || ( isOriginal && 'open original post' === linkText ) ) { var url = $( this ).attr( 'href' ); // If the shift key is pressed, open in new window / tab. if ( event.shiftKey ) { window.open( url, '_blank' ).focus(); } else { window.location = url; } return; } }); }); }, /** * Check for the maximum number of hashtags and mentions. * @param {string} message The message being posted. * @return {boolean} True = submit, False = cancel, Null = not too many */ checkMaximumHashtagsMentions : function( message ) { // Check if the setting is enabled. if ( ! settings.checkMaxHM ) { return null; } // Get number of hashtags and mentions in the message. var nrOfHashtags = message.split( '#' ).length - 1; doLog( nrOfHashtags + ' Hashtags found.' ); var nrOfMentions = message.split( '@' ).length - 1; doLog( nrOfMentions + ' Mentions found.' ); // If the limits aren't exeeded, just go on to post. if ( nrOfHashtags <= TSUConst.maxHashtags && nrOfMentions <= TSUConst.maxMentions ) { return null; } // Set up warning message. var warning = 'Limits may be exceeded, check your message!\nAre you sure you want to continue?\n'; if ( nrOfHashtags > TSUConst.maxHashtags ) { warning += '\n' + nrOfHashtags + ' #hashtags found. (Max. ' + TSUConst.maxHashtags + ')'; doLog( 'Too many hashtags found! (' + nrOfHashtags + ')', 'w' ); } if ( nrOfMentions > TSUConst.maxMentions ) { warning += '\n' + nrOfMentions + ' @mentions found. (Max. ' + TSUConst.maxMentions + ')'; doLog( 'Too many mentions found! (' + nrOfMentions + ')', 'w' ); } // Last chance to make sure about hashtags and mentions. return confirm( warning ); }, /** * Check if the social network sharing has been selected. * @param {jQuery} $form Form jQuery object of the form being submitted. * @return {boolean} True = submit, False = cancel, Null = all selected */ checkSocialNetworkSharing : function( $form ) { // Check if the setting is enabled. if ( ! settings.checkSocial ) { return null; } var share_facebook = null; var share_twitter = null; // Get all visible (connected) checkboxes. If any are not checked, show warning. $form.find( '.checkboxes_options_create_post input:visible' ).each(function() { switch ( $( this ).attr( 'id' ) ) { case 'facebook': share_facebook = $( this ).prop( 'checked' ); break; case 'twitter': share_twitter = $( this ).prop( 'checked' ); break; } }); // If no social network accounts are connected, just go on to post. if ( false !== share_facebook && false !== share_twitter ) { return null; } var post_to = 'OK = Post to Tsu'; // Share to facebook? if ( true === share_facebook ) { post_to += ', Facebook'; } // Share to twitter? if ( true === share_twitter ) { post_to += ', Twitter'; } // Last chance to enable sharing to social networks... return confirm( post_to + '\nCancel = Choose other social networks' ); }, /** * Called on form submit. * @param {jQuery} $form Form jQuery object of the form being submitted. * @param {event} event The form submit event. */ postFormSubmit : function( $form, event ) { // In case the post gets cancelled, make sure the message field is focused. var message = $form.find( '#text' ).focus().val(); var title = $form.find( '#title' ).val(); var hasPic = ( '' !== $form.find( '#create_post_pic_preview' ).text() ); // Make sure something was entered (title, text or image. // Check for the maximum number of hashtags and mentions, // and if the Social network sharing warning has been approved. if ( ( '' !== message || '' !== title || hasPic ) && false !== Posting.checkMaximumHashtagsMentions( message ) && false !== Posting.checkSocialNetworkSharing( $form ) ) { doLog( 'Post!' ); return; } /************************** * CANCEL FORM SUBMISSION! * **************************/ doLog( 'DONT Post!' ); // Prevent form post. event.preventDefault(); // Hide the loader wheel. $form.find( '.loading' ).hide(); // Make sure to enable the post button again. Give it some time, as Tsu internal script sets it to disabled. setTimeout(function(){ $form.find( '#create_post_button' ).removeAttr( 'disabled' ); }, 500 ); return false; }, /** * Wait for the fancybox popup to create a new post. */ waitForPopup : function( action ) { Posting.waitingForPopup = true; var formSelector; var inputSelector; switch ( action ) { case 'post' : formSelector = '.fancybox-wrap #create_post_form'; inputSelector = '#text'; break; case 'message' : formSelector = '.fancybox-wrap #new_message'; inputSelector = '#message_body'; break; } var $form = $( formSelector ); if ( $form.length ) { $form.find( inputSelector ).focus(); // Apply checks to posts only! if ( 'post' === action ) { $form.submit(function( event ) { return Posting.postFormSubmit( $( this ), event ); }); } Posting.waitingForPopup = false; return; } // Wait around for it longer... setTimeout( function() { Posting.waitForPopup( action ); }, 500 ); } }; /** * User object containing info for Friends and Followers counter. * @param {string|integer} userID Depending on the context, this is either the user id as a number or unique username / identifier. * @param {string} userName The user's full name. * @param {string} userUrl The url to the user profile page. */ function UserObject( userID, userName, userUrl ) { // Keep track if this user object has already finished loading. this.finished = false; this.userID = userID; this.userName = userName; this.userUrl = userUrl; // Add to all userObjects list. Users.userObjects[ userID ] = this; // Queue of user link spans to refresh once the user object has finished loading. this.userLinkSpans = []; doLog( '(' + userID + ':' + userName + ') New user loaded.' ); /** * Set the friends info. * @param {jQuery} $friendsLink The jQuery <a> object linking to the user's Friends page. * @param {[string} friendsUrl The URL to the user's Friends page. * @param {[string} friendsCount The user's number of friends. */ this.setFriendsInfo = function( $friendsLink, friendsUrl, friendsCount ) { this.$friendsLink = $friendsLink; this.friendsUrl = friendsUrl; this.friendsCount = friendsCount; }; /** * Set the followers info. * @param {jQuery} $followersLink The jQuery <a> object linking to the user's Followers page. * @param {string} followersUrl The URL to the user's Followers page. * @param {string} followersCount The user's number of Followers. */ this.setFollowersInfo = function( $followersLink, followersUrl, followersCount ) { this.$followersLink = $followersLink; this.followersUrl = followersUrl; this.followersCount = followersCount; }; /** * Return a clone of the Friends page link. * @return {jQuery} Friends page link. */ this.getFriendsLink = function() { return this.$friendsLink.clone(); }; /** * Return a clone of the Followers page link. * @return {jQuery} Followers page link. */ this.getFollowersLink = function() { return this.$followersLink.clone(); }; /** * Set this user object as finished loading. */ this.setFinished = function() { this.finished = true; doLog( '(id:' + this.userID + ') Finished loading.' ); }; /** * Is this user object already loaded? * @return {Boolean} */ this.isFinished = function() { return this.finished; }; /** * Add a user link span to the queue to be refreshed once the user object is loaded. * @param {jQuery} $userLinkSpan The user link span object. */ this.queueUserLinkSpan = function( $userLinkSpan ) { this.userLinkSpans.push( $userLinkSpan ); }; /** * Refresh the passed $userLinkSpan with the user details. * @param {jQuery} $userLinkSpan The <span> jQuery object to appent the details to. * @param {integer} tries The number of tries that have already been used to refresh the details. */ this.refresh = function( $userLinkSpan, tries ) { if ( undefined === tries || null === tries ) { tries = 0; } // If the maximum tries has been exeeded, return. if ( tries > FFC.maxTries ) { // Just remove the failed link span, maybe it will work on the next run. $userLinkSpan.remove(); // Remove all queued ones too to prevent infinite loader image. for ( var i = this.userLinkSpans.length - 1; i >= 0; i-- ) { this.userLinkSpans[ i ].remove(); } this.userLinkSpans = []; doLog( '(id:' + this.userID + ') Maximum tries exeeded!', 'w' ); return; } if ( this.isFinished() ) { // Add the user details after the user link. this.queueUserLinkSpan( $userLinkSpan ); // Update all listening user link spans. for ( var i = this.userLinkSpans.length - 1; i >= 0; i-- ) { this.userLinkSpans[ i ].empty().append( this.getFriendsLink(), this.getFollowersLink() ); } // Empty the queue, as there is no need to reload already loaded ones. this.userLinkSpans = []; doLog( '(' + this.userID + ':' + this.userName + ') Friends and Followers set.' ); } else { var t = this; setTimeout(function() { t.refresh( $userLinkSpan, tries + 1); }, 1000); } }; } /** * Friends and Followers counts manager. */ var FFC = { // Max number of tries to get friend and follower info (= nr of seconds). maxTries : 60, /** * Load a user link. * @param {jQuery} $userElement The element that contains the user link. * @param {boolean} onHoverCard Is this element a hover card? */ loadUserLink : function( $userElement, onHoverCard ) { // If this link has already been processed, skip it. if ( $userElement.hasClass( 'ffc-processed' ) ) { return; } // Set the "processed" flag to prevent loading the same link multiple times. $userElement.addClass( 'ffc-processed' ); // Because the user link is in a nested entry. var $userLink = $userElement.find( 'a:first' ); // If no link has been found, continue with the next one. Fail-safe. if ( 0 === $userLink.length ) { return; } // Add a new <span> element to the user link. var $userLinkSpan = $( '<span/>', { html: '<img class="th-ffc-loader-wheel" src="/assets/loader.gif" alt="Loading..." />', class: 'th-ffc-span' } ); $userLink.after( $userLinkSpan ); // Special case for these pages, to make it look nicer and fitting. if ( onHoverCard ) { $userLinkSpan.before( '<br class="th-ffc-br" />' ); } // Get the user info from the link. var userName = $userLink.text().trim(); var userUrl = $userLink.attr( 'href' ); // Extract the userID from the url. var userID = userUrl.split( '/' )[1]; // Check if the current user has already been loaded. var userObject = Users.getUserObject( userID, true ); // Add this span to the list that needs updating when completed. if ( userObject instanceof UserObject ) { // If this user has already finished loading, just update the span, else add it to the queue. if ( userObject.isFinished() ) { userObject.refresh( $userLinkSpan, 0 ); } else { userObject.queueUserLinkSpan( $userLinkSpan ); } return; } // Create a new UserObject and load it's data. userObject = new UserObject( userID, userName, userUrl ); // Load the numbers from the user profile page. setTimeout( function() { $.get( userUrl, function( response ) { // Get rid of all images first, no need to load those, then find the links. var $numbers = $( response.replace( /<img[^>]*>/g, '' ) ).find( '.profile_details .numbers a' ); // If the user doesn't exist, just remove the span. if ( 0 === $numbers.length ) { $userLinkSpan.remove(); return; } // Set up the Friends link. var $friends = $numbers.eq( 0 ); var friendsUrl = $friends.attr( 'href' ); var friendsCount = $friends.find( 'span' ).text(); var $friendsLink = $( '<a/>', { href: friendsUrl, html: friendsCount }); // Set up the Followers link. var $followers = $numbers.eq( 1 ); var followersUrl = $followers.attr( 'href' ); var followersCount = $followers.find( 'span' ).text(); var $followersLink = $( '<a/>', { href: followersUrl, html: followersCount }); // Add titles to pages without posts and not on hover cards. if ( ! onHoverCard && ! Page.is( 'has-posts' ) ) { $friendsLink.attr( 'title', 'Friends' ); $followersLink.attr( 'title', 'Followers' ); } // Add the Friends and Followers details, then refresh all userlink spans. userObject.setFriendsInfo( $friendsLink, friendsUrl, friendsCount ); userObject.setFollowersInfo( $followersLink, followersUrl, followersCount ); userObject.refresh( $userLinkSpan, 0 ); }) .always(function() { // Make sure to set the user as finished loading. Users.finishedLoading( userID ); }); }, 100 ); }, /** * Load the FF counts for a user's hover card. * @param {jQuery} $userHoverCard Hover card selector. */ loadUserHoverCard : function( $userHoverCard ) { var t = this; // As long as the hover tooltip exists but the card inside it doesn't, loop and wait till it's loaded. if ( $( '.tooltipster-user-profile' ).length && $userHoverCard.length === 0 ) { setTimeout(function(){ t.loadUserHoverCard( $( $userHoverCard.selector, $userHoverCard.context ) ); }, 500); return; } doLog( 'Start loading Friends and Followers (Hover Card).', 'i' ); FFC.loadUserLink( $userHoverCard, true ); }, /** * Load Friends and Followers * @param {boolean} clean Delete saved details and refetch all. */ loadAll : function( clean ) { if ( Page.is( 'has-posts' ) ) { return; } doLog( 'Start loading Friends and Followers.', 'i' ); // Find all users and process them. var $newUserLinks = $( Observer.queryToLoadFF ).not( '.ffc-processed' ); doLog( 'New user links found: ' + $newUserLinks.length ); // Load all userlinks. $newUserLinks.each(function() { var $userElement = $( this ); // Is this link on a tooltip hover card? var onHoverCard = ( $userElement.closest( '.tooltipster-base' ).length !== 0 ); FFC.loadUserLink( $userElement, onHoverCard ); }); } }; /** * Manage all users for the Friends and Followers counting. */ var Users = { userObjects : {}, getUserObject : function( userID, setLoading ) { if ( Users.userObjects.hasOwnProperty( userID ) ) { doLog( '(' + userID + ':' + Users.userObjects[ userID ].userName + ') Already loaded.' ); return Users.userObjects[ userID ]; } if ( setLoading ) { doLog( '(id:' + userID + ') Set to loading.' ); Users.userObjects[ userID ] = true; } doLog( '(id:' + userID + ') Not loaded yet.' ); return false; }, finishedLoading : function( userID ) { if ( Users.userObjects.hasOwnProperty( userID ) ) { Users.userObjects[ userID ].setFinished(); } } }; // Initialise after all variables are defined. Page.init(); Observer.init(); Posting.init(); // Add the required CSS rules. addCSS(); // Add the About (and Settings) window to the menu. addAboutWindow(); // As the observer can't detect any changes on static pages, run functions now. FFC.loadAll(); QM.load(); emphasizeNestedRepliesParents(); tweakMessagesPage(); // Load Notifications Reloaded? if ( settings.notifReloaded > 0 ) { notifications.urls.notifications = '/notifications/request/?count=' + settings.notifReloaded; notifications.get().success(function() { $.event.trigger( 'notificationsRender' ); }); } // Add Notifications Reloaded to the notifications page. if ( Page.is( 'notifications' ) ) { var $ajaxNRRequest = null; var $notificationsList = $( '#new_notifications_list' ); // Add the empty filter message var $emptyFilterDiv = $( '<div>No notifications match the selected filter. Coose a different one.</div>' ); // Select input to filter kinds. var $kindSelect = $( '<select/>', { 'id' : 'th-nr-nk-select', title : 'Filter by the kind of notification' }); // Select input to filter users. var $userSelect = $( '<select/>', { 'id' : 'th-nr-nu-select', title : 'Filter by user' }); // List the available count options. var selectCount = ''; [ 30, 50, 100, 200, 300, 400, 500 ].forEach(function( val ) { selectCount += '<option value="' + val + '">' + val + '</option>'; }); /** * Filter the current items according to the selected filters. * @param {string} which Either "kind" or "user". */ function filterNotifications( which ) { var $notificationItems = $notificationsList.find( '.notifications_item' ).show(); var kindVal = $kindSelect.val(); var userVal = $userSelect.val(); if ( undefined === which ) updateSelectFilters( $notificationItems ); // Filter kinds. if ( '' !== $kindSelect.val() ) { $notificationItems.not( '[data-kind="' + kindVal + '"]' ).hide(); } // Filter users. if ( '' !== $userSelect.val() ) { $notificationItems.not( '[data-user-id="' + userVal + '"]' ).hide(); } var $notificationItemsVisible = $notificationItems.filter( function() { return $( this ).find( '.new_notification:visible' ).length; } ); if ( $notificationItemsVisible.length ) { $emptyFilterDiv.hide(); } else { $emptyFilterDiv.show(); } // The other select filter, not the current one. var other = ( 'kind' === which ) ? 'user' : 'kind'; if ( '' === kindVal && '' === userVal ) { updateSelectFilters( $notificationItems, other ); } else { updateSelectFilters( $notificationItemsVisible, which ); } } /** * Update the entries and count values of the filter select fields. * @param {jQuery} $notificationItems List of notification items to take into account. * @param {string} which The filter that is being set ("kind" or "user"). */ function updateSelectFilters( $notificationItems, which ) { // Remember the last selections. var lastKindSelected = $kindSelect.val(); var lastUserSelected = $userSelect.val(); // The available items to populate the select fields. var availableKinds = {}; var availableUsers = {}; $notificationItems.each(function() { var $notificationItem = $( this ); // Remember all the available kinds and the number of occurrences. if ( availableKinds.hasOwnProperty( $notificationItem.attr( 'data-kind' ) ) ) { availableKinds[ $notificationItem.attr( 'data-kind' ) ]++; } else { availableKinds[ $notificationItem.attr( 'data-kind' ) ] = 1; } // Remember all the available users and the number of occurrences. if ( availableUsers.hasOwnProperty( $notificationItem.attr( 'data-user-id' ) ) ) { availableUsers[ $notificationItem.attr( 'data-user-id' ) ].count++; } else { availableUsers[ $notificationItem.attr( 'data-user-id' ) ] = { username : $notificationItem.attr( 'data-user-name' ), count : 1 }; } }); // Update the kinds if the "User" filter has been changed. if ( undefined === which || 'user' === which || '' === lastKindSelected ) { // List the available kinds, adding the number of occurances. $kindSelect.removeAttr( 'disabled' ).html( '<option value="">All Kinds</option>' ); for ( var key in availableKinds ) { if ( TSUConst.kindsTexts.hasOwnProperty( key ) && availableKinds.hasOwnProperty( key ) ) { $kindSelect.append( '<option value="' + key + '"' + selected( key, lastKindSelected ) + '>' + TSUConst.kindsTexts[ key ] + ' (' + availableKinds[ key ] + ')</option>' ); } } } // Update the users if the "Kind" filter has been changed. if ( undefined === which || 'kind' === which || '' === lastUserSelected ) { // List the available users, adding the number of occurances. $userSelect.removeAttr( 'disabled' ).html( '<option value="">All Users - (' + Object.keys( availableUsers ).length + ' Users)</option>' ); // Sort alphabetically. var availableUsersSorted = []; for ( var userID in availableUsers ) { availableUsersSorted.push( [ userID, availableUsers[ userID ].username.toLowerCase() ] ); } availableUsersSorted.sort( function( a, b ) { return ( a[1] > b[1] ) ? 1 : ( ( b[1] > a[1] ) ? -1 : 0 ); } ); availableUsersSorted.forEach(function( val ) { var userID = val[0]; if ( availableUsers.hasOwnProperty( userID ) ) { $userSelect.append( '<option value="' + userID + '"' + selected( userID, lastUserSelected ) + '>' + availableUsers[ userID ].username + ' (' + availableUsers[ userID ].count + ')</option>' ); } }); } } /** * Refresh the selected number of notifications. */ var reloadNotifications = function() { // If a request is already busy, cancel it and start the new one. if ( $ajaxNRRequest ) { $ajaxNRRequest.abort(); } doLog( 'Loading ' + $countSelect.val() + ' notifications.', 'i' ); // Show loader wheel. $notificationsList.html( '<img src="/assets/loader.gif" alt="Loading..." />' ); // Disable select inputs. $kindSelect.attr( 'disabled', 'disabled' ); $userSelect.attr( 'disabled', 'disabled' ); // Request the selected amount of notifications. $ajaxNRRequest = $.getJSON( '/notifications/request/?count=' + $countSelect.val(), function( data ) { // Clear the loader wheel. $notificationsList.empty(); // Make sure we have access to the notifications. if ( ! data.hasOwnProperty( 'notifications' ) ) { // Some error occured. $notificationsList.html( '<div>Error loading notifications, please try again later.</div>' ); return; } // No notifications. if ( 0 === data.notifications.length ) { $notificationsList.html( '<div>You don\'t have any notifications.</div>'); return; } // Append the notifications to the list. Function used is the one used by Tsu. $( data.notifications ).each(function( i, item ) { //var $notificationItem = $( window.notifications_fr._templates['new_comment_in_post']( var $notificationItem = $( window.notifications_fr._templates.new_comment_in_post( item.url, item.user, item.message, item.created_at_int )) .attr({ 'data-kind' : item.kind, 'data-user-id' : item.user.id, 'data-user-name' : item.user.first_name + ' ' + item.user.last_name }); $notificationsList.append( $notificationItem ); }); // Add the empty filter message. $notificationsList.append( $emptyFilterDiv ); // Filter the notifications to make sure that the previously selected filter gets reapplied. filterNotifications(); }) .fail(function() { $notificationsList.html( '<div>Error loading notifications, please try again later.</div>' ); }); }; var $countSelect = $( '<select/>', { 'id' : 'th-nc-select', html : selectCount }) .change( reloadNotifications ); $( '<div/>', { 'id' : 'th-nr-div' } ) .append( $kindSelect.change( function() { filterNotifications( 'kind' ); } ) ) .append( $userSelect.change( function() { filterNotifications( 'user' ); } ) ) .append( $( '<label/>', { html : 'Show:', title : 'How many notifications to show' } ).append( $countSelect ) ) .append( $( '<i/>', { 'id' : 'th-nr-reload', title : 'Reload notifications' } ).click( reloadNotifications ) ) // Add reload button. .appendTo( $( '#new_notifications_wrapper' ) ); // Reload all notifications and set data attributes. reloadNotifications(); } /** * Convert timestamp to date and time, simplified date() function from PHP. * @param {string} format Format of the date and time. * @param {integer} timestamp UNIX timestamp. * @return {string} The pretty date and time string. */ function phpDate( format, timestamp ) { var d = new Date( timestamp * 1000 ); var months = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ]; var year = d.getFullYear(); var month = d.getMonth(); var day = d.getDate(); var hour = d.getHours(); var mins = d.getMinutes(); var secs = d.getSeconds(); // Check date() of PHP. var mapObj = { H : ( '0' + hour ).slice( -2 ), i : ( '0' + mins ).slice( -2 ), s : ( '0' + secs ).slice( -2 ), Y : d.getFullYear(), F : months[ month ], m : ( '0' + month ).slice( -2 ), d : ( '0' + day ).slice( -2 ), j : day }; var re = new RegExp( Object.keys( mapObj ).join( '|' ), 'gi' ); return format.replace( re, function( matched ){ return mapObj[ matched ]; }); } /** * Add Post Archive to Analytics page to find previous post easier and add ALL the details! */ if ( Page.is( 'analytics' ) ) { /** * Update the table zebra striping. * @param {jQuery} $rowsVisible If the visible rows have already been found and passed, use those. */ function paZebra( $rowsVisible ) { // If no visible rows have been passed, load them from the table. $rowsVisible = $rowsVisible || $paTableBody.find( 'tr:visible' ); $rowsVisible.filter( ':even' ).addClass( 'th-pa-row-even' ).removeClass( 'th-pa-row-odd' ); $rowsVisible.filter( ':odd' ).addClass( 'th-pa-row-odd' ).removeClass( 'th-pa-row-even' ); } /** * Filter the posts by the selected types and update the table's zebra striping. */ function paFilter() { $paFilterCheckboxes.each(function() { var $rows = $paTableBody.find( '.th-pa-pt-' + $( this ).attr( 'data-type' ) ); if ( this.checked ) { $rows.show(); } else { $rows.hide(); } $( this ).siblings( 'span' ).html( '(' + $rows.length + ')' ); }); var $rowsVisible = $paTableBody.find( 'tr:visible' ); if ( $rowsVisible.length > 0 ) { // Update the zebra striping of the table. paZebra( $rowsVisible ); $paFilterEmpty.hide(); } else { $paFilterEmpty.show(); } } /** * Get all the posts before the passed post ID. * @param {integer} before The last loaded post ID. */ function paGetPosts( before ) { // Disable the filter checkboxes while loading. $paFilterCheckboxes.attr( 'disabled', 'disabled' ); var url = '/api/v1/posts/list/' + window.current_user.id + '?_=' + Date.now() + ( ( undefined !== before ) ? '&before=' + before : '' ); $.getJSON( url, function( data ) { if ( ! data.hasOwnProperty( 'data' ) ) { $paLoaderDiv.html( 'Error occured, please try again later.' ); return false; } // We have our list of posts, extract all the info and add the rows to the table. data.data.forEach(function( post ) { // Remember the last post ID. $paLoadMorePosts.attr( 'data-before', post.id ); // Find out what type of post this is. var postTypeText = 'Personal Post'; var postTypeClass = 'post'; if ( post.is_share ) { postTypeText = 'Shared Post'; postTypeClass = 'share'; } else if ( post.user_id != window.current_user.id ) { postTypeText = 'Wall Post by ' + post.user.full_name; postTypeClass = 'wallpost'; } // Put together the post links. var postLink = '/' + post.user.username + '/' + post.id var originalLink = ''; if ( post.is_share ) { originalLink = '/' + post.original_user.username + '/' + post.shared_id; } var privacyIcon = ''; var selectBox = '<ul class="privacy_' + post.id + ' black_dropdown_box" >' + '<li class="' + ( ( 1 === post.privacy ) ? 'checked_privacy_option' : '' ) + '"><img src="/assets/check_mark.png" height="16" width="16" alt="Check mark" style="display: ' + ( ( 1 === post.privacy ) ? 'inline' : 'none' ) + ';"><a href="/posts/change_privacy/' + post.id + '/1/' + ( post.is_share ? 'share' : 'normal' ) + '" data-method="patch" data-remote="true">only friends</a></li>' + '<li class="' + ( ( 0 === post.privacy ) ? 'checked_privacy_option' : '' ) + '"><img src="/assets/check_mark.png" height="16" width="16" alt="Check mark" style="display: ' + ( ( 0 === post.privacy ) ? 'inline' : 'none' ) + ';"><a href="/posts/change_privacy/' + post.id + '/0/' + ( post.is_share ? 'share' : 'normal' ) + '" data-method="patch" data-remote="true">public</a></li>' + '</ul>'; // Depending on the post type, the privacy options are handled differently. if ( 'post' === postTypeClass ) { privacyIcon = '<a href="#" id="privacy_icon">' + ( ( 0 === post.privacy ) ? '<span title="Public" class="privacy_icon_public"></span>' : '' ) + ( ( 1 === post.privacy ) ? '<span title="Only Friends" class="privacy_icon_private"></span>' : '' ) + '</a>' + selectBox; } else if ( 'share' === postTypeClass ) { privacyIcon = '<a href="#" id="privacy_icon">' + ( ( 0 === post.privacy ) ? '<span title="Public" class="privacy_icon_public"></span>' : '' ) + ( ( 1 === post.privacy ) ? '<span><img alt="Only Friends" title="Only Friends" src="/assets/friends_icon.png" width="16" height="16" /></span>' : '' ) + '</a>' + selectBox; } else if ( 'wallpost' === postTypeClass ) { privacyIcon = '<a href="#" id="privacy_icon" title="Can only be changed in Settings » Privacy » Post on your Diary">' + ( ( 0 === post.privacy ) ? '<span class="privacy_icon_public"></span>' : '' ) + ( ( 1 === post.privacy ) ? '<span class="privacy_icon_private"></span>' : '' ) + '</a>'; // No select box, as this can't be changed per post. Only in Settings->Privacy for ALL wall posts. } // The post entry row for the table. var $tr = $( '<tr data-post-id="' + post.id + '" class="th-pa-pt-' + postTypeClass + '">' + '<td>' + ( ( post.has_picture ) ? '<a class="th-pa-picture" rel="posts-archive-gallery" href="' + post.picture_url + '"><img alt="' + post.picture_url.split( '/' ).pop() + '" src="' + post.picture_url + '" /></a>' : '' ) + '<div class="th-pa-post">' + '<ul class="th-pa-meta">' + '<li title="' + postTypeText + '"><i class="th-icon th-pa-pt"></i></li>' + '<li class="th-pa-privacy privacy_box">' + privacyIcon + '</li>' + '<li class="th-pa-date" data-date="' + post.created_at_int + '" title="' + phpDate( 'd. F Y - H:i:s', post.created_at_int ) + '">' + phpDate( 'd. F', post.created_at_int ) + '</li>' + '<li class="th-pa-expand button th-pa-stealth" title="Expand text">+</li>' + '<li class="th-pa-post-link th-pa-stealth"><a href="' + postLink + '" target="_blank" title="Open post">Open</a></li>' + ( ( post.is_share ) ? '<li class="th-pa-original-link th-pa-stealth"><a href="' + originalLink + '" target="_blank" title="Open original post">Open original</a></li>' : '' ) + '</ul>' + ( ( '' !== post.title && null !== post.title ) ? '<div class="th-pa-title th-pa-ellipsis">' + post.title.trim() + '</div>' : '' ) + ( ( '' !== post.content && null !== post.content ) ? '<div class="th-pa-content th-pa-ellipsis">' + post.content.trim() + '</div>' : '' ) + '</div>' + '</td>' + '<td>' + post.view_count + '</td>' + '<td>' + post.like_count + '</td>' + '<td>' + post.comment_count + '</td>' + '<td>' + post.share_count + '</td>' + '</tr>' ) .appendTo( $paTableBody ); // Make the picture clickable to expand into a Fancybox. $tr.find( '.th-pa-picture' ).fancybox( { padding : 0 } ); $tr.find( '.th-pa-expand' ).click(function() { // Are we expanding or extracting. if ( '+' === $( this ).text() ) { $tr.find( '.th-pa-title, .th-pa-content' ).removeClass( 'th-pa-ellipsis' ); $( this ).text( '-' ).tooltipster( 'update', 'Collapse text' ); } else { $tr.find( '.th-pa-title, .th-pa-content' ).addClass( 'th-pa-ellipsis' ); $( this ).text( '+' ).tooltipster( 'update', 'Expand text' ); } }); }); // Initialise or update the tablesorter. if ( undefined === before ) { $paTable .tablesorter({ // First column by date, others by text. textExtraction : function( t ) { return ( 1 === $( t ).find( '.th-pa-date' ).length ) ? $( t ).find( '.th-pa-date' ).attr( 'data-date' ) : $( t ).text() } }) .bind( 'sortEnd', function() { paZebra(); }); } else { $paTable.trigger( 'update' ); } // Update zebra striping. paFilter(); // Are there more posts? if ( data.data.length < 10 ) { $paLoaderDiv.html( 'No more posts to load.' ); } else { $paLoaderWheel.hide(); $paLoadMorePosts.show(); } }) .always(function() { // Enable the filter checkboxes. $paFilterCheckboxes.removeAttr( 'disabled' ); }); } // Posts Archive table. var $paTable = $( '<table/>', { 'id' : 'th-pa-table' }); // Table header. var $paTableHeader = $( '<thead/>', { html : '<tr>' + '<th title="Sort by Date">Posts Archive (by Tsu Helper)</th>' + '<th title="Sort by Views"><span class="icon view_icon"></span></th>' + '<th title="Sort by Likes"><span class="icon like_icon"></span></th>' + '<th title="Sort by Comments"><span class="icon comment_icon"></span></th>' + '<th title="Sort by Shares"><span class="icon share_icon"></span></th>' + '</tr>' }) .appendTo( $paTable ); // Add a filter to choose which type of posts to display. var $paFilter = $( '<div/>', { 'id' : 'th-pa-filter', html : 'Filter Posts: ' + '<ul>' + '<li><label><input id="th-pa-cb-post" type="checkbox" checked="checked" data-type="post" />Personal Posts <span></span></label></li>' + '<li><label><input id="th-pa-cb-share" type="checkbox" checked="checked" data-type="share" />Shared Posts <span></span></label></li>' + '<li><label><input id="th-pa-cb-wallpost" type="checkbox" checked="checked" data-type="wallpost" />Wall Posts by other Users <span></span></label></li>' + '</ul>' }); // Call the filter when a checkbox value gets changed. var $paFilterCheckboxes = $paFilter.find( 'input[type=checkbox]' ).attr( 'disabled', 'disabled' ).change( function() { paFilter(); } ); // Message to display if no posts match the chosen filter. var $paFilterEmpty = $( '<div/>', { 'id' : 'th-pa-filter-empty', html : 'No posts match the selected filter.' }) .hide(); // Table body. var $paTableBody = $( '<tbody/>' ) .appendTo( $paTable ); // The row of the table to display the loading wheel and the "Load More Posts" button. var $paLoaderDiv = $( '<div/>', { 'id' : 'th-pa-loader-div' } ); // Show only the loader wheel to start with. var $paLoaderWheel = $( '<span><img src="/assets/loader.gif" alt="Loading..." />Loading more posts...</span>' ) .appendTo( $paLoaderDiv ); // Button to "Load More Posts". var $paLoadMorePosts = $( '<span class="button" style="float:none;">Load More Posts</span>' ) .hide() .click(function() { $paLoaderWheel.show(); $paLoadMorePosts.hide(); paGetPosts( $paLoadMorePosts.attr( 'data-before' ) ); }) .appendTo( $paLoaderDiv ); // Table wrapper. $( '<div/>', { 'id' : 'th-pa-wrapper', html : $paTable }) .prepend( $paFilter ) .append( $paFilterEmpty ) .append( $paLoaderDiv ) .insertBefore( $( '.dashboard_post_statistic' ) ); // Get the first lot of posts. paGetPosts(); } /** * Add a specific class to all nested reply parent elements to emphasize them. */ function emphasizeNestedRepliesParents() { // Make sure the setting is enabled and we're on the right page. if ( ! settings.emphasizeNRP || ! Page.is( 'has-posts' ) ) { return; } doLog( 'Emphasizing Nested Replies Parents.', 'i' ); $( '.post_comment .load_more_post_comment_replies' ).not( '.th-nrp' ).each(function(){ if ( /\d+/.exec( $( this ).text() ) > 0 ) { $( this ).addClass( 'th-nrp' ); } }); } /** * Autofocus text input and add line breaks to messages. */ function tweakMessagesPage() { // Make sure we're on the right page. if ( ! Page.is( 'messages' ) ) { return; } doLog( 'Tweaking messages page.', 'i' ); // Focus the recipient field if this is a new message. if ( document.URL.endsWith( '/new' ) ) { $( '.new_message #message_to_textarea' ).focus(); } else { $( '.new_message #message_body' ).focus(); } // Add line breaks to all messages. $( '.messages_content .message_box' ).not( '.tsu-helper-tweaked' ).each(function(){ var $text = $( this ).find( '.message-text' ); $text.html( $text.html().trim().replace( /(?:\r\n|\r|\n)/g, '<br />' ) ); $( this ).addClass( 'tsu-helper-tweaked' ); }); } /** * Make a log entry if debug mode is active. * @param {string} logMessage Message to write to the log console. * @param {string} level Level to log ([l]og,[i]nfo,[w]arning,[e]rror). * @param {boolean} alsoAlert Also echo the message in an alert box. */ function doLog( logMessage, level, alsoAlert ) { if ( ! publicDebug && 104738 !== window.current_user.id ) { return; } var logLevels = { l : 0, i : 1, w : 2, e : 3 }; // Default to "log" if nothing is provided. level = level || 'l'; if ( 'disabled' !== settings.debugLevel && logLevels[ settings.debugLevel ] <= logLevels[ level ] ) { switch( level ) { case 'l' : console.log( logMessage ); break; case 'i' : console.info( logMessage ); break; case 'w' : console.warn( logMessage ); break; case 'e' : console.error( logMessage ); break; } if ( alsoAlert ) { alert( logMessage ); } } } /** * Add the required CSS rules. */ function addCSS() { doLog( 'Added CSS.', 'i' ); // Remember to take care of setting-specific CSS! var settingSpecificCSS = ''; // Hide Ads. if ( settings.hideAds ) { settingSpecificCSS += '.homepage_advertisement, .rectangle_advertisement, .skyscraper_advertisement { position: absolute !important; left: -999999999px !important; }'; } // Nested replies parents. if ( settings.emphasizeNRP ) { settingSpecificCSS += '.th-nrp { text-decoration: underline; color: #777 !important; }'; } // Quick Mention links for comments. if ( settings.quickMention ) { settingSpecificCSS += '.th-qm-comment, .th-qm-reply { z-index: 1; font-weight: bold; font-size: 0.8em; display: block; position: absolute; background: #1abc9c; color: #fff; border-radius: 3px; padding: 2px; }' + '.th-qm-comment { margin-left: 11px; }' + '.th-qm-active-input { border-color: rgba(0,0,0,.4) !important; }' + '.post_comment { position: relative; }'; } // Add the styles to the head. $( '<style>' ).html( settingSpecificCSS + // Menu item. '#th-menuitem-about a:before { display: none !important; }' + '#th-menuitem-about a { background-color: #1ea588; color: #fff !important; width: 100% !important; padding: 8px !important; box-sizing: border-box; text-align: center; }' + '#th-menuitem-about a:hover { background-color: #1ea588 !important; }' + // FFC. '.th-ffc-span .th-ffc-loader-wheel { margin-left: 5px; height: 12px; }' + '.th-ffc-span a { font-size: smaller; margin-left: 5px; border-radius: 3px; background-color: #1abc9c; color: #fff !important; padding: 1px 3px; font-weight: bold; }' + // About & Settings windows. '#th-aw, #th-sw { width: 400px; height: auto; }' + '#th-aw *, #th-sw * { box-sizing: border-box; }' + '#th-aw h1, #th-sw h1 { margin: 5px; }' + '.th-buttons-div { padding: 5px; text-align: center; display: inline-block; border: 1px solid #d7d8d9; width: 100%; line-height: 23px; background-color: #fff; border-radius: 2px; margin-top: 10px; }' + // About window. '.th-update { background-color: #f1b054 !important; color: #fff !important; }' + '#th-aw > div { display: block; margin: 5px 0; }' + '#th-aw .card { padding: 5px; min-width: 100%; border: 1px solid #d7d8d9; border-top-left-radius: 30px; border-bottom-left-radius: 30px; }' + '#th-aw .card .button { width: 123px; }' + '#th-aw-update-button { margin: 5px; }' + '#th-aw-settings-button { float: right; height: 32px; width: 32px; background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAJBklEQVRYR5VXe2xbZxU/175+v+JnHMexY+fJwqpKU5HYgCEeg05oG4KBVvFHK7G0K9vatUmbPra6SdemlddHnFJUsTGJvwBpMBVNCMEQBSRUZUBXytq84zjxM7Hjt69fnO8+4tskm9QrXd/X953zO+f7nd/5TMFDHj6fj5abTSVXu0vSmEpBajUJ2Ux6/9DLr157GJPUwwwmY0/4/W67xTzz/K4XpOK5kVAIPvzzh78+9GL/Dx/G5qcCOBsIPCOjpS/korE9GHVRMDp88cKXm5sdf+np6pRCHUACFNSoOmQyGbg/M3tvoH/v58QAfG+99bhGoxzKReLfRzvMRnBbAhgdv/KsQqn+ravdTc1OTyfW1jJfOXP06Cc+v99C05KzTmfbj93ONpyLCBAAIIBqpQa3/vURU2bKP1q6f/93169fL48GAkNyhfxMq8spDS+Fp1OLob6NIDYBwMifU6lV7337m9+givkCFGs1+Mffblbq9epHFCV9rKunl9YqleizzrknFngcIKVhORKuLy8tlaBWnzSaTI9+8YnHqTLamVsMQnAhuAnEAwDOXQt8TylX/eZb6DydTEEul8McS8BstUCuUIRWhx1WojFI4jdhIvHNHRRI8CUZq1BrIJ5IgF6L10iU/dqk10MwGiYgZmb+fbsvEAiUuFn8QdIrU8hj33n6aSqfSUM2lxeFRnBIoFar8lOEkMVDhHd4rSMYKXIDsyeuFL1WC3enpmBlJTE2uPelAw8AIA/j7777Dkaxu8PrpVhoaIjLL+e3Xq1BJptFwmWBwoESzL+ElkKFKUOlWgWVUgVarRrUKjU7ZeMS5fJ5mJyczKXSWfe548dXNgEgbs4Fxv7abLN9yWFvoYh/IUWpVAqWlpfBbreD2WQSRdbIAgERiUQgjRXR6fWAXK5Yp0cVs3H3f3fL+Xzxa76Bgb8LBjaR8Mj58zqtXLa6fdvnaVoqYxGsrq5CIrEKXm870Ei0xsLxZoRMsdYoyCGAhaUQdHo4EISosXiCBPCH468e2ClGvwnAG37/TqvFfMPjdqPQ1KCQL0IQRaarswN5wGsPVgDrCi1zK1/DKxFGsuaoDPg9m89BaGkZurs6cZwE6sifO5/cK8aZWdOlQ5cKn5oB//Wf3e/q7OqW1DkCTc3MgdPRAiqVkiMCuhLohjUPJYZhvxEucAvPxUSgRbBipFIpWCxmdmY4Fq+lM+mDA/37AusARi5fPCyXy/ZWa/Xmeq2u0mg0dLvLRZEoCoUCxBIr4Ha1oWF0jMSjanVgKmUIBherhXI5jgsyX6lU+oxGo9bhcGCsPA68QXswPTsL3R2drFgV8iWYDwWxmKoVrKoMYlygLr/z9qS3w9tls9owxRSsYY1jU2HjjEbjyGgV6PU6jmk8K+9PTVXypWL/8KHBXxB3/f39Mtcjj/zKZDJ+197czFGELBOCWAwtQ5uzlUsOnmaLlc2YVCaHj2//B6jRq2MTDrvjMY2aKx1WTYh7XIFFXPsWNCiTIRn5I4+lhJz474kDBx9df4k3u30+pdegT/X19irE7xNIYIvJzLsn9vHk5SG0vFyjzo5dueWw23doNNrGIL78w9Eo2G02tuaFok6mkhCLRn9+8rXDL4odkfszly/FOjweKy3DheFFANcc9Do9lxFyiCSUBfAmAsAoEQDJAK8/fMIyqIZazAxhu2CgUCzC/OLipO/Q4R4xgP0+n9ai1yX6urv5DHAOc2hDg9K87px9jT9oMxRGAMOXL92yWSw79Dod60ikfUDEg0iwUGzkmwRZPTM3V83nc0MjR4b8xFy/z6d26HS/t1nMXzUam1grQo8olkqgVHCYxLaJTJPGRfn8F4aqQO2HatWKziRqtYrGdishVSBTKtj1z2dzmIBG28ExMDU9XctkcxmpRBJBY25HS4sCK4GqVCuE8BxfyRQMoM72BArKZQYw7RWGYWpor4QluvCAEL3i8+n1SsU9T3t7i0IuRz5KwNnuhlAwCDXsA1ibGBk3pcmgB4+zDfcBVUggL+IrK1DG8lwPHcepNEpUQiWkk0n2NRKyHo0nxlbn5gbJfkFMifXlHBwZOWA1Gy82N9tJJUOT0QRqjQaWFxc5LgjHhobIGmMj5z4QAWpzI/iFICa3wmvCTDFXznRfPPnmomBmkxS/cuyYFVO83NPTQ5NuR4629nZgcC2j4TD7TLJAssE/NNLN0QukKNkuTzsk4jHIpjPsMNKgIrHoxNmh4zsaUWxuK3Bi9Nx7pqam53A309ATvHN7vWxU83OzmPYKRyk2WBI2uXLPaqwmNzahcHgJd8opHmMdarhU88EgU2DyX7j4xsjtLTNwdOT0Sxab7WpnRwe1ihLcoB033Iqa4Ma0prMZNrp8rsCCkSNfNFhFVouFbUvzKL8F3IZtPHRNBrjz8Z3MQize8ku/H7dbogwc9vkstFQSfeLJJyVki82Wkjg6NlhMPhLTaDYCAmU3IKRKmBIDa+k0ttwoFB6oGGGpuOwQezkElkyuXhkeOHLwAQDk4cJPx0+nUmvHnW1OxMJ3N0wtqVlsIEDTqHDiWRtTJKp0UhGshAtjEAPZ1OC5oGAq23F3zK7PJhIOjpz+iYyWXW51OGgiOiUkXzgSKaMoMTrc4DcZDFI5CotYVAQjWNyofFlYS60VCwxTxz2g0mq1YvFgk0Pnq8nkXCEW344b0vSWHBBeHh4+tU8hlQV0BgONu6F0qcTsDE9OTrT29u6Sy+gRg9HoRKJywfHlSIRqFhUSteNGuVz2L92790/Ptm3XMJV7NFotnVpbmytucL5lBgQQR4aH9+HOdtdaJvfstdHRpPD+tVMnnzLoDB9YrbbGXzNMAaobRMKRufOvv+4Vk2/g1KkfULRksFhNfD3ga0T+mRnYRF/Ri5ePHes16HW3WxwOuRA+ib5QyOPeMXlj9MTJZz5r/sZvGzlAoiIdm1wJ44STbAjISXs8Ht3ze3ZPtLlctEAwwu4VLNvQ/MKFt8fHySaFCAWRWvFVuCd/LkhzIFd+A8c5JWAEx+QqnBgpkH04aWkEhHz0auDmVlHe/OOfDn3w/vsTvHHyz4c4IWzP8vfkmZwEDAvg/5gaEzsDHsw6AAAAAElFTkSuQmCC"); }' + '.th-aw-donate-buttons { margin: inherit; border-top-left-radius: 20px; border-bottom-left-radius: 20px; }' + '.th-aw-donate-paypal { float: left; }' + '.th-aw-donate-paypal img { vertical-align: middle; }' + '.th-aw-get-in-touch li { display: inline-block; margin-right: 10px; }' + '.th-aw-info li { margin: 2px 0; }' + // 16px icons. '.th-icon { display: inline-block; width: 16px; height: 16px; vertical-align: text-bottom; }' + '.th-icon-bug { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAADXklEQVQ4T02TYUwTZxjHn3vp3bXXu94hWNIW1EApU6ajshTciHFhumGMLlmyZNvnZXYmW/aFZPswY7JkGOMWE7fol31a4ofFZOyDzkkii5kEBKyEEsEKG2thBUvbu9L2rtf39lyRhUt+uefu3t//fe+e9xjYcQwEg7y1T/nCMulPd0anE9uP+vtfCbEM+wFdMy79PjOzudNhdlzYNTna+3KIE9jr1Kx2UAu8hJDndQTmTcP4+N6fc09wDEWsba8W0NW1T4nF/jKwdEa6O74WBGdUcLqAkDqoVk0olktQLBUvP5xKfINjyj09wbrx8YRqu3YA0/9G10W9bO71mLnVAwp8NlVsBL/fC7JHgkw2D+paGiJiRp9YZ65qnOJz8Y7S3dHHH9krsQNYWZabj7RKnw+9xkaVYITcHPmDbChtQKoOMEkFAloCTp88RTdm79HBscqP8XT5cir1fAndih0gIIHrx91fDQycfM97+ASXXYrTzMIk+Xd2ERqDXvCH36RSSztZnx4xbt2+9fPZkc0L6KSQoh3gQVp/OyPdePXEOyFOFAkR6oEofliciEFrTxhoNgm0mANDK9DJu78svD2svY/OIqIyx/oODhVK+utDe9OR8JE+ztLSKPuA8CIsJ4uwp0UAWi4AVdeBiI0QG7tvDC41TYhu5+zo/ZmovYJdSPvoGfl2Z3dXvZmKA3F5gEgNsFrwgE9UcfY80M0sOPz7IT71OHtsOD+AzlNkww6QkbZf35JvHu7u2MOsLRCg1VqbUzkXBJTSVsuxpZY3RKen5pdP38m/i3eeIXk7wC0IbOh82HPjVDjQLpdTxKqUa05ac0GTtBXAsE7I8QE6HFtd+vaJ/mEmo87i7U07gOvr7fxud0U9dLYFIvt9FmcVMjUpU3RCg7AVZrl3w1yyYnyfcjzQnJ7lxN/x6MrKVhdIm39X4NnKhvxDr3It5BN7mnjNwYMOmsGDyOlgAA9pXTLnVwpj58Zz0ebmhlwymVlFl9Z2IsIiyiHZcfCTTulKk4d7iScmcQDDUIZapSpL1/KVhWtz6qeP8uYMjs0h9tavydtnHgsZk/wXuuUvfS7HUWcdU1+1LH29VJ08H8sNqhX454Ws22+1M2C75rBwI/bmkuzvg5iIhuSRwouZ//8b/wNDgFsg+yoSRgAAAABJRU5ErkJggg=="); }' + '.th-icon-idea { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACv0lEQVQ4T5WSXUiTURzGn/O++zB1HxTOqav8apZfjBgFflyotewmKBQhL7qxxFAIKiiSopI+vEkwpGTQTXpT0mWUmFRqorlEK/Nzqb3TbeKcujm3ve/bUVJc2kUHDgfO8z+////hPAQ7LNv9Q6fBkgtUOvJH7gEvNsZeH3r1dznZejFdo48jEvaGMru8ItxQDEYeCdG/DH6Rw8pIOzw9zU95/8rdvdUj3Ma7EMBkbcaTqKK68rD4LKozEIVVCnBB9DpBFjgE/AIcr++Z91/uPb8NMH7nwFmlsbhJfawSLKsCYcIhBJfABxwQvBzI/DgYhxVurwTu7uelSTdHm9cgmxMM30r8GHvuYY5cawDDRlJAGJ1gBbzfDt47CXH+B1j7JALxx2F7dqUj5fZEbgjgW3XCkq6sMlKmTAdhlRQgX7cgBJzgPT8hzn0HO2tFMO0Efpnrl9NqrIoQwMC1fQsxpSaVRK6AJEIPQtYAPgircxCXp6kFK90cVvU5mGl64858MKUOAViuxrVpC435TMQEpLIkCqAWRB7wuUCW7CBuJwJ7whEU0zH7tq/1cC1nCgF0X9IWRcRoXmiyA2A8LrDBMICngKAPxO+HIGMQ1Gjh6JTCO+s8c/TRzHomQr6xqyq6fndGdKU6eQbSOc+mKEgJgqpwLIzFYH7Q/jir3l61Yw7aL0ZFSlhSG21QV6h0Ntqd5mCthVQGj1MHW5erOSiI5XkNzuUdAWuX78tUCRKVYkKTuYvmYHG9jmEj4Pzqx6rXk5vX4OrYmt4QCxtCXV2dmKhPRZbRCOvoELr6+jE48AVms3lb/T8BBfkF0GgUmJ1zo/NDByyW/wTkZOfiYKoew6Nj+NzxCb39lp0naG1tVfE8n0IIMdCdQm0k63S6Uy0vW9DW9g4+nxcnCwsXi0tKlBzHNVJ9UBTFKXpOmUym/t+tniogNDUKLAAAAABJRU5ErkJggg=="); }' + '.th-icon-heart { margin-right: 5px; background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACrUlEQVQ4T31T3UsUURQ/987dnR33Y9x1LT92TQSL1upFKQxLrIU1QpKioggKFKG3/o1eo6dQiHrrQUppQVAqoowFMwtiSXzR3dZsPxx3nZ11Zu7tztj6QdKBC+fj/n73d87hIuD2dVjuFIANc/e6FTOAWcrY2Kmx4ku7PuQdxAgNcbcLE6ebmltJxNBox6jyBM3f87U7JNfnw903RU/zMQcwBlph1cx/m65sppOPeSh4w5H7/hMXRFGux1gQkK6WaPrts3Ill7qFvtyVHx46M/BAdnucZn4FgJqAPUFA9W2wOj9dZoxCU2dMomtLQEtZXjcA+xqgIspG6s3zOJq740u29V47CqkFZKz/4hcoILEGhEAYWF0LtVqA3DKmnJxVVLtBweMH0toJix9eF1Hiti/b3nWuzlxeAF21LmwbctZwJQHbp6U8sK3dGhFFIKEOWFxI5NCnG77Z1uMdp9HvJVzZrOwQAOIuJtsxl21Ntmqi2wWG/4i58iM5g94PyiPBUPOjAFJENa+ASffc3MXseIKAoSZQC1ndzWf9cxi9ABAar/gLjaE6SdRyRN0oW2M40DAGcPskKItBPZNae3p+QhmxhML05do+gvGrpga3JG4ppLiu8W3uV4IQAq+fgx2yvpopaqZuRi9ObSRsApskKkeRQxhvapQkl64QJbdLgjACOeCCMvHrmUxJY7oRi84UZ+1h79Ua7/P0Cg7nZMgiMQpkPavZ5dqgBQ7o6fSmRpkRu/QX/A+BlZjgJAQck+FmSZLMgr0GlYNTKVUzqREbeLf9ctX2KagmJ3u8PQiTeLjF47JyK8tctsHBH/eDD1RQJRk/6+0mmExZscFfvnoAeIcgHo9HBEGI8EnbhxdO8hMB/g9sQ3x/AN/5ZhK8PmeaZqK/vz/xXwV7+/yf/wc6MhxIaZWcBAAAAABJRU5ErkJggg=="); }' + '.th-icon-heartp { margin-right: 5px; background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAC0klEQVQ4T5VTXUiTURh+zr5vP7rNDV2KTl1Oc6YY9IOQqIVZLAjBKAUvBOmmQBCli+gmKKgMsqTAq7ow8mJEhMHM0goTMkkwIRn+DqeRuuXPdH/fX+f7NMG0iw4czvue532e94dzCP5ztboQ11yN8B8akY3W/oQialyRCKlVAAkD1G9vKlt9IbsP+k0XJIoTkCKGURt4gRsDxPbmsrXHpOW90aHV6D15GRWwmLIVdjC0CO/CIAJrs20EEpOUkNVgSymCIc4CFWER5YIYnemicUvnyb2+pIeOzJONZkMqwpyf0kVoGCPitYnwzH6AJEk4mHkKoVgAMSFIfRFa1gyBFzAy3fWO3O1NnjvmqLKux+ZpEBWQBDAqLeLV+6BVG5WOIjRjmFuCIEblAqHTmGGOy8bw+CuQ2z1p0SOOM5qV8DRCkTWlBYBA5DV4+TSgCNRctmyStzCdVg+zzo6Rib4YueVOHy2wHy8M8z+xHqIlQlBIXAzofh5S7Mp6/RZZlmZg1BuhJhZ4vF8HyI3XtgarZf8jk0mLwIofdMJ407lJ/HudrY0Hy6phMSfDv7yOBf9cPbnoAlOgzeHtGbl03kEEfgXR6wruKVBRY4QlMQFiLB7TvolnN6um6pR3cN114DTLMm/tmTZIqiAW/RuIRngMdkcUoZLKODAMg5RkA0TOgBmfFzwvFN+pnvysCMjrmsvhZAjbbbdlgLCr8M1u4EvPhoIVn9MjLVUPiUvA1KwPgsSXtlSPD8jYtoDsXO0sKFermL6cLCttZxlzc5sVpKfrAN6MSe88JFEsban9rpB3CSgiHYfKVayqL9dupZWsK0Eip8fEzA/6eKTS+3Xftsl7CsiXTR2HTzCEfMzLsSoCnkmaWRJ2kf8pIAONT46WMGrySbYFTiptuzS8I/OOFtxudz6dcj4hRNkULKQ7X/4Xm1lU8jFG/8UQxYcFQRhyOp1D8uVvoY8fXsjKUkIAAAAASUVORK5CYII="); }' + '.th-icon-help { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACtklEQVQ4T6WTTUhUURTH//dd33uOM8/GQafRZhaSNTmmLWqRIEI7i5blIgqiyBoNkgSjohiK6DuMaoYEKQg3btqYLV0UUUILMy2pheH356i9+XjvzZvbnTcqfiw7cLlwDud3/ufccwn+08jm/FDXoJQvyA0gOAaGagJCGWEfwcjb5XQyEqqv0NfnbAA86vrdQKnwsqBQgV2xQZRlEEKgJ5KIqwkszP1F2mSNLfU7I6uQNcCDrl9BWRbDxT439JQA3WDg2VwEvxiDJBJIOWlMjc4gqRmNrfW7LIgFCIUGpbyArPlKi7EcI0hz3353FB6HaRVaSAgYnFegGiKUPIbxkUnEZjQl1FShWoC7ncMXXW7nM4gKNMNEymQ47NfwZVTCPAceDRjQdR29Iw7IIgVJqZidWWy9fmL3Qwtwu3P4Q7HXU6PySinTBFe8wfZ5uSKfiVefKXKoAMVOMPFnYuDGSX9VtoU3PzWP1yepMd2qvtmO7GUQBAHd3xgoJXDkiZgeH9NDp/bIFuDm6x9aUYmXA7QN1SXKUFumw+20obs/jeWkNVcOkDE7OabfOl2eBVzrGPpUuN1THU/y6iw7uIwdLDVQ4pLwrp9BS604iQC7jWJuemrgzpnybAtX2ocubXPltzFBQUpPrAFq/YRLpugdWs0GqGgDZSoWo4tX75+ruGcBjvNnLN1BtIJCDwy+A6Ye3zKHjEPgybLIEJ2fRtSMOdvPH1haW6TLkYGgKIphh7OIz4FyJUnUlBlQcoH330VeORdUMKEuzcEw9ObHFyqfZqAbVrn5+UCQUCFsdygQJTuP0qwSPhdDjyEeU5FOp1raGquerErc8pkaX/Q6KHMF+R84ywn+FcIwY6zDJAuRcNMhdX1/FqCnpyfAhxXgSdbhrkp+Mvd6G+KQPh7/appmX11dXV8m+A+i8hIg7mH0KgAAAABJRU5ErkJggg=="); }' + '.th-icon-manual { margin-right: 10px; float: left; display: inline-block; width: 32px; height: 32px; vertical-align: text-bottom; background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAE/klEQVRYR8WVW0wcVRjH/3vjtjt7mdlFSmkLLtJYWKTF2rpgwXJRY6oxaLVYbR80KYmJocYaTRNjo1Fqi4t9wAcSo9b65qu+GDVqSKppK1qUgLSlQOkChYVlgb36nZm9zHLbYW3sSXYzmXPmfL/z//7fd1S4w0PF4peWluYZzcZXBYvwIC8IhmP1lsrbwcWXVCF/1zNijNWGOOlwOHo4jnPwvGWM54W+prs9NbcDwG4vQtkLrtQA5eXlEYNB72lrO2myWq0wGAziLzs7G5mZmWmzXD7bqhxAb9APdLg6ikmJOwPATtzcfABVVdWIqfC/KsAAeIGHhbeG3qg3a9LWfcmH6/AAB0GwwGTNxZlDpUAk8t8YVCoMDw3B8WKHMhNyHClg4UUA18ESREKhOMAyFPHF2oBqjQajozdQfuhjZQBiCngBZpsN7c/aEQoGVwwixV4jOM2xWa1WhzG3G/cdPqMQgNwv8FIKTu8vigIkshBZISh7FwqFxUXhpPkINNoM3CSAnS93KgPgDBwsZEKzYMOp/YUIBgJJHpDOTP/Rw7N4/kAQs75FzC0sIhAMkzAJZTRaLSYmp9B0/JwygFgVmEmBD5u2ICACyKSOP0oPwXAEUzM+3LDWIrPImRQ8Ri6mQpc1m+xmVVfdvaajsXcindQJmQckE55s2oSA3y/7LnbsBFKQpB8Z98C/5wQcdtuSFKxcQN1/TkCrUQdzcnRdjaXmFrYqAUBVIJAJTZSCD54qIIBFSXSK/culgWWGDBDA+PQcHEc+Q0lRLvyh1GXb238LOVlaqNTwUcZe2VchfBoH4KKNyCjk4v0n8+FfXJA5UHqU/C2NQDCEoTEPwvXvoXCzDQu0Y6oxeGVaXJKVoYn4g+GwPkcn1ShLAbsDxD5AZfjuvjz4FyQAFrK7Z3DZ3kEK6CYFyo58jmJSYD0Asc1y9LrZ5BSIALk48XguFufn40ET5pYpECAF3B5E6ttgVwgg5pwiqukvW6tC//UZGQCVIbsLTFYb3nmMJPX5kk8dbTDne6+J75kJx6d8pMAXuMd+l6jAd2c7U2VBnK872IIsrRr/XPckAFgfYFVgJIC3H+ExLweQ1XdMA1b3w+5pRBpPoaQ4T1EK5HQMYODatFwB6TZkfeB4gwU+rzexfiUASsHwxAzQeBpbSzaIAN8rVODhqAIDV1YBeKvOhLkYwJIWHFeAAEYIINLYjm1b89NSoG9wSp4CA7Vi6gOUgjdrDfAygFUuHQbBynBknJpc40co27YR86TAD18q80Dt8y1kQjX+6r+V7AELecBMAMdq9PDO0uZrAYidkKXABUdZgQiwnsEAevsmk6tAIA8YqRO+vicb3hnanPWBVSBYJxydmEW4oQMV5ZswHwjjx3PKFKhpJgV0avT8PSFTgDUidh1TJ3ytOhOzHglgtREDCBHA9orNIsB6BgP4vXc82QOsCoxUBUedDMCjAMCLUIMLO7ZvEQF+UqjAQ1EFLl12xwEuUh+oiPWBVmdGSgDxNpwkgDoXKisL01Lgwh834wBPGDjua8HCa4x0F7Tu1mEmhQIxgOBeF3beXwQfKfDzV8o8UH2gBTmUgt96xiQANpxO515S4DCXt+Hp5wrc2anyyQDGqBVrHu3E7gckgPUMBvDrRRmA7ONcejbQT69kw0++vdq9a8dGfToA5y+MJhRQEmylNZ3fXG0nGV9K53tqaF3/Aot1QCKE5DaSAAAAAElFTkSuQmCC"); }' + // Heartbeat. '.th-about-love:hover .th-icon-heart { -webkit-animation: heartbeat 1s linear infinite; -moz-animation: heartbeat 1s linear infinite; animation: heartbeat 1s linear infinite; }' + '@keyframes "heartbeat" { 0% { -webkit-transform: scale(1); -moz-transform: scale(1); -o-transform: scale(1); transform: scale(1); } 80% { -webkit-transform: scale(0.9); -moz-transform: scale(0.9); -o-transform: scale(0.9); transform: scale(0.9); } 100% { -webkit-transform: scale(1); -moz-transform: scale(1); -o-transform: scale(1); transform: scale(1); } }' + '@-webkit-keyframes "heartbeat" { 0% { -webkit-transform: scale(1); transform: scale(1); } 80% { -webkit-transform: scale(0.9); transform: scale(0.9); } 100% { -webkit-transform: scale(1); transform: scale(1); } }' + '@-moz-keyframes "heartbeat" { 0% { -moz-transform: scale(1); transform: scale(1); } 80% { -moz-transform: scale(0.9); transform: scale(0.9); } 100% { -moz-transform: scale(1); transform: scale(1); } }' + '@-o-keyframes "heartbeat" { 0% { -o-transform: scale(1); transform: scale(1); } 80% { -o-transform: scale(0.9); transform: scale(0.9); } 100% { -o-transform: scale(1); transform: scale(1); } }' + // Settings window. '#th-sw label, #th-sw input, #th-sw select { display: inline-block; cursor: pointer; }' + '#th-sw form > div { margin: 5px 0; }' + '#th-sw-back-button { float: left !important; }' + '.th-sw-help { margin-left: 4px; cursor: help; }' + // Show custom number of notifications. '#new_notifications_wrapper { position: relative; }' + '#th-nr-div { position: absolute; top: 0; right: 15px; }' + '#th-nr-div select { cursor: pointer; margin: 0 5px; }' + '#th-nr-nk-select, #th-nr-nu-select { width: 80px; }' + '#th-nr-reload { display: inline-block; height: 16px; width: 16px; vertical-align: text-bottom; cursor: pointer; margin: 0 5px; background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAC2klEQVQ4T32SS08TURiG37l0ept2aGnxAqIoprXVyNqNW1YaF3YhwcR4SVRYiFFJRDHES8UYjBeMMUYj0RhIZCXxFxhDwBAUsKlKJAJFaellpu1MZ+Y4rbEBL5zdOfnOc773Ow+FVVa4/7PA0aQ7rcunL4WC4r9KqdUAV/ujXgfHflcU9ZMKvelcyDf8Z/1fgOv9H30U2HYKZI/JxLq9HgfsVhbRL4uaDj08grHOgVBI+w1aAbj+ItICir7l2+JhPG4ekgLE0xoYGqiwU4gvJjEzlxouKPrBC83+SBFSBlx9Fj2+scbZ6650QNEZSHkdOYUgJ5PSY7yVRoWNILWUwdx8avDd4Nj+gYGQVgJ09n2oNzGmiYC/mhNlBpmsiqwoQpVlsBY7GLMVRM1Dl7OQ87nOjgO+rhURuvoid2pr3C0aa0daVCDGFzRdJ2GKkFeM2fKG4SxQsqmETtB0qdn/evkgf3XwNBLdULuuPpOnkBMzyGfTHV0H/VfOPRgRbDYhSXRtVFWw//JR//Q/f+HikynZW13DFTNL8RiyWmHdjUPBWBFg5vibmaVEa0/brtx/Peh4PCm71lRzqg5IiQXkMgbgZDC2miMrZtD+aGrcWendQTMmQBGRTKQ6wkcDV5YD2h9OdlM0daZ8RjB97ci2zaUZnH04cdnGO8+bbQ44LcDXmTlDFBJGQe3TaJYxPDi8dZOrzV/vAUVRiE4nMPVp8W73sWBrCXDm3sRawpKow+XlBacVa12MMUgR3+bTpQu16wXwDh5JSUdGzGNmJpbT9MLWnuMNs2WRTt1/v48zW17abDw8lXZUVbAQeAbE8CgpaviRVBFfkpCTJMOFfFPPie3PV5hY3LT2ju9mQT024tQ5eBt43lyKnJFkiGLWkCs9awhy5PbJnWUXyh0MDQ0FGIYJFAjdEFkU9iZldkta5qxFgGAuoMouo65C+miitbdGrFFN04YbGxuHfwJN4z4gjV4RIAAAAABJRU5ErkJggg=="); }' + // Notifications Reloaded. '#new_notifications_popup .notifications, #new_notifications_popup .messages, #new_notifications_popup .friend_requests { max-height: 160px; width: 100%; overflow: auto; }' + '#new_notifications_popup .notifications .notifications_item, #new_notifications_popup .messages .notifications_item { width: 100%; }' + // Posts Archive. '#th-pa-wrapper * { box-sizing: border-box; }' + '#th-pa-wrapper { float: left; border: 0px solid rgba(0,0,0,0.1); border-collapse: collapse; width: 100%; background: white; margin: 10px 0; background-color: #f6f7f8; }' + '#th-pa-wrapper ul { margin: 0; }' + '#th-pa-filter-empty { padding: 10px; font-weight: bold; background-color: #eee; }' + '#th-pa-filter { padding: 4px 10px; font-weight: bold; }' + '#th-pa-filter ul { display: inline-block; }' + '#th-pa-filter label { cursor: pointer; font-weight: normal; }' + '#th-pa-table { border: 0; border-collapse: collapse; width: 100%; line-height: 20px; }' + '#th-pa-table thead { font-size: 1.5em; background-color: #fff; text-align: left; border-bottom: 1px solid rgba(0,0,0,0.05); }' + '#th-pa-table th { padding: 10px; }' + '#th-pa-table th:not(:first-child), #th-pa-table td:not(:first-child) { width: 40px; text-align: center; border-left: 1px solid rgba(0,0,0,0.05); }' + '.th-pa-row-even { background-color: #eee; }' + '.th-pa-row-odd { background-color: #fff; }' + '#th-pa-loader-div { padding: 15px; font-weight: bold; }' + '#th-pa-loader-div img { vertical-align: middle; margin-right: 5px; }' + '#th-pa-table thead .icon { margin: 0; width: 24px; height: 24px; }' + '#th-pa-table .view_icon { background-position: -202px -7px; }' + '#th-pa-table .share_icon { background-position: -232px -7px; }' + '#th-pa-table .like_icon { background-position: -263px -7px; }' + '#th-pa-table .comment_icon { background-position: -292px -7px; }' + '.th-pa-privacy .privacy_icon_private { background: url("/assets/friends_icon.png") no-repeat !important; background-size: 15px !important; }' + '.th-pa-privacy #privacy_icon span { width: 15px; height: 15px; }' + '.th-pa-privacy #privacy_icon span img { width: 15px; height: 15px; }' + '.th-pa-pt-post .th-pa-pt { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACkUlEQVQ4T3XSW0hTcRwH8O9/N5vb2XTeNufCG0q2lKWYCoGQURQiPSzpISiIQnzoQkbgS9FDINGjUb30kqQUBEIaURR7qNzyNmuoZdPMXd3cObtfPJ0lyg7T39Phd37/D1/4/Qgy6r0t8BBgL2X2tr9ZliUikXCIoYP9nU2l3u0+yRx+Z/PT+v1KapNls4yZX15o1Xms2xd8mgig71RLAZ0e4gHj3310lVZBJVLZwI+fLhxrKMXKRpx1ekJPAkz4+tk2XYQHvLGu0+WlCiqa3MxKsGT34HSjFtEkCzuHOFzM42AoeoMHjE576coyJWUZG+YB+uNG/F7eAtIV4ZBlf5xd5RAe8NrioavL83ZNYOeAzJJLJYjGCcMDXn110zWV+dTkOD9BXYcR6z5m5336kVBAsEHHozvAA5PysoTUPkqyCwJZjh4a5QWoFed32+j/nlQkgG3Rt5Vg4EPBoEZV21OuboZMqkIo4oPdOYHAnyJQG8ZdEcPJbszNrzPk/lt1n66kbqBS24hQzI1EKgyxMBdCNg8rnknIREboCq9kIVKxADM2D0PujemW2vSdFeGEgwPWkGRj2IyLYTGF0XWmHeZpEzT+W1lAA5dgas7NkLujVWxrfTu+mCcgz49DqQIsnxIoLMnF0bYj+Dz7ER0GKwgR8pBcLoFl1sWQ/pc1S02HDlesOuZhNvmxT0ogVxIYWoogl1RhxrqIisjV7AQnumGecjLk9osDfZpizYC6RAKHy4EFaxCGViUUsmL8dURQQJ1DddnFLCCdYOLb2tYWbj6vHyxS5fdoiimIc1JIxIRwuhnEgs2JQllvdK9dBvzekZ07uPassYcb7OXu42Ayllr0rsWGh+7YRvY8BCDE/XP9AzObEEcy9WOXAAAAAElFTkSuQmCC"); }' + '.th-pa-pt-share .th-pa-pt { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACaElEQVQ4T4XSX0hTURwH8O+5u9e8btetP1JuLUrBSIuRFUQvlpWQFiihyIJ6sYcei3qqhx6lB4nqxVw9BEX0EPQHKyKE/hCUIpZuRKl3Nsttbrpzt3nd3D3daZPdZnXgci+/c36f++P8fgR565Uv1gWwjvxY7psxRnjedE+h8YvHdtunc3GSf/ilb4Zu32SVNMYKjKHRaTg22FgoGu9Jx3Chce9amj1kAJ6PRGmlo1RKZwoB77cgDrrsmJhNsalw4mZMSZ5t2+ecMwC9nyN0c7lZUlMLgEkwVDEmh9G0ywF1gUHWkZ9BpTueUM8ZgCeDYVpxq1YihMN8ew+0yrplZNy/BGTXnI74Z1IsoCMG4PG7MWXL/QaLvc6Nyb47WDh+FVr1kcUkWQfyl0UsgpoiChlwl3YxQhZvnvGihRfNpKrhBJLxOAJvHyLT3Am2swWRqLKcn/2riSOYpSmVvG+1UteZKxJLJXVBQybyHZlJL0iJDWmrE/LrR9DcN0C2HTBUIPIcfF+jCnnTbKM1hxulTFjO1gCmJqAloiCCCMG5A3PmcshBCr7DUwAMf4kopK/JRqtrt0rpwIix9xwPZnVgIpQGTl2DqeaPCgQOQ76wQl4cKu0Cxy1Nn6DfgcCTqrJ5zDMTArOrQI6eR1HD6YLBEnVgcDikGLpw+6lX2djTaKmoWIPx0QiE1ksoqj+50mSjRAf6PwWNgKd3lJZfr5cIx6HYfRnF+9tXTM4Gs8DHwSkj0P3MT/e4yqRkIql3Qfprcg74MPDDCHTe7fdYV69r+2dm3mZsZvqB4Q70PbP+rP/9/p+T0A8EfwHTFvU4JDA5ZgAAAABJRU5ErkJggg=="); }' + '.th-pa-pt-wallpost .th-pa-pt { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACeElEQVQ4T4WTXUiTURzGn/djc7pPW2vTqZgf3Wh+ZMgM0sI7LaSLjEC60ouii5K8KC9UiOimCLqIMoJIo7S7QIIQEqILNcXUVqx01nTfe91edWvOnd7X5dt0ix44nMOf5/z4f5xDIUmj1uBdgLQnx3bOhBCKZZnnfGit+/TRXN9OnEo2v7VyofICrTpOSApj5rsPZpOOeAJr/ZtBdDVZ9CHRtAvwZj4QKjJlqWNgUgCfv7nRWJmLH6tR4vKuPwryG1dbj+WHJcDQ2TK536AN/+KWaVaTj9quF2DVBgm0YPeiucaMSIzALkCcbv7h2nqkUwI86ai+RRj5daMpB06HC0ZLC8ynOiXA4lICICosQJa4KHEIEAkwcLl2odpy8qB2nxYrPx2wr3hRdHFAAtgFQLJUmXJEohQvAV52WnoLD1X06A1GLC8tIlrYAF3dBemOP8BLZ/ESQ1NYDUUjEuDZpSMlSo3GptLowK9yMLQ9gMJQnG6i27FMlobVFkhkcHNUb1Zw7J268ivn3K/GUHC+BbP2Dygu7UOGLFH3XomAua9+nup9ndeuyJD3l+bX4HBeC/iwE5lyDb44R2FzTKHE1AOzvjUVIKMxY/XyVPdw6VSjpbmaYePYiPqxFd8U6pMJED3iMRoTswuorxpOC5ie8/DUtcHKWFNDPRPc8Ahd5UDIFiiKgUKeDZ3SiJF3YzhzYjwFkCVkMPnJzVMd96qeKtVsG2iKTldrjq4Rx2v70gImpl3bTVQJyygs9Y37I+8ryg4oI7F4otNKFcSxivteiRmMf1z5+w5Ew+3Bycfa7P2pHfvHMIOcb2jXZxJ8yj/ZiPv/tC4Y3L8B/vXnOFesbhoAAAAASUVORK5CYII="); }' + '.th-pa-picture { float: right; width: 70px; height: 70px; padding: 5px; text-align: center; }' + '.th-pa-picture img { max-width: 60px; max-height: 60px; }' + '.th-pa-post { float: left; width: 480px; padding: 5px; min-height: 70px; }' + '.th-pa-title { font-weight: bold; }' + '.th-pa-title, .th-pa-content { white-space: pre-line; }' + '.th-pa-ellipsis { white-space: nowrap; text-overflow: ellipsis; overflow: hidden; }' + '.th-pa-meta { opacity: 0.6; }' + '#th-pa-table tr:hover .th-pa-meta { opacity: 1; }' + '.th-pa-meta li, #th-pa-filter li { display: inline-block; margin-right: 10px; }' + '.th-pa-expand { float: none; padding: 0px; width: 13px; height: 13px; vertical-align: text-top; }' + '.th-pa-post-link, .th-pa-original-link { float: right; }' + '.th-pa-stealth { display: none !important; }' + '#th-pa-table tr:hover .th-pa-stealth { display: inline-block !important; }' ).appendTo( 'head' ); } /** * Add the about window which shows the version and changelog. * It also displays donate buttons and an update button if a newer version is available. */ function addAboutWindow() { doLog( 'Added about window.', 'i' ); // About window. var $aboutWindow = $( '<div/>', { 'id' : 'th-aw', html : '<h1>About Tsu Helper</h1>' + '<div class="th-about-love"><i class="th-icon th-icon-heart"></i>Made with love and care.</div>' + '<div><ul class="th-aw-info">' + '<li>Version <strong>' + Updater.localVersion + '</strong> (<a href="https://j.mp/tsu-helper-changelog" target="_blank">changelog</a>)<br />' + '<li>©2014-2015 Armando Lüscher (<a href="/noplanman">@noplanman</a>)<br />' + '<li><em>Disclaimer</em>: Tsu Helper is in no way affiliated with Tsu LLC.' + '<li>Use it at your own risk.' + '</ul></div>' + '<div><i class="th-icon-manual"></i>For more details about this script, to see how it works,<br />and an overview of all the features and how to use them,<br /> take a look at the extensive manual <a href="https://j.mp/tsu-helper-readme" target="_blank">here</a>.</div>' + '<div><ul class="th-aw-get-in-touch">' + '<li>Found a <i class="th-icon th-icon-bug" title="Bug"></i>' + '<li>Have a great <i class="th-icon th-icon-idea" title="Idea"></i>' + '<li>Just want to say hi?' + '<li><a class="message_pop_up fancybox.ajax" href="/messages/new/noplanman">Let me know!</a>' + '</ul></div>' + '<div>If you like this script and would like to support my work, please consider a small donation. It is very much appreciated <i class="th-icon th-icon-heartp"></i>' + '<div class="th-buttons-div th-aw-donate-buttons">' + '<a class="th-aw-donate-paypal" href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=CRQ829DME6CNW" target="_blank"><img alt="Donate via PayPal" title="Donate via PayPal" src="https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif" /></a>' + '<span>« PayPal <i> - or - </i> Tsu »</span>' + '<a class="th-aw-donate-tsu button message_pop_up fancybox.ajax donation" href="/users/profiles/donation/104738" title="Donate via Tsu">Donate</a>' + '</div>' + '</div>' + '<div id="th-about-followme">Follow me and stay up to date!</div>' + '<div>Iconset <a href="https://www.iconfinder.com/iconsets/essen" target="_blank">Essen</a> by <a href="http://pc.de" target="_blank">PC.de</a></div>' }); // Get my card and add it to the about window. $.get( '/users/profile_summary/104738', function( card ) { $aboutWindow.find( '#th-about-followme' ).after( card ); }); // Settings window. var $settingsWindow = $( '<div/>', { 'id' : 'th-sw', html : '<h1>Tsu Helper Settings</h1>' }); // Settings which are only a checkbox. var checkboxSettings = ''; [ { name : 'hideAds', txt : 'Hide Ads', help : 'Show or Hide all the Ads.' }, { name : 'quickMention', txt : 'Enable Quick Mentions', help : 'Add Quick Mention links to comments and replies.' }, { name : 'emphasizeNRP', txt : 'Emphasize Nested Replies', help : 'Emphasize the parent of nested comment replies, to make them more visible.' }, { name : 'checkSocial', txt : 'Check Social Networks', help : 'Check if your new post is being shared to your connected Social Network accounts.' }, { name : 'checkMaxHM', txt : 'Check Max. Hashtags & Mentions', help : 'Check if the maximum number of Hashtags & Mentions has been reached before posting.' } ].forEach(function( item ) { checkboxSettings += '<div><label><input type="checkbox" name="' + item.name + '"' + checked( settings[ item.name ] ) + ' />' + item.txt + '</label><i class="th-icon th-icon-help th-sw-help" title="' + item.help + '"></i></div>'; }); // The debug level dropdown. var debugLevelSettings = ( publicDebug || 104738 === window.current_user.id ) ? '<div><label>Debug level: ' + '<select name="debugLevel">' + '<option value="disabled"' + selected( 'disabled', settings.debugLevel ) + '>Disabled</option>' + '<option value="l"' + selected( 'l', settings.debugLevel ) + '>Log</option>' + '<option value="i"' + selected( 'i', settings.debugLevel ) + '>Info</option>' + '<option value="w"' + selected( 'w', settings.debugLevel ) + '>Warn</option>' + '<option value="e"' + selected( 'e', settings.debugLevel ) + '>Error</option>' + '</select>' + '</label></div>' : ''; // List the available count options. var selectNotifReloaded = '<select name="notifReloaded"><option value="0">Disabled</option>'; [ 5, 10, 15, 20, 25, 30 ].forEach(function( val ) { selectNotifReloaded += '<option value="' + val + '">' + val + '</option>'; }); selectNotifReloaded += '</select>'; var $settingsForm = $( '<form/>', { 'id' : 'th-settings-form', html : checkboxSettings += // Notifications Reloaded '<div><label>Notifications Reloaded count: ' + selectNotifReloaded + '</label><i class="th-icon th-icon-help th-sw-help" title="How many notifications to show in the notification popup."></i></div>' + debugLevelSettings }) .appendTo( $settingsWindow ); // Defaults button on Settings window. var $defaultsButton = $( '<a/>', { 'id' : 'th-sw-defaults-button', class : 'button red', title : 'Reset to default values', html : 'Defaults', click : function() { Settings.setDefaults( $settingsForm ); } }) .appendTo( $settingsWindow.find( 'h1' ) ); // The state in which the Settings are closed (back or save). var settingsCloseState = null; // Save button on Settings window. var $saveButton = $( '<a/>', { 'id' : 'th-sw-save-button', class : 'button', title : 'Save Settings', html : 'Save', click : function() { if ( confirm( 'Refresh page now for changes to take effect?' ) ) { Settings.save( $settingsForm ); settingsCloseState = 'save'; $.fancybox.close(); } } }); // Back button on Settings window. var $backButton = $( '<a/>', { 'id' : 'th-sw-back-button', class : 'button grey', title : 'Go Back without saving', html : '« Back', click : function() { // Close this window. settingsCloseState = 'back'; $.fancybox.close(); } }); // Buttons on Settings window. $( '<div/>', { class : 'th-buttons-div', html : '<span><a href="https://j.mp/tsu-helper-settings" target="_blank">Detailed Help</a></span>' }) .prepend( $backButton ) .append( $saveButton ) .appendTo( $settingsWindow ); // Settings button on About window. $( '<a/>', { 'id' : 'th-aw-settings-button', title : 'Change Settings', html : '', click : function() { // Open settings window in a fancybox. Settings.populateForm( $settingsForm ); $.fancybox( $settingsWindow, { closeBtn : false, modal : true, beforeClose : function() { // If the Back button was pressed, reopen the About window. if ( 'back' === settingsCloseState ) { setTimeout(function() { $.fancybox( $aboutWindow ); }, 10); return false; } }, afterClose : function() { // If the Save button was pressed, reload the page. if ( 'save' === settingsCloseState ) { location.reload(); return; } } }); } }) .appendTo( $aboutWindow.find( 'h1' ) ); // Check if there is a newer version available. if ( Updater.hasUpdate() ) { $( '<a/>', { 'id' : 'th-aw-update-button', class : 'button th-update', title : 'Update Tsu Helper to the newest version (' + Updater.remoteVersion + ')', href : Updater.scriptURL, html : 'New Version!', click : function() { if ( ! confirm( 'Upgrade to the newest version (' + Updater.remoteVersion + ')?\n\n(refresh this page after the script has been updated)' ) ) { return false; } } }) .attr( 'target', '_blank' ) // Open in new window / tab. .appendTo( $aboutWindow.find( 'h1' ) ); } // Link in the menu that opens the about window. var $aboutWindowLink = $( '<a/>', { title : 'About noplanman\'s Tsu Helper', html : 'About Tsu Helper', click : function() { // Close the menu. $( '#navBarHead .sub_nav' ).hide(); // Open about window in a fancybox. $.fancybox( $aboutWindow ); } }); // Check if there is a newer version available. if ( Updater.hasUpdate() ) { // Change the background color of the name tab on the top right. $( '#navBarHead .tab.name' ).addClass( 'th-update' ); $aboutWindowLink.addClass( 'th-update' ); } // Add "About" menu item. $( '<li/>', { 'id' : 'th-menuitem-about', html : $aboutWindowLink } ) .appendTo( '#navBarHead .sub_nav' ); } });