This commit is contained in:
Mateusz Gruszczyński
2026-09-17 13:11:37 +02:00
parent 86a6fa1f2f
commit eede4e88e6
10 changed files with 145 additions and 20 deletions
+41 -12
View File
@@ -52,7 +52,7 @@ export function startNoteEditor(adapter) {
: `${Date.now().toString(36)}${Math.random().toString(36).slice(2)}`;
const collaboration = new CollaborationSession(collaborationClientId);
let accessToken = shareToken || getAccessToken(adapter.access.kind, adapter.access.key), password = "", nickname = getNickname(), info, socket, saveTimer, applyingRemote = false, applyingHistory = false, resourceUnlocked = false, uiState = readEditorState(), authorship = parseAuthorship("", "[]"), previousContent = "", globalColor = "", noteColor = "", presenceUsers = [], authorshipMode = "simple", authorshipColorsEnabled = true, lastRevealedLineHash = "", flushRequested = false, accountSession = null;
let editorSettingsSaveTimer, editorSettingsSaveInFlight = false, pendingPersonalSettingsSave = false, pendingAuthorshipSettingsSave = false, connectionNoticeTimer = 0, connectionWasInterrupted = false;
let editorSettingsSaveTimer, editorSettingsSaveInFlight = false, pendingPersonalSettingsSave = false, pendingAuthorshipSettingsSave = false, connectionNoticeTimer = 0, connectionWasInterrupted = false, frontendErrorToastAt = 0;
let pendingPreviewViewport = null;
const editHistory = {
entries: [],
@@ -370,37 +370,66 @@ export function startNoteEditor(adapter) {
function clearUnread() { unreadChat = 0; chatUnread.hidden = true; chatUnread.textContent = ""; if (mobileChatUnread) { mobileChatUnread.hidden = true; mobileChatUnread.textContent = ""; } document.title = document.title.replace(/^● /, ""); }
function setStatus(kind, text) { const className = `status__dot${kind ? ` is-${kind}` : ""}`; document.querySelector("#status-dot").className = className; document.querySelector("#status-text").textContent = text; const mobileDot = document.querySelector("#mobile-status-dot"); const mobileText = document.querySelector("#mobile-status-text"); if (mobileDot) mobileDot.className = className; if (mobileText) mobileText.textContent = text; }
function showConnectionNotice(title, message, restored = false) {
function showConnectionNotice(title, message, state = "reconnecting") {
clearTimeout(connectionNoticeTimer);
connectionNoticeTitle.textContent = title;
connectionNoticeMessage.textContent = message;
connectionNotice.hidden = false;
connectionNotice.classList.toggle("is-restored", restored);
connectionNotice.classList.remove("is-restored", "is-error", "is-reconnecting");
connectionNotice.classList.add(`is-${state}`);
requestAnimationFrame(() => connectionNotice.classList.add("is-visible"));
if (restored) connectionNoticeTimer = window.setTimeout(() => {
if (state === "restored") connectionNoticeTimer = window.setTimeout(() => {
connectionNotice.classList.remove("is-visible", "is-restored");
connectionNoticeTimer = window.setTimeout(() => { connectionNotice.hidden = true; }, 220);
}, 1800);
}
function hideConnectionNotice() {
clearTimeout(connectionNoticeTimer);
connectionNotice.classList.remove("is-visible", "is-restored");
connectionNotice.classList.remove("is-visible", "is-restored", "is-error", "is-reconnecting");
connectionNotice.hidden = true;
}
function handleSocketStatus(status, details = {}) {
if (status === "online") {
setStatus("online", "Connected");
if (connectionWasInterrupted || details.restored) showConnectionNotice("Connection restored", "Live editing is active again.", true);
setStatus("online", t("status.connected", {}, "Connected"));
if (connectionWasInterrupted || details.restored) showConnectionNotice(
t("editor.connectionRestored", {}, "Connection restored"),
t("editor.connectionRestored.message", {}, "Live editing is active again."),
"restored",
);
connectionWasInterrupted = false;
return;
}
if (status === "error") {
connectionWasInterrupted = true;
setStatus("error", t("status.connectionError", {}, "Connection error"));
const message = details.persistent
? t("editor.connectionPersistent", {}, "The live connection is still unavailable. Automatic reconnect continues.")
: translateSource(details.message || "The server connection was interrupted.");
showConnectionNotice(t("editor.connectionError", {}, "Editor connection error"), message, "error");
return;
}
if (status === "reconnecting") {
connectionWasInterrupted = true;
setStatus("offline", "Reconnecting…");
showConnectionNotice("Connection interrupted", details.message || "Trying to reconnect automatically.");
setStatus("reconnecting", t("status.reconnecting", {}, "Reconnecting..."));
showConnectionNotice(
t("editor.connectionInterrupted", {}, "Connection interrupted"),
translateSource(details.message || t("editor.reconnectAuto", {}, "Trying to reconnect automatically.")),
"reconnecting",
);
return;
}
setStatus(null, "Connecting…");
setStatus("connecting", t("status.connecting", {}, "Connecting..."));
}
function notifyFrontendError() {
const now = Date.now();
if (now - frontendErrorToastAt < 30000) return;
frontendErrorToastAt = now;
try {
toast.warning(
t("editor.interfaceError.message", {}, "An interface error occurred. Connection status is monitored separately."),
{ title: t("editor.interfaceError", {}, "Interface error"), duration: 6500 },
);
} catch { }
}
function updateAddressLabel() { document.querySelector(adapter.addressSelector).textContent = `${location.pathname}${location.search}`; }
let mermaidRenderVersion = 0;
@@ -2343,7 +2372,7 @@ export function startNoteEditor(adapter) {
const historyPanel = document.querySelector("#history-panel"); document.querySelector("#history-button").addEventListener("click", async () => { if (info?.protected && !resourceUnlocked) { if (!passwordDialog.open) passwordDialog.showModal(); document.querySelector("#open-password")?.focus(); return; } historyPanel.classList.add("open"); historyPanel.setAttribute("aria-hidden", "false"); document.body.classList.add("history-open"); const list = document.querySelector("#history-list"); list.innerHTML = '<p class="empty">Loading…</p>'; try { const revisions = await adapter.loadHistory(accessToken); list.innerHTML = revisions.length ? revisions.map((r, i) => { const snippet = escapeHtml(r.content.trim().split("\n").slice(0, 3).join(" · ").slice(0, 150) || "Empty note"); const author = r.author || "Unknown author"; return `<article class="revision"><span class="revision__marker" style="--owner:${colorFor(author)}"></span><div><div class="revision__meta"><strong>${escapeHtml(author)}</strong><time>${formatDate(r.created_at)}</time></div><p class="revision__snippet">${snippet}</p><button data-preview="${r.id}">Preview</button><button data-revision="${r.id}">Restore</button><div class="revision__preview" id="preview-${r.id}" hidden></div></div></article>`; }).join("") : '<p class="empty">No history yet.</p>'; for (const r of revisions) { list.querySelector(`[data-preview="${r.id}"]`)?.addEventListener("click", () => { const el = list.querySelector(`#preview-${r.id}`); el.hidden = !el.hidden; el.textContent = r.content; }); list.querySelector(`[data-revision="${r.id}"]`)?.addEventListener("click", async () => { await adapter.restoreRevision(r.id, accessToken); toast.success("The selected revision is now the current version.", { title: "Version restored" }); }); } } catch (e) { list.innerHTML = `<p class="error">${escapeHtml(e.message)}</p>`; toast.danger(e.message, { title: "Could not load version history" }); } }); document.querySelector("#close-history").addEventListener("click", () => { historyPanel.classList.remove("open"); historyPanel.setAttribute("aria-hidden", "true"); document.body.classList.remove("history-open"); });
const deleteNoteButton = document.querySelector("#delete-note"); if (deleteNoteButton && adapter.deleteNote) deleteNoteButton.addEventListener("click", async () => { try { await adapter.deleteNote(info, accessToken); } catch (error) { toast.danger(error.message, { title: "Could not delete note" }); } });
window.addEventListener("error", event => { setStatus("offline", "Application error"); console.error(event.error || event.message); });
window.addEventListener("unhandledrejection", event => { setStatus("offline", "Application error"); console.error(event.reason); });
window.addEventListener("error", () => notifyFrontendError());
window.addEventListener("unhandledrejection", () => notifyFrontendError());
initialize();
}