Tsu Helper

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

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