This commit is contained in:
Bryan1029384756
2026-04-18 15:41:51 -05:00
parent 938df217f4
commit 593eaba82e
47 changed files with 3539 additions and 212 deletions

View File

@@ -12,7 +12,7 @@ See also: [CONVEX_RULES.md](./CONVEX_RULES.md) | [CONVEX_EXAMPLES.md](./CONVEX_E
- **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, updates APIs
- **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)
@@ -86,7 +86,7 @@ All Vite configs use `envDir: '../../'` to pick up root `.env.local`.
| `@discord-clone/platform-web` | `packages/platform-web/src/` |
| `@shared` | `packages/shared/src/` |
Convex imports from shared components use relative path `../../../../convex/_generated/api` (4 levels up from shared src subdirs).
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())
@@ -98,6 +98,7 @@ All platform-specific APIs are accessed via the `usePlatform()` hook:
- `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
@@ -123,6 +124,13 @@ All platform-specific APIs are accessed via the `usePlatform()` hook:
- `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