feat: tray, window-state, sqlite cache, decrypt worker
System tray (desktop) - tauri tray-icon feature + tray with menu (Öffnen/Ausblenden/Beenden) - Left-click toggles main window; right-click shows menu - JS emits tray-unread-update event, Rust mirrors into tooltip + macOS dock badge via set_badge_label - ConversationsContext wires totalUnread → tray Window state persistence - tauri-plugin-window-state (desktop-only target guard) - Auto-restore size/position/maximized between restarts Local SQLite message cache - tauri-plugin-sql hydration of conversation view on mount - persistMessages after each refresh, deleteCachedMessage on realtime DELETE, pruneCache keeps latest 1000 per conversation - Stores plaintext only (same trust boundary as stronghold device key; cache never leaves the device, E2EE w.r.t. server unchanged) Web Worker for decryption - workers/decrypt.worker.ts runs crypto_secretbox_open_easy + utf-8 decode off the main thread with its own libsodium instance - lib/decryptWorker.ts is a request/response wrapper with inline fallback when Worker spawn fails - shared decryptMessages accepts aeadBatchDelegate so key lookup stays on the main thread while the AEAD loop offloads Build fix - Enable tauri tray-icon feature - Import Listener + Manager traits, clone tray handle for the event listener, conditional icon attach
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
// Web Worker — runs XSalsa20-Poly1305 decrypt + utf8 decode off the main
|
||||
// thread. The conv-key lookup (which is network-bound) stays in the main
|
||||
// thread; only the CPU-heavy AEAD + UTF-8 step runs here.
|
||||
//
|
||||
// Message protocol:
|
||||
// request: { id: string, items: Array<{ id, ciphertext, nonce, key }> }
|
||||
// response: { id: string, results: Array<{ id, plaintext: string | null }> }
|
||||
|
||||
/// <reference lib="webworker" />
|
||||
|
||||
import sodium from 'libsodium-wrappers-sumo';
|
||||
|
||||
interface DecryptItem {
|
||||
id: string;
|
||||
ciphertext: Uint8Array;
|
||||
nonce: Uint8Array;
|
||||
key: Uint8Array;
|
||||
}
|
||||
|
||||
interface DecryptRequest {
|
||||
id: string;
|
||||
items: DecryptItem[];
|
||||
}
|
||||
|
||||
interface DecryptResult {
|
||||
id: string;
|
||||
plaintext: string | null;
|
||||
}
|
||||
|
||||
interface DecryptResponse {
|
||||
id: string;
|
||||
results: DecryptResult[];
|
||||
}
|
||||
|
||||
let sodiumReady: Promise<typeof sodium> | null = null;
|
||||
|
||||
async function ensureSodium(): Promise<typeof sodium> {
|
||||
if (!sodiumReady) sodiumReady = sodium.ready.then(() => sodium);
|
||||
return sodiumReady;
|
||||
}
|
||||
|
||||
function decodeOne(s: typeof sodium, item: DecryptItem): string | null {
|
||||
try {
|
||||
const plain = s.crypto_secretbox_open_easy(item.ciphertext, item.nonce, item.key);
|
||||
return new TextDecoder('utf-8', { fatal: false }).decode(plain);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
self.addEventListener('message', (ev: MessageEvent<DecryptRequest>) => {
|
||||
const req = ev.data;
|
||||
void (async () => {
|
||||
const s = await ensureSodium();
|
||||
const results: DecryptResult[] = req.items.map((item) => ({
|
||||
id: item.id,
|
||||
plaintext: decodeOne(s, item),
|
||||
}));
|
||||
const resp: DecryptResponse = { id: req.id, results };
|
||||
(self as unknown as Worker).postMessage(resp);
|
||||
})();
|
||||
});
|
||||
Reference in New Issue
Block a user