mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-30 23:14:37 +00:00
Add comprehensive documentation for application architecture, state management, services layer, MCP system, pages and routing, components, and providers
- Introduced detailed architecture overview outlining system components and entry points. - Documented state management using Redux Toolkit and Redux-Persist, including store configuration and slice structures. - Described services layer architecture, detailing API client, socket service, and MTProto service functionalities. - Explained the Model Context Protocol (MCP) system, including tool categories and implementation examples. - Outlined routing structure and page components, emphasizing protected and public routes. - Organized reusable components by feature, detailing their structure and usage. - Provided insights into provider management for service lifecycle and shared state. This documentation enhances understanding of the application's structure and improves onboarding for new developers, ensuring clarity in the system's design and functionality.
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
# Architecture Overview
|
||||
|
||||
## System Architecture
|
||||
|
||||
The Outsourced platform is built on a layered architecture supporting:
|
||||
- Redux-based state management with persistence
|
||||
- Socket.io real-time communication
|
||||
- Telegram MTProto integration via service layer
|
||||
- 81-tool MCP (Model Context Protocol) system for AI interactions
|
||||
- Multi-step onboarding flow
|
||||
- URL-based settings modal system
|
||||
- Deep link authentication handoff
|
||||
- Cross-platform compatibility (desktop + mobile)
|
||||
|
||||
## Entry Points
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `main.tsx` | React root, polyfill imports, lazy deep link listener init |
|
||||
| `App.tsx` | Provider chain: Redux → PersistGate → Socket → Telegram → HashRouter |
|
||||
| `AppRoutes.tsx` | Route definitions with route guards |
|
||||
| `polyfills.ts` | Node.js polyfills (Buffer, process, util) for telegram npm package |
|
||||
|
||||
## Provider Chain
|
||||
|
||||
The application wraps components in a specific order due to dependencies:
|
||||
|
||||
```
|
||||
Redux Provider
|
||||
└─ PersistGate (rehydrate auth + telegram state from localStorage)
|
||||
└─ UserProvider
|
||||
└─ SocketProvider (manages Socket.io connection + MCP init)
|
||||
└─ TelegramProvider (manages MTProto connection)
|
||||
└─ HashRouter
|
||||
└─ AppRoutes (route definitions + SettingsModal overlay)
|
||||
```
|
||||
|
||||
**Why this order matters:**
|
||||
1. Redux must be outermost for state access
|
||||
2. PersistGate rehydrates persisted state before rendering
|
||||
3. SocketProvider depends on Redux auth token
|
||||
4. TelegramProvider depends on Redux telegram state
|
||||
5. HashRouter provides navigation context to all routes
|
||||
|
||||
## Module Relationships
|
||||
|
||||
```
|
||||
main.tsx (entry)
|
||||
↓
|
||||
App.tsx (providers chain)
|
||||
├─ Redux Store ←→ Persist
|
||||
├─ SocketProvider
|
||||
│ ├─ listens to auth.token changes
|
||||
│ ├─ calls socketService.connect(token)
|
||||
│ └─ init MCP server when socket connected
|
||||
├─ TelegramProvider
|
||||
│ ├─ listens to telegram state
|
||||
│ ├─ calls mtprotoService.initialize(userId)
|
||||
│ └─ exposes useTelegram hook
|
||||
└─ HashRouter + AppRoutes
|
||||
├─ ProtectedRoute ← checks Redux auth + onboarded
|
||||
├─ PublicRoute ← redirects authenticated users
|
||||
├─ pages/Home
|
||||
│ ├─ uses useUser hook → calls userApi
|
||||
│ └─ uses useNavigate → opens /settings
|
||||
├─ pages/Login
|
||||
│ ├─ uses TelegramLoginButton
|
||||
│ └─ calls deeplink.ts functions
|
||||
└─ SettingsModal
|
||||
├─ listens to location.pathname
|
||||
├─ uses useSettingsNavigation hook
|
||||
└─ renders panels with Redux state
|
||||
```
|
||||
|
||||
## Services Layer
|
||||
|
||||
```
|
||||
Services Layer:
|
||||
├─ apiClient (singleton)
|
||||
│ ├─ reads auth.token from Redux
|
||||
│ └─ makes HTTP requests to BACKEND_URL
|
||||
├─ socketService (singleton)
|
||||
│ ├─ manages Socket.io connection
|
||||
│ └─ emits/listens for MCP messages
|
||||
└─ mtprotoService (singleton)
|
||||
├─ manages TelegramClient
|
||||
└─ stores session in Redux telegram.byUser[userId].sessionString
|
||||
```
|
||||
|
||||
## MCP System
|
||||
|
||||
```
|
||||
MCP System:
|
||||
├─ TelegramMCPServer (instantiated in SocketProvider)
|
||||
├─ SocketIOMCPTransport (wraps socketService)
|
||||
└─ 81 tools in telegram/tools/
|
||||
├─ each tool calls mtprotoService
|
||||
└─ results returned via socket transport to backend MCP client
|
||||
```
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Authentication Flow (Deep Link)
|
||||
1. User authenticates in browser → receives `loginToken`
|
||||
2. Browser redirects to `outsourced://auth?token=<loginToken>`
|
||||
3. Desktop app catches deep link via Tauri plugin
|
||||
4. `desktopDeepLinkListener` invokes Rust `exchange_token` command
|
||||
5. Rust calls backend `POST /auth/desktop-exchange` (CORS-free)
|
||||
6. Backend returns `{ sessionToken, user }`
|
||||
7. App stores session in Redux, navigates to `/onboarding` or `/home`
|
||||
|
||||
### Socket.io Connection Flow
|
||||
1. SocketProvider detects `auth.token` change
|
||||
2. Calls `socketService.connect(token)`
|
||||
3. On successful connection, initializes MCP server
|
||||
4. MCP server registers 81 Telegram tools
|
||||
5. Backend can invoke tools via JSON-RPC over Socket.io
|
||||
|
||||
### Telegram Connection Flow
|
||||
1. TelegramProvider detects auth state
|
||||
2. Calls `mtprotoService.initialize(userId)` and `connect()` in parallel
|
||||
3. MTProto client authenticates with Telegram servers
|
||||
4. Session string stored in Redux `telegram.byUser[userId].sessionString`
|
||||
5. Chats/messages fetched and stored in Redux
|
||||
|
||||
## Key Patterns
|
||||
|
||||
### No localStorage Usage
|
||||
- **Rule**: Avoid `localStorage`; use Redux with persistence instead
|
||||
- **Exceptions**: Redux-persist's storage adapter (persistence layer)
|
||||
- **Telegram session**: Stored in `telegram.byUser[userId].sessionString`
|
||||
|
||||
### Route Guard Pattern
|
||||
- **PublicRoute**: Redirects authenticated users away
|
||||
- **ProtectedRoute**: Requires `token` and optionally `isOnboarded`
|
||||
- **DefaultRedirect**: Fallback based on auth state
|
||||
|
||||
### Settings Modal Pattern
|
||||
- Renders via `createPortal` when `location.pathname.startsWith('/settings')`
|
||||
- URL-based navigation without affecting main route
|
||||
- Uses `useSettingsNavigation` hook
|
||||
|
||||
### MCP Tool Pattern
|
||||
Each tool exports a handler:
|
||||
```typescript
|
||||
export const toolName: TelegramMCPToolHandler = {
|
||||
call: async (args, { telegramClient, userId }) => {
|
||||
// Perform Telegram API operation
|
||||
return { success: true, data: result };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## File Organization
|
||||
|
||||
### Feature-Based Structure
|
||||
- `pages/` - Full-page route components
|
||||
- `pages/onboarding/` - Onboarding flow with steps
|
||||
- `components/settings/` - Settings modal system
|
||||
- `lib/mcp/telegram/tools/` - Individual MCP tools
|
||||
|
||||
### Slice-Based State
|
||||
- `store/authSlice.ts` - Authentication
|
||||
- `store/socketSlice.ts` - Socket connection
|
||||
- `store/userSlice.ts` - User profile
|
||||
- `store/telegram/` - Complex Telegram state (5 files)
|
||||
|
||||
### Singleton Services
|
||||
- `services/socketService.ts` - Socket.io
|
||||
- `services/mtprotoService.ts` - Telegram MTProto
|
||||
- `services/apiClient.ts` - HTTP REST
|
||||
|
||||
---
|
||||
|
||||
*Next: [State Management](./02-state-management.md)*
|
||||
@@ -0,0 +1,243 @@
|
||||
# State Management
|
||||
|
||||
The application uses Redux Toolkit with Redux-Persist for robust state management.
|
||||
|
||||
## Store Configuration
|
||||
|
||||
**File:** `store/index.ts`
|
||||
|
||||
```typescript
|
||||
// Combines all slices with persistence
|
||||
const persistConfig = {
|
||||
key: 'root',
|
||||
storage,
|
||||
whitelist: ['auth', 'telegram'] // Persisted slices
|
||||
};
|
||||
```
|
||||
|
||||
## Redux State Structure
|
||||
|
||||
```typescript
|
||||
RootState = {
|
||||
auth: {
|
||||
token: string | null, // JWT (persisted)
|
||||
isOnboardedByUser: Record<string, boolean> // Per-user flag (persisted)
|
||||
},
|
||||
socket: {
|
||||
byUser: Record<string, { // Per user ID
|
||||
status: "connecting" | "connected" | "disconnected",
|
||||
socketId: string | null
|
||||
}>
|
||||
},
|
||||
user: {
|
||||
profile: User | null,
|
||||
loading: boolean,
|
||||
error: string | null
|
||||
},
|
||||
telegram: {
|
||||
byUser: Record<string, TelegramState> // Per Telegram user (persisted)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Slices
|
||||
|
||||
### Auth Slice (`store/authSlice.ts`)
|
||||
|
||||
Manages JWT token and per-user onboarding status.
|
||||
|
||||
**State:**
|
||||
```typescript
|
||||
interface AuthState {
|
||||
token: string | null;
|
||||
isOnboardedByUser: Record<string, boolean>;
|
||||
}
|
||||
```
|
||||
|
||||
**Actions:**
|
||||
- `setToken(token: string)` - Store JWT after login
|
||||
- `clearToken()` - Remove token on logout
|
||||
- `setOnboarded({ userId, isOnboarded })` - Mark user as onboarded
|
||||
|
||||
**Selectors (`store/authSelectors.ts`):**
|
||||
- `selectToken` - Get current JWT
|
||||
- `selectIsOnboarded(userId)` - Check if user completed onboarding
|
||||
|
||||
### Socket Slice (`store/socketSlice.ts`)
|
||||
|
||||
Tracks Socket.io connection status per user.
|
||||
|
||||
**State:**
|
||||
```typescript
|
||||
interface SocketState {
|
||||
byUser: Record<string, {
|
||||
status: 'connecting' | 'connected' | 'disconnected';
|
||||
socketId: string | null;
|
||||
}>;
|
||||
}
|
||||
```
|
||||
|
||||
**Actions:**
|
||||
- `setSocketStatus({ userId, status })` - Update connection status
|
||||
- `setSocketId({ userId, socketId })` - Store socket ID
|
||||
- `clearSocketState(userId)` - Clear user's socket state
|
||||
|
||||
**Selectors (`store/socketSelectors.ts`):**
|
||||
- `selectSocketStatus(userId)` - Get connection status
|
||||
- `selectIsSocketConnected(userId)` - Boolean connected check
|
||||
|
||||
### User Slice (`store/userSlice.ts`)
|
||||
|
||||
Stores user profile data.
|
||||
|
||||
**State:**
|
||||
```typescript
|
||||
interface UserState {
|
||||
profile: User | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
```
|
||||
|
||||
**Actions:**
|
||||
- `setUser(user)` - Store user profile
|
||||
- `setUserLoading(loading)` - Set loading state
|
||||
- `setUserError(error)` - Set error state
|
||||
- `clearUser()` - Clear profile on logout
|
||||
|
||||
### Telegram Slice (`store/telegram/`)
|
||||
|
||||
Complex nested state management for Telegram integration.
|
||||
|
||||
**Files:**
|
||||
- `index.ts` - Slice exports (actions, thunks)
|
||||
- `types.ts` - Entity and state interfaces
|
||||
- `reducers.ts` - Synchronous reducers
|
||||
- `extraReducers.ts` - Async thunk handlers
|
||||
- `thunks.ts` - Async operations
|
||||
|
||||
**State Structure:**
|
||||
```typescript
|
||||
telegram.byUser[telegramUserId] = {
|
||||
connectionStatus: "disconnected" | "connecting" | "connected" | "error",
|
||||
authStatus: "not_authenticated" | "authenticating" | "authenticated" | "error",
|
||||
currentUser: TelegramUser | null,
|
||||
sessionString: string | null, // Stored here, NOT localStorage
|
||||
chats: Record<string, TelegramChat>,
|
||||
chatsOrder: string[],
|
||||
messages: Record<chatId, Record<msgId, TelegramMessage>>,
|
||||
threads: Record<chatId, TelegramThread[]>
|
||||
}
|
||||
```
|
||||
|
||||
**Reducers:**
|
||||
- `setCurrentUser` - Store authenticated Telegram user
|
||||
- `setSessionString` - Store MTProto session (for persistence)
|
||||
- `setConnectionStatus` - Update connection state
|
||||
- `setAuthStatus` - Update authentication state
|
||||
- `addChat` / `updateChat` - Manage chat list
|
||||
- `addMessage` / `updateMessage` - Manage message history
|
||||
- `setThreads` - Store thread data
|
||||
|
||||
**Thunks (`store/telegram/thunks.ts`):**
|
||||
- `initializeTelegram(userId)` - Initialize MTProto client
|
||||
- `connectTelegram(userId)` - Establish Telegram connection
|
||||
- `fetchChats(userId)` - Load chat list
|
||||
- `fetchMessages({ userId, chatId })` - Load message history
|
||||
- `disconnectTelegram(userId)` - Clean disconnect
|
||||
|
||||
**Selectors (`store/telegramSelectors.ts`):**
|
||||
- `selectTelegramState(userId)` - Get full Telegram state
|
||||
- `selectTelegramConnectionStatus(userId)` - Get connection status
|
||||
- `selectTelegramAuthStatus(userId)` - Get auth status
|
||||
- `selectTelegramChats(userId)` - Get chat list
|
||||
- `selectTelegramMessages(userId, chatId)` - Get messages for chat
|
||||
|
||||
## Typed Hooks
|
||||
|
||||
**File:** `store/hooks.ts`
|
||||
|
||||
```typescript
|
||||
// Use these instead of plain useDispatch/useSelector
|
||||
export const useAppDispatch: () => AppDispatch = useDispatch;
|
||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
||||
```
|
||||
|
||||
## Persistence Configuration
|
||||
|
||||
### What's Persisted
|
||||
- `auth.token` - JWT for authentication
|
||||
- `auth.isOnboardedByUser` - Per-user onboarding status
|
||||
- `telegram.byUser` - Telegram state (sessions, chats, etc.)
|
||||
|
||||
### What's NOT Persisted
|
||||
- `socket` - Connection state (reconnects on app start)
|
||||
- `user.loading` / `user.error` - Transient UI states
|
||||
- Telegram loading/error states
|
||||
|
||||
### Storage Backend
|
||||
Redux-Persist uses localStorage adapter by default. This is the ONLY acceptable use of localStorage in the application.
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Reading State
|
||||
```typescript
|
||||
import { useAppSelector } from '../store/hooks';
|
||||
|
||||
function MyComponent() {
|
||||
const token = useAppSelector((state) => state.auth.token);
|
||||
const isConnected = useAppSelector((state) =>
|
||||
state.socket.byUser[userId]?.status === 'connected'
|
||||
);
|
||||
const chats = useAppSelector((state) =>
|
||||
state.telegram.byUser[userId]?.chats
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Dispatching Actions
|
||||
```typescript
|
||||
import { useAppDispatch } from '../store/hooks';
|
||||
import { setToken, clearToken } from '../store/authSlice';
|
||||
import { initializeTelegram } from '../store/telegram/thunks';
|
||||
|
||||
function MyComponent() {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
// Sync action
|
||||
const handleLogin = (token: string) => {
|
||||
dispatch(setToken(token));
|
||||
};
|
||||
|
||||
// Async thunk
|
||||
const handleConnect = async () => {
|
||||
await dispatch(initializeTelegram(userId)).unwrap();
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Using Selectors
|
||||
```typescript
|
||||
import { useAppSelector } from '../store/hooks';
|
||||
import { selectIsOnboarded } from '../store/authSelectors';
|
||||
import { selectTelegramConnectionStatus } from '../store/telegramSelectors';
|
||||
|
||||
function MyComponent({ userId }) {
|
||||
const isOnboarded = useAppSelector((state) => selectIsOnboarded(state, userId));
|
||||
const connectionStatus = useAppSelector((state) =>
|
||||
selectTelegramConnectionStatus(state, userId)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always use typed hooks** - `useAppDispatch` and `useAppSelector`
|
||||
2. **Use selectors for derived state** - Memoized and testable
|
||||
3. **Keep thunks in separate files** - Better organization
|
||||
4. **Per-user state scoping** - Key state by user ID
|
||||
5. **Avoid localStorage** - Use Redux-Persist instead
|
||||
|
||||
---
|
||||
|
||||
*Previous: [Architecture Overview](./01-architecture.md) | Next: [Services Layer](./03-services.md)*
|
||||
@@ -0,0 +1,297 @@
|
||||
# Services Layer
|
||||
|
||||
The application uses singleton services for external communication. This prevents connection leaks and provides consistent API access.
|
||||
|
||||
## Service Architecture
|
||||
|
||||
```
|
||||
Services Layer
|
||||
├─ apiClient (HTTP REST)
|
||||
│ ├─ reads auth.token from Redux
|
||||
│ └─ makes requests to BACKEND_URL
|
||||
├─ socketService (Socket.io)
|
||||
│ ├─ manages real-time connection
|
||||
│ └─ emits/listens for MCP messages
|
||||
└─ mtprotoService (Telegram)
|
||||
├─ manages TelegramClient
|
||||
└─ stores session in Redux
|
||||
```
|
||||
|
||||
## API Client (`services/apiClient.ts`)
|
||||
|
||||
HTTP REST client for backend communication.
|
||||
|
||||
### Features
|
||||
- Fetch-based implementation
|
||||
- Auto-injects JWT from Redux store
|
||||
- Typed request/response handling
|
||||
- Error handling with typed errors
|
||||
|
||||
### Usage
|
||||
```typescript
|
||||
import apiClient from '../services/apiClient';
|
||||
|
||||
// GET request
|
||||
const user = await apiClient.get<User>('/users/me');
|
||||
|
||||
// POST request
|
||||
const result = await apiClient.post<LoginResponse>('/auth/login', {
|
||||
email,
|
||||
password
|
||||
});
|
||||
|
||||
// With custom headers
|
||||
const data = await apiClient.get<Data>('/endpoint', {
|
||||
headers: { 'X-Custom': 'value' }
|
||||
});
|
||||
```
|
||||
|
||||
### Configuration
|
||||
Reads `VITE_BACKEND_URL` from environment or uses default:
|
||||
```typescript
|
||||
const BACKEND_URL = import.meta.env.VITE_BACKEND_URL || 'https://api.example.com';
|
||||
```
|
||||
|
||||
## API Endpoints (`services/api/`)
|
||||
|
||||
### Auth API (`services/api/authApi.ts`)
|
||||
|
||||
Authentication-related endpoints.
|
||||
|
||||
```typescript
|
||||
import { authApi } from '../services/api/authApi';
|
||||
|
||||
// Login
|
||||
const { token, user } = await authApi.login(credentials);
|
||||
|
||||
// Token exchange (for deep link flow)
|
||||
const { sessionToken, user } = await authApi.exchangeToken(loginToken);
|
||||
|
||||
// Logout
|
||||
await authApi.logout();
|
||||
```
|
||||
|
||||
### User API (`services/api/userApi.ts`)
|
||||
|
||||
User profile endpoints.
|
||||
|
||||
```typescript
|
||||
import { userApi } from '../services/api/userApi';
|
||||
|
||||
// Get current user
|
||||
const user = await userApi.getCurrentUser();
|
||||
|
||||
// Update profile
|
||||
const updated = await userApi.updateProfile({ firstName, lastName });
|
||||
|
||||
// Get settings
|
||||
const settings = await userApi.getSettings();
|
||||
```
|
||||
|
||||
## Socket Service (`services/socketService.ts`)
|
||||
|
||||
Socket.io client singleton for real-time communication.
|
||||
|
||||
### Features
|
||||
- Singleton pattern - single connection per app
|
||||
- Auth token passed in socket `auth` object
|
||||
- Transports: polling first, then WebSocket upgrade
|
||||
- Auto-reconnection handling
|
||||
|
||||
### API
|
||||
```typescript
|
||||
import socketService from '../services/socketService';
|
||||
|
||||
// Connect with auth token
|
||||
socketService.connect(token);
|
||||
|
||||
// Disconnect
|
||||
socketService.disconnect();
|
||||
|
||||
// Emit event
|
||||
socketService.emit('event-name', data);
|
||||
|
||||
// Listen for events
|
||||
socketService.on('event-name', (data) => {
|
||||
// Handle event
|
||||
});
|
||||
|
||||
// Remove listener
|
||||
socketService.off('event-name', handler);
|
||||
|
||||
// One-time listener
|
||||
socketService.once('event-name', (data) => {
|
||||
// Handle once
|
||||
});
|
||||
|
||||
// Get socket instance
|
||||
const socket = socketService.getSocket();
|
||||
|
||||
// Check connection status
|
||||
const isConnected = socketService.isConnected();
|
||||
```
|
||||
|
||||
### Connection Flow
|
||||
```typescript
|
||||
// In SocketProvider.tsx
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
socketService.connect(token);
|
||||
|
||||
socketService.on('connect', () => {
|
||||
dispatch(setSocketStatus({ userId, status: 'connected' }));
|
||||
dispatch(setSocketId({ userId, socketId: socket.id }));
|
||||
// Initialize MCP server
|
||||
initMCPServer(socketService.getSocket());
|
||||
});
|
||||
|
||||
socketService.on('disconnect', () => {
|
||||
dispatch(setSocketStatus({ userId, status: 'disconnected' }));
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
socketService.disconnect();
|
||||
};
|
||||
}, [token]);
|
||||
```
|
||||
|
||||
### Configuration
|
||||
```typescript
|
||||
const socket = io(BACKEND_URL, {
|
||||
auth: { token },
|
||||
transports: ['polling', 'websocket'],
|
||||
reconnection: true,
|
||||
reconnectionAttempts: 5,
|
||||
reconnectionDelay: 1000
|
||||
});
|
||||
```
|
||||
|
||||
## MTProto Service (`services/mtprotoService.ts`)
|
||||
|
||||
Telegram MTProto client singleton.
|
||||
|
||||
### Features
|
||||
- Singleton pattern - one client per user
|
||||
- Session persistence via Redux (not localStorage)
|
||||
- Auto-retry for FLOOD_WAIT up to 60s
|
||||
- Supports QR login and phone auth
|
||||
|
||||
### Initialization
|
||||
```typescript
|
||||
import mtprotoService from '../services/mtprotoService';
|
||||
|
||||
// Get or create instance for user
|
||||
const client = await mtprotoService.getInstance().initialize(userId);
|
||||
|
||||
// Set session string (from Redux)
|
||||
await client.setSession(sessionString);
|
||||
|
||||
// Connect to Telegram
|
||||
await client.connect();
|
||||
```
|
||||
|
||||
### Session Management
|
||||
```typescript
|
||||
// Session is stored in Redux, not localStorage
|
||||
// In TelegramProvider:
|
||||
const sessionString = useAppSelector((state) =>
|
||||
state.telegram.byUser[userId]?.sessionString
|
||||
);
|
||||
|
||||
// When session updates
|
||||
useEffect(() => {
|
||||
if (client && sessionString) {
|
||||
client.setSession(sessionString);
|
||||
}
|
||||
}, [sessionString]);
|
||||
|
||||
// Save session after auth
|
||||
const newSession = await client.getSession();
|
||||
dispatch(setSessionString({ userId, sessionString: newSession }));
|
||||
```
|
||||
|
||||
### API Operations
|
||||
```typescript
|
||||
// Get current user
|
||||
const me = await client.getMe();
|
||||
|
||||
// Get dialogs (chats)
|
||||
const dialogs = await client.getDialogs({ limit: 20 });
|
||||
|
||||
// Send message
|
||||
await client.sendMessage(peer, { message: 'Hello!' });
|
||||
|
||||
// Get history
|
||||
const messages = await client.getMessages(peer, { limit: 50 });
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
```typescript
|
||||
try {
|
||||
await client.connect();
|
||||
} catch (error) {
|
||||
if (error.message.includes('FLOOD_WAIT')) {
|
||||
const seconds = parseInt(error.message.match(/\d+/)?.[0] || '60');
|
||||
if (seconds <= 60) {
|
||||
// Auto-retry after wait
|
||||
await new Promise(r => setTimeout(r, seconds * 1000));
|
||||
await client.connect();
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
```
|
||||
|
||||
## Service Integration with Providers
|
||||
|
||||
### SocketProvider
|
||||
```typescript
|
||||
// providers/SocketProvider.tsx
|
||||
export function SocketProvider({ children }) {
|
||||
const token = useAppSelector((state) => state.auth.token);
|
||||
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
socketService.connect(token);
|
||||
// On connect, initialize MCP
|
||||
}
|
||||
return () => socketService.disconnect();
|
||||
}, [token]);
|
||||
|
||||
return <SocketContext.Provider value={...}>{children}</SocketContext.Provider>;
|
||||
}
|
||||
```
|
||||
|
||||
### TelegramProvider
|
||||
```typescript
|
||||
// providers/TelegramProvider.tsx
|
||||
export function TelegramProvider({ children }) {
|
||||
const dispatch = useAppDispatch();
|
||||
const userId = useAppSelector((state) => state.user.profile?.id);
|
||||
|
||||
useEffect(() => {
|
||||
if (userId) {
|
||||
// Parallel init + connect for faster startup
|
||||
Promise.all([
|
||||
dispatch(initializeTelegram(userId)),
|
||||
dispatch(connectTelegram(userId))
|
||||
]);
|
||||
}
|
||||
}, [userId]);
|
||||
|
||||
return <TelegramContext.Provider value={...}>{children}</TelegramContext.Provider>;
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use singletons** - Never create multiple service instances
|
||||
2. **Store sessions in Redux** - Not localStorage
|
||||
3. **Clean up on unmount** - Disconnect in useEffect cleanup
|
||||
4. **Handle errors gracefully** - Retry for transient failures
|
||||
5. **Pass auth via proper channels** - Socket auth object, not query string
|
||||
|
||||
---
|
||||
|
||||
*Previous: [State Management](./02-state-management.md) | Next: [MCP System](./04-mcp-system.md)*
|
||||
@@ -0,0 +1,437 @@
|
||||
# MCP System
|
||||
|
||||
The Model Context Protocol (MCP) system enables AI-driven Telegram interactions through 81 specialized tools.
|
||||
|
||||
## Overview
|
||||
|
||||
```
|
||||
MCP System Architecture
|
||||
├── lib/mcp/
|
||||
│ ├── index.ts # Singleton lifecycle management
|
||||
│ ├── types.ts # MCP interfaces
|
||||
│ ├── transport.ts # Socket.IO JSON-RPC 2.0 transport
|
||||
│ ├── logger.ts # Logging utilities
|
||||
│ ├── errorHandler.ts # Error handling
|
||||
│ ├── validation.ts # Input validation
|
||||
│ │
|
||||
│ └── telegram/
|
||||
│ ├── index.ts # Server initialization
|
||||
│ ├── server.ts # TelegramMCPServer (81 tools)
|
||||
│ ├── types.ts # Tool name types
|
||||
│ ├── telegramApi.ts # Telegram API layer
|
||||
│ ├── apiCastHelpers.ts # Type casting
|
||||
│ ├── apiResultTypes.ts # Result types
|
||||
│ ├── args.ts # Argument schemas
|
||||
│ ├── toolActionParser.ts # Human-readable parsing
|
||||
│ │
|
||||
│ └── tools/ # 81 individual tool files
|
||||
```
|
||||
|
||||
## Core Components
|
||||
|
||||
### MCP Types (`lib/mcp/types.ts`)
|
||||
|
||||
```typescript
|
||||
interface MCPTool {
|
||||
name: string;
|
||||
description: string;
|
||||
inputSchema: JSONSchema;
|
||||
}
|
||||
|
||||
interface MCPRequest {
|
||||
jsonrpc: '2.0';
|
||||
id: string | number;
|
||||
method: string;
|
||||
params?: unknown;
|
||||
}
|
||||
|
||||
interface MCPResponse {
|
||||
jsonrpc: '2.0';
|
||||
id: string | number;
|
||||
result?: unknown;
|
||||
error?: MCPError;
|
||||
}
|
||||
|
||||
interface MCPError {
|
||||
code: number;
|
||||
message: string;
|
||||
data?: unknown;
|
||||
}
|
||||
```
|
||||
|
||||
### Transport Layer (`lib/mcp/transport.ts`)
|
||||
|
||||
Socket.IO-based JSON-RPC 2.0 transport.
|
||||
|
||||
**Features:**
|
||||
- Request ID tracking
|
||||
- 30-second timeout
|
||||
- Error handling
|
||||
- Event emission
|
||||
|
||||
```typescript
|
||||
class SocketIOMCPTransport {
|
||||
constructor(socket: Socket);
|
||||
|
||||
// Send request and await response
|
||||
async send(request: MCPRequest): Promise<MCPResponse>;
|
||||
|
||||
// Register handler for incoming requests
|
||||
onRequest(handler: (req: MCPRequest) => Promise<MCPResponse>): void;
|
||||
|
||||
// Clean up listeners
|
||||
dispose(): void;
|
||||
}
|
||||
```
|
||||
|
||||
### MCP Singleton (`lib/mcp/index.ts`)
|
||||
|
||||
Lifecycle management for MCP server.
|
||||
|
||||
```typescript
|
||||
// Initialize MCP server with socket
|
||||
initMCPServer(socket: Socket): TelegramMCPServer;
|
||||
|
||||
// Get existing server instance
|
||||
getMCPServer(): TelegramMCPServer | null;
|
||||
|
||||
// Update socket reference
|
||||
updateMCPSocket(socket: Socket): void;
|
||||
|
||||
// Clean up
|
||||
cleanupMCP(): void;
|
||||
```
|
||||
|
||||
## Telegram MCP Server
|
||||
|
||||
### Server Class (`lib/mcp/telegram/server.ts`)
|
||||
|
||||
```typescript
|
||||
class TelegramMCPServer {
|
||||
constructor(transport: SocketIOMCPTransport, userId: string);
|
||||
|
||||
// Register all 81 tools
|
||||
async initialize(): Promise<void>;
|
||||
|
||||
// List available tools
|
||||
listTools(): MCPTool[];
|
||||
|
||||
// Execute a tool
|
||||
async executeTool(name: string, args: unknown): Promise<ToolResult>;
|
||||
|
||||
// Handle incoming JSON-RPC request
|
||||
async handleRequest(request: MCPRequest): Promise<MCPResponse>;
|
||||
|
||||
// Clean up
|
||||
dispose(): void;
|
||||
}
|
||||
```
|
||||
|
||||
### Tool Handler Interface
|
||||
|
||||
Each tool file exports a handler:
|
||||
|
||||
```typescript
|
||||
interface TelegramMCPToolHandler {
|
||||
name: string;
|
||||
description: string;
|
||||
inputSchema: JSONSchema;
|
||||
call: (
|
||||
args: unknown,
|
||||
context: {
|
||||
telegramClient: TelegramClient;
|
||||
userId: string;
|
||||
}
|
||||
) => Promise<ToolResult>;
|
||||
}
|
||||
|
||||
interface ToolResult {
|
||||
success: boolean;
|
||||
data?: unknown;
|
||||
error?: string;
|
||||
}
|
||||
```
|
||||
|
||||
## Tool Categories
|
||||
|
||||
### 81 Telegram Tools
|
||||
|
||||
#### User & Profile (5 tools)
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `getMe` | Get current authenticated user |
|
||||
| `getUserInfo` | Get info about any user |
|
||||
| `getUserPhotos` | Get user profile photos |
|
||||
| `getUserStatus` | Get online status |
|
||||
| `setUserStatus` | Update own status |
|
||||
|
||||
#### Chats & Dialogs (12 tools)
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `getChats` | Fetch chat list |
|
||||
| `getChatInfo` | Get chat details |
|
||||
| `createGroup` | Create new group |
|
||||
| `createChannel` | Create new channel |
|
||||
| `leaveChat` | Leave chat/group/channel |
|
||||
| `deleteChat` | Delete chat |
|
||||
| `muteChat` | Mute notifications |
|
||||
| `unmuteChat` | Unmute notifications |
|
||||
| `pinChat` | Pin chat to top |
|
||||
| `unpinChat` | Unpin chat |
|
||||
| `archiveChat` | Archive chat |
|
||||
| `unarchiveChat` | Unarchive chat |
|
||||
|
||||
#### Messages (15 tools)
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `getHistory` | Fetch message history |
|
||||
| `sendMessage` | Send text message |
|
||||
| `replyToMessage` | Reply to specific message |
|
||||
| `editMessage` | Edit sent message |
|
||||
| `deleteMessage` | Delete message |
|
||||
| `forwardMessage` | Forward to another chat |
|
||||
| `pinMessage` | Pin message in chat |
|
||||
| `unpinMessage` | Unpin message |
|
||||
| `markAsRead` | Mark messages as read |
|
||||
| `searchMessages` | Search in chat |
|
||||
| `translateMessage` | Translate message text |
|
||||
| `getMessageReactions` | Get reactions |
|
||||
| `addReaction` | React to message |
|
||||
| `removeReaction` | Remove reaction |
|
||||
| `reportMessage` | Report spam/abuse |
|
||||
|
||||
#### Media (10 tools)
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `sendPhoto` | Send image |
|
||||
| `sendVideo` | Send video |
|
||||
| `sendDocument` | Send file |
|
||||
| `sendVoice` | Send voice message |
|
||||
| `sendAudio` | Send audio file |
|
||||
| `sendSticker` | Send sticker |
|
||||
| `sendGif` | Send animation |
|
||||
| `sendLocation` | Send location |
|
||||
| `sendContact` | Send contact card |
|
||||
| `downloadMedia` | Download media file |
|
||||
|
||||
#### Contacts (8 tools)
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `addContact` | Add new contact |
|
||||
| `deleteContact` | Remove contact |
|
||||
| `getContacts` | Get contact list |
|
||||
| `searchContacts` | Search contacts |
|
||||
| `importContacts` | Bulk import |
|
||||
| `exportContacts` | Export contact list |
|
||||
| `blockUser` | Block user |
|
||||
| `unblockUser` | Unblock user |
|
||||
|
||||
#### Groups & Channels (15 tools)
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `getAdmins` | Get admin list |
|
||||
| `getMembers` | Get member list |
|
||||
| `addMember` | Add user to group |
|
||||
| `removeMember` | Remove from group |
|
||||
| `banUser` | Ban user |
|
||||
| `unbanUser` | Unban user |
|
||||
| `promoteAdmin` | Promote to admin |
|
||||
| `demoteAdmin` | Remove admin rights |
|
||||
| `setGroupTitle` | Change group name |
|
||||
| `setGroupPhoto` | Change group photo |
|
||||
| `setGroupDescription` | Change description |
|
||||
| `subscribePublicChannel` | Join public channel |
|
||||
| `inviteToChannel` | Invite user |
|
||||
| `getInviteLink` | Get invite link |
|
||||
| `revokeInviteLink` | Revoke invite link |
|
||||
|
||||
#### Polls & Interactive (5 tools)
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `createPoll` | Create poll |
|
||||
| `votePoll` | Vote on poll |
|
||||
| `stopPoll` | Close poll |
|
||||
| `pressInlineButton` | Click inline button |
|
||||
| `answerCallback` | Respond to callback |
|
||||
|
||||
#### Drafts (3 tools)
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `saveDraft` | Save draft message |
|
||||
| `getDrafts` | Get all drafts |
|
||||
| `deleteDraft` | Delete draft |
|
||||
|
||||
#### Privacy & Settings (5 tools)
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `getBlockedUsers` | Get blocked list |
|
||||
| `getPrivacySettings` | Get privacy config |
|
||||
| `updatePrivacy` | Update privacy |
|
||||
| `get2FAStatus` | Check 2FA status |
|
||||
| `getActiveSessions` | Get login sessions |
|
||||
|
||||
#### Misc (3 tools)
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `resolveUsername` | Resolve @username |
|
||||
| `checkUsername` | Check availability |
|
||||
| `getWebPage` | Get link preview |
|
||||
|
||||
## Tool Implementation Example
|
||||
|
||||
```typescript
|
||||
// lib/mcp/telegram/tools/sendMessage.ts
|
||||
import { TelegramMCPToolHandler } from '../types';
|
||||
|
||||
export const sendMessage: TelegramMCPToolHandler = {
|
||||
name: 'sendMessage',
|
||||
description: 'Send a text message to a chat',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
chatId: {
|
||||
type: 'string',
|
||||
description: 'Chat ID to send message to'
|
||||
},
|
||||
text: {
|
||||
type: 'string',
|
||||
description: 'Message text'
|
||||
},
|
||||
replyToMsgId: {
|
||||
type: 'string',
|
||||
description: 'Optional message ID to reply to'
|
||||
}
|
||||
},
|
||||
required: ['chatId', 'text']
|
||||
},
|
||||
|
||||
call: async (args, { telegramClient, userId }) => {
|
||||
const { chatId, text, replyToMsgId } = args as {
|
||||
chatId: string;
|
||||
text: string;
|
||||
replyToMsgId?: string;
|
||||
};
|
||||
|
||||
try {
|
||||
// Use big-integer for chat ID (Telegram IDs exceed Number.MAX_SAFE_INTEGER)
|
||||
const peer = await telegramClient.getEntity(bigInt(chatId));
|
||||
|
||||
const result = await telegramClient.sendMessage(peer, {
|
||||
message: text,
|
||||
replyTo: replyToMsgId ? parseInt(replyToMsgId) : undefined
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
messageId: result.id.toString(),
|
||||
date: result.date
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## MCP Initialization Flow
|
||||
|
||||
```typescript
|
||||
// In SocketProvider.tsx
|
||||
useEffect(() => {
|
||||
if (socket && socket.connected) {
|
||||
// Initialize MCP server
|
||||
const mcpServer = initMCPServer(socket);
|
||||
|
||||
// Server registers all 81 tools
|
||||
mcpServer.initialize();
|
||||
|
||||
// Listen for tool execution requests
|
||||
mcpServer.onRequest(async (request) => {
|
||||
if (request.method === 'tools/call') {
|
||||
const { name, arguments: args } = request.params;
|
||||
return mcpServer.executeTool(name, args);
|
||||
}
|
||||
return { error: { code: -32601, message: 'Method not found' } };
|
||||
});
|
||||
}
|
||||
|
||||
return () => cleanupMCP();
|
||||
}, [socket?.connected]);
|
||||
```
|
||||
|
||||
## JSON-RPC Protocol
|
||||
|
||||
### Request Format
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": "sendMessage",
|
||||
"arguments": {
|
||||
"chatId": "123456789",
|
||||
"text": "Hello from AI!"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Response Format (Success)
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"result": {
|
||||
"success": true,
|
||||
"data": {
|
||||
"messageId": "12345",
|
||||
"date": 1706540000
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Response Format (Error)
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"error": {
|
||||
"code": -32000,
|
||||
"message": "Chat not found",
|
||||
"data": { "chatId": "invalid" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Big Integer Handling
|
||||
|
||||
Telegram IDs exceed JavaScript's `Number.MAX_SAFE_INTEGER`. Use `big-integer` library:
|
||||
|
||||
```typescript
|
||||
import bigInt from 'big-integer';
|
||||
|
||||
// Convert string ID to big integer
|
||||
const chatId = bigInt(args.chatId);
|
||||
|
||||
// Convert big integer back to string
|
||||
const idString = chatId.toString();
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use big-integer for IDs** - All Telegram IDs should use big-integer
|
||||
2. **Validate inputs** - Use JSON Schema validation before execution
|
||||
3. **Handle errors gracefully** - Return structured error responses
|
||||
4. **Log operations** - Use MCP logger for debugging
|
||||
5. **Timeout long operations** - 30s timeout for transport
|
||||
|
||||
---
|
||||
|
||||
*Previous: [Services Layer](./03-services.md) | Next: [Pages & Routing](./05-pages-routing.md)*
|
||||
@@ -0,0 +1,403 @@
|
||||
# Pages & Routing
|
||||
|
||||
The application uses HashRouter with protected and public route guards.
|
||||
|
||||
## Route Structure
|
||||
|
||||
```
|
||||
/ → Welcome (public)
|
||||
/login → Login (public)
|
||||
/onboarding → Onboarding (protected, requires auth, not yet onboarded)
|
||||
/home → Home (protected, requires auth + onboarded)
|
||||
/settings → Settings modal overlay
|
||||
/settings/* → Settings sub-panels
|
||||
* → DefaultRedirect (fallback)
|
||||
```
|
||||
|
||||
## Route Configuration (`AppRoutes.tsx`)
|
||||
|
||||
```typescript
|
||||
export function AppRoutes() {
|
||||
return (
|
||||
<>
|
||||
<Routes>
|
||||
{/* Public routes - redirect if authenticated */}
|
||||
<Route element={<PublicRoute />}>
|
||||
<Route path="/" element={<Welcome />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
</Route>
|
||||
|
||||
{/* Protected routes - require authentication */}
|
||||
<Route element={<ProtectedRoute />}>
|
||||
<Route path="/onboarding/*" element={<Onboarding />} />
|
||||
</Route>
|
||||
|
||||
{/* Protected + onboarded routes */}
|
||||
<Route element={<ProtectedRoute requireOnboarded />}>
|
||||
<Route path="/home" element={<Home />} />
|
||||
</Route>
|
||||
|
||||
{/* Fallback redirect */}
|
||||
<Route path="*" element={<DefaultRedirect />} />
|
||||
</Routes>
|
||||
|
||||
{/* Settings modal overlay - renders on top of routes */}
|
||||
<SettingsModal />
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Route Guards
|
||||
|
||||
### PublicRoute (`components/PublicRoute.tsx`)
|
||||
|
||||
Redirects authenticated users away from public pages.
|
||||
|
||||
```typescript
|
||||
export function PublicRoute() {
|
||||
const token = useAppSelector((state) => state.auth.token);
|
||||
const isOnboarded = useAppSelector((state) =>
|
||||
selectIsOnboarded(state, userId)
|
||||
);
|
||||
|
||||
if (token) {
|
||||
// Authenticated - redirect to appropriate page
|
||||
return <Navigate to={isOnboarded ? '/home' : '/onboarding'} replace />;
|
||||
}
|
||||
|
||||
return <Outlet />;
|
||||
}
|
||||
```
|
||||
|
||||
### ProtectedRoute (`components/ProtectedRoute.tsx`)
|
||||
|
||||
Enforces authentication and optionally onboarding status.
|
||||
|
||||
```typescript
|
||||
interface ProtectedRouteProps {
|
||||
requireOnboarded?: boolean;
|
||||
}
|
||||
|
||||
export function ProtectedRoute({ requireOnboarded = false }) {
|
||||
const token = useAppSelector((state) => state.auth.token);
|
||||
const isOnboarded = useAppSelector((state) =>
|
||||
selectIsOnboarded(state, userId)
|
||||
);
|
||||
|
||||
if (!token) {
|
||||
return <Navigate to="/login" replace />;
|
||||
}
|
||||
|
||||
if (requireOnboarded && !isOnboarded) {
|
||||
return <Navigate to="/onboarding" replace />;
|
||||
}
|
||||
|
||||
return <Outlet />;
|
||||
}
|
||||
```
|
||||
|
||||
### DefaultRedirect (`components/DefaultRedirect.tsx`)
|
||||
|
||||
Fallback route that redirects based on auth state.
|
||||
|
||||
```typescript
|
||||
export function DefaultRedirect() {
|
||||
const token = useAppSelector((state) => state.auth.token);
|
||||
const isOnboarded = useAppSelector((state) =>
|
||||
selectIsOnboarded(state, userId)
|
||||
);
|
||||
|
||||
if (!token) {
|
||||
return <Navigate to="/" replace />;
|
||||
}
|
||||
|
||||
if (!isOnboarded) {
|
||||
return <Navigate to="/onboarding" replace />;
|
||||
}
|
||||
|
||||
return <Navigate to="/home" replace />;
|
||||
}
|
||||
```
|
||||
|
||||
## Pages
|
||||
|
||||
### Welcome Page (`pages/Welcome.tsx`)
|
||||
|
||||
Landing page for unauthenticated users.
|
||||
|
||||
**Features:**
|
||||
- App introduction and branding
|
||||
- CTA to login/signup
|
||||
- Public route (redirects if authenticated)
|
||||
|
||||
### Login Page (`pages/Login.tsx`)
|
||||
|
||||
Authentication page.
|
||||
|
||||
**Features:**
|
||||
- Telegram OAuth button
|
||||
- Opens `/auth/telegram?platform=desktop` in browser
|
||||
- Handles deep link callback
|
||||
|
||||
```typescript
|
||||
export function Login() {
|
||||
const handleTelegramLogin = () => {
|
||||
// Opens Telegram OAuth in system browser
|
||||
openUrl(`${BACKEND_URL}/auth/telegram?platform=desktop`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="login-page">
|
||||
<TelegramLoginButton onClick={handleTelegramLogin} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Home Page (`pages/Home.tsx`)
|
||||
|
||||
Main dashboard after authentication.
|
||||
|
||||
**Features:**
|
||||
- Protected route (requires auth + onboarded)
|
||||
- Connection status indicators
|
||||
- Navigation to settings modal
|
||||
- Future: Chat list, messages, etc.
|
||||
|
||||
```typescript
|
||||
export function Home() {
|
||||
const navigate = useNavigate();
|
||||
const user = useAppSelector((state) => state.user.profile);
|
||||
const telegramStatus = useAppSelector((state) =>
|
||||
selectTelegramConnectionStatus(state, user?.id)
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="home-page">
|
||||
<header>
|
||||
<h1>Welcome, {user?.firstName}</h1>
|
||||
<button onClick={() => navigate('/settings')}>Settings</button>
|
||||
</header>
|
||||
|
||||
<TelegramConnectionIndicator status={telegramStatus} />
|
||||
<ConnectionIndicator />
|
||||
|
||||
{/* Main content */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Onboarding Flow (`pages/onboarding/`)
|
||||
|
||||
Multi-step onboarding process.
|
||||
|
||||
### Structure
|
||||
```
|
||||
pages/onboarding/
|
||||
├── Onboarding.tsx # Flow controller
|
||||
└── steps/
|
||||
├── GetStartedStep.tsx # Welcome
|
||||
├── PrivacyStep.tsx # Privacy policy
|
||||
├── AnalyticsStep.tsx # Analytics opt-in
|
||||
├── ConnectStep.tsx # Telegram connection
|
||||
└── FeaturesStep.tsx # Features overview
|
||||
```
|
||||
|
||||
### Onboarding Controller (`Onboarding.tsx`)
|
||||
|
||||
```typescript
|
||||
const STEPS = [
|
||||
{ id: 'get-started', component: GetStartedStep },
|
||||
{ id: 'privacy', component: PrivacyStep },
|
||||
{ id: 'analytics', component: AnalyticsStep },
|
||||
{ id: 'connect', component: ConnectStep },
|
||||
{ id: 'features', component: FeaturesStep }
|
||||
];
|
||||
|
||||
export function Onboarding() {
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const dispatch = useAppDispatch();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleNext = () => {
|
||||
if (currentStep < STEPS.length - 1) {
|
||||
setCurrentStep(currentStep + 1);
|
||||
} else {
|
||||
// Complete onboarding
|
||||
dispatch(setOnboarded({ userId, isOnboarded: true }));
|
||||
navigate('/home');
|
||||
}
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
if (currentStep > 0) {
|
||||
setCurrentStep(currentStep - 1);
|
||||
}
|
||||
};
|
||||
|
||||
const StepComponent = STEPS[currentStep].component;
|
||||
|
||||
return (
|
||||
<div className="onboarding">
|
||||
<ProgressIndicator current={currentStep} total={STEPS.length} />
|
||||
<StepComponent onNext={handleNext} onBack={handleBack} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Step Components
|
||||
|
||||
Each step receives `onNext` and `onBack` callbacks:
|
||||
|
||||
```typescript
|
||||
interface StepProps {
|
||||
onNext: () => void;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
export function ConnectStep({ onNext, onBack }: StepProps) {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const telegramStatus = useAppSelector(/* ... */);
|
||||
|
||||
return (
|
||||
<div className="step">
|
||||
<h2>Connect Your Accounts</h2>
|
||||
|
||||
{connectOptions.map((option) => (
|
||||
<ConnectionOption
|
||||
key={option.id}
|
||||
{...option}
|
||||
onClick={() => option.id === 'telegram' && setShowModal(true)}
|
||||
/>
|
||||
))}
|
||||
|
||||
<TelegramConnectionModal
|
||||
isOpen={showModal}
|
||||
onClose={() => setShowModal(false)}
|
||||
/>
|
||||
|
||||
<div className="actions">
|
||||
<button onClick={onBack}>Back</button>
|
||||
<button onClick={onNext}>Continue</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Settings Modal Routing
|
||||
|
||||
The settings modal overlays existing content using URL-based routing.
|
||||
|
||||
### Modal Detection
|
||||
```typescript
|
||||
// In SettingsModal.tsx
|
||||
const location = useLocation();
|
||||
const isOpen = location.pathname.startsWith('/settings');
|
||||
```
|
||||
|
||||
### Sub-Routes
|
||||
```
|
||||
/settings → SettingsHome (main menu)
|
||||
/settings/connections → ConnectionsPanel
|
||||
/settings/messaging → MessagingPanel (future)
|
||||
/settings/privacy → PrivacyPanel (future)
|
||||
/settings/profile → ProfilePanel (future)
|
||||
/settings/advanced → AdvancedPanel (future)
|
||||
/settings/billing → BillingPanel (future)
|
||||
```
|
||||
|
||||
### Navigation
|
||||
```typescript
|
||||
import { useSettingsNavigation } from './hooks/useSettingsNavigation';
|
||||
|
||||
function SettingsHome() {
|
||||
const { navigateTo, closeModal } = useSettingsNavigation();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SettingsMenuItem
|
||||
label="Connections"
|
||||
onClick={() => navigateTo('connections')}
|
||||
/>
|
||||
<button onClick={closeModal}>Close</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## HashRouter vs BrowserRouter
|
||||
|
||||
The app uses HashRouter for desktop compatibility:
|
||||
|
||||
```typescript
|
||||
// App.tsx
|
||||
import { HashRouter } from 'react-router-dom';
|
||||
|
||||
// URLs look like: app://localhost/#/home
|
||||
// Instead of: app://localhost/home
|
||||
```
|
||||
|
||||
**Why HashRouter:**
|
||||
1. Tauri deep links work with hash-based URLs
|
||||
2. No server configuration needed
|
||||
3. Works with file:// protocol
|
||||
4. Prevents 404 on direct URL access
|
||||
|
||||
## Deep Link Handling
|
||||
|
||||
Deep links are handled before routing:
|
||||
|
||||
```typescript
|
||||
// main.tsx
|
||||
import('./utils/desktopDeepLinkListener').then((m) => {
|
||||
m.setupDesktopDeepLinkListener().catch(console.error);
|
||||
});
|
||||
```
|
||||
|
||||
The listener intercepts `outsourced://auth?token=...` and:
|
||||
1. Exchanges token via Rust command
|
||||
2. Stores session in Redux
|
||||
3. Navigates to `/onboarding` or `/home`
|
||||
|
||||
## Navigation Patterns
|
||||
|
||||
### Programmatic Navigation
|
||||
```typescript
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Navigate to route
|
||||
navigate('/home');
|
||||
|
||||
// Replace history entry
|
||||
navigate('/login', { replace: true });
|
||||
|
||||
// Go back
|
||||
navigate(-1);
|
||||
```
|
||||
|
||||
### Link Component
|
||||
```typescript
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
<Link to="/settings">Settings</Link>
|
||||
```
|
||||
|
||||
### State Transfer
|
||||
```typescript
|
||||
// Pass state to route
|
||||
navigate('/details', { state: { itemId: 123 } });
|
||||
|
||||
// Receive state
|
||||
const location = useLocation();
|
||||
const { itemId } = location.state;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*Previous: [MCP System](./04-mcp-system.md) | Next: [Components](./06-components.md)*
|
||||
@@ -0,0 +1,511 @@
|
||||
# Components
|
||||
|
||||
Reusable React components organized by feature.
|
||||
|
||||
## Component Structure
|
||||
|
||||
```
|
||||
components/
|
||||
├── Route Guards
|
||||
│ ├── ProtectedRoute.tsx
|
||||
│ ├── PublicRoute.tsx
|
||||
│ └── DefaultRedirect.tsx
|
||||
│
|
||||
├── Authentication
|
||||
│ └── TelegramLoginButton.tsx
|
||||
│
|
||||
├── Connection Status
|
||||
│ ├── ConnectionIndicator.tsx
|
||||
│ ├── TelegramConnectionIndicator.tsx
|
||||
│ ├── TelegramConnectionModal.tsx
|
||||
│ └── GmailConnectionIndicator.tsx
|
||||
│
|
||||
├── Onboarding
|
||||
│ ├── ProgressIndicator.tsx
|
||||
│ └── LottieAnimation.tsx
|
||||
│
|
||||
├── Settings Modal (16 files)
|
||||
│ ├── SettingsModal.tsx
|
||||
│ ├── SettingsLayout.tsx
|
||||
│ ├── SettingsHome.tsx
|
||||
│ ├── panels/
|
||||
│ ├── components/
|
||||
│ └── hooks/
|
||||
│
|
||||
└── Development
|
||||
└── DesignSystemShowcase.tsx
|
||||
```
|
||||
|
||||
## Route Guard Components
|
||||
|
||||
### ProtectedRoute
|
||||
|
||||
Requires authentication and optionally onboarding.
|
||||
|
||||
```typescript
|
||||
interface ProtectedRouteProps {
|
||||
requireOnboarded?: boolean;
|
||||
}
|
||||
|
||||
// Usage in AppRoutes.tsx
|
||||
<Route element={<ProtectedRoute />}>
|
||||
<Route path="/onboarding/*" element={<Onboarding />} />
|
||||
</Route>
|
||||
|
||||
<Route element={<ProtectedRoute requireOnboarded />}>
|
||||
<Route path="/home" element={<Home />} />
|
||||
</Route>
|
||||
```
|
||||
|
||||
### PublicRoute
|
||||
|
||||
Redirects authenticated users away.
|
||||
|
||||
```typescript
|
||||
// Usage in AppRoutes.tsx
|
||||
<Route element={<PublicRoute />}>
|
||||
<Route path="/" element={<Welcome />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
</Route>
|
||||
```
|
||||
|
||||
### DefaultRedirect
|
||||
|
||||
Fallback that routes based on auth state.
|
||||
|
||||
```typescript
|
||||
// Redirects to:
|
||||
// - "/" if not authenticated
|
||||
// - "/onboarding" if authenticated but not onboarded
|
||||
// - "/home" if authenticated and onboarded
|
||||
```
|
||||
|
||||
## Authentication Components
|
||||
|
||||
### TelegramLoginButton
|
||||
|
||||
OAuth login button for Telegram.
|
||||
|
||||
```typescript
|
||||
interface TelegramLoginButtonProps {
|
||||
onClick: () => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
// Usage
|
||||
<TelegramLoginButton
|
||||
onClick={() => openUrl(`${BACKEND_URL}/auth/telegram?platform=desktop`)}
|
||||
/>
|
||||
```
|
||||
|
||||
## Connection Status Components
|
||||
|
||||
### ConnectionIndicator
|
||||
|
||||
Generic connection status badge.
|
||||
|
||||
```typescript
|
||||
interface ConnectionIndicatorProps {
|
||||
status: 'connected' | 'connecting' | 'disconnected' | 'error';
|
||||
label?: string;
|
||||
}
|
||||
|
||||
<ConnectionIndicator status="connected" label="Socket" />
|
||||
```
|
||||
|
||||
### TelegramConnectionIndicator
|
||||
|
||||
Telegram-specific status display.
|
||||
|
||||
```typescript
|
||||
interface TelegramConnectionIndicatorProps {
|
||||
status: 'connected' | 'connecting' | 'disconnected' | 'error';
|
||||
}
|
||||
|
||||
// Usage with Redux state
|
||||
const telegramStatus = useAppSelector((state) =>
|
||||
selectTelegramConnectionStatus(state, userId)
|
||||
);
|
||||
|
||||
<TelegramConnectionIndicator status={telegramStatus} />
|
||||
```
|
||||
|
||||
### TelegramConnectionModal
|
||||
|
||||
Modal for setting up Telegram connection.
|
||||
|
||||
```typescript
|
||||
interface TelegramConnectionModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
// Usage in onboarding/settings
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
|
||||
<TelegramConnectionModal
|
||||
isOpen={showModal}
|
||||
onClose={() => setShowModal(false)}
|
||||
/>
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- QR code login flow
|
||||
- Phone number login flow
|
||||
- Connection status display
|
||||
- Error handling
|
||||
|
||||
### GmailConnectionIndicator
|
||||
|
||||
Gmail status badge (future integration).
|
||||
|
||||
```typescript
|
||||
<GmailConnectionIndicator status="coming-soon" />
|
||||
```
|
||||
|
||||
## Onboarding Components
|
||||
|
||||
### ProgressIndicator
|
||||
|
||||
Visual progress through onboarding steps.
|
||||
|
||||
```typescript
|
||||
interface ProgressIndicatorProps {
|
||||
current: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
<ProgressIndicator current={2} total={5} />
|
||||
```
|
||||
|
||||
### LottieAnimation
|
||||
|
||||
Lottie animation player for onboarding.
|
||||
|
||||
```typescript
|
||||
interface LottieAnimationProps {
|
||||
animationData: object;
|
||||
loop?: boolean;
|
||||
autoplay?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
import welcomeAnimation from '../assets/animations/welcome.json';
|
||||
|
||||
<LottieAnimation
|
||||
animationData={welcomeAnimation}
|
||||
loop={true}
|
||||
autoplay={true}
|
||||
/>
|
||||
```
|
||||
|
||||
## Settings Modal System
|
||||
|
||||
Complete modal system with URL-based routing.
|
||||
|
||||
### File Structure
|
||||
```
|
||||
components/settings/
|
||||
├── SettingsModal.tsx # Route-based container
|
||||
├── SettingsLayout.tsx # Portal + backdrop wrapper
|
||||
├── SettingsHome.tsx # Main menu with profile
|
||||
├── panels/
|
||||
│ ├── ConnectionsPanel.tsx # Connection management
|
||||
│ ├── MessagingPanel.tsx # (Future)
|
||||
│ ├── PrivacyPanel.tsx # (Future)
|
||||
│ ├── ProfilePanel.tsx # (Future)
|
||||
│ ├── AdvancedPanel.tsx # (Future)
|
||||
│ └── BillingPanel.tsx # (Future)
|
||||
├── components/
|
||||
│ ├── SettingsHeader.tsx # User profile section
|
||||
│ ├── SettingsMenuItem.tsx # Menu item component
|
||||
│ ├── SettingsBackButton.tsx # Back navigation
|
||||
│ └── SettingsPanelLayout.tsx# Panel wrapper
|
||||
└── hooks/
|
||||
├── useSettingsNavigation.ts # URL routing
|
||||
└── useSettingsAnimation.ts # Animation state
|
||||
```
|
||||
|
||||
### SettingsModal
|
||||
|
||||
Main container that renders based on URL.
|
||||
|
||||
```typescript
|
||||
export function SettingsModal() {
|
||||
const location = useLocation();
|
||||
const isOpen = location.pathname.startsWith('/settings');
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<SettingsLayout>
|
||||
{/* Route to appropriate panel */}
|
||||
{location.pathname === '/settings' && <SettingsHome />}
|
||||
{location.pathname === '/settings/connections' && <ConnectionsPanel />}
|
||||
{/* ... more panels */}
|
||||
</SettingsLayout>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### SettingsLayout
|
||||
|
||||
Portal-based modal wrapper.
|
||||
|
||||
```typescript
|
||||
export function SettingsLayout({ children }) {
|
||||
const { closeModal } = useSettingsNavigation();
|
||||
|
||||
return createPortal(
|
||||
<div className="fixed inset-0 z-50">
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
|
||||
onClick={closeModal}
|
||||
/>
|
||||
|
||||
{/* Modal */}
|
||||
<div className="absolute inset-4 flex items-center justify-center">
|
||||
<div className="bg-white rounded-2xl w-full max-w-[520px] shadow-xl">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### SettingsHome
|
||||
|
||||
Main menu with user profile.
|
||||
|
||||
```typescript
|
||||
export function SettingsHome() {
|
||||
const { navigateTo, closeModal } = useSettingsNavigation();
|
||||
const user = useAppSelector((state) => state.user.profile);
|
||||
|
||||
const menuItems = [
|
||||
{ id: 'connections', label: 'Connections', icon: LinkIcon },
|
||||
{ id: 'messaging', label: 'Messaging', icon: MessageIcon },
|
||||
{ id: 'privacy', label: 'Privacy', icon: ShieldIcon },
|
||||
// ... more items
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SettingsHeader user={user} onClose={closeModal} />
|
||||
|
||||
{menuItems.map((item) => (
|
||||
<SettingsMenuItem
|
||||
key={item.id}
|
||||
{...item}
|
||||
onClick={() => navigateTo(item.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### ConnectionsPanel
|
||||
|
||||
Connection management interface.
|
||||
|
||||
```typescript
|
||||
export function ConnectionsPanel() {
|
||||
const { navigateBack } = useSettingsNavigation();
|
||||
const [telegramModalOpen, setTelegramModalOpen] = useState(false);
|
||||
|
||||
const telegramStatus = useAppSelector((state) =>
|
||||
selectTelegramConnectionStatus(state, userId)
|
||||
);
|
||||
|
||||
// Reuses connectOptions from onboarding
|
||||
const connections = connectOptions.map((opt) => ({
|
||||
...opt,
|
||||
status: opt.id === 'telegram' ? telegramStatus : 'coming-soon'
|
||||
}));
|
||||
|
||||
return (
|
||||
<SettingsPanelLayout title="Connections" onBack={navigateBack}>
|
||||
{connections.map((conn) => (
|
||||
<ConnectionItem
|
||||
key={conn.id}
|
||||
{...conn}
|
||||
onConnect={() => conn.id === 'telegram' && setTelegramModalOpen(true)}
|
||||
/>
|
||||
))}
|
||||
|
||||
<TelegramConnectionModal
|
||||
isOpen={telegramModalOpen}
|
||||
onClose={() => setTelegramModalOpen(false)}
|
||||
/>
|
||||
</SettingsPanelLayout>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Settings Hooks
|
||||
|
||||
#### useSettingsNavigation
|
||||
|
||||
URL-based navigation for settings modal.
|
||||
|
||||
```typescript
|
||||
interface UseSettingsNavigationReturn {
|
||||
currentRoute: string;
|
||||
navigateTo: (panel: string) => void;
|
||||
navigateBack: () => void;
|
||||
closeModal: () => void;
|
||||
}
|
||||
|
||||
const { navigateTo, navigateBack, closeModal } = useSettingsNavigation();
|
||||
|
||||
// Navigate to panel
|
||||
navigateTo('connections'); // → /settings/connections
|
||||
|
||||
// Go back
|
||||
navigateBack(); // → /settings
|
||||
|
||||
// Close modal
|
||||
closeModal(); // → previous non-settings route
|
||||
```
|
||||
|
||||
#### useSettingsAnimation
|
||||
|
||||
Animation state management.
|
||||
|
||||
```typescript
|
||||
interface UseSettingsAnimationReturn {
|
||||
isEntering: boolean;
|
||||
isExiting: boolean;
|
||||
animationClass: string;
|
||||
}
|
||||
|
||||
const { animationClass } = useSettingsAnimation();
|
||||
|
||||
<div className={`modal ${animationClass}`}>
|
||||
{/* Content */}
|
||||
</div>
|
||||
```
|
||||
|
||||
### Settings Components
|
||||
|
||||
#### SettingsHeader
|
||||
|
||||
User profile section at top of settings.
|
||||
|
||||
```typescript
|
||||
interface SettingsHeaderProps {
|
||||
user: User | null;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
<SettingsHeader user={user} onClose={handleClose} />
|
||||
```
|
||||
|
||||
#### SettingsMenuItem
|
||||
|
||||
Individual menu item with icon and chevron.
|
||||
|
||||
```typescript
|
||||
interface SettingsMenuItemProps {
|
||||
label: string;
|
||||
icon: React.ComponentType;
|
||||
onClick: () => void;
|
||||
badge?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
<SettingsMenuItem
|
||||
label="Connections"
|
||||
icon={LinkIcon}
|
||||
onClick={() => navigateTo('connections')}
|
||||
badge="2"
|
||||
/>
|
||||
```
|
||||
|
||||
#### SettingsBackButton
|
||||
|
||||
Back navigation button.
|
||||
|
||||
```typescript
|
||||
interface SettingsBackButtonProps {
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
<SettingsBackButton onClick={navigateBack} />
|
||||
```
|
||||
|
||||
#### SettingsPanelLayout
|
||||
|
||||
Wrapper for settings panels.
|
||||
|
||||
```typescript
|
||||
interface SettingsPanelLayoutProps {
|
||||
title: string;
|
||||
onBack: () => void;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
<SettingsPanelLayout title="Connections" onBack={navigateBack}>
|
||||
{/* Panel content */}
|
||||
</SettingsPanelLayout>
|
||||
```
|
||||
|
||||
## Component Patterns
|
||||
|
||||
### Reusing Connection Options
|
||||
|
||||
The `connectOptions` array is shared between onboarding and settings:
|
||||
|
||||
```typescript
|
||||
// Defined in ConnectStep.tsx, imported elsewhere
|
||||
export const connectOptions = [
|
||||
{
|
||||
id: 'telegram',
|
||||
label: 'Telegram',
|
||||
icon: TelegramIcon,
|
||||
description: 'Connect your Telegram account'
|
||||
},
|
||||
{
|
||||
id: 'gmail',
|
||||
label: 'Gmail',
|
||||
icon: GmailIcon,
|
||||
description: 'Connect your Gmail account',
|
||||
comingSoon: true
|
||||
}
|
||||
];
|
||||
```
|
||||
|
||||
### Modal via Portal
|
||||
|
||||
Settings modal uses `createPortal` to render outside the component tree:
|
||||
|
||||
```typescript
|
||||
return createPortal(
|
||||
<div className="modal-container">
|
||||
{/* Modal content */}
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
```
|
||||
|
||||
### Controlled vs Uncontrolled
|
||||
|
||||
Connection modals are controlled components:
|
||||
|
||||
```typescript
|
||||
// Parent controls open state
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
<TelegramConnectionModal
|
||||
isOpen={isOpen}
|
||||
onClose={() => setIsOpen(false)}
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*Previous: [Pages & Routing](./05-pages-routing.md) | Next: [Providers](./07-providers.md)*
|
||||
@@ -0,0 +1,404 @@
|
||||
# Providers
|
||||
|
||||
React context providers manage service lifecycle and provide shared state.
|
||||
|
||||
## Provider Chain
|
||||
|
||||
The providers wrap the application in a specific order:
|
||||
|
||||
```typescript
|
||||
// App.tsx
|
||||
<Provider store={store}>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<UserProvider>
|
||||
<SocketProvider>
|
||||
<TelegramProvider>
|
||||
<HashRouter>
|
||||
<AppRoutes />
|
||||
</HashRouter>
|
||||
</TelegramProvider>
|
||||
</SocketProvider>
|
||||
</UserProvider>
|
||||
</PersistGate>
|
||||
</Provider>
|
||||
```
|
||||
|
||||
**Order matters because:**
|
||||
1. Redux must be outermost for state access
|
||||
2. PersistGate rehydrates state before rendering children
|
||||
3. SocketProvider depends on Redux auth token
|
||||
4. TelegramProvider depends on Redux telegram state
|
||||
5. HashRouter provides navigation to all routes
|
||||
|
||||
## SocketProvider (`providers/SocketProvider.tsx`)
|
||||
|
||||
Manages Socket.io connection lifecycle and MCP initialization.
|
||||
|
||||
### Responsibilities
|
||||
- Auto-connect when auth token is available
|
||||
- Auto-disconnect when token is cleared
|
||||
- Initialize MCP server when socket connects
|
||||
- Update Redux with connection status
|
||||
|
||||
### Implementation
|
||||
|
||||
```typescript
|
||||
interface SocketContextValue {
|
||||
socket: Socket | null;
|
||||
isConnected: boolean;
|
||||
emit: (event: string, data: unknown) => void;
|
||||
on: (event: string, handler: Function) => void;
|
||||
off: (event: string, handler: Function) => void;
|
||||
}
|
||||
|
||||
export function SocketProvider({ children }) {
|
||||
const token = useAppSelector((state) => state.auth.token);
|
||||
const userId = useAppSelector((state) => state.user.profile?.id);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
if (!token || !userId) {
|
||||
socketService.disconnect();
|
||||
dispatch(setSocketStatus({ userId, status: 'disconnected' }));
|
||||
return;
|
||||
}
|
||||
|
||||
// Connect with auth token
|
||||
socketService.connect(token);
|
||||
dispatch(setSocketStatus({ userId, status: 'connecting' }));
|
||||
|
||||
// Handle connection events
|
||||
socketService.on('connect', () => {
|
||||
dispatch(setSocketStatus({ userId, status: 'connected' }));
|
||||
dispatch(setSocketId({ userId, socketId: socketService.getSocket()?.id }));
|
||||
|
||||
// Initialize MCP server
|
||||
initMCPServer(socketService.getSocket());
|
||||
});
|
||||
|
||||
socketService.on('disconnect', () => {
|
||||
dispatch(setSocketStatus({ userId, status: 'disconnected' }));
|
||||
cleanupMCP();
|
||||
});
|
||||
|
||||
socketService.on('connect_error', (error) => {
|
||||
console.error('Socket connection error:', error);
|
||||
dispatch(setSocketStatus({ userId, status: 'disconnected' }));
|
||||
});
|
||||
|
||||
return () => {
|
||||
socketService.disconnect();
|
||||
cleanupMCP();
|
||||
};
|
||||
}, [token, userId]);
|
||||
|
||||
const contextValue: SocketContextValue = {
|
||||
socket: socketService.getSocket(),
|
||||
isConnected: socketService.isConnected(),
|
||||
emit: socketService.emit.bind(socketService),
|
||||
on: socketService.on.bind(socketService),
|
||||
off: socketService.off.bind(socketService)
|
||||
};
|
||||
|
||||
return (
|
||||
<SocketContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</SocketContext.Provider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
```typescript
|
||||
import { useSocket } from '../providers/SocketProvider';
|
||||
|
||||
function MyComponent() {
|
||||
const { socket, isConnected, emit, on, off } = useSocket();
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (data) => console.log('Received:', data);
|
||||
on('event-name', handler);
|
||||
return () => off('event-name', handler);
|
||||
}, [on, off]);
|
||||
|
||||
const sendMessage = () => {
|
||||
emit('send-message', { text: 'Hello!' });
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span>Status: {isConnected ? 'Connected' : 'Disconnected'}</span>
|
||||
<button onClick={sendMessage}>Send</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## TelegramProvider (`providers/TelegramProvider.tsx`)
|
||||
|
||||
Manages Telegram MTProto connection lifecycle.
|
||||
|
||||
### Responsibilities
|
||||
- Initialize MTProto client when user is authenticated
|
||||
- Connect to Telegram servers
|
||||
- Store session string in Redux
|
||||
- Provide Telegram context to children
|
||||
|
||||
### Implementation
|
||||
|
||||
```typescript
|
||||
interface TelegramContextValue {
|
||||
client: TelegramClient | null;
|
||||
connectionStatus: ConnectionStatus;
|
||||
authStatus: AuthStatus;
|
||||
connect: () => Promise<void>;
|
||||
disconnect: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function TelegramProvider({ children }) {
|
||||
const dispatch = useAppDispatch();
|
||||
const userId = useAppSelector((state) => state.user.profile?.id);
|
||||
const telegramState = useAppSelector((state) =>
|
||||
state.telegram.byUser[userId]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!userId) return;
|
||||
|
||||
// Parallel initialization for faster startup
|
||||
const init = async () => {
|
||||
try {
|
||||
// Initialize and connect in parallel
|
||||
await Promise.all([
|
||||
dispatch(initializeTelegram(userId)).unwrap(),
|
||||
dispatch(connectTelegram(userId)).unwrap()
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error('Telegram initialization failed:', error);
|
||||
}
|
||||
};
|
||||
|
||||
init();
|
||||
|
||||
return () => {
|
||||
dispatch(disconnectTelegram(userId));
|
||||
};
|
||||
}, [userId]);
|
||||
|
||||
// Restore session from persisted state
|
||||
useEffect(() => {
|
||||
if (telegramState?.sessionString) {
|
||||
const client = mtprotoService.getInstance().getClient();
|
||||
if (client) {
|
||||
client.setSession(telegramState.sessionString);
|
||||
}
|
||||
}
|
||||
}, [telegramState?.sessionString]);
|
||||
|
||||
const contextValue: TelegramContextValue = {
|
||||
client: mtprotoService.getInstance().getClient(),
|
||||
connectionStatus: telegramState?.connectionStatus || 'disconnected',
|
||||
authStatus: telegramState?.authStatus || 'not_authenticated',
|
||||
connect: () => dispatch(connectTelegram(userId)).unwrap(),
|
||||
disconnect: () => dispatch(disconnectTelegram(userId)).unwrap()
|
||||
};
|
||||
|
||||
return (
|
||||
<TelegramContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</TelegramContext.Provider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
```typescript
|
||||
import { useTelegram } from '../providers/TelegramProvider';
|
||||
|
||||
function ChatList() {
|
||||
const { client, connectionStatus, authStatus } = useTelegram();
|
||||
const [chats, setChats] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (connectionStatus === 'connected' && authStatus === 'authenticated') {
|
||||
const fetchChats = async () => {
|
||||
const dialogs = await client.getDialogs({ limit: 20 });
|
||||
setChats(dialogs);
|
||||
};
|
||||
fetchChats();
|
||||
}
|
||||
}, [client, connectionStatus, authStatus]);
|
||||
|
||||
if (connectionStatus !== 'connected') {
|
||||
return <div>Connecting to Telegram...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<ul>
|
||||
{chats.map((chat) => (
|
||||
<li key={chat.id}>{chat.title}</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## UserProvider (`providers/UserProvider.tsx`)
|
||||
|
||||
Minimal user context provider (most user state is in Redux).
|
||||
|
||||
### Responsibilities
|
||||
- Legacy user context for compatibility
|
||||
- May be deprecated in favor of Redux
|
||||
|
||||
### Implementation
|
||||
|
||||
```typescript
|
||||
interface UserContextValue {
|
||||
user: User | null;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export function UserProvider({ children }) {
|
||||
const user = useAppSelector((state) => state.user.profile);
|
||||
const loading = useAppSelector((state) => state.user.loading);
|
||||
|
||||
return (
|
||||
<UserContext.Provider value={{ user, loading }}>
|
||||
{children}
|
||||
</UserContext.Provider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
```typescript
|
||||
import { useUserContext } from '../providers/UserProvider';
|
||||
|
||||
function Header() {
|
||||
const { user, loading } = useUserContext();
|
||||
|
||||
if (loading) return <Skeleton />;
|
||||
if (!user) return null;
|
||||
|
||||
return <span>Welcome, {user.firstName}</span>;
|
||||
}
|
||||
```
|
||||
|
||||
## Provider Patterns
|
||||
|
||||
### Effect-Based Lifecycle
|
||||
|
||||
Providers use `useEffect` to manage service lifecycle:
|
||||
|
||||
```typescript
|
||||
useEffect(() => {
|
||||
// Setup on mount or dependency change
|
||||
service.connect();
|
||||
|
||||
// Cleanup on unmount or dependency change
|
||||
return () => {
|
||||
service.disconnect();
|
||||
};
|
||||
}, [dependencies]);
|
||||
```
|
||||
|
||||
### Redux Integration
|
||||
|
||||
Providers read from and dispatch to Redux:
|
||||
|
||||
```typescript
|
||||
// Read state
|
||||
const token = useAppSelector((state) => state.auth.token);
|
||||
|
||||
// Dispatch actions
|
||||
const dispatch = useAppDispatch();
|
||||
dispatch(setStatus({ userId, status: 'connected' }));
|
||||
```
|
||||
|
||||
### Parallel Initialization
|
||||
|
||||
TelegramProvider runs init and connect in parallel:
|
||||
|
||||
```typescript
|
||||
await Promise.all([
|
||||
dispatch(initializeTelegram(userId)).unwrap(),
|
||||
dispatch(connectTelegram(userId)).unwrap()
|
||||
]);
|
||||
```
|
||||
|
||||
This reduces startup time compared to sequential operations.
|
||||
|
||||
### Session Restoration
|
||||
|
||||
Providers restore persisted state on mount:
|
||||
|
||||
```typescript
|
||||
useEffect(() => {
|
||||
if (persistedSession) {
|
||||
service.restoreSession(persistedSession);
|
||||
}
|
||||
}, [persistedSession]);
|
||||
```
|
||||
|
||||
## Context vs Redux
|
||||
|
||||
| Use Context For | Use Redux For |
|
||||
|-----------------|---------------|
|
||||
| Service instances (socket, client) | Serializable state (status, data) |
|
||||
| Methods (emit, on, off) | Persisted state (sessions, tokens) |
|
||||
| Derived values | Complex state logic |
|
||||
|
||||
Example:
|
||||
- `SocketContext` provides `socket` instance and `emit` method
|
||||
- Redux stores `socketStatus` and `socketId`
|
||||
|
||||
## Testing Providers
|
||||
|
||||
### Mock Provider for Tests
|
||||
|
||||
```typescript
|
||||
// test-utils.tsx
|
||||
const mockSocketContext: SocketContextValue = {
|
||||
socket: null,
|
||||
isConnected: true,
|
||||
emit: jest.fn(),
|
||||
on: jest.fn(),
|
||||
off: jest.fn()
|
||||
};
|
||||
|
||||
export function TestProviders({ children }) {
|
||||
return (
|
||||
<Provider store={testStore}>
|
||||
<SocketContext.Provider value={mockSocketContext}>
|
||||
{children}
|
||||
</SocketContext.Provider>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Testing Provider Effects
|
||||
|
||||
```typescript
|
||||
test('SocketProvider connects when token is available', () => {
|
||||
const store = createTestStore({ auth: { token: 'test-token' } });
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<SocketProvider>
|
||||
<TestComponent />
|
||||
</SocketProvider>
|
||||
</Provider>
|
||||
);
|
||||
|
||||
expect(socketService.connect).toHaveBeenCalledWith('test-token');
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*Previous: [Components](./06-components.md) | Next: [Hooks & Utils](./08-hooks-utils.md)*
|
||||
@@ -0,0 +1,505 @@
|
||||
# Hooks & Utilities
|
||||
|
||||
Custom React hooks and utility functions.
|
||||
|
||||
## Custom Hooks
|
||||
|
||||
### useSocket (`hooks/useSocket.ts`)
|
||||
|
||||
Access Socket.io functionality from any component.
|
||||
|
||||
```typescript
|
||||
interface UseSocketReturn {
|
||||
socket: Socket | null;
|
||||
isConnected: boolean;
|
||||
emit: (event: string, data: unknown) => void;
|
||||
on: (event: string, handler: Function) => void;
|
||||
off: (event: string, handler: Function) => void;
|
||||
once: (event: string, handler: Function) => void;
|
||||
}
|
||||
|
||||
function useSocket(): UseSocketReturn;
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```typescript
|
||||
import { useSocket } from '../hooks/useSocket';
|
||||
|
||||
function ChatInput() {
|
||||
const { emit, isConnected } = useSocket();
|
||||
|
||||
const sendMessage = (text: string) => {
|
||||
if (isConnected) {
|
||||
emit('chat:message', { text });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<input
|
||||
disabled={!isConnected}
|
||||
onKeyDown={(e) => e.key === 'Enter' && sendMessage(e.target.value)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**With event listeners:**
|
||||
|
||||
```typescript
|
||||
function Notifications() {
|
||||
const { on, off } = useSocket();
|
||||
const [notifications, setNotifications] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (notification) => {
|
||||
setNotifications((prev) => [...prev, notification]);
|
||||
};
|
||||
|
||||
on('notification', handler);
|
||||
return () => off('notification', handler);
|
||||
}, [on, off]);
|
||||
|
||||
return <NotificationList items={notifications} />;
|
||||
}
|
||||
```
|
||||
|
||||
### useUser (`hooks/useUser.ts`)
|
||||
|
||||
Access user profile data and loading state.
|
||||
|
||||
```typescript
|
||||
interface UseUserReturn {
|
||||
user: User | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
refetch: () => Promise<void>;
|
||||
}
|
||||
|
||||
function useUser(): UseUserReturn;
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```typescript
|
||||
import { useUser } from '../hooks/useUser';
|
||||
|
||||
function ProfileHeader() {
|
||||
const { user, loading, error, refetch } = useUser();
|
||||
|
||||
if (loading) return <Skeleton />;
|
||||
if (error) return <Error message={error} onRetry={refetch} />;
|
||||
if (!user) return null;
|
||||
|
||||
return (
|
||||
<div className="profile">
|
||||
<Avatar src={user.avatar} />
|
||||
<span>{user.firstName} {user.lastName}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Settings Modal Hooks
|
||||
|
||||
#### useSettingsNavigation (`components/settings/hooks/useSettingsNavigation.ts`)
|
||||
|
||||
URL-based navigation for settings modal.
|
||||
|
||||
```typescript
|
||||
interface UseSettingsNavigationReturn {
|
||||
currentRoute: string; // Current settings path
|
||||
navigateTo: (panel: string) => void; // Navigate to panel
|
||||
navigateBack: () => void; // Go back one level
|
||||
closeModal: () => void; // Close settings entirely
|
||||
}
|
||||
|
||||
function useSettingsNavigation(): UseSettingsNavigationReturn;
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```typescript
|
||||
import { useSettingsNavigation } from './hooks/useSettingsNavigation';
|
||||
|
||||
function SettingsMenu() {
|
||||
const { navigateTo, closeModal } = useSettingsNavigation();
|
||||
|
||||
return (
|
||||
<nav>
|
||||
<button onClick={() => navigateTo('connections')}>
|
||||
Connections
|
||||
</button>
|
||||
<button onClick={() => navigateTo('privacy')}>
|
||||
Privacy
|
||||
</button>
|
||||
<button onClick={closeModal}>Close</button>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### useSettingsAnimation (`components/settings/hooks/useSettingsAnimation.ts`)
|
||||
|
||||
Animation state management for settings modal.
|
||||
|
||||
```typescript
|
||||
interface UseSettingsAnimationReturn {
|
||||
isEntering: boolean; // Modal is animating in
|
||||
isExiting: boolean; // Modal is animating out
|
||||
animationClass: string; // CSS class for current state
|
||||
}
|
||||
|
||||
function useSettingsAnimation(): UseSettingsAnimationReturn;
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```typescript
|
||||
import { useSettingsAnimation } from './hooks/useSettingsAnimation';
|
||||
|
||||
function SettingsModal() {
|
||||
const { animationClass, isExiting } = useSettingsAnimation();
|
||||
|
||||
return (
|
||||
<div className={`modal ${animationClass}`}>
|
||||
{/* Content */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Utilities
|
||||
|
||||
### Configuration (`utils/config.ts`)
|
||||
|
||||
Environment variable access with defaults.
|
||||
|
||||
```typescript
|
||||
// Backend URL
|
||||
export const BACKEND_URL =
|
||||
import.meta.env.VITE_BACKEND_URL || 'https://api.example.com';
|
||||
|
||||
// Telegram configuration
|
||||
export const TELEGRAM_API_ID = import.meta.env.VITE_TELEGRAM_API_ID;
|
||||
export const TELEGRAM_API_HASH = import.meta.env.VITE_TELEGRAM_API_HASH;
|
||||
export const TELEGRAM_BOT_USERNAME = import.meta.env.VITE_TELEGRAM_BOT_USERNAME;
|
||||
export const TELEGRAM_BOT_ID = import.meta.env.VITE_TELEGRAM_BOT_ID;
|
||||
|
||||
// Debug mode
|
||||
export const DEBUG = import.meta.env.VITE_DEBUG === 'true';
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```typescript
|
||||
import { BACKEND_URL, DEBUG } from '../utils/config';
|
||||
|
||||
const response = await fetch(`${BACKEND_URL}/api/users`);
|
||||
|
||||
if (DEBUG) {
|
||||
console.log('Response:', response);
|
||||
}
|
||||
```
|
||||
|
||||
### Deep Link (`utils/deeplink.ts`)
|
||||
|
||||
Build deep link URLs for authentication handoff.
|
||||
|
||||
```typescript
|
||||
// Build auth deep link
|
||||
function buildAuthDeepLink(token: string): string;
|
||||
|
||||
// Parse deep link URL
|
||||
function parseDeepLink(url: string): { path: string; params: URLSearchParams };
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```typescript
|
||||
import { buildAuthDeepLink } from '../utils/deeplink';
|
||||
|
||||
// Build URL for browser redirect
|
||||
const deepLink = buildAuthDeepLink(loginToken);
|
||||
// → "outsourced://auth?token=abc123"
|
||||
|
||||
// In web frontend after auth:
|
||||
window.location.href = deepLink;
|
||||
```
|
||||
|
||||
### Desktop Deep Link Listener (`utils/desktopDeepLinkListener.ts`)
|
||||
|
||||
Handle incoming deep links in desktop app.
|
||||
|
||||
```typescript
|
||||
// Setup listener for deep link events
|
||||
async function setupDesktopDeepLinkListener(): Promise<void>;
|
||||
```
|
||||
|
||||
**Called in main.tsx:**
|
||||
|
||||
```typescript
|
||||
// Lazy import to ensure Tauri IPC is ready
|
||||
import('./utils/desktopDeepLinkListener').then((m) => {
|
||||
m.setupDesktopDeepLinkListener().catch(console.error);
|
||||
});
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
1. Listens for `onOpenUrl` events from Tauri deep-link plugin
|
||||
2. Parses `outsourced://auth?token=...` URLs
|
||||
3. Calls Rust `exchange_token` command (bypasses CORS)
|
||||
4. Stores session in Redux
|
||||
5. Navigates to `/onboarding` or `/home`
|
||||
|
||||
**Loop prevention:**
|
||||
```typescript
|
||||
// Set flag before navigation to prevent reprocessing
|
||||
localStorage.setItem('deepLinkHandled', 'true');
|
||||
window.location.replace('/');
|
||||
|
||||
// On next load, clear flag
|
||||
if (localStorage.getItem('deepLinkHandled') === 'true') {
|
||||
localStorage.removeItem('deepLinkHandled');
|
||||
return; // Don't process again
|
||||
}
|
||||
```
|
||||
|
||||
### URL Opener (`utils/openUrl.ts`)
|
||||
|
||||
Cross-platform URL opening.
|
||||
|
||||
```typescript
|
||||
// Open URL in system browser
|
||||
async function openUrl(url: string): Promise<void>;
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```typescript
|
||||
import { openUrl } from '../utils/openUrl';
|
||||
|
||||
// Opens in system browser (not in-app WebView)
|
||||
await openUrl('https://telegram.org/auth');
|
||||
```
|
||||
|
||||
**Implementation:**
|
||||
```typescript
|
||||
export async function openUrl(url: string): Promise<void> {
|
||||
try {
|
||||
// Try Tauri opener plugin first
|
||||
const { open } = await import('@tauri-apps/plugin-opener');
|
||||
await open(url);
|
||||
} catch {
|
||||
// Fallback to browser API
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Polyfills (`polyfills.ts`)
|
||||
|
||||
Node.js polyfills for browser environment.
|
||||
|
||||
The `telegram` npm package requires Node.js APIs. These are polyfilled:
|
||||
|
||||
```typescript
|
||||
// polyfills.ts
|
||||
import { Buffer } from 'buffer';
|
||||
import process from 'process';
|
||||
import util from 'util';
|
||||
|
||||
window.Buffer = Buffer;
|
||||
window.process = process;
|
||||
window.util = util;
|
||||
```
|
||||
|
||||
**Imported at app entry:**
|
||||
|
||||
```typescript
|
||||
// main.tsx
|
||||
import './polyfills';
|
||||
// ... rest of app
|
||||
```
|
||||
|
||||
**Vite configuration:**
|
||||
|
||||
```typescript
|
||||
// vite.config.ts
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
buffer: 'buffer',
|
||||
process: 'process/browser',
|
||||
util: 'util'
|
||||
}
|
||||
},
|
||||
define: {
|
||||
'process.env': {},
|
||||
global: 'globalThis'
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Types
|
||||
|
||||
### API Types (`types/api.ts`)
|
||||
|
||||
```typescript
|
||||
// API response wrapper
|
||||
interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// API error
|
||||
interface ApiError {
|
||||
code: string;
|
||||
message: string;
|
||||
details?: unknown;
|
||||
}
|
||||
|
||||
// User interface
|
||||
interface User {
|
||||
id: string;
|
||||
firstName: string;
|
||||
lastName?: string;
|
||||
username?: string;
|
||||
email?: string;
|
||||
avatar?: string;
|
||||
telegramId?: string;
|
||||
subscription?: SubscriptionInfo;
|
||||
usage?: UsageInfo;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
```
|
||||
|
||||
### Onboarding Types (`types/onboarding.ts`)
|
||||
|
||||
```typescript
|
||||
// Onboarding step definition
|
||||
interface OnboardingStep {
|
||||
id: string;
|
||||
title: string;
|
||||
component: React.ComponentType<StepProps>;
|
||||
}
|
||||
|
||||
// Step component props
|
||||
interface StepProps {
|
||||
onNext: () => void;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
// Connection option
|
||||
interface ConnectionOption {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: React.ComponentType;
|
||||
description: string;
|
||||
comingSoon?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
## Static Data
|
||||
|
||||
### Countries (`data/countries.ts`)
|
||||
|
||||
Country list for phone number input.
|
||||
|
||||
```typescript
|
||||
interface Country {
|
||||
code: string; // "US"
|
||||
name: string; // "United States"
|
||||
dialCode: string; // "+1"
|
||||
flag: string; // "🇺🇸"
|
||||
}
|
||||
|
||||
export const countries: Country[];
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```typescript
|
||||
import { countries } from '../data/countries';
|
||||
|
||||
function PhoneInput() {
|
||||
const [country, setCountry] = useState(countries[0]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<select
|
||||
value={country.code}
|
||||
onChange={(e) => setCountry(
|
||||
countries.find((c) => c.code === e.target.value)
|
||||
)}
|
||||
>
|
||||
{countries.map((c) => (
|
||||
<option key={c.code} value={c.code}>
|
||||
{c.flag} {c.name} ({c.dialCode})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<input placeholder="Phone number" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Hook Dependencies
|
||||
|
||||
Always include dependencies in useEffect:
|
||||
|
||||
```typescript
|
||||
// Good
|
||||
useEffect(() => {
|
||||
on('event', handler);
|
||||
return () => off('event', handler);
|
||||
}, [on, off, handler]);
|
||||
|
||||
// Bad - missing dependencies
|
||||
useEffect(() => {
|
||||
on('event', handler);
|
||||
return () => off('event', handler);
|
||||
}, []);
|
||||
```
|
||||
|
||||
### Cleanup Functions
|
||||
|
||||
Always clean up subscriptions:
|
||||
|
||||
```typescript
|
||||
useEffect(() => {
|
||||
const subscription = subscribe();
|
||||
return () => subscription.unsubscribe();
|
||||
}, []);
|
||||
```
|
||||
|
||||
### Error Boundaries
|
||||
|
||||
Wrap utility calls in try-catch:
|
||||
|
||||
```typescript
|
||||
try {
|
||||
await openUrl(url);
|
||||
} catch (error) {
|
||||
console.error('Failed to open URL:', error);
|
||||
// Fallback behavior
|
||||
}
|
||||
```
|
||||
|
||||
### Type Safety
|
||||
|
||||
Use TypeScript generics for API calls:
|
||||
|
||||
```typescript
|
||||
const user = await apiClient.get<User>('/users/me');
|
||||
// user is typed as User
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*Previous: [Providers](./07-providers.md) | [Back to Index](./README.md)*
|
||||
@@ -0,0 +1,78 @@
|
||||
# Source Code Documentation
|
||||
|
||||
This documentation covers the `/src` folder structure of the Outsourced Crypto Community Platform.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Document | Description |
|
||||
|----------|-------------|
|
||||
| [Architecture Overview](./01-architecture.md) | High-level system architecture and provider chain |
|
||||
| [State Management](./02-state-management.md) | Redux Toolkit slices, persistence, and selectors |
|
||||
| [Services Layer](./03-services.md) | Singleton services (Socket.io, MTProto, API client) |
|
||||
| [MCP System](./04-mcp-system.md) | Model Context Protocol with 81 Telegram tools |
|
||||
| [Pages & Routing](./05-pages-routing.md) | Route definitions, guards, and page components |
|
||||
| [Components](./06-components.md) | Reusable UI components and settings modal system |
|
||||
| [Providers](./07-providers.md) | React context providers and lifecycle management |
|
||||
| [Hooks & Utils](./08-hooks-utils.md) | Custom hooks and utility functions |
|
||||
|
||||
## File Count Summary
|
||||
|
||||
| Category | Files | Purpose |
|
||||
|----------|-------|---------|
|
||||
| Entry & Configuration | 7 | App init, routing, styles, types |
|
||||
| State Management | 13 | Redux slices, selectors, hooks |
|
||||
| Providers | 3 | Socket, Telegram, User contexts |
|
||||
| Services | 5 | Singleton API clients |
|
||||
| Pages | 9 | Full-page route components |
|
||||
| Components | 28 | Reusable UI + settings modal (16 files) |
|
||||
| MCP Core | 14 | MCP interfaces, transport, logging |
|
||||
| MCP Telegram Tools | 81 | Individual Telegram API operations |
|
||||
| Hooks | 2 | Custom React hooks |
|
||||
| Types | 2 | TypeScript interfaces |
|
||||
| Utils | 4 | Config, deep link, URL utilities |
|
||||
| Data | 1 | Static data (countries) |
|
||||
| Assets | 10+ | Icons and images |
|
||||
| **TOTAL** | **171+** | Complete frontend application |
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── App.tsx # Root component with provider chain
|
||||
├── AppRoutes.tsx # Route definitions
|
||||
├── main.tsx # Entry point
|
||||
├── polyfills.ts # Node.js polyfills for browser
|
||||
├── index.css # Global styles
|
||||
│
|
||||
├── store/ # Redux state management (13 files)
|
||||
├── providers/ # Context providers (3 files)
|
||||
├── services/ # Singleton services (5 files)
|
||||
├── lib/mcp/ # MCP system (95+ files)
|
||||
├── pages/ # Page components (9 files)
|
||||
├── components/ # UI components (28 files)
|
||||
├── hooks/ # Custom hooks (2 files)
|
||||
├── types/ # TypeScript types (2 files)
|
||||
├── utils/ # Utilities (4 files)
|
||||
├── data/ # Static data (1 file)
|
||||
└── assets/ # Icons and images
|
||||
```
|
||||
|
||||
## Key Architectural Decisions
|
||||
|
||||
1. **HashRouter over BrowserRouter** - Required for Tauri deep link compatibility
|
||||
2. **Redux Toolkit with Persistence** - Robust state management with rehydration
|
||||
3. **Singleton Services** - Prevents connection leaks for Socket.io and MTProto
|
||||
4. **Per-User State Scoping** - Telegram/socket state keyed by user ID
|
||||
5. **Portal-Based Settings Modal** - URL routing without affecting main routes
|
||||
6. **81-Tool MCP System** - Comprehensive Telegram API coverage
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Read [Architecture Overview](./01-architecture.md) for the big picture
|
||||
2. Understand [State Management](./02-state-management.md) for data flow
|
||||
3. Review [Services Layer](./03-services.md) for backend communication
|
||||
4. Explore [MCP System](./04-mcp-system.md) for AI tool integration
|
||||
|
||||
---
|
||||
|
||||
*Documentation maintained by stevenbaba*
|
||||
Reference in New Issue
Block a user