From 5619b356430274134a78e02599e6232b9c9b01d5 Mon Sep 17 00:00:00 2001 From: towards-a-new-leftypol Date: Tue, 27 Feb 2024 02:58:55 +0000 Subject: [PATCH 01/71] WIP: slowly but surely --- js/lcn/classes.js | 884 +++++++++++++++++++++++----------------------- js/lcn/utils.js | 40 ++- 2 files changed, 466 insertions(+), 458 deletions(-) diff --git a/js/lcn/classes.js b/js/lcn/classes.js index 255a09d5..8a48ddac 100644 --- a/js/lcn/classes.js +++ b/js/lcn/classes.js @@ -4,7 +4,6 @@ */ globalThis.LCNSite = class LCNSite { - static INSTANCE = null; static "createAbortable" () { const obj = { "abort": null, "controller": null, "signal": null } @@ -32,54 +31,60 @@ globalThis.LCNSite = class LCNSite { } return null + }; + + static "constructor" () { + this._isModerator = document.body.classList.contains("is-moderator"); + + this._isThreadPage = document.body.classList.contains("active-thread"); + this._isBoardPage = document.body.classList.contains("active-board"); + this._isCatalogPage = document.body.classList.contains("active-catalog"); + + this._isModPage = location.pathname == "/mod.php"; + this._isModRecentsPage = this._isModPage && (location.search == "?/recent" || location.search.startsWith("?/recent/")); + this._isModReportsPage = this._isModPage && (location.search == "?/reports" || location.search.startsWith("?/reports/")); + this._isModLogPage = this._isModPage && (location.search == "?/log" || location.search.startsWith("?/log/")); + this._unseen = 0; + this._pageTitle = document.title; + this._doTitleUpdate = () => { + document.title = (this._unseen > 0 ? `(${this._unseen}) ` : "") + this._pageTitle; + }; + this._favicon = document.querySelector("head > link[rel=\"shortcut icon\"]"); + this._generatedStyle = null; } - #isModerator = document.body.classList.contains("is-moderator"); - #isThreadPage = document.body.classList.contains("active-thread"); - #isBoardPage = document.body.classList.contains("active-board"); - #isCatalogPage = document.body.classList.contains("active-catalog"); + "isModerator" () { return this._isModerator; } + "isThreadPage" () { return this._isThreadPage; } + "isBoardPage" () { return this._isBoardPage; } + "isCatalogPage" () { return this._isCatalogPage; } - #isModPage = location.pathname == "/mod.php"; - #isModRecentsPage = this.#isModPage && (location.search == "?/recent" || location.search.startsWith("?/recent/")); - #isModReportsPage = this.#isModPage && (location.search == "?/reports" || location.search.startsWith("?/reports/")); - #isModLogPage = this.#isModPage && (location.search == "?/log" || location.search.startsWith("?/log/")); + "isModPage" () { return this._isModPage; } + "isModRecentsPage" () { return this._isModRecentsPage; } + "isModReportsPage" () { return this._isModReportsPage; } + "isModLogPage" () { return this._isModLogPage; } - "isModerator" () { return this.#isModerator; } - "isThreadPage" () { return this.#isThreadPage; } - "isBoardPage" () { return this.#isBoardPage; } - "isCatalogPage" () { return this.#isCatalogPage; } - - "isModPage" () { return this.#isModPage; } - "isModRecentsPage" () { return this.#isModRecentsPage; } - "isModReportsPage" () { return this.#isModReportsPage; } - "isModLogPage" () { return this.#isModLogPage; } - - #unseen = 0; - "getUnseen" () { return this.#unseen; } - "clearUnseen" () { if (this.#unseen != 0) { this.setUnseen(0); } } + "getUnseen" () { return this._unseen; } + "clearUnseen" () { if (this._unseen != 0) { this.setUnseen(0); } } "setUnseen" (int) { const bool = !!int - if (bool != !!this.#unseen) { + if (bool != !!this._unseen) { this.setFaviconType(bool ? "reply" : null) } - this.#unseen = int - this.#doTitleUpdate() + this._unseen = int + this._doTitleUpdate() } - #pageTitle = document.title; - "getTitle" () { return this.#pageTitle; } - "setTitle" (title) { this.#pageTitle = title; this.#doTitleUpdate(); } - #doTitleUpdate () { document.title = (this.#unseen > 0 ? `(${this.#unseen}) ` : "") + this.#pageTitle; } + "getTitle" () { return this._pageTitle; } + "setTitle" (title) { this._pageTitle = title; this._doTitleUpdate(); } - #favicon = document.querySelector("head > link[rel=\"shortcut icon\"]"); "setFaviconType" (type=null) { - if (this.#favicon == null) { - this.#favicon = document.createElement("link") - this.#favicon.rel = "shortcut icon" - document.head.appendChild(this.#favicon) + if (this._favicon == null) { + this._favicon = document.createElement("link") + this._favicon.rel = "shortcut icon" + document.head.appendChild(this._favicon) } - this.#favicon.href = `/favicon${type ? "-" + type : ""}.ico` + this._favicon.href = `/favicon${type ? "-" + type : ""}.ico` } "getFloaterLContainer" () { return document.getElementById("bar-bottom-l"); } @@ -87,413 +92,414 @@ globalThis.LCNSite = class LCNSite { "getThreadStatsLContainer" () { return document.getElementById("lcn-threadstats-l"); } "getThreadStatsRContainer" () { return document.getElementById("lcn-threadstats-r"); } - #generatedStyle = null; "writeCSSStyle" (origin, stylesheet) { - if (this.#generatedStyle == null && (this.#generatedStyle = document.querySelector("head > style.generated-css")) == null) { - this.#generatedStyle = document.createElement("style") - this.#generatedStyle.classList.add("generated-css") - document.head.appendChild(this.#generatedStyle) + if (this._generatedStyle == null && (this._generatedStyle = document.querySelector("head > style.generated-css")) == null) { + this._generatedStyle = document.createElement("style") + this._generatedStyle.classList.add("generated-css") + document.head.appendChild(this._generatedStyle) } - this.#generatedStyle.textContent += `${this.#generatedStyle.textContent.length ? "\n\n" : ""}/*** Generated by ${origin} ***/\n${stylesheet}` + this._generatedStyle.textContent += `${this._generatedStyle.textContent.length ? "\n\n" : ""}/*** Generated by ${origin} ***/\n${stylesheet}` } } -globalThis.LCNPostInfo = class LCNPostInfo { - - static nodeAttrib = "$LCNPostInfo"; - static selector = ".post:not(.grid-li)"; - #boardId = null; - #threadId = null; - #postId = null; - #name = null; - #email = null; - #capcode = null; - #flag = null; - #ip = null; - #subject = null; - #createdAt = null; - - #parent = null; - #isThread = false; - #isReply = false; - #isLocked = false; - #isSticky = false; - - static "assign" (post) { return post[this.nodeAttrib] ?? (post[this.nodeAttrib] = this.from(post)); } - static "from" (post) { - assert.ok(post.classList.contains("post"), "Arty must be expected Element.") - const inst = new this() - const intro = post.querySelector(".intro") - const link = intro.querySelector(".post_no:not([id])").href.split("/").reverse() - inst.#postId = link[0].slice(link[0].indexOf("#q") + 2) - inst.#threadId = link[0].slice(0, link[0].indexOf(".")) - inst.#boardId = link[2] - inst.#isThread = post.classList.contains("op") - inst.#isReply = !inst.#isThread - - inst.#subject = intro.querySelector(".subject")?.innerText ?? null - inst.#name = intro.querySelector(".name")?.innerText ?? null - inst.#email = intro.querySelector(".email")?.href.slice(7) ?? null - inst.#flag = intro.querySelector(".flag")?.src.split("/").reverse()[0].slice(0, -4) ?? null - - inst.#capcode = intro.querySelector(".capcode")?.innerText ?? null - inst.#ip = intro.querySelector(".ip-link")?.innerText ?? null - inst.#createdAt = new Date(intro.querySelector("time[datetime]").dateTime ?? NaN) - - inst.#isSticky = !!intro.querySelector("i.fa-thumb-tack") - inst.#isLocked = !!intro.querySelector("i.fa-lock") - - return inst - } - - "getParent" () { return this.#parent; } - "__setParent" (inst) { return this.#parent = inst; } - - "getBoardId" () { return this.#boardId; } - "getThreadId" () { return this.#threadId; } - "getPostId" () { return this.#postId; } - "getHref" () { return `/${this.boardId}/res/${this.threadId}.html#q${this.postId}`; } - - "getName" () { return this.#name; } - "getEmail" () { return this.#email; } - "getIP" () { return this.#ip; } - "getCapcode" () { return this.#capcode; } - "getSubject" () { return this.#subject; } - "getCreatedAt" () { return this.#createdAt; } - - "isSticky" () { return this.#isSticky; } - "isLocked" () { return this.#isLocked; } - "isThread" () { return this.#isThread; } - "isReply" () { return this.#isReply; } - - "is" (info) { - assert.ok(info, "Must be LCNPost.") - return this.getBoardId() == info.getBoardId() && this.getPostId() == info.getPostId() - } - -} - -globalThis.LCNPost = class LCNPost { - - static nodeAttrib = "$LCNPost"; - static selector = ".post:not(.grid-li)"; - #parent = null; - #post = null; - #info = null; - #ipLink = null; - #controls = null; - #customControlsSeperatorNode = null; - - static "assign" (post) { return post[this.nodeAttrib] ?? (post[this.nodeAttrib] = this.from(post)); } - static "from" (post) { return new this(post); } - - "constructor" (post) { - assert.ok(post.classList.contains("post"), "Arty must be expected Element.") - const intro = post.querySelector(".intro") - this.#post = post - this.#info = LCNPostInfo.assign(post) - this.#ipLink = intro.querySelector(".ip-link") - this.#controls = Array.prototype.at.apply(post.querySelectorAll(".controls"), [ -1 ]) - - assert.equal(this.#info.getParent(), null, "Info should not have parent.") - this.#info.__setParent(this) - } - - "jQuery" () { return $(this.#post); } - "trigger" (event_id, data=null) { $(this.#post).trigger(event_id, [ data ]); } - - "getElement" () { return this.#post; } - "getInfo" () { return this.#info; } - - "getIPLink" () { return this.#ipLink; } - "setIP" (ip) { this.#ipLink.innerText = ip; } - - "getParent" () { return this.#parent; } - "__setParent" (inst) { return this.#parent = inst; } - - static #NBSP = String.fromCharCode(160); - "addCustomControl" (obj) { - if (LCNSite.INSTANCE.isModerator()) { - const link = document.createElement("a") - link.innerText = `[${obj.btn}]` - link.title = obj.tooltip - - if (typeof obj.href == "string") { - link.href = obj.href - link.referrerPolicy = "no-referrer" - } else if (obj.onClick != undefined) { - link.style.cursor = "pointer" - link.addEventListener("click", e => { e.preventDefault(); obj.onClick(this); }) - } - - if (this.#customControlsSeperatorNode == null) { - this.#controls.insertBefore(this.#customControlsSeperatorNode = new Text(`${this.constructor.#NBSP}-${this.constructor.#NBSP}`), this.#controls.firstElementChild) - } else { - this.#controls.insertBefore(new Text(this.constructor.#NBSP), this.#customControlsSeperatorNode) - } - - this.#controls.insertBefore(link, this.#customControlsSeperatorNode) - } - } - -} - -globalThis.LCNThread = class LCNThread { - - static nodeAttrib = "$LCNThread"; - static selector = ".thread:not(.grid-li)"; - #element = null; - #parent = null; - #op = null; - - static "assign" (thread) { return thread[this.nodeAttrib] ?? (thread[this.nodeAttrib] = this.from(thread)); } - static "from" (thread) { return new this(thread); } - - "constructor" (thread) { - assert.ok(thread.classList.contains("thread"), "Arty must be expected Element.") - this.#element = thread - this.#op = LCNPost.assign(this.#element.querySelector(".post.op")) - - //assert.equal(this.#op.getParent(), null, "Op should not have parent.") - this.#op.__setParent(this) - } - - "getElement" () { return this.#element; } - "getContent" () { return this.#op; } - "getPosts" () { return Array.prototype.map.apply(this.#element.querySelectorAll(".post"), [ el => LCNPost.assign(el) ]); } - "getReplies" () { return Array.prototype.map.apply(this.#element.querySelectorAll(".post:not(.op)"), [ el => LCNPost.assign(el) ]); } - - "getParent" () { return this.#parent; } - "__setParent" (inst) { return this.#parent = inst; } -} - - -globalThis.LCNPostContainer = class LCNPostContainer { - - static nodeAttrib = "$LCNPostContainer"; - static selector = ".postcontainer"; - #parent = null; - #element = null; - #content = null; - #postId = null; - #boardId = null; - - static "assign" (container) { return container[this.nodeAttrib] ?? (container[this.nodeAttrib] = this.from(container)); } - static "from" (container) { return new this(container); } - - "constructor" (container) { - assert.ok(container.classList.contains("postcontainer"), "Arty must be expected Element.") - const child = container.querySelector(".thread, .post") - this.#element = container - this.#content = child.classList.contains("thread") ? LCNThread.assign(child) : LCNPost.assign(child) - this.#boardId = container.dataset.board - this.#postId = container.id.slice(2) - - assert.equal(this.#content.getParent(), null, "Content should not have parent.") - this.#content.__setParent(this) - } - - "getElement" () { return this.#element; } - "getContent" () { return this.#content; } - "getBoardId" () { return this.#boardId; } - "getPostId" () { return this.#postId; } - - "getParent" () { return this.#parent; } - "__setParent" (inst) { return this.#parent = inst; } - -} - -globalThis.LCNPostWrapper = class LCNPostWrapper { - - static nodeAttrib = "$LCNPostWrapper"; - static selector = ".post-wrapper"; - #wrapper = null; - #eitaLink = null; - #eitaId = null; - #eitaHref = null - #content = null; - - static "assign" (wrapper) { return wrapper[this.nodeAttrib] ?? (wrapper[this.nodeAttrib] = this.from(wrapper)); } - static "from" (wrapper) { return new this(wrapper); } - - "constructor" (wrapper) { - assert.ok(wrapper.classList.contains("post-wrapper"), "Arty must be expected Element.") - this.#wrapper = wrapper - this.#eitaLink = wrapper.querySelector(".eita-link") - this.#eitaId = this.#eitaLink.id - this.#eitaHref = this.#eitaLink.href - void Array.prototype.find.apply(wrapper.children, [ - el => { - if (el.classList.contains("thread")) { - return this.#content = LCNThread.assign(el) - } else if (el.classList.contains("postcontainer")) { - return this.#content = LCNPostContainer.assign(el) - } - } - ]) - - assert.ok(this.#content, "Wrapper should contain content.") - assert.equal(this.#content.getParent(), null, "Content should not have parent.") - this.#content.__setParent(this) - } - - "getPost" () { - const post = this.getContent().getContent() - assert.ok(post instanceof LCNPost, "Post should be LCNPost.") - return post - } - - "getElement" () { return this.#wrapper; } - "getContent" () { return this.#content; } - "getEitaId" () { return this.#eitaId; } - "getEitaHref" () { return this.#eitaHref; } - "getEitaLink" () { return this.#eitaLink; } - -} - -globalThis.LCNSetting = class LCNSetting { - #id = null; - #eventId = null; - #label = null; - #value = null; - #valueDefault = null; - - static "build" (id) { return new this(id); } - - "constructor" (id) { - this.#id = id; - this.#eventId = `lcnsetting::${this.#id}` - } - - #getValue () { - const v = localStorage.getItem(this.#id) - if (v != null) { - return this.__builtinValueImporter(v) - } else { - return this.#valueDefault - } - } - - "getValue" () { return this.#value ?? (this.#value = this.#getValue()); } - "setValue" (v) { - if (this.#value !== v) { - this.#value = v - localStorage.setItem(this.#id, this.__builtinValueExporter(this.#value)) - setTimeout(() => $(document).trigger(`${this.#eventId}::change`, [ v, this ]), 1) - } - } - - "getLabel" () { return this.#label; } - "setLabel" (label) { this.#label = label; return this; } - - "getDefaultValue" () { return this.#valueDefault; } - "setDefaultValue" (vd) { this.#valueDefault = vd; return this; } - - "onChange" (fn) { $(document).on(`${this.#eventId}::change`, (_,v,i) => fn(v, i)); } - __setIdPrefix (prefix) { this.#id = `${prefix}_${this.#id}`; } -} - -globalThis.LCNToggleSetting = class LCNToggleSetting extends LCNSetting { - __builtinValueImporter (v) { return v == "1"; } - __builtinValueExporter (v) { return v ? "1" : ""; } - __builtinDOMConstructor () { - const div = document.createElement("div") - const chk = document.createElement("input") - const txt = document.createElement("label") - txt.innerText = this.getLabel() - chk.type = "checkbox" - chk.checked = this.getValue() - chk.addEventListener("click", e => { - e.preventDefault(); - this.setValue(!this.getValue()) - }) - this.onChange(v => chk.checked = v) - - div.appendChild(chk) - div.appendChild(txt) - return div - } -} - -globalThis.LCNSettingsSubcategory = class LCNSettingsSubcategory { - - #tab_id = null; - #id = null; - - #fieldset = null; - #legend = null; - #label = null; - - static "for" (tab_id, id) { - const domid = `lcnssc_${tab_id}_${id}` - const inst = document.getElementById(domid)?.$LCNSettingsSubcategory - if (inst == null) { - const fieldset = document.createElement("fieldset") - const legend = document.createElement("legend") - fieldset.id = domid - fieldset.appendChild(legend) - - // XXX: extend_tab only takes a string so this hacky workaround is used to let us use the regular dom api - Options.extend_tab(tab_id, ``) - const div = document.getElementById(`__${domid}`)?.parentElement - assert.ok(div) - - div.replaceChildren(fieldset) - return new this(tab_id, id, fieldset) - } else { - return inst - } - } - - "constructor" (tab_id, id, fieldset) { - this.#tab_id = tab_id - this.#id = id - this.#fieldset = fieldset - this.#legend = this.#fieldset.querySelector("legend") - this.#fieldset.$LCNSettingsSubcategory = this - } - - "getLabel" () { return this.#label; } - "setLabel" (label) { this.#legend.innerText = this.#label = label; return this; } - "addSetting" (setting) { - assert.ok(setting instanceof LCNSetting) - setting.__setIdPrefix(`lcnsetting_${this.#tab_id}_${this.#id}`) - if (setting.__builtinDOMConstructor != null) { - const div = setting.__builtinDOMConstructor() - div.classList.add("lcn-setting-entry") - this.#fieldset.appendChild(div) - } - - return this - } - -} - -$().ready(() => { - LCNSite.INSTANCE = new LCNSite(); - - for (const clazz of [ LCNPost, LCNPostInfo, LCNThread, LCNPostContainer, LCNPostWrapper ]) { - clazz.allNodes = (node=document) => node.querySelectorAll(clazz.selector) - clazz.all = (node=document) => Array.prototype.map.apply(clazz.allNodes(node), [ elem => clazz.assign(elem) ]); - clazz.clear = (node=document) => Array.prototype.forEach.apply(clazz.allNodes(node), [ elem => elem[clazz.nodeAttrib] = null ]) - clazz.forEach = (fn, node=document) => clazz.allNodes(node).forEach(elem => fn(clazz.assign(elem))) - clazz.filter = (fn, node=document) => clazz.all(node).filter(fn) - clazz.find = fn => clazz.all().find(fn) - clazz.first = (node=document) => clazz.assign(node.querySelector(clazz.selector)) - clazz.last = (node=document) => clazz.assign(Array.prototype.at.apply(clazz.allNodes(node), [ -1 ])) - } - - // XXX: May be a cleaner way to do this but this should be fine for now. - for (const clazz of [ LCNPostContainer, LCNPostWrapper, LCNThread, LCNPost ]) { void clazz.all(); } - $(document).on("new_post", (e, post) => { - if (LCNSite.INSTANCE.isModRecentsPage()) { - void LCNPostWrapper.all() - } else { - void LCNPostContainer.all() - } - }) - - $(window).on("focus", () => LCNSite.INSTANCE.clearUnseen()) - $(document.body).on("mousemove", () => LCNSite.INSTANCE.clearUnseen()) -}) +LCNSite.INSTANCE = null; + +// globalThis.LCNPostInfo = class LCNPostInfo { +// +// static nodeAttrib = "$LCNPostInfo"; +// static selector = ".post:not(.grid-li)"; +// #boardId = null; +// #threadId = null; +// #postId = null; +// #name = null; +// #email = null; +// #capcode = null; +// #flag = null; +// #ip = null; +// #subject = null; +// #createdAt = null; +// +// #parent = null; +// #isThread = false; +// #isReply = false; +// #isLocked = false; +// #isSticky = false; +// +// static "assign" (post) { return post[this.nodeAttrib] ?? (post[this.nodeAttrib] = this.from(post)); } +// static "from" (post) { +// assert.ok(post.classList.contains("post"), "Arty must be expected Element.") +// const inst = new this() +// const intro = post.querySelector(".intro") +// const link = intro.querySelector(".post_no:not([id])").href.split("/").reverse() +// inst.#postId = link[0].slice(link[0].indexOf("#q") + 2) +// inst.#threadId = link[0].slice(0, link[0].indexOf(".")) +// inst.#boardId = link[2] +// inst.#isThread = post.classList.contains("op") +// inst.#isReply = !inst.#isThread +// +// inst.#subject = intro.querySelector(".subject")?.innerText ?? null +// inst.#name = intro.querySelector(".name")?.innerText ?? null +// inst.#email = intro.querySelector(".email")?.href.slice(7) ?? null +// inst.#flag = intro.querySelector(".flag")?.src.split("/").reverse()[0].slice(0, -4) ?? null +// +// inst.#capcode = intro.querySelector(".capcode")?.innerText ?? null +// inst.#ip = intro.querySelector(".ip-link")?.innerText ?? null +// inst.#createdAt = new Date(intro.querySelector("time[datetime]").dateTime ?? NaN) +// +// inst.#isSticky = !!intro.querySelector("i.fa-thumb-tack") +// inst.#isLocked = !!intro.querySelector("i.fa-lock") +// +// return inst +// } +// +// "getParent" () { return this.#parent; } +// "__setParent" (inst) { return this.#parent = inst; } +// +// "getBoardId" () { return this.#boardId; } +// "getThreadId" () { return this.#threadId; } +// "getPostId" () { return this.#postId; } +// "getHref" () { return `/${this.boardId}/res/${this.threadId}.html#q${this.postId}`; } +// +// "getName" () { return this.#name; } +// "getEmail" () { return this.#email; } +// "getIP" () { return this.#ip; } +// "getCapcode" () { return this.#capcode; } +// "getSubject" () { return this.#subject; } +// "getCreatedAt" () { return this.#createdAt; } +// +// "isSticky" () { return this.#isSticky; } +// "isLocked" () { return this.#isLocked; } +// "isThread" () { return this.#isThread; } +// "isReply" () { return this.#isReply; } +// +// "is" (info) { +// assert.ok(info, "Must be LCNPost.") +// return this.getBoardId() == info.getBoardId() && this.getPostId() == info.getPostId() +// } +// +// } +// +// globalThis.LCNPost = class LCNPost { +// +// static nodeAttrib = "$LCNPost"; +// static selector = ".post:not(.grid-li)"; +// #parent = null; +// #post = null; +// #info = null; +// #ipLink = null; +// #controls = null; +// #customControlsSeperatorNode = null; +// +// static "assign" (post) { return post[this.nodeAttrib] ?? (post[this.nodeAttrib] = this.from(post)); } +// static "from" (post) { return new this(post); } +// +// "constructor" (post) { +// assert.ok(post.classList.contains("post"), "Arty must be expected Element.") +// const intro = post.querySelector(".intro") +// this.#post = post +// this.#info = LCNPostInfo.assign(post) +// this.#ipLink = intro.querySelector(".ip-link") +// this.#controls = Array.prototype.at.apply(post.querySelectorAll(".controls"), [ -1 ]) +// +// assert.equal(this.#info.getParent(), null, "Info should not have parent.") +// this.#info.__setParent(this) +// } +// +// "jQuery" () { return $(this.#post); } +// "trigger" (event_id, data=null) { $(this.#post).trigger(event_id, [ data ]); } +// +// "getElement" () { return this.#post; } +// "getInfo" () { return this.#info; } +// +// "getIPLink" () { return this.#ipLink; } +// "setIP" (ip) { this.#ipLink.innerText = ip; } +// +// "getParent" () { return this.#parent; } +// "__setParent" (inst) { return this.#parent = inst; } +// +// static #NBSP = String.fromCharCode(160); +// "addCustomControl" (obj) { +// if (LCNSite.INSTANCE.isModerator()) { +// const link = document.createElement("a") +// link.innerText = `[${obj.btn}]` +// link.title = obj.tooltip +// +// if (typeof obj.href == "string") { +// link.href = obj.href +// link.referrerPolicy = "no-referrer" +// } else if (obj.onClick != undefined) { +// link.style.cursor = "pointer" +// link.addEventListener("click", e => { e.preventDefault(); obj.onClick(this); }) +// } +// +// if (this.#customControlsSeperatorNode == null) { +// this.#controls.insertBefore(this.#customControlsSeperatorNode = new Text(`${this.constructor.#NBSP}-${this.constructor.#NBSP}`), this.#controls.firstElementChild) +// } else { +// this.#controls.insertBefore(new Text(this.constructor.#NBSP), this.#customControlsSeperatorNode) +// } +// +// this.#controls.insertBefore(link, this.#customControlsSeperatorNode) +// } +// } +// +// } +// +// globalThis.LCNThread = class LCNThread { +// +// static nodeAttrib = "$LCNThread"; +// static selector = ".thread:not(.grid-li)"; +// #element = null; +// #parent = null; +// #op = null; +// +// static "assign" (thread) { return thread[this.nodeAttrib] ?? (thread[this.nodeAttrib] = this.from(thread)); } +// static "from" (thread) { return new this(thread); } +// +// "constructor" (thread) { +// assert.ok(thread.classList.contains("thread"), "Arty must be expected Element.") +// this.#element = thread +// this.#op = LCNPost.assign(this.#element.querySelector(".post.op")) +// +// //assert.equal(this.#op.getParent(), null, "Op should not have parent.") +// this.#op.__setParent(this) +// } +// +// "getElement" () { return this.#element; } +// "getContent" () { return this.#op; } +// "getPosts" () { return Array.prototype.map.apply(this.#element.querySelectorAll(".post"), [ el => LCNPost.assign(el) ]); } +// "getReplies" () { return Array.prototype.map.apply(this.#element.querySelectorAll(".post:not(.op)"), [ el => LCNPost.assign(el) ]); } +// +// "getParent" () { return this.#parent; } +// "__setParent" (inst) { return this.#parent = inst; } +// } +// +// +// globalThis.LCNPostContainer = class LCNPostContainer { +// +// static nodeAttrib = "$LCNPostContainer"; +// static selector = ".postcontainer"; +// #parent = null; +// #element = null; +// #content = null; +// #postId = null; +// #boardId = null; +// +// static "assign" (container) { return container[this.nodeAttrib] ?? (container[this.nodeAttrib] = this.from(container)); } +// static "from" (container) { return new this(container); } +// +// "constructor" (container) { +// assert.ok(container.classList.contains("postcontainer"), "Arty must be expected Element.") +// const child = container.querySelector(".thread, .post") +// this.#element = container +// this.#content = child.classList.contains("thread") ? LCNThread.assign(child) : LCNPost.assign(child) +// this.#boardId = container.dataset.board +// this.#postId = container.id.slice(2) +// +// assert.equal(this.#content.getParent(), null, "Content should not have parent.") +// this.#content.__setParent(this) +// } +// +// "getElement" () { return this.#element; } +// "getContent" () { return this.#content; } +// "getBoardId" () { return this.#boardId; } +// "getPostId" () { return this.#postId; } +// +// "getParent" () { return this.#parent; } +// "__setParent" (inst) { return this.#parent = inst; } +// +// } +// +// globalThis.LCNPostWrapper = class LCNPostWrapper { +// +// static nodeAttrib = "$LCNPostWrapper"; +// static selector = ".post-wrapper"; +// #wrapper = null; +// #eitaLink = null; +// #eitaId = null; +// #eitaHref = null +// #content = null; +// +// static "assign" (wrapper) { return wrapper[this.nodeAttrib] ?? (wrapper[this.nodeAttrib] = this.from(wrapper)); } +// static "from" (wrapper) { return new this(wrapper); } +// +// "constructor" (wrapper) { +// assert.ok(wrapper.classList.contains("post-wrapper"), "Arty must be expected Element.") +// this.#wrapper = wrapper +// this.#eitaLink = wrapper.querySelector(".eita-link") +// this.#eitaId = this.#eitaLink.id +// this.#eitaHref = this.#eitaLink.href +// void Array.prototype.find.apply(wrapper.children, [ +// el => { +// if (el.classList.contains("thread")) { +// return this.#content = LCNThread.assign(el) +// } else if (el.classList.contains("postcontainer")) { +// return this.#content = LCNPostContainer.assign(el) +// } +// } +// ]) +// +// assert.ok(this.#content, "Wrapper should contain content.") +// assert.equal(this.#content.getParent(), null, "Content should not have parent.") +// this.#content.__setParent(this) +// } +// +// "getPost" () { +// const post = this.getContent().getContent() +// assert.ok(post instanceof LCNPost, "Post should be LCNPost.") +// return post +// } +// +// "getElement" () { return this.#wrapper; } +// "getContent" () { return this.#content; } +// "getEitaId" () { return this.#eitaId; } +// "getEitaHref" () { return this.#eitaHref; } +// "getEitaLink" () { return this.#eitaLink; } +// +// } +// +// globalThis.LCNSetting = class LCNSetting { +// #id = null; +// #eventId = null; +// #label = null; +// #value = null; +// #valueDefault = null; +// +// static "build" (id) { return new this(id); } +// +// "constructor" (id) { +// this.#id = id; +// this.#eventId = `lcnsetting::${this.#id}` +// } +// +// #getValue () { +// const v = localStorage.getItem(this.#id) +// if (v != null) { +// return this.__builtinValueImporter(v) +// } else { +// return this.#valueDefault +// } +// } +// +// "getValue" () { return this.#value ?? (this.#value = this.#getValue()); } +// "setValue" (v) { +// if (this.#value !== v) { +// this.#value = v +// localStorage.setItem(this.#id, this.__builtinValueExporter(this.#value)) +// setTimeout(() => $(document).trigger(`${this.#eventId}::change`, [ v, this ]), 1) +// } +// } +// +// "getLabel" () { return this.#label; } +// "setLabel" (label) { this.#label = label; return this; } +// +// "getDefaultValue" () { return this.#valueDefault; } +// "setDefaultValue" (vd) { this.#valueDefault = vd; return this; } +// +// "onChange" (fn) { $(document).on(`${this.#eventId}::change`, (_,v,i) => fn(v, i)); } +// __setIdPrefix (prefix) { this.#id = `${prefix}_${this.#id}`; } +// } +// +// globalThis.LCNToggleSetting = class LCNToggleSetting extends LCNSetting { +// __builtinValueImporter (v) { return v == "1"; } +// __builtinValueExporter (v) { return v ? "1" : ""; } +// __builtinDOMConstructor () { +// const div = document.createElement("div") +// const chk = document.createElement("input") +// const txt = document.createElement("label") +// txt.innerText = this.getLabel() +// chk.type = "checkbox" +// chk.checked = this.getValue() +// chk.addEventListener("click", e => { +// e.preventDefault(); +// this.setValue(!this.getValue()) +// }) +// this.onChange(v => chk.checked = v) +// +// div.appendChild(chk) +// div.appendChild(txt) +// return div +// } +// } +// +// globalThis.LCNSettingsSubcategory = class LCNSettingsSubcategory { +// +// #tab_id = null; +// #id = null; +// +// #fieldset = null; +// #legend = null; +// #label = null; +// +// static "for" (tab_id, id) { +// const domid = `lcnssc_${tab_id}_${id}` +// const inst = document.getElementById(domid)?.$LCNSettingsSubcategory +// if (inst == null) { +// const fieldset = document.createElement("fieldset") +// const legend = document.createElement("legend") +// fieldset.id = domid +// fieldset.appendChild(legend) +// +// // XXX: extend_tab only takes a string so this hacky workaround is used to let us use the regular dom api +// Options.extend_tab(tab_id, ``) +// const div = document.getElementById(`__${domid}`)?.parentElement +// assert.ok(div) +// +// div.replaceChildren(fieldset) +// return new this(tab_id, id, fieldset) +// } else { +// return inst +// } +// } +// +// "constructor" (tab_id, id, fieldset) { +// this.#tab_id = tab_id +// this.#id = id +// this.#fieldset = fieldset +// this.#legend = this.#fieldset.querySelector("legend") +// this.#fieldset.$LCNSettingsSubcategory = this +// } +// +// "getLabel" () { return this.#label; } +// "setLabel" (label) { this.#legend.innerText = this.#label = label; return this; } +// "addSetting" (setting) { +// assert.ok(setting instanceof LCNSetting) +// setting.__setIdPrefix(`lcnsetting_${this.#tab_id}_${this.#id}`) +// if (setting.__builtinDOMConstructor != null) { +// const div = setting.__builtinDOMConstructor() +// div.classList.add("lcn-setting-entry") +// this.#fieldset.appendChild(div) +// } +// +// return this +// } +// +// } +// +// $().ready(() => { +// LCNSite.INSTANCE = new LCNSite(); +// +// for (const clazz of [ LCNPost, LCNPostInfo, LCNThread, LCNPostContainer, LCNPostWrapper ]) { +// clazz.allNodes = (node=document) => node.querySelectorAll(clazz.selector) +// clazz.all = (node=document) => Array.prototype.map.apply(clazz.allNodes(node), [ elem => clazz.assign(elem) ]); +// clazz.clear = (node=document) => Array.prototype.forEach.apply(clazz.allNodes(node), [ elem => elem[clazz.nodeAttrib] = null ]) +// clazz.forEach = (fn, node=document) => clazz.allNodes(node).forEach(elem => fn(clazz.assign(elem))) +// clazz.filter = (fn, node=document) => clazz.all(node).filter(fn) +// clazz.find = fn => clazz.all().find(fn) +// clazz.first = (node=document) => clazz.assign(node.querySelector(clazz.selector)) +// clazz.last = (node=document) => clazz.assign(Array.prototype.at.apply(clazz.allNodes(node), [ -1 ])) +// } +// +// // XXX: May be a cleaner way to do this but this should be fine for now. +// for (const clazz of [ LCNPostContainer, LCNPostWrapper, LCNThread, LCNPost ]) { void clazz.all(); } +// $(document).on("new_post", (e, post) => { +// if (LCNSite.INSTANCE.isModRecentsPage()) { +// void LCNPostWrapper.all() +// } else { +// void LCNPostContainer.all() +// } +// }) +// +// $(window).on("focus", () => LCNSite.INSTANCE.clearUnseen()) +// $(document.body).on("mousemove", () => LCNSite.INSTANCE.clearUnseen()) +// }) diff --git a/js/lcn/utils.js b/js/lcn/utils.js index a8b2771b..250f5bf9 100644 --- a/js/lcn/utils.js +++ b/js/lcn/utils.js @@ -6,36 +6,38 @@ const assert = { "equal": (actual, expected, message="No message set") => { if (actual !== expected) { - const err = new Error(`Assertion Failed. ${message}`) + const err = new Error(`Assertion Failed. ${message}`); err.data = { actual, expected} - Error.captureStackTrace?.(err, assert.equal) - debugger - throw err + //Error.captureStackTrace?.(err, assert.equal); + debugger; + throw err; } }, "ok": (actual, message="No message set") => { if (!actual) { - const err = new Error(`Assertion Failed. ${message}`) + const err = new Error(`Assertion Failed. ${message}`); err.data = { actual } - Error.captureStackTrace?.(err, assert.ok) - debugger - throw err + //Error.captureStackTrace?.(err, assert.ok); + debugger; + throw err; } } -} +}; + +if (AbortSignal.any == null) { + AbortSignal.any = (signals) => { + const controller = new AbortController(); + const abortFn = () => { + for (const signal of signals) { + signal.removeEventListener("abort", abortFn); + } + controller.abort(); + } -AbortSignal.any ??= function (signals) { - const controller = new AbortController() - const abortFn = () => { for (const signal of signals) { - signal.removeEventListener("abort", abortFn) + signal.addEventListener("abort", abortFn); } - controller.abort() - } - for (const signal of signals) { - signal.addEventListener("abort", abortFn) + return controller.signal; } - - return controller.signal } -- 2.44.1 From 19c090fe906344e8710a9fe96b94a5fa1f79963d Mon Sep 17 00:00:00 2001 From: towards-a-new-leftypol Date: Tue, 27 Feb 2024 05:20:43 +0000 Subject: [PATCH 02/71] get some of classes.js building --- js/lcn/classes.js | 106 ++++++++++++++++++++++++---------------------- 1 file changed, 56 insertions(+), 50 deletions(-) diff --git a/js/lcn/classes.js b/js/lcn/classes.js index 8a48ddac..32db607f 100644 --- a/js/lcn/classes.js +++ b/js/lcn/classes.js @@ -46,9 +46,11 @@ globalThis.LCNSite = class LCNSite { this._isModLogPage = this._isModPage && (location.search == "?/log" || location.search.startsWith("?/log/")); this._unseen = 0; this._pageTitle = document.title; + this._doTitleUpdate = () => { document.title = (this._unseen > 0 ? `(${this._unseen}) ` : "") + this._pageTitle; }; + this._favicon = document.querySelector("head > link[rel=\"shortcut icon\"]"); this._generatedStyle = null; } @@ -105,54 +107,55 @@ globalThis.LCNSite = class LCNSite { LCNSite.INSTANCE = null; -// globalThis.LCNPostInfo = class LCNPostInfo { -// -// static nodeAttrib = "$LCNPostInfo"; -// static selector = ".post:not(.grid-li)"; -// #boardId = null; -// #threadId = null; -// #postId = null; -// #name = null; -// #email = null; -// #capcode = null; -// #flag = null; -// #ip = null; -// #subject = null; -// #createdAt = null; -// -// #parent = null; -// #isThread = false; -// #isReply = false; -// #isLocked = false; -// #isSticky = false; -// -// static "assign" (post) { return post[this.nodeAttrib] ?? (post[this.nodeAttrib] = this.from(post)); } -// static "from" (post) { -// assert.ok(post.classList.contains("post"), "Arty must be expected Element.") -// const inst = new this() -// const intro = post.querySelector(".intro") -// const link = intro.querySelector(".post_no:not([id])").href.split("/").reverse() -// inst.#postId = link[0].slice(link[0].indexOf("#q") + 2) -// inst.#threadId = link[0].slice(0, link[0].indexOf(".")) -// inst.#boardId = link[2] -// inst.#isThread = post.classList.contains("op") -// inst.#isReply = !inst.#isThread -// -// inst.#subject = intro.querySelector(".subject")?.innerText ?? null -// inst.#name = intro.querySelector(".name")?.innerText ?? null -// inst.#email = intro.querySelector(".email")?.href.slice(7) ?? null -// inst.#flag = intro.querySelector(".flag")?.src.split("/").reverse()[0].slice(0, -4) ?? null -// -// inst.#capcode = intro.querySelector(".capcode")?.innerText ?? null -// inst.#ip = intro.querySelector(".ip-link")?.innerText ?? null -// inst.#createdAt = new Date(intro.querySelector("time[datetime]").dateTime ?? NaN) -// -// inst.#isSticky = !!intro.querySelector("i.fa-thumb-tack") -// inst.#isLocked = !!intro.querySelector("i.fa-lock") -// -// return inst -// } -// +globalThis.LCNPostInfo = class LCNPostInfo { + + static "constructor" () { + this._boardId = null; + this._threadId = null; + this._postId = null; + this._name = null; + this._email = null; + this._capcode = null; + this._flag = null; + this._ip = null; + this._subject = null; + this._createdAt = null; + this._parent = null; + this._isThread = false; + this._isReply = false; + this._isLocked = false; + this._isSticky = false; + } + + + static "assign" (post) { return post[this.nodeAttrib] ?? (post[this.nodeAttrib] = this.from(post)); } + static "from" (post) { + assert.ok(post.classList.contains("post"), "Arty must be expected Element.") + const inst = new this() + const intro = post.querySelector(".intro") + const link = intro.querySelector(".post_no:not([id])").href.split("/").reverse() + inst.#postId = link[0].slice(link[0].indexOf("#q") + 2) + inst.#threadId = link[0].slice(0, link[0].indexOf(".")) + inst.#boardId = link[2] + inst.#isThread = post.classList.contains("op") + inst.#isReply = !inst.#isThread + + inst.#subject = intro.querySelector(".subject")?.innerText ?? null + inst.#name = intro.querySelector(".name")?.innerText ?? null + inst.#email = intro.querySelector(".email")?.href.slice(7) ?? null + inst.#flag = intro.querySelector(".flag")?.src.split("/").reverse()[0].slice(0, -4) ?? null + + inst.#capcode = intro.querySelector(".capcode")?.innerText ?? null + inst.#ip = intro.querySelector(".ip-link")?.innerText ?? null + inst.#createdAt = new Date(intro.querySelector("time[datetime]").dateTime ?? NaN) + + inst.#isSticky = !!intro.querySelector("i.fa-thumb-tack") + inst.#isLocked = !!intro.querySelector("i.fa-lock") + + return inst + } + + // "getParent" () { return this.#parent; } // "__setParent" (inst) { return this.#parent = inst; } // @@ -178,8 +181,11 @@ LCNSite.INSTANCE = null; // return this.getBoardId() == info.getBoardId() && this.getPostId() == info.getPostId() // } // -// } -// +} + +LCNPostInfo.nodeAttrib = "$LCNPostInfo"; +LCNPostInfo.selector = ".post:not(.grid-li)"; + // globalThis.LCNPost = class LCNPost { // // static nodeAttrib = "$LCNPost"; -- 2.44.1 From 314dd4d2c30bbad9cdf636871b09e36f90d34076 Mon Sep 17 00:00:00 2001 From: towards-a-new-leftypol Date: Tue, 27 Feb 2024 05:28:20 +0000 Subject: [PATCH 03/71] This works so far --- js/lcn/classes.js | 55 +++++++++++++++++++++++------------------------ 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/js/lcn/classes.js b/js/lcn/classes.js index 32db607f..117bb0f3 100644 --- a/js/lcn/classes.js +++ b/js/lcn/classes.js @@ -128,34 +128,33 @@ globalThis.LCNPostInfo = class LCNPostInfo { } - static "assign" (post) { return post[this.nodeAttrib] ?? (post[this.nodeAttrib] = this.from(post)); } - static "from" (post) { - assert.ok(post.classList.contains("post"), "Arty must be expected Element.") - const inst = new this() - const intro = post.querySelector(".intro") - const link = intro.querySelector(".post_no:not([id])").href.split("/").reverse() - inst.#postId = link[0].slice(link[0].indexOf("#q") + 2) - inst.#threadId = link[0].slice(0, link[0].indexOf(".")) - inst.#boardId = link[2] - inst.#isThread = post.classList.contains("op") - inst.#isReply = !inst.#isThread - - inst.#subject = intro.querySelector(".subject")?.innerText ?? null - inst.#name = intro.querySelector(".name")?.innerText ?? null - inst.#email = intro.querySelector(".email")?.href.slice(7) ?? null - inst.#flag = intro.querySelector(".flag")?.src.split("/").reverse()[0].slice(0, -4) ?? null - - inst.#capcode = intro.querySelector(".capcode")?.innerText ?? null - inst.#ip = intro.querySelector(".ip-link")?.innerText ?? null - inst.#createdAt = new Date(intro.querySelector("time[datetime]").dateTime ?? NaN) - - inst.#isSticky = !!intro.querySelector("i.fa-thumb-tack") - inst.#isLocked = !!intro.querySelector("i.fa-lock") - - return inst - } - - +// static "assign" (post) { return post[this.nodeAttrib] ?? (post[this.nodeAttrib] = this.from(post)); } +// static "from" (post) { +// assert.ok(post.classList.contains("post"), "Arty must be expected Element.") +// const inst = new this() +// const intro = post.querySelector(".intro") +// const link = intro.querySelector(".post_no:not([id])").href.split("/").reverse() +// inst.#postId = link[0].slice(link[0].indexOf("#q") + 2) +// inst.#threadId = link[0].slice(0, link[0].indexOf(".")) +// inst.#boardId = link[2] +// inst.#isThread = post.classList.contains("op") +// inst.#isReply = !inst.#isThread +// +// inst.#subject = intro.querySelector(".subject")?.innerText ?? null +// inst.#name = intro.querySelector(".name")?.innerText ?? null +// inst.#email = intro.querySelector(".email")?.href.slice(7) ?? null +// inst.#flag = intro.querySelector(".flag")?.src.split("/").reverse()[0].slice(0, -4) ?? null +// +// inst.#capcode = intro.querySelector(".capcode")?.innerText ?? null +// inst.#ip = intro.querySelector(".ip-link")?.innerText ?? null +// inst.#createdAt = new Date(intro.querySelector("time[datetime]").dateTime ?? NaN) +// +// inst.#isSticky = !!intro.querySelector("i.fa-thumb-tack") +// inst.#isLocked = !!intro.querySelector("i.fa-lock") +// +// return inst +// } +// // "getParent" () { return this.#parent; } // "__setParent" (inst) { return this.#parent = inst; } // -- 2.44.1 From 4ce3e0060f2d24cdcdf5e3cbe8edca048243af37 Mon Sep 17 00:00:00 2001 From: towards-a-new-leftypol Date: Tue, 27 Feb 2024 18:28:51 +0000 Subject: [PATCH 04/71] class.js seems to load now, idk if i've introduced any bugs though --- js/lcn/classes.js | 768 +++++++++++++++++++++++----------------------- 1 file changed, 392 insertions(+), 376 deletions(-) diff --git a/js/lcn/classes.js b/js/lcn/classes.js index 117bb0f3..3f4e4cfa 100644 --- a/js/lcn/classes.js +++ b/js/lcn/classes.js @@ -3,6 +3,14 @@ * @author jonsmy */ +function cont(value_to_test, fn) { + if (value_to_test != null) { + return fn(value_to_test); + } else { + return null; + } +} + globalThis.LCNSite = class LCNSite { static "createAbortable" () { @@ -105,7 +113,7 @@ globalThis.LCNSite = class LCNSite { } -LCNSite.INSTANCE = null; +globalThis.LCNSite.INSTANCE = null; globalThis.LCNPostInfo = class LCNPostInfo { @@ -128,383 +136,391 @@ globalThis.LCNPostInfo = class LCNPostInfo { } -// static "assign" (post) { return post[this.nodeAttrib] ?? (post[this.nodeAttrib] = this.from(post)); } -// static "from" (post) { -// assert.ok(post.classList.contains("post"), "Arty must be expected Element.") -// const inst = new this() -// const intro = post.querySelector(".intro") -// const link = intro.querySelector(".post_no:not([id])").href.split("/").reverse() -// inst.#postId = link[0].slice(link[0].indexOf("#q") + 2) -// inst.#threadId = link[0].slice(0, link[0].indexOf(".")) -// inst.#boardId = link[2] -// inst.#isThread = post.classList.contains("op") -// inst.#isReply = !inst.#isThread -// -// inst.#subject = intro.querySelector(".subject")?.innerText ?? null -// inst.#name = intro.querySelector(".name")?.innerText ?? null -// inst.#email = intro.querySelector(".email")?.href.slice(7) ?? null -// inst.#flag = intro.querySelector(".flag")?.src.split("/").reverse()[0].slice(0, -4) ?? null -// -// inst.#capcode = intro.querySelector(".capcode")?.innerText ?? null -// inst.#ip = intro.querySelector(".ip-link")?.innerText ?? null -// inst.#createdAt = new Date(intro.querySelector("time[datetime]").dateTime ?? NaN) -// -// inst.#isSticky = !!intro.querySelector("i.fa-thumb-tack") -// inst.#isLocked = !!intro.querySelector("i.fa-lock") -// -// return inst -// } -// -// "getParent" () { return this.#parent; } -// "__setParent" (inst) { return this.#parent = inst; } -// -// "getBoardId" () { return this.#boardId; } -// "getThreadId" () { return this.#threadId; } -// "getPostId" () { return this.#postId; } -// "getHref" () { return `/${this.boardId}/res/${this.threadId}.html#q${this.postId}`; } -// -// "getName" () { return this.#name; } -// "getEmail" () { return this.#email; } -// "getIP" () { return this.#ip; } -// "getCapcode" () { return this.#capcode; } -// "getSubject" () { return this.#subject; } -// "getCreatedAt" () { return this.#createdAt; } -// -// "isSticky" () { return this.#isSticky; } -// "isLocked" () { return this.#isLocked; } -// "isThread" () { return this.#isThread; } -// "isReply" () { return this.#isReply; } -// -// "is" (info) { -// assert.ok(info, "Must be LCNPost.") -// return this.getBoardId() == info.getBoardId() && this.getPostId() == info.getPostId() -// } -// + static "assign" (post) { + if (post[this.nodeAttrib] == null) { + return post[this.nodeAttrib] = this.from(post); + } else { + return post[this.nodeAttrib]; + } + } + + static "from" (post) { + assert.ok(post.classList.contains("post"), "Arty must be expected Element."); + const inst = new this(); + const intro = post.querySelector(".intro"); + const link = intro.querySelector(".post_no:not([id])").href.split("/").reverse(); + inst._postId = link[0].slice(link[0].indexOf("#q") + 2); + inst._threadId = link[0].slice(0, link[0].indexOf(".")); + inst._boardId = link[2]; + inst._isThread = post.classList.contains("op"); + inst._isReply = !inst._isThread; + + inst._subject = cont(intro.querySelector(".subject"), x => x.innerText); + inst._name = cont(intro.querySelector(".name"), x => x.innerText); + inst._email = cont(intro.querySelector(".email"), x => x.href.slice(7)); + inst._flag = cont(intro.querySelector(".flag"), x => x.src.split("/").reverse()[0].slice(0, -4)); + + inst._capcode = cont(intro.querySelector(".capcode"), x => x.innerText); + inst._ip = cont(intro.querySelector(".ip-link"), x => x.innerText); + inst._createdAt = new Date(intro.querySelector("time[datetime]").dateTime || NaN); + + inst._isSticky = !!intro.querySelector("i.fa-thumb-tack"); + inst._isLocked = !!intro.querySelector("i.fa-lock"); + + return inst; + } + + "getParent" () { return this._parent; } + "__setParent" (inst) { return this._parent = inst; } + + "getBoardId" () { return this._boardId; } + "getThreadId" () { return this._threadId; } + "getPostId" () { return this._postId; } + "getHref" () { return `/${this.boardId}/res/${this.threadId}.html#q${this.postId}`; } + + "getName" () { return this._name; } + "getEmail" () { return this._email; } + "getIP" () { return this._ip; } + "getCapcode" () { return this._capcode; } + "getSubject" () { return this._subject; } + "getCreatedAt" () { return this._createdAt; } + + "isSticky" () { return this._isSticky; } + "isLocked" () { return this._isLocked; } + "isThread" () { return this._isThread; } + "isReply" () { return this._isReply; } + + "is" (info) { + assert.ok(info, "Must be LCNPost.") + return this.getBoardId() == info.getBoardId() && this.getPostId() == info.getPostId() + } + } LCNPostInfo.nodeAttrib = "$LCNPostInfo"; LCNPostInfo.selector = ".post:not(.grid-li)"; -// globalThis.LCNPost = class LCNPost { -// -// static nodeAttrib = "$LCNPost"; -// static selector = ".post:not(.grid-li)"; -// #parent = null; -// #post = null; -// #info = null; -// #ipLink = null; -// #controls = null; -// #customControlsSeperatorNode = null; -// -// static "assign" (post) { return post[this.nodeAttrib] ?? (post[this.nodeAttrib] = this.from(post)); } -// static "from" (post) { return new this(post); } -// -// "constructor" (post) { -// assert.ok(post.classList.contains("post"), "Arty must be expected Element.") -// const intro = post.querySelector(".intro") -// this.#post = post -// this.#info = LCNPostInfo.assign(post) -// this.#ipLink = intro.querySelector(".ip-link") -// this.#controls = Array.prototype.at.apply(post.querySelectorAll(".controls"), [ -1 ]) -// -// assert.equal(this.#info.getParent(), null, "Info should not have parent.") -// this.#info.__setParent(this) -// } -// -// "jQuery" () { return $(this.#post); } -// "trigger" (event_id, data=null) { $(this.#post).trigger(event_id, [ data ]); } -// -// "getElement" () { return this.#post; } -// "getInfo" () { return this.#info; } -// -// "getIPLink" () { return this.#ipLink; } -// "setIP" (ip) { this.#ipLink.innerText = ip; } -// -// "getParent" () { return this.#parent; } -// "__setParent" (inst) { return this.#parent = inst; } -// -// static #NBSP = String.fromCharCode(160); -// "addCustomControl" (obj) { -// if (LCNSite.INSTANCE.isModerator()) { -// const link = document.createElement("a") -// link.innerText = `[${obj.btn}]` -// link.title = obj.tooltip -// -// if (typeof obj.href == "string") { -// link.href = obj.href -// link.referrerPolicy = "no-referrer" -// } else if (obj.onClick != undefined) { -// link.style.cursor = "pointer" -// link.addEventListener("click", e => { e.preventDefault(); obj.onClick(this); }) -// } -// -// if (this.#customControlsSeperatorNode == null) { -// this.#controls.insertBefore(this.#customControlsSeperatorNode = new Text(`${this.constructor.#NBSP}-${this.constructor.#NBSP}`), this.#controls.firstElementChild) -// } else { -// this.#controls.insertBefore(new Text(this.constructor.#NBSP), this.#customControlsSeperatorNode) -// } -// -// this.#controls.insertBefore(link, this.#customControlsSeperatorNode) -// } -// } -// -// } -// -// globalThis.LCNThread = class LCNThread { -// -// static nodeAttrib = "$LCNThread"; -// static selector = ".thread:not(.grid-li)"; -// #element = null; -// #parent = null; -// #op = null; -// -// static "assign" (thread) { return thread[this.nodeAttrib] ?? (thread[this.nodeAttrib] = this.from(thread)); } -// static "from" (thread) { return new this(thread); } -// -// "constructor" (thread) { -// assert.ok(thread.classList.contains("thread"), "Arty must be expected Element.") -// this.#element = thread -// this.#op = LCNPost.assign(this.#element.querySelector(".post.op")) -// -// //assert.equal(this.#op.getParent(), null, "Op should not have parent.") -// this.#op.__setParent(this) -// } -// -// "getElement" () { return this.#element; } -// "getContent" () { return this.#op; } -// "getPosts" () { return Array.prototype.map.apply(this.#element.querySelectorAll(".post"), [ el => LCNPost.assign(el) ]); } -// "getReplies" () { return Array.prototype.map.apply(this.#element.querySelectorAll(".post:not(.op)"), [ el => LCNPost.assign(el) ]); } -// -// "getParent" () { return this.#parent; } -// "__setParent" (inst) { return this.#parent = inst; } -// } -// -// -// globalThis.LCNPostContainer = class LCNPostContainer { -// -// static nodeAttrib = "$LCNPostContainer"; -// static selector = ".postcontainer"; -// #parent = null; -// #element = null; -// #content = null; -// #postId = null; -// #boardId = null; -// -// static "assign" (container) { return container[this.nodeAttrib] ?? (container[this.nodeAttrib] = this.from(container)); } -// static "from" (container) { return new this(container); } -// -// "constructor" (container) { -// assert.ok(container.classList.contains("postcontainer"), "Arty must be expected Element.") -// const child = container.querySelector(".thread, .post") -// this.#element = container -// this.#content = child.classList.contains("thread") ? LCNThread.assign(child) : LCNPost.assign(child) -// this.#boardId = container.dataset.board -// this.#postId = container.id.slice(2) -// -// assert.equal(this.#content.getParent(), null, "Content should not have parent.") -// this.#content.__setParent(this) -// } -// -// "getElement" () { return this.#element; } -// "getContent" () { return this.#content; } -// "getBoardId" () { return this.#boardId; } -// "getPostId" () { return this.#postId; } -// -// "getParent" () { return this.#parent; } -// "__setParent" (inst) { return this.#parent = inst; } -// -// } -// -// globalThis.LCNPostWrapper = class LCNPostWrapper { -// -// static nodeAttrib = "$LCNPostWrapper"; -// static selector = ".post-wrapper"; -// #wrapper = null; -// #eitaLink = null; -// #eitaId = null; -// #eitaHref = null -// #content = null; -// -// static "assign" (wrapper) { return wrapper[this.nodeAttrib] ?? (wrapper[this.nodeAttrib] = this.from(wrapper)); } -// static "from" (wrapper) { return new this(wrapper); } -// -// "constructor" (wrapper) { -// assert.ok(wrapper.classList.contains("post-wrapper"), "Arty must be expected Element.") -// this.#wrapper = wrapper -// this.#eitaLink = wrapper.querySelector(".eita-link") -// this.#eitaId = this.#eitaLink.id -// this.#eitaHref = this.#eitaLink.href -// void Array.prototype.find.apply(wrapper.children, [ -// el => { -// if (el.classList.contains("thread")) { -// return this.#content = LCNThread.assign(el) -// } else if (el.classList.contains("postcontainer")) { -// return this.#content = LCNPostContainer.assign(el) -// } -// } -// ]) -// -// assert.ok(this.#content, "Wrapper should contain content.") -// assert.equal(this.#content.getParent(), null, "Content should not have parent.") -// this.#content.__setParent(this) -// } -// -// "getPost" () { -// const post = this.getContent().getContent() -// assert.ok(post instanceof LCNPost, "Post should be LCNPost.") -// return post -// } -// -// "getElement" () { return this.#wrapper; } -// "getContent" () { return this.#content; } -// "getEitaId" () { return this.#eitaId; } -// "getEitaHref" () { return this.#eitaHref; } -// "getEitaLink" () { return this.#eitaLink; } -// -// } -// -// globalThis.LCNSetting = class LCNSetting { -// #id = null; -// #eventId = null; -// #label = null; -// #value = null; -// #valueDefault = null; -// -// static "build" (id) { return new this(id); } -// -// "constructor" (id) { -// this.#id = id; -// this.#eventId = `lcnsetting::${this.#id}` -// } -// -// #getValue () { -// const v = localStorage.getItem(this.#id) -// if (v != null) { -// return this.__builtinValueImporter(v) -// } else { -// return this.#valueDefault -// } -// } -// -// "getValue" () { return this.#value ?? (this.#value = this.#getValue()); } -// "setValue" (v) { -// if (this.#value !== v) { -// this.#value = v -// localStorage.setItem(this.#id, this.__builtinValueExporter(this.#value)) -// setTimeout(() => $(document).trigger(`${this.#eventId}::change`, [ v, this ]), 1) -// } -// } -// -// "getLabel" () { return this.#label; } -// "setLabel" (label) { this.#label = label; return this; } -// -// "getDefaultValue" () { return this.#valueDefault; } -// "setDefaultValue" (vd) { this.#valueDefault = vd; return this; } -// -// "onChange" (fn) { $(document).on(`${this.#eventId}::change`, (_,v,i) => fn(v, i)); } -// __setIdPrefix (prefix) { this.#id = `${prefix}_${this.#id}`; } -// } -// -// globalThis.LCNToggleSetting = class LCNToggleSetting extends LCNSetting { -// __builtinValueImporter (v) { return v == "1"; } -// __builtinValueExporter (v) { return v ? "1" : ""; } -// __builtinDOMConstructor () { -// const div = document.createElement("div") -// const chk = document.createElement("input") -// const txt = document.createElement("label") -// txt.innerText = this.getLabel() -// chk.type = "checkbox" -// chk.checked = this.getValue() -// chk.addEventListener("click", e => { -// e.preventDefault(); -// this.setValue(!this.getValue()) -// }) -// this.onChange(v => chk.checked = v) -// -// div.appendChild(chk) -// div.appendChild(txt) -// return div -// } -// } -// -// globalThis.LCNSettingsSubcategory = class LCNSettingsSubcategory { -// -// #tab_id = null; -// #id = null; -// -// #fieldset = null; -// #legend = null; -// #label = null; -// -// static "for" (tab_id, id) { -// const domid = `lcnssc_${tab_id}_${id}` -// const inst = document.getElementById(domid)?.$LCNSettingsSubcategory -// if (inst == null) { -// const fieldset = document.createElement("fieldset") -// const legend = document.createElement("legend") -// fieldset.id = domid -// fieldset.appendChild(legend) -// -// // XXX: extend_tab only takes a string so this hacky workaround is used to let us use the regular dom api -// Options.extend_tab(tab_id, ``) -// const div = document.getElementById(`__${domid}`)?.parentElement -// assert.ok(div) -// -// div.replaceChildren(fieldset) -// return new this(tab_id, id, fieldset) -// } else { -// return inst -// } -// } -// -// "constructor" (tab_id, id, fieldset) { -// this.#tab_id = tab_id -// this.#id = id -// this.#fieldset = fieldset -// this.#legend = this.#fieldset.querySelector("legend") -// this.#fieldset.$LCNSettingsSubcategory = this -// } -// -// "getLabel" () { return this.#label; } -// "setLabel" (label) { this.#legend.innerText = this.#label = label; return this; } -// "addSetting" (setting) { -// assert.ok(setting instanceof LCNSetting) -// setting.__setIdPrefix(`lcnsetting_${this.#tab_id}_${this.#id}`) -// if (setting.__builtinDOMConstructor != null) { -// const div = setting.__builtinDOMConstructor() -// div.classList.add("lcn-setting-entry") -// this.#fieldset.appendChild(div) -// } -// -// return this -// } -// -// } -// -// $().ready(() => { -// LCNSite.INSTANCE = new LCNSite(); -// -// for (const clazz of [ LCNPost, LCNPostInfo, LCNThread, LCNPostContainer, LCNPostWrapper ]) { -// clazz.allNodes = (node=document) => node.querySelectorAll(clazz.selector) -// clazz.all = (node=document) => Array.prototype.map.apply(clazz.allNodes(node), [ elem => clazz.assign(elem) ]); -// clazz.clear = (node=document) => Array.prototype.forEach.apply(clazz.allNodes(node), [ elem => elem[clazz.nodeAttrib] = null ]) -// clazz.forEach = (fn, node=document) => clazz.allNodes(node).forEach(elem => fn(clazz.assign(elem))) -// clazz.filter = (fn, node=document) => clazz.all(node).filter(fn) -// clazz.find = fn => clazz.all().find(fn) -// clazz.first = (node=document) => clazz.assign(node.querySelector(clazz.selector)) -// clazz.last = (node=document) => clazz.assign(Array.prototype.at.apply(clazz.allNodes(node), [ -1 ])) -// } -// -// // XXX: May be a cleaner way to do this but this should be fine for now. -// for (const clazz of [ LCNPostContainer, LCNPostWrapper, LCNThread, LCNPost ]) { void clazz.all(); } -// $(document).on("new_post", (e, post) => { -// if (LCNSite.INSTANCE.isModRecentsPage()) { -// void LCNPostWrapper.all() -// } else { -// void LCNPostContainer.all() -// } -// }) -// -// $(window).on("focus", () => LCNSite.INSTANCE.clearUnseen()) -// $(document.body).on("mousemove", () => LCNSite.INSTANCE.clearUnseen()) -// }) +globalThis.LCNPost = class LCNPost { + + static "assign" (post) { return post[this.nodeAttrib] || (post[this.nodeAttrib] = this.from(post)); } + static "from" (post) { return new this(post); } + + "constructor" (post) { + this._parent = null; + this._post = null; + this._info = null; + this._ipLink = null; + this._controls = null; + this._customControlsSeperatorNode = null; + + assert.ok(post.classList.contains("post"), "Arty must be expected Element."); + const intro = post.querySelector(".intro"); + this._post = post; + this._info = LCNPostInfo.assign(post); + this._ipLink = intro.querySelector(".ip-link"); + this._controls = Array.prototype.at.apply(post.querySelectorAll(".controls"), [ -1 ]); + + assert.equal(this._info.getParent(), null, "Info should not have parent."); + this._info.__setParent(this); + } + + + "jQuery" () { return $(this._post); } + "trigger" (event_id, data=null) { $(this._post).trigger(event_id, [ data ]); } + + "getElement" () { return this._post; } + "getInfo" () { return this._info; } + + "getIPLink" () { return this._ipLink; } + "setIP" (ip) { this._ipLink.innerText = ip; } + + "getParent" () { return this._parent; } + "__setParent" (inst) { return this._parent = inst; } + + "addCustomControl" (obj) { + if (LCNSite.INSTANCE.isModerator()) { + const link = document.createElement("a") + link.innerText = `[${obj.btn}]` + link.title = obj.tooltip + + if (typeof obj.href == "string") { + link.href = obj.href + link.referrerPolicy = "no-referrer" + } else if (obj.onClick != undefined) { + link.style.cursor = "pointer" + link.addEventListener("click", e => { e.preventDefault(); obj.onClick(this); }) + } + + if (this._customControlsSeperatorNode == null) { + this._controls.insertBefore(this._customControlsSeperatorNode = new Text(`${this.constructor.NBSP}-${this.constructor.NBSP}`), this._controls.firstElementChild) + } else { + this._controls.insertBefore(new Text(this.constructor.NBSP), this._customControlsSeperatorNode) + } + + this._controls.insertBefore(link, this._customControlsSeperatorNode) + } + } + +} + +globalThis.LCNPost.nodeAttrib = "$LCNPost"; +globalThis.LCNPost.selector = ".post:not(.grid-li)"; +globalThis.LCNPost. NBSP = String.fromCharCode(160); + +globalThis.LCNThread = class LCNThread { + + static "assign" (thread) { return thread[this.nodeAttrib] || (thread[this.nodeAttrib] = this.from(thread)); } + static "from" (thread) { return new this(thread); } + + "constructor" (thread) { + this._element = null; + this._parent = null; + this._op = null; + assert.ok(thread.classList.contains("thread"), "Arty must be expected Element.") + this._element = thread + this._op = LCNPost.assign(this._element.querySelector(".post.op")) + + assert.equal(this._op.getParent(), null, "Op should not have parent.") + this._op.__setParent(this) + } + + "getElement" () { return this._element; } + "getContent" () { return this._op; } + "getPosts" () { return Array.prototype.map.apply(this._element.querySelectorAll(".post"), [ el => LCNPost.assign(el) ]); } + "getReplies" () { return Array.prototype.map.apply(this._element.querySelectorAll(".post:not(.op)"), [ el => LCNPost.assign(el) ]); } + + "getParent" () { return this._parent; } + "__setParent" (inst) { return this._parent = inst; } +} + +globalThis.LCNThread.nodeAttrib = "$LCNThread"; +globalThis.LCNThread.selector = ".thread:not(.grid-li)"; + + +globalThis.LCNPostContainer = class LCNPostContainer { + + static "assign" (container) { return container[this.nodeAttrib] || (container[this.nodeAttrib] = this.from(container)); } + static "from" (container) { return new this(container); } + + "constructor" (container) { + this._parent = null; + this._element = null; + this._content = null; + this._postId = null; + this._boardId = null; + + assert.ok(container.classList.contains("postcontainer"), "Arty must be expected Element.") + const child = container.querySelector(".thread, .post") + this._element = container + this._content = child.classList.contains("thread") ? LCNThread.assign(child) : LCNPost.assign(child) + this._boardId = container.dataset.board + this._postId = container.id.slice(2) + + assert.equal(this._content.getParent(), null, "Content should not have parent.") + this._content.__setParent(this) + } + + "getElement" () { return this._element; } + "getContent" () { return this._content; } + "getBoardId" () { return this._boardId; } + "getPostId" () { return this._postId; } + + "getParent" () { return this._parent; } + "__setParent" (inst) { return this._parent = inst; } + +} + +globalThis.LCNPostContainer.nodeAttrib = "$LCNPostContainer"; +globalThis.LCNPostContainer.selector = ".postcontainer"; + + +globalThis.LCNPostWrapper = class LCNPostWrapper { + + static "assign" (wrapper) { return wrapper[this.nodeAttrib] || (wrapper[this.nodeAttrib] = this.from(wrapper)); } + static "from" (wrapper) { return new this(wrapper); } + + "constructor" (wrapper) { + this._wrapper = null; + this._eitaLink = null; + this._eitaId = null; + this._eitaHref = null + this._content = null; + + assert.ok(wrapper.classList.contains("post-wrapper"), "Arty must be expected Element.") + this._wrapper = wrapper + this._eitaLink = wrapper.querySelector(".eita-link") + this._eitaId = this._eitaLink.id + this._eitaHref = this._eitaLink.href + + void Array.prototype.find.apply(wrapper.children, [ + el => { + if (el.classList.contains("thread")) { + return this._content = LCNThread.assign(el) + } else if (el.classList.contains("postcontainer")) { + return this._content = LCNPostContainer.assign(el) + } + } + ]) + + assert.ok(this._content, "Wrapper should contain content.") + assert.equal(this._content.getParent(), null, "Content should not have parent.") + this._content.__setParent(this) + } + + "getPost" () { + const post = this.getContent().getContent() + assert.ok(post instanceof LCNPost, "Post should be LCNPost.") + return post + } + + "getElement" () { return this._wrapper; } + "getContent" () { return this._content; } + "getEitaId" () { return this._eitaId; } + "getEitaHref" () { return this._eitaHref; } + "getEitaLink" () { return this._eitaLink; } +} + +globalThis.LCNPostWrapper.nodeAttrib = "$LCNPostWrapper"; +globalThis.LCNPostWrapper.selector = ".post-wrapper"; + + +globalThis.LCNSetting = class LCNSetting { + static "build" (id) { return new this(id); } + + "constructor" (id) { + this._id = null; + this._eventId = null; + this._label = null; + this._value = null; + this._valueDefault = null; + + this._id = id; + this._eventId = `lcnsetting::${this._id}` + } + + "_getValue" () { + const v = localStorage.getItem(this._id) + if (v != null) { + return this.__builtinValueImporter(v) + } else { + return this._valueDefault + } + } + + "getValue" () { return this._value || (this._value = this._getValue()); } + "setValue" (v) { + if (this._value !== v) { + this._value = v + localStorage.setItem(this._id, this.__builtinValueExporter(this._value)) + setTimeout(() => $(document).trigger(`${this._eventId}::change`, [ v, this ]), 1) + } + } + + "getLabel" () { return this._label; } + "setLabel" (label) { this._label = label; return this; } + + "getDefaultValue" () { return this._valueDefault; } + "setDefaultValue" (vd) { this._valueDefault = vd; return this; } + + "onChange" (fn) { $(document).on(`${this._eventId}::change`, (_,v,i) => fn(v, i)); } + __setIdPrefix (prefix) { this._id = `${prefix}_${this._id}`; } +} + +globalThis.LCNToggleSetting = class LCNToggleSetting extends LCNSetting { + __builtinValueImporter (v) { return v == "1"; } + __builtinValueExporter (v) { return v ? "1" : ""; } + __builtinDOMConstructor () { + const div = document.createElement("div") + const chk = document.createElement("input") + const txt = document.createElement("label") + txt.innerText = this.getLabel() + chk.type = "checkbox" + chk.checked = this.getValue() + chk.addEventListener("click", e => { + e.preventDefault(); + this.setValue(!this.getValue()) + }) + this.onChange(v => chk.checked = v) + + div.appendChild(chk) + div.appendChild(txt) + return div + } +} + + +globalThis.LCNSettingsSubcategory = class LCNSettingsSubcategory { + + static "for" (tab_id, id) { + const domid = `lcnssc_${tab_id}_${id}` + const inst = cont(document.getElementById(domid), x => x.$LCNSettingsSubcategory) + + if (inst == null) { + const fieldset = document.createElement("fieldset") + const legend = document.createElement("legend") + fieldset.id = domid + fieldset.appendChild(legend) + + // XXX: extend_tab only takes a string so this hacky workaround is used to let us use the regular dom api + Options.extend_tab(tab_id, ``) + const div = cont(document.getElementById(`__${domid}`), x => x.parentElement) + assert.ok(div) + + div.replaceChildren(fieldset) + return new this(tab_id, id, fieldset) + } else { + return inst + } + } + + "constructor" (tab_id, id, fieldset) { + this._tab_id = tab_id + this._id = id + this._fieldset = fieldset + this._legend = this._fieldset.querySelector("legend") + this._fieldset.$LCNSettingsSubcategory = this + } + + "getLabel" () { return this._label; } + "setLabel" (label) { this._legend.innerText = this._label = label; return this; } + "addSetting" (setting) { + assert.ok(setting instanceof LCNSetting) + setting.__setIdPrefix(`lcnsetting_${this._tab_id}_${this._id}`) + if (setting.__builtinDOMConstructor != null) { + const div = setting.__builtinDOMConstructor() + div.classList.add("lcn-setting-entry") + this._fieldset.appendChild(div) + } + + return this + } + +} + +$().ready(() => { + LCNSite.INSTANCE = new LCNSite(); + + for (const clazz of [ LCNPost, LCNPostInfo, LCNThread, LCNPostContainer, LCNPostWrapper ]) { + clazz.allNodes = (node=document) => node.querySelectorAll(clazz.selector) + clazz.all = (node=document) => Array.prototype.map.apply(clazz.allNodes(node), [ elem => clazz.assign(elem) ]); + clazz.clear = (node=document) => Array.prototype.forEach.apply(clazz.allNodes(node), [ elem => elem[clazz.nodeAttrib] = null ]) + clazz.forEach = (fn, node=document) => clazz.allNodes(node).forEach(elem => fn(clazz.assign(elem))) + clazz.filter = (fn, node=document) => clazz.all(node).filter(fn) + clazz.find = fn => clazz.all().find(fn) + clazz.first = (node=document) => clazz.assign(node.querySelector(clazz.selector)) + clazz.last = (node=document) => clazz.assign(Array.prototype.at.apply(clazz.allNodes(node), [ -1 ])) + } + + // XXX: May be a cleaner way to do this but this should be fine for now. + for (const clazz of [ LCNPostContainer, LCNPostWrapper, LCNThread, LCNPost ]) { void clazz.all(); } + $(document).on("new_post", (e, post) => { + if (LCNSite.INSTANCE.isModRecentsPage()) { + void LCNPostWrapper.all() + } else { + void LCNPostContainer.all() + } + }) + + $(window).on("focus", () => LCNSite.INSTANCE.clearUnseen()) + $(document.body).on("mousemove", () => LCNSite.INSTANCE.clearUnseen()) +}) -- 2.44.1 From d366f4cdf8f2062fa3d087f6f3548c187e97c8cf Mon Sep 17 00:00:00 2001 From: towards-a-new-leftypol Date: Tue, 27 Feb 2024 19:55:30 +0000 Subject: [PATCH 05/71] debugging and fixes. can't use Array.prototype.at --- js/lcn/classes.js | 58 +++++++++++++++++----------------- js/lcn/thread_autoupdater.js | 33 ++++++++++++++++---- js/lcn/utils.js | 60 ++++++++++++++++++++++++++++++++++++ 3 files changed, 117 insertions(+), 34 deletions(-) diff --git a/js/lcn/classes.js b/js/lcn/classes.js index 3f4e4cfa..43ebab4b 100644 --- a/js/lcn/classes.js +++ b/js/lcn/classes.js @@ -3,14 +3,6 @@ * @author jonsmy */ -function cont(value_to_test, fn) { - if (value_to_test != null) { - return fn(value_to_test); - } else { - return null; - } -} - globalThis.LCNSite = class LCNSite { static "createAbortable" () { @@ -41,7 +33,7 @@ globalThis.LCNSite = class LCNSite { return null }; - static "constructor" () { + constructor () { this._isModerator = document.body.classList.contains("is-moderator"); this._isThreadPage = document.body.classList.contains("active-thread"); @@ -55,16 +47,18 @@ globalThis.LCNSite = class LCNSite { this._unseen = 0; this._pageTitle = document.title; - this._doTitleUpdate = () => { - document.title = (this._unseen > 0 ? `(${this._unseen}) ` : "") + this._pageTitle; - }; - this._favicon = document.querySelector("head > link[rel=\"shortcut icon\"]"); this._generatedStyle = null; } + "_doTitleUpdate" () { + document.title = (this._unseen > 0 ? `(${this._unseen}) ` : "") + this._pageTitle; + }; + "isModerator" () { return this._isModerator; } - "isThreadPage" () { return this._isThreadPage; } + "isThreadPage" () { + return this._isThreadPage; + } "isBoardPage" () { return this._isBoardPage; } "isCatalogPage" () { return this._isCatalogPage; } @@ -117,7 +111,7 @@ globalThis.LCNSite.INSTANCE = null; globalThis.LCNPostInfo = class LCNPostInfo { - static "constructor" () { + constructor () { this._boardId = null; this._threadId = null; this._postId = null; @@ -202,10 +196,13 @@ LCNPostInfo.selector = ".post:not(.grid-li)"; globalThis.LCNPost = class LCNPost { - static "assign" (post) { return post[this.nodeAttrib] || (post[this.nodeAttrib] = this.from(post)); } + static "assign" (post) { + return post[this.nodeAttrib] || (post[this.nodeAttrib] = this.from(post)); + } + static "from" (post) { return new this(post); } - "constructor" (post) { + constructor (post) { this._parent = null; this._post = null; this._info = null; @@ -218,9 +215,13 @@ globalThis.LCNPost = class LCNPost { this._post = post; this._info = LCNPostInfo.assign(post); this._ipLink = intro.querySelector(".ip-link"); - this._controls = Array.prototype.at.apply(post.querySelectorAll(".controls"), [ -1 ]); - assert.equal(this._info.getParent(), null, "Info should not have parent."); + //this._controls = Array.prototype.at.apply(post.querySelectorAll(".controls"), [ -1 ]); + // the above line fails on older browsers so do this instead: + const elements = Array.prototype.slice.call(post.querySelectorAll(".controls")); + this._controls = elements[elements.length - 1]; + + //assert.equal(this._info.getParent(), null, "Info should not have parent."); this._info.__setParent(this); } @@ -269,17 +270,19 @@ globalThis.LCNPost. NBSP = String.fromCharCode(160); globalThis.LCNThread = class LCNThread { - static "assign" (thread) { return thread[this.nodeAttrib] || (thread[this.nodeAttrib] = this.from(thread)); } + static "assign" (thread) { + return thread[this.nodeAttrib] || (thread[this.nodeAttrib] = this.from(thread)); + } + static "from" (thread) { return new this(thread); } - "constructor" (thread) { + constructor (thread) { this._element = null; this._parent = null; this._op = null; assert.ok(thread.classList.contains("thread"), "Arty must be expected Element.") this._element = thread this._op = LCNPost.assign(this._element.querySelector(".post.op")) - assert.equal(this._op.getParent(), null, "Op should not have parent.") this._op.__setParent(this) } @@ -302,7 +305,7 @@ globalThis.LCNPostContainer = class LCNPostContainer { static "assign" (container) { return container[this.nodeAttrib] || (container[this.nodeAttrib] = this.from(container)); } static "from" (container) { return new this(container); } - "constructor" (container) { + constructor (container) { this._parent = null; this._element = null; this._content = null; @@ -339,7 +342,7 @@ globalThis.LCNPostWrapper = class LCNPostWrapper { static "assign" (wrapper) { return wrapper[this.nodeAttrib] || (wrapper[this.nodeAttrib] = this.from(wrapper)); } static "from" (wrapper) { return new this(wrapper); } - "constructor" (wrapper) { + constructor (wrapper) { this._wrapper = null; this._eitaLink = null; this._eitaId = null; @@ -387,7 +390,7 @@ globalThis.LCNPostWrapper.selector = ".post-wrapper"; globalThis.LCNSetting = class LCNSetting { static "build" (id) { return new this(id); } - "constructor" (id) { + constructor (id) { this._id = null; this._eventId = null; this._label = null; @@ -448,7 +451,6 @@ globalThis.LCNToggleSetting = class LCNToggleSetting extends LCNSetting { } } - globalThis.LCNSettingsSubcategory = class LCNSettingsSubcategory { static "for" (tab_id, id) { @@ -473,7 +475,7 @@ globalThis.LCNSettingsSubcategory = class LCNSettingsSubcategory { } } - "constructor" (tab_id, id, fieldset) { + constructor (tab_id, id, fieldset) { this._tab_id = tab_id this._id = id this._fieldset = fieldset @@ -507,7 +509,7 @@ $().ready(() => { clazz.forEach = (fn, node=document) => clazz.allNodes(node).forEach(elem => fn(clazz.assign(elem))) clazz.filter = (fn, node=document) => clazz.all(node).filter(fn) clazz.find = fn => clazz.all().find(fn) - clazz.first = (node=document) => clazz.assign(node.querySelector(clazz.selector)) + clazz.first = (node=document) => { return clazz.assign(node.querySelector(clazz.selector)); } clazz.last = (node=document) => clazz.assign(Array.prototype.at.apply(clazz.allNodes(node), [ -1 ])) } diff --git a/js/lcn/thread_autoupdater.js b/js/lcn/thread_autoupdater.js index dfa22441..d7860256 100644 --- a/js/lcn/thread_autoupdater.js +++ b/js/lcn/thread_autoupdater.js @@ -4,19 +4,19 @@ */ $().ready(() => { - const kIsEnabled = LCNToggleSetting.build("enabled") //const kIsBellEnabled = LCNToggleSetting.build("bellEnabled") void LCNSettingsSubcategory.for("general", "threadUpdater") .setLabel("Thread Updater") .addSetting(kIsEnabled .setLabel(_("Fetch new replies in the background")) - .setDefaultValue(true)) + .setDefaultValue(true)); /*.addSetting(kIsBellEnabled .setLabel(_("Play an audible chime when new replies are found")) .setDefaultValue(false))*/; if (LCNSite.INSTANCE.isThreadPage()) { + console.log("LCNSite.INSTANCE.isThreadPage() is true"); let threadUpdateStatus = null let secondsCounter = 0 let threadState = null @@ -30,7 +30,7 @@ $().ready(() => { const abortable = LCNSite.createAbortable() const threadStatsItems = [] const updateDOMStatus = () => { - const text = threadState ?? (secondsCounter >= 0 ? `${secondsCounter}s` : "…") + const text = threadState || (secondsCounter >= 0 ? `${secondsCounter}s` : "…") threadUpdateStatus.innerText = text } @@ -59,7 +59,7 @@ $().ready(() => { } const findMissingReplies = (thread_op, thread_dom, thread_latest) => { - const lastPostTs = (thread_dom.at(-1)?.getInfo() ?? thread_op.getInfo()).getCreatedAt().getTime() + const lastPostTs = (cont(thread_dom.at(-1), x => x.getInfo()) || thread_op.getInfo()).getCreatedAt().getTime() const missing = [] for (const pc of thread_latest.reverse()) { @@ -122,33 +122,48 @@ $().ready(() => { let onTickId = null const onTickFn = async () => { + console.log("tick function"); + void secondsCounter--; + console.log(secondsCounter); onTickClean() updateDOMStatus() + console.log("tick function2"); if (threadState == null) { + console.log("tick function3"); + console.log(secondsCounter); + if (secondsCounter < 0) { + console.log("tick function3.5"); const thread = LCNThread.first() + + console.log("tick function4"); try { await updateStatsFn(thread) if (threadState == null && threadStats.last_modified > (thread.getPosts().at(-1).getInfo().getCreatedAt().getTime() / 1000)) { updateThreadFn(thread, await fetchThreadFn()) } + console.log("tick function5"); + const threadEl = thread.getElement() statUniqueIPs.innerText = threadStats.unique_ips statReplies.innerText = thread.getReplies().length statFiles.innerText = threadEl.querySelectorAll(".files .file").length - threadEl.querySelectorAll(".files .file .post-image.deleted").length statPage.innerText = threadStats.page + 1 updateSecondsByTSLP(thread.getPosts().at(-1).getInfo()) + console.log("tick function6"); } catch (error) { console.error("threadAutoUpdater: Failed while processing update. Probably a network error", error) secondsCounter = 60 } } - onTickId = setTimeout(onTickFn, 1000) + //onTickId = setTimeout(onTickFn, 1000) } + + console.log("bottom of tick function"); } $(document).on("ajax_after_post", (_, xhr_body) => { @@ -167,15 +182,18 @@ $().ready(() => { $(document).on("thread_manual_refresh", () => { if (kIsEnabled.getValue() && secondsCounter >= 0) { secondsCounter = 0 + console.log("thread_manual_refresh handler"); onTickFn() } }) let floaterLinkBox = null const onStateChangeFn = v => { + console.log("onStateChangeFn"); onTickClean() if (v) { + console.log("v is true"); _domsetup_btn: { const container = LCNSite.INSTANCE.getFloaterLContainer() floaterLinkBox = document.createElement("span") @@ -231,9 +249,11 @@ $().ready(() => { } } + console.log("thread_manual_refresh trigger"); $(document).trigger("thread_manual_refresh") } else { - floaterLinkBox?.remove() + console.log("v is false"); + cont(floaterLinkBox, x => x.remove()) floaterLinkBox = null statReplies = null statFiles = null @@ -247,5 +267,6 @@ $().ready(() => { kIsEnabled.onChange(onStateChangeFn) onStateChangeFn(kIsEnabled.getValue()) + console.log("Bottom of if block"); } }) diff --git a/js/lcn/utils.js b/js/lcn/utils.js index 250f5bf9..b6309b7b 100644 --- a/js/lcn/utils.js +++ b/js/lcn/utils.js @@ -3,6 +3,54 @@ * @author jonsmy */ +function cont(value_to_test, fn) { + if (value_to_test != null) { + return fn(value_to_test); + } else { + return null; + } +} + +function text(s) { + return document.createTextNode(s); +} + +function prepend(elem, children) { + var child = elem.firstChild; + + if (child) { + elem.insertBefore(children, child); + } else { + elem.appendChild(children); + } +} + + +function _log() { + for (var arg of arguments) { + if (arg == null) { + continue; + } + + var pre = document.createElement('pre'); + pre.appendChild(text(arg.toString())); + document.body.appendChild(pre); + try { + prepend(document.body, pre); + } catch (e) { + var pre = document.createElement('pre'); + pre.appendChild(text(e.toString())); + document.body.appendChild(pre); + } + + } +} + +var console = { + log: _log, + error: _log +}; + const assert = { "equal": (actual, expected, message="No message set") => { if (actual !== expected) { @@ -41,3 +89,15 @@ if (AbortSignal.any == null) { return controller.signal; } } + +// polyfill for replaceChildren +if( Node.prototype.replaceChildren === undefined) { + Node.prototype.replaceChildren = function(addNodes) { + while(this.lastChild) { + this.removeChild(this.lastChild); + } + if (addNodes !== undefined) { + this.append(addNodes); + } + } +} -- 2.44.1 From cbe878c7d54520afd6417f914379959478d3a90d Mon Sep 17 00:00:00 2001 From: towards-a-new-leftypol Date: Tue, 27 Feb 2024 20:42:36 +0000 Subject: [PATCH 06/71] Finally fixed and tested --- js/lcn/classes.js | 16 +++++++--------- js/lcn/thread_autoupdater.js | 24 ++---------------------- js/lcn/utils.js | 10 ++++++++++ 3 files changed, 19 insertions(+), 31 deletions(-) diff --git a/js/lcn/classes.js b/js/lcn/classes.js index 43ebab4b..3200ea2d 100644 --- a/js/lcn/classes.js +++ b/js/lcn/classes.js @@ -215,12 +215,7 @@ globalThis.LCNPost = class LCNPost { this._post = post; this._info = LCNPostInfo.assign(post); this._ipLink = intro.querySelector(".ip-link"); - - //this._controls = Array.prototype.at.apply(post.querySelectorAll(".controls"), [ -1 ]); - // the above line fails on older browsers so do this instead: - const elements = Array.prototype.slice.call(post.querySelectorAll(".controls")); - this._controls = elements[elements.length - 1]; - + this._controls = Array.prototype.at.apply(post.querySelectorAll(".controls"), [ -1 ]); //assert.equal(this._info.getParent(), null, "Info should not have parent."); this._info.__setParent(this); } @@ -435,8 +430,11 @@ globalThis.LCNToggleSetting = class LCNToggleSetting extends LCNSetting { __builtinDOMConstructor () { const div = document.createElement("div") const chk = document.createElement("input") - const txt = document.createElement("label") - txt.innerText = this.getLabel() + const lbl = document.createElement("label") + const id = `${this._id}_input` + lbl.htmlFor = id + lbl.innerText = this.getLabel() + chk.id = id chk.type = "checkbox" chk.checked = this.getValue() chk.addEventListener("click", e => { @@ -446,7 +444,7 @@ globalThis.LCNToggleSetting = class LCNToggleSetting extends LCNSetting { this.onChange(v => chk.checked = v) div.appendChild(chk) - div.appendChild(txt) + div.appendChild(lbl) return div } } diff --git a/js/lcn/thread_autoupdater.js b/js/lcn/thread_autoupdater.js index d7860256..17a2ba86 100644 --- a/js/lcn/thread_autoupdater.js +++ b/js/lcn/thread_autoupdater.js @@ -16,7 +16,6 @@ $().ready(() => { .setDefaultValue(false))*/; if (LCNSite.INSTANCE.isThreadPage()) { - console.log("LCNSite.INSTANCE.isThreadPage() is true"); let threadUpdateStatus = null let secondsCounter = 0 let threadState = null @@ -122,52 +121,39 @@ $().ready(() => { let onTickId = null const onTickFn = async () => { - console.log("tick function"); - void secondsCounter--; - console.log(secondsCounter); onTickClean() updateDOMStatus() - console.log("tick function2"); if (threadState == null) { - console.log("tick function3"); - console.log(secondsCounter); - if (secondsCounter < 0) { - console.log("tick function3.5"); const thread = LCNThread.first() - console.log("tick function4"); try { await updateStatsFn(thread) if (threadState == null && threadStats.last_modified > (thread.getPosts().at(-1).getInfo().getCreatedAt().getTime() / 1000)) { updateThreadFn(thread, await fetchThreadFn()) } - console.log("tick function5"); - const threadEl = thread.getElement() statUniqueIPs.innerText = threadStats.unique_ips statReplies.innerText = thread.getReplies().length statFiles.innerText = threadEl.querySelectorAll(".files .file").length - threadEl.querySelectorAll(".files .file .post-image.deleted").length statPage.innerText = threadStats.page + 1 updateSecondsByTSLP(thread.getPosts().at(-1).getInfo()) - console.log("tick function6"); } catch (error) { console.error("threadAutoUpdater: Failed while processing update. Probably a network error", error) secondsCounter = 60 } } - //onTickId = setTimeout(onTickFn, 1000) + onTickId = setTimeout(onTickFn, 1000) } - console.log("bottom of tick function"); } $(document).on("ajax_after_post", (_, xhr_body) => { - if (kIsEnabled.getValue() && xhr_body != null) { + if (xhr_body != null) { if (!xhr_body.mod) { const thread = LCNThread.first() const dom = parser.parseFromString(xhr_body.thread, "text/html") @@ -182,18 +168,15 @@ $().ready(() => { $(document).on("thread_manual_refresh", () => { if (kIsEnabled.getValue() && secondsCounter >= 0) { secondsCounter = 0 - console.log("thread_manual_refresh handler"); onTickFn() } }) let floaterLinkBox = null const onStateChangeFn = v => { - console.log("onStateChangeFn"); onTickClean() if (v) { - console.log("v is true"); _domsetup_btn: { const container = LCNSite.INSTANCE.getFloaterLContainer() floaterLinkBox = document.createElement("span") @@ -249,10 +232,8 @@ $().ready(() => { } } - console.log("thread_manual_refresh trigger"); $(document).trigger("thread_manual_refresh") } else { - console.log("v is false"); cont(floaterLinkBox, x => x.remove()) floaterLinkBox = null statReplies = null @@ -267,6 +248,5 @@ $().ready(() => { kIsEnabled.onChange(onStateChangeFn) onStateChangeFn(kIsEnabled.getValue()) - console.log("Bottom of if block"); } }) diff --git a/js/lcn/utils.js b/js/lcn/utils.js index b6309b7b..7246bdf6 100644 --- a/js/lcn/utils.js +++ b/js/lcn/utils.js @@ -101,3 +101,13 @@ if( Node.prototype.replaceChildren === undefined) { } } } + +if (Array.prototype.at === undefined) { + Array.prototype.at = function(index) { + if (index >= 0) { + return this[index]; + } else { + return this[this.length + index]; + } + }; +} -- 2.44.1 From 22254fcf6c4b39aae51fee863e2dbfb4a31bb2e1 Mon Sep 17 00:00:00 2001 From: towards-a-new-leftypol Date: Tue, 27 Feb 2024 15:49:13 -0500 Subject: [PATCH 07/71] Remove console.log implementation that renders to the page from utils (was there to debug mobile) --- js/lcn/utils.js | 40 ---------------------------------------- 1 file changed, 40 deletions(-) diff --git a/js/lcn/utils.js b/js/lcn/utils.js index 7246bdf6..9485656b 100644 --- a/js/lcn/utils.js +++ b/js/lcn/utils.js @@ -11,46 +11,6 @@ function cont(value_to_test, fn) { } } -function text(s) { - return document.createTextNode(s); -} - -function prepend(elem, children) { - var child = elem.firstChild; - - if (child) { - elem.insertBefore(children, child); - } else { - elem.appendChild(children); - } -} - - -function _log() { - for (var arg of arguments) { - if (arg == null) { - continue; - } - - var pre = document.createElement('pre'); - pre.appendChild(text(arg.toString())); - document.body.appendChild(pre); - try { - prepend(document.body, pre); - } catch (e) { - var pre = document.createElement('pre'); - pre.appendChild(text(e.toString())); - document.body.appendChild(pre); - } - - } -} - -var console = { - log: _log, - error: _log -}; - const assert = { "equal": (actual, expected, message="No message set") => { if (actual !== expected) { -- 2.44.1 From da8afc8d13383b48b0c3e122d5a750f53c20c507 Mon Sep 17 00:00:00 2001 From: towards-a-new-leftypol Date: Thu, 14 Mar 2024 17:10:38 -0400 Subject: [PATCH 08/71] Rate limit opening posts for specific ip (basically copy flood-time-any filter and add ip condition) --- inc/instance-config.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/inc/instance-config.php b/inc/instance-config.php index 430fcd48..9f731e6f 100644 --- a/inc/instance-config.php +++ b/inc/instance-config.php @@ -569,5 +569,25 @@ $config['filters'][] = array( 'message' => 'Url shorteners are not allowed' ); +// Rate limit posting new threads over Tor +$config['filters'][] = array( + 'condition' => array( + /* + * Confusingly `isreply` is defined as: + * $flood_post['isreply'] == $post['op'] + * + * We only want to look at OP posts in the flood table. + */ + 'flood-match' => array('isreply'), + 'OP' => true, + 'flood-time-any' => 60 * 10 // 10 minutes + ), + 'noip' => true, + 'ip' => '127.0.0.1', + 'find_time' => 60 * 60 * 1, + 'action' => 'reject', + 'message' => 'New threads are being created too quickly. Wait [at most] 10 minutes' +); + $config['global_message'] = 'Matrix   IRC Chat   Mumble   Telegram   Discord'; $config['debug'] = false; -- 2.44.1 From 08afcdb42c8c5843f52786c0ff4d9a1488a8d194 Mon Sep 17 00:00:00 2001 From: towards-a-new-leftypol Date: Sun, 17 Mar 2024 16:54:27 -0400 Subject: [PATCH 09/71] Homepage: remove sidebar border, make sidebar wider (300px instead of 200px) --- templates/themes/categories/frames.html | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/templates/themes/categories/frames.html b/templates/themes/categories/frames.html index fbaf9a62..890963b7 100644 --- a/templates/themes/categories/frames.html +++ b/templates/themes/categories/frames.html @@ -10,11 +10,8 @@ .sidebar { grid-column: 1; grid-row: 1 / 3; - width: 200px; - border-right-color: gray; - border-right-style: solid; - border-width: 2px; - margin-right: 15px; + width: 100%; + margin-right: 15px; } .introduction { @@ -33,7 +30,7 @@ body { display: grid; - grid-template-columns: repeat(auto-fill,minmax(200px, 1fr)); + grid-template-columns: repeat(auto-fill,minmax(300px, 1fr)); gap: 20px; } -- 2.44.1 From 98b038bdb8b15c1749a02b100a0528a91f977e8e Mon Sep 17 00:00:00 2001 From: towards-a-new-leftypol Date: Sun, 17 Mar 2024 17:11:17 -0400 Subject: [PATCH 10/71] Further homepage changes --- templates/themes/categories/frames.html | 9 ++++----- templates/themes/categories/news.html | 16 ++++++++-------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/templates/themes/categories/frames.html b/templates/themes/categories/frames.html index 890963b7..5be39730 100644 --- a/templates/themes/categories/frames.html +++ b/templates/themes/categories/frames.html @@ -8,20 +8,19 @@