diff --git a/src/General/Get.ts b/src/General/Get.ts index 6ea0479..7c1d14d 100644 --- a/src/General/Get.ts +++ b/src/General/Get.ts @@ -38,7 +38,7 @@ const Get = { if (index) { return post.clones[+index] } else { return post } }, postFromNode(root): Post { - return Get.postFromRoot($.x(`ancestor-or-self::${g.SITE.xpath.postContainer}[1]`, root)) + return Get.postFromRoot($.x(`ancestor-or-self::${g.SITE.xpath.postContainer}[1]`, root)) as Post }, postDataFromLink(link) { let boardID, postID, threadID diff --git a/src/Miscellaneous/AntiAutoplay.ts b/src/Miscellaneous/AntiAutoplay.ts index 58d0aa8..c46094d 100644 --- a/src/Miscellaneous/AntiAutoplay.ts +++ b/src/Miscellaneous/AntiAutoplay.ts @@ -3,11 +3,7 @@ import { Conf, doc } from "../globals/globals" import $ from "../platform/$" import $$ from "../platform/$$" -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ + const AntiAutoplay = { init() { if (!Conf['Disable Autoplaying Sounds']) { return } @@ -31,10 +27,10 @@ const AntiAutoplay = { }, node() { - return AntiAutoplay.process(this.nodes.comment) + return AntiAutoplay.process(this.node()) }, - process(root) { + process(root: HTMLElement) { for (const iframe of $$('iframe[src*="youtube"][src*="autoplay=1"]', root)) { AntiAutoplay.processVideo(iframe, 'src') } @@ -43,7 +39,7 @@ const AntiAutoplay = { } }, - processVideo(el, attr) { + processVideo(el: HTMLIFrameElement | HTMLObjectElement, attr: 'src' | 'data') { el[attr] = el[attr].replace(/\?autoplay=1&?/, '?').replace('&autoplay=1', '') if (window.getComputedStyle(el).display === 'none') { el.style.display = 'block' } return $.addClass(el, 'autoplay-removed') diff --git a/src/Miscellaneous/Banner.ts b/src/Miscellaneous/Banner.ts index 3fcbbfe..7ae6abb 100644 --- a/src/Miscellaneous/Banner.ts +++ b/src/Miscellaneous/Banner.ts @@ -6,13 +6,7 @@ import $ from "../platform/$" import $$ from "../platform/$$" import { dict } from "../platform/helpers" -/* - * decaffeinate suggestions: - * DS101: Remove unnecessary use of Array.from - * DS102: Remove unnecessary code created because of implicit returns - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ + const Banner = { init() { if (Conf['Custom Board Titles']) { diff --git a/src/Miscellaneous/CatalogLinks.ts b/src/Miscellaneous/CatalogLinks.ts index 87fd67f..d874a24 100644 --- a/src/Miscellaneous/CatalogLinks.ts +++ b/src/Miscellaneous/CatalogLinks.ts @@ -81,7 +81,7 @@ const CatalogLinks = { }, toggle() { - $.event('CloseMenu') + $.event('CloseMenu', { menu: Header.menu }) $.set('Header catalog links', this.checked) return CatalogLinks.set(this.checked) }, diff --git a/src/classes/Board.ts b/src/classes/Board.ts index 5234b60..6773b7a 100644 --- a/src/classes/Board.ts +++ b/src/classes/Board.ts @@ -14,7 +14,7 @@ export default class Board { config: any toString() { return this.ID } - constructor(ID) { + constructor(ID: string) { this.ID = ID this.boardID = this.ID this.siteID = g.SITE.ID @@ -22,7 +22,7 @@ export default class Board { this.posts = new SimpleDict() this.config = BoardConfig.boards?.[this.ID] || {} - g.boards[this] = this + g.boards[this.ID] = this } cooldowns() { diff --git a/src/classes/Callbacks.ts b/src/classes/Callbacks.ts index 2d0c824..e272814 100644 --- a/src/classes/Callbacks.ts +++ b/src/classes/Callbacks.ts @@ -26,7 +26,7 @@ export default class Callbacks { return this[name] = cb } - execute(node, keys = this.keys, force = false) { + execute(node: Post, keys = this.keys, force = false) { let errors if (node.callbacksExecuted && !force) { return } node.callbacksExecuted = true diff --git a/src/classes/Connection.ts b/src/classes/Connection.ts index 115e84e..a63576c 100644 --- a/src/classes/Connection.ts +++ b/src/classes/Connection.ts @@ -4,8 +4,8 @@ import Callbacks from "./Callbacks" export default class Connection { - target: any - origin: any + target: Window | HTMLIFrameElement + origin: string cb: Callbacks constructor(target: Window, origin: string, cb: Callbacks) { this.send = this.send.bind(this) diff --git a/src/classes/DataBoard.ts b/src/classes/DataBoard.ts index f4bd2b9..73ccafe 100644 --- a/src/classes/DataBoard.ts +++ b/src/classes/DataBoard.ts @@ -1,6 +1,7 @@ import { Conf, d, g } from "../globals/globals" import $ from "../platform/$" import { dict, HOUR } from "../platform/helpers" +import { CacheOptions } from "../types/globals" /* * decaffeinate suggestions: @@ -20,7 +21,6 @@ export default class DataBoard { static keys: string[] static changes: string[] key: string - sync: VoidFunction data: any static initClass() { this.keys = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts', 'watchedThreads', 'watcherLastModified', 'customTitles'] @@ -98,7 +98,7 @@ export default class DataBoard { } else if (threadID) { if (!this.data[siteID].boards[boardID]) { return } delete this.data[siteID].boards[boardID][threadID] - return this.deleteIfEmpty({ siteID, boardID }) + return this.deleteIfEmpty({ siteID, boardID, threadID: null }) } else { return delete this.data[siteID].boards[boardID] } @@ -111,7 +111,7 @@ export default class DataBoard { if (threadID) { if (!Object.keys(this.data[siteID].boards[boardID][threadID]).length) { delete this.data[siteID].boards[boardID][threadID] - return this.deleteIfEmpty({ siteID, boardID }) + return this.deleteIfEmpty({ siteID, boardID, threadID: null }) } } else if (!Object.keys(this.data[siteID].boards[boardID]).length) { return delete this.data[siteID].boards[boardID] @@ -157,7 +157,10 @@ export default class DataBoard { setLastChecked(key = 'lastChecked') { return this.save(() => { return this.data[key] = Date.now() - }) + }, () => { + return this.sync?.() + } + ) } get({ siteID, boardID, threadID, postID, defaultValue }) { @@ -203,21 +206,21 @@ export default class DataBoard { } } - ajaxClean(boardID) { + ajaxClean(boardID: string) { const that = this const siteID = g.SITE.ID const threadsList = g.SITE.urls.threadsListJSON?.({ siteID, boardID }) if (!threadsList) { return } - return $.cache(threadsList, function () { + return $.cache(threadsList, () => { if (this.status !== 200) { return } const archiveList = g.SITE.urls.archiveListJSON?.({ siteID, boardID }) if (!archiveList) { return that.ajaxCleanParse(boardID, this.response) } const response1 = this.response - return $.cache(archiveList, function () { + return $.cache(archiveList, () => { if ((this.status !== 200) && (!!g.SITE.archivedBoardsKnown || (this.status !== 404))) { return } return that.ajaxCleanParse(boardID, response1, this.response) - }) - }) + }, { type: 'json' }) as CacheOptions + }, { type: 'json' }) as CacheOptions } ajaxCleanParse(boardID, response1, response2) { diff --git a/src/classes/Post.ts b/src/classes/Post.ts index df82a33..878f4d4 100644 --- a/src/classes/Post.ts +++ b/src/classes/Post.ts @@ -9,6 +9,7 @@ import Callbacks from "./Callbacks" import type Thread from "./Thread" export default class Post { + callbacksExecuted: boolean declare root: HTMLElement declare thread: Thread declare board: Board diff --git a/src/classes/ShimSet.ts b/src/classes/ShimSet.ts index 7954f6d..07f04f1 100644 --- a/src/classes/ShimSet.ts +++ b/src/classes/ShimSet.ts @@ -1,9 +1,8 @@ -import $ from '../platform/$' class ShimSet { - elements: any + elements: Element size: number constructor() { - this.elements = $.dict() + this.elements this.size = 0 } has(value) { diff --git a/src/classes/SimpleDict.ts b/src/classes/SimpleDict.ts index 1dc2ac3..71f660d 100644 --- a/src/classes/SimpleDict.ts +++ b/src/classes/SimpleDict.ts @@ -1,5 +1,3 @@ -import $ from "../platform/$" - export default class SimpleDict { keys: string[] @@ -9,17 +7,15 @@ export default class SimpleDict { push(key: string, data: T): T { key = `${key}` - if (!this[key]) { this.keys.push(key) } - return this[key] = data + this[key] = data + this.keys.push(key) + return data } rm(key: string) { - let i: number key = `${key}` - if ((i = this.keys.indexOf(key)) !== -1) { - this.keys.splice(i, 1) - return delete this[key] - } + delete this[key] + this.keys = this.keys.filter(k => k !== key) } forEach(fn: (value: T) => void): void { @@ -27,10 +23,6 @@ export default class SimpleDict { } get(key: string): T { - if (key === 'keys') { - return undefined - } else { - return $.getOwn(this, key) - } + return this[key] } } diff --git a/src/classes/Thread.ts b/src/classes/Thread.ts index 944001d..e9a00eb 100644 --- a/src/classes/Thread.ts +++ b/src/classes/Thread.ts @@ -7,7 +7,7 @@ import SimpleDict from "./SimpleDict" export default class Thread { catalogViewNative: CatalogThreadNative - ID: number | string + ID: string | number OP: Post isArchived: boolean isClosed: boolean @@ -16,7 +16,7 @@ export default class Thread { board: Board threadID: number boardID: number | string - siteID: number + siteID: number | string fullID: string isDead: boolean isHidden: boolean @@ -52,7 +52,7 @@ export default class Thread { this.nodes = { root: null } - this.board.threads.push(this.ID, this) + this.board.threads.push(this.ID.toString(), this) g.threads.push(this.fullID, this) } diff --git a/src/config/Config.ts b/src/config/Config.ts index 43e91c7..3724353 100644 --- a/src/config/Config.ts +++ b/src/config/Config.ts @@ -763,7 +763,7 @@ const Config = { comment: `\ # Filter Stallman copypasta on /g/: -#/what you\'re refer+ing to as linux/i;boards:g +#/what you're refer+ing to as linux/i;boards:g # Filter posts with 20 or more quote links: #/(?:>>\\d(?:(?!>>\\d)[^])*){20}/ # Filter posts like T H I S / H / I / S: @@ -817,7 +817,7 @@ http://eye.swfchan.com/search/?q=%name;types:swf `, FappeT: { - werk: false + werk: false }, 'Custom CSS': true, @@ -826,29 +826,29 @@ http://eye.swfchan.com/search/?q=%name;types:swf 'Index Mode': 'paged', 'Previous Index Mode': 'paged', 'Index Size': 'small', - 'Show Replies': [true, 'Show replies in the index, and also in the catalog if "Catalog hover expand" is checked.'], - 'Catalog Hover Expand': [false, 'Expand the comment and show more details when you hover over a thread in the catalog.'], - 'Catalog Hover Toggle': [true, 'Turn "Catalog hover expand" on and off by clicking in the catalog.'], - 'Pin Watched Threads': [false, 'Move watched threads to the start of the index.'], - 'Anchor Hidden Threads': [true, 'Move hidden threads to the end of the index.'], - 'Refreshed Navigation': [false, 'Refresh index when navigating through pages.'] + 'Show Replies': [true, 'Show replies in the index, and also in the catalog if "Catalog hover expand" is checked.'], + 'Catalog Hover Expand': [false, 'Expand the comment and show more details when you hover over a thread in the catalog.'], + 'Catalog Hover Toggle': [true, 'Turn "Catalog hover expand" on and off by clicking in the catalog.'], + 'Pin Watched Threads': [false, 'Move watched threads to the start of the index.'], + 'Anchor Hidden Threads': [true, 'Move hidden threads to the end of the index.'], + 'Refreshed Navigation': [false, 'Refresh index when navigating through pages.'] }, Header: { - 'Fixed Header': true, - 'Header auto-hide': false, + 'Fixed Header': true, + 'Header auto-hide': false, 'Header auto-hide on scroll': false, - 'Bottom Header': false, - 'Centered links': false, - 'Header catalog links': false, - 'Bottom Board List': true, - 'Shortcut Icons': true, - 'Custom Board Navigation': true + 'Bottom Header': false, + 'Centered links': false, + 'Header catalog links': false, + 'Bottom Board List': true, + 'Shortcut Icons': true, + 'Custom Board Navigation': true }, archives: { - archiveLists: 'https://4chenz.github.io/archives.json/archives.json', - lastarchivecheck: 0, + archiveLists: 'https://4chenz.github.io/archives.json/archives.json', + lastarchivecheck: 0, archiveAutoUpdate: true }, @@ -937,7 +937,7 @@ https://*.hcaptcha.com 'Alt+c', 'Insert code tags.' ], - 'Eqn tags': [ + 'Eqn tags': [ 'Alt+e', 'Insert eqn tags.' ], @@ -1184,11 +1184,11 @@ https://*.hcaptcha.com 'Autohiding Scrollbar': false, position: { - 'embedding.position': 'top: 50px; right: 0px;', - 'thread-stats.position': 'bottom: 0px; right: 0px;', - 'updater.position': 'bottom: 0px; left: 0px;', + 'embedding.position': 'top: 50px; right: 0px;', + 'thread-stats.position': 'bottom: 0px; right: 0px;', + 'updater.position': 'bottom: 0px; left: 0px;', 'thread-watcher.position': 'top: 50px; left: 0px;', - 'qr.position': 'top: 50px; right: 0px;' + 'qr.position': 'top: 50px; right: 0px;' }, fourchanImageHost: 'i.4cdn.org', diff --git a/src/globals/globals.ts b/src/globals/globals.ts index 5137df9..d2dca5e 100644 --- a/src/globals/globals.ts +++ b/src/globals/globals.ts @@ -61,7 +61,7 @@ export const g: { VERSION: string, NAMESPACE: string, sites: (typeof SWTinyboard)[], - boards: Board[], + boards: SimpleDict, posts?: SimpleDict, threads?: SimpleDict THREADID?: number, @@ -90,7 +90,7 @@ export const E = (function () { const output = function (text: string) { return text.toString().replace(regex, fn) } - output.cat = function (templates) { + output.cat = function (templates: HTMLCollectionOf) { let html = '' for (let i = 0; i < templates.length; i++) { html += templates[i].innerHTML diff --git a/src/platform/$.ts b/src/platform/$.ts index cee13b1..2294848 100644 --- a/src/platform/$.ts +++ b/src/platform/$.ts @@ -62,6 +62,97 @@ $.setValue = function (key: string, value: string, cb) { } } +interface AjaxDetail { + url: string; + timeout: number; + responseType: XMLHttpRequestResponseType; + withCredentials: boolean; + type: string; + onprogress?: (e: ProgressEvent) => void; + form?: [string, string][]; + headers?: Record; + id: string; +} + +$.ajaxPageInit = function (): void { + $.global(function (): void { + const r = new XMLHttpRequest() + window.FCX.requests = Object.create(null) + document.addEventListener('4chanXAjax', function (e): void { + let fd: FormData | null + const { url, timeout, responseType, withCredentials, type, onprogress, form, headers, id } = e.detail + window.FCX.requests[id] = (r = new XMLHttpRequest()) + r.open(type, url, true) + const object = headers || {} + for (const key in object) { + const value = object[key] + r.setRequestHeader(key, value) + } + r.responseType = responseType === 'document' ? 'text' : responseType + r.timeout = timeout + r.withCredentials = withCredentials + if (onprogress) { + r.upload.onprogress = function (e: ProgressEvent) { + const { loaded, total } = e + const detail = { loaded, total, id } + return document.dispatchEvent(new CustomEvent('4chanXAjaxProgress', { bubbles: true, detail })) + } + } + r.onloadend = function (): void { + delete window.FCX.requests[id] + const { status, statusText, response } = this + const responseHeaderString = this.getAllResponseHeaders() + const detail = { status, statusText, response, responseHeaderString, id } + return document.dispatchEvent(new CustomEvent('4chanXAjaxLoadend', { bubbles: true, detail })) as any + } + // connection error or content blocker + r.onerror = function (): void { + if (!r.status) { return console.warn(`4chan X failed to load: ${url}`) } + } + if (form) { + fd = new FormData() + for (const entry of form) { + fd.append(entry[0], entry[1]) + } + } else { + fd = null + } + return r.send(fd) + }, false) + + return document.addEventListener('4chanXAbort', function (e): void { + const { id } = e.detail + if (window.FCX.requests[id]) { + window.FCX.requests[id].abort() + return delete window.FCX.requests[id] + } + }, false) + + }, '4chanXAjax') + + $.on(d, '4chanXAjaxProgress', function (e: CustomEvent<{ id: string; loaded: number; total: number }>): void { + let req: XMLHttpRequest + if (!(req = requests[e.detail.id])) { return } + return req.upload.onprogress.call(req.upload, e.detail) + }) + + return $.on(d, '4chanXAjaxLoadend', function (e: CustomEvent): void { + let req: XMLHttpRequest + if (!(req = Request[e.detail.id])) { return } + delete Request[e.detail.id] + if (e.detail.status) { + for (const key of ['status', 'statusText', 'response', 'responseHeaderString']) { + req[key] = e.detail[key] + } + if (req.responseType === 'document') { + req.response = new DOMParser().parseFromString + (req.response, 'text/html') + } + return req.onloadend.call(req) + } + }) +} + $.ajaxPage = function (url: string, options: AjaxPageOptions) { const { responseType = 'json', @@ -192,84 +283,7 @@ $.ajax = (function () { // # XXX https://bugs.chromium.org/p/chromium/issues/detail?id=920638 let requestID = 0 const requests = dict() - - $.ajaxPageInit = function () { - $.global(function () { - window.FCX.requests = Object.create(null) - - document.addEventListener('4chanXAjax', function (e: CustomEvent) { - let fd, r - const { url, timeout, responseType, withCredentials, type, onprogress, form, headers, id } = e.detail - window.FCX.requests[id] = (r = new XMLHttpRequest()) - r.open(type, url, true) - const object = headers || {} - for (const key in object) { - const value = object[key] - r.setRequestHeader(key, value) - } - r.responseType = responseType === 'document' ? 'text' : responseType - r.timeout = timeout - r.withCredentials = withCredentials - if (onprogress) { - r.upload.onprogress = function (e) { - const { loaded, total } = e - const detail = { loaded, total, id } - return document.dispatchEvent(new CustomEvent('4chanXAjaxProgress', { bubbles: true, detail })) - } - } - r.onloadend = function () { - delete window.FCX.requests[id] - const { status, statusText, response } = this - const responseHeaderString = this.getAllResponseHeaders() - const detail = { status, statusText, response, responseHeaderString, id } - return document.dispatchEvent(new CustomEvent('4chanXAjaxLoadend', { bubbles: true, detail })) - } - // connection error or content blocker - r.onerror = function () { - if (!r.status) { return console.warn(`4chan X failed to load: ${url}`) } - } - if (form) { - fd = new FormData() - for (const entry of form) { - fd.append(entry[0], entry[1]) - } - } else { - fd = null - } - return r.send(fd) - } - , false) - - return document.addEventListener('4chanXAjaxAbort', function (e) { - let r - if (!(r = window.FCX.requests[e.detail.id])) { return } - return r.abort() - } - , false) - }, '4chanXAjax') - - $.on(d, '4chanXAjaxProgress', function (e) { - let req - if (!(req = requests[e.detail.id])) { return } - return req.upload.onprogress.call(req.upload, e.detail) - }) - - return $.on(d, '4chanXAjaxLoadend', function (e) { - let req - if (!(req = requests[e.detail.id])) { return } - delete requests[e.detail.id] - if (e.detail.status) { - for (const key of ['status', 'statusText', 'response', 'responseHeaderString']) { - req[key] = e.detail[key] - } - if (req.responseType === 'document') { - req.response = new DOMParser().parseFromString(e.detail.response, 'text/html') - } - } - return req.onloadend() - }) - } - + $.ajaxPageInit() return $.ajaxPage = function (url, options = {}) { let req: XMLHttpRequest const { onloadend, timeout, responseType, withCredentials, type, onprogress, headers } = options @@ -314,7 +328,7 @@ $.whenModified = function (url, bucket, cb, options = {}) { return r } -$.cache = function (url, cb, options = {}) { +$.cache = function (url, cb, options) { const reqs = dict() let req const { ajax } = options @@ -350,18 +364,9 @@ $.cleanCache = function (testf) { } -$.cb = { - checked() { - if ($.hasOwn(Conf, this.name)) { - $.set(this.name, this.checked, this.type) - return Conf[this.name] = this.checked - } - }, - value() { - if ($.hasOwn(Conf, this.name)) { - $.set(this.name, this.value.trim(), this.type) - return Conf[this.name] = this.value - } +$.cb = function (cb: VoidCallback) { + if (cb) { + return cb() } } @@ -492,7 +497,7 @@ $.one = function (el, events, handler) { return $.on(el, events, cb) } let cloneInto: (obj: object, win: Window) => object -$.event = function (event: Event, detail: object, root = d) { +$.event = function (event: string, detail: object, root = d) { if (!globalThis.chrome?.extension) { if ((detail != null) && (typeof cloneInto === 'function')) { detail = cloneInto(detail, d.defaultView) @@ -978,7 +983,7 @@ if (platform === 'crx') { }) $.forceSync = function () {/* empty */ } } else if ((typeof GM_deleteValue !== 'undefined' && GM_deleteValue !== null) || $.hasStorage) { - $.sync = function (key, cb) { + $.sync = function (key: string, cb: (newValue: any, key: string) => void) { key = g.NAMESPACE + key $.syncing[key] = cb return $.oldValue[key] = $.getValue(key, cb) @@ -1000,10 +1005,7 @@ if (platform === 'crx') { } $.on(window, 'storage', onChange) - return $.forceSync = function (key, cb) { - // Storage events don't work across origins - // e.g. http://boards.4chan.org and https://boards.4chan.org - // so force a check for changes to avoid lost data. + return $.forceSync = function (key: string, cb: (newValue: any, key: string) => void) { key = g.NAMESPACE + key return onChange({ key, newValue: $.getValue(key, cb) }) } diff --git a/src/site/SW.tinyboard.ts b/src/site/SW.tinyboard.ts index b6ad492..3fa0d29 100644 --- a/src/site/SW.tinyboard.ts +++ b/src/site/SW.tinyboard.ts @@ -19,6 +19,7 @@ const SWTinyboard = { } } }, + ID: 'sw-tinyboard', name: 'Tinyboard', software: 'Tinyboard', isOPContainerThread: true, diff --git a/src/types/globals.d.ts b/src/types/globals.d.ts index 025c7c4..b49af69 100644 --- a/src/types/globals.d.ts +++ b/src/types/globals.d.ts @@ -16,4 +16,9 @@ export interface File { sizeInBytes: number isDead: boolean docIndex: number +} +export interface CacheOptions { + dataType: string + sync: boolean + dontClean: boolean } \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index bbac8ea..75da942 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,7 @@ "allowJs": true, "checkJs": true, //TODO: Flip this to true - "strict": false, + "strict": true, "noEmit": true, "jsx": "react", "jsxFactory": "h",