Sharty Fixes 2025

Fixes/Enhancements for the 'party

  1. // ==UserScript==
  2. // @name Sharty Fixes 2025
  3. // @namespace soyjak.party
  4. // @match https://soyjak.party/*
  5. // @match https://soyjak.st/*
  6. // @version 1.152
  7. // @author Xyl (Currently Maintained by Swedewin)
  8. // @license MIT
  9. // @description Fixes/Enhancements for the 'party
  10. // ==/UserScript==
  11.  
  12. const version = "v1.152";
  13. console.log(`Sharty fixes ${version}`);
  14.  
  15. const namespace = "ShartyFixes.";
  16. function setValue(key, value) {
  17. if (key == "hiddenthreads" || key == "hiddenimages") {
  18. if (typeof GM_setValue == "function") {
  19. GM_setValue(key, value);
  20. }
  21. localStorage.setItem(key, value);
  22. } else {
  23. if (typeof GM_setValue == "function") {
  24. GM_setValue(namespace + key, value);
  25. } else {
  26. localStorage.setItem(namespace + key, value);
  27. }
  28. }
  29. }
  30.  
  31. function getValue(key) {
  32. if (key == "hiddenthreads" || key == "hiddenimages") {
  33. if (typeof GM_getValue == "function" && GM_getValue(key)) {
  34. localStorage.setItem(key, GM_getValue(key).toString());
  35. }
  36. return localStorage.getItem(key);
  37. }
  38. if (typeof GM_getValue == "function") {
  39. return GM_getValue(namespace + key);
  40. } else {
  41. return localStorage.getItem(namespace + key);
  42. }
  43. }
  44.  
  45. function isEnabled(key) {
  46. let value = getValue(key);
  47. if (value == null) {
  48. value = optionsEntries[key][2];
  49. setValue(key, value);
  50. }
  51. return value.toString() == "true";
  52. }
  53.  
  54. function getNumber(key) {
  55. let value = parseInt(getValue(key));
  56. if (Number.isNaN(value)) {
  57. value = 0;
  58. }
  59. return value;
  60. }
  61.  
  62. function getJson(key) {
  63. let value = getValue(key);
  64. if (value == null) {
  65. value = "{}";
  66. }
  67. return JSON.parse(value);
  68. }
  69.  
  70. function addToJson(key, jsonKey, value) {
  71. let json = getJson(key);
  72. let parent = json;
  73. jsonKey.split(".").forEach((e, index, array) => {
  74. if (index < array.length - 1) {
  75. if (!parent.hasOwnProperty(e)) {
  76. parent[e] = {};
  77. }
  78. parent = parent[e];
  79. } else {
  80. parent[e] = value;
  81. }
  82. });
  83. setValue(key, JSON.stringify(json));
  84. return json;
  85. }
  86.  
  87. function removeFromJson(key, jsonKey) {
  88. let json = getJson(key);
  89. let parent = json;
  90. jsonKey.split(".").forEach((e, index, array) => {
  91. if (index < array.length - 1) {
  92. parent = parent[e];
  93. } else {
  94. delete parent[e];
  95. }
  96. });
  97. setValue(key, JSON.stringify(json));
  98. return json;
  99. }
  100.  
  101. function customAlert(a) {
  102. document.body.insertAdjacentHTML("beforeend", `
  103. <div id="alert_handler">
  104. <div id="alert_background" onclick="this.parentNode.remove()"></div>
  105. <div id="alert_div">
  106. <a id='alert_close' href="javascript:void(0)" onclick="this.parentNode.parentNode.remove()"><i class='fa fa-times'></i></a>
  107. <div id="alert_message">${a}</div>
  108. <button class="button alert_button" onclick="this.parentNode.parentNode.remove()">OK</button>
  109. </div>
  110. </div>`);
  111. }
  112.  
  113. const optionsEntries = {
  114. "show-quote-button": ["checkbox", "Show quick quote button", false],
  115. "mass-reply-quote": ["checkbox", "Enable mass reply and mass quote buttons", true],
  116. "anonymise": ["checkbox", "Anonymise name and tripfags", false],
  117. "hide-blotter": ["checkbox", "Always hide blotter", false],
  118. "truncate-long-posts": ["checkbox", "Truncate line spam", true],
  119. "disable-submit-on-cooldown": ["checkbox", "Disable submit button on cooldown", false],
  120. "force-exact-time": ["checkbox", "Show exact time", false],
  121. "hide-sage-images": ["checkbox", "Hide sage images by default (help mitigate gross spam)", false],
  122. "catalog-navigation": ["checkbox", "Board list links to catalogs when on catalog", true]
  123. }
  124. let options = Options.add_tab("sharty-fixes", "gear", "Sharty Fixes").content[0];
  125. let optionsHTML = `<span style="display: block; text-align: center">${version}</span>`;
  126. optionsHTML += `<a style="display: block; text-align: center" href="https://booru.soyjak.st/post/list/variant%3Aimpish_soyak_ears/">#Impishgang</a><br>`;
  127. for ([optKey, optValue] of Object.entries(optionsEntries)) {
  128. optionsHTML += `<input type="${optValue[0]}" id="${optKey}" name="${optKey}"><label for="${optKey}">${optValue[1]}</label><br>`;
  129. }
  130. options.insertAdjacentHTML("beforeend", optionsHTML);
  131.  
  132. options.querySelectorAll("input[type=checkbox]").forEach(e => {
  133. e.checked = isEnabled(e.id);
  134. e.addEventListener("change", e => {
  135. setValue(e.target.id, e.target.checked);
  136. });
  137. });
  138.  
  139. // redirect (for some reason breaks hidden threads if removed)
  140. if (location.origin.match(/(http:|\/www)/g)) {
  141. location.replace(`https://soyjak.party${location.pathname}${location.hash}`);
  142. }
  143.  
  144. const board = window.location.pathname.split("/")[1];
  145.  
  146. // post fixes
  147. const weekdays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
  148. const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
  149.  
  150. // Update observer for active thread
  151. if (document.body.classList.contains("active-thread")) {
  152. const updateObserver = new MutationObserver(list => {
  153. const evt = new CustomEvent("post_update", { detail: list });
  154. document.dispatchEvent(evt);
  155. });
  156. updateObserver.observe(document.querySelector(".thread"), { childList: true });
  157. }
  158.  
  159. let intervals = {};
  160.  
  161. // Fix individual post
  162. function fixPost(post) {
  163. let timeElement = post.querySelector("[datetime]");
  164. let time = new Date(Date.parse(timeElement.getAttribute("datetime")));
  165. let postNumber = post.getElementsByClassName("post_no")[1];
  166. let postText = postNumber.textContent;
  167.  
  168. // Hide images for sage posts
  169. if (email = post.querySelector("a.email")) {
  170. if (isEnabled("hide-sage-images") && !post.classList.contains("image-hide-processed") && email.href.match(/mailto:sage$/i)) {
  171. let localStorageBackup = localStorage.getItem("hiddenimages");
  172. let interval = setInterval(() => {
  173. if (document.querySelector(`[id*="${postText}"] .hide-image-link`)) {
  174. post.classList.add("image-hide-processed");
  175. clearInterval(interval);
  176. document.querySelector(`[id*="${postText}"] .files`)
  177. .querySelectorAll(".hide-image-link:not([style*='none'])")
  178. .forEach(e => e.click());
  179. localStorage.setItem("hiddenimages", localStorageBackup);
  180. }
  181. }, 50);
  182. }
  183. }
  184.  
  185. // Check if post is own post
  186. let isOwnPost = false;
  187. try {
  188. isOwnPost = JSON.parse(localStorage.getItem("own_posts"))[board].includes(post.querySelector(".post_no[onclick*=cite]").innerText);
  189. } catch { }
  190.  
  191. if (isOwnPost && getNumber("lastTime") < time.getTime()) {
  192. setValue("lastTime", time.getTime());
  193. }
  194.  
  195. // Format time
  196. timeElement.outerHTML = `<span datetime=${timeElement.getAttribute("datetime")}>${timeElement.innerText}</span>`;
  197. post.querySelector(".intro").insertAdjacentHTML("beforeend", `<span class="quote-buttons"></span>`);
  198.  
  199. // Add quote buttons if enabled
  200. if (isEnabled("show-quote-button")) {
  201. post.querySelector(".quote-buttons").insertAdjacentHTML("beforeend", `<a href="javascript:void(0);" class="quick-quote">[>]</a>`);
  202. post.querySelector(".quote-buttons").insertAdjacentHTML("beforeend", `<a href="javascript:void(0);" class="quick-orange">[<]</a>`);
  203. }
  204.  
  205. // Mass reply/quote options for OP posts
  206. if (isEnabled("mass-reply-quote") && post.classList.contains("op") && post.closest(".active-thread")) {
  207. document.querySelector(".quote-buttons").insertAdjacentHTML("beforeend", `<a href="javascript:void(0);" id="mass-reply">[Mass Reply]</a><a href="javascript:void(0);" id="mass-quote">[Mass Quote]</a><a href="javascript:void(0);" id="mass-orange">[Mass Orange]</a>`);
  208. }
  209.  
  210. // Handle post text formatting
  211. let body = post.querySelector(".body");
  212. body.childNodes.forEach(e => {
  213. if (e.nodeType === 3) {
  214. let span = document.createElement("span");
  215. span.innerText = e.textContent;
  216. e.parentNode.replaceChild(span, e);
  217. }
  218. });
  219.  
  220. // Format post number and add click event for citations
  221. if (document.body.classList.contains("active-thread")) {
  222. postNumber.href = `#q${postNumber.textContent}`;
  223. postNumber.setAttribute("onclick", `$(window).trigger('cite', [${postNumber.textContent}, null]);`);
  224. postNumber.addEventListener("click", () => {
  225. let selection = window.getSelection().toString();
  226. document.querySelectorAll("textarea[name=body]").forEach(e => {
  227. e.value += `>>${postNumber.textContent}\n${selection !== "" ? selection.replace(/(\r\n|\r|\n|^)/g, "$1>") : ""}`;
  228. });
  229. });
  230. }
  231.  
  232. // Anonymize post if enabled
  233. if (isEnabled("anonymise")) {
  234. post.querySelector(".name").textContent = "Chud";
  235. if (trip = post.querySelector(".trip")) {
  236. trip.remove();
  237. }
  238. }
  239.  
  240. // DO NOT REMOVE THIS, IT WILL BREAK THE SCRIPT.
  241. undoFilter(post);
  242. }
  243.  
  244. // Add expandos for truncating long posts
  245. function addExpandos() {
  246. if (isEnabled("truncate-long-posts")) {
  247. document.querySelectorAll(".post").forEach(e => {
  248. let body = e.querySelector(".body");
  249. e.classList.add("sf-cutoff");
  250.  
  251. if (body.scrollHeight > body.offsetHeight) {
  252. if (!e.querySelector(".sf-expander")) {
  253. body.insertAdjacentHTML("afterend", `<br><a href="javascript:void(0)" class="sf-expander"></a>`);
  254. }
  255. if (e.getAttribute("manual-cutoff") === "false" || (window.location.hash.includes(e.id.split("_")[1]) && !e.getAttribute("manual-cutoff"))) {
  256. e.classList.remove("sf-cutoff");
  257. }
  258. } else if (body.scrollHeight === body.offsetHeight) {
  259. if (expander = e.querySelector(".sf-expander")) {
  260. expander.remove();
  261. }
  262. e.classList.remove("sf-cutoff");
  263. }
  264. });
  265. }
  266. }
  267.  
  268. window.addEventListener("resize", () => addExpandos());
  269.  
  270. // Function to modify the report form (File label, Urgent label, and Reason box)
  271. function modifyReportForm(form) {
  272. // Skip modification if the form has already been modified
  273. if (form.dataset.modified === 'true') return;
  274.  
  275. // Modify the "File" label and checkbox
  276. const fileLabel = form.querySelector('label[for^="delete_file_"]');
  277. if (fileLabel) {
  278. fileLabel.textContent = '[ File only ]'; // Modify the label text
  279. const fileCheckbox = form.querySelector('input[type="checkbox"][name="file"]');
  280. if (fileCheckbox) {
  281. // Move the checkbox inside the label
  282. fileLabel.insertBefore(fileCheckbox, fileLabel.firstChild);
  283. }
  284. }
  285.  
  286. // Modify the "Urgent report" label and checkbox
  287. const urgentCheckbox = form.querySelector('#urgent-checkbox');
  288. if (urgentCheckbox) {
  289. const urgentLabel = form.querySelector('label[for="urgent-checkbox"]');
  290. if (urgentLabel) {
  291. urgentLabel.textContent = 'Urgent'; // Modify the label text
  292. // Move the checkbox inside the label
  293. urgentLabel.parentNode.insertBefore(urgentCheckbox, urgentLabel);
  294. }
  295. }
  296.  
  297. // Set Reason input width equal to Password input width
  298. const passwordInput = form.querySelector('input[type="password"][name="password"]');
  299. const reasonInput = form.querySelector('input[type="text"][name="reason"]');
  300. if (passwordInput && reasonInput) {
  301. // Set the Reason input size equal to Password input's size
  302. reasonInput.setAttribute('size', passwordInput.getAttribute('size'));
  303. }
  304.  
  305. // Mark the form as modified using a dataset attribute to prevent re-modification
  306. form.dataset.modified = 'true';
  307. }
  308.  
  309. // Function to observe changes to the page (to handle dynamic form rendering)
  310. function observeFormChanges() {
  311. // Find all forms on the page that are for reporting
  312. const forms = document.querySelectorAll('form.post-actions');
  313.  
  314. forms.forEach(form => {
  315. modifyReportForm(form); // Apply modification to the form
  316. });
  317. }
  318.  
  319. // Initial run when the page loads
  320. window.addEventListener('load', function() {
  321. // Apply changes to any forms already present on the page
  322. observeFormChanges();
  323.  
  324. // Set up a MutationObserver to watch for changes in the DOM
  325. const observer = new MutationObserver((mutationsList) => {
  326. for (let mutation of mutationsList) {
  327. if (mutation.type === 'childList') {
  328. // Check for changes to form elements
  329. observeFormChanges();
  330. }
  331. }
  332. });
  333.  
  334. // Observe the body of the page for changes
  335. observer.observe(document.body, {
  336. childList: true,
  337. subtree: true
  338. });
  339. });
  340.  
  341. // Listen for any changes to checkboxes to ensure modifications are reapplied dynamically
  342. document.addEventListener('change', function(event) {
  343. if (event.target.classList.contains('delete')) {
  344. // When the checkbox is clicked, check for changes in the form
  345. const form = event.target.closest('form.post-actions');
  346. if (form) {
  347. modifyReportForm(form);
  348. }
  349. }
  350. });
  351.  
  352. // Custom CSS for .deadlink class
  353. const style = `<style>
  354. .deadlink {
  355. text-decoration: line-through !important;
  356. color: #789922;
  357. }
  358. </style>`;
  359. document.head.innerHTML += style;
  360.  
  361. // Modify quote links to be styled as dead links
  362. const quote = document.querySelectorAll('.quote');
  363.  
  364. quote.forEach(elem => {
  365. if (/^&gt;&gt;[1-9]+[0-9]*$/.test(elem.innerHTML)) {
  366. const postNum = elem.innerHTML;
  367. elem.outerHTML = `<span class="deadlink">${postNum}</span>`;
  368. }
  369. });
  370.  
  371. // Fix post times (relative and exact)
  372. function fixTime() {
  373. document.querySelectorAll(".post").forEach(e => {
  374. let timeElement = e.querySelector("[datetime]");
  375. let time = new Date(Date.parse(timeElement.getAttribute("datetime")));
  376. let exactTime = `${("0" + (time.getMonth() + 1)).slice(-2)}/${("0" + time.getDate()).slice(-2)}/${time.getYear().toString().slice(-2)} (${weekdays[time.getDay()]}) ${("0" + time.getHours()).slice(-2)}:${("0" + time.getMinutes()).slice(-2)}:${("0" + time.getSeconds()).slice(-2)}`;
  377. let relativeTime;
  378. let difference = (Date.now() - time.getTime()) / 1000;
  379.  
  380. if (difference < 10) relativeTime = "Just now";
  381. else if (difference < 60) relativeTime = `${Math.floor(difference)} seconds ago`;
  382. else if (difference < 120) relativeTime = `1 minute ago`;
  383. else if (difference < 3600) relativeTime = `${Math.floor(difference / 60)} minutes ago`;
  384. else if (difference < 7200) relativeTime = `1 hour ago`;
  385. else if (difference < 86400) relativeTime = `${Math.floor(difference / 3600)} hours ago`;
  386. else if (difference < 172800) relativeTime = `1 day ago`;
  387. else if (difference < 2678400) relativeTime = `${Math.floor(difference / 86400)} days ago`;
  388. else if (difference < 5356800) relativeTime = `1 month ago`;
  389. else if (difference < 31536000) relativeTime = `${Math.floor(difference / 2678400)} months ago`;
  390. else if (difference < 63072000) relativeTime = `1 year ago`;
  391. else relativeTime = `${Math.floor(difference / 31536000)} years ago`;
  392.  
  393. if (isEnabled("force-exact-time")) {
  394. timeElement.innerText = exactTime;
  395. timeElement.setAttribute("title", relativeTime);
  396. } else {
  397. timeElement.innerText = relativeTime;
  398. timeElement.setAttribute("title", exactTime);
  399. }
  400. });
  401. }
  402.  
  403. // Initialize post fixes
  404. function initFixes() {
  405. // Add formatting help to comment section
  406. document.querySelectorAll("form[name=post] th").forEach(e => {
  407. if (e.innerText === "Comment") {
  408. e.insertAdjacentHTML("beforeend", `<sup title="Formatting help" class="sf-formatting-help">?</sup><br><div class="comment-quotes"><a href="javascript:void(0);" class="comment-quote">[>]</a><a href="javascript:void(0);" class="comment-orange">[<]</a></div>`);
  409. }
  410. });
  411.  
  412. // Add the formatting help next to "Email" column header with a new class
  413. document.querySelectorAll("form[name=post] th").forEach(e => {
  414. if (e.innerText === "Email") {
  415. // Insert the formatting help button next to the "Email" header with a new class
  416. e.insertAdjacentHTML("beforeend", `<sup title="Formatting help for Email" class="sf-formatting-help-email">?</sup>`);
  417. }
  418. });
  419.  
  420. // Handle the click event for the new "sf-formatting-help-email" button
  421. document.addEventListener('click', function(event) {
  422. let t = event.target;
  423.  
  424. // Check if the clicked element is the "sf-formatting-help-email" button (for "Email")
  425. if (t.classList.contains("sf-formatting-help-email")) {
  426. // Trigger the site's existing popup system (if it exists, e.g., customAlert)
  427. customAlert(`
  428. <h1>Email Field</h1>
  429. <p>bump</p>
  430. <p>sage</p>
  431. <p>supersage</p>
  432. <p>anonymous</p>
  433. <p>anonymous sage</p>
  434. <p>flag</p>
  435. <p>flag sage</p>
  436. `); // Show the custom message with multiple lines inside the existing popup
  437. }
  438. });
  439.  
  440. // Add file selection URL input if GM_xmlhttpRequest is available (i don't think this is needed anymore, removing or keeping it doesn't break anything though.)
  441. if (typeof GM_xmlhttpRequest === "function") {
  442. let fileSelectionInterval = setInterval(() => {
  443. if (select = document.querySelector("#upload_selection")) {
  444. select.childNodes[0].insertAdjacentHTML('afterend', ` / <a href="javascript:void(0)" id="sf-file-url"></a>`);
  445. clearInterval(fileSelectionInterval);
  446. }
  447. }, 100);
  448. }
  449.  
  450. // Handle dynamic updates
  451. document.addEventListener("dyn_update", e => {
  452. e.detail.forEach(e => fixPost(e));
  453. fixTime();
  454. addExpandos();
  455. });
  456.  
  457. // Handle post updates
  458. document.addEventListener("post_update", e => {
  459. e.detail.forEach(node => {
  460. if (node.addedNodes[0].nodeName === "DIV") {
  461. fixPost(node.addedNodes[0]);
  462. }
  463. });
  464. fixTime();
  465. addExpandos();
  466. });
  467.  
  468. // Apply fixes to existing posts
  469. [...document.getElementsByClassName("post")].forEach(e => {
  470. fixPost(e);
  471. });
  472. fixTime();
  473. addExpandos();
  474. }
  475.  
  476. // DONT REMOVE THIS PART, IT WILL BREAK THE SCRIPT
  477. // undo filter
  478. function undoFilter(post) {
  479. // if (isEnabled("restore-filtered")) {
  480. // post.querySelectorAll(".body, .body *, .replies, .replies *").forEach(e => {
  481. // e.childNodes.forEach(e => {
  482. // if (e.nodeName == "#text") {
  483. // e.nodeValue = e.nodeValue.replaceAll("im trans btw", "kuz");
  484. // }
  485. // });
  486. // });
  487. // }
  488. }
  489.  
  490.  
  491. // Catalog Fixes: Adjust thread timestamps & undo filters
  492. document.querySelectorAll("#Grid > div").forEach(e => {
  493. let threadTime = new Date(parseInt(e.getAttribute("data-time")) * 1000);
  494. e.getElementsByClassName("thread-image")[0].setAttribute("title", `${months[threadTime.getMonth()]} ${("0" + threadTime.getDate()).slice(-2)}` +
  495. ` ${("0" + threadTime.getHours()).slice(-2)}:${("0" + threadTime.getMinutes()).slice(-2)}`);
  496. undoFilter(e);
  497. });
  498.  
  499. // Keyboard Shortcuts
  500. window.addEventListener("keydown", e => {
  501. if (e.key == "Enter" && (e.ctrlKey || e.metaKey)) {
  502. if (form = e.target.closest("form[name=post]")) {
  503. form.querySelector("input[type=submit]").click();
  504. }
  505. }
  506. });
  507.  
  508.  
  509. // Autofocus Textarea on Certain Pages
  510. if ((textarea = document.querySelector("textarea[name=body]")) && document.documentElement.classList.contains("desktop-style") && window.location.hash[1] != "q") {
  511. textarea.focus({
  512. preventScroll: true
  513. });
  514. }
  515.  
  516. // --- Character Counter Feature ---
  517. const observer = new MutationObserver(() => {
  518. const commentHeader = Array.from(document.querySelectorAll('th')).find(el => el.textContent.includes('Comment'));
  519.  
  520. if (commentHeader) {
  521. // Stop observing once the Comment header is found
  522. observer.disconnect();
  523.  
  524. // Create the character counter container under the "Comment" heading
  525. const counterContainer = document.createElement('div');
  526. counterContainer.style.marginTop = '10px';
  527.  
  528. const counterText = document.createElement('span');
  529. counterText.setAttribute('id', 'char-count');
  530. counterText.textContent = '0 / 24000';
  531.  
  532. counterContainer.appendChild(counterText);
  533. commentHeader.appendChild(counterContainer);
  534.  
  535. // Find the textarea for the comment
  536. const textArea = document.querySelector('textarea[name="body"]');
  537. if (textArea) {
  538. // Update character count as the user types
  539. textArea.addEventListener('input', function() {
  540. const currentLength = textArea.value.length;
  541. const maxLength = 24000;
  542.  
  543. // Update the counter
  544. counterText.textContent = `${currentLength} / ${maxLength}`;
  545. });
  546. }
  547. }
  548. });
  549.  
  550. // Start observing the DOM for changes in the body element
  551. observer.observe(document.body, {
  552. childList: true,
  553. subtree: true
  554. });
  555.  
  556. // Password Box Toggle (Show/Hide Password)
  557. if (passwordBox = document.querySelector("form[name=post] input[name=password]")) {
  558. passwordBox.setAttribute("type", "password");
  559. passwordBox.insertAdjacentHTML("afterend", `<input type="button" name="toggle-password" value="Show">`);
  560. }
  561.  
  562.  
  563. // Form Submit Cooldown
  564. document.querySelectorAll("form[name=post] input[type=submit]").forEach(e => {
  565. e.setAttribute("og-value", e.getAttribute("value"));
  566. });
  567.  
  568. setInterval(() => {
  569. let lastTime = getNumber("lastTime");
  570. let difference = 11 - Math.ceil((Date.now() - lastTime) / 1000);
  571. let buttons = document.querySelectorAll("form[name=post] input[type=submit]");
  572.  
  573. if ([...buttons].find(e => e.value.includes("Post"))) {
  574. return;
  575. } else if (difference > 0) {
  576. let disableButton = isEnabled("disable-submit-on-cooldown");
  577. buttons.forEach(e => {
  578. e.value = `${e.getAttribute("og-value")} (${difference})`;
  579. if (disableButton) {
  580. e.setAttribute("disabled", "disabled");
  581. }
  582. });
  583. } else {
  584. buttons.forEach(e => {
  585. e.value = e.getAttribute("og-value");
  586. e.removeAttribute("disabled");
  587. });
  588. }
  589. }, 100);
  590.  
  591.  
  592. // Thread Hiding Logic
  593. function areHiddenThreads() {
  594. let threadGrid = document.getElementById("Grid");
  595. if (document.querySelector(".catty-thread.hidden")) {
  596. if (!document.getElementById("toggle-hidden")) {
  597. document.querySelector(".desktop-style #image_size, .mobile-style header").insertAdjacentHTML("afterend", `<span id="toggle-hidden"></span>`);
  598. }
  599. } else if (toggleButton = document.getElementById("toggle-hidden")) {
  600. toggleButton.remove();
  601. document.body.classList.remove("showing-hidden");
  602. }
  603. }
  604.  
  605.  
  606. // Catalog Navigation and Search Form
  607. if (document.body.classList.contains("active-catalog")) {
  608. if (isEnabled("catalog-navigation")) {
  609. document.querySelectorAll(".boardlist a[href*='index.html']").forEach(e => e.href = e.href.replace("index.html", "catalog.html"));
  610. }
  611.  
  612. document.querySelector("#image_size").insertAdjacentHTML("afterend", `
  613. <form style="display: inline-block; margin-bottom: 0px; height: 10px;" action="/search.php">
  614. <p>
  615. <input type="text" name="search" placeholder="${board} search">
  616. <input type="hidden" name="board" value="${board}">
  617. <input type="submit" value="Search">
  618. </p>
  619. </form>
  620. `);
  621.  
  622. let hiddenThreads = getJson("hiddenthreads");
  623. let hasThreads = hiddenThreads.hasOwnProperty(board);
  624.  
  625. document.querySelectorAll(".mix").forEach(e => {
  626. e.classList.replace("mix", "catty-thread");
  627. if (hasThreads && hiddenThreads[board].hasOwnProperty(e.getAttribute("data-id"))) {
  628. e.classList.add("hidden");
  629. delete hiddenThreads[board][e.getAttribute("data-id")];
  630. }
  631. if (e.getAttribute("data-sticky") == "true") {
  632. e.parentNode.prepend(e);
  633. }
  634. });
  635.  
  636. if (hasThreads) {
  637. Object.keys(hiddenThreads[board]).forEach(e => {
  638. removeFromJson("hiddenthreads", `${board}.${e}`);
  639. });
  640. }
  641.  
  642. areHiddenThreads();
  643. }
  644.  
  645.  
  646. // Event Listeners for Clicks and Inputs
  647. document.addEventListener("click", e => {
  648. let t = e.target;
  649.  
  650. // Submit button in post form
  651. if (t.matches("form[name=post] input[type=submit]")) {
  652. t.value = t.getAttribute("og-value");
  653.  
  654. // Bypass filter and modify text
  655. if (isEnabled("bypass-filter")) {
  656. let textbox = t.closest("tbody").querySelector("textarea[name=body]");
  657. textbox.value = textbox.value.replaceAll(/(discord)/ig, str => {
  658. let arr = [];
  659. while (!arr.includes("​")) {
  660. arr = [];
  661. [...str].forEach((c, i) => {
  662. if (Math.random() < 0.5 && i != 0) {
  663. arr.push("​");
  664. }
  665. arr.push(c);
  666. if (Math.random() > 0.5 && i != str.length - 1) {
  667. arr.push("​");
  668. }
  669. });
  670. }
  671. return arr.join("");
  672. });
  673. }
  674. }
  675. // Toggle password visibility
  676. else if (t.matches("input[name=toggle-password]")) {
  677. if (passwordBox.getAttribute("type") == "password") {
  678. passwordBox.setAttribute("type", "text");
  679. t.value = "Hide";
  680. } else {
  681. passwordBox.setAttribute("type", "password");
  682. t.value = "Show";
  683. }
  684. }
  685.  
  686. // Mass reply functionality
  687. else if (t.id == "mass-reply") {
  688. let massReply = "";
  689. document.querySelectorAll("[href*='#q']").forEach(e => {
  690. massReply += `>>${e.textContent}\n`;
  691. });
  692. document.querySelectorAll("textarea[name=body]").forEach(e => {
  693. e.value += massReply;
  694. e.focus();
  695. });
  696. }
  697.  
  698. // Mass quote or mass orange functionality
  699. else if (t.id == "mass-quote" || t.id == "mass-orange") {
  700. document.body.classList.add("hide-quote-buttons");
  701. let selection = window.getSelection();
  702. let range = document.createRange();
  703. range.selectNodeContents(document.body);
  704. selection.removeAllRanges();
  705. selection.addRange(range);
  706.  
  707. let massQuote = window.getSelection().toString().replace(/(\r\n|\r|\n|^)/g, t.id == "mass-quote" ? "$1>" : "$1<") + "\n";
  708. selection.removeAllRanges();
  709. document.body.classList.remove("hide-quote-buttons");
  710.  
  711. document.querySelectorAll("textarea[name=body]").forEach(e => {
  712. e.value += massQuote;
  713. e.focus();
  714. });
  715. }
  716.  
  717. // Quick quote or quick orange functionality
  718. else if (t.classList.contains("quick-quote") || t.classList.contains("quick-orange")) {
  719. let quote = t.closest(".post").querySelector(".body").innerText.replace(/(\r\n|\r|\n|^)/g, t.classList.contains("quick-quote") ? "$1>" : "$1<") + "\n";
  720. document.querySelectorAll("textarea[name=body]").forEach(e => {
  721. e.value += quote;
  722. e.focus();
  723. });
  724. }
  725.  
  726. // Comment quote or comment orange functionality
  727. else if (t.classList.contains("comment-quote") || t.classList.contains("comment-orange")) {
  728. document.querySelectorAll("textarea[name=body]").forEach(e => {
  729. e.value = e.value.replace(/(\r\n|\r|\n|^)/g, t.classList.contains("comment-quote") ? "$1>" : "$1<");
  730. e.focus();
  731. });
  732. }
  733.  
  734. // Toggle visibility of threads
  735. else if ((e.shiftKey || (e.detail == 3 && (document.documentElement.matches(".mobile-style") || isEnabled("desktop-triple-click")))) &&
  736. t.matches(".active-catalog .catty-thread *, .active-catalog .catty-thread")) {
  737. e.preventDefault();
  738. let thread = t.closest(".catty-thread");
  739. thread.classList.toggle("hidden");
  740.  
  741. if (thread.classList.contains("hidden")) {
  742. addToJson("hiddenthreads", `${board}.${thread.getAttribute("data-id")}`, Math.floor(Date.now() / 1000));
  743. } else {
  744. removeFromJson("hiddenthreads", `${board}.${thread.getAttribute("data-id")}`);
  745. }
  746. areHiddenThreads();
  747. }
  748.  
  749. // Toggle hidden threads visibility
  750. else if (t.id == "toggle-hidden") {
  751. document.body.classList.toggle("showing-hidden");
  752. }
  753.  
  754. // Hide/unhide thread link functionality
  755. else if (t.classList.contains("hide-thread-link") || t.classList.contains("unhide-thread-link")) {
  756. setValue("hiddenthreads", localStorage.getItem("hiddenthreads"));
  757. }
  758.  
  759. // Hide blotter functionality
  760. else if (t.classList.contains("hide-blotter")) {
  761. setValue("hidden-blotter", document.querySelector(".blotter").innerText);
  762. document.body.classList.add("hidden-blotter");
  763. }
  764.  
  765. // SF-expander button functionality (manual cutoff)
  766. else if (t.classList.contains("sf-expander")) {
  767. t.closest(".post").setAttribute("manual-cutoff", t.closest(".post").classList.toggle("sf-cutoff"));
  768. }
  769.  
  770. // Formatting help popup
  771. else if (t.classList.contains("sf-formatting-help")) {
  772. let help = `
  773. <h1>Comment Field</h1>
  774. <span class="heading">Font Guide</span><br>
  775. <span class="spoiler">**Spoiler**</span><br>
  776. <em>''Italics''</em><br>
  777. <b>'''Bold'''</b><br>
  778. <code>\`\`\`Codetext\`\`\`</code><br>
  779. <u>__Underline__</u><br>
  780. <s>~~Strikethrough~~</s><br>
  781. <big>+=Bigtext=+</big><br>
  782. <span class="rotate">##Spintext##</span><small><sup> (disabled)</sup></small><br>
  783. <span class="quote">&gt;Greentext</span><br>
  784. <span class="quote2">&lt;Orangetext</span><br>
  785. <span class="quote3" style="color: #6577E6;">^Bluetext</span><br>
  786. <span class="heading">==Redtext==</span><br>
  787. <span class="heading2">--Bluetext--</span><br>
  788. <font color="FD3D98"><b>-~-Pinktext-~-</b></font><br>
  789. <span class="glow">%%Glowtext%%</span><br>
  790. <span style="text-shadow:0px 0px 40px #36d7f7, 0px 0px 2px #36d7f7">;;Blueglowtext;;</span><br>
  791. <span style="text-shadow:0px 0px 40px #fffb00, 0px 0px 2px #fffb00">::Yellowglowtext::</span><br>
  792. <span style="text-shadow:0px 0px 40px #ff0000, 0px 0px 2px #ff0000">!!Redglowtext!!</span><small><sup> (put after each word)</sup></small><br>
  793. <span style="background: linear-gradient(to left, red, orange , yellow, green, cyan, blue, violet);-webkit-background-clip: text;-webkit-text-fill-color: transparent;">~-~Rainbowtext~-~</span><br>
  794. <span class="glow"><span style="background: linear-gradient(to left, red, orange , yellow, green, cyan, blue, violet);-webkit-background-clip: text;-webkit-text-fill-color: transparent;">%%~-~Gemeraldtext~-~%%</span></span><br>
  795. <span style="background:#faf8f8;color:#3060a8">(((Zionisttext)))</span><br>
  796. <br>
  797. <span class="heading">Linking</span><br>
  798. Link to a post on the current board<br>
  799. >>1234<br>
  800. Link to another board<br>
  801. >>>/soy/<br>
  802. Link to a post on another board<br>
  803. >>>/soy/1234<br>
  804. <br>
  805. <span class="heading">Wordfilters</span><br>
  806. See <a href="https://wiki.soyjak.st/Wordfilter" target="_blank">https://wiki.soyjak.st/Wordfilter</a><br>
  807. `;
  808. customAlert(help);
  809. }
  810. });
  811.  
  812. // Search Input Filter for Catalog Threads
  813. document.addEventListener("input", e => {
  814. let t = e.target;
  815. if (t.matches("input[name=search]") && document.querySelector(".sf-catty, .active-catalog")) {
  816. document.querySelectorAll(".catty-thread").forEach(e => {
  817. if (e.innerText.toLowerCase().includes(t.value.toLowerCase())) {
  818. e.classList.remove("sf-filtered");
  819. } else {
  820. e.classList.add("sf-filtered");
  821. }
  822. });
  823. }
  824. });
  825.  
  826.  
  827. // Blotter Hide/Show Button
  828. if (blotter = document.querySelector(".blotter")) {
  829. blotter.insertAdjacentHTML("beforebegin", `<a class="hide-blotter" href="javascript:void(0)">[–]</a>`);
  830. if (blotter.innerText == getValue("hidden-blotter") || isEnabled("hide-blotter")) {
  831. document.body.classList.add("hidden-blotter");
  832. }
  833. }
  834.  
  835. document.head.insertAdjacentHTML("beforeend", `
  836. <style>
  837. /* Hide elements in specific conditions */
  838. .hide-blotter {
  839. float: left;
  840. }
  841.  
  842. .hidden-blotter .blotter,
  843. .hidden-blotter .blotter + hr,
  844. .hidden-blotter .hide-blotter,
  845. .catty-thread.hidden,
  846. .showing-hidden .catty-thread,
  847. .mobile-style .g-recaptcha-bubble-arrow,
  848. .catty-thread.sf-filtered,
  849. .showing-hidden .catty-thread.hidden.sf-filtered,
  850. .hide-quote-buttons .quote-buttons {
  851. display: none !important;
  852. }
  853.  
  854. /* Styling for expander button in replies */
  855. .reply .sf-expander {
  856. margin-left: 1.8em;
  857. padding-right: 3em;
  858. padding-bottom: 0.3em;
  859. }
  860.  
  861. .sf-expander::after {
  862. content: "[Hide Full Text]";
  863. }
  864.  
  865. .sf-cutoff .sf-expander::after {
  866. content: "[Show Full Text]";
  867. }
  868.  
  869. /* File URL input styling */
  870. #sf-file-url::after {
  871. content: "URL";
  872. }
  873.  
  874. #sf-file-url.sf-loading::after {
  875. content: "Loading...";
  876. }
  877.  
  878. /* Hover styling for sharty */
  879. #sharty-hover {
  880. pointer-events: none;
  881. position: fixed;
  882. z-index: 500;
  883. }
  884.  
  885. /* Display adjustments for threads */
  886. .catty-thread,
  887. .showing-hidden .catty-thread.hidden {
  888. display: inline-block !important;
  889. }
  890.  
  891. /* Toggle hidden threads button styling */
  892. #toggle-hidden {
  893. text-decoration: underline;
  894. color: #34345C;
  895. cursor: pointer;
  896. user-select: none;
  897. }
  898.  
  899. #toggle-hidden::before {
  900. content: "[Show Hidden]";
  901. }
  902.  
  903. .showing-hidden #toggle-hidden::before {
  904. content: "[Hide Hidden]";
  905. }
  906.  
  907. #image_size + #toggle-hidden {
  908. display: inline-block;
  909. padding-left: 5px;
  910. }
  911.  
  912. header + #toggle-hidden {
  913. display: block;
  914. margin: 1em auto;
  915. width: fit-content;
  916. }
  917.  
  918. /* Recaptcha bubble styling on mobile */
  919. .mobile-style .g-recaptcha-bubble-arrow + div {
  920. position: fixed !important;
  921. left: 50%;
  922. top: 50%;
  923. transform: translate(-50%, -50%);
  924. -webkit-transform: translate(-50%, -50%);
  925. }
  926.  
  927. /* Password input field adjustments */
  928. input[name=toggle-password] {
  929. margin-left: 2px;
  930. }
  931.  
  932. /* Formatting help link styling */
  933. .sf-formatting-help {
  934. cursor: pointer;
  935. }
  936.  
  937. /* Formatting email link styling */
  938. .sf-formatting-help-email {
  939. cursor: pointer;
  940. }
  941.  
  942. /* Comment quote button styling */
  943. .comment-quotes {
  944. text-align: center;
  945. }
  946.  
  947. /* Truncate long posts styling (conditionally enabled) */
  948. ${isEnabled("truncate-long-posts") ? `
  949. .sf-cutoff:not(.post-hover) .body {
  950. overflow: hidden;
  951. word-break: break-all;
  952. display: -webkit-box;
  953. min-width: min-content;
  954. -webkit-line-clamp: 20;
  955. -webkit-box-orient: vertical;
  956. }
  957.  
  958. .sf-cutoff.post-hover .sf-expander {
  959. display: none !important;
  960. }
  961.  
  962. div.post.reply.sf-cutoff div.body {
  963. margin-bottom: 0.3em;
  964. padding-bottom: unset;
  965. }
  966. ` : ""}
  967. </style>
  968. `);
  969.  
  970. // Initialize additional fixes
  971. initFixes();