**Update this file when making significant changes.** See also: [CONVEX_RULES.md](./CONVEX_RULES.md) | [CONVEX_EXAMPLES.md](./CONVEX_EXAMPLES.md) ## Architecture - **Backend**: Convex (reactive database + serverless functions) - **Frontend**: React + Vite (Electron app) - **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**: Client-side via Electron IPC (`window.cryptoAPI`) - **File Storage**: Convex built-in storage (`generateUploadUrl` + `getUrl`) ## Key Convex Files (convex/) - `schema.ts` - Full schema: userProfiles (with avatarStorageId, aboutMe, customStatus, joinSoundStorageId), categories (name, position), channels (with categoryId, topic, position), messages, messageReactions, channelKeys, roles, userRoles, invites, dmParticipants, typingIndicators, voiceStates, channelReadState, serverSettings (serverName, afkChannelId, afkTimeout, iconStorageId) - `auth.ts` - getSalt, verifyUser, createUserWithProfile, getPublicKeys (includes avatarUrl, aboutMe, customStatus, joinSoundUrl), updateProfile (includes joinSoundStorageId, removeJoinSound), updateStatus, getMyJoinSoundUrl - `categories.ts` - list, create, rename, remove, reorder - `channels.ts` - list, get, create (with categoryId/topic/position), rename, remove (cascade), updateTopic, moveChannel, reorderChannels - `members.ts` - getChannelMembers (includes isHoist on roles, avatarUrl, aboutMe, customStatus) - `channelKeys.ts` - uploadKeys, getKeysForUser - `messages.ts` - list (with reactions + username), send, edit, pin, listPinned, remove (with manage_messages permission check) - `reactions.ts` - add, remove - `serverSettings.ts` - get (resolves iconUrl), update (manage_channels permission), updateName (manage_channels permission), updateIcon (manage_channels permission), clearAfkChannel (internal) - `typing.ts` - startTyping, stopTyping, getTyping, cleanExpired (scheduled) - `dms.ts` - openDM, listDMs - `invites.ts` - create, use, revoke - `roles.ts` - list, create, update, remove, listMembers, assign, unassign, getMyPermissions - `voiceState.ts` - join, leave, updateState, getAll (includes joinSoundUrl per user), afkMove (self-move to AFK channel) - `voice.ts` - getToken (Node action, livekit-server-sdk) - `files.ts` - generateUploadUrl, getFileUrl - `gifs.ts` - search, categories (Node actions, Tenor API) - `readState.ts` - getReadState, markRead, getAllReadStates, getLatestMessageTimestamps (unread tracking) ## Frontend Structure (Frontend/Electron/src/) - `main.jsx` - ConvexProvider + VoiceProvider + HashRouter - `pages/Login.jsx` - Convex auth (getSalt + verifyUser) - `pages/Register.jsx` - Convex auth (createUserWithProfile + invite flow) - `pages/Chat.jsx` - useQuery for channels, categories, channelKeys, DMs - `components/ChatArea.jsx` - Messages, typing, reactions via Convex queries/mutations - `components/Sidebar.jsx` - Channel/category creation, key distribution, invites, drag-and-drop reordering via @dnd-kit - `contexts/VoiceContext.jsx` - Voice state via Convex + LiveKit room management + custom join sound playback - `components/ChannelSettingsModal.jsx` - Channel rename/delete via Convex mutations - `components/ServerSettingsModal.jsx` - Role management via Convex queries/mutations - `components/MessageItem.jsx` - Individual message rendering with unread divider support - `components/Avatar.jsx` - Reusable avatar component (image or colored-initial fallback) - `components/FriendsView.jsx` - User list via Convex query - `components/DMList.jsx` - DM user picker via Convex query - `components/GifPicker.jsx` - GIF search via Convex action - `components/VoiceRoom.jsx` - LiveKit token via Convex action ## 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 - Vite config uses `envDir: '../../'` to pick up root `.env.local` - `socket.io-client` fully removed, all socket refs replaced with Convex - No Express backend needed - `Backend/` directory is legacy and can be deleted - 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 are grouped by `categoryId` (references `categories` table) with collapsible headers and drag-and-drop reordering (@dnd-kit) - Categories are first-class entities with position-based ordering; uncategorized channels show under "Channels" group - Members list groups by hoisted roles (isHoist) then Online/Offline - Avatar component supports both image URLs and colored-initial fallback - Title bar has back/forward navigation arrows - Chat header includes thread, pin, members, notification icons + channel topic - Voice connected panel includes elapsed time timer - Keyboard shortcuts: Ctrl+K (quick switcher), Ctrl+Shift+M (mute toggle) - Unread tracking: `channelReadState` table stores last-read timestamp per user/channel. ChatArea shows red "NEW" divider, Sidebar shows white dot on unread channels - Server name: `serverSettings` singleton stores `serverName` (default "Secure Chat"), editable from Server Settings Overview tab (requires `manage_channels`). Sidebar header, tooltip, voice panel, Chat header, and welcome text all use the dynamic name. - AFK voice channel: `serverSettings` singleton table stores `afkChannelId` + `afkTimeout`. VoiceContext polls `idleAPI.getSystemIdleTime()` every 15s; auto-moves idle users to AFK channel via `voiceState.afkMove`. Users in AFK channel are force-muted and can't unmute. Sidebar shows "(AFK)" label. Server Settings Overview tab has AFK config UI. - Custom join sounds: Users can upload a custom audio file (max 10MB) via User Settings > My Account. Stored as `joinSoundStorageId` on `userProfiles`. When joining voice, `VoiceContext` plays the user's custom sound (or default `join_call.mp3`). Other users in the channel hear the joiner's custom sound via reactive `voiceStates` tracking (not LiveKit events) to avoid race conditions with URL availability. - Server icon: `serverSettings` stores `iconStorageId`. `get` query resolves it to `iconUrl`. Server Settings Overview tab has upload UI using `AvatarCropModal` with `cropShape="rect"` (square). Sidebar displays the icon image in the server strip (fallback to text initials). `AvatarCropModal` accepts a `cropShape` prop (`"round"` default for avatars, `"rect"` for server icon). ## 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) - `TENOR_API_KEY` - Tenor GIF API key (used in Convex Node action) ## Running the App 1. `npm install && npm run install:frontend` 2. `npx convex dev` (starts Convex backend, creates `.env.local`) 3. In another terminal: `cd Frontend/Electron && npm run dev` (or `npm run electron:dev`)