From accae507a6a123fc4fbcccc61ff7bfd05c4da3fb Mon Sep 17 00:00:00 2001 From: Jon <134811123+jonsmy@users.noreply.github.com> Date: Sun, 25 Feb 2024 22:15:37 +0000 Subject: [PATCH] thread_autoupdater.js: add --- js/lcn/thread_autoupdater.js | 229 +++++++++++++++++++++++++++++++++++ 1 file changed, 229 insertions(+) create mode 100644 js/lcn/thread_autoupdater.js diff --git a/js/lcn/thread_autoupdater.js b/js/lcn/thread_autoupdater.js new file mode 100644 index 00000000..cb90bc67 --- /dev/null +++ b/js/lcn/thread_autoupdater.js @@ -0,0 +1,229 @@ +/** + * @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 = "Pruned" + } + } else { + throw new Error(`Server responded with non-OK status '${res.status}'`) + } + } + + const updateThreadFn = async (thread) => { + const threadPost = thread.getContent() + const threadReplies = thread.getReplies() + const lastPostC = threadReplies.at(-1).getParent() + const lastPostTs = lastPostC.getContent().getInfo().getCreatedAt().getTime() + + 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) ]) + const documentPCList = 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) + secondsCounter = 11 + } else { + updateSecondsByTSLP(lastPostC.getContent().getInfo()) + } + } else if (res.status == 404) { + threadState = "Pruned" + } else { + throw new Error(`Server responded with non-OK status '${res.status}'`) + } + } + + 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 { + console.debug("fetch updates!") + await updateStatsFn(thread) + if (threadState == null && threadStats.last_modified > (thread.getReplies().at(-1).getInfo().getCreatedAt().getTime() / 1000)) { + await updateThreadFn(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 + } catch (error) { + console.error("threadAutoUpdater: Failed while processing update. Probably a network error", error) + } finally { + secondsCounter = secondsCounter > 0 ? 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()) + } +})