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:
@@ -54,30 +54,34 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
(async () => {
|
||||
const { data: sessionRes } = await supabase.auth.getSession();
|
||||
if (cancelled) return;
|
||||
if (sessionRes.session) {
|
||||
const { error } = await supabase.auth.getUser();
|
||||
if (cancelled) return;
|
||||
if (error) {
|
||||
const status = (error as { status?: number }).status;
|
||||
if (status === 401 || status === 403) {
|
||||
// Token genuinely invalid — wipe.
|
||||
await supabase.auth.signOut({ scope: 'local' }).catch(() => {
|
||||
/* ignore */
|
||||
});
|
||||
setSession(null);
|
||||
} else {
|
||||
// Network / server unreachable — keep cached session, let reads
|
||||
// fail gracefully and recover when the stack is back.
|
||||
console.warn('auth.getUser failed, keeping cached session:', error);
|
||||
setSession(sessionRes.session);
|
||||
}
|
||||
} else {
|
||||
setSession(sessionRes.session);
|
||||
}
|
||||
} else {
|
||||
setSession(null);
|
||||
}
|
||||
// Flip `ready` immediately on cached session read so the UI unblocks even
|
||||
// if the network is slow/down. Validate the token in the background and
|
||||
// only wipe on an unambiguous 401/403 — a stalled getUser (Tauri WebView
|
||||
// with no network, server unreachable) must not keep the app on the
|
||||
// loading spinner forever.
|
||||
setSession(sessionRes.session ?? null);
|
||||
setReady(true);
|
||||
|
||||
if (sessionRes.session) {
|
||||
supabase.auth
|
||||
.getUser()
|
||||
.then(({ error }) => {
|
||||
if (cancelled || !error) return;
|
||||
const status = (error as { status?: number }).status;
|
||||
if (status === 401 || status === 403) {
|
||||
void supabase.auth.signOut({ scope: 'local' }).catch(() => {
|
||||
/* ignore */
|
||||
});
|
||||
setSession(null);
|
||||
} else {
|
||||
// Network / server unreachable — keep cached session.
|
||||
console.warn('auth.getUser failed, keeping cached session:', error);
|
||||
}
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
console.warn('auth.getUser rejected, keeping cached session:', err);
|
||||
});
|
||||
}
|
||||
})();
|
||||
const { data: sub } = supabase.auth.onAuthStateChange((_event, s) => {
|
||||
setSession(s);
|
||||
|
||||
Reference in New Issue
Block a user