Tsu Helper

Tsu script that adds a bunch of tweaks to make Tsu more user friendly.

当前为 2015-01-08 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Tsu Helper
  3. // @namespace tsu-helper
  4. // @description Tsu script that adds a bunch of tweaks to make Tsu more user friendly.
  5. // @include http://*tsu.co*
  6. // @include https://*tsu.co*
  7. // @version 2.2
  8. // @author Armando Lüscher
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12. /**
  13. * For changelog see https://j.mp/tsu-helper-changelog
  14. */
  15.  
  16. /**
  17. * How nice of you to visit! I've tried to make this code as clean as possible with lots of
  18. * comments for everybody to learn from.
  19. *
  20. * Because that is what this life is about, to learn from each other and to grow together!
  21. *
  22. * If you have any questions, ideas, feature requests, (pretty much anything) about it, just ask :-)
  23. *
  24. * Simply visit the GitHub page here: https://j.mp/tsu-helper-issues and choose "New Issue".
  25. * I will then get back to you as soon as I can ;-)
  26. */
  27.  
  28. // Make sure we have jQuery loaded.
  29. if ( ! ( 'jQuery' in window ) ) { return; }
  30.  
  31. // Run everything as soon as the DOM is set up.
  32. jQuery( document ).ready(function( $ ) {
  33.  
  34. // Display Debug options? (for public).
  35. var publicDebug = false;
  36.  
  37. /**
  38. * Base64 library, just decoder: http://www.webtoolkit.info/javascript-base64.html
  39. * @param {string} e Base64 string to decode.
  40. */
  41. 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;}
  42.  
  43. // Check if a string starts with a certain string.
  44. 'function'!=typeof String.prototype.startsWith&&(String.prototype.startsWith=function(t){return this.slice(0,t.length)==t;});
  45.  
  46. // Check if a string ends with a certain string.
  47. 'function'!=typeof String.prototype.endsWith&&(String.prototype.endsWith=function(t){return this.slice(-t.length)==t;});
  48.  
  49. // Check if a string contains a certain string.
  50. 'function'!=typeof String.prototype.contains&&(String.prototype.contains=function(t){return this.indexOf(t)>=0;});
  51.  
  52. // Add stringify to jQuery (https://gist.github.com/chicagoworks/754454).
  53. 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?']':'}');}});
  54.  
  55. // Serialize form data to save settings (http://stackoverflow.com/questions/1184624/convert-form-data-to-js-object-with-jquery).
  56. $.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;};
  57.  
  58. /**
  59. * Like WP, return "selected" attribute text.
  60. * @param {string} val1 Value to compare.
  61. * @param {string} val2 Value to compare with.
  62. * @return {string} "Selected" text or nothing.
  63. */
  64. function selected( val1, val2 ) {
  65. return ( val1 === val2 ) ? ' selected="selected"' : '';
  66. }
  67.  
  68. /**
  69. * Like WP, return "checked" attribute text.
  70. * @param {string} val1 Value to compare.
  71. * @param {string} val2 Value to compare with.
  72. * @return {string} "Checked" text or nothing.
  73. */
  74. function checked( val1, val2 ) {
  75. // Compare to "true" by default.
  76. val2 = ( undefined === val2 ) ? true : val2;
  77. return ( val1 === val2 ) ? ' checked="checked"' : '';
  78. }
  79.  
  80.  
  81. /**
  82. * All settings related methods and variables.
  83. */
  84. var Settings = {
  85. // All available settings with default values.
  86. settingsDefault : {
  87. debugLevel : 'disabled', // Debugging level. (disabled,[l]og,[i]nfo,[w]arning,[e]rror)
  88. hideAds : false, // Hide all ads.
  89. showFFC : 'hovercard', // Show Friends and Followers count. (disabled,all,hovercard)
  90. quickMention : true, // Add quick mention links.
  91. emphasizeNRP : true, // Emphasize nested replies parents.
  92. checkSocial : true, // Check the social network sharing.
  93. checkMaxHM : true, // Check for maximum hashtags and mentions.
  94. notifReloaded : 10 // How many items to display on the Notifications popup (0=disabled)
  95. },
  96.  
  97. // Init with default settings on "load".
  98. settings : {},
  99.  
  100. // Name used for the settings cookie.
  101. cookieName : 'tsu-helper-settings',
  102.  
  103. /**
  104. * Set default settings.
  105. */
  106. setDefaults : function( $form ) {
  107. Settings.populateForm( $form, true );
  108. },
  109.  
  110. /**
  111. * Load settings from cookie.
  112. */
  113. load : function() {
  114. // Init with defaults and add all loaded settings.
  115. $.extend( true, Settings.settings, Settings.settingsDefault );
  116.  
  117. var savedJSON = $.cookie( Settings.cookieName );
  118. if ( savedJSON ) {
  119. $.extend( Settings.settings, $.parseJSON( savedJSON ) );
  120. }
  121.  
  122. return Settings.settings;
  123. },
  124.  
  125. /**
  126. * Populate the passed form with the current settings.
  127. * @param {jQuery} $form The form to be populated.
  128. * @param {boolean} defaults Load the default values?
  129. */
  130. populateForm : function( $form, defaults ) {
  131. if ( $form ) {
  132. for ( var setting in Settings.settings ) {
  133. if ( Settings.settings.hasOwnProperty( setting ) ) {
  134. var $input = $( '[name=' + setting + ']', $form );
  135. var val = ( defaults ) ? Settings.settingsDefault[ setting ] : Settings.settings[ setting ];
  136. if ( 'checkbox' === $input.attr( 'type' ) ) {
  137. $input.prop( 'checked', val );
  138. } else {
  139. $input.val( val );
  140. }
  141. }
  142. }
  143. }
  144. },
  145.  
  146. /**
  147. * Save settings to cookie.
  148. */
  149. save : function( $form ) {
  150. // First save?
  151. if ( undefined === $.cookie( Settings.cookieName ) && ! confirm( 'Settings will be saved in a cookie. Ok?' ) ) {
  152. return false;
  153. }
  154.  
  155. // If a form is passed, use those values.
  156. if ( $form ) {
  157. // Default to false and then get form settings.
  158. // This is necessary for checkbox inputs as they are assigned by checked state, not value.
  159. Settings.settings.hideAds = false;
  160. Settings.settings.quickMention = false;
  161. Settings.settings.emphasizeNRP = false;
  162. Settings.settings.checkSocial = false;
  163. Settings.settings.checkMaxHM = false;
  164.  
  165. $.extend( Settings.settings, $form.serializeObject() );
  166. }
  167.  
  168. $.cookie( Settings.cookieName, $.stringify( Settings.settings ), { expires: 999, path: '/' } );
  169. return true;
  170. }
  171. };
  172. // Load settings from cookie.
  173. var settings = Settings.load();
  174.  
  175.  
  176. /**
  177. * All updater and version related variables.
  178. */
  179. var Updater = {
  180. // The local version.
  181. localVersion : 2.2,
  182.  
  183. // The remote version (loaded in the "check" method).
  184. remoteVersion : null,
  185.  
  186. // URL where to get the newest script.
  187. scriptURL : 'https://openuserjs.org/install/noplanman/Tsu_Helper.user.js',
  188.  
  189. // Version details.
  190. versionAPIURL : 'https://api.github.com/repos/noplanman/tsu-helper/contents/VERSION',
  191.  
  192. // Get the remote version on GitHub.
  193. init : function() {
  194. try {
  195. var response = $.ajax({
  196. type: 'GET',
  197. url: Updater.versionAPIURL,
  198. async: false
  199. }).fail(function() {
  200. doLog( 'Couldn\'t get remote version number for Tsu Helper.', 'w' );
  201. }).responseJSON;
  202.  
  203. // Set the remote version.
  204. Updater.remoteVersion = parseFloat( base64_decode( response.content ) );
  205. doLog( 'Versions: Local (' + Updater.localVersion + '), Remote (' + Updater.remoteVersion + ')', 'i' );
  206. } catch( e ) {
  207. doLog( 'Couldn\'t get remote version number for Tsu Helper.', 'w' );
  208. }
  209. },
  210.  
  211. /**
  212. * Is there a newer version available?
  213. * @return {Boolean} If there is a newer version available.
  214. */
  215. hasUpdate : function() {
  216. return ( Updater.remoteVersion > Updater.localVersion );
  217. }
  218. };
  219. // Initialise the updater to fetch the remote version.
  220. Updater.init();
  221.  
  222.  
  223. /**
  224. * TSU constants.
  225. */
  226. var TSUConst = {
  227. // Define the maximum number of hashtags and mentions allowed.
  228. maxHashtags : 10,
  229. maxMentions : 10,
  230.  
  231. // Cards per page request of Friends, Followers and Following tabs.
  232. ffmCardsPerPage : 12
  233. };
  234.  
  235.  
  236. /**
  237. * Page related things.
  238. */
  239. var Page = {
  240.  
  241. // The current page.
  242. current : '',
  243.  
  244. /**
  245. * Get the current page to know which queries to load and observe and
  246. * also for special cases of how the Friends and Followers details are loaded.
  247. */
  248. init : function() {
  249. doLog( 'Getting current page.', 'i' );
  250.  
  251. if ( $( 'body.newsfeed' ).length ) { Page.current = 'home'; // Home feed.
  252. } else if ( $( 'body.notifications.show' ).length
  253. || $( 'body.notifications.index' ).length ) { Page.current = 'notifications'; // Show notifications.
  254. } else if ( $( 'body.search_hashtag' ).length ) { Page.current = 'hashtag'; // Hashtag page.
  255. } else if ( $( 'body.profile.diary' ).length ) { Page.current = 'diary'; // Diary.
  256. } else if ( $( 'body.show_post' ).length ) { Page.current = 'post'; // Single post.
  257. } else if ( $( 'body.discover' ).length ) { Page.current = 'discover'; // Discover Users.
  258. Observer.queryToLoadFF = 'body.discover .tree_child_fullname';
  259. // No observer necessary!
  260. // TODO: Also, with observer, this page goes crazy...
  261. Observer.queryToObserve = '';
  262. } else if ( $( 'body.tree' ).length ) { Page.current = 'tree'; // Family tree.
  263. Observer.queryToLoadFF = 'body.tree .tree_child_fullname';
  264. Observer.queryToObserve = '.tree_page';
  265. } else if ( $( 'body.profile.friends' ).length ) { Page.current = 'friends'; // Friends.
  266. } else if ( $( 'body.profile.followers' ).length ) { Page.current = 'followers'; // Followers.
  267. } else if ( $( 'body.profile.following' ).length ) { Page.current = 'following'; // Following.
  268. } else if ( $( 'body.messages' ).length ) { Page.current = 'messages'; // Messages.
  269. Observer.queryToLoad = '.messages_content .message_box';
  270. Observer.queryToLoadFF = '.message_box .content';
  271. Observer.queryToObserve = '.messages_content';
  272. }
  273.  
  274. // Group queries to load.
  275. if ( Page.is( 'has-posts' ) ) {
  276. queryToLoad = '.comment';
  277. // Add userlinks to query?
  278. switch ( settings.showFFC ) {
  279. case 'all' : Observer.queryToLoadFF = '.card .card_sub .info, .evac_user'; break;
  280. case 'hovercard' : Observer.queryToLoadFF = '.card .card_sub .info'; break;
  281. }
  282. } else if ( 'all' === settings.showFFC && Page.is( 'fff' ) ) {
  283. Observer.queryToLoadFF = '.card .card_sub .info';
  284. }
  285.  
  286. Observer.queryToLoad += ',' + Observer.queryToLoadFF;
  287.  
  288. doLog( 'Current page: ' + Page.current, 'i' );
  289. },
  290.  
  291. /**
  292. * Check if the passed page is the current one.
  293. * @param {string} pages Comma seperated list of pages.
  294. * @return {boolean} If the current page is in the list.
  295. */
  296. is : function( pages ) {
  297. // To make things easier, allow shortcuts.
  298. pages = pages.replace( /has-userlinks/g, 'has-posts fff messages discover tree' );
  299. pages = pages.replace( /has-posts/g, 'home hashtag diary post' );
  300. pages = pages.replace( /fff/g, 'friends followers following' );
  301.  
  302. // Make an array.
  303. pages = pages.split( ' ' );
  304.  
  305. // Is the current page in the passed page list?
  306. for ( var i = pages.length - 1; i >= 0; i-- ) {
  307. if ( Page.current === pages[i] ) {
  308. return true;
  309. }
  310. }
  311. return false;
  312. }
  313. };
  314.  
  315.  
  316. /**
  317. * Friends and Followers Manager.
  318. */
  319. var FFM = {
  320.  
  321. // The total amount of pages to preload.
  322. totalPages : 0,
  323.  
  324. // The real number of Friends / Followers / Following.
  325. totalFFFs : 0,
  326. $totalFFFs : null,
  327.  
  328. // List of active Ajax requests.
  329. ajaxRequests : {},
  330.  
  331. // The current chain request.
  332. $ajaxChainRequest : null,
  333.  
  334. // Is the search busy?
  335. busy : false,
  336. chainBusy : false,
  337.  
  338. // The number of found friend requests.
  339. counter : {
  340. fandf : 0,
  341. received : 0,
  342. sent : 0
  343. },
  344. filterCheckboxes : {},
  345.  
  346. // The buttons and status text.
  347. $linkCancel : null,
  348. $linkStart : null,
  349. $statusText : null,
  350. $linkUnfollowFriends : null,
  351.  
  352. /**
  353. * Initialise the Friends and Followers Manager.
  354. */
  355. init : function() {
  356. // Make sure we're on the right page.
  357. if ( ! Page.is( 'fff' ) ) {
  358. return;
  359. }
  360.  
  361. doLog( 'Loading FF Manager.', 'i' );
  362.  
  363. // Only possible on the current users profile.
  364. if ( window.current_user.username === $( '.profile_details .summary .username' ).text().trim().substring( 1 ) ) {
  365. var $title = $( '.profiles_list .title' );
  366.  
  367. // Get the number of pages required to load all users in the list, 12 per page.
  368. FFM.totalPages = Math.ceil( /\d+/.exec( $title.text() ) / TSUConst.ffmCardsPerPage ) || 1;
  369. doLog( 'Total number of pages to load: ' + FFM.totalPages, 'i' );
  370. // As this number isn't totally correct, load all the pages
  371. // and chain-load from the last page as far as needed.
  372.  
  373. // Real number of FFFs.
  374. FFM.$totalFFFs = $( '<span/>', {
  375. 'id' : 'th-ffm-total-fffs',
  376. title : 'Correct count',
  377. html : '-'
  378. })
  379. .hide() // Start hidden.
  380. .appendTo( $title );
  381.  
  382. // Cancel link.
  383. FFM.$linkCancel = $( '<a/>', {
  384. 'id' : 'th-ffm-link-cancel',
  385. title : 'Cancel current search',
  386. html : 'Cancel',
  387. click : function() { FFM.cancel(); }
  388. })
  389. .hide() // Start hidden.
  390. .appendTo( $title );
  391.  
  392. // Start link.
  393. FFM.$linkStart = $( '<a/>', {
  394. 'id' : 'th-ffm-link-start',
  395. title : 'Search for pending Friend Requests and Friends you also Follow.',
  396. html : 'F&F Manager',
  397. click : function() { FFM.start(); }
  398. })
  399. .appendTo( $title );
  400.  
  401. // Status text to display the number of found items.
  402. FFM.$statusText = $( '<span/>', {
  403. 'id' : 'th-ffm-status-text',
  404. html :
  405. '<label title="Friends also being Followed" class="th-ffm-card-fandf"><span>0</span> F&F</label>&nbsp;' +
  406. '<label title="Received Friend Requests" class="th-ffm-card-received"><span>0</span> received</label>&nbsp;' +
  407. '<label title="Sent Friend Requests" class="th-ffm-card-sent"><span>0</span> sent</label>'
  408. })
  409. .hide() // Start hidden.
  410. .appendTo( $title );
  411.  
  412. // Assign checkbox clicks to show / hide results.
  413. $( '<input/>', { 'id' : 'th-ffm-cb-fandf', type : 'checkbox', checked : 'checked' } ).change( function() { FFM.filter( 'fandf', this.checked ); } ).prependTo( FFM.$statusText.find( '.th-ffm-card-fandf' ) );
  414. $( '<input/>', { 'id' : 'th-ffm-cb-received', type : 'checkbox', checked : 'checked' } ).change( function() { FFM.filter( 'received', this.checked ); } ).prependTo( FFM.$statusText.find( '.th-ffm-card-received' ) );
  415. $( '<input/>', { 'id' : 'th-ffm-cb-sent', type : 'checkbox', checked : 'checked' } ).change( function() { FFM.filter( 'sent', this.checked ); } ).prependTo( FFM.$statusText.find( '.th-ffm-card-sent' ) );
  416.  
  417. FFM.$linkUnfollowFriends = $( '<a/>', {
  418. 'id' : 'th-ffm-unfollow-friends',
  419. title : 'Unfollow all Friends on this page',
  420. html : 'unfollow',
  421. click : function() { FFM.unfollowFriends(); }
  422. })
  423. .hide() // Start hidden.
  424. .prependTo( FFM.$statusText );
  425. }
  426. },
  427.  
  428. /**
  429. * Update the status text counts.
  430. * @param {string} type The type of card to count.
  431. */
  432. updateStatus : function( type ) {
  433. if ( null === type ) {
  434. FFM.$statusText.find( 'span' ).html( '0' );
  435. FFM.$totalFFFs.html( '-' );
  436. return;
  437. }
  438. FFM.$statusText.find( '.th-ffm-card-' + type + ' span' ).html( ++FFM.counter[ type ] );
  439. },
  440.  
  441. /**
  442. * Call getPage but with a delay to prevent flooding the server.
  443. * @param {integer} delay Delay in ms.
  444. * @param {integer} pageNr The page number to get.
  445. * @param {boolean} isChain If this a chain request, continue getting consecutive pages.
  446. */
  447. getPageDelay : function( delay, pageNr, isChain ) {
  448. setTimeout( function() { FFM.getPage( pageNr, isChain ); }, delay );
  449. },
  450.  
  451. /**
  452. * Get a page of Follower/Following cards.
  453. * @param {integer} pageNr The page number to get.
  454. * @param {boolean} isChain If this a chain request, continue getting consecutive pages.
  455. */
  456. getPage : function( pageNr, isChain ) {
  457. // Has the user cancelled the chain?
  458. if ( isChain && ! FFM.chainBusy ) {
  459. doLog( 'Jumped out of chain.', 'i' );
  460. return;
  461. }
  462.  
  463. // Make sure we have a valid page number.
  464. if ( pageNr < 1 ) {
  465. doLog( 'Invalid page number:' + pageNr, 'e' );
  466. return;
  467. }
  468.  
  469. doLog( 'Getting page ' + pageNr, 'l' );
  470.  
  471. var fetch_url = '/users/profiles/users_list/' + window.current_user.id + '/' + Page.current + '/' + pageNr;
  472.  
  473. var $ajaxCurrentRequest = $.get( fetch_url, function( data ) {
  474. // Get all the cards.
  475. var $cards = $( data ).siblings( '.card' );
  476.  
  477. // If we have only 1 card, there are no siblings, so get the card itself.
  478. if ( 0 === $cards.length ) {
  479. $cards = $( data );
  480. }
  481.  
  482. // Count each card to determine correct total count.
  483. FFM.totalFFFs += $cards.length;
  484. FFM.$totalFFFs.html( FFM.totalFFFs );
  485.  
  486. if ( $cards.length ) {
  487. // Flag each card and add to stack.
  488. $cards.each(function() {
  489. var $card = $( this );
  490.  
  491. // Is this card a friend?
  492. var $friendButton = $card.find( '.friend_button.grey' );
  493. if ( $friendButton.length ) {
  494.  
  495. var $followButton = $card.find( '.follow_button.grey' );
  496. var type;
  497. // Find only respond and request links. Also show cards that are friends and following.
  498. if ( $friendButton.hasClass( 'friend_request_box' ) ) {
  499. type = 'received';
  500. } else if ( $friendButton.attr( 'href' ).contains( '/cancel/' ) ) {
  501. type = 'sent';
  502. } else if ( $followButton.length ) {
  503. type = 'fandf';
  504. } else {
  505. // Next card.
  506. return;
  507. }
  508.  
  509. // Add card and update the status text.
  510. $( '.profiles_list .card:not(.th-ffm-card):first' ).before( $card.addClass( 'th-ffm-card th-ffm-card-' + type ) );
  511. FFM.updateStatus( type );
  512. }
  513. });
  514. }
  515.  
  516. // Are there more pages to load?
  517. if ( isChain ) {
  518. if ( $( data ).siblings( '.loadmore_profile' ).length ) {
  519. // Get the next page.
  520. FFM.getPage( pageNr + 1, true );
  521. } else {
  522. doLog( 'Chain completed on page ' + pageNr, 'i' );
  523. FFM.chainBusy = false;
  524. }
  525. }
  526. })
  527. .fail(function( xhr, status, error ) {
  528. if ( 'abort' === status ) {
  529. doLog( 'Aborted page ' + pageNr, 'e' );
  530. } else {
  531. doLog( 'Error on page ' + pageNr + '! (' + status + ':' + error + ')', 'e' );
  532. }
  533. })
  534. .always(function() {
  535. doLog( 'Finished page ' + pageNr, 'l' );
  536. // After the request is complete, remove it from the array.
  537. delete FFM.ajaxRequests[ pageNr ];
  538. FFM.checkIfFinished();
  539. });
  540.  
  541. // If this is a chain request, set the $ajaxChainRequest variable.
  542. // If not, add it to the requests array.
  543. if ( isChain ) {
  544. FFM.$ajaxChainRequest = $ajaxCurrentRequest;
  545. } else {
  546. FFM.ajaxRequests[ pageNr ] = $ajaxCurrentRequest;
  547. }
  548. },
  549.  
  550. /**
  551. * Check if the Friend Request search is finished.
  552. */
  553. checkIfFinished : function() {
  554. var activeRequests = Object.keys( FFM.ajaxRequests ).length;
  555. doLog( activeRequests + ' pages left.', 'l' );
  556.  
  557. // If no chain is active and the requests are completed, we're finished!
  558. if ( ! FFM.chainBusy && 0 === activeRequests ) {
  559. FFM.finished();
  560. }
  561. },
  562.  
  563. /**
  564. * Start the FFM.
  565. */
  566. start : function() {
  567. if ( ! confirm( 'WARNING: This may temporarily block access to Tsu!\n\nAre you really sure you want to start the F&F Manager?' ) ) {
  568. return;
  569. }
  570.  
  571. FFM.busy = FFM.chainBusy = true;
  572. FFM.totalFFFs = FFM.counter.received = FFM.counter.sent = FFM.counter.fandf = 0;
  573. FFM.updateStatus( null );
  574. FFM.$statusText.find( 'input' ).attr( 'disabled', true ).prop( 'checked', true );
  575. FFM.$statusText.show();
  576. FFM.$totalFFFs.show();
  577. FFM.$linkUnfollowFriends.hide();
  578.  
  579. // Clear any previous results.
  580. $( '.th-ffm-card' ).remove();
  581.  
  582. // Load all pages and start the chain loading on the last page.
  583. for ( var i = FFM.totalPages; i >= 1; i-- ) {
  584. FFM.getPageDelay( 50, i, i === FFM.totalPages );
  585. }
  586.  
  587. FFM.$linkStart.hide();
  588. FFM.$linkCancel.show();
  589. },
  590.  
  591.  
  592. /**
  593. * Cancel the FFM.
  594. */
  595. cancel : function() {
  596. // Call finished with the cancelled parameter.
  597. FFM.finished( true );
  598. },
  599.  
  600. /**
  601. * Finish off the FFM.
  602. * @param {boolean} cancelled If the FFM has been cancelled.
  603. */
  604. finished : function( cancelled ) {
  605. FFM.busy = FFM.chainBusy = false;
  606. FFM.$statusText.find( 'input' ).removeAttr( 'disabled' );
  607.  
  608. // If followed riends are found, show the "unfollow" button.
  609. if ( FFM.counter.fandf ) {
  610. FFM.$linkUnfollowFriends.show();
  611. }
  612.  
  613. if ( cancelled ) {
  614. // Abort all AJAX requests.
  615. for ( var page in FFM.ajaxRequests ) {
  616. FFM.ajaxRequests[ page ].abort();
  617. delete FFM.ajaxRequests[ page ];
  618. }
  619.  
  620. // Abort the current AJAX chain request.
  621. FFM.$ajaxChainRequest.abort();
  622.  
  623. // The total number is incomplete, so hide it.
  624. FFM.$totalFFFs.hide();
  625. }
  626.  
  627. FFM.$linkCancel.hide();
  628. FFM.$linkStart.show();
  629. },
  630.  
  631. /**
  632. * Display / Hide certain categories.
  633. * @param {string} type The type of cards to display / hide.
  634. * @param {boolean} state True: display, False: hide.
  635. */
  636. filter : function( type, state ) {
  637. if ( state ) {
  638. $( '.th-ffm-card.th-ffm-card-' + type ).show();
  639. } else {
  640. $( '.th-ffm-card.th-ffm-card-' + type ).hide();
  641. }
  642. },
  643.  
  644. /**
  645. * Automatically Unfollow all the loaded Friends.
  646. */
  647. unfollowFriends : function() {
  648. var $toUnfollow = $( '.th-ffm-card.th-ffm-card-fandf .follow_button.grey' );
  649. if ( $toUnfollow.length && confirm( 'WARNING: This may temporarily block access to Tsu!\n\nAre you really sure you want to Unfollow all ' + $toUnfollow.length + ' Friends on this page?\nThey will still be your Friends.\n\n(this cannot be undone and may take some time, be patient)' ) ) {
  650. var unfollowed = 0;
  651. $toUnfollow.each(function() {
  652. this.click();
  653. unfollowed++;
  654. });
  655. FFM.$linkUnfollowFriends.hide();
  656. alert( unfollowed + ' Friends have been Unfollowed!');
  657. }
  658. }
  659. };
  660.  
  661.  
  662. /**
  663. * Quick Mention links.
  664. */
  665. var QM = {
  666.  
  667. // The currently active textarea to insert the @mentions.
  668. $activeReplyTextArea : null,
  669.  
  670. /**
  671. * Add text to the passed textarea input field.
  672. * @param {jQuery} $textArea jQuery object of the textarea input field.
  673. * @param {string} text Text to add.
  674. */
  675. addTextToTextArea : function( $textArea, text ) {
  676. if ( $textArea ) {
  677. var textAreaText = $textArea.val();
  678. var caretPos1 = $textArea[0].selectionStart;
  679. var caretPos2 = $textArea[0].selectionEnd;
  680. $textArea.val( textAreaText.substring( 0, caretPos1 ) + text + textAreaText.substring( caretPos2 ) );
  681. $textArea[0].selectionStart = $textArea[0].selectionEnd = caretPos1 + text.length;
  682. $textArea.focus();
  683. }
  684. },
  685.  
  686. /**
  687. * Add the @mention links to the replies.
  688. */
  689. load : function() {
  690. // Make sure the setting is enabled and we're on the right page.
  691. if ( ! settings.quickMention || ! Page.is( 'has-posts' ) ) {
  692. return;
  693. }
  694.  
  695. doLog( 'Adding Quick Mention links.', 'i' );
  696.  
  697. // Process all reply links to autofocus the reply textarea input field.
  698. $( '.load_more_post_comment_replies' ).not( '.th-qm-reply-processed' ).each(function() {
  699. var $replyLink = $( this );
  700. $replyLink.click(function() {
  701. var $postComment = $replyLink.closest( '.post_comment' );
  702. var $replyContainer = $postComment.siblings( '.comment_reply_container' );
  703. var $textArea = $replyContainer.children( '.post_write_comment' ).find( '#comment_text' );
  704.  
  705. // This gets called before the "official" click, so the logic is inversed!
  706. // And delay everything a bit too, as it gets lazy-loaded.
  707. if ( $replyContainer.is( ':visible' ) ) {
  708. setTimeout(function() {
  709. // Only set the active textarea null if it's this one.
  710. if ( $textArea[0] === QM.$activeReplyTextArea[0] ) {
  711. QM.$activeReplyTextArea = null;
  712. // Hide all @ links.
  713. $( '.th-qm-reply' ).hide();
  714. }
  715. }, 100);
  716. } else {
  717. setTimeout(function() {
  718. $postComment.find( '.th-qm-reply' ).show();
  719. $textArea.focus();
  720. }, 100);
  721. }
  722. });
  723. $replyLink.addClass( 'th-qm-reply-processed' );
  724. });
  725.  
  726. // Process all comment / reply textarea input fields to set themselves as active on focus.
  727. $( '.post_write_comment #comment_text' ).not( '.th-qm-textarea-processed' ).each(function() {
  728. $( this ).focusin( function() {
  729. QM.$activeReplyTextArea = $( this );
  730. $( '.th-qm-active-input' ).removeClass( 'th-qm-active-input' );
  731. QM.$activeReplyTextArea.closest( '.expandingText_parent' ).addClass( 'th-qm-active-input' );
  732. });
  733. $( this ).addClass( 'th-qm-textarea-processed' );
  734. });
  735.  
  736. // Link for all comments.
  737. $( '.post_comment_header' ).not( '.th-qm-added' ).each(function() {
  738. var $head = $( this );
  739. var $commentArea = $head.closest( '.post_comment' );
  740.  
  741. // Get just the last part of the href, the username.
  742. var hrefBits = $head.find( 'a' ).attr( 'href' ).split( '/' );
  743. var atUsername = '@' + hrefBits[ hrefBits.length - 1 ] + ' ';
  744.  
  745. var $mentionLink = $( '<a/>', {
  746. class : 'th-qm-reply',
  747. html : '@ +',
  748. title : 'Add ' + atUsername + 'to current reply.',
  749. click : function() {
  750. QM.addTextToTextArea( QM.$activeReplyTextArea, atUsername );
  751. }
  752. })
  753. .hide(); // Start hidden, as it will appear with the mouse over event.
  754.  
  755. // Show / hide link on hover / blur if there is an active reply input selected.
  756. $commentArea.hover(
  757. function() { if ( QM.$activeReplyTextArea && QM.$activeReplyTextArea.length ) { $mentionLink.show(); } },
  758. function() { $mentionLink.hide(); }
  759. );
  760.  
  761. $head.addClass( 'th-qm-added' );
  762.  
  763. // Position the @ link.
  764. var $profilePic = $head.find( '.post_profile_picture' );
  765. var offset = $profilePic.position();
  766. $mentionLink.offset({ top: offset.top + $profilePic.height(), left: offset.left });
  767.  
  768. $head.append( $mentionLink );
  769. });
  770.  
  771. // Link for all textareas.
  772. $( '.post_write_comment' ).not( '.th-qm-added' ).each(function() {
  773. var $commentArea = $( this );
  774. var $commentInput = $commentArea.find( '#comment_text' );
  775. var $head = null;
  776. var linkElement = null;
  777. var isReply = $commentArea.hasClass( 'reply' );
  778.  
  779. // Is this a nested comment? Then use the previous reply as the username.
  780. if ( isReply ) {
  781. $head = $commentArea.closest( '.comment' ).find( '.post_comment .post_comment_header' );
  782. linkElement = 'a';
  783. } else {
  784. // Get the current post to determine the username.
  785. var $post = $commentArea.closest( '.post' );
  786.  
  787. // Defaults as if we have a shared post.
  788. $head = $post.find( '.share_header' );
  789. linkElement = '.evac_user a';
  790.  
  791. // If it's not a share, get the post header.
  792. if ( 0 === $head.length ) {
  793. $head = $post.find( '.post_header' );
  794. linkElement = '.post_header_pp a';
  795. }
  796. }
  797.  
  798. // Get just the last part of the href, the username.
  799. var hrefBits = $head.find( linkElement ).attr( 'href' ).split( '/' );
  800. var atUsername = '@' + hrefBits[ hrefBits.length - 1 ] + ' ';
  801.  
  802. var $mentionLink = $( '<a/>', {
  803. class : 'th-qm-comment',
  804. html : '@ >',
  805. title : 'Add ' + atUsername + 'to this ' + ( ( isReply ) ? 'reply.' : 'comment.' ),
  806. click : function() {
  807. QM.addTextToTextArea( $commentInput, atUsername );
  808. }
  809. })
  810. .hide() // Start hidden, as it will appear with the mouse over event.
  811.  
  812. // Show / hide link on hover / blur.
  813. $commentArea.hover(
  814. function() { $mentionLink.show(); },
  815. function() { $mentionLink.hide(); }
  816. );
  817.  
  818. $commentArea.addClass( 'th-qm-added' );
  819.  
  820. $commentArea.find( '.post_profile_picture' ).parent().after( $mentionLink );
  821. });
  822. }
  823. };
  824.  
  825.  
  826. /**
  827. * The MutationObserver to detect page changes.
  828. */
  829. var Observer = {
  830.  
  831. // The mutation observer object.
  832. observer : null,
  833.  
  834. // The elements that we are observing.
  835. queryToObserve : 'body',
  836. // The query of objects that trigger the observer.
  837. queryToLoad : '',
  838. // The query of userlinks to look for.
  839. queryToLoadFF : '',
  840.  
  841. /**
  842. * Start observing for DOM changes.
  843. */
  844. init : function() {
  845.  
  846. // Check if we can use the MutationObserver.
  847. if ( 'MutationObserver' in window ) {
  848. // Are we observing anything on this page?
  849. if ( '' === Observer.queryToObserve ) {
  850. return;
  851. }
  852.  
  853. var toObserve = document.querySelector( Observer.queryToObserve );
  854.  
  855. if ( toObserve ) {
  856.  
  857. doLog( 'Started Observer.', 'i' );
  858.  
  859. Observer.observer = new MutationObserver( function( mutations ) {
  860.  
  861. function itemsInArray( needles, haystack ) {
  862. for ( var i = needles.length - 1; i >= 0; i-- ) {
  863. if ( $.inArray( needles[ i ], haystack ) > -1 ) {
  864. return true;
  865. }
  866. }
  867. return false;
  868. }
  869.  
  870. // Helper to determine if added or removed nodes have a specific class.
  871. function mutationNodesHaveClass( mutation, classes ) {
  872. classes = classes.split( ' ' );
  873.  
  874. // Added nodes.
  875. for ( var ma = mutation.addedNodes.length - 1; ma >= 0; ma-- ) {
  876. // In case the node has no className (e.g. textnode), just ignore it.
  877. if ( mutation.addedNodes[ ma ].className && itemsInArray( mutation.addedNodes[ ma ].className.split( ' ' ), classes ) ) {
  878. return true;
  879. }
  880. }
  881.  
  882. // Removed nodes.
  883. for ( var mr = mutation.removedNodes.length - 1; mr >= 0; mr-- ) {
  884. // In case the node has no className (e.g. textnode), just ignore it.
  885. if ( mutation.removedNodes[ mr ].className && itemsInArray( mutation.removedNodes[ mr ].className.split( ' ' ), classes ) ) {
  886. return true;
  887. }
  888. }
  889. }
  890.  
  891. //doLog( mutations.length + ' DOM changes.' );
  892. doLog( mutations );
  893.  
  894. // Only react to changes we're interested in.
  895. for ( var m = mutations.length - 1; m >= 0; m-- ) {
  896. var $hoverCard = $( '.tooltipster-user-profile' );
  897.  
  898. // Are we on a hover card?
  899. if ( $hoverCard.length && mutationNodesHaveClass( mutations[ m ], 'tooltipster-user-profile' ) ) {
  900. FFC.loadUserHoverCard( $hoverCard.find( '.card .card_sub .info' ) );
  901. }
  902.  
  903. // Run all functions responding to DOM updates.
  904. // When loading a card, only if it's not a hover card, as those get loaded above.
  905. if ( mutationNodesHaveClass( mutations[ m ], 'post comment message_content_feed message_box tree_bar tree_child_wrapper' )
  906. || ( mutationNodesHaveClass( mutations[ m ], 'card' ) && $hoverCard.length == 0 ) ) {
  907. FFC.loadAll();
  908. QM.load();
  909. emphasizeNestedRepliesParents();
  910. tweakMessagesPage();
  911. }
  912. }
  913. });
  914.  
  915. // Observe child and subtree changes.
  916. Observer.observer.observe( toObserve, {
  917. childList: true,
  918. subtree: true
  919. });
  920. }
  921. } else {
  922. // If we have no MutationObserver, use "waitForKeyElements" function.
  923. // Instead of using queryToObserve, we wait for the ones that need to be loaded, queryToLoad.
  924. $.getScript( 'https://gist.github.com/raw/2625891/waitForKeyElements.js', function() {
  925.  
  926. doLog( 'Started Observer (waitForKeyElements).', 'i' );
  927.  
  928. // !! Specifically check for the correct page to prevent overhead !!
  929.  
  930. if ( Page.is( 'has-userlinks' ) ) {
  931. waitForKeyElements( Observer.queryToLoad, FFC.loadAll() );
  932. //waitForKeyElements( Observer.queryToLoad, delayLoadFriendsAndFollowers );
  933. }
  934. if ( Page.is( 'has-posts' ) ) {
  935. waitForKeyElements( Observer.queryToLoad, QM.load );
  936. waitForKeyElements( Observer.queryToLoad, emphasizeNestedRepliesParents );
  937. }
  938. if ( Page.is( 'messages' ) ) {
  939. waitForKeyElements( Observer.queryToLoad, tweakMessagesPage );
  940. }
  941. });
  942. }
  943. }
  944. };
  945.  
  946.  
  947. /**
  948. * Post related things.
  949. */
  950. var Posting = {
  951.  
  952. // Are we busy waiting for the popup to appear?
  953. waitingForPopup : false,
  954.  
  955. /**
  956. * Initialise.
  957. */
  958. init : function() {
  959. // Remind to post to FB and Twitter in case forgotten to click checkbox.
  960. $( '#create_post_form' ).submit(function( event ) {
  961. return Posting.postFormSubmit( $( this ), event );
  962. });
  963.  
  964. // Set focus to message entry field on page load.
  965. if ( Page.is( 'home diary' ) ) {
  966. $( '#text' ).focus();
  967. }
  968.  
  969. // When using the "Create" or "Message" buttons, wait for the post input form.
  970. $( 'body' ).on( 'click', '.create_post_popup, .message_pop_up', function() {
  971. if ( ! Posting.waitingForPopup ) {
  972. if ( $( this ).hasClass( 'create_post_popup' ) ) {
  973. Posting.waitForPopup( 'post' );
  974. } else if ( $( this ).hasClass( 'message_pop_up' ) ) {
  975. Posting.waitForPopup( 'message' );
  976. }
  977. }
  978. });
  979.  
  980. // Auto-focus title entry field when adding title.
  981. $( 'body' ).on( 'click', '.create_post .options .add_title', function() {
  982. var $postForm = $( this ).closest( '#create_post_form' );
  983. var $postTitle = $postForm.find( '#title' );
  984. // Focus title or text field, depending if the title is being added or removed.
  985. if ( $postTitle.is( ':visible' ) ) {
  986. setTimeout( function() { $postForm.find( '#text' ).focus(); }, 50 );
  987. } else {
  988. setTimeout( function() { $postTitle.focus(); }, 50 );
  989. }
  990. });
  991.  
  992. // Auto-focus message entry field when adding/removing image.
  993. $( 'body' ).on( 'click', '.create_post .options .filebutton, .cancel_icon_createpost', function() {
  994. var $postText = $( this ).closest( '#create_post_form' ).find( '#text' );
  995. setTimeout( function() { $postText.focus(); }, 50 );
  996. });
  997.  
  998.  
  999. /**
  1000. * Open post by double clicking header (only on pages with posts).
  1001. */
  1002. if ( ! Page.is( 'has-posts' ) ) {
  1003. return;
  1004. }
  1005.  
  1006. $( 'body' ).on( 'dblclick', '.post_header_name, .share_header', function( event ) {
  1007. //var post_id = $( this ).closest( '.post' ).data( 'post-id' );
  1008. var $post = $( this ).closest( '.post' );
  1009. var isShare = $post.find( '.share_header' ).length;
  1010. var isOriginal = ! $( this ).hasClass( 'share_header' );
  1011. $post.find( '#post_link_dropdown a' ).each(function() {
  1012. var linkText = $( this ).text().trim().toLowerCase();
  1013. if ( ( ! isShare && 'open' === linkText )
  1014. || ( ! isOriginal && 'open' === linkText )
  1015. || ( isOriginal && 'open original post' === linkText ) ) {
  1016.  
  1017. var url = $( this ).attr( 'href' );
  1018. // If the shift key is pressed, open in new window / tab.
  1019. if ( event.shiftKey ) {
  1020. window.open( url, '_blank' ).focus();
  1021. } else {
  1022. window.location = url;
  1023. }
  1024. return;
  1025. }
  1026. });
  1027. });
  1028. },
  1029.  
  1030. /**
  1031. * Check for the maximum number of hashtags and mentions.
  1032. * @param {string} message The message being posted.
  1033. * @return {boolean} True = submit, False = cancel, Null = not too many
  1034. */
  1035. checkMaximumHashtagsMentions : function( message ) {
  1036. // Check if the setting is enabled.
  1037. if ( ! settings.checkMaxHM ) {
  1038. return null;
  1039. }
  1040.  
  1041. // Get number of hashtags and mentions in the message.
  1042. var nrOfHashtags = message.split( '#' ).length - 1;
  1043. doLog( nrOfHashtags + ' Hashtags found.' );
  1044. var nrOfMentions = message.split( '@' ).length - 1;
  1045. doLog( nrOfMentions + ' Mentions found.' );
  1046.  
  1047. // If the limits aren't exeeded, just go on to post.
  1048. if ( nrOfHashtags <= TSUConst.maxHashtags && nrOfMentions <= TSUConst.maxMentions ) {
  1049. return null;
  1050. }
  1051.  
  1052. // Set up warning message.
  1053. var warning = 'Limits may be exceeded, check your message!\nAre you sure you want to continue?\n';
  1054. if ( nrOfHashtags > TSUConst.maxHashtags ) {
  1055. warning += '\n' + nrOfHashtags + ' #hashtags found. (Max. ' + TSUConst.maxHashtags + ')';
  1056. doLog( 'Too many hashtags found! (' + nrOfHashtags + ')', 'w' );
  1057. }
  1058. if ( nrOfMentions > TSUConst.maxMentions ) {
  1059. warning += '\n' + nrOfMentions + ' @mentions found. (Max. ' + TSUConst.maxMentions + ')';
  1060. doLog( 'Too many mentions found! (' + nrOfMentions + ')', 'w' );
  1061. }
  1062.  
  1063. // Last chance to make sure about hashtags and mentions.
  1064. return confirm( warning );
  1065. },
  1066.  
  1067. /**
  1068. * Check if the social network sharing has been selected.
  1069. * @param {jQuery} $form Form jQuery object of the form being submitted.
  1070. * @return {boolean} True = submit, False = cancel, Null = all selected
  1071. */
  1072. checkSocialNetworkSharing : function( $form ) {
  1073. // Check if the setting is enabled.
  1074. if ( ! settings.checkSocial ) {
  1075. return null;
  1076. }
  1077.  
  1078. var share_facebook = null;
  1079. var share_twitter = null;
  1080.  
  1081. // Get all visible (connected) checkboxes. If any are not checked, show warning.
  1082. $form.find( '.checkboxes_options_create_post input:visible' ).each(function() {
  1083. switch ( $( this ).attr( 'id' ) ) {
  1084. case 'facebook': share_facebook = $( this ).prop( 'checked' ); break;
  1085. case 'twitter': share_twitter = $( this ).prop( 'checked' ); break;
  1086. }
  1087. });
  1088.  
  1089. // If no social network accounts are connected, just go on to post.
  1090. if ( false !== share_facebook && false !== share_twitter ) {
  1091. return null;
  1092. }
  1093.  
  1094. var post_to = 'OK = Post to Tsu';
  1095.  
  1096. // Share to facebook?
  1097. if ( true === share_facebook ) {
  1098. post_to += ', Facebook';
  1099. }
  1100. // Share to twitter?
  1101. if ( true === share_twitter ) {
  1102. post_to += ', Twitter';
  1103. }
  1104.  
  1105. // Last chance to enable sharing to social networks...
  1106. return confirm( post_to + '\nCancel = Choose other social networks' );
  1107. },
  1108.  
  1109. /**
  1110. * Called on form submit.
  1111. * @param {jQuery} $form Form jQuery object of the form being submitted.
  1112. * @param {event} event The form submit event.
  1113. */
  1114. postFormSubmit : function( $form, event ) {
  1115. // In case the post gets cancelled, make sure the message field is focused.
  1116. var message = $form.find( '#text' ).focus().val();
  1117. var title = $form.find( '#title' ).val();
  1118. var hasPic = ( '' !== $form.find( '#create_post_pic_preview' ).text() );
  1119.  
  1120. // Make sure something was entered (title, text or image.
  1121. // Check for the maximum number of hashtags and mentions,
  1122. // and if the Social network sharing warning has been approved.
  1123. if ( ( '' !== message || '' !== title || hasPic )
  1124. && false !== Posting.checkMaximumHashtagsMentions( message )
  1125. && false !== Posting.checkSocialNetworkSharing( $form ) ) {
  1126. doLog( 'Post!' );
  1127. return;
  1128. }
  1129.  
  1130.  
  1131. /**************************
  1132. * CANCEL FORM SUBMISSION! *
  1133. **************************/
  1134. doLog( 'DONT Post!' );
  1135.  
  1136. // Prevent form post.
  1137. event.preventDefault();
  1138.  
  1139. // Hide the loader wheel.
  1140. $form.find( '.loading' ).hide();
  1141.  
  1142. // Make sure to enable the post button again. Give it some time, as Tsu internal script sets it to disabled.
  1143. setTimeout(function(){
  1144. $form.find( '#create_post_button' ).removeAttr( 'disabled' );
  1145. }, 500 );
  1146.  
  1147. return false;
  1148. },
  1149.  
  1150. /**
  1151. * Wait for the fancybox popup to create a new post.
  1152. */
  1153. waitForPopup : function( action ) {
  1154. Posting.waitingForPopup = true;
  1155.  
  1156. var formSelector;
  1157. var inputSelector;
  1158. switch ( action ) {
  1159. case 'post' :
  1160. formSelector = '.fancybox-wrap #create_post_form';
  1161. inputSelector = '#text';
  1162. break;
  1163. case 'message' :
  1164. formSelector = '.fancybox-wrap #new_message';
  1165. inputSelector = '#message_body';
  1166. break;
  1167. }
  1168.  
  1169. var $form = $( formSelector );
  1170. if ( $form.length ) {
  1171. $form.find( inputSelector ).focus();
  1172.  
  1173. // Apply checks to posts only!
  1174. if ( 'post' === action ) {
  1175. $form.submit(function( event ) {
  1176. return Posting.postFormSubmit( $( this ), event );
  1177. });
  1178. }
  1179. Posting.waitingForPopup = false;
  1180. return;
  1181. }
  1182.  
  1183. // Wait around for it longer...
  1184. setTimeout( function() { Posting.waitForPopup( action ); }, 500 );
  1185. }
  1186. };
  1187.  
  1188.  
  1189. /**
  1190. * User object containing info for Friends and Followers counter.
  1191. * @param {string|integer} userID Depending on the context, this is either the user id as a number or unique username / identifier.
  1192. * @param {string} userName The user's full name.
  1193. * @param {string} userUrl The url to the user profile page.
  1194. */
  1195. function UserObject( userID, userName, userUrl ) {
  1196.  
  1197. // Keep track if this user object has already finished loading.
  1198. this.finished = false;
  1199. this.userID = userID;
  1200. this.userName = userName;
  1201. this.userUrl = userUrl;
  1202.  
  1203. // Add to all userObjects list.
  1204. Users.userObjects[ userID ] = this;
  1205.  
  1206. // Queue of user link spans to refresh once the user object has finished loading.
  1207. this.userLinkSpans = [];
  1208.  
  1209. doLog( '(' + userID + ':' + userName + ') New user loaded.' );
  1210.  
  1211. /**
  1212. * Set the friends info.
  1213. * @param {jQuery} $friendsLink The jQuery <a> object linking to the user's Friends page.
  1214. * @param {[string} friendsUrl The URL to the user's Friends page.
  1215. * @param {[string} friendsCount The user's number of friends.
  1216. */
  1217. this.setFriendsInfo = function( $friendsLink, friendsUrl, friendsCount ) {
  1218. this.$friendsLink = $friendsLink;
  1219. this.friendsUrl = friendsUrl;
  1220. this.friendsCount = friendsCount;
  1221. };
  1222.  
  1223. /**
  1224. * Set the followers info.
  1225. * @param {jQuery} $followersLink The jQuery <a> object linking to the user's Followers page.
  1226. * @param {string} followersUrl The URL to the user's Followers page.
  1227. * @param {string} followersCount The user's number of Followers.
  1228. */
  1229. this.setFollowersInfo = function( $followersLink, followersUrl, followersCount ) {
  1230. this.$followersLink = $followersLink;
  1231. this.followersUrl = followersUrl;
  1232. this.followersCount = followersCount;
  1233. };
  1234.  
  1235. /**
  1236. * Return a clone of the Friends page link.
  1237. * @return {jQuery} Friends page link.
  1238. */
  1239. this.getFriendsLink = function() {
  1240. return this.$friendsLink.clone();
  1241. };
  1242.  
  1243. /**
  1244. * Return a clone of the Followers page link.
  1245. * @return {jQuery} Followers page link.
  1246. */
  1247. this.getFollowersLink = function() {
  1248. return this.$followersLink.clone();
  1249. };
  1250.  
  1251. /**
  1252. * Set this user object as finished loading.
  1253. */
  1254. this.setFinished = function() {
  1255. this.finished = true;
  1256. doLog( '(id:' + this.userID + ') Finished loading.' );
  1257. };
  1258.  
  1259. /**
  1260. * Is this user object already loaded?
  1261. * @return {Boolean}
  1262. */
  1263. this.isFinished = function() {
  1264. return this.finished;
  1265. };
  1266.  
  1267. /**
  1268. * Add a user link span to the queue to be refreshed once the user object is loaded.
  1269. * @param {jQuery} $userLinkSpan The user link span object.
  1270. */
  1271. this.queueUserLinkSpan = function( $userLinkSpan ) {
  1272. this.userLinkSpans.push( $userLinkSpan );
  1273. };
  1274.  
  1275. /**
  1276. * Refresh the passed $userLinkSpan with the user details.
  1277. * @param {jQuery} $userLinkSpan The <span> jQuery object to appent the details to.
  1278. * @param {integer} tries The number of tries that have already been used to refresh the details.
  1279. */
  1280. this.refresh = function( $userLinkSpan, tries ) {
  1281. if ( undefined === tries || null === tries ) {
  1282. tries = 0;
  1283. }
  1284.  
  1285. // If the maximum tries has been exeeded, return.
  1286. if ( tries > FFC.maxTries ) {
  1287. // Just remove the failed link span, maybe it will work on the next run.
  1288. $userLinkSpan.remove();
  1289.  
  1290. // Remove all queued ones too to prevent infinite loader image.
  1291. for ( var i = this.userLinkSpans.length - 1; i >= 0; i-- ) {
  1292. this.userLinkSpans[ i ].remove();
  1293. }
  1294. this.userLinkSpans = [];
  1295.  
  1296. doLog( '(id:' + this.userID + ') Maximum tries exeeded!', 'w' );
  1297. return;
  1298. }
  1299.  
  1300. if ( this.isFinished() ) {
  1301. // Add the user details after the user link.
  1302. this.queueUserLinkSpan( $userLinkSpan );
  1303.  
  1304. // Update all listening user link spans.
  1305. for ( var i = this.userLinkSpans.length - 1; i >= 0; i-- ) {
  1306. this.userLinkSpans[ i ].empty().append( this.getFriendsLink(), this.getFollowersLink() );
  1307. }
  1308.  
  1309. // Empty the queue, as there is no need to reload already loaded ones.
  1310. this.userLinkSpans = [];
  1311.  
  1312. doLog( '(' + this.userID + ':' + this.userName + ') Friends and Followers set.' );
  1313. } else {
  1314. var t = this;
  1315. setTimeout(function() {
  1316. t.refresh( $userLinkSpan, tries + 1);
  1317. }, 1000);
  1318. }
  1319. };
  1320. }
  1321.  
  1322. /**
  1323. * Friends and Followers counts manager.
  1324. */
  1325. var FFC = {
  1326.  
  1327. // Max number of tries to get friend and follower info (= nr of seconds).
  1328. maxTries : 60,
  1329.  
  1330. /**
  1331. * Load a user link.
  1332. * @param {jQuery} $userElement The element that contains the user link.
  1333. * @param {boolean} onHoverCard Is this element a hover card?
  1334. */
  1335. loadUserLink : function( $userElement, onHoverCard ) {
  1336.  
  1337. // If this link has already been processed, skip it.
  1338. if ( $userElement.hasClass( 'ffc-processed' ) ) {
  1339. return;
  1340. }
  1341.  
  1342. // Set the "processed" flag to prevent loading the same link multiple times.
  1343. $userElement.addClass( 'ffc-processed' );
  1344.  
  1345.  
  1346. // Because the user link is in a nested entry.
  1347. var $userLink = $userElement.find( 'a:first' );
  1348.  
  1349. // If no link has been found, continue with the next one. Fail-safe.
  1350. if ( 0 === $userLink.length ) {
  1351. return;
  1352. }
  1353.  
  1354. // Add a new <span> element to the user link.
  1355. var $userLinkSpan = $( '<span/>', { html: '<img class="th-ffc-loader-wheel" src="/assets/loader.gif" alt="Loading..." />', 'class': 'th-ffc-span' } );
  1356. $userLink.after( $userLinkSpan );
  1357.  
  1358. // Special case for these pages, to make it look nicer and fitting.
  1359. if ( onHoverCard || Page.is( 'fff discover tree' ) ) {
  1360. $userLinkSpan.before( '<br class="th-ffc-br" />' );
  1361. }
  1362.  
  1363. // Get the user info from the link.
  1364. var userName = $userLink.text().trim();
  1365. var userUrl = $userLink.attr( 'href' );
  1366.  
  1367. // Extract the userID from the url. Special case for discover page!
  1368. var userID = ( Page.is( 'discover' ) ) ? $userElement.attr( 'user_id' ) : userUrl.split( '/' )[1];
  1369.  
  1370. // Check if the current user has already been loaded.
  1371. var userObject = Users.getUserObject( userID, true );
  1372.  
  1373. // Add this span to the list that needs updating when completed.
  1374. if ( userObject instanceof UserObject ) {
  1375.  
  1376. // If this user has already finished loading, just update the span, else add it to the queue.
  1377. if ( userObject.isFinished() ) {
  1378. userObject.refresh( $userLinkSpan, 0 );
  1379. } else {
  1380. userObject.queueUserLinkSpan( $userLinkSpan );
  1381. }
  1382.  
  1383. return;
  1384. }
  1385.  
  1386. // Create a new UserObject and load it's data.
  1387. userObject = new UserObject( userID, userName, userUrl );
  1388.  
  1389. // Load the numbers from the user profile page.
  1390. setTimeout( function() { $.get( userUrl, function( response ) {
  1391. // Get rid of all images first, no need to load those, then find the links.
  1392. var $numbers = $( response.replace( /<img[^>]*>/g, '' ) ).find( '.profile_details .numbers a' );
  1393.  
  1394. // If the user doesn't exist, just remove the span.
  1395. if ( 0 === $numbers.length ) {
  1396. $userLinkSpan.remove();
  1397. return;
  1398. }
  1399.  
  1400. // Set up the Friends link.
  1401. var $friends = $numbers.eq( 0 );
  1402. var friendsUrl = $friends.attr( 'href' );
  1403. var friendsCount = $friends.find( 'span' ).text();
  1404. var $friendsLink = $( '<a/>', {
  1405. href: friendsUrl,
  1406. html: friendsCount
  1407. });
  1408.  
  1409. // Set up the Followers link.
  1410. var $followers = $numbers.eq( 1 );
  1411. var followersUrl = $followers.attr( 'href' );
  1412. var followersCount = $followers.find( 'span' ).text();
  1413. var $followersLink = $( '<a/>', {
  1414. href: followersUrl,
  1415. html: followersCount
  1416. });
  1417.  
  1418. // Add titles to pages without posts and not on hover cards.
  1419. if ( ! onHoverCard && ! Page.is( 'has-posts' ) ) {
  1420. $friendsLink.attr( 'title', 'Friends' );
  1421. $followersLink.attr( 'title', 'Followers' );
  1422. }
  1423.  
  1424. // Add the Friends and Followers details, then refresh all userlink spans.
  1425. userObject.setFriendsInfo( $friendsLink, friendsUrl, friendsCount );
  1426. userObject.setFollowersInfo( $followersLink, followersUrl, followersCount );
  1427. userObject.refresh( $userLinkSpan, 0 );
  1428. })
  1429. .always(function() {
  1430. // Make sure to set the user as finished loading.
  1431. Users.finishedLoading( userID );
  1432. }); }, 100 );
  1433. },
  1434.  
  1435. /**
  1436. * Load the FF counts for a user's hover card.
  1437. * @param {jQuery} $userHoverCard Hover card selector.
  1438. */
  1439. loadUserHoverCard : function( $userHoverCard ) {
  1440. if ( 'disabled' === settings.showFFC ) {
  1441. return;
  1442. }
  1443.  
  1444. var t = this;
  1445. // As long as the hover tooltip exists but the card inside it doesn't, loop and wait till it's loaded.
  1446. if ( $( '.tooltipster-user-profile' ).length && $userHoverCard.length === 0 ) {
  1447. setTimeout(function(){
  1448. t.loadUserHoverCard( $( $userHoverCard.selector, $userHoverCard.context ) );
  1449. }, 500);
  1450. return;
  1451. }
  1452.  
  1453. doLog( 'Start loading Friends and Followers (Hover Card).', 'i' );
  1454.  
  1455. FFC.loadUserLink( $userHoverCard, true );
  1456. },
  1457.  
  1458. /**
  1459. * Load Friends and Followers
  1460. * @param {boolean} clean Delete saved details and refetch all.
  1461. */
  1462. loadAll : function( clean ) {
  1463. if ( 'disabled' === settings.showFFC || ( 'hovercard' === settings.showFFC && Page.is( 'has-posts' ) ) ) {
  1464. return;
  1465. }
  1466.  
  1467. if ( clean ) {
  1468. if ( confirm( 'Reload all Friend and Follower details of all users on the current page?' ) ) {
  1469. doLog( '(clean) Start loading Friends and Followers.', 'i' );
  1470.  
  1471. // Reset list.
  1472. Users.userObjects = {};
  1473.  
  1474. // Remove all existing user links spans and brs.
  1475. $( '.th-ffc-span, .th-ffc-br' ).remove();
  1476. $( '.ffc-processed' ).removeClass( 'ffc-processed' );
  1477. } else {
  1478. return;
  1479. }
  1480. } else {
  1481. doLog( 'Start loading Friends and Followers.', 'i' );
  1482. }
  1483.  
  1484. // Special case for "Discover Users", convert all the usernames to links.
  1485. if ( Page.is( 'discover' ) ) {
  1486. $( '#discover .user_card_1_wrapper' ).each(function() {
  1487. var $convertToLinks = $( this ).find( '.tree_child_fullname, .tree_child_coverpicture, .tree_child_profile_image' );
  1488.  
  1489. // Get the last part of the "Follow" button link, which is the numerical user ID.
  1490. var userID = $( this ).find( '.follow_button' ).attr( 'href' ).split( '/' ).pop();
  1491.  
  1492. if ( userID ) {
  1493. // Make the username and profile images a link to the profile.
  1494. $convertToLinks.each(function() {
  1495. // Wrap each item in a link and add the user ID which we need for FFC.
  1496. $( this ).attr( 'user_id', userID ).wrapInner( '<a href="/users/' + userID + '"></a>' );
  1497. });
  1498. }
  1499. });
  1500. }
  1501.  
  1502. // Find all users and process them.
  1503. var $newUserLinks = $( Observer.queryToLoadFF ).not( '.ffc-processed' );
  1504.  
  1505. doLog( 'New user links found: ' + $newUserLinks.length );
  1506.  
  1507. $newUserLinks.each(function() {
  1508. var $userElement = $( this );
  1509.  
  1510. // Is this link on a tooltip hover card?
  1511. var onHoverCard = ( $userElement.closest( '.tooltipster-base' ).length !== 0 );
  1512.  
  1513. FFC.loadUserLink( $userElement, onHoverCard );
  1514. });
  1515. }
  1516. };
  1517.  
  1518. /**
  1519. * Manage all users for the Friends and Followers counting.
  1520. */
  1521. var Users = {
  1522.  
  1523. userObjects : {},
  1524.  
  1525. getUserObject : function( userID, setLoading ) {
  1526. if ( Users.userObjects.hasOwnProperty( userID ) ) {
  1527. doLog( '(' + userID + ':' + Users.userObjects[ userID ].userName + ') Already loaded.' );
  1528. return Users.userObjects[ userID ];
  1529. }
  1530.  
  1531. if ( setLoading ) {
  1532. doLog( '(id:' + userID + ') Set to loading.' );
  1533. Users.userObjects[ userID ] = true;
  1534. }
  1535.  
  1536. doLog( '(id:' + userID + ') Not loaded yet.' );
  1537. return false;
  1538. },
  1539.  
  1540. finishedLoading : function( userID ) {
  1541. if ( Users.userObjects.hasOwnProperty( userID ) ) {
  1542. Users.userObjects[ userID ].setFinished();
  1543. }
  1544. }
  1545. };
  1546.  
  1547.  
  1548. // Initialise after all variables are defined.
  1549. Page.init();
  1550. Observer.init();
  1551. Posting.init();
  1552.  
  1553.  
  1554. // Add the required CSS rules.
  1555. addCSS();
  1556.  
  1557. // Add the About (and Settings) window to the menu.
  1558. addAboutWindow();
  1559.  
  1560.  
  1561. // As the observer can't detect any changes on static pages, run functions now.
  1562. FFC.loadAll();
  1563. QM.load();
  1564. emphasizeNestedRepliesParents();
  1565. tweakMessagesPage();
  1566. FFM.init();
  1567.  
  1568.  
  1569. // Load Notifications Reloaded?
  1570. if ( settings.notifReloaded > 0 ) {
  1571. notifications.urls.notifications = '/notifications/request/?count=' + settings.notifReloaded;
  1572. notifications.get().success(function() {
  1573. $.event.trigger( 'notificationsRender' );
  1574. });
  1575. }
  1576.  
  1577. // Add Notifications Reloaded to the notifications page.
  1578. if ( Page.is( 'notifications' ) ) {
  1579.  
  1580. var $ajaxNCRequest = null;
  1581. var $notificationsList = $( '#new_notifications_list' );
  1582.  
  1583.  
  1584. // Add the empty filter message
  1585. var $emptyFilterDiv = $( '<div>No notifications match the selected filter. Coose a different one.</div>' );
  1586.  
  1587. // Select input to filter kinds.
  1588. var $kindSelect = $( '<select/>', {
  1589. 'id' : 'th-nr-nk-select',
  1590. title : 'Filter by the kind of notification'
  1591. });
  1592.  
  1593. // Select input to filter users.
  1594. var $userSelect = $( '<select/>', {
  1595. 'id' : 'th-nr-nu-select',
  1596. title : 'Filter by user'
  1597. });
  1598.  
  1599.  
  1600. // List the available count options.
  1601. var selectCount = '';
  1602. [ 30, 50, 100, 200, 300, 400, 500 ].forEach(function( val ) {
  1603. selectCount += '<option value="' + val + '">' + val + '</option>';
  1604. });
  1605.  
  1606. /**
  1607. * Filter the current items according to the selected filters.
  1608. */
  1609. var filterNotifications = function() {
  1610. var $notificationItems = $notificationsList.find( '.notifications_item' ).show();
  1611.  
  1612. if ( '' !== $kindSelect.val() ) {
  1613. $notificationItems.not( '[data-kind="' + $kindSelect.val() + '"]' ).hide();
  1614. }
  1615.  
  1616. if ( '' !== $userSelect.val() ) {
  1617. $notificationItems.not( '[data-user-id="' + $userSelect.val() + '"]' ).hide();
  1618. }
  1619.  
  1620. if ( $notificationItems.find( ':visible' ).length ) {
  1621. $emptyFilterDiv.hide();
  1622. } else {
  1623. $emptyFilterDiv.show();
  1624. }
  1625. };
  1626.  
  1627. /**
  1628. * Refresh the selected number of notifications.
  1629. */
  1630. var reloadNotifications = function() {
  1631. // If a request is already busy, cancel it and restart the new one.
  1632. if ( $ajaxNCRequest ) {
  1633. $ajaxNCRequest.abort();
  1634. }
  1635.  
  1636. doLog( 'Loading ' + $countSelect.val() + ' notifications.', 'i' );
  1637.  
  1638. // Show loader wheel.
  1639. $notificationsList.html( '<img src="/assets/loader.gif" alt="Loading..." />' );
  1640.  
  1641. // Disable select inputs but remember the selections.
  1642. var lastKindSelected = $kindSelect.attr( 'disabled', 'disabled' ).val();
  1643. var lastUserSelected = $userSelect.attr( 'disabled', 'disabled' ).val();
  1644.  
  1645. // Request the selected amount of notifications.
  1646. $ajaxNCRequest = $.getJSON( '/notifications/request/?count=' + $countSelect.val(), function( data ) {
  1647.  
  1648. // Clear the loader wheel.
  1649. $notificationsList.empty();
  1650.  
  1651. // Make sure we have access to the notifications.
  1652. if ( ! data.hasOwnProperty( 'notifications' ) ) {
  1653. // Some error occured.
  1654. $notificationsList.html( '<div>Error loading notifications, please try again later.</div>' );
  1655. return;
  1656. }
  1657.  
  1658. // No notifications.
  1659. if ( 0 === data.notifications.length ) {
  1660. $notificationsList.html( '<div>You don\'t have any notifications.</div>');
  1661. return;
  1662. }
  1663.  
  1664. // Texts for all possible notifications.
  1665. var kindsTexts = {
  1666. friend_request_accepted : 'Friend Requests accepted',
  1667. new_comment_on_post : 'Comments on your Posts',
  1668. new_comment_on_post_you_commented_on : 'Comments on other Posts',
  1669. new_follower : 'New Followers',
  1670. new_like_on_post : 'Likes on your Posts',
  1671. new_like_on_comment : 'Likes on your Comments',
  1672. new_post_on_your_wall : 'Posts on your Wall',
  1673. someone_mentioned_you_in_a_post : 'Mentioned in a Post or Comment',
  1674. someone_shared_your_post : 'Shares of your Posts'
  1675. };
  1676.  
  1677. // The available items to populate the select fields.
  1678. var availableKinds = {};
  1679. var availableUsers = {};
  1680.  
  1681. // Append the notifications to the list. Function used is the one used by Tsu.
  1682. $( data.notifications ).each(function( i, item ) {
  1683.  
  1684. // Remember all the available kinds and the number of occurrences.
  1685. if ( availableKinds.hasOwnProperty( item.kind ) ) {
  1686. availableKinds[ item.kind ]++;
  1687. } else {
  1688. availableKinds[ item.kind ] = 1;
  1689. }
  1690.  
  1691. // Remember all the available users and the number of occurrences.
  1692. if ( availableUsers.hasOwnProperty( item.user.id ) ) {
  1693. availableUsers[ item.user.id ].count++;
  1694. } else {
  1695. availableUsers[ item.user.id ] = {
  1696. username : item.user.first_name + ' ' + item.user.last_name,
  1697. count : 1
  1698. };
  1699. }
  1700.  
  1701. var $notificationItem = $( window.notifications_fr._templates['new_comment_in_post'](
  1702. item.url,
  1703. item.user,
  1704. item.message,
  1705. item.created_at_int
  1706. ))
  1707. .attr( { 'data-user-id' : item.user.id, 'data-kind' : item.kind } );
  1708.  
  1709. $notificationsList.append( $notificationItem );
  1710. });
  1711.  
  1712. // Add the empty filter message.
  1713. $notificationsList.append( $emptyFilterDiv );
  1714.  
  1715. // List the available kinds, adding the number of occurances.
  1716. $kindSelect.removeAttr( 'disabled' ).html( '<option value="">All Kinds</option>' );
  1717. for ( var key in availableKinds ) {
  1718. if ( kindsTexts.hasOwnProperty( key ) && availableKinds.hasOwnProperty( key ) ) {
  1719. $kindSelect.append( '<option value="' + key + '"' + selected( key, lastKindSelected ) + '>' + kindsTexts[ key ] + ' (' + availableKinds[ key ] + ')</option>' );
  1720. }
  1721. }
  1722.  
  1723. // List the available users, adding the number of occurances.
  1724. $userSelect.removeAttr( 'disabled' ).html( '<option value="">All Users - (' + Object.keys( availableUsers ).length + ' Users)</option>' );
  1725.  
  1726. // Sort alphabetically.
  1727. var availableUsersSorted = [];
  1728. for ( var userID in availableUsers ) {
  1729. availableUsersSorted.push( [ userID, availableUsers[ userID ].username.toLowerCase() ] );
  1730. }
  1731. availableUsersSorted.sort( function( a, b ) { return ( a[1] > b[1] ) ? 1 : ( ( b[1] > a[1] ) ? -1 : 0 ); } );
  1732.  
  1733. availableUsersSorted.forEach(function( val ) {
  1734. var userID = val[0];
  1735. if ( availableUsers.hasOwnProperty( userID ) ) {
  1736. $userSelect.append( '<option value="' + userID + '"' + selected( userID, lastUserSelected ) + '>' + availableUsers[ userID ].username + ' (' + availableUsers[ userID ].count + ')</option>' );
  1737. }
  1738. });
  1739.  
  1740. // Filter the notifications to make sure that the previously selected filter gets reapplied.
  1741. filterNotifications();
  1742. })
  1743. .fail(function() {
  1744. $notificationsList.html( '<div>Error loading notifications, please try again later.</div>' );
  1745. });
  1746. };
  1747.  
  1748. var $countSelect = $( '<select/>', {
  1749. 'id' : 'th-nc-select',
  1750. html : selectCount
  1751. })
  1752. .change( reloadNotifications );
  1753.  
  1754. $( '<div/>', { 'id' : 'th-nr-div' } )
  1755. .append( $kindSelect.change( filterNotifications ) )
  1756. .append( $userSelect.change( filterNotifications ) )
  1757. .append( $( '<label/>', { html : 'Show:', title : 'How many notifications to show' } ).append( $countSelect ) )
  1758. .append( $( '<i/>', { 'id' : 'th-nr-reload', title : 'Reload notifications' } ).click( reloadNotifications ) ) // Add reload button.
  1759. .appendTo( $( '#new_notifications_wrapper' ) );
  1760.  
  1761. reloadNotifications();
  1762. }
  1763.  
  1764. /**
  1765. * Add a specific class to all nested reply parent elements to emphasize them.
  1766. */
  1767. function emphasizeNestedRepliesParents() {
  1768. // Make sure the setting is enabled and we're on the right page.
  1769. if ( ! settings.emphasizeNRP || ! Page.is( 'has-posts' ) ) {
  1770. return;
  1771. }
  1772.  
  1773. doLog( 'Emphasizing Nested Replies Parents.', 'i' );
  1774.  
  1775. $( '.post_comment .load_more_post_comment_replies' ).not( '.th-nrp' ).each(function(){
  1776. if ( /\d+/.exec( $( this ).text() ) > 0 ) {
  1777. $( this ).addClass( 'th-nrp' );
  1778. }
  1779. });
  1780. }
  1781.  
  1782. /**
  1783. * Autofocus text input and add line breaks to messages.
  1784. */
  1785. function tweakMessagesPage() {
  1786. // Make sure we're on the right page.
  1787. if ( ! Page.is( 'messages' ) ) {
  1788. return;
  1789. }
  1790.  
  1791. doLog( 'Tweaking messages page.', 'i' );
  1792.  
  1793. // Focus the recipient field if this is a new message.
  1794. if ( document.URL.endsWith( '/new' ) ) {
  1795. $( '.new_message #message_to_textarea' ).focus();
  1796. } else {
  1797. $( '.new_message #message_body' ).focus();
  1798. }
  1799.  
  1800. // Add line breaks to all messages.
  1801. $( '.messages_content .message_box' ).each(function(){
  1802. if ( ! $( this ).hasClass( 'tsu-helper-tweaked' ) ) {
  1803. var $text = $( this ).find( '.message-text' );
  1804. $text.html( $text.html().trim().replace(/(?:\r\n|\r|\n)/g, '<br />') );
  1805. $( this ).addClass( 'tsu-helper-tweaked' );
  1806. }
  1807. });
  1808. }
  1809.  
  1810. /**
  1811. * Make a log entry if debug mode is active.
  1812. * @param {string} logMessage Message to write to the log console.
  1813. * @param {string} level Level to log ([l]og,[i]nfo,[w]arning,[e]rror).
  1814. * @param {boolean} alsoAlert Also echo the message in an alert box.
  1815. */
  1816. function doLog( logMessage, level, alsoAlert ) {
  1817. if ( ! publicDebug && 104738 !== window.current_user.id ) {
  1818. return;
  1819. }
  1820.  
  1821. var logLevels = { l : 0, i : 1, w : 2, e : 3 };
  1822.  
  1823. // Default to "log" if nothing is provided.
  1824. level = level || 'l';
  1825.  
  1826. if ( 'disabled' !== settings.debugLevel && logLevels[ settings.debugLevel ] <= logLevels[ level ] ) {
  1827. switch( level ) {
  1828. case 'l' : console.log( logMessage ); break;
  1829. case 'i' : console.info( logMessage ); break;
  1830. case 'w' : console.warn( logMessage ); break;
  1831. case 'e' : console.error( logMessage ); break;
  1832. }
  1833. if ( alsoAlert ) {
  1834. alert( logMessage );
  1835. }
  1836. }
  1837. }
  1838.  
  1839. /**
  1840. * Add the required CSS rules.
  1841. */
  1842. function addCSS() {
  1843. doLog( 'Added CSS.', 'i' );
  1844.  
  1845. // Remember to take care of setting-specific CSS!
  1846. var settingSpecificCSS = '';
  1847.  
  1848. // Hide Ads.
  1849. if ( settings.hideAds ) {
  1850. settingSpecificCSS +=
  1851. '.homepage_advertisement, .rectangle_advertisement, .skyscraper_advertisement { position: absolute !important; left: -999999999px !important; }';
  1852. }
  1853.  
  1854. // Nested replies parents.
  1855. if ( settings.emphasizeNRP ) {
  1856. settingSpecificCSS +=
  1857. '.th-nrp { text-decoration: underline; color: #777 !important; }';
  1858. }
  1859.  
  1860. // Quick Mention links for comments.
  1861. if ( settings.quickMention ) {
  1862. settingSpecificCSS +=
  1863. '.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; }' +
  1864. '.th-qm-comment { margin-left: 11px; }' +
  1865. '.th-qm-active-input { border-color: rgba(0,0,0,.4) !important; }' +
  1866. '.post_comment { position: relative; }';
  1867. }
  1868.  
  1869. // Friends and Followers
  1870. if ( 'disabled' !== settings.showFFC ) {
  1871. settingSpecificCSS +=
  1872. '.th-ffc-span .th-ffc-loader-wheel { margin-left: 5px; height: 12px; }' +
  1873. '.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; }' +
  1874. '.user_card_1_wrapper .tree_child_fullname { height: 32px !important; }';
  1875. }
  1876.  
  1877. // Add the styles to the head.
  1878. $( '<style>' ).html(
  1879. settingSpecificCSS +
  1880.  
  1881. // Menu item.
  1882. '#th-menuitem-about a:before { display: none !important; }' +
  1883. '#th-menuitem-about a { background-color: #1ea588; color: #fff !important; width: 100% !important; padding: 8px !important; box-sizing: border-box; text-align: center; }' +
  1884. '#th-menuitem-about a:hover { background-color: #1ea588 !important; }' +
  1885.  
  1886. // FFM.
  1887. '#th-ffm-unfollow-friends{ background: #dc3a50; color: #fff; font-size: .8em; }' +
  1888. '#th-ffm-total-fffs { background: #090; color: #fff; margin-left: 8px !important; }' +
  1889. '#th-ffm-link-start, #th-ffm-link-cancel { float: right; padding: 2px; }' +
  1890. '#th-ffm-link-cancel { background: url(/assets/loader.gif) no-repeat; padding-left: 24px; }' +
  1891. '#th-ffm-status-text { float: right; margin-right: 8px; }' +
  1892. '#th-ffm-status-text label, #th-ffm-total-fffs, #th-ffm-unfollow-friends { padding: 2px 5px; border-radius: 3px; border: 1px solid rgba(0,0,0,.5); margin: -1px; }' +
  1893. '#th-ffm-status-text input { margin: 0 5px 0 2px; }' +
  1894. '#th-ffm-status-text * { display: inline-block; cursor: pointer; }' +
  1895. '.th-ffm-card-received { background: #cfc; }' +
  1896. '.th-ffm-card-sent { background: #eef; }' +
  1897. '.th-ffm-card-fandf { background: #ffc; }' +
  1898.  
  1899. // About & Settings windows.
  1900. '#th-aw, #th-sw { width: 400px; height: auto; }' +
  1901. '#th-aw *, #th-sw * { box-sizing: border-box; }' +
  1902. '#th-aw h1, #th-sw h1 { margin: 5px; }' +
  1903. '.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; }' +
  1904.  
  1905. // About window.
  1906. '.th-update { background-color: #f1b054 !important; color: #fff !important; }' +
  1907. '#th-aw > div { display: block; margin: 5px 0; }' +
  1908. '#th-aw .card { padding: 5px; min-width: 100%; border: 1px solid #d7d8d9; border-top-left-radius: 30px; border-bottom-left-radius: 30px; }' +
  1909. '#th-aw .card .button { width: 123px; }' +
  1910. '#th-aw-update-button { margin: 5px; }' +
  1911. '#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"); }' +
  1912. '.th-aw-donate-buttons { margin: inherit; border-top-left-radius: 20px; border-bottom-left-radius: 20px; }' +
  1913. '.th-aw-donate-paypal { float: left; }' +
  1914. '.th-aw-donate-paypal img { vertical-align: middle; }' +
  1915. '.th-aw-get-in-touch li { display: inline-block; margin-right: 10px; }' +
  1916. '.th-aw-info li { margin: 2px 0; }' +
  1917.  
  1918. // 16px icons.
  1919. '.th-icon { display: inline-block; width: 16px; height: 16px; vertical-align: text-bottom; }' +
  1920. '.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=="); }' +
  1921. '.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=="); }' +
  1922. '.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=="); }' +
  1923. '.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="); }' +
  1924. '.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=="); }' +
  1925.  
  1926. '.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"); }' +
  1927.  
  1928. // Heartbeat.
  1929. '.th-about-love:hover .th-icon-heart { -webkit-animation: heartbeat 1s linear infinite; -moz-animation: heartbeat 1s linear infinite; animation: heartbeat 1s linear infinite; }' +
  1930. '@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); } }' +
  1931. '@-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); } }' +
  1932. '@-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); } }' +
  1933. '@-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); } }' +
  1934.  
  1935. // Settings window.
  1936. '#th-sw label, #th-sw input, #th-sw select { display: inline-block; cursor: pointer; }' +
  1937. '#th-sw form > div { margin: 5px 0; }' +
  1938. '#th-sw-back-button { float: left !important; }' +
  1939. '.th-sw-help { margin-left: 4px; cursor: help; }' +
  1940.  
  1941. // Show custom number of notifications.
  1942. '#new_notifications_wrapper { position: relative; }' +
  1943. '#th-nr-div { position: absolute; top: 0; right: 15px; }' +
  1944. '#th-nr-div select { cursor: pointer; margin: 0 5px; }' +
  1945. '#th-nr-nk-select, #th-nr-nu-select { width: 80px; }' +
  1946. '#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=="); }' +
  1947.  
  1948. // Notifications Reloaded.
  1949. '#new_notifications_popup .notifications, #new_notifications_popup .messages, #new_notifications_popup .friend_requests { max-height: 160px; width: 100%; overflow: scroll; }' +
  1950. '#new_notifications_popup .notifications .notifications_item, #new_notifications_popup .messages .notifications_item { width: 100%; }'
  1951. ).appendTo( 'head' );
  1952. }
  1953.  
  1954. /**
  1955. * Add the about window which shows the version and changelog.
  1956. * It also displays donate buttons and an update button if a newer version is available.
  1957. */
  1958. function addAboutWindow() {
  1959. doLog( 'Added about window.', 'i' );
  1960.  
  1961. // About window.
  1962. var $aboutWindow = $( '<div/>', {
  1963. 'id' : 'th-aw',
  1964. html :
  1965. '<h1>About Tsu Helper</h1>' +
  1966. '<div class="th-about-love"><i class="th-icon th-icon-heart"></i>Made with love and care.</div>' +
  1967. '<div><ul class="th-aw-info">' +
  1968. '<li>Version <strong>' + Updater.localVersion + '</strong> (<a href="https://j.mp/tsu-helper-changelog" target="_blank">changelog</a>)<br />' +
  1969. '<li>&copy;2014-2015 Armando L&uuml;scher (<a href="/noplanman">@noplanman</a>)<br />' +
  1970. '<li><em>Disclaimer</em>: Tsu Helper is in no way affiliated with Tsu LLC.' +
  1971. '<li>Use it at your own risk.' +
  1972. '</ul></div>' +
  1973. '<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>' +
  1974. '<div><ul class="th-aw-get-in-touch">' +
  1975. '<li>Found a <i class="th-icon th-icon-bug" title="Bug"></i>' +
  1976. '<li>Have a great <i class="th-icon th-icon-idea" title="Idea"></i>' +
  1977. '<li>Just want to say hi?' +
  1978. '<li><a class="message_pop_up fancybox.ajax" href="/messages/new/noplanman">Let me know!</a>' +
  1979. '</ul></div>' +
  1980. '<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>' +
  1981. '<div class="th-buttons-div th-aw-donate-buttons">' +
  1982. '<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>' +
  1983. '<span>&laquo; PayPal <i> - or - </i> Tsu &raquo;</span>' +
  1984. '<a class="th-aw-donate-tsu button message_pop_up fancybox.ajax donation" href="/users/profiles/donation/104738" title="Donate via Tsu">Donate</a>' +
  1985. '</div>' +
  1986. '</div>' +
  1987. '<div id="th-about-followme">Follow me and stay up to date!</div>' +
  1988. '<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>'
  1989. });
  1990.  
  1991. // Get my card and add it to the about window.
  1992. $.get( '/users/profile_summary/104738', function( card ) {
  1993. $aboutWindow.find( '#th-about-followme' ).after( card );
  1994. });
  1995.  
  1996. // Settings window.
  1997. var $settingsWindow = $( '<div/>', {
  1998. 'id' : 'th-sw',
  1999. html : '<h1>Tsu Helper Settings</h1>'
  2000. });
  2001.  
  2002. // Settings which are only a checkbox.
  2003. var checkboxSettings = '';
  2004. [
  2005. { name : 'hideAds', txt : 'Hide Ads', help : 'Show or Hide all the Ads.' },
  2006. { name : 'quickMention', txt : 'Enable Quick Mentions', help : 'Add Quick Mention links to comments and replies.' },
  2007. { name : 'emphasizeNRP', txt : 'Emphasize Nested Replies', help : 'Emphasize the parent of nested comment replies, to make them more visible.' },
  2008. { name : 'checkSocial', txt : 'Check Social Networks', help : 'Check if your new post is being shared to your connected Social Network accounts.' },
  2009. { name : 'checkMaxHM', txt : 'Check Max. Hashtags & Mentions', help : 'Check if the maximum number of Hashtags & Mentions has been reached before posting.' }
  2010. ].forEach(function( item ) {
  2011. 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>';
  2012. });
  2013.  
  2014. // The debug level dropdown.
  2015. var debugLevelSettings = ( publicDebug || 104738 === window.current_user.id ) ?
  2016. '<div><label>Debug level: ' +
  2017. '<select name="debugLevel">' +
  2018. '<option value="disabled"' + selected( 'disabled', settings.debugLevel ) + '>Disabled</option>' +
  2019. '<option value="l"' + selected( 'l', settings.debugLevel ) + '>Log</option>' +
  2020. '<option value="i"' + selected( 'i', settings.debugLevel ) + '>Info</option>' +
  2021. '<option value="w"' + selected( 'w', settings.debugLevel ) + '>Warn</option>' +
  2022. '<option value="e"' + selected( 'e', settings.debugLevel ) + '>Error</option>' +
  2023. '</select>' +
  2024. '</label></div>' : '';
  2025.  
  2026. // List the available count options.
  2027. var selectNotifReloaded = '<select name="notifReloaded"><option value="0">Disabled</option>';
  2028. [ 5, 10, 15, 20, 25, 30 ].forEach(function( val ) {
  2029. selectNotifReloaded += '<option value="' + val + '">' + val + '</option>';
  2030. });
  2031. selectNotifReloaded += '</select>';
  2032.  
  2033. var $settingsForm = $( '<form/>', {
  2034. 'id' : 'th-settings-form',
  2035. html :
  2036. checkboxSettings +=
  2037.  
  2038. // FFC.
  2039. '<div><label>Show Friends and Followers on ' +
  2040. '<select name="showFFC">' +
  2041. '<option value="disabled"' + selected( 'disabled', settings.showFFC ) + '>No Links (disabled)</option>' +
  2042. '<option value="all"' + selected( 'all', settings.showFFC ) + '>All Links</option>' +
  2043. '<option value="hovercard"' + selected( 'hovercard', settings.showFFC ) + '>Hover Cards Only</option>' +
  2044. '</select>' +
  2045. '</label><i class="th-icon th-icon-help th-sw-help" title="Where to display Friends and Followers counts."></i></div>' +
  2046.  
  2047. // Notifications Reloaded
  2048. '<div><label>Notifications Reloaded count: ' +
  2049. selectNotifReloaded +
  2050. '</label><i class="th-icon th-icon-help th-sw-help" title="How many notifications to show in the notification popup."></i></div>'
  2051.  
  2052. + debugLevelSettings
  2053. })
  2054. .appendTo( $settingsWindow );
  2055.  
  2056. // Defaults button on Settings window.
  2057. var $defaultsButton = $( '<a/>', {
  2058. 'id' : 'th-sw-defaults-button',
  2059. class : 'button red',
  2060. title : 'Reset to default values',
  2061. html : 'Defaults',
  2062. click : function() {
  2063. Settings.setDefaults( $settingsForm );
  2064. }
  2065. })
  2066. .appendTo( $settingsWindow.find( 'h1' ) );
  2067.  
  2068. // The state in which the Settings are closed (back or save).
  2069. var settingsCloseState = null;
  2070.  
  2071. // Save button on Settings window.
  2072. var $saveButton = $( '<a/>', {
  2073. 'id' : 'th-sw-save-button',
  2074. class : 'button',
  2075. title : 'Save Settings',
  2076. html : 'Save',
  2077. click : function() {
  2078. if ( confirm( 'Refresh page now for changes to take effect?' ) ) {
  2079. Settings.save( $settingsForm );
  2080. settingsCloseState = 'save';
  2081. $.fancybox.close();
  2082. }
  2083. }
  2084. });
  2085.  
  2086. // Back button on Settings window.
  2087. var $backButton = $( '<a/>', {
  2088. 'id' : 'th-sw-back-button',
  2089. class : 'button grey',
  2090. title : 'Go Back without saving',
  2091. html : '&laquo; Back',
  2092. click : function() {
  2093. // Close this window.
  2094. settingsCloseState = 'back';
  2095. $.fancybox.close();
  2096. }
  2097. });
  2098.  
  2099. // Buttons on Settings window.
  2100. $( '<div/>', {
  2101. class : 'th-buttons-div',
  2102. html : '<span><a href="https://j.mp/tsu-helper-settings" target="_blank">Detailed Help</a></span>'
  2103. })
  2104. .prepend( $backButton )
  2105. .append( $saveButton )
  2106. .appendTo( $settingsWindow );
  2107.  
  2108.  
  2109. // Settings button on About window.
  2110. $( '<a/>', {
  2111. 'id' : 'th-aw-settings-button',
  2112. title : 'Change Settings',
  2113. html : '',
  2114. click : function() {
  2115. // Open settings window in a fancybox.
  2116. Settings.populateForm( $settingsForm );
  2117. $.fancybox( $settingsWindow, {
  2118. closeBtn : false,
  2119. modal : true,
  2120. beforeClose : function() {
  2121. // If the Back button was pressed, reopen the About window.
  2122. if ( 'back' === settingsCloseState ) {
  2123. setTimeout(function() {
  2124. $.fancybox( $aboutWindow );
  2125. }, 10);
  2126. return false;
  2127. }
  2128. },
  2129. afterClose : function() {
  2130. // If the Save button was pressed, reload the page.
  2131. if ( 'save' === settingsCloseState ) {
  2132. location.reload();
  2133. return;
  2134. }
  2135. }
  2136. });
  2137. }
  2138. })
  2139. .appendTo( $aboutWindow.find( 'h1' ) );
  2140.  
  2141.  
  2142. // Check if there is a newer version available.
  2143. if ( Updater.hasUpdate() ) {
  2144. $( '<a/>', {
  2145. 'id' : 'th-aw-update-button',
  2146. class : 'button th-update',
  2147. title : 'Update Tsu Helper to the newest version (' + Updater.remoteVersion + ')',
  2148. href : Updater.scriptURL,
  2149. html : 'New Version!',
  2150. click : function() {
  2151. if ( ! confirm( 'Upgrade to the newest version (' + Updater.remoteVersion + ')?\n\n(refresh this page after the script has been updated)' ) ) {
  2152. return false;
  2153. }
  2154. }
  2155. })
  2156. .attr( 'target', '_blank' ) // Open in new window / tab.
  2157. .appendTo( $aboutWindow.find( 'h1' ) );
  2158. }
  2159.  
  2160. // Link in the menu that opens the about window.
  2161. var $aboutWindowLink = $( '<a/>', {
  2162. title : 'About noplanman\'s Tsu Helper',
  2163. html : 'About Tsu Helper',
  2164. click : function() {
  2165. // Close the menu.
  2166. $( '#navBarHead .sub_nav' ).hide();
  2167.  
  2168. // Open about window in a fancybox.
  2169. $.fancybox( $aboutWindow );
  2170. }
  2171. });
  2172.  
  2173. // Check if there is a newer version available.
  2174. if ( Updater.hasUpdate() ) {
  2175. // Change the background color of the name tab on the top right.
  2176. $( '#navBarHead .tab.name' ).addClass( 'th-update' );
  2177. $aboutWindowLink.addClass( 'th-update' );
  2178. }
  2179.  
  2180. // Add "About" menu item.
  2181. $( '<li/>', { 'id' : 'th-menuitem-about', html : $aboutWindowLink } )
  2182. .appendTo( '#navBarHead .sub_nav' );
  2183. }
  2184.  
  2185. });