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:
+69
-37
@@ -1,6 +1,7 @@
|
||||
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
|
||||
import { BrowserRouter, Navigate, Outlet, Route, Routes } from 'react-router-dom';
|
||||
|
||||
import { AppShell } from './components/AppShell';
|
||||
import { ErrorBoundary } from './components/ErrorBoundary';
|
||||
import { UpdateToast } from './components/UpdateToast';
|
||||
import { RequireAdmin, RequireAuth, RequireDevice } from './components/guards';
|
||||
import { AuthProvider } from './context/AuthContext';
|
||||
@@ -17,42 +18,73 @@ import { DevicePage } from './pages/DevicePage';
|
||||
import { FriendsPage } from './pages/FriendsPage';
|
||||
import { SettingsPage } from './pages/SettingsPage';
|
||||
|
||||
export function App() {
|
||||
// Isolates each top-level route so a crash in one page doesn't take the whole
|
||||
// shell down. The boundary auto-retries via `ErrorBoundary`'s backoff schedule.
|
||||
function RouteBoundary({ scope }: { scope: string }) {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<AuthProvider>
|
||||
<FriendshipsProvider>
|
||||
<ConversationsProvider>
|
||||
<CallProvider>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/auth" element={<AuthPage />} />
|
||||
<Route path="/auth/callback" element={<AuthCallbackPage />} />
|
||||
<Route element={<RequireAuth />}>
|
||||
<Route path="/device" element={<DevicePage />} />
|
||||
<Route element={<RequireDevice />}>
|
||||
<Route element={<AppShell />}>
|
||||
<Route index element={<Navigate to="/chats" replace />} />
|
||||
<Route path="/chats" element={<ChatsPage />}>
|
||||
<Route index element={<ChatsEmptyState />} />
|
||||
<Route path=":id" element={<ConversationPage />} />
|
||||
</Route>
|
||||
<Route path="/friends" element={<FriendsPage />} />
|
||||
<Route path="/settings" element={<SettingsPage />} />
|
||||
<Route element={<RequireAdmin />}>
|
||||
<Route path="/admin" element={<AdminPage />} />
|
||||
</Route>
|
||||
</Route>
|
||||
</Route>
|
||||
</Route>
|
||||
<Route path="*" element={<Navigate to="/chats" replace />} />
|
||||
</Routes>
|
||||
<UpdateToast />
|
||||
</BrowserRouter>
|
||||
</CallProvider>
|
||||
</ConversationsProvider>
|
||||
</FriendshipsProvider>
|
||||
</AuthProvider>
|
||||
</ThemeProvider>
|
||||
<ErrorBoundary scope={scope}>
|
||||
<Outlet />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<ErrorBoundary scope="root">
|
||||
<ThemeProvider>
|
||||
<AuthProvider>
|
||||
<FriendshipsProvider>
|
||||
<ConversationsProvider>
|
||||
<CallProvider>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route element={<RouteBoundary scope="auth" />}>
|
||||
<Route path="/auth" element={<AuthPage />} />
|
||||
<Route path="/auth/callback" element={<AuthCallbackPage />} />
|
||||
</Route>
|
||||
<Route element={<RequireAuth />}>
|
||||
<Route element={<RouteBoundary scope="device" />}>
|
||||
<Route path="/device" element={<DevicePage />} />
|
||||
</Route>
|
||||
<Route element={<RequireDevice />}>
|
||||
<Route element={<AppShell />}>
|
||||
<Route index element={<Navigate to="/chats" replace />} />
|
||||
<Route element={<RouteBoundary scope="chats" />}>
|
||||
<Route path="/chats" element={<ChatsPage />}>
|
||||
<Route index element={<ChatsEmptyState />} />
|
||||
<Route
|
||||
path=":id"
|
||||
element={
|
||||
<ErrorBoundary scope="conversation">
|
||||
<ConversationPage />
|
||||
</ErrorBoundary>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
</Route>
|
||||
<Route element={<RouteBoundary scope="friends" />}>
|
||||
<Route path="/friends" element={<FriendsPage />} />
|
||||
</Route>
|
||||
<Route element={<RouteBoundary scope="settings" />}>
|
||||
<Route path="/settings" element={<SettingsPage />} />
|
||||
</Route>
|
||||
<Route element={<RequireAdmin />}>
|
||||
<Route element={<RouteBoundary scope="admin" />}>
|
||||
<Route path="/admin" element={<AdminPage />} />
|
||||
</Route>
|
||||
</Route>
|
||||
</Route>
|
||||
</Route>
|
||||
</Route>
|
||||
<Route path="*" element={<Navigate to="/chats" replace />} />
|
||||
</Routes>
|
||||
<UpdateToast />
|
||||
</BrowserRouter>
|
||||
</CallProvider>
|
||||
</ConversationsProvider>
|
||||
</FriendshipsProvider>
|
||||
</AuthProvider>
|
||||
</ThemeProvider>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user