"use strict"; const ZULIP_AUTHORITY='chat.yuri-project.net';const form = document.querySelector("form"); const topic_input = form.elements.namedItem("topic"); let current_channel = "safe"; const channel_links = new Map( [...form.querySelector(".channels").children] .map(el => { el.addEventListener("click", () => topic_input.value = ""); make_spa_link(el); return [el.textContent.slice(1), el]; }) ); const content_input = form.elements.namedItem("content"); const attach_input = form.elements.namedItem("attach"); const button = form.querySelector("button"); const back_to_zulip_link = form.querySelector(".back-to-zulip"); const back_to_channel_link = form.querySelector(".back-to-channel"); make_spa_link(back_to_channel_link); const iframe = document.querySelector("iframe"); const messages_div = document.getElementById("messages"); const loading_message = document.getElementById("loading"); let canonical; function set_canonical(path) { if (canonical === undefined) { canonical = document.createElement("link"); canonical.setAttribute("rel", "canonical"); canonical.href = "https://anon.yuri-project.net" + path; document.head.append(canonical); } else { canonical.href = "https://anon.yuri-project.net" + path; } } let noindex_element; function set_noindex(should_noindex) { if (should_noindex && noindex_element === undefined) { noindex_element = document.createElement("meta"); noindex_element.setAttribute("name", "robots"); noindex_element.setAttribute("content", "noindex"); document.head.append(noindex_element); } else if (!should_noindex && noindex_element !== undefined) { noindex_element.remove(); noindex_element = undefined; } } form.addEventListener("submit", async () => { button.disabled = true; const body = new FormData(); const channel = current_channel; const topic = topic_input.value; const content = content_input.value; body.append("req", JSON.stringify({ channel, topic, content })); for (const file of attach_input.files) { body.append("attach", file); } try { await do_fetch_with( "posting message", () => {}, "/messages", { body, method: "POST" }, ); content_input.value = ""; attach_input.value = ""; if (topic_input.value === topic) { update_messages({ replace_url: true }); } } finally { button.disabled = false; } }); // inputs: // - `current_channel` // - `message` for permalinks, defaults to `topic_input.value` // - `page` for the page number // updates: // - async: the list of messages on the page // - async: `topic_input.value` and links // - async: the URL of the page let active_request_aborter = new AbortController(); async function update_messages(...args) { loading_message.style.visibility = "visible"; try { await update_messages_inner(...args); } finally { loading_message.style.visibility = "hidden"; } } async function update_messages_inner({ message, page, replace_url }) { const channel = current_channel; const topic = message === undefined ? topic_input.value : (message === null ? "" : undefined); active_request_aborter.abort(); active_request_aborter = new AbortController(); if (topic === "") { document.title = `#${channel}${page !== 0 ? ` page ${page}` : ""} | Yuri Project`; set_path( { replace: replace_url, canonical: true, noindex: true }, `/${channel}${page !== 0 ? `/page/${page}` : ""}` ); if (message === null) { topic_input.value = ""; on_topic_change({ messages_needs_update: false }); } const params = new URLSearchParams({ channel, page }); const response = await do_fetch_json("loading topics", `/imageboard_page?${params}`, { signal: active_request_aborter.signal, }); if (response === null) return; messages_div.replaceChildren(make_page_list(channel, response.pages, page)); for (const topic of response.topics) { const replylink_outer = document.createElement("span"); replylink_outer.classList.add("replylink"); const replylink = document.createElement("a"); replylink.href = `/${channel}/${topic.initial_message.id}/${slug_topic_name(topic.initial_message.subject)}`; make_spa_link(replylink); replylink.append("Reply"); replylink_outer.append("[", replylink, "]"); const actuallink = document.createElement("a"); actuallink.href = `https://${ZULIP_AUTHORITY}/${make_url_hash(channel, topic.name)}`; actuallink.append(topic.name); const h = document.createElement("h3"); h.append(actuallink, " ", replylink_outer); const div = document.createElement("div"); div.classList.add("topic"); div.append(h, make_message_element(topic.initial_message, topic.name)); if (topic.omitted) div.append(make_omitted()); div.append(...topic.messages.map(make_message_element)); messages_div.append(div); } messages_div.append(make_page_list(response.pages, page)); if (messages_div.children[0].getBoundingClientRect().top < 0) { messages_div.children[0].scrollIntoView(); } } else { let params = { channel }; if (topic !== undefined) params.topic = topic; if (message !== undefined) params.message = message; params = new URLSearchParams(params); const response = await do_fetch_json("loading messages", `/messages?${params}`, { signal: active_request_aborter.signal, }); if (response === null) return; messages_div.replaceChildren(); if (!response.found_oldest) messages_div.append(make_omitted()); messages_div.append(...response.messages.map(make_message_element)); if (form.getBoundingClientRect().top < 0) { form.scrollIntoView(); } if (response.messages.length !== 0) { const message = response.messages[0].id; const topic = response.messages[0].subject; document.title = `${topic} | #${channel} | Yuri Project`; set_path( { replace: replace_url, canonical: true }, `/${channel}/${message}/${slug_topic_name(topic)}`, ); topic_input.value = topic; on_topic_change({ messages_needs_update: false }); } } } function make_omitted() { const em = document.createElement("em"); em.append("[messages omitted]"); return em; } function make_page_list(channel, pages, page) { const list = document.createElement("div"); for (let i = 0; i < pages; ++i) { list.append("["); if (i != page) { const a = document.createElement("a"); a.href = `/${channel}${i !== 0 ? `/page/${i}` : ""}`; make_spa_link(a); a.append(`${i}`); list.append(a); } else { list.append(`${i}`); } list.append("] "); } if (page + 1 < pages) { const l = document.createElement("a"); l.href = `/${channel}/page/${page + 1}`; make_spa_link(l); l.append("Next"); list.append(l); } return list; } function make_message_element(message, subject = "") { let avatar_url = message.avatar_url; let avatar; if (avatar_url !== null) { avatar = document.createElement("img"); if (avatar_url.startsWith("/")) { avatar_url = `https://${ZULIP_AUTHORITY}${avatar_url}`; } avatar.src = avatar_url; } else { avatar = document.createElement("div"); } const is_wakaba = message.sender_email === "wakaba-bot@chat.yuri-project.org"; const main = document.createElement("div"); if (!is_wakaba) { const name = document.createElement("span"); name.append(message.sender_full_name); if (message.sender_email.includes("-bot@")) { name.append(" 🤖"); } name.append(" "); name.classList.add("name"); const time = document.createElement("time"); time.append(format_date(new Date(message.timestamp * 1000))); main.append(name, time); } main.innerHTML += message.content; if (is_wakaba) { // Remove duplicated topic title from post const strong_part = main.querySelector("strong"); const child = strong_part?.childNodes?.[0]; if (child?.nodeValue?.startsWith?.(`${subject} | `)) { child.nodeValue = child.nodeValue.slice(`${subject} | `.length); } // format title and name correctly if (child?.nodeValue?.includes?.(" | ")) { const [before, after] = child.nodeValue.split(" | ", 2); const title = document.createElement("span"); title.classList.add("title"); title.append(before); const name = document.createElement("span"); name.classList.add("name"); name.append(after); strong_part.removeChild(child); strong_part.prepend(title, name); } else { strong_part.classList.add("name"); } // remove superfluous pipe if (strong_part.nextSibling.nodeValue === " | ") { strong_part.nextSibling.nodeValue = " "; } // Make the top line look more normal const para = main.children[0]; if (para?.tagName === "P" && 0 < para.getElementsByTagName("br").length) { const to_move = []; for (const child of para.childNodes) { if (child.tagName === "BR") { child.remove(); break; } else { to_move.push(child); } } main.prepend(...to_move); } } // Float images to the left const images = main.getElementsByTagName("img"); if (images.length === 1) { const img = images.item(0); main.prepend(img); img.classList.add("float"); } const base = `${location.protocol}//${location.host}`; for (const img of main.querySelectorAll("img")) { if (img.src.startsWith(base)) { img.src = `https://${ZULIP_AUTHORITY}${img.src.slice(base.length)}`; } } for (const a of main.querySelectorAll("a")) { if (a.href.startsWith(base)) { a.href = `https://${ZULIP_AUTHORITY}${a.href.slice(base.length)}`; } } const div = document.createElement("div"); div.classList.add("message"); div.append(avatar, main); return div; } function do_fetch_json(cx, ...args) { return do_fetch_with(cx, r => r.json(), ...args); } class AlreadyLogged extends Error {} async function do_fetch_with(cx, cb, ...args) { try { let response = await fetch(...args); if (!response.ok) { let msg = await response.text(); if (msg === "") { msg = `${cx}: ${response.status}`; } else { msg = `${cx}: ${response.status}: ${msg}`; } error(msg); throw new AlreadyLogged(msg); } return await cb(response); } catch (e) { if (e.name === "AbortError") { return null; } if (!(e instanceof AlreadyLogged)) error(cx); throw e; } } // inputs: // - `current_channel` // - `topic_input.value` // updates: // - links next to the topic input // - async: if `messages_needs_update`, the list of messages on the page // - async: if `messages_needs_update`, the URL of the page function on_topic_change({ messages_needs_update, replace_url }) { const channel = current_channel; for (const [name, link] of channel_links) { link.classList.toggle("active", channel === name); } const topic = topic_input.value; if (topic === "") { back_to_channel_link.classList.add("disabled"); } else { back_to_channel_link.classList.remove("disabled"); back_to_channel_link.href = `/${channel}`; } back_to_zulip_link.href = `https://${ZULIP_AUTHORITY}/${make_url_hash(channel, topic)}`; if (messages_needs_update) { update_messages({ page: 0, replace_url }); } } // inputs: // - the URL of the page // updates: // - `current_channel` // - `topic_input.value` (plus links) // - async: the list of messages on the page // - async: the URL of the page, in-place function on_url_change() { if (location.pathname === "/") { for (const [channel, _] of channel_links) { const base = make_url_hash(channel); const s = `${base}/topic/`; if (location.hash.startsWith(s)) { const rest = location.hash.slice(s.length); current_channel = channel; topic_input.value = decodeURIComponent(rest.replaceAll(".", "%")); break; } else if (location.hash.startsWith(base)) { current_channel = channel; topic_input.value = ""; break; } } on_topic_change({ messages_needs_update: true, replace_url: true }); } else if (location.pathname.startsWith("/")) { const update_messages_args = { replace_url: true, page: 0 }; const parts = location.pathname.slice(1).split("/"); for (const [channel, _] of channel_links) { if (parts[0] === channel) { current_channel = channel; break; } } if (parts[1] === "page") { const page = parseInt(parts[2]); update_messages_args.page = (Number.isInteger(page) && 0 <= page) ? page : 0; } else { update_messages_args.message = parts[1] ?? null; } update_messages(update_messages_args); } } topic_input.addEventListener("input", () => { on_topic_change({ messages_needs_update: true, replace_url: false }); }); addEventListener("popstate", () => { on_url_change(); }); on_url_change(); on_topic_change({ messages_needs_update: false }); function make_spa_link(link) { link.addEventListener("click", e => { history.pushState(null, "", link.href); on_url_change(); e.preventDefault(); }); } const style_input = document.getElementById("style_input"); if (localStorage.getItem("style") !== null) { style_input.value = localStorage.getItem("style"); } function on_style_change() { const style = style_input.value; localStorage.setItem("style", style); const s = document.createElement("link"); s.rel = "stylesheet"; s.href = `/${style}.css` s.id = "styles"; const old = document.getElementById("styles"); if (old === null) { document.head.append(s); } else { old.replaceWith(s); } } style_input.addEventListener("change", on_style_change); on_style_change(); function make_url_hash(channel, topic = "") { const id = channel_links.get(channel).dataset.id; if (topic === "") { return `#narrow/stream/${id}`; } else { return `#narrow/stream/${id}/topic/${encode_topic_name(topic)}`; } } function encode_topic_name(topic) { return encodeURIComponent(topic) .replaceAll(".", "%2E") .replaceAll(" ", "%20") .replaceAll("%", "."); } function slug_topic_name(topic) { return topic .toLowerCase() // remove diacritics .normalize("NFD") .replaceAll(/\p{Diacritic}|\[(raws|needs edit|needs raws|needs tl|needs qc|reserved)\]|[%.()\[\]{}-✔]/gu, "") .trim() .replaceAll(" ", "-") .replaceAll(/-+/g, "-"); } function set_path({ replace, canonical = false, noindex = false }, path) { if (replace) { history.replaceState(null, "", path); } else { history.pushState(null, "", path); } if (canonical) { set_canonical(path); } set_noindex(noindex); } function error(msg) { const error = document.createElement("p"); error.style.color = "red"; error.append(`Error: ${msg}`); document.querySelector(".center").append(error); setTimeout(() => error.remove(), 15 * 1000); } function format_date(date) { return date.getFullYear() + '-' + (date.getMonth() + 1).toString().padStart(2, '0') + '-' + (date.getDate()).toString().padStart(2, '0') + ' ' + (date.getHours()).toString().padStart(2, '0') + ':' + (date.getMinutes()).toString().padStart(2, '0') + ':' + (date.getSeconds()).toString().padStart(2, '0'); }