NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Tsu Friends and Followers // @namespace tsu-friends-and-followers // @description Tsu script to display Friends and Followers of all users on the current page // @include http://*tsu.co* // @include https://*tsu.co* // @version 1.2 // @author Armando Lüscher // @grant none // ==/UserScript== /** * Display Friends and Followers of all user links on the current page. * Automatically loads new data for newly appearing user links. * * waitForKeyElements is used in case the current browser does not support the MutationObserver. * * For changelog see https://github.com/noplanman/tsu-friends-and-followers/blob/master/CHANGELOG.md */ $( document ).ready(function () { /*************** HELPER FUNCTIONS ***************/ /** * 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. */ if ( 'function' !== typeof String.prototype.startsWith ) { String.prototype.startsWith = function( str ) { return ( this.slice( 0, str.length ) == str ); }; } /** * Check if a string ends with a certain string. */ if ( 'function' !== typeof String.prototype.endsWith ) { String.prototype.endsWith = function( str ) { return ( this.slice( -str.length ) == str ); }; } /** * Check if a string contains a certain string. */ if ( 'function' !== typeof String.prototype.contains ) { String.prototype.contains = function( str ) { return ( this.indexOf( str ) >= 0 ); }; } // Add the required CSS rules. addCSS(); // Output console messages? var debug = false; // Remember the user objects that are currently loading. var userObjectsBusyLoading = []; // The user objects that have finished loading. var userObjects = []; // Max number of tries to get friend and follower info. var maxTries = 60; // Add all the sub nav items. addSubNavItems(); // URL where to get the newest script. var scriptURL = 'https://greasyfork.org/scripts/6108-tsu-friends-and-followers/code/Tsu%20Friends%20and%20Followers.user.js'; var localVersion = 1.2; var getVersionAPIURL = 'https://api.github.com/repos/noplanman/tsu-friends-and-followers/contents/VERSION'; // Check for remote version number. checkRemoteVersion(); // Get the current page and start the observer to automatically update user links. setTimeout( function() { getCurrentPage(); startObserver(); }, 500); // Pages with preloaded data should be processed instantly, // as the observer can't necessarily detect any change in those. setTimeout( function() { loadFriendsAndFollowers(); }, 1500); var currentPage; /** * 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. */ function getCurrentPage() { doLog( 'Getting current page.' ); if( $( 'body.newsfeed' ).length ) { // Home feed. currentPage = 'home'; queryToLoad = '.evac_user'; queryToObserve = 'body.newsfeed'; } else if ( $( 'body.discover' ).length ) { currentPage = 'discover'; queryToLoad = 'body.discover .tree_child_fullname'; // No observer necessary! // TODO: Also, with observer, this page goes crazy... queryToObserve = ''; } else if ( $( '#profile_feed' ).length ) { // Diary currentPage = 'diary'; queryToLoad = '.evac_user'; queryToObserve = '#profile_feed'; } else if ( $( 'body.tree' ).length ) { // Family tree. currentPage = 'tree'; queryToLoad = 'body.tree .tree_child_fullname'; queryToObserve = '.tree_page'; } else if ( document.URL.contains( '/post/' ) ) { // Single post. currentPage = 'post'; queryToLoad = '.evac_user'; queryToObserve = '.post'; } else if ( document.URL.endsWith( '/friends' ) ) { // Friends. currentPage = 'friends'; queryToLoad = '.card .card_sub .info'; queryToObserve = '.profiles_list'; } else if ( document.URL.endsWith( '/followers' ) ) { // Followers. currentPage = 'followers'; queryToLoad = '.card .card_sub .info'; queryToObserve = '.profiles_list'; } else if ( document.URL.endsWith( '/following' ) ) { // Following. currentPage = 'following'; queryToLoad = '.card .card_sub .info'; queryToObserve = '.profiles_list'; } else if ( document.URL.contains( '/messages/' ) || document.URL.endsWith( '/messages' ) ) { // Messages. currentPage = 'messages'; queryToLoad = '.message_box .content'; queryToObserve = '.messages_content'; } doLog( 'Current page: ' + currentPage ); } // The elements that we are looking for. Get set in getCurrentPage(); var queryToLoad; /** * Load Friends and Followers * @param {boolean} clean Delete saved details and refetch all. */ function loadFriendsAndFollowers( clean ) { if ( clean ) { if ( confirm( 'Reload all Friend and Follower details of all users on the current page?' ) ) { doLog( '- (clean) Start loading Friends and Followers.' ); // Reset lists. userObjectsBusyLoading = []; userObjects = []; // Remove all existing user links spans and brs. $( '.tff-span, .tff-br' ).remove(); $( '.tff-processed' ).removeClass( 'tff-processed' ); } else { return; } } else { doLog( '- Start loading Friends and Followers.' ); } // Special case for "Discover Users". if ( 'discover' == currentPage ) { $( '#discover .user_card_1_wrapper' ).each(function() { $currentUserLink = $( this ).find( '.tree_child_fullname' ); var userID; var classes = $( this ).find( '.follow_button' ).attr( 'class' ).split( ' ' ); var search = 'follow_button_'; for ( var i = classes.length - 1; i >= 0; i-- ) { if ( search == classes[i].slice( 0, search.length ) ) { userID = classes[i].substr( search.length ); break; } } // Make the username a link to the profile. $currentUserLink.html( $( '<a/>', { href: '/users/' + userID, html: $currentUserLink.html() })); }); } // Find all users and process them. $allUserLinks = $( queryToLoad ); doLog( 'User links found: ' + $allUserLinks.length ); $allUserLinks.each(function() { var $currentUserLink = $( this ); // If this link is on a tooltip, ignore it! if ( $currentUserLink.closest( '.tooltipster-base' ).length ) { return; } // Try to get a numeric user id. var userID = $currentUserLink.attr( 'user_id' ); // Special case for comments, because the html is "a.evac_user" instead of a nested entry "div.evac_user a". if ( 'a' != $currentUserLink.prop( 'tagName' ).toLowerCase() ) { $currentUserLink = $currentUserLink.find( 'a:first' ); } // If no link has been found, continue with the next one. Fail-safe. if ( 0 === $currentUserLink.length ) { return; } // If the user link has already been processed, continue with the next one. if ( $currentUserLink.hasClass( 'tff-processed' ) || $currentUserLink.siblings( '.tff-span' ).length ) { return; } // Add a new <span> element to the user link. var $userLinkSpan = $( '<span/>', { html: '<img class="tff-loader-wheel" src="/assets/loader.gif" title="Loading..." />', 'class': 'tff-span' } ); $currentUserLink.after( $userLinkSpan ); // Special case for these pages, to make it look nicer and fitting. if ( 'friends' == currentPage || 'followers' == currentPage || 'following' == currentPage || 'discover' == currentPage ) { $userLinkSpan.before( '<br class="tff-br" />' ); } // Get the user info from the link. var userName = $currentUserLink.text().trim(); var userUrl = $currentUserLink.attr( 'href' ); // Special case for the Discover page to get the userID. if ( 'discover' == currentPage ) { userID = userUrl.split( '/' )[2]; } else { userID = userID || userUrl.split( '/' )[1]; } // Check if the current user has already been loaded. if ( userBusyLoading( userID, true ) ) { refreshUserObject( userID, $userLinkSpan, 0 ); $currentUserLink.addClass( 'tff-processed' ); return; } var loadedUserObject = userLoaded( userID ); if ( loadedUserObject instanceof UserObject ) { refreshUserObject( loadedUserObject, $userLinkSpan, 0 ); $currentUserLink.addClass( 'tff-processed' ); return; } // Load the numbers from the user profile page. $( '<span/>' ).load( userUrl + ' .profile_details .numbers', function( response, status ) { if ( 'success' == status ) { var $numbers = $( 'a', this ); var $friends = $( $numbers[0] ); var friendsUrl = $friends.attr( 'href' ); var friendsCount = $friends.find( 'span' ).text(); var $friendsLink = $( '<a/>', { title: 'Friends', href: friendsUrl, html: friendsCount }); var $followers = $( $numbers[1] ); var followersUrl = $followers.attr( 'href' ); var followersCount = $followers.find( 'span' ).text(); var $followersLink = $( '<a/>', { title: 'Followers', href: followersUrl, html: followersCount }); // Add the user details after the user link. refreshUserObject( userID, $userLinkSpan ); addUserObject( userID, userName, userUrl, $friendsLink, friendsUrl, friendsCount, $followersLink, followersUrl, followersCount ); $currentUserLink.addClass( 'tff-processed' ); } else if ( 'error' == status ) { doLog( response, 'e' ); $(this).html( 'n/a' ); } // Make sure to set the user as finished loading. finishedLoading( userID ); }); }); } var busyLoading = false; /** * If already busy loading, just wait a while before initiating another load. */ var delayLoadFriendsAndFollowers = function() { if ( ! busyLoading ) { loadFriendsAndFollowers(); setTimeout(function() { busyLoading = false; }, 2000 ); } busyLoading = true; }; /** * UserObject "class". * @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. * @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. * @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. */ function UserObject( userID, userName, userUrl, $friendsLink, friendsUrl, friendsCount, $followersLink, followersUrl, followersCount ) { this.userID = userID.trim(); this.userName = userName.trim(); this.userUrl = userUrl.trim(); this.$friendsLink = $friendsLink; this.friendsUrl = friendsUrl.trim(); this.friendsCount = friendsCount.trim(); this.$followersLink = $followersLink; this.followersUrl = followersUrl.trim(); this.followersCount = followersCount.trim(); // Return a clone of the Friends page link. this.getFriendsLink = function() { return this.$friendsLink.clone(); }; // Return a clone of the Followers page link. this.getFollowersLink = function() { return this.$followersLink.clone(); }; } /** * Add a user object to the userObjects array. * See param info for UserObject(). */ function addUserObject( userID, userName, userUrl, $friendsLink, friendsUrl, friendsCount, $followersLink, followersUrl, followersCount ) { userObjects.push( new UserObject( userID, userName, userUrl, $friendsLink, friendsUrl, friendsCount, $followersLink, followersUrl, followersCount ) ); doLog( '(' + userID + ':' + userName + ') New user loaded.' ); } /** * Check to see if the user is still being loaded. * @param {integer|string} userID User ID as a numeric value or username string. * @param {boolean} setLoading If the user isn't being loaded yet, should it be set as loading? * @return {boolean} If the user is busy loading. */ function userBusyLoading( userID, setLoading ) { for ( var i = userObjectsBusyLoading.length - 1; i >= 0; i-- ) { if ( userID == userObjectsBusyLoading[i] ) { return true; } } // If the user isn't loading and it should be set, add to loading list. if ( setLoading ) { userObjectsBusyLoading.push( userID ); } return false; } /** * A user object has finished loading, remove it from the busyLoading array. * @param {integer|string} userID User ID as a numeric value or username string. */ function finishedLoading( userID ) { for ( var i = userObjectsBusyLoading.length - 1; i >= 0; i-- ) { if ( userID == userObjectsBusyLoading[i] ) { doLog( '(id:' + userID + ') Finished loading.' ); delete userObjectsBusyLoading[i]; return; } } } /** * Check if a user has already been loaded, if so, return the requested user object. * @param {integer|string} userID User ID as a numeric value or username string. * @return {boolean|UserObject} If the user has been loaded, return user object, else return false. */ function userLoaded( userID ) { for ( var i = userObjects.length - 1; i >= 0; i-- ) { if ( userID == userObjects[i].userID ) { doLog( '(' + userObjects[i].userID + ':' + userObjects[i].userName + ') Already loaded.' ); return userObjects[i]; } } doLog( '(id:' + userID + ') Not loaded yet.' ); return false; } /** * Refresh the passed $userLinkSpan with the user details. * @param {integer|UserObject} userID The userID or userObject to get the details from. * @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. */ function refreshUserObject( userID, $userLinkSpan, tries ) { if ( undefined === tries || null === tries ) { tries = 0; } // If the user object has been passed, use it and reset the userID. Otherwise, get the loaded user object if available. var userObject; if ( userID instanceof UserObject ) { userObject = userID; userID = userObject.userID; } else { userObject = userLoaded( userID ); } // If the maximum tries has been exeeded, return. if ( tries > maxTries ) { // Just remove the failed link span, maybe it will work on the next run. $userLinkSpan.remove(); doLog( '(id:' + userID + ') Maximum tries exeeded!', 'w' ); return; } if ( userObject instanceof UserObject ) { // Add the user details after the user link. $userLinkSpan.empty().append( userObject.getFriendsLink(), userObject.getFollowersLink() ); doLog( '(' + userObject.userID + ':' + userObject.userName + ') Friends and Followers set.' ); } else { setTimeout(function() { refreshUserObject( userID, $userLinkSpan, tries + 1); }, 500); } } // The elements that we are observing. Get set in getCurrentPage(). var queryToObserve; /** * Start observing for DOM changes. */ function startObserver() { // The discover page needs special treatement here, as it doesn't need an observer. if ( 'discover' == currentPage ) { return; } doLog( 'Start Observer.', 'i' ); // Check if we can use the MutationObserver. if ( 'MutationObserver' in window ) { var toObserve = document.querySelector( queryToObserve ); if ( toObserve ) { var observer = new MutationObserver( function( mutations ) { doLog( mutations.length + ' DOM changes.' ); doLog( mutations ); var reload = false; // Ignore post and comment time updates. for ( var i = mutations.length - 1; i >= 0; i-- ) { var classes = mutations[i].target.className; if ( ! classes.contains( 'comment_time_from_now' ) && ! classes.contains( 'time_to_update' ) ) { reload = true; break; } } if ( reload ) { delayLoadFriendsAndFollowers(); } }); // Observe child and subtree changes. 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() { waitForKeyElements( queryToLoad, delayLoadFriendsAndFollowers ); }); } } /** * Add the required CSS rules. */ function addCSS() { doLog( 'Added CSS.' ); $( '<style>' ) .html( '\ .tff-span a {\ font-size: smaller;\ margin-left: 5px;\ border-radius: 3px;\ background-color: #1ABC9C;\ color: #fff !important;\ padding: 2px 4px 0;\ }\ .tff-span .tff-loader-wheel {\ margin-left: 5px;\ height: 12px;\ }\ .user_card_1_wrapper .tree_child_fullname {\ height: 32px !important;\ }\ #tff-menuitem-update a:before {\ display: none !important;\ }\ ') .appendTo( 'head' ); } /** * Add sub nav items to the menu. */ function addSubNavItems() { doLog( 'Loading Sub Nav Items...' ); // Load Friends and Followers if ( 0 === $( '#tff-menuitem-load-friends-and-followers' ).length ) { var $loadFriendsAndFollowersAnchor = $( '<a/>', { html: 'Load Friends and Followers' } ); $loadFriendsAndFollowersAnchor.click( function() { loadFriendsAndFollowers( true ); } ); var $loadFriendsAndFollowersListItem = $( '<li/>', { 'id': 'tff-menuitem-load-friends-and-followers' ,html: $loadFriendsAndFollowersAnchor } ); $( '#navBarHead .sub_nav' ).append( $loadFriendsAndFollowersListItem ); doLog( '- "Load Friends and Followers" appended.' ); } doLog( 'Sub Nav Items loaded.' ); } /** * 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 ( debug ) { switch( level ) { case 'i': console.info( logMessage ); break; case 'w': console.warn( logMessage ); break; case 'e': console.error( logMessage ); break; default: console.log( logMessage ); } if ( alsoAlert ) { alert( logMessage ); } } } /** * Get the remote version on GitHub and add a menuitem and notification if a newer version is found. */ function checkRemoteVersion() { $.getJSON( getVersionAPIURL, function ( response ) { var remoteVersion = parseFloat( base64_decode( response.content ) ); doLog( 'Versions: Local (' + localVersion + '), Remote (' + remoteVersion + ')', 'i' ); // Check if there is a newer version available. if ( remoteVersion > localVersion ) { // Change the background color of the name tab on the top right. $( '#navBarHead .tab.name' ).css( 'background-color', '#F1B054' ); // Make sure the update link doesn't already exist! if ( 0 === $( '#tff-menuitem-update' ).length ) { var $updateLink = $( '<a/>', { title: 'Update Friends and Followers script to the newest version (' + remoteVersion + ')', href: scriptURL, html: 'Update Tsu F&F!' }) .attr( 'target', '_blank' ) // Open in new window / tab. .css( { 'background-color' : '#F1B054', 'color' : '#fff' } ) // White text on orange background. .click(function() { if ( ! confirm( 'Upgrade to the newest version (' + remoteVersion + ')?\n\n(refresh this page after the script has been updated)' ) ) { return false; } }); $( '<li/>', { 'id': 'tff-menuitem-update', html: $updateLink } ) .appendTo( '#navBarHead .sub_nav' ); } } }) .fail(function() { doLog( 'Couldn\'t get remote version number for TFF.', 'w' ); }); } })();