/** * @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 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 = String(res.status) } 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 { 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()) } })