mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
180 lines
5.6 KiB
JavaScript
180 lines
5.6 KiB
JavaScript
export const SITE_UPDATE_LAST_SEEN_KEY = "bitvid:site-update:last-seen";
|
||
export const SITE_UPDATE_DISMISSED_KEY = "bitvid:site-update:dismissed";
|
||
export const GITHUB_COMMITS_ENDPOINT =
|
||
"https://api.github.com/repos/PR0M3TH3AN/bitvid/commits?sha=unstable&per_page=5";
|
||
|
||
export const LATEST_UPDATE_ITEMS = Object.freeze([
|
||
"Clearer Nostr sign-in choices alongside BitLogin.",
|
||
"Creator approval is now separate from web-of-trust moderation.",
|
||
"Trusted-report thresholds now require broader agreement before action.",
|
||
]);
|
||
|
||
function readStorage(storage, key) {
|
||
try {
|
||
return storage?.getItem?.(key) || "";
|
||
} catch {
|
||
return "";
|
||
}
|
||
}
|
||
|
||
function writeStorage(storage, key, value) {
|
||
try {
|
||
storage?.setItem?.(key, value);
|
||
} catch {
|
||
// Privacy mode or quota errors should never block the application.
|
||
}
|
||
}
|
||
|
||
function createElement(doc, tagName, className, text = "") {
|
||
const element = doc.createElement(tagName);
|
||
if (className) {
|
||
element.className = className;
|
||
}
|
||
if (text) {
|
||
element.textContent = text;
|
||
}
|
||
return element;
|
||
}
|
||
|
||
function renderUpdateItems(doc, list, items) {
|
||
list.replaceChildren();
|
||
for (const item of Array.isArray(items) ? items : []) {
|
||
if (typeof item === "string" && item.trim()) {
|
||
list.appendChild(createElement(doc, "li", "", item.trim()));
|
||
}
|
||
}
|
||
}
|
||
|
||
function normalizeCommitItems(payload) {
|
||
if (!Array.isArray(payload)) {
|
||
return [];
|
||
}
|
||
|
||
return payload
|
||
.map((entry) => {
|
||
const sha = typeof entry?.sha === "string" ? entry.sha.slice(0, 8) : "";
|
||
const message =
|
||
typeof entry?.commit?.message === "string"
|
||
? entry.commit.message.split("\n")[0].trim()
|
||
: "";
|
||
return sha && message ? `${sha} — ${message}` : "";
|
||
})
|
||
.filter(Boolean);
|
||
}
|
||
|
||
async function loadGitHubCommitItems({ doc, list, fetchImpl, fallbackItems }) {
|
||
if (typeof fetchImpl !== "function") {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetchImpl(GITHUB_COMMITS_ENDPOINT, {
|
||
headers: { Accept: "application/vnd.github+json" },
|
||
});
|
||
if (!response?.ok) {
|
||
throw new Error("github-commits-unavailable");
|
||
}
|
||
const commits = normalizeCommitItems(await response.json());
|
||
if (!commits.length) {
|
||
throw new Error("github-commits-empty");
|
||
}
|
||
renderUpdateItems(doc, list, commits);
|
||
} catch {
|
||
renderUpdateItems(doc, list, fallbackItems);
|
||
}
|
||
}
|
||
|
||
export function initializeSiteUpdateNotice({
|
||
doc = typeof document === "undefined" ? null : document,
|
||
storage = typeof window === "undefined" ? null : window.localStorage,
|
||
updateItems = LATEST_UPDATE_ITEMS,
|
||
fetchImpl = typeof fetch === "function" ? fetch : null,
|
||
} = {}) {
|
||
if (!doc?.body) {
|
||
return { shown: false, reason: "document-unavailable" };
|
||
}
|
||
|
||
const versionMarker = doc.querySelector("[data-site-version]");
|
||
const version = versionMarker?.dataset?.siteVersion?.trim() || "";
|
||
const date = versionMarker?.dataset?.siteVersionDate?.trim() || "";
|
||
if (!version) {
|
||
return { shown: false, reason: "version-unavailable" };
|
||
}
|
||
|
||
const lastSeen = readStorage(storage, SITE_UPDATE_LAST_SEEN_KEY);
|
||
const dismissed = readStorage(storage, SITE_UPDATE_DISMISSED_KEY);
|
||
if (!lastSeen) {
|
||
writeStorage(storage, SITE_UPDATE_LAST_SEEN_KEY, version);
|
||
return { shown: false, reason: "first-visit" };
|
||
}
|
||
|
||
if (lastSeen === version || dismissed === version) {
|
||
return { shown: false, reason: "already-seen" };
|
||
}
|
||
|
||
const root = createElement(doc, "aside", "site-update-notice");
|
||
root.setAttribute("aria-label", "Latest site update");
|
||
|
||
const panel = createElement(doc, "section", "site-update-notice__panel hidden");
|
||
panel.id = "siteUpdateNoticePanel";
|
||
panel.setAttribute("role", "status");
|
||
panel.setAttribute("aria-live", "polite");
|
||
|
||
const panelHeader = createElement(doc, "div", "flex items-start justify-between gap-3");
|
||
const title = createElement(doc, "h2", "text-base font-semibold", "Latest update");
|
||
const closeButton = createElement(doc, "button", "site-update-notice__close", "×");
|
||
closeButton.type = "button";
|
||
closeButton.setAttribute("aria-label", "Dismiss this update");
|
||
panelHeader.append(title, closeButton);
|
||
|
||
const intro = createElement(
|
||
doc,
|
||
"p",
|
||
"mt-2 text-sm leading-relaxed text-text-muted",
|
||
"BitVid has been updated. Here is what changed:",
|
||
);
|
||
const list = createElement(doc, "ul", "mt-3 list-disc space-y-1 pl-5 text-sm text-text-muted");
|
||
renderUpdateItems(doc, list, updateItems);
|
||
const build = createElement(
|
||
doc,
|
||
"p",
|
||
"mt-3 text-xs font-mono text-text-muted",
|
||
`Build ${version}${date ? ` • ${date}` : ""}`,
|
||
);
|
||
panel.append(panelHeader, intro, list, build);
|
||
|
||
const toggleButton = createElement(doc, "button", "site-update-notice__button", "Latest update");
|
||
toggleButton.type = "button";
|
||
toggleButton.setAttribute("aria-expanded", "false");
|
||
toggleButton.setAttribute("aria-controls", panel.id);
|
||
|
||
const setPanelOpen = (open) => {
|
||
panel.classList.toggle("hidden", !open);
|
||
toggleButton.setAttribute("aria-expanded", String(open));
|
||
};
|
||
|
||
let commitLoadStarted = false;
|
||
toggleButton.addEventListener("click", () => {
|
||
const nextOpen = panel.classList.contains("hidden");
|
||
setPanelOpen(nextOpen);
|
||
if (nextOpen && !commitLoadStarted) {
|
||
commitLoadStarted = true;
|
||
void loadGitHubCommitItems({
|
||
doc,
|
||
list,
|
||
fetchImpl,
|
||
fallbackItems: updateItems,
|
||
});
|
||
}
|
||
});
|
||
closeButton.addEventListener("click", () => {
|
||
writeStorage(storage, SITE_UPDATE_LAST_SEEN_KEY, version);
|
||
writeStorage(storage, SITE_UPDATE_DISMISSED_KEY, version);
|
||
root.remove();
|
||
});
|
||
|
||
root.append(panel, toggleButton);
|
||
doc.body.appendChild(root);
|
||
return { shown: true, version, root };
|
||
}
|