Compare commits

...

27 Commits

Author SHA1 Message Date
towards-a-new-leftypol c94774c9ab ajax POST: return whole thread 2024-02-26 20:04:46 +00:00
towards-a-new-leftypol cb7c818996 Fix posting images
- since we return the new post html now, we need the Post class to
  not try and decode files as json, since we're not loading that class
  from the db anymore.
2024-02-26 14:18:20 -05:00
towards-a-new-leftypol 7838520c63 thread_updater.js: listen to ajax_after_post event 2024-02-26 13:55:36 -05:00
Jon 00c37364c6
Merge remote-tracking branch 'refs/remotes/origin/deploy_spamnoticer' into deploy_spamnoticer 2024-02-26 18:48:32 +00:00
Jon 7ea730f14f
thread_autoupdater.js: add interop event 2024-02-26 18:39:41 +00:00
Jon a5969f4e80
thread_autoupdater.js: decrease tslp backoff 2024-02-26 18:13:20 +00:00
Jon e6ef9d0d83
js/lcn/classes.js: link toggle label to checkbox 2024-02-26 18:12:57 +00:00
Jon d9a2cd78e0
thread_autoupdater.js: fix for threads with only an op 2024-02-25 22:58:24 +00:00
Jon 97ff7914bb
thread_autoupdater.js: fix tslp calc 2024-02-25 22:39:22 +00:00
Jon e295c7b17c
js/lcn/classes.js: add semicolon 2024-02-25 22:28:05 +00:00
Jon fc733f694c
inc/instance-config.php: fix typo 2024-02-25 22:24:10 +00:00
Jon 0dad3dc61b
thread_autoupdater.js: Display status code on err 2024-02-25 22:21:16 +00:00
Jon a57e175b5a
thread_autoupdater.js: remove debug 2024-02-25 22:17:41 +00:00
Jon 3b0292f658
inc/instance-config.php: replace legacy with new autoreloader 2024-02-25 22:16:53 +00:00
Jon accae507a6
thread_autoupdater.js: add 2024-02-25 22:15:37 +00:00
Jon da8898624a
js/lcn/utils.js: remove unnessesary tab 2024-02-25 22:10:44 +00:00
Jon 9912a672f2
js/lcn/classes.js: add missing semicolon 2024-02-25 22:10:01 +00:00
Jon 650b8a8520
stylesheets/style.css: swap missing dashes 2024-02-25 22:07:48 +00:00
Jon 45c3fdfe4f
add supporting changes 2024-02-25 22:06:24 +00:00
Jon 2006aa6fd6
stylesheets/style.css: add space between barcol children 2024-02-25 22:04:55 +00:00
Jon b82269a54e
swap dashes in threadstat attribs 2024-02-25 21:44:58 +00:00
Jon 98856d6a6e
templates/thread.html: Replace top button with catalog button 2024-02-25 21:00:04 +00:00
Jon c772135db8
templates/thread.html: remove dev prefix 2024-02-25 20:58:30 +00:00
Jon 5b492e1d57
templates/thread.html: migrate threadbar and threadlinks 2024-02-25 20:52:30 +00:00
Jon 3a464a0c2a
stylesheets/style.css: add styles 2024-02-25 20:50:33 +00:00
Jon 34b72f25c5
stylesheets/style.css: add lcn_threadstats style 2024-02-24 22:17:55 +00:00
Jon c67fad9e6b
post_thread.html: use structured block for thread stats 2024-02-24 22:09:30 +00:00
10 changed files with 584 additions and 61 deletions

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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, `<div id="__${domid}" hidden></div>`)
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())
})

View File

@ -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);
}
}
})

View File

@ -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
}

View File

@ -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
));
}

View File

@ -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;
}

View File

@ -100,7 +100,12 @@
{% endfor %}
<br class="clear"/>
{% if not index %}
<div id="uniqueip"><span style="display: block; float: left;">Unique IPs: {{ iparray|length }} </span></div>
<div class="lcn-threadstats">
<div class="lcn-threadstats-container" id="lcn-threadstats-l">
<span>Unique IPs: <span id="lcn-uniqueips">{{ iparray|length }}</span></span>
</div>
<div class="lcn-threadstats-container" id="lcn-threadstats-r"></div>
</div>
{% endif %}
{% if hr %}<hr/>{% endif %}
</div>

View File

@ -56,9 +56,12 @@
{% if config.global_message %}<hr /><div class="blotter">{{ config.global_message }}</div>{% endif %}
<hr />
<div class="threadlinks-noup">
<span class="threadlink">[ <a href="{{ return }}">{% trans %}Return{% endtrans %}</a> /</span>
<span class="threadlink"><a href="#bottom" style="padding-left: 10px"> {% trans %}Go to bottom{% endtrans %}</a> ]</span>
<div class="threadlinks">
<span class="threadlink"><a href="{{ return }}">{% trans %}Return{% endtrans %}</a></span>
{% if config.catalog_link %}
<span class="threadlink"><a href="{{ config.root }}{{ board.dir }}{{ config.catalog_link }}">{% trans %}Catalog{% endtrans %}</a></span>
{% endif %}
<span class="threadlink"><a href="#bottom">{% trans %}Bottom{% endtrans %}</a></span>
</div>
<hr />
<form name="postcontrols" action="{{ config.post_url }}" method="post">
@ -68,15 +71,19 @@
{{ body }}
<div id="thread-interactions">
<span id="thread-links">
<a id="thread-return" href="{{ return }}">[{% trans %}Return{% endtrans %}]</a>
<a id="thread-top" href="#top">[{% trans %}Go to top{% endtrans %}]</a>
{% if config.catalog_link %}
<a id="thread-catalog" href="{{ config.root }}{{ board.dir }}{{ config.catalog_link }}">[{% trans %}Catalog{% endtrans %}]</a>
{% endif %}
{% if config.home_link %}
| <a id="thread-home" href="{{ config.root }}">[{% trans %}Home{% endtrans %}]</a>
{% endif %}
<span class="indielinks-row">
<span class="indielinks" id="thread-indielinks-l">
<span class="indielink"><a href="{{ return }}">[{% trans %}Return{% endtrans %}]</a></span>
{% if config.catalog_link %}
<span class="indielink"><a href="{{ config.root }}{{ board.dir }}{{ config.catalog_link }}">[{% trans %}Catalog{% endtrans %}]</a></span>
{% endif %}
<span class="indielink"><a href="#top">[{% trans %}Top{% endtrans %}]</a></span>
</span>
<span class="indielinks" id="thread-indielinks-r">
{% if config.home_link %}
<span class="indielink"><a href="{{ config.root }}">[{% trans %}Home{% endtrans %}]</a></span>
{% endif %}
</span>
</span>
<span id="thread-quick-reply">
@ -104,12 +111,24 @@
{% for footer in config.footer %}<p class="unimportant" style="text-align:center;">{{ footer }}</p>{% endfor %}
</footer>
</div>
<div class="bar bottom">
<div class="threadlinks">
<span class="threadlink">[ <a href="{{ return }}">{% trans %}Return{% endtrans %}</a> /</span>
<span class="threadlink"><a href="#" style="padding-left: 10px"> {% trans %}Go to top{% endtrans %}</a> /</span>
</div>
<div class=pages></div>
<div class="bar lcn-bar bottom">
<span class="bar-collection" id="bar-bottom-l">
<span class="threadlinks">
<span class="threadlink"><a href="{{ return }}">{% trans %}Return{% endtrans %}</a></span>
{% if config.catalog_link %}
<span class="threadlink"><a href="{{ config.root }}{{ board.dir }}{{ config.catalog_link }}">{% trans %}Catalog{% endtrans %}</a></span>
{% endif %}
<span class="threadlink"><a href="#top">{% trans %}Top{% endtrans %}</a></span>
<span class="threadlink"><a href="#bottom">{% trans %}Bottom{% endtrans %}</a></span>
</span>
</span>
<span class="bar-collection" id="bar-bottom-r">
<span class="threadlinks">
{% if config.home_link %}
<span class="threadlink"><a href="{{ config.root }}">{% trans %}Home{% endtrans %}</a></span>
{% endif %}
</span>
</span>
</div>
<script type="text/javascript">{% verbatim %}
ready();