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:
@@ -13,6 +13,13 @@ import {
|
||||
import { bytesToPgHex, pgBytesToBytes } from '@chat-app/shared/supabase';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { decryptBatch as decryptBatchWorker } from './decryptWorker';
|
||||
import {
|
||||
loadCachedMessages,
|
||||
persistMessages,
|
||||
pruneCache,
|
||||
deleteCachedMessage,
|
||||
} from './messageCache';
|
||||
import {
|
||||
enqueueOutbox,
|
||||
getOutbox,
|
||||
@@ -103,6 +110,10 @@ export function useConversationMessages({ conversationId, userId, deviceId }: Ar
|
||||
messages,
|
||||
ownDeviceId: deviceId,
|
||||
ownPrivateKey: priv,
|
||||
// Offload the symmetric decrypt + utf-8 decode to a Web Worker so
|
||||
// the main thread stays responsive during bulk operations (initial
|
||||
// fetch, backfill after sleep).
|
||||
aeadBatchDelegate: decryptBatchWorker,
|
||||
});
|
||||
},
|
||||
[deviceId],
|
||||
@@ -120,6 +131,10 @@ export function useConversationMessages({ conversationId, userId, deviceId }: Ar
|
||||
const rows = await fetchConversationMessages(supabase, conversationId);
|
||||
const decrypted = await decryptBatch(rows);
|
||||
setState({ messages: decrypted, loading: false, error: null });
|
||||
// Persist the fresh batch to the local cache so next conversation
|
||||
// switch / app start can hydrate instantly. Fire-and-forget — cache
|
||||
// write failure is never user-visible.
|
||||
void persistMessages(conversationId, decrypted);
|
||||
} catch (err: unknown) {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
@@ -129,6 +144,31 @@ export function useConversationMessages({ conversationId, userId, deviceId }: Ar
|
||||
}
|
||||
}, [conversationId, decryptBatch]);
|
||||
|
||||
// Hydrate from the local SQLite cache the moment the conversation id
|
||||
// changes. Runs in parallel with the network fetch — whichever resolves
|
||||
// first populates the UI, and `refresh` will replace stale cache data
|
||||
// when the server response lands. On cache-miss this is a ~5ms no-op.
|
||||
useEffect(() => {
|
||||
if (!conversationId) return;
|
||||
let cancelled = false;
|
||||
void loadCachedMessages(conversationId).then((cached) => {
|
||||
if (cancelled || cached.length === 0) return;
|
||||
setState((prev) => {
|
||||
// Don't clobber a fresh server response that already landed.
|
||||
if (prev.messages.length > 0) return prev;
|
||||
return { messages: cached, loading: false, error: null };
|
||||
});
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [conversationId]);
|
||||
|
||||
// Prune cache once per app session.
|
||||
useEffect(() => {
|
||||
void pruneCache();
|
||||
}, []);
|
||||
|
||||
// Realtime INSERT handler — refetches the row via REST so we get the
|
||||
// canonical bytea encoding (postgres_changes payloads serialize bytea
|
||||
// differently and decoding them inline is brittle). Then decrypt + append.
|
||||
@@ -296,6 +336,7 @@ export function useConversationMessages({ conversationId, userId, deviceId }: Ar
|
||||
...prev,
|
||||
messages: prev.messages.filter((m) => m.id !== id),
|
||||
}));
|
||||
void deleteCachedMessage(id);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user