/*
==8Kun Baker Tools v0.7.3==
We have entered the STORM. Be STRONG!
GOD WINS! Pray MEME LOVE!
For God and Country! WWG1WGA
==Features:==
'''Post Highlighting'''
* Highlight posts that are marked notable (I.E. someone has replied and said
notable) in light green
* Highlight nominating posts in dark green
* Highlight nominating posts in posts mentions in light green
* Highlight Q Posts in yellow
* Highlight Q posts in mentions (I.E. on posts that get (YOU)'ed)
* Highlight links to Q Posts in sparkle (Like q's trip)
* Highlight previous bread links in blue
'''Navigation'''
* Cycle through Q Posts
* Cycle through (You)'s
* Cycle through own posts
* Jump To Bottom Link
* Jump To Bottom Top link
* NEW IN v0.7.0 Jump to last reading location (like when you post and it
sends you to bottom, you can jump right back)
* Easy access to Breads via Bread List window
* Scrollbar navigation shows location of Q/You/Notable/etc posts
* NEW IN v0.7.0: Hover over post marker to preview post
* Click on a post marker in scrollbar to jump to post
'''Filtering'''
* Filter to only nominating and notable posts, Q posts, Q replies
* Option to blur images until hover
* Image blacklist (AKA the NOPE button)
* NEW IN v0.7.0: SpamFader with multiple spam detection strategies:
* NameFag strategy: Marks namefags as spam
* Breadshitter strategy: Marks bread shitters as spam
* High post count strategy: Marks those with high post count as spam
* Flood fag strategy: Marks those who post in short intervals as spam
* Mark user as not spam button
* Spam badges tell WHY the algorithm marked as post as spam. TRANSPARENCY!
'''Customizable'''
* NEW IN v0.7.0: Customizable post highlighting colors
* Hide/Show features
* Settings saved in localStorage
'''Notables'''
* Generate notables post
* Adds "Notable Nomination" button to posts that opens the
Quick Reply box and prefills it with a BAKER NOTABLE Template
'''Stats'''
* Thread stats overlay with
* color coded reply count that goes from green to red as bread ages
* UID Count
* Post rate chart shows how many posts per min
==To Install:==
1. Copy this source code
2. Go to 8kun
3. Click "Options" in the top right
4. Choose "User JS" tab
5. Paste Baker tools JS
6. WWG1WGA
==Changelog:==
'''0.7.3'''
* Fix previous bread vs next bread link highlighting logic
* Update navigation controls colo(u)r to play nicer with dark theme
'''0.7.2'''
* Use flex layout in bakerwindow controls
* Reorder navcontrols
* Reduce space of controls in boardlist
* Disable spamfader new post event listener on disable
* Don't mark q posts as notable
* Make windows resizable width, fix table formatting in breadlist
* Use boardlist height in current index calcs
'''0.7.1'''
* Fix notable navigation in boardlist checkbox not working
* Differentiate between previous and newer breads when highlighting
'''0.7.0'''
* Switched color scheme to match other tools
* Post Per Minute Graph
* Spam Fading with multiple strategies and spam badges to tell why post is spam
* Allow customization of post highligting colors
* Add go back to last reading location button
* Improve Q post detection (all past trip codes)
* Add post preview on hover to Scrollbar Navigation
* Navigation controls are now aware of current page location
* Bugfixes
'''0.6.0'''
* Navigation bar shows scroll location of q/you/notable
posts and allows jumping to posts
* Notable navigation controls in baker window and board list
* Persistent Image Blacklist (AKA Nope Button)
* Many bugfixes
'''0.5.2'''
* Fixes bread list table population bug
'''0.5.0'''
* Option to show Q/(YOU)/Own Post navigation controls in the boardlist
* Option to hide Notable nomination button
* List of research breads
* BakerTools settings are now saved in local storage
'''0.4.0'''
* Option to blur images until hover
* Adds a "Notable Nomination" button to posts that opens the Quick Reply
box and prefills it with a BAKER NOTABLE Template
* Add Q Post navigation links to the Baker Window
* Add (You) navigation links to the Baker Window
* Add own post navigation links to the Baker Window
* Cleaned up baker window design
* More code cleanup and linting changes
'''0.3.0'''
* Highlights Q Posts with white BG -> DARK TO LIGHT!
* Highlights Q posts in mentions (I.E. posts that get (YOU)'ed)
* Highlights links to Q Posts
* Refactored code into classes for easier maint.
'''0.2.0'''
* Highlight pb links
* Thread stats overlay with
* color coded reply count that goes from green to red as bread ages
* UID Count
* Jump To Bottom Link
* Jump To Bottom Top link
'''0.1.0'''
Initial release:
* Highlight notables and nominators
* Filter to only show notables and nominators
* Create notables post
Version History:
https://pastebin.com/L1p6iRzZ 0.7.2
https://pastebin.com/dN5FhHCv 0.7.1
https://pastebin.com/6XuDuHYu 0.7.0
https://pastebin.com/YTSSmH7t 0.6.0
https://pastebin.com/mPVxr7Lz 0.5.2
https://pastebin.com/nEhm7yyY 0.5.1
https://pastebin.com/i9sF0Rd3 0.4.0
https://pastebin.com/kz9LrcE9 0.3.0
https://pastebin.com/4aEFsPwK 0.2.0
https://pastebin.com/eNmTtzdi 0.1.0
*/
(function($) {
"use strict";
/* globals $, board_name */
/* exported 8kun */
/**
* Functions and vars related to EightKun functionality
*/
class EightKun {
/**
* Get reply links in post
* @param {Element} post div.post
* @return {JQuery}
*/
static getReplyLinksFromPost(post) {
return $(post).find(EightKun.REPLY_SELECTOR)
.filter(function(idx, link) {
return $(link).text().match(EightKun.REPLY_SHORTLINK_REGEX);
});
}
/**
* Get the post number that is being replied to
* @param {Anchor} link
* @return {string}
*/
static getPostNumberFromReplyLink(link) {
return $(link).text()
.match(EightKun.REPLY_SHORTLINK_REGEX)[1];
}
/**
* Get time of post
* @param {Element} post div.post
* @return {number} post time in unixtime
*/
static getPostTime(post) {
return $(post).find('.intro time').attr('unixtime');
}
/**
* Get date of post
* @param {Element} post div.post
* @return {number} post time in unixtime
*/
static getPostDateTime(post) {
return $(post).find('.intro time').attr('datetime');
}
/**
* Get poster id of provided post
* @param {Element} post div.post
* @return {string} id of poster
*/
static getPosterId(post) {
return $(post).find('p > span.poster_id').first().text();
}
/**
* Get name from post
* @param {Element} post div.post
* @return {string} name of post
*/
static getPostName(post) {
return $(post).find('.intro > label > .name').text();
}
/**
* Get trip from post
* @param {Element} post div.post
* @return {string} trip of post
*/
static getPostTrip(post) {
return $(post).find('.intro .trip').text();
}
/**
* Get the opening post of the thread
* @return {Element} div.post
*/
static getOpPost() {
return $(EightKun.OP_POST_SELECTOR);
}
/**
* Get poster id of OP
* @return {number} poster id
*/
static getOpPosterId() {
return EightKun.getPosterId(EightKun.getOpPost());
}
/**
* Is the post made by op?
* @param {Element} post div.post
* @return {boolean} true if op's post
*/
static isPostFromOp(post) {
return EightKun.getPosterId(post) === EightKun.getOpPosterId();
}
/**
* Get the thread id
* @return {number} id of thread
*/
static getThreadId() {
return $('.thread').get(0).id.split('_')[1];
}
/**
* Use 8kun hide function on post
* @param {Element} post div.post
*/
static hidePost(post) {
// TODO: implement it and use in spam and blacklist
}
/**
* Get current board
* @return {string}
*/
static getCurrentBoard() {
/* eslint-disable camelcase */
return board_name;
}
/**
* Get post number of post
* @param {Element} post div.post
* @return {number} Number of the post
*/
static getPostNumber(post) {
return post.id.split('_')[1];
}
/**
* Get the top boardlist element
* @return {Element} div.boardlist
*/
static getTopBoardlist() {
return $(EightKun.TOP_BOARDLIST_SELECTOR).get(0);
}
}
EightKun.POST_SELECTOR = 'div.post';
EightKun.POST_REPLY_SELECTOR = 'div.post.reply';
EightKun.OP_POST_SELECTOR = 'div.post.op';
EightKun.POST_BODY_SELECTOR = '.body';
EightKun.POST_MODIFIED_SELECTOR = '.post_modified';
EightKun.NEW_POST_EVENT = 'new_post';
EightKun.OP_SUBJECT_SELECTOR = '.post.op > p > label > span.subject';
EightKun.REPLY_SELECTOR = 'div.body:first a:not([rel="nofollow"])';
EightKun.REPLY_SHORTLINK_REGEX = /^>>(\d+)$/;
EightKun.REPLY_REGEX = /highlightReply\('(.+?)'/;
EightKun.BOARDLIST_SELECTOR = `.boardlist`;
EightKun.TOP_BOARDLIST_SELECTOR = `${EightKun.BOARDLIST_SELECTOR}:first`;
/**
* Wrapper for 8kun active_page variable to determine the type of
* page the user is on.
*/
class ActivePage {
/**
* Are we currently on the thread index page?
* @return {boolean} True if on index
*/
static isIndex() {
return window.active_page == ActivePage.Index;
}
/**
* Are we currently on the thread catalog page?
* @return {boolean} True if on catalog
*/
static isCatalog() {
return window.active_page == ActivePage.Catalog;
}
/**
* Are we on a thread page?
* @return {boolean} True if on thread
*/
static isThread() {
return window.active_page == ActivePage.Thread;
}
}
ActivePage.Index = 'index';
ActivePage.Catalog = 'catalog';
ActivePage.Thread = 'thread';
/* globals $ */
/* exported ColorPicker */
/**
* A color picker control that saves to localStorage
*/
class ColorPicker {
/**
* Construct color picker
*
* @param {string} label The label for the control
* @param {string} title Mouseover title
* @param {string} setting localStorage setting name
* @param {string} defaultValue the default color when setting is missing
* @param {Function} changeHandler handler for value changes. Passes color
*/
constructor(label, title, setting, defaultValue, changeHandler) {
this.styleId = 'bakertools-colorpickers-styles';
this.class = 'bakertools-colorpicker';
this.labelClass = 'bakertools-colorpicker-label';
this.inputClass = 'bakertools-colorpicker-input';
this.resetButtonClass = 'bakertools-colorpicker-reset';
this.changeHandler = changeHandler;
this.label = label;
this.title = title;
this.setting = setting;
this.defaultValue = defaultValue;
this.strippedName = label.replace(/(\s|\(|\)|'|"|:)/g, '');
this.defaultValue = defaultValue;
this._createStyles();
this._createElement();
}
/**
* Create the HTML Element
*/
_createElement() {
this.element = $(`
`).get(0);
this.input = $(`
`).get(0);
$(this.element).append(this.input);
$(this.input).change(function(e) {
this.setColor(this.input.value);
}.bind(this));
this.reset = $(`
`).get(0);
$(this.element).append(this.reset);
$(this.reset).click(function(e) {
e.preventDefault();
this.setColor(this.defaultValue);
}.bind(this));
this.setColor(localStorage.getItem(this.setting) || this.defaultValue);
}
/**
* Set the color
* @param {string} color valid css color string
*/
setColor(color) {
localStorage.setItem(this.setting, color);
this.input.value = color;
this.changeHandler(color);
}
/**
* Get the color
* @return {string} color
*/
getColor() {
return this.input.value;
}
/**
* Create styles for the control
*/
_createStyles() {
if ($(`#${this.styleId}`).length) {
return;
}
$('head').append(`
`);
}
}
/* global $, debounce, EightKun */
/**
* Creates first, prev, next, last navigation controls
*/
class NavigationControl {
/**
* Construct navigatio control manager object
*
* @param {string} label the label for the control
* @param {Function} updateFunction Called to get latest data
* @param {string} updateEventName Called to get latest data
*/
constructor(label, updateFunction, updateEventName) {
const strippedName = label.replace(/(\s|\(|\)|'|"|:)/g, '');
this.styleId = 'bakertools-navigationcontrol-styles';
this.label = label;
this.updateFunction = updateFunction;
this.updateEventName = updateEventName;
this.list = this.updateFunction();
this.currentIndex = -1;
const instanceId = $(NavigationControl.containerClass).length;
this.navigationClass = `bakertools-navcontrol-${strippedName}`;
this.indexChangeEvent =
`bakertools-navcontrol-${strippedName}-index-changed`;
this.currentIndexId = `${this.navigationClass}-current-index-${instanceId}`;
this.currentIndexClass = `bakertools-navcontrol-current-index`;
this.totalClass = `${this.navigationClass}-total`;
this.goToFirstClass = `${this.navigationClass}-goto-first`;
this.goToPreviousClass = `${this.navigationClass}-goto-prev`;
this.goToNextClass = `${this.navigationClass}-goto-next`;
this.goToLastClass = `${this.navigationClass}-goto-last`;
this._setupStyles();
this._createElement();
this.updateIndexFromCurrentScrollPosition();
this.updateIndexFromCurrentScrollPosition =
debounce(this.updateIndexFromCurrentScrollPosition, 500);
this._setupListeners();
}
// TODO: switch to flexbox layout
/**
* setup styles for nav control
*/
_setupStyles() {
if ($(`#${this.styleId}`).length) {
return;
}
const boardListNavSelector =
`${EightKun.BOARDLIST_SELECTOR} .${NavigationControl.containerClass}`;
$('head').append(`
`);
}
/**
* Create nav element
*/
_createElement() {
this.element = $(`
${this.currentIndex+1}
:
${this.list.length}
`).get(0);
}
/**
* Setup button event listeners
*/
_setupListeners() {
$(this.element).find('.'+this.goToFirstClass).click(function(e) {
this.goToFirstPost();
}.bind(this));
$(this.element).find('.'+this.goToPreviousClass).click(function(e) {
this.goToPreviousPost();
}.bind(this));
$(this.element).find('.'+this.goToNextClass).click(function(e) {
this.goToNextPost();
}.bind(this));
$(this.element).find('.'+this.goToLastClass).click(function(e) {
this.goToLastPost();
}.bind(this));
$(document).on(this.indexChangeEvent, function(e, index) {
if (this.currentIndex == index) return;
this._setCurrentIndex(index);
}.bind(this));
$(document).on(this.updateEventName, function() {
this.list = this.updateFunction();
$(this.element).find(`.${this.totalClass}`).text(this.list.length);
}.bind(this));
$(document).scroll(this.updateIndexFromCurrentScrollPosition.bind(this));
}
/**
* Determine the current index based on scroll position
*/
updateIndexFromCurrentScrollPosition() {
const boardListHeight = $(EightKun.getTopBoardlist()).height();
for (let i = 0; i < this.list.length; ++i) {
const post = this.list[i];
const boundingRect = post.getBoundingClientRect();
const postTopAboveBottomOfScreen = boundingRect.top < window.innerHeight;
const postBottomBelowTopOfScreen = boundingRect.bottom > boardListHeight;
const currentPostIsInViewport = postTopAboveBottomOfScreen &&
postBottomBelowTopOfScreen;
if (currentPostIsInViewport) {
this._setCurrentIndex(i);
break;
}
const isFirstPost = i === 0;
const isBeforeFirstNotable = isFirstPost && !postTopAboveBottomOfScreen;
if (isBeforeFirstNotable) {
this._setCurrentIndex(-1);
break;
}
const isLastPost = i === (this.list.length - 1);
const isPastLastNotable = isLastPost && !postBottomBelowTopOfScreen;
if (isPastLastNotable) {
this._setCurrentIndex(i + .5);
break;
}
const nextPost = this.list[i+1];
const nextPostBounds = nextPost.getBoundingClientRect();
const nextPostIsBelowBottomOfScreen =
nextPostBounds.top >= window.innerHeight;
const inBetweenPosts = !postBottomBelowTopOfScreen &&
nextPostIsBelowBottomOfScreen;
if (inBetweenPosts) {
this._setCurrentIndex(i + .5);
break;
}
}
}
/**
* Scroll to first post
*/
goToFirstPost() {
if (!this.list.length) {
return;
}
this._setCurrentIndex(0);
this.scrollToCurrentPost();
}
/**
* Scroll to next navigated post
*/
goToPreviousPost() {
if (!this.list.length) {
return;
}
if (this.currentIndex <= 0) {
this._setCurrentIndex(this.list.length - 1);
} else {
this._setCurrentIndex(Math.ceil(this.currentIndex) - 1);
}
this.scrollToCurrentPost();
}
/**
* Scroll to next navigated post
*/
goToNextPost() {
if (!this.list.length) {
return;
}
const lastPostIndex = this.list.length - 1;
if (this.currentIndex >= lastPostIndex) {
this._setCurrentIndex(0);
} else {
this._setCurrentIndex(Math.floor(this.currentIndex) + 1);
}
this.scrollToCurrentPost();
}
/**
* Scroll the last post in this bread into view
*/
goToLastPost() {
if (!this.list.length) {
return;
}
const numPosts = this.list.length;
this._setCurrentIndex(numPosts - 1);
this.scrollToCurrentPost();
}
/**
* Scrolls the current selected post into view
*/
scrollToCurrentPost() {
const post = this.list[this.currentIndex];
$(post).get(0).scrollIntoView();
// Trigger events for other views of this data
$(document).trigger(this.indexChangeEvent,
this.currentIndex);
const boardListHeight = $(EightKun.getTopBoardlist()).height();
window.scrollBy(0, -boardListHeight);
}
/**
* Set internal index var and UI
* @param {number} index
*/
_setCurrentIndex(index) {
this.currentIndex = index;
this._setCurrentIndexControlValue(index + 1);
}
/**
* Sets the value of the current index in the UI
* @param {number} val
*/
_setCurrentIndexControlValue(val) {
$('#'+this.currentIndexId).text(val);
}
}
NavigationControl.containerClass = `bakertools-navcontrol-container`;
NavigationControl.navigationControlClass = 'bakertools-navigation-control';
/* global EightKun, $, NotableHighlighter */
/**
* Wrapper for a post nominated as notable
*/
class NotablePost {
/**
* Construct an empty notable post object
*/
constructor() {
this.element = null;
this.postNumber = null;
this.description = '[DESCRIPTION]';
this.nominatingPosts = [];
}
/**
* Create a notable post from a nominating post
*
* @param {Element} nominatingPost A post that is nominating a notable
* @return {NotablePost} a Notable post or NullNotablePost if it fails
*/
static fromNominatingPost(nominatingPost) {
const notables = [];
EightKun.getReplyLinksFromPost(nominatingPost)
.each(function(idx, link) {
const postNumber = EightKun.getPostNumberFromReplyLink(link);
const notablePostElement = $(`#reply_${postNumber}`).get(0);
if (window.bakerTools.qPostHighlighter.isQ(notablePostElement)) {
return false;
}
if (!NotablePost.findNotableByPostNumber(postNumber)) {
const notable = new NotablePost();
if (notablePostElement) {
notable.setElement(notablePostElement);
} else {
// TODO: set pb description
// get the json from the post number
notable.postNumber = postNumber;
}
notable.addNominatingPost(nominatingPost);
NotablePost.addToListOfNotables(notable);
notables.push(notable);
if (notable.element) { // Not pb will need to figure something out
$(document).trigger(NotablePost.NEW_NOTABLE_POST_EVENT,
notable.element);
}
}
});
return notables;
}
/**
* Add notable to list, and sort list
* @param {NotablePost} notable
*/
static addToListOfNotables(notable) {
NotablePost._notables.push(notable);
NotablePost._notables.sort(function(n1, n2) {
if (n1.postNumber < n2.postNumber) {
return -1;
} else if ( n1.postNumber > n2.postNumber) {
return 1;
}
return 0;
});
}
/**
* Is this a NullNotablePost
* @return {boolean} false
*/
isNull() {
return false;
}
/**
* @return {Array} Array of the current notables
*/
static getNotables() {
return NotablePost._notables;
}
/**
* Get notable posts as regular 8kun div.post
* @return {Array} of div.post
*/
static getNotablesAsPosts() {
return NotablePost._notables
.filter((n) => n.element !== null)
.map((n) => n.element);
}
/**
* @arg {number} postNumber The post number of notable
* @return {NotablePost}
*/
static findNotableByPostNumber(postNumber) {
return NotablePost._notables.find((notable) => notable.postNumber ==
postNumber);
}
/**
* Set the element of the post
* @arg {Element} element
*/
setElement(element) {
this.element = element;
this._markAsNotable(this.element);
this.description = element.querySelector('.body')
.innerText
.replace(/\n/g, ' ');
this.postNumber = $(this.element).find('.intro .post_no')
.text()
.replace('No.', '');
}
/**
* Get the reply shortlink for the post
* @return {string}
*/
shortLink() {
return '>>' + this.postNumber;
}
/**
* Add a nominator to the notable
*
* @param {Element} nominatingPost A .div.post that nominates this post
*/
addNominatingPost(nominatingPost) {
this.nominatingPosts.push(nominatingPost);
this._markAsNominator(nominatingPost);
this._markNominatorInMentions(nominatingPost);
}
/**
* @arg {Element} nominatorPost .post
*/
_markAsNominator(nominatorPost) {
nominatorPost.classList.add(NotableHighlighter.NOMINATOR_CLASS);
}
/**
* @arg {Element} post .post
*/
_markAsNotable(post) {
post.classList.add(NotableHighlighter.NOTABLE_CLASS);
}
/**
* Gives links to nominators a special style in notable mentions
*
* @param {Element} nominatingPost A .div.post that is nominating this
* notable
*/
_markNominatorInMentions(nominatingPost) {
if (!this.element) {
console.info(`Notable post is null - possible pb/lb`);
return;
}
const nominatingPostId = nominatingPost.id.replace('reply_', '');
$(this.element).find('.mentioned-'+nominatingPostId)
.addClass(NotableHighlighter.NOMINATOR_CLASS);
}
}
NotablePost._notables = [];
NotablePost.NULL = null; // NullNotablePost
NotablePost.NEW_NOTABLE_POST_EVENT = 'bakertools-new-notable-post-event';
/* globals EightKun */
/**
* Research Bread Class
*/
class ResearchBread {
/**
* Get an array of post bodies with dough posts filtered out
* @return {NodeList} of .post elements
*/
static getPostsWithoutDough() {
const posts = Array.from(document
.querySelectorAll(EightKun.POST_SELECTOR));
const filteredPosts = posts.filter(function(post) {
return !post.querySelector(EightKun.POST_BODY_SELECTOR)
.innerText.match(ResearchBread.DOUGH_POSTS_REGEX);
});
return filteredPosts;
}
/**
* Determine what the bread number is
* @return {number} the number of the research bread
*/
static getBreadNumber() {
const breadNumberRegex = /#(.+?) /;
return document.querySelector(EightKun.OP_SUBJECT_SELECTOR)
.innerText
.match(breadNumberRegex)[1] || 'COULD NOT FIND BREAD NUMBER';
}
/**
* Find the post with the dough
* @return {Element} div.post
*/
static getDoughPost() {
const posts = Array.from(document
.querySelectorAll(EightKun.POST_SELECTOR));
const dough = posts.find(function(post) {
return post.querySelector(EightKun.POST_BODY_SELECTOR)
.innerText.toUpperCase().match(ResearchBread.DOUGH_POST_TITLE);
});
return dough;
}
}
ResearchBread.BOARD_NAME = 'qresearch';
ResearchBread.WELCOME_POST_TITLE = 'Welcome To Q Research General';
ResearchBread.ANNOUNCEMENTS_POST_TITLE = 'Global Announcements';
ResearchBread.WAR_ROOM_POST_TITLE = 'War Room';
ResearchBread.ARCHIVES_POST_TITLE = 'QPosts Archives';
ResearchBread.DOUGH_POST_TITLE = 'DOUGH';
ResearchBread.DOUGH_POSTS_REGEX = new RegExp(
`^(${ResearchBread.WELCOME_POST_TITLE}|` +
`${ResearchBread.ANNOUNCEMENTS_POST_TITLE}|` +
`${ResearchBread.WAR_ROOM_POST_TITLE}|` +
`${ResearchBread.ARCHIVES_POST_TITLE}|` +
`${ResearchBread.DOUGH_POST_TITLE}).*`);
/* globals $, EightKun, debounce, POST_BACKGROUND_CHANGE_EVENT,
BakerWindow */
/* exported ScrollbarNavigation */
/**
* Scrollbar navigation
*/
class ScrollbarNavigation {
/**
* Construct a scrollbar nav
* @param {Array} addPostEvents List of event names that produce posts
* to show on scrollbar
*/
constructor(addPostEvents = []) {
this.id = 'bakertools-scrollbar-navigation';
this.showScrollbarNavigationId = 'bakertools-show-scrollbar-nav';
this.width = '20px';
this.posts = [];
this.coordsToPost = new Map();
this.addPostEvents = addPostEvents;
this.draw = debounce(this.draw, 80);
this._setupBakerWindowControls();
this._setupStyles();
this._createElement();
this._readSettings();
this._setupListeners();
}
/**
* Read settings from localStorage
*/
_readSettings() {
let showScrollBar = JSON.parse(localStorage
.getItem(ScrollbarNavigation.SHOW_SCROLLBAR_NAV));
showScrollBar = showScrollBar === null ? true : showScrollBar;
this.showScrollBar(showScrollBar);
}
/**
* Add hide/show option to bakerwindow
*/
_setupBakerWindowControls() {
window.bakerTools.mainWindow
.addNavigationOption(`
`);
}
/**
* Setup event listeners
*/
_setupListeners() {
$('#'+this.showScrollbarNavigationId).change(function(e) {
this.showScrollBar(e.target.checked);
}.bind(this));
$(document).on(EightKun.NEW_POST_EVENT, this.draw.bind(this));
$(window).on('resize', this.draw.bind(this));
$(window).on(POST_BACKGROUND_CHANGE_EVENT, this.draw.bind(this));
$('#'+this.id).click(function(e) {
const post = this.findFirstPostUnderMouse(e.clientX, e.clientY);
if (post) {
$(post).get(0).scrollIntoView();
window.scrollBy(0, -20);
}
}.bind(this));
$(this.element).mousemove(function(e) {
const [post, coords] = this.findFirstPostUnderMouse(e.clientX, e.clientY);
const notOverAPost = !post;
const hoveringOverADifferentPost = this.hoveringPost &&
this.hoveringPost != post;
if (notOverAPost || hoveringOverADifferentPost) {
this.endHover();
}
if (this.hovering) {
return;
}
const top = coords.top;
if (post) {
this.postHover(post, top);
}
}.bind(this));
$(this.element).mouseout(this.endHover.bind(this));
this.addPostEvents.forEach(function(eventName) {
$(document).on(eventName, function(event, posts) {
this.addPosts(posts);
}.bind(this));
}.bind(this));
}
/**
* Find the first post that is under the mouse
* @param {number} clientX x location of mouse
* @param {number} clientY y location of mouse
* @return {Array} div.post, [top,bottom] or null if not found
*/
findFirstPostUnderMouse(clientX, clientY) {
let post = null;
let coords = null;
for (const keyValue of this.coordsToPost) {
coords = keyValue[0];
// if (clientY >= (top - 10) && clientY <= (bottom+10)) {
if (clientY >= (coords.top) && clientY <= (coords.bottom)) {
post = keyValue[1];
break;
}
}
return [post, coords];
}
/**
* Perform post hover functionality for provided post
* @param {Element} post div.post
* @param {number} hoverY y location to hover at
*/
postHover(post, hoverY) {
this.hovering = true;
this.hoveringPost = post;
const $post = $(post);
if ($post.is(':visible') &&
$post.offset().top >= $(window).scrollTop() &&
$post.offset().top + $post.height() <=
$(window).scrollTop() + $(window).height()) {
// post is in view
$post.addClass('highlighted');
} else {
const newPost = $post.clone();
newPost.find('>.reply, >br').remove();
newPost.find('a.post_anchor').remove();
const postNumber = EightKun.getPostNumber(post);
newPost.attr('id', 'post-hover-' + postNumber)
.attr('data-board', EightKun.getCurrentBoard())
.addClass('post-hover')
.css('border-style', 'solid')
.css('box-shadow', '1px 1px 1px #999')
.css('display', 'block')
.css('position', 'absolute')
.css('font-style', 'normal')
.css('z-index', '100')
.css('left', '0')
.css('margin-left', '')
.addClass('reply')
.addClass('post')
.appendTo('.thread');
// shrink expanded images
newPost.find('div.file img.post-image').css({
'display': '',
'opacity': '',
});
newPost.find('div.file img.full-image').remove();
let previewWidth = newPost.outerWidth(true);
const widthDiff = previewWidth - newPost.width();
const scrollNavLeft = $(this.element).offset().left;
let left;
if (scrollNavLeft < $(document).width() * 0.7) {
left = scrollNavLeft + $(this.element).width();
if (left + previewWidth > $(window).width()) {
newPost.css('width', $(window).width() - left - widthDiff);
}
} else {
if (previewWidth > scrollNavLeft) {
newPost.css('width', scrollNavLeft - widthDiff);
previewWidth = scrollNavLeft;
}
left = scrollNavLeft - previewWidth;
}
newPost.css('left', left);
const scrollTop = $(window).scrollTop();
let top = scrollTop + hoverY;
if (top < scrollTop + 15) {
top = scrollTop;
} else if (top > scrollTop + $(window).height() - newPost.height() - 15) {
top = scrollTop + $(window).height() - newPost.height() - 15;
}
if (newPost.height() > $(window).height()) {
top = scrollTop;
}
newPost.css('top', top);
}
}
/**
* End hovering
*/
endHover() {
this.hovering = false;
if (!this.hoveringPost) {
return;
}
$(this.hoveringPost).removeClass('highlighted');
if ($(this.hoveringPost).hasClass('hidden')) {
$(this.hoveringPost).css('display', 'none');
}
$('.post-hover').remove();
}
/**
* Show/hide scrollbar
* @param {boolean} shouldShow Shows if true
*/
showScrollBar(shouldShow) {
$('#'+this.showScrollbarNavigationId).prop('checked',
shouldShow);
localStorage.setItem(ScrollbarNavigation.SHOW_SCROLLBAR_NAV, shouldShow);
if (shouldShow) {
$(`#${this.id}`).show();
} else {
$(`#${this.id}`).hide();
}
}
/**
* Setup styles for canvas
*/
_setupStyles() {
$('head').append(`
`);
}
/**
* Create the canvas
*/
_createElement() {
$(document.body).append(`
`);
this.element = $(`#${this.id}`).get(0);
}
/**
* Draw the scrollbar
*/
draw() {
const canvas = this.element;
canvas.height = window.innerHeight;
const ctx = canvas.getContext('2d');
if (!ctx) {
console.info('no ctx - is the element created yet?');
return;
}
ctx.clearRect(0, 0, canvas.width, canvas.height);
const cachedHeight = $(document).height();
const scrollHeight = canvas.height;
this.coordsToPost = new Map();
let lastCoords = null;
this.posts.forEach(function(post, index) {
const color = $(post).css('backgroundColor');
const postRect = post.getBoundingClientRect();
const scrollLocationPercentage =
(window.scrollY + postRect.top) / cachedHeight;
let drawLocation = scrollLocationPercentage * scrollHeight;
const overlappingPrevious = lastCoords &&
drawLocation <= (lastCoords.bottom + 2);
if (overlappingPrevious) {
drawLocation = lastCoords.bottom + 4;
}
const drawHeight = Math.max(
(postRect.height / cachedHeight) * scrollHeight,
5,
);
const coords = new ScrollbarCoordinates(drawLocation,
drawLocation + drawHeight);
this.coordsToPost.set(coords, post);
ctx.fillStyle = color;
ctx.fillRect(0, drawLocation, canvas.width, drawHeight);
lastCoords = coords;
}.bind(this));
}
/**
* Add posts to scrollbar
* @param {Element|Array} post div.post
*/
addPosts(post) {
if (Array.isArray(post)) {
post.forEach((p) => this._addPost(p));
} else {
this._addPost(post);
}
this._sortPosts();
this.draw();
}
/**
* Add post to post array if not already included
* @param {Element} post div.post
*/
_addPost(post) {
if (this.posts.includes(post)) {
return;
}
this.posts.push(post);
}
/**
* Sort posts by time
*/
_sortPosts() {
this.posts.sort(function(p1, p2) {
const p1PostTime = EightKun.getPostTime(p1);
const p2PostTime = EightKun.getPostTime(p2);
if (p1PostTime < p2PostTime) {
return -1;
}
if (p1PostTime > p2PostTime) {
return 1;
}
return 0;
});
}
}
ScrollbarNavigation.SHOW_SCROLLBAR_NAV = 'bakertools-show-scrollbar-nav';
/**
* Coordinates on the scrollbar
*/
class ScrollbarCoordinates {
/**
* Construct coords
* @param {number} top top of rect
* @param {number} bottom top of rect
*/
constructor(top, bottom) {
this.top = top;
this.bottom = bottom;
}
}
/* exported debounce, POST_BACKGROUND_CHANGE_EVENT */
/**
* Returns a function, that, as long as it continues to be invoked, will not
* be triggered. The function will be called after it stops being called for
* N milliseconds. If `immediate` is passed, trigger the function on the
* leading edge, instead of the trailing.
* https://davidwalsh.name/javascript-debounce-function
*
* @param {Function} func
* @param {number} wait
* @param {boolean} immediate
* @return {Function} debounced function
*/
function debounce(func, wait, immediate) {
let timeout;
return function(...args) {
const context = this;
const later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
};
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
}
const POST_BACKGROUND_CHANGE_EVENT = 'bakertools-post-background-change';
/* globals $, EightKun */
/* exported WindowElement */
/**
* Class for windows
*/
class WindowElement {
/**
* Construct WindowElement
* @param {string} windowName
* @param {string} linkText
*/
constructor(windowName, linkText) {
this.styleId = 'bakertools-WindowElement-basestyles';
this.id = `bakertools-${windowName}-window`;
this.linkText = linkText;
this.class = 'bakertools-WindowElement';
this.headerClass = 'bakertools-WindowElement-header';
this.windowCloseId = `bakertools-${windowName}-WindowElement-close`;
this.windowCloseClass = `bakertools-WindowElement-close`;
this.element = null;
this._createWindowStyles();
this._createElement();
this._setupWindowLink();
}
/**
* Create the window element
*/
_createElement() {
this.element = document.createElement('div');
this.element.id = this.id;
$(this.element).addClass(this.class);
this.element.innerHTML = `
${this.linkText}
`;
document.body.appendChild(this.element);
$(this.element).resizable({
'handles': 'e, w',
}).draggable();
$(this.element).hide();
$('#'+this.windowCloseId).click(function(e) {
this.hide();
}.bind(this));
}
/*
* Create CSS styles needed by the window
*/
_createWindowStyles() {
if ($('#' + this.styleId).length) {
return;
}
$('head').append(`
`);
}
/**
* Create link for show/hiding window, placed in boardlist bar
*/
_setupWindowLink() {
this.link = document.createElement('a');
this.link.textContent = `[${this.linkText}]`;
this.link.style.cssText = 'float: right;';
this.link.title = this.linkText;
this.link.href = 'javascript:void(0)';
$(EightKun.getTopBoardlist()).append(this.link);
this.link.onclick = this.toggle.bind(this);
}
/**
* Setup timeout for updating bread list
*/
_setupListeners() {
// window.setTimeout(this.updateBreadList, 1000)
}
/**
* Show the window
*/
show() {
$(this.element).css({'top': 15});
$(this.element).show();
}
/**
* Hide the window
*/
hide() {
$(this.element).hide();
}
/**
* Is the window visible?
* @return {boolean} true if window is visible
*/
isVisible() {
return $(this.element).is(':visible');
}
/**
* Toggle visibility of window
*/
toggle() {
if (this.isVisible()) {
this.hide();
} else {
this.show();
}
}
}
/* exported BakerWindow */
/* global NavigationControl, $, WindowElement */
/**
* Baker Window
*/
class BakerWindow extends WindowElement {
/**
* Construct Baker window element, register listeners
*/
constructor() {
super('baker', 'Baker Tools');
this.bakerWindowStyleId = 'bakertools-bakerwindow-style';
this.bakerWindowOptionsId = 'bakertools-window-options';
this.bakerWindowColorOptionsId = 'bakertools-window-color-options';
this.bakerWindowNavigationOptionsId =
'bakertools-window-navigation-options';
this.bakerWindowNotableOptionsId =
'bakertools-window-notable-options';
this.bakerWindowSpamOptionsId =
'bakertools-window-spam-options';
this.bakerWindowNavigationId = 'bakertools-window-navigation';
this.bakerWindowBakerId = 'bakertools-window-baker';
this.bakerWindowBodyId = 'bakertools-bakerwindow-body';
this._createStyles();
this._createBody();
}
/**
* Create CSS styles needed by the window
*/
_createStyles() {
if ($('#' + this.bakerWindowStyleId).length) {
return;
}
$('head').append(`
`);
}
/**
* Create the actual window HTML element
*/
_createBody() {
$('#'+this.id).append(`
`);
}
/**
* Add form controls to options section of baker window
* @arg {Element} htmlContentString form controls
*/
addOption(htmlContentString) {
$('#'+this.bakerWindowOptionsId).append(htmlContentString);
}
/**
* Add form controls to notable options section of baker window
* @arg {Element} htmlContentString form controls
*/
addNotableOption(htmlContentString) {
$('#'+this.bakerWindowNotableOptionsId)
.append(htmlContentString);
}
/**
* Add form controls to spam options section of baker window
* @arg {Element} htmlContentString form controls
*/
addSpamOption(htmlContentString) {
$('#'+this.bakerWindowSpamOptionsId)
.append(htmlContentString);
}
/**
* Add form controls to navigation options section of baker window
* @arg {Element} htmlContentString form controls
*/
addNavigationOption(htmlContentString) {
$('#'+this.bakerWindowNavigationOptionsId)
.append(htmlContentString);
}
/**
* Add form controls to color options section of baker window
* @arg {Element} htmlContentString form controls
*/
addColorOption(htmlContentString) {
$('#'+this.bakerWindowColorOptionsId).append(htmlContentString);
}
/**
* Add html elements to the navigation section of the baker window
* @arg {Element} htmlContentString form controls
*/
addNavigation(htmlContentString) {
$('#'+this.bakerWindowNavigationId).append(htmlContentString);
}
/**
* Add html elements to the baker section of the baker window
* @arg {Element} htmlContentString form controls
*/
addBaker(htmlContentString) {
$('#'+this.bakerWindowBakerId).append(htmlContentString);
}
} // end class BakerWindow
BakerWindow.CONTROL_GROUP_CLASS = 'bakertools-bakerwindow-control-group';
BakerWindow.CONTROL_GROUP_SELECTOR =
`.${BakerWindow.CONTROL_GROUP_CLASS}`;
/* global $, BakerWindow */
/**
* Blur images until highlighted
*/
class BlurImages {
/**
* Construct blur images object and setup styles
*/
constructor() {
this.blurImages = 'bakertools-blur-images';
this.blurImagesStyleId = 'bakertools-blur-images-style';
window.bakerTools.mainWindow.addOption(`
`);
this.navigation = new NavigationControl('Notables',
() => NotablePost.getNotablesAsPosts(),
NotablePost.NEW_NOTABLE_POST_EVENT);
window.bakerTools.mainWindow
.addNavigation(this.navigation.element);
this.boardListNav = new NavigationControl('Notables',
() => NotablePost.getNotablesAsPosts(),
NotablePost.NEW_NOTABLE_POST_EVENT);
$(EightKun.getTopBoardlist()).append(this.boardListNav.element);
$(this.boardListNav.element).hide();
}
/**
* Set the background color of notable posts
* @param {string} color A valid css color value.
* E.G. ('#ff00ee', 'rgba()' or 'blue')
*/
set notableColor(color) {
this._notableColor = color;
document.getElementById(this.styleId)
.sheet.cssRules[0].style.background = color;
$(document).trigger(POST_BACKGROUND_CHANGE_EVENT, color);
}
/**
* Get color for notable post backgrounds
*/
get notableColor() {
return this._notableColor;
}
/**
* Set the background color of nominator posts
* @param {string} color A valid css color value.
* E.G. ('#ff00ee', 'rgba()' or 'blue')
*/
set nominatorColor(color) {
this._nominatorColor = color;
document.getElementById(this.styleId)
.sheet.cssRules[1].style.background = color;
$(document).trigger(POST_BACKGROUND_CHANGE_EVENT, color);
}
/**
* Get color for notable post backgrounds
*/
get nominatorColor() {
return this._nominatorColor;
}
/**
* Set the color of nominator mention links posts
* @param {string} color A valid css color value.
* E.G. ('#ff00ee', 'rgba()' or 'blue')
*/
set nominatorMentionLinkColor(color) {
this._nominatorMentionLinkColor = color;
document.getElementById(this.styleId)
.sheet.cssRules[2].style.color = color;
}
/**
* Get color for notable post backgrounds
*/
get nominatorMentionLinkColor() {
return this._nominatorMentionLinkColor;
}
/**
* Setup listeners for new posts, bakerwindow controls, etc
*/
_setupListeners() {
$('#'+this.showOnlyNotablesCheckboxId).change(function(e) {
this.setOnlyShowNotables(e.target.checked);
}.bind(this));
$('#'+this.createNotablePostButtonId).click(function() {
if ($('#'+this.notableEditorId).val()) {
if (!confirm(`If you continue, any changes you made will be
overwritten!`)) {
return;
}
}
$('#'+this.notableEditorId).val(this.createNotablesPost());
}.bind(this));
$(document).on(EightKun.NEW_POST_EVENT, function(e, post) {
this.checkNewPostsForNotables(post);
}.bind(this));
$('#'+this.showNotableNavigationInBoardListId).change(function(e) {
this.showNotableNavigationInBoardList(e.target.checked);
}.bind(this));
}
/**
* Show or hide notable nav control in the boardlist
*
* @param {boolean} show
*/
showNotableNavigationInBoardList(show) {
$('#'+this.showNotableNavigationInBoardListId).prop('checked',
show);
localStorage
.setItem(NotableHighlighter.SHOW_NOTABLE_NAV_IN_BOARDLIST_SETTING,
show);
if (show) {
$(this.boardListNav.element).show();
} else {
$(this.boardListNav.element).hide();
}
}
/**
* Create the notables post for review
* @return {string} Returns the notable post string
*/
createNotablesPost() {
const notables = NotablePost.getNotables();
const breadNumber = ResearchBread.getBreadNumber();
let post = `'''#${breadNumber}'''\n\n`;
notables.forEach(function(notable) {
post += `${notable.shortLink()} ${notable.description}\n\n`;
});
return post;
}
/**
* Checks a post for notable nominations
* @param {Element} post
*/
checkNewPostsForNotables(post) {
$(post).removeAttr('style'); // TODO: try removing
if (this.isNominatingPost(post)) {
NotablePost.fromNominatingPost(post);
}
}
/**
* Finds posts that are being tagged as notable.
*
* I.E. Finding any post that has been replied to by a post with the string
* "notable" in it. Maybe at somepoint this can be smarter. Q give me some
* dwave snow white tech!
*
* Highlights notable posts in yellow
* Highlights nominating posts in pink <3
* Highlights nominating posts in mentions
* Add nominee count to post
* @return {Array}
*/
findNominatedNotables() {
const postsWithoutDough = ResearchBread.getPostsWithoutDough();
// ^s to ignore notables review posts
const nominatingPosts = postsWithoutDough
.filter((post) => this.isNominatingPost(post));
nominatingPosts.forEach(function(nominatingPost) {
NotablePost.fromNominatingPost(nominatingPost);
});
console.log(NotablePost.getNotables());
return NotablePost.getNotables();
}
/**
* Is the post nominating a notable
* @arg {Element} post .post
* @return {boolean} True if post nominates a notable
*/
isNominatingPost(post) {
const postContainsNotable = post.textContent
.search(this.NOMINATING_REGEX) != -1;
const postIsReplying = EightKun.getReplyLinksFromPost(post).length;
return postContainsNotable && postIsReplying;
}
/**
* Toggle whether only the notable/nominee posts are shown or not
* @arg {boolean} onlyShowNotables boolean If true, only show
* notables/nominators, else show all
*/
setOnlyShowNotables(onlyShowNotables) {
$('#'+this.showOnlyNotablesCheckboxId).prop('checked', onlyShowNotables);
localStorage.setItem(NotableHighlighter.ONLY_SHOW_NOTABLES_SETTING,
onlyShowNotables);
const notableOrNominationPostsSelector =
`div.post.${NotableHighlighter.NOTABLE_CLASS},
div.post.${NotableHighlighter.NOMINATOR_CLASS}`;
const notableOrNominationPostBreaksSelector =
`div.post.${NotableHighlighter.NOTABLE_CLASS}+br,
div.post.${NotableHighlighter.NOMINATOR_CLASS}+br`;
const onlyShowNotablesStyleId = 'bakertools-only-show-notables';
if (onlyShowNotables) {
$(``).appendTo('head');
} else {
$(`#${onlyShowNotablesStyleId}`).remove();
// For whatever reason, when the non notable posts are filtered and new
// posts come through the auto_update, the posts are created with
// style="display:block" which messes up display. Remove style attr
// TODO: can we remove this now that we have !important?
$(EightKun.POST_SELECTOR).removeAttr('style');
}
}
/**
* Retrieves only show notable ssetting from localStorage
* @return {boolean} true if only show notables is turned on
*/
getOnlyShowNotables() {
return localStorage
.getItem(NotableHighlighter.ONLY_SHOW_NOTABLES_SETTING);
}
}
NotableHighlighter.NOMINATOR_CLASS = 'bakertools-notable-nominator';
NotableHighlighter.NOTABLE_CLASS = 'bakertools-notable';
NotableHighlighter.ONLY_SHOW_NOTABLES_SETTING =
'bakertools-only-show-notables';
NotableHighlighter.SHOW_NOTABLE_NAV_IN_BOARDLIST_SETTING =
'bakertools-show-notable-nav-in-boardlist';
NotableHighlighter.NOMINATOR_COLOR_SETTTING =
'bakertools-nominator-color';
NotableHighlighter.NOTABLE_COLOR_SETTTING =
'bakertools-notable-color';
NotableHighlighter.NOMINATOR_MENTION_LINK_COLOR_SETTTING =
'bakertools-nominator-metion-link-color';
NotableHighlighter.DEFAULT_NOTABLE_COLOR = '#E5FFCC';
NotableHighlighter.DEFAULT_NOMINATOR_COLOR = '#ACC395';
NotableHighlighter.DEFAULT_NOMINATOR_MENTION_LINK_COLOR = '#00CC00';
/* globals $, EightKun, debounce, BakerWindow */
/* exported PostRateChart */
/**
* Displays chart of post/min
*/
class PostRateChart {
/**
* Construct a postrate chart
*/
constructor() {
this.containerClass = 'bakertools-postrate-container';
this.chartClass = 'bakertools-postrate-chart';
this.rateClass = 'bakertools-postrate-rate';
this.styleId = 'bakertools-postrate-style';
this.hidePostRateChartId = 'bakertools-postrate-hide-postrate';
this.numberOfPostsForAverage = 10;
this.numberOfDataPointsShownOnChart = 10;
this.postTimes = [];
this.postsPerMinuteHistory = [];
this._setupStyles();
this._setupBakerWindowControls();
this._createElement();
this._getExistingPostRates();
this._setupListeners();
this.draw();
this.draw = debounce(this.draw, 1000 *2);
this._readSettings();
}
/**
* Read settings from local storage
*/
_readSettings() {
const hidePostRate = JSON.parse(localStorage
.getItem(PostRateChart.HIDE_POSTRATE_SETTING));
this.showPostRateChart(!hidePostRate);
}
/**
* Setup chart styles
*/
_setupStyles() {
$('head').append(`
`);
}
/**
* Setup listeners for pb highlighting
*/
_setupListeners() {
$(document).on(EightKun.NEW_POST_EVENT, function(e, post) {
$(post).find(this._linkSelector)
.each((index, link) => this.markLinkIfPreviousBread(link));
}.bind(this));
}
/**
* Marks the link if it is pb
*
* @param {Anchor} link
*/
markLinkIfPreviousBread(link) {
const currentBreadNumber = document.location.pathname
.split('/')
.slice(-1)[0]
.split('.')[0];
const linkBreadNumber = link.href.split('/')
.slice(-1)[0]
.split('#')[0]
.split('.')[0];
const isAReplyLink = $(link)
.attr('onclick')
.search(EightKun.REPLY_REGEX) != 1;
if (isAReplyLink &&
parseInt(currentBreadNumber, 10) > parseInt(linkBreadNumber, 10)) {
$(link).addClass(this.previousBreadClass);
} else if (isAReplyLink &&
parseInt(currentBreadNumber, 10) < parseInt(linkBreadNumber, 10)) {
$(link).addClass(this.newerBreadClass);
}
}
}
PreviousBreadHighlighter.PREVIOUS_BREAD_LINK_COLOR_SETTING =
'bakertools-previous-bread-link-color';
PreviousBreadHighlighter.DEFAULT_PREVIOUS_BREAD_LINK_COLOR =
'#0000CC';
PreviousBreadHighlighter.NEWER_BREAD_LINK_COLOR_SETTING =
'bakertools-newer-bread-link-color';
PreviousBreadHighlighter.DEFAULT_NEWER_BREAD_LINK_COLOR =
'#00CC00';
/* global $, EightKun, ResearchBread, NavigationControl,
ColorPicker, POST_BACKGROUND_CHANGE_EVENT, BakerWindow */
/**
* Highlight Q posts, replies to q, q replies.
* Adds navigation to baker window
*/
class QPostHighlighter {
/**
* Construct qposthighlighter object and setup listeners
*/
constructor() {
this.styleId = 'bakertools-q-style';
this.qPostClass = 'bakertools-q-post';
this.qReplyClass = 'bakertools-q-reply';
this.qMentionClass = 'bakertools-q-mention';
this.qLinkClass = 'bakertools-q-link';
this.styleId = 'bakertools-q-styles';
this._linkSelector = 'div.body > p.body-line.ltr > a';
this.showQNavigationInBoardListId =
'bakertools-show-q-nav-in-boardlist';
this.currentQTripCode = null;
this._setupStyles();
this._setupBakerWindowControls();
this._readSettings();
this._findQPosts();
this._setupListeners();
}
/**
* Read settings from localStorage
*/
_readSettings() {
this.showQNavigationInBoardList(JSON.parse(
localStorage
.getItem(QPostHighlighter.SHOW_Q_NAV_IN_BOARDLIST_SETTING),
));
}
/**
* Setup styles for highlighting q posts
*/
_setupStyles() {
$('head').append(`
`);
}
/**
* Set the background color of q posts
* @param {string} color A valid css color value.
* E.G. ('#ff00ee', 'rgba()' or 'blue')
*/
set qPostColor(color) {
this._qPostColor = color;
document.getElementById(this.styleId)
.sheet.cssRules[0].style.background = color;
$(document).trigger(POST_BACKGROUND_CHANGE_EVENT, color);
}
/**
* Get color for q post backgrounds
*/
get qPostColor() {
return this._qPostColor;
}
/**
* Set the background color of q posts
* @param {string} color A valid css color value.
* E.G. ('#ff00ee', 'rgba()' or 'blue')
*/
set qYouColor(color) {
this._qYouColor = color;
document.getElementById(this.styleId)
.sheet.cssRules[1].style.background = color;
$(document).trigger(POST_BACKGROUND_CHANGE_EVENT, color);
}
/**
* Get bg color for posts q replies to
*/
get qYouColor() {
return this._qYouColor;
}
/**
* Get Q's current trip code from the bread
*/
_getCurrentQTripFromBread() {
const tripCodeMatch = $(EightKun.getOpPost())
.text()
.match(/Q's Trip-code: Q (.+?\s)/);
if (!tripCodeMatch) {
console.error('Could not find Q\'s tripcode');
return;
}
this.currentQTripCode = tripCodeMatch[1].split(' ')[0];
}
/**
* Find current Q posts in bread
*/
_findQPosts() {
const posts = ResearchBread.getPostsWithoutDough();
$(posts).each(function(i, post) {
this._doItQ(post);
}.bind(this));
}
/**
* Check if the post is Q
* WWG1WGA
*
* @param {Element} post a div.post
*/
_doItQ(post) {
if (this._markIfQPost(post)) { // Q Post, lets check for q replies
const qPostNumber = $(post)
.find('.intro .post_no')
.text()
.replace('No.', '');
const links = $(post)
.find(this._linkSelector)
.filter('[onClick]');
$(links).each(function(i, link) {
const postNumber = link.href.split('#')[1];
// Enlightened post
$(`#reply_${postNumber}`).addClass(this.qReplyClass);
const metionLinkSelector = `#reply_${postNumber} .intro .mentioned a`;
$(metionLinkSelector).each(function(i, mentionAnchor) {
const mentionPostNumber = $(mentionAnchor).text().replace('>>', '');
if (mentionPostNumber == qPostNumber) {
$(mentionAnchor).addClass(this.qMentionClass);
}
}.bind(this));
}.bind(this));
} else { // Not Q, but lets check if this post replies to Q
const links = $(post).find(this._linkSelector).filter('[onClick]');
$(links).each(function(i, link) {
const postNumber = link.href.split('#')[1];
const replyPost = document.querySelector(`#reply_${postNumber}`);
// TODO: need to handle pb posts
if (this.isQ(replyPost)) {
$(link).addClass(this.qLinkClass);
}
}.bind(this));
}
}
/**
* @arg {Element} post div.post.reply
* @return {boolean} true if it is a q post
*/
_markIfQPost(post) {
let isQ = false;
if (this.isQ(post)) {
isQ = true;
$(post).addClass(this.qPostClass);
QPostHighlighter.qPosts.push(post);
$(document).trigger(QPostHighlighter.NEW_Q_POST_EVENT, post);
}
return isQ;
}
/**
* Is the post Q?
* @param {Element} post a div.post.reply
* @return {boolean} true if the post is Q
*/
isQ(post) {
const qTripHistory = QTripCodeHistory.INSTANCE;
const dateOfPost = new Date(EightKun.getPostDateTime(post));
const expectedQTripBasedOnDate = qTripHistory.getTripCodeByDate(dateOfPost);
if (!expectedQTripBasedOnDate) {
console.info(`Could not find Q trip code for date: ${dateOfPost}`);
return false;
}
return EightKun.getPostTrip(post) == expectedQTripBasedOnDate.tripCode;
}
/**
* Add Q post navigation to bakerwindow
*/
_setupBakerWindowControls() {
window.bakerTools.mainWindow
.addNavigationOption(`
`);
this.navigation = new NavigationControl('Q Posts',
() => QPostHighlighter.qPosts, QPostHighlighter.NEW_Q_POST_EVENT);
window.bakerTools.mainWindow
.addNavigation(this.navigation.element);
this.boardListNav = new NavigationControl('Q',
() => QPostHighlighter.qPosts, QPostHighlighter.NEW_Q_POST_EVENT);
$(EightKun.getTopBoardlist()).append(this.boardListNav.element);
$(this.boardListNav.element).hide();
const qColorPicker = new ColorPicker(
'Q Post Color',
'Set background color of Q Posts',
QPostHighlighter.Q_POST_COLOR_SETTING,
QPostHighlighter.DEFAULT_Q_POST_COLOR,
(color) => this.qPostColor = color,
);
const qYouColorPicker = new ColorPicker(
'Q (You) Color',
'Set background color of posts Q Replies to',
QPostHighlighter.Q_YOU_POST_COLOR_SETTING,
QPostHighlighter.DEFAULT_Q_YOU_POST_COLOR,
(color) => this.qYouColor = color,
);
window.bakerTools.mainWindow.addColorOption(qColorPicker.element);
window.bakerTools.mainWindow.addColorOption(qYouColorPicker.element);
}
/**
* Setup listeners for new posts
*/
_setupListeners() {
$(document).on(EightKun.NEW_POST_EVENT, function(e, post) {
this._doItQ(post);
}.bind(this));
$('#'+this.showQNavigationInBoardListId).change(function(e) {
this.showQNavigationInBoardList(e.target.checked);
}.bind(this));
}
/**
* Show or hide q nav control in the boardlist
*
* @param {boolean} showNavInBoardList
*/
showQNavigationInBoardList(showNavInBoardList) {
$('#'+this.showQNavigationInBoardListId).prop('checked',
showNavInBoardList);
localStorage.setItem(QPostHighlighter.SHOW_Q_NAV_IN_BOARDLIST_SETTING,
showNavInBoardList);
if (showNavInBoardList) {
$(this.boardListNav.element).show();
} else {
$(this.boardListNav.element).hide();
}
}
}
QPostHighlighter.qPosts = [];
QPostHighlighter.NEW_Q_POST_EVENT = 'bakertools-new-q-post';
QPostHighlighter.SHOW_Q_NAV_IN_BOARDLIST_SETTING =
'bakertools-show-q-nav-in-boardlist';
QPostHighlighter.Q_YOU_COLOR_SETTING =
'bakertools-q-you-color';
QPostHighlighter.Q_POST_COLOR_SETTING =
'bakertools-q-post-color';
QPostHighlighter.DEFAULT_Q_POST_COLOR = '#FFFFCC';
QPostHighlighter.DEFAULT_Q_YOU_POST_COLOR = '#DDDDDD';
/**
* History of Q's tripcodes and their date ranges
*/
class QTripCodeHistory {
/**
* Construct the q trip history
*/
constructor() {
// Hat tip to https://8kun.top/qresearch/res/7762733.html#7832643 for Q trip history
this.history = [
new QTripCode('!ITPb.qbhqo',
new Date('2017-11-10 04:07:15Z'), new Date('2017-12-15 06:04:43Z')),
new QTripCode('!UW.yye1fxo',
new Date('2017-12-15 06:04:06Z'), new Date('2018-03-24 13:09:02Z')),
new QTripCode('!xowAT4Z3VQ',
new Date('2018-03-24 13:09:37Z'), new Date('2018-05-04 20:02:22Z')),
new QTripCode('!2jsTvXXmXs',
new Date('2018-05-04 20:01:19Z'), new Date('2018-05-08 23:46:39Z')),
new QTripCode('!4pRcUA0lBE',
new Date('2018-05-08 23:47:17Z'), new Date('2018-05-19 22:06:20Z')),
new QTripCode('!CbboFOtcZs',
new Date('2018-05-19 22:07:06Z'), new Date('2018-08-05 20:12:52Z')),
new QTripCode('!A6yxsPKia.',
new Date('2018-08-05 20:14:24Z'), new Date('2018-08-10 18:24:24Z')),
new QTripCode('!!mG7VJxZNCI',
new Date('2018-08-10 18:26:08Z'), new Date('2019-11-25 22:35:45Z')),
new QTripCode('!!Hs1Jq13jV6',
new Date('2019-12-02 17:55:59Z'), null),
];
}
/**
* Get Q Tripcode by the provided date
* @param {Date} date
* @return {QTripCode}
*/
getTripCodeByDate(date) {
let returnTripCode = null;
for (const tripCode of this.history) {
if (tripCode.isValidForDate(date)) {
returnTripCode = tripCode;
break;
}
}
return returnTripCode;
}
/**
* Get Q Tripcode by the current
* @return {QTripCode}
*/
getCurrentTripCode() {
return this.getTripCodeByDate(new Date());
}
}
/**
* Represents a Tripcode used by Q and the timeframe
*/
class QTripCode {
/**
* Create a new QTripCode
* @param {string} tripCode
* @param {DateTime} startDate
* @param {DateTime} endDate
*/
constructor(tripCode, startDate, endDate) {
this.tripCode = tripCode;
this.startDate = startDate;
this.isCurrentTrip = false;
if (!endDate) {
this.isCurrentTrip = true;
}
this.endDate = endDate;
}
/**
* Is this tripcode valid for the provided date?
* @param {Date} date
* @return {boolean} true if this trip code is valid for the date
*/
isValidForDate(date) {
const dateIsOnOrAfterTripStart = date >= this.startDate;
const dateIsOnOrBeforeTripEnd = date <= this.endDate;
return dateIsOnOrAfterTripStart &&
(this.isCurrentTrip || dateIsOnOrBeforeTripEnd);
}
}
QTripCodeHistory.INSTANCE = new QTripCodeHistory();
/* globals $, EightKun, ResearchBread, BakerWindow */
/* exported SpamFader, NameFagStrategy, HighPostCountFagStrategy,
* FloodFagStrategy, BreadShitterFagStrategy */
/**
* Fade posts that post too fast
*/
class SpamFader {
/**
* Construct spamfader
* @param {Array} spamDetectionStrategies An array of SpamDetectionStrategy's
*/
constructor(spamDetectionStrategies) {
this.spamDetectionStrategies = spamDetectionStrategies;
this.styleId = 'bakertools-spamfader-style';
this.spamClass = 'bakertools-spamfader-spam';
this.disableSpamFaderId = 'bakertools-spamfader-disable';
this.hideSpamBadgesId = 'bakertools-spamfader-hide-spam-badges';
this._createStyles();
this._setupBakerWindowControls();
this._readSettings();
this._spamFadeExistingPosts();
this._setupListeners();
}
/**
* Create stylesheets
*/
_createStyles() {
$('head').append(`
`);
}
/**
* Setup settings UI for spamfading
*/
_setupBakerWindowControls() {
window.bakerTools.mainWindow.addSpamOption(`
`);
}
/**
* Loop through posts for spam
*/
_spamFadeExistingPosts() {
$(EightKun.POST_REPLY_SELECTOR).each(function(i, post) {
this._detectSpam(post);
}.bind(this));
}
/**
* Determine if provided post is spam, if so, add spam class
*
* @param {Element} post div.post
*/
_detectSpam(post) {
const posterStats = SpamFader.getPosterStats(post);
posterStats.addPost(post);
if (SpamFader.isMarkedAsNotSpam(posterStats)) {
return;
}
this.spamDetectionStrategies.forEach((sds) => sds.isSpam(post));
this._takeSpamAction(posterStats);
}
/**
* Performs the spam action against the poster's posts.
* @param {PosterStats} posterStats
*/
_takeSpamAction(posterStats) {
if (!posterStats.isSpam) {
return;
}
if (this.spamAction === SpamFader.FADE) {
const opacity =
Math.max(SpamFader.MIN_OPACITY, (1 - posterStats.fadeProgress));
posterStats.posts.forEach(function(p) {
$(p).css('opacity', opacity);
$(p).off('mouseenter mouseleave');
$(p).hover(function() {
$(p).animate({opacity: 1.0}, SpamFader.ANIMATION_DURATION);
}, function() {
$(p).animate({opacity: opacity}, SpamFader.ANIMATION_DURATION);
});
});
} else if (this.spamAction === SpamFader.HIDE) {
posterStats.posts.forEach(function(p) {
EightKun.hidePost(p);
});
}
}
/**
* Setup new post listener
*/
_setupListeners() {
this._setupNewPostListener();
$(`#${this.disableSpamFaderId}`).change(function(e) {
this.disableSpamFader(e.target.checked);
}.bind(this));
$(`#${this.hideSpamBadgesId}`).change(function(e) {
this.hideSpamBadges(e.target.checked);
}.bind(this));
}
/**
* Setup listener to check new posts for spam
*/
_setupNewPostListener() {
$(document).on(EightKun.NEW_POST_EVENT, function(e, post) {
this._detectSpam(post);
}.bind(this));
}
/**
* Hide the actions of the spamfader.
* @param {boolean} disable
*/
disableSpamFader(disable) {
$('#'+this.disableSpamFaderId).prop('checked', disable);
localStorage.setItem(SpamFader.DISABLE_SPAM_FADER_SETTING, disable);
if (disable) {
$(SpamFader.SPAM_BADGES_SELECTOR).hide();
$(SpamFader.NOT_SPAM_BUTTON_SELECTOR).hide();
$(EightKun.POST_REPLY_SELECTOR)
.css({'opacity': ''})
.off('mouseenter mouseleave');
$(document).off(EightKun.NEW_POST_EVENT);
} else {
$(SpamFader.SPAM_BADGES_SELECTOR).show();
$(SpamFader.NOT_SPAM_BUTTON_SELECTOR).show();
SpamFader.posterStats.forEach(this._takeSpamAction.bind(this));
this._setupNewPostListener();
}
}
/**
* Hide spam badges on posts
* @param {boolean} hide
*/
hideSpamBadges(hide) {
$('#'+this.hideSpamBadgesId).prop('checked', hide);
localStorage.setItem(SpamFader.HIDE_SPAM_BADGES_SETTING, hide);
if (hide) {
$(SpamFader.SPAM_BADGES_SELECTOR).hide();
} else {
$(SpamFader.SPAM_BADGES_SELECTOR).show();
}
}
/**
* Read spamfader settings
*/
_readSettings() {
this.spamAction = localStorage[SpamFader.SPAM_ACTION_SETTING] ||
SpamFader.FADE;
this.hideSpamBadges(JSON.parse(
localStorage.getItem(
SpamFader.HIDE_SPAM_BADGES_SETTING),
));
this.disableSpamFader(JSON.parse(
localStorage.getItem(
SpamFader.DISABLE_SPAM_FADER_SETTING),
));
}
/**
* Get post stats for post
* @param {Element} post div.post
* @return {PosterStats}
*/
static getPosterStats(post) {
const posterId = EightKun.getPosterId(post);
if (!SpamFader.posterStats.has(posterId)) {
SpamFader.posterStats.set(posterId, new PosterStats(posterId));
}
return SpamFader.posterStats.get(posterId);
}
/**
* Adds spam badge to the posts by the poster.
* Wear them proudly fag!
*
* @param {PosterStats} posterStats The posterStats object representing
* spam fag
* @param {string} badge Font-Awesome glyph for badge
* @param {string} badgeTitle The title describing the badge
*/
static addSpamBadge(posterStats, badge, badgeTitle) {
posterStats.posts.forEach(function(post) {
if (!$(post).find(SpamFader.SPAM_BADGES_SELECTOR).length) {
SpamFader.createSpamBadgeSection(post);
}
const alreadyHasBadge = $(post)
.find(SpamFader.SPAM_BADGES_SELECTOR)
.find(`.fa-${badge}`).length;
if (!alreadyHasBadge) {
$(post).find(SpamFader.SPAM_BADGES_SELECTOR).append(
``,
);
}
});
}
/**
* Create section for spam badges
* @param {Element} post div.post
*/
static createSpamBadgeSection(post) {
const $postModifiedSection = $(post).find(EightKun.POST_MODIFIED_SELECTOR);
const button = $(`
`);
button.click(function(e) {
e.preventDefault();
SpamFader.markNotSpam(post);
});
button.appendTo($postModifiedSection);
$postModifiedSection.append(`
Spam Badges:`);
}
/**
* Mark poster as not spam.
*
* @param {Element} post div.post
*/
static markNotSpam(post) {
const stats = SpamFader.getPosterStats(post);
stats.markNotSpam();
stats.posts.forEach(function(p) {
$(p).css('opacity', 1);
$(p).off('mouseenter mouseleave');
$(p).find(SpamFader.SPAM_BADGES_SELECTOR).remove();
$(p).find(`.${SpamFader.NOT_SPAM_BUTTON_CLASS}`).remove();
});
SpamFader.addToNotSpamList(stats);
}
/**
* Save not spam in localstorage
* @param {PosterStats} posterStats
*/
static addToNotSpamList(posterStats) {
const threadId = EightKun.getThreadId();
const notSpamList = SpamFader.getNotSpamList();
if (!(threadId in notSpamList)) {
notSpamList[threadId] = [];
}
if (!SpamFader.isMarkedAsNotSpam(posterStats)) {
notSpamList[threadId].push(posterStats.posterId);
localStorage.setItem(SpamFader.NOT_SPAM_SETTING,
JSON.stringify(notSpamList));
}
}
/**
* Has this poster been marked as not spam?
* @param {PosterStats} posterStats
* @return {boolean} true if not spam
*/
static isMarkedAsNotSpam(posterStats) {
const threadId = EightKun.getThreadId();
const notSpamList = SpamFader.getNotSpamList();
return threadId in notSpamList &&
notSpamList[threadId].includes(posterStats.posterId);
}
/**
* Get not spam list from localStorage
* @return {Array} map of thread to not spam poster ids
*/
static getNotSpamList() {
return JSON.parse(
localStorage.getItem(SpamFader.NOT_SPAM_SETTING) || '{}',
);
}
}
SpamFader.posterStats = new Map();
SpamFader.FADE = 'fade';
SpamFader.HIDE = 'hide';
SpamFader.SPAM_BADGES_CLASS = 'bakertools-spam-badges';
SpamFader.SPAM_BADGES_SELECTOR = `.${SpamFader.SPAM_BADGES_CLASS}`;
SpamFader.MIN_OPACITY = .2;
SpamFader.ANIMATION_DURATION = 200; // milliseconds
SpamFader.SPAM_ACTION_SETTING = 'bakertools-spamfader-action';
SpamFader.HIDE_SPAM_BADGES_SETTING = 'bakertools-spamfader-hide-badges';
SpamFader.DISABLE_SPAM_FADER_SETTING = 'bakertools-spamfader-disable';
SpamFader.NOT_SPAM_SETTING = 'bakertools-spamfader-notspam';
SpamFader.NOT_SPAM_BUTTON_CLASS = 'bakertools-spamfader-notspam';
SpamFader.NOT_SPAM_BUTTON_SELECTOR = `.${SpamFader.NOT_SPAM_BUTTON_CLASS}`;
/**
* Holds spam stats
*/
class PosterStats {
/**
* Construct poststats for post
* @param {number} posterId id of poster
*/
constructor(posterId) {
this.posts = [];
this.posterId = posterId;
this.markNotSpam();
}
/**
* Reset spam indicators
*/
markNotSpam() {
this._spamCertainty = 0;
this._fadeProgress = 0;
this.floodCount = 0;
this.breadShitCount = 0;
this.isBreadShitter = false;
}
/**
* Add post to poster's list of post
* @param {Element} post div.post
*/
addPost(post) {
if (!this.posts.includes(post)) {
this.posts.push(post);
}
}
/**
* Set spam certainty property
* @param {number} certainty
*/
set spamCertainty(certainty) {
if (certainty > this._spamCertainty) {
this._spamCertainty = certainty;
}
}
/**
* Get spam spamCertainty
* @return {number} 1 represents 100% certainty.
*/
get spamCertainty() {
return this._spamCertainty;
}
/**
* Set fade progress property
* @param {number} progress
*/
set fadeProgress(progress) {
if (progress > this._fadeProgress) {
this._fadeProgress = progress;
}
}
/**
* Get spam fade progress
* @return {number} 1 represents 100% progress.
*/
get fadeProgress() {
return this._fadeProgress;
}
/**
* Number of posts by id
* @return {number}
*/
get postCount() {
return this.posts.length;
}
/**
* Is this post spam?
* @return {boolean} true if spam
*/
get isSpam() {
return this._spamCertainty >= 1;
}
}
/**
* Base class for spamDetectionStrategies
*/
class SpamDetectionStrategy {
/**
* Determine if the provided post is spam
* @param {Element} post div.post
* @return {boolean} true if is spam
*/
isSpam(post) {
return false;
}
}
/**
* Marks namefags as spam
*/
class NameFagStrategy extends SpamDetectionStrategy {
/**
* Construct NameFagStrategy
*/
constructor() {
super();
this.nameRegex = /^Anonymous( \(You\))?$/;
this.badge = 'tag';
this.badgeTitle = 'Namefag';
}
/**
* Returns true if a namefag, sets spamCertainty to 100% for post
* to begin fading
* @param {Element} post div.post
* @return {boolean} true if is namefag spam
*/
isSpam(post) {
const isNameFag = !window.bakerTools.qPostHighlighter.isQ(post) &&
(
!this.nameRegex.test(EightKun.getPostName(post)) ||
EightKun.getPostTrip(post) != ''
);
if (isNameFag) {
const stats = SpamFader.getPosterStats(post);
stats.spamCertainty = 1;
stats.fadeProgress = .2;
stats.isNameFag = true;
SpamFader.addSpamBadge(stats, this.badge, this.badgeTitle);
}
return isNameFag;
}
}
/**
* Marks floodfags with high post count as spam
*/
class HighPostCountFagStrategy extends SpamDetectionStrategy {
/**
* Construct HighPostCountFagStrategy
*/
constructor() {
super();
this.postCountSpamThreshold = 15;
this.postCountHideThreshold = 25;
this.badge = 'bullhorn';
this.badgeTitle = 'High Post Count Fag';
}
/**
* Returns true if the poster has posted more than the threshold
* @param {Element} post div.post
* @return {boolean} true if spam
*/
isSpam(post) {
if (EightKun.isPostFromOp(post)) {
return;
}
const posterStats = SpamFader.getPosterStats(post);
const highCountSpamCertainty =
Math.min(1, posterStats.postCount / this.postCountSpamThreshold);
posterStats.spamCertainty = highCountSpamCertainty;
if (highCountSpamCertainty === 1) {
posterStats.isHighPostCountFag = true;
SpamFader.addSpamBadge(posterStats, this.badge, this.badgeTitle);
}
// We already hit spam threshold
// Either we have hit threshold count or some other strategy says its spam
if (posterStats.isSpam) {
// Number of posts needed past threshold to hide
const hideCount =
this.postCountHideThreshold - this.postCountSpamThreshold;
const progressIncrement = 1/hideCount;
posterStats.fadeProgress += progressIncrement;
}
return posterStats.isSpam;
}
}
/**
* Marks floodfags with quick succession posts as spam
*/
class FloodFagStrategy extends SpamDetectionStrategy {
/**
* Construct flood fag strategy
*/
constructor() {
super();
this.postIntervalConsideredFlooding = 60; // seconds
this.floodCountSpamThreshold = 5;
this.floodCountHideThreshold = 10;
this.badge = 'tint';
this.badgeTitle = 'Floodfag';
}
/**
* Returns true if a spam
* @param {Element} post div.post
* @return {boolean} true if is spam
*/
isSpam(post) {
const posterStats = SpamFader.getPosterStats(post);
if (EightKun.isPostFromOp(post) || !this.isPostFlooded(posterStats)) {
return;
}
posterStats.floodCount++;
const floodSpamCertainty =
Math.min(1, posterStats.floodCount / this.floodCountSpamThreshold);
posterStats.spamCertainty = floodSpamCertainty;
if (floodSpamCertainty === 1) {
posterStats.isFloodFag = true;
SpamFader.addSpamBadge(posterStats, this.badge, this.badgeTitle);
}
// We already hit spam threshold
// Either we have hit threshold count or some other strategy says its spam
if (posterStats.isSpam) {
// Number of posts needed past threshold to hide
const hideCount =
this.floodCountHideThreshold - this.floodCountSpamThreshold;
const progressIncrement = 1/hideCount;
posterStats.fadeProgress += progressIncrement;
}
return posterStats.isSpam;
}
/**
* Is this a flooded post?
* @param {PosterStats} posterStats
* @return {boolean} true if flooded
*/
isPostFlooded(posterStats) {
if (posterStats.posts.length <= 1) {
return false;
}
const currentPost = posterStats.posts.slice(-1)[0];
const previousPost = posterStats.posts.slice(-2)[0];
const previousPostTime = EightKun.getPostTime(previousPost);
const currentPostTime = EightKun.getPostTime(currentPost);
return (currentPostTime - previousPostTime) <=
this.postIntervalConsideredFlooding;
}
}
/**
* Marks breadshitters as spam
*/
class BreadShitterFagStrategy extends SpamDetectionStrategy {
// TODO: dont check for bread shitting on non research thread?
/**
* Construct flood fag strategy
*/
constructor() {
super();
// Let's go easy, maybe its a newfag?
this.breadShittingIncrement = .1;
this.badge = 'clock-o';
this.badgeTitle = 'Bread shitter';
}
/**
* Returns true if a spam
* @param {Element} post div.post
* @return {boolean} true if is spam
*/
isSpam(post) {
const posterStats = SpamFader.getPosterStats(post);
if (EightKun.isPostFromOp(post) || !this.isBreadShitter(post)) {
return;
}
posterStats.breadShitCount++;
posterStats.isBreadShitter = true;
SpamFader.addSpamBadge(posterStats, this.badge, this.badgeTitle);
posterStats.spamCertainty = 1;
posterStats.fadeProgress += this.breadShittingIncrement;
return posterStats.isSpam;
}
/**
* Is this a bread shitting post?
* @param {Element} post div.post
* @return {boolean} true if bread shitter
*/
isBreadShitter(post) {
const dough = ResearchBread.getDoughPost();
const doughTime = EightKun.getPostTime(dough);
const postTime = EightKun.getPostTime(post);
return postTime <= doughTime;
}
}
/* exported StatsOverlay */
/* global $, EightKun, QPostHighlighter, NotablePost, debounce */
/**
* Overlays bread stats (and some other controls) in the bottom right of the
* screen.
*/
class StatsOverlay {
/**
* Construct statsoverlay, html element, setup listeners
*/
constructor() {
this.id = 'bakertools-stats-overlay';
this.maxPosts = 750;
this.postCountId = 'bakertools-stats-post-count';
this.userCountId = 'bakertools-stats-uid-count';
this.qCountId = 'bakertools-stats-q-count';
this.notableCountId = 'bakertools-stats-notable-count';
this._createStyles();
this._createElement();
this._updateStats();
$(document).on(EightKun.NEW_POST_EVENT, function(e, post) {
this._updateStats();
}.bind(this));
}
/**
* Create styles for stats overlay
*/
_createStyles() {
const sheet = window.document.styleSheets[0];
sheet.insertRule(`#${this.id} {
padding: 5px;
position: fixed;
z-index: 100;
float: right;
right:28.25px;
bottom: 28.25px;
}`, sheet.cssRules.length);
}
/**
* Create actual html element for style overlay
*/
_createElement() {
this.element = document.createElement('div');
this.element.id = this.id;
this.$goToLast = $(`
`);
this.saveLastReadingLocation = debounce(this.saveLastReadingLocation, 450);
this.currentReadingLocation = $(window).scrollTop();
$(window).scroll(function() {
this.saveLastReadingLocation();
}.bind(this));
this.$goToLast.click(function() {
$(window).scrollTop(this.lastReadingLocation);
}.bind(this));
$(this.element).append( `
Posts:
UIDS:
`);
$(this.element).append(this.$goToLast);
$(this.element).append(`
Q's:
Notables:
`);
document.body.appendChild(this.element);
this._setPostCount($('div.post.reply').length);
}
/**
* Save the last spot before scrolling or navigation
*/
saveLastReadingLocation() {
const scrollDistance = Math.abs(
this.currentReadingLocation - $(window).scrollTop());
const scrolledMoreThanThirdScreenHeight =
scrollDistance > (window.innerHeight / 3);
if (!scrolledMoreThanThirdScreenHeight) {
return;
}
this.lastReadingLocation = this.currentReadingLocation;
this.currentReadingLocation = $(window).scrollTop();
}
/**
* Update the stats fields
*/
_updateStats() {
const postCount = $('#thread_stats_posts').text();
if (postCount) {
this._setPostCount(postCount);
}
// TODO: uids dont load at first load.
$('#'+this.userCountId).text($('#thread_stats_uids').text() || '0');
$('#'+this.qCountId).text(QPostHighlighter.qPosts.length);
$('#'+this.notableCountId).text(NotablePost.getNotables().length);
}
/**
* Set post count in overlay
* @param {number} count
*/
_setPostCount(count) {
const progress = count/this.maxPosts;
let postColor = 'green';
if (progress >= .87) { // ~ 650 posts (100 posts left)
postColor = 'red';
} else if (progress >= .5) {
postColor = 'goldenrod';
}
$('#'+this.postCountId).text(count).css({'color': postColor});
}
} // End StatsOverlay class
/* global $, NavigationControl, EightKun, ResearchBread,
ColorPicker, POST_BACKGROUND_CHANGE_EVENT, BakerWindow */
/**
* Highlight posts that (you)
* Adds (You) navigation links to baker window
*/
class YouHighlighter {
/**
* Construct YN object
*/
constructor() {
this.styleId = 'bakertools-you-styles';
this.yous = [];
this.ownPosts = [];
this.showYouNavigationInBoardListId =
'bakertools-show-you-nav-in-boardlist';
this.showOwnNavigationInBoardListId =
'bakertools-show-own-nav-in-boardlist';
this._createStyles();
this._setupBakerWindowControls();
this._readSettings();
this._initOwnAndYouPosts();
this._setupListeners();
}
/**
* Create styles
*/
_createStyles() {
$('head').append(`
`);
}
/**
* Read settings from localStorage
*/
_readSettings() {
this.showYouNavigationInBoardList(JSON.parse(
localStorage.getItem(
YouHighlighter.SHOW_YOU_NAV_IN_BOARDLIST_SETTING),
));
this.showOwnNavigationInBoardList(JSON.parse(
localStorage.getItem(
YouHighlighter.SHOW_OWN_NAV_IN_BOARDLIST_SETTING),
));
}
/**
* Add (you) navigation to bakerwindow
*/
_setupBakerWindowControls() {
const youColorPicker = new ColorPicker(
'(You) Post Color',
'Set background color of posts replying to (you)',
YouHighlighter.YOU_COLOR_SETTING,
YouHighlighter.DEFAULT_YOU_COLOR,
(color) => this.youColor = color,
);
const ownPostColorPicker = new ColorPicker(
'Own Post Color',
'Set background color your own posts',
YouHighlighter.OWN_COLOR_SETTING,
YouHighlighter.DEFAULT_OWN_COLOR,
(color) => this.ownPostColor = color,
);
window.bakerTools.mainWindow.addColorOption(ownPostColorPicker.element);
window.bakerTools.mainWindow.addColorOption(youColorPicker.element);
this.ownNavigation = new NavigationControl('Own Posts',
this.getOwnPosts.bind(this), YouHighlighter.NEW_OWN_POST_EVENT);
this.ownBoardListNav = new NavigationControl('Own',
this.getOwnPosts.bind(this), YouHighlighter.NEW_OWN_POST_EVENT);
$(EightKun.getTopBoardlist()).append(this.ownBoardListNav.element);
$(this.ownBoardListNav.element).hide();
window.bakerTools.mainWindow.addNavigationOption(`
`);
window.bakerTools.mainWindow.addNavigation(this.ownNavigation.element);
this.youNavigation = new NavigationControl(`(You)'s`,
this.getYous.bind(this), YouHighlighter.NEW_YOU_POST_EVENT);
this.youBoardListNav = new NavigationControl('You',
this.getYous.bind(this), YouHighlighter.NEW_YOU_POST_EVENT);
$(EightKun.getTopBoardlist()).append(this.youBoardListNav.element);
$(this.youBoardListNav.element).hide();
window.bakerTools.mainWindow.addNavigationOption(`
`);
window.bakerTools.mainWindow.addNavigation(this.youNavigation.element);
}
/**
* Set the background color of posts replying to (you)
* @param {string} color A valid css color value.
* E.G. ('#ff00ee', 'rgba()' or 'blue')
*/
set youColor(color) {
this._youColor = color;
document.getElementById(this.styleId)
.sheet.cssRules[0].style.background = color;
$(document).trigger(POST_BACKGROUND_CHANGE_EVENT, color);
}
/**
* Get background color for posts replying to (you)
*/
get youColor() {
return this._youColor;
}
/**
* Set the background color of your own posts
* @param {string} color A valid css color value.
* E.G. ('#ff00ee', 'rgba()' or 'blue')
*/
set ownPostColor(color) {
this._ownPostColor = color;
document.getElementById(this.styleId)
.sheet.cssRules[1].style.background = color;
$(document).trigger(POST_BACKGROUND_CHANGE_EVENT, color);
}
/**
* Get background color for your own posts
*/
get ownPostColor() {
return this._ownPostColor;
}
/**
* Setup listeners for baker window controls
*/
_setupListeners() {
$('#'+this.showOwnNavigationInBoardListId).change(function(e) {
this.showOwnNavigationInBoardList(e.target.checked);
}.bind(this));
$('#'+this.showYouNavigationInBoardListId).change(function(e) {
this.showYouNavigationInBoardList(e.target.checked);
}.bind(this));
$(document).on(EightKun.NEW_POST_EVENT, function(e, post) {
if (this.isAYou(post)) {
this._addYouPost(post);
}
if (this.isOwnPost(post)) {
this._addOwnPost(post);
}
}.bind(this));
}
/**
* Show/hide you nav in boardlist
*
* @param {boolean} showYouNavInBoardList
*/
showYouNavigationInBoardList(showYouNavInBoardList) {
$('#'+this.showYouNavigationInBoardListId).prop('checked',
showYouNavInBoardList);
localStorage.setItem(YouHighlighter.SHOW_YOU_NAV_IN_BOARDLIST_SETTING,
showYouNavInBoardList);
if (showYouNavInBoardList) {
$(this.youBoardListNav.element).show();
} else {
$(this.youBoardListNav.element).hide();
}
}
/**
* Show/hide own nav in boardlist
*
* @param {boolean} showOwnNavInBoardList
*/
showOwnNavigationInBoardList(showOwnNavInBoardList) {
$('#'+this.showOwnNavigationInBoardListId).prop('checked',
showOwnNavInBoardList);
localStorage.setItem(YouHighlighter.SHOW_OWN_NAV_IN_BOARDLIST_SETTING,
showOwnNavInBoardList);
if (showOwnNavInBoardList) {
$(this.ownBoardListNav.element).show();
} else {
$(this.ownBoardListNav.element).hide();
}
}
/**
* Get (You)'s
* @return {Array} of div.post
*/
getYous() {
return this.yous;
}
/**
* Is the post replying to you
* @param {Element} post div.post
* @return {boolean} True if post is replying to you
*/
isAYou(post) {
return post.querySelector('.body')
.innerHTML
.indexOf('(You)') != -1;
}
/**
* Is this your own post
* @param {Element} post div.post
* @return {boolean} True if post is you
*/
isOwnPost(post) {
return $(post).hasClass('you');
}
/**
* Add you post and trigger event
* @param {Element} post
*/
_addYouPost(post) {
this.yous.push(post);
$(post).addClass(YouHighlighter.YOU_CLASS);
$(document).trigger(YouHighlighter.NEW_YOU_POST_EVENT, post);
}
/**
* Add own post and trigger event
* @param {Element} post
*/
_addOwnPost(post) {
this.ownPosts.push(post);
$(post).addClass(YouHighlighter.OWN_CLASS);
$(document).trigger(YouHighlighter.NEW_OWN_POST_EVENT, post);
}
/**
* Get own and you posts that are present at page load
*/
_initOwnAndYouPosts() {
const ownPosts = JSON.parse(localStorage.own_posts || '{}');
const board = ResearchBread.BOARD_NAME;
$('div.post').each(function(i, post) {
const postId = $(post).attr('id').split('_')[1];
if (ownPosts[board] &&
ownPosts[board].indexOf(postId) !== -1) {
this._addOwnPost(post);
}
EightKun.getReplyLinksFromPost(post).each(function(i, link) {
const youPostId = EightKun.getPostNumberFromReplyLink(link);
if (ownPosts[board] && ownPosts[board].indexOf(youPostId) !== -1) {
this._addYouPost(post);
}
}.bind(this));
}.bind(this));
window.bakerTools.scrollBar.addPosts(this.ownPosts);
window.bakerTools.scrollBar.addPosts(this.yous);
}
/**
* Get own posts
* @return {Array} of div.post
*/
getOwnPosts() {
return this.ownPosts;
}
}
YouHighlighter.SHOW_YOU_NAV_IN_BOARDLIST_SETTING =
'bakertools-show-you-nav-in-boardlist';
YouHighlighter.SHOW_OWN_NAV_IN_BOARDLIST_SETTING =
'bakertools-show-own-nav-in-boardlist';
YouHighlighter.NEW_YOU_POST_EVENT =
'bakertools-new-you-post-event';
YouHighlighter.NEW_OWN_POST_EVENT =
'bakertools-new-own-post-event';
YouHighlighter.YOU_CLASS = 'bakertools-you-post';
YouHighlighter.OWN_CLASS = 'bakertools-own-post';
YouHighlighter.OWN_COLOR_SETTING =
'bakertools-own-post-color';
YouHighlighter.YOU_COLOR_SETTING =
'bakertools-you-post-color';
YouHighlighter.DEFAULT_OWN_COLOR = '#F8D2D2';
YouHighlighter.DEFAULT_YOU_COLOR = '#E1B3DA';
/* global ActivePage, $, QPostHighlighter, YouHighlighter, StatsOverlay,
NotableHighlighter, BakerWindow, BlurImages, PreviousBreadHighlighter,
NominatePostButtons, BreadList, ScrollbarNavigation, NotablePost,
ImageBlacklist, PostRateChart, SpamFader */
/**
* MAIN
*/
if (ActivePage.isThread()) { // Only setup the tools if we are on a thread
$(document).ready(function() {
console.info('Thanks for using bakertools! For God and Country! WWG1WGA');
window.bakerTools = {};
window.bakerTools.mainWindow = new BakerWindow();
window.bakerTools.scrollBar = new ScrollbarNavigation([
NotablePost.NEW_NOTABLE_POST_EVENT,
YouHighlighter.NEW_OWN_POST_EVENT,
YouHighlighter.NEW_YOU_POST_EVENT,
QPostHighlighter.NEW_Q_POST_EVENT,
]);
new BlurImages();
window.bakerTools.PreviousBreadHighlighter =
new PreviousBreadHighlighter();
window.bakerTools.qPostHighlighter = new QPostHighlighter();
window.bakerTools.notableHighlighter = new NotableHighlighter();
window.bakerTools.youHighlighter = new YouHighlighter();
window.bakerTools.statsOverlay = new StatsOverlay();
new NominatePostButtons();
new BreadList();
new ImageBlacklist();
new PostRateChart();
new SpamFader([new NameFagStrategy(), new HighPostCountFagStrategy(),
new FloodFagStrategy(), new BreadShitterFagStrategy()]);
});
}
}(window.jQuery));