8.1 KiB
Vojo — Project Guide
Matrix chat client (React 18 + TypeScript), forked from Cinny and rebranded as Vojo. This is only client repo (there is no server side sources)
Quick Start
npm start # dev server on :8080
npm run build # production build → dist/
npm run lint # eslint + prettier
npm run typecheck # tsc --noEmit
Build: Vite 5.4 with vanilla-extract, WASM, PWA plugins.
Source Layout
src/
├── index.tsx # Entry point
├── client/
│ ├── initMatrix.ts # Matrix SDK init (createClient, startClient, logout)
│ └── secretStorageKeys.js # Crypto callbacks
├── types/matrix/ # Matrix protocol types (room.ts, accountData.ts, common.ts)
└── app/
├── i18n.ts # i18next config
├── pages/
│ └── App.tsx # Root component (providers, config loader)
├── features/ # Feature modules
├── components/ # Shared components
├── hooks/ # ~117 custom hooks
├── state/ # Jotai atoms
├── plugins/ # Content plugins
├── utils/ # Utilities
└── styles/ # Vanilla-extract global styles
Pages & Routing (src/app/pages/)
Router in Router.tsx — createBrowserRouter / createHashRouter.
- Auth:
auth/login/,auth/register/,auth/reset-password/ - Client:
client/— main layout after loginhome/— Home timeline (path:/home/, root/redirects here)direct/— DMs (path:/direct/)space/— Space view (path:/:spaceIdOrAlias/)explore/— Public rooms (path:/explore/)inbox/— Notifications, invites (path:/inbox/)create/— New room/space (path:/create/)sidebar/— Tab components for sidebar navWelcomePage.tsx— Empty stateSyncStatus.tsx,SpecVersions.tsx— Connection status
Features (src/app/features/)
| Dir | Purpose |
|---|---|
room/ |
Core room view — RoomTimeline.tsx (63KB), RoomInput.tsx (24KB), RoomViewHeader.tsx (18KB), MembersDrawer, MessageEditor, RoomTombstone |
room-nav/ |
Room list navigation items & categories |
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) |
lobby/ |
Space/room lobby view |
search/ |
Global search |
message-search/ |
In-room message search |
create-chat/ |
DM creation flow |
create-room/ |
Room creation |
create-space/ |
Space creation |
add-existing/ |
Join existing rooms |
join-before-navigate/ |
Pre-join navigation logic |
call/ |
Element Call integration |
call-status/ |
Call state display |
Key Components (src/app/components/)
message/— Message rendering (layout variants: compact, bubble, modern)editor/— Slate-based rich text editoremoji-board/— Emoji pickerimage-pack-view/— Custom emoji pack managementimage-viewer/,Pdf-viewer/— Media viewerssidebar/— Sidebar navigationuser-profile/— User info, power chips, moderationmember-tile/— Member list itemspower/— Power level UIupload-card/— Upload progress cardsurl-preview/— Link previewspage/— Page layout wrapper (Page, PageHeader, PageContent)setting-tile/— Settings list item patternsequence-card/,cutout-card/— Card layoutsuia-stages/— User-interactive auth stages (email, captcha, token)room-intro/— Room introduction cardinvite-user-prompt/,join-address-prompt/,leave-room-prompt/— Dialogs
State Management
Jotai atoms in src/app/state/:
settings.ts— User preferences (MessageLayout, DateFormat, etc.)sessions.ts— Active sessionupload.ts— Upload progressroom/— roomInputDrafts, roomToParents, roomToUnreadroom-list/— roomList, inviteList, sorting/filtering
Some atoms persist to localStorage (e.g. settings.ts, navToActivePath.ts), others are in-memory only (e.g. upload.ts, roomInputDrafts.ts). Access via hooks in state/hooks/.
Localisation (i18n)
Config: src/app/i18n.ts — i18next + HTTP backend + language detector, fallbackLng: 'ru'
Locale files: public/locales/en.json, public/locales/ru.json
Namespaces (top-level keys in JSON):
Organisms, Auth, Settings, Search, Home, Direct, Room, Inbox, Explore, Create, RoomSettings
Pattern:
import { useTranslation } from 'react-i18next';
const { t } = useTranslation();
return <Text>{t('RoomSettings.some_key')}</Text>;
Conventions:
- Each component gets its own
useTranslation()call - In custom hooks with
useMemo, add[t]to dependency array react-i18nextimport goes after framework imports, before local imports- For dynamic values:
t('key', { count: 5 }) - For rare cases with HTML in translations:
dangerouslySetInnerHTML(used sparingly, e.g. in RoomAddress.tsx)
Key Libraries
- React 18.2 + React Router DOM 6
- matrix-js-sdk 38.2 — Matrix protocol
- folds 2.6 — UI component library
- jotai 2.6 — State management
- vanilla-extract — Type-safe CSS
- slate 0.123 — Rich text editor
- @tanstack/react-query 5 — Data fetching
- @tanstack/react-virtual 3 — Virtual scrolling
- i18next 23 + react-i18next 15 — Localisation
- Capacitor 8.3 — Native Android wrapper
- @capacitor/browser 8.0 — External link handling in native
Android (Capacitor)
Requirements: Node >=22, JDK 17+ (21 used), Android SDK with platform 36 + build-tools 36.0.0
Config: capacitor.config.ts — appId: chat.vojo.app, webDir: dist
Android project: android/ — generated, targetSdkVersion 36, compileSdkVersion 36, minSdkVersion 24
Build scripts:
npm run build:android:debug # full chain: build → sync → debug APK
npm run build:android:release # full chain: build → sync → release APK
npm run build:android:aab # full chain: build → sync → release AAB
npm run android:sync # sync dist/ → android assets
npm run android:apk:debug # gradle debug build only
APK output: android/app/build/outputs/apk/debug/app-debug.apk
Version: versionCode and versionName auto-derived from package.json version (major1000000 + minor1000 + patch)
Key architecture decisions:
- Bundled build (dist/ copied into APK), not remote WebView
- Service Worker kept active — critical for authenticated Matrix media (MSC3916 / spec v1.11+). Do NOT disable.
resolveServiceWorkerRequestsdefaulttrue. - Edge-to-edge via
EdgeToEdge.enable()inMainActivity.java+windowLayoutInDisplayCutoutMode: shortEdges - External links opened via
@capacitor/browserplugin (seesrc/app/utils/capacitor.ts) bodybackground-color bound to folds theme variablevar(--oq6d070)for consistent safe-area coloring- Safe-area insets applied on
#root(notbody) so theme background extends behind system bars
VSCode tasks (.vscode/tasks.json):
Deploy to vojo.chat(Ctrl+Shift+D) — web deployDeploy to Android (ADB)(Ctrl+Shift+A) — build + adb install
ADB wireless: pair via adb pair <ip>:<port> <code>, connect via adb connect <ip>:<port>. Ports for pair and connect are different.
SDK location: /usr/lib/android-sdk, also set in android/local.properties
Matrix SDK Patterns
const mx = useMatrixClient(); // Get SDK instance
const room = useRoom(); // Current room
const stateEvent = useStateEvent(room, StateEvent.Type); // Room state
const powerLevels = usePowerLevels(room); // Permissions
Git
- Main branch:
dev - Current work branch:
vojo/dev - Semantic-release on
devbranch - CI: GitHub Actions (build, deploy, docker, netlify)