Compare commits
27 Commits
deploy_spa
...
new_posts_
Author | SHA1 | Date |
---|---|---|
towards-a-new-leftypol | c94774c9ab | |
towards-a-new-leftypol | cb7c818996 | |
towards-a-new-leftypol | 7838520c63 | |
Jon | 00c37364c6 | |
Jon | 7ea730f14f | |
Jon | a5969f4e80 | |
Jon | e6ef9d0d83 | |
Jon | d9a2cd78e0 | |
Jon | 97ff7914bb | |
Jon | e295c7b17c | |
Jon | fc733f694c | |
Jon | 0dad3dc61b | |
Jon | a57e175b5a | |
Jon | 3b0292f658 | |
Jon | accae507a6 | |
Jon | da8898624a | |
Jon | 9912a672f2 | |
Jon | 650b8a8520 | |
Jon | 45c3fdfe4f | |
Jon | 2006aa6fd6 | |
Jon | b82269a54e | |
Jon | 98856d6a6e | |
Jon | c772135db8 | |
Jon | 5b492e1d57 | |
Jon | 3a464a0c2a | |
Jon | 34b72f25c5 | |
Jon | c67fad9e6b |
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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())
|
||||
})
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
})
|
|
@ -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
|
||||
}
|
||||
|
|
9
post.php
9
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
|
||||
));
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Reference in New Issue