diff --git a/apps/web/docs/concepts-app-routing.md b/apps/web/docs/concepts-app-routing.md new file mode 100644 index 0000000..bb753d2 --- /dev/null +++ b/apps/web/docs/concepts-app-routing.md @@ -0,0 +1,167 @@ +# App Routing & Conversation UI Architecture + +Source: [`src/app/`](../src/app/), in particular +[`layout.tsx`](../src/app/layout.tsx), [`page.tsx`](../src/app/page.tsx), +[`app/layout.tsx`](../src/app/app/layout.tsx) and +[`app/conversations/[id]/page.tsx`](../src/app/app/conversations/%5Bid%5D/page.tsx). + +This describes the routing structure after the route-consolidation work (commit `f6a40ef`, +"retire duplicate conversation route and legacy chat demo"): there is now a single conversation +route, `/app/conversations/[id]`. For auth state see +[`concepts-auth-device-lifecycle.md`](concepts-auth-device-lifecycle.md); for the message +pipeline behind the conversation view see +[`concepts-message-pipeline.md`](concepts-message-pipeline.md) and +[`concepts-e2ee-architecture.md`](concepts-e2ee-architecture.md). + +## Next.js App Router in one paragraph + +The web app uses the App Router (`src/app/`). Each folder containing a `page.tsx` is a URL +segment; a `layout.tsx` wraps every page beneath it and persists across navigation between those +pages; a folder named `[id]` is a dynamic segment read with `useParams()`. Nearly every page here +is a client component (`'use client'`) because it depends on wallet, auth token or socket state. + +## Route tree + +``` +src/app/ +├── layout.tsx Root layout (all routes) +├── page.tsx / Landing page +├── globals.css, favicon.ico +├── providers.tsx (not mounted — see "Leftovers") +├── routes/conversations.js (not a route — see "Leftovers") +└── app/ /app/* Authenticated shell + ├── layout.tsx Sidebar, wallet button, push prompt + ├── page.tsx /app Demo messages view + ├── messages/page.tsx /app/messages Re-exports /app page + ├── conversations/[id]/page.tsx /app/conversations/:id Real conversation view + ├── devices/page.tsx /app/devices + ├── profile/page.tsx /app/profile + ├── proposals/page.tsx /app/proposals + ├── search/page.tsx /app/search + └── treasury/page.tsx /app/treasury +``` + +### Root layout and landing page + +- [`app/layout.tsx`](../src/app/layout.tsx) sets metadata and fonts and nests the global + providers, outermost first: `WalletContext` → `AuthProvider` → `ToastProvider`. Every route, + including `/app/*`, can therefore call `useWallet()`, `useAuth()` and the toast API. +- [`app/page.tsx`](../src/app/page.tsx) (`/`) is the marketing page, wrapped in + ``: a visitor who already has a token is redirected to + `/app`. + +### The `/app` shell + +[`app/app/layout.tsx`](../src/app/app/layout.tsx) is the shell shared by every route below. It +renders a fixed left sidebar (nav links for Messages, Treasury, Proposals and Devices, plus a +Connect/Disconnect wallet button using `useWallet()`) and puts the active page in `
`. It +also mounts `PushPermissionPrompt` and listens for `sw:sync` messages from the service worker +(see [`concepts-service-worker.md`](concepts-service-worker.md)): a notification click navigates +to `/app/conversations/:id`, or to `/app/messages` if no conversation id was sent. + +The shell does **not** wrap its children in `ProtectedRoute mode="authenticated"`. Pages that +need a token handle a missing one themselves (for instance the conversation page shows "Sign in +to view this conversation."). + +### Top-level routes under `app/app/` + +| Route | Purpose | +|---|---| +| `/app` | Default landing inside the shell; currently a **demo** chat view with hard-coded messages and a local search panel, not wired to the backend. The sidebar highlights "Messages" for this path. | +| `/app/messages` | Alias of `/app` (`export { default } from '../page'`), and the sidebar's "Messages" target. | +| `/app/conversations/[id]` | The real, end-to-end encrypted conversation view for one direct or group conversation. Detailed below. | +| `/app/devices` | Lists the signed-in user's registered devices and lets them be managed/revoked, via `apiFetch` and modals. | +| `/app/profile` | Shows and edits the signed-in user's profile and linked wallets, via `apiFetch`. | +| `/app/proposals` | Community funding proposals list with voting UI; currently driven by local sample state. | +| `/app/search` | Full-page local message search (`MessageSearch` over the on-device index) with an indexed-message count. | +| `/app/treasury` | Group treasury view: fetches withdraw proposals, shows `ProposalCard`s, and opens `ProposeWithdrawalModal`; updates live through `useSocket`. See [`components-treasury-wallet.md`](components-treasury-wallet.md). | + +`/app/profile` is routed but has no sidebar entry; it is reached by direct link. + +## Conversation view (`/app/conversations/[id]`) + +`ConversationPage` is a single client component (~900 lines) that owns data loading, send +handlers, and rendering. It does **not** delegate to the components in `components/messaging/` +(`MessageThread`, `InboundMessageRow`) or `components/chat/` (`ConversationHeader`, +`NewConversationModal`), nor to `ConversationListSidebar`; none of those is imported by any +route. Orient yourself in the page file by these parts. + +### Hooks and libraries it pulls together + +| Concern | What it uses | +|---|---| +| Route param | `useParams<{ id }>()` gives the conversation id. | +| Auth | `useAuth()` supplies `token` and `loading`; no token → error state. | +| Realtime | `useSocket(token)`; on mount emits `join_room` via `emitSocketEnvelope`. | +| Incoming messages | `useInboundPipeline({ socket, token, conversationId })` receives, decrypts and orders envelopes, returning `messages` with `decrypted` / `unavailable` status. | +| Sending text | `sendEncryptedMessage` from `lib/crypto`, then `socket.emit('send_message')`. | +| Sending files | `fetchConversationDevices`, `generateEncryptedThumbnail`, `sendEncryptedFile`, then `socket.emit('send_file_message')`; download via `downloadAndDecryptFile`. | +| Key-change reset | `clearSessionKeys()` and `sessionStore.clear()` when a key-change socket event arrives. | +| Presentation | `Avatar`, `EmptyState`, `TransferCard`, `EncryptedThumbnail`, `UnavailableMessagePlaceholder`. | + +### Data flow + +1. **Initial load.** An effect (waits for auth) fetches in parallel `GET /users/me`, + `GET /conversations/:id` and `GET /conversations/:id/messages` into `currentUser`, + `conversation` and `messages`. +2. **Merge.** The `allMessages` memo overlays pipeline results on the REST history by message + id: a decrypted inbound entry supplies `content` (and a parsed `filePayload` for + `file`/`image`/`video` types), an `unavailable` one marks the row undecryptable, and inbound + messages not yet in REST history are appended. The result is sorted by `createdAt`. +3. **Derive.** `contacts` (members other than the current user), `title` and + `hasChangedSafetyNumber` are computed from state. + +### Layout and the message list + +The component renders a flex row of two panes: + +- **Main column (`
`)**: a header (avatar, title, member count or primary wallet, and the + safety-numbers toggle button), an optional amber "Safety number changed" banner, the scrolling + **message list**, and the **input bar**. +- **Safety panel (`