144 lines
11 KiB
Markdown
144 lines
11 KiB
Markdown
# CLAUDE.md
|
|
|
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
|
|
**Update this file when making significant changes.**
|
|
|
|
See also: [CONVEX_RULES.md](./CONVEX_RULES.md) | [CONVEX_EXAMPLES.md](./CONVEX_EXAMPLES.md)
|
|
|
|
## Architecture
|
|
|
|
- **Monorepo**: npm workspaces (`packages/*`, `apps/*`)
|
|
- **Backend**: Convex (reactive database + serverless functions)
|
|
- **Frontend**: React + Vite, shared codebase in `packages/shared/`
|
|
- **Platforms**: Electron (`apps/electron/`), Web (`apps/web/`), Android via Capacitor (`apps/android/`)
|
|
- **Platform Abstraction**: `usePlatform()` hook provides crypto, session, settings, idle, links, screenCapture, windowControls, notifications, updates APIs
|
|
- **Auth**: Zero-knowledge custom auth via Convex mutations (getSalt, verifyUser, createUserWithProfile)
|
|
- **Real-time**: Convex reactive queries (`useQuery` auto-updates all connected clients)
|
|
- **Voice/Video**: LiveKit (token generation via Convex Node action)
|
|
- **E2E Encryption**: Platform-specific crypto (Electron: Node crypto via IPC, Web: Web Crypto API)
|
|
- **File Storage**: Convex built-in storage (`generateUploadUrl` + `getUrl`)
|
|
|
|
## Development Commands
|
|
|
|
```bash
|
|
# Install
|
|
npm install # Installs all workspaces
|
|
|
|
# Backend
|
|
npx convex dev # Start Convex dev server (creates .env.local)
|
|
|
|
# Frontend (run alongside backend)
|
|
npm run dev:web # Web app at localhost:5173
|
|
npm run dev:electron # Electron app (Vite + Electron concurrently)
|
|
|
|
# Production builds
|
|
npm run build:web # Web production build -> apps/web/dist
|
|
npm run build:electron # Electron build with electron-builder
|
|
npm run build:android # Web build + Capacitor sync
|
|
|
|
# Android
|
|
cd apps/android && npx cap sync && npx cap open android
|
|
|
|
# Preview
|
|
cd apps/web && npx vite preview # Preview web production build
|
|
```
|
|
|
|
**No test framework or linter is configured in this project.**
|
|
|
|
## Project Structure
|
|
|
|
```
|
|
Discord Clone/
|
|
├── convex/ # Backend (Convex functions + schema)
|
|
├── packages/
|
|
│ ├── shared/ # Shared React app (all components, pages, contexts, styles)
|
|
│ │ └── src/
|
|
│ │ ├── components/ # All UI components
|
|
│ │ ├── pages/ # Login, Register, Chat
|
|
│ │ ├── contexts/ # VoiceContext, ThemeContext, PresenceContext
|
|
│ │ ├── platform/ # PlatformProvider + usePlatform hook
|
|
│ │ ├── styles/ # themes.css
|
|
│ │ ├── utils/ # userPreferences.js, streamUtils.jsx
|
|
│ │ ├── assets/ # sounds, icons, emojis, fonts
|
|
│ │ ├── App.jsx # Router + AuthGuard
|
|
│ │ └── index.css # Global styles
|
|
│ └── platform-web/ # Web/Capacitor platform implementations
|
|
│ └── src/ # Web Crypto API, localStorage session/settings, Page Visibility idle
|
|
├── apps/
|
|
│ ├── electron/ # Electron desktop app (main.cjs, preload.cjs, updater.cjs)
|
|
│ │ └── src/main.jsx # Entry: PlatformProvider + HashRouter
|
|
│ ├── web/ # Web browser app (PWA enabled via VitePWA)
|
|
│ │ └── src/main.jsx # Entry: PlatformProvider + BrowserRouter
|
|
│ └── android/ # Capacitor Android wrapper
|
|
├── package.json # Root workspace config
|
|
├── .env.local # Convex + LiveKit + Klipy keys
|
|
└── CLAUDE.md
|
|
```
|
|
|
|
## Vite & Import Aliases
|
|
|
|
All Vite configs use `envDir: '../../'` to pick up root `.env.local`.
|
|
|
|
| Alias | Resolves to |
|
|
|-------|-------------|
|
|
| `@discord-clone/shared` | `packages/shared/src/` |
|
|
| `@discord-clone/platform-web` | `packages/platform-web/src/` |
|
|
| `@shared` | `packages/shared/src/` |
|
|
|
|
Convex imports from shared code use a relative path whose depth depends on the file location: `../../../../convex/_generated/api` from `packages/shared/src/<dir>/file.tsx` (4 up), `../../../../../convex/_generated/api` from `packages/shared/src/<dir>/<subdir>/file.tsx` (5 up — applies to `components/layout/`, `components/channel/`, etc.). Count: go up until you're at the repo root, then into `convex/`.
|
|
|
|
## Platform Abstraction (usePlatform())
|
|
|
|
All platform-specific APIs are accessed via the `usePlatform()` hook:
|
|
- `crypto` - generateKeys, randomBytes, sha256, signMessage, verifySignature, deriveAuthKeys, encryptData, decryptData, decryptBatch, verifyBatch, publicEncrypt, privateDecrypt
|
|
- `session` - save, load, clear
|
|
- `settings` - get, set
|
|
- `idle` - getSystemIdleTime, onIdleStateChanged, removeIdleStateListener
|
|
- `links` - openExternal, fetchMetadata
|
|
- `screenCapture` - getScreenSources
|
|
- `windowControls` - minimize, maximize, close (Electron only, null on web)
|
|
- `notifications` - show, setBadge, flashFrame, ensurePermission (Electron: native; Web: Notification API + Badging where available; null on Android for now)
|
|
- `updates` - checkUpdate (Electron only, null on web)
|
|
- `features` - hasWindowControls, hasScreenCapture, hasNativeUpdates
|
|
|
|
## Important Patterns
|
|
|
|
- Channel IDs use Convex `_id` (not `id`) - all references use `channel._id`
|
|
- Auth: client hashes DAK -> HAK before sending, server does string comparison
|
|
- First user bootstrap: createUserWithProfile creates Owner + @everyone roles
|
|
- Convex queries are reactive - no need for manual refresh or socket listeners
|
|
- File uploads use Convex storage: `generateUploadUrl` -> POST blob -> `getFileUrl`
|
|
- Typing indicators use scheduled functions for TTL cleanup
|
|
- CSS uses Discord dark theme colors via `:root` variables (`--bg-primary: #313338`, `--bg-secondary: #2b2d31`, `--bg-tertiary: #1e1f22`)
|
|
- Sidebar width is 312px (72px server strip + 240px channel panel)
|
|
- Channels grouped by `categoryId` with collapsible headers and @dnd-kit drag-and-drop
|
|
- Members list groups by hoisted roles (isHoist) then Online/Offline
|
|
- Unread tracking: `channelReadState` table per user/channel. ChatArea shows red "NEW" divider, Sidebar shows white dot
|
|
- Server name from `serverSettings` singleton, editable via Server Settings (requires `manage_channels`)
|
|
- AFK voice channel: VoiceContext polls idle time, auto-moves idle users
|
|
- Custom join sounds: stored as `joinSoundStorageId` on `userProfiles`
|
|
- Server icon: `serverSettings` stores `iconStorageId`, resolved to `iconUrl`
|
|
- `userPreferences.js` `setUserPref` takes optional `settings` param for disk persistence via platform
|
|
- Module-scope functions needing crypto accept it as parameter (e.g., `encryptKeyForUsers(users, channelId, keyHex, crypto)`)
|
|
- `randomBytes(size)` returns hex string on both platforms
|
|
- Keys exchanged as PEM strings (SPKI public, PKCS8 private) for cross-platform interop
|
|
- TitleBar/UpdateBanner render conditionally based on `platform.features.*`
|
|
- `MessageContent.tsx` parses Discord-style markdown (**bold**, *italic*, __underline__, ~~strike~~, `code`, ```codeblock```, > blockquote, ||spoiler||) on render — raw text is stored; parsing happens after decrypt. Inline emoji/mention/URL/custom-emoji tokenization runs inside each text leaf
|
|
- `NotificationManager` (mounted in `AppLayout`) watches `readState.getLatestMessageTimestamps` across all channels. On a new `messageId` when the window is unfocused and `senderId !== self`, it calls `platform.notifications.show` + flash + badge. Own sends and initial snapshot are suppressed. Focus auto-clears flash/badge
|
|
- Electron update flow is **check-only on launch** (no auto-install). `updater.cjs` emits status events; `platform.updates.{getStatus,downloadAndInstall,onStatusChanged}` expose it. `HeaderUpdateIcon` (mounted in `TitleBar`) renders a green download icon for optional updates and a full-screen blocker for required ones. Mark a release required by starting its release notes with `[REQUIRED]`
|
|
- Moderation: `bans` table blocks login (`auth.verifyUser`) and message send (`messages.sendInternal`). `auditLog` table is append-only; `audit.logAudit(ctx, {...})` is the helper that mutations call (best-effort — never throws). Permission check: `roles.hasPermission(ctx, userId, key)` — treats `isAdmin` and the `Owner` role as superusers so new permission keys like `ban_members` work without a migration. Server Settings → Bans + Audit Log tabs (desktop + mobile)
|
|
- Profile banner: `userProfiles.bannerStorageId` (optional), resolved to `bannerUrl` in `auth.getPublicKeys`. `auth.updateProfileInternal` takes `bannerStorageId` + `removeBanner` (the remove path also `ctx.storage.delete`s the blob). All four profile card surfaces (`MemberProfilePopout`, `MemberProfileModal`, `MobileMemberProfileSheet`, `UserAreaProfilePopout`) render the image when present, fall back to accent color when not
|
|
- Voice messages: mic button in `ChannelTextarea` records via `MediaRecorder` (picks `audio/webm;codecs=opus` where supported), stages the resulting `File` through the existing attachment pipeline — no new backend. Receivers render it via the standard `AttachmentAudio` player. Filename convention: `voice-message-{timestamp}.{webm|ogg|m4a}`. Voice-recorded messages set `isVoiceMessage: true` + `peaks: number[]` + `durationSec` in the attachment metadata; `EncryptedAttachment` dispatches those to `VoiceMessagePlayer` (pill with play button + waveform) instead of the full audio card
|
|
- Push-to-talk: `voiceSettings.inputMode` is `'voice-activity'` (default) or `'push-to-talk'`. Paired with the `voice.pushToTalk` keybind (marked `pressAndHold: true`). `KeybindContext` dispatches `brycord:keybind:voice.pushToTalk:down` / `:up` events — pressAndHold actions never `preventDefault`, so binding PTT to a letter still lets you type. `VoiceContext` reads the settings via the `brycord:voice-settings-changed` window event, listens for the PTT events, and routes them through a configurable release-delay timer before reconciling the LiveKit mic track. All mic-on/mic-off sources (user mute, deafen, server mute, PTT gate) converge on a single `setMicrophoneEnabled` effect
|
|
|
|
## Environment Variables
|
|
|
|
In `.env.local` at project root:
|
|
- `CONVEX_DEPLOYMENT` - Convex deployment URL (set by `npx convex dev`)
|
|
- `VITE_CONVEX_URL` - Convex URL for frontend (set by `npx convex dev`)
|
|
- `VITE_LIVEKIT_URL` - LiveKit server URL
|
|
- `LIVEKIT_API_KEY` - LiveKit API key (used in Convex Node action)
|
|
- `LIVEKIT_API_SECRET` - LiveKit API secret (used in Convex Node action)
|
|
- `KLIPY_API_KEY` - Klipy GIF API customer id (used in `convex/gifs.ts`). Replaces the old `TENOR_API_KEY` after Tenor's shutdown — the legacy var name is still read as a fallback so existing deployments only need to swap the value.
|