Advertisement
Guest User

8kun Bakertools v0.7.2

a guest
Mar 22nd, 2020
10,684
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. /*
  2. ==8Kun Baker Tools v0.7.2==
  3. We have entered the STORM. Be STRONG!
  4. GOD WINS!  Pray MEME LOVE!
  5.  
  6. For God and Country! WWG1WGA
  7.  
  8. ==Features:==
  9. '''Post Highlighting'''
  10. * Highlight posts that are marked notable (I.E. someone has replied and said
  11.   notable) in light green
  12. * Highlight nominating posts in dark green
  13. * Highlight nominating posts in posts mentions in light green
  14. * Highlight Q Posts in yellow
  15. * Highlight Q posts in mentions (I.E. on posts that get (YOU)'ed)
  16. * Highlight links to Q Posts in sparkle (Like q's trip)
  17. * Highlight previous bread links in blue
  18.  
  19. '''Navigation'''
  20. * Cycle through Q Posts
  21. * Cycle through (You)'s
  22. * Cycle through own posts
  23. * Jump To Bottom Link
  24. * Jump To Bottom Top link
  25. * NEW IN v0.7.0 Jump to last reading location (like when you post and it
  26.   sends you to bottom, you can jump right back)
  27. * Easy access to Breads via Bread List window
  28. * Scrollbar navigation shows location of Q/You/Notable/etc posts
  29.   * NEW IN v0.7.0: Hover over post marker to preview post
  30.   * Click on a post marker in scrollbar to jump to post
  31.  
  32. '''Filtering'''
  33. * Filter to only nominating and notable posts, Q posts, Q replies
  34. * Option to blur images until hover
  35. * Image blacklist (AKA the NOPE button)
  36. * NEW IN v0.7.0: SpamFader with multiple spam detection strategies:
  37.   * NameFag strategy: Marks namefags as spam
  38.   * Breadshitter strategy: Marks bread shitters as spam
  39.   * High post count strategy: Marks those with high post count as spam
  40.   * Flood fag strategy: Marks those who post in short intervals as spam
  41.   * Mark user as not spam button
  42.   * Spam badges tell WHY the algorithm marked as post as spam. TRANSPARENCY!
  43.  
  44. '''Customizable'''
  45. * NEW IN v0.7.0: Customizable post highlighting colors
  46. * Hide/Show features
  47. * Settings saved in localStorage
  48.  
  49. '''Notables'''
  50. * Generate notables post
  51. * Adds "Notable Nomination" button to posts that opens the
  52.   Quick Reply box and prefills it with a BAKER NOTABLE Template
  53.  
  54. '''Stats'''
  55.   * Thread stats overlay with
  56.     * color coded reply count that goes from green to red as bread ages
  57.     * UID Count
  58.   * Post rate chart shows how many posts per min
  59.  
  60. ==To Install:==
  61. 1. Copy this source code
  62. 2. Go to 8kun
  63. 3. Click "Options" in the top right
  64. 4. Choose "User JS" tab
  65. 5. Paste Baker tools JS
  66. 6. WWG1WGA
  67.  
  68. ==Changelog:==
  69. '''0.7.2'''
  70. * Use flex layout in bakerwindow controls
  71. * Reorder navcontrols
  72. * Reduce space of controls in boardlist
  73. * Disable spamfader new post event listener on disable
  74. * Don't mark q posts as notable
  75. * Make windows resizable width, fix table formatting in breadlist
  76. * Use boardlist height in current index calcs
  77.  
  78. '''0.7.1'''
  79. * Fix notable navigation in boardlist checkbox not working
  80. * Differentiate between previous and newer breads when highlighting
  81.  
  82. '''0.7.0'''
  83. * Switched color scheme to match other tools
  84. * Post Per Minute Graph
  85. * Spam Fading with multiple strategies and spam badges to tell why post is spam
  86. * Allow customization of post highligting colors
  87. * Add go back to last reading location button
  88. * Improve Q post detection (all past trip codes)
  89. * Add post preview on hover to Scrollbar Navigation
  90. * Navigation controls are now aware of current page location
  91. * Bugfixes
  92.  
  93. '''0.6.0'''
  94. * Navigation bar shows scroll location of q/you/notable
  95.   posts and allows jumping to posts
  96. * Notable navigation controls in baker window and board list
  97. * Persistent Image Blacklist (AKA Nope Button)
  98. * Many bugfixes
  99.  
  100. '''0.5.2'''
  101. * Fixes bread list table population bug
  102.  
  103. '''0.5.0'''
  104. * Option to show Q/(YOU)/Own Post navigation controls in the boardlist
  105. * Option to hide Notable nomination button
  106. * List of research breads
  107. * BakerTools settings are now saved in local storage
  108.  
  109. '''0.4.0'''
  110. * Option to blur images until hover
  111. * Adds a "Notable Nomination" button to posts that opens the Quick Reply
  112.   box and prefills it with a BAKER NOTABLE Template
  113. * Add Q Post navigation links to the Baker Window
  114. * Add (You) navigation links to the Baker Window
  115. * Add own post navigation links to the Baker Window
  116. * Cleaned up baker window design
  117.  
  118. * More code cleanup and linting changes
  119.  
  120. '''0.3.0'''
  121. * Highlights Q Posts with white BG -> DARK TO LIGHT!
  122. * Highlights Q posts in mentions (I.E. posts that get (YOU)'ed)
  123. * Highlights links to Q Posts
  124.  
  125. * Refactored code into classes for easier maint.
  126.  
  127. '''0.2.0'''
  128. * Highlight pb links
  129. * Thread stats overlay with
  130.     * color coded reply count that goes from green to red as bread ages
  131.     * UID Count
  132.     * Jump To Bottom Link
  133.     * Jump To Bottom Top link
  134.  
  135. '''0.1.0'''
  136. Initial release:
  137. * Highlight notables and nominators
  138. * Filter to only show notables and nominators
  139. * Create notables post
  140.  
  141. Version History:
  142. https://pastebin.com/dN5FhHCv 0.7.1
  143. https://pastebin.com/6XuDuHYu 0.7.0
  144. https://pastebin.com/YTSSmH7t 0.6.0
  145. https://pastebin.com/mPVxr7Lz 0.5.2
  146. https://pastebin.com/nEhm7yyY 0.5.1
  147. https://pastebin.com/i9sF0Rd3 0.4.0
  148. https://pastebin.com/kz9LrcE9 0.3.0
  149. https://pastebin.com/4aEFsPwK 0.2.0
  150. https://pastebin.com/eNmTtzdi 0.1.0
  151. */
  152. (function($) {
  153. "use strict";
  154.  
  155. /* globals $, board_name */
  156. /* exported 8kun */
  157.  
  158. /**
  159.  * Functions and vars related to EightKun functionality
  160.  */
  161. class EightKun {
  162.   /**
  163.    * Get reply links in post
  164.    * @param {Element} post div.post
  165.    * @return {JQuery}
  166.    */
  167.   static getReplyLinksFromPost(post) {
  168.     return $(post).find(EightKun.REPLY_SELECTOR)
  169.         .filter(function(idx, link) {
  170.           return $(link).text().match(EightKun.REPLY_SHORTLINK_REGEX);
  171.         });
  172.   }
  173.  
  174.   /**
  175.    * Get the post number that is being replied to
  176.    * @param {Anchor} link
  177.    * @return {string}
  178.    */
  179.   static getPostNumberFromReplyLink(link) {
  180.     return $(link).text()
  181.         .match(EightKun.REPLY_SHORTLINK_REGEX)[1];
  182.   }
  183.  
  184.   /**
  185.    * Get time of post
  186.    * @param {Element} post div.post
  187.    * @return {number} post time in unixtime
  188.    */
  189.   static getPostTime(post) {
  190.     return $(post).find('.intro time').attr('unixtime');
  191.   }
  192.  
  193.   /**
  194.    * Get date of post
  195.    * @param {Element} post div.post
  196.    * @return {number} post time in unixtime
  197.    */
  198.   static getPostDateTime(post) {
  199.     return $(post).find('.intro time').attr('datetime');
  200.   }
  201.  
  202.   /**
  203.    * Get poster id of provided post
  204.    * @param {Element} post div.post
  205.    * @return {string} id of poster
  206.    */
  207.   static getPosterId(post) {
  208.     return $(post).find('p > span.poster_id').first().text();
  209.   }
  210.  
  211.   /**
  212.    * Get name from post
  213.    * @param {Element} post div.post
  214.    * @return {string} name of post
  215.    */
  216.   static getPostName(post) {
  217.     return $(post).find('.intro > label > .name').text();
  218.   }
  219.  
  220.   /**
  221.    * Get trip from post
  222.    * @param {Element} post div.post
  223.    * @return {string} trip of post
  224.    */
  225.   static getPostTrip(post) {
  226.     return $(post).find('.intro .trip').text();
  227.   }
  228.  
  229.   /**
  230.    * Get the opening post of the thread
  231.    * @return {Element} div.post
  232.    */
  233.   static getOpPost() {
  234.     return $(EightKun.OP_POST_SELECTOR);
  235.   }
  236.  
  237.   /**
  238.    * Get poster id of OP
  239.    * @return {number} poster id
  240.    */
  241.   static getOpPosterId() {
  242.     return EightKun.getPosterId(EightKun.getOpPost());
  243.   }
  244.  
  245.   /**
  246.    * Is the post made by op?
  247.    * @param {Element} post div.post
  248.    * @return {boolean} true if op's post
  249.    */
  250.   static isPostFromOp(post) {
  251.     return EightKun.getPosterId(post) === EightKun.getOpPosterId();
  252.   }
  253.  
  254.   /**
  255.    * Get the thread id
  256.    * @return {number} id of thread
  257.    */
  258.   static getThreadId() {
  259.     return $('.thread').get(0).id.split('_')[1];
  260.   }
  261.  
  262.   /**
  263.    * Use 8kun hide function on post
  264.    * @param {Element} post div.post
  265.    */
  266.   static hidePost(post) {
  267.     // TODO: implement it and use in spam and blacklist
  268.   }
  269.  
  270.   /**
  271.    * Get current board
  272.    * @return {string}
  273.    */
  274.   static getCurrentBoard() {
  275.     /* eslint-disable camelcase */
  276.     return board_name;
  277.   }
  278.  
  279.   /**
  280.    * Get post number of post
  281.    * @param {Element} post div.post
  282.    * @return {number} Number of the post
  283.    */
  284.   static getPostNumber(post) {
  285.     return post.id.split('_')[1];
  286.   }
  287.  
  288.   /**
  289.    * Get the top boardlist element
  290.    * @return {Element} div.boardlist
  291.    */
  292.   static getTopBoardlist() {
  293.     return $(EightKun.TOP_BOARDLIST_SELECTOR).get(0);
  294.   }
  295. }
  296. EightKun.POST_SELECTOR = 'div.post';
  297. EightKun.POST_REPLY_SELECTOR = 'div.post.reply';
  298. EightKun.OP_POST_SELECTOR = 'div.post.op';
  299. EightKun.POST_BODY_SELECTOR = '.body';
  300. EightKun.POST_MODIFIED_SELECTOR = '.post_modified';
  301. EightKun.NEW_POST_EVENT = 'new_post';
  302. EightKun.OP_SUBJECT_SELECTOR = '.post.op > p > label > span.subject';
  303. EightKun.REPLY_SELECTOR = 'div.body:first a:not([rel="nofollow"])';
  304. EightKun.REPLY_SHORTLINK_REGEX = /^>>(\d+)$/;
  305. EightKun.REPLY_REGEX = /highlightReply\('(.+?)'/;
  306. EightKun.BOARDLIST_SELECTOR = `.boardlist`;
  307. EightKun.TOP_BOARDLIST_SELECTOR = `${EightKun.BOARDLIST_SELECTOR}:first`;
  308.  
  309. /**
  310.  * Wrapper for 8kun active_page variable to determine the type of
  311.  * page the user is on.
  312.  */
  313. class ActivePage {
  314.   /**
  315.    * Are we currently on the thread index page?
  316.    * @return {boolean} True if on index
  317.    */
  318.   static isIndex() {
  319.     return window.active_page == ActivePage.Index;
  320.   }
  321.  
  322.   /**
  323.    * Are we currently on the thread catalog page?
  324.    * @return {boolean} True if on catalog
  325.    */
  326.   static isCatalog() {
  327.     return window.active_page == ActivePage.Catalog;
  328.   }
  329.  
  330.   /**
  331.    * Are we on a thread page?
  332.    * @return {boolean} True if on thread
  333.    */
  334.   static isThread() {
  335.     return window.active_page == ActivePage.Thread;
  336.   }
  337. }
  338. ActivePage.Index = 'index';
  339. ActivePage.Catalog = 'catalog';
  340. ActivePage.Thread = 'thread';
  341.  
  342.  
  343. /* globals $ */
  344. /* exported ColorPicker */
  345.  
  346. /**
  347.  * A color picker control that saves to localStorage
  348.  */
  349. class ColorPicker {
  350.   /**
  351.    * Construct color picker
  352.    *
  353.    * @param {string} label The label for the control
  354.    * @param {string} title Mouseover title
  355.    * @param {string} setting localStorage setting name
  356.    * @param {string} defaultValue the default color when setting is missing
  357.    * @param {Function} changeHandler handler for value changes. Passes color
  358.    */
  359.   constructor(label, title, setting, defaultValue, changeHandler) {
  360.     this.styleId = 'bakertools-colorpickers-styles';
  361.     this.class = 'bakertools-colorpicker';
  362.     this.labelClass = 'bakertools-colorpicker-label';
  363.     this.inputClass = 'bakertools-colorpicker-input';
  364.     this.resetButtonClass = 'bakertools-colorpicker-reset';
  365.     this.changeHandler = changeHandler;
  366.  
  367.     this.label = label;
  368.     this.title = title;
  369.     this.setting = setting;
  370.     this.defaultValue = defaultValue;
  371.     this.strippedName = label.replace(/(\s|\(|\)|'|"|:)/g, '');
  372.     this.defaultValue = defaultValue;
  373.  
  374.     this._createStyles();
  375.     this._createElement();
  376.   }
  377.  
  378.   /**
  379.    * Create the HTML Element
  380.    */
  381.   _createElement() {
  382.     this.element = $(`
  383.     <div class='${this.class}'>
  384.       <label class='${this.labelClass}'
  385.         for="${this.strippedName}" title="${this.title}" >
  386.         ${this.label}:
  387.       </label>
  388.     </div>
  389.     `).get(0);
  390.  
  391.     this.input = $(`
  392.     <input type="color" class='${this.inputClass}'
  393.         id="${this.strippedName}" title="${this.title}" />
  394.     `).get(0);
  395.     $(this.element).append(this.input);
  396.  
  397.     $(this.input).change(function(e) {
  398.       this.setColor(this.input.value);
  399.     }.bind(this));
  400.  
  401.     this.reset = $(`
  402.     <button class='${this.resetButtonClass}' title="Reset to Default">
  403.       <i class="fa fa-undo"></i>
  404.     </button>
  405.     `).get(0);
  406.     $(this.element).append(this.reset);
  407.  
  408.     $(this.reset).click(function(e) {
  409.       e.preventDefault();
  410.       this.setColor(this.defaultValue);
  411.     }.bind(this));
  412.  
  413.     this.setColor(localStorage.getItem(this.setting) || this.defaultValue);
  414.   }
  415.  
  416.   /**
  417.    * Set the color
  418.    * @param {string} color valid css color string
  419.    */
  420.   setColor(color) {
  421.     localStorage.setItem(this.setting, color);
  422.     this.input.value = color;
  423.     this.changeHandler(color);
  424.   }
  425.  
  426.   /**
  427.    * Get the color
  428.    * @return {string} color
  429.    */
  430.   getColor() {
  431.     return this.input.value;
  432.   }
  433.   /**
  434.    * Create styles for the control
  435.    */
  436.   _createStyles() {
  437.     if ($(`#${this.styleId}`).length) {
  438.       return;
  439.     }
  440.     $('head').append(`
  441.     <style id='${this.styleId}'>
  442.       .${this.class} {
  443.         display: flex;
  444.         align-items: center;
  445.       }
  446.      
  447.       .${this.class} .${this.labelClass} {
  448.         flex-grow: 1;
  449.       }
  450.      
  451.       .${this.class} .${this.inputClass} {
  452.         margin-right: .5em;
  453.       }
  454.  
  455.       .${this.resetButtonClass} {
  456.         padding: 0;
  457.         background-color: Transparent;
  458.         background-repeat: no-repeat;
  459.         border: none;
  460.         cursor: pointer;
  461.         overflow: hidden;
  462.         outline: none;
  463.       }
  464.     </style>
  465.     `);
  466.   }
  467. }
  468.  
  469. /* global $, debounce, EightKun */
  470. /**
  471.  * Creates first, prev, next, last navigation controls
  472.  */
  473. class NavigationControl {
  474.   /**
  475.    * Construct navigatio control manager object
  476.    *
  477.    * @param {string} label the label for the control
  478.    * @param {Function} updateFunction Called to get latest data
  479.    * @param {string} updateEventName Called to get latest data
  480.    */
  481.   constructor(label, updateFunction, updateEventName) {
  482.     const strippedName = label.replace(/(\s|\(|\)|'|"|:)/g, '');
  483.     this.styleId = 'bakertools-navigationcontrol-styles';
  484.     this.label = label;
  485.     this.updateFunction = updateFunction;
  486.     this.updateEventName = updateEventName;
  487.     this.list = this.updateFunction();
  488.  
  489.     this.currentIndex = -1;
  490.     const instanceId = $(NavigationControl.containerClass).length;
  491.     this.navigationClass = `bakertools-navcontrol-${strippedName}`;
  492.     this.indexChangeEvent =
  493.       `bakertools-navcontrol-${strippedName}-index-changed`;
  494.  
  495.     this.currentIndexId = `${this.navigationClass}-current-index-${instanceId}`;
  496.     this.currentIndexClass = `bakertools-navcontrol-current-index`;
  497.     this.totalClass = `${this.navigationClass}-total`;
  498.     this.goToFirstClass = `${this.navigationClass}-goto-first`;
  499.     this.goToPreviousClass = `${this.navigationClass}-goto-prev`;
  500.     this.goToNextClass = `${this.navigationClass}-goto-next`;
  501.     this.goToLastClass = `${this.navigationClass}-goto-last`;
  502.  
  503.  
  504.     this._setupStyles();
  505.     this._createElement();
  506.     this.updateIndexFromCurrentScrollPosition();
  507.     this.updateIndexFromCurrentScrollPosition =
  508.         debounce(this.updateIndexFromCurrentScrollPosition, 500);
  509.     this._setupListeners();
  510.   }
  511.  
  512.   // TODO: switch to flexbox layout
  513.   /**
  514.    * setup styles for nav control
  515.    */
  516.   _setupStyles() {
  517.     if ($(`#${this.styleId}`).length) {
  518.       return;
  519.     }
  520.     const boardListNavSelector =
  521.       `${EightKun.BOARDLIST_SELECTOR} .${NavigationControl.containerClass}`;
  522.     $('head').append(`
  523.     <style id='${this.styleId}'>
  524.     ${boardListNavSelector}:before {
  525.       content: '[';
  526.       color: #89A;
  527.     }
  528.  
  529.     ${boardListNavSelector}:after {
  530.       content: ']';
  531.       color: #89A;
  532.     }
  533.    
  534.     ${boardListNavSelector} {
  535.       color: rgb(52, 52, 92);
  536.     }
  537.     </style>
  538.     `);
  539.   }
  540.  
  541.   /**
  542.    * Create nav element
  543.    */
  544.   _createElement() {
  545.     this.element = $(`
  546.     <span title="Navigate ${this.label}"
  547.         class="${NavigationControl.containerClass}">
  548.  
  549.         <label for="${this.navigationClass}">${this.label}:</label>
  550.         <span class="${this.navigationClass}
  551.                ${NavigationControl.navigationControlClass}">
  552.                 <i class="fa fa-angle-double-left ${this.goToFirstClass}"></i>
  553.                 <i class="fa fa-angle-left ${this.goToPreviousClass}"></i>
  554.  
  555.                 <span class="${this.currentIndexClass}" id='${this.currentIndexId}'>
  556.           ${this.currentIndex+1}
  557.         </span>
  558.         :
  559.         <span class="${this.totalClass}">${this.list.length}</span>
  560.  
  561.         <i class="fa fa-angle-right ${this.goToNextClass}"></i>
  562.         <i class="fa fa-angle-double-right ${this.goToLastClass}"></i>
  563.       </span>
  564.     </span>
  565.     `).get(0);
  566.   }
  567.  
  568.   /**
  569.    * Setup button event listeners
  570.    */
  571.   _setupListeners() {
  572.     $(this.element).find('.'+this.goToFirstClass).click(function(e) {
  573.       this.goToFirstPost();
  574.     }.bind(this));
  575.  
  576.     $(this.element).find('.'+this.goToPreviousClass).click(function(e) {
  577.       this.goToPreviousPost();
  578.     }.bind(this));
  579.  
  580.     $(this.element).find('.'+this.goToNextClass).click(function(e) {
  581.       this.goToNextPost();
  582.     }.bind(this));
  583.  
  584.     $(this.element).find('.'+this.goToLastClass).click(function(e) {
  585.       this.goToLastPost();
  586.     }.bind(this));
  587.  
  588.     $(document).on(this.indexChangeEvent, function(e, index) {
  589.       if (this.currentIndex == index) return;
  590.       this._setCurrentIndex(index);
  591.     }.bind(this));
  592.  
  593.     $(document).on(this.updateEventName, function() {
  594.       this.list = this.updateFunction();
  595.       $(this.element).find(`.${this.totalClass}`).text(this.list.length);
  596.     }.bind(this));
  597.  
  598.     $(document).scroll(this.updateIndexFromCurrentScrollPosition.bind(this));
  599.   }
  600.  
  601.   /**
  602.    * Determine the current index based on scroll position
  603.    */
  604.   updateIndexFromCurrentScrollPosition() {
  605.     const boardListHeight = $(EightKun.getTopBoardlist()).height();
  606.     for (let i = 0; i < this.list.length; ++i) {
  607.       const post = this.list[i];
  608.       const boundingRect = post.getBoundingClientRect();
  609.  
  610.       const postTopAboveBottomOfScreen = boundingRect.top < window.innerHeight;
  611.       const postBottomBelowTopOfScreen = boundingRect.bottom > boardListHeight;
  612.       const currentPostIsInViewport = postTopAboveBottomOfScreen &&
  613.         postBottomBelowTopOfScreen;
  614.  
  615.       if (currentPostIsInViewport) {
  616.         this._setCurrentIndex(i);
  617.         break;
  618.       }
  619.  
  620.       const isFirstPost = i === 0;
  621.       const isBeforeFirstNotable = isFirstPost && !postTopAboveBottomOfScreen;
  622.       if (isBeforeFirstNotable) {
  623.         this._setCurrentIndex(-1);
  624.         break;
  625.       }
  626.  
  627.       const isLastPost = i === (this.list.length - 1);
  628.       const isPastLastNotable = isLastPost && !postBottomBelowTopOfScreen;
  629.       if (isPastLastNotable) {
  630.         this._setCurrentIndex(i + .5);
  631.         break;
  632.       }
  633.  
  634.       const nextPost = this.list[i+1];
  635.       const nextPostBounds = nextPost.getBoundingClientRect();
  636.       const nextPostIsBelowBottomOfScreen =
  637.         nextPostBounds.top >= window.innerHeight;
  638.       const inBetweenPosts = !postBottomBelowTopOfScreen &&
  639.         nextPostIsBelowBottomOfScreen;
  640.       if (inBetweenPosts) {
  641.         this._setCurrentIndex(i + .5);
  642.         break;
  643.       }
  644.     }
  645.   }
  646.  
  647.   /**
  648.    * Scroll to first post
  649.    */
  650.   goToFirstPost() {
  651.     if (!this.list.length) {
  652.       return;
  653.     }
  654.     this._setCurrentIndex(0);
  655.     this.scrollToCurrentPost();
  656.   }
  657.  
  658.   /**
  659.    * Scroll to next navigated post
  660.    */
  661.   goToPreviousPost() {
  662.     if (!this.list.length) {
  663.       return;
  664.     }
  665.     if (this.currentIndex <= 0) {
  666.       this._setCurrentIndex(this.list.length - 1);
  667.     } else {
  668.       this._setCurrentIndex(Math.ceil(this.currentIndex) - 1);
  669.     }
  670.     this.scrollToCurrentPost();
  671.   }
  672.   /**
  673.    * Scroll to next navigated post
  674.    */
  675.   goToNextPost() {
  676.     if (!this.list.length) {
  677.       return;
  678.     }
  679.     const lastPostIndex = this.list.length - 1;
  680.     if (this.currentIndex >= lastPostIndex) {
  681.       this._setCurrentIndex(0);
  682.     } else {
  683.       this._setCurrentIndex(Math.floor(this.currentIndex) + 1);
  684.     }
  685.     this.scrollToCurrentPost();
  686.   }
  687.  
  688.   /**
  689.    * Scroll the last  post in this bread into view
  690.    */
  691.   goToLastPost() {
  692.     if (!this.list.length) {
  693.       return;
  694.     }
  695.     const numPosts = this.list.length;
  696.     this._setCurrentIndex(numPosts - 1);
  697.     this.scrollToCurrentPost();
  698.   }
  699.  
  700.   /**
  701.    * Scrolls the current selected  post into view
  702.    */
  703.   scrollToCurrentPost() {
  704.     const post = this.list[this.currentIndex];
  705.     $(post).get(0).scrollIntoView();
  706.  
  707.     // Trigger events for other views of this data
  708.     $(document).trigger(this.indexChangeEvent,
  709.         this.currentIndex);
  710.  
  711.     const boardListHeight = $(EightKun.getTopBoardlist()).height();
  712.     window.scrollBy(0, -boardListHeight);
  713.   }
  714.  
  715.   /**
  716.    * Set internal index var and UI
  717.    * @param {number} index
  718.    */
  719.   _setCurrentIndex(index) {
  720.     this.currentIndex = index;
  721.     this._setCurrentIndexControlValue(index + 1);
  722.   }
  723.  
  724.   /**
  725.    * Sets the value of the current index in the UI
  726.    * @param {number} val
  727.    */
  728.   _setCurrentIndexControlValue(val) {
  729.     $('#'+this.currentIndexId).text(val);
  730.   }
  731. }
  732. NavigationControl.containerClass = `bakertools-navcontrol-container`;
  733. NavigationControl.navigationControlClass = 'bakertools-navigation-control';
  734.  
  735. /* global EightKun, $, NotableHighlighter */
  736. /**
  737. * Wrapper for a post nominated as notable
  738. */
  739. class NotablePost {
  740.   /**
  741.    * Construct an empty notable post object
  742.    */
  743.   constructor() {
  744.     this.element = null;
  745.     this.postNumber = null;
  746.     this.description = '[DESCRIPTION]';
  747.     this.nominatingPosts = [];
  748.   }
  749.  
  750.   /**
  751.    * Create a notable post from a nominating post
  752.    *
  753.    * @param {Element} nominatingPost A post that is nominating a notable
  754.    * @return {NotablePost} a Notable post or NullNotablePost if it fails
  755.    */
  756.   static fromNominatingPost(nominatingPost) {
  757.     const notables = [];
  758.     EightKun.getReplyLinksFromPost(nominatingPost)
  759.         .each(function(idx, link) {
  760.           const postNumber = EightKun.getPostNumberFromReplyLink(link);
  761.           const notablePostElement = $(`#reply_${postNumber}`).get(0);
  762.           if (window.bakerTools.qPostHighlighter.isQ(notablePostElement)) {
  763.             return false;
  764.           }
  765.           if (!NotablePost.findNotableByPostNumber(postNumber)) {
  766.             const notable = new NotablePost();
  767.  
  768.             if (notablePostElement) {
  769.               notable.setElement(notablePostElement);
  770.             } else {
  771.               // TODO: set pb description
  772.               // get the json from the post number
  773.               notable.postNumber = postNumber;
  774.             }
  775.             notable.addNominatingPost(nominatingPost);
  776.  
  777.             NotablePost.addToListOfNotables(notable);
  778.             notables.push(notable);
  779.  
  780.             if (notable.element) { // Not pb will need to figure something out
  781.               $(document).trigger(NotablePost.NEW_NOTABLE_POST_EVENT,
  782.                   notable.element);
  783.             }
  784.           }
  785.         });
  786.     return notables;
  787.   }
  788.  
  789.   /**
  790.    * Add notable to list, and sort list
  791.    * @param {NotablePost} notable
  792.    */
  793.   static addToListOfNotables(notable) {
  794.     NotablePost._notables.push(notable);
  795.     NotablePost._notables.sort(function(n1, n2) {
  796.       if (n1.postNumber < n2.postNumber) {
  797.         return -1;
  798.       } else if ( n1.postNumber > n2.postNumber) {
  799.         return 1;
  800.       }
  801.       return 0;
  802.     });
  803.   }
  804.  
  805.   /**
  806.    * Is this a NullNotablePost
  807.    * @return {boolean} false
  808.    */
  809.   isNull() {
  810.     return false;
  811.   }
  812.  
  813.   /**
  814.    * @return {Array<NotablePost>} Array of the current notables
  815.    */
  816.   static getNotables() {
  817.     return NotablePost._notables;
  818.   }
  819.  
  820.   /**
  821.    * Get notable posts as regular 8kun div.post
  822.    * @return {Array} of div.post
  823.    */
  824.   static getNotablesAsPosts() {
  825.     return NotablePost._notables
  826.         .filter((n) => n.element !== null)
  827.         .map((n) => n.element);
  828.   }
  829.  
  830.   /**
  831.    * @arg {number} postNumber The post number of notable
  832.    * @return {NotablePost}
  833.    */
  834.   static findNotableByPostNumber(postNumber) {
  835.     return NotablePost._notables.find((notable) => notable.postNumber ==
  836.       postNumber);
  837.   }
  838.  
  839.   /**
  840.    * Set the element of the post
  841.    * @arg {Element} element
  842.    */
  843.   setElement(element) {
  844.     this.element = element;
  845.     this._markAsNotable(this.element);
  846.     this.description = element.querySelector('.body')
  847.         .innerText
  848.         .replace(/\n/g, ' ');
  849.     this.postNumber = $(this.element).find('.intro .post_no')
  850.         .text()
  851.         .replace('No.', '');
  852.   }
  853.  
  854.   /**
  855.    * Get the reply shortlink for the post
  856.    * @return {string}
  857.    */
  858.   shortLink() {
  859.     return '>>' + this.postNumber;
  860.   }
  861.  
  862.   /**
  863.    * Add a nominator to the notable
  864.    *
  865.    * @param {Element} nominatingPost A .div.post that nominates this post
  866.    */
  867.   addNominatingPost(nominatingPost) {
  868.     this.nominatingPosts.push(nominatingPost);
  869.     this._markAsNominator(nominatingPost);
  870.     this._markNominatorInMentions(nominatingPost);
  871.   }
  872.  
  873.   /**
  874.    * @arg {Element} nominatorPost .post
  875.    */
  876.   _markAsNominator(nominatorPost) {
  877.     nominatorPost.classList.add(NotableHighlighter.NOMINATOR_CLASS);
  878.   }
  879.  
  880.   /**
  881.    * @arg {Element} post .post
  882.    */
  883.   _markAsNotable(post) {
  884.     post.classList.add(NotableHighlighter.NOTABLE_CLASS);
  885.   }
  886.  
  887.  
  888.   /**
  889.    * Gives links to nominators a special style in notable mentions
  890.    *
  891.    * @param {Element} nominatingPost A .div.post that is nominating this
  892.    *  notable
  893.    */
  894.   _markNominatorInMentions(nominatingPost) {
  895.     if (!this.element) {
  896.       console.info(`Notable post is null - possible pb/lb`);
  897.       return;
  898.     }
  899.     const nominatingPostId = nominatingPost.id.replace('reply_', '');
  900.     $(this.element).find('.mentioned-'+nominatingPostId)
  901.         .addClass(NotableHighlighter.NOMINATOR_CLASS);
  902.   }
  903. }
  904. NotablePost._notables = [];
  905. NotablePost.NULL = null; // NullNotablePost
  906. NotablePost.NEW_NOTABLE_POST_EVENT = 'bakertools-new-notable-post-event';
  907.  
  908. /* globals EightKun */
  909. /**
  910.  * Research Bread Class
  911.  */
  912. class ResearchBread {
  913.   /**
  914.    * Get an array of post bodies with dough posts filtered out
  915.    * @return {NodeList} of .post elements
  916.    */
  917.   static getPostsWithoutDough() {
  918.     const posts = Array.from(document
  919.         .querySelectorAll(EightKun.POST_SELECTOR));
  920.  
  921.     const filteredPosts = posts.filter(function(post) {
  922.       return !post.querySelector(EightKun.POST_BODY_SELECTOR)
  923.           .innerText.match(ResearchBread.DOUGH_POSTS_REGEX);
  924.     });
  925.  
  926.     return filteredPosts;
  927.   }
  928.  
  929.   /**
  930.    * Determine what the bread number is
  931.    * @return {number} the number of the research bread
  932.    */
  933.   static getBreadNumber() {
  934.     const breadNumberRegex = /#(.+?) /;
  935.     return document.querySelector(EightKun.OP_SUBJECT_SELECTOR)
  936.         .innerText
  937.         .match(breadNumberRegex)[1] || 'COULD NOT FIND BREAD NUMBER';
  938.   }
  939.  
  940.   /**
  941.    * Find the post with the dough
  942.    * @return {Element} div.post
  943.    */
  944.   static getDoughPost() {
  945.     const posts = Array.from(document
  946.         .querySelectorAll(EightKun.POST_SELECTOR));
  947.  
  948.     const dough = posts.find(function(post) {
  949.       return post.querySelector(EightKun.POST_BODY_SELECTOR)
  950.           .innerText.toUpperCase().match(ResearchBread.DOUGH_POST_TITLE);
  951.     });
  952.     return dough;
  953.   }
  954. }
  955. ResearchBread.BOARD_NAME = 'qresearch';
  956. ResearchBread.WELCOME_POST_TITLE = 'Welcome To Q Research General';
  957. ResearchBread.ANNOUNCEMENTS_POST_TITLE = 'Global Announcements';
  958. ResearchBread.WAR_ROOM_POST_TITLE = 'War Room';
  959. ResearchBread.ARCHIVES_POST_TITLE = 'QPosts Archives';
  960. ResearchBread.DOUGH_POST_TITLE = 'DOUGH';
  961. ResearchBread.DOUGH_POSTS_REGEX = new RegExp(
  962.     `^(${ResearchBread.WELCOME_POST_TITLE}|` +
  963.     `${ResearchBread.ANNOUNCEMENTS_POST_TITLE}|` +
  964.     `${ResearchBread.WAR_ROOM_POST_TITLE}|` +
  965.     `${ResearchBread.ARCHIVES_POST_TITLE}|` +
  966.     `${ResearchBread.DOUGH_POST_TITLE}).*`);
  967.  
  968.  
  969. /* globals $, EightKun, debounce, POST_BACKGROUND_CHANGE_EVENT,
  970.    BakerWindow */
  971. /* exported ScrollbarNavigation */
  972. /**
  973.  * Scrollbar navigation
  974.  */
  975. class ScrollbarNavigation {
  976.   /**
  977.    * Construct a scrollbar nav
  978.    * @param {Array} addPostEvents List of event names that produce posts
  979.    *        to show on scrollbar
  980.    */
  981.   constructor(addPostEvents = []) {
  982.     this.id = 'bakertools-scrollbar-navigation';
  983.     this.showScrollbarNavigationId = 'bakertools-show-scrollbar-nav';
  984.     this.width = '20px';
  985.     this.posts = [];
  986.     this.coordsToPost = new Map();
  987.     this.addPostEvents = addPostEvents;
  988.  
  989.     this.draw = debounce(this.draw, 80);
  990.  
  991.     this._setupBakerWindowControls();
  992.     this._setupStyles();
  993.     this._createElement();
  994.     this._readSettings();
  995.     this._setupListeners();
  996.   }
  997.  
  998.   /**
  999.    * Read settings from localStorage
  1000.    */
  1001.   _readSettings() {
  1002.     let showScrollBar = JSON.parse(localStorage
  1003.         .getItem(ScrollbarNavigation.SHOW_SCROLLBAR_NAV));
  1004.  
  1005.     showScrollBar = showScrollBar === null ? true : showScrollBar;
  1006.  
  1007.     this.showScrollBar(showScrollBar);
  1008.   }
  1009.  
  1010.   /**
  1011.    * Add hide/show option to bakerwindow
  1012.    */
  1013.   _setupBakerWindowControls() {
  1014.     window.bakerTools.mainWindow
  1015.         .addNavigationOption(`
  1016.     <div class='${BakerWindow.CONTROL_GROUP_CLASS}'>
  1017.       <label for="${this.showScrollbarNavigationId}"
  1018.         title="Show scrollbar navigation" >
  1019.         Show Scrollbar Navigation:
  1020.       </label>
  1021.       <input type="checkbox" id="${this.showScrollbarNavigationId}"
  1022.         title="Show scrollbar navigation" /><br />
  1023.     </div>
  1024.     `);
  1025.   }
  1026.  
  1027.   /**
  1028.    * Setup event listeners
  1029.    */
  1030.   _setupListeners() {
  1031.     $('#'+this.showScrollbarNavigationId).change(function(e) {
  1032.       this.showScrollBar(e.target.checked);
  1033.     }.bind(this));
  1034.  
  1035.     $(document).on(EightKun.NEW_POST_EVENT, this.draw.bind(this));
  1036.     $(window).on('resize', this.draw.bind(this));
  1037.     $(window).on(POST_BACKGROUND_CHANGE_EVENT, this.draw.bind(this));
  1038.  
  1039.     $('#'+this.id).click(function(e) {
  1040.       const post = this.findFirstPostUnderMouse(e.clientX, e.clientY);
  1041.       if (post) {
  1042.         $(post).get(0).scrollIntoView();
  1043.         window.scrollBy(0, -20);
  1044.       }
  1045.     }.bind(this));
  1046.  
  1047.     $(this.element).mousemove(function(e) {
  1048.       const [post, coords] = this.findFirstPostUnderMouse(e.clientX, e.clientY);
  1049.       const notOverAPost = !post;
  1050.       const hoveringOverADifferentPost = this.hoveringPost &&
  1051.           this.hoveringPost != post;
  1052.  
  1053.       if (notOverAPost || hoveringOverADifferentPost) {
  1054.         this.endHover();
  1055.       }
  1056.       if (this.hovering) {
  1057.         return;
  1058.       }
  1059.  
  1060.       const top = coords.top;
  1061.       if (post) {
  1062.         this.postHover(post, top);
  1063.       }
  1064.     }.bind(this));
  1065.  
  1066.     $(this.element).mouseout(this.endHover.bind(this));
  1067.  
  1068.     this.addPostEvents.forEach(function(eventName) {
  1069.       $(document).on(eventName, function(event, posts) {
  1070.         this.addPosts(posts);
  1071.       }.bind(this));
  1072.     }.bind(this));
  1073.   }
  1074.  
  1075.   /**
  1076.    * Find the first post that is under the mouse
  1077.    * @param {number} clientX x location of mouse
  1078.    * @param {number} clientY y location of mouse
  1079.    * @return {Array} div.post, [top,bottom] or null if not found
  1080.    */
  1081.   findFirstPostUnderMouse(clientX, clientY) {
  1082.     let post = null;
  1083.     let coords = null;
  1084.     for (const keyValue of this.coordsToPost) {
  1085.       coords = keyValue[0];
  1086.       // if (clientY >= (top - 10) && clientY <= (bottom+10)) {
  1087.       if (clientY >= (coords.top) && clientY <= (coords.bottom)) {
  1088.         post = keyValue[1];
  1089.         break;
  1090.       }
  1091.     }
  1092.     return [post, coords];
  1093.   }
  1094.  
  1095.   /**
  1096.    * Perform post hover functionality for provided post
  1097.    * @param {Element} post div.post
  1098.    * @param {number} hoverY y location to hover at
  1099.    */
  1100.   postHover(post, hoverY) {
  1101.     this.hovering = true;
  1102.     this.hoveringPost = post;
  1103.     const $post = $(post);
  1104.     if ($post.is(':visible') &&
  1105.         $post.offset().top >= $(window).scrollTop() &&
  1106.         $post.offset().top + $post.height() <=
  1107.           $(window).scrollTop() + $(window).height()) {
  1108.       // post is in view
  1109.       $post.addClass('highlighted');
  1110.     } else {
  1111.       const newPost = $post.clone();
  1112.       newPost.find('>.reply, >br').remove();
  1113.       newPost.find('a.post_anchor').remove();
  1114.  
  1115.       const postNumber = EightKun.getPostNumber(post);
  1116.       newPost.attr('id', 'post-hover-' + postNumber)
  1117.           .attr('data-board', EightKun.getCurrentBoard())
  1118.           .addClass('post-hover')
  1119.           .css('border-style', 'solid')
  1120.           .css('box-shadow', '1px 1px 1px #999')
  1121.           .css('display', 'block')
  1122.           .css('position', 'absolute')
  1123.           .css('font-style', 'normal')
  1124.           .css('z-index', '100')
  1125.           .css('left', '0')
  1126.           .css('margin-left', '')
  1127.           .addClass('reply')
  1128.           .addClass('post')
  1129.           .appendTo('.thread');
  1130.  
  1131.       // shrink expanded images
  1132.       newPost.find('div.file img.post-image').css({
  1133.         'display': '',
  1134.         'opacity': '',
  1135.       });
  1136.       newPost.find('div.file img.full-image').remove();
  1137.  
  1138.       let previewWidth = newPost.outerWidth(true);
  1139.       const widthDiff = previewWidth - newPost.width();
  1140.       const scrollNavLeft = $(this.element).offset().left;
  1141.       let left;
  1142.  
  1143.       if (scrollNavLeft < $(document).width() * 0.7) {
  1144.         left = scrollNavLeft + $(this.element).width();
  1145.         if (left + previewWidth > $(window).width()) {
  1146.           newPost.css('width', $(window).width() - left - widthDiff);
  1147.         }
  1148.       } else {
  1149.         if (previewWidth > scrollNavLeft) {
  1150.           newPost.css('width', scrollNavLeft - widthDiff);
  1151.           previewWidth = scrollNavLeft;
  1152.         }
  1153.         left = scrollNavLeft - previewWidth;
  1154.       }
  1155.       newPost.css('left', left);
  1156.  
  1157.       const scrollTop = $(window).scrollTop();
  1158.       let top = scrollTop + hoverY;
  1159.  
  1160.       if (top < scrollTop + 15) {
  1161.         top = scrollTop;
  1162.       } else if (top > scrollTop + $(window).height() - newPost.height() - 15) {
  1163.         top = scrollTop + $(window).height() - newPost.height() - 15;
  1164.       }
  1165.  
  1166.       if (newPost.height() > $(window).height()) {
  1167.         top = scrollTop;
  1168.       }
  1169.       newPost.css('top', top);
  1170.     }
  1171.   }
  1172.  
  1173.   /**
  1174.    * End hovering
  1175.    */
  1176.   endHover() {
  1177.     this.hovering = false;
  1178.     if (!this.hoveringPost) {
  1179.       return;
  1180.     }
  1181.  
  1182.     $(this.hoveringPost).removeClass('highlighted');
  1183.     if ($(this.hoveringPost).hasClass('hidden')) {
  1184.       $(this.hoveringPost).css('display', 'none');
  1185.     }
  1186.     $('.post-hover').remove();
  1187.   }
  1188.  
  1189.   /**
  1190.    * Show/hide scrollbar
  1191.    * @param {boolean} shouldShow Shows if true
  1192.    */
  1193.   showScrollBar(shouldShow) {
  1194.     $('#'+this.showScrollbarNavigationId).prop('checked',
  1195.         shouldShow);
  1196.  
  1197.     localStorage.setItem(ScrollbarNavigation.SHOW_SCROLLBAR_NAV, shouldShow);
  1198.  
  1199.     if (shouldShow) {
  1200.       $(`#${this.id}`).show();
  1201.     } else {
  1202.       $(`#${this.id}`).hide();
  1203.     }
  1204.   }
  1205.  
  1206.   /**
  1207.    * Setup styles for canvas
  1208.    */
  1209.   _setupStyles() {
  1210.     $('head').append(`
  1211.     <style id='${this.id + '-style'}'>
  1212.       #${this.id} {
  1213.         position: fixed;
  1214.         top: 0;
  1215.         right: 0;
  1216.         height: 100%;
  1217.         width: ${this.width};
  1218.         background: #000000;
  1219.         background: linear-gradient(
  1220.           90deg,
  1221.           rgba(0,0,0,1) 0%,
  1222.           rgba(92,92,92,1) 50%,
  1223.           rgba(0,0,0,1) 100%
  1224.         );
  1225.       }
  1226.     </style>
  1227.     `);
  1228.   }
  1229.  
  1230.   /**
  1231.    * Create the canvas
  1232.    */
  1233.   _createElement() {
  1234.     $(document.body).append(`
  1235.       <canvas id='${this.id}' width='${this.width}' height='300'>
  1236.       </canvas>
  1237.     `);
  1238.     this.element = $(`#${this.id}`).get(0);
  1239.   }
  1240.  
  1241.   /**
  1242.    * Draw the scrollbar
  1243.    */
  1244.   draw() {
  1245.     const canvas = this.element;
  1246.     canvas.height = window.innerHeight;
  1247.     const ctx = canvas.getContext('2d');
  1248.     if (!ctx) {
  1249.       console.info('no ctx - is the element created yet?');
  1250.       return;
  1251.     }
  1252.     ctx.clearRect(0, 0, canvas.width, canvas.height);
  1253.  
  1254.     const cachedHeight = $(document).height();
  1255.     const scrollHeight = canvas.height;
  1256.  
  1257.     this.coordsToPost = new Map();
  1258.  
  1259.     let lastCoords = null;
  1260.     this.posts.forEach(function(post, index) {
  1261.       const color = $(post).css('backgroundColor');
  1262.       const postRect = post.getBoundingClientRect();
  1263.       const scrollLocationPercentage =
  1264.           (window.scrollY + postRect.top) / cachedHeight;
  1265.       let drawLocation = scrollLocationPercentage * scrollHeight;
  1266.  
  1267.       const overlappingPrevious = lastCoords &&
  1268.           drawLocation <= (lastCoords.bottom + 2);
  1269.       if (overlappingPrevious) {
  1270.         drawLocation = lastCoords.bottom + 4;
  1271.       }
  1272.       const drawHeight = Math.max(
  1273.           (postRect.height / cachedHeight) * scrollHeight,
  1274.           5,
  1275.       );
  1276.       const coords = new ScrollbarCoordinates(drawLocation,
  1277.           drawLocation + drawHeight);
  1278.       this.coordsToPost.set(coords, post);
  1279.  
  1280.       ctx.fillStyle = color;
  1281.       ctx.fillRect(0, drawLocation, canvas.width, drawHeight);
  1282.       lastCoords = coords;
  1283.     }.bind(this));
  1284.   }
  1285.  
  1286.   /**
  1287.    * Add posts to scrollbar
  1288.    * @param {Element|Array} post div.post
  1289.    */
  1290.   addPosts(post) {
  1291.     if (Array.isArray(post)) {
  1292.       post.forEach((p) => this._addPost(p));
  1293.     } else {
  1294.       this._addPost(post);
  1295.     }
  1296.     this._sortPosts();
  1297.     this.draw();
  1298.   }
  1299.  
  1300.   /**
  1301.    * Add post to post array if not already included
  1302.    * @param {Element} post div.post
  1303.    */
  1304.   _addPost(post) {
  1305.     if (this.posts.includes(post)) {
  1306.       return;
  1307.     }
  1308.     this.posts.push(post);
  1309.   }
  1310.  
  1311.   /**
  1312.    * Sort posts by time
  1313.    */
  1314.   _sortPosts() {
  1315.     this.posts.sort(function(p1, p2) {
  1316.       const p1PostTime = EightKun.getPostTime(p1);
  1317.       const p2PostTime = EightKun.getPostTime(p2);
  1318.       if (p1PostTime < p2PostTime) {
  1319.         return -1;
  1320.       }
  1321.       if (p1PostTime > p2PostTime) {
  1322.         return 1;
  1323.       }
  1324.       return 0;
  1325.     });
  1326.   }
  1327. }
  1328. ScrollbarNavigation.SHOW_SCROLLBAR_NAV = 'bakertools-show-scrollbar-nav';
  1329.  
  1330. /**
  1331.  * Coordinates on the scrollbar
  1332.  */
  1333. class ScrollbarCoordinates {
  1334.   /**
  1335.    * Construct coords
  1336.    * @param {number} top top of rect
  1337.    * @param {number} bottom top of rect
  1338.    */
  1339.   constructor(top, bottom) {
  1340.     this.top = top;
  1341.     this.bottom = bottom;
  1342.   }
  1343. }
  1344.  
  1345. /* exported debounce, POST_BACKGROUND_CHANGE_EVENT */
  1346. /**
  1347. * Returns a function, that, as long as it continues to be invoked, will not
  1348. * be triggered. The function will be called after it stops being called for
  1349. * N milliseconds. If `immediate` is passed, trigger the function on the
  1350. * leading edge, instead of the trailing.
  1351. * https://davidwalsh.name/javascript-debounce-function
  1352. *
  1353. * @param {Function} func
  1354. * @param {number} wait
  1355. * @param {boolean} immediate
  1356. * @return {Function} debounced function
  1357. */
  1358. function debounce(func, wait, immediate) {
  1359.   let timeout;
  1360.   return function(...args) {
  1361.     const context = this;
  1362.     const later = function() {
  1363.       timeout = null;
  1364.       if (!immediate) func.apply(context, args);
  1365.     };
  1366.     const callNow = immediate && !timeout;
  1367.     clearTimeout(timeout);
  1368.     timeout = setTimeout(later, wait);
  1369.     if (callNow) func.apply(context, args);
  1370.   };
  1371. }
  1372.  
  1373. const POST_BACKGROUND_CHANGE_EVENT = 'bakertools-post-background-change';
  1374.  
  1375. /* globals $, EightKun */
  1376. /* exported WindowElement */
  1377. /**
  1378.  * Class for windows
  1379.  */
  1380. class WindowElement {
  1381.   /**
  1382.    * Construct WindowElement
  1383.    * @param {string} windowName
  1384.    * @param {string} linkText
  1385.    */
  1386.   constructor(windowName, linkText) {
  1387.     this.styleId = 'bakertools-WindowElement-basestyles';
  1388.     this.id = `bakertools-${windowName}-window`;
  1389.     this.linkText = linkText;
  1390.     this.class = 'bakertools-WindowElement';
  1391.     this.headerClass = 'bakertools-WindowElement-header';
  1392.     this.windowCloseId = `bakertools-${windowName}-WindowElement-close`;
  1393.     this.windowCloseClass = `bakertools-WindowElement-close`;
  1394.     this.element = null;
  1395.  
  1396.     this._createWindowStyles();
  1397.     this._createElement();
  1398.     this._setupWindowLink();
  1399.   }
  1400.  
  1401.   /**
  1402.    * Create the window element
  1403.    */
  1404.   _createElement() {
  1405.     this.element = document.createElement('div');
  1406.     this.element.id = this.id;
  1407.     $(this.element).addClass(this.class);
  1408.  
  1409.     this.element.innerHTML = `
  1410.     <header class="${this.headerClass}">
  1411.       <h3>${this.linkText}</h3>
  1412.       <a id="${this.windowCloseId}" class='${this.windowCloseClass}'
  1413.         href="javascript:void(0)">
  1414.         <i class="fa fa-times"></i>
  1415.       </a>
  1416.     </header>
  1417.     `;
  1418.     document.body.appendChild(this.element);
  1419.  
  1420.     $(this.element).resizable({
  1421.       'handles': 'e, w',
  1422.     }).draggable();
  1423.     $(this.element).hide();
  1424.  
  1425.     $('#'+this.windowCloseId).click(function(e) {
  1426.       this.hide();
  1427.     }.bind(this));
  1428.   }
  1429.  
  1430.   /*
  1431.    * Create CSS styles needed by the window
  1432.    */
  1433.   _createWindowStyles() {
  1434.     if ($('#' + this.styleId).length) {
  1435.       return;
  1436.     }
  1437.     $('head').append(`
  1438.       <style id='${this.styleId}'>
  1439.       /*
  1440.        * ui-resizable styles: https://stackoverflow.com/a/11339280
  1441.        */
  1442.       .ui-resizable { position: relative;}
  1443.       .ui-resizable-handle {
  1444.         position: absolute;
  1445.         font-size: 0.1px;
  1446.         display: block;
  1447.       }
  1448.       .ui-resizable-disabled .ui-resizable-handle,
  1449.       .ui-resizable-autohide .ui-resizable-handle {
  1450.         display: none;
  1451.       }
  1452.       .ui-resizable-n {
  1453.         cursor: n-resize;
  1454.         height: 7px;
  1455.         width: 100%;
  1456.         top: -5px;
  1457.         left: 0;
  1458.       }
  1459.       .ui-resizable-s {
  1460.         cursor: s-resize;
  1461.         height: 7px;
  1462.         width: 100%;
  1463.         bottom: -5px;
  1464.         left: 0;
  1465.       }
  1466.       .ui-resizable-e {
  1467.         cursor: e-resize;
  1468.         width: 7px;
  1469.         right: -5px;
  1470.         top: 0;
  1471.         height: 100%;
  1472.       }
  1473.       .ui-resizable-w {
  1474.         cursor: w-resize;
  1475.         width: 7px;
  1476.         left: -5px;
  1477.         top: 0;
  1478.         height: 100%;
  1479.       }
  1480.       .ui-resizable-se {
  1481.         cursor: se-resize;
  1482.         width: 12px;
  1483.         height: 12px;
  1484.         right: 1px;
  1485.         bottom: 1px;
  1486.       }
  1487.       .ui-resizable-sw {
  1488.         cursor: sw-resize;
  1489.         width: 9px;
  1490.         height: 9px;
  1491.         left: -5px;
  1492.         bottom: -5px;
  1493.       }
  1494.       .ui-resizable-nw {
  1495.         cursor: nw-resize;
  1496.         width: 9px;
  1497.         height: 9px;
  1498.         left: -5px;
  1499.         top: -5px;
  1500.       }
  1501.       .ui-resizable-ne {
  1502.         cursor: ne-resize;
  1503.         width: 9px;
  1504.         height: 9px;
  1505.         right: -5px;
  1506.         top: -5px;
  1507.       }
  1508.  
  1509.       .${this.class} {
  1510.         width: 300px;
  1511.         background-color: rgb(214, 218, 240);
  1512.         position: fixed;
  1513.         z-index: 100;
  1514.         float: right;
  1515.         right:28.25px;
  1516.         border: 1px solid;
  1517.       }
  1518.  
  1519.       .${this.class} .${this.headerClass} {
  1520.         background: #98E;
  1521.         border: solid 1px;
  1522.         text-align: center;
  1523.         margin: 0px;
  1524.       }
  1525.      
  1526.       .${this.class} .${this.headerClass} h3 {
  1527.         margin: 0;
  1528.       }
  1529.  
  1530.       .${this.class} .${this.windowCloseClass} {
  1531.         top: 0px;
  1532.         right: 0px;
  1533.         position: absolute;
  1534.         margin-right: 3px;
  1535.         font-size: 20px;
  1536.       }
  1537.  
  1538.       .${this.class} details {
  1539.         padding: 5px;
  1540.       }
  1541.  
  1542.       .${this.class} summary {
  1543.         margin: 0 0 8px;
  1544.         font-weight: bold;
  1545.         border-bottom: solid 2px;
  1546.       }
  1547.     </style>
  1548.     `);
  1549.   }
  1550.  
  1551.   /**
  1552.    * Create link for show/hiding window, placed in boardlist bar
  1553.    */
  1554.   _setupWindowLink() {
  1555.     this.link = document.createElement('a');
  1556.     this.link.textContent = `[${this.linkText}]`;
  1557.     this.link.style.cssText = 'float: right;';
  1558.     this.link.title = this.linkText;
  1559.     this.link.href = 'javascript:void(0)';
  1560.     $(EightKun.getTopBoardlist()).append(this.link);
  1561.  
  1562.     this.link.onclick = this.toggle.bind(this);
  1563.   }
  1564.  
  1565.   /**
  1566.    * Setup timeout for updating bread list
  1567.    */
  1568.   _setupListeners() {
  1569.     // window.setTimeout(this.updateBreadList, 1000)
  1570.   }
  1571.  
  1572.   /**
  1573.    * Show the window
  1574.    */
  1575.   show() {
  1576.     $(this.element).css({'top': 15});
  1577.     $(this.element).show();
  1578.   }
  1579.  
  1580.   /**
  1581.    * Hide the window
  1582.    */
  1583.   hide() {
  1584.     $(this.element).hide();
  1585.   }
  1586.  
  1587.   /**
  1588.    * Is the window visible?
  1589.    * @return {boolean} true if window is visible
  1590.    */
  1591.   isVisible() {
  1592.     return $(this.element).is(':visible');
  1593.   }
  1594.  
  1595.   /**
  1596.    * Toggle visibility of window
  1597.    */
  1598.   toggle() {
  1599.     if (this.isVisible()) {
  1600.       this.hide();
  1601.     } else {
  1602.       this.show();
  1603.     }
  1604.   }
  1605. }
  1606.  
  1607. /* exported BakerWindow */
  1608. /* global NavigationControl, $, WindowElement */
  1609. /**
  1610. * Baker Window
  1611. */
  1612. class BakerWindow extends WindowElement {
  1613.   /**
  1614.    * Construct Baker window element, register listeners
  1615.    */
  1616.   constructor() {
  1617.     super('baker', 'Baker Tools');
  1618.     this.bakerWindowStyleId = 'bakertools-bakerwindow-style';
  1619.     this.bakerWindowOptionsId = 'bakertools-window-options';
  1620.     this.bakerWindowColorOptionsId = 'bakertools-window-color-options';
  1621.     this.bakerWindowNavigationOptionsId =
  1622.         'bakertools-window-navigation-options';
  1623.     this.bakerWindowNotableOptionsId =
  1624.         'bakertools-window-notable-options';
  1625.     this.bakerWindowSpamOptionsId =
  1626.         'bakertools-window-spam-options';
  1627.     this.bakerWindowNavigationId = 'bakertools-window-navigation';
  1628.     this.bakerWindowBakerId = 'bakertools-window-baker';
  1629.     this.bakerWindowBodyId = 'bakertools-bakerwindow-body';
  1630.  
  1631.     this._createStyles();
  1632.     this._createBody();
  1633.   }
  1634.  
  1635.   /**
  1636.    * Create CSS styles needed by the window
  1637.    */
  1638.   _createStyles() {
  1639.     if ($('#' + this.bakerWindowStyleId).length) {
  1640.       return;
  1641.     }
  1642.     $('head').append(`
  1643.     <style id='${this.bakerWindowStyleId}'>
  1644.       #${this.id} #${this.bakerWindowNavigationId}
  1645.       .${NavigationControl.containerClass} {
  1646.         display: inline-block;
  1647.         width: 100%;
  1648.       }
  1649.  
  1650.       #${this.id} #${this.bakerWindowNavigationId}
  1651.       .${NavigationControl.navigationControlClass} {
  1652.         float: right;
  1653.       }
  1654.  
  1655.       ${BakerWindow.CONTROL_GROUP_SELECTOR} {
  1656.         display: flex;
  1657.       }
  1658.  
  1659.       ${BakerWindow.CONTROL_GROUP_SELECTOR} label {
  1660.         flex-grow: 1;
  1661.       }
  1662.     </style>
  1663.     `);
  1664.   }
  1665.  
  1666.   /**
  1667.    * Create the actual window HTML element
  1668.    */
  1669.   _createBody() {
  1670.     $('#'+this.id).append(`
  1671.     <form id="${this.bakerWindowBodyId}">
  1672.       <details id='${this.bakerWindowOptionsId}' open>
  1673.         <summary>Options</summary>
  1674.         <details id='${this.bakerWindowColorOptionsId}' open>
  1675.           <summary>Colors</summary>
  1676.         </details>
  1677.         <details id='${this.bakerWindowNavigationOptionsId}' open>
  1678.           <summary>Navigation</summary>
  1679.         </details>
  1680.         <details id='${this.bakerWindowNotableOptionsId}' open>
  1681.           <summary>Notables</summary>
  1682.         </details>
  1683.         <details id='${this.bakerWindowSpamOptionsId}' open>
  1684.           <summary>Spam</summary>
  1685.         </details>
  1686.  
  1687.  
  1688.       </details>
  1689.       <details id='${this.bakerWindowNavigationId}' open>
  1690.         <summary>Navigation</summary>
  1691.       </details>
  1692.       <details id='${this.bakerWindowBakerId}' open>
  1693.         <summary>Baker Tools</summary>
  1694.       </details>
  1695.     </form>
  1696.     `);
  1697.   }
  1698.  
  1699.   /**
  1700.    * Add form controls to options section of baker window
  1701.    * @arg {Element} htmlContentString form controls
  1702.    */
  1703.   addOption(htmlContentString) {
  1704.     $('#'+this.bakerWindowOptionsId).append(htmlContentString);
  1705.   }
  1706.  
  1707.   /**
  1708.    * Add form controls to notable options section of baker window
  1709.    * @arg {Element} htmlContentString form controls
  1710.    */
  1711.   addNotableOption(htmlContentString) {
  1712.     $('#'+this.bakerWindowNotableOptionsId)
  1713.         .append(htmlContentString);
  1714.   }
  1715.  
  1716.   /**
  1717.    * Add form controls to spam options section of baker window
  1718.    * @arg {Element} htmlContentString form controls
  1719.    */
  1720.   addSpamOption(htmlContentString) {
  1721.     $('#'+this.bakerWindowSpamOptionsId)
  1722.         .append(htmlContentString);
  1723.   }
  1724.  
  1725.   /**
  1726.    * Add form controls to navigation options section of baker window
  1727.    * @arg {Element} htmlContentString form controls
  1728.    */
  1729.   addNavigationOption(htmlContentString) {
  1730.     $('#'+this.bakerWindowNavigationOptionsId)
  1731.         .append(htmlContentString);
  1732.   }
  1733.  
  1734.   /**
  1735.    * Add form controls to color options section of baker window
  1736.    * @arg {Element} htmlContentString form controls
  1737.    */
  1738.   addColorOption(htmlContentString) {
  1739.     $('#'+this.bakerWindowColorOptionsId).append(htmlContentString);
  1740.   }
  1741.  
  1742.  
  1743.   /**
  1744.    * Add html elements to the navigation section of the baker window
  1745.    * @arg {Element} htmlContentString form controls
  1746.    */
  1747.   addNavigation(htmlContentString) {
  1748.     $('#'+this.bakerWindowNavigationId).append(htmlContentString);
  1749.   }
  1750.  
  1751.   /**
  1752.    * Add html elements to the baker section of the baker window
  1753.    * @arg {Element} htmlContentString form controls
  1754.    */
  1755.   addBaker(htmlContentString) {
  1756.     $('#'+this.bakerWindowBakerId).append(htmlContentString);
  1757.   }
  1758. } // end class BakerWindow
  1759. BakerWindow.CONTROL_GROUP_CLASS = 'bakertools-bakerwindow-control-group';
  1760. BakerWindow.CONTROL_GROUP_SELECTOR =
  1761.   `.${BakerWindow.CONTROL_GROUP_CLASS}`;
  1762.  
  1763. /* global $, BakerWindow */
  1764. /**
  1765. * Blur images until highlighted
  1766. */
  1767. class BlurImages {
  1768.   /**
  1769.    * Construct blur images object and setup styles
  1770.    */
  1771.   constructor() {
  1772.     this.blurImages = 'bakertools-blur-images';
  1773.     this.blurImagesStyleId = 'bakertools-blur-images-style';
  1774.     window.bakerTools.mainWindow.addOption(`
  1775.     <div class='${BakerWindow.CONTROL_GROUP_CLASS}'>
  1776.       <label for="${this.blurImages}">Blur Images Until Hover</label>
  1777.       <input type="checkbox" id="${this.blurImages}"
  1778.         title="Blur images until mouse hover" /></br>
  1779.     </div>
  1780.     `);
  1781.  
  1782.     $('#'+this.blurImages).change(function(e) {
  1783.       this.setBlurImages(e.target.checked);
  1784.     }.bind(this));
  1785.  
  1786.     this._readSettings();
  1787.   }
  1788.  
  1789.   /**
  1790.    * Read settings from localStorage
  1791.    */
  1792.   _readSettings() {
  1793.     this.setBlurImages(JSON.parse(
  1794.         localStorage.getItem(
  1795.             BlurImages.BLUR_IMAGES_SETTING),
  1796.     ));
  1797.   }
  1798.  
  1799.   /**
  1800.    * Set whether or not images are blurred
  1801.    * @param {boolean} blurImages if true, blur images
  1802.    */
  1803.   setBlurImages(blurImages) {
  1804.     $('#'+this.blurImages).prop('checked',
  1805.         blurImages);
  1806.  
  1807.     localStorage.setItem(BlurImages.BLUR_IMAGES_SETTING,
  1808.         blurImages);
  1809.  
  1810.     if (blurImages) {
  1811.       $(`<style id='${this.blurImagesStyleId}' type='text/css'>
  1812.           .post-image {
  1813.               filter: blur(5px);
  1814.               transition: all 233ms;
  1815.           }
  1816.           .post-image:hover {
  1817.               filter: blur(.5px);
  1818.               transition: all 89ms;
  1819.           }
  1820.       </style>`).appendTo('head');
  1821.     } else {
  1822.       $(`#${this.blurImagesStyleId}`).remove();
  1823.     }
  1824.   }
  1825. }
  1826. BlurImages.BLUR_IMAGES_SETTING = 'bakertools-blur-images';
  1827.  
  1828.  
  1829.  
  1830. /* globals $, WindowElement, ResearchBread */
  1831. /* exported BreadList */
  1832. /**
  1833.  * Creates a list of breads for navigation comfyness
  1834.  */
  1835. class BreadList extends WindowElement {
  1836.   /**
  1837.    * Construct breadlist object
  1838.    */
  1839.   constructor() {
  1840.     super('breadlist', 'Bread List');
  1841.     $('#'+this.id).css('height', '400px');
  1842.     this.breadListWindowHeaderId = 'bakertools-breadlist-window-header';
  1843.     this.breadListWindowCloseId = 'bakertools-breadlist-window-close';
  1844.     this.breadListWindowBody = 'bakertools-breadlist-window-body';
  1845.     this.breadListTable = 'bakertools-breadlist-table';
  1846.     this.lastUpdatedId = 'bakertools-breadlist-lastupdated';
  1847.  
  1848.     this._breads = [];
  1849.     ResearchBread.BOARD_NAME = 'qresearch';
  1850.     this.breadRegex = /(.+)\s+#(\d+):\s+(.+?$)/;
  1851.     // /\(.+\) #\(\d+\): \(.+?$\)/;
  1852.     this.indexPage = `${window.location.protocol}//${window.location.host}` +
  1853.         `/${ResearchBread.BOARD_NAME}/`;
  1854.  
  1855.     this._createBody();
  1856.     this._setupStyles();
  1857.     this.updateBreadList();
  1858.     this._setupListeners();
  1859.   }
  1860.  
  1861.   /**
  1862.    * setup table styles
  1863.    */
  1864.   _setupStyles() {
  1865.     // https://stackoverflow.com/questions/21168521/table-fixed-header-and-scrollable-body
  1866.     $('head').append(`
  1867.     <style id='baketools-breadlist-window-styles'>
  1868.       #${this.id} {
  1869.         right: 380px;
  1870.         width: 380px;
  1871.       }
  1872.  
  1873.       #${this.breadListWindowBody} {
  1874.         overflow-y: auto;
  1875.         height: 365px;
  1876.         font-size: .8em;
  1877.       }
  1878.  
  1879.       #${this.breadListTable} {
  1880.         border-collapse: collapse;
  1881.         border-spacing: 0px;
  1882.         margin: 0;
  1883.         width: 100%;
  1884.       }
  1885.  
  1886.       #${this.breadListTable} thead th {
  1887.         position: sticky;
  1888.         top: 0;
  1889.       }
  1890.  
  1891.       #${this.breadListTable} th,
  1892.       #${this.breadListTable} td {
  1893.         border: 1px solid #000;
  1894.         border-top: 0;
  1895.         padding: 1px 2px 1px 2px;
  1896.         margin: 0;
  1897.       }
  1898.       #${this.breadListTable} thead th {
  1899.         box-shadow: 1px 1px 0 #000;
  1900.       }
  1901.     </style>
  1902.     `);
  1903.   }
  1904.  
  1905.   /**
  1906.    * Create the actual window HTML element
  1907.    */
  1908.   _createBody() {
  1909.     $('#'+this.id).append(`
  1910.     <div id='${this.breadListWindowBody}'>
  1911.       <table id='${this.breadListTable}'>
  1912.         <thead>
  1913.           <tr>
  1914.             <th>Group</th>
  1915.             <th>No.</th>
  1916.             <th>Bread</th>
  1917.             <th>replies</th>
  1918.           </tr>
  1919.         </thead>
  1920.         <tbody>
  1921.         </tbody>
  1922.       </table>
  1923.     </div>
  1924.     <footer>
  1925.       Last Updated: <span id="${this.lastUpdatedId}"></span>
  1926.     </footer>
  1927.     `);
  1928.   }
  1929.  
  1930.   /**
  1931.    * Setup timeout for updating bread list
  1932.    */
  1933.   _setupListeners() {
  1934.     window.setInterval(function(e) {
  1935.       this.updateBreadList();
  1936.     }.bind(this), 1000 * 60 * 2.5); // 2.5min update
  1937.   }
  1938.  
  1939.   /**
  1940.    * Get the list of breads
  1941.    */
  1942.   updateBreadList() {
  1943.     this.breads = [];
  1944.  
  1945.     const promises = [];
  1946.     for (let page = 0; page < 3; page++) {
  1947.       promises.push(
  1948.           $.getJSON(this.indexPage + `${page}.json`,
  1949.               this.parseIndex.bind(this)),
  1950.       );
  1951.     }
  1952.     Promise.all(promises).then(function() {
  1953.       this.breads.sort(function(a, b) {
  1954.         if (a.lastModified < b.lastModified) return -1;
  1955.         if (a.lastModified == b.lastModified) return 0;
  1956.         if (a.lastModified > b.lastModified) return 1;
  1957.       }).reverse();
  1958.       this.populateBreadTable();
  1959.     }.bind(this));
  1960.   }
  1961.  
  1962.   /**
  1963.    * Parse index json for breads
  1964.    * @param {Object} index
  1965.    */
  1966.   parseIndex(index) {
  1967.     if (index && index.threads) {
  1968.       index.threads.forEach(function(thread) {
  1969.         const op = thread.posts[0];
  1970.         const match = op.sub.match(this.breadRegex);
  1971.  
  1972.         if (match) {
  1973.           const researchGroup = match[1];
  1974.           const breadNumber = match[2];
  1975.           const breadName = match[3];
  1976.           this.breads.push(new Bread(
  1977.               ResearchBread.BOARD_NAME,
  1978.               researchGroup,
  1979.               breadNumber,
  1980.               breadName,
  1981.               op.replies,
  1982.               op.no,
  1983.               op.last_modified,
  1984.           ));
  1985.         }
  1986.       }.bind(this)); // Index foreach
  1987.     } // if index and index.threads
  1988.   }
  1989.  
  1990.   /**
  1991.    * Populate the bread list table
  1992.    */
  1993.   populateBreadTable() {
  1994.     $(`#${this.breadListTable} tbody`).empty();
  1995.     this.breads.forEach(function(bread) {
  1996.       this._addBread(bread);
  1997.     }.bind(this));
  1998.  
  1999.     const lastUpdated = new Date();
  2000.     $('#'+this.lastUpdatedId).text(lastUpdated.toLocaleString());
  2001.   }
  2002.  
  2003.   /**
  2004.    * Add bread
  2005.    * @param {Bread} bread
  2006.    */
  2007.   _addBread(bread) {
  2008.     $(`#${this.breadListTable} tbody`).append(`
  2009.       <tr>
  2010.         <td><a href='${bread.url}'>${bread.researchGroup}</a></td>
  2011.         <td><a href='${bread.url}'>${bread.researchNumber}</a></td>
  2012.         <td><a href='${bread.url}'>${bread.breadName}</a></td>
  2013.         <td><a href='${bread.url}'>${bread.replies}</a></td>
  2014.       </tr>
  2015.     `);
  2016.   }
  2017. }
  2018.  
  2019. /**
  2020.  * Represents a research bread
  2021.  */
  2022. class Bread {
  2023.   /**
  2024.    * Construct a bread
  2025.    *
  2026.    * @param {string} boardName
  2027.    * @param {string} researchGroup
  2028.    * @param {number} researchNumber
  2029.    * @param {string} breadName
  2030.    * @param {number} replies
  2031.    * @param {number} postId
  2032.    * @param {number} lastModified
  2033.    */
  2034.   constructor(boardName, researchGroup, researchNumber, breadName,
  2035.       replies, postId, lastModified) {
  2036.     this.boardName = boardName;
  2037.     this.researchGroup = researchGroup;
  2038.     this.researchNumber = researchNumber;
  2039.     this.breadName = breadName;
  2040.     this.replies = replies;
  2041.     this.postId = postId;
  2042.     this.lastModified = lastModified;
  2043.   }
  2044.  
  2045.   /**
  2046.    * Get bread url
  2047.    *
  2048.    * @return {string} url to bread
  2049.    */
  2050.   get url() {
  2051.     return `${window.location.protocol}//${window.location.host}` +
  2052.         `/${this.boardName}/res/${this.postId}.html`;
  2053.   }
  2054. }
  2055.  
  2056. /* global $, EightKun, BakerWindow */
  2057. /**
  2058. * Persistent image blacklist (AKA NOPE BUTTON)
  2059. */
  2060. class ImageBlacklist {
  2061.   /**
  2062.    * Construct ImageBlacklist object
  2063.    */
  2064.   constructor() {
  2065.     this.blacklist = [];
  2066.     this.styleId = 'bakertools-blacklist-style';
  2067.     this.postBlacklistButtonClass = 'bakertools-blacklist-post';
  2068.     this.imgBlacklistButtonClass = 'bakertools-blacklist-image';
  2069.     this.hidePostBlacklistButtonCheckboxId =
  2070.         'bakertools-hide-post-blacklist-buttons';
  2071.  
  2072.     this._setupBakerWindowControls();
  2073.     this._readSettings();
  2074.     this._setupStyles();
  2075.     this._setupListeners();
  2076.     this.removeBlacklistedImages();
  2077.     this.addBlacklistButtons();
  2078.   }
  2079.  
  2080.   /**
  2081.    * Add options to baker window
  2082.    */
  2083.   _setupBakerWindowControls() {
  2084.     window.bakerTools.mainWindow.addSpamOption(`
  2085.     <div class='${BakerWindow.CONTROL_GROUP_CLASS}'>
  2086.       <label for="${this.hidePostBlacklistButtonCheckboxId}"
  2087.         title="Hide post 'Blacklist' buttons" >
  2088.         Hide "Blacklist" buttons
  2089.       </label>
  2090.       <input type="checkbox" id="${this.hidePostBlacklistButtonCheckboxId}"
  2091.         title="Hide post 'Blacklist' buttons" /><br />
  2092.       </div>
  2093.     `);
  2094.   }
  2095.  
  2096.  
  2097.   /**
  2098.    * Show or hide the post blacklist buttons
  2099.    *
  2100.    * @param {boolean} hide
  2101.    */
  2102.   hidePostBlacklistButton(hide) {
  2103.     $('#'+this.hidePostBlacklistButtonCheckboxId).prop('checked',
  2104.         hide);
  2105.  
  2106.     localStorage.setItem(ImageBlacklist.HIDE_POST_BLACKLIST_BUTTON_SETTING,
  2107.         hide);
  2108.  
  2109.     const styleId = 'baker-tools-post-blacklist-button-style';
  2110.     if (hide) {
  2111.       $('head').append(`
  2112.         <style id='${styleId}'>
  2113.           .${this.postBlacklistButtonClass} {
  2114.             display: none;
  2115.           }
  2116.       `);
  2117.     } else {
  2118.       $(`#${styleId}`).remove();
  2119.     }
  2120.   }
  2121.  
  2122.   /**
  2123.    * Setup styles for blacklist buttons
  2124.    */
  2125.   _setupStyles() {
  2126.     $('head').append(`
  2127.       <style id='${this.styleId}'>
  2128.         .${this.imgBlacklistButtonClass} {
  2129.           padding: 0px;
  2130.           background-color: Transparent;
  2131.           background-repeat: no-repeat;
  2132.           border: none;
  2133.           overflow: hidden;
  2134.           outline: none;
  2135.           cursor: pointer;
  2136.         }
  2137.       </style>
  2138.     `);
  2139.   }
  2140.  
  2141.   /**
  2142.    * Read settings from localstorage
  2143.    */
  2144.   _readSettings() {
  2145.     this.loadBlacklist();
  2146.  
  2147.     const hideBlacklistButton = JSON.parse(
  2148.         localStorage.getItem(ImageBlacklist.HIDE_POST_BLACKLIST_BUTTON_SETTING),
  2149.     );
  2150.     this.hidePostBlacklistButton(hideBlacklistButton);
  2151.   }
  2152.  
  2153.   /**
  2154.    * Setup new post event listeners
  2155.    */
  2156.   _setupListeners() {
  2157.     $(document).on(EightKun.NEW_POST_EVENT, function(e, post) {
  2158.       this.addBlacklistButtonToPost(post);
  2159.     }.bind(this));
  2160.  
  2161.     $('#'+this.hidePostBlacklistButtonCheckboxId).change(function(e) {
  2162.       this.hidePostBlacklistButton(e.target.checked);
  2163.     }.bind(this));
  2164.   }
  2165.  
  2166.   /**
  2167.    * Load blacklist from localStorage
  2168.    */
  2169.   loadBlacklist() {
  2170.     this.blacklist = JSON.parse(localStorage.imageBlacklist || '[]');
  2171.   }
  2172.  
  2173.   /**
  2174.    * Save blacklist to localStorage
  2175.    */
  2176.   saveBlacklist() {
  2177.     localStorage.imageBlacklist = JSON.stringify(this.blacklist);
  2178.   }
  2179.  
  2180.   /**
  2181.    * Add MD5 of an image to the blacklist
  2182.    * @param {string} md5 md5 hash of image
  2183.    */
  2184.   addToBlacklist(md5) {
  2185.     if (md5 && -1 === this.blacklist.indexOf(md5)) {
  2186.       this.blacklist.push(md5);
  2187.     }
  2188.   }
  2189.  
  2190.   /**
  2191.    * Blacklist images in post
  2192.    * @param {Element} post
  2193.    */
  2194.   blacklistPostImages(post) {
  2195.     $(post).find(ImageBlacklist.POST_IMG_SELECTOR).each(function(i, postImage) {
  2196.       const md5 = postImage.getAttribute('data-md5');
  2197.       this.addToBlacklist(md5);
  2198.       this.deletePostImage(postImage);
  2199.     }.bind(this));
  2200.   }
  2201.  
  2202.   /**
  2203.    * Remove blacklist images on page load
  2204.    * @return {number} number of images removed
  2205.    */
  2206.   removeBlacklistedImages() {
  2207.     let removed = 0;
  2208.     $(ImageBlacklist.POST_IMG_SELECTOR).each(function(i, postImage) {
  2209.       if (-1 !== this.blacklist.indexOf(postImage.getAttribute('data-md5'))) {
  2210.         this.deletePostImage(postImage);
  2211.         removed += 1;
  2212.       }
  2213.     }.bind(this));
  2214.     return removed;
  2215.   }
  2216.  
  2217.   /**
  2218.    * Add blacklist buttons to post images
  2219.    */
  2220.   addBlacklistButtons() {
  2221.     $('div.post').each(function(i, post) {
  2222.       this.addBlacklistButtonToPost(post);
  2223.     }.bind(this));
  2224.   }
  2225.  
  2226.   /**
  2227.    * Add blacklist buttons to post
  2228.    * @param {Element} post div.post
  2229.    */
  2230.   addBlacklistButtonToPost(post) {
  2231.     const postImageCount = $(post)
  2232.         .find(ImageBlacklist.POST_IMG_SELECTOR).length;
  2233.  
  2234.     if (postImageCount == 0) {
  2235.       return;
  2236.     }
  2237.  
  2238.     const postBlacklistButton = document.createElement('button');
  2239.     $(postBlacklistButton).addClass(this.postBlacklistButtonClass);
  2240.     $(postBlacklistButton).append(`
  2241.     <i class="fa fa-trash" style="color: crimson;"></i>
  2242.     Blacklist all post images`);
  2243.  
  2244.     $(postBlacklistButton).click(function(e) {
  2245.       e.preventDefault();
  2246.       $(post).hide();
  2247.       this.blacklistPostImages(post);
  2248.       this.saveBlacklist();
  2249.     }.bind(this));
  2250.  
  2251.     $(post).find(EightKun.POST_MODIFIED_SELECTOR).append(postBlacklistButton);
  2252.  
  2253.     $(post).find(ImageBlacklist.POST_IMG_SELECTOR).each(function(i, img) {
  2254.       const imgBlacklistButton = document.createElement('button');
  2255.       $(imgBlacklistButton).addClass(this.imgBlacklistButtonClass);
  2256.       $(imgBlacklistButton).append(`
  2257.       <i class="fa fa-trash" style="color: crimson;"
  2258.         title="Blacklist image"
  2259.         ></i>`);
  2260.  
  2261.       $(img)
  2262.           .parents('div.file')
  2263.           .find('.fileinfo')
  2264.           .prepend(imgBlacklistButton);
  2265.  
  2266.       $(imgBlacklistButton).click(function(e) {
  2267.         e.preventDefault();
  2268.         const md5 = img.getAttribute('data-md5');
  2269.         this.addToBlacklist(md5);
  2270.         this.deletePostImage(img);
  2271.         this.saveBlacklist();
  2272.       }.bind(this));
  2273.     }.bind(this));
  2274.   }
  2275.  
  2276.   /**
  2277.    * Delete post image
  2278.    * @param {Element} image
  2279.    */
  2280.   deletePostImage(image) {
  2281.     const imageParent = $(image).parent().parent();
  2282.     $(imageParent).append(`
  2283.       Image blacklisted
  2284.       ${image.getAttribute('data-md5')}
  2285.     `);
  2286.     $(image).remove();
  2287.   }
  2288. }
  2289. ImageBlacklist.POST_IMG_SELECTOR = 'img.post-image';
  2290. ImageBlacklist.HIDE_POST_BLACKLIST_BUTTON_SETTING =
  2291.   'bakertools-hide-post-blacklist-button';
  2292.  
  2293. /* global $, EightKun */
  2294. /**
  2295. * Add notable button to posts that opens quick reply
  2296. * and populates with a template message
  2297. */
  2298. class NominatePostButtons {
  2299.   /**
  2300.    * Construct NPB object and setup listeners
  2301.    */
  2302.   constructor() {
  2303.     this.nominateButtonClass = 'bakertools-nominate-button';
  2304.     this.hidePostNotableButtonCheckboxId = 'bakertools-hide-notables';
  2305.     this.bakerNotableHeader = '==BAKER NOTABLE==\n';
  2306.     this.notableReasonPlaceholder = '[REASON FOR NOTABLE HERE]';
  2307.  
  2308.     $('div.post.reply').each(function(i, post) {
  2309.       this._addButtonToPost(post);
  2310.     }.bind(this));
  2311.  
  2312.  
  2313.     this._setupBakerWindowControls();
  2314.     this._setupListeners();
  2315.     this._readSettings();
  2316.   }
  2317.  
  2318.   /**
  2319.    * Read settings from localStorage
  2320.    */
  2321.   _readSettings() {
  2322.     this.showNotableNominationButton(JSON.parse(
  2323.         localStorage.getItem(
  2324.             NominatePostButtons.HIDE_NOMINATE_BUTTON_SETTING),
  2325.     ));
  2326.   }
  2327.  
  2328.   /**
  2329.    * Add options to baker window
  2330.    */
  2331.   _setupBakerWindowControls() {
  2332.     window.bakerTools.mainWindow.addNotableOption(`
  2333.     <div class='${BakerWindow.CONTROL_GROUP_CLASS}'>
  2334.       <label for="${this.hidePostNotableButtonCheckboxId}"
  2335.         title="Hide post 'Notable' buttons" >
  2336.         Hide "Notable" buttons
  2337.       </label>
  2338.       <input type="checkbox" id="${this.hidePostNotableButtonCheckboxId}"
  2339.         title="Hide post 'Notable' buttons" /><br />
  2340.     </div>
  2341.     `);
  2342.   }
  2343.  
  2344.   /**
  2345.    * Setup event listeners
  2346.    */
  2347.   _setupListeners() {
  2348.     $(document).on(EightKun.NEW_POST_EVENT, function(e, post) {
  2349.       this._addButtonToPost(post);
  2350.     }.bind(this));
  2351.  
  2352.     $('#'+this.hidePostNotableButtonCheckboxId).change(function(e) {
  2353.       this.showNotableNominationButton(e.target.checked);
  2354.     }.bind(this));
  2355.   }
  2356.  
  2357.   /**
  2358.    * Show or hide the notable nomination buttons
  2359.    *
  2360.    * @param {boolean} showNotableNominationButton
  2361.    */
  2362.   showNotableNominationButton(showNotableNominationButton) {
  2363.     $('#'+this.hidePostNotableButtonCheckboxId).prop('checked',
  2364.         showNotableNominationButton);
  2365.  
  2366.     localStorage.setItem(NominatePostButtons.HIDE_NOMINATE_BUTTON_SETTING,
  2367.         showNotableNominationButton);
  2368.  
  2369.     const styleId = 'baker-tools-notable-button-style';
  2370.     if (showNotableNominationButton) {
  2371.       $('head').append(`
  2372.         <style id='${styleId}'>
  2373.           .${this.nominateButtonClass} {
  2374.             display: none;
  2375.           }
  2376.       `);
  2377.     } else {
  2378.       $(`#${styleId}`).remove();
  2379.     }
  2380.   }
  2381.  
  2382.  
  2383.   /**
  2384.    * Add button to the provided post
  2385.    * @param {Element} post
  2386.    */
  2387.   _addButtonToPost(post) {
  2388.     const button = document.createElement('button');
  2389.     $(button).addClass(this.nominateButtonClass);
  2390.     $(button).append(`
  2391.       <i class="fa fa-star" style="color: goldenrod;"></i>
  2392.       Notable`);
  2393.  
  2394.     $(button).click(function(e) {
  2395.       const postNumber = $(post)
  2396.           .find('.intro .post_no')
  2397.           .text()
  2398.           .replace('No.', '');
  2399.       const href = $(post)
  2400.           .find('.intro .post_no')
  2401.           .get(0).href;
  2402.  
  2403.       // 8kun core - adds >>postnumber to- and unhides quickreply
  2404.       window.citeReply(postNumber, href);
  2405.  
  2406.       const quickReplyBody = $('#quick-reply #body');
  2407.       const oldText = quickReplyBody.val();
  2408.  
  2409.       quickReplyBody.val(oldText + this.bakerNotableHeader +
  2410.         this.notableReasonPlaceholder);
  2411.  
  2412.       // Don't ask me why i have to do this, ask CodeMonkeyZ
  2413.       // Not sure why citeReply which calls cite needs to set a timeout to
  2414.       // replace the body of the quickreply with itself.  We need to combat
  2415.       // that here
  2416.       // setTimeout(function() {
  2417.       //     var tmp = $('#quick-reply textarea[name="body"]').val();
  2418.       //     $('#quick-reply textarea[name="body"]').val('').focus().val(tmp);
  2419.       //  }, 1);
  2420.       // $(window).on('cite', function(e, id, with_link) {
  2421.       // TODO: Figure this out
  2422.       const self = this;
  2423.       setTimeout(function() {
  2424.         quickReplyBody.select();
  2425.         quickReplyBody.prop('selectionStart',
  2426.             oldText.length + self.bakerNotableHeader.length);
  2427.       }, 1.2);
  2428.     }.bind(this));
  2429.  
  2430.     $(post).find(EightKun.POST_MODIFIED_SELECTOR).append(button);
  2431.   }
  2432. }
  2433. NominatePostButtons.HIDE_NOMINATE_BUTTON_SETTING =
  2434.     'bakertools-hide-nominate-button';
  2435.  
  2436. /* global $, EightKun, ResearchBread,  NotablePost, NavigationControl,
  2437.  ColorPicker, POST_BACKGROUND_CHANGE_EVENT */
  2438. /**
  2439. * Makes notable posts easier to see by highlighting posts that anons nominate
  2440. * as notable.
  2441. *
  2442. * If someone replies to a post and their post contains the word 'notable',
  2443. * the replied to post will be considered notable.
  2444. *
  2445. * Both the notable post and the nominator posts will be highlighted, as well
  2446. * as the nominator link in the notable's mentions will be highlighted.
  2447. */
  2448. class NotableHighlighter {
  2449.   /**
  2450.    * Construct notablehighlighter object, find and highlight
  2451.    * current notable sand setup listeners
  2452.    */
  2453.   constructor() {
  2454.     this.styleId = 'bakertools-notable-style';
  2455.     this.NOMINATING_REGEX = /notable/i;
  2456.  
  2457.     this.showOnlyNotablesCheckboxId = 'bakertools-show-only-notable';
  2458.     this.createNotablePostButtonId = 'bakertools-create-notable-post';
  2459.     this.notableEditorId = 'bakertools-notable-editor';
  2460.     this.showNotableNavigationInBoardListId =
  2461.         'bakertools-show-notable-nav-in-boardlist';
  2462.  
  2463.     this._createStyles();
  2464.     this._setupBakerWindowControls();
  2465.     this._readSettings();
  2466.  
  2467.     this.findNominatedNotables();
  2468.     this._setupListeners();
  2469.   }
  2470.  
  2471.   /**
  2472.    * Read settings from local storage
  2473.    */
  2474.   _readSettings() {
  2475.     this.setOnlyShowNotables(JSON.parse(
  2476.         localStorage.getItem(
  2477.             NotableHighlighter.ONLY_SHOW_NOTABLES_SETTING),
  2478.     ));
  2479.     this.showNotableNavigationInBoardList(JSON.parse(
  2480.         localStorage
  2481.             .getItem(NotableHighlighter.SHOW_NOTABLE_NAV_IN_BOARDLIST_SETTING),
  2482.     ));
  2483.   }
  2484.  
  2485.   /**
  2486.    * Create styles that determine how notables are highlighted
  2487.    */
  2488.   _createStyles() {
  2489.     const nominatorSelector =
  2490.       `${EightKun.POST_REPLY_SELECTOR}.${NotableHighlighter.NOMINATOR_CLASS}`;
  2491.     const notableSelector =
  2492.       `.thread ${EightKun.POST_REPLY_SELECTOR}` +
  2493.       `.${NotableHighlighter.NOTABLE_CLASS}`;
  2494.     $('head').append(`
  2495.     <style id='${this.styleId}'>
  2496.     ${notableSelector} {
  2497.       background-color: ${this.notableColor};
  2498.     }
  2499.     /* less specificity than notable so it has less preference */
  2500.     ${nominatorSelector} {  
  2501.       background-color: ${this.nominatorColor};  
  2502.     }
  2503.     div.post.reply .mentioned .${NotableHighlighter.NOMINATOR_CLASS} {
  2504.       color: ${this.nominatorMentionLinkColor};
  2505.       font-weight: bold;
  2506.       font-size: 1.5em;
  2507.     }
  2508.     ${notableSelector}.highlighted {
  2509.       background: #d6bad0;
  2510.     }
  2511.     ${nominatorSelector}.highlighted {
  2512.       background: #d6bad0;
  2513.     }
  2514.     </style>
  2515.     `);
  2516.   }
  2517.  
  2518.   /**
  2519.    * Add controls to the bakerwindow
  2520.    */
  2521.   _setupBakerWindowControls() {
  2522.     const notablePostsTitle = `Only show, notables, nominators, q, q replied
  2523.       posts`;
  2524.  
  2525.     const notableColorPicker = new ColorPicker(
  2526.         'Notable Post Color',
  2527.         'Set background color of notable Posts',
  2528.         NotableHighlighter.NOTABLE_COLOR_SETTTING,
  2529.         NotableHighlighter.DEFAULT_NOTABLE_COLOR,
  2530.         (color) => this.notableColor = color,
  2531.     );
  2532.     const nominatorColorPicker = new ColorPicker(
  2533.         'Nominator Color',
  2534.         'Set background color of nominator posts',
  2535.         NotableHighlighter.NOMINATOR_COLOR_SETTTING,
  2536.         NotableHighlighter.DEFAULT_NOMINATOR_COLOR,
  2537.         (color) => this.nominatorColor = color,
  2538.     );
  2539.     const nominatorMentionLinkColorPicker = new ColorPicker(
  2540.         'Nominator Mention Link Color',
  2541.         'Set color of nominator mention links',
  2542.         NotableHighlighter.NOMINATOR_MENTION_LINK_COLOR_SETTTING,
  2543.         NotableHighlighter.DEFAULT_NOMINATOR_MENTION_LINK_COLOR,
  2544.         (color) => this.nominatorMentionLinkColor = color,
  2545.     );
  2546.  
  2547.     window.bakerTools.mainWindow.addColorOption(notableColorPicker.element);
  2548.     window.bakerTools.mainWindow.addColorOption(nominatorColorPicker.element);
  2549.     window.bakerTools.mainWindow
  2550.         .addColorOption(nominatorMentionLinkColorPicker.element);
  2551.  
  2552.     window.bakerTools.mainWindow.addNotableOption(`
  2553.     <div class='${BakerWindow.CONTROL_GROUP_CLASS}'>
  2554.       <label for="${this.showOnlyNotablesCheckboxId}"
  2555.         title="${notablePostsTitle}" >
  2556.         Only Show Notable/Nomination Posts:
  2557.       </label>
  2558.       <input type="checkbox" id="${this.showOnlyNotablesCheckboxId}"
  2559.         title="${notablePostsTitle}" />
  2560.     </div>
  2561.     `);
  2562.  
  2563.  
  2564.     window.bakerTools.mainWindow.addBaker(`
  2565.     <button type="button" id="${this.createNotablePostButtonId}"
  2566.       title="Create notables list post based on current nominated notables" >
  2567.       Create Notable Post
  2568.     </button>
  2569.     <textarea id="${this.notableEditorId}"></textarea>
  2570.     `);
  2571.  
  2572.     window.bakerTools.mainWindow
  2573.         .addNavigationOption(`
  2574.     <div class='${BakerWindow.CONTROL_GROUP_CLASS}'>
  2575.       <label for="${this.showNotableNavigationInBoardListId}"
  2576.         title="Show navigation controls in board list bar" >
  2577.         Show Notable Nav in Board List:
  2578.       </label>
  2579.       <input type="checkbox" id="${this.showNotableNavigationInBoardListId}"
  2580.         title="Show navigation controls in board list bar" /><br />
  2581.     </div>
  2582.     `);
  2583.  
  2584.     this.navigation = new NavigationControl('Notables',
  2585.         () => NotablePost.getNotablesAsPosts(),
  2586.         NotablePost.NEW_NOTABLE_POST_EVENT);
  2587.     window.bakerTools.mainWindow
  2588.         .addNavigation(this.navigation.element);
  2589.  
  2590.     this.boardListNav = new NavigationControl('Notables',
  2591.         () => NotablePost.getNotablesAsPosts(),
  2592.         NotablePost.NEW_NOTABLE_POST_EVENT);
  2593.  
  2594.     $(EightKun.getTopBoardlist()).append(this.boardListNav.element);
  2595.     $(this.boardListNav.element).hide();
  2596.   }
  2597.  
  2598.   /**
  2599.    * Set the background color of notable posts
  2600.    * @param {string} color A valid css color value.
  2601.    *        E.G. ('#ff00ee', 'rgba()' or 'blue')
  2602.    */
  2603.   set notableColor(color) {
  2604.     this._notableColor = color;
  2605.     document.getElementById(this.styleId)
  2606.         .sheet.cssRules[0].style.background = color;
  2607.     $(document).trigger(POST_BACKGROUND_CHANGE_EVENT, color);
  2608.   }
  2609.  
  2610.   /**
  2611.    * Get color for notable post backgrounds
  2612.    */
  2613.   get notableColor() {
  2614.     return this._notableColor;
  2615.   }
  2616.  
  2617.   /**
  2618.    * Set the background color of nominator posts
  2619.    * @param {string} color A valid css color value.
  2620.    *        E.G. ('#ff00ee', 'rgba()' or 'blue')
  2621.    */
  2622.   set nominatorColor(color) {
  2623.     this._nominatorColor = color;
  2624.     document.getElementById(this.styleId)
  2625.         .sheet.cssRules[1].style.background = color;
  2626.     $(document).trigger(POST_BACKGROUND_CHANGE_EVENT, color);
  2627.   }
  2628.  
  2629.   /**
  2630.    * Get color for notable post backgrounds
  2631.    */
  2632.   get nominatorColor() {
  2633.     return this._nominatorColor;
  2634.   }
  2635.  
  2636.   /**
  2637.    * Set the color of nominator mention links posts
  2638.    * @param {string} color A valid css color value.
  2639.    *        E.G. ('#ff00ee', 'rgba()' or 'blue')
  2640.    */
  2641.   set nominatorMentionLinkColor(color) {
  2642.     this._nominatorMentionLinkColor = color;
  2643.     document.getElementById(this.styleId)
  2644.         .sheet.cssRules[2].style.color = color;
  2645.   }
  2646.  
  2647.   /**
  2648.    * Get color for notable post backgrounds
  2649.    */
  2650.   get nominatorMentionLinkColor() {
  2651.     return this._nominatorMentionLinkColor;
  2652.   }
  2653.  
  2654.   /**
  2655.    * Setup listeners for new posts, bakerwindow controls, etc
  2656.    */
  2657.   _setupListeners() {
  2658.     $('#'+this.showOnlyNotablesCheckboxId).change(function(e) {
  2659.       this.setOnlyShowNotables(e.target.checked);
  2660.     }.bind(this));
  2661.  
  2662.     $('#'+this.createNotablePostButtonId).click(function() {
  2663.       if ($('#'+this.notableEditorId).val()) {
  2664.         if (!confirm(`If you continue, any changes you made will be
  2665.             overwritten!`)) {
  2666.           return;
  2667.         }
  2668.       }
  2669.       $('#'+this.notableEditorId).val(this.createNotablesPost());
  2670.     }.bind(this));
  2671.  
  2672.     $(document).on(EightKun.NEW_POST_EVENT, function(e, post) {
  2673.       this.checkNewPostsForNotables(post);
  2674.     }.bind(this));
  2675.  
  2676.     $('#'+this.showNotableNavigationInBoardListId).change(function(e) {
  2677.       this.showNotableNavigationInBoardList(e.target.checked);
  2678.     }.bind(this));
  2679.   }
  2680.  
  2681.   /**
  2682.    * Show or hide notable nav control in the boardlist
  2683.    *
  2684.    * @param {boolean} show
  2685.    */
  2686.   showNotableNavigationInBoardList(show) {
  2687.     $('#'+this.showNotableNavigationInBoardListId).prop('checked',
  2688.         show);
  2689.  
  2690.     localStorage
  2691.         .setItem(NotableHighlighter.SHOW_NOTABLE_NAV_IN_BOARDLIST_SETTING,
  2692.             show);
  2693.  
  2694.     if (show) {
  2695.       $(this.boardListNav.element).show();
  2696.     } else {
  2697.       $(this.boardListNav.element).hide();
  2698.     }
  2699.   }
  2700.  
  2701.   /**
  2702.    * Create the notables post for review
  2703.    * @return {string} Returns the notable post string
  2704.    */
  2705.   createNotablesPost() {
  2706.     const notables = NotablePost.getNotables();
  2707.     const breadNumber = ResearchBread.getBreadNumber();
  2708.     let post = `'''#${breadNumber}'''\n\n`;
  2709.  
  2710.     notables.forEach(function(notable) {
  2711.       post += `${notable.shortLink()} ${notable.description}\n\n`;
  2712.     });
  2713.  
  2714.     return post;
  2715.   }
  2716.  
  2717.   /**
  2718.    * Checks a post for notable nominations
  2719.    * @param {Element} post
  2720.    */
  2721.   checkNewPostsForNotables(post) {
  2722.     $(post).removeAttr('style'); // TODO: try removing
  2723.  
  2724.     if (this.isNominatingPost(post)) {
  2725.       NotablePost.fromNominatingPost(post);
  2726.     }
  2727.   }
  2728.  
  2729.   /**
  2730.    * Finds posts that are being tagged as notable.
  2731.    *
  2732.    * I.E. Finding any post that has been replied to by a post with the string
  2733.    * "notable" in it. Maybe at somepoint this can be smarter.  Q give me some
  2734.    * dwave snow white tech!
  2735.    *
  2736.    * Highlights notable posts in yellow
  2737.    * Highlights nominating posts in pink <3
  2738.    * Highlights nominating posts in mentions
  2739.    * Add nominee count to post
  2740.    * @return {Array<NotablePost>}
  2741.    */
  2742.   findNominatedNotables() {
  2743.     const postsWithoutDough = ResearchBread.getPostsWithoutDough();
  2744.  
  2745.     // ^s to ignore notables review posts
  2746.     const nominatingPosts = postsWithoutDough
  2747.         .filter((post) => this.isNominatingPost(post));
  2748.  
  2749.     nominatingPosts.forEach(function(nominatingPost) {
  2750.       NotablePost.fromNominatingPost(nominatingPost);
  2751.     });
  2752.     console.log(NotablePost.getNotables());
  2753.     return NotablePost.getNotables();
  2754.   }
  2755.  
  2756.   /**
  2757.    * Is the post nominating a notable
  2758.    * @arg {Element} post .post
  2759.    * @return {boolean} True if post nominates a notable
  2760.    */
  2761.   isNominatingPost(post) {
  2762.     const postContainsNotable = post.textContent
  2763.         .search(this.NOMINATING_REGEX) != -1;
  2764.     const postIsReplying = EightKun.getReplyLinksFromPost(post).length;
  2765.     return postContainsNotable && postIsReplying;
  2766.   }
  2767.  
  2768.   /**
  2769.    * Toggle whether only the notable/nominee posts are shown or not
  2770.    * @arg {boolean} onlyShowNotables boolean If true, only show
  2771.    *               notables/nominators, else show all
  2772.    */
  2773.   setOnlyShowNotables(onlyShowNotables) {
  2774.     $('#'+this.showOnlyNotablesCheckboxId).prop('checked', onlyShowNotables);
  2775.  
  2776.     localStorage.setItem(NotableHighlighter.ONLY_SHOW_NOTABLES_SETTING,
  2777.         onlyShowNotables);
  2778.  
  2779.     const notableOrNominationPostsSelector =
  2780.       `div.post.${NotableHighlighter.NOTABLE_CLASS},
  2781.       div.post.${NotableHighlighter.NOMINATOR_CLASS}`;
  2782.     const notableOrNominationPostBreaksSelector =
  2783.       `div.post.${NotableHighlighter.NOTABLE_CLASS}+br,
  2784.       div.post.${NotableHighlighter.NOMINATOR_CLASS}+br`;
  2785.     const onlyShowNotablesStyleId = 'bakertools-only-show-notables';
  2786.  
  2787.     if (onlyShowNotables) {
  2788.       $(`<style id='${onlyShowNotablesStyleId}' type='text/css'>
  2789.         div.reply:not(.post-hover),
  2790.         div.post+br {
  2791.           display: none !important;
  2792.           visibility: hidden !important;
  2793.         }
  2794.         ${notableOrNominationPostsSelector},
  2795.         ${notableOrNominationPostBreaksSelector} {
  2796.           display: inline-block !important;
  2797.           visibility: visible !important;
  2798.         }
  2799.         </style>`).appendTo('head');
  2800.     } else {
  2801.       $(`#${onlyShowNotablesStyleId}`).remove();
  2802.       // For whatever reason, when the non notable posts are filtered and new
  2803.       // posts come through the auto_update, the posts are created with
  2804.       // style="display:block" which messes up display.  Remove style attr
  2805.       // TODO: can we remove this now that we have !important?
  2806.       $(EightKun.POST_SELECTOR).removeAttr('style');
  2807.     }
  2808.   }
  2809.  
  2810.   /**
  2811.    * Retrieves only show notable ssetting from localStorage
  2812.    * @return {boolean} true if only show notables is turned on
  2813.    */
  2814.   getOnlyShowNotables() {
  2815.     return localStorage
  2816.         .getItem(NotableHighlighter.ONLY_SHOW_NOTABLES_SETTING);
  2817.   }
  2818. }
  2819. NotableHighlighter.NOMINATOR_CLASS = 'bakertools-notable-nominator';
  2820. NotableHighlighter.NOTABLE_CLASS = 'bakertools-notable';
  2821. NotableHighlighter.ONLY_SHOW_NOTABLES_SETTING =
  2822.     'bakertools-only-show-notables';
  2823. NotableHighlighter.SHOW_NOTABLE_NAV_IN_BOARDLIST_SETTING =
  2824.     'bakertools-show-notable-nav-in-boardlist';
  2825. NotableHighlighter.NOMINATOR_COLOR_SETTTING =
  2826.     'bakertools-nominator-color';
  2827. NotableHighlighter.NOTABLE_COLOR_SETTTING =
  2828.     'bakertools-notable-color';
  2829. NotableHighlighter.NOMINATOR_MENTION_LINK_COLOR_SETTTING =
  2830.     'bakertools-nominator-metion-link-color';
  2831. NotableHighlighter.DEFAULT_NOTABLE_COLOR = '#E5FFCC';
  2832. NotableHighlighter.DEFAULT_NOMINATOR_COLOR = '#ACC395';
  2833. NotableHighlighter.DEFAULT_NOMINATOR_MENTION_LINK_COLOR = '#00CC00';
  2834.  
  2835. /* globals $, EightKun, debounce, BakerWindow */
  2836. /* exported PostRateChart */
  2837. /**
  2838.  * Displays chart of post/min
  2839.  */
  2840. class PostRateChart {
  2841.   /**
  2842.    * Construct a postrate chart
  2843.    */
  2844.   constructor() {
  2845.     this.containerClass = 'bakertools-postrate-container';
  2846.     this.chartClass = 'bakertools-postrate-chart';
  2847.     this.rateClass = 'bakertools-postrate-rate';
  2848.     this.styleId = 'bakertools-postrate-style';
  2849.     this.hidePostRateChartId = 'bakertools-postrate-hide-postrate';
  2850.     this.numberOfPostsForAverage = 10;
  2851.     this.numberOfDataPointsShownOnChart = 10;
  2852.     this.postTimes = [];
  2853.     this.postsPerMinuteHistory = [];
  2854.     this._setupStyles();
  2855.     this._setupBakerWindowControls();
  2856.     this._createElement();
  2857.     this._getExistingPostRates();
  2858.     this._setupListeners();
  2859.     this.draw();
  2860.     this.draw = debounce(this.draw, 1000 *2);
  2861.     this._readSettings();
  2862.   }
  2863.  
  2864.   /**
  2865.    * Read settings from local storage
  2866.    */
  2867.   _readSettings() {
  2868.     const hidePostRate = JSON.parse(localStorage
  2869.         .getItem(PostRateChart.HIDE_POSTRATE_SETTING));
  2870.  
  2871.     this.showPostRateChart(!hidePostRate);
  2872.   }
  2873.  
  2874.   /**
  2875.    * Setup chart styles
  2876.    */
  2877.   _setupStyles() {
  2878.     $('head').append(`
  2879.     <style id='${this.styleId}'>
  2880.     .${this.containerClass} {
  2881.       height: 20px;
  2882.       padding: 0;
  2883.       color: rgb(52, 52, 92);
  2884.     }
  2885.  
  2886.     .${this.chartClass} {
  2887.       border: 1px solid;
  2888.       vertical-align: middle;
  2889.       padding: 1px;
  2890.     }
  2891.  
  2892.     ${EightKun.BOARDLIST_SELECTOR} .${this.containerClass}:before {
  2893.       content: '[';
  2894.       color: #89A;
  2895.     }
  2896.  
  2897.     ${EightKun.BOARDLIST_SELECTOR} .${this.containerClass}:after {
  2898.       content: ']';
  2899.       color: #89A;
  2900.     }
  2901.     `);
  2902.   }
  2903.  
  2904.   /**
  2905.    * Add controls to the bakerwindow
  2906.    */
  2907.   _setupBakerWindowControls() {
  2908.     window.bakerTools.mainWindow.addOption(`
  2909.     <div class='${BakerWindow.CONTROL_GROUP_CLASS}'>
  2910.       <label for="${this.hidePostRateChartId}"
  2911.         title="Hide the postrate chart in boardlist" >
  2912.         Hide Post/Min Chart:
  2913.       </label>
  2914.       <input type="checkbox" id="${this.hidePostRateChartId}"
  2915.         title="Hide the postrate chart in boardlist" />
  2916.     </div>
  2917.     `);
  2918.   }
  2919.  
  2920.  
  2921.   /**
  2922.    * Setup listener to record post times
  2923.    */
  2924.   _setupListeners() {
  2925.     $(document).on(EightKun.NEW_POST_EVENT, function(idx, post) {
  2926.       this._addDataPointFromPost(post);
  2927.       this.draw();
  2928.     }.bind(this));
  2929.  
  2930.     $('#'+this.hidePostRateChartId).change(function(e) {
  2931.       this.showPostRateChart(!e.target.checked);
  2932.     }.bind(this));
  2933.   }
  2934.  
  2935.   /**
  2936.    * Show or hide post rate chart in the boardlist
  2937.    *
  2938.    * @param {boolean} show
  2939.    */
  2940.   showPostRateChart(show) {
  2941.     $('#'+this.hidePostRateChartId).prop('checked',
  2942.         !show);
  2943.  
  2944.     localStorage
  2945.         .setItem(PostRateChart.HIDE_POSTRATE_SETTING,
  2946.             !show);
  2947.  
  2948.     if (show) {
  2949.       $(this.element).show();
  2950.     } else {
  2951.       $(this.element).hide();
  2952.     }
  2953.   }
  2954.  
  2955.   /**
  2956.    * Create the canvas element
  2957.    */
  2958.   _createElement() {
  2959.     this.element = $(`
  2960.     <span class='${this.containerClass}' title = 'Posts Per Minute'>
  2961.       <span class='${this.rateClass}'>0</span> ppm
  2962.       <canvas class='${this.chartClass}'></canvas>
  2963.     </span>`,
  2964.     ).get(0);
  2965.  
  2966.     $(EightKun.getTopBoardlist()).append(this.element);
  2967.  
  2968.     this.canvas = $(this.element).find('canvas').get(0);
  2969.  
  2970.     this.canvas.height = 10;
  2971.     this.canvas.width = 100;
  2972.   }
  2973.  
  2974.   /**
  2975.    * Collect post rate data on posts at page load
  2976.    */
  2977.   _getExistingPostRates() {
  2978.     $('div.post').each(function(idx, post) {
  2979.       this._addDataPointFromPost(post);
  2980.     }.bind(this));
  2981.   }
  2982.  
  2983.   /**
  2984.    * Add a data point (aka the time a post was made)
  2985.    * @param {Element} post div.post
  2986.    */
  2987.   _addDataPointFromPost(post) {
  2988.     this.postTimes.push(EightKun.getPostTime(post));
  2989.     if (this._isEnoughDataToAverage()) {
  2990.       this._recordPostPerMinute();
  2991.     }
  2992.   }
  2993.  
  2994.   /**
  2995.    * Return true if theres enough data to perform averaging
  2996.    * @return {boolean} true if enough data
  2997.    */
  2998.   _isEnoughDataToAverage() {
  2999.     return this.postTimes.length > (this.numberOfPostsForAverage + 1);
  3000.   }
  3001.  
  3002.   /**
  3003.    * Record post per minute with the current set of post times
  3004.    * Calc is done with the last ${this.numberOfPostsForAverage} post times
  3005.    */
  3006.   _recordPostPerMinute() {
  3007.     const startPostIndex =
  3008.       this.postTimes.length - this.numberOfPostsForAverage - 1;
  3009.     const endPostIndex = this.postTimes.length - 1;
  3010.     const startPostTime = this.postTimes[startPostIndex];
  3011.     const endPostTime = this.postTimes[endPostIndex];
  3012.  
  3013.     const postsPerMinute =
  3014.         this.numberOfPostsForAverage / ((endPostTime - startPostTime) / 60);
  3015.     this.postsPerMinuteHistory.push(postsPerMinute);
  3016.   }
  3017.  
  3018.   /**
  3019.    * Draw the post rate chart
  3020.    */
  3021.   draw() {
  3022.     if (!this.postsPerMinuteHistory.length) {
  3023.       return;
  3024.     }
  3025.  
  3026.     const canvas = this.canvas;
  3027.     const ctx = canvas.getContext('2d');
  3028.  
  3029.     this._setPostRateText();
  3030.  
  3031.     const normalizedPostPerMinutes = this._normalizePostPerMinutes();
  3032.  
  3033.     ctx.clearRect(0, 0, canvas.width, canvas.height);
  3034.     ctx.strokeStyle = $(`${EightKun.BOARDLIST_SELECTOR} a`).css('color');
  3035.     ctx.beginPath();
  3036.  
  3037.     let x = 0;
  3038.     let y = canvas.height * normalizedPostPerMinutes[0];
  3039.  
  3040.     ctx.moveTo(x, y);
  3041.     normalizedPostPerMinutes.slice(1).forEach(function(ppm, i) {
  3042.       x = (i+1) * (canvas.width / this.numberOfDataPointsShownOnChart);
  3043.       y = canvas.height * ppm;
  3044.       ctx.lineTo(x, y);
  3045.     }.bind(this));
  3046.     ctx.stroke();
  3047.     ctx.closePath();
  3048.   }
  3049.  
  3050.   /**
  3051.    * Set the text label of current PPM
  3052.    */
  3053.   _setPostRateText() {
  3054.     const lastIndex = this.postsPerMinuteHistory.length-1;
  3055.     const currentPPM =
  3056.       this.postsPerMinuteHistory[lastIndex].toFixed(2);
  3057.     $(`.${this.rateClass}`).text(currentPPM);
  3058.   }
  3059.  
  3060.   /**
  3061.    * Normalize the data points to be within 0-1 range.
  3062.    * Slice the array to only contain the currently drawn slice
  3063.    * @return {Array}
  3064.    */
  3065.   _normalizePostPerMinutes() {
  3066.     const slicedArray =
  3067.       this.postsPerMinuteHistory.slice(-this.numberOfDataPointsShownOnChart);
  3068.     const maxPPM = Math.max(...slicedArray);
  3069.     const minPPM = Math.min(...slicedArray);
  3070.     const range = maxPPM - minPPM;
  3071.     return slicedArray.map(function(ppm) {
  3072.       return (ppm - minPPM) / range;
  3073.     });
  3074.   }
  3075. }
  3076. PostRateChart.HIDE_POSTRATE_SETTING =
  3077.   'bakertools-hide-postrate-chart';
  3078.  
  3079. /* global $, EightKun, ColorPicker */
  3080. /* exported PreviousBreadHighlighter */
  3081. /**
  3082. * Highlights previous bread post links
  3083. */
  3084. class PreviousBreadHighlighter {
  3085.   /**
  3086.    * Construct pb highlighter object, setup listeners
  3087.    */
  3088.   constructor() {
  3089.     this.styleId = 'bakertools-previous-bread-styles';
  3090.     this.previousBreadClass = 'bakertools-PreviousBread';
  3091.     this.newerBreadClass = 'bakertools-NewBread';
  3092.     this._linkSelector = 'div.body > p.body-line.ltr > a';
  3093.  
  3094.     this._setupStyles();
  3095.     this._setupBakerWindowControls();
  3096.  
  3097.     const links = $(this._linkSelector).filter('[onClick]');
  3098.     links.each(function(index, link) {
  3099.       this.markLinkIfPreviousBread(link);
  3100.     }.bind(this));
  3101.  
  3102.     this._setupListeners();
  3103.   }
  3104.  
  3105.   /**
  3106.    * Setup color picker controls
  3107.    */
  3108.   _setupBakerWindowControls() {
  3109.     const colorPicker = new ColorPicker(
  3110.         'Previous Bread Link Color',
  3111.         'Set the color of links to previous breads',
  3112.         PreviousBreadHighlighter.PREVIOUS_BREAD_LINK_COLOR_SETTING,
  3113.         PreviousBreadHighlighter.DEFAULT_PREVIOUS_BREAD_LINK_COLOR,
  3114.         (color) => this.previousBreadLinkColor = color,
  3115.     );
  3116.  
  3117.     window.bakerTools.mainWindow.addColorOption(colorPicker.element);
  3118.  
  3119.     const newerBreadColorPicker = new ColorPicker(
  3120.         'Newer Bread Link Color',
  3121.         'Set the color of links to newer breads',
  3122.         PreviousBreadHighlighter.NEWER_BREAD_LINK_COLOR_SETTING,
  3123.         PreviousBreadHighlighter.DEFAULT_NEWER_BREAD_LINK_COLOR,
  3124.         (color) => this.newerBreadLinkColor = color,
  3125.     );
  3126.  
  3127.     window.bakerTools.mainWindow.addColorOption(newerBreadColorPicker.element);
  3128.   }
  3129.  
  3130.   /**
  3131.    * Set the  color of pb links
  3132.    * @param {string} color A valid css color value.
  3133.    *        E.G. ('#ff00ee', 'rgba()' or 'blue')
  3134.    */
  3135.   set previousBreadLinkColor(color) {
  3136.     this._previousBreadLinkColor = color;
  3137.     document.getElementById(this.styleId)
  3138.         .sheet.cssRules[0].style.color = color;
  3139.   }
  3140.  
  3141.   /**
  3142.    * Get color of pb links
  3143.    */
  3144.   get previousBreadLinkColor() {
  3145.     return this._previousBreadLinkColor;
  3146.   }
  3147.  
  3148.   /**
  3149.    * Set the  color of nb links
  3150.    * @param {string} color A valid css color value.
  3151.    *        E.G. ('#ff00ee', 'rgba()' or 'blue')
  3152.    */
  3153.   set newerBreadLinkColor(color) {
  3154.     this._newerBreadLinkColor = color;
  3155.     document.getElementById(this.styleId)
  3156.         .sheet.cssRules[2].style.color = color;
  3157.   }
  3158.  
  3159.   /**
  3160.    * Get color of nb links
  3161.    */
  3162.   get newerBreadLinkColor() {
  3163.     return this._newerBreadLinkColor;
  3164.   }
  3165.  
  3166.   /**
  3167.    * Setup styles for pb links
  3168.    */
  3169.   _setupStyles() {
  3170.     $('head').append(`
  3171.     <style id='${this.styleId}'>
  3172.     ${EightKun.POST_REPLY_SELECTOR} div.body a.${this.previousBreadClass} {
  3173.         color: ${this.previousBreadLinkColor};
  3174.     }
  3175.     a.${this.previousBreadClass}::after {
  3176.       content: " (pb)";
  3177.     }
  3178.  
  3179.     ${EightKun.POST_REPLY_SELECTOR} div.body a.${this.newerBreadClass} {
  3180.         color: ${this.newerBreadLinkColor};
  3181.     }
  3182.     a.${this.newerBreadClass}::after {
  3183.       content: " (nb)";
  3184.     }
  3185.     </style>
  3186.     `);
  3187.   }
  3188.  
  3189.   /**
  3190.    * Setup listeners for pb highlighting
  3191.    */
  3192.   _setupListeners() {
  3193.     $(document).on(EightKun.NEW_POST_EVENT, function(e, post) {
  3194.       $(post).find(this._linkSelector)
  3195.           .each((index, link) => this.markLinkIfPreviousBread(link));
  3196.     }.bind(this));
  3197.   }
  3198.  
  3199.   /**
  3200.    * Marks the link if it is pb
  3201.    *
  3202.    * @param {Anchor} link
  3203.    */
  3204.   markLinkIfPreviousBread(link) {
  3205.     const currentBreadNumber = document.location.pathname
  3206.         .split('/')
  3207.         .slice(-1)[0]
  3208.         .split('.')[0];
  3209.     const linkBreadNumber = link.href.split('/')
  3210.         .slice(-1)[0]
  3211.         .split('#')[0]
  3212.         .split('.')[0];
  3213.  
  3214.     const isAReplyLink = $(link)
  3215.         .attr('onclick')
  3216.         .search(EightKun.REPLY_REGEX) != 1;
  3217.     if (isAReplyLink &&
  3218.         currentBreadNumber > linkBreadNumber) {
  3219.       $(link).addClass(this.previousBreadClass);
  3220.     } else if (isAReplyLink &&
  3221.       currentBreadNumber < linkBreadNumber) {
  3222.       $(link).addClass(this.newerBreadClass);
  3223.     }
  3224.   }
  3225. }
  3226. PreviousBreadHighlighter.PREVIOUS_BREAD_LINK_COLOR_SETTING =
  3227.   'bakertools-previous-bread-link-color';
  3228. PreviousBreadHighlighter.DEFAULT_PREVIOUS_BREAD_LINK_COLOR =
  3229.   '#0000CC';
  3230. PreviousBreadHighlighter.NEWER_BREAD_LINK_COLOR_SETTING =
  3231.   'bakertools-newer-bread-link-color';
  3232. PreviousBreadHighlighter.DEFAULT_NEWER_BREAD_LINK_COLOR =
  3233.   '#00CC00';
  3234.  
  3235. /* global $, EightKun, ResearchBread, NavigationControl,
  3236.  ColorPicker, POST_BACKGROUND_CHANGE_EVENT, BakerWindow */
  3237. /**
  3238. * Highlight Q posts, replies to q, q replies.
  3239. * Adds navigation to baker window
  3240. */
  3241. class QPostHighlighter {
  3242.   /**
  3243.    * Construct qposthighlighter object and setup listeners
  3244.    */
  3245.   constructor() {
  3246.     this.styleId = 'bakertools-q-style';
  3247.     this.qPostClass = 'bakertools-q-post';
  3248.     this.qReplyClass = 'bakertools-q-reply';
  3249.     this.qMentionClass = 'bakertools-q-mention';
  3250.     this.qLinkClass = 'bakertools-q-link';
  3251.     this.styleId = 'bakertools-q-styles';
  3252.     this._linkSelector = 'div.body > p.body-line.ltr > a';
  3253.     this.showQNavigationInBoardListId =
  3254.         'bakertools-show-q-nav-in-boardlist';
  3255.  
  3256.     this.currentQTripCode = null;
  3257.  
  3258.     this._setupStyles();
  3259.     this._setupBakerWindowControls();
  3260.     this._readSettings();
  3261.     this._findQPosts();
  3262.     this._setupListeners();
  3263.   }
  3264.  
  3265.   /**
  3266.    * Read settings from localStorage
  3267.    */
  3268.   _readSettings() {
  3269.     this.showQNavigationInBoardList(JSON.parse(
  3270.         localStorage
  3271.             .getItem(QPostHighlighter.SHOW_Q_NAV_IN_BOARDLIST_SETTING),
  3272.     ));
  3273.   }
  3274.  
  3275.   /**
  3276.    * Setup styles for highlighting q posts
  3277.    */
  3278.   _setupStyles() {
  3279.     $('head').append(`
  3280.     <style id='${this.styleId}'>
  3281.      ${EightKun.POST_REPLY_SELECTOR}.${this.qPostClass} {
  3282.       background: ${this.qPostColor};
  3283.       display: inline-block !important;
  3284.       visibility: visible !important;
  3285.     }
  3286.  
  3287.     ${EightKun.POST_REPLY_SELECTOR}.${this.qReplyClass} {
  3288.       background: ${this.qYouColor};
  3289.       display: inline-block !important;
  3290.       visibility: visible !important;
  3291.     }
  3292.  
  3293.     ${EightKun.POST_REPLY_SELECTOR}.${this.qPostClass}.highlighted {
  3294.       background: #d6bad0;
  3295.     }
  3296.  
  3297.     ${EightKun.POST_REPLY_SELECTOR} .intro .${this.qMentionClass},
  3298.     .${this.qLinkClass} {
  3299.       padding:1px 3px 1px 3px;
  3300.       background-color:black;
  3301.       border-radius:8px;
  3302.       border:1px solid #bbbbee;
  3303.       color:gold;
  3304.       background: linear-gradient(300deg, #ff0000, #ff0000, #ff0000, #bbbbbb,
  3305.                 #4444ff);
  3306.       background-size: 800% 800%;
  3307.  
  3308.       -webkit-animation: Patriot 5s ease infinite;
  3309.       -moz-animation: Patriot 5s ease infinite;
  3310.       -o-animation: Patriot 5s ease infinite;
  3311.       animation: Patriot 5s ease infinite;
  3312.       -webkit-text-fill-color: transparent;
  3313.        
  3314.       background: -o-linear-gradient(transparent, transparent);
  3315.       -webkit-background-clip: text;
  3316.     }
  3317.     </style>
  3318.     `);
  3319.   }
  3320.  
  3321.   /**
  3322.    * Set the background color of q posts
  3323.    * @param {string} color A valid css color value.
  3324.    *        E.G. ('#ff00ee', 'rgba()' or 'blue')
  3325.    */
  3326.   set qPostColor(color) {
  3327.     this._qPostColor = color;
  3328.     document.getElementById(this.styleId)
  3329.         .sheet.cssRules[0].style.background = color;
  3330.     $(document).trigger(POST_BACKGROUND_CHANGE_EVENT, color);
  3331.   }
  3332.  
  3333.   /**
  3334.    * Get color for q post backgrounds
  3335.    */
  3336.   get qPostColor() {
  3337.     return this._qPostColor;
  3338.   }
  3339.  
  3340.   /**
  3341.    * Set the background color of q posts
  3342.    * @param {string} color A valid css color value.
  3343.    *        E.G. ('#ff00ee', 'rgba()' or 'blue')
  3344.    */
  3345.   set qYouColor(color) {
  3346.     this._qYouColor = color;
  3347.     document.getElementById(this.styleId)
  3348.         .sheet.cssRules[1].style.background = color;
  3349.     $(document).trigger(POST_BACKGROUND_CHANGE_EVENT, color);
  3350.   }
  3351.  
  3352.   /**
  3353.    * Get bg color for posts q replies to
  3354.    */
  3355.   get qYouColor() {
  3356.     return this._qYouColor;
  3357.   }
  3358.  
  3359.   /**
  3360.    * Get Q's current trip code from the bread
  3361.    */
  3362.   _getCurrentQTripFromBread() {
  3363.     const tripCodeMatch = $(EightKun.getOpPost())
  3364.         .text()
  3365.         .match(/Q's Trip-code: Q (.+?\s)/);
  3366.  
  3367.     if (!tripCodeMatch) {
  3368.       console.error('Could not find Q\'s tripcode');
  3369.       return;
  3370.     }
  3371.     this.currentQTripCode = tripCodeMatch[1].split(' ')[0];
  3372.   }
  3373.  
  3374.   /**
  3375.    * Find current Q posts in bread
  3376.    */
  3377.   _findQPosts() {
  3378.     const posts = ResearchBread.getPostsWithoutDough();
  3379.  
  3380.     $(posts).each(function(i, post) {
  3381.       this._doItQ(post);
  3382.     }.bind(this));
  3383.   }
  3384.  
  3385.   /**
  3386.    * Check if the post is Q
  3387.    * WWG1WGA
  3388.    *
  3389.    * @param {Element} post a div.post
  3390.    */
  3391.   _doItQ(post) {
  3392.     if (this._markIfQPost(post)) { // Q Post, lets check for q replies
  3393.       const qPostNumber = $(post)
  3394.           .find('.intro .post_no')
  3395.           .text()
  3396.           .replace('No.', '');
  3397.  
  3398.       const links = $(post)
  3399.           .find(this._linkSelector)
  3400.           .filter('[onClick]');
  3401.  
  3402.       $(links).each(function(i, link) {
  3403.         const postNumber = link.href.split('#')[1];
  3404.         // Enlightened post
  3405.         $(`#reply_${postNumber}`).addClass(this.qReplyClass);
  3406.  
  3407.         const metionLinkSelector = `#reply_${postNumber} .intro .mentioned a`;
  3408.         $(metionLinkSelector).each(function(i, mentionAnchor) {
  3409.           const mentionPostNumber = $(mentionAnchor).text().replace('>>', '');
  3410.           if (mentionPostNumber == qPostNumber) {
  3411.             $(mentionAnchor).addClass(this.qMentionClass);
  3412.           }
  3413.         }.bind(this));
  3414.       }.bind(this));
  3415.     } else { // Not Q, but lets check if this post replies to Q
  3416.       const links = $(post).find(this._linkSelector).filter('[onClick]');
  3417.  
  3418.       $(links).each(function(i, link) {
  3419.         const postNumber = link.href.split('#')[1];
  3420.         const replyPost = document.querySelector(`#reply_${postNumber}`);
  3421.         // TODO: need to handle pb posts
  3422.         if (this.isQ(replyPost)) {
  3423.           $(link).addClass(this.qLinkClass);
  3424.         }
  3425.       }.bind(this));
  3426.     }
  3427.   }
  3428.  
  3429.   /**
  3430.    * @arg {Element} post div.post.reply
  3431.    * @return {boolean} true if it is a q post
  3432.    */
  3433.   _markIfQPost(post) {
  3434.     let isQ = false;
  3435.     if (this.isQ(post)) {
  3436.       isQ = true;
  3437.       $(post).addClass(this.qPostClass);
  3438.       QPostHighlighter.qPosts.push(post);
  3439.  
  3440.       $(document).trigger(QPostHighlighter.NEW_Q_POST_EVENT, post);
  3441.     }
  3442.     return isQ;
  3443.   }
  3444.  
  3445.   /**
  3446.    * Is the post Q?
  3447.    * @param {Element} post a div.post.reply
  3448.    * @return {boolean} true if the post is Q
  3449.    */
  3450.   isQ(post) {
  3451.     const qTripHistory = QTripCodeHistory.INSTANCE;
  3452.     const dateOfPost = new Date(EightKun.getPostDateTime(post));
  3453.     const expectedQTripBasedOnDate = qTripHistory.getTripCodeByDate(dateOfPost);
  3454.     if (!expectedQTripBasedOnDate) {
  3455.       console.info(`Could not find Q trip code for date: ${dateOfPost}`);
  3456.       return false;
  3457.     }
  3458.  
  3459.     return EightKun.getPostTrip(post) == expectedQTripBasedOnDate.tripCode;
  3460.   }
  3461.  
  3462.   /**
  3463.    * Add Q post navigation to bakerwindow
  3464.    */
  3465.   _setupBakerWindowControls() {
  3466.     window.bakerTools.mainWindow
  3467.         .addNavigationOption(`
  3468.     <div class='${BakerWindow.CONTROL_GROUP_CLASS}'>
  3469.       <label for="${this.showQNavigationInBoardListId}"
  3470.         title="Show navigation controls in board list bar" >
  3471.         Show Q Nav in Board List:
  3472.       </label>
  3473.       <input type="checkbox" id="${this.showQNavigationInBoardListId}"
  3474.         title="Show navigation controls in board list bar" /><br />
  3475.     </div>
  3476.     `);
  3477.  
  3478.     this.navigation = new NavigationControl('Q Posts',
  3479.         () => QPostHighlighter.qPosts, QPostHighlighter.NEW_Q_POST_EVENT);
  3480.     window.bakerTools.mainWindow
  3481.         .addNavigation(this.navigation.element);
  3482.  
  3483.     this.boardListNav = new NavigationControl('Q',
  3484.         () => QPostHighlighter.qPosts, QPostHighlighter.NEW_Q_POST_EVENT);
  3485.  
  3486.     $(EightKun.getTopBoardlist()).append(this.boardListNav.element);
  3487.     $(this.boardListNav.element).hide();
  3488.  
  3489.     const qColorPicker = new ColorPicker(
  3490.         'Q Post Color',
  3491.         'Set background color of Q Posts',
  3492.         QPostHighlighter.Q_POST_COLOR_SETTING,
  3493.         QPostHighlighter.DEFAULT_Q_POST_COLOR,
  3494.         (color) => this.qPostColor = color,
  3495.     );
  3496.     const qYouColorPicker = new ColorPicker(
  3497.         'Q (You) Color',
  3498.         'Set background color of posts Q Replies to',
  3499.         QPostHighlighter.Q_YOU_POST_COLOR_SETTING,
  3500.         QPostHighlighter.DEFAULT_Q_YOU_POST_COLOR,
  3501.         (color) => this.qYouColor = color,
  3502.     );
  3503.  
  3504.     window.bakerTools.mainWindow.addColorOption(qColorPicker.element);
  3505.     window.bakerTools.mainWindow.addColorOption(qYouColorPicker.element);
  3506.   }
  3507.  
  3508.  
  3509.   /**
  3510.    * Setup listeners for new posts
  3511.    */
  3512.   _setupListeners() {
  3513.     $(document).on(EightKun.NEW_POST_EVENT, function(e, post) {
  3514.       this._doItQ(post);
  3515.     }.bind(this));
  3516.  
  3517.     $('#'+this.showQNavigationInBoardListId).change(function(e) {
  3518.       this.showQNavigationInBoardList(e.target.checked);
  3519.     }.bind(this));
  3520.   }
  3521.  
  3522.   /**
  3523.    * Show or hide q nav control in the boardlist
  3524.    *
  3525.    * @param {boolean} showNavInBoardList
  3526.    */
  3527.   showQNavigationInBoardList(showNavInBoardList) {
  3528.     $('#'+this.showQNavigationInBoardListId).prop('checked',
  3529.         showNavInBoardList);
  3530.  
  3531.     localStorage.setItem(QPostHighlighter.SHOW_Q_NAV_IN_BOARDLIST_SETTING,
  3532.         showNavInBoardList);
  3533.  
  3534.     if (showNavInBoardList) {
  3535.       $(this.boardListNav.element).show();
  3536.     } else {
  3537.       $(this.boardListNav.element).hide();
  3538.     }
  3539.   }
  3540. }
  3541. QPostHighlighter.qPosts = [];
  3542. QPostHighlighter.NEW_Q_POST_EVENT = 'bakertools-new-q-post';
  3543. QPostHighlighter.SHOW_Q_NAV_IN_BOARDLIST_SETTING =
  3544.     'bakertools-show-q-nav-in-boardlist';
  3545. QPostHighlighter.Q_YOU_COLOR_SETTING =
  3546.     'bakertools-q-you-color';
  3547. QPostHighlighter.Q_POST_COLOR_SETTING =
  3548.     'bakertools-q-post-color';
  3549. QPostHighlighter.DEFAULT_Q_POST_COLOR = '#FFFFCC';
  3550. QPostHighlighter.DEFAULT_Q_YOU_POST_COLOR = '#DDDDDD';
  3551.  
  3552. /**
  3553.  * History of Q's tripcodes and their date ranges
  3554.  */
  3555. class QTripCodeHistory {
  3556.   /**
  3557.    * Construct the q trip history
  3558.    */
  3559.   constructor() {
  3560.     // Hat tip to https://8kun.top/qresearch/res/7762733.html#7832643 for Q trip history
  3561.     this.history = [
  3562.       new QTripCode('!ITPb.qbhqo',
  3563.           new Date('2017-11-10 04:07:15Z'), new Date('2017-12-15 06:04:43Z')),
  3564.       new QTripCode('!UW.yye1fxo',
  3565.           new Date('2017-12-15 06:04:06Z'), new Date('2018-03-24 13:09:02Z')),
  3566.       new QTripCode('!xowAT4Z3VQ',
  3567.           new Date('2018-03-24 13:09:37Z'), new Date('2018-05-04 20:02:22Z')),
  3568.       new QTripCode('!2jsTvXXmXs',
  3569.           new Date('2018-05-04 20:01:19Z'), new Date('2018-05-08 23:46:39Z')),
  3570.       new QTripCode('!4pRcUA0lBE',
  3571.           new Date('2018-05-08 23:47:17Z'), new Date('2018-05-19 22:06:20Z')),
  3572.       new QTripCode('!CbboFOtcZs',
  3573.           new Date('2018-05-19 22:07:06Z'), new Date('2018-08-05 20:12:52Z')),
  3574.       new QTripCode('!A6yxsPKia.',
  3575.           new Date('2018-08-05 20:14:24Z'), new Date('2018-08-10 18:24:24Z')),
  3576.       new QTripCode('!!mG7VJxZNCI',
  3577.           new Date('2018-08-10 18:26:08Z'), new Date('2019-11-25 22:35:45Z')),
  3578.       new QTripCode('!!Hs1Jq13jV6',
  3579.           new Date('2019-12-02 17:55:59Z'), null),
  3580.     ];
  3581.   }
  3582.  
  3583.   /**
  3584.    * Get Q Tripcode by the provided date
  3585.    * @param {Date} date
  3586.    * @return {QTripCode}
  3587.    */
  3588.   getTripCodeByDate(date) {
  3589.     let returnTripCode = null;
  3590.     for (const tripCode of this.history) {
  3591.       if (tripCode.isValidForDate(date)) {
  3592.         returnTripCode = tripCode;
  3593.         break;
  3594.       }
  3595.     }
  3596.     return returnTripCode;
  3597.   }
  3598.  
  3599.   /**
  3600.    * Get Q Tripcode by the current
  3601.    * @return {QTripCode}
  3602.    */
  3603.   getCurrentTripCode() {
  3604.     return this.getTripCodeByDate(new Date());
  3605.   }
  3606. }
  3607.  
  3608. /**
  3609.  * Represents a Tripcode used by Q and the timeframe
  3610.  */
  3611. class QTripCode {
  3612.   /**
  3613.    * Create a new QTripCode
  3614.    * @param {string} tripCode
  3615.    * @param {DateTime} startDate
  3616.    * @param {DateTime} endDate
  3617.    */
  3618.   constructor(tripCode, startDate, endDate) {
  3619.     this.tripCode = tripCode;
  3620.     this.startDate = startDate;
  3621.     this.isCurrentTrip = false;
  3622.     if (!endDate) {
  3623.       this.isCurrentTrip = true;
  3624.     }
  3625.     this.endDate = endDate;
  3626.   }
  3627.  
  3628.   /**
  3629.    * Is this tripcode valid for the provided date?
  3630.    * @param {Date} date
  3631.    * @return {boolean} true if this trip code is valid for the date
  3632.    */
  3633.   isValidForDate(date) {
  3634.     const dateIsOnOrAfterTripStart = date >= this.startDate;
  3635.     const dateIsOnOrBeforeTripEnd = date <= this.endDate;
  3636.     return dateIsOnOrAfterTripStart &&
  3637.       (this.isCurrentTrip || dateIsOnOrBeforeTripEnd);
  3638.   }
  3639. }
  3640.  
  3641. QTripCodeHistory.INSTANCE = new QTripCodeHistory();
  3642.  
  3643. /* globals $, EightKun, ResearchBread, BakerWindow */
  3644. /* exported SpamFader, NameFagStrategy, HighPostCountFagStrategy,
  3645. * FloodFagStrategy, BreadShitterFagStrategy */
  3646.  
  3647. /**
  3648. * Fade posts that post too fast
  3649. */
  3650. class SpamFader {
  3651.   /**
  3652.    * Construct spamfader
  3653.    * @param {Array} spamDetectionStrategies An array of SpamDetectionStrategy's
  3654.    */
  3655.   constructor(spamDetectionStrategies) {
  3656.     this.spamDetectionStrategies = spamDetectionStrategies;
  3657.     this.styleId = 'bakertools-spamfader-style';
  3658.     this.spamClass = 'bakertools-spamfader-spam';
  3659.     this.disableSpamFaderId = 'bakertools-spamfader-disable';
  3660.     this.hideSpamBadgesId = 'bakertools-spamfader-hide-spam-badges';
  3661.     this._createStyles();
  3662.     this._setupBakerWindowControls();
  3663.     this._readSettings();
  3664.     this._spamFadeExistingPosts();
  3665.     this._setupListeners();
  3666.   }
  3667.  
  3668.   /**
  3669.    * Create stylesheets
  3670.    */
  3671.   _createStyles() {
  3672.     $('head').append(`
  3673.     <style id='${this.styleId}'>
  3674.       div.post.post-hover {
  3675.         opacity: 1 !important;
  3676.       }
  3677.     </style>
  3678.     `);
  3679.   }
  3680.  
  3681.   /**
  3682.    * Setup settings UI for spamfading
  3683.    */
  3684.   _setupBakerWindowControls() {
  3685.     window.bakerTools.mainWindow.addSpamOption(`
  3686.     <div class='${BakerWindow.CONTROL_GROUP_CLASS}'>
  3687.       <label for='${this.disableSpamFaderId}'>Disable SpamFader</label>
  3688.       <input type='checkbox' id='${this.disableSpamFaderId}' />
  3689.     </div>
  3690.  
  3691.     <div class='${BakerWindow.CONTROL_GROUP_CLASS}'>
  3692.       <label for='${this.hideSpamBadgesId}'>Hide spam badges</label>
  3693.       <input type='checkbox' id='${this.hideSpamBadgesId}'/>
  3694.     </div>
  3695.     `);
  3696.   }
  3697.  
  3698.   /**
  3699.    * Loop through posts for spam
  3700.    */
  3701.   _spamFadeExistingPosts() {
  3702.     $(EightKun.POST_REPLY_SELECTOR).each(function(i, post) {
  3703.       this._detectSpam(post);
  3704.     }.bind(this));
  3705.   }
  3706.  
  3707.   /**
  3708.    * Determine if provided post is spam, if so, add spam class
  3709.    *
  3710.    * @param {Element} post div.post
  3711.    */
  3712.   _detectSpam(post) {
  3713.     const posterStats = SpamFader.getPosterStats(post);
  3714.     posterStats.addPost(post);
  3715.  
  3716.     if (SpamFader.isMarkedAsNotSpam(posterStats)) {
  3717.       return;
  3718.     }
  3719.  
  3720.     this.spamDetectionStrategies.forEach((sds) => sds.isSpam(post));
  3721.     this._takeSpamAction(posterStats);
  3722.   }
  3723.  
  3724.   /**
  3725.    * Performs the spam action against the poster's posts.
  3726.    * @param {PosterStats} posterStats
  3727.    */
  3728.   _takeSpamAction(posterStats) {
  3729.     if (!posterStats.isSpam) {
  3730.       return;
  3731.     }
  3732.     if (this.spamAction === SpamFader.FADE) {
  3733.       const opacity =
  3734.         Math.max(SpamFader.MIN_OPACITY, (1 - posterStats.fadeProgress));
  3735.  
  3736.       posterStats.posts.forEach(function(p) {
  3737.         $(p).css('opacity', opacity);
  3738.         $(p).off('mouseenter mouseleave');
  3739.         $(p).hover(function() {
  3740.           $(p).animate({opacity: 1.0}, SpamFader.ANIMATION_DURATION);
  3741.         }, function() {
  3742.           $(p).animate({opacity: opacity}, SpamFader.ANIMATION_DURATION);
  3743.         });
  3744.       });
  3745.     } else if (this.spamAction === SpamFader.HIDE) {
  3746.       posterStats.posts.forEach(function(p) {
  3747.         EightKun.hidePost(p);
  3748.       });
  3749.     }
  3750.   }
  3751.  
  3752.   /**
  3753.    * Setup new post listener
  3754.    */
  3755.   _setupListeners() {
  3756.     this._setupNewPostListener();
  3757.     $(`#${this.disableSpamFaderId}`).change(function(e) {
  3758.       this.disableSpamFader(e.target.checked);
  3759.     }.bind(this));
  3760.  
  3761.     $(`#${this.hideSpamBadgesId}`).change(function(e) {
  3762.       this.hideSpamBadges(e.target.checked);
  3763.     }.bind(this));
  3764.   }
  3765.  
  3766.   /**
  3767.    * Setup listener to check new posts for spam
  3768.    */
  3769.   _setupNewPostListener() {
  3770.     $(document).on(EightKun.NEW_POST_EVENT, function(e, post) {
  3771.       this._detectSpam(post);
  3772.     }.bind(this));
  3773.   }
  3774.  
  3775.   /**
  3776.    * Hide the actions of the spamfader.
  3777.    * @param {boolean} disable
  3778.    */
  3779.   disableSpamFader(disable) {
  3780.     $('#'+this.disableSpamFaderId).prop('checked', disable);
  3781.     localStorage.setItem(SpamFader.DISABLE_SPAM_FADER_SETTING, disable);
  3782.  
  3783.     if (disable) {
  3784.       $(SpamFader.SPAM_BADGES_SELECTOR).hide();
  3785.       $(SpamFader.NOT_SPAM_BUTTON_SELECTOR).hide();
  3786.       $(EightKun.POST_REPLY_SELECTOR)
  3787.           .css({'opacity': ''})
  3788.           .off('mouseenter mouseleave');
  3789.  
  3790.       $(document).off(EightKun.NEW_POST_EVENT);
  3791.     } else {
  3792.       $(SpamFader.SPAM_BADGES_SELECTOR).show();
  3793.       $(SpamFader.NOT_SPAM_BUTTON_SELECTOR).show();
  3794.       SpamFader.posterStats.forEach(this._takeSpamAction.bind(this));
  3795.       this._setupNewPostListener();
  3796.     }
  3797.   }
  3798.  
  3799.   /**
  3800.    * Hide spam badges on posts
  3801.    * @param {boolean} hide
  3802.    */
  3803.   hideSpamBadges(hide) {
  3804.     $('#'+this.hideSpamBadgesId).prop('checked', hide);
  3805.  
  3806.     localStorage.setItem(SpamFader.HIDE_SPAM_BADGES_SETTING, hide);
  3807.  
  3808.     if (hide) {
  3809.       $(SpamFader.SPAM_BADGES_SELECTOR).hide();
  3810.     } else {
  3811.       $(SpamFader.SPAM_BADGES_SELECTOR).show();
  3812.     }
  3813.   }
  3814.  
  3815.   /**
  3816.    * Read spamfader settings
  3817.    */
  3818.   _readSettings() {
  3819.     this.spamAction = localStorage[SpamFader.SPAM_ACTION_SETTING] ||
  3820.       SpamFader.FADE;
  3821.  
  3822.     this.hideSpamBadges(JSON.parse(
  3823.         localStorage.getItem(
  3824.             SpamFader.HIDE_SPAM_BADGES_SETTING),
  3825.     ));
  3826.  
  3827.     this.disableSpamFader(JSON.parse(
  3828.         localStorage.getItem(
  3829.             SpamFader.DISABLE_SPAM_FADER_SETTING),
  3830.     ));
  3831.   }
  3832.  
  3833.   /**
  3834.    * Get post stats for post
  3835.    * @param {Element} post div.post
  3836.    * @return {PosterStats}
  3837.    */
  3838.   static getPosterStats(post) {
  3839.     const posterId = EightKun.getPosterId(post);
  3840.     if (!SpamFader.posterStats.has(posterId)) {
  3841.       SpamFader.posterStats.set(posterId, new PosterStats(posterId));
  3842.     }
  3843.     return SpamFader.posterStats.get(posterId);
  3844.   }
  3845.  
  3846.   /**
  3847.    * Adds spam badge to the posts by the poster.
  3848.    * Wear them proudly fag!
  3849.    *
  3850.    * @param {PosterStats} posterStats The posterStats object representing
  3851.    *        spam fag
  3852.    * @param {string} badge Font-Awesome glyph for badge
  3853.    * @param {string} badgeTitle The title describing the badge
  3854.    */
  3855.   static addSpamBadge(posterStats, badge, badgeTitle) {
  3856.     posterStats.posts.forEach(function(post) {
  3857.       if (!$(post).find(SpamFader.SPAM_BADGES_SELECTOR).length) {
  3858.         SpamFader.createSpamBadgeSection(post);
  3859.       }
  3860.       const alreadyHasBadge = $(post)
  3861.           .find(SpamFader.SPAM_BADGES_SELECTOR)
  3862.           .find(`.fa-${badge}`).length;
  3863.  
  3864.       if (!alreadyHasBadge) {
  3865.         $(post).find(SpamFader.SPAM_BADGES_SELECTOR).append(
  3866.             `<i class="fa fa-${badge}" title='${badgeTitle}'></i>`,
  3867.         );
  3868.       }
  3869.     });
  3870.   }
  3871.  
  3872.   /**
  3873.    * Create section for spam badges
  3874.    * @param {Element} post div.post
  3875.    */
  3876.   static createSpamBadgeSection(post) {
  3877.     const $postModifiedSection = $(post).find(EightKun.POST_MODIFIED_SELECTOR);
  3878.     const button = $(`<button class='${SpamFader.NOT_SPAM_BUTTON_CLASS}'>
  3879.         <i class="fa fa-undo" title='Not spam'></i>Not Spam
  3880.       </button>
  3881.     `);
  3882.  
  3883.     button.click(function(e) {
  3884.       e.preventDefault();
  3885.       SpamFader.markNotSpam(post);
  3886.     });
  3887.  
  3888.     button.appendTo($postModifiedSection);
  3889.  
  3890.     $postModifiedSection.append(`
  3891.           <span class='${SpamFader.SPAM_BADGES_CLASS}'>Spam Badges:</span>`);
  3892.   }
  3893.  
  3894.   /**
  3895.    * Mark poster as not spam.
  3896.    *
  3897.    * @param {Element} post div.post
  3898.    */
  3899.   static markNotSpam(post) {
  3900.     const stats = SpamFader.getPosterStats(post);
  3901.     stats.markNotSpam();
  3902.  
  3903.     stats.posts.forEach(function(p) {
  3904.       $(p).css('opacity', 1);
  3905.       $(p).off('mouseenter mouseleave');
  3906.       $(p).find(SpamFader.SPAM_BADGES_SELECTOR).remove();
  3907.       $(p).find(`.${SpamFader.NOT_SPAM_BUTTON_CLASS}`).remove();
  3908.     });
  3909.  
  3910.     SpamFader.addToNotSpamList(stats);
  3911.   }
  3912.  
  3913.   /**
  3914.    * Save not spam in localstorage
  3915.    * @param {PosterStats} posterStats
  3916.    */
  3917.   static addToNotSpamList(posterStats) {
  3918.     const threadId = EightKun.getThreadId();
  3919.     const notSpamList = SpamFader.getNotSpamList();
  3920.     if (!(threadId in notSpamList)) {
  3921.       notSpamList[threadId] = [];
  3922.     }
  3923.     if (!SpamFader.isMarkedAsNotSpam(posterStats)) {
  3924.       notSpamList[threadId].push(posterStats.posterId);
  3925.       localStorage.setItem(SpamFader.NOT_SPAM_SETTING,
  3926.           JSON.stringify(notSpamList));
  3927.     }
  3928.   }
  3929.  
  3930.   /**
  3931.    * Has this poster been marked as not spam?
  3932.    * @param {PosterStats} posterStats
  3933.    * @return {boolean} true if not spam
  3934.    */
  3935.   static isMarkedAsNotSpam(posterStats) {
  3936.     const threadId = EightKun.getThreadId();
  3937.     const notSpamList = SpamFader.getNotSpamList();
  3938.     return threadId in notSpamList &&
  3939.         notSpamList[threadId].includes(posterStats.posterId);
  3940.   }
  3941.  
  3942.   /**
  3943.    * Get not spam list from localStorage
  3944.    * @return {Array} map of thread to not spam poster ids
  3945.    */
  3946.   static getNotSpamList() {
  3947.     return JSON.parse(
  3948.         localStorage.getItem(SpamFader.NOT_SPAM_SETTING) || '{}',
  3949.     );
  3950.   }
  3951. }
  3952. SpamFader.posterStats = new Map();
  3953. SpamFader.FADE = 'fade';
  3954. SpamFader.HIDE = 'hide';
  3955. SpamFader.SPAM_BADGES_CLASS = 'bakertools-spam-badges';
  3956. SpamFader.SPAM_BADGES_SELECTOR = `.${SpamFader.SPAM_BADGES_CLASS}`;
  3957. SpamFader.MIN_OPACITY = .2;
  3958. SpamFader.ANIMATION_DURATION = 200; // milliseconds
  3959. SpamFader.SPAM_ACTION_SETTING = 'bakertools-spamfader-action';
  3960. SpamFader.HIDE_SPAM_BADGES_SETTING = 'bakertools-spamfader-hide-badges';
  3961. SpamFader.DISABLE_SPAM_FADER_SETTING = 'bakertools-spamfader-disable';
  3962. SpamFader.NOT_SPAM_SETTING = 'bakertools-spamfader-notspam';
  3963. SpamFader.NOT_SPAM_BUTTON_CLASS = 'bakertools-spamfader-notspam';
  3964. SpamFader.NOT_SPAM_BUTTON_SELECTOR = `.${SpamFader.NOT_SPAM_BUTTON_CLASS}`;
  3965.  
  3966. /**
  3967.  * Holds spam stats
  3968.  */
  3969. class PosterStats {
  3970.   /**
  3971.    * Construct poststats for post
  3972.    * @param {number} posterId id of poster
  3973.    */
  3974.   constructor(posterId) {
  3975.     this.posts = [];
  3976.     this.posterId = posterId;
  3977.     this.markNotSpam();
  3978.   }
  3979.  
  3980.   /**
  3981.    * Reset spam indicators
  3982.    */
  3983.   markNotSpam() {
  3984.     this._spamCertainty = 0;
  3985.     this._fadeProgress = 0;
  3986.     this.floodCount = 0;
  3987.     this.breadShitCount = 0;
  3988.     this.isBreadShitter = false;
  3989.   }
  3990.  
  3991.   /**
  3992.    * Add post to poster's list of post
  3993.    * @param {Element} post div.post
  3994.    */
  3995.   addPost(post) {
  3996.     if (!this.posts.includes(post)) {
  3997.       this.posts.push(post);
  3998.     }
  3999.   }
  4000.  
  4001.   /**
  4002.    * Set spam certainty property
  4003.    * @param {number} certainty
  4004.    */
  4005.   set spamCertainty(certainty) {
  4006.     if (certainty > this._spamCertainty) {
  4007.       this._spamCertainty = certainty;
  4008.     }
  4009.   }
  4010.  
  4011.   /**
  4012.    * Get spam spamCertainty
  4013.    * @return {number} 1 represents 100% certainty.
  4014.    */
  4015.   get spamCertainty() {
  4016.     return this._spamCertainty;
  4017.   }
  4018.  
  4019.   /**
  4020.    * Set fade progress property
  4021.    * @param {number} progress
  4022.    */
  4023.   set fadeProgress(progress) {
  4024.     if (progress > this._fadeProgress) {
  4025.       this._fadeProgress = progress;
  4026.     }
  4027.   }
  4028.  
  4029.   /**
  4030.    * Get spam fade progress
  4031.    * @return {number} 1 represents 100% progress.
  4032.    */
  4033.   get fadeProgress() {
  4034.     return this._fadeProgress;
  4035.   }
  4036.  
  4037.   /**
  4038.    * Number of posts by id
  4039.    * @return {number}
  4040.    */
  4041.   get postCount() {
  4042.     return this.posts.length;
  4043.   }
  4044.  
  4045.   /**
  4046.    * Is this post spam?
  4047.    * @return {boolean} true if spam
  4048.    */
  4049.   get isSpam() {
  4050.     return this._spamCertainty >= 1;
  4051.   }
  4052. }
  4053.  
  4054. /**
  4055.  * Base class for spamDetectionStrategies
  4056.  */
  4057. class SpamDetectionStrategy {
  4058.   /**
  4059.    * Determine if the provided post is spam
  4060.    * @param {Element} post div.post
  4061.    * @return {boolean} true if is spam
  4062.    */
  4063.   isSpam(post) {
  4064.     return false;
  4065.   }
  4066. }
  4067.  
  4068. /**
  4069.  * Marks namefags as spam
  4070.  */
  4071. class NameFagStrategy extends SpamDetectionStrategy {
  4072.   /**
  4073.    * Construct NameFagStrategy
  4074.    */
  4075.   constructor() {
  4076.     super();
  4077.     this.nameRegex = /^Anonymous( \(You\))?$/;
  4078.     this.badge = 'tag';
  4079.     this.badgeTitle = 'Namefag';
  4080.   }
  4081.  
  4082.   /**
  4083.    * Returns true if a namefag, sets spamCertainty to 100% for post
  4084.    * to begin fading
  4085.    * @param {Element} post div.post
  4086.    * @return {boolean} true if is namefag spam
  4087.    */
  4088.   isSpam(post) {
  4089.     const isNameFag = !window.bakerTools.qPostHighlighter.isQ(post) &&
  4090.     (
  4091.       !this.nameRegex.test(EightKun.getPostName(post)) ||
  4092.       EightKun.getPostTrip(post) != ''
  4093.     );
  4094.  
  4095.     if (isNameFag) {
  4096.       const stats = SpamFader.getPosterStats(post);
  4097.       stats.spamCertainty = 1;
  4098.       stats.fadeProgress = .2;
  4099.       stats.isNameFag = true;
  4100.       SpamFader.addSpamBadge(stats, this.badge, this.badgeTitle);
  4101.     }
  4102.     return isNameFag;
  4103.   }
  4104. }
  4105.  
  4106. /**
  4107.  * Marks floodfags with high post count as spam
  4108.  */
  4109. class HighPostCountFagStrategy extends SpamDetectionStrategy {
  4110.   /**
  4111.    * Construct HighPostCountFagStrategy
  4112.    */
  4113.   constructor() {
  4114.     super();
  4115.     this.postCountSpamThreshold = 15;
  4116.     this.postCountHideThreshold = 25;
  4117.     this.badge = 'bullhorn';
  4118.     this.badgeTitle = 'High Post Count Fag';
  4119.   }
  4120.  
  4121.   /**
  4122.    * Returns true if the poster has posted more than the threshold
  4123.    * @param {Element} post div.post
  4124.    * @return {boolean} true if spam
  4125.    */
  4126.   isSpam(post) {
  4127.     if (EightKun.isPostFromOp(post)) {
  4128.       return;
  4129.     }
  4130.  
  4131.     const posterStats = SpamFader.getPosterStats(post);
  4132.  
  4133.     const highCountSpamCertainty =
  4134.       Math.min(1, posterStats.postCount / this.postCountSpamThreshold);
  4135.  
  4136.     posterStats.spamCertainty = highCountSpamCertainty;
  4137.  
  4138.     if (highCountSpamCertainty === 1) {
  4139.       posterStats.isHighPostCountFag = true;
  4140.       SpamFader.addSpamBadge(posterStats, this.badge, this.badgeTitle);
  4141.     }
  4142.  
  4143.     // We already hit spam threshold
  4144.     // Either we have hit threshold count or some other strategy says its spam
  4145.     if (posterStats.isSpam) {
  4146.       // Number of posts needed past threshold to hide
  4147.       const hideCount =
  4148.         this.postCountHideThreshold - this.postCountSpamThreshold;
  4149.  
  4150.       const progressIncrement = 1/hideCount;
  4151.  
  4152.       posterStats.fadeProgress += progressIncrement;
  4153.     }
  4154.     return posterStats.isSpam;
  4155.   }
  4156. }
  4157.  
  4158. /**
  4159.  * Marks floodfags with quick succession posts as spam
  4160.  */
  4161. class FloodFagStrategy extends SpamDetectionStrategy {
  4162.   /**
  4163.    * Construct flood fag strategy
  4164.    */
  4165.   constructor() {
  4166.     super();
  4167.     this.postIntervalConsideredFlooding = 60; // seconds
  4168.     this.floodCountSpamThreshold = 5;
  4169.     this.floodCountHideThreshold = 10;
  4170.     this.badge = 'tint';
  4171.     this.badgeTitle = 'Floodfag';
  4172.   }
  4173.  
  4174.   /**
  4175.    * Returns true if a spam
  4176.    * @param {Element} post div.post
  4177.    * @return {boolean} true if is spam
  4178.    */
  4179.   isSpam(post) {
  4180.     const posterStats = SpamFader.getPosterStats(post);
  4181.     if (EightKun.isPostFromOp(post) || !this.isPostFlooded(posterStats)) {
  4182.       return;
  4183.     }
  4184.     posterStats.floodCount++;
  4185.  
  4186.     const floodSpamCertainty =
  4187.       Math.min(1, posterStats.floodCount / this.floodCountSpamThreshold);
  4188.  
  4189.     posterStats.spamCertainty = floodSpamCertainty;
  4190.  
  4191.     if (floodSpamCertainty === 1) {
  4192.       posterStats.isFloodFag = true;
  4193.       SpamFader.addSpamBadge(posterStats, this.badge, this.badgeTitle);
  4194.     }
  4195.  
  4196.     // We already hit spam threshold
  4197.     // Either we have hit threshold count or some other strategy says its spam
  4198.     if (posterStats.isSpam) {
  4199.       // Number of posts needed past threshold to hide
  4200.       const hideCount =
  4201.         this.floodCountHideThreshold - this.floodCountSpamThreshold;
  4202.  
  4203.       const progressIncrement = 1/hideCount;
  4204.  
  4205.       posterStats.fadeProgress += progressIncrement;
  4206.     }
  4207.     return posterStats.isSpam;
  4208.   }
  4209.  
  4210.   /**
  4211.    * Is this a flooded post?
  4212.    * @param {PosterStats} posterStats
  4213.    * @return {boolean} true if flooded
  4214.    */
  4215.   isPostFlooded(posterStats) {
  4216.     if (posterStats.posts.length <= 1) {
  4217.       return false;
  4218.     }
  4219.     const currentPost = posterStats.posts.slice(-1)[0];
  4220.     const previousPost = posterStats.posts.slice(-2)[0];
  4221.     const previousPostTime = EightKun.getPostTime(previousPost);
  4222.     const currentPostTime = EightKun.getPostTime(currentPost);
  4223.     return (currentPostTime - previousPostTime) <=
  4224.       this.postIntervalConsideredFlooding;
  4225.   }
  4226. }
  4227.  
  4228. /**
  4229.  * Marks breadshitters as spam
  4230.  */
  4231. class BreadShitterFagStrategy extends SpamDetectionStrategy {
  4232.   // TODO: dont check for bread shitting on non research thread?
  4233.   /**
  4234.    * Construct flood fag strategy
  4235.    */
  4236.   constructor() {
  4237.     super();
  4238.     // Let's go easy, maybe its a newfag?
  4239.     this.breadShittingIncrement = .1;
  4240.     this.badge = 'clock-o';
  4241.     this.badgeTitle = 'Bread shitter';
  4242.   }
  4243.  
  4244.   /**
  4245.    * Returns true if a spam
  4246.    * @param {Element} post div.post
  4247.    * @return {boolean} true if is spam
  4248.    */
  4249.   isSpam(post) {
  4250.     const posterStats = SpamFader.getPosterStats(post);
  4251.     if (EightKun.isPostFromOp(post) || !this.isBreadShitter(post)) {
  4252.       return;
  4253.     }
  4254.     posterStats.breadShitCount++;
  4255.  
  4256.     posterStats.isBreadShitter = true;
  4257.     SpamFader.addSpamBadge(posterStats, this.badge, this.badgeTitle);
  4258.     posterStats.spamCertainty = 1;
  4259.     posterStats.fadeProgress += this.breadShittingIncrement;
  4260.  
  4261.     return posterStats.isSpam;
  4262.   }
  4263.  
  4264.   /**
  4265.    * Is this a bread shitting post?
  4266.    * @param {Element} post div.post
  4267.    * @return {boolean} true if bread shitter
  4268.    */
  4269.   isBreadShitter(post) {
  4270.     const dough = ResearchBread.getDoughPost();
  4271.     const doughTime = EightKun.getPostTime(dough);
  4272.     const postTime = EightKun.getPostTime(post);
  4273.  
  4274.     return postTime <= doughTime;
  4275.   }
  4276. }
  4277.  
  4278. /* exported StatsOverlay */
  4279. /* global $, EightKun, QPostHighlighter, NotablePost, debounce */
  4280. /**
  4281. * Overlays bread stats (and some other controls) in the bottom right of the
  4282. * screen.
  4283. */
  4284. class StatsOverlay {
  4285.   /**
  4286.    * Construct statsoverlay, html element, setup listeners
  4287.    */
  4288.   constructor() {
  4289.     this.id = 'bakertools-stats-overlay';
  4290.     this.maxPosts = 750;
  4291.     this.postCountId = 'bakertools-stats-post-count';
  4292.     this.userCountId = 'bakertools-stats-uid-count';
  4293.     this.qCountId = 'bakertools-stats-q-count';
  4294.     this.notableCountId = 'bakertools-stats-notable-count';
  4295.  
  4296.     this._createStyles();
  4297.     this._createElement();
  4298.     this._updateStats();
  4299.     $(document).on(EightKun.NEW_POST_EVENT, function(e, post) {
  4300.       this._updateStats();
  4301.     }.bind(this));
  4302.   }
  4303.  
  4304.   /**
  4305.    * Create styles for stats overlay
  4306.    */
  4307.   _createStyles() {
  4308.     const sheet = window.document.styleSheets[0];
  4309.     sheet.insertRule(`#${this.id} {
  4310.       padding: 5px;
  4311.       position: fixed;
  4312.       z-index: 100;
  4313.       float: right;
  4314.       right:28.25px;
  4315.       bottom: 28.25px;
  4316.     }`, sheet.cssRules.length);
  4317.   }
  4318.  
  4319.   /**
  4320.    * Create actual html element for style overlay
  4321.    */
  4322.   _createElement() {
  4323.     this.element = document.createElement('div');
  4324.     this.element.id = this.id;
  4325.  
  4326.     this.$goToLast = $(`
  4327.     <a href="javascript:void(0)" alt="last" title="Go to last reading location">
  4328.     <i class="fa fa-step-backward"></i>
  4329.     </a>`);
  4330.  
  4331.  
  4332.     this.saveLastReadingLocation = debounce(this.saveLastReadingLocation, 450);
  4333.     this.currentReadingLocation = $(window).scrollTop();
  4334.     $(window).scroll(function() {
  4335.       this.saveLastReadingLocation();
  4336.     }.bind(this));
  4337.  
  4338.     this.$goToLast.click(function() {
  4339.       $(window).scrollTop(this.lastReadingLocation);
  4340.     }.bind(this));
  4341.  
  4342.     $(this.element).append( `
  4343.     Posts: <span id="${this.postCountId}" ></span>
  4344.     UIDS: <span id="${this.userCountId}"></span>
  4345.     `);
  4346.  
  4347.     $(this.element).append(this.$goToLast);
  4348.  
  4349.     $(this.element).append(`
  4350.     <a href="#bottom" alt="to-bottom" title="Go to bottom">
  4351.     <i class="fa fa-angle-double-down"></i>
  4352.     </a>
  4353.     <a href="#top" alt="to-top" title="Go to top">
  4354.     <i class="fa fa-angle-double-up"></i>
  4355.     </a>
  4356.  
  4357.     <br/>
  4358.  
  4359.     Q's: <span id="${this.qCountId}" ></span>
  4360.    Notables: <span id="${this.notableCountId}"></span>
  4361.    `);
  4362.    document.body.appendChild(this.element);
  4363.    this._setPostCount($('div.post.reply').length);
  4364.  }
  4365.  
  4366.  /**
  4367.   * Save the last spot before scrolling or navigation
  4368.   */
  4369.  saveLastReadingLocation() {
  4370.    const scrollDistance = Math.abs(
  4371.        this.currentReadingLocation - $(window).scrollTop());
  4372.    const scrolledMoreThanThirdScreenHeight =
  4373.        scrollDistance > (window.innerHeight / 3);
  4374.  
  4375.    if (!scrolledMoreThanThirdScreenHeight) {
  4376.      return;
  4377.    }
  4378.    this.lastReadingLocation = this.currentReadingLocation;
  4379.    this.currentReadingLocation = $(window).scrollTop();
  4380.  }
  4381.  
  4382.  /**
  4383.   * Update the stats fields
  4384.   */
  4385.  _updateStats() {
  4386.    const postCount = $('#thread_stats_posts').text();
  4387.  
  4388.    if (postCount) {
  4389.      this._setPostCount(postCount);
  4390.    }
  4391.    // TODO: uids dont load at first load.
  4392.    $('#'+this.userCountId).text($('#thread_stats_uids').text() || '0');
  4393.    $('#'+this.qCountId).text(QPostHighlighter.qPosts.length);
  4394.    $('#'+this.notableCountId).text(NotablePost.getNotables().length);
  4395.  }
  4396.  
  4397.  /**
  4398.   * Set post count in overlay
  4399.   * @param {number} count
  4400.   */
  4401.  _setPostCount(count) {
  4402.    const progress = count/this.maxPosts;
  4403.    let postColor = 'green';
  4404.    if (progress >= .87) { // ~ 650 posts (100 posts left)
  4405.      postColor = 'red';
  4406.    } else if (progress >= .5) {
  4407.      postColor = 'goldenrod';
  4408.    }
  4409.    $('#'+this.postCountId).text(count).css({'color': postColor});
  4410.  }
  4411. } // End StatsOverlay class
  4412.  
  4413. /* global $, NavigationControl, EightKun, ResearchBread,
  4414. ColorPicker, POST_BACKGROUND_CHANGE_EVENT, BakerWindow */
  4415. /**
  4416. * Highlight posts that (you)
  4417. * Adds (You) navigation links to baker window
  4418. */
  4419. class YouHighlighter {
  4420.  /**
  4421.   * Construct YN object
  4422.   */
  4423.  constructor() {
  4424.    this.styleId = 'bakertools-you-styles';
  4425.    this.yous = [];
  4426.    this.ownPosts = [];
  4427.  
  4428.    this.showYouNavigationInBoardListId =
  4429.        'bakertools-show-you-nav-in-boardlist';
  4430.  
  4431.    this.showOwnNavigationInBoardListId =
  4432.        'bakertools-show-own-nav-in-boardlist';
  4433.  
  4434.    this._createStyles();
  4435.    this._setupBakerWindowControls();
  4436.    this._readSettings();
  4437.  
  4438.    this._initOwnAndYouPosts();
  4439.    this._setupListeners();
  4440.  }
  4441.  
  4442.  /**
  4443.   * Create styles
  4444.   */
  4445.  _createStyles() {
  4446.    $('head').append(`
  4447.    <style id='${this.styleId}'>
  4448.    ${EightKun.POST_SELECTOR}.${YouHighlighter.YOU_CLASS} {
  4449.      background: ${this.youColor};
  4450.    }
  4451.  
  4452.    ${EightKun.POST_SELECTOR}.${YouHighlighter.OWN_CLASS} {
  4453.      background: ${this.ownPostColor};
  4454.    }
  4455.    </style>
  4456.    `);
  4457.  }
  4458.  
  4459.  /**
  4460.   * Read settings from localStorage
  4461.   */
  4462.  _readSettings() {
  4463.    this.showYouNavigationInBoardList(JSON.parse(
  4464.        localStorage.getItem(
  4465.            YouHighlighter.SHOW_YOU_NAV_IN_BOARDLIST_SETTING),
  4466.    ));
  4467.  
  4468.    this.showOwnNavigationInBoardList(JSON.parse(
  4469.        localStorage.getItem(
  4470.            YouHighlighter.SHOW_OWN_NAV_IN_BOARDLIST_SETTING),
  4471.    ));
  4472.  }
  4473.  
  4474.  /**
  4475.   * Add (you) navigation to bakerwindow
  4476.   */
  4477.  _setupBakerWindowControls() {
  4478.    const youColorPicker = new ColorPicker(
  4479.        '(You) Post Color',
  4480.        'Set background color of posts replying to (you)',
  4481.        YouHighlighter.YOU_COLOR_SETTING,
  4482.        YouHighlighter.DEFAULT_YOU_COLOR,
  4483.        (color) => this.youColor = color,
  4484.    );
  4485.  
  4486.    const ownPostColorPicker = new ColorPicker(
  4487.        'Own Post Color',
  4488.        'Set background color your own posts',
  4489.        YouHighlighter.OWN_COLOR_SETTING,
  4490.        YouHighlighter.DEFAULT_OWN_COLOR,
  4491.        (color) => this.ownPostColor = color,
  4492.    );
  4493.  
  4494.    window.bakerTools.mainWindow.addColorOption(ownPostColorPicker.element);
  4495.    window.bakerTools.mainWindow.addColorOption(youColorPicker.element);
  4496.  
  4497.    this.ownNavigation = new NavigationControl('Own Posts',
  4498.    this.getOwnPosts.bind(this), YouHighlighter.NEW_OWN_POST_EVENT);
  4499.  
  4500.    this.ownBoardListNav = new NavigationControl('Own',
  4501.        this.getOwnPosts.bind(this), YouHighlighter.NEW_OWN_POST_EVENT);
  4502.  
  4503.    $(EightKun.getTopBoardlist()).append(this.ownBoardListNav.element);
  4504.    $(this.ownBoardListNav.element).hide();
  4505.  
  4506.    window.bakerTools.mainWindow.addNavigationOption(`
  4507.    <div class='${BakerWindow.CONTROL_GROUP_CLASS}'>
  4508.      <label for="${this.showOwnNavigationInBoardListId}"
  4509.        title="Show navigation controls in board list bar" >
  4510.        Show Own Post Nav in Board List:
  4511.      </label>
  4512.      <input type="checkbox" id="${this.showOwnNavigationInBoardListId}"
  4513.        title="Show navigation controls in board list bar" /><br />
  4514.    </div>
  4515.    `);
  4516.  
  4517.    window.bakerTools.mainWindow.addNavigation(this.ownNavigation.element);
  4518.  
  4519.    this.youNavigation = new NavigationControl(`(You)'s`,
  4520.         this.getYous.bind(this), YouHighlighter.NEW_YOU_POST_EVENT);
  4521.  
  4522.     this.youBoardListNav = new NavigationControl('You',
  4523.         this.getYous.bind(this), YouHighlighter.NEW_YOU_POST_EVENT);
  4524.  
  4525.     $(EightKun.getTopBoardlist()).append(this.youBoardListNav.element);
  4526.     $(this.youBoardListNav.element).hide();
  4527.  
  4528.     window.bakerTools.mainWindow.addNavigationOption(`
  4529.     <div class='${BakerWindow.CONTROL_GROUP_CLASS}'>
  4530.       <label for="${this.showYouNavigationInBoardListId}"
  4531.         title="Show navigation controls in board list bar" >
  4532.         Show (You) Nav in Board List:
  4533.       </label>
  4534.       <input type="checkbox" id="${this.showYouNavigationInBoardListId}"
  4535.         title="Show navigation controls in board list bar" /><br />
  4536.     </div>
  4537.     `);
  4538.  
  4539.  
  4540.     window.bakerTools.mainWindow.addNavigation(this.youNavigation.element);
  4541.   }
  4542.  
  4543.   /**
  4544.    * Set the background color of posts replying to (you)
  4545.    * @param {string} color A valid css color value.
  4546.    *        E.G. ('#ff00ee', 'rgba()' or 'blue')
  4547.    */
  4548.   set youColor(color) {
  4549.     this._youColor = color;
  4550.     document.getElementById(this.styleId)
  4551.         .sheet.cssRules[0].style.background = color;
  4552.     $(document).trigger(POST_BACKGROUND_CHANGE_EVENT, color);
  4553.   }
  4554.  
  4555.   /**
  4556.    * Get background color for posts replying to (you)
  4557.    */
  4558.   get youColor() {
  4559.     return this._youColor;
  4560.   }
  4561.  
  4562.   /**
  4563.    * Set the background color of your own posts
  4564.    * @param {string} color A valid css color value.
  4565.    *        E.G. ('#ff00ee', 'rgba()' or 'blue')
  4566.    */
  4567.   set ownPostColor(color) {
  4568.     this._ownPostColor = color;
  4569.     document.getElementById(this.styleId)
  4570.         .sheet.cssRules[1].style.background = color;
  4571.     $(document).trigger(POST_BACKGROUND_CHANGE_EVENT, color);
  4572.   }
  4573.  
  4574.   /**
  4575.    * Get background color for your own posts
  4576.    */
  4577.   get ownPostColor() {
  4578.     return this._ownPostColor;
  4579.   }
  4580.  
  4581.   /**
  4582.    * Setup listeners for baker window controls
  4583.    */
  4584.   _setupListeners() {
  4585.     $('#'+this.showOwnNavigationInBoardListId).change(function(e) {
  4586.       this.showOwnNavigationInBoardList(e.target.checked);
  4587.     }.bind(this));
  4588.  
  4589.     $('#'+this.showYouNavigationInBoardListId).change(function(e) {
  4590.       this.showYouNavigationInBoardList(e.target.checked);
  4591.     }.bind(this));
  4592.  
  4593.     $(document).on(EightKun.NEW_POST_EVENT, function(e, post) {
  4594.       if (this.isAYou(post)) {
  4595.         this._addYouPost(post);
  4596.       }
  4597.       if (this.isOwnPost(post)) {
  4598.         this._addOwnPost(post);
  4599.       }
  4600.     }.bind(this));
  4601.   }
  4602.  
  4603.   /**
  4604.    * Show/hide you nav in boardlist
  4605.    *
  4606.    * @param {boolean} showYouNavInBoardList
  4607.    */
  4608.   showYouNavigationInBoardList(showYouNavInBoardList) {
  4609.     $('#'+this.showYouNavigationInBoardListId).prop('checked',
  4610.         showYouNavInBoardList);
  4611.  
  4612.     localStorage.setItem(YouHighlighter.SHOW_YOU_NAV_IN_BOARDLIST_SETTING,
  4613.         showYouNavInBoardList);
  4614.  
  4615.     if (showYouNavInBoardList) {
  4616.       $(this.youBoardListNav.element).show();
  4617.     } else {
  4618.       $(this.youBoardListNav.element).hide();
  4619.     }
  4620.   }
  4621.  
  4622.   /**
  4623.    * Show/hide own nav in boardlist
  4624.    *
  4625.    * @param {boolean} showOwnNavInBoardList
  4626.    */
  4627.   showOwnNavigationInBoardList(showOwnNavInBoardList) {
  4628.     $('#'+this.showOwnNavigationInBoardListId).prop('checked',
  4629.         showOwnNavInBoardList);
  4630.  
  4631.     localStorage.setItem(YouHighlighter.SHOW_OWN_NAV_IN_BOARDLIST_SETTING,
  4632.         showOwnNavInBoardList);
  4633.  
  4634.     if (showOwnNavInBoardList) {
  4635.       $(this.ownBoardListNav.element).show();
  4636.     } else {
  4637.       $(this.ownBoardListNav.element).hide();
  4638.     }
  4639.   }
  4640.  
  4641.   /**
  4642.    * Get (You)'s
  4643.    * @return {Array} of div.post
  4644.    */
  4645.   getYous() {
  4646.     return this.yous;
  4647.   }
  4648.  
  4649.   /**
  4650.    * Is the post replying to you
  4651.    * @param {Element} post div.post
  4652.    * @return {boolean} True if post is replying to you
  4653.    */
  4654.   isAYou(post) {
  4655.     return post.querySelector('.body')
  4656.         .innerHTML
  4657.         .indexOf('<small>(You)</small>') != -1;
  4658.   }
  4659.  
  4660.   /**
  4661.    * Is this your own post
  4662.    * @param {Element} post div.post
  4663.    * @return {boolean} True if post is you
  4664.    */
  4665.   isOwnPost(post) {
  4666.     return $(post).hasClass('you');
  4667.   }
  4668.  
  4669.   /**
  4670.    * Add you post and trigger event
  4671.    * @param {Element} post
  4672.    */
  4673.   _addYouPost(post) {
  4674.     this.yous.push(post);
  4675.     $(post).addClass(YouHighlighter.YOU_CLASS);
  4676.     $(document).trigger(YouHighlighter.NEW_YOU_POST_EVENT, post);
  4677.   }
  4678.  
  4679.   /**
  4680.    * Add own post and trigger event
  4681.    * @param {Element} post
  4682.    */
  4683.   _addOwnPost(post) {
  4684.     this.ownPosts.push(post);
  4685.     $(post).addClass(YouHighlighter.OWN_CLASS);
  4686.     $(document).trigger(YouHighlighter.NEW_OWN_POST_EVENT, post);
  4687.   }
  4688.  
  4689.   /**
  4690.    * Get own and you posts that are present at page load
  4691.    */
  4692.   _initOwnAndYouPosts() {
  4693.     const ownPosts = JSON.parse(localStorage.own_posts || '{}');
  4694.     const board = ResearchBread.BOARD_NAME;
  4695.  
  4696.     $('div.post').each(function(i, post) {
  4697.       const postId = $(post).attr('id').split('_')[1];
  4698.       if (ownPosts[board] &&
  4699.           ownPosts[board].indexOf(postId) !== -1) {
  4700.         this._addOwnPost(post);
  4701.       }
  4702.  
  4703.       EightKun.getReplyLinksFromPost(post).each(function(i, link) {
  4704.         const youPostId = EightKun.getPostNumberFromReplyLink(link);
  4705.  
  4706.         if (ownPosts[board] && ownPosts[board].indexOf(youPostId) !== -1) {
  4707.           this._addYouPost(post);
  4708.         }
  4709.       }.bind(this));
  4710.     }.bind(this));
  4711.  
  4712.     window.bakerTools.scrollBar.addPosts(this.ownPosts);
  4713.     window.bakerTools.scrollBar.addPosts(this.yous);
  4714.   }
  4715.  
  4716.   /**
  4717.    * Get own posts
  4718.    * @return {Array} of div.post
  4719.    */
  4720.   getOwnPosts() {
  4721.     return this.ownPosts;
  4722.   }
  4723. }
  4724. YouHighlighter.SHOW_YOU_NAV_IN_BOARDLIST_SETTING =
  4725.     'bakertools-show-you-nav-in-boardlist';
  4726. YouHighlighter.SHOW_OWN_NAV_IN_BOARDLIST_SETTING =
  4727.     'bakertools-show-own-nav-in-boardlist';
  4728. YouHighlighter.NEW_YOU_POST_EVENT =
  4729.     'bakertools-new-you-post-event';
  4730. YouHighlighter.NEW_OWN_POST_EVENT =
  4731.     'bakertools-new-own-post-event';
  4732. YouHighlighter.YOU_CLASS = 'bakertools-you-post';
  4733. YouHighlighter.OWN_CLASS = 'bakertools-own-post';
  4734. YouHighlighter.OWN_COLOR_SETTING =
  4735.     'bakertools-own-post-color';
  4736. YouHighlighter.YOU_COLOR_SETTING =
  4737.     'bakertools-you-post-color';
  4738. YouHighlighter.DEFAULT_OWN_COLOR = '#F8D2D2';
  4739. YouHighlighter.DEFAULT_YOU_COLOR = '#E1B3DA';
  4740.  
  4741.  
  4742. /* global ActivePage, $, QPostHighlighter, YouHighlighter, StatsOverlay,
  4743.  NotableHighlighter, BakerWindow, BlurImages, PreviousBreadHighlighter,
  4744.  NominatePostButtons, BreadList, ScrollbarNavigation, NotablePost,
  4745.  ImageBlacklist, PostRateChart, SpamFader */
  4746. /**
  4747. * MAIN
  4748. */
  4749. if (ActivePage.isThread()) { // Only setup the tools if we are on a thread
  4750.   $(document).ready(function() {
  4751.     console.info('Thanks for using bakertools! For God and Country! WWG1WGA');
  4752.     window.bakerTools = {};
  4753.     window.bakerTools.mainWindow = new BakerWindow();
  4754.     window.bakerTools.scrollBar = new ScrollbarNavigation([
  4755.       NotablePost.NEW_NOTABLE_POST_EVENT,
  4756.       YouHighlighter.NEW_OWN_POST_EVENT,
  4757.       YouHighlighter.NEW_YOU_POST_EVENT,
  4758.       QPostHighlighter.NEW_Q_POST_EVENT,
  4759.     ]);
  4760.     new BlurImages();
  4761.     window.bakerTools.PreviousBreadHighlighter =
  4762.       new PreviousBreadHighlighter();
  4763.     window.bakerTools.qPostHighlighter = new QPostHighlighter();
  4764.     window.bakerTools.notableHighlighter = new NotableHighlighter();
  4765.     window.bakerTools.youHighlighter = new YouHighlighter();
  4766.     window.bakerTools.statsOverlay = new StatsOverlay();
  4767.     new NominatePostButtons();
  4768.     new BreadList();
  4769.     new ImageBlacklist();
  4770.     new PostRateChart();
  4771.     new SpamFader([new NameFagStrategy(), new HighPostCountFagStrategy(),
  4772.       new FloodFagStrategy(), new BreadShitterFagStrategy()]);
  4773.   });
  4774. }
  4775. }(window.jQuery));
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement