feat: reply + search + forward + archive/mute + error boundary
Messages: - Reply-to: hover action, composer chip with cancel, quote bubble inside the replying message with tap-to-jump + amber highlight ring - Search: header search button toggles in-conversation search bar with prev/next + match counter, auto-jump to active match - Forward: multi-select conversation picker. Attachments are now carried over: download + decrypt source, re-encrypt under each target conv-key, re-upload with fresh per-attachment keys, insert new attachment rows Conversations: - Archive + mute per member. New migration 20260420000001 adds `archived` + `muted_until` on conversation_members. Shared helpers: setConversationArchived / setConversationMutedUntil / isConversationMuted - ChatsPage: archive toggle in header with unread badge for archived bucket, split active/archived lists, muted indicator (BellOff icon, dimmed unread badge) - ConversationRowMenu via createPortal (escapes sidebar overflow clip), forwardRef-based MenuItem so submenu positioning refs survive React 18 - ConversationsContext: suppresses notification sound + OS notif when target conversation is muted - Refresh on `profiles UPDATE` realtime so peer avatar / displayName changes flow to conversation.members without manual refresh Resilience: - ErrorBoundary (Discord-style): centred spinner + escalating copy, no manual reload button. Backoff retry schedule [2s, 4s, 8s, 15s, 30s]. Uses a keyed Fragment (not a div wrapper) so flex h-full chains survive - App wrapped root + per-route RouteBoundary, conversation-level boundary - AuthContext: flip `ready` immediately on cached session read; validate getUser in background so a stalled/offline Supabase doesn't freeze the app on the loading spinner Crypto: - Swap libsodium-wrappers -> libsodium-wrappers-sumo (compact build was missing crypto_pwhash so Argon2id vault KDF threw, falling back to plaintext localStorage on every launch) - Shim d.ts for sumo types (sumo is API superset, no official types ship) - vite optimizeDeps includes sumo with the "require" condition - secureFileStore: exists(dir) check before mkdir; surface genuine permission errors instead of silent catch Tauri: - fs scope adds `$APPLOCALDATA` direct entry (not just /**) so the app data directory itself can be mkdir'd on first launch Chat layout: - Skip call_event messages when computing avatar run boundaries so a regular bubble followed by a call event from the same sender still shows its avatar
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import type { CryptoBackend } from '@chat-app/shared/crypto';
|
||||
import _sodium from 'libsodium-wrappers';
|
||||
import _sodium from 'libsodium-wrappers-sumo';
|
||||
|
||||
// Build the libsodium-backed CryptoBackend. Awaits the WASM ready-gate once,
|
||||
// then returns a synchronous implementation of the CryptoBackend contract.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getCryptoBackend } from '@chat-app/shared/crypto';
|
||||
import sodium from 'libsodium-wrappers';
|
||||
import sodium from 'libsodium-wrappers-sumo';
|
||||
|
||||
// Encrypts/decrypts the device private key with a user-provided passphrase
|
||||
// so the backup string can be safely written down or stored in a password
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { SecretStore } from '@chat-app/shared/auth';
|
||||
import { exists, mkdir, readFile, rename, writeFile } from '@tauri-apps/plugin-fs';
|
||||
import { appLocalDataDir } from '@tauri-apps/api/path';
|
||||
import sodium from 'libsodium-wrappers';
|
||||
// sumo variant ships crypto_pwhash (Argon2id). Standard `libsodium-wrappers`
|
||||
// is the compact build without Argon2 — vault KDF would error otherwise.
|
||||
import sodium from 'libsodium-wrappers-sumo';
|
||||
|
||||
// Encrypted single-file SecretStore for Tauri. Replaces the flaky
|
||||
// tauri-plugin-stronghold implementation.
|
||||
@@ -93,10 +95,13 @@ async function loadOrCreateVault(userId: string): Promise<VaultState> {
|
||||
const path = joinPath(dir, fileName);
|
||||
const tmpPath = path + '.tmp';
|
||||
|
||||
try {
|
||||
// First-run: AppLocalData dir may not exist yet. `mkdir(recursive)` is
|
||||
// idempotent on macOS/Linux, but we need to surface genuine permission
|
||||
// errors (silent catch masked a previous bug where the dir was never
|
||||
// created and every subsequent writeFile failed with ENOENT).
|
||||
const dirExists = await exists(dir).catch(() => false);
|
||||
if (!dirExists) {
|
||||
await mkdir(dir, { recursive: true });
|
||||
} catch {
|
||||
/* parent likely already exists */
|
||||
}
|
||||
|
||||
const fileExists = await exists(path).catch(() => false);
|
||||
|
||||
@@ -51,7 +51,7 @@ function rowToMessage(row: Record<string, unknown>): MessageWithCipher {
|
||||
}
|
||||
|
||||
export function useConversationMessages({ conversationId, userId, deviceId }: Args): State & {
|
||||
send: (text: string, images?: File[]) => Promise<void>;
|
||||
send: (text: string, images?: File[], replyToId?: string | null) => Promise<void>;
|
||||
refresh: () => Promise<void>;
|
||||
} {
|
||||
const [state, setState] = useState<State>({ messages: [], loading: true, error: null });
|
||||
@@ -262,7 +262,7 @@ export function useConversationMessages({ conversationId, userId, deviceId }: Ar
|
||||
}, [conversationId, userId, deviceId, refresh, handleInsert, handleUpdate, handleDelete]);
|
||||
|
||||
const send = useCallback(
|
||||
async (text: string, images: File[] = []) => {
|
||||
async (text: string, images: File[] = [], replyToId: string | null = null) => {
|
||||
const trimmed = text.trim();
|
||||
if ((!trimmed && images.length === 0) || !conversationId || !userId || !deviceId) return;
|
||||
const priv = privateKeyRef.current;
|
||||
@@ -299,6 +299,7 @@ export function useConversationMessages({ conversationId, userId, deviceId }: Ar
|
||||
senderDeviceId: deviceId,
|
||||
senderPrivateKey: priv,
|
||||
...(handles.length > 0 ? { attachmentHandles: handles } : {}),
|
||||
...(replyToId ? { replyToId } : {}),
|
||||
});
|
||||
|
||||
// 3. Optimistic insert — we already have the plaintext in hand and the
|
||||
|
||||
Reference in New Issue
Block a user