Files
im_webapp/web/src/App.tsx
T

142 lines
7.1 KiB
TypeScript

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Navigate, createBrowserRouter, RouterProvider, useRouteError } from 'react-router-dom';
import { AppShell } from '@/app/AppShell';
import { ConfirmDialogRoot } from '@/app/ConfirmDialogRoot';
import { GlobalToastRoot } from '@/app/GlobalToastRoot';
import { Icon } from '@/app/Icon';
import { KickedNoticeRoot } from '@/app/KickedNoticeRoot';
import { RequireAuth } from '@/app/RequireAuth';
import { SecurityChallengeModal } from '@/app/SecurityChallengeModal';
import { EnterpriseRegisterPage } from '@/features/auth/EnterpriseRegisterPage';
import { LoginPage } from '@/features/auth/LoginPage';
import { RegisterPage } from '@/features/auth/RegisterPage';
import { ResetPasswordPage } from '@/features/auth/ResetPasswordPage';
import { TenantEntryPage } from '@/features/auth/TenantEntryPage';
import { TenantFeedbackPage } from '@/features/auth/TenantFeedbackPage';
import { CallHistoryView } from '@/features/calls/CallHistoryView';
import { BlocklistView } from '@/features/contacts/BlocklistView';
import { ContactsLayout } from '@/features/contacts/ContactsLayout';
import { EmptyContactsMain } from '@/features/contacts/EmptyContactsMain';
import { FriendProfileView } from '@/features/contacts/FriendProfileView';
import { GroupListView } from '@/features/contacts/GroupListView';
import { NewFriendsView } from '@/features/contacts/NewFriendsView';
import { FavoritesLayout } from '@/features/favorites/FavoritesLayout';
import { ConversationView } from '@/features/messages/ConversationView';
import { EmptyMain } from '@/features/messages/EmptyMain';
import { MessagesLayout } from '@/features/messages/MessagesLayout';
import { GeneralSettingsPage } from '@/features/settings/GeneralSettingsPage';
import { NotificationsSettingsPage } from '@/features/settings/NotificationsSettingsPage';
import { PrivacySettingsPage } from '@/features/settings/PrivacySettingsPage';
import { ProfileSettingsPage } from '@/features/settings/ProfileSettingsPage';
import { SecuritySettingsPage } from '@/features/settings/SecuritySettingsPage';
import { SettingsHome } from '@/features/settings/SettingsHome';
import { SettingsLayout } from '@/features/settings/SettingsLayout';
import { SystemMessagesView } from '@/features/system/SystemMessagesView';
const queryClient = new QueryClient();
/**
* Without this, an uncaught render-time exception ANYWHERE under `<AppShell>` (any page, any one
* bad field in server data) falls through to react-router's own built-in default error boundary --
* real, not hypothetical (confirmed live: `node_modules/react-router` DOES implicitly wrap the
* outermost matched route the way this codebase never configured one to). That default shows
* "Unexpected Application Error!" plus the raw error message/stack to the end user and replaces the
* WHOLE logged-in shell, sidebar included, with no way back except a full page reload. Attached to
* AppShell (not each leaf route) so it covers every authenticated page with one component, and low
* enough that `RequireAuth`/login/tenant-entry are never affected by a bug in, say, 收藏.
*/
function RouteErrorBoundary() {
const error = useRouteError();
if (import.meta.env.DEV) console.error(error);
return (
<div className="flex h-dvh w-full flex-col items-center justify-center gap-3 bg-surface-page px-6 text-center">
<Icon name="error_outline" className="text-[40px] text-text-tertiary-3" />
<p className="text-sm text-text-secondary">页面出现异常,请刷新重试</p>
<button type="button" onClick={() => window.location.reload()} className="rounded-lg bg-accent px-4 py-2 text-sm text-white">
刷新页面
</button>
</div>
);
}
const router = createBrowserRouter([
{ path: 'tenant', element: <TenantEntryPage /> },
{ path: 'login', element: <LoginPage /> },
{ path: 'register', element: <RegisterPage /> },
{ path: 'reset-password', element: <ResetPasswordPage /> },
{ path: 'enterprise-register', element: <EnterpriseRegisterPage /> },
{ path: 'feedback', element: <TenantFeedbackPage /> },
{
element: <RequireAuth />,
children: [
{
element: <AppShell />,
errorElement: <RouteErrorBoundary />,
children: [
{ index: true, element: <Navigate to="/messages" replace /> },
{
path: 'messages',
element: <MessagesLayout />,
children: [
{ index: true, element: <EmptyMain /> },
{ path: 'system', element: <SystemMessagesView /> },
{ path: ':topicId', element: <ConversationView /> },
],
},
{
path: 'contacts',
element: <ContactsLayout />,
children: [
{ index: true, element: <EmptyContactsMain /> },
{ path: 'requests', element: <NewFriendsView /> },
{ path: 'groups', element: <GroupListView /> },
{ path: 'blocklist', element: <BlocklistView /> },
{ path: ':userId', element: <FriendProfileView /> },
],
},
{ path: 'favorites', element: <FavoritesLayout /> },
// README §9.2's dedicated file-assistant browser (categories, storage quota, upload
// progress) has NO backing REST API at all (confirmed via a research pass -- the design
// spec's own state-management notes admit reuse of existing APIs, i.e. reading the `slf`
// topic's messages, same as any other conversation) -- redirecting into the real
// conversation on that topic is honest; a fake quota card with invented numbers isn't.
{ path: 'files', element: <Navigate to="/messages/slf" replace /> },
{ path: 'calls', element: <CallHistoryView /> },
{
path: 'settings',
element: <SettingsLayout />,
children: [
{ index: true, element: <SettingsHome /> },
{ path: 'profile', element: <ProfileSettingsPage /> },
{ path: 'security', element: <SecuritySettingsPage /> },
{ path: 'notifications', element: <NotificationsSettingsPage /> },
{ path: 'privacy', element: <PrivacySettingsPage /> },
{ path: 'general', element: <GeneralSettingsPage /> },
],
},
],
},
],
},
// A stale bookmark/link to a route renamed during this redesign should land somewhere the app
// itself resolves (tenant/login/messages, per RequireAuth's own gating), not React Router's
// generic unstyled default error boundary.
{ path: '*', element: <Navigate to="/" replace /> },
]);
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
{/* Mounted above the router (not inside AppShell) -- window.confirm()/window.alert()
replacements need to work on unauthenticated pages too (e.g. TenantEntryPage's "switch
tenant" confirmation), not just the logged-in app tree. */}
<ConfirmDialogRoot />
<GlobalToastRoot />
<KickedNoticeRoot />
<SecurityChallengeModal />
</QueryClientProvider>
);
}