199 lines
8.3 KiB
TypeScript
199 lines
8.3 KiB
TypeScript
import { lazy, Suspense, useEffect } from 'react';
|
|
import { HashRouter, Navigate, Outlet, Route, Routes, useParams } from 'react-router-dom';
|
|
|
|
import { AppShell } from './components/AppShell';
|
|
import { CrashToast } from './components/CrashToast';
|
|
import { ErrorBoundary } from './components/ErrorBoundary';
|
|
import { RemoteRevokedScreen } from './components/RemoteRevokedScreen';
|
|
import { SpinnerIcon } from './components/icons';
|
|
import { UpdateToast } from './components/UpdateToast';
|
|
import { RequireAdmin, RequireAuth, RequireDevice } from './components/guards';
|
|
import { AuthProvider } from './context/AuthContext';
|
|
import { CallProvider } from './context/CallContext';
|
|
import { ConversationsProvider } from './context/ConversationsContext';
|
|
import { FriendshipsProvider } from './context/FriendshipsContext';
|
|
import { ThemeProvider } from './context/ThemeContext';
|
|
import { hydrateDrafts } from './lib/composerDraftStore';
|
|
import { AuthPage } from './pages/AuthPage';
|
|
import { ChatsEmptyState, ChatsPage } from './pages/ChatsPage';
|
|
import { ConversationPage } from './pages/ConversationPage';
|
|
|
|
// Routes rarely visited on first render are pulled out of the initial bundle.
|
|
// AuthPage stays eager because it's the first screen unauthenticated users
|
|
// see; ChatsPage + ConversationPage stay eager because every authenticated
|
|
// session renders them immediately.
|
|
const AdminPage = lazy(() => import('./pages/AdminPage').then((m) => ({ default: m.AdminPage })));
|
|
const AuthCallbackPage = lazy(() =>
|
|
import('./pages/AuthCallbackPage').then((m) => ({ default: m.AuthCallbackPage })),
|
|
);
|
|
const DevicePage = lazy(() => import('./pages/DevicePage').then((m) => ({ default: m.DevicePage })));
|
|
const FriendsPage = lazy(() =>
|
|
import('./pages/FriendsPage').then((m) => ({ default: m.FriendsPage })),
|
|
);
|
|
const SettingsPage = lazy(() =>
|
|
import('./pages/SettingsPage').then((m) => ({ default: m.SettingsPage })),
|
|
);
|
|
const ChangelogPage = lazy(() =>
|
|
import('./pages/ChangelogPage').then((m) => ({ default: m.ChangelogPage })),
|
|
);
|
|
|
|
function RouteSuspense({ children }: { children: React.ReactNode }) {
|
|
return (
|
|
<Suspense
|
|
fallback={
|
|
<div className="flex min-h-full w-full items-center justify-center bg-surface-3">
|
|
<SpinnerIcon className="h-5 w-5 text-accent" />
|
|
</div>
|
|
}
|
|
>
|
|
{children}
|
|
</Suspense>
|
|
);
|
|
}
|
|
|
|
// 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 (
|
|
<ErrorBoundary scope={scope}>
|
|
<Outlet />
|
|
</ErrorBoundary>
|
|
);
|
|
}
|
|
|
|
// Forces a fresh `ConversationPage` instance per `:id` so React unmounts
|
|
// the previous conversation entirely on switch. Without this, the same
|
|
// component instance handles every conversation, which leaks state
|
|
// between chats (messages, scroll position, composer drafts) for one
|
|
// render frame and gives the "flicker" we're trying to remove.
|
|
// `useConversationMessages` re-hydrates from `messageMemoryCache` on the
|
|
// fresh mount so previously-visited chats still render instantly.
|
|
function ConversationRoute() {
|
|
const { id } = useParams<{ id: string }>();
|
|
return <ConversationPage key={id ?? '__no_id__'} />;
|
|
}
|
|
|
|
export function App() {
|
|
useEffect(() => {
|
|
void hydrateDrafts();
|
|
}, []);
|
|
|
|
return (
|
|
<ErrorBoundary scope="root">
|
|
<ThemeProvider>
|
|
<AuthProvider>
|
|
<FriendshipsProvider>
|
|
<ConversationsProvider>
|
|
<CallProvider>
|
|
<HashRouter
|
|
future={{
|
|
// Opt into v7 behaviour early so the upgrade is a no-op:
|
|
// - `v7_startTransition` wraps navigations in startTransition
|
|
// so Suspense / concurrent rendering deal with the new tree
|
|
// - `v7_relativeSplatPath` matches relative paths inside
|
|
// splat routes against the parent splat segment (not the
|
|
// full matched path). Our tree has no splat routes today
|
|
// but this kills the runtime warning and future-proofs.
|
|
v7_startTransition: true,
|
|
v7_relativeSplatPath: true,
|
|
}}
|
|
>
|
|
<Routes>
|
|
<Route element={<RouteBoundary scope="auth" />}>
|
|
<Route path="/auth" element={<AuthPage />} />
|
|
<Route
|
|
path="/auth/callback"
|
|
element={
|
|
<RouteSuspense>
|
|
<AuthCallbackPage />
|
|
</RouteSuspense>
|
|
}
|
|
/>
|
|
</Route>
|
|
<Route element={<RequireAuth />}>
|
|
<Route element={<RouteBoundary scope="device" />}>
|
|
<Route
|
|
path="/device"
|
|
element={
|
|
<RouteSuspense>
|
|
<DevicePage />
|
|
</RouteSuspense>
|
|
}
|
|
/>
|
|
</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">
|
|
<ConversationRoute />
|
|
</ErrorBoundary>
|
|
}
|
|
/>
|
|
</Route>
|
|
</Route>
|
|
<Route element={<RouteBoundary scope="friends" />}>
|
|
<Route
|
|
path="/friends"
|
|
element={
|
|
<RouteSuspense>
|
|
<FriendsPage />
|
|
</RouteSuspense>
|
|
}
|
|
/>
|
|
</Route>
|
|
<Route element={<RouteBoundary scope="settings" />}>
|
|
<Route
|
|
path="/settings"
|
|
element={
|
|
<RouteSuspense>
|
|
<SettingsPage />
|
|
</RouteSuspense>
|
|
}
|
|
/>
|
|
</Route>
|
|
<Route element={<RouteBoundary scope="changelog" />}>
|
|
<Route
|
|
path="/changelog"
|
|
element={
|
|
<RouteSuspense>
|
|
<ChangelogPage />
|
|
</RouteSuspense>
|
|
}
|
|
/>
|
|
</Route>
|
|
<Route element={<RequireAdmin />}>
|
|
<Route element={<RouteBoundary scope="admin" />}>
|
|
<Route
|
|
path="/admin"
|
|
element={
|
|
<RouteSuspense>
|
|
<AdminPage />
|
|
</RouteSuspense>
|
|
}
|
|
/>
|
|
</Route>
|
|
</Route>
|
|
</Route>
|
|
</Route>
|
|
</Route>
|
|
<Route path="*" element={<Navigate to="/chats" replace />} />
|
|
</Routes>
|
|
<UpdateToast />
|
|
<CrashToast />
|
|
<RemoteRevokedScreen />
|
|
</HashRouter>
|
|
</CallProvider>
|
|
</ConversationsProvider>
|
|
</FriendshipsProvider>
|
|
</AuthProvider>
|
|
</ThemeProvider>
|
|
</ErrorBoundary>
|
|
);
|
|
}
|