From aea9793160a7588c1db5f118f127d77441d0066e Mon Sep 17 00:00:00 2001 From: Jon <134811123+jonsmy@users.noreply.github.com> Date: Wed, 31 Jan 2024 02:32:42 +0000 Subject: [PATCH] js/mod/recent_posts.js: polyfill AbortSignal.any, add load wrapper, silence AbortSignal DOMException warn --- js/mod/recent_posts.js | 240 ++++++++++++++++++++++++----------------- 1 file changed, 139 insertions(+), 101 deletions(-) diff --git a/js/mod/recent_posts.js b/js/mod/recent_posts.js index 73a7b16b..8be69723 100644 --- a/js/mod/recent_posts.js +++ b/js/mod/recent_posts.js @@ -2,116 +2,154 @@ * Implements live-reloading on the recent posts mod page. */ (() => { - const kLocationPath = location.href.slice(location.origin.length) - if (/^\/mod\.php\?\/recent\/\d+$/.test(kLocationPath)) { - const loadIntFromStorage = key => { - const value = localStorage.getItem(`jon-liveposts-enabled::${key}`) - return value != null ? parseInt(value) : null + if (AbortSignal.any === undefined) { + AbortSignal.any = function (signals) { + const controller = new AbortController() + const callbacks = [] + + const abortFn = () => { + for (const signal of signals) { + signal.removeEventListener("abort", abortFn) + } + controller.abort() + } + + for (const signal of signals) { + signal.addEventListener("abort", abortFn) + } + + return controller.signal } + } - const kPullXPosts = loadIntFromStorage("pull-x-posts") ?? 6 - const kPullEveryX = loadIntFromStorage("pull-every-x") ?? 8000 - const kRecentXPosts = parseInt(kLocationPath.slice(17).split(/[^\d]/)[0]) + const scriptFn = () => { + const kLocationPath = location.href.slice(location.origin.length) + if (/^\/mod\.php\?\/recent\/\d+$/.test(kLocationPath)) { + const loadIntFromStorage = key => { + const value = localStorage.getItem(`jon-liveposts-enabled::${key}`) + return value != null ? parseInt(value) : null + } - let liveUpdateEnabled = false - let liveUpdateAbortController = null - let liveUpdateIntervalId = null - let liveUpdateFaviconChanged = false + const kPullXPosts = loadIntFromStorage("pull-x-posts") ?? 6 + const kPullEveryX = loadIntFromStorage("pull-every-x") ?? 8000 + const kRecentXPosts = parseInt(kLocationPath.slice(17).split(/[^\d]/)[0]) - const parser = new DOMParser() - const fetchLatest = async () => { - const res = await fetch(`/mod.php?/recent/${kPullXPosts}`, { - "signal": AbortSignal.any([ liveUpdateAbortController.signal, AbortSignal.timeout(kPullEveryX - 500) ]) + let liveUpdateEnabled = false + let liveUpdateAbortController = null + let liveUpdateIntervalId = null + let liveUpdateFaviconChanged = false + + const parser = new DOMParser() + const fetchLatest = async () => { + const res = await fetch(`/mod.php?/recent/${kPullXPosts}`, { + "signal": AbortSignal.any([ liveUpdateAbortController.signal, AbortSignal.timeout(kPullEveryX - 500) ]) + }) + + if (res.ok) { + const dom = parser.parseFromString(await res.text(), "text/html") + const posts = Array.from(dom.querySelectorAll("body > .post-wrapper[data-board]")) + return posts.map(el => ({ + "element": el, + "href": Array.prototype.find.apply(el.children, [ el => el.classList.contains("eita-link") ]).href, + "id": Array.prototype.find.apply(el.children, [ el => el.classList.contains("thread") || el.classList.contains("postcontainer") ]).id + })) + } else { + return [] + } + } + + const getPostTimestamp = post => new Date(post.lastElementChild.querySelector(".intro time").dateTime) + const updateRecentPosts = () => { + if (liveUpdateEnabled) { + fetchLatest() + .then(latestPosts => { + const lastPost = document.body.querySelector(".post-wrapper") + const lastPostTs = getPostTimestamp(lastPost).getTime() + const posts = latestPosts.filter(post => document.getElementById(post.id) == null && getPostTimestamp(post.element).getTime() > lastPostTs) + if (posts.length > 0) { + const totalPosts = Array.from(document.body.querySelectorAll(".post-wrapper")) + for (const post of posts) { + lastPost.prepend(post.element) + totalPosts.unshift(post.element) + } + + while (totalPosts.length > kRecentXPosts) { + document.body.removeChild(totalPosts.pop()) + } + + updatePageNext() + makeIcon("reply") + liveUpdateFaviconChanged = true + } + }) + .catch(error => { + // XXX: Why the hell does gecko have a space at the end?? + if (!((error instanceof DOMException) && [ "The user aborted a request", "The operation was aborted." ].some(msg => error.message.slice(0, 26) === msg))) { + throw error + } + }) + } + } + + const pageNext = Array.prototype.find.apply(document.body.children, [ el => el.nodeName == "A" && el.href.startsWith(`${location.origin}/mod.php?/recent/${kRecentXPosts}&last=`) ]) + const updatePageNext = () => { + const posts = document.body.querySelectorAll(".post-wrapper") + const oldestPost = posts[posts.length-1] + pageNext.href = `/mod.php?/recent/${kRecentXPosts}&last=${getPostTimestamp(oldestPost).getTime() / 1000}` + } + + const createCheckbox = () => { + const id = "jon-liveposts-enabled" + const div = document.createElement("div") + const label = document.createElement("label") + const checkbox = document.createElement("input") + checkbox.type = "checkbox" + checkbox.id = id + label.innerText = "live update: " + label.for = id + div.style.display = "inline" + div.append(" ") + div.appendChild(label) + div.appendChild(checkbox) + div.append(" ") + return { checkbox, label, div } + } + + const pageNums = Array.prototype.find.apply(document.body.children, [ el => el.nodeName == "P" ]) + const form = createCheckbox() + + form.checkbox.addEventListener("click", () => { + setTimeout(() => { + if (form.checkbox.checked) { + liveUpdateEnabled = true + liveUpdateAbortController = new AbortController() + liveUpdateIntervalId = setInterval(() => updateRecentPosts(), kPullEveryX) + } else { + liveUpdateEnabled = false + clearInterval(liveUpdateIntervalId) + liveUpdateAbortController.abort() + liveUpdateIntervalId = null + liveUpdateAbortController = null + } + }, 700) }) - if (res.ok) { - const dom = parser.parseFromString(await res.text(), "text/html") - const posts = Array.from(dom.querySelectorAll("body > .post-wrapper[data-board]")) - return posts.map(el => ({ - "element": el, - "href": Array.prototype.find.apply(el.children, [ el => el.classList.contains("eita-link") ]).href, - "id": Array.prototype.find.apply(el.children, [ el => el.classList.contains("thread") || el.classList.contains("postcontainer") ]).id - })) - } else { - return [] - } - } - - const getPostTimestamp = post => new Date(post.lastElementChild.querySelector(".intro time").dateTime) - const updateRecentPosts = async () => { - if (liveUpdateEnabled) { - const lastPost = document.body.querySelector(".post-wrapper") - const lastPostTs = getPostTimestamp(lastPost).getTime() - const posts = (await fetchLatest()).filter(post => document.getElementById(post.id) == null && getPostTimestamp(post.element).getTime() > lastPostTs) - if (posts.length > 0) { - const totalPosts = Array.from(document.body.querySelectorAll(".post-wrapper")) - for (const post of posts) { - lastPost.prepend(post.element) - totalPosts.unshift(post.element) - } - - while (totalPosts.length > kRecentXPosts) { - document.body.removeChild(totalPosts.pop()) - } - - updatePageNext() - makeIcon("reply") - liveUpdateFaviconChanged = true + document.body.addEventListener("mousemove", () => { + if (liveUpdateFaviconChanged) { + liveUpdateFaviconChanged = false + makeIcon(false) } - } + }) + + pageNums.append("|") + pageNums.append(form.div) } + } - const pageNext = Array.prototype.find.apply(document.body.children, [ el => el.nodeName == "A" && el.href.startsWith(`${location.origin}/mod.php?/recent/${kRecentXPosts}&last=`) ]) - const updatePageNext = () => { - const posts = document.body.querySelectorAll(".post-wrapper") - const oldestPost = posts[posts.length-1] - pageNext.href = `/mod.php?/recent/${kRecentXPosts}&last=${getPostTimestamp(oldestPost).getTime() / 1000}` - } - const createCheckbox = () => { - const id = "jon-liveposts-enabled" - const div = document.createElement("div") - const label = document.createElement("label") - const checkbox = document.createElement("input") - checkbox.type = "checkbox" - checkbox.id = id - label.innerText = "live update: " - label.for = id - div.style.display = "inline" - div.append(" ") - div.appendChild(label) - div.appendChild(checkbox) - div.append(" ") - return { checkbox, label, div } - } - - const pageNums = Array.prototype.find.apply(document.body.children, [ el => el.nodeName == "P" ]) - const form = createCheckbox() - - form.checkbox.addEventListener("click", () => { - setTimeout(() => { - if (form.checkbox.checked) { - liveUpdateEnabled = true - liveUpdateAbortController = new AbortController() - liveUpdateIntervalId = setInterval(() => updateRecentPosts(), kPullEveryX) - } else { - liveUpdateEnabled = false - clearInterval(liveUpdateIntervalId) - liveUpdateAbortController.abort() - liveUpdateIntervalId = null - liveUpdateAbortController = null - } - }, 700) - }) - - document.body.addEventListener("mousemove", () => { - if (liveUpdateFaviconChanged) { - liveUpdateFaviconChanged = false - makeIcon(false) - } - }) - - pageNums.append("|") - pageNums.append(form.div) + if (document.readyState != "complete") { + window.addEventListener("load", scriptFn, { "once": true }) + } else { + setTimeout(scriptFn, 1) } })()