fix: make conversation restore reliable in Safari

This commit is contained in:
2026-08-03 14:32:52 +08:00
parent 328b7dd2f1
commit 3d561f049c
2 changed files with 62 additions and 21 deletions
+39 -11
View File
@@ -75,6 +75,20 @@ function scrollLatest(): void { messages.scrollTop = messages.scrollHeight; }
function setPresence(value: string): void { presence.textContent = value; }
function addSystem(message: string): void { const item = document.createElement("p"); item.className = "system"; item.textContent = message; messages.append(item); scrollLatest(); }
function newLocalID(prefix: string): string {
// `crypto.randomUUID` is absent from older Safari / WKWebView versions.
// A local item id needs uniqueness within this browser store, not a
// transport identity, so the timestamp-and-random fallback is sufficient
// and must not prevent the form submit handler from reaching HTTP.
const uuid = globalThis.crypto?.randomUUID?.();
return `${prefix}_${uuid ?? `${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`}`;
}
function storageFailureMessage(reason: unknown): string {
const detail = reason instanceof Error && reason.message ? `${reason.message}` : "";
return `本地对话历史暂时无法保存${detail};消息仍会尝试发送,刷新后将从服务端重新同步。`;
}
function normalizeAPIAddress(value: string): string {
const candidate = value.trim();
if (!candidate) throw new Error("请输入 AppServer 地址。");
@@ -278,24 +292,38 @@ $<HTMLFormElement>("#login-form").addEventListener("submit", async event => {
$<HTMLFormElement>("#message-form").addEventListener("submit", async event => {
event.preventDefault();
const input = $<HTMLTextAreaElement>("#message-input"); const send = $<HTMLButtonElement>("#send"); const text = input.value.trim();
if (!text || !session.uid) return;
if (!text) return;
if (!session.uid) {
addSystem("当前会话尚未初始化,无法发送消息;请退出后重新登录。");
input.focus();
return;
}
input.value = ""; send.disabled = true;
const localID = `local_${crypto.randomUUID()}`;
const localID = newLocalID("local");
const createdAt = new Date().toISOString();
conversationStore.appendLocalText(localID, text, createdAt);
const row = appendBubble("human", text, false, "发送中"); const meta = row.querySelector<HTMLElement>(".meta");
// A browser storage failure must never prevent the user from seeing or
// submitting their message. The Store remains the durable source when it
// is available; the server sync is the recovery path when it is not.
const row = appendBubble("human", text, false, "发送中");
const meta = row.querySelector<HTMLElement>(".meta");
let storedLocally = true;
try { conversationStore.appendLocalText(localID, text, createdAt); }
catch (reason) { storedLocally = false; addSystem(storageFailureMessage(reason)); }
try {
const sequence = await sendPayload(text);
if (sequence) {
session.lastSeq = Math.max(session.lastSeq, sequence);
conversationStore.markSubmitted(localID, sequence, new Date().toISOString());
if (sequence && storedLocally) {
try { conversationStore.markSubmitted(localID, sequence, new Date().toISOString()); }
catch (reason) { storedLocally = false; addSystem(storageFailureMessage(reason)); }
}
if (meta) meta.textContent = `${meta.textContent?.replace(" · 发送中", "") ?? ""} · 已发送"`;
if (meta) meta.textContent = `${meta.textContent?.replace(" · 发送中", "") ?? ""} · 已发送`;
}
catch (reason) {
const message = reason instanceof Error ? reason.message : "发送失败";
conversationStore.markFailed(localID, message, new Date().toISOString());
if (meta) meta.textContent = `${meta.textContent?.replace(" · 发送中", "") ?? ""} · 发送失败"`;
if (storedLocally) {
try { conversationStore.markFailed(localID, message, new Date().toISOString()); }
catch (storageReason) { addSystem(storageFailureMessage(storageReason)); }
}
if (meta) meta.textContent = `${meta.textContent?.replace(" · 发送中", "") ?? ""} · 发送失败`;
addSystem(message);
}
finally { send.disabled = false; input.focus(); }
@@ -312,7 +340,7 @@ export function makeProtocolEvent(
): Envelope {
return {
v: 1,
id: `msg_${crypto.randomUUID()}`,
id: newLocalID("msg"),
type,
conversation_id: conversationID,
sender,