diff --git a/src/General/BoardConfig.ts b/src/General/BoardConfig.js similarity index 88% rename from src/General/BoardConfig.ts rename to src/General/BoardConfig.js index b73e275..e1c5248 100644 --- a/src/General/BoardConfig.ts +++ b/src/General/BoardConfig.js @@ -10,22 +10,7 @@ import { dict, HOUR } from '../platform/helpers' * DS205: Consider reworking code to avoid use of IIFEs * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md */ - -interface BoardConfig { - cbs: (() => void)[] - init(): void - load(): void - set(boards: { [key: string]: any }): void - ready(cb: () => void): void - sfwBoards(sfw: boolean): string[] - isSFW(board: string): boolean - domain(board: string): string - isArchived(board: string): boolean - noAudio(boardID: string): boolean - title(boardID: string): string -} - -var BoardConfig: BoardConfig = { +var BoardConfig = { cbs: [], init() { diff --git a/src/Linkification/Linkify.js b/src/Linkification/Linkify.js index e3ac40a..c2b64c1 100644 --- a/src/Linkification/Linkify.js +++ b/src/Linkification/Linkify.js @@ -161,9 +161,7 @@ aero|asia|biz|cat|com|coop|dance|info|int|jobs|mobi|moe|museum|name|net|org|post |\ [\\d]{1,3}\\.[\\d]{1,3}\\.[\\d]{1,3}\\.[\\d]{1,3}\ |\ -(\ [-\\w\\d.@]+@[a-z\\d.-]+\\.[a-z\\d]\ -)\ )`, 'i', ), diff --git a/src/Menu/ReportLink.js b/src/Menu/ReportLink.js index a19dbb6..4859e5f 100644 --- a/src/Menu/ReportLink.js +++ b/src/Menu/ReportLink.js @@ -40,7 +40,7 @@ var ReportLink = { const { url, dims } = ReportLink const id = Date.now() const set = `toolbar=0,scrollbars=1,location=0,status=1,menubar=0,resizable=1,${dims}` - return window.open(url, id.toString(), set) + return window.open(url, id, set) }, } export default ReportLink diff --git a/src/Miscellaneous/Report.js b/src/Miscellaneous/Report.js new file mode 100644 index 0000000..182d132 --- /dev/null +++ b/src/Miscellaneous/Report.js @@ -0,0 +1,161 @@ +import Redirect from '../Archive/Redirect' +import $ from '../platform/$' +import ReportPage from './Report/ArchiveReport.html' +import CSS from '../css/CSS' +import Captcha from '../Posting/Captcha' +import { Conf, d, g } from '../globals/globals' + +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ + +var Report = { + init() { + let match + if (!(match = location.search.match(/\bno=(\d+)/))) { + return + } + Captcha.replace.init() + this.postID = +match[1] + return $.ready(this.ready) + }, + + ready() { + $.addStyle(CSS.report) + + if (Conf['Archive Report']) { + Report.archive() + } + + new MutationObserver(function () { + Report.fit('iframe[src^="https://www.google.com/recaptcha/api2/frame"]') + return Report.fit('body') + }).observe(d.body, { + childList: true, + attributes: true, + subtree: true, + }) + return Report.fit('body') + }, + + fit(selector) { + let el + if ( + !((el = $(selector, el)) && getComputedStyle(el).visibility !== 'hidden') + ) { + return + } + const dy = el.getBoundingClientRect().bottom - el.clientHeight + 8 + if (dy > 0) { + return window.resizeBy(0, dy) + } + }, + + archive() { + let match, urls + if (!(urls = Redirect.report(g.BOARD.ID)).length) { + return + } + + const form = $('form') + const types = $.id('reportTypes') + const message = $('h3') + + const fieldset = $.el( + 'fieldset', + { + id: 'archive-report', + hidden: true, + }, + { innerHTML: ReportPage }, + ) + const enabled = $('#archive-report-enabled', fieldset) + const reason = $('#archive-report-reason', fieldset) + const submit = $('#archive-report-submit', fieldset) + + $.on(enabled, 'change', function () { + return (reason.disabled = !this.checked) + }) + + if (form && types) { + fieldset.hidden = !$('[value="31"]', types).checked + $.on(types, 'change', function (e) { + fieldset.hidden = e.target.value !== '31' + return Report.fit('body') + }) + $.after(types, fieldset) + Report.fit('body') + $.one(form, 'submit', function (e) { + if (!fieldset.hidden && enabled.checked) { + e.preventDefault() + return Report.archiveSubmit(urls, reason.value, (results) => { + this.action = + '#archiveresults=' + encodeURIComponent(JSON.stringify(results)) + return this.submit() + }) + } + }) + } else if (message) { + fieldset.hidden = /Report submitted!/.test(message.textContent) + $.on(enabled, 'change', function () { + return (submit.hidden = !this.checked) + }) + $.after(message, fieldset) + $.on(submit, 'click', () => + Report.archiveSubmit(urls, reason.value, Report.archiveResults), + ) + } + + if ((match = location.hash.match(/^#archiveresults=(.*)$/))) { + try { + return Report.archiveResults(JSON.parse(decodeURIComponent(match[1]))) + } catch (error) {} + } + }, + + archiveSubmit(urls, reason, cb) { + const form = $.formData({ + board: g.BOARD.ID, + num: Report.postID, + reason, + }) + const results = [] + for (var [name, url] of urls) { + ;(function (name, url) { + return $.ajax(url, { + onloadend() { + results.push([name, this.response || { error: '' }]) + if (results.length === urls.length) { + return cb(results) + } + }, + form, + }) + })(name, url) + } + }, + + archiveResults(results) { + const fieldset = $.id('archive-report') + for (var [name, response] of results) { + var line = $.el('h3', { className: 'archive-report-response' }) + if ('success' in response) { + $.addClass(line, 'archive-report-success') + line.textContent = `${name}: ${response.success}` + } else { + $.addClass(line, 'archive-report-error') + line.textContent = `${name}: ${ + response.error || 'Error reporting post.' + }` + } + if (fieldset) { + $.before(fieldset, line) + } else { + $.add(d.body, line) + } + } + }, +} +export default Report diff --git a/src/Posting/Captcha.js b/src/Posting/Captcha.js index e6d7bf6..af760a6 100644 --- a/src/Posting/Captcha.js +++ b/src/Posting/Captcha.js @@ -1,4 +1,5 @@ import $ from '../platform/$' +import CaptchaReplace from './Captcha.replace' import CaptchaT from './Captcha.t' import meta from '../../package.json' import Main from '../main/Main' @@ -177,6 +178,7 @@ const Captcha = { return $.event('CaptchaCount', this.captchas.length) }, }, + Replace: CaptchaReplace, t: CaptchaT, v2: { lifetime: 2 * MINUTE, diff --git a/src/Posting/Captcha.replace.js b/src/Posting/Captcha.replace.js new file mode 100644 index 0000000..8b780d0 --- /dev/null +++ b/src/Posting/Captcha.replace.js @@ -0,0 +1,79 @@ +import { g, Conf, doc, d } from '../globals/globals' +import Main from '../main/Main' +import $ from '../platform/$' +import Captcha from './Captcha' + +const CaptchaReplace = { + init() { + if ( + g.SITE.software !== 'yotsuba' || + d.cookie.indexOf('pass_enabled=1') >= 0 + ) { + return + } + + if (Conf['Force Noscript Captcha'] && Main.jsEnabled) { + $.ready(Captcha.replace.noscript) + return + } + + if (Conf['captchaLanguage'].trim()) { + if ( + ['boards.4chan.org', 'boards.4channel.org'].includes(location.hostname) + ) { + return $.onExists(doc, '#captchaFormPart', (node) => + $.onExists( + node, + 'iframe[src^="https://www.google.com/recaptcha/"]', + Captcha.replace.iframe, + ), + ) + } else { + return $.onExists( + doc, + 'iframe[src^="https://www.google.com/recaptcha/"]', + Captcha.replace.iframe, + ) + } + } + }, + + noscript() { + let noscript, original, toggle + if ( + !( + (original = $('#g-recaptcha')) && + (noscript = $('noscript', original.parentNode)) + ) + ) { + return + } + const span = $.el('span', { id: 'captcha-forced-noscript' }) + $.replace(noscript, span) + $.rm(original) + const insert = function () { + span.innerHTML = noscript.textContent + return Captcha.replace.iframe( + $('iframe[src^="https://www.google.com/recaptcha/"]', span), + ) + } + if ((toggle = $('#togglePostFormLink a, #form-link'))) { + return $.on(toggle, 'click', insert) + } else { + return insert() + } + }, + + iframe(iframe) { + let lang + if ((lang = Conf['captchaLanguage'].trim())) { + const src = /[?&]hl=/.test(iframe.src) + ? iframe.src.replace(/([?&]hl=)[^&]*/, '$1' + encodeURIComponent(lang)) + : iframe.src + `&hl=${encodeURIComponent(lang)}` + if (iframe.src !== src) { + iframe.src = src + } + } + }, +} +export default CaptchaReplace diff --git a/src/Posting/PassLink.ts b/src/Posting/PassLink.ts index 123d7ae..6b5b72f 100644 --- a/src/Posting/PassLink.ts +++ b/src/Posting/PassLink.ts @@ -11,7 +11,7 @@ const PassLink = { }, ready(): void { - let styleSelector: HTMLElement + let styleSelector if (!(styleSelector = $.id('styleSelector'))) { return } diff --git a/src/Posting/PostRedirect.ts b/src/Posting/PostRedirect.js similarity index 77% rename from src/Posting/PostRedirect.ts rename to src/Posting/PostRedirect.js index 617439b..9dd3d78 100644 --- a/src/Posting/PostRedirect.ts +++ b/src/Posting/PostRedirect.js @@ -1,8 +1,13 @@ import { d } from '../globals/globals' 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 PostRedirect = { - init(): void { + init() { return $.on(d, 'QRPostSuccessful', (e) => { if (!e.detail.redirect) { return @@ -19,7 +24,7 @@ const PostRedirect = { delays: 0, - delay(): (() => void) | null { + delay() { if (!this.event) { return null } diff --git a/src/Quotelinks/QuotePreview.js b/src/Quotelinks/QuotePreview.js index 3a31cea..344ffbe 100644 --- a/src/Quotelinks/QuotePreview.js +++ b/src/Quotelinks/QuotePreview.js @@ -69,7 +69,14 @@ var QuotePreview = { $.add(Header.hover, qp) new Fetcher(boardID, threadID, postID, qp, Get.postFromNode(this)) - UI.hover({root: this, el: qp, latestEvent: e, endEvents: 'mouseout click', cb: QuotePreview.mouseout, cbArgs: [this]}) + UI.hover({ + root: this, + el: qp, + latestEvent: e, + endEvents: 'mouseout click', + cb: QuotePreview.mouseout, + }) + if ( Conf['Quote Highlighting'] && (origin = g.posts.get(`${boardID}.${postID}`)) diff --git a/src/Quotelinks/QuoteStrikeThrough.ts b/src/Quotelinks/QuoteStrikeThrough.js similarity index 78% rename from src/Quotelinks/QuoteStrikeThrough.ts rename to src/Quotelinks/QuoteStrikeThrough.js index 35d24c3..0a9110c 100644 --- a/src/Quotelinks/QuoteStrikeThrough.ts +++ b/src/Quotelinks/QuoteStrikeThrough.js @@ -3,8 +3,13 @@ import Get from '../General/Get' import { g, Conf } from '../globals/globals' 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 QuoteStrikeThrough = { - init(): void { + init() { if ( !['index', 'thread'].includes(g.VIEW) || (!Conf['Reply Hiding Buttons'] && @@ -20,7 +25,7 @@ const QuoteStrikeThrough = { }) }, - node(): void { + node() { if (this.isClone) { return } diff --git a/src/classes/Board.js b/src/classes/Board.js new file mode 100644 index 0000000..5b68197 --- /dev/null +++ b/src/classes/Board.js @@ -0,0 +1,42 @@ +import BoardConfig from '../General/BoardConfig' +import { d, g } from '../globals/globals' +import SimpleDict from './SimpleDict' + +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +export default class Board { + toString() { + return this.ID + } + + constructor(ID) { + this.ID = ID + this.boardID = this.ID + this.siteID = g.SITE.ID + this.threads = new SimpleDict() + this.posts = new SimpleDict() + this.config = BoardConfig.boards?.[this.ID] || {} + + g.boards[this] = this + } + + cooldowns() { + const c2 = (this.config || {}).cooldowns || {} + const c = { + thread: c2.threads || 0, + reply: c2.replies || 0, + image: c2.images || 0, + thread_global: 300, // inter-board thread cooldown + } + // Pass users have reduced cooldowns. + if (d.cookie.indexOf('pass_enabled=1') >= 0) { + for (var key of ['reply', 'image']) { + c[key] = Math.ceil(c[key] / 2) + } + } + return c + } +} diff --git a/src/classes/Board.ts b/src/classes/Board.ts deleted file mode 100644 index e95b5ab..0000000 --- a/src/classes/Board.ts +++ /dev/null @@ -1,46 +0,0 @@ -import BoardConfig from '../General/BoardConfig'; -import { d, g } from '../globals/globals'; -import Post from './Post'; -import Thread from './Thread'; -import SimpleDict from './SimpleDict'; - -export default class Board { - ID: string; - boardID: string; - siteID: string; - threads: SimpleDict; - posts: SimpleDict; - config: any; - - constructor(ID: string) { - this.ID = ID; - this.boardID = this.ID; - this.siteID = g.SITE.ID; - this.threads = new SimpleDict(); - this.posts = new SimpleDict(); - this.config = BoardConfig.domain(this.ID) - - g.boards[this.ID] = this; - } - - toString() { - return this.ID; - } - - cooldowns() { - const c2 = (this.config || {}).cooldowns || {}; - const c = { - thread: c2.threads || 0, - reply: c2.replies || 0, - image: c2.images || 0, - thread_global: 300, // inter-board thread cooldown - }; - // Pass users have reduced cooldowns. - if (d.cookie.indexOf('pass_enabled=1') >= 0) { - for (let key of ['reply', 'image']) { - c[key] = Math.ceil(c[key] / 2); - } - } - return c; - } -} diff --git a/src/classes/Callbacks.js b/src/classes/Callbacks.js new file mode 100644 index 0000000..c35106b --- /dev/null +++ b/src/classes/Callbacks.js @@ -0,0 +1,65 @@ +import Main from '../main/Main' + +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +export default class Callbacks { + static initClass() { + this.Post = new Callbacks('Post') + this.Thread = new Callbacks('Thread') + this.CatalogThread = new Callbacks('Catalog Thread') + this.CatalogThreadNative = new Callbacks('Catalog Thread') + } + + constructor(type) { + this.type = type + this.keys = [] + } + + push({ name, cb }) { + if (!this[name]) { + this.keys.push(name) + } + return (this[name] = cb) + } + + execute(node, keys = this.keys, force = false) { + let errors + if (node.callbacksExecuted && !force) { + return + } + node.callbacksExecuted = true + for (var name of keys) { + try { + this[name]?.call(node) + } catch (err) { + if (!errors) { + errors = [] + } + errors.push({ + message: [ + '"', + name, + '" crashed on node ', + this.type, + ' No.', + node.ID, + ' (', + node.board, + ').', + ].join(''), + error: err, + html: node.nodes?.root?.outerHTML, + }) + } + } + + if (errors) { + return Main.handleErrors(errors) + } + } +} +Callbacks.initClass() diff --git a/src/classes/Callbacks.ts b/src/classes/Callbacks.ts deleted file mode 100644 index 1f8dc1f..0000000 --- a/src/classes/Callbacks.ts +++ /dev/null @@ -1,68 +0,0 @@ -import Main from '../main/Main'; - -export default class Callbacks { - private type: string; - private keys: string[]; - - static Post: Callbacks; - static Thread: Callbacks; - static CatalogThread: Callbacks; - static CatalogThreadNative: Callbacks; - - static initClass() { - this.Post = new Callbacks('Post'); - this.Thread = new Callbacks('Thread'); - this.CatalogThread = new Callbacks('Catalog Thread'); - this.CatalogThreadNative = new Callbacks('Catalog Thread'); - } - - constructor(type: string) { - this.type = type; - this.keys = []; - } - - push({ name, cb }: { name: string; cb: () => void }) { - if (!this[name]) { - this.keys.push(name); - } - return (this[name] = cb); - } - - execute(node: any, keys = this.keys, force = false) { - let errors: any[]; - if (node.callbacksExecuted && !force) { - return; - } - node.callbacksExecuted = true; - for (let name of keys) { - try { - this[name]?.call(node); - } catch (err: any) { - if (!errors) { - errors = []; - } - errors.push({ - message: [ - '"', - name, - '" crashed on node ', - this.type, - ' No.', - node.ID, - ' (', - node.board, - ').', - ].join(''), - error: err, - html: node.nodes?.root?.outerHTML, - }); - } - } - - if (errors) { - return Main.handleErrors(errors); - } - } -} - -Callbacks.initClass(); diff --git a/src/classes/CatalogThread.js b/src/classes/CatalogThread.js new file mode 100644 index 0000000..829ec4d --- /dev/null +++ b/src/classes/CatalogThread.js @@ -0,0 +1,24 @@ +import $ from '../platform/$' + +export default class CatalogThread { + toString() { + return this.ID + } + + constructor(root, thread) { + this.thread = thread + this.ID = this.thread.ID + this.board = this.thread.board + const { post } = this.thread.OP.nodes + this.nodes = { + root, + thumb: $('.catalog-thumb', post), + icons: $('.catalog-icons', post), + postCount: $('.post-count', post), + fileCount: $('.file-count', post), + pageCount: $('.page-count', post), + replies: null, + } + this.thread.catalogView = this + } +} diff --git a/src/classes/CatalogThread.ts b/src/classes/CatalogThread.ts deleted file mode 100644 index 8542f16..0000000 --- a/src/classes/CatalogThread.ts +++ /dev/null @@ -1,37 +0,0 @@ -import $ from '../platform/$'; - -export default class CatalogThread { - private thread: any; - private ID: number; - private board: string; - private nodes: { - root: any, - thumb: HTMLElement, - icons: HTMLElement, - postCount: HTMLElement, - fileCount: HTMLElement, - pageCount: HTMLElement, - replies: null | any, - }; - - constructor(root: any, thread: any) { - this.thread = thread; - this.ID = this.thread.ID; - this.board = this.thread.board; - const { post } = this.thread.OP.nodes; - this.nodes = { - root, - thumb: $('.catalog-thumb', post), - icons: $('.catalog-icons', post), - postCount: $('.post-count', post), - fileCount: $('.file-count', post), - pageCount: $('.page-count', post), - replies: null, - }; - this.thread.catalogView = this; - } - - public toString(): string { - return this.ID.toString(); - } -} \ No newline at end of file diff --git a/src/classes/CatalogThreadNative.js b/src/classes/CatalogThreadNative.js new file mode 100644 index 0000000..8cfc701 --- /dev/null +++ b/src/classes/CatalogThreadNative.js @@ -0,0 +1,23 @@ +import { g } from '../globals/globals' +import $ from '../platform/$' +import Board from './Board' +import Thread from './Thread' + +export default class CatalogThreadNative { + toString() { + return this.ID + } + + constructor(root) { + this.nodes = { + root, + thumb: $(g.SITE.selectors.catalog.thumb, root), + } + this.siteID = g.SITE.ID + this.boardID = this.nodes.thumb.parentNode.pathname.split(/\/+/)[1] + this.board = g.boards[this.boardID] || new Board(this.boardID) + this.ID = this.threadID = +(root.dataset.id || root.id).match(/\d*$/)[0] + this.thread = + this.board.threads.get(this.ID) || new Thread(this.ID, this.board) + } +} diff --git a/src/classes/CatalogThreadNative.ts b/src/classes/CatalogThreadNative.ts deleted file mode 100644 index 2a322d8..0000000 --- a/src/classes/CatalogThreadNative.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { g } from '../globals/globals'; -import $ from '../platform/$'; -import Board from './Board'; -import Thread from './Thread'; - -export default class CatalogThreadNative { - nodes: { - root: HTMLElement; - thumb: any; - }; - siteID: string; - boardID: string; - board: Board; - ID: number; - threadID: number; - thread: Thread; - - toString() { - return this.ID.toString(); - } - - constructor(root: HTMLElement) { - this.nodes = { - root, - thumb: $(g.SITE.selectors.catalog.thumb, root), - }; - this.siteID = g.SITE.ID; - this.boardID = this.nodes.thumb[0].parentNode.pathname.split(/\/+/)[1]; - this.board = g.boards[this.boardID] || new Board(this.boardID); - this.ID = this.threadID = +(root.dataset.id || root.id).match(/\d*$/)[0]; - this.thread = - this.board.threads.get(this.ID) || new Thread(this.ID, this.board); - } -} diff --git a/src/classes/Connection.js b/src/classes/Connection.js new file mode 100644 index 0000000..647cfb9 --- /dev/null +++ b/src/classes/Connection.js @@ -0,0 +1,51 @@ +import $ from '../platform/$' +import { g } from '../globals/globals' + +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +export default class Connection { + constructor(target, origin, cb = {}) { + this.send = this.send.bind(this) + this.onMessage = this.onMessage.bind(this) + this.target = target + this.origin = origin + this.cb = cb + $.on(window, 'message', this.onMessage) + } + + targetWindow() { + if (this.target instanceof window.HTMLIFrameElement) { + return this.target.contentWindow + } else { + return this.target + } + } + + send(data) { + return this.targetWindow().postMessage( + `${g.NAMESPACE}${JSON.stringify(data)}`, + this.origin, + ) + } + + onMessage(e) { + if ( + e.source !== this.targetWindow() || + e.origin !== this.origin || + typeof e.data !== 'string' || + e.data.slice(0, g.NAMESPACE.length) !== g.NAMESPACE + ) { + return + } + const data = JSON.parse(e.data.slice(g.NAMESPACE.length)) + for (var type in data) { + var value = data[type] + if ($.hasOwn(this.cb, type)) { + this.cb[type](value) + } + } + } +} diff --git a/src/classes/Connection.ts b/src/classes/Connection.ts deleted file mode 100644 index 31ea358..0000000 --- a/src/classes/Connection.ts +++ /dev/null @@ -1,54 +0,0 @@ -import $ from '../platform/$'; -import { g } from '../globals/globals'; - -interface Callbacks { - [key: string]: (value: any) => void; -} - -export default class Connection { - private target: Window | HTMLIFrameElement; - private origin: string; - private cb: Callbacks; - - constructor(target: Window | HTMLIFrameElement, origin: string, cb: Callbacks = {}) { - this.send = this.send.bind(this); - this.onMessage = this.onMessage.bind(this); - this.target = target; - this.origin = origin; - this.cb = cb; - $.on(window, 'message', this.onMessage); - } - - private targetWindow(): Window { - if (this.target instanceof window.HTMLIFrameElement) { - return this.target.contentWindow; - } else { - return this.target; - } - } - - public send(data: any): void { - return this.targetWindow().postMessage( - `${g.NAMESPACE}${JSON.stringify(data)}`, - this.origin, - ); - } - - private onMessage(e: MessageEvent): void { - if ( - e.source !== this.targetWindow() || - e.origin !== this.origin || - typeof e.data !== 'string' || - e.data.slice(0, g.NAMESPACE.length) !== g.NAMESPACE - ) { - return; - } - const data = JSON.parse(e.data.slice(g.NAMESPACE.length)); - for (const type in data) { - const value = data[type]; - if ($.hasOwn(this.cb, type)) { - this.cb[type](value); - } - } - } -} diff --git a/src/classes/Fetcher.js b/src/classes/Fetcher.js index 6f8d8b7..48c5bee 100644 --- a/src/classes/Fetcher.js +++ b/src/classes/Fetcher.js @@ -11,6 +11,7 @@ import CrossOrigin from '../platform/CrossOrigin' import Get from '../General/Get' import { dict } from '../platform/helpers' + export default class Fetcher { static initClass() { this.prototype.archiveTags = { @@ -79,20 +80,53 @@ export default class Fetcher { this.root.textContent = `Loading post No.${this.postID}...` if (this.threadID) { const that = this - $.cache( - g.SITE.urls.threadJSON({ - boardID: this.boardID, - threadID: this.threadID, - }), - function ({ isCached }) { - return that.fetchedPost(this, isCached) + Fetcher.fetchThread( + this.boardID, + this.threadID, + function (req, isCached) { + that.fetchedThread(req, isCached) }, + true, ) } else { - this.archivedPost() + const that = this + Fetcher.fetchPost( + this.boardID, + this.postID, + function (req, isCached) { + that.fetchedPost(req, isCached) + }, + true, + ) + } + } + + fetchedThread(req) { + const { status, response } = req + const { boardID, threadID } = this + const board = g.boards[boardID] + if (status === 404) { + this.root.textContent = `Thread No.${threadID} not found.` + return + } + if (status !== 200) { + this.root.textContent = `Error loading thread No.${threadID}.` + return + } + if (response === '') { + this.root.textContent = `Thread No.${threadID} is empty.` + return + } + const thread = new Thread( + g.SITE.Build.threadFromObject(response, boardID),board) + Main.callbackNodes('Thread', [thread]) + const post = thread.posts.get(this.postID) + if (post) { + this.insert(post) + } else { + this.root.textContent = `Post No.${this.postID} not found.` } } - insert(post) { // Stop here if the container has been removed while loading. if (!this.root.parentNode) { @@ -146,76 +180,75 @@ export default class Fetcher { } fetchedPost(req, isCached) { - // In case of multiple callbacks for the same request, - // don't parse the same original post more than once. - let post - if ((post = g.posts.get(`${this.boardID}.${this.postID}`))) { - this.insert(post) - return + const { status, response } = req; + const { boardID, postID, threadID } = this; + const postKey = `${boardID}.${postID}`; + + const post = g.posts.get(postKey); + if (post) { + this.insert(post); + return; } - - const { status } = req + if (status !== 200) { - // The thread can die by the time we check a quote. - if (status && this.archivedPost()) { - return - } - - $.addClass(this.root, 'warning') - this.root.textContent = - status === 404 - ? `Thread No.${this.threadID} 404'd.` - : !status - ? 'Connection Error' - : `Error ${req.statusText} (${req.status}).` - return + this.handleNon200Status(status); + return; } - - const { posts } = req.response - g.SITE.Build.spoilerRange[this.boardID] = posts[0].custom_spoiler - for (post of posts) { - if (post.no === this.postID) { - break - } - } // we found it! - - if (post.no !== this.postID) { - // Cached requests can be stale and must be rechecked. - if (isCached) { - const api = g.SITE.urls.threadJSON({ - boardID: this.boardID, - threadID: this.threadID, - }) - $.cleanCache((url) => url === api) - const that = this - $.cache(api, function () { - return that.fetchedPost(this, false) - }) - return - } - - // The post can be deleted by the time we check a quote. - if (this.archivedPost()) { - return - } - - $.addClass(this.root, 'warning') - this.root.textContent = `Post No.${this.postID} was not found.` - return + + const { posts } = response; + g.SITE.Build.spoilerRange[boardID] = posts[0].custom_spoiler; + + const foundPost = posts.find((p) => p.no === postID); + + if (!foundPost) { + this.handlePostNotFound(isCached); + return; } - - const board = g.boards[this.boardID] || new Board(this.boardID) - const thread = - g.threads.get(`${this.boardID}.${this.threadID}`) || - new Thread(this.threadID, board) - post = new Post( - g.SITE.Build.postFromObject(post, this.boardID), + + const board = g.boards[boardID] || new Board(boardID); + const threadKey = `${boardID}.${threadID}`; + const thread = g.threads.get(threadKey) || new Thread(threadID, board); + const newPost = new Post( + g.SITE.Build.postFromObject(foundPost, boardID), thread, board, { isFetchedQuote: true }, - ) - Main.callbackNodes('Post', [post]) - return this.insert(post) + ); + Main.callbackNodes("Post", [newPost]); + return this.insert(newPost); + } + + handleNon200Status(status, req) { + $.addClass(this.root, "warning"); + this.root.textContent = + status === 404 + ? `Thread No.${this.threadID} 404'd.` + : !status + ? "Connection Error" + : `Error ${req.statusText} (${req.status}).`; + + if (status && this.archivedPost()) { + return; + } + } + + handlePostNotFound(isCached) { + if (isCached) { + const api = g.SITE.urls.threadJSON({ + boardID: this.boardID, + threadID: this.threadID, + }); + $.cleanCache((url) => url === api); + $.cache(api, () => this.fetchedPost(this, false)); + return; + } + + if (this.archivedPost()) { + return; + } + + $.addClass(this.root, "warning"); + this.root.textContent = `Post No.${this.postID} was not found.`; } archivedPost() { @@ -420,4 +453,4 @@ export default class Fetcher { return this.insert(post) } } -Fetcher.initClass() \ No newline at end of file +Fetcher.initClass() diff --git a/src/classes/ShimSet.ts b/src/classes/ShimSet.js similarity index 63% rename from src/classes/ShimSet.ts rename to src/classes/ShimSet.js index b89ff77..0ddff27 100644 --- a/src/classes/ShimSet.ts +++ b/src/classes/ShimSet.js @@ -1,22 +1,25 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ import $ from '../platform/$' class ShimSet { - elements: { [key: string]: boolean } - size: number constructor() { this.elements = $.dict() this.size = 0 } - has(value: string) { + has(value) { return value in this.elements } - add(value: string) { + add(value) { if (this.elements[value]) { return } this.elements[value] = true return this.size++ } - delete(value: string) { + delete(value) { if (!this.elements[value]) { return } @@ -26,6 +29,5 @@ class ShimSet { } if (!('Set' in window)) { - // @ts-ignore window.Set = ShimSet } diff --git a/src/main/Main.js b/src/main/Main.js index f951515..980675e 100644 --- a/src/main/Main.js +++ b/src/main/Main.js @@ -86,6 +86,7 @@ import Header from '../General/Header' import { c, Conf, d, doc, docSet, E, g } from '../globals/globals' import Menu from '../Menu/Menu' import BoardConfig from '../General/BoardConfig' +import CaptchaReplace from '../Posting/Captcha.replace' import Get from '../General/Get' import { dict, platform } from '../platform/helpers' import Polyfill from '../General/Polyfill' @@ -1074,6 +1075,7 @@ User agent: ${navigator.userAgent}\ ['Board Configuration', BoardConfig], ['Normalize URL', NormalizeURL], ['Delay Redirect on Post', PostRedirect], + ['Captcha Configuration', CaptchaReplace], ['Image Host Rewriting', ImageHost], ['Redirect', Redirect], ['Header', Header], diff --git a/src/platform/$$.js b/src/platform/$$.js new file mode 100644 index 0000000..11ec207 --- /dev/null +++ b/src/platform/$$.js @@ -0,0 +1,12 @@ +import { d } from '../globals/globals' + +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +const $$ = (selector, root = d.body) => [ + ...Array.from(root.querySelectorAll(selector)), +] +export default $$ diff --git a/src/platform/$$.ts b/src/platform/$$.ts deleted file mode 100644 index 48e8e4e..0000000 --- a/src/platform/$$.ts +++ /dev/null @@ -1,4 +0,0 @@ -const $$ = (selector: string, root: HTMLElement | null = document.body): Element[] => - Array.from(root?.querySelectorAll(selector) ?? []) as Element[]; - -export default $$; diff --git a/src/platform/$.js b/src/platform/$.js index e4520c3..a6f63b5 100644 --- a/src/platform/$.js +++ b/src/platform/$.js @@ -54,20 +54,6 @@ $.ajaxPage = function (url, options) { r.send(form) return r } -$.cache = function (key, value, time) { - if (value == null) { - value = null - } - if (time == null) { - time = MINUTE - } - if (value) { - return $.set(key, value, time) - } else { - return $.get(key) - } -} - $.ready = function (fc) { if (d.readyState !== 'loading') { $.queueTask(fc) diff --git a/src/platform/helpers.ts b/src/platform/helpers.ts index 758dada..5cbdb27 100644 --- a/src/platform/helpers.ts +++ b/src/platform/helpers.ts @@ -25,7 +25,7 @@ export const debounce = (wait: number, fn: Function) => { export const dict = () => Object.create(null) -dict.clone = function (obj: object) { +dict.clone = function (obj) { if (typeof obj !== 'object' || obj === null) { return obj } else if (obj instanceof Array) { diff --git a/src/site/SW.yotsuba.Build/CatalogThreadHtml.tsx b/src/site/SW.yotsuba.Build/CatalogThreadHtml.tsx index 90b6bdd..cd59623 100644 --- a/src/site/SW.yotsuba.Build/CatalogThreadHtml.tsx +++ b/src/site/SW.yotsuba.Build/CatalogThreadHtml.tsx @@ -2,14 +2,14 @@ import h, { hFragment, EscapedHtml } from '../../globals/jsx' export default function generateCatalogThreadHtml( thread, - src: string, - imgClass: string, - data: any, - postCount: number, - fileCount: number, - pageCount: number, - staticPath: string, - gifIcon: string, + src, + imgClass, + data, + postCount, + fileCount, + pageCount, + staticPath, + gifIcon, ): EscapedHtml { return ( <> diff --git a/src/site/SW.yotsuba.Build/FileHtml.tsx b/src/site/SW.yotsuba.Build/FileHtml.tsx index d7eeec6..c0d1497 100644 --- a/src/site/SW.yotsuba.Build/FileHtml.tsx +++ b/src/site/SW.yotsuba.Build/FileHtml.tsx @@ -1,29 +1,15 @@ import h, { EscapedHtml, isEscaped } from '../../globals/jsx' -type File = { - MD5: string - name: string - size: string - dimensions: string - tag: string - width: number - height: number - twidth: number - theight: number - hasDownscale: boolean - isSpoiler: boolean -} - export default function generateFileHtml( - file: File | null, - ID: number, - boardID: string, - fileURL: string, - shortFilename: string, - fileThumb: string, - o: any, - staticPath: string, - gifIcon: string, + file, + ID, + boardID, + fileURL, + shortFilename, + fileThumb, + o, + staticPath, + gifIcon, ): EscapedHtml { if (file) { const fileContent: (EscapedHtml | string)[] = [] diff --git a/src/site/SW.yotsuba.Build/PostInfoHtml.tsx b/src/site/SW.yotsuba.Build/PostInfoHtml.tsx index f24d248..7eaf061 100644 --- a/src/site/SW.yotsuba.Build/PostInfoHtml.tsx +++ b/src/site/SW.yotsuba.Build/PostInfoHtml.tsx @@ -2,29 +2,29 @@ import { g } from '../../globals/globals' import h, { EscapedHtml } from '../../globals/jsx' export default function generatePostInfoHtml( - ID: number, - o: any, - subject: string, - capcode: string, - email: string, - name: string, - tripcode: string, - pass: string, - capcodeLC: string, - capcodePlural: string, - staticPath: string, - gifIcon: string, - capcodeDescription: string, - uniqueID: string, - flag: string, - flagCode: string, - flagCodeTroll: string, - dateUTC: string, - dateText: string, - postLink: string, - quoteLink: string, - boardID: string, - threadID: number, + ID, + o, + subject, + capcode, + email, + name, + tripcode, + pass, + capcodeLC, + capcodePlural, + staticPath, + gifIcon, + capcodeDescription, + uniqueID, + flag, + flagCode, + flagCodeTroll, + dateUTC, + dateText, + postLink, + quoteLink, + boardID, + threadID, ): EscapedHtml { const nameHtml: (EscapedHtml | string)[] = [ {name}, diff --git a/src/site/SW.yotsuba.tsx b/src/site/SW.yotsuba.tsx index 0ec08c0..a1475aa 100644 --- a/src/site/SW.yotsuba.tsx +++ b/src/site/SW.yotsuba.tsx @@ -1,5 +1,6 @@ import Redirect from "../Archive/Redirect"; import PassMessage from "../Miscellaneous/PassMessage"; +import Report from "../Miscellaneous/Report"; import $ from "../platform/$"; import $$ from "../platform/$$"; import Captcha from "../Posting/Captcha"; @@ -8,12 +9,20 @@ import ImageHost from "../Images/ImageHost"; import { g, Conf, E, d, doc } from "../globals/globals"; import BoardConfig from "../General/BoardConfig"; import CSS from "../css/CSS"; + import generatePostInfoHtml from './SW.yotsuba.Build/PostInfoHtml'; import generateFileHtml from "./SW.yotsuba.Build/FileHtml"; import generateCatalogThreadHtml from "./SW.yotsuba.Build/CatalogThreadHtml"; import h, { hFragment, isEscaped } from "../globals/jsx"; import { dict, MINUTE } from "../platform/helpers"; +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS205: Consider reworking code to avoid use of IIFEs + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ const SWYotsuba = { isOPContainerThread: false, hasIPCount: true,