From 0e87787f9531e366d4a9926ad08c01cded79c0ee Mon Sep 17 00:00:00 2001 From: heaven Date: Sat, 30 May 2026 22:43:19 +0300 Subject: [PATCH] docs(ai): actualize architecture.md against current vojo/dev code after the Channels, Bots, threads and settings-route redesign --- docs/ai/architecture.md | 554 +++++++++++++++++++++++----------------- 1 file changed, 313 insertions(+), 241 deletions(-) diff --git a/docs/ai/architecture.md b/docs/ai/architecture.md index 78ee29e3..b308f9b3 100644 --- a/docs/ai/architecture.md +++ b/docs/ai/architecture.md @@ -1,189 +1,300 @@ # Architecture +> Last actualized 2026-05-30 against the code on `vojo/dev` (HEAD `a84c5341`). The +> Dawn redesign that this doc tracks has progressed well past the "P3c" baseline +> the older revision described: **Channels**, **Bots**, **threads**, a first-class +> **/settings/** route, the **mobile swipe pager**, and the **share-target** flow +> have all shipped. Where a plan doc (`docs/plans/*.md`) says something was +> "deferred", check the code first — much of it landed. + ## Quick Start ```bash -npm start # dev server on :8080 +npm start # dev server on :8080 (strictPort, host:true) npm run build # production build → dist/ -npm run lint # eslint + prettier +npm run lint # check:eslint (eslint --max-warnings 0 src) + check:prettier npm run typecheck # tsc --noEmit ``` Build: **Vite 5.4** with vanilla-extract, WASM, PWA plugins. -> **Note:** `.husky/pre-commit` is enabled and runs `tsc --noEmit` + `lint-staged` (which calls `eslint --max-warnings 0` on staged JS/TS files). Both gates are zero: `npm run typecheck` and `npm run check:eslint` are green (0 errors, 0 warnings). Custom Matrix event-types (`AccountDataEvent.Vojo*`, `PoniesRoomEmotes`, `m.bridge`, `m.call.member` etc.) live in [`src/types/matrix/sdkAugmentation.d.ts`](../../src/types/matrix/sdkAugmentation.d.ts) — add new custom types there to keep `mx.getAccountData` / `mx.getStateEvent` calls type-safe. +> **Note:** `.husky/pre-commit` runs `tsc --noEmit` + `lint-staged` (which calls `eslint --max-warnings 0` on staged JS/TS files — NOT the full `npm run lint`). Both gates are zero: `npm run typecheck` and `npm run check:eslint` are green. Custom Matrix event-types live in [`src/types/matrix/sdkAugmentation.d.ts`](../../src/types/matrix/sdkAugmentation.d.ts) — it augments `AccountDataEvents` with `in.vojo.spaces`, `io.element.recent_emoji`, `im.ponies.user_emotes`, `im.ponies.emote_rooms` and `StateEvents` with `im.ponies.room_emotes`, `in.vojo.room.power_level_tags`, `m.bridge`, `uk.half-shot.bridge` (all typed `unknown`; callsites use `.getContent()`). Add new custom event-type strings there to keep `mx.getAccountData` / `mx.getStateEvent` type-safe. ## Source Layout ``` src/ -├── index.tsx # Entry point -├── colors.css.ts # Vojo dark + light themes via createTheme(color, …) — both palettes are Vojo-owned, folds defaults are not used -├── config.css.ts # fontWeight overrides +├── index.tsx # Entry point (calls pushSessionToSW, capacitor/electron link handlers) +├── colors.css.ts # darkTheme (Dawn) + lightTheme (Vojo) via createTheme(color, …) — both Vojo-owned +├── config.css.ts # onDarkFontWeight / onLightFontWeight overrides +├── sw.ts # hand-written service worker (auth media, push, language bridge) +├── sw-session.ts # pushSessionToSW(): re-posts setSession on controllerchange ├── client/ │ ├── initMatrix.ts # Matrix SDK init (createClient, startClient, logout) │ └── secretStorageKeys.js # Crypto callbacks -├── types/matrix/ # Matrix protocol types (room.ts, accountData.ts, common.ts) +├── types/matrix/ # Matrix protocol types + sdkAugmentation.d.ts └── app/ - ├── i18n.ts # i18next config + ├── i18n.ts # i18next config (single-file locales) ├── pages/ - │ ├── App.tsx # Root component (providers, config loader) - │ ├── Router.tsx # createBrowserRouter / createHashRouter, all routes - │ └── MobileFriendly.tsx # MobileFriendlyClientNav + MobileFriendlyPageNav (responsive split) + │ ├── App.tsx # Root component (ScreenSizeProvider, query client, config loader) + │ ├── Router.tsx # createBrowserRouter / createHashRouter, all routes (~497 LOC) + │ ├── paths.ts # canonical path constants (DIRECT_PATH, CHANNELS_*, BOTS_*, …) + │ ├── pathUtils.ts # getXxxPath builders + │ ├── ThemeManager.tsx # UnAuthRouteThemeManager + AuthRouteThemeManager (body-class swap) + │ ├── HorseshoeContainer.tsx # app-shell wrapper + bottom call rail + share strip + │ ├── CallStatusRenderer.tsx / IncomingCallStripRenderer.tsx # global call surfaces + │ ├── MobileFriendly.tsx # MobileFriendlyPageNav (live) + MobileFriendlyClientNav (dead) + │ └── client/ # per-tab pages (direct, channels, bots, explore, space, home, create, settings, sidebar) ├── features/ # Feature modules - ├── components/ # Shared components - ├── hooks/ # ~117 custom hooks - ├── state/ # Jotai atoms - ├── plugins/ # Content plugins - ├── utils/ # Utilities - └── styles/ # Vanilla-extract global styles + ├── components/ # Shared components (~70 dirs) + ├── hooks/ # ~132 custom hooks + ├── state/ # Jotai atoms (~35 top-level files + room/, room-list/, hooks/, utils/) + ├── plugins/ # Content plugins (call widget driver, emoji-data, color, react-prism) + ├── utils/ # Utilities (room.ts, matrix.ts, time.ts, capacitor.ts, electron.ts) + └── styles/ # Vanilla-extract global styles (global.css.ts, horseshoe.ts) +electron/ # Electron desktop wrapper (see electron.md) +apps/widget-{telegram,discord,whatsapp}/ # Preact bot-widget apps (see overview.md) ``` ## Pages & Routing (`src/app/pages/`) -Router in `Router.tsx`. Each top-level tab (`/direct/`, `/space/...`, `/explore/`, `/inbox/`) is wrapped in `PageRoot` with a `nav` prop (the tab's PageNav — e.g. `Direct`) and an `` for the active room/sub-route. +Router in `Router.tsx::createRouter(clientConfig, screenSize)`. All authed routes hang +off one big `` whose `element` is the provider stack: -- **Auth** (`auth/login/`, `auth/register/`, `auth/reset-password/`) — NOTE: bistable layout fragility, see `bugs.md`. Recent fixes: c466848, e6623b2, cf5ee9a, 9b41cbb. **Don't change `body`/`#root` background CSS-vars** — auth uses computed sizes for safe-area painting. -- **Client** (`client/`) — main layout after login (wrapped by `ClientLayout` = nav + content) - - `home/` — **Redirect-only shim after P3c.** `HomeRouteRoomProvider` redirects `/home/{roomId}/` to `/direct/{roomId}/` so cold-start push deep links and pre-P3c bookmarks resolve. No Home page or PageNav exists. The `/home/_create`, `/home/_join`, `/home/_search` routes redirect to `/direct/`. - - `direct/` — Universal room list (path: `/direct/`). After P3c contains every joined «orphan» non-space room (1:1 DMs, group DMs, group rooms, bridged chats, anything that used to live in `/home/`) plus every `m.direct`-tagged non-space room — implementation-wise `useOrphanRooms ∪ useDirects`, see [`useDirectRooms.ts`](../../src/app/pages/client/direct/useDirectRooms.ts) for the full union semantics. **Non-`m.direct` rooms that are space children stay only in the parent space tab** — they are not duplicated in `/direct/`. See `dm_1x1_redesign.md` §6.8. - - `space/` — Space view (path: `/:spaceIdOrAlias/`). Spaces (= future Channels) keep their own tab and child-room route; they render Stream-style timelines too. - - `explore/` — Public rooms (path: `/explore/`) - - `inbox/` — Notifications, invites (path: `/inbox/`) - - `create/` — New room/space (path: `/create/`) - - `sidebar/` — Tab components (`DirectTab`, `SpaceTabs`, `ExploreTab`, `CreateTab`, `SearchTab`, `UnverifiedTab`, `InboxTab`, `SettingsTab`). The legacy `HomeTab` was removed in P3c. - - `WelcomePage.tsx` — Empty state (vojo SVG + version). **Mobile is intentionally null** at the index route (`{mobile ? null : } />}`) — by design, not a bug. - - `SidebarNav.tsx` — global 66px icon-rail (DirectTab, SpaceTabs, …, sticky bottom: SearchTab, UnverifiedTab, InboxTab, SettingsTab). Earmarked for removal in a follow-up `sidebar_cleanup` plan once the new Direct/Channels surfaces are self-sufficient. - - `SyncStatus.tsx`, `SpecVersions.tsx` — Connection status +``` +AuthRouteThemeManager → ClientRoot → ClientInitStorageAtom → +ClientRoomsNotificationPreferences → ClientBindAtoms → ClientNonUIFeatures → +CallEmbedProvider → HorseshoeContainer → ClientLayout(nav={null}) → +``` -### Universal Stream routing + DM classification (post-P3c) +**The global 66px `SidebarNav` rail is no longer mounted** — `ClientLayout` gets `nav={null}` (Router.tsx:249, with a Russian comment explaining its 5 buttons will be redistributed in a `sidebar_cleanup` pass). `SidebarNav.tsx` + `pages/client/sidebar/*` survive as **dead code**. + +Each visible top-level tab is still wrapped in `PageRoot` with a `nav` prop (the tab's PageNav) and an `` for the active room/sub-route. + +### Top-level tabs / routes + +| Route | Path const | What it is | +|---|---|---| +| `direct/` | `DIRECT_PATH = /direct/` | Universal room list — every joined "orphan" non-space room (1:1 DMs, group DMs, group rooms, bridged chats) ∪ every `m.direct`-tagged non-space room. `useOrphanRooms ∪ useDirects`. Non-`m.direct` space children stay only in the parent workspace. | +| `channels/` | `CHANNELS_PATH = /channels/` | **NEW.** Mattermost-style surface presenting **Spaces as "workspaces"**. Active workspace selection + room list + thread routing. | +| `bots/` | `BOTS_PATH = /bots/` | **NEW.** Bridge-bot catalog + per-bot widget/chat host. | +| `explore/` | `EXPLORE_PATH = /explore/` | Public rooms (featured + per-server). | +| `:spaceIdOrAlias/` | `SPACE_PATH = /:spaceIdOrAlias/` | **Legacy** space tab — still fully live (lobby + child rooms). Coexists with `/channels/`; both render the Channel timeline. | +| `create/` | `CREATE_PATH = /create` (no trailing slash) | New room/space. | +| `settings/` | `SETTINGS_PATH = /settings/` | **NEW first-class route.** Reuses the `/direct/` shell (DM list as left nav) with `SettingsScreen` in the right pane. `?page=` deep-links a sub-screen. On mobile it redirects to `/direct/` and opens `MobileSettingsHorseshoe` via `settingsSheetAtom`. Replaces the old `Modal500` settings dialog. | +| `home/` | `HOME_PATH = /home/` | **Redirect-only shim.** `HomeRouteRoomProvider` redirects `/home/{roomId}/` → `/direct/{roomId}/`, OR → `/channels/{space}/{roomId}/` when the room has orphan-space parents. `/home/create` → `/direct/create/`, `/home/{join,search}` → `/direct/`. Keeps cold-start push deep links + old bookmarks resolving. No Home page / HomeTab. | +| `u/:userIdOrLocalPart` | `USER_LINK_PATH` | `UserLinkRedirect` normalizes `vojo.chat/u/` → `/direct/create?userId=`. | +| `inbox/*` | — | **GONE.** Only a literal `'/inbox/*'` → `Navigate to /direct/` redirect remains (Router.tsx:485). No `INBOX_PATH`, no InboxTab. (The `Inbox` *i18n namespace* still exists for notification-card previews.) | + +`pages/client/sidebar/` exports only `DirectTab, SpaceTabs, ExploreTab, SettingsTab, UnverifiedTab, SearchTab` (CreateTab, InboxTab, HomeTab all removed). `WelcomePage.tsx` is the desktop empty state; at index routes it is intentionally `mobile ? null` by design. + +### Channels tab (`pages/client/channels/`) + +- `Channels.tsx` exports **two** components: `ChannelsRootNav` (the `/channels/` index nav — `StreamHeader` segment switcher + `ChannelsLanding` empty-state, Plus = create space) and `Channels` (the workspace listing — `ChannelsWorkspaceHorseshoe` > `StreamHeader pinKey="channels"` > `ChannelsList`, `WorkspaceFooter`, Plus = create channel in active space; writes `activeChannelsSpaceAtom`). +- `ChannelsLanding.tsx` resolves the active space (URL > localStorage via `useActiveSpace` > first joined orphan) and `Navigate`-redirects to `/channels/:space/`. +- `ChannelPickPlaceholder.tsx` is the desktop center-pane stub when no room is selected. +- `WorkspaceSwitcherSheet.tsx` / `WorkspaceFooter.tsx` / `SpaceAvatar.tsx` build the workspace switcher. +- Rooms under `/channels/` are wrapped in `ChannelsModeProvider value={true}` and rendered through `SpaceRouteRoomProvider` (shared with the legacy space tree). The `/channels/` route is declared **before** the `/:spaceIdOrAlias/` catch-all so the prefix isn't swallowed. + +### Bots tab (`pages/client/bots/`) + +- `Bots.tsx` (listing) renders `StreamHeader pinKey="bots"` + `BotCard` rows from `useBotPresets()`. Catalog-only, no Matrix listeners. +- `/bots/:botId` → `BotExperienceHost.tsx` (lazy): resolves the preset, runs `useBotRoom(preset)` state machine (`none / self-invite / bot-invite / bot-kicked / unsafe-membership / ready`) → renders `BotNotConnected / BotInvitePending / BotKicked / BotUnsafeRoom / BotStatePage`, or on `ready` wraps `BotRoomProvider` → `BotExperienceRoute` which branches on `botShowChatAtomFamily(roomId)`: chat → ``, else `` (the widget). See `features/bots/` for the host internals. + +### Mobile responsive nav + +- `MobileFriendlyPageNav(path)` (LIVE) — on Mobile renders the per-tab PageNav only when `useMatch(path, {end:true})` matches exactly; otherwise null (navigating into a room hides the list). +- `MobileFriendlyClientNav` is **dead code** (never invoked since `nav={null}`). +- `components/mobile-tabs-pager/` drives the mobile listing nav: `MobileTabsLayout` (a no-path layout route) renders `MobileTabsPager` only on **mobile + native + a listing-root URL** (`/direct/`, `/channels/`, `/channels/:space/`, `/bots/`); everywhere else it falls through to ``. `MobileTabsPager` mounts Direct + (Channels|ChannelsRootNav) + Bots panes once and slides between them via CSS transform + swipe gesture, navigating with `replace`. It's intentionally **not** exported (only `MobileTabsLayout` is). Gesture is disabled while `settingsSheetAtom` / `channelsWorkspaceSheetAtom` are open. State: `activeChannelsSpace.ts`, `mobilePagerHeader.ts`, `settingsSheet.ts`, `channelsWorkspaceSheet.ts`. + +### Lazy vs eager loading (load-bearing perf invariant) + +**Eager** (top-level imports, NOT `React.lazy`): `Bots`, `Channels` + `ChannelsRootNav`, `Direct`/`DirectCreate`/`DirectRouteRoomProvider`, `Space`/`SpaceSearch`/`RouteSpaceProvider`/`SpaceRouteRoomProvider`, `HomeRouteRoomProvider`, `ChannelPickPlaceholder`, `WelcomePage`, `SettingsScreen`. +**Lazy** (`React.lazy` + `routeSuspense()`): `Room`, `Lobby`, `Explore`, `FeaturedRooms`, `PublicRooms`, `BotExperienceHost`, `Create`. + +The Channels/Bots **listing** tabs must stay eager — lazy-splitting them reintroduced a web tab-switch flicker (a `grow="Yes"` Suspense fallback in the fixed-width nav slot reflowed the content column). See `bugs.md`. `BotExperienceHost` stays lazy and must be imported from the concrete file `./client/bots/BotExperienceHost`, not the barrel. + +### Universal Stream routing + DM classification ``` /direct/:roomIdOrAlias → PageRoot (nav=Direct, outlet=…) - → DirectRouteRoomProvider (sets IsOneOnOneProvider=room.getInvitedAndJoinedMemberCount() === 2) - → Room.tsx (RoomViewHeader + RoomView, screen-size branching for MembersDrawer) - → RoomTimeline + RoomViewTyping + RoomInput + → DirectRouteRoomProvider (ResolvedRoomProvider sets IsOneOnOneProvider reactively) + → Room.tsx (RoomViewHeader + RoomView, screen-size branching for side panels) + → RoomTimeline + RoomTimelineTyping + RoomInput ``` -After P3c the timeline picks between two layouts via a single **member-count** check, mirroring Element-Web's tier-2 pattern (`room.getInvitedAndJoinedMemberCount() === 2`). There is no DM-vs-non-DM render gate — the classification is purely participant-count + channels-route: +The timeline picks a layout via a single **member-count** check (Element-Web's tier-2 pattern). The authoritative decision lives in `RoomTimeline.tsx`: -- 1:1 rooms (member-count = 2) get peer-style header chrome (peer avatar fallback in `useRoomAvatar(room, isOneOnOne)`), the `DmCallButton`, the **Stream** timeline layout (rail + dot + bubble), and unconditionally hide membership/nick/avatar syslines. -- Group rooms (member-count > 2) get the room-style header (no peer fallback), no `DmCallButton`, the **Channel** timeline layout (avatar + in-bubble header + bubble — same silhouette as channels), and respect the `hideMembershipEvents` / `hideNickAvatarEvents` user settings for syslines. -- Rooms under `/channels/` always get the Channel layout regardless of member count — `channelsMode` short-circuits the 1:1 check and additionally enables channels-only filtering (thread surfacing, RTC/edit hiding). See `RoomTimeline.tsx::channelStyleLayout`. -- Bridged Telegram puppet rooms automatically classify correctly because the gate is server-side authoritative. +```ts +const isOneOnOne = useIsOneOnOne(); // member-count === 2 (from IsOneOnOneProvider) +const channelsMode = useChannelsMode(); // true for any room under /channels/ +const channelStyleLayout = channelsMode || !isOneOnOne; +const messageLayout: 'stream' | 'channel' = channelStyleLayout ? 'channel' : 'stream'; +``` -`useAutoDirectSync` (commit 84eeac9) still round-trips `m.direct` on join — **interop only**, so other Matrix clients (Element, FluffyChat) still categorize the same room as a DM. Vojo no longer reads `m.direct` for UI classification; the `mDirectAtom` is kept alive for `useDirectRooms` ordering and other read-only consumers but its truth is no longer load-bearing for the layout. +- **Stream** layout (rail + dot + bubble, the DM "VS Code chat" look) = **1:1 non-channels rooms only**. +- **Channel** layout (avatar + in-bubble header + bubble, Discord-style) = **every group room (>2)** in any surface **AND every room under `/channels/`** regardless of member count. +- `channelsMode` additionally enables channels-only filtering (thread surfacing via `ThreadSummaryCard`, hiding thread-replies/edits/reactions/RTC from the centre column via `isChannelsModeHidden`). +- Bridged Telegram puppet rooms classify correctly because the gate is server-side authoritative; `isBridged = channelsMode && isBridgedRoom(room)` disables thread/RTC affordances. -Use `useIsOneOnOne()` from `hooks/useRoom.ts` whenever you need the 1:1 vs group split — it reads the `IsOneOnOneProvider` context value set per route. Do NOT introduce a new `useIsDirect*` helper or the four-source `m.direct` gate that P3c removed (`isDirectStreamRoom`, `useIsDirectStream`, `IsDirectRoomProvider`, `useIsDirectRoom`). See `docs/plans/dm_1x1_redesign.md` §6.8 for the full rationale. +Route providers each wrap an inner `ResolvedRoomProvider` that runs the reactive `useIsOneOnOneRoom(room)` (subscribes `RoomStateEvent.Members`) and sets ``: +- `DirectRouteRoomProvider` (`direct/RoomProvider.tsx`) — bounces invite rooms to `/direct/`, missing/space rooms to `JoinBeforeNavigate`. +- `SpaceRouteRoomProvider` (`space/RoomProvider.tsx`) — used by **both** legacy `/:space/:room` AND `/channels/:space/:room`; validates parent-child membership. +- `BotRoomProvider` — for bot rooms. +- `HomeRouteRoomProvider` only redirects (sets no IsOneOnOne context). + +Use **`useIsOneOnOne()`** from `hooks/useRoom.ts` whenever you need the 1:1 vs group split. The old four-source `m.direct` gate (`isDirectStreamRoom`, `useIsDirectStream`, `IsDirectRoomProvider`, `useIsDirectRoom`, `IsStreamProvider`) is **fully removed** (zero grep hits) — do NOT reintroduce it. `useAutoDirectSync` still round-trips `m.direct` on join for **interop only** (so Element/FluffyChat agree); `mDirectAtom` is kept alive for `useDirectRooms` ordering and the `useDmCallVisible` ring gate, but is not load-bearing for layout. ## Features (`src/app/features/`) | Dir | Purpose | |-----|---------| -| `room/` | Core room view — **RoomTimeline.tsx** (~1700 LOC after P3c collapse), **RoomInput.tsx** (~691 LOC), **RoomViewHeader.tsx** (thin wrapper after P4) → **RoomViewHeaderDm.tsx** (Dawn header for every room class; 1:1 chrome via avatar fallback + peer-profile-sheet, group chrome via `N members` line; phone button three-gated per §6.8b; search/pinned/invite/leave moved into the `…` menu), MembersDrawer (suppressed for 1:1 in `Room.tsx`), MessageEditor, RoomTombstone, RoomViewTyping, CallChatView, CommandAutocomplete | -| `room/message/` | `Message.tsx` (~1170 LOC after P3c) — branches between **Stream** (1:1 rooms) and **Channel** (groups + channels) layouts on the `layout` prop driven by `RoomTimeline.tsx::channelStyleLayout`. Renders edit/delete/react menu, mention/hashtag links, reactions viewer. The legacy `Compact`/`Bubble` layouts and `MessageLayout` enum are gone; `Modern.tsx` survives only as a card-preview layout for pin-menu / message-search / inbox. | -| `room-nav/` | `RoomNavItem.tsx` (~435 LOC) — list-row component, used by Home/Direct/Spaces. Carries call-room behaviour (`useCallSession`, `useCallMembers`, `useCallStart`) | -| `room-settings/` | Room-specific settings page | -| `common-settings/` | Shared settings: general, members, permissions, emojis-stickers, developer-tools | -| `space-settings/` | Space-specific settings | -| `settings/` | User settings (general, account, notifications, devices, emojis, about, dev-tools). `MessageLayout` / `messageSpacing` / `legacyUsernameColor` were removed in P3c — layout is no longer user-configurable (Stream/Channel pick automatically on member count), and the cleanup migration drops orphan persisted fields on first load. `hideMembershipEvents` / `hideNickAvatarEvents` survive — they still gate the group-room syslines. **Logout lives here only.** | -| `lobby/` | Space/room lobby view | -| `search/` | Global search | -| `message-search/` | In-room message search | -| `create-chat/` | DM creation flow (recent commit 58ec12d split it into username + server fields) | -| `create-room/` | Room creation | -| `create-space/` | Space creation | -| `add-existing/` | Join existing rooms | -| `join-before-navigate/` | Pre-join navigation logic | -| `call/` | Element Call integration. **Vojo-DM voice call lifecycle lives here** (phases 0..5.35). Don't touch unless redesigning calls. | -| `call-status/` | Call state display + IncomingCallStrip (mounted as sibling in `Router.tsx:184`, **not** inside RoomViewHeader). | +| `room/` | Core room view. **RoomTimeline.tsx** (~2516 LOC), **RoomInput.tsx** (~828 LOC), **RoomViewHeader.tsx** (11-line wrapper → **RoomViewHeaderDm.tsx**, ~791 LOC — the real Dawn header for *every* room class; identity area branches 3 ways: 1:1 → peer-profile sheet, group → members sheet, callView → static; subline shows `local:server` + presence for 1:1 or `N members` for groups; phone button via `useDmCallVisible`; search/pinned/invite/leave/settings/jump-to-time live in the `…` `RoomMenu`). Also **ThreadDrawer.tsx** (~1344 LOC, full thread surface with its own composer), `ThreadSummaryCard.tsx`, `RoomView.tsx` (composer-overlay pattern), `RoomViewMembersPanel`/`MembersSidePanel`, `RoomViewProfilePanel`/`ProfileSidePanel`, `RoomViewMediaSidePanel`/`MobileMediaViewerHorseshoe`, `RoomTimelineTyping.tsx`, `EmptyTimeline.tsx`, `RoomTombstone`, `CallChatView`, `CommandAutocomplete`, `room-pin-menu/`, `jump-to-time/`, `reaction-viewer/`. `MembersDrawer.tsx` still exists but is used **only** by lobby + `members-list/`, not Room.tsx. | +| `room/message/` | `Message.tsx` (~1506 LOC). The Stream/Channel branch is `Message.tsx:1160` (`layout === 'channel' ? : `), driven by the `layout` prop from `RoomTimeline`. Hosts the edit/delete/react/report/pin/copy-link/source menu, `useDotColor` (Stream rail dot only), thread reply handler. Also `MessageEditor`, `CallMessage`, `SyslineMessage`, `Reactions`, `EncryptedContent`. | +| `room-nav/` | **Three** list-row components now: `RoomNavItem.tsx` (~434 LOC, channels + spaces lists), `DmStreamRow.tsx` (~496 LOC, the Direct-list row), `DirectInviteRow.tsx` (~282 LOC, inline accept/decline invite row in the Direct list). | +| `bots/` | **NEW.** Bridge-bot widget host (a bot's control room = the DM with its mxid). `catalog.ts` loads `BotPreset[]` from `config.json` `bots[]` (validates widget-origin allowlist + command prefix). `useBotRoom.ts` classifies control-room membership into a 6-state union. `BotShell` mounts a `matrix-widget-api` iframe (`BotWidgetEmbed`/`BotWidgetDriver`, tight `m.text`/`m.notice`-only capability allowlist). `botShowChatAtomFamily` toggles widget vs chat-fallback. `room.ts` = single source for portal-vs-control-room (`isBotControlRoom`). Pairs with `pages/client/bots/`. | +| `share-target/` | **NEW.** Android/web system share-sheet hand-off. `ShareTargetStrip.tsx` is a top banner (mounted in `HorseshoeContainer`) shown while `pendingShareAtom` holds a payload; the next `RoomInput` mount consumes it (injects files + text, then nulls the atom). Native slot drained by `hooks/useShareTargetReceiver.ts`. | +| `call/` | **In-room call pane** — `CallView` (prescreen/join screen + member list + livekit checks), `CallControls`/`Controls`/`PrescreenControls`/`CallMemberCard`. Mounted in `Room.tsx` via ``. Consumes `plugins/call` CallEmbed + `state/callEmbed`. Don't unmount/remount the widget root carelessly — Android FGS is keyed on `joined`. | +| `call-status/` | **Global bottom call rail** — `IncomingCallStrip` (incoming-ring row) + `CallStatus` (active-call pill) + `CallControl`. Mounted via `pages/CallStatusRenderer.tsx` + `pages/IncomingCallStripRenderer.tsx` inside `HorseshoeContainer` (NOT directly in Router). Call **lifecycle** hooks (`useIncomingRtcNotifications`, `useCallerAutoHangup`, `usePendingCallActionConsumer`) run in Router's `IncomingCallsFeature()`. | +| `settings/` | User settings as 7 pages (`GeneralPage, AccountPage, NotificationPage, DevicesPage, EmojisStickersPage, DeveloperToolsPage, AboutPage`; `SETTINGS_PAGE_PARAM` deep-links). `MessageLayout` / `messageSpacing` / `legacyUsernameColor` / `hour24Clock` / `dateFormatString` were removed — layout is no longer user-configurable and time/date derive from the runtime locale (`utils/time.ts`). `hideMembershipEvents` / `hideNickAvatarEvents` survive (gate group-room syslines). **Logout lives here only** (`LogoutDialog`). `MobileSettingsHorseshoe` + `SettingsScreen` are the mobile sheet / route entry. | +| `common-settings/` | **Shared** settings modules reused by both room and space settings: `general/` (RoomProfile, Address, Encryption, HistoryVisibility, JoinRules, Publish, Upgrade), `members/`, `permissions/` (Powers, PowersEditor, PermissionGroups), `emojis-stickers/` (RoomPacks), `developer-tools/` (SendRoomEvent, StateEventEditor). | +| `room-settings/` / `space-settings/` | Each defines only its own General + Permissions and **imports** Members/EmojisStickers/DeveloperTools from `common-settings`. Mounted globally via `RoomSettingsRenderer` / `SpaceSettingsRenderer`. | +| `lobby/` | Space lobby (`Lobby` + Hierarchy/Item + pragmatic-drag-and-drop reordering). Still routed under `SPACE_PATH/_LOBBY_PATH` and reached from the **legacy** Space tab — the new Channels surface does **not** use it. | +| `search/` | Global room/space switcher modal (Cmd+K style) — `Search.tsx` (mounted via `SearchModalRenderer`) + `useRoomSearch.ts` (extracted `useAsyncSearch` over directs/rooms/spaces/orphan-spaces). | +| `message-search/` | In-room message search (`MessageSearch` + filters/input/`SearchResultGroup`). | +| `create-chat/` | DM creation — username + server fields (`FALLBACK_SERVER='vojo.chat'`), dedup via `getDMRoomFor`. | +| `create-room/` / `create-space/` | Room/space creation (`CreateRoom`/`CreateSpace` + their modal wrappers, mounted via `*ModalRenderer`). | +| `add-existing/` | Add existing rooms to a space. | +| `join-before-navigate/` | Pre-join room card (`JoinBeforeNavigate.tsx`, ~82 LOC). | ### Virtualization in features/room/ -`RoomTimeline.tsx` does **not** use `@tanstack/react-virtual`. It uses **`useVirtualPaginator`** + `IntersectionObserver` for pagination + scroll-anchoring. There is no `estimateSize` to retune; row heights are measured live. (The tanstack `useVirtualizer` is used elsewhere — e.g. `Direct.tsx:199` for the DM-list column with `estimateSize: () => 38`.) +`RoomTimeline.tsx` does **not** use `@tanstack/react-virtual`. It uses **`useVirtualPaginator`** + `IntersectionObserver` for pagination + scroll-anchoring; row heights are measured live (no `estimateSize`). The tanstack `useVirtualizer` is used elsewhere for **list panels** (e.g. the DM-list column in `Direct.tsx`, via `components/virtualizer/VirtualTile.tsx`). ## Key Components (`src/app/components/`) -- `message/` — Message rendering. Only `Stream.tsx` and `Modern.tsx` layouts ship after P3c (`Compact`/`Bubble` deleted along with `MessageLayout` enum). Every timeline row uses ``. `Modern.tsx` survives as the card-preview layout for pin-menu, message-search and inbox notifications — those are not timelines. `EventContent.tsx` is a single-branch sysline renderer (the legacy `IsStreamProvider` context was deleted; rail metadata flows in via `railStart` / `railEnd` props directly). -- `message/MessageStatus.tsx` + `hooks/useMessageStatus.ts` — **Kept but no longer rendered in the timeline.** P3c retired the WhatsApp-style checkmarks because the Stream-rail dot now encodes the same delivery / read state via colour and opacity. `useMessageStatus` is still consumed inside `useDotColor.ts`, so the file is load-bearing. -- `editor/` — Slate-based rich text editor. Autocomplete: `RoomMentionAutocomplete`, `UserMentionAutocomplete`, `EmoticonAutocomplete`, `CommandAutocomplete`. `Editor.tsx` is the Slate root — preserve. -- `emoji-board/` — Emoji picker -- `image-pack-view/` — Custom emoji pack management -- `image-viewer/`, `Pdf-viewer/` — Media viewers -- `sidebar/` — `Sidebar.tsx` (66px wrapper), `SidebarItem.tsx`, `SidebarStack.tsx`, `SidebarContent.tsx`, `Sidebar.css.ts` -- `user-profile/` — User info, power chips, moderation -- `member-tile/` — Member list items -- `power/` — Power level UI -- `upload-card/` — Upload progress cards -- `url-preview/` — Link previews -- `page/` — Page layout wrapper (`Page`, `PageRoot`, `PageNav`, `PageHeader`, `PageContent`, `PageHero`) -- `setting-tile/` — Settings list item pattern -- `sequence-card/`, `cutout-card/` — Card layouts -- `uia-stages/` — User-interactive auth stages (email, captcha, token) -- `invite-user-prompt/`, `join-address-prompt/`, `leave-room-prompt/` — Dialogs -- `BackRouteHandler.tsx` — Web back-button → back-stack collapse via `replace` (commit dce6be9) +### Message rendering — `message/` + +- `message/layout/` — **three shipping layouts** + base: `Stream.tsx` (1:1/Bots rail+dot+bubble, `STREAM_MESSAGE_SPACING='400'`), `Channel.tsx` (groups + channels: avatar + in-bubble header, `CHANNEL_MESSAGE_SPACING='300'`, `headerInBubble` compact mode for the thread drawer), `Modern.tsx` (card-preview only — pin-menu, message-search, `DefaultPlaceholder`; inbox is gone), `Base.tsx` (MessageBase/AvatarBase/Username primitives). `Compact.tsx`/`Bubble.tsx` and the `MessageLayout` enum are deleted. Also `layout.css.ts`, `Channel.css.ts`, `streamDebug.ts`. +- `message/content/` — `EventContent.tsx` is now a **two-branch** sysline renderer (`layout?: 'stream'|'channel'`; `'channel'` → ``, else the Stream rail/dot grid). The legacy `IsStreamProvider` context is gone; rail metadata flows via `railStart`/`railEnd` props. Plus `Image/Video/Audio/File/Thumbnail/FallbackContent`. +- `message/attachment/` — `Attachment.tsx` + the Stream-media bubble shells (`StreamMediaShell` aspect-clamped 320px, `StreamMediaImage`, `StreamMediaVideo`). +- `message/placeholder/` — `DefaultPlaceholder`, `LinePlaceholder`. +- Top-level: `MessageStatus.tsx` (kept, **not rendered in timeline** — consumed only by `hooks/useMessageStatus.ts` → `hooks/useDotColor.ts`, which encodes delivery/read state on the Stream rail dot: red=failed, green=read, gold=mention, gray=neutral), `Reaction`, `Reply`, `Time`, `RenderBody`, `FileHeader`. + +### Editor — `editor/` + +Slate-based. `Editor.tsx` (Slate root — preserve), `Editor.preview.tsx`, `Elements`, `Toolbar`, `input/output/keyboard/utils`. `editor/autocomplete/`: `RoomMentionAutocomplete`, `UserMentionAutocomplete`, `EmoticonAutocomplete`, `AutocompleteMenu`. **`CommandAutocomplete` is NOT here** — it lives in `features/room/`. + +### Media / viewers + +- `media/` (**NEW**) — primitive ``/`