2024-02-25 22:15:37 +00:00
|
|
|
/**
|
|
|
|
* @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 => {
|
2024-02-26 20:54:36 +00:00
|
|
|
secondsCounter = Math.floor(((Date.now() - post_info.getCreatedAt().getTime()) / 120000))
|
2024-02-25 22:15:37 +00:00
|
|
|
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 {
|
2024-02-25 22:21:16 +00:00
|
|
|
threadState = String(res.status)
|
2024-02-25 22:15:37 +00:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
throw new Error(`Server responded with non-OK status '${res.status}'`)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-26 20:54:36 +00:00
|
|
|
const findMissingReplies = (thread_op, thread_dom, thread_latest) => {
|
|
|
|
const lastPostTs = (thread_dom.at(-1)?.getInfo() ?? thread_op).getCreatedAt().getTime()
|
|
|
|
const missing = []
|
2024-02-25 22:15:37 +00:00
|
|
|
|
2024-02-26 20:54:36 +00:00
|
|
|
for (const pc of thread_latest.reverse()) {
|
2024-02-26 18:55:36 +00:00
|
|
|
if (pc.getContent().getInfo().getCreatedAt().getTime() > lastPostTs) {
|
2024-02-26 20:54:36 +00:00
|
|
|
missing.unshift(pc)
|
2024-02-26 18:55:36 +00:00
|
|
|
} else {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
2024-02-26 20:54:36 +00:00
|
|
|
return missing
|
|
|
|
}
|
2024-02-26 18:55:36 +00:00
|
|
|
|
2024-02-26 20:54:36 +00:00
|
|
|
const updateRepliesFn = (thread, missingPCList) => {
|
2024-02-26 18:55:36 +00:00
|
|
|
if (missingPCList.length) {
|
2024-02-26 20:54:36 +00:00
|
|
|
const documentPCList = [ thread.getContent(), ...(thread.getReplies()).map(p => p.getParent()) ]
|
|
|
|
|
2024-02-26 18:55:36 +00:00
|
|
|
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)
|
|
|
|
}
|
2024-02-26 20:54:36 +00:00
|
|
|
}
|
2024-02-26 18:55:36 +00:00
|
|
|
|
2024-02-26 20:54:36 +00:00
|
|
|
const updateThreadFn = async (thread, dom) => {
|
|
|
|
const threadPost = thread.getContent()
|
|
|
|
const threadReplies = thread.getReplies()
|
|
|
|
const missingPCList = findMissingReplies(
|
|
|
|
threadPost,
|
|
|
|
threadReplies,
|
|
|
|
LCNPostContainer.all(dom.querySelector(`#thread_${threadPost.getInfo().getThreadId()}`)))
|
|
|
|
|
|
|
|
updateRepliesFn(thread, missingPCList)
|
|
|
|
}
|
|
|
|
|
|
|
|
const fetchThreadFn = async () => {
|
|
|
|
const res = await fetch(location.href, { "signal": abortable.signal })
|
|
|
|
if (res.ok) {
|
|
|
|
return parser.parseFromString(await res.text(), "text/html")
|
|
|
|
} else {
|
|
|
|
if (res.status == 404) {
|
|
|
|
threadState = String(res.status)
|
|
|
|
}
|
|
|
|
throw new Error(`Server responded with non-OK status '${res.status}'`)
|
|
|
|
}
|
2024-02-26 18:55:36 +00:00
|
|
|
}
|
|
|
|
|
2024-02-25 22:15:37 +00:00
|
|
|
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)) {
|
2024-02-26 20:54:36 +00:00
|
|
|
updateThreadFn(thread, await fetchThreadFn())
|
2024-02-25 22:15:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
2024-02-25 22:39:22 +00:00
|
|
|
updateSecondsByTSLP(thread.getReplies().at(-1).getInfo())
|
2024-02-25 22:15:37 +00:00
|
|
|
} catch (error) {
|
|
|
|
console.error("threadAutoUpdater: Failed while processing update. Probably a network error", error)
|
2024-02-25 22:39:22 +00:00
|
|
|
secondsCounter = 60
|
2024-02-25 22:15:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
onTickId = setTimeout(onTickFn, 1000)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-26 20:54:36 +00:00
|
|
|
$(document).on("ajax_after_post", (_, xhr_body) => {
|
|
|
|
if (kIsEnabled.getValue() && xhr_body != null) {
|
2024-02-26 21:49:46 +00:00
|
|
|
if (!xhr_body.mod) {
|
2024-02-26 21:22:26 +00:00
|
|
|
const thread = LCNThread.first()
|
|
|
|
const dom = parser.parseFromString(xhr_body.thread, "text/html")
|
|
|
|
updateThreadFn(thread, dom)
|
|
|
|
updateSecondsByTSLP(thread.getReplies().at(-1).getInfo())
|
2024-02-26 21:49:46 +00:00
|
|
|
} else {
|
|
|
|
$(document).trigger("thread_manual_refresh")
|
2024-02-26 21:22:26 +00:00
|
|
|
}
|
2024-02-26 20:54:36 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
$(document).on("thread_manual_refresh", () => {
|
|
|
|
if (kIsEnabled.getValue() && secondsCounter >= 0) {
|
2024-02-26 21:49:46 +00:00
|
|
|
secondsCounter = 0
|
|
|
|
onTickFn()
|
2024-02-26 20:54:36 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
|
2024-02-25 22:15:37 +00:00
|
|
|
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()
|
2024-02-26 20:54:36 +00:00
|
|
|
$(document).trigger("thread_manual_refresh")
|
2024-02-25 22:15:37 +00:00
|
|
|
})
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-26 21:49:46 +00:00
|
|
|
$(document).trigger("thread_manual_refresh")
|
2024-02-25 22:15:37 +00:00
|
|
|
} else {
|
|
|
|
floaterLinkBox?.remove()
|
|
|
|
floaterLinkBox = null
|
|
|
|
statReplies = null
|
|
|
|
statFiles = null
|
|
|
|
statPage = null
|
|
|
|
|
|
|
|
while (threadStatsItems.length) {
|
|
|
|
threadStatsItems.shift().remove()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
kIsEnabled.onChange(onStateChangeFn)
|
|
|
|
onStateChangeFn(kIsEnabled.getValue())
|
|
|
|
}
|
|
|
|
})
|