diff --git a/inc/display.php b/inc/display.php index 76468bea..43b54063 100644 --- a/inc/display.php +++ b/inc/display.php @@ -384,6 +384,7 @@ class Post extends PostProps { private $raw_body; private $has_file; private $tracked_cites; + private $filesize; public function __construct($post, $root=null, $mod=false) { global $config; @@ -394,8 +395,10 @@ class Post extends PostProps { $this->$key = $value; } - if (isset($this->files) && $this->files) + if (isset($this->files) && $this->files + && is_string($this->files)) { $this->files = @json_decode($this->files); + } $this->subject = utf8tohtml($this->subject); $this->name = utf8tohtml($this->name); diff --git a/inc/functions.php b/inc/functions.php index e0de1c2c..d7192f43 100644 --- a/inc/functions.php +++ b/inc/functions.php @@ -2303,6 +2303,8 @@ function buildThread($id, $return = false, $mod = false) { $action = generation_strategy('sb_thread', array($board['uri'], $id)); + $rendered_thread = null; + if ($action == 'rebuild' || $return || $mod) { $query = prepare(sprintf("SELECT *,'%s' as board FROM ``posts_%s`` WHERE (`thread` IS NULL AND `id` = :id) OR `thread` = :id ORDER BY `thread`,`id`", $board['uri'],$board['uri'])); $query->bindValue(':id', $id, PDO::PARAM_INT); @@ -2323,10 +2325,12 @@ function buildThread($id, $return = false, $mod = false) { $hasnoko50 = $thread->postCount() >= $config['noko50_min']; $antibot = $mod || $return ? false : create_antibot($board['uri'], $id); + $rendered_thread = $thread->build(); + $body = Element('thread.html', array( 'board' => $board, 'thread' => $thread, - 'body' => $thread->build(), + 'body' => $rendered_thread, 'config' => $config, 'id' => $id, 'mod' => $mod, @@ -2364,6 +2368,8 @@ function buildThread($id, $return = false, $mod = false) { } file_write($board['dir'] . $config['dir']['res'] . link_for($thread), $body); + + return $rendered_thread; } } diff --git a/inc/instance-config.php b/inc/instance-config.php index b18e24b0..430fcd48 100644 --- a/inc/instance-config.php +++ b/inc/instance-config.php @@ -403,8 +403,6 @@ $config['additional_javascript'][] = 'js/options/user-css.js'; $config['additional_javascript'][] = 'js/options/user-js.js'; $config['additional_javascript'][] = 'js/flag-preview.js'; $config['additional_javascript'][] = 'js/file-selector.js'; -$config['additional_javascript_defer'][] = 'js/auto-reload.js'; -$config['additional_javascript_defer'][] = 'js/thread-stats.js'; $config['additional_javascript_defer'][] = 'js/image-hover.js'; @@ -419,6 +417,7 @@ $config['additional_javascript_defer'][] = 'js/expand-video.js'; // New LCN scripts $config['additional_javascript'][] = 'js/lcn/utils.js'; $config['additional_javascript'][] = 'js/lcn/classes.js'; +$config['additional_javascript'][] = 'js/lcn/thread_autoupdater.js'; $config['additional_javascript_compile'] = true; $config['minify_js'] = true; diff --git a/js/lcn/classes.js b/js/lcn/classes.js index 035173e9..255a09d5 100644 --- a/js/lcn/classes.js +++ b/js/lcn/classes.js @@ -6,6 +6,34 @@ globalThis.LCNSite = class LCNSite { static INSTANCE = null; + static "createAbortable" () { + const obj = { "abort": null, "controller": null, "signal": null } + const setupController = () => { + obj.controller = new AbortController() + obj.signal = obj.controller.signal + } + + obj.abort = () => { + obj.controller.abort() + setupController() + } + + setupController() + return obj + } + + static "getThreadFromPages" (pages, thread_id) { + for (const page of pages) { + for (const thread of page.threads) { + if (thread_id == String(thread.no)) { + return { "page": page.page, ...thread } + } + } + } + + return null + } + #isModerator = document.body.classList.contains("is-moderator"); #isThreadPage = document.body.classList.contains("active-thread"); #isBoardPage = document.body.classList.contains("active-board"); @@ -53,10 +81,28 @@ globalThis.LCNSite = class LCNSite { this.#favicon.href = `/favicon${type ? "-" + type : ""}.ico` } + + "getFloaterLContainer" () { return document.getElementById("bar-bottom-l"); } + "getFloaterRContainer" () { return document.getElementById("bar-bottom-r"); } + "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) + } + 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; @@ -74,7 +120,7 @@ globalThis.LCNPostInfo = class LCNPostInfo { #isLocked = false; #isSticky = false; - static "assign" (post) { return post.$LCNPostInfo ?? (post.$LCNPostInfo = this.from(post)); } + 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() @@ -130,6 +176,8 @@ globalThis.LCNPostInfo = class LCNPostInfo { globalThis.LCNPost = class LCNPost { + static nodeAttrib = "$LCNPost"; + static selector = ".post:not(.grid-li)"; #parent = null; #post = null; #info = null; @@ -137,7 +185,7 @@ globalThis.LCNPost = class LCNPost { #controls = null; #customControlsSeperatorNode = null; - static "assign" (post) { return post.$LCNPost ?? (post.$LCNPost = 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) { @@ -146,7 +194,7 @@ globalThis.LCNPost = class LCNPost { this.#post = post this.#info = LCNPostInfo.assign(post) this.#ipLink = intro.querySelector(".ip-link") - this.#controls = arrLast(post.querySelectorAll(".controls")) + 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) @@ -193,26 +241,28 @@ globalThis.LCNPost = class LCNPost { globalThis.LCNThread = class LCNThread { + static nodeAttrib = "$LCNThread"; + static selector = ".thread:not(.grid-li)"; + #element = null; #parent = null; - #thread = null; #op = null; - static "assign" (thread) { return thread.$LCNThread ?? (thread.$LCNThread = 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) { assert.ok(thread.classList.contains("thread"), "Arty must be expected Element.") - this.#thread = thread - this.#op = LCNPost.assign(this.#thread.querySelector(".post.op")) + this.#element = thread + this.#op = LCNPost.assign(this.#element.querySelector(".post.op")) - assert.equal(this.#op.getParent(), null, "Op should not have parent.") + //assert.equal(this.#op.getParent(), null, "Op should not have parent.") this.#op.__setParent(this) } - "getElement" () { return this.#thread; } + "getElement" () { return this.#element; } "getContent" () { return this.#op; } - "getPosts" () { return Array.prototype.map.apply(this.#thread.querySelectorAll(".post"), [ el => LCNPost.assign(el) ]); } - "getReplies" () { return Array.prototype.map.apply(this.#thread.querySelectorAll(".post:not(.op)"), [ el => LCNPost.assign(el) ]); } + "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; } @@ -221,19 +271,21 @@ globalThis.LCNThread = class LCNThread { globalThis.LCNPostContainer = class LCNPostContainer { + static nodeAttrib = "$LCNPostContainer"; + static selector = ".postcontainer"; #parent = null; - #container = null; + #element = null; #content = null; #postId = null; #boardId = null; - static "assign" (container) { return container.$LCNPostContainer ?? (container.$LCNPostContainer = this.from(container)); } + 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.#container = container + 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) @@ -242,7 +294,7 @@ globalThis.LCNPostContainer = class LCNPostContainer { this.#content.__setParent(this) } - "getContainer" () { return this.#container; } + "getElement" () { return this.#element; } "getContent" () { return this.#content; } "getBoardId" () { return this.#boardId; } "getPostId" () { return this.#postId; } @@ -254,13 +306,15 @@ globalThis.LCNPostContainer = class LCNPostContainer { 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.$LCNPostWrapper ?? (wrapper.$LCNPostWrapper = this.from(wrapper)); } + static "assign" (wrapper) { return wrapper[this.nodeAttrib] ?? (wrapper[this.nodeAttrib] = this.from(wrapper)); } static "from" (wrapper) { return new this(wrapper); } "constructor" (wrapper) { @@ -298,23 +352,140 @@ globalThis.LCNPostWrapper = class LCNPostWrapper { } -globalThis.LCNPost.all = () => Array.prototype.map.apply(document.querySelectorAll(".post:not(.grid-li)"), [ node => LCNPost.assign(node) ]); -globalThis.LCNThread.all = () => Array.prototype.map.apply(document.querySelectorAll(".thread:not(.grid-li)"), [ node => LCNThread.assign(node) ]); -globalThis.LCNPostContainer.all = () => Array.prototype.map.apply(document.querySelectorAll(".postcontainer"), [ node => LCNPostContainer.assign(node) ]); -globalThis.LCNPostWrapper.all = () => Array.prototype.map.apply(document.querySelectorAll(".post-wrapper"), [ node => LCNPostWrapper.assign(node) ]); +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(); - const clazzes = [ LCNPost, LCNThread, LCNPostContainer, LCNPostWrapper ] - for (const clazz of clazzes) { - clazz.forEach = fn => clazz.all().forEach(fn) - clazz.filter = fn => clazz.all().filter(fn) + 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 clazzes) { void clazz.all(); } + for (const clazz of [ LCNPostContainer, LCNPostWrapper, LCNThread, LCNPost ]) { void clazz.all(); } $(document).on("new_post", (e, post) => { if (LCNSite.INSTANCE.isModRecentsPage()) { void LCNPostWrapper.all() @@ -322,4 +493,7 @@ $().ready(() => { void LCNPostContainer.all() } }) + + $(window).on("focus", () => LCNSite.INSTANCE.clearUnseen()) + $(document.body).on("mousemove", () => LCNSite.INSTANCE.clearUnseen()) }) diff --git a/js/lcn/thread_autoupdater.js b/js/lcn/thread_autoupdater.js new file mode 100644 index 00000000..2308e293 --- /dev/null +++ b/js/lcn/thread_autoupdater.js @@ -0,0 +1,253 @@ +/** + * @file Thread auto updater. + * @author jonsmy + */ + +$().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)) + /*.addSetting(kIsBellEnabled + .setLabel(_("Play an audible chime when new replies are found")) + .setDefaultValue(false))*/; + + if (LCNSite.INSTANCE.isThreadPage()) { + let threadUpdateStatus = null + let secondsCounter = 0 + let threadState = null + let threadStats = null + let statReplies = null + let statFiles = null + let statPage = null + let statUniqueIPs = null + + const parser = new DOMParser() + const abortable = LCNSite.createAbortable() + const threadStatsItems = [] + const updateDOMStatus = () => { + const text = threadState ?? (secondsCounter >= 0 ? `${secondsCounter}s` : "…") + threadUpdateStatus.innerText = text + } + + const updateSecondsByTSLP = post_info => { + secondsCounter = Math.floor(((Date.now() - post_info.getCreatedAt().getTime()) / 30000)) + secondsCounter = secondsCounter > 1000 ? 1000 : secondsCounter + secondsCounter = secondsCounter < 11 ? 11 : secondsCounter + } + + const updateStatsFn = async thread => { + // XXX: Using /%b/%d.json would be better however the page number isn't provided. + const res = await fetch(`/${thread.getContent().getInfo().getBoardId()}/threads.json`, { + "signal": abortable.signal + }) + + if (res.ok) { + const stats = LCNSite.getThreadFromPages(await res.json(), thread.getContent().getInfo().getThreadId()) + if (stats != null) { + threadStats = stats + } else { + threadState = String(res.status) + } + } else { + throw new Error(`Server responded with non-OK status '${res.status}'`) + } + } + + const handleThreadUpdate = async (thread) => { + const threadPost = thread.getContent() + + const res = await fetch(location.href, { + "signal": abortable.signal + }) + + if (res.ok) { + const dom = parser.parseFromString(await res.text(), "text/html") + const livePCList = Array.prototype.map.apply(dom.querySelectorAll(`#thread_${threadPost.getInfo().getThreadId()} > .postcontainer`), [ pc => LCNPostContainer.assign(pc) ]) + updateThreadFn(thread, livePCList); + } else if (res.status == 404) { + threadState = String(res.status) + } else { + throw new Error(`Server responded with non-OK status '${res.status}'`) + } + } + + function updateThreadFn(thread, lcn_pc_list) { + const threadPost = thread.getContent() + const threadReplies = thread.getReplies() + const lastPostC = threadReplies.at(-1).getParent() + const lastPostTs = lastPostC.getContent().getInfo().getCreatedAt().getTime() + + const livePCList = lcn_pc_list; + const documentPCList = [ threadPost, ...threadReplies.map(p => p.getParent()) ] + const missingPCList = [] + + for (const pc of livePCList.reverse()) { + if (pc.getContent().getInfo().getCreatedAt().getTime() > lastPostTs) { + missingPCList.unshift(pc) + } else { + break + } + } + + if (missingPCList.length) { + for (const pc of missingPCList) { + documentPCList.at(-1).getElement().after(pc.getElement()) + documentPCList.push(pc) + } + + for (const pc of missingPCList) { + $(document).trigger("new_post", [ pc.getContent().getElement() ]) + } + + LCNSite.INSTANCE.setUnseen(LCNSite.INSTANCE.getUnseen() + missingPCList.length) + } + + } + + const onTickClean = () => { + if (onTickId != null) { + clearTimeout(onTickId) + onTickId = null + } + abortable.abort() + } + + let onTickId = null + const onTickFn = async () => { + void secondsCounter--; + onTickClean() + updateDOMStatus() + + if (threadState == null) { + if (secondsCounter < 0) { + const thread = LCNThread.first() + try { + await updateStatsFn(thread) + if (threadState == null && threadStats.last_modified > (thread.getReplies().at(-1).getInfo().getCreatedAt().getTime() / 1000)) { + await handleThreadUpdate(thread) + } + + 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.getReplies().at(-1).getInfo()) + } catch (error) { + console.error("threadAutoUpdater: Failed while processing update. Probably a network error", error) + secondsCounter = 60 + } + } + + onTickId = setTimeout(onTickFn, 1000) + } + } + + let floaterLinkBox = null + const onStateChangeFn = v => { + onTickClean() + + if (v) { + _domsetup_btn: { + const container = LCNSite.INSTANCE.getFloaterLContainer() + floaterLinkBox = document.createElement("span") + const threadlink = document.createElement("span") + const threadUpdateLink = document.createElement("a") + threadUpdateStatus = document.createElement("span") + + threadUpdateStatus.id = "thread-update-status" + threadUpdateStatus.innerText = "…" + threadUpdateLink.addEventListener("click", e => { + e.preventDefault() + if (secondsCounter >= 0) { + secondsCounter = 0 + onTickFn() + } + }) + threadUpdateLink.href = "#" + threadUpdateLink.appendChild(new Text("Refresh: ")) + threadUpdateLink.appendChild(threadUpdateStatus) + threadlink.classList.add("threadlink") + threadlink.appendChild(threadUpdateLink) + floaterLinkBox.classList.add("threadlinks") + floaterLinkBox.appendChild(threadlink) + container.appendChild(floaterLinkBox) + } + + _domsetup_stats: { + const container = LCNSite.INSTANCE.getThreadStatsRContainer() + const span1 = document.createElement("span") + const span2 = document.createElement("span") + const span3 = document.createElement("span") + statUniqueIPs = document.getElementById("lcn-uniqueips") + statReplies = document.createElement("span") + statFiles = document.createElement("span") + statPage = document.createElement("span") + + statReplies.id = "lcn_replies_n" + statReplies.innerText = "…" + + statFiles.id = "lcn_files_n" + statReplies.innerText = "…" + + statPage.id = "lcn_page_n" + statPage.innerText = "…" + + span1.appendChild(new Text("Replies: ")) + span1.appendChild(statReplies) + span2.appendChild(new Text("Files: ")) + span2.appendChild(statFiles) + span3.appendChild(new Text("Page: ")) + span3.appendChild(statPage) + + for (const span of [ span1, span2, span3 ]) { + threadStatsItems.push(span) + container.appendChild(span) + } + } + + secondsCounter = 0 + setTimeout(onTickFn, 1) + } else { + floaterLinkBox?.remove() + floaterLinkBox = null + statReplies = null + statFiles = null + statPage = null + + while (threadStatsItems.length) { + threadStatsItems.shift().remove() + } + } + } + + kIsEnabled.onChange(onStateChangeFn) + onStateChangeFn(kIsEnabled.getValue()) + $(document).on("ajax_after_post", onNewPost); + + function onNewPost(_, post_response) { + if (post_response == null) { + console.log("onNewPost data is null, can't do anything."); + return; + } + + const thread_dom = parser.parseFromString( + post_response['thread'], + "text/html"); + + const thread_id_sel = "#thread_" + post_response['thread_id']; + const post_containers = [...thread_dom.querySelectorAll(`${thread_id_sel} > .postcontainer`)] + .map(elem => LCNPostContainer.assign(elem)); + + const thread_elem = document.querySelector(thread_id_sel); + const lcn_thread = new LCNThread(thread_elem); + + updateThreadFn(lcn_thread, post_containers); + } + } +}) diff --git a/js/lcn/utils.js b/js/lcn/utils.js index 907fce95..465a490e 100644 --- a/js/lcn/utils.js +++ b/js/lcn/utils.js @@ -3,20 +3,13 @@ * @author jonsmy */ -const arrLast = arr => arr[arr.length-1] ?? undefined; -const getConfigBool = (k,d) => { const v = localStorage.getItem(`jon-modjs::${k}`); return v ? v == "1" : d; } -const writeCSSStyle = textContent => { - const style = document.createElement("style") - style.textContent = textContent - document.head.appendChild(style) -} - const assert = { "equal": (actual, expected, message="No message set") => { if (actual !== expected) { const err = new Error(`Assertion Failed. ${message}`) err.data = { actual, expected} - Error.captureStackTrace(err, assert.equal) + // Seems like there's no such thing as captureStackTrace in firefox? + //Error.captureStackTrace(err, assert.equal) debugger throw err } @@ -25,7 +18,7 @@ const assert = { if (!actual) { const err = new Error(`Assertion Failed. ${message}`) err.data = { actual } - Error.captureStackTrace(err, assert.ok) + // Error.captureStackTrace(err, assert.ok) debugger throw err } diff --git a/post.php b/post.php index 43dd359a..52ed7605 100644 --- a/post.php +++ b/post.php @@ -1475,7 +1475,9 @@ function handle_post(){ } - buildThread($post['op'] ? $id : $post['thread']); + $thread_id = $post['op'] ? $id : $post['thread']; + + $rendered_thread = buildThread($thread_id); if ($config['syslog']) _syslog(LOG_INFO, 'New post: /' . $board['dir'] . $config['dir']['res'] . @@ -1487,12 +1489,13 @@ function handle_post(){ header('Location: ' . $redirect, true, $config['redirect_http']); } else { header('Content-Type: text/json; charset=utf-8'); - $api = new Api(); + echo json_encode(array( 'redirect' => $redirect, 'noko' => $noko, 'id' => $id, - 'post' => $api->translatePost(new Post($post)) + 'thread_id' => $thread_id, + 'thread' => $rendered_thread )); } diff --git a/stylesheets/style.css b/stylesheets/style.css index 1556231c..ef47d00f 100644 --- a/stylesheets/style.css +++ b/stylesheets/style.css @@ -2015,3 +2015,71 @@ span.orangeQuote { .options_general_tab--select_opt select { float: none; } + +/* LCN */ +.lcn-threadstats { + display: flex; + justify-content: space-between; +} + +.lcn-threadstats-container { + display: flex; + gap: 8px; +} + +.lcn-threadstats-container > :not(:first-child):before { + content: ' | '; +} + +.lcn-setting-entry { + display: flex; + align-items: flex-end; + gap: 0.5em; +} + +.lcn-bar { + display: flex!important; + justify-content: space-between; + gap: 6px; +} + +#thread-update-status { + display: inline-block; + width: 2.5em; + text-align: right; + overflow-x: clip; +} + +.bar-collection { + padding: 0px 3px; +} + +.bar-collection .threadlinks:not(:first-child) { + margin-left:4px +} + +.lcn-bar .threadlinks:before, +.lcn-bar .threadlinks:after, +.lcn-bar .threadlinks .threadlink:not(:first-child):before { + vertical-align: middle; +} + +.threadlinks:before { + content: '[ '; +} + +.threadlinks:after { + content: ' ]'; +} + +.threadlinks .threadlink:not(:first-child):before { + content: ' / '; +} + +.indielinks-row .indielinks:not(:first-child):before { + content: ' | '; +} + +.indielinks .indielink:not(:first-child) { + margin-left: 6px; +} diff --git a/templates/post_thread.html b/templates/post_thread.html index 94d73700..4b64d43a 100644 --- a/templates/post_thread.html +++ b/templates/post_thread.html @@ -100,7 +100,12 @@ {% endfor %}