// ==UserScript==
// @name Reddit++
// @name:ru Reddit++
// @namespace RedditPlusPlus
// @license CC-BY-SA-4.0
// @version 0.1.9
// @description Improved experience for reddit.com
// @description:ru Улучшение интерфейса reddit.com
// @author lnm95
// @match *://www.reddit.com/*
// @icon data:image/gif;base64,AAABAAEAICAAAAEAIACoEAAAFgAAACgAAAAgAAAAQAAAAAEAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEf/NABE//QARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//QBE/+8ARP/MAEP/ngBF/1wAIv8QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARf9OAEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//gAR/+gAEj/JQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARf9/AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf/7AFP/kABI/woAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARf9/AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX/1wBi/yQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQv+YAEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX/6wA9/zQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACL/JABF/+sARf//AEX//wBF//8ARf//AEX//wBF//8BRf//E1L9/yVe/P8xZ/v/N2r6/zdq+v8wZvr/Jl77/xNR/f8BRv7/AEX//wBF//8ARf//AEX//wBF//8ARf//AEX/6wAj/yIAAAAAAAAAAAAAAAAAAAAAAAAAAABI/woARf/XAEX//wBF//8ARf//AEX//wBF//8GSf3/K2H3/0l28v9Re+//UXru/1B67f9Qee3/UHnt/1B67f9Qeu7/UXrv/0p28f8pYPf/Bkn9/wBF//8ARf//AEX//wBF//8ARf//AEX/2ABG/wgAAAAAAAAAAAAAAAAAAAAAAFP/jwBF//8ARf//AEX//wBF//8ARf//HFf4/0p17f9Qeez/UHns/1B56/9Qeez/UHrs/1J87/9SfO//UHrt/1B57P9Qeez/UHnr/1B57P9Lde3/HVj4/wBF//8ARf//AEX//wBF//8ARf//AEX/kQAAAAAAAAAAAAAAAABH/yUARf/7AEX//wBF//8ARf//AEX//ydf9f9Qeev/UHrs/1F77v9TfO//U37w/0Zx5f8iTcD/Ejyt/xE7rP8hTL//RXDk/1R+8f9TffD/Unzv/1F77f9Qeez/KF/1/wBF//8ARf//AEX//wBF//8ARf/5AEX/LAAAAAAAAAAAAEP/oABF//8ARf//AEX//wBF//8dWPj/UXvt/1N98P9Uf/P/VoH0/1aB9f9CbuL/CTSo/wgzpv8JNKf/CTSn/wczpv8JNKj/QW3i/1aC9v9WgfT/VYDz/1R/8f9Sfe//H1n4/wBF//8ARf//AEX//wBF//8ARf+gAAAAAAAi/xAARf/4AEX//wBF//8ARf//BEj+/0168v9VgPP/VoL1/1eC9v9Yg/j/WIP5/xA8sv8DLqT/BC+k/wUwpf8FMKX/BC+k/wMupP8PO7D/WIT5/1iD+P9Xg/f/V4L2/1aB9P9Oe/P/BEj+/wBF//8ARf//AEX//wBF//cARf8QAEX/XABF//8ARf//AEX//wBF//8fW/v/VoL1/1eD9/9Yg/j/x83h/+fi2P/Fyt3/VH/0/0x37P9Ic+j/RnHm/0Vx5v9Ic+j/THjt/1R/9P9ahvv/yM7h/+fi2P/Fytz/WIP3/1eC9v8hXPv/AEX//wBF//8ARf//AEX//wBF/10ARf+eAEX//wBF//8ARf//AEX//zRp+v9Yg/j/WYT6/1mF+//x7un/7+vk/+Tf1P9Le/3/W4f+/1uH/v9ch/7/XIf+/1uH/v9bh/7/TX3+/x5e/v/x7un/7+vk/+Tf1P9Zhfr/WIT5/zZr+v8ARf//AEX//wBF//8ARf//AEX/nQBF/8wARf//AEX//wBF//8XVPr/T3z2/1mF+v9ahvv/W4b8//Xy7v/y8Or/5d/U/xZV/v9ciP//XIj//1yI//9ciP//XIj//1yI//8YVv3/AE7///Xy7v/y8Or/5d/U/1qG/P9Zhfv/T3z3/xZU+f8ARf//AEX//wBF//8ARf/QAET97wBF//8ARf//EVD7/1B77/9hifT/nbHp/6G06/+lufL/+Pbz//f18f/r597/eJXn/56w5f+esOX/g6Dw/1yI//9mjvz/nrLr/3yY6v94mvL/+Pbz//f18f/r597/n7Lo/52w5P+breH/eZbk/xBQ+/8ARf//AEX//wBE/u0ARf/9AEX//wBF//84bPj/WIP4/6O49P/y7+n/8/Dr//f28v/8/Pr//Pv5//b07//s6OH/5uHX/+Xf1f/j39f/XIj//6S69v/y7+n/8/Dr//f28v/8/Pr//Pv5//b07//s6OH/5uHX/+Xf1f/j3tf/OW34/wBF//8ARf//AEX//gBF//4ARf//AEX//0d4+/9bh/3/qL/8//z8+v/8/Pr//f38//7+/f/+/vz/+/r3//b07//z8Ov/8/Hr//Hw7/9ciP//qL/9//z8+v/8/Pr//f38//7+/f/+/vz/+/r3//b07//z8Ov/8/Hr//Hw7/9IePv/AEX//wBF//8ARf/9AEX97QBF//8ARf//PXH9/1yI/v9nkP//qcD+/63D/v+tw/7//v79//z7+f/18u7/p7v0/6e89P+qv/n/i6r9/1yI//9nkP//qcD+/63D/v+tw/7//v79//z7+f/18u7/p7v0/6e89P+qv/n/i6r9/z5y/f8ARf//AEX//wBF/u4ARf/QAEX//wBF//8WVf7/W4f//1yI//9ciP//XIj//1yI///+/vz/+Pbz/+rl3P9ciP//XIj//1yI//9ciP//XIj//1yI//9ciP//XIj//1yI///+/vz/+Pbz/+rl3P9ciP//XIj//1yI//9bh///FlX+/wBF//8ARf//AET/zQBG/50ARf//AEX//wBF//8iXv//VoT//1yI//9ciP//U4H///39/P/18+7/5eDV/1yI//9ciP//XIj//1yI//9ciP//XIj//1yI//9ciP//XIj///39/P/18+7/5eDV/1yI//9ciP//V4T//yFd//8ARf//AEX//wBF//8ARf+eAET/XQBF//8ARf//AEX//wBF//8ESP//FlX//xRT//8CR///9/n8//b08P/j39r/UoH//1yI//9ciP//XIj//1yI//9ciP//XIj//1KB//82bP//9/n8//b08P/i39r/FVT//xZV//8ESP//AEX//wBF//8ARf//AEX//wBF/1wARf8QAEX/9wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//9LfP7/fZ76/0d29/8ARf//Ckz//xdW//8UR87/G03T/xdW//8KTP//AEX//wBF//9LfP7/fZ76/0d29/8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf/4ACL/EAAAAAAARv+gAEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wU+2f8OPr3/AEX//wBF//8ARf//D0/7/yxi9v8nXvb/B0n9/wBF//8ARf//AEX//wBF//8ARf//AEX//wBH/58AAAAAAAAAAABG/ywARf/5AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AUT3/wo3rv8MRNv/AEX//xZT+v9RfPD/VX/y/1R/8v9JdvH/B0n9/wBF//8ARf//AEX//wBF//8ARf/7AEf/JQAAAAAAAAAAAAAAAABF/5EARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//A0Hn/wo3rf8OPr//JlTQ/1J+8/9ahvz/Wob8/1mF+f8lX/v/AEX//wBF//8ARf//AEX//wBT/5AAAAAAAAAAAAAAAAAAAAAAAEj/CABF/9gARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AUT4/wU+1/8dS8j/Un70/1yI//9ciP7/XIj+/ypj/f8ARf//AEX//wBF//8ARf/XAEj/CgAAAAAAAAAAAAAAAAAAAAAAAAAAAKL/IgBF/+sARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//yJe/v9ciP//XIj//12J//9fiv//DE7//wBF//8ARf//AEX/6wBi/yQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD3/NABF/+sARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//x9c//8/c///PnL//xFS//8ARf//AEX//wBF/+sAPf80AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGL/JABF/9cARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf/YACP/IgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEP/CgBT/48ARf/7AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf/5AEX/kQBI/wgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABI/yUAR/+gAEX/+ABF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf/3AEX/oABG/ywAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAo/8QAEX/XABF/54ARf/MAET/7gBF//0ARf/+AEX/7QBF/88ARv+dAEX/XQBF/xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAD/4AAAP/AAAA/4AAAH/AAAA/gAAAHwAAAA8AAAAOAAAABgAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAGAAAABwAAAA8AAAAPgAAAH8AAAD/gAAB/8AAA//wAA///AA/8=
// @run-at document-end
// @grant unsafeWindow
// @grant GM_getValue
// @grant GM_setValue
// ==/UserScript==
(function() {
'use strict';
const REVISION = 5;
const pp_app = document.body.querySelector(`shreddit-app`);
if(pp_app == null || pp_app.getAttribute(`devicetype`) != `desktop`)
{
console.log(`Reddit++ was stopped for a non compatible page`);
return;
}
let pp_meta = document.head.querySelector(`meta[name="reddit-plus-plus"]`);
if(pp_meta != null)
{
console.log(`Reddit++ already runned`);
return;
}
pp_meta = document.createElement(`meta`);
pp_meta.setAttribute(`name`, `reddit-plus-plus`);
pp_meta.setAttribute(`rev`, REVISION);
document.head.append(pp_meta);
// ***********************************************************************************************************************
// ************************************************** CLASSES ************************************************************
// ***********************************************************************************************************************
class Database
{
constructor(name, isCleanupable) {
this.databaseKey = name+`_DATABASE`;
this.refreshKey = name+`_REFRESHED`;
this.cleanupKey = name+`_CLEANUP`;
this.isCleanupable = (isCleanupable != undefined && isCleanupable == true);
this.refresh();
// cleanup database
if(this.isCleanupable && GM_getValue(this.cleanupKey, 0) < Date.now())
{
// limit data storage for 1 day
const timestampLimit = Date.now() - (1000 * 60 * 60 * 24);
this.data = Object.fromEntries(Object.entries(this.data).filter(([key, value]) => value.timestamp > timestampLimit) );
this.refreshed = Date.now();
GM_setValue(this.databaseKey, this.data);
GM_setValue(this.refreshKey, this.refreshed);
GM_setValue(this.cleanupKey, Date.now() + (1000 * 60 * 10));
}
}
refresh()
{
const lastRefreshed = GM_getValue(this.refreshKey, 0);
if(this.data == undefined || this.refreshed < lastRefreshed)
{
this.refreshed = lastRefreshed;
this.data = GM_getValue(this.databaseKey, {});
}
}
get(id)
{
this.refresh();
const data = this.data[id];
return (data == undefined) ? {} : data;
}
isDisabled(id)
{
return this.get(id) == false;
}
isEnabled(id)
{
return !this.isDisabled(id);
}
set(id, value)
{
this.refresh();
if(this.isCleanupable)
{
value.timestamp = Date.now();
}
this.data[id] = value;
this.refreshed = Date.now();
GM_setValue(this.databaseKey, this.data);
GM_setValue(this.refreshKey, this.refreshed);
}
}
class CustomCSS
{
constructor()
{
this.stylesheet = new CSSStyleSheet();
this.registry(document);
}
registry(source)
{
source.adoptedStyleSheets.push(this.stylesheet);
}
addRule(rule)
{
this.stylesheet.insertRule(rule, 0);
}
addVar(name, lightValue, darkValue)
{
this.addRule(`:root.theme-light { ${name}: ${lightValue} !important;}`);
this.addRule(`:root { ${name}: ${darkValue ?? lightValue};}`);
}
}
class Window
{
constructor(tittle, render, onClose) {
this.tittle = tittle;
this.render = render;
this.onClose = onClose;
this.container = null;
this.content = null;
this.closeButton = null;
}
build()
{
this.container = document.createElement(`div`);
this.container.classList.add(`pp_WindowContainer`);
this.container.addEventListener('click', e => { if(e.target == this.container) { this.close(); }});
const win = AppendNew(this.container, `div`, `pp_Window`);
const tittleContainer = AppendNew(win, `div`, `pp_WindowTittleContainer`);
let tittle = AppendNew(tittleContainer, `div`, [`pp_WindowTittle`, `flex`, `flex-row`]);
tittle = AppendNew(tittle, `span`, [`text-24`, `font-semibold`]);
tittle.textContent = this.tittle;
this.closeButton = AppendNew(tittleContainer, `div`, [`pp_WindowCloseButton`, `flex`, `items-center`]);
this.closeButton = AppendNew(this.closeButton, `button`, [`button`, `icon`, `inline-flex`, `items-center`, `justify-center`, `button-small`, `button-secondary`, `px-[var(--rem6)]`]);
this.closeButton.setAttribute(`tittle`, `Close ${this.tittle}`);
this.closeButton.addEventListener('click', e => { this.close(); });
this.closeButton = AppendNew(this.closeButton, `span`, [`flex`, `items-center`, `justify-center`]);
this.closeButton = AppendNew(this.closeButton, `span`, [`flex`]);
const svg = buildSvg(20, 20, closeWindowGraphic, {c:'none'});
svg.setAttribute(`height`, `16`);
svg.setAttribute(`width`, `16`);
this.closeButton.append(svg);
AppendNew(win, `hr`, `border-b-neutral-border-weak`); //`border-0`, `border-b-sm`, `border-solid`, `border-b-neutral-border-weak`
this.content = AppendNew(win, `div`, `pp_WindowContent`);
AppendNew(win, `div`, `pp_WindowFooter`).textContent = ` `;
}
open(context)
{
if(this.container == null)
{
this.build();
}
while (this.content.firstChild) {
this.content.removeChild(this.content.lastChild);
}
this.render(this, context);
document.body.appendChild(this.container);
document.body.style.overflow = 'hidden';
}
close()
{
this.container.remove();
document.body.style.overflow = 'visible';
if(this.onClose != undefined)
{
this.onClose();
}
}
}
class ImageViewer
{
constructor()
{
this.openned = false;
this.viewer = null;
this.container = null;
this.image = null;
this.mouse = {x:0, y:0};
this.drag = { enabled:false, start: {x:0, y:0}, current: {x:0, y:0, scale:1} };
this.scrollImage = this.scrollImage.bind(this);
this.startDrag = this.startDrag.bind(this);
this.mouseMove = this.mouseMove.bind(this);
this.endDrag = this.endDrag.bind(this);
}
open(src)
{
if(this.openned) return;
this.openned = true;
if(this.viewer == null)
{
this.build();
}
this.image.src = src;
window.addEventListener('wheel', this.scrollImage, { passive: false });
this.image.addEventListener('mousedown', this.startDrag);
document.addEventListener('mousemove', this.mouseMove );
this.image.addEventListener('mouseup', this.endDrag);
this.image.addEventListener('mouseleave', this.endDrag);
// reset pos
this.drag.current = {x:0, y:0, scale:1};
this.updateTransform();
document.body.appendChild(this.viewer);
}
close()
{
this.viewer.remove();
this.drag.enabled = false;
this.container.classList.toggle(`pp_imageViewer_drag`, false);
window.removeEventListener('wheel', this.scrollImage, { passive: false });
this.image.removeEventListener('mousedown', this.startDrag);
document.removeEventListener('mousemove', this.mouseMove);
this.image.removeEventListener('mouseup', this.endDrag);
this.image.removeEventListener('mouseleave', this.endDrag);
this.openned = false;
}
build()
{
this.viewer = document.createElement(`div`);
this.viewer.classList.add(`pp_imageViewer`);
this.viewer.dataset.open = false;
const closeButton = AppendNew(this.viewer, `div`, `pp_imageViewer_closeButton`);
const closeSvg = buildSvg(40, 40, closeImageGraphic, {w:1});
closeButton.appendChild(closeSvg);
this.container = AppendNew(this.viewer, `div`, `pp_imageViewer_imageContainer`);
this.image = AppendNew(this.container, `img`, `pp_imageViewer_image`);
this.image.alt = `Comment image`;
this.image.ondragstart = function() { return false; };
// close
this.viewer.addEventListener('click', (e) => {
if(e.target != this.image)
{
this.close();
}
});
closeButton.addEventListener('click', () => { this.close(); });
}
updateTransform()
{
this.container.style.transform = `translate(${this.drag.current.x}px, ${this.drag.current.y}px) scale(${this.drag.current.scale}, ${this.drag.current.scale})`;
}
startDrag(event)
{
this.drag.start.x = event.screenX - this.drag.current.x;
this.drag.start.y = event.screenY - this.drag.current.y;
this.drag.enabled = true;
this.container.classList.toggle(`pp_imageViewer_drag`, true);
}
mouseMove(event)
{
this.mouse.x = event.clientX;
this.mouse.y = event.clientY;
if(this.drag.enabled)
{
this.drag.current.x = event.screenX - this.drag.start.x;
this.drag.current.y = event.screenY - this.drag.start.y;
this.updateTransform();
}
}
endDrag()
{
this.fit(1);
this.drag.enabled = false;
this.container.classList.toggle(`pp_imageViewer_drag`, false);
}
scrollImage(e)
{
const m = Math.max(1.0, 1.0 + Math.log2(this.drag.current.scale * this.drag.current.scale));
const prevScale = this.drag.current.scale;
this.drag.current.scale = Math.max(0.5, this.drag.current.scale + (-e.deltaY / 1000) * m );
const rect = this.image.getBoundingClientRect();
const hh = rect.height / 2;
const hw = rect.width / 2;
const dy = rect.y + hh;
const dx = rect.x + hw;
const os = ( this.drag.current.scale / prevScale - 1);
this.drag.current.y -= Math.min(Math.max(this.mouse.y - dy, -hh), hh) * os;
this.drag.current.x -= Math.min(Math.max(this.mouse.x - dx, -hw), hw) * os;
if(e.deltaY > 0)
{
this.drag.current.y /= 1.1;
this.drag.current.x /= 1.1;
}
this.fit(0.33);
e.preventDefault();
}
fit(force)
{
const offset = 0;
const rect = this.image.getBoundingClientRect();
const left = offset - rect.left;
const right = rect.right - window.innerWidth + offset;
if(left > 0 && right < 0)
{
this.drag.current.x += ((rect.width > window.innerWidth) ? -right : left) * force;
}
else if(left < 0 && right > 0)
{
this.drag.current.x += ((rect.width > window.innerWidth) ? left : -right) * force;
}
const top = offset - rect.top;
const bottom = rect.bottom - window.innerHeight + offset;
if(top > 0 && bottom < 0)
{
this.drag.current.y += ((rect.height > window.innerHeight) ? -bottom : top) * force;
}
else if(top < 0 && bottom > 0)
{
this.drag.current.y += ((rect.height > window.innerHeight) ? top : -bottom) * force;
}
this.updateTransform();
}
}
// ***********************************************************************************************************************
// **************************************************** VARS *************************************************************
// ***********************************************************************************************************************
const HOUR_SECONDS = 60 * 60;
const DAY_SECONDS = HOUR_SECONDS * 24;
const settingsButtonGraphic = 'M15.07,2.25a.33.33,0,0,1,.33.33V6.72h1.25a2.11,2.11,0,0,1,0,4.21H15.4v4.14a.33.33,0,0,1-.33.33H10.93v1.25a2.11,2.11,0,0,1-4.21,0V15.4H2.58a.33.33,0,0,1-.33-.33v-3A3.51,3.51,0,0,0,4.49,8.82,3.48,3.48,0,0,0,2.25,5.57v-3a.33.33,0,0,1,.33-.33H5.34a3.49,3.49,0,0,0,7,0h2.76m0-1.25H10.75A2.24,2.24,0,1,1,6.9,1H2.58A1.58,1.58,0,0,0,1,2.58v4a2.24,2.24,0,1,1,0,4.47v4a1.58,1.58,0,0,0,1.58,1.58H5.47a3.36,3.36,0,0,0,6.71,0h2.89a1.58,1.58,0,0,0,1.58-1.58V12.18a3.36,3.36,0,0,0,0-6.71V2.58A1.58,1.58,0,0,0,15.07,1Z';
const settingsGraphic = [{d:'M10 20c-.401 0-.802-.027-1.2-.079a1.145 1.145 0 0 1-.992-1.137v-1.073a.97.97 0 0 0-.627-.878A.98.98 0 0 0 6.1 17l-.755.753a1.149 1.149 0 0 1-1.521.1 10.16 10.16 0 0 1-1.671-1.671 1.149 1.149 0 0 1 .1-1.523L3 13.906a.97.97 0 0 0 .176-1.069.98.98 0 0 0-.887-.649H1.216A1.145 1.145 0 0 1 .079 11.2a9.1 9.1 0 0 1 0-2.393 1.145 1.145 0 0 1 1.137-.992h1.073a.97.97 0 0 0 .878-.627A.979.979 0 0 0 3 6.1l-.754-.754a1.15 1.15 0 0 1-.1-1.522 10.16 10.16 0 0 1 1.673-1.676 1.155 1.155 0 0 1 1.522.1L6.1 3a.966.966 0 0 0 1.068.176.98.98 0 0 0 .649-.887V1.216A1.145 1.145 0 0 1 8.8.079a9.129 9.129 0 0 1 2.393 0 1.144 1.144 0 0 1 .991 1.137v1.073a.972.972 0 0 0 .628.878A.977.977 0 0 0 13.905 3l.754-.754a1.152 1.152 0 0 1 1.522-.1c.62.49 1.18 1.05 1.671 1.671a1.15 1.15 0 0 1-.1 1.522L17 6.1a.967.967 0 0 0-.176 1.068.98.98 0 0 0 .887.649h1.073a1.145 1.145 0 0 1 1.137.991 9.096 9.096 0 0 1 0 2.392 1.145 1.145 0 0 1-1.137.992h-1.073A1.041 1.041 0 0 0 17 13.905l.753.755a1.149 1.149 0 0 1 .1 1.521c-.49.62-1.05 1.18-1.671 1.671a1.149 1.149 0 0 1-1.522-.1L13.906 17a.97.97 0 0 0-1.069-.176.981.981 0 0 0-.65.887v1.073a1.144 1.144 0 0 1-.99 1.137A9.431 9.431 0 0 1 10 20Zm-.938-1.307a7.638 7.638 0 0 0 1.875 0v-.982a2.292 2.292 0 0 1 3.853-1.6l.693.694a8.796 8.796 0 0 0 1.326-1.326l-.694-.694a2.29 2.29 0 0 1 1.6-3.851h.982a7.746 7.746 0 0 0 0-1.876h-.982a2.213 2.213 0 0 1-2.034-1.4 2.223 2.223 0 0 1 .438-2.451l.694-.693a8.76 8.76 0 0 0-1.327-1.326l-.692.694a2.22 2.22 0 0 1-2.434.445 2.221 2.221 0 0 1-1.419-2.041v-.979a7.638 7.638 0 0 0-1.875 0v.982a2.213 2.213 0 0 1-1.4 2.034 2.23 2.23 0 0 1-2.456-.438l-.693-.694a8.757 8.757 0 0 0-1.326 1.327l.694.692a2.216 2.216 0 0 1 .445 2.434 2.22 2.22 0 0 1-2.041 1.418h-.982a7.746 7.746 0 0 0 0 1.876h.982a2.213 2.213 0 0 1 2.034 1.4 2.223 2.223 0 0 1-.438 2.451l-.694.693c.394.488.838.933 1.326 1.326l.694-.694a2.218 2.218 0 0 1 2.433-.445 2.22 2.22 0 0 1 1.418 2.041v.983ZM10 13.229a3.23 3.23 0 1 1 0-6.458 3.23 3.23 0 0 1 0 6.458Zm0-5.208a1.979 1.979 0 1 0 0 3.958 1.979 1.979 0 0 0 0-3.958Z'}];
const closeWindowGraphic = [{d:'m18.442 2.442-.884-.884L10 9.116 2.442 1.558l-.884.884L9.116 10l-7.558 7.558.884.884L10 10.884l7.558 7.558.884-.884L10.884 10l7.558-7.558Z'}];
const linkGraphics = [{d:'M14.111 12.5a3.701 3.701 0 0 1-1.09 2.41c-.479.47-.928.922-1.378 1.373-.45.45-.894.9-1.368 1.366a3.852 3.852 0 0 1-2.698 1.099 3.852 3.852 0 0 1-2.698-1.099 3.738 3.738 0 0 1-1.116-2.659c0-.997.402-1.953 1.116-2.658.479-.472.928-.923 1.378-1.375.45-.45.893-.9 1.368-1.365A3.936 3.936 0 0 1 9.638 8.59a3.968 3.968 0 0 1 2.24.258c.27-.269.546-.54.812-.806l.131-.13a5.086 5.086 0 0 0-3.182-.624A5.052 5.052 0 0 0 6.732 8.71c-.48.471-.929.922-1.377 1.373-.449.451-.894.9-1.37 1.366A4.982 4.982 0 0 0 2.5 14.992c0 1.328.534 2.602 1.486 3.543A5.13 5.13 0 0 0 7.58 20a5.13 5.13 0 0 0 3.595-1.465c.478-.471.927-.923 1.377-1.374.451-.451.894-.9 1.368-1.366a4.993 4.993 0 0 0 1.263-2.071c.243-.781.288-1.61.132-2.412L14.11 12.5Z'}, {d:'M16.017 1.467A5.123 5.123 0 0 0 12.422 0a5.123 5.123 0 0 0-3.595 1.467c-.478.471-.926.923-1.377 1.374-.45.451-.894.9-1.367 1.366a4.966 4.966 0 0 0-1.106 1.624 4.907 4.907 0 0 0-.291 2.86l1.2-1.19a3.699 3.699 0 0 1 1.092-2.41c.478-.472.928-.923 1.377-1.374.45-.45.894-.9 1.368-1.366a3.844 3.844 0 0 1 2.698-1.101c1.012 0 1.982.396 2.698 1.101a3.736 3.736 0 0 1 1.116 2.66c0 .996-.401 1.953-1.116 2.658-.478.471-.927.922-1.377 1.373-.45.451-.893.9-1.368 1.367a3.933 3.933 0 0 1-2.014 1.003 3.966 3.966 0 0 1-2.24-.26c-.273.274-.551.549-.818.818l-.123.12a5.087 5.087 0 0 0 3.183.624 5.053 5.053 0 0 0 2.906-1.423c.477-.472.926-.923 1.376-1.375.45-.452.894-.9 1.368-1.365A4.977 4.977 0 0 0 17.5 5.008a4.977 4.977 0 0 0-1.488-3.543l.005.002Z'}];
const newUserGraphic = [{w:'2', f:'none', d:'M12.5,11.5a3.39,3.39,0,0,1,2,2,3.16,3.16,0,0,1,0,2'}, {w:'0', d:'M1.46,1.5S3.49,4.89,7,5.07c1.49.07,3.35.25,4.06.79,1.41,1.09,2.3,2.08,1.74,4.37a4.91,4.91,0,0,1-4.36,3.49C5.08,14,2.89,10.29,2.33,9.35.41,6.12,1.46,1.5,1.46,1.5Z'}];
const closeImageGraphic = [{d:'m33.16001,9.52439l-2.37671,-2.37671l-10.691,10.691l-10.691,-10.691l-2.37671,2.37671l10.691,10.691l-10.691,10.691l2.37671,2.37671l10.691,-10.691l10.691,10.691l2.37671,-2.37671l-10.691,-10.691l10.691,-10.691z'}];
const feedGraphics =
{
Best:[{c:'none', d:'M14.51,6.56,9.13,1.17a1.61,1.61,0,0,0-2.26,0L1.49,6.56a1,1,0,0,0,.72,1.73H5.52V14A1.28,1.28,0,0,0,6.8,15.3H9.23A1.29,1.29,0,0,0,10.52,14V8.29h3.27A1,1,0,0,0,14.51,6.56Z'}],
Hot:[{w:'0.5', d:'M8.49,2.93c.7,1.56,3,2.81,3.69,3.52a5.14,5.14,0,0,1,1.36,5.45c-1.09,3.37-4.49,3.38-6.21,3.38s-4.18-.28-5-3,.8-4.41,1-5,1.06,2.52,2,3.12c1.19.79,2.85,0,2.85-1.18S6.72,7.65,6.44,5.37a10.59,10.59,0,0,1,1-4.9S7.83,1.46,8.49,2.93Z'}],
New:[{f:'none', w:'1.5', d:'M7.5,3 L7.5,9 L12,6'}, {w:'0.5', d:'M8,15.5A7.5,7.5,0,1,1,15.5,8,7.5,7.5,0,0,1,8,15.5Zm0-14A6.5,6.5,0,1,0,14.5,8,6.51,6.51,0,0,0,8,1.5Z'}],
Top:[{f:'none', w:'1.5', d:'M14.51,6.56,9.13,1.17a1.61,1.61,0,0,0-2.26,0L1.49,6.56a1,1,0,0,0,.72,1.73H5.52V14A1.28,1.28,0,0,0,6.8,15.3H9.23A1.29,1.29,0,0,0,10.52,14V8.29h3.27A1,1,0,0,0,14.51,6.56Z'}],
Rising:[{f:'none', w:'2', d:'M8.5,4.5l4.68-2.45S14,5.31,14.5,7.5'}, {f:'none', w:'2',d:'M1.1,14.67c1.4-2.8,3.62-6.84,3.62-6.84L9,12.18l4.13-9.94'}]
};
const tagGraphics =
{
Followed : [{d:'m0.43678,5.49532l4.46205,-0.8817l2.13151,-3.90349l2.39985,3.78849l4.1937,0.9967l-3.65339,3.14905l1.13489,4.59352l-4.19007,-2.03743l-3.96007,2.03743l0.86655,-4.78519l-3.38505,-2.95738l0,-0.00001l0.00001,0z'}],
Liked : [{d:'m5.10986,11.44595c-3.39081,-2.55084 -4.60316,-4.16445 -4.61299,-6.1398c-0.00896,-1.80077 1.48763,-3.53879 3.03857,-3.52877c0.77447,0.00501 2.43577,0.6678 3.02404,1.20648c0.29641,0.27142 0.4368,0.24465 1.09671,-0.20908c1.79601,-1.23486 3.54983,-1.26078 4.68568,-0.06924c1.81534,1.90433 1.48483,4.17844 -0.95008,6.53732c-1.29454,1.25411 -4.12153,3.47898 -4.4205,3.47898c-0.09098,0 -0.92862,-0.57415 -1.86143,-1.27588l0,0l0,-0.00001z'}],
Warning : [{d:'m0.74313,12.0159l6.28126,-9.83543l6.28126,9.83543l-12.56252,0z'}, {d:'m6.91754,5.38201l0,4.06927'}, {d:'m6.91754,10.29973l0,0.7153'}],
Blocked : [{d:'M3.11,3l7.94,8'}, {d:'M7,12.88A5.88,5.88,0,1,1,12.91,7,5.88,5.88,0,0,1,7,12.88Z'}]
};
const tagButtonGraphics =
{
Followed: [{d:'m1.56177,7.99265l5.62517,-1.11153l2.68713,-4.92101l3.02542,4.77603l5.28687,1.25651l-4.60571,3.96991l1.43072,5.79091l-5.28229,-2.56852l-4.99233,2.56852l1.09243,-6.03254l-4.26742,-3.72827l0,-0.00001z'}],
Liked: [{d:'m7.28935,15.93702c-4.80908,-3.55432 -6.52852,-5.80272 -6.54246,-8.55516c-0.0127,-2.50919 2.10985,-4.93093 4.30951,-4.91697c1.09841,0.00698 3.45457,0.93051 4.2889,1.6811c0.42039,0.37819 0.6195,0.3409 1.55543,-0.29133c2.54723,-1.72064 5.03461,-1.75676 6.64556,-0.09648c2.57463,2.65349 2.10589,5.82222 -1.34747,9.10906c-1.836,1.74747 -5.84543,4.84759 -6.26946,4.84759c-0.12903,0 -1.31703,-0.80001 -2.64001,-1.7778l0,0l0,-0.00001z'}],
Warning: [{d:'m1.24313,16.49297l8.78125,-13.75l8.78125,13.75l-17.5625,0z'}, {d:'m9.875,7.5l0,4.5'}, {d:'m9.875,14.09375l0,1'}],
Blocked: [{d:'m9.78126,18.09375c-4.41989,0 -8,-3.58011 -8,-8c0,-4.41989 3.58011,-8 8,-8c4.41989,0 8,3.58011 8,8c0,4.41989 -3.58011,8 -8,8z'}, {d:'m4.43767,4.59392l10.81217,10.93716'}]
};
const USERTAG_FOLLOWED = `Followed`;
const USERTAG_LIKED = `Liked`;
const USERTAG_WARNING = `Warning`;
const USERTAG_BLOCKED = `Blocked`;
const TAG_CONFIG_PRIORITY = 0;
const TAG_CONFIG_HINT = 1;
const TAG_CONFIG_HINT_ACTIVATED = 2;
const TAG_CONFIG_COLOR = 3;
const tagConfigs =
{
Followed:[100, `Follow`, `Unfollow`, `#0b7ed3`],
Liked:[2, `Tag as liked`, `Remove liked tag`, `#C95A54`],
Warning:[1, `Tag as warned`, `Remove warned tag`, `#D4A343`],
Blocked:[0, `Block`, `Unblock`, `#663988`]
};
const FEED_BUTTONS_EXTENDED = [`Best`, `Hot`, `New`, `Top`, `Rising`];
const FEED_BUTTONS = [`Hot`, `New`, `Top`, `Rising`];
const SETTING_WIDE_MODE = `wideMode`;
const SETTING_SIDEBAR_CUSTOMS = `sidebarCustoms`;
const SETTING_SIDEBAR_RECENT = `sidebarRecent`;
const SETTING_SIDEBAR_SUBS = `sidebarSubs`;
const SETTING_SIDEBAR_RESOURCES = `sidebarResources`;
const SETTING_FEED_BUTTONS = `feedButtons`;
const SETTING_FLAIR_BAR = `flairbar`;
const SETTING_BACKPLATES = `backplates`;
const SETTING_USER_INFO = `userInfo`;
const SETTING_USER_TAGS = `userTags`;
const SETTING_SHOW_NAMES = `showNames`;
const SETTING_BIGGER_FONTS = `biggerFonts`;
const SETTING_HIDE_SHARE = `hideShare`;
const SETTING_GHOSTED_COMMENTS = `ghostedComments`;
const SETTING_COLLAPSE_AUTOMODERATOR = `collapseAutomoderator`;
const css = new CustomCSS();
let settingsRevision = getSettingsRevision();
let settings = new Database(`SETTINGS`);
let tags = new Database(`TAGS`);
let users = new Database(`USERS`, true);
let flairs = new Database(`FLAIRS`);
let subs = new Database(`SUBS`, true);
let flairsWindow = new Window(`Subreddit flairs settings`, renderFlairsWindow);
let settingsWindow = new Window(`Reddit++ settings`, renderSettingsWindow);
let imageViewer = new ImageViewer();
// ***********************************************************************************************************************
// ************************************************ COMMON CSS ***********************************************************
// ***********************************************************************************************************************
css.addVar('--color-neutral-background-transparent', '#fff0', '#0b141600');
css.addVar('--stickiedColor', '#0e8a001c', '#0e8a001c');
css.addVar('--stickiedHoverColor', '#18900b3d', '#18900b3d');
// windows
css.addRule('.pp_WindowContainer { cursor:pointer; position: fixed; top:0px; z-index: 10; width:100%; height:100%; display: flex; justify-content: center; align-items: center; background-color: #000000b3; }');
css.addRule('.pp_Window { cursor: auto; display: flex; flex-direction: column; width:900px; height: fit-content; min-height: 200px; max-height: 75%; border-radius: 15px; background-color: var(--color-neutral-background); box-shadow: 0px 0px 50px 0px #00000070; }');
css.addRule('.pp_WindowTittleContainer { height: 48px; margin: 1rem; display: flex; flex-direction: row; justify-content: space-between; align-items: center; }');
css.addRule('.pp_WindowTittle { margin-left: 1rem; }');
css.addRule('.pp_WindowCloseButton { margin: 1rem; }');
css.addRule('.pp_WindowContent { display: flex; flex-direction: column; overflow-y: overlay; }');
css.addRule('.pp_WindowFooter { height: 2rem; min-height: 2rem; }');
css.addRule('.pp_WindowElementsContainer { display: flex; flex-direction: column; padding: 0px; margin: 20px 40px; gap: 0.5rem;}');
css.addRule('.pp_WindowElement { display: flex; flex-direction: row; justify-content: flex-end; height: 3rem; }');
css.addRule('.pp_WindowElementsContainer > .pp_WindowElement:hover { background-color: var(--color-neutral-background-hover); border-radius: 15px;}');
// image viewer
css.addRule('.pp_imageViewer { display: flex; justify-content: center; align-items: center; position: fixed; top:0px; z-index: 10; cursor:pointer; width:100%; height:100%; background-color: #000000b3; }');
css.addRule('.pp_imageViewer_closeButton { display: flex; align-items: center; justify-content: center; position: fixed; top:50px; z-index: 11; right:50px; cursor:pointer; width:50px; height:50px; color: #ffffff9c; background-color: #00000069; border-radius:30px; }');
css.addRule('.pp_imageViewer_closeButton:hover { color: #ffffffc7; }');
css.addRule('.pp_imageViewer_imageContainer {display: flex; justify-content: center; align-items: center; width:80%; height:90%;}');
css.addRule('.pp_imageViewer_imageContainer:not(.pp_imageViewer_drag) { transition: transform 0.5s;}');
css.addRule('.pp_imageViewer_image {cursor: grab; object-fit: scale-down; max-width:100%; max-height:100%; box-shadow: 0px 0px 20px 3px #14141485; }');
css.addRule('.pp_imageViewer_image:active {cursor: grabbing; }');
// ban hint
css.addRule('faceplate-banner { max-width: 1000px !important;}');
if(settings.isEnabled(SETTING_BIGGER_FONTS))
{
// resize fonts
css.addRule(':is(.text-14):not(.pp_defaultText .text-14) { font-size: 1.0rem !important; line-height: 1.4rem !important;}');
css.addRule('faceplate-hovercard .text-12 { font-size: 0.9rem !important; font-color: #595959 !important;}');
// comments more space
css.addRule('shreddit-comment-action-row { margin-bottom: 15px !important;}');
}
// hide native share
if(settings.isEnabled(SETTING_HIDE_SHARE))
{
css.addRule('shreddit-comment-share-button { display:none !important;}');
}
// posts gradient selection
if(settings.isEnabled(SETTING_BACKPLATES))
{
//[view-context="SubredditFeed"]
css.addRule('article > shreddit-post { background-color: #00000000 !important; padding-top:10px !important; margin-top: 10px !important; margin-bottom: 10px !important;}'); //border-radius: 0px !important;
css.addRule('article > shreddit-post::before { border-radius: 15px !important; position: absolute; content: ""; top: 0; right: 0; bottom: 0; left: 0; opacity: 0; z-index: -1; background: linear-gradient(var(--color-neutral-background-hover), var(--color-neutral-background)); transition: opacity 0.2s;}');
css.addRule('article > shreddit-post:hover::before { opacity: 1;}');
// override gold
css.addRule('shreddit-post[gold-count]:not(shreddit-post[gold-count=""]) { background-image: linear-gradient(rgba(255, 214, 53, 0.2), rgba(255, 214, 53, 0)) !important;}');
css.addRule('shreddit-post[gold-count]:not(shreddit-post[gold-count=""])::before { background: linear-gradient(#fbed2966, var(--color-neutral-background)) !important;}');
// stickied
css.addRule('.stickied::after { border-radius: 15px !important; position: absolute; content: ""; top: 0; right: 0; bottom: 0; left: 0; opacity: 1; z-index: -2; background: linear-gradient(var(--stickiedColor), var(--color-neutral-background)) !important;}');
css.addRule('.stickied::before { background: linear-gradient(var(--stickiedHoverColor), var(--color-neutral-background)) !important;}');
}
// ***********************************************************************************************************************
// ************************************************ Events ***************************************************************
// ***********************************************************************************************************************
// Settings changes
window.addEventListener("storage", (event) => {
const currentRevision = getSettingsRevision();
if(currentRevision != settingsRevision)
{
settingsRevision = currentRevision;
document.addEventListener("visibilitychange", () => { window.location.reload(); }, {once:true});
}
});
// Intersections
const commentRenderIntersector = new IntersectionObserver(entries => {
for (const entry of entries) {
if (entry.isIntersecting)
{
renderComment(entry.target.parentNode);
commentRenderIntersector.unobserve(entry.target);
}
}
}, { threshold: 0.05});
// Mutations
new MutationObserver(mutations => {
const hasComments = window.location.href.includes(`/comments/`);
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node instanceof HTMLElement)
{
// check sidebar`s sections
if(node.matches(`reddit-recent-pages`))
{
awaitShadowrootSection(node, SETTING_SIDEBAR_RECENT);
}
if(node.matches(`faceplate-expandable-section-helper`))
{
const customs = node.querySelector(`summary[aria-controls="multireddits_section"]`);
if(customs != null)
{
awaitSection(node, customs, SETTING_SIDEBAR_CUSTOMS);
}
const subs = node.querySelector(`summary[aria-controls="communities_section"]`);
if(subs != null)
{
awaitSection(node, subs, SETTING_SIDEBAR_SUBS);
}
}
// check new posts
if(node.matches(`faceplate-batch`))
{
node.querySelectorAll(`shreddit-post`).forEach((post) => {
renderPost(post);
});
}
// check new comments
if(hasComments)
{
// post tree
if(node.id != null && node.id.includes(`comment-tree`) == true)
{
registryAllComments(node);
}
// single thread
if(node.matches(`shreddit-comment-tree`) == true)
{
registryAllComments(node);
}
// more comments
if(node.matches(`shreddit-comment`) == true)
{
registryComment(node);
registryAllComments(node);
}
}
// check refresh header
if(node.matches(`reddit-header-large`) == true)
{
renderHeader(node.parentNode);
}
// check refresh sub pages
if(node.matches(`dsa-transparency-modal-provider`) == true)
{
renderApp(node);
renderFeedButtons(node);
renderSub(node);
checkNativePosts(node);
}
// check refresh main pages
if(node.classList.contains(`grid-container`) && node.querySelector(`.main-container`) != null)
{
renderApp(node);
renderFeedButtons(node);
checkNativePosts(node);
}
}
}
}
}).observe(document.body, {childList: true, subtree: true});
// ***********************************************************************************************************************
// ************************************************ CORE APP *************************************************************
// ***********************************************************************************************************************
if(settings.isEnabled(SETTING_WIDE_MODE))
{
css.addRule('@media (min-width: 1392px) { .pp_pageContainer { margin-right: 300px;}}');
css.addRule('@media (min-width: 1392px) { .pp_mainFeed { width: 700px !important; }}');
css.addRule('@media (min-width: 1392px) { .pp_rightSidebar { grid-column-start: 3; order: 10;}}');
css.addRule('@media (min-width: 1392px) { #right-sidebar-container { position: fixed; right: 0px; margin: 15px 10px 0px 0px; } }');
}
renderApp(document.body);
function renderApp(container)
{
if(settings.isDisabled(SETTING_WIDE_MODE)) return;
const leftSidebar = container.querySelector(`#left-sidebar-container`);
leftSidebar.classList.add(`pp_defaultText`);
// Firefox first-load fix
if(awaitElement(leftSidebar, renderApp)) return;
const resources = leftSidebar.querySelector(`summary[aria-controls="RESOURCES"]`);
if(resources != null)
{
renderSidebarSection(resources.parentNode.parentNode, resources, null, SETTING_SIDEBAR_RESOURCES);
}
const pageContainer = leftSidebar.parentNode;
pageContainer.classList.add(`pp_pageContainer`);
pageContainer.querySelector(`.subgrid-container`).classList.add(`pp_mainFeed`);
const rightSidebar = container.querySelector(`#right-sidebar-container`);
rightSidebar.classList.add(`pp_rightSidebar`);
rightSidebar.classList.toggle(`styled-scrollbars`, true);
const originContainer = rightSidebar.parentNode;
let isWideMode = !(window.innerWidth >= 1392);
applyRightBar();
window.addEventListener("resize", (event) => { applyRightBar(); });
function applyRightBar()
{
if(window.innerWidth >= 1392 && !isWideMode)
{
pageContainer.prepend(rightSidebar);
isWideMode = true;
}
if(window.innerWidth < 1392 && isWideMode)
{
originContainer.append(rightSidebar);
isWideMode = false;
}
}
}
// ***********************************************************************************************************************
// ************************************************ SIDEBAR **************************************************************
// ***********************************************************************************************************************
css.addRule('.pp_loadingSection { max-height: 0px !important; visibility: hidden ! important;}');
css.addRule('.pp_collapsedSection { max-height: 43px !important; overflow-y: hidden ! important;}');
function awaitShadowrootSection(container, settingName)
{
container.classList.add(`pp_loadingSection`);
const recentId = setInterval(() => {
const helper = container.shadowRoot?.querySelector(`faceplate-expandable-section-helper`);
const button = helper?.querySelector(`summary`);
if(button != null && helper.getAttribute(`open`) != null)
{
clearInterval(recentId);
css.registry(container.shadowRoot);
helper.classList.add(`pp_defaultText`);
container.classList.remove(`pp_loadingSection`);
renderSidebarSection(helper, button, container.querySelector(`hr`), settingName);
}
}, 10);
}
function awaitSection(container, button, settingName)
{
container.classList.add(`pp_loadingSection`);
const recentId = setInterval(() => {
if(container.getAttribute(`open`) != null)
{
clearInterval(recentId);
container.classList.remove(`pp_loadingSection`);
renderSidebarSection(container, button, container.nextElementSibling, settingName);
}
}, 10);
}
function renderSidebarSection(container, button, hr, settingName)
{
const collapsedSettingName = settingName + `Collapsed`;
if(settings.isEnabled(settingName))
{
const details = container.querySelector(`details`);
if(!settings.isEnabled(collapsedSettingName))
{
container.toggleAttribute(`open`, false);
details.classList.add(`pp_collapsedSection`);
}
button.addEventListener(`click`, (e) => {
const isCollapsed = !(e.currentTarget.getAttribute(`aria-expanded`) === "false");
settings.set(collapsedSettingName, isCollapsed);
details.classList.toggle(`pp_collapsedSection`, false);
});
}
else
{
container.remove();
hr?.remove();
}
}
// ***********************************************************************************************************************
// *********************************************** HEADER **************************************************************
// ***********************************************************************************************************************
css.addRule('#reddit-logo { text-decoration: none; }');
css.addRule('.pp_logo { width: max-content; color: var(--shreddit-color-wordmark); font-size: 22px; font-weight: 1000; letter-spacing: -2px; }');
css.addRule('.pp_userPanel { grid-column: span3 / span3 !important; }');
renderHeader(document.body);
function renderHeader(container)
{
const nav = container.querySelector(`reddit-header-large`).querySelector(`nav`);
const userPanel = nav.childNodes.item(4);
// Firefox first-load fix
if(awaitElement(userPanel, renderHeader)) return;
if(checkIsRendered(nav)) return;
userPanel.classList.add(`pp_userPanel`);
userPanel.addEventListener(`click`, () => { renderSettingsButton(); }, {once:true});
const logo = container.querySelector(`#reddit-logo`);
const logoPP = AppendNew(logo, `div`, `pp_logo`);
logoPP.textContent = `++`;
}
function renderSettingsButton()
{
let userMenu = document.getElementById(`user-drawer-content`);
if(userMenu.querySelector(`faceplate-tracker[noun="pp-settings"]`) != null)
{
return;
}
let originSettingsButton = userMenu.querySelector(`faceplate-tracker[noun="settings"]`);
let ppSettingsButton = originSettingsButton.cloneNode(true);
ppSettingsButton.setAttribute(`noun`, `pp-settings`);
originSettingsButton.parentNode.appendChild(ppSettingsButton);
ppSettingsButton.querySelector(`a`).removeAttribute(`href`);
let path = ppSettingsButton.querySelector(`path`);
path.setAttribute(`d`, settingsButtonGraphic);
let text = ppSettingsButton.querySelector(`.text-14`);
text.textContent = `Reddit++`;
settingsWindow.onClose = () => {
if(settingsWindow.changes > 0)
{
nextSettingsRevision();
window.location.reload();
}
};
ppSettingsButton.addEventListener(`click`, () => { settingsWindow.open(); });
}
// ***********************************************************************************************************************
// ********************************************** FEED BUTTONS ***********************************************************
// ***********************************************************************************************************************
css.addRule('.pp_feedButtonsContainer { justify-content: space-between; height: 40px !important; }');
css.addRule('.pp_feedButton { gap: 0.25rem !important;}');
css.addRule('.pp_feedContainer { gap: 0.25rem; height: 40px; }');
renderFeedButtons(document.body);
function renderFeedButtons(container)
{
if(settings.isDisabled(SETTING_FEED_BUTTONS)) return;
const main = container.querySelector(`.main-container`)?.querySelector(`main`);
// Firefox first-load fix
if(awaitElement(main, renderFeedButtons)) return;
const feedDropdown = main.querySelector(`shreddit-sort-dropdown`);
// skip non feed page
if((feedDropdown == null || feedDropdown.getAttribute(`trigger-id`) == `comment-sort-button`) && !window.location.href.includes(`/about/`)) return;
if(window.location.href.includes(`/user/`)) return;
// get current feed
const currentFeed = feedDropdown?.querySelector(`div[slot="selected-item"]`)?.textContent;
feedDropdown?.remove();
const isHome = window.location.href.includes(`?feed=home`) || window.location.href == `https://www.reddit.com/`;
const isPopular = window.location.href.includes(`reddit.com/r/popular/`);
const isAll = window.location.href.includes(`reddit.com/r/all/`);
// get container
let buttonsContainer = null;
let hrefGenerator = null;
if(isHome || isPopular || isAll)
{
const originContainer = main.querySelector(`shreddit-layout-event-setter`).parentNode;
originContainer.classList.add(`pp_feedButtonsContainer`);
buttonsContainer = document.createElement(`div`);
buttonsContainer.classList.add(`flex`, `mx-md`, `shrink-0`, `pp_feedContainer`);
originContainer.prepend(buttonsContainer);
if(isHome)
{
hrefGenerator = (feed) => { return `/${feed.toLowerCase()}/?feed=home`; };
}
else
{
hrefGenerator = (feed) => { return `/r/${isPopular ? `popular` : `all`}/${feed.toLowerCase()}/`; };
}
}
else
{
const aboutContainer = main.querySelector(`a[slot="page-3"]`)?.parentNode?.parentNode;
if(aboutContainer == null)
{
console.log(`FLAIR BAR WAS CORRUPTED`);
setTimeout(() => {renderFeedButtons(container); }, 100 );
return;
}
buttonsContainer = document.createElement(`div`);
buttonsContainer.classList.add(`flex`, `mx-md`, `shrink-0`, `pp_feedContainer`);
aboutContainer.before(buttonsContainer);
aboutContainer.remove();
const subName = getCurrentSub();
hrefGenerator = (feed) => { return `/r/${subName}/${feed.toLowerCase()}/`; };
}
const feeds = (isHome || isPopular) ? FEED_BUTTONS_EXTENDED : FEED_BUTTONS;
for(const feed of feeds)
{
const button = AppendNew(buttonsContainer, `a`, [`inline-flex`, `flex-col`, `text-neutral-content-weak`, `font-semibold`, `rounded-full`, `hover:no-underline`, `hover:bg-secondary-background-hover`, `hover:text-secondary-content`, `active:bg-secondary-background`, `pl-[var(--rem16)]`, `pr-[var(--rem16)]`]);
button.href = hrefGenerator(feed);
const isCurrent = feed == currentFeed;
button.classList.toggle(`bg-secondary-background-selected`, isCurrent);
button.classList.toggle(`!text-neutral-content-strong`, isCurrent);
const spanContainer = AppendNew(button, `span`, [`inline-flex`, `flex-row`, `items-center`, `gap-xs`, `py-[var(--rem10)]`, `leading-5`, `font-14`, `pp_feedButton`]);
let graphic = feedGraphics[feed];
if(graphic != null)
{
let svg = buildSvg(16, 16, graphic);
spanContainer.append(svg);
}
const spanText = AppendNew(spanContainer, `span`);
spanText.textContent = feed;
}
}
// ***********************************************************************************************************************
// ************************************************** SUBS ***************************************************************
// ***********************************************************************************************************************
renderSub(document.body);
function renderSub(container)
{
// skip page without feed
const checkIsFeed = container.querySelector(`.main-container`)?.querySelector(`main`)?.querySelector(`shreddit-feed-error-banner`);
if(checkIsFeed == null) return;
renderMasthead();
// flairBar
awaitSubInfo();
}
css.addRule('.masthead > section > div { display: flex; flex-direction: column; align-items: flex-start;}');
css.addRule('.masthead > section > div > div:last-child { align-self: flex-end;}');
function renderMasthead()
{
const masthead = document.body.querySelector(`.masthead`);
if(masthead == null) return;
const section = masthead.querySelector(`section`);
section.style.top= `-3rem`;
const div = section.querySelector(`div`);
div.style.gap = `1rem`;
}
function awaitSubInfo()
{
const sub = getCurrentSub();
const subData = subs.get(sub);
if(subData.flairs != undefined)
{
renderSubFlairs(sub);
return;
}
fetch(`https://www.reddit.com/r/${sub}/api/link_flair_v2.json?raw_json=1`, {
method: `get`
})
.then(r => r.json())
.then(result => {
if(result != null)
{
const loadedFlairs = [];
for (const loadedFlair of result)
{
const flair = { text:loadedFlair.text, color:loadedFlair.text_color, background:loadedFlair.background_color, richtext: loadedFlair.richtext};
loadedFlairs.push(flair);
}
subData.flairs = loadedFlairs;
subs.set(sub, subData);
renderSubFlairs(sub);
}
else
{
console.log(`Reddit++: Unable to load subreddit flairs list`);
}
});
}
css.addRule('.pp_flairMenuContainer { }');
css.addRule('.pp_flairMenu { display: flex; flex-direction: row; overflow: hidden; }');
css.addRule('.pp_flairUL { margin-top: 5px !important; flex-wrap: nowrap !important; position: relative; }'); //transition: left 0.2s;
css.addRule('.pp_flairUL-smoothed { transition: left 0.1s ease-out;}');
css.addRule('.pp_flairBorderContainer { width: 100%; display: flex; justify-content: space-between; }');
css.addRule('.pp_flairBorderPrev { width: 20px; }');
css.addRule('.pp_flairBorder { z-index: 1; position: absolute; height: 40px; width: 20px; margin-top: 5px; }');
css.addRule('.pp_flairBorderLeft { background: linear-gradient(90deg, var(--color-neutral-background), 60%, var(--color-neutral-background-transparent));}');
css.addRule('.pp_flairBorderRight { background: linear-gradient(270deg, var(--color-neutral-background), 60%, var(--color-neutral-background-transparent));}');
css.addRule('.pp_flairSettings { color: #434849; padding-top:0.4rem !important; cursor:pointer;}');
function renderFlair(conatiner, sub, flair)
{
const a = AppendNew(conatiner, `a`, `no-decoration`);
a.href = `/r/`+sub+`/?f=flair_name%3A%22`+flair.text+`%22`;
const span = AppendNew(a, `span`, [`bg-tone-4`, `inline-block`, `truncate`, `max-w-full`, `text-12`, `font-normal`, `box-border`, `px-[6px]`, `rounded-[20px]`, `leading-4`, `max-w-full`, `py-xs`, `!px-sm`, `leading-4`, `h-xl`, `inline-flex`]);
span.classList.add(flair.color == `light` ? `text-global-white` : `text-global-black`);
span.style.backgroundColor = flair.background;
for(const richElement of flair.richtext)
{
if(richElement.e == `text`)
{
const content = document.createTextNode(richElement.t);
span.appendChild(content);
}
if(richElement.e == `emoji`)
{
const fimg = document.createElement(`faceplate-img`);
fimg.classList.add(`flair-image`);
fimg.setAttribute(`loading`, `lazy`);
fimg.setAttribute(`width`, 16);
fimg.setAttribute(`height`, 16);
fimg.setAttribute(`src`, richElement.u);
fimg.setAttribute(`alt`, richElement.a);
span.appendChild(fimg);
}
}
}
function renderSubFlairs(sub)
{
if(settings.isDisabled(SETTING_FLAIR_BAR)) return;
const feedContent = document.body.querySelector(`report-flow-provider`)?.querySelector(`.main-container`)?.querySelector(`shreddit-title`)?.parentNode;
// skip render for non feed page
if(feedContent == null) return;
const prevFlairMenu = feedContent.parentNode.querySelector(`.pp_flairMenuContainer`)
if(prevFlairMenu != null)
{
prevFlairMenu.remove();
}
// load data
const subData = subs.get(sub);
const flairsData = flairs.get(sub);
// skip render when sub haven't flairs
if(subData.flairs == undefined || subData.flairs.length == 0) return;
const flairMenuContainer = document.createElement(`div`);
flairMenuContainer.classList.add(`pp_flairMenuContainer`);
feedContent.before(flairMenuContainer);
const flairMenu = AppendNew(flairMenuContainer, `div`, `pp_flairMenu`);
const ul = AppendNew(flairMenu, `ul`, [`p-0`, `m-0`, `list-none`, `gap-xs`, `flex`, `flex-row`, `pp_flairUL`]);
let flairsRendered = 0;
for (const flair of subData.flairs)
{
if(flairsData.hidden != undefined && flairsData.hidden.includes(flair.text)) continue;
const li = AppendNew(ul, `li`, `max-w-full`);
renderFlair(li, sub, flair);
flairsRendered++;
}
// prevent render empty menu
if(flairsRendered == 0)
{
flairMenuContainer.remove();
return;
}
// borders
const borderContainer = document.createElement(`div`);
borderContainer.classList.add(`pp_flairBorderContainer`);
flairMenuContainer.prepend(borderContainer);
const borderLeftC = AppendNew(borderContainer, `div`, `pp_flairBorderPrev`);
const borderLeft = AppendNew(borderLeftC, `div`, [`pp_flairBorder`, `pp_flairBorderLeft`]);
borderLeft.textContent = ` `;
const borderRightC = AppendNew(borderContainer, `div`, `pp_flairBorderPrev`);
const borderRight = AppendNew(borderRightC, `div`, [`pp_flairBorder`, `pp_flairBorderRight`]);
borderRight.textContent = ` `;
const hr = document.createElement(`hr`);
hr.classList.add(`border-0`, `border-b-sm`, `border-solid`, `border-b-neutral-border-weak`);
flairMenuContainer.prepend(hr);
const mymx = document.createElement(`div`);
mymx.classList.add(`my-xs`, `mx-2xs0`);
flairMenuContainer.prepend(mymx);
// navigation
ul.style.left = `25px`;
const ulRect = ul.getBoundingClientRect();
const menuRect = flairMenu.getBoundingClientRect();
flairMenu.addEventListener(`mousemove`, e => {onMoveOverFlairs(e, ul, flairMenu)});
if(ulRect.width > menuRect.width * 1.72)
{
ul.classList.add(`pp_flairUL-smoothed`);
}
}
function onMoveOverFlairs(e, ul, flairMenu)
{
const ulRect = ul.getBoundingClientRect();
const menuRect = flairMenu.getBoundingClientRect();
if(ulRect.width < menuRect.width)
{
ul.style.left = `25px`;
return;
}
let scale = (e.clientX - (menuRect.x + 25)) / ((menuRect.right - 25) - (menuRect.x + 25));
scale = Math.max(0, Math.min(scale, 1));
ul.style.left = `${Math.round(25 - (ulRect.width - menuRect.width + 50) * scale)}px`;
}
//************ Flairs Settings *************
css.addRule('.pp_WindowScrollContent { overflow-y: scroll; }');
css.addRule('.pp_checkBoxContainer { float: right; position: relative; }');
css.addVar('--checkBox-background', '#1a1a1b1a', '#81818152');
css.addRule('.pp_checkBoxButtonActive { justify-content: flex-end; background-color: #0079d3; }');
css.addRule('.pp_checkBoxButton { position: relative; cursor:pointer; overflow:visible; display:flex; justify-content: start; background: transparent; background-color: var(--checkBox-background); padding: initial; height: 24px; width:37.5px; border-radius: 100px; border: 2px solid transparent; transition: background-color 0.2s linear; }');
css.addRule('.pp_checkBoxKnob { height: 19.5px; width:19.5px; background-color: #fff; box-shadow: 0 0 0 1px rgba(0,0,0,.1),0 2px 3px 0 rgba(0,0,0,.2); transition: 0.5s linear; border-radius: 57%; }');
css.addRule('.pp_fs_flairContainer { width: 100%; display: flex; align-items: center; margin-left: 3rem; }');
css.addRule('.pp_fs_flairPanelCheckboxArea { width: 200px; min-width: 200px; display: flex; justify-content: center; align-items: center; }');
css.addRule('.pp_fs_columnTittle { margin: 20px 57px 10px 40px; }');
function renderFlairsWindow(win, context)
{
const titlePanel = AppendNew(win.content, `div`, [`pp_WindowElement`, `pp_fs_columnTittle`]);
const tittleBarArea = AppendNew(titlePanel, `div`, `pp_fs_flairPanelCheckboxArea`);
const tittleBar = AppendNew(tittleBarArea, `div`, [`text-14`, `font-semibold`, `mb-xs`]);
tittleBar.textContent = `show on bar:`;
const tittleFeedArea = AppendNew(titlePanel, `div`, `pp_fs_flairPanelCheckboxArea`);
const tittleFeed = AppendNew(tittleFeedArea, `div`, [`text-14`, `font-semibold`, `mb-xs`]);
tittleFeed.textContent = `show in feed:`;
const scroll = AppendNew(win.content, `div`, [`pp_WindowScrollContent`, `styled-scrollbars`]);
const elements = AppendNew(scroll, `div`, `pp_WindowElementsContainer`);
const subData = subs.get(context.sub);
for (const flair of subData.flairs)
{
const panel = AppendNew(elements, `div`, `pp_WindowElement`);
const flairContainer = AppendNew(panel, `div`, `pp_fs_flairContainer`);
addFlairToggle(`hidden`);
addFlairToggle(`banned`);
function addFlairToggle(target)
{
const checkboxArea = AppendNew(panel, `div`, `pp_fs_flairPanelCheckboxArea`);
const checkBoxContainer = AppendNew(checkboxArea, `div`, `pp_checkBoxContainer`);
const checkBoxBack = AppendNew(checkBoxContainer, `button`, `pp_checkBoxButton`);
const initState = !(flairs.get(context.sub)[target]?.includes(flair.text) ?? false);
checkBoxBack.classList.toggle(`pp_checkBoxButtonActive`, initState);
const knob = AppendNew(checkBoxBack, `div`, `pp_checkBoxKnob`);
checkBoxBack.addEventListener(`click`, e => {
const flairData = flairs.get(context.sub);
let state = flairData[target]?.includes(flair.text) ?? false;
checkBoxBack.classList.toggle(`pp_checkBoxButtonActive`, state);
if(state)
{
flairData[target] = flairData[target].filter(f => f != flair.text);
}
else
{
const targetList = flairData[target] ?? [];
targetList.push(flair.text);
flairData[target] = targetList;
}
flairs.set(context.sub, flairData);
});
}
// flair
renderFlair(flairContainer, context.sub, flair);
}
}
// *********** Sub ContextMenu *************
document.body.addEventListener(`click`, clickSubredditMenu);
function clickSubredditMenu(e)
{
if(e.target.matches(`shreddit-subreddit-header-buttons`) == true)
{
if(checkIsRendered(e.target)) return;
const controlMenu = e.target.shadowRoot.querySelector(`shreddit-subreddit-overflow-control`).shadowRoot.querySelector(`faceplate-menu`);
const originButton = controlMenu.querySelector(`li`);
// flairs settings
const menuFlairsButton = originButton.cloneNode(true);
menuFlairsButton.querySelector(`.text-14`).textContent = `Flairs settings`;
controlMenu.prepend(menuFlairsButton);
const sub = getCurrentSub();
flairsWindow.onClose = () => { renderSubFlairs(sub); };
menuFlairsButton.addEventListener(`click`, () => { flairsWindow.open({sub:sub}); });
// about
const link = document.createElement(`a`);
link.href = `https://www.reddit.com/`+e.target.getAttribute(`prefixed-name`)+`/about/`;
link.classList.add(`no-underline`);
controlMenu.prepend(link);
const menuAboutButton = originButton.cloneNode(true);
menuAboutButton.querySelector(`.text-14`).textContent = `About`;
link.prepend(menuAboutButton);
}
}
// ***********************************************************************************************************************
// ************************************************* POSTS ***************************************************************
// ***********************************************************************************************************************
css.addRule('.pp_bannedPost { visibility: hidden !important; max-height: 0px;}');
checkNativePosts(document.body);
function checkNativePosts(container)
{
const main = container.querySelector(`main`);
if(main != null)
{
main.querySelectorAll(`shreddit-post`).forEach((post) => {
renderPost(post);
});
}
}
function renderPost(post)
{
const sub = post.getAttribute(`subreddit-prefixed-name`).replace(`r/`, ``);
const flairData = flairs.get(sub);
const postFlair = post.querySelector(`shreddit-post-flair`)?.querySelector(`a`);
if(postFlair != null)
{
const postFlairText = decodeURIComponent(postFlair.href.split(`%22`)[1]);
if(flairData.banned != undefined && flairData.banned.includes(postFlairText))
{
const next = post.parentElement.nextElementSibling;
next.classList.add(`pp_bannedPost`);
post.parentElement.classList.add(`pp_bannedPost`);
post.classList.add(`pp_bannedPost`);
post.querySelector(`faceplate-tracker[source="post_credit_bar"]`).classList.add(`pp_bannedPost`);
}
}
}
// ***********************************************************************************************************************
// *********************************************** COMMENTS **************************************************************
// ***********************************************************************************************************************
const NEWUSER_SECONDS_SHIFT = DAY_SECONDS * 64;
const REFRESH_USER_SECONDS_SHIFT = DAY_SECONDS;
const BLOCK_COOLDOWN_SECONDS = DAY_SECONDS + 42;
let loadingComments = {};
let currentLoads = 0;
function registryAllComments(container)
{
container.querySelectorAll(`shreddit-comment`).forEach((comment) => {
registryComment(comment);
});
}
function registryComment(comment)
{
if(checkIsRendered(comment)) return;
commentRenderIntersector.observe(comment.querySelector(`div[slot="commentMeta"]`));
}
css.addRule('.pp_muted_avatar { opacity: 0.5; }');
css.addRule('.pp_muted_content { color: #a5a5a5; transition: color 0.2s; }');
css.addRule('.pp_muted_content:hover { color: var(--color-tone-1); }');
function renderComment(comment)
{
// collapse automoderator
if(settings.isEnabled(SETTING_COLLAPSE_AUTOMODERATOR))
{
const author = comment.getAttribute(`author`);
if(author != null && author == `AutoModerator`)
{
comment.setAttribute(`collapsed`, ``);
return;
}
}
// add anchors
const nickname = comment.querySelector(`div[slot="commentMeta"]`).querySelector(`faceplate-hovercard[data-id="user-hover-card"]`);
// skip [deleted]
if(nickname == null) return;
const tagsAnchor = document.createElement(`div`);
tagsAnchor.setAttribute(`pp-anchor`, `tags`);
nickname.parentNode.querySelector(`.ml-2xs`).after(tagsAnchor);
const time = nickname.parentNode.querySelector(`time`).parentNode.parentNode;
const infoAnchor = document.createElement(`div`);
infoAnchor.setAttribute(`pp-anchor`, `info`);
time.before(infoAnchor);
// make ghosted when karma below zero
if(settings.isEnabled(SETTING_GHOSTED_COMMENTS) && comment.getAttribute(`score`) < 0)
{
comment.querySelector(`div[slot="commentAvatar"]`).classList.add(`pp_muted_avatar`);
comment.querySelector(`faceplate-tracker[noun="comment_author"]`).querySelector(`a`).style.color = `#a5a5a5`;
comment.querySelector(`div[slot="comment"]`).classList.add(`pp_muted_content`);
}
// registry image
const image = comment.querySelector(`div[slot="comment"]`).querySelector(`faceplate-img`);
if(image != null)
{
registryImage(image);
}
renderCommentTags(comment);
awaitContextMenu(comment);
awaitUserInfo(comment);
}
css.addRule('.pp_imageViewable { cursor: pointer; }');
function registryImage(imgContainer)
{
imgContainer.classList.add(`pp_imageViewable`);
imgContainer.parentNode.removeAttribute(`href`);
imgContainer.addEventListener(`click`, () => { imageViewer.open(imgContainer.getAttribute(`src`)); });
}
function renderCommentTags(comment)
{
if(settings.isDisabled(SETTING_USER_TAGS)) return;
const userId = comment.getAttribute(`author`);
if(userId == null) return;
const tagsData = tags.get(userId);
const tagsContainer = comment.querySelector(`div[pp-anchor="tags"]`);
// skip not ininitialized comments
if(tagsContainer == null) return;
// clear old tags
tagsContainer.parentNode.querySelectorAll(`svg[tag="true"]`).forEach(tag => { tag.remove(); });
if(tagsData.tags != undefined)
{
for(const tag of tagsData.tags)
{
renderTag(tag);
}
}
function renderTag(tag)
{
let tagSvg = buildSvg(20, 20, tagGraphics[tag], {w:1.5, f:`none`});
tagSvg.setAttribute(`tag`, `true`);
tagSvg.setAttribute(`viewBox`, `-4 -4 20 20`);
tagSvg.style.color = tagConfigs[tag][TAG_CONFIG_COLOR];
tagsContainer.after(tagSvg);
}
}
function awaitUserInfo(comment)
{
const userId = comment.getAttribute(`author`);
let userData = users.get(userId);
if(userData.accountId == undefined)
{
loadUserInfo(comment, userId);
return;
}
renderUserInfo(comment);
}
function loadUserInfo(comment, userId)
{
if(comment == null)
{
// do nothing
}
else if(loadingComments[userId] != undefined && loadingComments[userId] != null)
{
loadingComments[userId].push(comment);
return;
}
else
{
loadingComments[userId] = [comment];
currentLoads++;
}
fetch(`https://oauth.reddit.com/user/${userId}/about.json`, {
cache: `no-cache`,
method: `get`
})
.then(r => r.json())
.then(result => {
if(result != null)
{
let userData = users.get(userId);
userData.rating = result.data.link_karma + result.data.comment_karma / 2;
if(result.data.subreddit.title)
{
userData.nick = result.data.subreddit.title;
}
userData.created = result.data.created;
userData.accountId = result.kind + `_` + result.data.id;
users.set(userId, userData);
for (let comment of loadingComments[userId])
{
renderUserInfo(comment);
}
loadingComments[userId] = null;
currentLoads--;
if(currentLoads == 0)
{
loadingComments = {};
}
}
});
}
// TODO: need replace vars in top
function renderUserInfo(comment)
{
let nickName = comment.querySelector(`faceplate-tracker[noun="comment_author"]`).querySelector(`a`);
const userId = comment.getAttribute(`author`);
let userData = users.get(userId);
if(settings.isEnabled(SETTING_SHOW_NAMES) && userData.nick != undefined && userData.nick)
{
nickName.textContent = userData.nick;
}
// skip render user info
if(settings.isDisabled(SETTING_USER_INFO)) return;
const infoAnchor = comment.querySelector(`div[pp-anchor="info"]`);
const rating = document.createElement(`div`);
rating.classList.add(`text-neutral-content-weak`, `text-12`);
rating.textContent = (userData.rating < 10000) ? `${Math.round(userData.rating / 100) / 10}K` : `${Math.round(userData.rating / 1000)}K`;
infoAnchor.after(rating);
const point = document.createElement(`span`);
point.classList.add(`inline-block`, `my-0`, `mx-2xs`, `text-12`, `text-neutral-content-weak`);
point.textContent = `•`;
rating.after(point);
if(userData.created > (Date.now()/1000 - NEWUSER_SECONDS_SHIFT))
{
const tagsAnchor = comment.querySelector(`div[pp-anchor="tags"]`);
const newSvg = buildSvg(20, 20, newUserGraphic);
newSvg.setAttribute(`viewBox`, `-2 -2 20 20`);
newSvg.style.color = `#69b508`;
tagsAnchor.before(newSvg);
}
}
function awaitContextMenu(comment)
{
let contextMenuButton = comment.querySelector(`shreddit-overflow-menu`)?.shadowRoot?.querySelector(`faceplate-dropdown-menu`);
if(contextMenuButton == null)
{
setTimeout(() => { awaitContextMenu(comment); }, 50);
return;
}
contextMenuButton.addEventListener(`click`, () => { renderContextMenu(comment); }, {once:true});
}
css.addRule('.pp_tagsPanel { display: flex; justify-content: space-around; width:auto; border-bottom: solid 1px var(--color-neutral-border-weak); padding: 4px; gap: 8px; margin-bottom: 4px; }');
css.addRule('.pp_tagButton { cursor:pointer; display: flex; align-content: center; flex-wrap: wrap; height:45px; padding: 4px 20px; margin: 0px 0px; color: var(--color-neutral-border-weak); border-radius: 5px; }');
css.addRule('.pp_tagButton svg { width:20px; transition: transform 0.15s; }');
css.addRule('.pp_tagButton:hover svg { transform: scale(1.2, 1.2); transition: transform 0.3s; }');
css.addRule('.pp_tagButton:hover { background-color: var(--color-neutral-background-hover); }');
css.addRule('.pp_tagButtonActive:hover { opacity: 0.8; }');
css.addRule('.pp_tagHintOffset { left: 50%; position:absolute; }');
css.addRule('.pp_tagHintContainer { display: flex; justify-content: center; position:fixed; }');
css.addRule('.pp_tagHint { display: flex; align-items: center; position: absolute; top: -35px; height: 25px; padding: 0px 12px; color: var(--color-neutral-background-weak); font: var(--font-small); background-color: var(--color-neutral-content-strong); border-radius: 5px;}');
function renderContextMenu(comment)
{
// close other context menu
document.body.click();
let contextMenuButton = comment.querySelector(`shreddit-overflow-menu`).shadowRoot;
let originButton = contextMenuButton.querySelector(`faceplate-tracker[noun="report"]`);
let contextMenuContainer = originButton.parentNode;
if(settings.isEnabled(SETTING_HIDE_SHARE))
{
let linkButton = originButton.cloneNode(true);
linkButton.querySelector(`span .text-14`).textContent = `Copy link`;
originButton.before(linkButton);
let linkPath = linkButton.querySelector(`path`);
linkPath.setAttribute(`d`, linkGraphics[0].d);
let linkPathB = linkPath.cloneNode(true);
linkPathB.setAttribute(`d`, linkGraphics[1].d);
linkPath.after(linkPathB);
const permalink = comment.getAttribute(`permalink`);
linkButton.addEventListener(`click`, () => {
navigator.clipboard.writeText(`https://www.reddit.com${permalink}`);
notify(`Link copied`);
});
}
css.registry(contextMenuButton);
// close context menu
let openButton = contextMenuButton.querySelector(`button`);
openButton.addEventListener(`click`, () => {document.body.click(); });
// skip render tags
if(settings.isDisabled(SETTING_USER_TAGS)) return;
const tagHintOffset = document.createElement(`div`);
tagHintOffset.classList.add(`pp_tagHintOffset`);
contextMenuContainer.prepend(tagHintOffset);
const tagHintContainer = document.createElement(`div`);
tagHintContainer.classList.add(`pp_tagHintContainer`);
tagHintOffset.prepend(tagHintContainer);
const tagHint = AppendNew(tagHintContainer, `div`, `pp_tagHint`);
tagHint.style.display = `none`;
const tagsPanel = document.createElement(`div`);
tagsPanel.classList.add(`pp_tagsPanel`);
tagHintOffset.after(tagsPanel);
const userId = comment.getAttribute(`author`);
renderTagButton(USERTAG_FOLLOWED);
renderTagButton(USERTAG_LIKED);
renderTagButton(USERTAG_WARNING);
renderTagButton(USERTAG_BLOCKED);
function renderTagButton(tag)
{
const tagButton = AppendNew(tagsPanel, `span`, `pp_tagButton`);
tagButton.setAttribute(`tag`, tag);
const subscribeIcon = buildSvg(20, 20, tagButtonGraphics[tag], {w:2, f:`none`});
tagButton.appendChild(subscribeIcon);
tagButton.addEventListener(`click`, () => { tagButtonClick(userId, tag, tagButton); });
tagButton.addEventListener(`mouseenter`, () => {tagHintEnter(userId, tag, tagButton, tagHint); });
tagButton.addEventListener(`mouseleave`, () => {tagHintOut(userId, tag, tagButton, tagHint); });
}
refreshTagButtons(tagsPanel, userId);
}
function refreshTagButtons(tagsPanel, userId)
{
const tagsData = tags.get(userId);
const tagsList = tagsData?.tags ?? [];
tagsPanel.querySelectorAll(`.pp_tagButton`).forEach(button => {
const tag = button.getAttribute(`tag`);
button.removeAttribute(`has-cooldown`);
button.removeAttribute(`has-blocked`);
if(tagsList.includes(tag))
{
button.classList.add(`pp_tagButtonActive`);
button.style.backgroundColor = tagConfigs[tag][TAG_CONFIG_COLOR];
button.style.color = `white`;
}
else
{
button.classList.remove(`pp_tagButtonActive`);
button.style.color = tagConfigs[tag][TAG_CONFIG_COLOR];
button.style.removeProperty(`background-color`);
if(tag == USERTAG_BLOCKED && tagsData.blockCooldown != undefined && Date.now()/1000 < tagsData.blockCooldown)
{
button.setAttribute(`has-cooldown`, ``);
button.style.color = `#adadad`;
}
if(tag == USERTAG_FOLLOWED && tagsList.includes(USERTAG_BLOCKED))
{
button.setAttribute(`has-blocked`, ``);
button.style.color = `#adadad`;
}
}
});
}
function tagButtonClick(userId, tag, button)
{
if(button.getAttribute(`has-cooldown`) != null || button.getAttribute(`has-blocked`) != null)
{
notify(`Unable to do this`);
return;
}
let tagsData = tags.get(userId);
if(tagsData.tags == undefined)
{
tagsData.tags = [];
}
let isAdded = false;
if(tagsData.tags.includes(tag))
{
tagsData.tags = tagsData.tags.filter(t => t != tag);
}
else
{
tagsData.tags.push(tag);
isAdded = true;
// auto clear follow state
if(tag == USERTAG_BLOCKED)
{
tagsData.tags = tagsData.tags.filter(t => t != USERTAG_FOLLOWED);
}
}
if(tagsData.tags.length > 1)
{
tagsData.tags.sort((firstItem, secondItem) => tagConfigs[firstItem][TAG_CONFIG_PRIORITY] - tagConfigs[secondItem][TAG_CONFIG_PRIORITY]);
}
tags.set(userId, tagsData);
// refresh comments tags
document.body.querySelectorAll(`shreddit-comment[author="${userId}"]`).forEach(comment => {
renderCommentTags(comment);
if(isAdded && tag == USERTAG_BLOCKED)
{
comment.setAttribute(`collapsed`, ``);
}
});
// execute specific operations
if(tag == USERTAG_FOLLOWED)
{
executeOperation(`UpdateProfileFollowState`, isAdded ? `FOLLOWED` : `NONE`, userId);
}
if(tag == USERTAG_BLOCKED)
{
if(!isAdded)
{
tagsData.blockCooldown = Date.now()/1000 + BLOCK_COOLDOWN_SECONDS;
tags.set(userId, tagsData);
}
executeOperation(`UpdateRedditorBlockState`, isAdded ? `BLOCKED` : `NONE`, userId);
}
// refresh context menu
refreshTagButtons(button.parentNode, userId);
}
function tagHintEnter(userId, tag, button, tagHint)
{
tagHint.style.display = null;
tagHint.dataset.target = tag;
const tagsData = tags.get(userId);
tagHint.innerText = tagConfigs[tag][(tagsData?.tags ?? []).includes(tag) ? TAG_CONFIG_HINT_ACTIVATED : TAG_CONFIG_HINT];
if(button.getAttribute(`has-cooldown`) != null)
{
const cooldownHours = Math.round((tagsData.blockCooldown - Date.now()/1000) / HOUR_SECONDS);
tagHint.innerText = `Unable block for ${cooldownHours}h after unblocking`;
}
if(button.getAttribute(`has-blocked`) != null)
{
tagHint.innerText = `Unable to follow on blocked user`;
}
}
function tagHintOut(userId, tag, button, tagHint)
{
if(tagHint.dataset?.target == tag)
{
tagHint.style.display = `none`;
}
}
function executeOperation(operation, state, userId)
{
let userData = users.get(userId);
const body = {
csrf_token:getCookie(`csrf_token`),
operation:operation,
variables:{
input: operation == `UpdateRedditorBlockState` ? {
redditorId:userData.accountId,
blockState:state
} : {
accountId:userData.accountId,
state:state,
}
}
};
fetch(`https://www.reddit.com/svc/shreddit/graphql`, {
method: `post`,
headers: new Headers({
'Accept': `application/json`,
'Content-Type': `application/json`
}),
body: JSON.stringify(body)
})
.then(r => r.json())
.then(result => {
if(result != null && result.errors?.message)
{
notify(result.errors.message);
}
});
}
// ***********************************************************************************************************************
// *********************************************** SETTINGS **************************************************************
// ***********************************************************************************************************************
css.addRule(' .pp_SettingSubtittle { font-size: 10px; font-weight: 700; letter-spacing: 0.5px; line-height: 12px; min-height: 20px; color: #7c7c7c; text-transform: uppercase; border-bottom: 1px solid #edeff1; margin-top: 1rem; padding: 0rem 3rem;}');
css.addRule(' .pp_SettingPropertyHeader { display: flex; flex-direction: column; margin-left: 3rem; justify-content: center; }');
css.addRule(' .pp_SettingPropertyHeaderTittle { font-size: 16px; font-weight: 500; line-height: 20px; margin-bottom: 4px; }');
css.addRule(' .pp_SettingPropertyHeaderDescription { font-size: 12px; font-weight: 400; line-height: 16px; color: #7c7c7c; }');
css.addRule(' .pp_SettingPropertyButtonContainer { display: flex; align-items: center; justify-content:flex-end; flex-grow:1; }');
css.addRule(' .pp_SettingChangesBannerContainer { position: absolute; top: 0px; width: 900px; overflow-y: hidden; opacity: 0; transition: opacity 0.15s ease-in-out; }');
css.addRule(' .pp_SettingChangesBanner { display: flex; justify-content: center; margin: 2rem 15%; padding: 1rem; border-radius: 15px; background-color: #ffd40017; border: solid 1px #ffd400; color: #d7b300; font-weight: 500; }');
css.addRule(' .pp_SettingChangesBannerActive { opacity: 1 !important; }');
function renderSettingsWindow(win, context)
{
// close user context menu
document.body.click();
win.changes = 0;
const changesBannerContainer = AppendNew(win.content, `div`, `pp_SettingChangesBannerContainer`);
const changesBanner = AppendNew(changesBannerContainer, `div`, `pp_SettingChangesBanner`);
changesBanner.textContent = `Page will be reloaded to apply new settings`;
const scroll = AppendNew(win.content, `div`, [`pp_WindowScrollContent`, `styled-scrollbars`]);
const elements = AppendNew(scroll, `div`, `pp_WindowElementsContainer`);
addSettingToggle(`Wide mode`, `Fit sidebars to screen borders and limit content width to 700px`, SETTING_WIDE_MODE);
addSubtittle(`Left sidebar`);
addSettingToggle(`Show Custom feeds`, null, SETTING_SIDEBAR_CUSTOMS);
addSettingToggle(`Show Recent`, null, SETTING_SIDEBAR_RECENT);
addSettingToggle(`Show Communities`, null, SETTING_SIDEBAR_SUBS);
addSettingToggle(`Show Resources`, null, SETTING_SIDEBAR_RESOURCES);
addSubtittle(`Feed`);
addSettingToggle(`Feed buttons`, `Unwrap feed sorting buttons`, SETTING_FEED_BUTTONS);
addSettingToggle(`Flair bar`, `Display available flairs to faster navigation. Specific flairs may be hidden in subreddit flairs settings.`, SETTING_FLAIR_BAR);
addSettingToggle(`Soft backplates`, `Make post backplates with gradient color`, SETTING_BACKPLATES);
addSubtittle(`Comments`);
addSettingToggle(`User info`, `Show user karma and leaf mark for new user`, SETTING_USER_INFO);
addSettingToggle(`User tags`, `Enable custom tags for users`, SETTING_USER_TAGS);
addSettingToggle(`Show names`, `Use display names (if user set it) instead usernames`, SETTING_SHOW_NAMES);
addSettingToggle(`Bigger fonts`, `Make fonts bigger for better reading`, SETTING_BIGGER_FONTS);
addSettingToggle(`Hide share button`, `Replace share button to context menu`, SETTING_HIDE_SHARE);
addSettingToggle(`Ghosted comments`, `Make comments ghosted when karma below zero`, SETTING_GHOSTED_COMMENTS);
addSettingToggle(`Collapse Automoderator`, `Automatic collapse all automoderator comments`, SETTING_COLLAPSE_AUTOMODERATOR);
function addSubtittle(text)
{
const subtittle = AppendNew(elements, `h3`, `pp_SettingSubtittle`);
subtittle.textContent = text;
}
function addSettingToggle(tittleText, descriptionText, settingName)
{
const propertyArea = AppendNew(elements, `div`, `pp_WindowElement`);
const header = AppendNew(propertyArea, `div`, `pp_SettingPropertyHeader`);
const tittle = AppendNew(header, `div`, `pp_SettingPropertyHeaderTittle`);
tittle.textContent = tittleText;
if(descriptionText != null)
{
const description = AppendNew(header, `div`, `pp_SettingPropertyHeaderDescription`);
description.textContent = descriptionText;
}
const buttonContainer = AppendNew(propertyArea, `div`, `pp_SettingPropertyButtonContainer`);
const toggleArea = AppendNew(buttonContainer, `div`, `pp_fs_flairPanelCheckboxArea`);
const toggleContainer = AppendNew(toggleArea, `div`, `pp_checkBoxContainer`);
const toggleBack = AppendNew(toggleContainer, `button`, `pp_checkBoxButton`);
toggleBack.classList.toggle(`pp_checkBoxButtonActive`, settings.get(settingName) != false);
const knob = AppendNew(toggleBack, `div`, `pp_checkBoxKnob`);
let changed = false;
toggleBack.addEventListener(`click`, e => {
let state = !(settings.get(settingName) == false);
toggleBack.classList.toggle(`pp_checkBoxButtonActive`, !state);
settings.set(settingName, !state);
win.changes += changed ? -1 : 1;
changed = !changed;
changesBannerContainer.classList.toggle(`pp_SettingChangesBannerActive`, win.changes > 0);
});
}
}
// ***********************************************************************************************************************
// ************************************************* UTILS ***************************************************************
// ***********************************************************************************************************************
function getCurrentSub()
{
const raw = window.location.href.split(`reddit.com/r/`);
return raw.length > 1 ? raw[1].split(`/`)[0] : null;
}
function checkIsRendered(node)
{
if(node.getAttribute(`pp-rendered`) != null)
{
return true;
}
else
{
node.setAttribute(`pp-rendered`, ``);
return false;
}
}
function notify(message, actionTittle, actionCallback)
{
const hasAction = actionTittle != undefined;
let toaster = document.body?.querySelector(`alert-controller`)?.shadowRoot?.querySelector(`toaster-lite`);
if(toaster == null)
{
console.log(`Reddit++: Failed to notify (${message})`);
return;
}
let toast = document.createElement(`faceplate-toast`);
toast.classList.add(`theme-rpl`);
toast.textContent = message;
toaster.appendChild(toast);
setTimeout(() => {toast.setAttribute(`_fading`, ``); }, hasAction ? 8000 : 3000);
}
function buildSvg(w, h, graphics, settings)
{
const XMLNS = `http://www.w3.org/2000/svg`;
const svg = document.createElementNS(XMLNS, `svg`);
svg.setAttribute(`viewBox`, `0 0 ${w} ${h}`);
svg.setAttribute(`width`, `${w}px`);
svg.setAttribute( `height`, `${h}px`);
svg.setAttribute(`fill`, `none`);
for(var graphic of graphics)
{
let path = document.createElementNS(XMLNS, `path`);
path.setAttribute(`stroke-linecap`, `round`);
path.setAttribute(`stroke-linejoin`, `round`);
path.setAttribute(`stroke`, graphic?.c ?? (settings?.c ?? `currentColor`));
path.setAttribute(`stroke-width`, graphic?.w ?? (settings?.w ?? 1));
path.setAttribute(`fill`, graphic?.f ?? (settings?.f ?? `currentColor`));
path.setAttribute(`d`, graphic.d);
svg.appendChild(path);
}
return svg;
}
function AppendNew(prev, name, classes)
{
const el = document.createElement(name);
if(classes != undefined)
{
if(typeof classes === `string` && classes)
{
el.classList.add(classes);
}
else
{
for(const c of classes)
{
el.classList.add(c);
}
}
}
prev.append(el);
return el;
}
function getSettingsRevision()
{
return localStorage.getItem(`pp_settings_revision`) ?? 0;
}
function nextSettingsRevision()
{
settingsRevision++;
localStorage.setItem(`pp_settings_revision`, settingsRevision);
}
function getCookie(key)
{
return document.cookie
.split(`; `)
.find((row) => row.startsWith(key))
?.split(`=`)[1];
}
function awaitElement(element, callback)
{
if(element == null)
{
setTimeout(() => { callback(document.body); }, 10);
return true;
}
return false;
}
})();