mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-05 04:12:25 +00:00
update
This commit is contained in:
1 parent
589925f1fe
commit
ef29bb6eb3
97 files changed
+46855
No files matched your search
Vendored
+35
@@ -0,0 +1,35 @@
|
||||
# Documentation Index
|
||||
|
||||
Welcome to the bitvid documentation. This directory contains detailed architectural guides, API references, and system overviews.
|
||||
|
||||
## Core Systems
|
||||
|
||||
- **[Application Coordinators](./app-coordinators.md)**: Overview of the coordinator pattern used to manage app-wide state and services.
|
||||
- **[Authentication Architecture](./auth-architecture.md)**: How session management, login flows, and signer integration work.
|
||||
- **[Feed Engine](./feed-engine.md)**: The pipeline for fetching, filtering, scoring, and sorting video feeds.
|
||||
- **[Playback & Fallback](./playback-fallback.md)**: The URL-first playback strategy with WebTorrent fallback orchestration.
|
||||
- **[Watch History System](./watch-history-system.md)**: Encrypted watch history synchronization and storage.
|
||||
|
||||
## Nostr Protocol
|
||||
|
||||
- **[Nostr Event Schemas](./nostr-event-schemas.md)**: The definitive catalogue of all Nostr event kinds and data structures used by bitvid.
|
||||
- **[DM Privacy Model](./dm-privacy-model.md)**: How Direct Messages are handled, including NIP-17 relays and privacy controls.
|
||||
- **[NIP-46 Client Overview](./nip46-client-overview.md)**: Details on the NIP-46 remote signer implementation.
|
||||
|
||||
## Moderation & Safety
|
||||
|
||||
- **[Moderation Service](./moderation-service-overview.md)**: How content moderation, reporting, and admin lists function.
|
||||
- **[Logging Strategy](./logging.md)**: Guidelines for using `devLogger` vs `userLogger` and production logging policies.
|
||||
|
||||
## Design & UI
|
||||
|
||||
- **[Design System](./design-system.md)**: Guide to the token-based design system and CSS architecture.
|
||||
- **[Menus & Popovers](./menus.md)**: Documentation for the menu and popover system.
|
||||
|
||||
## Operations
|
||||
|
||||
- **[Instance Configuration](./instance-config.md)**: Guide to configuring a bitvid instance.
|
||||
- **[Deployment Notes](./deployment-notes.md)**: Notes and checklists for deploying bitvid.
|
||||
- **[Manual QA Checklist](./qa.md)**: The standard manual QA script for releases.
|
||||
|
||||
For more details on contributing, please see [CONTRIBUTING.md](../CONTRIBUTING.md).
|
||||
Vendored
+82
@@ -0,0 +1,82 @@
|
||||
# VideoModal Component Overview
|
||||
|
||||
The `VideoModal` (`js/ui/components/VideoModal.js`) is the primary UI controller for the full-screen video playback experience in bitvid. It manages the modal lifecycle, orchestrates playback, and coordinates sub-controllers for social interactions (comments, reactions, moderation).
|
||||
|
||||
## Core Responsibilities
|
||||
|
||||
1. **Lifecycle Management**: Handles lazy-loading of the modal HTML, DOM injection, and visibility toggling.
|
||||
2. **Playback Orchestration**: Connects the video metadata to the underlying media player (via `mediaLoader` or direct video element manipulation).
|
||||
3. **Social Coordination**: Instantiates and manages `CommentsController`, `ReactionsController`, `SimilarContentController`, and `ModerationController`.
|
||||
4. **Accessibility**: Enforces focus trapping and ARIA attributes via `ModalAccessibility`.
|
||||
5. **Global State Integration**: Updates the application's modal state (`setGlobalModalState`) to pause background feeds and handle back-button navigation.
|
||||
|
||||
## Public API
|
||||
|
||||
The `VideoModal` exposes a minimal public interface for the application orchestrator (`ModalManager` or `App`):
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `constructor(deps)` | Initializes the controller with dependencies (DOM, logger, state helpers). Does *not* touch the DOM. |
|
||||
| `async load()` | Fetches `components/video-modal.html`, injects it into the DOM, and hydrates references. Safe to call multiple times (idempotent). |
|
||||
| `open(video, options)` | Resets state, populates the modal with the given `video` object, and makes it visible. |
|
||||
| `close()` | Hides the modal, pauses playback, clears state, and returns focus to the trigger element. |
|
||||
|
||||
### Dependencies
|
||||
The constructor requires a robust dependency injection object:
|
||||
- `document`: The DOM document.
|
||||
- `setGlobalModalState`: Function to notify the app of modal visibility changes.
|
||||
- `logger`: Logging utility.
|
||||
- `mediaLoader`: Service for handling media source resolution.
|
||||
- `assets`, `state`, `helpers`: Configuration objects.
|
||||
|
||||
## internal Architecture
|
||||
|
||||
### Lazy Loading & Hydration
|
||||
To improve initial page load performance, `VideoModal` does not render its markup until `load()` is called (typically on the first user interaction).
|
||||
1. **Fetch**: Retrieves `components/video-modal.html`.
|
||||
2. **Inject**: Appends the HTML to the `#modalContainer`.
|
||||
3. **Hydrate**: Queries for all necessary DOM elements (buttons, containers, inputs) and binds event listeners.
|
||||
4. **Sub-Controller Init**: Initializes `CommentsController`, `ReactionsController`, etc., passing them the fresh DOM references.
|
||||
|
||||
### Sub-Controllers
|
||||
The `VideoModal` acts as a parent controller, delegating specific logic to specialized classes:
|
||||
- **`CommentsController`**: Manages the comment thread, composer, and fetching.
|
||||
- **`ReactionsController`**: Handles likes/dislikes and reaction counts.
|
||||
- **`SimilarContentController`**: Fetches and renders related videos.
|
||||
- **`ModerationController`**: Manages content warnings, blurring, and reporting flows.
|
||||
|
||||
## Usage Example
|
||||
|
||||
```javascript
|
||||
// Instantiation (usually in ModalManager)
|
||||
const videoModal = new VideoModal({
|
||||
document: document,
|
||||
setGlobalModalState: (key, active) => app.setModalState(key, active),
|
||||
logger: logger,
|
||||
mediaLoader: mediaLoader
|
||||
});
|
||||
|
||||
// Initialization (lazy)
|
||||
await videoModal.load();
|
||||
|
||||
// Opening
|
||||
const video = {
|
||||
id: "...",
|
||||
title: "My Video",
|
||||
pubkey: "...",
|
||||
// ... other metadata
|
||||
};
|
||||
videoModal.open(video, { triggerElement: clickedCard });
|
||||
```
|
||||
|
||||
## Key Invariants
|
||||
|
||||
1. **Single Active Instance**: Only one video modal exists in the DOM at a time. Opening a new video replaces the content of the existing modal.
|
||||
2. **Focus Management**: When open, the modal traps focus. When closed, it restores focus to the element that triggered it (via `options.triggerElement`).
|
||||
3. **State Synchronization**: The `setGlobalModalState` callback MUST be called on open/close to ensure the back button works correctly and background videos pause.
|
||||
|
||||
## When to Change
|
||||
|
||||
- **Refactoring**: This file is extremely large (>6000 LOC). Consider extracting more logic into sub-controllers (e.g., a `MetadataController` for title/description/tags).
|
||||
- **Styling**: Changes to the modal layout should be mirrored in `components/video-modal.html` and tested for responsiveness.
|
||||
- **Playback Logic**: Changes to how videos load or play should likely be done in `mediaLoader` or `PlaybackService` rather than here, unless it's strictly UI-related (e.g., custom controls).
|
||||
Vendored
+104
@@ -0,0 +1,104 @@
|
||||
# Application Coordinator Architecture
|
||||
|
||||
The `Application` class (`js/app.js`) acts as the **composition root** for bitvid.
|
||||
Business logic that previously lived directly on `Application` has been extracted
|
||||
into **coordinator modules** under `js/app/`. `Application` retains thin
|
||||
delegator methods so that every public call-site continues to work unchanged.
|
||||
|
||||
## Coordinator Modules
|
||||
|
||||
| Module | File | Responsibility |
|
||||
|--------|------|----------------|
|
||||
| Feed | `js/app/feedCoordinator.js` | Feed registration, runtime building, refresh, video loading, URL-health helpers, feed telemetry |
|
||||
| Playback | `js/app/playbackCoordinator.js` | URL-first + magnet fallback pipeline, probing, torrent status, video timestamps |
|
||||
| Auth/Session | `js/app/authSessionCoordinator.js` | Signer lifecycle, profile switching, relay/block/wallet management, cleanup |
|
||||
| Modal | `js/app/modalCoordinator.js` | Modal open/close, view-count subscriptions, watch-history metadata, playback logging |
|
||||
| Moderation | `js/app/moderationCoordinator.js` | Moderation settings, video decoration, override/block/hide actions |
|
||||
|
||||
A shared helper (`js/app/bindCoordinator.js`) binds every method returned by a
|
||||
coordinator factory to the `Application` instance so that `this` works as expected.
|
||||
|
||||
## Dependency Injection
|
||||
|
||||
Each coordinator is a factory function that receives a `deps` object containing
|
||||
every module-level dependency it needs. Coordinators **never import globals**
|
||||
themselves; all references come through the closure created by `deps`:
|
||||
|
||||
```javascript
|
||||
export function createFeedCoordinator(deps) {
|
||||
const { devLogger, nostrClient, watchHistoryService, ... } = deps;
|
||||
|
||||
return {
|
||||
async loadVideos(forceFetch = false) {
|
||||
devLogger.log("Starting loadVideos..."); // closure var, not import
|
||||
// `this` is the Application instance (bound by caller)
|
||||
this.checkRelayHealthWarning();
|
||||
// ...
|
||||
},
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
`Application._initCoordinators()` creates all five coordinators and injects their
|
||||
dependencies from the module scope of `js/app.js`. It is called eagerly in the
|
||||
constructor and idempotently from every delegator, so test harnesses that use
|
||||
`Object.create(Application.prototype)` also work.
|
||||
|
||||
## Delegation Pattern
|
||||
|
||||
Every extracted method has a matching thin delegator on `Application`:
|
||||
|
||||
```javascript
|
||||
class Application {
|
||||
async loadVideos(...args) {
|
||||
this._initCoordinators();
|
||||
return this._feed.loadVideos(...args);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This preserves the public API surface. Callers (views, services, test harnesses)
|
||||
do not need to know about coordinators.
|
||||
|
||||
## Old-to-New Mapping
|
||||
|
||||
| Old location (`Application` method) | New location |
|
||||
|--------------------------------------|--------------|
|
||||
| `registerRecentFeed`, `registerForYouFeed`, `registerKidsFeed`, `registerExploreFeed`, `registerSubscriptionsFeed`, `registerWatchHistoryFeed` | `feedCoordinator` |
|
||||
| `buildForYouFeedRuntime`, `buildExploreFeedRuntime`, `buildRecentFeedRuntime`, `buildKidsFeedRuntime` | `feedCoordinator` |
|
||||
| `refreshForYouFeed`, `refreshKidsFeed`, `refreshExploreFeed`, `refreshRecentFeed` | `feedCoordinator` |
|
||||
| `loadVideos`, `loadForYouVideos`, `loadKidsVideos`, `loadExploreVideos`, `loadOlderVideos` | `feedCoordinator` |
|
||||
| `renderVideoList`, `refreshVideoDiscussionCounts` | `feedCoordinator` |
|
||||
| `checkRelayHealthWarning`, `hasOlderVersion` | `feedCoordinator` |
|
||||
| Feed telemetry (`getFeedTelemetryState`, `emitFeedImpressions`, `recordFeedClick`, ...) | `feedCoordinator` |
|
||||
| `playHttp`, `playViaWebTorrent`, `playVideoWithFallback` | `playbackCoordinator` |
|
||||
| `playVideoByEventId`, `playVideoWithoutEvent` | `playbackCoordinator` |
|
||||
| `probeUrl`, `probeUrlWithVideoElement`, `checkUrlParams` | `playbackCoordinator` |
|
||||
| `autoplayModalVideo`, `startTorrentStatusMirrors` | `playbackCoordinator` |
|
||||
| `buildModalTimestampPayload`, `resolveVideoPostedAt`, `ensureModalPostedTimestamp` | `playbackCoordinator` |
|
||||
| `shouldDeferModeratedPlayback`, `resumePendingModeratedPlayback` | `playbackCoordinator` |
|
||||
| `handleAuthLogin`, `handleAuthLogout`, `requestLogout` | `authSessionCoordinator` |
|
||||
| `handleBlocksLoaded`, `handleRelaysLoaded`, `scheduleRelayUiRefresh` | `authSessionCoordinator` |
|
||||
| `cleanup`, `waitForCleanup`, `clearActiveIntervals` | `authSessionCoordinator` |
|
||||
| `handleProfileSwitchRequest`, `waitForIdentityRefresh`, `handleProfileLogoutRequest` | `authSessionCoordinator` |
|
||||
| `handleProfileRelayOperation`, `handleProfileBlocklistMutation`, `handleProfileAdminMutation` | `authSessionCoordinator` |
|
||||
| `handleProfileWalletPersist`, `handleProfileWalletTest`, `handleProfileWalletDisconnect` | `authSessionCoordinator` |
|
||||
| `showModalWithPoster`, `hideModal`, `ensureVideoModalReady` | `modalCoordinator` |
|
||||
| `subscribeModalViewCount`, `teardownModalViewCountSubscription` | `modalCoordinator` |
|
||||
| `preparePlaybackLogging`, `teardownVideoElement` | `modalCoordinator` |
|
||||
| `persistWatchHistoryMetadataForVideo`, `handleRemoveHistoryAction` | `modalCoordinator` |
|
||||
| `setShareButtonState`, `getShareUrlBase` | `modalCoordinator` |
|
||||
| `handleModerationSettingsChange`, `refreshVisibleModerationUi` | `moderationCoordinator` |
|
||||
| `decorateVideoModeration`, `initializeModerationActionController` | `moderationCoordinator` |
|
||||
| `handleModerationOverride`, `handleModerationBlock`, `handleModerationHide` | `moderationCoordinator` |
|
||||
| `normalizeModerationSettings`, `getActiveModerationThresholds` | `moderationCoordinator` |
|
||||
|
||||
## Adding New Methods
|
||||
|
||||
When adding a method that belongs to an existing coordinator:
|
||||
|
||||
1. Add the method to the coordinator's return object.
|
||||
2. Add a thin delegator on `Application` that calls `this._initCoordinators()`
|
||||
then delegates to the coordinator.
|
||||
3. If the method uses a new module-level import, add it to the coordinator's
|
||||
`deps` destructuring and pass it from `_initCoordinators()`.
|
||||
+849
@@ -0,0 +1,849 @@
|
||||
# Capacitor Native App Plan
|
||||
|
||||
> **Status**: Planning
|
||||
> **Platforms**: Android, iOS, Windows, macOS, Linux
|
||||
> **Framework**: [Capacitor](https://capacitorjs.com/) by Ionic
|
||||
> **Goal**: Ship bitvid as a native app on all major platforms while keeping a single codebase with the existing web/PWA deployment.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Why Capacitor](#why-capacitor)
|
||||
2. [Architecture Overview](#architecture-overview)
|
||||
3. [Repository Structure](#repository-structure)
|
||||
4. [Phase 1 — Foundation](#phase-1--foundation)
|
||||
5. [Phase 2 — Android & iOS](#phase-2--android--ios)
|
||||
6. [Phase 3 — Desktop (Electron via Capacitor)](#phase-3--desktop-electron-via-capacitor)
|
||||
7. [Phase 4 — Platform-Specific Enhancements](#phase-4--platform-specific-enhancements)
|
||||
8. [Phase 5 — CI/CD for Native Builds](#phase-5--cicd-for-native-builds)
|
||||
9. [Phase 6 — Testing Strategy](#phase-6--testing-strategy)
|
||||
10. [Platform Considerations & Risks](#platform-considerations--risks)
|
||||
11. [App Store Strategy](#app-store-strategy)
|
||||
12. [Task Checklist](#task-checklist)
|
||||
|
||||
---
|
||||
|
||||
## Why Capacitor
|
||||
|
||||
### Fit for bitvid
|
||||
|
||||
bitvid is a fully static, client-side web application. There is no backend server — all authentication, data, and state management happens in the browser via Nostr relays and WebTorrent. This makes it an ideal candidate for Capacitor, which wraps an existing web app in a native WebView shell.
|
||||
|
||||
### Key reasons
|
||||
|
||||
| Reason | Detail |
|
||||
|--------|--------|
|
||||
| **Zero rewrite** | The existing `dist/` build output drops directly into Capacitor as the web layer. No framework migration needed. |
|
||||
| **Single codebase** | Web, PWA, Android, iOS, and desktop all share the same HTML/CSS/JS. Platform-specific behavior is handled via feature detection and Capacitor's plugin bridge. |
|
||||
| **Progressive native access** | Start with a pure web wrapper, then incrementally add native plugins (deep links, push notifications, share targets, file system) only when needed. |
|
||||
| **Mature mobile support** | Capacitor has first-class Android and iOS support with a large plugin ecosystem. |
|
||||
| **Desktop via Electron** | The `@nicepkg/capacitor-electron` community plugin (or the `@nicepkg/cap-electron` plugin) lets Capacitor drive an Electron shell for Windows/macOS/Linux using the same web build. |
|
||||
| **Existing PWA foundation** | bitvid already has `site.webmanifest`, a service worker (`sw.min.js`), and app icons — all of which Capacitor respects and builds on. |
|
||||
| **Active ecosystem** | Large community, official Ionic backing, regular releases, good documentation. |
|
||||
|
||||
### Alternatives considered
|
||||
|
||||
| Framework | Why not primary |
|
||||
|-----------|----------------|
|
||||
| **Tauri** | Excellent for desktop (small binaries, Rust backend), but mobile support is still in beta. Could be revisited later as a desktop-only option if Electron bundle size becomes a concern. |
|
||||
| **Electron standalone** | Bundles ~150MB of Chromium per platform. Capacitor+Electron gives us the same result but with a unified build pipeline across all platforms. |
|
||||
| **React Native / Flutter** | Would require a complete rewrite. bitvid is vanilla JS with no component framework — porting to RN or Flutter would be a ground-up rebuild. |
|
||||
| **PWA only** | Already partially implemented. Good for web-savvy users but lacks app store discoverability, has weaker iOS support (no push notifications, limited background execution), and misses users who expect a "real" app install. |
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ bitvid codebase │
|
||||
│ │
|
||||
│ ┌───────────┐ npm run build ┌───────────────┐ │
|
||||
│ │ Source │ ────────────────→ │ dist/ │ │
|
||||
│ │ (js/, │ │ (static web │ │
|
||||
│ │ css/, │ │ assets) │ │
|
||||
│ │ views/) │ └──────┬────────┘ │
|
||||
│ └───────────┘ │ │
|
||||
│ │ │
|
||||
│ ┌────────────────┼─────┐ │
|
||||
│ │ npx cap sync │ │ │
|
||||
│ ▼ ▼ ▼ │
|
||||
│ ┌─────────┐ ┌─────┐ ┌────────┐ │
|
||||
│ │ android/│ │ ios/│ │electron/│ │
|
||||
│ │ (native │ │ │ │ │ │
|
||||
│ │ project)│ │ │ │ │ │
|
||||
│ └─────────┘ └─────┘ └────────┘ │
|
||||
│ │
|
||||
│ Deploy targets: │
|
||||
│ • Web/PWA → dist/ served via Netlify/Vercel/etc │
|
||||
│ • Android → android/ → Play Store APK/AAB │
|
||||
│ • iOS → ios/ → App Store IPA │
|
||||
│ • Desktop → electron/ → Windows/macOS/Linux │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Data flow (unchanged)
|
||||
|
||||
The native shell is purely a container. All app logic remains in the web layer:
|
||||
|
||||
1. User action triggers a UI controller event handler
|
||||
2. Controller invokes callback provided by `bitvidApp`
|
||||
3. `bitvidApp` executes logic, communicates with Nostr relays via WebSocket
|
||||
4. WebTorrent handles P2P video streaming via WebRTC data channels
|
||||
5. UI updates via DOM manipulation
|
||||
|
||||
The only new data path is the **Capacitor bridge** — a thin JS-to-native channel used for:
|
||||
- Deep link handling (Nostr URIs, `nostr:` protocol)
|
||||
- Push notification tokens (optional, future)
|
||||
- Native share sheet integration
|
||||
- App lifecycle events (background/foreground)
|
||||
- Status bar / navigation bar styling
|
||||
|
||||
---
|
||||
|
||||
## Repository Structure
|
||||
|
||||
The native project directories live at the repo root alongside the existing web source. This is the standard Capacitor monorepo layout.
|
||||
|
||||
```
|
||||
bitvid/
|
||||
├── android/ # NEW — Native Android project (Android Studio)
|
||||
│ ├── app/
|
||||
│ │ ├── src/main/
|
||||
│ │ │ ├── AndroidManifest.xml
|
||||
│ │ │ ├── java/com/bitvid/app/
|
||||
│ │ │ └── res/
|
||||
│ │ └── build.gradle
|
||||
│ ├── capacitor.settings.gradle
|
||||
│ └── build.gradle
|
||||
├── ios/ # NEW — Native iOS project (Xcode)
|
||||
│ └── App/
|
||||
│ ├── App/
|
||||
│ │ ├── AppDelegate.swift
|
||||
│ │ ├── Info.plist
|
||||
│ │ └── capacitor.config.json (generated)
|
||||
│ ├── App.xcodeproj
|
||||
│ └── Podfile
|
||||
├── electron/ # NEW — Electron desktop shell
|
||||
│ ├── src/
|
||||
│ │ ├── index.ts
|
||||
│ │ └── preload.ts
|
||||
│ ├── capacitor.config.ts
|
||||
│ └── package.json
|
||||
├── capacitor.config.ts # NEW — Capacitor root config
|
||||
├── dist/ # Existing build output (web assets)
|
||||
├── js/ # Existing source
|
||||
├── css/ # Existing source
|
||||
├── config/
|
||||
│ └── instance-config.js # Extended with platform detection
|
||||
├── scripts/
|
||||
│ ├── build-dist.mjs # Existing web build
|
||||
│ ├── build-android.mjs # NEW — Android build helper
|
||||
│ ├── build-ios.mjs # NEW — iOS build helper
|
||||
│ └── build-desktop.mjs # NEW — Electron build helper
|
||||
├── tests/
|
||||
│ ├── unit/ # Existing
|
||||
│ ├── e2e/ # Existing (web)
|
||||
│ ├── visual/ # Existing
|
||||
│ ├── native/ # NEW — Native-specific tests
|
||||
│ │ ├── android/
|
||||
│ │ ├── ios/
|
||||
│ │ └── desktop/
|
||||
│ └── platform/ # NEW — Cross-platform behavior tests
|
||||
├── .github/workflows/
|
||||
│ ├── ci.yml # Existing (web CI)
|
||||
│ ├── ci-android.yml # NEW — Android build + test
|
||||
│ ├── ci-ios.yml # NEW — iOS build + test
|
||||
│ └── ci-desktop.yml # NEW — Electron build + test
|
||||
├── package.json # Extended with native scripts
|
||||
├── capacitor.config.ts # Capacitor root configuration
|
||||
└── site.webmanifest # Existing PWA manifest
|
||||
```
|
||||
|
||||
### What gets gitignored
|
||||
|
||||
```gitignore
|
||||
# Native build artifacts
|
||||
android/app/build/
|
||||
android/.gradle/
|
||||
android/capacitor-cordova-android-plugins/build/
|
||||
ios/App/Pods/
|
||||
ios/App/DerivedData/
|
||||
electron/dist/
|
||||
electron/node_modules/
|
||||
|
||||
# Capacitor copies of web assets (regenerated by cap sync)
|
||||
android/app/src/main/assets/public/
|
||||
ios/App/App/public/
|
||||
```
|
||||
|
||||
### What gets committed
|
||||
|
||||
The native project scaffolding (`android/`, `ios/`, `electron/`) **is committed** to the repo. This is the Capacitor convention — the native projects are source code, not generated artifacts. They contain platform-specific configuration (permissions, entitlements, splash screens, icons) that must be version-controlled.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — Foundation
|
||||
|
||||
**Goal**: Initialize Capacitor, get the web app running inside native shells on Android and iOS simulators, and Electron on desktop.
|
||||
|
||||
### Tasks
|
||||
|
||||
1. **Install Capacitor core**
|
||||
```bash
|
||||
npm install @capacitor/core @capacitor/cli
|
||||
```
|
||||
|
||||
2. **Create `capacitor.config.ts`**
|
||||
```typescript
|
||||
import type { CapacitorConfig } from '@capacitor/cli';
|
||||
|
||||
const config: CapacitorConfig = {
|
||||
appId: 'com.bitvid.app',
|
||||
appName: 'bitvid',
|
||||
webDir: 'dist',
|
||||
server: {
|
||||
// Allow WebSocket connections to Nostr relays
|
||||
allowNavigation: ['*'],
|
||||
},
|
||||
plugins: {
|
||||
SplashScreen: {
|
||||
launchShowDuration: 2000,
|
||||
backgroundColor: '#0f172a', // matches theme
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
3. **Add native platforms**
|
||||
```bash
|
||||
npx cap add android
|
||||
npx cap add ios
|
||||
```
|
||||
|
||||
4. **Update build pipeline** — extend `build-dist.mjs` or add a wrapper:
|
||||
```bash
|
||||
npm run build && npx cap sync
|
||||
```
|
||||
The `cap sync` command copies `dist/` into each native project and updates native dependencies.
|
||||
|
||||
5. **Add Electron support** via community plugin:
|
||||
```bash
|
||||
npm install @nicepkg/capacitor-electron
|
||||
npx cap add electron
|
||||
```
|
||||
|
||||
6. **Add npm scripts**
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"cap:sync": "npx cap sync",
|
||||
"cap:android": "npm run build && npx cap sync android && npx cap open android",
|
||||
"cap:ios": "npm run build && npx cap sync ios && npx cap open ios",
|
||||
"cap:electron": "npm run build && npx cap sync electron && npx cap open electron",
|
||||
"build:all": "npm run build && npx cap sync"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
7. **Verify basic functionality**
|
||||
- App loads in Android emulator
|
||||
- App loads in iOS simulator
|
||||
- App loads in Electron window
|
||||
- Nostr relay WebSocket connections work
|
||||
- Theme renders correctly
|
||||
- Navigation / SPA routing works
|
||||
|
||||
### Platform detection helper
|
||||
|
||||
Add a lightweight platform detection utility that the rest of the app can use:
|
||||
|
||||
```javascript
|
||||
// js/utils/platform.js
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
|
||||
export const platform = {
|
||||
isNative: Capacitor.isNativePlatform(),
|
||||
isAndroid: Capacitor.getPlatform() === 'android',
|
||||
isIos: Capacitor.getPlatform() === 'ios',
|
||||
isElectron: Capacitor.getPlatform() === 'electron',
|
||||
isWeb: Capacitor.getPlatform() === 'web',
|
||||
};
|
||||
```
|
||||
|
||||
This module is safe to import on web — `@capacitor/core` detects it's running in a browser and returns `'web'` for the platform.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — Android & iOS
|
||||
|
||||
**Goal**: Production-quality mobile builds ready for app store submission.
|
||||
|
||||
### Android-specific work
|
||||
|
||||
1. **Permissions** in `AndroidManifest.xml`:
|
||||
- `INTERNET` (already default)
|
||||
- `ACCESS_NETWORK_STATE` (connectivity detection)
|
||||
- `CAMERA` / `MICROPHONE` — only if video recording is added later (not needed now)
|
||||
|
||||
2. **WebView configuration**:
|
||||
- Enable mixed content mode (for WebSocket `wss://` alongside `https://`)
|
||||
- Enable JavaScript (default in Capacitor)
|
||||
- Set `webSettings.mediaPlaybackRequiresUserGesture = false` for autoplay
|
||||
|
||||
3. **App icons and splash screens**:
|
||||
- Use `@capacitor/splash-screen` plugin
|
||||
- Generate from existing `assets/png/android-chrome-512x512.png`
|
||||
- Use `cordova-res` or `@capacitor/assets` to generate all density variants
|
||||
|
||||
4. **Deep links**:
|
||||
- Register `nostr:` URI scheme via Android intent filters
|
||||
- Register `https://bitvid.tv` domain for App Links (needs `.well-known/assetlinks.json`)
|
||||
|
||||
5. **Status bar and navigation bar**:
|
||||
- `@capacitor/status-bar` plugin to match the dark theme (`#0f172a`)
|
||||
- Handle safe areas / notch insets via CSS `env(safe-area-inset-*)`
|
||||
|
||||
### iOS-specific work
|
||||
|
||||
1. **Info.plist configuration**:
|
||||
- `NSAppTransportSecurity` — allow WebSocket connections
|
||||
- App description strings for any permission prompts
|
||||
|
||||
2. **WKWebView settings**:
|
||||
- `allowsInlineMediaPlayback: true`
|
||||
- `mediaTypesRequiringUserActionForPlayback: []` (enables autoplay)
|
||||
- These are configurable in `capacitor.config.ts` under `ios.webContentsConfiguration`
|
||||
|
||||
3. **Deep links**:
|
||||
- Universal Links (needs `apple-app-site-association` on the domain)
|
||||
- Custom URL scheme `bitvid://`
|
||||
|
||||
4. **Safe areas**:
|
||||
- iOS notch, Dynamic Island, home indicator
|
||||
- CSS `env(safe-area-inset-top)` etc. — update layout in `index.html` and key UI components
|
||||
|
||||
5. **App icons**:
|
||||
- Generate full icon set from existing 512x512 source
|
||||
- Must include 1024x1024 for App Store
|
||||
|
||||
### Shared mobile work
|
||||
|
||||
1. **Hardware back button** (Android):
|
||||
- Handle back navigation within the SPA instead of exiting the app
|
||||
- Use `@capacitor/app` plugin's `backButton` listener
|
||||
|
||||
2. **App lifecycle**:
|
||||
- Pause/resume WebSocket connections on app background/foreground
|
||||
- Use `@capacitor/app` `appStateChange` listener
|
||||
|
||||
3. **Keyboard handling**:
|
||||
- Adjust layout when virtual keyboard appears (search bar, comment inputs)
|
||||
- Capacitor provides `Keyboard` plugin for this
|
||||
|
||||
4. **Haptic feedback** (optional):
|
||||
- Light haptics on zap actions using `@capacitor/haptics`
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — Desktop (Electron via Capacitor)
|
||||
|
||||
**Goal**: Ship bitvid as a standalone desktop app for Windows, macOS, and Linux.
|
||||
|
||||
### Approach
|
||||
|
||||
Use Capacitor's Electron integration so the desktop build uses the same `dist/` web assets and the same Capacitor plugin bridge as mobile.
|
||||
|
||||
### Desktop-specific work
|
||||
|
||||
1. **Window configuration**:
|
||||
- Default size: 1280x800 (fits well for video content)
|
||||
- Minimum size: 800x600
|
||||
- Remember window position/size across launches
|
||||
- Frameless window option with custom title bar (matches bitvid header)
|
||||
|
||||
2. **Menu bar**:
|
||||
- Minimal native menu (File, Edit, View, Help)
|
||||
- Or hide native menu and use the web app's own navigation
|
||||
|
||||
3. **Tray icon** (optional):
|
||||
- System tray icon for background seeding (WebTorrent)
|
||||
- "Minimize to tray" behavior
|
||||
|
||||
4. **Auto-updater**:
|
||||
- Electron's `autoUpdater` for seamless desktop updates
|
||||
- Or use GitHub Releases as the update feed
|
||||
|
||||
5. **Protocol handler**:
|
||||
- Register `nostr:` and `bitvid:` URI schemes at OS level
|
||||
|
||||
6. **Packaging**:
|
||||
- Windows: NSIS installer or MSIX
|
||||
- macOS: DMG + notarization
|
||||
- Linux: AppImage, .deb, .rpm
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 — Platform-Specific Enhancements
|
||||
|
||||
These are optional features that improve the native experience. Implement after core functionality is solid.
|
||||
|
||||
### Native share sheet
|
||||
|
||||
```javascript
|
||||
import { Share } from '@capacitor/share';
|
||||
|
||||
async function shareVideo(video) {
|
||||
if (platform.isNative) {
|
||||
await Share.share({
|
||||
title: video.title,
|
||||
text: video.description,
|
||||
url: `https://bitvid.tv/#video/${video.id}`,
|
||||
dialogTitle: 'Share video',
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Push notifications (future)
|
||||
|
||||
Nostr-based push could be implemented via:
|
||||
- A lightweight push relay that watches for mentions/zaps and sends FCM/APNs
|
||||
- `@capacitor/push-notifications` plugin to receive them
|
||||
|
||||
This is a significant feature and should be its own planning document.
|
||||
|
||||
### Offline support
|
||||
|
||||
- Cache the app shell and critical assets in the service worker
|
||||
- Cache recently viewed video metadata for offline browsing
|
||||
- Queue zaps and reactions for when connectivity returns
|
||||
|
||||
### Picture-in-picture
|
||||
|
||||
- Android: native PiP mode via custom Capacitor plugin
|
||||
- iOS: `AVPictureInPictureController` via the WebView's native video player
|
||||
- Desktop: Electron `BrowserWindow` PiP API
|
||||
|
||||
---
|
||||
|
||||
## Phase 5 — CI/CD for Native Builds
|
||||
|
||||
### New CI workflows
|
||||
|
||||
#### `ci-android.yml`
|
||||
|
||||
```yaml
|
||||
name: CI — Android
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'js/**'
|
||||
- 'css/**'
|
||||
- 'views/**'
|
||||
- 'components/**'
|
||||
- 'android/**'
|
||||
- 'capacitor.config.ts'
|
||||
- 'package.json'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'js/**'
|
||||
- 'css/**'
|
||||
- 'views/**'
|
||||
- 'components/**'
|
||||
- 'android/**'
|
||||
- 'capacitor.config.ts'
|
||||
|
||||
jobs:
|
||||
build-android:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Build web assets
|
||||
run: npm run build
|
||||
- name: Sync Capacitor
|
||||
run: npx cap sync android
|
||||
- name: Build Android debug APK
|
||||
working-directory: android
|
||||
run: ./gradlew assembleDebug
|
||||
- name: Upload APK artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: bitvid-android-debug
|
||||
path: android/app/build/outputs/apk/debug/app-debug.apk
|
||||
```
|
||||
|
||||
#### `ci-ios.yml`
|
||||
|
||||
```yaml
|
||||
name: CI — iOS
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths: ['js/**', 'css/**', 'views/**', 'ios/**', 'capacitor.config.ts']
|
||||
pull_request:
|
||||
paths: ['js/**', 'css/**', 'views/**', 'ios/**', 'capacitor.config.ts']
|
||||
|
||||
jobs:
|
||||
build-ios:
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Build web assets
|
||||
run: npm run build
|
||||
- name: Sync Capacitor
|
||||
run: npx cap sync ios
|
||||
- name: Install CocoaPods
|
||||
working-directory: ios/App
|
||||
run: pod install
|
||||
- name: Build iOS (no sign)
|
||||
run: |
|
||||
xcodebuild build \
|
||||
-workspace ios/App/App.xcworkspace \
|
||||
-scheme App \
|
||||
-destination 'generic/platform=iOS Simulator' \
|
||||
CODE_SIGN_IDENTITY="" \
|
||||
CODE_SIGNING_REQUIRED=NO
|
||||
```
|
||||
|
||||
#### `ci-desktop.yml`
|
||||
|
||||
```yaml
|
||||
name: CI — Desktop
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths: ['js/**', 'css/**', 'views/**', 'electron/**', 'capacitor.config.ts']
|
||||
pull_request:
|
||||
paths: ['js/**', 'css/**', 'views/**', 'electron/**', 'capacitor.config.ts']
|
||||
|
||||
jobs:
|
||||
build-desktop:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Build web assets
|
||||
run: npm run build
|
||||
- name: Sync Capacitor
|
||||
run: npx cap sync electron
|
||||
- name: Build Electron
|
||||
working-directory: electron
|
||||
run: npm run build
|
||||
```
|
||||
|
||||
### Path-filtered triggers
|
||||
|
||||
Each native CI workflow uses `paths` filters so that:
|
||||
- A CSS-only change triggers Android + iOS + Desktop builds (the web layer changed)
|
||||
- A change to only `android/` triggers just the Android build
|
||||
- Existing web CI (`ci.yml`) continues to run independently
|
||||
|
||||
### CI cost management
|
||||
|
||||
Native CI is more expensive than web CI:
|
||||
- **iOS builds require `macos-latest` runners** (2-3x cost on GitHub Actions)
|
||||
- **Android builds need JDK + Gradle** (slower startup)
|
||||
- **Desktop builds run on 3 OS matrix** entries
|
||||
|
||||
To manage costs:
|
||||
- Only run native CI on PRs that touch relevant paths
|
||||
- Cache Gradle/CocoaPods/node_modules aggressively
|
||||
- Consider running iOS CI only on `main` merges (not every PR) initially
|
||||
- Use GitHub Actions concurrency groups with `cancel-in-progress: true`
|
||||
|
||||
---
|
||||
|
||||
## Phase 6 — Testing Strategy
|
||||
|
||||
### Testing layers
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ Unit Tests (existing) │ ← Platform-agnostic
|
||||
│ vitest — js logic, nostr, schemas, utils │ No changes needed
|
||||
├─────────────────────────────────────────────┤
|
||||
│ Visual Regression (existing) │ ← Web viewport
|
||||
│ Playwright — screenshot comparison │ Add mobile viewports
|
||||
├─────────────────────────────────────────────┤
|
||||
│ E2E Web (existing) │ ← Browser-based
|
||||
│ Playwright — user journeys in Chrome │ No changes needed
|
||||
├─────────────────────────────────────────────┤
|
||||
│ E2E Mobile (NEW) │ ← Device/emulator
|
||||
│ Appium or Detox — native app user flows │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ Platform Smoke Tests (NEW) │ ← Quick validation
|
||||
│ Per-platform build + launch + basic checks │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ Cross-Platform Behavior Tests (NEW) │ ← Feature parity
|
||||
│ Verify features work consistently across │
|
||||
│ web, Android, iOS, and desktop │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### New test categories
|
||||
|
||||
#### Platform smoke tests (`tests/native/`)
|
||||
|
||||
Quick checks that run after each native build:
|
||||
- App launches without crash
|
||||
- Main view renders
|
||||
- WebSocket connection to a relay succeeds
|
||||
- Video playback starts (URL source)
|
||||
- Theme renders correctly (screenshot comparison)
|
||||
|
||||
#### Mobile viewport visual tests
|
||||
|
||||
Extend existing Playwright visual tests with mobile viewports:
|
||||
```javascript
|
||||
// playwright.config.ts addition
|
||||
projects: [
|
||||
// existing desktop viewports...
|
||||
{
|
||||
name: 'Mobile Android',
|
||||
use: { ...devices['Pixel 7'] },
|
||||
},
|
||||
{
|
||||
name: 'Mobile iOS',
|
||||
use: { ...devices['iPhone 14'] },
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
This runs in Playwright's browser emulation (not on real devices) but catches responsive layout issues early.
|
||||
|
||||
#### WebView-specific E2E tests
|
||||
|
||||
For testing inside the actual native WebView (not just a browser), use:
|
||||
- **Android**: Espresso + WebView assertions, or Appium with WebView context switching
|
||||
- **iOS**: XCUITest + WebView inspection, or Appium
|
||||
|
||||
These are heavier to set up but catch WebView-specific issues (service worker behavior, WebRTC in WKWebView, etc).
|
||||
|
||||
#### What to test per platform
|
||||
|
||||
| Test | Web | Android | iOS | Desktop |
|
||||
|------|-----|---------|-----|---------|
|
||||
| Unit tests | yes | n/a | n/a | n/a |
|
||||
| Visual regression (browser) | yes | mobile viewport | mobile viewport | yes |
|
||||
| E2E web journeys | yes | — | — | — |
|
||||
| App launches | — | yes | yes | yes |
|
||||
| Relay WebSocket connects | yes | yes | yes | yes |
|
||||
| Video playback (URL) | yes | yes | yes | yes |
|
||||
| Video playback (WebTorrent) | yes | yes | **test carefully** | yes |
|
||||
| Deep links (`nostr:`) | — | yes | yes | yes |
|
||||
| Back button navigation | — | yes | — | — |
|
||||
| Safe area rendering | — | — | yes | — |
|
||||
| Keyboard interaction | — | yes | yes | — |
|
||||
| Offline behavior | yes | yes | yes | yes |
|
||||
|
||||
---
|
||||
|
||||
## Platform Considerations & Risks
|
||||
|
||||
### WebTorrent on iOS (HIGH RISK)
|
||||
|
||||
**Issue**: WKWebView's WebRTC support is more limited than desktop Chrome. WebTorrent relies on WebRTC data channels for peer-to-peer streaming.
|
||||
|
||||
**Mitigation**:
|
||||
- bitvid already has a URL-first playback fallback (`DEFAULT_PLAYBACK_SOURCE` in instance config)
|
||||
- For iOS builds, default to URL-first playback: `DEFAULT_PLAYBACK_SOURCE: 'url'`
|
||||
- Use platform detection to set this automatically:
|
||||
```javascript
|
||||
if (platform.isIos) {
|
||||
setUrlFirstEnabled(true);
|
||||
}
|
||||
```
|
||||
- Test WebTorrent streaming on real iOS devices — it may work in modern iOS versions (16+), but should not be the primary path
|
||||
|
||||
### Service worker in Capacitor WebView
|
||||
|
||||
**Issue**: The `sw.min.js` service worker intercepts fetch requests to proxy WebTorrent stream data. Service worker support in native WebViews varies.
|
||||
|
||||
**Mitigation**:
|
||||
- Android: WKWebView supports service workers (API level 33+). For older Android, the URL fallback handles it.
|
||||
- iOS: WKWebView supports service workers in iOS 14+, but with some caveats around scope and lifecycle.
|
||||
- Test the WebTorrent streaming service worker flow specifically in native WebViews.
|
||||
- If service worker is unreliable, consider a Capacitor plugin that bridges native HTTP interception as an alternative stream proxy.
|
||||
|
||||
### WebSocket connectivity
|
||||
|
||||
**Issue**: Native platforms may restrict background WebSocket connections.
|
||||
|
||||
**Mitigation**:
|
||||
- Use `@capacitor/app` lifecycle events to gracefully disconnect/reconnect:
|
||||
```javascript
|
||||
App.addListener('appStateChange', ({ isActive }) => {
|
||||
if (isActive) {
|
||||
nostrClient.reconnect();
|
||||
} else {
|
||||
nostrClient.pause();
|
||||
}
|
||||
});
|
||||
```
|
||||
- This also reduces battery drain from idle relay connections.
|
||||
|
||||
### Content Security Policy
|
||||
|
||||
**Issue**: Capacitor serves web assets from a local origin (`capacitor://localhost` on iOS, `https://localhost` on Android). CSP rules may need adjustment.
|
||||
|
||||
**Mitigation**:
|
||||
- Update `_headers` / CSP meta tags to allow the Capacitor local origin
|
||||
- Allow `wss://*` for Nostr relay connections
|
||||
- Allow `blob:` and `data:` for WebTorrent stream playback
|
||||
|
||||
### App Store rejection risk
|
||||
|
||||
See the [App Store Strategy](#app-store-strategy) section below.
|
||||
|
||||
---
|
||||
|
||||
## App Store Strategy
|
||||
|
||||
### Google Play Store
|
||||
|
||||
**Risk level**: Low-Medium
|
||||
|
||||
Google is generally more permissive. Key considerations:
|
||||
- **Content rating**: Declare user-generated content (UGC) in the rating questionnaire
|
||||
- **Moderation**: bitvid's existing moderation system (NIP-56 reports, NIP-51 mute lists, blur thresholds, autoplay blocking) demonstrates active content moderation
|
||||
- **Crypto/payments**: Lightning/Zap functionality may trigger financial services review. Frame it as "tips" or "donations", not as a payment system. Emphasize that bitvid does not custody funds.
|
||||
|
||||
### Apple App Store
|
||||
|
||||
**Risk level**: Medium-High
|
||||
|
||||
Apple has been unpredictable with Nostr/decentralized apps. Key considerations:
|
||||
- **Guideline 4.2 (Minimum Functionality)**: The app must provide functionality beyond a "repackaged website". Native features (deep links, share sheet, haptics, PiP) help differentiate.
|
||||
- **Guideline 1.2 (User Generated Content)**: Must have content reporting, blocking, and moderation. bitvid already has this.
|
||||
- **Guideline 3.1.1 (In-App Purchase)**: If Lightning zaps are interpreted as digital purchases, Apple may require IAP. Mitigation: frame zaps as peer-to-peer tips external to the app (similar to how other Nostr apps handle this).
|
||||
- **Review strategy**: Submit early with a clear App Review note explaining the decentralized architecture and content moderation approach. Engage with the review team proactively.
|
||||
|
||||
### Desktop distribution
|
||||
|
||||
**Risk level**: Low
|
||||
|
||||
- **Windows**: Distribute via GitHub Releases initially. Microsoft Store submission optional.
|
||||
- **macOS**: Notarize the DMG with an Apple Developer account. Distribute via GitHub Releases or the Mac App Store.
|
||||
- **Linux**: AppImage on GitHub Releases. Snap Store / Flathub optional.
|
||||
|
||||
---
|
||||
|
||||
## Task Checklist
|
||||
|
||||
### Phase 1 — Foundation
|
||||
- [ ] Install `@capacitor/core` and `@capacitor/cli`
|
||||
- [ ] Create `capacitor.config.ts` with bitvid app ID and config
|
||||
- [ ] Run `npx cap add android` and verify project structure
|
||||
- [ ] Run `npx cap add ios` and verify project structure
|
||||
- [ ] Set up Electron via community Capacitor plugin
|
||||
- [ ] Add `js/utils/platform.js` platform detection utility
|
||||
- [ ] Update `package.json` with Capacitor scripts (`cap:sync`, `cap:android`, `cap:ios`, `cap:electron`)
|
||||
- [ ] Update `.gitignore` for native build artifacts
|
||||
- [ ] Verify web app loads in Android emulator
|
||||
- [ ] Verify web app loads in iOS simulator
|
||||
- [ ] Verify web app loads in Electron window
|
||||
- [ ] Verify Nostr relay connections work on all platforms
|
||||
- [ ] Document local development setup for native builds
|
||||
|
||||
### Phase 2 — Android & iOS
|
||||
- [ ] Configure Android permissions in `AndroidManifest.xml`
|
||||
- [ ] Configure iOS `Info.plist` settings
|
||||
- [ ] Generate app icons for all densities (Android + iOS)
|
||||
- [ ] Generate splash screens
|
||||
- [ ] Implement safe area CSS for iOS notch / Dynamic Island
|
||||
- [ ] Add `@capacitor/status-bar` and theme it to match bitvid
|
||||
- [ ] Add `@capacitor/app` for lifecycle events (background/foreground)
|
||||
- [ ] Implement hardware back button handling (Android)
|
||||
- [ ] Set up deep link handling for `nostr:` URIs
|
||||
- [ ] Configure WebView settings for media autoplay
|
||||
- [ ] Test video playback (URL source) on both platforms
|
||||
- [ ] Test video playback (WebTorrent) on both platforms
|
||||
- [ ] Test service worker behavior in native WebViews
|
||||
- [ ] Set iOS default to URL-first playback if WebTorrent is unreliable
|
||||
|
||||
### Phase 3 — Desktop
|
||||
- [ ] Configure Electron window defaults (size, min size, position memory)
|
||||
- [ ] Set up native menu bar or hide it
|
||||
- [ ] Register `nostr:` protocol handler at OS level
|
||||
- [ ] Configure packaging (NSIS/DMG/AppImage)
|
||||
- [ ] Test on Windows, macOS, and Linux
|
||||
- [ ] Set up auto-updater with GitHub Releases feed
|
||||
|
||||
### Phase 4 — Platform Enhancements
|
||||
- [ ] Implement native share sheet integration
|
||||
- [ ] Add `@capacitor/keyboard` for mobile input handling
|
||||
- [ ] Explore picture-in-picture on each platform
|
||||
- [ ] Add haptic feedback for zap actions (optional)
|
||||
- [ ] Implement app shell caching in service worker for offline support
|
||||
|
||||
### Phase 5 — CI/CD
|
||||
- [ ] Create `ci-android.yml` workflow
|
||||
- [ ] Create `ci-ios.yml` workflow
|
||||
- [ ] Create `ci-desktop.yml` workflow (3-OS matrix)
|
||||
- [ ] Add path filters to limit when native CI runs
|
||||
- [ ] Set up artifact uploads for debug builds
|
||||
- [ ] Configure caching (Gradle, CocoaPods, node_modules)
|
||||
- [ ] Add concurrency groups to cancel stale runs
|
||||
- [ ] Document release signing process (keystores, certificates, notarization)
|
||||
|
||||
### Phase 6 — Testing
|
||||
- [ ] Add mobile viewport projects to Playwright config
|
||||
- [ ] Create platform smoke test suite (`tests/native/`)
|
||||
- [ ] Set up Android emulator tests in CI
|
||||
- [ ] Set up iOS simulator tests in CI
|
||||
- [ ] Test WebTorrent streaming in native WebViews on real devices
|
||||
- [ ] Test service worker behavior across platforms
|
||||
- [ ] Test deep link handling on all platforms
|
||||
- [ ] Create cross-platform feature parity test matrix
|
||||
- [ ] Document known platform-specific limitations in `KNOWN_ISSUES.md`
|
||||
|
||||
### App Store Submission
|
||||
- [ ] Create Google Play developer account
|
||||
- [ ] Prepare Play Store listing (screenshots, description, content rating)
|
||||
- [ ] Submit Android app for review
|
||||
- [ ] Create Apple Developer account (if not existing)
|
||||
- [ ] Prepare App Store listing (screenshots, description, review notes)
|
||||
- [ ] Submit iOS app for review
|
||||
- [ ] Set up GitHub Releases for desktop distribution
|
||||
- [ ] Notarize macOS DMG
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Playback Fallback Architecture](playback-fallback.md) — URL-first strategy that mitigates WebTorrent issues on mobile
|
||||
- [Instance Configuration](instance-config.md) — Platform-specific config overrides
|
||||
- [WebTorrent Architecture](webtorrent-architecture.md) — Service worker streaming details
|
||||
- [Moderation Service Overview](moderation-service-overview.md) — Content moderation (relevant for app store review)
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
# EventsCacheStore Overview
|
||||
|
||||
The `EventsCacheStore` is a specialized persistence layer designed to handle the application's critical state: thousands of video events and their deletion status (tombstones). It uses **IndexedDB** as the primary storage backend, falling back to `localStorage` (via `PersistenceManager`) only if IDB is unavailable.
|
||||
|
||||
## 1. Problem Statement
|
||||
|
||||
Nostr clients must handle high-volume event streams. Storing every event naively to disk on every change causes:
|
||||
- **Write Trashing**: Frequent, redundant writes degrade performance and battery life.
|
||||
- **I/O Blocking**: Large JSON serializations on the main thread cause UI jank.
|
||||
- **Startup Latency**: Reading thousands of small files or one giant blob is slow.
|
||||
|
||||
## 2. Architecture: Incremental Persistence
|
||||
|
||||
`EventsCacheStore` solves this with a **Differential State Snapshot** approach.
|
||||
|
||||
### Key Concepts
|
||||
|
||||
1. **Memory Fingerprints**:
|
||||
- The store maintains a `Map<ID, Fingerprint>` in memory, mirroring what is currently on disk.
|
||||
- `Event Fingerprint`: `JSON.stringify(videoObject)` (fast enough for 1000s of items).
|
||||
- `Tombstone Fingerprint`: `ts:<timestamp>`.
|
||||
|
||||
2. **Debounced Snapshots**:
|
||||
- `PersistenceManager` debounces writes (e.g., 75ms). When a write triggers, it passes the *entire* current state (`allEvents`) plus a set of `dirtyKeys` (IDs that changed since last save).
|
||||
|
||||
3. **The Diff Algorithm (`_diffAndStore`)**:
|
||||
- Instead of clearing and rewriting the DB, it iterates over the `dirtyKeys`.
|
||||
- It computes the new fingerprint.
|
||||
- **Optimization**: If `newFingerprint === persistedFingerprint`, the write is skipped.
|
||||
- If different, it performs a `put()` operation and updates the memory fingerprint map.
|
||||
|
||||
4. **Deletion Detection**:
|
||||
- After processing updates, it compares the size of the memory state (`items.size`) vs the persisted state (`fingerprintsMap.size`).
|
||||
- Since `fingerprintsMap` is updated during the loop to include new items, if `items.size < fingerprintsMap.size`, it implies some items in the persisted map are no longer in memory.
|
||||
- In this case, it iterates the *entire* fingerprint map to find and `delete()` the missing keys.
|
||||
|
||||
### Flow Diagram
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant App as NostrClient
|
||||
participant PM as PersistenceManager
|
||||
participant ECS as EventsCacheStore
|
||||
participant IDB as IndexedDB
|
||||
|
||||
App->>App: Event arrives (updates allEvents)
|
||||
App->>PM: saveLocalData(reason, dirtyKeys)
|
||||
PM->>PM: Debounce (75ms)
|
||||
PM->>ECS: persistSnapshot(payload, dirtyKeys)
|
||||
|
||||
activate ECS
|
||||
ECS->>ECS: Ensure fingerprints loaded (once)
|
||||
|
||||
loop For each Dirty Key
|
||||
ECS->>ECS: Compute Fingerprint
|
||||
alt Fingerprint Changed
|
||||
ECS->>IDB: put(record)
|
||||
ECS->>ECS: Update Fingerprint Map
|
||||
else Same Fingerprint
|
||||
ECS->>ECS: Skip
|
||||
end
|
||||
end
|
||||
|
||||
alt items.size < fingerprintsMap.size
|
||||
loop For each Persisted Key
|
||||
alt Not in Memory
|
||||
ECS->>IDB: delete(key)
|
||||
ECS->>ECS: Remove from Fingerprint Map
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
ECS-->>PM: { writes, deletes, skipped }
|
||||
deactivate ECS
|
||||
```
|
||||
|
||||
## 3. Data Schema
|
||||
|
||||
The IndexedDB `bitvid-events-cache` (v1) contains three object stores:
|
||||
|
||||
| Store | Key Path | Value Description |
|
||||
| :--- | :--- | :--- |
|
||||
| `events` | `id` | `{ id, video, fingerprint }` |
|
||||
| `tombstones` | `key` | `{ key, timestamp, fingerprint }` |
|
||||
| `meta` | `key` | `{ key: "meta", savedAt: <ts>, version: 1 }` |
|
||||
|
||||
## 4. Invariants & Assumptions
|
||||
|
||||
- **Single Threaded**: The store assumes it is the only writer. It does not handle cross-tab locking (though IDB transactions provide atomicity).
|
||||
- **Memory-First**: The `allEvents` Map in memory is the source of truth. IDB is a cache.
|
||||
- **TTL**: On `restoreSnapshot()`, if the data is older than the TTL (e.g. 10 minutes), the entire cache is wiped to ensure freshness.
|
||||
|
||||
## 5. Maintenance
|
||||
|
||||
- **Adding Fields**: If the `Video` object shape changes, the fingerprint (JSON string) automatically changes, forcing a rewrite. No migration needed for content changes.
|
||||
- **Schema Changes**: To add a new store, increment `EVENTS_CACHE_DB_VERSION` and update `onupgradeneeded` in `getDb()`.
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
# ModerationService Overview
|
||||
|
||||
The `ModerationService` is the central authority for Trust & Safety within the application. It implements a "Web of Trust" model to filter content, aggregating reports (NIP-56) and mute lists (NIP-51) to determine the visibility and safety level of content.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### 1. Web of Trust
|
||||
Instead of a centralized moderation authority, the service builds a local "trust graph" based on the user's social connections:
|
||||
- **Viewer**: The currently logged-in user.
|
||||
- **Trusted Contacts**: Users followed by the Viewer (Kind 3 contact list).
|
||||
- **Trusted Seeds**: An optional set of admin-defined pubkeys that provide a baseline of trust (e.g., project maintainers).
|
||||
- **Admin Whitelist/Blacklist**: Hardcoded lists in `accessControl` that override dynamic trust.
|
||||
|
||||
### 2. Trust Aggregation
|
||||
When a piece of content (Event) is reported (Kind 1984), the service aggregates these reports but *only counts reports from trusted users*.
|
||||
- **Untrusted Reports**: Ignored by default.
|
||||
- **Trusted Reports**: Count towards the "trust score" for a specific category (e.g., "nudity", "spam").
|
||||
|
||||
### 3. Thresholds
|
||||
The service defines thresholds for automatic actions based on the trusted report count:
|
||||
- **`AUTOPLAY_TRUST_THRESHOLD` (1)**: If ≥1 trusted user reports content as NSFW/Spam, autoplay is disabled.
|
||||
- **`BLUR_TRUST_THRESHOLD` (1)**: If ≥1 trusted user reports content, it is blurred by default.
|
||||
- **`TRUSTED_MUTE_WINDOW_DAYS` (60)**: Mute list entries are considered valid for 60 days to prevent stale mutes from persisting forever.
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
User[User / Viewer] -->|Follows| Contacts[Trusted Contacts]
|
||||
Admins[Admin / Seed List] -->|Augments| Contacts
|
||||
|
||||
Reports[Report Events (Kind 1984)] -->|Ingested| Service[Moderation Service]
|
||||
Contacts -->|Filters| Service
|
||||
|
||||
Service -->|Aggregates| Summary[Trusted Report Summary]
|
||||
Summary -->|Checks Thresholds| UI[UI Components]
|
||||
|
||||
UI -->|Blur / Hide| Content
|
||||
```
|
||||
|
||||
## Data Flow
|
||||
|
||||
1. **Initialization**:
|
||||
- The service initializes with empty sets.
|
||||
- When `setViewerPubkey(pubkey)` is called, it fetches the user's Kind 3 Contact List.
|
||||
- It merges these contacts with any configured `trustedSeedContacts`.
|
||||
|
||||
2. **Report Ingestion**:
|
||||
- The service subscribes to Kind 1984 events referencing active content.
|
||||
- When a report arrives, `ingestReportEvent` checks if the reporter is in the `trustedContacts` set.
|
||||
- If trusted, the report is indexed by `targetEventId` -> `reportType`.
|
||||
|
||||
3. **Summary Recomputation**:
|
||||
- `recomputeSummaryForEvent` calculates the total number of *trusted* reports per category.
|
||||
- It emits a `summary` event, which UI components listen to (e.g., `VideoCard`).
|
||||
- It logs "threshold transitions" (e.g., "autoplay-block-enabled") to the user logger.
|
||||
|
||||
4. **Muting**:
|
||||
- **Viewer Mutes**: The user's own Kind 10000 mute list. Directly hides content.
|
||||
- **Trusted Mutes**: Aggregates mute lists from trusted contacts. If enough trusted users mute an author, that author may be flagged (feature partially implemented via `trustedMutedAuthors`).
|
||||
|
||||
## Key Methods
|
||||
|
||||
- `setViewerPubkey(pubkey)`: Switches the active user context, reloading trust lists.
|
||||
- `submitReport({ eventId, type, ... })`: Publishes a NIP-56 report to relays.
|
||||
- `getTrustedReportSummary(eventId)`: Returns the current trust status for an event.
|
||||
- `getAccessControlStatus(pubkey)`: Checks against the hardcoded Admin Whitelist/Blacklist.
|
||||
|
||||
## Internal Utilities
|
||||
|
||||
The file includes internal implementations of `bech32` encoding/decoding. This is done to:
|
||||
1. Reduce external dependencies for critical path logic.
|
||||
2. Ensure consistent handling of `npub` / `hex` conversions without relying on the global `NostrTools` object potentially being unavailable or different versions.
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
# Moderation Service Overview
|
||||
|
||||
**File**: `js/services/moderationService.js`
|
||||
|
||||
## Purpose
|
||||
|
||||
The `ModerationService` implements the decentralized Trust & Safety model for bitvid. It filters content based on a "Web of Trust" derived from the current user's social graph (NIP-02 Contact List, Kind 3) and administrative overrides.
|
||||
|
||||
Instead of a central authority deciding what is "safe," the service aggregates reports (NIP-56) only from users the viewer follows (their "trusted contacts"). This creates a personalized moderation layer where spam and abuse are filtered by community consensus within the user's network.
|
||||
|
||||
## Key Concepts
|
||||
|
||||
### Web of Trust
|
||||
- **Viewer Context**: The service tracks the currently logged-in user (`viewerPubkey`).
|
||||
- **Trusted Contacts**: The set of pubkeys the viewer follows (from their Kind 3 event).
|
||||
- **Trust Score**: The number of unique trusted users who have reported a specific piece of content.
|
||||
- **Thresholds**: Actions (e.g., blurring, disabling autoplay) are triggered when the Trust Score exceeds defined limits (`AUTOPLAY_TRUST_THRESHOLD`, `BLUR_TRUST_THRESHOLD`).
|
||||
|
||||
### NIP-56 Reports (Kind 1984)
|
||||
- Users can report content for reasons like `nudity`, `spam`, `illegal`, etc.
|
||||
- Reports are public events signed by the reporter.
|
||||
- The service subscribes to reports for active content and aggregates them locally.
|
||||
- **Crucially**: Reports from users *outside* the trusted contact list are ignored (unless whitelisted by admin).
|
||||
|
||||
### NIP-51 Mutes (Kind 10000)
|
||||
- Users can mute other users to hide their content entirely.
|
||||
- The service syncs with `UserBlocks` to enforce mutes.
|
||||
- It also supports "Trusted Mutes" where mutes from trusted contacts can influence visibility (though this feature is evolving).
|
||||
|
||||
### Admin Overrides
|
||||
- The service integrates with `AccessControl` to respect an instance-wide **Blacklist** (always hidden) and **Whitelist** (always trusted).
|
||||
|
||||
## Architecture & Data Flow
|
||||
|
||||
### 1. Initialization (Setting the Viewer)
|
||||
When a user logs in, `setViewerPubkey(pubkey)` is called:
|
||||
1. The service fetches the user's latest Kind 3 Contact List.
|
||||
2. It extracts all followed pubkeys into `trustedContacts`.
|
||||
3. It merges any admin-defined "Trust Seeds" (default trusted users).
|
||||
4. It recomputes trust scores for all currently loaded content based on the new graph.
|
||||
|
||||
### 2. Report Aggregation
|
||||
When viewing a video, the app calls `subscribeToReports(eventId)`:
|
||||
1. The service subscribes to Kind 1984 events referencing the video ID.
|
||||
2. Incoming reports are ingested via `ingestReportEvent`.
|
||||
3. `recomputeSummaryForEvent` checks if the reporter is in `trustedContacts`.
|
||||
4. If trusted, the report increments the score for its category.
|
||||
5. If the score crosses a threshold, the service emits a `summary` event, updating the UI (e.g., to blur the video).
|
||||
|
||||
## Usage Example
|
||||
|
||||
```javascript
|
||||
import moderationService from './services/moderationService.js';
|
||||
|
||||
// 1. Initialize with the logged-in user
|
||||
await moderationService.setViewerPubkey(myPubkey);
|
||||
|
||||
// 2. Subscribe to reports for a video
|
||||
await moderationService.subscribeToReports(videoEventId);
|
||||
|
||||
// 3. Listen for trust score updates
|
||||
const unsubscribe = moderationService.on('summary', ({ eventId, summary }) => {
|
||||
if (summary.totalTrusted >= 1) {
|
||||
console.log(`Video ${eventId} is flagged by trusted users!`);
|
||||
// UI logic: blur thumbnail, show warning
|
||||
}
|
||||
});
|
||||
|
||||
// 4. Submit a report (as the viewer)
|
||||
await moderationService.submitReport({
|
||||
eventId: videoEventId,
|
||||
type: 'nudity',
|
||||
targetPubkey: videoAuthorPubkey,
|
||||
content: 'Contains explicit content'
|
||||
});
|
||||
```
|
||||
|
||||
## Public API Summary
|
||||
|
||||
| Method | Description |
|
||||
| :--- | :--- |
|
||||
| `setViewerPubkey(pubkey)` | Sets the active user and rebuilds the trust graph from their contacts. |
|
||||
| `subscribeToReports(eventId)` | Starts listening for NIP-56 reports for the given event ID. |
|
||||
| `submitReport(params)` | Publishes a signed Kind 1984 report event to relays. |
|
||||
| `getTrustedReportSummary(eventId)` | Returns the current aggregated trust score and report types. |
|
||||
| `isPubkeyBlockedByViewer(pubkey)` | Checks if the author is blocked/muted by the viewer. |
|
||||
| `refreshViewerFromClient()` | Helper to sync the viewer with the active Nostr client state. |
|
||||
|
||||
## Invariants
|
||||
|
||||
- **Trust is Subjective**: A report only counts if the *viewer* follows the reporter. Changing the viewer changes all trust scores immediately.
|
||||
- **Admin Trumps Trust**: Blacklisted users are ignored even if followed. Whitelisted users are trusted even if not followed.
|
||||
- **Reactive**: The service emits events (`summary`, `contacts`, `user-blocks`) to drive UI updates; it does not directly manipulate the DOM.
|
||||
|
||||
## When to Change
|
||||
|
||||
- **Refactoring**: Logic for "Trusted Mutes" (NIP-51 lists from others) is complex and partially deprecated/evolved. This area may need cleanup.
|
||||
- **Performance**: If the number of reports or contacts grows large, the in-memory aggregation (`reportEvents` map) might need optimization or worker offloading.
|
||||
- **Protocol Updates**: Changes to NIP-56 or NIP-51 should be reflected here.
|
||||
Vendored
+53
@@ -0,0 +1,53 @@
|
||||
# NIP-71 Video Events Module (`js/nostr/nip71.js`)
|
||||
|
||||
This module implements the **NIP-71** specification for video events in BitVid. It handles the lifecycle of video metadata, including creation, parsing, caching, and association with playable video notes (Kind 30078).
|
||||
|
||||
## Core Responsibilities
|
||||
|
||||
1. **Event Building**: Constructs NIP-71 events (Kind 22/21) with standardized tags (`imeta`, `segment`, `p`, etc.).
|
||||
2. **Parsing**: Extracts structured metadata from raw Nostr events.
|
||||
3. **Caching**: Manages a memory cache of NIP-71 metadata, indexed by video root ID, event ID, and `d` tags.
|
||||
4. **Hydration**: Merges cached NIP-71 metadata into legacy or standard video objects for the UI.
|
||||
|
||||
## Typical Data Flow
|
||||
|
||||
### 1. Publishing a Video
|
||||
When a user uploads a video, `Nip71FormManager` (in `js/ui/`) collects metadata and calls:
|
||||
```javascript
|
||||
import { buildNip71VideoEvent } from './js/nostr/nip71.js';
|
||||
|
||||
const event = buildNip71VideoEvent({
|
||||
metadata: { ... }, // title, summary, imeta, etc.
|
||||
pubkey: userPubkey,
|
||||
title: "My Video",
|
||||
pointerIdentifiers: { videoRootId: "..." }
|
||||
});
|
||||
// Sign and publish `event`
|
||||
```
|
||||
|
||||
### 2. Viewing the Feed
|
||||
The app fetches video notes (Kind 30078) and potential NIP-71 metadata events (Kind 22/21).
|
||||
1. **Ingestion**: `processNip71Events(events, { nip71Cache })` parses incoming NIP-71 events and updates the cache.
|
||||
2. **Hydration**: `mergeNip71MetadataIntoVideo(video, { nip71Cache })` attaches the best available metadata to the video object.
|
||||
- It checks `videoRootId` -> `eventId` -> `d` tag priority.
|
||||
- Falls back to the most recent metadata if specific pointers miss.
|
||||
|
||||
## Key Exports
|
||||
|
||||
| Function | Purpose |
|
||||
|----------|---------|
|
||||
| `buildNip71VideoEvent` | Creates a signed-ready NIP-71 event object. |
|
||||
| `extractNip71MetadataFromTags` | Parses a raw event into `{ metadata, pointers }`. |
|
||||
| `processNip71Events` | Updates the global `nip71Cache` with new events. |
|
||||
| `mergeNip71MetadataIntoVideo` | Enriches a video object with cached metadata. |
|
||||
| `populateNip71MetadataForVideos` | Batch-fetches metadata for a list of videos (if missing). |
|
||||
|
||||
## Invariants & Assumptions
|
||||
|
||||
- **Video Root ID**: The stable identifier for a video series/content, usually the `d` tag of the first Kind 30078 note.
|
||||
- **Cache Structure**: The `nip71Cache` is a `Map<VideoRootId, CacheEntry>`.
|
||||
- **Immutability**: Metadata objects in the cache are cloned before being attached to video objects to prevent mutation side-effects.
|
||||
|
||||
## When to Refactor
|
||||
- If the cache logic becomes too complex or memory-intensive, move `nip71Cache` to a dedicated `Nip71Service` or `CacheService`.
|
||||
- If NIP-71 spec changes significantly, versioning logic in `extractNip71MetadataFromTags` may need expansion.
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
# Nostr Event Schemas (`js/nostrEventSchemas.js`)
|
||||
|
||||
This module is the central definition for all Nostr event kinds, tags, and content structures used in the application. It provides builder functions to construct valid events and validation logic to ensure compliance with the defined schemas.
|
||||
|
||||
## Purpose
|
||||
|
||||
* **Centralized Definitions**: Maps application-specific event types (e.g., `VIDEO_POST`) to Nostr Kinds (e.g., `30078`) and required tags.
|
||||
* **Event Builders**: Pure functions that take parameters and return a signed-ready event object (excluding the signature).
|
||||
* **Validation**: Development-mode validation to catch malformed events before they are signed or published.
|
||||
* **Sanitization**: Helpers to clean and normalize input data (e.g., UTF-8 enforcement, tag normalization).
|
||||
|
||||
## Public API
|
||||
|
||||
### Constants
|
||||
|
||||
* `NOTE_TYPES`: An enum of all supported event types (e.g., `NOTE_TYPES.VIDEO_POST`, `NOTE_TYPES.ZAP_REQUEST`).
|
||||
* `KIND_MUTE_LIST`: The kind number for NIP-51 Mute Lists (10000).
|
||||
|
||||
### Builder Functions
|
||||
|
||||
Each builder function accepts a parameters object and returns a standard Nostr event object:
|
||||
`{ kind, pubkey, created_at, tags, content }`.
|
||||
|
||||
Common parameters:
|
||||
* `pubkey`: The hex public key of the author.
|
||||
* `created_at`: Unix timestamp (in seconds).
|
||||
* `additionalTags`: Array of extra tags to append (e.g., `[['t', 'hashtag']]`).
|
||||
|
||||
**Key Builders:**
|
||||
|
||||
* `buildVideoPostEvent(params)`: Constructs a Kind 30078 video metadata event.
|
||||
* `buildVideoCommentEvent(params)`: Constructs a Kind 1111 comment event.
|
||||
* `buildVideoReactionEvent(params)`: Constructs a Kind 7 reaction event.
|
||||
* `buildRepostEvent(params)`: Constructs a Kind 6 (Repost) or Kind 16 (Generic Repost).
|
||||
* `buildZapRequestEvent(params)`: Constructs a Kind 9734 zap request.
|
||||
* `buildViewEvent(params)`: Constructs a view analytics event (Kind 30078 variant).
|
||||
|
||||
### Schema & Validation
|
||||
|
||||
* `getNostrEventSchema(type)`: Returns the schema configuration for a given `NOTE_TYPE`.
|
||||
* `validateEventStructure(type, event)`: checks if an event object matches the schema for `type`.
|
||||
* `setNostrEventSchemaOverrides(overrides)`: Allows runtime injection of schema overrides (useful for feature flags or testing).
|
||||
|
||||
## Usage Example
|
||||
|
||||
```javascript
|
||||
import { NOTE_TYPES, buildVideoPostEvent } from './nostrEventSchemas.js';
|
||||
|
||||
const params = {
|
||||
pubkey: '...hex_pubkey...',
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
dTagValue: 'my-video-slug',
|
||||
content: {
|
||||
title: 'My Awesome Video',
|
||||
videoRootId: 'my-video-slug',
|
||||
url: 'https://example.com/video.mp4',
|
||||
thumbnail: 'https://example.com/thumb.jpg'
|
||||
},
|
||||
additionalTags: [['t', 'art']]
|
||||
};
|
||||
|
||||
const event = buildVideoPostEvent(params);
|
||||
|
||||
// Result:
|
||||
// {
|
||||
// kind: 30078,
|
||||
// pubkey: '...',
|
||||
// created_at: ...,
|
||||
// tags: [['d', 'my-video-slug'], ['t', 'video'], ['t', 'art'], ['s', 'nostr:my-video-slug']],
|
||||
// content: '{"title":"My Awesome Video",...}'
|
||||
// }
|
||||
```
|
||||
|
||||
## Internal Details
|
||||
|
||||
### Schema Configuration
|
||||
Each entry in `BASE_SCHEMAS` defines:
|
||||
* `kind`: The Nostr event kind.
|
||||
* `requiredTagNames`: List of tag names that must be present.
|
||||
* `identifierTag`: Configuration for NIP-33 `d` tags.
|
||||
* `appendTags`: Fixed tags to always include.
|
||||
* `content`: Format specification (`json`, `text`, `empty`, etc.).
|
||||
|
||||
### Sanitization
|
||||
* `ensureValidUtf8Content`: Ensures the `content` field contains valid UTF-8, stripping invalid surrogates to prevent relay rejection.
|
||||
* `sanitizeAdditionalTags`: Normalizes user-provided tags (trimming strings, handling pointers).
|
||||
@@ -0,0 +1,50 @@
|
||||
# Weekly Performance Report — 2026-02-13
|
||||
|
||||
**Agent:** `bitvid-perf-deepdive-agent`
|
||||
**Focus:** Relay Request Concurrency
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Identified a critical performance risk in `js/nostr/reactionEvents.js` where reaction list and publish operations were using unbounded `Promise.all` across all configured relays. This could lead to network saturation, browser connection limits (blocking other requests), and potential timeouts for users with many relays.
|
||||
|
||||
**Optimization:** Replaced `Promise.all` with `pMap` to enforce a concurrency limit of 3 (`RELAY_BACKGROUND_CONCURRENCY`), aligning with best practices established in `commentEvents.js`.
|
||||
|
||||
## Methodology
|
||||
|
||||
### Scenario
|
||||
Simulate `listVideoReactions` call with a client configured for 20 relays.
|
||||
- **Tools:** `benchmarks/reaction_loading_bench.mjs` (new harness)
|
||||
- **Environment:** Node.js 22 (Agent Environment)
|
||||
- **Metric:** Peak concurrent `pool.list()` calls.
|
||||
|
||||
### Baseline Measurements
|
||||
|
||||
| Metric | Value | Notes |
|
||||
| :--- | :--- | :--- |
|
||||
| Peak Concurrency | **20** | Unbounded; fired all requests simultaneously. |
|
||||
| Total Time (Mock) | ~111ms | Limited only by the longest single request (mock delay 100ms). |
|
||||
|
||||
### After Optimization
|
||||
|
||||
| Metric | Value | Notes |
|
||||
| :--- | :--- | :--- |
|
||||
| Peak Concurrency | **3** | Capped by `RELAY_BACKGROUND_CONCURRENCY`. |
|
||||
| Total Time (Mock) | ~705ms | Increased latency is expected and acceptable for background operations to preserve network health. |
|
||||
|
||||
## Changes
|
||||
|
||||
### 1. Concurrency Limiting in `js/nostr/reactionEvents.js`
|
||||
- **Before:** `await Promise.all(relayList.map(...))`
|
||||
- **After:** `await pMap(relayList, ..., { concurrency: 3 })`
|
||||
- **Impact:** Prevents "thundering herd" of WebSocket requests.
|
||||
|
||||
### 2. New Benchmark Harness
|
||||
- Added `benchmarks/reaction_loading_bench.mjs` to reproducible measure concurrency of reaction loading.
|
||||
|
||||
## Verification
|
||||
- Unit tests (`tests/nostr/reaction-events.test.mjs`) passed.
|
||||
- Benchmark confirmed concurrency cap is active.
|
||||
|
||||
## Next Steps
|
||||
- **Audit `js/nostr/viewEvents.js`**: Similar unbounded `Promise.all` pattern exists there and should be patched in a future run.
|
||||
- **Audit `js/nostr/dmSignalEvents.js`**: Another potential candidate for concurrency limiting.
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
# PlaybackService Overview
|
||||
|
||||
**File:** `js/services/playbackService.js`
|
||||
|
||||
The `PlaybackService` is the central orchestrator for video playback in bitvid. It implements the "Hybrid Playback Strategy", ensuring users get the fastest possible start (via direct URL) while maintaining the censorship-resistance of P2P (via WebTorrent) as a seamless fallback.
|
||||
|
||||
## Key Concepts
|
||||
|
||||
### 1. Hybrid Playback Strategy
|
||||
The goal is to play video from a standard HTTPS URL (CDN/Server) first because it offers instant seek and low latency. If that fails (404, blocked, or stalls), the system automatically switches to the WebTorrent engine to stream the content from the P2P network using the magnet link.
|
||||
|
||||
### 2. PlaybackSession
|
||||
Each attempt to play a video creates a `PlaybackSession`. This object encapsulates the state machine for that specific video request.
|
||||
- **Request Signature:** A unique ID derived from the URL and Magnet. Used to prevent race conditions (e.g., user clicks the same video twice, or switches videos rapidly).
|
||||
- **Watchdogs:** Event listeners attached to the `<video>` element that monitor for stalls, errors, or network timeouts. If a watchdog barks, the session triggers a fallback.
|
||||
|
||||
## Execution Flow
|
||||
|
||||
The `execute()` method in `PlaybackSession` drives the logic:
|
||||
|
||||
### URL-First (Default)
|
||||
1. **Probe:** Send a HEAD request to the HTTPS URL to check reachability and CORS status.
|
||||
2. **Play:** If reachable, set `video.src = url` and attempt playback.
|
||||
3. **Monitor:** Attach watchdogs.
|
||||
* **Success:** If `playing` event fires and progress continues, keep playing.
|
||||
* **Failure:** If `error`, `stalled`, or `timeout` occurs, stop playback and trigger **Torrent Fallback**.
|
||||
4. **Fallback:** Initialize WebTorrent with the magnet link.
|
||||
|
||||
### Torrent-First (Configurable)
|
||||
If `urlFirstEnabled` is false or the user explicitly selects "P2P":
|
||||
1. **Torrent:** Start WebTorrent immediately.
|
||||
2. **Fallback:** If no peers are found within a timeout, fall back to the HTTPS URL (if available).
|
||||
|
||||
## Public API
|
||||
|
||||
### `playbackService.createSession(options)`
|
||||
Creates a new session.
|
||||
- `url`: HTTPS URL string.
|
||||
- `magnet`: Magnet URI string.
|
||||
- `videoElement`: The HTMLVideoElement to control.
|
||||
- `forcedSource`: Optional 'url' or 'torrent' override.
|
||||
|
||||
### `session.start()`
|
||||
Begins the playback flow. Returns a `Promise` that resolves when playback successfully starts (from either source) or rejects if both fail.
|
||||
|
||||
### `session.cleanupWatchdog()`
|
||||
Removes all event listeners and timers. Automatically called when a session ends or is replaced.
|
||||
|
||||
## Example Usage
|
||||
|
||||
```javascript
|
||||
import { playbackService } from './services/playbackService.js';
|
||||
|
||||
// 1. Create a session
|
||||
const session = playbackService.createSession({
|
||||
url: 'https://example.com/video.mp4',
|
||||
magnet: 'magnet:?xt=urn:btih:...',
|
||||
videoElement: document.querySelector('video'),
|
||||
// Optional callbacks
|
||||
onSuccess: () => console.log('Playback started!'),
|
||||
onFallback: (reason) => console.log('Switched to torrent due to:', reason)
|
||||
});
|
||||
|
||||
// 2. Start playback
|
||||
try {
|
||||
const result = await session.start();
|
||||
console.log(`Playing via: ${result.source}`); // 'url' or 'torrent'
|
||||
} catch (err) {
|
||||
console.error('Playback failed completely:', err);
|
||||
}
|
||||
```
|
||||
|
||||
## When to Refactor
|
||||
- If the `execute()` method grows beyond 500 lines, consider splitting the URL and Torrent logic into separate strategy classes (`UrlPlaybackStrategy`, `TorrentPlaybackStrategy`).
|
||||
- If the "Watchdog" logic becomes more complex (e.g., adaptive bitrates), extract it into a dedicated `PlaybackMonitor` class.
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
# ProfileModalController Architecture Overview
|
||||
|
||||
The `ProfileModalController` (`js/ui/profileModalController.js`) is the central orchestrator for the "Profile" modal in the application. This modal acts as the user's dashboard, aggregating account management, direct messages, settings, and moderation tools.
|
||||
|
||||
## Architecture Pattern: Facade & Orchestrator
|
||||
|
||||
Due to the modal's complexity, the `ProfileModalController` implements a **Facade Pattern**. It delegates domain-specific logic to specialized sub-controllers while presenting a unified interface to the main application (`bitvidApp`).
|
||||
|
||||
### Sub-Controllers
|
||||
|
||||
* **`ProfileDirectMessageController`**: Manages the Direct Messages UI, including conversation lists, message rendering, attachments, and NIP-17 sealing.
|
||||
* **`ProfileRelayController`**: Handles the relay list, including adding/removing relays, restoring defaults, and displaying health metrics.
|
||||
* **`ProfileWalletController`**: Manages NWC (Nostr Wallet Connect) settings and zap configurations.
|
||||
* **`ProfileAdminController`**: Handles moderation lists (mute/block) and admin tools (if the user is an instance admin).
|
||||
* **`ProfileModerationController`**: Manages content safety settings (blur thresholds, autoplay blocking).
|
||||
* **`ProfileHashtagController`**: Manages hashtag preferences (interests/disinterests).
|
||||
* **`ProfileStorageController`**: Manages R2/S3 storage credentials for file uploads.
|
||||
|
||||
### Responsibilities of the Main Controller
|
||||
|
||||
1. **Lifecycle Management**:
|
||||
* `load()`: Fetches the HTML template and injects it into the DOM.
|
||||
* `show(pane)` / `hide()`: Controls visibility and z-index stacking.
|
||||
* `registerEventListeners()`: Binds global navigation and close buttons.
|
||||
|
||||
2. **Navigation State**:
|
||||
* Tracks the `activePane` (e.g., 'account', 'messages', 'relays').
|
||||
* Handles responsive layout switching (Mobile Menu vs. Desktop Pane).
|
||||
|
||||
3. **Authentication Synchronization**:
|
||||
* `handleAuthLogin()`: Triggered when the user logs in. Hydrates all sub-controllers.
|
||||
* `handleAuthLogout()`: Clears sensitive data (DMs, subscriptions) from the UI.
|
||||
* `switchProfile()`: Orchestrates the session switch to another saved account.
|
||||
|
||||
4. **Shared UI Elements**:
|
||||
* Manages the "Saved Profiles" switcher in the header.
|
||||
* Handles the "Add Account" flow and login modal coordination.
|
||||
|
||||
## Initialization Flow
|
||||
|
||||
1. **Instantiation**: Created by `bitvidApp` with references to `services` (Nostr, State, etc.) and `callbacks`.
|
||||
2. **Loading**: The `load()` method is called once. It:
|
||||
* Fetches `components/profile-modal.html`.
|
||||
* Injects it into the `modalContainer`.
|
||||
* Caches DOM references (`cacheDomReferences()`).
|
||||
* Initializes sub-controllers.
|
||||
3. **Display**: When `show()` is called:
|
||||
* The modal becomes visible.
|
||||
* The requested pane is activated.
|
||||
* Data for that pane is hydrated (e.g., fetching relay lists).
|
||||
|
||||
## State Management
|
||||
|
||||
The controller relies on a `state` object passed during construction (typically wrapping `js/state/cache.js` or similar). It avoids maintaining authoritative state internally, preferring to read from the global store and re-render.
|
||||
|
||||
* **Saved Profiles**: Stored in `state.getSavedProfiles()`.
|
||||
* **Active User**: Derived from `state.getActivePubkey()`.
|
||||
|
||||
## Direct Messages Integration
|
||||
|
||||
The DM interface inside the modal is effectively a "mini-app". The `ProfileDirectMessageController` handles:
|
||||
* Real-time subscription to Kind 4 and Kind 1059 events.
|
||||
* Decryption (via `nostrClient` helpers).
|
||||
* Attachment uploads (via `r2Service`).
|
||||
* Typing indicators and read receipts.
|
||||
|
||||
## Key Invariants
|
||||
|
||||
1. **Sub-Controller Isolation**: Sub-controllers should not directly manipulate the DOM of other panes.
|
||||
2. **Authentication Guard**: Sensitive panes (DMs, Wallet) must clear their state immediately upon logout (`handleAuthLogout`).
|
||||
3. **Responsive Design**: The controller monitors window size to toggle between the mobile "Menu View" and the desktop "Pane View".
|
||||
|
||||
## When to Refactor
|
||||
|
||||
* **New Panes**: If adding a new feature pane, create a new `Profile[Feature]Controller.js` and instantiate it in the constructor. Do not add logic directly to the main file.
|
||||
* **DOM Bloat**: If `cacheDomReferences` grows too large, move pane-specific selectors to their respective sub-controllers.
|
||||
@@ -0,0 +1,44 @@
|
||||
# Weekly Race Condition Report: 2026-02-14
|
||||
|
||||
## Audit Overview
|
||||
- **Agent**: bitvid-race-condition-agent
|
||||
- **Focus Areas**: `js/nostr/client.js`, `js/nostr/managers/ConnectionManager.js`, `js/nostr/videoEventBuffer.js`
|
||||
- **Cadence**: Weekly
|
||||
|
||||
## Executive Summary
|
||||
One critical race condition was identified in the `NostrClient` initialization logic. The client flagged itself as `isInitialized = true` synchronously at the start of the `init()` method, before the connection pool was actually ready. This allowed concurrent calls (or UI components checking the flag) to access the uninitialized pool, causing crashes (`TypeError: Cannot read properties of null`).
|
||||
|
||||
## Findings
|
||||
|
||||
### 1. Critical: NostrClient Initialization Race
|
||||
- **Location**: `js/nostr/client.js`
|
||||
- **Severity**: Critical
|
||||
- **Description**: The `init()` method set `this.isInitialized = true` immediately to prevent re-entrant calls. However, `ensurePool()` and `connectToRelays()` are asynchronous. Any component checking `client.isInitialized` during this window would proceed to use the client and crash.
|
||||
- **Interleaving**:
|
||||
1. Caller A calls `client.init()`.
|
||||
2. `client.isInitialized` becomes `true`.
|
||||
3. Caller A awaits `restoreLocalData()` (async).
|
||||
4. Caller B checks `client.isInitialized` -> `true`.
|
||||
5. Caller B calls `client.subscribeVideos()`.
|
||||
6. `subscribeVideos` calls `this.pool.sub`.
|
||||
7. `this.pool` is `null` (because Caller A hasn't reached `ensurePool` yet).
|
||||
8. Crash.
|
||||
|
||||
- **Fix Implemented**:
|
||||
- Introduced `this.initPromise` to track the ongoing initialization.
|
||||
- `init()` now returns the existing promise if called concurrently.
|
||||
- `this.isInitialized` is only set to `true` **after** the pool is ready and relays are connected.
|
||||
- Fix validated with a new reproduction test `tests/race/nostr-client-init-race.test.mjs`.
|
||||
|
||||
## Other Observations
|
||||
|
||||
### VideoEventBuffer (Low Risk)
|
||||
- `js/nostr/videoEventBuffer.js` uses a debounce timer (`scheduleFlush`) and checks `document.hidden` to prevent UI thrashing.
|
||||
- A potential "render race" exists where `flush()` triggers `onVideo` before NIP-71 metadata is fully hydrated. This is an intended trade-off for responsiveness (Stale-While-Revalidate pattern) and not a data corruption bug.
|
||||
|
||||
### ConnectionManager (Safe)
|
||||
- `js/nostr/managers/ConnectionManager.js` properly guards `ensurePool` with `this.poolPromise`, which prevented double-instantiation but didn't protect the `NostrClient` wrapper from the race described above.
|
||||
|
||||
## Next Steps
|
||||
- **Next Audit**: Review `js/nostr/nip07Permissions.js` and `js/nostr/nip46Client.js` for signer negotiation races.
|
||||
- **Monitoring**: Watch for `Cannot read properties of null` errors in Sentry related to `pool.sub`.
|
||||
Vendored
+115
@@ -0,0 +1,115 @@
|
||||
# Scheduled Agent Prompts
|
||||
|
||||
Copy-paste these prompts to schedule recurring maintenance tasks for AI agents.
|
||||
Each prompt is self-contained and targets one specific problem. Run them against
|
||||
the `unstable` branch.
|
||||
|
||||
---
|
||||
|
||||
## 1. File Decomposition (run weekly or biweekly)
|
||||
|
||||
**Best agent:** Claude Code (needs full codebase context for safe refactoring)
|
||||
|
||||
```
|
||||
Target the `unstable` branch. Run `node scripts/check-file-size.mjs --report`
|
||||
to see all grandfathered oversized files and their line counts.
|
||||
|
||||
Pick the SINGLE largest grandfathered file that has not been decomposed recently.
|
||||
Do NOT try to decompose multiple files in one PR.
|
||||
|
||||
For the chosen file:
|
||||
1. Read the entire file and identify 2-3 cohesive blocks of logic that can be
|
||||
extracted into separate modules (look for: groups of related helper functions,
|
||||
self-contained class methods that could become a separate service, rendering
|
||||
logic that could become its own controller).
|
||||
2. Extract each block into a new file in the same directory.
|
||||
3. Export the extracted code and import it from the original file.
|
||||
4. Run `npm run lint` and `npm run test:unit` to verify nothing broke.
|
||||
5. Update the grandfathered line count in `scripts/check-file-size.mjs` to
|
||||
reflect the new (smaller) size of the original file.
|
||||
6. Commit with message prefix `[decompose]` and create a PR targeting `unstable`.
|
||||
|
||||
Goal: reduce the file by at least 200 lines per session. Do not change any
|
||||
behavior — this is a pure extraction refactor.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. innerHTML Migration (run weekly or biweekly)
|
||||
|
||||
**Best agent:** Claude Code or Codex (scoped to individual files)
|
||||
|
||||
```
|
||||
Target the `unstable` branch. Run `node scripts/check-innerhtml.mjs --report`
|
||||
to see all files with innerHTML usage and their counts.
|
||||
|
||||
Pick ONE file from the list (prefer files with the highest count or files
|
||||
tagged RISKY in the audit). Do NOT try to migrate multiple files at once.
|
||||
|
||||
For the chosen file:
|
||||
1. Read the file and find all innerHTML assignments.
|
||||
2. For each assignment, determine if it can be replaced with safe DOM APIs:
|
||||
- Static text: use `element.textContent = "..."`.
|
||||
- Creating elements: use `document.createElement()` + `appendChild()`.
|
||||
- Template HTML with user data: ensure ALL interpolated values pass through
|
||||
`escapeHtml()` from `js/utils/domUtils.js`.
|
||||
3. Replace as many innerHTML assignments as possible with safe alternatives.
|
||||
4. Run `npm run lint` and `npm run test:unit` to verify nothing broke.
|
||||
5. Update the baseline: run `node scripts/check-innerhtml.mjs --update` and
|
||||
copy the new BASELINE object into `scripts/check-innerhtml.mjs`.
|
||||
6. Commit with message prefix `[security]` and create a PR targeting `unstable`.
|
||||
|
||||
Priority order for migration:
|
||||
- RISKY files first (user data without escaping)
|
||||
- High-count files next (profileModalController, searchView, channelProfile)
|
||||
- Low-count static files last (these are low risk)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Constant Deduplication Audit (run monthly)
|
||||
|
||||
**Best agent:** Any agent (simple search task)
|
||||
|
||||
```
|
||||
Target the `unstable` branch.
|
||||
|
||||
Search for numeric constants in `js/` that appear in multiple files with
|
||||
the same value and similar purpose. Focus on:
|
||||
- Timeout values (5000, 10000, 30000, 60000, etc.)
|
||||
- Retry counts
|
||||
- Cache TTLs
|
||||
- Threshold values
|
||||
|
||||
For each duplicate found:
|
||||
1. Determine the canonical location (check `js/constants.js`,
|
||||
`js/nostr/relayConstants.js`, `js/nostr/cachePolicies.js`).
|
||||
2. If a canonical location exists, replace the duplicate with an import.
|
||||
3. If no canonical location exists, create one in the most appropriate
|
||||
shared module.
|
||||
4. Run `npm run lint` and `npm run test:unit`.
|
||||
5. Commit with message prefix `[constants]` and create a PR targeting `unstable`.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Code Health Dashboard (run weekly)
|
||||
|
||||
**Best agent:** Any agent (read-only audit)
|
||||
|
||||
```
|
||||
Target the `unstable` branch. Run all three audit scripts in report mode:
|
||||
|
||||
1. `node scripts/check-file-size.mjs --report`
|
||||
2. `node scripts/check-innerhtml.mjs --report`
|
||||
3. `npm run lint`
|
||||
|
||||
Report the following metrics:
|
||||
- Total grandfathered oversized files and sum of excess lines
|
||||
- Total innerHTML assignments across all files
|
||||
- Any lint failures
|
||||
|
||||
Compare against the previous week's report (if available in issues/PRs).
|
||||
Post the results as a GitHub issue comment or a summary in the PR description
|
||||
so the maintainer can track progress.
|
||||
```
|
||||
Vendored
+77
@@ -0,0 +1,77 @@
|
||||
# Smoke Test Harness
|
||||
|
||||
The `scripts/agent/smoke-test.mjs` script verifies critical application flows (login, relay connection, publishing, DM decryption) using a headless browser (Playwright) and a Node.js Nostr client.
|
||||
|
||||
It is designed to be lightweight, fast, and suitable for both local development and CI/CD pipelines.
|
||||
|
||||
## Features
|
||||
|
||||
- **Ephemeral Keys**: Generates fresh keys for every run; never touches your private keys.
|
||||
- **Headless Browser**: Uses Playwright to verify UI rendering and login state.
|
||||
- **End-to-End Verification**: Publishes events via Node.js client and verifies them in the browser, and vice-versa.
|
||||
- **Artifact Generation**: produces logs, JSON summaries, and screenshots on failure (or success).
|
||||
- **Flexible Configuration**: Supports local relays, external relays, and custom timeouts.
|
||||
|
||||
## Usage
|
||||
|
||||
### Local Development (Default)
|
||||
|
||||
Runs with a local ephemeral relay (port 8899) and starts the app with `npx serve` (port 8000).
|
||||
|
||||
```bash
|
||||
npm run test:smoke
|
||||
# OR
|
||||
node scripts/agent/smoke-test.mjs
|
||||
```
|
||||
|
||||
### Custom Output Directory
|
||||
|
||||
```bash
|
||||
node scripts/agent/smoke-test.mjs --out=artifacts/my-run
|
||||
```
|
||||
|
||||
### External Relays
|
||||
|
||||
To test against real relays (e.g., in a staging environment):
|
||||
|
||||
```bash
|
||||
node scripts/agent/smoke-test.mjs --relays="wss://relay.example.com,wss://relay2.example.com" --serve=none --confirm-public
|
||||
```
|
||||
|
||||
**Note**: When using public/external relays, you must provide `--confirm-public` to acknowledge that test data will be published. The test uses a unique `d` tag and ephemeral keys to minimize pollution, but always use test-specific relays when possible.
|
||||
|
||||
### CLI Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `--relays` | string | `ws://localhost:8899` | Comma-separated list of relay URLs. |
|
||||
| `--serve` | string | `npx` | How to serve the app: `npx`, `python`, or `none`. |
|
||||
| `--out` | string | `artifacts` | Directory to save logs, screenshots, and summaries. |
|
||||
| `--timeout` | number | `30` | Timeout in seconds for UI and network operations. |
|
||||
| `--burst` | number | `1` | (Unused currently) Number of events to publish in burst. |
|
||||
| `--dry-run` | boolean | `false` | If true, skips publishing and browser interactions. |
|
||||
| `--confirm-public` | boolean | `false` | Required if non-localhost relays are used. |
|
||||
|
||||
## Artifacts
|
||||
|
||||
After a run, check the output directory (default `artifacts/`):
|
||||
|
||||
- `smoke-YYYYMMDD.log`: Full execution log.
|
||||
- `smoke-summary-YYYYMMDD.json`: Structured summary (status, event IDs, pubkeys).
|
||||
- `smoke-test-ui.png`: Screenshot of the feed (success state).
|
||||
- `smoke-fail-*.png`: Screenshots captured upon failure (login or feed).
|
||||
|
||||
## CI Integration
|
||||
|
||||
To run this in GitHub Actions or other CI:
|
||||
|
||||
```yaml
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install --with-deps chromium
|
||||
|
||||
- name: Build App
|
||||
run: npm run build
|
||||
|
||||
- name: Run Smoke Test
|
||||
run: node scripts/agent/smoke-test.mjs --serve=npx --timeout=60
|
||||
```
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
# High-Quality, Low-Flake Playwright Testing Guide
|
||||
|
||||
This guide defines how we write and evolve Playwright tests in bitvid so they stay reliable while increasing coverage.
|
||||
|
||||
## Goals
|
||||
|
||||
1. Catch user-visible regressions early.
|
||||
2. Keep CI signal trustworthy (low flake rate).
|
||||
3. Improve runtime coverage with deterministic tests, not brittle ones.
|
||||
|
||||
## Core Principles
|
||||
|
||||
1. Test behavior, not implementation details.
|
||||
2. Prefer deterministic setup over live-network dependence.
|
||||
3. Wait for explicit app state, not arbitrary sleep.
|
||||
4. Use resilient selectors and scoped locators.
|
||||
5. Keep assertions strict on outcomes, flexible on timing and non-critical internals.
|
||||
|
||||
## Determinism First
|
||||
|
||||
Use the existing harness and fixture stack:
|
||||
|
||||
- `tests/e2e/helpers/bitvidTestFixture.ts`
|
||||
- `window.__bitvidTest__` APIs in `js/testHarness.js`
|
||||
|
||||
Prefer these patterns:
|
||||
|
||||
1. Use seeded relay data instead of remote relays.
|
||||
2. Use programmatic login (`loginAs`) instead of manual UI auth for most tests.
|
||||
3. Use harness APIs to force edge conditions (`setSignerDecryptBehavior`, relay overrides).
|
||||
4. Open complex modals via deterministic controller/harness methods when available.
|
||||
|
||||
Why: nondeterministic setup is the top source of flaky tests.
|
||||
|
||||
## Selector Strategy
|
||||
|
||||
1. Prefer `data-testid` selectors.
|
||||
2. Scope locators to visible/active containers when duplicates can exist.
|
||||
3. Avoid broad selectors like `button` unless nested under a stable root.
|
||||
|
||||
Example:
|
||||
|
||||
- Better: `#playerModal #closeModal:visible`
|
||||
- Worse: `#closeModal` (can resolve hidden duplicate elements)
|
||||
|
||||
## Waiting Strategy (Do This, Not That)
|
||||
|
||||
Do:
|
||||
|
||||
1. Wait for explicit state changes (`data-open="true"`, absence of `.hidden`, expected text/attribute).
|
||||
2. Use `waitForFunction` only with concrete conditions and clear timeouts.
|
||||
3. Add fallback action paths for known intermittent UI event misses when justified.
|
||||
|
||||
Avoid:
|
||||
|
||||
1. `waitForTimeout(...)` as primary synchronization.
|
||||
2. Assuming a click always opens a modal immediately.
|
||||
3. Chained interactions without state confirmation between steps.
|
||||
|
||||
## Modal Testing Pattern
|
||||
|
||||
Recommended flow:
|
||||
|
||||
1. Trigger modal open via deterministic helper (harness/controller), fallback to UI click.
|
||||
2. Wait for modal-open condition.
|
||||
3. Assert visible + semantic state (`role`, `aria-modal`, `data-open`).
|
||||
4. Interact with modal.
|
||||
5. Close modal and verify closed state.
|
||||
|
||||
If your tests repeatedly fail opening the same modal, add a harness helper for that modal instead of adding sleeps.
|
||||
|
||||
## Accessibility Test Stability
|
||||
|
||||
Accessibility tests are especially prone to timing flake. For keyboard and modal tests:
|
||||
|
||||
1. Ensure focus target exists and is visible before key presses.
|
||||
2. Prefer element-scoped key actions (`locator.press("Enter")`) over global keyboard when possible.
|
||||
3. Verify modal opened before asserting internals.
|
||||
4. For Escape-close tests, first confirm modal open state deterministically.
|
||||
|
||||
## Coverage Without Brittle UI Tests
|
||||
|
||||
Not all coverage gains require full UI choreography.
|
||||
|
||||
Use runtime module tests in browser context for pure/mostly-pure modules:
|
||||
|
||||
1. `page.evaluate(async () => import("/js/module.js"))`
|
||||
2. Execute exported APIs with deterministic inputs.
|
||||
3. Assert normalized outputs and edge cases.
|
||||
|
||||
This is effective for utilities, state transforms, and view-logic modules that are hard to reach through full flows.
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
1. Duplicate clicks on close/open controls “just in case.”
|
||||
2. Assertions that only check element attachment but not visible state when visibility matters.
|
||||
3. Overly permissive assertions (`expect(x || true).toBe(true)`) unless documented as intentional.
|
||||
4. Tests that rely on console noise rather than asserting intended state transitions.
|
||||
|
||||
## Flake Triage Workflow
|
||||
|
||||
When a test flakes:
|
||||
|
||||
1. Reproduce with repeats:
|
||||
- `npx playwright test <spec> --project=e2e --repeat-each=5`
|
||||
2. Identify failure type:
|
||||
- setup nondeterminism
|
||||
- selector ambiguity
|
||||
- missing state wait
|
||||
- race in app init
|
||||
3. Fix in this order:
|
||||
- add deterministic harness/controller hook
|
||||
- tighten selectors
|
||||
- add explicit state waits
|
||||
- only then consider timeout tuning
|
||||
4. Re-run repeats and then run coverage lane.
|
||||
|
||||
## Quality Gates for Sustainable Improvement
|
||||
|
||||
1. Keep `npm run test:e2e:coverage` green before raising coverage thresholds.
|
||||
2. Track coverage trend, but do not trade reliability for raw percentage.
|
||||
3. Ratchet coverage targets gradually (example: 40% -> 45% -> 50%).
|
||||
4. Require flaky-test fixes to include deterministic synchronization changes, not just longer timeouts.
|
||||
|
||||
## PR Checklist for E2E Changes
|
||||
|
||||
1. Does this test use deterministic fixture/harness setup?
|
||||
2. Are selectors stable (`data-testid` or scoped stable IDs)?
|
||||
3. Are waits condition-based (not sleep-based)?
|
||||
4. Does the test fail for real regressions and pass reliably under repeats?
|
||||
5. If adding coverage tests, do they increase useful coverage in high-risk paths?
|
||||
|
||||
## Useful Commands
|
||||
|
||||
1. Single spec:
|
||||
- `npx playwright test tests/e2e/<spec>.spec.ts --project=e2e`
|
||||
2. Repeat to detect flake:
|
||||
- `npx playwright test tests/e2e/<spec>.spec.ts --project=e2e --repeat-each=5`
|
||||
3. Full deterministic E2E coverage run:
|
||||
- `npm run test:e2e:coverage`
|
||||
4. Regenerate report from existing raw artifacts:
|
||||
- `npm run test:e2e:coverage:report`
|
||||
|
||||
## Related Docs
|
||||
|
||||
- `docs/testing/playwright-function-coverage.md`
|
||||
- `docs/testing/playwright-integration-recommendations-2026-02-22.md`
|
||||
- `AGENTS.md` (testing infrastructure and harness references)
|
||||
@@ -0,0 +1,90 @@
|
||||
# Playwright Function Coverage + Console Diagnostics
|
||||
|
||||
Use this workflow to measure how much of bitvid's runtime code is exercised by E2E Playwright tests and to export browser console/page errors for triage.
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
npm run test:e2e:coverage
|
||||
```
|
||||
|
||||
This command:
|
||||
|
||||
1. Builds `dist/`
|
||||
2. Runs `tests/e2e` on Chromium with browser JS coverage enabled
|
||||
3. Captures browser `console` and `pageerror` output for every test
|
||||
4. Generates aggregate reports
|
||||
|
||||
## Artifacts
|
||||
|
||||
- `artifacts/playwright-coverage/raw/*.json`
|
||||
- Per-test raw coverage + console/page-error logs
|
||||
- `artifacts/playwright-coverage/function-coverage-summary.json`
|
||||
- Machine-readable function coverage report
|
||||
- `artifacts/playwright-coverage/function-coverage-summary.md`
|
||||
- Human-readable summary with coverage percentage and low-coverage files
|
||||
- `artifacts/playwright-coverage/console-log-summary.json`
|
||||
- Full exported console/page-error logs
|
||||
- `artifacts/playwright-coverage/console-issues.log`
|
||||
- Grouped issue signals for fast triage
|
||||
|
||||
## Notes
|
||||
|
||||
- Coverage is captured from Chromium runtime execution (`page.coverage`), then combined with static function estimates for source files in `js/` and `torrent/`.
|
||||
- Firefox coverage is not included in this report because Playwright JS coverage is Chromium-only.
|
||||
- For reliability patterns while increasing coverage, see `docs/testing/high-quality-playwright-tests.md`.
|
||||
|
||||
## Current Baseline (2026-02-22)
|
||||
|
||||
From a full real run (`npm run test:e2e:coverage`) on 2026-02-22:
|
||||
|
||||
- E2E result: `40 passed`
|
||||
- Function coverage: `29.9%` (`2863/9576`)
|
||||
- Loaded source files: `297/313`
|
||||
- Console logs: `72`
|
||||
- Console error-like entries: `4`
|
||||
- Page errors: `0`
|
||||
|
||||
Primary issue groups captured:
|
||||
|
||||
1. `404 Not Found` resource errors (2 occurrences)
|
||||
2. Forced decrypt-timeout warning in diagnostics test (expected by test, 1 occurrence)
|
||||
3. CORS-blocked `.torrent` fetch in webseed playback test (1 occurrence)
|
||||
4. `net::ERR_FAILED` in webseed playback test (1 occurrence)
|
||||
5. WebSocket tracker handshake failure for `wss://tracker.btorrent.xyz/` (1 occurrence)
|
||||
|
||||
Recent high-impact coverage improvements:
|
||||
|
||||
- `js/nostr/commentEvents.js`: `90.91%` (`20/20`)
|
||||
- `js/nostr/reactionEvents.js`: `78.57%` (`11/14`)
|
||||
- `js/nostr/publishHelpers.js`: `79.41%` (`27/34`)
|
||||
- `js/nostr/sessionActor.js`: `79.41%` (`27/34`)
|
||||
|
||||
## Integration Rules For Agents
|
||||
|
||||
1. For `tests/e2e/*.spec.ts`, import from `tests/e2e/helpers/instrumentedTest.ts` so coverage/log capture stays enabled.
|
||||
2. For harness-based tests, use `tests/e2e/helpers/bitvidTestFixture.ts` (it already includes auto-capture).
|
||||
3. Keep coverage runs Chromium-only when collecting percentages (`page.coverage` is Chromium-specific).
|
||||
4. Do not remove `playwright.config.ts` global setup (`tests/e2e/helpers/playwrightCoverageGlobalSetup.ts`), which resets raw coverage artifacts.
|
||||
5. Use `npm run test:e2e:coverage:report` only after a prior coverage-enabled run; it reads existing raw artifacts.
|
||||
|
||||
## How To Improve Coverage
|
||||
|
||||
Priority order for fastest gains:
|
||||
|
||||
1. Add E2E for high-function files currently at `0%` but loaded:
|
||||
- Start with `js/nostr/commentEvents.js`, `js/nostr/reactionEvents.js`, `js/nostr/publishHelpers.js`, `js/nostr/sessionActor.js`.
|
||||
2. Add deterministic DM coverage lane in E2E harness:
|
||||
- Exercise NIP-04 and NIP-44 decrypt/encrypt flows through UI and harness hooks.
|
||||
3. Add upload + publish deep-path tests:
|
||||
- Validate URL-only, magnet-only, and dual-source publish paths with assertions on fallback behavior.
|
||||
4. Add negative-path assertions that currently only surface in console:
|
||||
- Explicitly assert/handle CORS/torrent fetch failures and tracker handshake failures.
|
||||
5. Keep tests routed through test-harness APIs:
|
||||
- Prefer `setTestRelays`, `setSignerDecryptBehavior`, and seeded relay events over live-network dependence.
|
||||
|
||||
## Suggested Quality Gates
|
||||
|
||||
1. Track `functionCoveragePct` trend in CI artifacts per PR.
|
||||
2. Add a soft warning threshold first (for example `>=30%`), then ratchet upward as flaky paths are stabilized.
|
||||
3. Fail PRs only after baseline is stable across multiple runs.
|
||||
@@ -0,0 +1,63 @@
|
||||
# Playwright Integration Recommendations (2026-02-22)
|
||||
|
||||
This document captures practical follow-ups for improving bitvid's end-to-end and extension integration testing.
|
||||
|
||||
## What is now in place
|
||||
|
||||
1. Deterministic E2E matrix projects:
|
||||
- Chromium (`e2e`)
|
||||
- Firefox (`e2e-firefox`)
|
||||
|
||||
2. Optional extension project:
|
||||
- `e2e-nip07-extension` (opt-in via `PLAYWRIGHT_ENABLE_EXTENSION_E2E=1` and `PLAYWRIGHT_EXTENSION_PATH`)
|
||||
|
||||
3. Persistent-context extension launcher:
|
||||
- Script: `scripts/playwright/run-extension-persistent.mjs`
|
||||
- Command: `npm run test:e2e:extension`
|
||||
- Purpose: run extension tests in Chromium with a real persistent profile context.
|
||||
|
||||
4. Test harness upgrades:
|
||||
- `setTestRelays(urls)` aligns `relayManager` and `nostrClient`.
|
||||
- `setSignerDecryptBehavior(mode, opts)` forces decrypt behavior for deterministic list-sync tests.
|
||||
- `getListSyncEvents()`, `clearListSyncEvents()`, `waitForListSyncEvent()` capture and assert sync lifecycle.
|
||||
|
||||
5. Stable sync/test selectors:
|
||||
- Permission prompt, hashtag/subscription statuses, blocked-list status/list/empty state.
|
||||
|
||||
## Recommended next steps
|
||||
|
||||
1. Add CI extension smoke lane (manual workflow dispatch only)
|
||||
- Keep extension smoke out of default PR path.
|
||||
- Add a separate workflow that runs only when maintainers provide:
|
||||
- `PLAYWRIGHT_EXTENSION_PATH` artifact or unpacked extension directory
|
||||
- any extension secrets/profile setup needed by the signer.
|
||||
|
||||
2. Add extension artifact capture on failure
|
||||
- Always upload:
|
||||
- `playwright-report`
|
||||
- video/trace/screenshot for `tests/e2e-extension/**`
|
||||
- JSON summary of `window.nostr` capabilities.
|
||||
|
||||
3. Add capability contract test
|
||||
- Add a single test that asserts minimum NIP-07 surface before running deeper flows:
|
||||
- `getPublicKey`
|
||||
- `signEvent`
|
||||
- at least one decrypt path (`nip04` or `nip44`).
|
||||
|
||||
4. Add "auth ready" test harness API
|
||||
- Add a harness method that waits for authenticated app-ready state (`header UI + signer + relay ready`) so tests stop duplicating custom waits.
|
||||
|
||||
5. Add deterministic clock/random seed helper in test mode
|
||||
- Expose optional fixed clock/random behavior to reduce cross-run variance in event ordering and timeout-sensitive tests.
|
||||
|
||||
6. Add a small extension compatibility matrix note
|
||||
- Track known-good extension versions for:
|
||||
- Alby/Nostr Connect style extensions
|
||||
- Chromium version pinned in CI.
|
||||
|
||||
## Operational guardrails
|
||||
|
||||
1. Keep deterministic tests (mock signer + mock relay) as the default required PR gate.
|
||||
2. Keep extension tests opt-in until flake rate is low and extension lifecycle setup is automated.
|
||||
3. Do not treat extension `content.js` runtime noise as app regression unless bitvid telemetry/events also fail in the same window.
|
||||
|
||||
Vendored
+50
@@ -0,0 +1,50 @@
|
||||
# WebTorrent Architecture & Strategy
|
||||
|
||||
This document details the architecture of the WebTorrent implementation in bitvid, focusing on the client management, Service Worker integration, and the critical logic for handling webseeds.
|
||||
|
||||
## Core Principle: Webseeds Are Peers
|
||||
|
||||
**Crucial Logic (Regression Risk):**
|
||||
A recurring regression in this codebase involves treating "Webseeds" (HTTP sources) as distinct from "Peers" (P2P sources) in a way that marks webseed-only videos as "unhealthy".
|
||||
|
||||
**The Correct Behavior:**
|
||||
* We **must** pass webseeds (`urlList`) directly to the WebTorrent client via `client.add(url, { urlList: [...] })`.
|
||||
* We **must** rely on the WebTorrent client to count that webseed as a connected peer.
|
||||
* **Health = Peers > 0**. If a webseed connects, `numPeers` becomes 1. This is "Healthy".
|
||||
|
||||
**Anti-Pattern (Do Not Implement):**
|
||||
* *Do not* attempt to manually check if `peers === 0 && hasWebseed` and label it as a special "healthy" state in the UI logic.
|
||||
* *Do not* fail to pass `urlList` to `client.add`. If you don't pass it, WebTorrent won't see the peer, `numPeers` stays 0, and the video is marked unhealthy.
|
||||
|
||||
## Architecture Components
|
||||
|
||||
### 1. The Singleton Client (`TorrentClient`)
|
||||
`js/webtorrent.js` exports a singleton `torrentClient`. We do not instantiate multiple clients.
|
||||
* **Reason:** WebTorrent clients are expensive. They open many socket connections and maintain DHT state.
|
||||
* **Reuse:** The same client instance is used for both "Probing" (checking health) and "Streaming" (playback).
|
||||
|
||||
### 2. The Service Worker (`sw.min.js`)
|
||||
Browser-based WebTorrent cannot write directly to the filesystem or stream directly to a `<video>` tag's `src` attribute efficiently.
|
||||
* **Solution:** We register a Service Worker that acts as a proxy.
|
||||
* **Flow:** `<video src="/webtorrent/...">` -> Browser Network Request -> Service Worker Intercept -> Fetch bytes from `TorrentClient` -> Return stream.
|
||||
|
||||
### 3. The "Claim" Dance (Critical Startup Logic)
|
||||
A major source of "grey screen" bugs (video loading forever) occurs when the Service Worker is installed but **not controlling the page**.
|
||||
* **Scenario:** User loads page -> SW installs -> User plays video. If the SW hasn't "claimed" the client, the network request bypasses the SW and fails (404).
|
||||
* **Fix:** `TorrentClient.init()` explicitly waits for `navigator.serviceWorker.controller` to exist. It calls `clients.claim()` in the SW and waits for the `controllerchange` event.
|
||||
|
||||
## Probing Strategy (`probePeers`)
|
||||
|
||||
We probe videos before playing them to determine if they are available.
|
||||
1. **Input:** Magnet URI + optional Webseed URL.
|
||||
2. **Action:** Call `client.add` with `urlList` set to the webseed.
|
||||
3. **Wait:** Wait for `torrent.on('wire')` (connection established) or `peers > 0`.
|
||||
4. **Result:**
|
||||
* **Healthy:** `peers > 0`. (Includes webseeds!)
|
||||
* **Unhealthy:** Timeout with `peers === 0`.
|
||||
|
||||
## Browser Specifics
|
||||
|
||||
* **Brave:** Requires aggressive Service Worker cleanup (`unregister` all) on startup due to lingering stale workers preventing new registrations.
|
||||
* **Firefox:** Uses a specific `highWaterMark` tuning and separate handler (`handleFirefoxTorrent`) to manage memory buffering differences.
|
||||
* **Chrome/Chromium:** Standard path (`handleChromeTorrent`), but includes logic to strip specific trackers/webseeds that trigger CORS warnings (e.g., specific demo trackers).
|
||||
Reference in new issue
Block a user