fix: retain WeChat share archives until memo save
This commit is contained in:
1 parent
5fdf7f5935
commit
e21583dfb8
11 files changed
+199
-50
No files matched your search
@@ -1801,9 +1801,10 @@ const startApplication = async () => {
|
||||
ipcMain.handle("desktop:read-wechat-import-media", async (_event, importId, mediaId) => (
|
||||
wechatShare().readMedia(importId, mediaId)
|
||||
));
|
||||
ipcMain.handle("desktop:finish-wechat-import", async (_event, importId) => {
|
||||
await wechatShare().finish(importId);
|
||||
ipcMain.handle("desktop:finish-wechat-import", async (_event, importId, success) => {
|
||||
await wechatShare().finish(importId, success === true);
|
||||
});
|
||||
ipcMain.handle("desktop:retry-wechat-import", (_event, importId) => wechatShare().retry(importId));
|
||||
ipcMain.handle("desktop:remove-staged-resource", async (_event, id) => {
|
||||
if (!isSafeResourceId(id)) throw new Error("Invalid staged resource id");
|
||||
const directory = stagedResourceDirectory();
|
||||
|
||||
@@ -159,13 +159,16 @@ const isMessageHeader = (lines, index) =>
|
||||
|
||||
const parseMessages = (text) => {
|
||||
const lines = text.replace(/^\uFEFF/, "").split(/\r?\n/);
|
||||
const messages = [];
|
||||
const sections = [];
|
||||
let index = 0;
|
||||
while (index < lines.length) {
|
||||
while (index < lines.length && lines[index].trim() === "") index += 1;
|
||||
if (index >= lines.length) break;
|
||||
if (!isMessageHeader(lines, index)) {
|
||||
index += 1;
|
||||
const start = index;
|
||||
while (index < lines.length && !isMessageHeader(lines, index)) index += 1;
|
||||
const raw = lines.slice(start, index).join("\n").trim();
|
||||
if (raw) sections.push({ raw: raw.split("\n") });
|
||||
continue;
|
||||
}
|
||||
const sender = lines[index].slice(1).trim();
|
||||
@@ -185,9 +188,9 @@ const parseMessages = (text) => {
|
||||
body.push(lines[index]);
|
||||
index += 1;
|
||||
}
|
||||
messages.push({ sender, time, body });
|
||||
sections.push({ sender, time, body });
|
||||
}
|
||||
return messages;
|
||||
return sections;
|
||||
};
|
||||
|
||||
const parseLink = (line) => {
|
||||
@@ -234,7 +237,7 @@ const titleTimestamp = (time) => {
|
||||
const archiveTitle = (archiveName, messages) => {
|
||||
const base = posix.basename(archiveName || "").replace(/\.(?:zip|txt)$/i, "").trim();
|
||||
if (!base || base === "聊天记录") {
|
||||
const timestamp = titleTimestamp(messages[0]?.time);
|
||||
const timestamp = titleTimestamp(messages.find((message) => message.time)?.time);
|
||||
if (timestamp) return `聊天记录 · ${timestamp}`;
|
||||
}
|
||||
return base || "聊天记录";
|
||||
@@ -283,6 +286,7 @@ export const wechatChatNoteFromArchive = (bytes, archiveName = TRANSCRIPT_NAME)
|
||||
const messages = parseMessages(text);
|
||||
const sections = messages.length > 0
|
||||
? messages.map((message) => {
|
||||
if (message.raw) return renderBody(message.raw, mediaByName);
|
||||
const body = renderBody(message.body, mediaByName);
|
||||
const header = `**${escapeMarkdownText(message.sender)}** · ${escapeMarkdownText(message.time)}`;
|
||||
return body ? `${header}\n\n${body}` : header;
|
||||
|
||||
@@ -116,6 +116,26 @@ describe("WeChat chat archive", () => {
|
||||
expect(wechatChatNoteFromArchive(withoutMessages, "聊天记录.zip").title).toBe("聊天记录");
|
||||
});
|
||||
|
||||
test("preserves transcript text that does not match a message header", () => {
|
||||
const archive = zipOf([{ name: "聊天记录.txt", data: [
|
||||
"聊天开始前的说明",
|
||||
"·鱼",
|
||||
"2026年9月22日 22:15",
|
||||
"你好",
|
||||
"",
|
||||
"[系统消息] 群名已更改",
|
||||
"",
|
||||
"·鱼",
|
||||
"2026年9月22日 22:16",
|
||||
"收到",
|
||||
].join("\n") }]);
|
||||
const note = wechatChatNoteFromArchive(archive, "聊天记录.zip");
|
||||
expect(note.title).toBe("聊天记录 · 2026-09-22 22:15");
|
||||
expect(note.markdown).toContain("聊天开始前的说明");
|
||||
expect(note.markdown).toContain("\\[系统消息\\] 群名已更改");
|
||||
expect(note.markdown).toContain("收到");
|
||||
});
|
||||
|
||||
test("rejects archives that are not a WeChat transcript", () => {
|
||||
expect(() => wechatChatNoteFromArchive(zipOf([{ name: "readme.txt", data: "hello" }]))).toThrow(WeChatArchiveError);
|
||||
expect(() => wechatChatNoteFromArchive(Buffer.from("not a zip"))).toThrow(WeChatArchiveError);
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { mkdir, readFile, realpath, readdir, rm, stat, writeFile } from "node:fs/promises";
|
||||
import { mkdir, readFile, realpath, readdir, rmdir, rm, writeFile } from "node:fs/promises";
|
||||
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
||||
import { WeChatArchiveError, wechatChatNoteFromArchive } from "./wechat-chat-archive.mjs";
|
||||
|
||||
export const WECHAT_INCOMING_DIRECTORY_NAME = "EdgeEver Incoming";
|
||||
const STALE_INCOMING_MS = 24 * 60 * 60 * 1000;
|
||||
const SAFE_ID = /^[A-Za-z0-9_-]{1,80}$/;
|
||||
|
||||
export const isPathInsideDirectory = (filePath, directory) => {
|
||||
@@ -46,23 +45,10 @@ const removeImportedZip = async (zipPath, downloadsPath) => {
|
||||
const parent = dirname(zipPath);
|
||||
const realParent = await realpath(parent).catch(() => null);
|
||||
if (root && realParent && isPathInsideDirectory(realParent, root)) {
|
||||
await rm(realParent, { recursive: true, force: true }).catch(() => {});
|
||||
await rm(zipPath, { force: true }).catch(() => {});
|
||||
await rmdir(realParent).catch(() => {});
|
||||
return;
|
||||
}
|
||||
await rm(zipPath, { force: true }).catch(() => {});
|
||||
};
|
||||
|
||||
const removeStaleIncoming = async (downloadsPath, keepPath) => {
|
||||
const root = incomingDirectory(downloadsPath);
|
||||
const entries = await readdir(root, { withFileTypes: true }).catch(() => []);
|
||||
const cutoff = Date.now() - STALE_INCOMING_MS;
|
||||
await Promise.all(entries.map(async (entry) => {
|
||||
const path = join(root, entry.name);
|
||||
if (keepPath && (path === keepPath || keepPath.startsWith(`${path}${sep}`))) return;
|
||||
const info = await stat(path).catch(() => null);
|
||||
if (!info || info.mtimeMs > cutoff) return;
|
||||
await rm(path, { recursive: true, force: true }).catch(() => {});
|
||||
}));
|
||||
};
|
||||
|
||||
export const createWeChatShareController = ({
|
||||
@@ -73,15 +59,38 @@ export const createWeChatShareController = ({
|
||||
writeDiagnostic = () => {},
|
||||
}) => {
|
||||
const sessions = new Map();
|
||||
const preparedPaths = new Set();
|
||||
const inFlightPaths = new Set();
|
||||
const completedPaths = new Set();
|
||||
const failedPaths = new Set();
|
||||
|
||||
const finish = async (importId) => {
|
||||
const finish = async (importId, success) => {
|
||||
if (!SAFE_ID.test(importId || "")) return;
|
||||
const session = sessions.get(importId);
|
||||
sessions.delete(importId);
|
||||
if (!session) return;
|
||||
if (!success) {
|
||||
session.failed = true;
|
||||
failedPaths.add(session.zipPath);
|
||||
void writeDiagnostic("wechat-import.save-failed");
|
||||
return;
|
||||
}
|
||||
sessions.delete(importId);
|
||||
preparedPaths.delete(session.zipPath);
|
||||
completedPaths.add(session.zipPath);
|
||||
failedPaths.delete(session.zipPath);
|
||||
await removeImportedZip(session.zipPath, downloadsPath());
|
||||
await rm(session.directory, { recursive: true, force: true }).catch(() => {});
|
||||
void writeDiagnostic("wechat-import.saved", { media: session.media.size });
|
||||
};
|
||||
|
||||
const retry = (importId) => {
|
||||
if (!SAFE_ID.test(importId || "")) return false;
|
||||
const session = sessions.get(importId);
|
||||
if (!session?.failed) return false;
|
||||
session.failed = false;
|
||||
failedPaths.delete(session.zipPath);
|
||||
sendToRenderer(session.payload);
|
||||
return true;
|
||||
};
|
||||
|
||||
const readMedia = async (importId, mediaId) => {
|
||||
@@ -98,14 +107,14 @@ export const createWeChatShareController = ({
|
||||
|
||||
const importFromProtocolUrl = async (value) => {
|
||||
const requestedPath = wechatImportFilePath(value);
|
||||
if (!requestedPath || inFlightPaths.has(requestedPath) || completedPaths.has(requestedPath)) return;
|
||||
if (!requestedPath || inFlightPaths.has(requestedPath) || preparedPaths.has(requestedPath)
|
||||
|| completedPaths.has(requestedPath) || failedPaths.has(requestedPath)) return;
|
||||
inFlightPaths.add(requestedPath);
|
||||
onActivity();
|
||||
const downloads = downloadsPath();
|
||||
let zipPath = null;
|
||||
let preparedDirectory = null;
|
||||
try {
|
||||
await removeStaleIncoming(downloads, requestedPath);
|
||||
zipPath = await assertIncomingZip(requestedPath, downloads);
|
||||
const archive = await readFile(zipPath);
|
||||
const note = wechatChatNoteFromArchive(archive, basename(zipPath));
|
||||
@@ -131,24 +140,25 @@ export const createWeChatShareController = ({
|
||||
byteSize: item.bytes.length,
|
||||
});
|
||||
}
|
||||
sessions.set(importId, { directory, media });
|
||||
preparedDirectory = null;
|
||||
completedPaths.add(requestedPath);
|
||||
sendToRenderer({
|
||||
const payload = {
|
||||
ok: true,
|
||||
importId,
|
||||
title: note.title,
|
||||
markdown: note.markdown,
|
||||
media: listed,
|
||||
});
|
||||
void writeDiagnostic("wechat-import.accepted", { media: listed.length, bytes: archive.length });
|
||||
};
|
||||
sessions.set(importId, { directory, media, zipPath, payload, failed: false });
|
||||
preparedPaths.add(requestedPath);
|
||||
preparedDirectory = null;
|
||||
sendToRenderer(payload);
|
||||
void writeDiagnostic("wechat-import.prepared", { media: listed.length, bytes: archive.length });
|
||||
} catch (error) {
|
||||
if (preparedDirectory) await rm(preparedDirectory, { recursive: true, force: true }).catch(() => {});
|
||||
const reason = error instanceof WeChatArchiveError ? error.code : "failed";
|
||||
failedPaths.add(requestedPath);
|
||||
sendToRenderer({ ok: false, reason: reason === "unrecognized" ? "unrecognized" : "failed" });
|
||||
void writeDiagnostic("wechat-import.rejected", { reason });
|
||||
} finally {
|
||||
await removeImportedZip(zipPath, downloads);
|
||||
inFlightPaths.delete(requestedPath);
|
||||
}
|
||||
};
|
||||
@@ -169,5 +179,5 @@ export const createWeChatShareController = ({
|
||||
}
|
||||
};
|
||||
|
||||
return { importFromProtocolUrl, importPending, readMedia, finish };
|
||||
return { importFromProtocolUrl, importPending, readMedia, finish, retry };
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { deflateRawSync } from "node:zlib";
|
||||
import { mkdir, mkdtemp, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
||||
import { mkdir, mkdtemp, readFile, rename, rm, stat, utimes, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, describe, expect, test } from "bun:test";
|
||||
@@ -110,9 +110,10 @@ describe("WeChat share handoff", () => {
|
||||
expect(sent[0].markdown).toContain("");
|
||||
const media = await controller.readMedia(sent[0].importId, "m1");
|
||||
expect(Buffer.from(media.bytes).toString()).toBe("jpeg");
|
||||
expect((await stat(zipPath)).isFile()).toBe(true);
|
||||
await controller.finish(sent[0].importId, true);
|
||||
await expect(stat(zipPath)).rejects.toThrow();
|
||||
await expect(stat(batch)).rejects.toThrow();
|
||||
await controller.finish(sent[0].importId);
|
||||
await expect(readFile(join(temp, "edgeever-wechat-import", sent[0].importId, "m1.bin"))).rejects.toThrow();
|
||||
});
|
||||
|
||||
@@ -154,7 +155,77 @@ describe("WeChat share handoff", () => {
|
||||
await Promise.all([controller.importPending(), controller.importFromProtocolUrl(url)]);
|
||||
expect(sent).toHaveLength(1);
|
||||
expect(sent[0].ok).toBe(true);
|
||||
expect((await stat(zipPath)).isFile()).toBe(true);
|
||||
await controller.importFromProtocolUrl(url);
|
||||
expect(sent).toHaveLength(1);
|
||||
await controller.finish(sent[0].importId, false);
|
||||
await controller.importPending();
|
||||
expect(sent).toHaveLength(1);
|
||||
expect(controller.retry(sent[0].importId)).toBe(true);
|
||||
expect(sent).toHaveLength(2);
|
||||
await controller.finish(sent[0].importId, true);
|
||||
await expect(stat(zipPath)).rejects.toThrow();
|
||||
});
|
||||
|
||||
test("keeps multiple old pending shares available across app restarts", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "edgeever-wechat-share-"));
|
||||
roots.push(root);
|
||||
const downloads = join(root, "Downloads");
|
||||
const incoming = join(downloads, WECHAT_INCOMING_DIRECTORY_NAME);
|
||||
const old = new Date(Date.now() - 48 * 60 * 60 * 1000);
|
||||
for (const name of ["first", "second"]) {
|
||||
const batch = join(incoming, name);
|
||||
await mkdir(batch, { recursive: true });
|
||||
const zipPath = join(batch, "聊天记录.zip");
|
||||
await writeFile(zipPath, zipOf("聊天记录.txt", `·鱼\n2026年9月22日 22:15\n${name}\n`, "附件/readme.txt", "hello"));
|
||||
await utimes(batch, old, old);
|
||||
}
|
||||
const prepared = [];
|
||||
const controller = createWeChatShareController({
|
||||
downloadsPath: () => downloads,
|
||||
tempPath: () => join(root, "temp"),
|
||||
sendToRenderer: (payload) => prepared.push(payload),
|
||||
});
|
||||
await controller.importPending();
|
||||
expect(prepared).toHaveLength(2);
|
||||
await controller.finish(prepared[0].importId, false);
|
||||
const resumed = [];
|
||||
const restarted = createWeChatShareController({
|
||||
downloadsPath: () => downloads,
|
||||
tempPath: () => join(root, "new-temp"),
|
||||
sendToRenderer: (payload) => resumed.push(payload),
|
||||
});
|
||||
await restarted.importPending();
|
||||
expect(resumed).toHaveLength(2);
|
||||
for (const name of ["first", "second"]) {
|
||||
expect((await stat(join(incoming, name, "聊天记录.zip"))).isFile()).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test("finishing one zip keeps another zip in the same incoming batch", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "edgeever-wechat-share-"));
|
||||
roots.push(root);
|
||||
const downloads = join(root, "Downloads");
|
||||
const batch = join(downloads, WECHAT_INCOMING_DIRECTORY_NAME, "batch");
|
||||
await mkdir(batch, { recursive: true });
|
||||
for (const name of ["first", "second"]) {
|
||||
await writeFile(join(batch, `${name}.zip`), zipOf("聊天记录.txt", `·鱼\n2026年9月22日 22:15\n${name}\n`, "附件/readme.txt", "hello"));
|
||||
}
|
||||
const sent = [];
|
||||
const controller = createWeChatShareController({
|
||||
downloadsPath: () => downloads,
|
||||
tempPath: () => join(root, "temp"),
|
||||
sendToRenderer: (payload) => sent.push(payload),
|
||||
});
|
||||
await controller.importPending();
|
||||
expect(sent).toHaveLength(2);
|
||||
const first = sent.find((payload) => payload.title === "first");
|
||||
const second = sent.find((payload) => payload.title === "second");
|
||||
expect(first).toBeDefined();
|
||||
expect(second).toBeDefined();
|
||||
await controller.finish(first.importId, true);
|
||||
expect((await stat(join(batch, "second.zip"))).isFile()).toBe(true);
|
||||
await controller.finish(second.importId, true);
|
||||
await expect(stat(batch)).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -108,7 +108,8 @@ contextBridge.exposeInMainWorld("edgeeverDesktop", Object.freeze({
|
||||
mimeType: file.mimeType,
|
||||
bytes: normalizeIpcBytes(file.bytes),
|
||||
})),
|
||||
finishWeChatImport: (importId) => ipcRenderer.invoke("desktop:finish-wechat-import", importId),
|
||||
finishWeChatImport: (importId, success) => ipcRenderer.invoke("desktop:finish-wechat-import", importId, success),
|
||||
retryWeChatImport: (importId) => ipcRenderer.invoke("desktop:retry-wechat-import", importId),
|
||||
onImportWeChatChat: (callback) => {
|
||||
const listener = (_event, payload) => callback(payload);
|
||||
ipcRenderer.on("desktop:import-wechat-chat", listener);
|
||||
|
||||
@@ -262,6 +262,8 @@ export const WorkspaceApp = ({
|
||||
const [notebookNameDialog, setNotebookNameDialog] = useState<NotebookNameDialogState | null>(null);
|
||||
const [notebookDeleteConfirmation, setNotebookDeleteConfirmation] = useState<Notebook | null>(null);
|
||||
const [appNoticeDialog, setAppNoticeDialog] = useState<AppNoticeDialogState | null>(null);
|
||||
const [wechatImportFailureId, setWeChatImportFailureId] = useState<string | null>(null);
|
||||
const [wechatImportsInProgress, setWeChatImportsInProgress] = useState(0);
|
||||
const [pendingCatalogTrustPluginIds, setPendingCatalogTrustPluginIds] = useState<string[]>([]);
|
||||
const [isExportingSelectedMemos, setIsExportingSelectedMemos] = useState(false);
|
||||
const [selectedMarkdownExportProgress, setSelectedMarkdownExportProgress] = useState<MarkdownExportProgress>({
|
||||
@@ -1784,11 +1786,7 @@ export const WorkspaceApp = ({
|
||||
media?: Array<{ id: string; filename: string; mimeType: string; byteSize: number }>;
|
||||
}) => {
|
||||
const bridge = window.edgeeverDesktop;
|
||||
const finish = () => {
|
||||
if (payload.importId && bridge?.finishWeChatImport) void bridge.finishWeChatImport(payload.importId);
|
||||
};
|
||||
if (!payload.ok || !payload.importId || !payload.markdown) {
|
||||
finish();
|
||||
setAppNoticeDialog({
|
||||
title: t("memoList.importWeChatFailedTitle"),
|
||||
description: payload.reason === "unrecognized"
|
||||
@@ -1809,6 +1807,8 @@ export const WorkspaceApp = ({
|
||||
setTemplatesOpen(false);
|
||||
setMobileBottomNavActive("home");
|
||||
creatingMemoSelectionRef.current = true;
|
||||
setWeChatImportsInProgress((count) => count + 1);
|
||||
let savedMemo: MemoDetail | null = null;
|
||||
try {
|
||||
const memo = await createWeChatChatMemo({
|
||||
notebookId,
|
||||
@@ -1856,16 +1856,18 @@ export const WorkspaceApp = ({
|
||||
}),
|
||||
deleteMemo: (memoId) => repository.deleteMemo(memoId, true),
|
||||
});
|
||||
await putLocalMemo(localDataScope, memo);
|
||||
savedMemo = memo;
|
||||
await bridge?.finishWeChatImport?.(importId, true).catch(() => undefined);
|
||||
await putLocalMemo(localDataScope, memo).catch(() => undefined);
|
||||
revealCreatedMemo(memo);
|
||||
} catch {
|
||||
if (!savedMemo) {
|
||||
await bridge?.finishWeChatImport?.(importId, false).catch(() => undefined);
|
||||
setWeChatImportFailureId(importId);
|
||||
}
|
||||
creatingMemoSelectionRef.current = false;
|
||||
setAppNoticeDialog({
|
||||
title: t("memoList.importWeChatFailedTitle"),
|
||||
description: t("memoList.importWeChatFailed"),
|
||||
});
|
||||
} finally {
|
||||
finish();
|
||||
setWeChatImportsInProgress((count) => Math.max(0, count - 1));
|
||||
}
|
||||
}, [defaultMemoNotebookId, imageCompressionEnabled, localDataScope, memoView, notebooks, repository, selectedNotebookId, t]);
|
||||
|
||||
@@ -3003,6 +3005,13 @@ export const WorkspaceApp = ({
|
||||
return (
|
||||
<WorkspaceMotionProvider>
|
||||
<div className="edgeever-workspace-shell flex h-[100dvh] overflow-hidden text-slate-950">
|
||||
{wechatImportsInProgress > 0 && (
|
||||
<div className="pointer-events-none fixed inset-x-0 top-3 z-50 flex justify-center" role="status" aria-live="polite">
|
||||
<div className="rounded-full border border-slate-200 bg-card px-4 py-2 text-sm font-medium text-slate-700 shadow-lg">
|
||||
{t("memoList.importWeChatInProgress")}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{pullToRefreshVisible && (
|
||||
<div
|
||||
className="pointer-events-none fixed inset-x-0 top-[max(0.75rem,env(safe-area-inset-top))] z-50 flex justify-center lg:hidden"
|
||||
@@ -3545,6 +3554,29 @@ export const WorkspaceApp = ({
|
||||
onConfirm={() => setAppNoticeDialog(null)}
|
||||
/>
|
||||
)}
|
||||
{wechatImportFailureId && (
|
||||
<AppConfirmDialog
|
||||
title={t("memoList.importWeChatFailedTitle")}
|
||||
description={t("memoList.importWeChatRetryHint")}
|
||||
confirmLabel={t("memoList.importWeChatRetry")}
|
||||
closeOnBrowserBack={false}
|
||||
tone="neutral"
|
||||
onCancel={() => setWeChatImportFailureId(null)}
|
||||
onConfirm={() => {
|
||||
const importId = wechatImportFailureId;
|
||||
setWeChatImportFailureId(null);
|
||||
void Promise.resolve(window.edgeeverDesktop?.retryWeChatImport?.(importId) ?? false).then((retried) => {
|
||||
if (!retried) setAppNoticeDialog({
|
||||
title: t("memoList.importWeChatFailedTitle"),
|
||||
description: t("memoList.importWeChatFailed"),
|
||||
});
|
||||
}).catch(() => setAppNoticeDialog({
|
||||
title: t("memoList.importWeChatFailedTitle"),
|
||||
description: t("memoList.importWeChatFailed"),
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{pendingCatalogTrustPluginIds.length > 0 ? (
|
||||
<AppConfirmDialog
|
||||
title={t(PLUGIN_TRUST_WARNING_COPY.titleKey)}
|
||||
|
||||
Vendored
+2
-1
@@ -87,7 +87,8 @@ interface EdgeEverDesktopBridge {
|
||||
onImportMarkdown(callback: (payload: { name: string; content: string }) => void): () => void;
|
||||
onImportScreenshot?(callback: (payload: { captureId?: string; name: string; type: string; title?: string; bytes: Uint8Array }) => void): () => void;
|
||||
readWeChatImportMedia?(importId: string, mediaId: string): Promise<{ filename: string; mimeType: string; bytes: Uint8Array }>;
|
||||
finishWeChatImport?(importId: string): Promise<void>;
|
||||
finishWeChatImport?(importId: string, success: boolean): Promise<void>;
|
||||
retryWeChatImport?(importId: string): Promise<boolean>;
|
||||
onImportWeChatChat?(callback: (payload: {
|
||||
ok: boolean;
|
||||
reason?: string;
|
||||
|
||||
@@ -1622,6 +1622,9 @@ export const enUS = {
|
||||
importWeChatFailedTitle: "Could not save the WeChat chat",
|
||||
importWeChatUnrecognized: "This archive is not a WeChat chat export.",
|
||||
importWeChatFailed: "Couldn't save the chat as a note. Try again.",
|
||||
importWeChatInProgress: "Importing the WeChat chat…",
|
||||
importWeChatRetryHint: "The chat export is still in Downloads. Try importing it again.",
|
||||
importWeChatRetry: "Retry import",
|
||||
loadingMore: "Loading more notes",
|
||||
openMemo: "Open note",
|
||||
unpin: "Unpin",
|
||||
|
||||
@@ -1622,6 +1622,9 @@ export const ja = {
|
||||
importWeChatFailedTitle: "WeChat のチャットを保存できませんでした",
|
||||
importWeChatUnrecognized: "このアーカイブは WeChat のチャット書き出しではありません。",
|
||||
importWeChatFailed: "チャットをノートとして保存できませんでした。再試行してください。",
|
||||
importWeChatInProgress: "WeChat のチャットを取り込んでいます…",
|
||||
importWeChatRetryHint: "チャットの書き出しファイルはダウンロードフォルダに残っています。もう一度お試しください。",
|
||||
importWeChatRetry: "取り込みを再試行",
|
||||
loadingMore: "さらにノートを読み込み中",
|
||||
openMemo: "ノートを開く",
|
||||
unpin: "ピン留めを外す",
|
||||
|
||||
@@ -1622,6 +1622,9 @@ export const zhCN = {
|
||||
importWeChatFailedTitle: "无法保存微信聊天记录",
|
||||
importWeChatUnrecognized: "这个压缩包不是微信导出的聊天记录。",
|
||||
importWeChatFailed: "无法把聊天记录保存为笔记。请重试。",
|
||||
importWeChatInProgress: "正在导入微信聊天记录…",
|
||||
importWeChatRetryHint: "聊天记录仍保存在下载目录。请重试导入。",
|
||||
importWeChatRetry: "重试导入",
|
||||
loadingMore: "正在加载更多笔记",
|
||||
openMemo: "打开笔记",
|
||||
unpin: "取消置顶",
|
||||
|
||||
Reference in new issue
Block a user